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

View File

@@ -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<Option<TerminalResponse>> {
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 {}: {}",