From 73066749e3526b27ae14bb15396dcd6fd969ddda Mon Sep 17 00:00:00 2001 From: EdisonJwa Date: Sat, 16 May 2026 12:14:28 +0800 Subject: [PATCH] fix(audio,linux): isolate zbus blocking probe on a fresh OS thread LinuxGnomeWaylandBackend::probe() called zbus::blocking::Connection::session() directly. The blocking facade internally constructs a current-thread tokio runtime and block_on()s its async D-Bus client. probe() runs from PttController::new (sync) which is called from start_audio (async on the bridge tokio runtime). Nested runtimes panic with 'Cannot start a runtime from within a runtime'. Symptom on Linux: the first voice-channel join surfaced a SnackBar 'Could not join channel: join: task N panicked ...' while the channel-move command had already succeeded server-side. User saw 'channel joined but voice not enabled'. Fix: run the cheap blocking probe on a dedicated std::thread (no ambient runtime), join it synchronously, propagate the version / error. Probe is microseconds; the join cost is negligible. --- .../chanora_audio/src/ptt_backends/linux.rs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/crates/chanora_audio/src/ptt_backends/linux.rs b/crates/chanora_audio/src/ptt_backends/linux.rs index 68146e5..a0ac894 100644 --- a/crates/chanora_audio/src/ptt_backends/linux.rs +++ b/crates/chanora_audio/src/ptt_backends/linux.rs @@ -231,16 +231,33 @@ enum WorkerCmd { impl LinuxGnomeWaylandBackend { /// Synchronous probe via the blocking proxy. Cheap; runs once /// at `try_select` time before we commit to the live flow. + /// + /// `zbus::blocking` internally spins up its own current-thread + /// tokio runtime to drive the async D-Bus client. If we called + /// it directly here we would panic with "Cannot start a runtime + /// from within a runtime" because `try_select()` is invoked + /// from `PttController::new`, which is called from the audio + /// engine start path, which runs on the bridge's tokio + /// multi-thread runtime. We therefore push the blocking probe + /// onto a fresh OS thread (no ambient runtime), join it + /// synchronously, and propagate the result. The probe is in + /// the millisecond range so the join cost is negligible. fn probe() -> Result { - let conn = BlockingConnection::session() - .map_err(|e| PttBackendError::Init(format!("session bus: {e}")))?; - let version = { - let proxy = BlockingGlobalShortcutsProxy::new(&conn) - .map_err(|e| PttBackendError::Init(format!("proxy: {e}")))?; - proxy - .version() - .map_err(|e| PttBackendError::Init(format!("portal version: {e}")))? - }; + let join_result = std::thread::Builder::new() + .name("chanora-ptt-portal-probe".to_string()) + .spawn(|| -> Result { + let conn = BlockingConnection::session() + .map_err(|e| format!("session bus: {e}"))?; + let proxy = BlockingGlobalShortcutsProxy::new(&conn) + .map_err(|e| format!("proxy: {e}"))?; + proxy + .version() + .map_err(|e| format!("portal version: {e}")) + }) + .map_err(|e| PttBackendError::Init(format!("probe thread spawn: {e}")))? + .join() + .map_err(|_| PttBackendError::Init("probe thread panicked".to_string()))?; + let version = join_result.map_err(PttBackendError::Init)?; info!( target: "chanora_audio", portal_version = version,