Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions flutter/lib/desktop/pages/terminal_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ class _TerminalPageState extends State<TerminalPage>
// Register this terminal model with FFI for event routing
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);

// Auto-close tab when shell exits
_terminalModel.onClosed = () {
if (mounted) {
widget.tabController.closeBy(widget.tabKey);
}
};

// Initialize terminal connection
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id);
Expand Down
7 changes: 7 additions & 0 deletions flutter/lib/mobile/pages/terminal_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ class _TerminalPageState extends State<TerminalPage>
// Register this terminal model with FFI for event routing
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);

// Auto-close connection when shell exits
_terminalModel.onClosed = () {
if (mounted) {
closeConnection(id: widget.id);
}
};

// Web desktop users have full hardware keyboard access, so the on-screen
// terminal extra keys bar is unnecessary and disabled.
_showTerminalExtraKeys = !isWebDesktop &&
Expand Down
6 changes: 6 additions & 0 deletions flutter/lib/models/terminal_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class TerminalModel with ChangeNotifier {

void Function(int w, int h, int pw, int ph)? onResizeExternal;

/// Called when the terminal session ends (shell exits).
/// The listener (typically TerminalPage) can use this to auto-close the tab/page.
VoidCallback? onClosed;

Future<void> _handleInput(String data) async {
// Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a
// real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'.
Expand Down Expand Up @@ -473,6 +477,8 @@ class TerminalModel with ChangeNotifier {
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
_terminalOpened = false;
notifyListeners();
// Auto-close the tab/page
onClosed?.call();
}

void _handleTerminalError(Map<String, dynamic> evt) {
Expand Down
136 changes: 114 additions & 22 deletions libs/clipboard/src/windows/wf_cliprdr.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@

/* Maximum number of clipboard streams accepted from a remote peer (integer overflow / DoS guard) */
#define WF_CLIPRDR_MAX_STREAMS 16384
/* Registered clipboard formats use IDs 0xC000 through 0xFFFF.
* https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclipboardformatw */
#define WF_CLIPRDR_MAX_FORMATS 0x4000u
/* Registered format names are string atoms; cap the converted WCHAR name.
* https://learn.microsoft.com/en-us/windows/win32/dataxchg/about-atom-tables */
#define WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS 255u
/* Bound the peer-provided UTF-8 scan separately from the converted Windows name. */
#define WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES (WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS * 4u)

/* Validates the remote descriptor array size after cItems has been read safely. */
static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
Expand All @@ -61,6 +69,25 @@
return size >= descriptors_size;
}

static BOOL wf_cliprdr_bounded_strlen(const char *value, size_t max_len, size_t *len)
{
size_t i;

if (!value || !len)
return FALSE;

for (i = 0; i <= max_len; i++)
{
if (value[i] == '\0')
{
*len = i;
return TRUE;
}
}

return FALSE;
}

/**
* Clipboard Formats
*/
Expand Down Expand Up @@ -1406,25 +1433,35 @@
return local_format;
}

static void map_ensure_capacity(wfClipboard *clipboard)
static BOOL map_ensure_capacity(wfClipboard *clipboard, size_t capacity)
{
size_t old_size;
formatMapping *new_map;

if (!clipboard)
return;
return FALSE;

if (clipboard->map_size >= clipboard->map_capacity)
{
size_t new_size;
formatMapping *new_map;
new_size = clipboard->map_capacity * 2;
new_map =
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * new_size);
if (!clipboard->format_mappings)
return FALSE;

if (!new_map)
return;
if (capacity <= clipboard->map_capacity)
return TRUE;

clipboard->format_mappings = new_map;
clipboard->map_capacity = new_size;
}
if (capacity > WF_CLIPRDR_MAX_FORMATS ||
capacity > ((size_t)-1) / sizeof(formatMapping))
return FALSE;

old_size = clipboard->map_capacity;
new_map =
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * capacity);

if (!new_map)
return FALSE;

memset(new_map + old_size, 0, sizeof(formatMapping) * (capacity - old_size));
clipboard->format_mappings = new_map;
clipboard->map_capacity = capacity;
return TRUE;
}

static BOOL clear_format_map(wfClipboard *clipboard)
Expand All @@ -1451,6 +1488,13 @@
return TRUE;
}

static UINT wf_cliprdr_server_format_list_fail(wfClipboard *clipboard)
{
clear_format_map(clipboard);
clipboard->copied = FALSE;
return ERROR_INTERNAL_ERROR;
}

static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
{
CLIPRDR_TEMP_DIRECTORY tempDirectory;
Expand Down Expand Up @@ -2284,7 +2328,7 @@
StringCchCatW(DirSpec, MAX_PATH, L"\\*");

// hFind = FindFirstFile(DirSpec, &FindFileData);
hFind = FindFirstFileW(DirSpec, &FindFileData);

Check warning on line 2331 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'WIN32_FIND_DATA *' to 'LPWIN32_FIND_DATAW' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

Check warning on line 2331 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'WIN32_FIND_DATA *' to 'LPWIN32_FIND_DATAW' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

if (hFind == INVALID_HANDLE_VALUE)
{
Expand All @@ -2293,14 +2337,14 @@
return FALSE;
}

while (FindNextFileW(hFind, &FindFileData))

Check warning on line 2340 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'WIN32_FIND_DATA *' to 'LPWIN32_FIND_DATAW' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

Check warning on line 2340 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'WIN32_FIND_DATA *' to 'LPWIN32_FIND_DATAW' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]
{
// if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 &&
// wcscmp(FindFileData.cFileName, _T(".")) == 0 ||
// wcscmp(FindFileData.cFileName, _T("..")) == 0)
if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 &&
wcscmp(FindFileData.cFileName, L".") == 0 ||

Check warning on line 2346 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'CHAR [260]' to 'const wchar_t *' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

Check warning on line 2346 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'CHAR [260]' to 'const wchar_t *' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]
wcscmp(FindFileData.cFileName, L"..") == 0)

Check warning on line 2347 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'CHAR [260]' to 'const wchar_t *' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

Check warning on line 2347 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'CHAR [260]' to 'const wchar_t *' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]
{
continue;
}
Expand All @@ -2308,11 +2352,11 @@
if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
{
WCHAR DirAdd[MAX_PATH];
if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)

Check warning on line 2355 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'CHAR [260]' to 'const wchar_t *' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

Check warning on line 2355 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'CHAR [260]' to 'const wchar_t *' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]
return FALSE;
StringCchCopyW(DirAdd, MAX_PATH, Dir);
StringCchCatW(DirAdd, MAX_PATH, L"\\");
StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName);

Check warning on line 2359 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'CHAR [260]' to 'STRSAFE_LPCWSTR' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

Check warning on line 2359 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'CHAR [260]' to 'STRSAFE_LPCWSTR' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

if (!wf_cliprdr_add_to_file_arrays(clipboard, DirAdd, pathLen))
return FALSE;
Expand All @@ -2323,11 +2367,11 @@
else
{
WCHAR fileName[MAX_PATH];
if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)

Check warning on line 2370 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'CHAR [260]' to 'const wchar_t *' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

Check warning on line 2370 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'CHAR [260]' to 'const wchar_t *' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]
return FALSE;
StringCchCopyW(fileName, MAX_PATH, Dir);
StringCchCatW(fileName, MAX_PATH, L"\\");
StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName);

Check warning on line 2374 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'CHAR [260]' to 'STRSAFE_LPCWSTR' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

Check warning on line 2374 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': incompatible types - from 'CHAR [260]' to 'STRSAFE_LPCWSTR' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

if (!wf_cliprdr_add_to_file_arrays(clipboard, fileName, pathLen))
return FALSE;
Expand Down Expand Up @@ -2443,26 +2487,75 @@

if (!clear_format_map(clipboard))
return ERROR_INTERNAL_ERROR;
clipboard->copied = FALSE;

if (formatList->numFormats > WF_CLIPRDR_MAX_FORMATS)
return ERROR_INTERNAL_ERROR;

if (formatList->numFormats > 0 && !formatList->formats)
return ERROR_INTERNAL_ERROR;

if (!map_ensure_capacity(clipboard, formatList->numFormats))
return ERROR_INTERNAL_ERROR;

clipboard->copied = TRUE;

for (i = 0; i < formatList->numFormats; i++)
{
format = &(formatList->formats[i]);
mapping = &(clipboard->format_mappings[i]);
/* Do not validate the peer-provided formatId as a Windows registered format.
* It is only a remote protocol ID used when requesting data from the peer.
* For named formats, RegisterClipboardFormatW creates the local Windows
* clipboard ID below, and that local ID is checked before publishing. */
mapping->remote_format_id = format->formatId;

if (format->formatName)
{
int size = MultiByteToWideChar(CP_UTF8, 0, format->formatName,
strlen(format->formatName), NULL, 0);
mapping->name = calloc(size + 1, sizeof(WCHAR));
size_t name_len;
int size;

if (!wf_cliprdr_bounded_strlen(format->formatName,
WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES, &name_len))
{
return wf_cliprdr_server_format_list_fail(clipboard);
}

if (name_len == 0)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}

size = MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
NULL, 0);
if (size <= 0)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}

if ((UINT)size > WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}

mapping->name = calloc((size_t)size + 1, sizeof(WCHAR));
if (!mapping->name)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}

if (MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
mapping->name, size) != size)
{
free(mapping->name);
mapping->name = NULL;
return wf_cliprdr_server_format_list_fail(clipboard);
}

if (mapping->name)
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
if (mapping->local_format_id == 0)
{
MultiByteToWideChar(CP_UTF8, 0, format->formatName, strlen(format->formatName),
mapping->name, size);
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
return wf_cliprdr_server_format_list_fail(clipboard);
}
}
else
Expand All @@ -2472,7 +2565,6 @@
}

clipboard->map_size++;
map_ensure_capacity(clipboard);
}

if (file_transferring(clipboard))
Expand All @@ -2482,7 +2574,7 @@
UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32));
if (p_conn_id) {
*p_conn_id = formatList->connID;
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id))

Check warning on line 2577 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'PostMessageA': different types for formal and actual parameter 4 [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

Check warning on line 2577 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': 'LPARAM' differs in levels of indirection from 'UINT32 *' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

Check warning on line 2577 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'PostMessageA': different types for formal and actual parameter 4 [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]

Check warning on line 2577 in libs/clipboard/src/windows/wf_cliprdr.c

View workflow job for this annotation

GitHub Actions / wf_cliprdr invariant test

'function': 'LPARAM' differs in levels of indirection from 'UINT32 *' [D:\a\rustdesk\rustdesk\build\wf-cliprdr\out\test_invariant_wf_cliprdr.vcxproj]
rc = CHANNEL_RC_OK;
}
}
Expand Down
31 changes: 28 additions & 3 deletions src/server/terminal_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1857,15 +1857,33 @@ impl TerminalServiceProxy {
// Process each session with its own lock
for (terminal_id, session_arc) in sessions {
if let Ok(mut session) = session_arc.try_lock() {
// Check if reader thread is still alive and we haven't sent closed message yet
// Check if the session has ended (reader thread finished or child exited).
// On Linux, the PTY reader thread may not return EOF when the shell exits
// (the cloned master fd keeps the read side open), so we also poll the child
// process via try_wait() as a fallback detection mechanism.
let mut should_send_closed = false;
if !session.closed_message_sent {
if let Some(thread) = &session.reader_thread {
if thread.is_finished() {
should_send_closed = true;
session.closed_message_sent = true;
}
}
if !should_send_closed {
if let Some(child) = &mut session.child {
match child.try_wait() {
Ok(Some(_)) => {
should_send_closed = true;
}
Ok(None) => {} // still running
Err(e) => {
log::warn!("Terminal {} child wait error: {}", terminal_id, e);
}
}
}
}
if should_send_closed {
session.closed_message_sent = true;
}
}
// It's Ok to put the closed message here.
// Because the `reader_thread` is joined in `stop()`,
Expand Down Expand Up @@ -2018,7 +2036,8 @@ impl TerminalServiceProxy {
}
}
} else {
// For persistent sessions, just clear the child reference
// For persistent sessions, clear the child reference and remove the session
// if the closed message has been sent (shell has exited).
if let Some(session_arc) = sessions.get(&terminal_id) {
let mut session = session_arc.lock().unwrap();
if let Some(mut child) = session.child.take() {
Expand All @@ -2028,6 +2047,12 @@ impl TerminalServiceProxy {
}
add_to_reaper(child);
}
if session.closed_message_sent {
// Shell has exited, remove the dead session
drop(session);
sessions.remove(&terminal_id);
service.lock().unwrap().sessions.remove(&terminal_id);
}
}
}

Expand Down
Loading