diff options
Diffstat (limited to 'src/platform')
| -rw-r--r-- | src/platform/fallback.rs | 22 | ||||
| -rw-r--r-- | src/platform/linux.rs | 7 | ||||
| -rw-r--r-- | src/platform/macos.rs | 7 | ||||
| -rw-r--r-- | src/platform/unix_common.rs | 84 | ||||
| -rw-r--r-- | src/platform/windows.rs | 183 |
5 files changed, 289 insertions, 14 deletions
diff --git a/src/platform/fallback.rs b/src/platform/fallback.rs index f6380df8..2086e2a7 100644 --- a/src/platform/fallback.rs +++ b/src/platform/fallback.rs @@ -89,6 +89,28 @@ pub(crate) fn should_draw_host_cursor_by_default() -> bool { false } +pub(crate) fn hostname() -> Option<String> { + None +} + +pub(crate) fn local_datetime() -> Option<time::PrimitiveDateTime> { + None +} + +pub(crate) fn status_commands_supported() -> bool { + false +} + +pub(crate) fn configure_status_command(_process: &mut std::process::Command) {} + +pub(crate) struct StatusCommandGuard; + +impl StatusCommandGuard { + pub(crate) fn new(_child: &tokio::process::Child) -> std::io::Result<Self> { + Ok(Self) + } +} + fn raw_command_argv(command: &str, flag: &str) -> Vec<std::ffi::OsString> { vec!["/bin/sh".into(), flag.into(), command.into()] } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 7932ae09..89e6c96d 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -13,9 +13,10 @@ use super::{ }; pub(crate) use super::unix_common::{ - create_remote_private_dir, create_remote_ssh_config_dir, create_remote_ssh_config_file, - remote_bridge_endpoint_path, remote_private_temp_base, remote_reattach_argument, - remote_reattach_program, remote_ssh_config_paths, + configure_status_command, create_remote_private_dir, create_remote_ssh_config_dir, + create_remote_ssh_config_file, hostname, local_datetime, remote_bridge_endpoint_path, + remote_private_temp_base, remote_reattach_argument, remote_reattach_program, + remote_ssh_config_paths, status_commands_supported, StatusCommandGuard, }; const WSL_MARKER_ENV_VARS: &[&str] = &["WSL_DISTRO_NAME", "WSL_INTEROP"]; diff --git a/src/platform/macos.rs b/src/platform/macos.rs index fdb1e2eb..d85da440 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -13,9 +13,10 @@ use super::{ }; pub(crate) use super::unix_common::{ - create_remote_private_dir, create_remote_ssh_config_dir, create_remote_ssh_config_file, - remote_bridge_endpoint_path, remote_private_temp_base, remote_reattach_argument, - remote_reattach_program, remote_ssh_config_paths, + configure_status_command, create_remote_private_dir, create_remote_ssh_config_dir, + create_remote_ssh_config_file, hostname, local_datetime, remote_bridge_endpoint_path, + remote_private_temp_base, remote_reattach_argument, remote_reattach_program, + remote_ssh_config_paths, status_commands_supported, StatusCommandGuard, }; const PROC_PGRP_ONLY: u32 = 2; diff --git a/src/platform/unix_common.rs b/src/platform/unix_common.rs index 754e2fc2..3004e6da 100644 --- a/src/platform/unix_common.rs +++ b/src/platform/unix_common.rs @@ -119,6 +119,90 @@ fn fits_unix_socket_path(path: &Path) -> bool { path.as_os_str().as_bytes().len() <= 103 } +/// The machine's node name, as shown by tmux's `#h`. +pub(crate) fn hostname() -> Option<String> { + let mut buffer = [0_u8; 256]; + let result = + unsafe { libc::gethostname(buffer.as_mut_ptr().cast::<libc::c_char>(), buffer.len()) }; + if result != 0 { + return None; + } + let end = buffer + .iter() + .position(|&byte| byte == 0) + .unwrap_or(buffer.len()); + let name = String::from_utf8_lossy(&buffer[..end]).into_owned(); + (!name.is_empty()).then_some(name) +} + +pub(crate) fn local_datetime() -> Option<time::PrimitiveDateTime> { + let mut timestamp: libc::time_t = 0; + if unsafe { libc::time(&mut timestamp) } == -1 { + return None; + } + let mut local: libc::tm = unsafe { std::mem::zeroed() }; + if unsafe { libc::localtime_r(×tamp, &mut local) }.is_null() { + return None; + } + datetime_from_tm(&local) +} + +pub(crate) fn status_commands_supported() -> bool { + true +} + +pub(crate) fn configure_status_command(process: &mut std::process::Command) { + use std::os::unix::process::CommandExt; + + process.process_group(0); +} + +pub(crate) struct StatusCommandGuard { + process_group_id: Option<i32>, +} + +impl StatusCommandGuard { + pub(crate) fn new(child: &tokio::process::Child) -> std::io::Result<Self> { + let process_id = child + .id() + .ok_or_else(|| std::io::Error::other("status command has no process id"))?; + let process_group_id = i32::try_from(process_id) + .map_err(|_| std::io::Error::other("status command process id exceeds i32"))?; + Ok(Self { + process_group_id: Some(process_group_id), + }) + } +} + +impl Drop for StatusCommandGuard { + fn drop(&mut self) { + if let Some(process_group_id) = self.process_group_id.take() { + // The command was spawned as this process group's leader. Killing the + // group also cleans up background descendants on completion/cancellation. + unsafe { + libc::kill(-process_group_id, libc::SIGKILL); + } + } + } +} + +fn datetime_from_tm(value: &libc::tm) -> Option<time::PrimitiveDateTime> { + let month = time::Month::try_from(u8::try_from(value.tm_mon + 1).ok()?).ok()?; + let date = time::Date::from_calendar_date( + value.tm_year + 1900, + month, + u8::try_from(value.tm_mday).ok()?, + ) + .ok()?; + let time = time::Time::from_hms( + u8::try_from(value.tm_hour).ok()?, + u8::try_from(value.tm_min).ok()?, + u8::try_from(value.tm_sec).ok()?, + ) + .ok()?; + Some(time::PrimitiveDateTime::new(date, time)) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index a5b78bbe..aef528e5 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -33,13 +33,16 @@ use windows_sys::{ Diagnostics::{ Debug::ReadProcessMemory, ToolHelp::{ - CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, - TH32CS_SNAPPROCESS, + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, Thread32First, + Thread32Next, PROCESSENTRY32W, TH32CS_SNAPPROCESS, TH32CS_SNAPTHREAD, + THREADENTRY32, }, }, JobObjects::{ - IsProcessInJob, JobObjectExtendedLimitInformation, QueryInformationJobObject, - JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, + AssignProcessToJobObject, CreateJobObjectW, IsProcessInJob, + JobObjectExtendedLimitInformation, QueryInformationJobObject, + SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, }, Memory::{ GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock, VirtualQueryEx, GMEM_MOVEABLE, @@ -47,10 +50,11 @@ use windows_sys::{ }, Ole::{CF_DIB, CF_DIBV5, CF_UNICODETEXT}, Threading::{ - GetCurrentProcess, GetExitCodeProcess, GetProcessTimes, OpenProcess, - QueryFullProcessImageNameW, TerminateProcess, CREATE_NO_WINDOW, DETACHED_PROCESS, - PROCESS_BASIC_INFORMATION, PROCESS_QUERY_INFORMATION, - PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_READ, + GetCurrentProcess, GetExitCodeProcess, GetProcessTimes, OpenProcess, OpenThread, + QueryFullProcessImageNameW, ResumeThread, TerminateProcess, CREATE_NO_WINDOW, + CREATE_SUSPENDED, DETACHED_PROCESS, PROCESS_BASIC_INFORMATION, + PROCESS_QUERY_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_READ, + THREAD_SUSPEND_RESUME, }, }, UI::{ @@ -263,6 +267,38 @@ pub(crate) fn should_draw_host_cursor_by_default() -> bool { true } +/// The machine's node name, as shown by tmux's `#h`. +pub(crate) fn hostname() -> Option<String> { + std::env::var("COMPUTERNAME") + .ok() + .filter(|name| !name.is_empty()) +} + +pub(crate) fn local_datetime() -> Option<time::PrimitiveDateTime> { + let mut timestamp: libc::time_t = 0; + if unsafe { libc::time(&mut timestamp) } == -1 { + return None; + } + let mut local: libc::tm = unsafe { std::mem::zeroed() }; + if unsafe { libc::localtime_s(&mut local, ×tamp) } != 0 { + return None; + } + let month = time::Month::try_from(u8::try_from(local.tm_mon + 1).ok()?).ok()?; + let date = time::Date::from_calendar_date( + local.tm_year + 1900, + month, + u8::try_from(local.tm_mday).ok()?, + ) + .ok()?; + let time = time::Time::from_hms( + u8::try_from(local.tm_hour).ok()?, + u8::try_from(local.tm_min).ok()?, + u8::try_from(local.tm_sec).ok()?, + ) + .ok()?; + Some(time::PrimitiveDateTime::new(date, time)) +} + #[derive(Debug, Clone, PartialEq, Eq)] struct WindowsProcessEntry { pid: u32, @@ -394,6 +430,137 @@ pub(crate) fn detached_custom_command_process_platform(command: &str) -> std::pr detached_custom_command_process_with_comspec(command, std::env::var_os("ComSpec")) } +pub(crate) fn status_commands_supported() -> bool { + true +} + +pub(crate) fn configure_status_command(process: &mut std::process::Command) { + use std::os::windows::process::CommandExt; + + // The process must not run before it is assigned to the kill-on-close job. + process.creation_flags(CREATE_NO_WINDOW | CREATE_SUSPENDED); +} + +pub(crate) struct StatusCommandGuard { + job: usize, +} + +impl StatusCommandGuard { + pub(crate) fn new(child: &tokio::process::Child) -> std::io::Result<Self> { + let job = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) }; + if job.is_null() { + return Err(std::io::Error::last_os_error()); + } + + let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { std::mem::zeroed() }; + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + let limits_size = match u32::try_from(size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>()) { + Ok(size) => size, + Err(_) => { + unsafe { + CloseHandle(job); + } + return Err(std::io::Error::other("job limits size exceeds u32")); + } + }; + if unsafe { + SetInformationJobObject( + job, + JobObjectExtendedLimitInformation, + std::ptr::from_ref(&limits).cast(), + limits_size, + ) + } == 0 + { + let error = std::io::Error::last_os_error(); + unsafe { + CloseHandle(job); + } + return Err(error); + } + + let Some(process) = child.raw_handle() else { + unsafe { + CloseHandle(job); + } + return Err(std::io::Error::other( + "status command has no process handle", + )); + }; + if unsafe { AssignProcessToJobObject(job, process.cast()) } == 0 { + let error = std::io::Error::last_os_error(); + unsafe { + CloseHandle(job); + } + return Err(error); + } + if let Err(error) = resume_suspended_process(child.id()) { + unsafe { + CloseHandle(job); + } + return Err(error); + } + + Ok(Self { job: job as usize }) + } +} + +fn resume_suspended_process(process_id: Option<u32>) -> std::io::Result<()> { + let process_id = + process_id.ok_or_else(|| std::io::Error::other("status command has no process id"))?; + let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) }; + if snapshot == INVALID_HANDLE_VALUE { + return Err(std::io::Error::last_os_error()); + } + + let result = (|| { + let mut entry: THREADENTRY32 = unsafe { std::mem::zeroed() }; + entry.dwSize = u32::try_from(size_of::<THREADENTRY32>()) + .map_err(|_| std::io::Error::other("thread entry size exceeds u32"))?; + if unsafe { Thread32First(snapshot, &mut entry) } == 0 { + return Err(std::io::Error::last_os_error()); + } + + loop { + if entry.th32OwnerProcessID == process_id { + let thread = unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, entry.th32ThreadID) }; + if thread.is_null() { + return Err(std::io::Error::last_os_error()); + } + let resume_result = unsafe { ResumeThread(thread) }; + let resume_error = (resume_result == u32::MAX).then(std::io::Error::last_os_error); + unsafe { + CloseHandle(thread); + } + if let Some(error) = resume_error { + return Err(error); + } + return Ok(()); + } + if unsafe { Thread32Next(snapshot, &mut entry) } == 0 { + return Err(std::io::Error::other( + "status command primary thread was not found", + )); + } + } + })(); + + unsafe { + CloseHandle(snapshot); + } + result +} + +impl Drop for StatusCommandGuard { + fn drop(&mut self) { + // KILL_ON_JOB_CLOSE terminates the shell and every descendant still in + // the job, including on task cancellation and config reload. + unsafe { + CloseHandle(self.job as HANDLE); + } + } +} + fn detached_custom_command_process_with_comspec( command: &str, comspec: Option<std::ffi::OsString>, |
