mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-04-14 15:51:30 +03:00
fix(terminal): linux, macOS, win as the controlled (#13930)
1. `TERM` on linux terminal. 2. `htop` command not found on macOS. 3. `vim` and `claude code cli` hung up on windows. Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
11
Cargo.toml
11
Cargo.toml
@@ -123,10 +123,19 @@ winapi = { version = "0.3", features = [
|
|||||||
] }
|
] }
|
||||||
windows = { version = "0.61", features = [
|
windows = { version = "0.61", features = [
|
||||||
"Win32",
|
"Win32",
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_Security_Authorization",
|
||||||
|
"Win32_Storage_FileSystem",
|
||||||
"Win32_System",
|
"Win32_System",
|
||||||
"Win32_System_Diagnostics",
|
"Win32_System_Diagnostics",
|
||||||
"Win32_System_Threading",
|
|
||||||
"Win32_System_Diagnostics_ToolHelp",
|
"Win32_System_Diagnostics_ToolHelp",
|
||||||
|
"Win32_System_Environment",
|
||||||
|
"Win32_System_IO",
|
||||||
|
"Win32_System_Memory",
|
||||||
|
"Win32_System_Pipes",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
"Win32_UI_Shell",
|
||||||
] }
|
] }
|
||||||
winreg = "0.11"
|
winreg = "0.11"
|
||||||
windows-service = "0.6"
|
windows-service = "0.6"
|
||||||
|
|||||||
@@ -615,6 +615,17 @@ pub fn core_main() -> Option<Vec<String>> {
|
|||||||
#[cfg(feature = "hwcodec")]
|
#[cfg(feature = "hwcodec")]
|
||||||
crate::ipc::hwcodec_process();
|
crate::ipc::hwcodec_process();
|
||||||
return None;
|
return None;
|
||||||
|
} else if args[0] == "--terminal-helper" {
|
||||||
|
// Terminal helper process - runs as user to create ConPTY
|
||||||
|
// This is needed because ConPTY has compatibility issues with CreateProcessAsUserW
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let helper_args: Vec<String> = args[1..].to_vec();
|
||||||
|
if let Err(e) = crate::server::terminal_helper::run_terminal_helper(&helper_args) {
|
||||||
|
log::error!("Terminal helper failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return None;
|
||||||
} else if args[0] == "--cm" {
|
} else if args[0] == "--cm" {
|
||||||
// call connection manager to establish connections
|
// call connection manager to establish connections
|
||||||
// meanwhile, return true to call flutter window to show control panel
|
// meanwhile, return true to call flutter window to show control panel
|
||||||
|
|||||||
@@ -35,13 +35,20 @@ static mut UNMODIFIED: bool = true;
|
|||||||
const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"];
|
const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"];
|
||||||
const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"];
|
const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"];
|
||||||
|
|
||||||
|
// Terminal type constants
|
||||||
|
const TERM_XTERM_256COLOR: &str = "xterm-256color";
|
||||||
|
const TERM_SCREEN_256COLOR: &str = "screen-256color";
|
||||||
|
const TERM_XTERM: &str = "xterm";
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref IS_X11: bool = hbb_common::platform::linux::is_x11_or_headless();
|
pub static ref IS_X11: bool = hbb_common::platform::linux::is_x11_or_headless();
|
||||||
|
// Cache for TERM value - once TERM_XTERM_256COLOR is found, reuse it directly
|
||||||
|
static ref CACHED_TERM: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
|
||||||
static ref DATABASE_XTERM_256COLOR: Option<Database> = {
|
static ref DATABASE_XTERM_256COLOR: Option<Database> = {
|
||||||
match Database::from_name("xterm-256color") {
|
match Database::from_name(TERM_XTERM_256COLOR) {
|
||||||
Ok(database) => Some(database),
|
Ok(database) => Some(database),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("Failed to initialize xterm-256color database: {}", err);
|
log::error!("Failed to initialize {} database: {}", TERM_XTERM_256COLOR, err);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,12 +317,12 @@ fn start_uinput_service() {
|
|||||||
/// modern features required by many applications.
|
/// modern features required by many applications.
|
||||||
fn suggest_best_term() -> String {
|
fn suggest_best_term() -> String {
|
||||||
if is_running_in_tmux() || is_running_in_screen() {
|
if is_running_in_tmux() || is_running_in_screen() {
|
||||||
return "screen-256color".to_string();
|
return TERM_SCREEN_256COLOR.to_string();
|
||||||
}
|
}
|
||||||
if term_supports_256_colors("xterm-256color") {
|
if term_supports_256_colors(TERM_XTERM_256COLOR) {
|
||||||
return "xterm-256color".to_string();
|
return TERM_XTERM_256COLOR.to_string();
|
||||||
}
|
}
|
||||||
"xterm".to_string()
|
TERM_XTERM.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_running_in_tmux() -> bool {
|
fn is_running_in_tmux() -> bool {
|
||||||
@@ -332,7 +339,7 @@ fn supports_256_colors(db: &Database) -> bool {
|
|||||||
|
|
||||||
fn term_supports_256_colors(term: &str) -> bool {
|
fn term_supports_256_colors(term: &str) -> bool {
|
||||||
match term {
|
match term {
|
||||||
"xterm-256color" => DATABASE_XTERM_256COLOR
|
TERM_XTERM_256COLOR => DATABASE_XTERM_256COLOR
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(false, |db| supports_256_colors(db)),
|
.map_or(false, |db| supports_256_colors(db)),
|
||||||
_ => Database::from_name(term).map_or(false, |db| supports_256_colors(&db)),
|
_ => Database::from_name(term).map_or(false, |db| supports_256_colors(&db)),
|
||||||
@@ -340,25 +347,140 @@ fn term_supports_256_colors(term: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_cur_term(uid: &str) -> Option<String> {
|
fn get_cur_term(uid: &str) -> Option<String> {
|
||||||
|
// Check cache first - if TERM_XTERM_256COLOR was found before, reuse it
|
||||||
|
if let Ok(cache) = CACHED_TERM.lock() {
|
||||||
|
if let Some(ref cached) = *cache {
|
||||||
|
if cached == TERM_XTERM_256COLOR {
|
||||||
|
return Some(cached.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if uid.is_empty() {
|
if uid.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check current process environment
|
||||||
if let Ok(term) = std::env::var("TERM") {
|
if let Ok(term) = std::env::var("TERM") {
|
||||||
if !INVALID_TERM_VALUES.contains(&term.as_str()) {
|
if term == TERM_XTERM_256COLOR {
|
||||||
|
if let Ok(mut cache) = CACHED_TERM.lock() {
|
||||||
|
*cache = Some(term.clone());
|
||||||
|
}
|
||||||
return Some(term);
|
return Some(term);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for proc in SHELL_PROCESSES {
|
// Collect all TERM values from shell processes, looking for TERM_XTERM_256COLOR
|
||||||
// Construct a regex pattern to match either the process name followed by '$' or 'bin/' followed by the process name.
|
let terms = get_all_term_values(uid);
|
||||||
let term = get_env("TERM", uid, &format!("{}$|bin/{}", proc, proc));
|
|
||||||
if !INVALID_TERM_VALUES.contains(&term.as_str()) {
|
// Prefer TERM_XTERM_256COLOR
|
||||||
return Some(term);
|
if terms.iter().any(|t| t == TERM_XTERM_256COLOR) {
|
||||||
|
if let Ok(mut cache) = CACHED_TERM.lock() {
|
||||||
|
*cache = Some(TERM_XTERM_256COLOR.to_string());
|
||||||
|
}
|
||||||
|
return Some(TERM_XTERM_256COLOR.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return first valid TERM if no TERM_XTERM_256COLOR found
|
||||||
|
let fallback = terms.into_iter().next();
|
||||||
|
if let Some(ref term) = fallback {
|
||||||
|
log::debug!(
|
||||||
|
"TERM_XTERM_256COLOR not found, using fallback TERM: {}",
|
||||||
|
term
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all TERM values from shell processes (bash, zsh, fish, sh).
|
||||||
|
/// Returns a Vec of unique, valid TERM values.
|
||||||
|
fn get_all_term_values(uid: &str) -> Vec<String> {
|
||||||
|
let Ok(uid_num) = uid.parse::<u32>() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build regex pattern to match shell processes using only argv[0] (the executable path)
|
||||||
|
// Pattern: match process name at start or after '/', followed by space or end
|
||||||
|
// e.g., "bash", "/bin/bash", "/usr/bin/zsh"
|
||||||
|
let shell_pattern = SHELL_PROCESSES
|
||||||
|
.iter()
|
||||||
|
.map(|p| format!(r"(^|/){p}(\s|$)"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("|");
|
||||||
|
let Ok(re) = Regex::new(&shell_pattern) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(entries) = std::fs::read_dir("/proc") else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut terms = Vec::new();
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let file_name = entry.file_name();
|
||||||
|
let Some(pid_str) = file_name.to_str() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !pid_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let proc_path = entry.path();
|
||||||
|
|
||||||
|
// Check if process belongs to the specified uid
|
||||||
|
if let Ok(meta) = std::fs::metadata(&proc_path) {
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
if meta.uid() != uid_num {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cmdline matches process pattern
|
||||||
|
// /proc/<pid>/cmdline is a sequence of null-terminated strings; the first
|
||||||
|
// one (argv[0]) is the executable path. Match the regex only against that
|
||||||
|
// to avoid false positives from arguments (e.g., "python /path/to/bash-script.py").
|
||||||
|
let cmdline_path = proc_path.join("cmdline");
|
||||||
|
let Ok(cmdline) = std::fs::read(&cmdline_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let exe_end = cmdline.iter().position(|&b| b == 0).unwrap_or(cmdline.len());
|
||||||
|
let exe_str = String::from_utf8_lossy(&cmdline[..exe_end]);
|
||||||
|
if !re.is_match(&exe_str) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read environ and extract TERM
|
||||||
|
let environ_path = proc_path.join("environ");
|
||||||
|
let Ok(environ) = std::fs::read(&environ_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
for part in environ.split(|&b| b == 0) {
|
||||||
|
if part.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(eq) = part.iter().position(|&b| b == b'=') {
|
||||||
|
let key_bytes = &part[..eq];
|
||||||
|
if key_bytes == b"TERM" {
|
||||||
|
let val_bytes = &part[eq + 1..];
|
||||||
|
let term = String::from_utf8_lossy(val_bytes).into_owned();
|
||||||
|
if !INVALID_TERM_VALUES.contains(&term.as_str()) && !terms.contains(&term) {
|
||||||
|
// Early return if we found the preferred term
|
||||||
|
if term == TERM_XTERM_256COLOR {
|
||||||
|
return vec![term];
|
||||||
|
}
|
||||||
|
terms.push(term);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
terms
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ use video_service::VideoSource;
|
|||||||
use crate::ipc::Data;
|
use crate::ipc::Data;
|
||||||
|
|
||||||
pub mod audio_service;
|
pub mod audio_service;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub mod terminal_helper;
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub mod terminal_service;
|
pub mod terminal_service;
|
||||||
cfg_if::cfg_if! {
|
cfg_if::cfg_if! {
|
||||||
|
|||||||
@@ -3231,12 +3231,15 @@ impl Connection {
|
|||||||
if !token.is_null() {
|
if !token.is_null() {
|
||||||
match crate::platform::ensure_primary_token(token) {
|
match crate::platform::ensure_primary_token(token) {
|
||||||
Ok(t) => {
|
Ok(t) => {
|
||||||
self.terminal_user_token = Some(TerminalUserToken::CurrentLogonUser(t as _));
|
self.terminal_user_token = Some(TerminalUserToken::CurrentLogonUser(
|
||||||
|
crate::terminal_service::UserToken::new(t as usize),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to ensure primary token: {}", e);
|
log::error!("Failed to ensure primary token: {}", e);
|
||||||
self.terminal_user_token =
|
self.terminal_user_token = Some(TerminalUserToken::CurrentLogonUser(
|
||||||
Some(TerminalUserToken::CurrentLogonUser(token as _));
|
crate::terminal_service::UserToken::new(token as usize),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -5049,9 +5052,9 @@ impl Drop for Connection {
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
if let Some(TerminalUserToken::CurrentLogonUser(token)) = self.terminal_user_token.take() {
|
if let Some(TerminalUserToken::CurrentLogonUser(token)) = self.terminal_user_token.take() {
|
||||||
if token != 0 {
|
if token.as_raw() != 0 {
|
||||||
unsafe {
|
unsafe {
|
||||||
hbb_common::allow_err!(CloseHandle(HANDLE(token as _)));
|
hbb_common::allow_err!(CloseHandle(HANDLE(token.as_raw() as _)));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1062
src/server/terminal_helper.rs
Normal file
1062
src/server/terminal_helper.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,15 @@ use std::{
|
|||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Windows-specific imports from terminal_helper module
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use super::terminal_helper::{
|
||||||
|
create_named_pipe_server, encode_helper_message, encode_resize_message,
|
||||||
|
is_helper_process_running, launch_terminal_helper_with_token, wait_for_pipe_connection,
|
||||||
|
HelperProcessGuard, OwnedHandle, SendableHandle, WinCloseHandle, WinTerminateProcess,
|
||||||
|
WinWaitForSingleObject, MSG_TYPE_DATA, PIPE_CONNECTION_TIMEOUT_MS, WIN_WAIT_OBJECT_0,
|
||||||
|
};
|
||||||
|
|
||||||
const MAX_OUTPUT_BUFFER_SIZE: usize = 1024 * 1024; // 1MB per terminal
|
const MAX_OUTPUT_BUFFER_SIZE: usize = 1024 * 1024; // 1MB per terminal
|
||||||
const MAX_BUFFER_LINES: usize = 10000;
|
const MAX_BUFFER_LINES: usize = 10000;
|
||||||
const MAX_SERVICES: usize = 100; // Maximum number of persistent terminal services
|
const MAX_SERVICES: usize = 100; // Maximum number of persistent terminal services
|
||||||
@@ -53,28 +62,8 @@ pub fn generate_service_id() -> String {
|
|||||||
fn get_default_shell() -> String {
|
fn get_default_shell() -> String {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
// Try PowerShell Core first (cross-platform version)
|
// Use shared implementation from terminal_helper
|
||||||
// Common installation paths for PowerShell Core
|
super::terminal_helper::get_default_shell()
|
||||||
let pwsh_paths = [
|
|
||||||
"pwsh.exe",
|
|
||||||
r"C:\Program Files\PowerShell\7\pwsh.exe",
|
|
||||||
r"C:\Program Files\PowerShell\6\pwsh.exe",
|
|
||||||
];
|
|
||||||
|
|
||||||
for path in &pwsh_paths {
|
|
||||||
if std::path::Path::new(path).exists() {
|
|
||||||
return path.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try Windows PowerShell (should be available on all Windows systems)
|
|
||||||
let powershell_path = r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe";
|
|
||||||
if std::path::Path::new(powershell_path).exists() {
|
|
||||||
return powershell_path.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final fallback to cmd.exe
|
|
||||||
std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string())
|
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
{
|
{
|
||||||
@@ -280,7 +269,30 @@ pub fn get_terminal_session_count(include_zombie_tasks: bool) -> usize {
|
|||||||
c
|
c
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type UserToken = u64;
|
/// User token wrapper for cross-module use.
|
||||||
|
///
|
||||||
|
/// # Design Note
|
||||||
|
/// On Windows, this type is defined in terminal_helper.rs and re-exported here.
|
||||||
|
/// On non-Windows platforms, it's defined here directly.
|
||||||
|
/// This design avoids circular dependencies while keeping the API consistent.
|
||||||
|
/// Both definitions MUST have identical public API (new, as_raw methods).
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct UserToken(pub usize);
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
impl UserToken {
|
||||||
|
pub fn new(handle: usize) -> Self {
|
||||||
|
Self(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_raw(&self) -> usize {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use super::terminal_helper::UserToken;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TerminalService {
|
pub struct TerminalService {
|
||||||
@@ -458,6 +470,12 @@ pub struct TerminalSession {
|
|||||||
// Track if we've already sent the closed message
|
// Track if we've already sent the closed message
|
||||||
closed_message_sent: bool,
|
closed_message_sent: bool,
|
||||||
is_opened: bool,
|
is_opened: bool,
|
||||||
|
// Helper mode: PTY is managed by helper process, communication via message protocol
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
is_helper_mode: bool,
|
||||||
|
// Handle to helper process for termination when session closes
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
helper_process_handle: Option<SendableHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TerminalSession {
|
impl TerminalSession {
|
||||||
@@ -479,6 +497,10 @@ impl TerminalSession {
|
|||||||
cols,
|
cols,
|
||||||
closed_message_sent: false,
|
closed_message_sent: false,
|
||||||
is_opened: false,
|
is_opened: false,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
is_helper_mode: false,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
helper_process_handle: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,14 +519,58 @@ impl TerminalSession {
|
|||||||
// Send a final newline to ensure the reader can read some data, and then exit.
|
// Send a final newline to ensure the reader can read some data, and then exit.
|
||||||
// This is required on Windows and Linux.
|
// This is required on Windows and Linux.
|
||||||
// Although `self.pty_pair = None;` is called below, we can still send a final newline here.
|
// Although `self.pty_pair = None;` is called below, we can still send a final newline here.
|
||||||
if let Err(e) = input_tx.send(b"\r\n".to_vec()) {
|
#[cfg(target_os = "windows")]
|
||||||
|
let final_msg = if self.is_helper_mode {
|
||||||
|
encode_helper_message(MSG_TYPE_DATA, b"\r\n")
|
||||||
|
} else {
|
||||||
|
b"\r\n".to_vec()
|
||||||
|
};
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let final_msg = b"\r\n".to_vec();
|
||||||
|
|
||||||
|
if let Err(e) = input_tx.send(final_msg) {
|
||||||
log::warn!("Failed to send final newline to the terminal: {}", e);
|
log::warn!("Failed to send final newline to the terminal: {}", e);
|
||||||
}
|
}
|
||||||
drop(input_tx);
|
drop(input_tx);
|
||||||
}
|
}
|
||||||
self.output_rx = None;
|
self.output_rx = None;
|
||||||
|
|
||||||
// 1. Windows
|
// CRITICAL: In helper mode, we must terminate the helper process BEFORE joining threads!
|
||||||
|
// The reader thread is blocking on output_pipe.read(), which only returns EOF when
|
||||||
|
// the helper process exits. If we try to join the reader thread first, we deadlock.
|
||||||
|
//
|
||||||
|
// Sequence for helper mode:
|
||||||
|
// 1. Signal exiting and close input channel (done above)
|
||||||
|
// 2. Terminate helper process (causes output pipe EOF)
|
||||||
|
// 3. Join reader thread (now unblocked due to EOF)
|
||||||
|
// 4. Join writer thread
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if self.is_helper_mode {
|
||||||
|
if let Some(helper_handle) = self.helper_process_handle.take() {
|
||||||
|
let handle = helper_handle.as_raw();
|
||||||
|
log::debug!("Helper mode: terminating helper process before joining threads...");
|
||||||
|
|
||||||
|
// Give helper a very short time to exit gracefully (it should detect pipe close)
|
||||||
|
// But don't wait too long - we need to unblock the reader thread
|
||||||
|
let wait_result = unsafe { WinWaitForSingleObject(handle, 100) };
|
||||||
|
|
||||||
|
if wait_result == WIN_WAIT_OBJECT_0 {
|
||||||
|
log::debug!("Helper process exited gracefully");
|
||||||
|
} else {
|
||||||
|
// Force terminate to unblock reader thread
|
||||||
|
log::debug!("Force terminating helper process to unblock reader thread");
|
||||||
|
unsafe {
|
||||||
|
let _ = WinTerminateProcess(handle, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let _ = WinCloseHandle(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Windows (non-helper mode)
|
||||||
// `pty_pair` uses pipe. https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/win/conpty.rs#L16
|
// `pty_pair` uses pipe. https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/win/conpty.rs#L16
|
||||||
// `read()` may stuck at https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/filedescriptor/src/windows.rs#L345
|
// `read()` may stuck at https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/filedescriptor/src/windows.rs#L345
|
||||||
// We can close the pipe to signal the reader thread to exit.
|
// We can close the pipe to signal the reader thread to exit.
|
||||||
@@ -747,6 +813,15 @@ impl TerminalServiceProxy {
|
|||||||
return Ok(Some(response));
|
return Ok(Some(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Windows with user_token: use helper process to run shell as the logged-in user
|
||||||
|
// This solves the ConPTY + CreateProcessAsUserW incompatibility issue where
|
||||||
|
// vim, Claude Code, and other TUI applications hang when ConPTY is created
|
||||||
|
// by SYSTEM service but shell runs as user via CreateProcessAsUserW.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if self.user_token.is_some() {
|
||||||
|
return self.handle_open_with_helper(service, open);
|
||||||
|
}
|
||||||
|
|
||||||
// Create new terminal session
|
// Create new terminal session
|
||||||
log::info!(
|
log::info!(
|
||||||
"Creating new terminal {} for service: {}",
|
"Creating new terminal {} for service: {}",
|
||||||
@@ -774,12 +849,19 @@ impl TerminalServiceProxy {
|
|||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut cmd = CommandBuilder::new(&shell);
|
let mut cmd = CommandBuilder::new(&shell);
|
||||||
|
|
||||||
// Set `TERM` environment variable for macOS to ensure proper terminal behavior
|
// macOS-specific terminal configuration
|
||||||
// This fixes issues with control sequences (e.g., Delete/Backspace keys)
|
// 1. Use login shell (-l) to load user's shell profile (~/.zprofile, ~/.bash_profile)
|
||||||
// macOS terminfo uses hex naming: '78' = 'x' for xterm entries
|
// This ensures PATH includes Homebrew paths (/opt/homebrew/bin, /usr/local/bin)
|
||||||
|
// 2. Set TERM environment variable for proper terminal behavior
|
||||||
|
// This fixes issues with control sequences (e.g., Delete/Backspace keys)
|
||||||
|
// macOS terminfo uses hex naming: '78' = 'x' for xterm entries
|
||||||
// Note: For Linux, `TERM` is set in src/platform/linux.rs try_start_server_()
|
// Note: For Linux, `TERM` is set in src/platform/linux.rs try_start_server_()
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
|
// Start as login shell to load user environment (PATH, etc.)
|
||||||
|
cmd.arg("-l");
|
||||||
|
log::debug!("Added -l flag for macOS login shell");
|
||||||
|
|
||||||
let term = if std::path::Path::new("/usr/share/terminfo/78/xterm-256color").exists() {
|
let term = if std::path::Path::new("/usr/share/terminfo/78/xterm-256color").exists() {
|
||||||
"xterm-256color"
|
"xterm-256color"
|
||||||
} else {
|
} else {
|
||||||
@@ -789,10 +871,9 @@ impl TerminalServiceProxy {
|
|||||||
log::debug!("Set TERM={} for macOS PTY", term);
|
log::debug!("Set TERM={} for macOS PTY", term);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
// Note: On Windows with user_token, we use helper mode (handle_open_with_helper)
|
||||||
if let Some(token) = &self.user_token {
|
// which is dispatched earlier in this function. This code path is only reached
|
||||||
cmd.set_user_token(*token as _);
|
// when user_token is None (e.g., running directly as user, not as SYSTEM service).
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!("Spawning shell process...");
|
log::debug!("Spawning shell process...");
|
||||||
let child = pty_pair
|
let child = pty_pair
|
||||||
@@ -820,17 +901,6 @@ impl TerminalServiceProxy {
|
|||||||
let terminal_id = open.terminal_id;
|
let terminal_id = open.terminal_id;
|
||||||
let writer_thread = thread::spawn(move || {
|
let writer_thread = thread::spawn(move || {
|
||||||
let mut writer = writer;
|
let mut writer = writer;
|
||||||
// Write initial carriage return:
|
|
||||||
// 1. Windows requires at least one carriage return for `drop()` to work properly.
|
|
||||||
// Without this, the reader may fail to read the buffer after `input_tx.send(b"\r\n".to_vec()).ok();`.
|
|
||||||
// 2. This also refreshes the terminal interface on the controlling side (workaround for blank content on connect).
|
|
||||||
if let Err(e) = writer.write_all(b"\r") {
|
|
||||||
log::error!("Terminal {} initial write error: {}", terminal_id, e);
|
|
||||||
} else {
|
|
||||||
if let Err(e) = writer.flush() {
|
|
||||||
log::error!("Terminal {} initial flush error: {}", terminal_id, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while let Ok(data) = input_rx.recv() {
|
while let Ok(data) = input_rx.recv() {
|
||||||
if let Err(e) = writer.write_all(&data) {
|
if let Err(e) = writer.write_all(&data) {
|
||||||
log::error!("Terminal {} write error: {}", terminal_id, e);
|
log::error!("Terminal {} write error: {}", terminal_id, e);
|
||||||
@@ -930,6 +1000,222 @@ impl TerminalServiceProxy {
|
|||||||
Ok(Some(response))
|
Ok(Some(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Windows-only: Open terminal using helper process pattern
|
||||||
|
/// This solves the ConPTY + CreateProcessAsUserW incompatibility issue.
|
||||||
|
/// The helper process runs as the logged-in user and creates ConPTY + shell,
|
||||||
|
/// communicating with this service via named pipes.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn handle_open_with_helper(
|
||||||
|
&self,
|
||||||
|
service: &mut PersistentTerminalService,
|
||||||
|
open: &OpenTerminal,
|
||||||
|
) -> Result<Option<TerminalResponse>> {
|
||||||
|
let mut response = TerminalResponse::new();
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Creating new terminal {} using helper process for service: {}",
|
||||||
|
open.terminal_id,
|
||||||
|
service.service_id
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut session =
|
||||||
|
TerminalSession::new(open.terminal_id, open.rows as u16, open.cols as u16);
|
||||||
|
|
||||||
|
// Generate unique pipe names for this terminal
|
||||||
|
let pipe_id = uuid::Uuid::new_v4();
|
||||||
|
let input_pipe_name = format!(r"\\.\pipe\rustdesk_term_in_{}", pipe_id);
|
||||||
|
let output_pipe_name = format!(r"\\.\pipe\rustdesk_term_out_{}", pipe_id);
|
||||||
|
|
||||||
|
log::debug!(
|
||||||
|
"Creating pipes: input={}, output={}",
|
||||||
|
input_pipe_name,
|
||||||
|
output_pipe_name
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get user_token early - needed for both DACL creation and helper launch
|
||||||
|
let user_token = self
|
||||||
|
.user_token
|
||||||
|
.ok_or_else(|| anyhow!("user_token is required for helper mode"))?;
|
||||||
|
|
||||||
|
// Create pipes (server side, don't wait for connection yet)
|
||||||
|
// input_pipe: service WRITES to this, helper READS from this
|
||||||
|
// output_pipe: service READS from this, helper WRITES to this
|
||||||
|
// Using OwnedHandle for RAII - handles are automatically closed on error
|
||||||
|
// Pass user_token to create restricted DACL (only SYSTEM + user can access)
|
||||||
|
let input_pipe_handle = OwnedHandle::new(create_named_pipe_server(
|
||||||
|
&input_pipe_name,
|
||||||
|
false,
|
||||||
|
user_token,
|
||||||
|
)?);
|
||||||
|
let output_pipe_handle = OwnedHandle::new(create_named_pipe_server(
|
||||||
|
&output_pipe_name,
|
||||||
|
true,
|
||||||
|
user_token,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
let helper_process_info = launch_terminal_helper_with_token(
|
||||||
|
user_token,
|
||||||
|
&input_pipe_name,
|
||||||
|
&output_pipe_name,
|
||||||
|
open.terminal_id,
|
||||||
|
open.rows as u16,
|
||||||
|
open.cols as u16,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Use HelperProcessGuard for RAII cleanup - terminates process on error
|
||||||
|
// Unlike OwnedHandle which only closes the handle, this guard ensures
|
||||||
|
// the helper process is terminated if pipe connection fails or other errors occur.
|
||||||
|
let helper_process_guard =
|
||||||
|
HelperProcessGuard::new(helper_process_info.handle, helper_process_info.pid);
|
||||||
|
let helper_pid = helper_process_guard.pid();
|
||||||
|
|
||||||
|
// Wait for helper to connect to pipes
|
||||||
|
// If this fails, HelperProcessGuard will terminate the helper process
|
||||||
|
let mut input_pipe = wait_for_pipe_connection(
|
||||||
|
input_pipe_handle,
|
||||||
|
&input_pipe_name,
|
||||||
|
PIPE_CONNECTION_TIMEOUT_MS,
|
||||||
|
)?;
|
||||||
|
let mut output_pipe = wait_for_pipe_connection(
|
||||||
|
output_pipe_handle,
|
||||||
|
&output_pipe_name,
|
||||||
|
PIPE_CONNECTION_TIMEOUT_MS,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Check if helper process is still running after pipe connection
|
||||||
|
// This provides early detection if helper crashed during startup
|
||||||
|
if !is_helper_process_running(helper_process_guard.as_raw()) {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Helper process (PID {}) exited unexpectedly after pipe connection",
|
||||||
|
helper_pid
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disarm the guard and transfer ownership to session
|
||||||
|
// From this point, the session is responsible for terminating the helper
|
||||||
|
let helper_raw_handle = helper_process_guard.disarm();
|
||||||
|
|
||||||
|
// Use helper process PID for session tracking
|
||||||
|
// Note: This is the helper process PID, not the actual shell PID.
|
||||||
|
// The real shell runs inside the helper process but its PID is not exposed here.
|
||||||
|
// For process management (termination, status), the helper PID is what we need.
|
||||||
|
session.pid = helper_pid;
|
||||||
|
|
||||||
|
// Create channels for input/output (same as direct PTY mode)
|
||||||
|
let (input_tx, input_rx) = mpsc::sync_channel::<Vec<u8>>(CHANNEL_BUFFER_SIZE);
|
||||||
|
let (output_tx, output_rx) = mpsc::sync_channel::<Vec<u8>>(CHANNEL_BUFFER_SIZE);
|
||||||
|
|
||||||
|
// Spawn writer thread: reads from channel, writes to input pipe
|
||||||
|
let terminal_id = open.terminal_id;
|
||||||
|
let writer_thread = thread::spawn(move || {
|
||||||
|
while let Ok(data) = input_rx.recv() {
|
||||||
|
if let Err(e) = input_pipe.write_all(&data) {
|
||||||
|
log::error!("Terminal {} pipe write error: {}", terminal_id, e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Err(e) = input_pipe.flush() {
|
||||||
|
log::error!("Terminal {} pipe flush error: {}", terminal_id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::debug!(
|
||||||
|
"Terminal {} writer thread (helper mode) exiting",
|
||||||
|
terminal_id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spawn reader thread: reads from output pipe, sends to channel
|
||||||
|
// Note: The output pipe was created with FILE_FLAG_OVERLAPPED for timeout support
|
||||||
|
// during ConnectNamedPipe. However, once converted to a File handle, reads are
|
||||||
|
// performed synchronously. The WouldBlock handling below is defensive but may
|
||||||
|
// not be triggered in practice since File::read() blocks until data is available.
|
||||||
|
let exiting = session.exiting.clone();
|
||||||
|
let terminal_id = open.terminal_id;
|
||||||
|
let reader_thread = thread::spawn(move || {
|
||||||
|
let mut buf = vec![0u8; 4096];
|
||||||
|
loop {
|
||||||
|
match output_pipe.read(&mut buf) {
|
||||||
|
Ok(0) => {
|
||||||
|
// EOF - helper process exited
|
||||||
|
log::debug!("Terminal {} helper output EOF", terminal_id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(n) => {
|
||||||
|
if exiting.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let data = buf[..n].to_vec();
|
||||||
|
match output_tx.try_send(data) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(mpsc::TrySendError::Full(_)) => {
|
||||||
|
log::debug!(
|
||||||
|
"Terminal {} output channel full, dropping data",
|
||||||
|
terminal_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||||
|
log::debug!("Terminal {} output channel disconnected", terminal_id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
// Defensive: WouldBlock is unlikely with synchronous File::read(),
|
||||||
|
// but handle it gracefully just in case.
|
||||||
|
if exiting.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Terminal {} pipe read error: {}", terminal_id, e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::debug!(
|
||||||
|
"Terminal {} reader thread (helper mode) exiting",
|
||||||
|
terminal_id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// In helper mode, we don't have pty_pair or child - helper manages those
|
||||||
|
session.pty_pair = None;
|
||||||
|
session.child = None;
|
||||||
|
session.input_tx = Some(input_tx);
|
||||||
|
session.output_rx = Some(output_rx);
|
||||||
|
session.reader_thread = Some(reader_thread);
|
||||||
|
session.writer_thread = Some(writer_thread);
|
||||||
|
session.is_opened = true;
|
||||||
|
session.is_helper_mode = true;
|
||||||
|
session.helper_process_handle = Some(SendableHandle::new(helper_raw_handle));
|
||||||
|
|
||||||
|
let mut opened = TerminalOpened::new();
|
||||||
|
opened.terminal_id = open.terminal_id;
|
||||||
|
opened.success = true;
|
||||||
|
opened.message = "Terminal opened (helper mode)".to_string();
|
||||||
|
opened.pid = session.pid;
|
||||||
|
opened.service_id = service.service_id.clone();
|
||||||
|
if service.needs_session_sync {
|
||||||
|
if !service.sessions.is_empty() {
|
||||||
|
opened.persistent_sessions = service.sessions.keys().cloned().collect();
|
||||||
|
}
|
||||||
|
service.needs_session_sync = false;
|
||||||
|
}
|
||||||
|
response.set_opened(opened);
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Terminal {} opened successfully using helper process (PID {})",
|
||||||
|
open.terminal_id,
|
||||||
|
session.pid
|
||||||
|
);
|
||||||
|
|
||||||
|
service
|
||||||
|
.sessions
|
||||||
|
.insert(open.terminal_id, Arc::new(Mutex::new(session)));
|
||||||
|
|
||||||
|
Ok(Some(response))
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_resize(
|
fn handle_resize(
|
||||||
&self,
|
&self,
|
||||||
session: Option<Arc<Mutex<TerminalSession>>>,
|
session: Option<Arc<Mutex<TerminalSession>>>,
|
||||||
@@ -941,18 +1227,50 @@ impl TerminalServiceProxy {
|
|||||||
session.rows = resize.rows as u16;
|
session.rows = resize.rows as u16;
|
||||||
session.cols = resize.cols as u16;
|
session.cols = resize.cols as u16;
|
||||||
|
|
||||||
if let Some(pty_pair) = &session.pty_pair {
|
// Windows: handle helper mode vs direct PTY mode
|
||||||
pty_pair.master.resize(PtySize {
|
#[cfg(target_os = "windows")]
|
||||||
rows: resize.rows as u16,
|
{
|
||||||
cols: resize.cols as u16,
|
if session.is_helper_mode {
|
||||||
pixel_width: 0,
|
// Helper mode: send resize command via message protocol
|
||||||
pixel_height: 0,
|
if let Some(input_tx) = &session.input_tx {
|
||||||
})?;
|
let msg = encode_resize_message(resize.rows as u16, resize.cols as u16);
|
||||||
|
if let Err(e) = input_tx.send(msg) {
|
||||||
|
log::error!("Failed to send resize to helper: {}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"Terminal {} is in helper mode but input_tx is None, cannot send resize",
|
||||||
|
resize.terminal_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Direct PTY mode
|
||||||
|
Self::resize_pty(&session, resize)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-Windows: always direct PTY mode
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
Self::resize_pty(&session, resize)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resize PTY directly (used for non-helper mode)
|
||||||
|
fn resize_pty(session: &TerminalSession, resize: &ResizeTerminal) -> Result<()> {
|
||||||
|
if let Some(pty_pair) = &session.pty_pair {
|
||||||
|
pty_pair.master.resize(PtySize {
|
||||||
|
rows: resize.rows as u16,
|
||||||
|
cols: resize.cols as u16,
|
||||||
|
pixel_width: 0,
|
||||||
|
pixel_height: 0,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_data(
|
fn handle_data(
|
||||||
&self,
|
&self,
|
||||||
session: Option<Arc<Mutex<TerminalSession>>>,
|
session: Option<Arc<Mutex<TerminalSession>>>,
|
||||||
@@ -962,8 +1280,18 @@ impl TerminalServiceProxy {
|
|||||||
let mut session = session_arc.lock().unwrap();
|
let mut session = session_arc.lock().unwrap();
|
||||||
session.update_activity();
|
session.update_activity();
|
||||||
if let Some(input_tx) = &session.input_tx {
|
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
|
// Send data to writer thread
|
||||||
if let Err(e) = input_tx.send(data.data.to_vec()) {
|
if let Err(e) = input_tx.send(msg) {
|
||||||
log::error!(
|
log::error!(
|
||||||
"Failed to send data to terminal {}: {}",
|
"Failed to send data to terminal {}: {}",
|
||||||
data.terminal_id,
|
data.terminal_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user