mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-06-22 10:02:20 +00:00
Fix iOS auto-reconnect stall when prior session was on a non-primary display
Symptom
-------
With a self-hosted (or public) hbbs/hbbr stack, an iOS RustDesk client
controlling a Windows host with multiple monitors hangs at "Connected,
Waiting for image" after auto-reconnecting from a network drop, but only
when the previous session was viewing a non-primary display. Reconnects
that started from the primary display always succeed.
Root cause
----------
On reconnect the iOS client re-establishes the peer connection but does
NOT re-issue its display selection (no `CaptureDisplays` or
`SwitchDisplay` is sent). The host correctly defaults `display_idx` to
PRIMARY_DISPLAY_IDX in `Connection::start()` and brings up only
`monitor0`'s capturer. The iOS UI is still in "display N" mode from the
previous session and silently drops or waits-on frames it considers
mismatched, producing the indefinite "Waiting for image" state.
Verified against real service logs: in the failure case the host emits
`new video service: monitor0` and the matching snapshot, but no
`monitor1` is ever created during the entire lifetime of the
auto-reconnected session.
Fix
---
Persist the most recent active display set per remote peer id in a new
`LAST_VIEW_BY_PEER` map (in-memory, same pattern as the adjacent
`LOGIN_FAILURES` / `SESSIONS` statics). Hook the persist on both code
paths that change the active capture set (`switch_display_to` for older
clients, `capture_displays` for >= 1.2.4 clients).
On `handle_login_request_without_validation`, look up the stored set for
this peer's `my_id`. If it differs from primary-only:
* Validate each stored index against the currently-present displays
(drops stale entries if a monitor was unplugged between sessions).
* Pre-set `self.display_idx` so the upcoming PeerInfo response carries
the restored display index, keeping the iOS UI's expected display in
sync with the frames the host is about to emit.
* Stash the validated set in `pending_display_set`.
After `try_sub_monitor_services()` finishes (both call sites updated),
`apply_pending_display_set()` runs `self.capture_displays(&[], &[],
&set)` to actually swap to the restored capturer. Both paths fully
release locks before any `.await`.
Also clears any prior pending state at the top of
`handle_login_request_without_validation` to avoid leaking pending state
across re-logins on the same `Connection` instance.
Behaviour for the common case (primary-only, fresh peer, anonymous peer
with empty `my_id`) is unchanged.
Verification
------------
Reproduced and verified on Windows 11 host (build under
`flutter,hwcodec,vram`) with a 2-monitor setup against an iOS RustDesk
client through a self-hosted hbbs 1.1.15.
Failing run logs (before patch):
monitor0 created on reconnect; no monitor1 ever created; iOS stalls.
Passing run logs (after patch):
L36 Connection #1381 opened
L84 Persisted view [1] for peer 1130614991
L111 Connection closed: Timeout <- airplane mode
L144 Connection #1382 opened <- auto-reconnect
L145 Will restore previously viewed displays [1] for peer 1130614991
L155 Restoring previously viewed display set [1] after services subscribed
L162 new video service: monitor1 <- without any iOS message
iOS resumes display 2 video, no stall.
This commit is contained in:
parent
5abae617dc
commit
89f6f6f0ad
1 changed files with 153 additions and 0 deletions
|
|
@ -76,6 +76,10 @@ lazy_static::lazy_static! {
|
|||
static ref SWITCH_SIDES_UUID: Arc::<Mutex<HashMap<String, (Instant, uuid::Uuid)>>> = Default::default();
|
||||
static ref WAKELOCK_SENDER: Arc::<Mutex<std::sync::mpsc::Sender<(usize, usize)>>> = Arc::new(Mutex::new(start_wakelock_thread()));
|
||||
static ref WAKELOCK_KEEP_AWAKE_OPTION: Arc::<Mutex<Option<bool>>> = Default::default();
|
||||
// Last-active display set per remote peer id. Restored on next login from
|
||||
// the same peer so that auto-reconnect resumes on the previously selected
|
||||
// display (iOS does not re-issue its display selection on auto-reconnect).
|
||||
static ref LAST_VIEW_BY_PEER: Arc::<Mutex<HashMap<String, Vec<usize>>>> = Default::default();
|
||||
}
|
||||
|
||||
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
|
||||
|
|
@ -292,6 +296,14 @@ pub struct Connection {
|
|||
file_remove_log_control: FileRemoveLogControl,
|
||||
last_supported_encoding: Option<SupportedEncoding>,
|
||||
services_subed: bool,
|
||||
// Buffered SwitchDisplay (from early arrival) and pending display set to
|
||||
// restore after services are subscribed. Together these fix the iOS
|
||||
// auto-reconnect "waiting for image" hang when the prior session was on
|
||||
// a non-primary display: iOS does not re-issue its display selection on
|
||||
// auto-reconnect, so the host must restore the previously-active display
|
||||
// set itself based on remembered per-peer state.
|
||||
pending_display_switch: Option<usize>,
|
||||
pending_display_set: Option<Vec<usize>>,
|
||||
delayed_read_dir: Option<(String, bool)>,
|
||||
#[cfg(target_os = "macos")]
|
||||
retina: Retina,
|
||||
|
|
@ -482,6 +494,8 @@ impl Connection {
|
|||
file_remove_log_control: FileRemoveLogControl::new(id),
|
||||
last_supported_encoding: None,
|
||||
services_subed: false,
|
||||
pending_display_switch: None,
|
||||
pending_display_set: None,
|
||||
delayed_read_dir: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
retina: Retina::default(),
|
||||
|
|
@ -1733,6 +1747,14 @@ impl Connection {
|
|||
self.retina.set_displays(&displays);
|
||||
}
|
||||
pi.displays = displays;
|
||||
// Clamp against the just-built `pi.displays` in case
|
||||
// self.display_idx was pre-set in handle_login_request_without_validation
|
||||
// from a snapshot that has since become stale (a monitor
|
||||
// unplugged in the brief window between login validation
|
||||
// and PeerInfo emit).
|
||||
if self.display_idx >= pi.displays.len() {
|
||||
self.display_idx = *display_service::PRIMARY_DISPLAY_IDX;
|
||||
}
|
||||
pi.current_display = self.display_idx as _;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
|
|
@ -1790,6 +1812,8 @@ impl Connection {
|
|||
} else if sub_service {
|
||||
if !wait_session_id_confirm {
|
||||
self.try_sub_monitor_services();
|
||||
self.apply_pending_display_switch().await;
|
||||
self.apply_pending_display_set().await;
|
||||
}
|
||||
}
|
||||
true
|
||||
|
|
@ -2258,6 +2282,33 @@ impl Connection {
|
|||
|
||||
async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) {
|
||||
self.lr = lr.clone();
|
||||
// Clear any prior pending state in case this Connection handles re-login.
|
||||
self.pending_display_set = None;
|
||||
self.pending_display_switch = None;
|
||||
// Restore previously-active display set for this peer so auto-reconnect
|
||||
// resumes on the prior display. Only kicks in when the prior set is
|
||||
// non-empty and isn't just the primary (no behaviour change for the
|
||||
// common case). lr.my_id uniquely identifies the remote peer across
|
||||
// reconnections.
|
||||
if !lr.my_id.is_empty() {
|
||||
if let Some(prev_set) = LAST_VIEW_BY_PEER.lock().unwrap().get(&lr.my_id).cloned() {
|
||||
let primary = *display_service::PRIMARY_DISPLAY_IDX;
|
||||
let valid = Self::validate_display_set(&prev_set, &lr.my_id);
|
||||
if !valid.is_empty() && valid != vec![primary] {
|
||||
log::info!(
|
||||
"Will restore previously viewed displays {:?} for peer {} after services subscribed",
|
||||
valid, lr.my_id
|
||||
);
|
||||
// Pre-set display_idx so the PeerInfo response carries the
|
||||
// restored display index, keeping the iOS UI's expected
|
||||
// display in sync with the frames we'll soon emit.
|
||||
if valid.len() == 1 {
|
||||
self.display_idx = valid[0];
|
||||
}
|
||||
self.pending_display_set = Some(valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.peer_argb = crate::str2color(&format!("{}{}", &lr.my_id, &lr.my_platform), 0xff);
|
||||
if let Some(o) = lr.option.as_ref() {
|
||||
self.options_in_login = Some(o.clone());
|
||||
|
|
@ -3354,6 +3405,8 @@ impl Connection {
|
|||
self.try_sub_camera_displays();
|
||||
} else if !self.terminal {
|
||||
self.try_sub_monitor_services();
|
||||
self.apply_pending_display_switch().await;
|
||||
self.apply_pending_display_set().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3686,6 +3739,24 @@ impl Connection {
|
|||
|
||||
async fn handle_switch_display(&mut self, s: SwitchDisplay) {
|
||||
let display_idx = s.display as usize;
|
||||
if !self.services_subed {
|
||||
// Defer until try_sub_monitor_services() registers this connection
|
||||
// as a subscriber. Otherwise switch_display_to() calls
|
||||
// server.subscribe() for a connection the server doesn't yet know,
|
||||
// the subscription is silently lost, and the client (notably iOS
|
||||
// on auto-reconnect to a non-primary display) hangs waiting for
|
||||
// frames that never arrive.
|
||||
log::info!(
|
||||
"Deferring SwitchDisplay({}) until video services subscribed",
|
||||
display_idx
|
||||
);
|
||||
// An explicit client-side display selection should win over any
|
||||
// login-time auto-restore — clear the restore queue so it cannot
|
||||
// overwrite this newer choice.
|
||||
self.pending_display_set = None;
|
||||
self.pending_display_switch = Some(display_idx);
|
||||
return;
|
||||
}
|
||||
if self.display_idx != display_idx {
|
||||
if let Some(server) = self.server.upgrade() {
|
||||
self.switch_display_to(display_idx, server.clone());
|
||||
|
|
@ -3723,6 +3794,61 @@ impl Connection {
|
|||
}
|
||||
}
|
||||
|
||||
async fn apply_pending_display_switch(&mut self) {
|
||||
if let Some(display_idx) = self.pending_display_switch.take() {
|
||||
log::info!(
|
||||
"Applying deferred SwitchDisplay({}) after services subscribed",
|
||||
display_idx
|
||||
);
|
||||
let s = SwitchDisplay {
|
||||
display: display_idx as i32,
|
||||
..Default::default()
|
||||
};
|
||||
// Safe re-entry: at this call site `services_subed` is true, so
|
||||
// handle_switch_display's defer branch (`!self.services_subed`)
|
||||
// is guaranteed to be skipped — no infinite re-deferral loop.
|
||||
self.handle_switch_display(s).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn apply_pending_display_set(&mut self) {
|
||||
if let Some(set) = self.pending_display_set.take() {
|
||||
// Re-validate against current monitor topology: this method can
|
||||
// fire well after handle_login_request_without_validation (e.g.,
|
||||
// after SelectedSid in multi-user-session-confirm flows), so the
|
||||
// set we validated at login time may have gone stale if a monitor
|
||||
// was unplugged or the display config changed in the interim.
|
||||
let valid = Self::validate_display_set(&set, &self.lr.my_id);
|
||||
if valid.is_empty() {
|
||||
return;
|
||||
}
|
||||
log::info!(
|
||||
"Restoring previously viewed display set {:?} after services subscribed",
|
||||
valid
|
||||
);
|
||||
self.capture_displays(&[], &[], &valid).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop any indices in `set` that no longer correspond to a currently-present
|
||||
/// display. Validates against the cached `SYNC_DISPLAYS` rather than calling
|
||||
/// `Display::all()` so we don't reintroduce the Wayland blocking/crash issue
|
||||
/// that `display_service::check_displays_changed()` explicitly avoids. If the
|
||||
/// cache is empty (e.g., a login racing `update_get_sync_displays_on_login`)
|
||||
/// we conservatively skip the restore — starting a capturer for an unknown
|
||||
/// index would defeat the whole point of this validation.
|
||||
fn validate_display_set(set: &[usize], peer_id: &str) -> Vec<usize> {
|
||||
let displays = display_service::get_sync_displays();
|
||||
if displays.is_empty() {
|
||||
log::warn!(
|
||||
"Skipping display restore for peer {} because display cache is empty",
|
||||
peer_id
|
||||
);
|
||||
return Vec::new();
|
||||
}
|
||||
set.iter().copied().filter(|idx| *idx < displays.len()).collect()
|
||||
}
|
||||
|
||||
fn switch_display_to(&mut self, display_idx: usize, server: Arc<RwLock<Server>>) {
|
||||
let new_service_name = video_service::get_service_name(self.video_source(), display_idx);
|
||||
let old_service_name =
|
||||
|
|
@ -3743,6 +3869,15 @@ impl Connection {
|
|||
}
|
||||
lock.subscribe(&new_service_name, self.inner.clone(), true);
|
||||
self.display_idx = display_idx;
|
||||
// Persist for auto-reconnect restoration (older-client SwitchDisplay
|
||||
// path). Camera-mode indices are not persisted: they're meaningless for
|
||||
// a future monitor session and would confuse the restore path.
|
||||
if !self.lr.my_id.is_empty() && matches!(self.video_source(), VideoSource::Monitor) {
|
||||
LAST_VIEW_BY_PEER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(self.lr.my_id.clone(), vec![display_idx]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
|
|
@ -3800,6 +3935,24 @@ impl Connection {
|
|||
}
|
||||
drop(lock);
|
||||
}
|
||||
// Persist the current view for this peer so a future auto-reconnect
|
||||
// can restore it. We persist only when:
|
||||
// - `set` is non-empty (the canonical "view this exact set" call;
|
||||
// add/sub updates are partial),
|
||||
// - the video source is the monitor (camera-mode display indices are
|
||||
// meaningless in a future monitor session and would only confuse the
|
||||
// restore path), and
|
||||
// - the peer has a non-empty id we can key by.
|
||||
if !set.is_empty()
|
||||
&& !self.lr.my_id.is_empty()
|
||||
&& matches!(video_source, VideoSource::Monitor)
|
||||
{
|
||||
LAST_VIEW_BY_PEER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(self.lr.my_id.clone(), set.to_vec());
|
||||
log::info!("Persisted view {:?} for peer {}", set, self.lr.my_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue