mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-08 15:18:13 +03:00
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>
This commit is contained in:
@@ -43,6 +43,9 @@ class TerminalPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _TerminalPageState extends State<TerminalPage>
|
class _TerminalPageState extends State<TerminalPage>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
static const EdgeInsets _defaultTerminalPadding =
|
||||||
|
EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
|
||||||
|
|
||||||
late FFI _ffi;
|
late FFI _ffi;
|
||||||
late TerminalModel _terminalModel;
|
late TerminalModel _terminalModel;
|
||||||
double? _cellHeight;
|
double? _cellHeight;
|
||||||
@@ -155,11 +158,22 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
// extra space left after dividing the available height by the height of a single
|
// 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.
|
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
|
||||||
EdgeInsets _calculatePadding(double heightPx) {
|
EdgeInsets _calculatePadding(double heightPx) {
|
||||||
if (_cellHeight == null) {
|
final cellHeight = _cellHeight;
|
||||||
return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
|
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;
|
final topBottom = extraSpace / 2.0;
|
||||||
return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom);
|
return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,13 @@ class TerminalModel with ChangeNotifier {
|
|||||||
// Buffer for output data received before terminal view has valid dimensions.
|
// Buffer for output data received before terminal view has valid dimensions.
|
||||||
// This prevents NaN errors when writing to terminal before layout is complete.
|
// This prevents NaN errors when writing to terminal before layout is complete.
|
||||||
final _pendingOutputChunks = <String>[];
|
final _pendingOutputChunks = <String>[];
|
||||||
|
final _pendingOutputSuppressFlags = <bool>[];
|
||||||
int _pendingOutputSize = 0;
|
int _pendingOutputSize = 0;
|
||||||
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 _suppressTerminalOutput = false;
|
||||||
|
bool _suppressNextTerminalDataOutput = false;
|
||||||
|
|
||||||
void Function(int w, int h, int pw, int ph)? onResizeExternal;
|
void Function(int w, int h, int pw, int ph)? onResizeExternal;
|
||||||
|
|
||||||
@@ -71,7 +74,10 @@ class TerminalModel with ChangeNotifier {
|
|||||||
terminalController = TerminalController();
|
terminalController = TerminalController();
|
||||||
|
|
||||||
// Setup terminal callbacks
|
// Setup terminal callbacks
|
||||||
terminal.onOutput = _handleInput;
|
terminal.onOutput = (data) {
|
||||||
|
if (_suppressTerminalOutput) return;
|
||||||
|
_handleInput(data);
|
||||||
|
};
|
||||||
|
|
||||||
terminal.onResize = (w, h, pw, ph) async {
|
terminal.onResize = (w, h, pw, ph) async {
|
||||||
// Validate all dimensions before using them
|
// Validate all dimensions before using them
|
||||||
@@ -278,9 +284,11 @@ class TerminalModel with ChangeNotifier {
|
|||||||
if (success) {
|
if (success) {
|
||||||
_terminalOpened = true;
|
_terminalOpened = true;
|
||||||
|
|
||||||
// On reconnect ("Reconnected to existing terminal"), server may replay recent output.
|
// On reconnect, the server may replay recent output. That replay can include
|
||||||
// If this TerminalView instance is reused (not rebuilt), duplicate lines can appear.
|
// terminal queries like DSR/DA; xterm answers them through onOutput as
|
||||||
// We intentionally accept this tradeoff for now to keep logic simple.
|
// "^[[1;1R^[[2;2R^[[>0;0;0c", which must not be sent back to the peer.
|
||||||
|
_suppressNextTerminalDataOutput =
|
||||||
|
message == 'Reconnected to existing terminal with pending output';
|
||||||
|
|
||||||
// Fallback: if terminal view is not yet ready but already has valid
|
// Fallback: if terminal view is not yet ready but already has valid
|
||||||
// dimensions (e.g. layout completed before open response arrived),
|
// dimensions (e.g. layout completed before open response arrived),
|
||||||
@@ -339,6 +347,8 @@ class TerminalModel with ChangeNotifier {
|
|||||||
final data = evt['data'];
|
final data = evt['data'];
|
||||||
|
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
|
final suppressTerminalOutput = _suppressNextTerminalDataOutput;
|
||||||
|
_suppressNextTerminalDataOutput = false;
|
||||||
try {
|
try {
|
||||||
String text = '';
|
String text = '';
|
||||||
if (data is String) {
|
if (data is String) {
|
||||||
@@ -358,7 +368,7 @@ class TerminalModel with ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_writeToTerminal(text);
|
_writeToTerminal(text, suppressTerminalOutput: suppressTerminalOutput);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[TerminalModel] Failed to process terminal data: $e');
|
debugPrint('[TerminalModel] Failed to process terminal data: $e');
|
||||||
}
|
}
|
||||||
@@ -368,7 +378,10 @@ class TerminalModel with ChangeNotifier {
|
|||||||
/// Write text to terminal, buffering if the view is not yet ready.
|
/// Write text to terminal, buffering if the view is not yet ready.
|
||||||
/// All terminal output should go through this method to avoid NaN errors
|
/// All terminal output should go through this method to avoid NaN errors
|
||||||
/// from writing before the terminal view has valid layout dimensions.
|
/// from writing before the terminal view has valid layout dimensions.
|
||||||
void _writeToTerminal(String text) {
|
void _writeToTerminal(
|
||||||
|
String text, {
|
||||||
|
bool suppressTerminalOutput = false,
|
||||||
|
}) {
|
||||||
if (!_terminalViewReady) {
|
if (!_terminalViewReady) {
|
||||||
// If a single chunk exceeds the cap, keep only its tail.
|
// If a single chunk exceeds the cap, keep only its tail.
|
||||||
// Note: truncation may split a multi-byte ANSI escape sequence,
|
// Note: truncation may split a multi-byte ANSI escape sequence,
|
||||||
@@ -380,33 +393,59 @@ class TerminalModel with ChangeNotifier {
|
|||||||
_pendingOutputChunks
|
_pendingOutputChunks
|
||||||
..clear()
|
..clear()
|
||||||
..add(truncated);
|
..add(truncated);
|
||||||
|
_pendingOutputSuppressFlags
|
||||||
|
..clear()
|
||||||
|
..add(suppressTerminalOutput);
|
||||||
_pendingOutputSize = truncated.length;
|
_pendingOutputSize = truncated.length;
|
||||||
} else {
|
} else {
|
||||||
_pendingOutputChunks.add(text);
|
_pendingOutputChunks.add(text);
|
||||||
|
_pendingOutputSuppressFlags.add(suppressTerminalOutput);
|
||||||
_pendingOutputSize += text.length;
|
_pendingOutputSize += text.length;
|
||||||
// Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences)
|
// Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences)
|
||||||
while (_pendingOutputSize > _kMaxOutputBufferChars &&
|
while (_pendingOutputSize > _kMaxOutputBufferChars &&
|
||||||
_pendingOutputChunks.length > 1) {
|
_pendingOutputChunks.length > 1) {
|
||||||
final removed = _pendingOutputChunks.removeAt(0);
|
final removed = _pendingOutputChunks.removeAt(0);
|
||||||
|
_pendingOutputSuppressFlags.removeAt(0);
|
||||||
_pendingOutputSize -= removed.length;
|
_pendingOutputSize -= removed.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
terminal.write(text);
|
_writeTerminalChunk(text, suppressTerminalOutput: suppressTerminalOutput);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _flushOutputBuffer() {
|
void _flushOutputBuffer() {
|
||||||
if (_pendingOutputChunks.isEmpty) return;
|
if (_pendingOutputChunks.isEmpty) return;
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)');
|
'[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)');
|
||||||
for (final chunk in _pendingOutputChunks) {
|
for (var i = 0; i < _pendingOutputChunks.length; i++) {
|
||||||
terminal.write(chunk);
|
_writeTerminalChunk(
|
||||||
|
_pendingOutputChunks[i],
|
||||||
|
suppressTerminalOutput: _pendingOutputSuppressFlags[i],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_pendingOutputChunks.clear();
|
_pendingOutputChunks.clear();
|
||||||
|
_pendingOutputSuppressFlags.clear();
|
||||||
_pendingOutputSize = 0;
|
_pendingOutputSize = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _writeTerminalChunk(
|
||||||
|
String text, {
|
||||||
|
required bool suppressTerminalOutput,
|
||||||
|
}) {
|
||||||
|
if (!suppressTerminalOutput) {
|
||||||
|
terminal.write(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final previous = _suppressTerminalOutput;
|
||||||
|
_suppressTerminalOutput = true;
|
||||||
|
try {
|
||||||
|
terminal.write(text);
|
||||||
|
} finally {
|
||||||
|
_suppressTerminalOutput = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark terminal view as ready and flush buffered output.
|
/// Mark terminal view as ready and flush buffered output.
|
||||||
void _markViewReady() {
|
void _markViewReady() {
|
||||||
if (_terminalViewReady) return;
|
if (_terminalViewReady) return;
|
||||||
@@ -433,7 +472,9 @@ class TerminalModel with ChangeNotifier {
|
|||||||
// Clear buffers to free memory
|
// Clear buffers to free memory
|
||||||
_inputBuffer.clear();
|
_inputBuffer.clear();
|
||||||
_pendingOutputChunks.clear();
|
_pendingOutputChunks.clear();
|
||||||
|
_pendingOutputSuppressFlags.clear();
|
||||||
_pendingOutputSize = 0;
|
_pendingOutputSize = 0;
|
||||||
|
_suppressNextTerminalDataOutput = false;
|
||||||
// Terminal cleanup is handled server-side when service closes
|
// Terminal cleanup is handled server-side when service closes
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user