mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-17 21:35:03 +03:00
refact: restart remote device, autoconnect (#15290)
* refact: restart remote device, autoconnect Signed-off-by: fufesou <linlong1266@gmail.com> * fix: guard restart reconnect timer after session close Signed-off-by: fufesou <linlong1266@gmail.com> * Simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(restart): auto connect, comments Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -55,6 +55,8 @@ import 'package:flutter_hbb/native/custom_cursor.dart'
|
|||||||
typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
|
typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
|
||||||
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
|
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
|
||||||
final _constSessionId = Uuid().v4obj();
|
final _constSessionId = Uuid().v4obj();
|
||||||
|
// Empirical restart reconnect cadence: keep the last frame briefly and retry quickly.
|
||||||
|
const _restartReconnectSilentDelaySecs = 5;
|
||||||
|
|
||||||
class CachedPeerData {
|
class CachedPeerData {
|
||||||
Map<String, dynamic> updatePrivacyMode = {};
|
Map<String, dynamic> updatePrivacyMode = {};
|
||||||
@@ -119,6 +121,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
bool _touchMode = false;
|
bool _touchMode = false;
|
||||||
late VirtualMouseMode virtualMouseMode;
|
late VirtualMouseMode virtualMouseMode;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
|
Timer? _restartReconnectDelayTimer;
|
||||||
var _reconnects = 1;
|
var _reconnects = 1;
|
||||||
DateTime? _offlineReconnectStartTime;
|
DateTime? _offlineReconnectStartTime;
|
||||||
bool _viewOnly = false;
|
bool _viewOnly = false;
|
||||||
@@ -250,6 +253,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
_inputBlocked = false;
|
_inputBlocked = false;
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = null;
|
_timer = null;
|
||||||
|
resetRestartReconnectState();
|
||||||
clearPermissions();
|
clearPermissions();
|
||||||
waitForImageTimer?.cancel();
|
waitForImageTimer?.cancel();
|
||||||
timerScreenshot?.cancel();
|
timerScreenshot?.cancel();
|
||||||
@@ -341,6 +345,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'connection_ready') {
|
} else if (name == 'connection_ready') {
|
||||||
setConnectionType(peerId, evt['secure'] == 'true',
|
setConnectionType(peerId, evt['secure'] == 'true',
|
||||||
evt['direct'] == 'true', evt['stream_type'] ?? '');
|
evt['direct'] == 'true', evt['stream_type'] ?? '');
|
||||||
|
resetRestartReconnectState();
|
||||||
} else if (name == 'switch_display') {
|
} else if (name == 'switch_display') {
|
||||||
// switch display is kept for backward compatibility
|
// switch display is kept for backward compatibility
|
||||||
handleSwitchDisplay(evt, sessionId, peerId);
|
handleSwitchDisplay(evt, sessionId, peerId);
|
||||||
@@ -922,8 +927,28 @@ class FfiModel with ChangeNotifier {
|
|||||||
enterUserLoginAndPasswordDialog(
|
enterUserLoginAndPasswordDialog(
|
||||||
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
||||||
} else if (type == 'restarting') {
|
} else if (type == 'restarting') {
|
||||||
showMsgBox(sessionId, type, title, text, link, false, dialogManager,
|
// Treat restart messages as reconnect control events. Rust still sends
|
||||||
hasCancel: false);
|
// title/text for legacy UI and translation reuse; Flutter keeps the last
|
||||||
|
// frame briefly, then shows the Connecting overlay.
|
||||||
|
if (_restartReconnectDelayTimer == null) {
|
||||||
|
parent.target?.inputModel.setRelativeMouseMode(false);
|
||||||
|
bind.sessionReconnect(sessionId: sessionId, forceRelay: false);
|
||||||
|
clearPermissions();
|
||||||
|
// Retry once more after the silent window so restart reconnect attempts
|
||||||
|
// are spaced by the empirical short cadence instead of only updating UI.
|
||||||
|
_restartReconnectDelayTimer =
|
||||||
|
Timer(Duration(seconds: _restartReconnectSilentDelaySecs), () {
|
||||||
|
_restartReconnectDelayTimer = null;
|
||||||
|
if (parent.target?.closed == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reconnect(dialogManager, sessionId, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (type == 'restarting-show') {
|
||||||
|
_restartReconnectDelayTimer?.cancel();
|
||||||
|
_restartReconnectDelayTimer = null;
|
||||||
|
reconnect(dialogManager, sessionId, false);
|
||||||
} else if (type == 'wait-remote-accept-nook') {
|
} else if (type == 'wait-remote-accept-nook') {
|
||||||
showWaitAcceptDialog(sessionId, type, title, text, dialogManager);
|
showWaitAcceptDialog(sessionId, type, title, text, dialogManager);
|
||||||
} else if (type == 'on-uac' || type == 'on-foreground-elevated') {
|
} else if (type == 'on-uac' || type == 'on-foreground-elevated') {
|
||||||
@@ -949,6 +974,11 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetRestartReconnectState() {
|
||||||
|
_restartReconnectDelayTimer?.cancel();
|
||||||
|
_restartReconnectDelayTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Auto-retry check for "Remote desktop is offline" error.
|
/// Auto-retry check for "Remote desktop is offline" error.
|
||||||
/// returns true to auto-retry, false otherwise.
|
/// returns true to auto-retry, false otherwise.
|
||||||
bool shouldAutoRetryOnOffline(
|
bool shouldAutoRetryOnOffline(
|
||||||
@@ -1374,6 +1404,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
if (displays.isNotEmpty) {
|
if (displays.isNotEmpty) {
|
||||||
_reconnects = 1;
|
_reconnects = 1;
|
||||||
_offlineReconnectStartTime = null;
|
_offlineReconnectStartTime = null;
|
||||||
|
resetRestartReconnectState();
|
||||||
waitForFirstImage.value = true;
|
waitForFirstImage.value = true;
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
}
|
}
|
||||||
@@ -3666,6 +3697,7 @@ class FFI {
|
|||||||
|
|
||||||
/// Mobile reuse FFI
|
/// Mobile reuse FFI
|
||||||
void mobileReset() {
|
void mobileReset() {
|
||||||
|
ffiModel.resetRestartReconnectState();
|
||||||
ffiModel.waitForFirstImage.value = true;
|
ffiModel.waitForFirstImage.value = true;
|
||||||
ffiModel.isRefreshing = false;
|
ffiModel.isRefreshing = false;
|
||||||
ffiModel.waitForImageDialogShow.value = true;
|
ffiModel.waitForImageDialogShow.value = true;
|
||||||
@@ -3879,6 +3911,7 @@ class FFI {
|
|||||||
}
|
}
|
||||||
if (ffiModel.waitForFirstImage.value == true) {
|
if (ffiModel.waitForFirstImage.value == true) {
|
||||||
ffiModel.waitForFirstImage.value = false;
|
ffiModel.waitForFirstImage.value = false;
|
||||||
|
ffiModel.resetRestartReconnectState();
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
await canvasModel.updateViewStyle();
|
await canvasModel.updateViewStyle();
|
||||||
await canvasModel.updateScrollStyle();
|
await canvasModel.updateScrollStyle();
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ pub mod screenshot;
|
|||||||
|
|
||||||
pub const MILLI1: Duration = Duration::from_millis(1);
|
pub const MILLI1: Duration = Duration::from_millis(1);
|
||||||
pub const SEC30: Duration = Duration::from_secs(30);
|
pub const SEC30: Duration = Duration::from_secs(30);
|
||||||
|
// Empirical restart reconnect grace window.
|
||||||
|
const RESTART_REMOTE_DEVICE_GRACE: Duration = Duration::from_secs(5 * 60);
|
||||||
pub const VIDEO_QUEUE_SIZE: usize = 120;
|
pub const VIDEO_QUEUE_SIZE: usize = 120;
|
||||||
const MAX_DECODE_FAIL_COUNTER: usize = 3;
|
const MAX_DECODE_FAIL_COUNTER: usize = 3;
|
||||||
|
|
||||||
@@ -1740,7 +1742,10 @@ pub struct LoginConfigHandler {
|
|||||||
features: Option<Features>,
|
features: Option<Features>,
|
||||||
pub session_id: u64, // used for local <-> server communication
|
pub session_id: u64, // used for local <-> server communication
|
||||||
pub supported_encoding: SupportedEncoding,
|
pub supported_encoding: SupportedEncoding,
|
||||||
pub restarting_remote_device: bool,
|
restarting_remote_device: bool,
|
||||||
|
// Start time of the restart grace window. On Windows the peer may briefly
|
||||||
|
// reconnect before the real reboot disconnect.
|
||||||
|
restart_remote_device_at: Option<Instant>,
|
||||||
pub force_relay: bool,
|
pub force_relay: bool,
|
||||||
pub direct: Option<bool>,
|
pub direct: Option<bool>,
|
||||||
pub received: bool,
|
pub received: bool,
|
||||||
@@ -1849,7 +1854,7 @@ impl LoginConfigHandler {
|
|||||||
}
|
}
|
||||||
self.session_id = sid;
|
self.session_id = sid;
|
||||||
self.supported_encoding = Default::default();
|
self.supported_encoding = Default::default();
|
||||||
self.restarting_remote_device = false;
|
self.clear_restarting_remote_device();
|
||||||
self.force_relay =
|
self.force_relay =
|
||||||
config::option2bool("force-always-relay", &self.get_option("force-always-relay"))
|
config::option2bool("force-always-relay", &self.get_option("force-always-relay"))
|
||||||
|| force_relay
|
|| force_relay
|
||||||
@@ -2779,6 +2784,30 @@ impl LoginConfigHandler {
|
|||||||
msg_out
|
msg_out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mark_restarting_remote_device(&mut self) {
|
||||||
|
self.restarting_remote_device = true;
|
||||||
|
self.restart_remote_device_at = Some(Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_restarting_remote_device(&mut self) {
|
||||||
|
self.restarting_remote_device = false;
|
||||||
|
self.restart_remote_device_at = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_restarting_remote_device(&self) -> bool {
|
||||||
|
if !self.restarting_remote_device {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Keep this flag alive for a short grace window instead of clearing it on
|
||||||
|
// connection_ready or the first peer bytes. During OS restart the peer can
|
||||||
|
// briefly reconnect before the real reboot disconnect, and clearing it too
|
||||||
|
// early would let the next disconnect escape the restart flow and fall back
|
||||||
|
// to the normal error dialog / manual reconnect path.
|
||||||
|
self.restart_remote_device_at
|
||||||
|
.map(|started_at| started_at.elapsed() < RESTART_REMOTE_DEVICE_GRACE)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_conn_token(&self) -> Option<String> {
|
pub fn get_conn_token(&self) -> Option<String> {
|
||||||
if self.password.is_empty() {
|
if self.password.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
@@ -3718,9 +3747,18 @@ pub trait Interface: Send + Clone + 'static + Sized {
|
|||||||
fn on_establish_connection_error(&self, err: String) {
|
fn on_establish_connection_error(&self, err: String) {
|
||||||
let title = "Connection Error";
|
let title = "Connection Error";
|
||||||
let text = err.to_string();
|
let text = err.to_string();
|
||||||
let lc = self.get_lch();
|
let lch = self.get_lch();
|
||||||
let direct = lc.read().unwrap().direct;
|
let (is_restarting, direct, received) = {
|
||||||
let received = lc.read().unwrap().received;
|
let lc = lch.read().unwrap();
|
||||||
|
(lc.is_restarting_remote_device(), lc.direct, lc.received)
|
||||||
|
};
|
||||||
|
if is_restarting {
|
||||||
|
log::info!("Restart remote device, suppress connection error: {err}");
|
||||||
|
// Flutter treats this as a reconnect control event. The text is kept
|
||||||
|
// for legacy UI and existing translation reuse.
|
||||||
|
self.msgbox("restarting", "Restarting remote device", "Connection in progress. Please wait.", "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let mut relay_hint = false;
|
let mut relay_hint = false;
|
||||||
let mut relay_hint_type = "relay-hint";
|
let mut relay_hint_type = "relay-hint";
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ use crate::{
|
|||||||
common::get_default_sound_input,
|
common::get_default_sound_input,
|
||||||
ui_session_interface::{InvokeUiSession, Session},
|
ui_session_interface::{InvokeUiSession, Session},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Empirical no-data window before exposing the restart reconnect state to the UI.
|
||||||
|
// Restart msgbox text is kept as a legacy UI fallback; Flutter handles the type as a control event.
|
||||||
|
const RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
#[cfg(feature = "unix-file-copy-paste")]
|
#[cfg(feature = "unix-file-copy-paste")]
|
||||||
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
|
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
@@ -153,7 +157,6 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut last_recv_time = Instant::now();
|
|
||||||
let mut received = false;
|
let mut received = false;
|
||||||
let conn_type = if self.handler.is_file_transfer() {
|
let conn_type = if self.handler.is_file_transfer() {
|
||||||
ConnType::FILE_TRANSFER
|
ConnType::FILE_TRANSFER
|
||||||
@@ -219,6 +222,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
let mut fps_instant = Instant::now();
|
let mut fps_instant = Instant::now();
|
||||||
|
|
||||||
let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await;
|
let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await;
|
||||||
|
let mut last_recv_time = Instant::now();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@@ -244,7 +248,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
} else {
|
} else {
|
||||||
if self.handler.is_restarting_remote_device() {
|
if self.handler.is_restarting_remote_device() {
|
||||||
log::info!("Restart remote device");
|
log::info!("Restart remote device");
|
||||||
self.handler.msgbox("restarting", "Restarting remote device", "remote_restarting_tip", "");
|
self.handler.msgbox("restarting", "Restarting remote device", "Connection in progress. Please wait.", "");
|
||||||
} else {
|
} else {
|
||||||
log::info!("Reset by the peer");
|
log::info!("Reset by the peer");
|
||||||
self.handler.msgbox("error", "Connection Error", "Reset by the peer", "");
|
self.handler.msgbox("error", "Connection Error", "Reset by the peer", "");
|
||||||
@@ -279,6 +283,12 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = status_timer.tick() => {
|
_ = status_timer.tick() => {
|
||||||
|
if self.handler.is_restarting_remote_device()
|
||||||
|
&& last_recv_time.elapsed() >= RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT
|
||||||
|
{
|
||||||
|
self.handler.msgbox("restarting-show", "Restarting remote device", "Connection in progress. Please wait.", "");
|
||||||
|
break;
|
||||||
|
}
|
||||||
let elapsed = fps_instant.elapsed().as_millis();
|
let elapsed = fps_instant.elapsed().as_millis();
|
||||||
if elapsed < 1000 {
|
if elapsed < 1000 {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ impl<T: InvokeUiSession> Session<T> {
|
|||||||
|
|
||||||
pub fn restart_remote_device(&self) {
|
pub fn restart_remote_device(&self) {
|
||||||
let mut lc = self.lc.write().unwrap();
|
let mut lc = self.lc.write().unwrap();
|
||||||
lc.restarting_remote_device = true;
|
lc.mark_restarting_remote_device();
|
||||||
let msg = lc.restart_remote_device();
|
let msg = lc.restart_remote_device();
|
||||||
self.send(Data::Message(msg));
|
self.send(Data::Message(msg));
|
||||||
}
|
}
|
||||||
@@ -656,7 +656,7 @@ impl<T: InvokeUiSession> Session<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_restarting_remote_device(&self) -> bool {
|
pub fn is_restarting_remote_device(&self) -> bool {
|
||||||
self.lc.read().unwrap().restarting_remote_device
|
self.lc.read().unwrap().is_restarting_remote_device()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
Reference in New Issue
Block a user