mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-02 10:16:28 +02:00
Compare commits
6 Commits
4f5c7db70a
...
0eff404323
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eff404323 | ||
|
|
67b5484ded | ||
|
|
268827ef64 | ||
|
|
c4542b4a5d | ||
|
|
59f3060a04 | ||
|
|
0a1500a72a |
@@ -716,6 +716,13 @@ closeConnection({String? id}) {
|
||||
stateGlobal.isInMainPage = true;
|
||||
} else {
|
||||
final controller = Get.find<DesktopTabController>();
|
||||
if (controller.tabType == DesktopTabType.terminal &&
|
||||
controller.onCloseWindow != null) {
|
||||
unawaited(controller.onCloseWindow!().catchError((e, _) {
|
||||
debugPrint('[closeConnection] Failed to close terminal window: $e');
|
||||
}));
|
||||
return;
|
||||
}
|
||||
controller.closeBy(id);
|
||||
}
|
||||
}
|
||||
@@ -4179,8 +4186,7 @@ Widget? buildAvatarWidget({
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
fallback ?? SizedBox.shrink(),
|
||||
errorBuilder: (_, __, ___) => fallback ?? SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -54,7 +58,8 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
super.initState();
|
||||
|
||||
// Listen for tab selection changes to request focus
|
||||
_tabStateSubscription = widget.tabController.state.listen(_onTabStateChanged);
|
||||
_tabStateSubscription =
|
||||
widget.tabController.state.listen(_onTabStateChanged);
|
||||
|
||||
// Use shared FFI instance from connection manager
|
||||
_ffi = TerminalConnectionManager.getConnection(
|
||||
@@ -141,7 +146,9 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
// Use post-frame callback to ensure widget is fully laid out in focus tree
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Re-check conditions after frame: mounted, focusable, still selected, not already focused
|
||||
if (!mounted || !_terminalFocusNode.canRequestFocus || _terminalFocusNode.hasFocus) return;
|
||||
if (!mounted ||
|
||||
!_terminalFocusNode.canRequestFocus ||
|
||||
_terminalFocusNode.hasFocus) return;
|
||||
final state = widget.tabController.state.value;
|
||||
if (state.selected >= 0 && state.selected < state.tabs.length) {
|
||||
if (state.tabs[state.selected].key == widget.tabKey) {
|
||||
@@ -155,11 +162,22 @@ 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);
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
@@ -370,6 +371,12 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
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;
|
||||
@@ -380,6 +387,11 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
.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) {
|
||||
if (!existingTerminalIds.add(terminalId)) {
|
||||
continue;
|
||||
@@ -561,6 +573,11 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _closeWindowFromConnection() async {
|
||||
await _closeAllTabs();
|
||||
await WindowController.fromWindowId(windowId()).close();
|
||||
}
|
||||
|
||||
int windowId() {
|
||||
return widget.params["windowId"];
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ class DesktopTabController {
|
||||
/// index, key
|
||||
Function(int, String)? onRemoved;
|
||||
Function(String)? onSelected;
|
||||
Future<void> Function()? onCloseWindow;
|
||||
|
||||
DesktopTabController(
|
||||
{required this.tabType, this.onRemoved, this.onSelected});
|
||||
|
||||
@@ -27,10 +27,14 @@ class TerminalModel with ChangeNotifier {
|
||||
// Buffer for output data received before terminal view has valid dimensions.
|
||||
// This prevents NaN errors when writing to terminal before layout is complete.
|
||||
final _pendingOutputChunks = <String>[];
|
||||
final _pendingOutputSuppressFlags = <bool>[];
|
||||
int _pendingOutputSize = 0;
|
||||
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;
|
||||
|
||||
void Function(int w, int h, int pw, int ph)? onResizeExternal;
|
||||
|
||||
@@ -71,7 +75,10 @@ class TerminalModel with ChangeNotifier {
|
||||
terminalController = TerminalController();
|
||||
|
||||
// Setup terminal callbacks
|
||||
terminal.onOutput = _handleInput;
|
||||
terminal.onOutput = (data) {
|
||||
if (_suppressTerminalOutput) return;
|
||||
_handleInput(data);
|
||||
};
|
||||
|
||||
terminal.onResize = (w, h, pw, ph) async {
|
||||
// Validate all dimensions before using them
|
||||
@@ -85,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) {
|
||||
@@ -278,9 +285,11 @@ class TerminalModel with ChangeNotifier {
|
||||
if (success) {
|
||||
_terminalOpened = true;
|
||||
|
||||
// On reconnect ("Reconnected to existing terminal"), server may replay recent output.
|
||||
// If this TerminalView instance is reused (not rebuilt), duplicate lines can appear.
|
||||
// We intentionally accept this tradeoff for now to keep logic simple.
|
||||
// On reconnect, the server may replay recent output. That replay can include
|
||||
// terminal queries like DSR/DA; xterm answers them through onOutput as
|
||||
// "^[[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
|
||||
// dimensions (e.g. layout completed before open response arrived),
|
||||
@@ -288,7 +297,7 @@ class TerminalModel with ChangeNotifier {
|
||||
if (!_terminalViewReady &&
|
||||
terminal.viewWidth > 0 &&
|
||||
terminal.viewHeight > 0) {
|
||||
_markViewReady();
|
||||
_scheduleMarkViewReady();
|
||||
}
|
||||
|
||||
// Process any buffered input
|
||||
@@ -301,9 +310,9 @@ class TerminalModel with ChangeNotifier {
|
||||
|
||||
final persistentSessions =
|
||||
(evt['persistent_sessions'] as List<dynamic>? ?? [])
|
||||
.whereType<int>()
|
||||
.where((id) => !parent.terminalModels.containsKey(id))
|
||||
.toList();
|
||||
.whereType<int>()
|
||||
.where((id) => !parent.terminalModels.containsKey(id))
|
||||
.toList();
|
||||
if (kWindowId != null && persistentSessions.isNotEmpty) {
|
||||
DesktopMultiWindow.invokeMethod(
|
||||
kWindowId!,
|
||||
@@ -339,6 +348,8 @@ class TerminalModel with ChangeNotifier {
|
||||
final data = evt['data'];
|
||||
|
||||
if (data != null) {
|
||||
final suppressTerminalOutput = _suppressNextTerminalDataOutput;
|
||||
_suppressNextTerminalDataOutput = false;
|
||||
try {
|
||||
String text = '';
|
||||
if (data is String) {
|
||||
@@ -358,7 +369,7 @@ class TerminalModel with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
_writeToTerminal(text);
|
||||
_writeToTerminal(text, suppressTerminalOutput: suppressTerminalOutput);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Failed to process terminal data: $e');
|
||||
}
|
||||
@@ -368,7 +379,10 @@ class TerminalModel with ChangeNotifier {
|
||||
/// Write text to terminal, buffering if the view is not yet ready.
|
||||
/// All terminal output should go through this method to avoid NaN errors
|
||||
/// from writing before the terminal view has valid layout dimensions.
|
||||
void _writeToTerminal(String text) {
|
||||
void _writeToTerminal(
|
||||
String text, {
|
||||
bool suppressTerminalOutput = false,
|
||||
}) {
|
||||
if (!_terminalViewReady) {
|
||||
// If a single chunk exceeds the cap, keep only its tail.
|
||||
// Note: truncation may split a multi-byte ANSI escape sequence,
|
||||
@@ -380,34 +394,72 @@ class TerminalModel with ChangeNotifier {
|
||||
_pendingOutputChunks
|
||||
..clear()
|
||||
..add(truncated);
|
||||
_pendingOutputSuppressFlags
|
||||
..clear()
|
||||
..add(suppressTerminalOutput);
|
||||
_pendingOutputSize = truncated.length;
|
||||
} else {
|
||||
_pendingOutputChunks.add(text);
|
||||
_pendingOutputSuppressFlags.add(suppressTerminalOutput);
|
||||
_pendingOutputSize += text.length;
|
||||
// Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences)
|
||||
while (_pendingOutputSize > _kMaxOutputBufferChars &&
|
||||
_pendingOutputChunks.length > 1) {
|
||||
final removed = _pendingOutputChunks.removeAt(0);
|
||||
_pendingOutputSuppressFlags.removeAt(0);
|
||||
_pendingOutputSize -= removed.length;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
terminal.write(text);
|
||||
_writeTerminalChunk(text, suppressTerminalOutput: suppressTerminalOutput);
|
||||
}
|
||||
|
||||
void _flushOutputBuffer() {
|
||||
if (_pendingOutputChunks.isEmpty) return;
|
||||
debugPrint(
|
||||
'[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)');
|
||||
for (final chunk in _pendingOutputChunks) {
|
||||
terminal.write(chunk);
|
||||
for (var i = 0; i < _pendingOutputChunks.length; i++) {
|
||||
_writeTerminalChunk(
|
||||
_pendingOutputChunks[i],
|
||||
suppressTerminalOutput: _pendingOutputSuppressFlags[i],
|
||||
);
|
||||
}
|
||||
_pendingOutputChunks.clear();
|
||||
_pendingOutputSuppressFlags.clear();
|
||||
_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.
|
||||
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;
|
||||
@@ -433,7 +485,10 @@ class TerminalModel with ChangeNotifier {
|
||||
// Clear buffers to free memory
|
||||
_inputBuffer.clear();
|
||||
_pendingOutputChunks.clear();
|
||||
_pendingOutputSuppressFlags.clear();
|
||||
_pendingOutputSize = 0;
|
||||
_markViewReadyScheduled = false;
|
||||
_suppressNextTerminalDataOutput = false;
|
||||
// Terminal cleanup is handled server-side when service closes
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1037,18 +1048,33 @@ impl TerminalServiceProxy {
|
||||
if let Some(session_arc) = service.sessions.get(&open.terminal_id) {
|
||||
// Reconnect to existing terminal
|
||||
let mut session = session_arc.lock().unwrap();
|
||||
// Directly enter Active state with pending buffer for immediate streaming.
|
||||
// Historical buffer is sent first by read_outputs(), then real-time data follows.
|
||||
// No overlap: pending_buffer comes from output_buffer (pre-disconnect history),
|
||||
// while received_data in read_outputs() comes from the channel (post-reconnect).
|
||||
// During disconnect, the run loop (sp.ok()) exits so read_outputs() stops being
|
||||
// called; output_buffer is not updated, and channel data may be lost if it fills up.
|
||||
// Directly enter Active state with pending replay for immediate streaming.
|
||||
// The replay starts with output_buffer history, then drains any current channel
|
||||
// backlog into the same pending response. Keeping reconnect backlog in the first
|
||||
// response lets the client suppress xterm query answers for the whole replay batch.
|
||||
// During disconnect, read_outputs() is not called; channel data can still be lost
|
||||
// if output_rx fills before reconnect drains it.
|
||||
let buffer = session
|
||||
.output_buffer
|
||||
.get_recent(DEFAULT_RECONNECT_BUFFER_BYTES);
|
||||
let has_pending = !buffer.is_empty();
|
||||
let mut pending_buffer = buffer;
|
||||
let mut reconnect_backlog = Vec::new();
|
||||
if let Some(output_rx) = &session.output_rx {
|
||||
while let Ok(data) = output_rx.try_recv() {
|
||||
reconnect_backlog.push(data);
|
||||
}
|
||||
}
|
||||
for data in reconnect_backlog {
|
||||
session.output_buffer.append(&data);
|
||||
pending_buffer.extend_from_slice(&data);
|
||||
}
|
||||
let has_pending = !pending_buffer.is_empty();
|
||||
session.state = SessionState::Active {
|
||||
pending_buffer: if has_pending { Some(buffer) } else { None },
|
||||
pending_buffer: if has_pending {
|
||||
Some(pending_buffer)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
// Always trigger two-phase SIGWINCH on reconnect to force TUI app redraw,
|
||||
// regardless of whether there's pending buffer data. This avoids edge cases
|
||||
// where buffer is empty but a TUI app (top/htop) still needs a full redraw.
|
||||
@@ -1059,7 +1085,11 @@ impl TerminalServiceProxy {
|
||||
let mut opened = TerminalOpened::new();
|
||||
opened.terminal_id = open.terminal_id;
|
||||
opened.success = true;
|
||||
opened.message = "Reconnected to existing terminal".to_string();
|
||||
opened.message = if has_pending {
|
||||
"Reconnected to existing terminal with pending output".to_string()
|
||||
} else {
|
||||
"Reconnected to existing terminal".to_string()
|
||||
};
|
||||
opened.pid = session.pid;
|
||||
opened.service_id = self.service_id.clone();
|
||||
if service.needs_session_sync {
|
||||
@@ -1588,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 {}: {}",
|
||||
@@ -1840,9 +1869,18 @@ impl TerminalServiceProxy {
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
// Send pending buffer response first (set on reconnection in handle_open).
|
||||
// This ensures historical buffer is sent before any real-time data.
|
||||
// Send pending replay first (set on reconnection in handle_open). If new
|
||||
// channel data was drained in this same read_outputs() cycle, keep it in the
|
||||
// replay response so the client suppresses one complete reconnect batch.
|
||||
if let Some(buffer) = pending_buffer.take() {
|
||||
let mut buffer = buffer;
|
||||
for data in received_data.drain(..) {
|
||||
// Reconnect replay can include terminal queries like DSR/DA.
|
||||
// Keep this first backlog batch in one response so the client can
|
||||
// suppress xterm-generated answers and avoid printing
|
||||
// "^[[1;1R^[[2;2R^[[>0;0;0c" back to the remote shell.
|
||||
buffer.extend_from_slice(&data);
|
||||
}
|
||||
if !buffer.is_empty() {
|
||||
responses
|
||||
.push(Self::create_terminal_data_response(terminal_id, buffer));
|
||||
@@ -1983,11 +2021,7 @@ impl TerminalServiceProxy {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
find_utf8_split_point, OutputBuffer, TerminalData, TerminalServiceProxy, TerminalSession,
|
||||
Utf8ChunkAccumulator, MAX_BUFFER_LINES,
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use super::{find_utf8_split_point, OutputBuffer, Utf8ChunkAccumulator, MAX_BUFFER_LINES};
|
||||
|
||||
#[test]
|
||||
fn utf8_split_point_returns_full_len_for_complete_input() {
|
||||
@@ -2096,29 +2130,4 @@ mod tests {
|
||||
let actual_size: usize = buffer.lines.iter().map(|line| line.len()).sum();
|
||||
assert_eq!(buffer.total_size, actual_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_data_returns_error_when_session_mutex_poisoned() {
|
||||
let session = Arc::new(Mutex::new(TerminalSession::new(1, 24, 80)));
|
||||
let poison_target = session.clone();
|
||||
let _ = std::thread::spawn(move || {
|
||||
let _guard = poison_target.lock().unwrap();
|
||||
panic!("poison terminal session mutex");
|
||||
})
|
||||
.join();
|
||||
|
||||
let proxy = TerminalServiceProxy {
|
||||
service_id: "test_service".to_string(),
|
||||
is_persistent: false,
|
||||
#[cfg(target_os = "windows")]
|
||||
user_token: None,
|
||||
};
|
||||
|
||||
let mut data = TerminalData::new();
|
||||
data.terminal_id = 1;
|
||||
data.data = bytes::Bytes::from_static(b"echo test");
|
||||
|
||||
let result = proxy.handle_data(Some(session), &data);
|
||||
assert!(result.is_err(), "expected poisoned lock to return error");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user