From bdd3bb946e94b967efa2796b1f5333e314b271a8 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:51:53 +0800 Subject: [PATCH] refact: restore terminals (#12334) Signed-off-by: fufesou --- flutter/lib/consts.dart | 1 + .../lib/desktop/pages/terminal_tab_page.dart | 41 ++++++++++++++++--- flutter/lib/models/terminal_model.dart | 14 +++++++ libs/hbb_common | 2 +- src/flutter.rs | 3 ++ src/server/terminal_service.rs | 23 +++++++++++ 6 files changed, 78 insertions(+), 6 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index ef42318f0..eda0e11cf 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -64,6 +64,7 @@ const String kWindowEventNewFileTransfer = "new_file_transfer"; const String kWindowEventNewViewCamera = "new_view_camera"; const String kWindowEventNewPortForward = "new_port_forward"; const String kWindowEventNewTerminal = "new_terminal"; +const String kWindowEventRestoreTerminalSessions = "restore_terminal_sessions"; const String kWindowEventActiveSession = "active_session"; const String kWindowEventActiveDisplaySession = "active_display_session"; const String kWindowEventGetRemoteList = "get_remote_list"; diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart index ee2529107..60f20e8b0 100644 --- a/flutter/lib/desktop/pages/terminal_tab_page.dart +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -171,6 +171,8 @@ class _TerminalTabPageState extends State { forceRelay: args['forceRelay'], connToken: args['connToken'], )); + } else if (call.method == kWindowEventRestoreTerminalSessions) { + _restoreSessions(call.arguments); } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { @@ -188,6 +190,32 @@ class _TerminalTabPageState extends State { super.dispose(); } + Future _restoreSessions(String arguments) async { + Map? args; + try { + args = jsonDecode(arguments) as Map; + } catch (e) { + debugPrint("Error parsing JSON arguments in _restoreSessions: $e"); + return; + } + final persistentSessions = + args['persistent_sessions'] as List? ?? []; + final sortedSessions = persistentSessions.whereType().toList()..sort(); + for (final terminalId in sortedSessions) { + _addNewTerminalForCurrentPeer(terminalId: terminalId); + // A delay is required to ensure the UI has sufficient time to update + // before adding the next terminal. Without this delay, `_TerminalPageState::dispose()` + // may be called prematurely while the tab widget is still in the tab controller. + // This behavior is likely due to a race condition between the UI rendering lifecycle + // and the addition of new tabs. Attempts to use `_TerminalPageState::addPostFrameCallback()` + // to wait for the previous page to be ready were unsuccessful, as the observed call sequence is: + // `initState() 2 -> dispose() 2 -> postFrameCallback() 2`, followed by `initState() 3`. + // The `Future.delayed` approach mitigates this issue by introducing a buffer period, + // allowing the UI to stabilize before proceeding. + await Future.delayed(const Duration(milliseconds: 300)); + } + } + bool _handleKeyEvent(KeyEvent event) { if (event is KeyDownEvent) { // Use Cmd+T on macOS, Ctrl+Shift+T on other platforms @@ -276,17 +304,20 @@ class _TerminalTabPageState extends State { return false; } - void _addNewTerminal(String peerId) { + void _addNewTerminal(String peerId, {int? terminalId}) { // Find first tab for this peer to get connection parameters final firstTab = tabController.state.value.tabs.firstWhere( (tab) => tab.key.startsWith('$peerId\_'), ); if (firstTab.page is TerminalPage) { final page = firstTab.page as TerminalPage; - final terminalId = _nextTerminalId++; + final newTerminalId = terminalId ?? _nextTerminalId++; + if (terminalId != null && terminalId >= _nextTerminalId) { + _nextTerminalId = terminalId + 1; + } tabController.add(_createTerminalTab( peerId: peerId, - terminalId: terminalId, + terminalId: newTerminalId, password: page.password, isSharedPassword: page.isSharedPassword, forceRelay: page.forceRelay, @@ -295,12 +326,12 @@ class _TerminalTabPageState extends State { } } - void _addNewTerminalForCurrentPeer() { + void _addNewTerminalForCurrentPeer({int? terminalId}) { final currentTab = tabController.state.value.selectedTabInfo; final parts = currentTab.key.split('_'); if (parts.isNotEmpty) { final peerId = parts[0]; - _addNewTerminal(peerId); + _addNewTerminal(peerId, terminalId: terminalId); } } diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart index 3284c539b..ef4730097 100644 --- a/flutter/lib/models/terminal_model.dart +++ b/flutter/lib/models/terminal_model.dart @@ -1,7 +1,10 @@ import 'dart:async'; import 'dart:convert'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/main.dart'; import 'package:xterm/xterm.dart'; import 'model.dart'; @@ -195,6 +198,17 @@ class TerminalModel with ChangeNotifier { debugPrint('[TerminalModel] Error processing buffered input: $e'); notifyListeners(); }); + + final persistentSessions = + evt['persistent_sessions'] as List? ?? []; + if (kWindowId != null && persistentSessions.isNotEmpty) { + DesktopMultiWindow.invokeMethod( + kWindowId!, + kWindowEventRestoreTerminalSessions, + jsonEncode({ + 'persistent_sessions': persistentSessions, + })); + } } else { terminal.write('Failed to open terminal: $message\r\n'); } diff --git a/libs/hbb_common b/libs/hbb_common index 25e761f46..f91459c4a 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 25e761f46778b567061770bc64d66332a4503332 +Subproject commit f91459c4ab80fc3cfdef0882b2af51f984bc914c diff --git a/src/flutter.rs b/src/flutter.rs index e3c3c8c0d..602f5701a 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1119,6 +1119,9 @@ impl InvokeUiSession for FlutterHandler { ("pid", json!(opened.pid)), ("service_id", json!(&opened.service_id)), ]; + if !opened.persistent_sessions.is_empty() { + event_data.push(("persistent_sessions", json!(opened.persistent_sessions))); + } self.push_event_("terminal_response", &event_data, &[], &[]); } Some(Union::Data(data)) => { diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index e369de8f8..a1ff5f18e 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -131,6 +131,8 @@ fn get_or_create_service( // Ensure cleanup task is running ensure_cleanup_task(); + service.lock().unwrap().needs_session_sync = true; + Ok(service) } @@ -540,6 +542,7 @@ pub struct PersistentTerminalService { pub created_at: Instant, last_activity: Instant, pub is_persistent: bool, + needs_session_sync: bool, } impl PersistentTerminalService { @@ -550,6 +553,7 @@ impl PersistentTerminalService { created_at: Instant::now(), last_activity: Instant::now(), is_persistent, + needs_session_sync: false, } } @@ -696,6 +700,19 @@ impl TerminalServiceProxy { if self.is_persistent { opened.service_id = self.service_id.clone(); } + if service.needs_session_sync { + if service.sessions.len() > 1 { + // No need to include the current terminal in the list. + // Because the `persistent_sessions` is used to restore the other sessions. + opened.persistent_sessions = service + .sessions + .keys() + .filter(|&id| *id != open.terminal_id) + .cloned() + .collect(); + } + service.needs_session_sync = false; + } response.set_opened(opened); // Send buffered output @@ -856,6 +873,12 @@ impl TerminalServiceProxy { if self.is_persistent { opened.service_id = service.service_id.clone(); } + if service.needs_session_sync { + if !service.sessions.is_empty() { + opened.persistent_sessions = service.sessions.keys().cloned().collect(); + } + service.needs_session_sync = false; + } response.set_opened(opened); log::info!(