mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-09 23:58:10 +03:00
Terminal utf8 and reconnect (#14895)
* fix: handle incomplete UTF-8 sequences in terminal output, rework on https://github.com/rustdesk/rustdesk/pull/14736 * Fix terminal auto-reconnect freeze: reconnect resumes terminal output, while multi-tab reconnect avoids restoring duplicate tabs for terminals that are already open. * fix(terminal): subtract with overflow ``` thread '<unnamed>' panicked at src\server\terminal_service.rs:476:17: attempt to subtract with overflow note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace thread 'tokio-runtime-worker' panicked at src\server\terminal_service.rs:1576:50: called `Result::unwrap()` on an `Err` value: PoisonError { .. } [2026-04-25T07:17:34Z ERROR librustdesk::server::service] Failed to join thread for service ts_9badd3fe-2411-4996-9f40-93c979009edd, Any { .. } ``` Signed-off-by: fufesou <linlong1266@gmail.com> * fix ios enter: https://github.com/rustdesk/rustdesk/issues/14907 * fix(terminal): reconnect, error handling 1. Terminal shows "^[[1;1R^[[2;2R^[[>0;0;0c" 2. NaN ``` [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Converting object to an encodable object failed: NaN ... ``` Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): dialog, close window Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): close terminal window on disconnect dialog Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): merge reconnect backlog into replay output Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): avoid reconnect stalls and delayed layout writes Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): remove invalid test Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): schedule frame before flushing buffered output Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): windows&macos, charset utf-8 Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): reconnect suppress next output Signed-off-by: fufesou <linlong1266@gmail.com> * fix: cap terminal reconnect replay output - split reconnect replay backlog into capped chunks - mark terminal data replay chunks for client-side suppression - avoid using open-message text to suppress xterm replies - reuse default terminal padding value - remove misleading Enter-key normalization PR link Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): env en_US.UTF-8 Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): reconnect, refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): flag, retry output Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): update hbb_common Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): comments Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): comments utf-8 chunk accumulator Signed-off-by: fufesou <linlong1266@gmail.com> * fix(terminal): update hbb_common Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -27,6 +27,7 @@ class TerminalPage extends StatefulWidget {
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
final int terminalId;
|
||||
|
||||
/// Tab key for focus management, passed from parent to avoid duplicate construction
|
||||
final String tabKey;
|
||||
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
|
||||
@@ -43,6 +44,9 @@ class TerminalPage extends StatefulWidget {
|
||||
|
||||
class _TerminalPageState extends State<TerminalPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
static const EdgeInsets _defaultTerminalPadding =
|
||||
EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
|
||||
|
||||
late FFI _ffi;
|
||||
late TerminalModel _terminalModel;
|
||||
double? _cellHeight;
|
||||
@@ -155,13 +159,27 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
// extra space left after dividing the available height by the height of a single
|
||||
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
|
||||
EdgeInsets _calculatePadding(double heightPx) {
|
||||
if (_cellHeight == null) {
|
||||
return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
|
||||
final cellHeight = _cellHeight;
|
||||
if (!heightPx.isFinite ||
|
||||
heightPx <= 0 ||
|
||||
cellHeight == null ||
|
||||
!cellHeight.isFinite ||
|
||||
cellHeight <= 0) {
|
||||
return _defaultTerminalPadding;
|
||||
}
|
||||
final rows = (heightPx / cellHeight).floor();
|
||||
if (rows <= 0) {
|
||||
return _defaultTerminalPadding;
|
||||
}
|
||||
final extraSpace = heightPx - rows * cellHeight;
|
||||
if (!extraSpace.isFinite || extraSpace < 0) {
|
||||
return _defaultTerminalPadding;
|
||||
}
|
||||
final rows = (heightPx / _cellHeight!).floor();
|
||||
final extraSpace = heightPx - rows * _cellHeight!;
|
||||
final topBottom = extraSpace / 2.0;
|
||||
return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom);
|
||||
return EdgeInsets.symmetric(
|
||||
horizontal: _defaultTerminalPadding.horizontal / 2,
|
||||
vertical: topBottom,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -46,6 +46,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
.setTitle(getWindowNameWithId(id));
|
||||
};
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
tabController.onCloseWindow = _closeWindowFromConnection;
|
||||
final terminalId = params['terminalId'] ?? _nextTerminalId++;
|
||||
tabController.add(_createTerminalTab(
|
||||
peerId: params['id'],
|
||||
@@ -144,6 +145,8 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
_windowClosing = true;
|
||||
final tabKeys = tabController.state.value.tabs.map((t) => t.key).toList();
|
||||
// Remove all UI tabs immediately (same instant behavior as the old tabController.clear())
|
||||
// Keep the cleanup target lookup below synchronous before its first await:
|
||||
// it relies on the current frame still retaining each TerminalPage's FFI/model.
|
||||
tabController.clear();
|
||||
// Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout).
|
||||
// Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls.
|
||||
@@ -368,8 +371,34 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
final persistentSessions =
|
||||
args['persistent_sessions'] as List<dynamic>? ?? [];
|
||||
final sortedSessions = persistentSessions.whereType<int>().toList()..sort();
|
||||
var peerId = args['peer_id'] as String? ?? '';
|
||||
if (peerId.isEmpty) {
|
||||
if (tabController.state.value.tabs.isEmpty ||
|
||||
tabController.state.value.selected >=
|
||||
tabController.state.value.tabs.length) {
|
||||
debugPrint('[TerminalTabPage] Skip restore: no selected tab');
|
||||
return;
|
||||
}
|
||||
final currentTab = tabController.state.value.selectedTabInfo;
|
||||
final parsed = _parseTabKey(currentTab.key);
|
||||
if (parsed == null) return;
|
||||
peerId = parsed.$1;
|
||||
}
|
||||
final existingTerminalIds = tabController.state.value.tabs
|
||||
.map((tab) => _parseTabKey(tab.key))
|
||||
.where((parsed) => parsed != null && parsed.$1 == peerId)
|
||||
.map((parsed) => parsed!.$2)
|
||||
.toSet();
|
||||
if (existingTerminalIds.isEmpty) {
|
||||
debugPrint(
|
||||
'[TerminalTabPage] Skip restore: no seed tab for peer $peerId');
|
||||
return;
|
||||
}
|
||||
for (final terminalId in sortedSessions) {
|
||||
_addNewTerminalForCurrentPeer(terminalId: terminalId);
|
||||
if (!existingTerminalIds.add(terminalId)) {
|
||||
continue;
|
||||
}
|
||||
_addNewTerminal(peerId, 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.
|
||||
@@ -546,6 +575,11 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _closeWindowFromConnection() async {
|
||||
await _closeAllTabs();
|
||||
await WindowController.fromWindowId(windowId()).close();
|
||||
}
|
||||
|
||||
int windowId() {
|
||||
return widget.params["windowId"];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user