fix(android): 8s Rust timeout on audio_start; always emit connect: debug events
- engine.rs: wrap spawn_blocking(audio_start) in an 8s tokio timeout so the connect command fails fast with a clear error if the Oboe HAL never returns, instead of blocking the JS 45s timer - lib.rs: emit_call_debug now always forwards connect: and register_signal: steps to the JS overlay regardless of the debug-logs toggle — needed because app-data clears reset the toggle to false, making join failures invisible on first install - main.ts: JS timeout bumped to 45s (Rust 8s fires first); timeout message now includes last native connect: step so the toast is actionable without opening the debug log Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -683,19 +683,32 @@ impl CallEngine {
|
|||||||
"connect:audio_start_start",
|
"connect:audio_start_start",
|
||||||
serde_json::json!({ "t_ms": t_pre_audio }),
|
serde_json::json!({ "t_ms": t_pre_audio }),
|
||||||
);
|
);
|
||||||
let audio_start_result = tokio::task::spawn_blocking(crate::wzp_native::audio_start)
|
let audio_start_task = tokio::task::spawn_blocking(crate::wzp_native::audio_start);
|
||||||
.await
|
let audio_start_result =
|
||||||
.map_err(|e| {
|
match tokio::time::timeout(std::time::Duration::from_secs(8), audio_start_task).await {
|
||||||
crate::emit_call_debug(
|
Ok(join_result) => join_result.map_err(|e| {
|
||||||
&app,
|
crate::emit_call_debug(
|
||||||
"connect:audio_start_panic",
|
&app,
|
||||||
serde_json::json!({
|
"connect:audio_start_panic",
|
||||||
"t_ms": call_t0.elapsed().as_millis(),
|
serde_json::json!({
|
||||||
"error": e.to_string(),
|
"t_ms": call_t0.elapsed().as_millis(),
|
||||||
}),
|
"error": e.to_string(),
|
||||||
);
|
}),
|
||||||
anyhow::anyhow!("audio_start task panic: {e}")
|
);
|
||||||
})?;
|
anyhow::anyhow!("audio_start task panic: {e}")
|
||||||
|
})?,
|
||||||
|
Err(_) => {
|
||||||
|
crate::emit_call_debug(
|
||||||
|
&app,
|
||||||
|
"connect:audio_start_timeout",
|
||||||
|
serde_json::json!({
|
||||||
|
"t_ms": call_t0.elapsed().as_millis(),
|
||||||
|
"timeout_ms": 8000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return Err(anyhow::anyhow!("wzp_native_audio_start timed out after 8s"));
|
||||||
|
}
|
||||||
|
};
|
||||||
if let Err(code) = audio_start_result {
|
if let Err(code) = audio_start_result {
|
||||||
crate::emit_call_debug(
|
crate::emit_call_debug(
|
||||||
&app,
|
&app,
|
||||||
|
|||||||
@@ -59,13 +59,15 @@ fn set_call_debug_logs_internal(on: bool) {
|
|||||||
CALL_DEBUG_LOGS.store(on, Ordering::Relaxed);
|
CALL_DEBUG_LOGS.store(on, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emit a `call-debug-log` event to the JS side IF the flag is on.
|
/// Emit a `call-debug-log` event to the JS side.
|
||||||
/// Also mirrors to `tracing::info!` so logcat keeps its copy
|
/// Also mirrors to `tracing::info!` so logcat keeps its copy
|
||||||
/// regardless of the flag — the toggle only controls the GUI
|
/// regardless of the flag. Connect/register steps are always emitted
|
||||||
/// overlay, not the underlying Android log stream.
|
/// because they are needed to diagnose failed joins after app data is
|
||||||
|
/// cleared and the GUI debug toggle is back to its default false value.
|
||||||
pub(crate) fn emit_call_debug(app: &tauri::AppHandle, step: &str, details: serde_json::Value) {
|
pub(crate) fn emit_call_debug(app: &tauri::AppHandle, step: &str, details: serde_json::Value) {
|
||||||
tracing::info!(step, ?details, "call-debug");
|
tracing::info!(step, ?details, "call-debug");
|
||||||
if !call_debug_logs_enabled() {
|
let force_emit = step.starts_with("connect:") || step.starts_with("register_signal:");
|
||||||
|
if !force_emit && !call_debug_logs_enabled() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let payload = serde_json::json!({
|
let payload = serde_json::json!({
|
||||||
|
|||||||
@@ -195,11 +195,24 @@ function errorMessage(e: unknown): string {
|
|||||||
return String(e);
|
return String(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectWithTimeout(args: Record<string, unknown>, timeoutMs = 15000) {
|
function connectDebugSummary(entry: CallDebugEntry | null): string {
|
||||||
|
if (!entry) return "no native connect event received";
|
||||||
|
const details = entry.details && typeof entry.details === "object"
|
||||||
|
? JSON.stringify(entry.details)
|
||||||
|
: String(entry.details ?? "");
|
||||||
|
return `${entry.step}${details ? ` ${details}` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastConnectDebug: CallDebugEntry | null = null;
|
||||||
|
|
||||||
|
function connectWithTimeout(args: Record<string, unknown>, timeoutMs = 45000) {
|
||||||
|
lastConnectDebug = null;
|
||||||
return Promise.race([
|
return Promise.race([
|
||||||
invoke("connect", args),
|
invoke("connect", args),
|
||||||
new Promise<never>((_, reject) =>
|
new Promise<never>((_, reject) =>
|
||||||
setTimeout(() => reject(new Error("connect timed out (15s) - check audio permissions")), timeoutMs)
|
setTimeout(() => reject(new Error(
|
||||||
|
`connect timed out (${Math.round(timeoutMs / 1000)}s); last native step: ${connectDebugSummary(lastConnectDebug)}`
|
||||||
|
)), timeoutMs)
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -221,6 +234,7 @@ const CALL_DEBUG_MAX = 200;
|
|||||||
listen("call-debug-log", (event: any) => {
|
listen("call-debug-log", (event: any) => {
|
||||||
const entry: CallDebugEntry = event.payload;
|
const entry: CallDebugEntry = event.payload;
|
||||||
callDebugBuffer.push(entry);
|
callDebugBuffer.push(entry);
|
||||||
|
if (entry.step?.startsWith("connect:")) lastConnectDebug = entry;
|
||||||
if (callDebugBuffer.length > CALL_DEBUG_MAX) callDebugBuffer.shift();
|
if (callDebugBuffer.length > CALL_DEBUG_MAX) callDebugBuffer.shift();
|
||||||
renderCallDebugLog();
|
renderCallDebugLog();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user