From 394987a349b7af229ed5b2dc8884d8b4ca7a400a Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Mon, 25 May 2026 07:49:21 +0400 Subject: [PATCH] fix(android): 8s Rust timeout on audio_start; always emit connect: debug events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- desktop/src-tauri/src/engine.rs | 39 ++++++++++++++++++++++----------- desktop/src-tauri/src/lib.rs | 10 +++++---- desktop/src/main.ts | 18 +++++++++++++-- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index 770f683..d500dfe 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -683,19 +683,32 @@ impl CallEngine { "connect:audio_start_start", serde_json::json!({ "t_ms": t_pre_audio }), ); - let audio_start_result = tokio::task::spawn_blocking(crate::wzp_native::audio_start) - .await - .map_err(|e| { - crate::emit_call_debug( - &app, - "connect:audio_start_panic", - serde_json::json!({ - "t_ms": call_t0.elapsed().as_millis(), - "error": e.to_string(), - }), - ); - anyhow::anyhow!("audio_start task panic: {e}") - })?; + let audio_start_task = tokio::task::spawn_blocking(crate::wzp_native::audio_start); + let audio_start_result = + match tokio::time::timeout(std::time::Duration::from_secs(8), audio_start_task).await { + Ok(join_result) => join_result.map_err(|e| { + crate::emit_call_debug( + &app, + "connect:audio_start_panic", + serde_json::json!({ + "t_ms": call_t0.elapsed().as_millis(), + "error": e.to_string(), + }), + ); + 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 { crate::emit_call_debug( &app, diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 4a5da33..b2d2dbc 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -59,13 +59,15 @@ fn set_call_debug_logs_internal(on: bool) { 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 -/// regardless of the flag — the toggle only controls the GUI -/// overlay, not the underlying Android log stream. +/// regardless of the flag. Connect/register steps are always emitted +/// 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) { 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; } let payload = serde_json::json!({ diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 993986a..abf59fb 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -195,11 +195,24 @@ function errorMessage(e: unknown): string { return String(e); } -function connectWithTimeout(args: Record, 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, timeoutMs = 45000) { + lastConnectDebug = null; return Promise.race([ invoke("connect", args), new Promise((_, 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) => { const entry: CallDebugEntry = event.payload; callDebugBuffer.push(entry); + if (entry.step?.startsWith("connect:")) lastConnectDebug = entry; if (callDebugBuffer.length > CALL_DEBUG_MAX) callDebugBuffer.shift(); renderCallDebugLog(); });