From 67b5484dedbb9fd2df6daea3c5bdc70247f86850 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 28 Apr 2026 20:21:15 +0800 Subject: [PATCH] fix(terminal): avoid reconnect stalls and delayed layout writes Signed-off-by: fufesou --- flutter/lib/models/terminal_model.dart | 18 ++++++++-- src/server/terminal_service.rs | 50 +++++++++++++++----------- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart index bcf14bde2..8700176e6 100644 --- a/flutter/lib/models/terminal_model.dart +++ b/flutter/lib/models/terminal_model.dart @@ -32,6 +32,7 @@ class TerminalModel with ChangeNotifier { static const int _kMaxOutputBufferChars = 8 * 1024; // View ready state: true when terminal has valid dimensions, safe to write bool _terminalViewReady = false; + bool _markViewReadyScheduled = false; bool _suppressTerminalOutput = false; bool _suppressNextTerminalDataOutput = false; @@ -91,7 +92,7 @@ class TerminalModel with ChangeNotifier { // Mark terminal view as ready and flush any buffered output on first valid resize. // Must be after onResizeExternal so the view layer has valid dimensions before flushing. if (!_terminalViewReady) { - _markViewReady(); + _scheduleMarkViewReady(); } if (_terminalOpened) { @@ -296,7 +297,7 @@ class TerminalModel with ChangeNotifier { if (!_terminalViewReady && terminal.viewWidth > 0 && terminal.viewHeight > 0) { - _markViewReady(); + _scheduleMarkViewReady(); } // Process any buffered input @@ -447,6 +448,18 @@ class TerminalModel with ChangeNotifier { } /// Mark terminal view as ready and flush buffered output. + void _scheduleMarkViewReady() { + if (_disposed || _terminalViewReady || _markViewReadyScheduled) return; + _markViewReadyScheduled = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + _markViewReadyScheduled = false; + if (_disposed || _terminalViewReady) return; + if (terminal.viewWidth > 0 && terminal.viewHeight > 0) { + _markViewReady(); + } + }); + } + void _markViewReady() { if (_terminalViewReady) return; _terminalViewReady = true; @@ -474,6 +487,7 @@ class TerminalModel with ChangeNotifier { _pendingOutputChunks.clear(); _pendingOutputSuppressFlags.clear(); _pendingOutputSize = 0; + _markViewReadyScheduled = false; _suppressNextTerminalDataOutput = false; // Terminal cleanup is handled server-side when service closes super.dispose(); diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index ca6a66ff2..4c3b5ff6e 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -485,6 +485,17 @@ impl OutputBuffer { } else { self.total_size -= removed.len(); } + if self.lines.is_empty() { + self.last_line_incomplete = false; + } + } else { + log::error!( + "OutputBuffer trim invariant broken: total_size={}, lines_len=0", + self.total_size + ); + self.total_size = 0; + self.last_line_incomplete = false; + break; } } } @@ -1607,29 +1618,28 @@ impl TerminalServiceProxy { data: &TerminalData, ) -> Result> { if let Some(session_arc) = session { - let mut session = match session_arc.lock() { - Ok(guard) => guard, - Err(e) => { - return Err(anyhow!( - "Failed to lock terminal session {} for input handling: {}", - data.terminal_id, - e - )); + let input = { + let mut session = session_arc.lock().unwrap(); + session.update_activity(); + if let Some(input_tx) = session.input_tx.clone() { + // Encode data for helper mode or send raw for direct PTY mode + #[cfg(target_os = "windows")] + let msg = if session.is_helper_mode { + encode_helper_message(MSG_TYPE_DATA, &data.data) + } else { + data.data.to_vec() + }; + #[cfg(not(target_os = "windows"))] + let msg = data.data.to_vec(); + + Some((input_tx, msg)) + } else { + None } }; - session.update_activity(); - if let Some(input_tx) = &session.input_tx { - // Encode data for helper mode or send raw for direct PTY mode - #[cfg(target_os = "windows")] - let msg = if session.is_helper_mode { - encode_helper_message(MSG_TYPE_DATA, &data.data) - } else { - data.data.to_vec() - }; - #[cfg(not(target_os = "windows"))] - let msg = data.data.to_vec(); - // Send data to writer thread + if let Some((input_tx, msg)) = input { + // Send outside the session lock; SyncSender::send can block when full. if let Err(e) = input_tx.send(msg) { log::error!( "Failed to send data to terminal {}: {}",