fix(terminal): avoid reconnect stalls and delayed layout writes

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-04-28 20:21:15 +08:00
parent 268827ef64
commit 67b5484ded
2 changed files with 46 additions and 22 deletions

View File

@@ -32,6 +32,7 @@ class TerminalModel with ChangeNotifier {
static const int _kMaxOutputBufferChars = 8 * 1024; static const int _kMaxOutputBufferChars = 8 * 1024;
// View ready state: true when terminal has valid dimensions, safe to write // View ready state: true when terminal has valid dimensions, safe to write
bool _terminalViewReady = false; bool _terminalViewReady = false;
bool _markViewReadyScheduled = false;
bool _suppressTerminalOutput = false; bool _suppressTerminalOutput = false;
bool _suppressNextTerminalDataOutput = 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. // 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. // Must be after onResizeExternal so the view layer has valid dimensions before flushing.
if (!_terminalViewReady) { if (!_terminalViewReady) {
_markViewReady(); _scheduleMarkViewReady();
} }
if (_terminalOpened) { if (_terminalOpened) {
@@ -296,7 +297,7 @@ class TerminalModel with ChangeNotifier {
if (!_terminalViewReady && if (!_terminalViewReady &&
terminal.viewWidth > 0 && terminal.viewWidth > 0 &&
terminal.viewHeight > 0) { terminal.viewHeight > 0) {
_markViewReady(); _scheduleMarkViewReady();
} }
// Process any buffered input // Process any buffered input
@@ -447,6 +448,18 @@ class TerminalModel with ChangeNotifier {
} }
/// Mark terminal view as ready and flush buffered output. /// 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() { void _markViewReady() {
if (_terminalViewReady) return; if (_terminalViewReady) return;
_terminalViewReady = true; _terminalViewReady = true;
@@ -474,6 +487,7 @@ class TerminalModel with ChangeNotifier {
_pendingOutputChunks.clear(); _pendingOutputChunks.clear();
_pendingOutputSuppressFlags.clear(); _pendingOutputSuppressFlags.clear();
_pendingOutputSize = 0; _pendingOutputSize = 0;
_markViewReadyScheduled = false;
_suppressNextTerminalDataOutput = false; _suppressNextTerminalDataOutput = false;
// Terminal cleanup is handled server-side when service closes // Terminal cleanup is handled server-side when service closes
super.dispose(); super.dispose();

View File

@@ -485,6 +485,17 @@ impl OutputBuffer {
} else { } else {
self.total_size -= removed.len(); 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,18 +1618,10 @@ impl TerminalServiceProxy {
data: &TerminalData, data: &TerminalData,
) -> Result<Option<TerminalResponse>> { ) -> Result<Option<TerminalResponse>> {
if let Some(session_arc) = session { if let Some(session_arc) = session {
let mut session = match session_arc.lock() { let input = {
Ok(guard) => guard, let mut session = session_arc.lock().unwrap();
Err(e) => {
return Err(anyhow!(
"Failed to lock terminal session {} for input handling: {}",
data.terminal_id,
e
));
}
};
session.update_activity(); session.update_activity();
if let Some(input_tx) = &session.input_tx { if let Some(input_tx) = session.input_tx.clone() {
// Encode data for helper mode or send raw for direct PTY mode // Encode data for helper mode or send raw for direct PTY mode
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let msg = if session.is_helper_mode { let msg = if session.is_helper_mode {
@@ -1629,7 +1632,14 @@ impl TerminalServiceProxy {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
let msg = data.data.to_vec(); let msg = data.data.to_vec();
// Send data to writer thread Some((input_tx, msg))
} else {
None
}
};
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) { if let Err(e) = input_tx.send(msg) {
log::error!( log::error!(
"Failed to send data to terminal {}: {}", "Failed to send data to terminal {}: {}",