aboutsummaryrefslogtreecommitdiff
path: root/src/platform
diff options
context:
space:
mode:
Diffstat (limited to 'src/platform')
-rw-r--r--src/platform/fallback.rs22
-rw-r--r--src/platform/linux.rs7
-rw-r--r--src/platform/macos.rs7
-rw-r--r--src/platform/unix_common.rs84
-rw-r--r--src/platform/windows.rs183
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(&timestamp, &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, &timestamp) } != 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>,