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.
This commit is contained in:
EdisonJwa
2026-05-16 12:14:28 +08:00
parent 6df5ea960e
commit 73066749e3
+26 -9
View File
@@ -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<Self, PttBackendError> {
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<u32, String> {
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,