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
This commit is contained in:
dailz
2026-06-21 10:37:43 +08:00
parent 727893fdc2
commit 6ccb225784
+55 -9
View File
@@ -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<WlRegistry, GlobalListContents> 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::<u32>("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::<u32>("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
})