From 6ccb225784b9859c88e6eccea02e98c0b724569e Mon Sep 17 00:00:00 2001 From: dailz Date: Sun, 21 Jun 2026 10:37:43 +0800 Subject: [PATCH] fix(backend_detect): add 5s timeout to Portal availability check Prevents indefinite hang when xdg-desktop-portal service is stuck. Previously the check used zbus::Connection::session() with no timeout, waiting forever for D-Bus responses. User observed 11+ second delay at startup when Portal service was wedged, causing 'client can't connect' because wl-webrtc never reached the WebRTC signaling stage. Changes per Oracle review (Phase 1 of 2 for Portal resilience): - Replace Connection::session() with connection::Builder::session() + method_timeout(5s) to bound method replies - Wrap each async operation (connection build, proxy build, version query) with tokio::time::timeout(5s) for comprehensive coverage - Add log_portal_unresponsive() helper with actionable diagnostic: 'systemctl --user restart xdg-desktop-portal xdg-desktop-portal-kde' - Return false on timeout (existing behavior) so caller falls through to wlr-screencopy detection or fails with clear error Why both method_timeout AND tokio::time::timeout (per Oracle): - method_timeout bounds D-Bus method reply waits - tokio::time::timeout bounds connection/proxy setup and any ashpd future composition (relevant for Phase 2) - Neither alone is sufficient What this does NOT do (deferred to Phase 2 / cap_portal.rs): - Token-aware retry logic (Phase 2) - --no-persist suggestion in diagnostic (Phase 2: only appropriate when restore token was actually in use) - Phased diagnostics for CreateSession/SelectSources/Start operations - Runtime watchdog zbus version note: crate uses zbus 5.x with tokio feature only. Builder::method_timeout() available in zbus 5.x. Tests: - cargo build --release: 0 new warnings (19 baseline preserved) - cargo test: 97 lib + 3 integration, 0 failed - 1 file changed, +55/-9 lines --- src/backend_detect.rs | 64 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/src/backend_detect.rs b/src/backend_detect.rs index 757b604..93186bc 100644 --- a/src/backend_detect.rs +++ b/src/backend_detect.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use anyhow::Result; use wayland_client::globals::registry_queue_init; use wayland_client::globals::GlobalListContents; @@ -40,6 +42,20 @@ impl Dispatch for RegistryLs { // CAUTION: must NOT use ashpd here — ashpd caches zbus::Connection in a global // OnceLock; if the tokio runtime owning that connection is dropped before // setup_portal() runs, the cached connection becomes dead and hangs forever. + +/// Per-operation D-Bus timeout for Portal backend detection. +/// Portal 后端检测期间每个 D-Bus 操作的超时时间。 +const PORTAL_DBUS_TIMEOUT: Duration = Duration::from_secs(5); + +fn log_portal_unresponsive(operation: &str) { + tracing::error!( + "Portal service did not respond within 5s while {operation}. \ + This usually means xdg-desktop-portal or xdg-desktop-portal-kde is stuck. \ + Try: systemctl --user restart xdg-desktop-portal xdg-desktop-portal-kde, \ + then re-run wl-webrtc." + ); +} + fn check_portal_available() -> bool { let rt = match tokio::runtime::Runtime::new() { Ok(rt) => rt, @@ -50,12 +66,27 @@ fn check_portal_available() -> bool { }; rt.block_on(async { - let conn = match zbus::Connection::session().await { - Ok(c) => c, - Err(e) => { + // Set method_timeout on the connection (bounds method replies) and wrap + // the build itself in tokio::time::timeout (bounds connection setup). + // 同时设置 method_timeout 与外层 tokio::time::timeout 双重保护。 + let conn = match tokio::time::timeout(PORTAL_DBUS_TIMEOUT, async { + zbus::connection::Builder::session() + .expect("D-Bus session bus builder failed") + .method_timeout(PORTAL_DBUS_TIMEOUT) + .build() + .await + }) + .await + { + Ok(Ok(c)) => c, + Ok(Err(e)) => { tracing::info!("D-Bus session bus unavailable: {e}"); return false; } + Err(_) => { + log_portal_unresponsive("connecting to D-Bus session bus"); + return false; + } }; let inner: zbus::Proxy = match zbus::proxy::Builder::new(&conn) @@ -63,12 +94,16 @@ fn check_portal_available() -> bool { .and_then(|b| b.path("/org/freedesktop/portal/desktop")) .and_then(|b| b.interface("org.freedesktop.portal.ScreenCast")) { - Ok(b) => match b.build().await { - Ok(p) => p, - Err(e) => { + Ok(b) => match tokio::time::timeout(PORTAL_DBUS_TIMEOUT, b.build()).await { + Ok(Ok(p)) => p, + Ok(Err(e)) => { tracing::info!("Portal ScreenCast interface not available: {e}"); return false; } + Err(_) => { + log_portal_unresponsive("building ScreenCast proxy"); + return false; + } }, Err(e) => { tracing::info!("Portal ScreenCast proxy build failed: {e}"); @@ -76,15 +111,26 @@ fn check_portal_available() -> bool { } }; - let version = match inner.get_property::("version").await { - Ok(version) => { + // The most likely operation to hang — requires actual Portal-side work. + // 最可能卡住的操作,需要 Portal 端实际处理。 + let version = match tokio::time::timeout( + PORTAL_DBUS_TIMEOUT, + inner.get_property::("version"), + ) + .await + { + Ok(Ok(version)) => { tracing::info!("Portal ScreenCast available (version: {version})"); true } - Err(e) => { + Ok(Err(e)) => { tracing::info!("Portal ScreenCast version query failed: {e}"); false } + Err(_) => { + log_portal_unresponsive("querying ScreenCast version"); + false + } }; version })