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:
fufesou
2026-06-17 17:50:18 +08:00
committed by GitHub
parent 7c26575dbd
commit 88ae00ba73
4 changed files with 92 additions and 11 deletions

View File

@@ -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();

View File

@@ -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";

View File

@@ -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;

View File

@@ -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]