From 478233918df78df98222214fcda975d28e30f837 Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:37:23 +0800 Subject: [PATCH 01/18] fix(linux): re-request Wayland screen sharing permission on session failure Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- src/server/connection.rs | 60 +++++++++++++++++- src/server/wayland.rs | 127 ++++++++++++++++++++++++++++----------- 2 files changed, 151 insertions(+), 36 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 10b578042..fe0e2a690 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1600,7 +1600,63 @@ impl Connection { try_activate_screen(); - match super::display_service::update_get_sync_displays_on_login().await { + #[cfg(target_os = "linux")] + let displays_result = if !crate::platform::linux::is_x11() { + let first = super::display_service::update_get_sync_displays_on_login().await; + if first.is_err() { + log::warn!( + "Wayland login: display init failed, waiting for re-authorization: {:?}", + first.as_ref().err() + ); + let mut msg_out = Message::new(); + msg_out.set_message_box(MessageBox { + msgtype: "nook-nocancel-hasclose".to_owned(), + title: "Wayland".to_owned(), + text: "Please Select the screen to be shared(Operate on the peer side).".to_owned(), + link: "".to_owned(), + ..Default::default() + }); + self.send(msg_out).await; + + const WAYLAND_AUTH_TIMEOUT_SECS: u64 = 120; + let deadline = + time::Instant::now() + Duration::from_secs(WAYLAND_AUTH_TIMEOUT_SECS); + let mut result = first; + loop { + time::sleep(Duration::from_secs(1)).await; + result = + super::display_service::update_get_sync_displays_on_login().await; + if result.is_ok() { + log::info!( + "Wayland login: re-authorization succeeded ({} display(s))", + result.as_ref().map(|d| d.len()).unwrap_or(0) + ); + break; + } + if time::Instant::now() >= deadline { + log::error!( + "Wayland login: timed out after {}s waiting for re-authorization", + WAYLAND_AUTH_TIMEOUT_SECS + ); + break; + } + log::debug!( + "Wayland login: still waiting for portal authorization ({:?})", + result.as_ref().err() + ); + } + result + } else { + first + } + } else { + super::display_service::update_get_sync_displays_on_login().await + }; + #[cfg(not(target_os = "linux"))] + let displays_result = + super::display_service::update_get_sync_displays_on_login().await; + + match displays_result { Err(err) => { res.set_error(format!("{}", err)); } @@ -5553,4 +5609,4 @@ mod test { assert!(Ipv6Addr::from_str("127.0.0.1").is_err()); assert!(Ipv6Addr::from_str("0").is_err()); } -} +} \ No newline at end of file diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 6eb6a97bf..b9be5b379 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -166,7 +166,14 @@ pub(super) async fn check_init() -> ResultType<()> { } log::debug!("Attempting to fix logical size with try_fix_logical_size()"); try_fix_logical_size(&mut all); - *PIPEWIRE_INITIALIZED.write().unwrap() = true; + + // Bail early if no displays were found: the portal session was likely revoked (e.g., after screen lock). + if all.is_empty() { + log::warn!("check_init: no displays from PipeWire portal, session revoked. Closing stale session so next attempt triggers re-authorization."); + scrap::wayland::pipewire::close_session(); + bail!("No displays returned by PipeWire portal. Try reconnecting to request a new screen-sharing session."); + } + let num = all.len(); let primary = super::display_service::get_primary_2(&all); super::display_service::check_update_displays(&all); @@ -190,24 +197,45 @@ pub(super) async fn check_init() -> ResultType<()> { ); // Create individual CapDisplayInfo for each display with its own capturer - for (idx, display) in all.into_iter().enumerate() { - let capturer = - Box::into_raw(Box::new(Capturer::new(display).with_context(|| { - format!("Failed to create capturer for display {}", idx) - })?)); - let capturer = CapturerPtr(capturer); + let init_result: ResultType<()> = (|| { + for (idx, display) in all.into_iter().enumerate() { + let capturer = + Box::into_raw(Box::new(Capturer::new(display).with_context(|| { + format!("Failed to create capturer for display {}. Try reconnecting to request a new screen sharing session.", idx) + })?)); + let capturer = CapturerPtr(capturer); - let cap_display_info = Box::into_raw(Box::new(CapDisplayInfo { - rects: rects.clone(), - displays: displays.clone(), - num, - primary, - current: idx, - capturer, - })); + let cap_display_info = Box::into_raw(Box::new(CapDisplayInfo { + rects: rects.clone(), + displays: displays.clone(), + num, + primary, + current: idx, + capturer, + })); - lock.insert(idx, cap_display_info as u64); + lock.insert(idx, cap_display_info as u64); + } + Ok(()) + })(); + + if let Err(e) = init_result { + log::error!("check_init: capturer loop failed, cleaning up partial state: {:?}", e); + for (_, addr) in lock.iter() { + let cap_display_info: *mut CapDisplayInfo = *addr as _; + unsafe { + let _box_capturer = Box::from_raw((*cap_display_info).capturer.0); + let _box_cap_display_info = Box::from_raw(cap_display_info); + } + } + lock.clear(); + + scrap::wayland::pipewire::close_session(); + return Err(e); } + + // Only mark as initialized after the entire loop succeeds. + *PIPEWIRE_INITIALIZED.write().unwrap() = true; } } } @@ -224,7 +252,12 @@ pub(super) async fn get_displays() -> ResultType> { Ok(cap_display_info.displays.clone()) } } else { - bail!("Failed to get capturer display info"); + drop(cap_map); + log::warn!( + "get_displays: map empty after check_init(); resetting PIPEWIRE_INITIALIZED to allow retry." + ); + *PIPEWIRE_INITIALIZED.write().unwrap() = false; + bail!("Failed to get capturer display info. Please re-authorize screen sharing."); } } @@ -265,26 +298,52 @@ pub(super) fn get_capturer_for_display( if is_x11() { bail!("Do not call this function if not wayland"); } - let cap_map = CAP_DISPLAY_INFO.read().unwrap(); - if let Some(addr) = cap_map.get(&display_idx) { - let cap_display_info: *const CapDisplayInfo = *addr as _; - unsafe { - let cap_display_info = &*cap_display_info; - let rect = cap_display_info.rects[cap_display_info.current]; - Ok(super::video_service::CapturerInfo { - origin: rect.0, - width: rect.1, - height: rect.2, - ndisplay: cap_display_info.num, - current: cap_display_info.current, - privacy_mode_id: 0, - _capturer_privacy_mode_id: 0, - capturer: Box::new(cap_display_info.capturer.clone()), - }) + + let build_capturer_info = + |addr: u64| -> ResultType { + let cap_display_info: *const CapDisplayInfo = addr as _; + unsafe { + let cap_display_info = &*cap_display_info; + let rect = cap_display_info.rects[cap_display_info.current]; + Ok(super::video_service::CapturerInfo { + origin: rect.0, + width: rect.1, + height: rect.2, + ndisplay: cap_display_info.num, + current: cap_display_info.current, + privacy_mode_id: 0, + _capturer_privacy_mode_id: 0, + capturer: Box::new(cap_display_info.capturer.clone()), + }) + } + }; + + { + let cap_map = CAP_DISPLAY_INFO.read().unwrap(); + if let Some(&addr) = cap_map.get(&display_idx) { + return build_capturer_info(addr); } + } + + log::warn!( + "get_capturer_for_display: display {} not found in CAP_DISPLAY_INFO. \ + Attempting PipeWire session re-initialization (session may have been revoked \ + after screen lock or idle timeout).", + display_idx + ); + clear(); + ensure_inited()?; + + let cap_map = CAP_DISPLAY_INFO.read().unwrap(); + if let Some(&addr) = cap_map.get(&display_idx) { + log::info!( + "get_capturer_for_display: re-initialization succeeded for display {}.", + display_idx + ); + build_capturer_info(addr) } else { bail!( - "Failed to get capturer display info for display {}", + "Failed to get capturer display info for display {}. A new screen sharing permission grant may be required on the remote side.", display_idx ); } From dbaab9fd5cd48fcd61ee77725ae4d7a4eab87dc5 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:23:57 +0800 Subject: [PATCH 02/18] Update connection.rs --- src/server/connection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index fe0e2a690..70c04e0cf 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1612,7 +1612,7 @@ impl Connection { msg_out.set_message_box(MessageBox { msgtype: "nook-nocancel-hasclose".to_owned(), title: "Wayland".to_owned(), - text: "Please Select the screen to be shared(Operate on the peer side).".to_owned(), + text: "select-wayland-screen-tip".to_owned(), link: "".to_owned(), ..Default::default() }); @@ -5609,4 +5609,4 @@ mod test { assert!(Ipv6Addr::from_str("127.0.0.1").is_err()); assert!(Ipv6Addr::from_str("0").is_err()); } -} \ No newline at end of file +} From 2b95e6b4d73d7d68e8f38e4586787dc791f71474 Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:52:44 +0800 Subject: [PATCH 03/18] fix: automatically maintain unstable remote_desktop_portal sessions to prevent session suspension due to idle timeouts Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- libs/scrap/src/common/wayland.rs | 11 +- libs/scrap/src/wayland/pipewire.rs | 78 +++++++++- src/server/connection.rs | 55 +------ src/server/wayland.rs | 241 ++++++++++++++++------------- 4 files changed, 217 insertions(+), 168 deletions(-) diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index 30b5f4d54..7c96f162b 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -17,10 +17,17 @@ pub fn set_map_err(f: fn(err: String) -> io::Error) { } fn map_err(err: E) -> io::Error { + let err_str = err.to_string(); + + if err_str.contains("SESSION_REVOKED") + { + return io::Error::new(io::ErrorKind::ConnectionAborted, err_str); + } + if let Some(f) = *MAP_ERR.read().unwrap() { - f(err.to_string()) + f(err_str) } else { - io::Error::new(io::ErrorKind::Other, err.to_string()) + io::Error::new(io::ErrorKind::Other, err_str) } } diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index d29677c7a..4ee699d47 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -13,6 +13,7 @@ use dbus::{ arg::{OwnedFd, PropMap, RefArg, Variant}, blocking::{Proxy, SyncConnection}, message::{MatchRule, MessageType}, + channel::Sender, Message, }; @@ -95,6 +96,14 @@ pub fn try_close_session() { } } +pub fn force_close_dead_session() { + if let Ok(mut rdp_info) = RDP_SESSION_INFO.lock() { + *rdp_info = None; + } + clear_wayland_displays_cache(); + HAS_POSITION_ATTR.store(false, Ordering::SeqCst); +} + pub struct RdpSessionInfo { pub conn: Arc, pub streams: Vec, @@ -102,7 +111,15 @@ pub struct RdpSessionInfo { pub session: dbus::Path<'static>, pub is_support_restore_token: bool, pub resolution: Arc>>, + pub keepalive_flag: Arc, } + +impl Drop for RdpSessionInfo { + fn drop(&mut self) { + self.keepalive_flag.store(false, Ordering::SeqCst); + } +} + #[derive(Debug, Clone, Copy)] pub struct PwStreamInfo { pub path: u64, @@ -270,7 +287,7 @@ impl PipeWireRecorder { let src = gst::ElementFactory::make("pipewiresrc", None)?; src.set_property("fd", &capturable.fd.as_raw_fd())?; src.set_property("path", &format!("{}", capturable.path))?; - src.set_property("keepalive_time", &1_000.as_raw_fd())?; + src.set_property("keepalive_time", &1000i32)?; // For some reason pipewire blocks on destruction of AppSink if this is not set to true, // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 @@ -306,7 +323,15 @@ impl PipeWireRecorder { "[gstreamer] Setting pipeline {} to PLAYING state...", capturable.fd.as_raw_fd() ); - pipeline.set_state(gst::State::Playing)?; + if let Err(e) = pipeline.set_state(gst::State::Playing) { + warn!( + "[gstreamer] Failed to set PLAYING state: {:?}. Session likely revoked by user or token expired.", + e + ); + + config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); + return Err(hbb_common::anyhow::Error::msg(format!("SESSION_REVOKED: GStreamer failed: {:?}", e))); + } // If `is_server_running()` is false, it means using remote_desktop_portal, // which does not use multiple streams, so no need to wait for state change. @@ -323,9 +348,16 @@ impl PipeWireRecorder { } (result, state, pending) => { warn!( - "[gstreamer] Pipeline {} state change incomplete: result={:?}, state={:?}, pending={:?}", - capturable.fd.as_raw_fd(), result, state, pending - ); + "[gstreamer] Pipeline {} state change incomplete: result={:?}, state={:?}, pending={:?}", + capturable.fd.as_raw_fd(), result, state, pending + ); + + if let Err(err) = result { + warn!("[gstreamer] Pipeline error detected. Session was likely terminated, clearing XDP token..."); + config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); + let _ = pipeline.set_state(gst::State::Null); + return Err(hbb_common::anyhow::Error::msg(format!("SESSION_REVOKED: GStreamer pipeline failed: {:?}", err))); + } } } std::thread::sleep(std::time::Duration::from_millis(150)); @@ -934,16 +966,48 @@ pub fn get_capturables() -> Result, Box> { if rdp_connection.is_none() { let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop(false)?; let conn = Arc::new(conn); + let keepalive_flag = Arc::new(AtomicBool::new(true)); let rdp_info = RdpSessionInfo { - conn, + conn: conn.clone(), streams, fd, - session, + session: session.clone(), is_support_restore_token, resolution: Arc::new(Mutex::new(None)), + keepalive_flag: keepalive_flag.clone(), }; *rdp_connection = Some(rdp_info); + + let conn_clone = conn.clone(); + let session_clone = session.clone(); + std::thread::spawn(move || { + let mut ticks = 0; + while keepalive_flag.load(Ordering::SeqCst) { + std::thread::sleep(Duration::from_secs(1)); + ticks += 1; + + if ticks >= 60 { + ticks = 0; + if !is_server_running() { + if let Ok(msg) = Message::new_method_call( + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.RemoteDesktop", + "NotifyPointerMotion", + ) { + let msg = msg.append1(( + session_clone.clone(), + HashMap::>>::new(), + 0.0f64, + 0.0f64, + )); + let _ = conn_clone.send(msg); + } + } + } + } + }); } let rdp_info = match rdp_connection.as_mut() { diff --git a/src/server/connection.rs b/src/server/connection.rs index 70c04e0cf..cffcd4b8b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1599,60 +1599,7 @@ impl Connection { } try_activate_screen(); - - #[cfg(target_os = "linux")] - let displays_result = if !crate::platform::linux::is_x11() { - let first = super::display_service::update_get_sync_displays_on_login().await; - if first.is_err() { - log::warn!( - "Wayland login: display init failed, waiting for re-authorization: {:?}", - first.as_ref().err() - ); - let mut msg_out = Message::new(); - msg_out.set_message_box(MessageBox { - msgtype: "nook-nocancel-hasclose".to_owned(), - title: "Wayland".to_owned(), - text: "select-wayland-screen-tip".to_owned(), - link: "".to_owned(), - ..Default::default() - }); - self.send(msg_out).await; - - const WAYLAND_AUTH_TIMEOUT_SECS: u64 = 120; - let deadline = - time::Instant::now() + Duration::from_secs(WAYLAND_AUTH_TIMEOUT_SECS); - let mut result = first; - loop { - time::sleep(Duration::from_secs(1)).await; - result = - super::display_service::update_get_sync_displays_on_login().await; - if result.is_ok() { - log::info!( - "Wayland login: re-authorization succeeded ({} display(s))", - result.as_ref().map(|d| d.len()).unwrap_or(0) - ); - break; - } - if time::Instant::now() >= deadline { - log::error!( - "Wayland login: timed out after {}s waiting for re-authorization", - WAYLAND_AUTH_TIMEOUT_SECS - ); - break; - } - log::debug!( - "Wayland login: still waiting for portal authorization ({:?})", - result.as_ref().err() - ); - } - result - } else { - first - } - } else { - super::display_service::update_get_sync_displays_on_login().await - }; - #[cfg(not(target_os = "linux"))] + let displays_result = super::display_service::update_get_sync_displays_on_login().await; diff --git a/src/server/wayland.rs b/src/server/wayland.rs index b9be5b379..e91c74b9e 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -128,116 +128,134 @@ pub(super) fn is_inited() -> Option { } pub(super) async fn check_init() -> ResultType<()> { - if !is_x11() { - if CAP_DISPLAY_INFO.read().unwrap().is_empty() { - if crate::input_service::wayland_use_uinput() { - if let Some((minx, maxx, miny, maxy)) = - scrap::wayland::display::get_desktop_rect_for_uinput() - { - log::info!( - "update mouse resolution: ({}, {}), ({}, {})", - minx, - maxx, - miny, - maxy - ); - allow_err!( - input_service::update_mouse_resolution(minx, maxx, miny, maxy).await - ); - } else { - log::warn!("Failed to get desktop rect for uinput"); - } - } - - let mut lock = CAP_DISPLAY_INFO.write().unwrap(); - if lock.is_empty() { - // Check if PipeWire is already initialized to prevent duplicate recorder creation - if *PIPEWIRE_INITIALIZED.read().unwrap() { - log::warn!("wayland_diag: Preventing duplicate PipeWire initialization"); - return Ok(()); - } - - let mut all = Display::all()?; - log::debug!("Initializing displays with fill_displays()"); - { - let temp_mouse_move_handle = input_service::TemporaryMouseMoveHandle::new(); - let move_mouse_to = |x, y| temp_mouse_move_handle.move_mouse_to(x, y); - fill_displays(move_mouse_to, crate::get_cursor_pos, &mut all)?; - } - log::debug!("Attempting to fix logical size with try_fix_logical_size()"); - try_fix_logical_size(&mut all); - - // Bail early if no displays were found: the portal session was likely revoked (e.g., after screen lock). - if all.is_empty() { - log::warn!("check_init: no displays from PipeWire portal, session revoked. Closing stale session so next attempt triggers re-authorization."); - scrap::wayland::pipewire::close_session(); - bail!("No displays returned by PipeWire portal. Try reconnecting to request a new screen-sharing session."); - } - - let num = all.len(); - let primary = super::display_service::get_primary_2(&all); - super::display_service::check_update_displays(&all); - let mut displays = super::display_service::get_sync_displays(); - for display in displays.iter_mut() { - display.cursor_embedded = is_cursor_embedded(); - } - - let mut rects: Vec<((i32, i32), usize, usize)> = Vec::new(); - for d in &all { - rects.push((d.origin(), d.width(), d.height())); - } - - log::debug!( - "#displays={}, primary={}, rects: {:?}, cpus={}/{}", - num, - primary, - rects, - num_cpus::get_physical(), - num_cpus::get() - ); - - // Create individual CapDisplayInfo for each display with its own capturer - let init_result: ResultType<()> = (|| { - for (idx, display) in all.into_iter().enumerate() { - let capturer = - Box::into_raw(Box::new(Capturer::new(display).with_context(|| { - format!("Failed to create capturer for display {}. Try reconnecting to request a new screen sharing session.", idx) - })?)); - let capturer = CapturerPtr(capturer); - - let cap_display_info = Box::into_raw(Box::new(CapDisplayInfo { - rects: rects.clone(), - displays: displays.clone(), - num, - primary, - current: idx, - capturer, - })); - - lock.insert(idx, cap_display_info as u64); + let mut retry_count = 0; + const MAX_RETRIES: usize = 1; + loop { + if !is_x11() { + if CAP_DISPLAY_INFO.read().unwrap().is_empty() { + if crate::input_service::wayland_use_uinput() { + if let Some((minx, maxx, miny, maxy)) = + scrap::wayland::display::get_desktop_rect_for_uinput() + { + log::info!( + "update mouse resolution: ({}, {}), ({}, {})", + minx, + maxx, + miny, + maxy + ); + allow_err!( + input_service::update_mouse_resolution(minx, maxx, miny, maxy).await + ); + } else { + log::warn!("Failed to get desktop rect for uinput"); } - Ok(()) - })(); + } - if let Err(e) = init_result { - log::error!("check_init: capturer loop failed, cleaning up partial state: {:?}", e); - for (_, addr) in lock.iter() { - let cap_display_info: *mut CapDisplayInfo = *addr as _; - unsafe { - let _box_capturer = Box::from_raw((*cap_display_info).capturer.0); - let _box_cap_display_info = Box::from_raw(cap_display_info); + let mut lock = CAP_DISPLAY_INFO.write().unwrap(); + if lock.is_empty() { + // Check if PipeWire is already initialized to prevent duplicate recorder creation + if *PIPEWIRE_INITIALIZED.read().unwrap() { + log::warn!("wayland_diag: Preventing duplicate PipeWire initialization"); + return Ok(()); + } + + let mut all = Display::all()?; + log::debug!("Initializing displays with fill_displays()"); + { + let temp_mouse_move_handle = input_service::TemporaryMouseMoveHandle::new(); + let move_mouse_to = |x, y| temp_mouse_move_handle.move_mouse_to(x, y); + fill_displays(move_mouse_to, crate::get_cursor_pos, &mut all)?; + } + log::debug!("Attempting to fix logical size with try_fix_logical_size()"); + try_fix_logical_size(&mut all); + + // Bail early if no displays were found: the portal session was likely revoked (e.g., after screen lock). + if all.is_empty() { + log::warn!("check_init: no displays from PipeWire portal, session revoked. Closing stale session so next attempt triggers re-authorization."); + scrap::wayland::pipewire::close_session(); + bail!("No displays returned by PipeWire portal. Try reconnecting to request a new screen-sharing session."); + } + + let num = all.len(); + let primary = super::display_service::get_primary_2(&all); + super::display_service::check_update_displays(&all); + let mut displays = super::display_service::get_sync_displays(); + for display in displays.iter_mut() { + display.cursor_embedded = is_cursor_embedded(); + } + + let mut rects: Vec<((i32, i32), usize, usize)> = Vec::new(); + for d in &all { + rects.push((d.origin(), d.width(), d.height())); + } + + log::debug!( + "#displays={}, primary={}, rects: {:?}, cpus={}/{}", + num, + primary, + rects, + num_cpus::get_physical(), + num_cpus::get() + ); + + // Create individual CapDisplayInfo for each display with its own capturer + let init_result: ResultType<()> = (|| { + for (idx, display) in all.into_iter().enumerate() { + let capturer = + Box::into_raw(Box::new(Capturer::new(display).with_context(|| { + format!("Failed to create capturer for display {}. Try reconnecting to request a new screen sharing session.", idx) + })?)); + let capturer = CapturerPtr(capturer); + + let cap_display_info = Box::into_raw(Box::new(CapDisplayInfo { + rects: rects.clone(), + displays: displays.clone(), + num, + primary, + current: idx, + capturer, + })); + + lock.insert(idx, cap_display_info as u64); } + Ok(()) + })(); + + if let Err(e) = init_result { + log::error!("check_init: capturer loop failed, cleaning up partial state: {:?}", e); + for (_, addr) in lock.iter() { + let cap_display_info: *mut CapDisplayInfo = *addr as _; + unsafe { + let _box_capturer = Box::from_raw((*cap_display_info).capturer.0); + let _box_cap_display_info = Box::from_raw(cap_display_info); + } + } + lock.clear(); + + let err_str = format!("{:?}", e); + if err_str.contains("SESSION_REVOKED") { + if retry_count < MAX_RETRIES { + log::warn!("check_init: Detected Wayland session death. Forcing hard reset"); + *PIPEWIRE_INITIALIZED.write().unwrap() = false; + scrap::wayland::pipewire::force_close_dead_session(); + retry_count += 1; + continue; + } + } else { + scrap::wayland::pipewire::close_session(); + } + + return Err(e); } - lock.clear(); - scrap::wayland::pipewire::close_session(); - return Err(e); + // Only mark as initialized after the entire loop succeeds. + *PIPEWIRE_INITIALIZED.write().unwrap() = true; + break; } - - // Only mark as initialized after the entire loop succeeds. - *PIPEWIRE_INITIALIZED.write().unwrap() = true; } } + break; } Ok(()) } @@ -326,9 +344,22 @@ pub(super) fn get_capturer_for_display( } log::warn!( - "get_capturer_for_display: display {} not found in CAP_DISPLAY_INFO. \ - Attempting PipeWire session re-initialization (session may have been revoked \ - after screen lock or idle timeout).", + "get_capturer_for_display: display {} not found in CAP_DISPLAY_INFO.", + display_idx + ); + + // Wait until all active capturers have exited before reinitializing. + let active_count = *ACTIVE_DISPLAY_COUNT.read().unwrap(); + if active_count > 0 { + bail!( + "Display {} not found in CAP_DISPLAY_INFO, but {} active capturer(s) are still running. Skipping reinitialization now.", + display_idx, + active_count + ); + } + + log::info!( + "get_capturer_for_display: no active capturers, reinitializing PipeWire session for display {}.", display_idx ); clear(); From 430408b0958fa1e2ef66d06496968eb50dc631f8 Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:19:39 +0800 Subject: [PATCH 04/18] fix: fix keepalive silent failure and ACTIVE_DISPLAY_COUNT race Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- libs/scrap/src/wayland/pipewire.rs | 34 ++++++++++++++++++++---------- src/server/wayland.rs | 25 +++++++++++----------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 4ee699d47..49030b307 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -328,7 +328,7 @@ impl PipeWireRecorder { "[gstreamer] Failed to set PLAYING state: {:?}. Session likely revoked by user or token expired.", e ); - + let _ = pipeline.set_state(gst::State::Null); config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); return Err(hbb_common::anyhow::Error::msg(format!("SESSION_REVOKED: GStreamer failed: {:?}", e))); } @@ -981,28 +981,40 @@ pub fn get_capturables() -> Result, Box> { let conn_clone = conn.clone(); let session_clone = session.clone(); + + // Remote Desktop sessions are observed to time out after ~15 minutes of inactivity. + // Pinging every 60s gives a wide safety margin while keeping DBus traffic negligible. + const KEEPALIVE_INTERVAL_SECS: u64 = 60; + std::thread::spawn(move || { let mut ticks = 0; while keepalive_flag.load(Ordering::SeqCst) { std::thread::sleep(Duration::from_secs(1)); ticks += 1; - - if ticks >= 60 { + if ticks >= KEEPALIVE_INTERVAL_SECS { ticks = 0; if !is_server_running() { - if let Ok(msg) = Message::new_method_call( + match Message::new_method_call( "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.RemoteDesktop", "NotifyPointerMotion", ) { - let msg = msg.append1(( - session_clone.clone(), - HashMap::>>::new(), - 0.0f64, - 0.0f64, - )); - let _ = conn_clone.send(msg); + Ok(msg) => { + let msg = msg.append1(( + session_clone.clone(), + HashMap::>>::new(), + 0.0f64, + 0.0f64, + )); + if let Err(_) = conn_clone.send(msg) { + // Session may have been revoked; the keepalive is no longer effective. + break; + } + } + Err(_) => { + break; + } } } } diff --git a/src/server/wayland.rs b/src/server/wayland.rs index e91c74b9e..d532d644c 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -27,6 +27,7 @@ pub fn init() { } pub(super) fn increment_active_display_count() -> usize { + let _cap_map_guard = CAP_DISPLAY_INFO.read().unwrap(); let mut count = ACTIVE_DISPLAY_COUNT.write().unwrap(); *count += 1; *count @@ -292,11 +293,15 @@ pub(super) fn get_primary() -> ResultType { } } -pub fn clear() { +pub fn clear() -> bool { if is_x11() { - return; + return true; } let mut write_lock = CAP_DISPLAY_INFO.write().unwrap(); + let active_count = *ACTIVE_DISPLAY_COUNT.read().unwrap(); + if active_count > 0 { + return false; + } for (_, addr) in write_lock.iter() { let cap_display_info: *mut CapDisplayInfo = *addr as _; unsafe { @@ -305,9 +310,8 @@ pub fn clear() { } } write_lock.clear(); - - // Reset PipeWire initialization flag to allow recreation on next init *PIPEWIRE_INITIALIZED.write().unwrap() = false; + true } pub(super) fn get_capturer_for_display( @@ -348,13 +352,10 @@ pub(super) fn get_capturer_for_display( display_idx ); - // Wait until all active capturers have exited before reinitializing. - let active_count = *ACTIVE_DISPLAY_COUNT.read().unwrap(); - if active_count > 0 { + if !clear() { bail!( - "Display {} not found in CAP_DISPLAY_INFO, but {} active capturer(s) are still running. Skipping reinitialization now.", - display_idx, - active_count + "Display {} not found in CAP_DISPLAY_INFO, but active capturer(s) are still running. Skipping reinitialization now.", + display_idx ); } @@ -362,7 +363,6 @@ pub(super) fn get_capturer_for_display( "get_capturer_for_display: no active capturers, reinitializing PipeWire session for display {}.", display_idx ); - clear(); ensure_inited()?; let cap_map = CAP_DISPLAY_INFO.read().unwrap(); @@ -374,7 +374,8 @@ pub(super) fn get_capturer_for_display( build_capturer_info(addr) } else { bail!( - "Failed to get capturer display info for display {}. A new screen sharing permission grant may be required on the remote side.", + "Failed to get capturer for display {} after reinitialization. \ + A new screen-sharing grant may be required.", display_idx ); } From 4b2d98fa38e3623ccea73c584d3034aabef96d39 Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Thu, 26 Feb 2026 02:03:17 +0800 Subject: [PATCH 05/18] inhibit suspend in pipewire.rs for a more graceful handling of idle timeout issues Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- libs/scrap/src/wayland/pipewire.rs | 85 +++++++++++++----------------- 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 49030b307..e1cfecee5 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -13,7 +13,6 @@ use dbus::{ arg::{OwnedFd, PropMap, RefArg, Variant}, blocking::{Proxy, SyncConnection}, message::{MatchRule, MessageType}, - channel::Sender, Message, }; @@ -111,12 +110,14 @@ pub struct RdpSessionInfo { pub session: dbus::Path<'static>, pub is_support_restore_token: bool, pub resolution: Arc>>, - pub keepalive_flag: Arc, + inhibit_request_path: Option>, } impl Drop for RdpSessionInfo { fn drop(&mut self) { - self.keepalive_flag.store(false, Ordering::SeqCst); + if let Some(ref path) = self.inhibit_request_path { + release_inhibit(&self.conn, path); + } } } @@ -957,6 +958,38 @@ fn on_start_response( } } +fn request_inhibit(conn: &SyncConnection) -> Option> { + let proxy = conn.with_proxy( + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + Duration::from_millis(1000), + ); + let mut args: PropMap = HashMap::new(); + args.insert("handle_token".to_string(), Variant(Box::new("inhibit1".to_string())),); + // flags: 4 = inhibit idle, 8 = inhibit suspend + match proxy.method_call::<(dbus::Path<'static>,), _, _, _>("org.freedesktop.portal.Inhibit", "Inhibit", ("", 8u32, args)) { + Ok((path,)) => { + debug!("Inhibit requested, request path: {:?}", path); + Some(path) + } + Err(e) => { + warn!("Failed to request inhibit: {}", e); + None + } + } +} + +fn release_inhibit(conn: &SyncConnection, path: &dbus::Path<'static>) { + let proxy = conn.with_proxy( + "org.freedesktop.portal.Desktop", + path, + Duration::from_millis(1000), + ); + if let Err(e) = proxy.method_call::<(), _, _, _>("org.freedesktop.portal.Request", "Close", ()) { + warn!("Failed to release inhibit: {}", e); + } +} + pub fn get_capturables() -> Result, Box> { let mut rdp_connection = match RDP_SESSION_INFO.lock() { Ok(conn) => conn, @@ -966,7 +999,7 @@ pub fn get_capturables() -> Result, Box> { if rdp_connection.is_none() { let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop(false)?; let conn = Arc::new(conn); - let keepalive_flag = Arc::new(AtomicBool::new(true)); + let inhibit_path = request_inhibit(&conn); let rdp_info = RdpSessionInfo { conn: conn.clone(), @@ -975,51 +1008,9 @@ pub fn get_capturables() -> Result, Box> { session: session.clone(), is_support_restore_token, resolution: Arc::new(Mutex::new(None)), - keepalive_flag: keepalive_flag.clone(), + inhibit_request_path: inhibit_path, }; *rdp_connection = Some(rdp_info); - - let conn_clone = conn.clone(); - let session_clone = session.clone(); - - // Remote Desktop sessions are observed to time out after ~15 minutes of inactivity. - // Pinging every 60s gives a wide safety margin while keeping DBus traffic negligible. - const KEEPALIVE_INTERVAL_SECS: u64 = 60; - - std::thread::spawn(move || { - let mut ticks = 0; - while keepalive_flag.load(Ordering::SeqCst) { - std::thread::sleep(Duration::from_secs(1)); - ticks += 1; - if ticks >= KEEPALIVE_INTERVAL_SECS { - ticks = 0; - if !is_server_running() { - match Message::new_method_call( - "org.freedesktop.portal.Desktop", - "/org/freedesktop/portal/desktop", - "org.freedesktop.portal.RemoteDesktop", - "NotifyPointerMotion", - ) { - Ok(msg) => { - let msg = msg.append1(( - session_clone.clone(), - HashMap::>>::new(), - 0.0f64, - 0.0f64, - )); - if let Err(_) = conn_clone.send(msg) { - // Session may have been revoked; the keepalive is no longer effective. - break; - } - } - Err(_) => { - break; - } - } - } - } - } - }); } let rdp_info = match rdp_connection.as_mut() { From 5a12989697ac870e71462a9ff8cc8fd65f19ee24 Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Thu, 26 Feb 2026 02:39:27 +0800 Subject: [PATCH 06/18] fix: use is_server_running to correctly determine whether the StateChangeError is caused by a configuration error or an operational error. Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- libs/scrap/src/wayland/pipewire.rs | 40 ++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index e1cfecee5..16cde7819 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -98,9 +98,9 @@ pub fn try_close_session() { pub fn force_close_dead_session() { if let Ok(mut rdp_info) = RDP_SESSION_INFO.lock() { *rdp_info = None; + clear_wayland_displays_cache(); + HAS_POSITION_ATTR.store(false, Ordering::SeqCst); } - clear_wayland_displays_cache(); - HAS_POSITION_ATTR.store(false, Ordering::SeqCst); } pub struct RdpSessionInfo { @@ -325,13 +325,28 @@ impl PipeWireRecorder { capturable.fd.as_raw_fd() ); if let Err(e) = pipeline.set_state(gst::State::Playing) { - warn!( - "[gstreamer] Failed to set PLAYING state: {:?}. Session likely revoked by user or token expired.", - e - ); let _ = pipeline.set_state(gst::State::Null); - config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); - return Err(hbb_common::anyhow::Error::msg(format!("SESSION_REVOKED: GStreamer failed: {:?}", e))); + + let is_revoked = if !is_server_running() { + // remote_desktop_portal: always treat sync failure as revocation. + true + } else { + // screencast_portal: only treat as revocation when a stale token exists. + !config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY).is_empty() + }; + + if is_revoked { + warn!("[gstreamer] Failed to set PLAYING state, session was likely revoked: {:?}", e); + config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); + return Err(hbb_common::anyhow::Error::msg(format!("SESSION_REVOKED: GStreamer failed: {:?}", e))); + } else { + warn!( + "[gstreamer] Failed to set PLAYING state on a fresh screencast session \ + (no restore token). Likely a pipeline misconfiguration: {:?}", + e + ); + return Err(hbb_common::anyhow::Error::msg(format!("GStreamer pipeline failed to start: {:?}", e))); + } } // If `is_server_running()` is false, it means using remote_desktop_portal, @@ -354,7 +369,7 @@ impl PipeWireRecorder { ); if let Err(err) = result { - warn!("[gstreamer] Pipeline error detected. Session was likely terminated, clearing XDP token..."); + warn!("[gstreamer] Async pipeline error detected. Session was likely terminated, clearing XDP token..."); config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); let _ = pipeline.set_state(gst::State::Null); return Err(hbb_common::anyhow::Error::msg(format!("SESSION_REVOKED: GStreamer pipeline failed: {:?}", err))); @@ -966,8 +981,11 @@ fn request_inhibit(conn: &SyncConnection) -> Option> { ); let mut args: PropMap = HashMap::new(); args.insert("handle_token".to_string(), Variant(Box::new("inhibit1".to_string())),); - // flags: 4 = inhibit idle, 8 = inhibit suspend - match proxy.method_call::<(dbus::Path<'static>,), _, _, _>("org.freedesktop.portal.Inhibit", "Inhibit", ("", 8u32, args)) { + // flags: 8 = inhibit idle, 4 = inhibit suspend + // Based on current testing, Dim Screen will forcibly terminate the session. + // Solving this problem requires preventing entry into the idle state. + // However, for future considerations, entering the suspended state is also prevented here. + match proxy.method_call::<(dbus::Path<'static>,), _, _, _>("org.freedesktop.portal.Inhibit", "Inhibit", ("", 12u32, args)) { Ok((path,)) => { debug!("Inhibit requested, request path: {:?}", path); Some(path) From b5196ef8129676e217fe3635612ca8ed9aaf4be2 Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:07:05 +0800 Subject: [PATCH 07/18] cleanup: remove unnecessary code changes Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- libs/scrap/src/wayland/pipewire.rs | 6 +++--- src/server/connection.rs | 5 +---- src/server/wayland.rs | 29 ++++++++++++++--------------- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 16cde7819..604cdf287 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -288,7 +288,7 @@ impl PipeWireRecorder { let src = gst::ElementFactory::make("pipewiresrc", None)?; src.set_property("fd", &capturable.fd.as_raw_fd())?; src.set_property("path", &format!("{}", capturable.path))?; - src.set_property("keepalive_time", &1000i32)?; + src.set_property("keepalive_time", &1_000.as_raw_fd())?; // For some reason pipewire blocks on destruction of AppSink if this is not set to true, // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 @@ -1020,10 +1020,10 @@ pub fn get_capturables() -> Result, Box> { let inhibit_path = request_inhibit(&conn); let rdp_info = RdpSessionInfo { - conn: conn.clone(), + conn, streams, fd, - session: session.clone(), + session, is_support_restore_token, resolution: Arc::new(Mutex::new(None)), inhibit_request_path: inhibit_path, diff --git a/src/server/connection.rs b/src/server/connection.rs index cffcd4b8b..10b578042 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1599,11 +1599,8 @@ impl Connection { } try_activate_screen(); - - let displays_result = - super::display_service::update_get_sync_displays_on_login().await; - match displays_result { + match super::display_service::update_get_sync_displays_on_login().await { Err(err) => { res.set_error(format!("{}", err)); } diff --git a/src/server/wayland.rs b/src/server/wayland.rs index d532d644c..26702014f 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -236,10 +236,10 @@ pub(super) async fn check_init() -> ResultType<()> { let err_str = format!("{:?}", e); if err_str.contains("SESSION_REVOKED") { + log::warn!("check_init: Detected Wayland session death. Forcing hard reset"); + *PIPEWIRE_INITIALIZED.write().unwrap() = false; + scrap::wayland::pipewire::force_close_dead_session(); if retry_count < MAX_RETRIES { - log::warn!("check_init: Detected Wayland session death. Forcing hard reset"); - *PIPEWIRE_INITIALIZED.write().unwrap() = false; - scrap::wayland::pipewire::force_close_dead_session(); retry_count += 1; continue; } @@ -293,15 +293,11 @@ pub(super) fn get_primary() -> ResultType { } } -pub fn clear() -> bool { +pub fn clear() { if is_x11() { - return true; + return; } let mut write_lock = CAP_DISPLAY_INFO.write().unwrap(); - let active_count = *ACTIVE_DISPLAY_COUNT.read().unwrap(); - if active_count > 0 { - return false; - } for (_, addr) in write_lock.iter() { let cap_display_info: *mut CapDisplayInfo = *addr as _; unsafe { @@ -310,8 +306,9 @@ pub fn clear() -> bool { } } write_lock.clear(); + + // Reset PipeWire initialization flag to allow recreation on next init *PIPEWIRE_INITIALIZED.write().unwrap() = false; - true } pub(super) fn get_capturer_for_display( @@ -352,10 +349,12 @@ pub(super) fn get_capturer_for_display( display_idx ); - if !clear() { + // Wait until all active capturers have exited before reinitializing. + let active_count = *ACTIVE_DISPLAY_COUNT.read().unwrap(); + if active_count > 0 { bail!( - "Display {} not found in CAP_DISPLAY_INFO, but active capturer(s) are still running. Skipping reinitialization now.", - display_idx + "Display {} not found in CAP_DISPLAY_INFO, but {} active capturer(s) are still running. Skipping reinitialization now.", + display_idx, active_count ); } @@ -363,6 +362,7 @@ pub(super) fn get_capturer_for_display( "get_capturer_for_display: no active capturers, reinitializing PipeWire session for display {}.", display_idx ); + clear(); ensure_inited()?; let cap_map = CAP_DISPLAY_INFO.read().unwrap(); @@ -374,8 +374,7 @@ pub(super) fn get_capturer_for_display( build_capturer_info(addr) } else { bail!( - "Failed to get capturer for display {} after reinitialization. \ - A new screen-sharing grant may be required.", + "Failed to get capturer for display {}", display_idx ); } From 230cc3d00bde11c82dca660b479e7108ebe587c3 Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:17:23 +0800 Subject: [PATCH 08/18] refactor: move check_init loop logic into dedicated function Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- src/server/wayland.rs | 261 ++++++++++++++++++++++-------------------- 1 file changed, 134 insertions(+), 127 deletions(-) diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 26702014f..607317bfc 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -128,139 +128,146 @@ pub(super) fn is_inited() -> Option { } } -pub(super) async fn check_init() -> ResultType<()> { - let mut retry_count = 0; - const MAX_RETRIES: usize = 1; - loop { - if !is_x11() { - if CAP_DISPLAY_INFO.read().unwrap().is_empty() { - if crate::input_service::wayland_use_uinput() { - if let Some((minx, maxx, miny, maxy)) = - scrap::wayland::display::get_desktop_rect_for_uinput() - { - log::info!( - "update mouse resolution: ({}, {}), ({}, {})", - minx, - maxx, - miny, - maxy - ); - allow_err!( - input_service::update_mouse_resolution(minx, maxx, miny, maxy).await - ); - } else { - log::warn!("Failed to get desktop rect for uinput"); - } - } - - let mut lock = CAP_DISPLAY_INFO.write().unwrap(); - if lock.is_empty() { - // Check if PipeWire is already initialized to prevent duplicate recorder creation - if *PIPEWIRE_INITIALIZED.read().unwrap() { - log::warn!("wayland_diag: Preventing duplicate PipeWire initialization"); - return Ok(()); - } - - let mut all = Display::all()?; - log::debug!("Initializing displays with fill_displays()"); - { - let temp_mouse_move_handle = input_service::TemporaryMouseMoveHandle::new(); - let move_mouse_to = |x, y| temp_mouse_move_handle.move_mouse_to(x, y); - fill_displays(move_mouse_to, crate::get_cursor_pos, &mut all)?; - } - log::debug!("Attempting to fix logical size with try_fix_logical_size()"); - try_fix_logical_size(&mut all); - - // Bail early if no displays were found: the portal session was likely revoked (e.g., after screen lock). - if all.is_empty() { - log::warn!("check_init: no displays from PipeWire portal, session revoked. Closing stale session so next attempt triggers re-authorization."); - scrap::wayland::pipewire::close_session(); - bail!("No displays returned by PipeWire portal. Try reconnecting to request a new screen-sharing session."); - } - - let num = all.len(); - let primary = super::display_service::get_primary_2(&all); - super::display_service::check_update_displays(&all); - let mut displays = super::display_service::get_sync_displays(); - for display in displays.iter_mut() { - display.cursor_embedded = is_cursor_embedded(); - } - - let mut rects: Vec<((i32, i32), usize, usize)> = Vec::new(); - for d in &all { - rects.push((d.origin(), d.width(), d.height())); - } - - log::debug!( - "#displays={}, primary={}, rects: {:?}, cpus={}/{}", - num, - primary, - rects, - num_cpus::get_physical(), - num_cpus::get() +async fn check_init_once() -> ResultType<()> { + if !is_x11() { + if CAP_DISPLAY_INFO.read().unwrap().is_empty() { + if crate::input_service::wayland_use_uinput() { + if let Some((minx, maxx, miny, maxy)) = + scrap::wayland::display::get_desktop_rect_for_uinput() + { + log::info!( + "update mouse resolution: ({}, {}), ({}, {})", + minx, + maxx, + miny, + maxy ); - - // Create individual CapDisplayInfo for each display with its own capturer - let init_result: ResultType<()> = (|| { - for (idx, display) in all.into_iter().enumerate() { - let capturer = - Box::into_raw(Box::new(Capturer::new(display).with_context(|| { - format!("Failed to create capturer for display {}. Try reconnecting to request a new screen sharing session.", idx) - })?)); - let capturer = CapturerPtr(capturer); - - let cap_display_info = Box::into_raw(Box::new(CapDisplayInfo { - rects: rects.clone(), - displays: displays.clone(), - num, - primary, - current: idx, - capturer, - })); - - lock.insert(idx, cap_display_info as u64); - } - Ok(()) - })(); - - if let Err(e) = init_result { - log::error!("check_init: capturer loop failed, cleaning up partial state: {:?}", e); - for (_, addr) in lock.iter() { - let cap_display_info: *mut CapDisplayInfo = *addr as _; - unsafe { - let _box_capturer = Box::from_raw((*cap_display_info).capturer.0); - let _box_cap_display_info = Box::from_raw(cap_display_info); - } - } - lock.clear(); - - let err_str = format!("{:?}", e); - if err_str.contains("SESSION_REVOKED") { - log::warn!("check_init: Detected Wayland session death. Forcing hard reset"); - *PIPEWIRE_INITIALIZED.write().unwrap() = false; - scrap::wayland::pipewire::force_close_dead_session(); - if retry_count < MAX_RETRIES { - retry_count += 1; - continue; - } - } else { - scrap::wayland::pipewire::close_session(); - } - - return Err(e); - } - - // Only mark as initialized after the entire loop succeeds. - *PIPEWIRE_INITIALIZED.write().unwrap() = true; - break; + allow_err!( + input_service::update_mouse_resolution(minx, maxx, miny, maxy).await + ); + } else { + log::warn!("Failed to get desktop rect for uinput"); } } + + let mut lock = CAP_DISPLAY_INFO.write().unwrap(); + if lock.is_empty() { + // Check if PipeWire is already initialized to prevent duplicate recorder creation + if *PIPEWIRE_INITIALIZED.read().unwrap() { + log::warn!("wayland_diag: Preventing duplicate PipeWire initialization"); + return Ok(()); + } + + let mut all = Display::all()?; + log::debug!("Initializing displays with fill_displays()"); + { + let temp_mouse_move_handle = input_service::TemporaryMouseMoveHandle::new(); + let move_mouse_to = |x, y| temp_mouse_move_handle.move_mouse_to(x, y); + fill_displays(move_mouse_to, crate::get_cursor_pos, &mut all)?; + } + log::debug!("Attempting to fix logical size with try_fix_logical_size()"); + try_fix_logical_size(&mut all); + + // Bail early if no displays were found: the portal session was likely revoked (e.g., after screen lock). + if all.is_empty() { + log::warn!("check_init: no displays from PipeWire portal, session revoked."); + scrap::wayland::pipewire::close_session(); + bail!("No displays returned by PipeWire portal. Try reconnecting to request a new screen-sharing session."); + } + + let num = all.len(); + let primary = super::display_service::get_primary_2(&all); + super::display_service::check_update_displays(&all); + let mut displays = super::display_service::get_sync_displays(); + for display in displays.iter_mut() { + display.cursor_embedded = is_cursor_embedded(); + } + + let mut rects: Vec<((i32, i32), usize, usize)> = Vec::new(); + for d in &all { + rects.push((d.origin(), d.width(), d.height())); + } + + log::debug!( + "#displays={}, primary={}, rects: {:?}, cpus={}/{}", + num, + primary, + rects, + num_cpus::get_physical(), + num_cpus::get() + ); + + // Create individual CapDisplayInfo for each display with its own capturer + let init_result: ResultType<()> = (|| { + + for (idx, display) in all.into_iter().enumerate() { + let capturer = + Box::into_raw(Box::new(Capturer::new(display).with_context(|| { + format!("Failed to create capturer for display {}", idx) + })?)); + let capturer = CapturerPtr(capturer); + + let cap_display_info = Box::into_raw(Box::new(CapDisplayInfo { + rects: rects.clone(), + displays: displays.clone(), + num, + primary, + current: idx, + capturer, + })); + + lock.insert(idx, cap_display_info as u64); + } + + Ok(()) + })(); + + if let Err(e) = init_result { + log::error!("check_init: capturer loop failed, cleaning up partial state: {:?}", e); + for (_, addr) in lock.iter() { + let cap_display_info: *mut CapDisplayInfo = *addr as _; + unsafe { + let _box_capturer = Box::from_raw((*cap_display_info).capturer.0); + let _box_cap_display_info = Box::from_raw(cap_display_info); + } + } + lock.clear(); + + let err_str = format!("{:?}", e); + if err_str.contains("SESSION_REVOKED") { + log::warn!("check_init: Detected Wayland session death. Forcing hard reset"); + *PIPEWIRE_INITIALIZED.write().unwrap() = false; + scrap::wayland::pipewire::force_close_dead_session(); + } else { + scrap::wayland::pipewire::close_session(); + } + + return Err(e); + } + + // Only mark as initialized after the entire loop succeeds. + *PIPEWIRE_INITIALIZED.write().unwrap() = true; + } } - break; } Ok(()) } +pub(super) async fn check_init() -> ResultType<()> { + const MAX_RETRIES: usize = 1; + let mut retry_count = 0; + loop { + let result = check_init_once().await; + if let Err(ref e) = result { + if format!("{:?}", e).contains("SESSION_REVOKED") && retry_count < MAX_RETRIES { + retry_count += 1; + continue; + } + } + return result; + } +} + pub(super) async fn get_displays() -> ResultType> { check_init().await?; let cap_map = CAP_DISPLAY_INFO.read().unwrap(); @@ -276,7 +283,7 @@ pub(super) async fn get_displays() -> ResultType> { "get_displays: map empty after check_init(); resetting PIPEWIRE_INITIALIZED to allow retry." ); *PIPEWIRE_INITIALIZED.write().unwrap() = false; - bail!("Failed to get capturer display info. Please re-authorize screen sharing."); + bail!("Failed to get capturer display info"); } } @@ -374,7 +381,7 @@ pub(super) fn get_capturer_for_display( build_capturer_info(addr) } else { bail!( - "Failed to get capturer for display {}", + "Failed to get capturer display info for display {}", display_idx ); } From 6796c5d0232747eba2d7256f97ca7a1d0afa44ce Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:49:56 +0800 Subject: [PATCH 09/18] chore: add TODO about potential use-after-free in get_capturer_for_display() Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- src/server/wayland.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 607317bfc..a29ec0bca 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -369,6 +369,9 @@ pub(super) fn get_capturer_for_display( "get_capturer_for_display: no active capturers, reinitializing PipeWire session for display {}.", display_idx ); + + // to-do: Potential use-after-free: clone() aliases raw pointer and clear() frees it. + // Requires ownership refactor to make lifetime sound. clear(); ensure_inited()?; From cac4961595a7fd2f02a021b19c045dd54421416c Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Sat, 28 Feb 2026 15:44:06 +0800 Subject: [PATCH 10/18] fix: centralize SESSION_REVOKED sentinel and add retry delay Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- libs/scrap/src/wayland/pipewire.rs | 23 ++++++++++++++++++----- src/server/wayland.rs | 2 ++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 604cdf287..048969977 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -163,6 +163,17 @@ impl std::fmt::Display for GStreamerError { impl Error for GStreamerError {} +#[derive(Debug)] +pub struct SessionRevokedError; + +impl std::fmt::Display for SessionRevokedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SESSION_REVOKED") + } +} + +impl Error for SessionRevokedError {} + #[derive(Clone)] pub struct PipeWireCapturable { // connection needs to be kept alive for recording @@ -328,17 +339,19 @@ impl PipeWireRecorder { let _ = pipeline.set_state(gst::State::Null); let is_revoked = if !is_server_running() { - // remote_desktop_portal: always treat sync failure as revocation. + // remote_desktop_portal: the portal opened the fd, so a PLAYING failure + // means the underlying PipeWire node is gone -> session revoked. true } else { - // screencast_portal: only treat as revocation when a stale token exists. + // screencast_portal: only treat as revoked when a persisted restore token + // exists, meaning we were trying to resume a prior session. !config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY).is_empty() }; if is_revoked { warn!("[gstreamer] Failed to set PLAYING state, session was likely revoked: {:?}", e); config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); - return Err(hbb_common::anyhow::Error::msg(format!("SESSION_REVOKED: GStreamer failed: {:?}", e))); + return Err(hbb_common::anyhow::Error::new(SessionRevokedError)); } else { warn!( "[gstreamer] Failed to set PLAYING state on a fresh screencast session \ @@ -368,11 +381,11 @@ impl PipeWireRecorder { capturable.fd.as_raw_fd(), result, state, pending ); - if let Err(err) = result { + if let Err(_) = result { warn!("[gstreamer] Async pipeline error detected. Session was likely terminated, clearing XDP token..."); config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); let _ = pipeline.set_state(gst::State::Null); - return Err(hbb_common::anyhow::Error::msg(format!("SESSION_REVOKED: GStreamer pipeline failed: {:?}", err))); + return Err(hbb_common::anyhow::Error::new(SessionRevokedError)); } } } diff --git a/src/server/wayland.rs b/src/server/wayland.rs index a29ec0bca..bad5eef9d 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -261,6 +261,8 @@ pub(super) async fn check_init() -> ResultType<()> { if let Err(ref e) = result { if format!("{:?}", e).contains("SESSION_REVOKED") && retry_count < MAX_RETRIES { retry_count += 1; + // Brief pause before re-requesting the portal permission dialog, to avoid back-to-back prompts firing immediately. + tokio::time::sleep(std::time::Duration::from_millis(500)).await; continue; } } From 788c5d406ce13e12b9d496696f18191cab28b4e8 Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Sat, 28 Feb 2026 21:18:48 +0800 Subject: [PATCH 11/18] fix: move active display increment after capturer creation Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- src/server/video_service.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 13a781c28..eb9612dc5 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -538,6 +538,15 @@ fn run(vs: VideoService) -> ResultType<()> { // But for now, we do not support multi-screen capture on wayland. #[cfg(target_os = "linux")] super::wayland::ensure_inited()?; + + #[cfg(windows)] + let last_portable_service_running = crate::portable_service::client::running(); + #[cfg(not(windows))] + let last_portable_service_running = false; + + let display_idx = vs.idx; + let sp = vs.sp; + let mut c = get_capturer(vs.source, display_idx, last_portable_service_running)?; #[cfg(target_os = "linux")] let _wayland_call_on_ret = { // Increment active display count when starting @@ -554,15 +563,6 @@ fn run(vs: VideoService) -> ResultType<()> { }), } }; - - #[cfg(windows)] - let last_portable_service_running = crate::portable_service::client::running(); - #[cfg(not(windows))] - let last_portable_service_running = false; - - let display_idx = vs.idx; - let sp = vs.sp; - let mut c = get_capturer(vs.source, display_idx, last_portable_service_running)?; #[cfg(windows)] if !scrap::codec::enable_directx_capture() && !c.is_gdi() { log::info!("disable dxgi with option, fall back to gdi"); From 91eba100f5d8ef300a58b16160d07de09925a01b Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Sat, 28 Feb 2026 22:15:33 +0800 Subject: [PATCH 12/18] fix: UAF race between capturer clone and clear() Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- src/server/video_service.rs | 3 --- src/server/wayland.rs | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index eb9612dc5..ffe60551f 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -549,9 +549,6 @@ fn run(vs: VideoService) -> ResultType<()> { let mut c = get_capturer(vs.source, display_idx, last_portable_service_running)?; #[cfg(target_os = "linux")] let _wayland_call_on_ret = { - // Increment active display count when starting - let _display_count = super::wayland::increment_active_display_count(); - SimpleCallOnReturn { b: true, f: Box::new(|| { diff --git a/src/server/wayland.rs b/src/server/wayland.rs index bad5eef9d..d6e25c089 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -349,6 +349,7 @@ pub(super) fn get_capturer_for_display( { let cap_map = CAP_DISPLAY_INFO.read().unwrap(); if let Some(&addr) = cap_map.get(&display_idx) { + increment_active_display_count(); return build_capturer_info(addr); } } @@ -383,6 +384,7 @@ pub(super) fn get_capturer_for_display( "get_capturer_for_display: re-initialization succeeded for display {}.", display_idx ); + increment_active_display_count(); build_capturer_info(addr) } else { bail!( From 021d5a1b8337d33f19e305acb4a9be001c10461d Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Sun, 1 Mar 2026 14:37:12 +0800 Subject: [PATCH 13/18] fix: Wayland capture lifecycle and potential deadlock Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- src/server/video_service.rs | 21 ++++++++++++--------- src/server/wayland.rs | 5 +---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index ffe60551f..13a781c28 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -538,17 +538,11 @@ fn run(vs: VideoService) -> ResultType<()> { // But for now, we do not support multi-screen capture on wayland. #[cfg(target_os = "linux")] super::wayland::ensure_inited()?; - - #[cfg(windows)] - let last_portable_service_running = crate::portable_service::client::running(); - #[cfg(not(windows))] - let last_portable_service_running = false; - - let display_idx = vs.idx; - let sp = vs.sp; - let mut c = get_capturer(vs.source, display_idx, last_portable_service_running)?; #[cfg(target_os = "linux")] let _wayland_call_on_ret = { + // Increment active display count when starting + let _display_count = super::wayland::increment_active_display_count(); + SimpleCallOnReturn { b: true, f: Box::new(|| { @@ -560,6 +554,15 @@ fn run(vs: VideoService) -> ResultType<()> { }), } }; + + #[cfg(windows)] + let last_portable_service_running = crate::portable_service::client::running(); + #[cfg(not(windows))] + let last_portable_service_running = false; + + let display_idx = vs.idx; + let sp = vs.sp; + let mut c = get_capturer(vs.source, display_idx, last_portable_service_running)?; #[cfg(windows)] if !scrap::codec::enable_directx_capture() && !c.is_gdi() { log::info!("disable dxgi with option, fall back to gdi"); diff --git a/src/server/wayland.rs b/src/server/wayland.rs index d6e25c089..e56dbbb04 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -27,7 +27,6 @@ pub fn init() { } pub(super) fn increment_active_display_count() -> usize { - let _cap_map_guard = CAP_DISPLAY_INFO.read().unwrap(); let mut count = ACTIVE_DISPLAY_COUNT.write().unwrap(); *count += 1; *count @@ -349,7 +348,6 @@ pub(super) fn get_capturer_for_display( { let cap_map = CAP_DISPLAY_INFO.read().unwrap(); if let Some(&addr) = cap_map.get(&display_idx) { - increment_active_display_count(); return build_capturer_info(addr); } } @@ -361,7 +359,7 @@ pub(super) fn get_capturer_for_display( // Wait until all active capturers have exited before reinitializing. let active_count = *ACTIVE_DISPLAY_COUNT.read().unwrap(); - if active_count > 0 { + if active_count > 1 { bail!( "Display {} not found in CAP_DISPLAY_INFO, but {} active capturer(s) are still running. Skipping reinitialization now.", display_idx, active_count @@ -384,7 +382,6 @@ pub(super) fn get_capturer_for_display( "get_capturer_for_display: re-initialization succeeded for display {}.", display_idx ); - increment_active_display_count(); build_capturer_info(addr) } else { bail!( From 66e0e0c9e37e8e77bce00bd9cfa34dcda2169f94 Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Sun, 1 Mar 2026 20:41:23 +0800 Subject: [PATCH 14/18] fix: only clear RESTORE_TOKEN when is_server_running() Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- libs/scrap/src/wayland/pipewire.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 048969977..f91807efc 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -350,7 +350,9 @@ impl PipeWireRecorder { if is_revoked { warn!("[gstreamer] Failed to set PLAYING state, session was likely revoked: {:?}", e); - config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); + if is_server_running() { + config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); + } return Err(hbb_common::anyhow::Error::new(SessionRevokedError)); } else { warn!( @@ -383,7 +385,9 @@ impl PipeWireRecorder { if let Err(_) = result { warn!("[gstreamer] Async pipeline error detected. Session was likely terminated, clearing XDP token..."); - config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); + if is_server_running() { + config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); + } let _ = pipeline.set_state(gst::State::Null); return Err(hbb_common::anyhow::Error::new(SessionRevokedError)); } From c998f96c31d0d8f9bf2a4772c4f4d3b6b8bf5311 Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:16:15 +0800 Subject: [PATCH 15/18] fix: acquire the Inhitbit lock only when !is_server_running() Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- libs/scrap/src/wayland/pipewire.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index f91807efc..059d57cd7 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -1034,7 +1034,7 @@ pub fn get_capturables() -> Result, Box> { if rdp_connection.is_none() { let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop(false)?; let conn = Arc::new(conn); - let inhibit_path = request_inhibit(&conn); + let inhibit_path = if !is_server_running() { request_inhibit(&conn) } else { None }; let rdp_info = RdpSessionInfo { conn, From a629f5d95aad3cd3f14e3ffd0df936ea172796da Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Sun, 1 Mar 2026 22:01:38 +0800 Subject: [PATCH 16/18] chore: add TODO for optional keep-awake inhibition Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- libs/scrap/src/wayland/pipewire.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 059d57cd7..ba15cfa20 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -1034,6 +1034,8 @@ pub fn get_capturables() -> Result, Box> { if rdp_connection.is_none() { let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop(false)?; let conn = Arc::new(conn); + // to-do: Add an option like "Keep awake while RustDesk is running" to avoid + // impacting users whose sessions are not automatically terminated upon entering the Dim Screen. let inhibit_path = if !is_server_running() { request_inhibit(&conn) } else { None }; let rdp_info = RdpSessionInfo { From 194253a5d9aa1a5f4d689d29deea09d9c71733a0 Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:02:26 +0800 Subject: [PATCH 17/18] fix: simplify session revoked detection, let portal handle restore token Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- libs/scrap/src/wayland/pipewire.rs | 32 ++++++------------------------ 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index ba15cfa20..76666faa8 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -338,29 +338,12 @@ impl PipeWireRecorder { if let Err(e) = pipeline.set_state(gst::State::Playing) { let _ = pipeline.set_state(gst::State::Null); - let is_revoked = if !is_server_running() { - // remote_desktop_portal: the portal opened the fd, so a PLAYING failure - // means the underlying PipeWire node is gone -> session revoked. - true - } else { - // screencast_portal: only treat as revoked when a persisted restore token - // exists, meaning we were trying to resume a prior session. - !config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY).is_empty() - }; - - if is_revoked { - warn!("[gstreamer] Failed to set PLAYING state, session was likely revoked: {:?}", e); - if is_server_running() { - config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); - } - return Err(hbb_common::anyhow::Error::new(SessionRevokedError)); - } else { - warn!( - "[gstreamer] Failed to set PLAYING state on a fresh screencast session \ - (no restore token). Likely a pipeline misconfiguration: {:?}", - e - ); + if is_server_running() { + warn!("[gstreamer] Failed to set PLAYING state: {:?}", e); return Err(hbb_common::anyhow::Error::msg(format!("GStreamer pipeline failed to start: {:?}", e))); + } else { + warn!("[gstreamer] Failed to set PLAYING state, session was likely revoked: {:?}", e); + return Err(hbb_common::anyhow::Error::new(SessionRevokedError)); } } @@ -384,10 +367,7 @@ impl PipeWireRecorder { ); if let Err(_) = result { - warn!("[gstreamer] Async pipeline error detected. Session was likely terminated, clearing XDP token..."); - if is_server_running() { - config::LocalConfig::set_option(RESTORE_TOKEN_CONF_KEY.to_owned(), "".to_owned()); - } + warn!("[gstreamer] Async pipeline error detected. Session was likely terminated."); let _ = pipeline.set_state(gst::State::Null); return Err(hbb_common::anyhow::Error::new(SessionRevokedError)); } From dd4381880e850b938689da9d9e10ab3bdd719cd5 Mon Sep 17 00:00:00 2001 From: Steve3184 <77789906+Steve3184@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:47:24 +0800 Subject: [PATCH 18/18] revert: drop Inhibit portal workaround for KDE session termination Signed-off-by: Steve3184 <77789906+Steve3184@users.noreply.github.com> --- libs/scrap/src/wayland/pipewire.rs | 49 ------------------------------ 1 file changed, 49 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 76666faa8..7cf18bdd0 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -110,17 +110,7 @@ pub struct RdpSessionInfo { pub session: dbus::Path<'static>, pub is_support_restore_token: bool, pub resolution: Arc>>, - inhibit_request_path: Option>, } - -impl Drop for RdpSessionInfo { - fn drop(&mut self) { - if let Some(ref path) = self.inhibit_request_path { - release_inhibit(&self.conn, path); - } - } -} - #[derive(Debug, Clone, Copy)] pub struct PwStreamInfo { pub path: u64, @@ -970,41 +960,6 @@ fn on_start_response( } } -fn request_inhibit(conn: &SyncConnection) -> Option> { - let proxy = conn.with_proxy( - "org.freedesktop.portal.Desktop", - "/org/freedesktop/portal/desktop", - Duration::from_millis(1000), - ); - let mut args: PropMap = HashMap::new(); - args.insert("handle_token".to_string(), Variant(Box::new("inhibit1".to_string())),); - // flags: 8 = inhibit idle, 4 = inhibit suspend - // Based on current testing, Dim Screen will forcibly terminate the session. - // Solving this problem requires preventing entry into the idle state. - // However, for future considerations, entering the suspended state is also prevented here. - match proxy.method_call::<(dbus::Path<'static>,), _, _, _>("org.freedesktop.portal.Inhibit", "Inhibit", ("", 12u32, args)) { - Ok((path,)) => { - debug!("Inhibit requested, request path: {:?}", path); - Some(path) - } - Err(e) => { - warn!("Failed to request inhibit: {}", e); - None - } - } -} - -fn release_inhibit(conn: &SyncConnection, path: &dbus::Path<'static>) { - let proxy = conn.with_proxy( - "org.freedesktop.portal.Desktop", - path, - Duration::from_millis(1000), - ); - if let Err(e) = proxy.method_call::<(), _, _, _>("org.freedesktop.portal.Request", "Close", ()) { - warn!("Failed to release inhibit: {}", e); - } -} - pub fn get_capturables() -> Result, Box> { let mut rdp_connection = match RDP_SESSION_INFO.lock() { Ok(conn) => conn, @@ -1014,9 +969,6 @@ pub fn get_capturables() -> Result, Box> { if rdp_connection.is_none() { let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop(false)?; let conn = Arc::new(conn); - // to-do: Add an option like "Keep awake while RustDesk is running" to avoid - // impacting users whose sessions are not automatically terminated upon entering the Dim Screen. - let inhibit_path = if !is_server_running() { request_inhibit(&conn) } else { None }; let rdp_info = RdpSessionInfo { conn, @@ -1025,7 +977,6 @@ pub fn get_capturables() -> Result, Box> { session, is_support_restore_token, resolution: Arc::new(Mutex::new(None)), - inhibit_request_path: inhibit_path, }; *rdp_connection = Some(rdp_info); }