From d829292445e871ca9fa607f90d495a3c199b7117 Mon Sep 17 00:00:00 2001 From: Nikola Lukovic Date: Sat, 11 Jul 2026 16:34:22 +0200 Subject: [PATCH 1/6] uucore: compile signals and process modules on Windows with emulated signal delivery --- src/uucore/Cargo.toml | 6 +- src/uucore/src/lib/features.rs | 7 +- src/uucore/src/lib/features/process/mod.rs | 45 ++ .../features/{process.rs => process/unix.rs} | 22 +- .../src/lib/features/process/windows.rs | 426 ++++++++++++++++++ src/uucore/src/lib/features/signals.rs | 11 + src/uucore/src/lib/lib.rs | 7 +- 7 files changed, 499 insertions(+), 25 deletions(-) create mode 100644 src/uucore/src/lib/features/process/mod.rs rename src/uucore/src/lib/features/{process.rs => process/unix.rs} (89%) create mode 100644 src/uucore/src/lib/features/process/windows.rs diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index f797b5dc1e9..fd6eb801c34 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -122,12 +122,16 @@ wild = "2.2.1" winapi-util = { workspace = true, optional = true } windows-sys = { workspace = true, optional = true, default-features = false, features = [ "Wdk_System_SystemInformation", + "Win32_Security", "Win32_Storage_FileSystem", "Win32_Foundation", + "Win32_System_Console", "Win32_System_IO", "Win32_System_Ioctl", + "Win32_System_JobObjects", "Win32_System_RemoteDesktop", "Win32_System_SystemInformation", + "Win32_System_Threading", "Win32_System_WindowsProgramming", ] } @@ -179,7 +183,7 @@ parser-size = ["parser-num", "procfs"] parser-glob = ["glob"] parser = ["parser-num", "parser-size", "parser-glob"] pipes = ["fs"] -process = ["libc"] +process = ["libc", "windows-sys"] proc-info = ["tty", "walkdir"] quoting-style = ["i18n-common"] ranges = [] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index e0e99aee6b0..be5cbad7d23 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -73,7 +73,7 @@ pub mod perms; pub mod pipes; #[cfg(all(target_os = "linux", feature = "proc-info"))] pub mod proc_info; -#[cfg(all(unix, feature = "process"))] +#[cfg(all(any(unix, windows), feature = "process"))] pub mod process; #[cfg(all(unix, feature = "safe-copy"))] pub mod safe_copy; @@ -88,7 +88,10 @@ pub mod fsxattr; pub mod hardware; #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] pub mod selinux; -#[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))] +#[cfg(all( + any(windows, all(unix, not(target_os = "fuchsia"))), + feature = "signals" +))] pub mod signals; #[cfg(all(feature = "smack", target_os = "linux"))] pub mod smack; diff --git a/src/uucore/src/lib/features/process/mod.rs b/src/uucore/src/lib/features/process/mod.rs new file mode 100644 index 00000000000..75d0b00c4b6 --- /dev/null +++ b/src/uucore/src/lib/features/process/mod.rs @@ -0,0 +1,45 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Process helpers, most notably [`ChildExt`], which extends +//! [`std::process::Child`] with signal delivery and a wait-with-timeout. +//! +//! Unix delivers real POSIX signals; Windows emulates them (see the `windows` +//! submodule). Both provide identical trait signatures. + +use std::io; +use std::process::ExitStatus; +use std::sync::atomic::AtomicBool; +use std::time::Duration; + +/// Missing methods for Child objects +pub trait ChildExt { + /// Send a signal to a Child process. + /// + /// Caller beware: if the process already exited then you may accidentally + /// send the signal to an unrelated process that recycled the PID. + fn send_signal(&mut self, signal: usize) -> io::Result<()>; + + /// Send a signal to a process group. + fn send_signal_group(&mut self, signal: usize) -> io::Result<()>; + + /// Wait for a process to finish or return after the specified duration. + /// A `timeout` of zero disables the timeout. + fn wait_or_timeout( + &mut self, + timeout: Duration, + signaled: Option<&AtomicBool>, + ) -> io::Result>; +} + +#[cfg(unix)] +mod unix; +#[cfg(unix)] +pub use unix::*; + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub use windows::*; diff --git a/src/uucore/src/lib/features/process.rs b/src/uucore/src/lib/features/process/unix.rs similarity index 89% rename from src/uucore/src/lib/features/process.rs rename to src/uucore/src/lib/features/process/unix.rs index 06274eb59f9..b13ac684f2c 100644 --- a/src/uucore/src/lib/features/process.rs +++ b/src/uucore/src/lib/features/process/unix.rs @@ -20,6 +20,8 @@ use std::sync::atomic::AtomicBool; use std::thread; use std::time::{Duration, Instant}; +use super::ChildExt; + /// `geteuid()` returns the effective user ID of the calling process. pub fn geteuid() -> uid_t { nix::unistd::geteuid().as_raw() @@ -75,26 +77,6 @@ pub fn getsid(pid: i32) -> Result { nix::unistd::getsid(pid).map(Pid::as_raw) } -/// Missing methods for Child objects -pub trait ChildExt { - /// Send a signal to a Child process. - /// - /// Caller beware: if the process already exited then you may accidentally - /// send the signal to an unrelated process that recycled the PID. - fn send_signal(&mut self, signal: usize) -> io::Result<()>; - - /// Send a signal to a process group. - fn send_signal_group(&mut self, signal: usize) -> io::Result<()>; - - /// Wait for a process to finish or return after the specified duration. - /// A `timeout` of zero disables the timeout. - fn wait_or_timeout( - &mut self, - timeout: Duration, - signaled: Option<&AtomicBool>, - ) -> io::Result>; -} - impl ChildExt for Child { fn send_signal(&mut self, signal: usize) -> io::Result<()> { let pid = Pid::from_raw(self.id() as pid_t); diff --git a/src/uucore/src/lib/features/process/windows.rs b/src/uucore/src/lib/features/process/windows.rs new file mode 100644 index 00000000000..a8185a1e7e5 --- /dev/null +++ b/src/uucore/src/lib/features/process/windows.rs @@ -0,0 +1,426 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore (win-api) WAITABLE Waitable HIRES +// spell-checker:ignore (signals) CHLD TSTP TTIN TTOU WINCH + +//! Windows emulation of POSIX signal delivery for child processes. +//! +//! Windows has no signals, so this module emulates the POSIX *default +//! dispositions* using native primitives: +//! +//! - Signal numbers follow the Linux layout (the same table +//! `uucore::signals::ALL_SIGNALS` uses on Windows), so `-s HUP`, `-s 9`, +//! etc. keep meaning what cross-platform scripts expect. +//! - "Terminate" signals force-exit the target with exit code `128 + n`, +//! which is what observers of the exit status see on unix when a process +//! dies to signal `n`. +//! - `INT`/`QUIT` can be delivered to a console process group as a +//! `CTRL_BREAK_EVENT`: targetable, catchable by the child, and fatal by +//! default — the closest analog to a catchable SIGINT. (`CTRL_C_EVENT` +//! cannot target a specific group; it would be broadcast to the whole +//! console, including the sender.) +//! - Discard-by-default signals (`CHLD`, `CONT`, `URG`, `WINCH`) and the +//! stop family (`STOP`, `TSTP`, `TTIN`, `TTOU`), which cannot be emulated +//! with documented APIs, are accepted as no-ops. +//! +//! [`Job`] provides process-tree termination (the analog of signalling a +//! process group), and [`enable_ctrl_forwarding`] + [`last_ctrl_signal`] +//! translate console control events (Ctrl-C, Ctrl-Break, console close) into +//! POSIX signal numbers so callers can implement signal forwarding. + +use std::io; +use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle}; +use std::process::{Child, Command, ExitStatus}; +use std::ptr; +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicPtr, Ordering}; +use std::time::Duration; + +use windows_sys::Win32::Foundation::{ + FALSE, HANDLE, TRUE, WAIT_OBJECT_0, WAIT_TIMEOUT, +}; +use windows_sys::Win32::System::Console::{ + CTRL_BREAK_EVENT, CTRL_C_EVENT, CTRL_CLOSE_EVENT, GenerateConsoleCtrlEvent, + SetConsoleCtrlHandler, +}; +use windows_sys::Win32::System::JobObjects::{ + AssignProcessToJobObject, CreateJobObjectW, TerminateJobObject, +}; +use windows_sys::Win32::System::Threading::{ + CREATE_NEW_PROCESS_GROUP, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, CreateEventW, + CreateWaitableTimerExW, INFINITE, SetEvent, SetWaitableTimer, TIMER_ALL_ACCESS, + TerminateProcess, WaitForMultipleObjects, WaitForSingleObject, +}; +use windows_sys::core::BOOL; + +use super::ChildExt; + +// POSIX (Linux-layout) signal numbers, matching the Windows `ALL_SIGNALS` +// table in `uucore::signals`. Kept local so the `process` feature does not +// depend on the `signals` feature. +const SIGNAL_HUP: i32 = 1; +const SIGNAL_INT: i32 = 2; +const SIGNAL_QUIT: i32 = 3; + +/// What delivering a given POSIX signal number means on Windows. +enum Disposition { + /// Signal 0: existence check only. + Probe, + /// Discarded-by-default and stop signals: accepted, nothing to do. + Ignore, + /// `INT`/`QUIT`: deliverable to a console process group as CTRL_BREAK. + Interrupt, + /// Everything else: forced termination with exit code `128 + n`. + Terminate, +} + +fn disposition(signal: usize) -> io::Result { + match signal { + 0 => Ok(Disposition::Probe), + 2 | 3 => Ok(Disposition::Interrupt), + // CHLD, CONT, URG and WINCH are discarded by default on POSIX; the + // stop family (STOP, TSTP, TTIN, TTOU) cannot be emulated with + // documented APIs. All are accepted as no-ops. + 17..=23 | 28 => Ok(Disposition::Ignore), + 1..=31 => Ok(Disposition::Terminate), + _ => Err(io::ErrorKind::InvalidInput.into()), + } +} + +/// Terminate the process behind `handle` so that its exit status becomes +/// `128 + signal`, emulating "killed by signal" for exit-code observers. +fn terminate_with_signal(handle: HANDLE, signal: usize) -> io::Result<()> { + // SAFETY: the handle is a valid process handle; terminating an + // already-exited process fails cleanly with an OS error. + if unsafe { TerminateProcess(handle, (128 + signal) as u32) } == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +/// Deliver `signal` (POSIX numbering) to the child process only. +/// +/// A console control event cannot target a single process, so `INT`/`QUIT` +/// fall back to their POSIX default disposition here: termination. Callers +/// that want a catchable interrupt must target a process group via +/// [`send_signal_to_console_group`]. +pub fn send_signal_to_process(child: &Child, signal: usize) -> io::Result<()> { + let handle = child.as_raw_handle() as HANDLE; + match disposition(signal)? { + Disposition::Probe => { + // SAFETY: valid process handle; a zero timeout makes this a poll. + match unsafe { WaitForSingleObject(handle, 0) } { + WAIT_TIMEOUT => Ok(()), + // The process has exited: the POSIX analog is ESRCH. + WAIT_OBJECT_0 => Err(io::ErrorKind::NotFound.into()), + _ => Err(io::Error::last_os_error()), + } + } + Disposition::Ignore => Ok(()), + Disposition::Interrupt | Disposition::Terminate => { + terminate_with_signal(handle, signal) + } + } +} + +/// Deliver `signal` (POSIX numbering) to the console process group led by +/// `pid` (the process must have been created with `CREATE_NEW_PROCESS_GROUP`, +/// e.g. via [`configure_process_group`]). +/// +/// Only `INT`/`QUIT` are deliverable this way (as `CTRL_BREAK_EVENT`); +/// terminating a whole group requires a [`Job`]. +pub fn send_signal_to_console_group(pid: u32, signal: usize) -> io::Result<()> { + match disposition(signal)? { + Disposition::Probe | Disposition::Ignore => Ok(()), + Disposition::Interrupt => { + // SAFETY: no pointers involved; fails cleanly without a console. + if unsafe { GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid) } == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) + } + Disposition::Terminate => Err(io::ErrorKind::Unsupported.into()), + } +} + +/// Deliver `signal` (POSIX numbering) to the child's whole process tree: +/// terminating signals (including `INT`/`QUIT`, which cannot reach a whole +/// tree as console events) terminate the job with exit code `128 + n`, +/// falling back to the direct child when `job` is `None` or termination via +/// the job fails; probe and ignored signals behave as in +/// [`send_signal_to_process`]. +pub fn send_signal_to_tree(child: &Child, job: Option<&Job>, signal: usize) -> io::Result<()> { + match disposition(signal)? { + Disposition::Probe | Disposition::Ignore => send_signal_to_process(child, signal), + Disposition::Interrupt | Disposition::Terminate => { + if let Some(job) = job { + if job.terminate((128 + signal) as u32).is_ok() { + return Ok(()); + } + } + terminate_with_signal(child.as_raw_handle() as HANDLE, signal) + } + } +} + +/// Make `cmd` spawn its child as the leader of a new console process group, +/// so that `CTRL_BREAK_EVENT` can be targeted at exactly that child's group +/// and the console's own Ctrl-C no longer reaches the child directly (the +/// analog of `setpgid(0, 0)` isolation on unix). +/// +/// Note: this sets the command's creation flags, overwriting any flags set +/// earlier via `CommandExt::creation_flags`. +pub fn configure_process_group(cmd: &mut Command) { + use std::os::windows::process::CommandExt; + cmd.creation_flags(CREATE_NEW_PROCESS_GROUP); +} + +/// An anonymous Job Object: the Windows primitive for operating on a whole +/// process tree, used here as the analog of signalling a process group. +/// +/// No limits (in particular no `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`) are set: +/// the job is only a handle for [`Job::terminate`], and processes that +/// outlive the job's owner deliberately keep running, just like a process +/// group outlives its creator on unix. +pub struct Job(OwnedHandle); + +impl Job { + /// Create a new anonymous job object. + pub fn new() -> io::Result { + // SAFETY: null attributes and name are documented as valid. + let raw = unsafe { CreateJobObjectW(ptr::null(), ptr::null()) }; + if raw.is_null() { + return Err(io::Error::last_os_error()); + } + // SAFETY: `raw` is a valid handle exclusively owned by us. + Ok(Self(unsafe { OwnedHandle::from_raw_handle(raw) })) + } + + /// Assign `child` (and, transitively, every process it spawns from then + /// on) to this job. + /// + /// This can fail when nested jobs are unsupported (pre-Windows 8) and the + /// current process already runs inside a job; callers should degrade to + /// per-process operations in that case. + pub fn assign(&self, child: &Child) -> io::Result<()> { + // SAFETY: both handles are valid for the duration of the call. + if unsafe { + AssignProcessToJobObject( + self.0.as_raw_handle() as HANDLE, + child.as_raw_handle() as HANDLE, + ) + } == 0 + { + return Err(io::Error::last_os_error()); + } + Ok(()) + } + + /// Terminate every process in the job with the given exit code + /// (pass `128 + signal` to emulate death by signal). + pub fn terminate(&self, exit_code: u32) -> io::Result<()> { + // SAFETY: the job handle is valid for the duration of the call. + if unsafe { TerminateJobObject(self.0.as_raw_handle() as HANDLE, exit_code) } == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) + } +} + +/// Manual-reset event signalled by the console control handler to wake +/// [`ChildExt::wait_or_timeout`]. Null until [`enable_ctrl_forwarding`] runs. +/// Intentionally lives for the rest of the process; the OS reclaims it. +static WAKE_EVENT: AtomicPtr = AtomicPtr::new(ptr::null_mut()); +/// POSIX signal number of the last console control event received (0 = none). +static LAST_CTRL_SIGNAL: AtomicI32 = AtomicI32::new(0); + +/// The console control handler runs on a system-spawned thread: it must only +/// touch atomics and signal the pre-created event (no allocation, no locks). +/// +/// # Safety +/// +/// Only registered via `SetConsoleCtrlHandler` and called by the system with +/// a valid `ctrl_type`; touches nothing but atomics and a live event handle. +unsafe extern "system" fn console_ctrl_handler(ctrl_type: u32) -> BOOL { + let signal = match ctrl_type { + CTRL_C_EVENT => SIGNAL_INT, + CTRL_BREAK_EVENT => SIGNAL_QUIT, + // The console window is going away; the analog of losing the + // controlling terminal. The system still terminates us after a grace + // period, so the main thread must react promptly. + CTRL_CLOSE_EVENT => SIGNAL_HUP, + // Logoff/shutdown notifications are only delivered to services; + // leave them to the default handler. + _ => return FALSE, + }; + LAST_CTRL_SIGNAL.store(signal, Ordering::Release); + let event = WAKE_EVENT.load(Ordering::Acquire); + if !event.is_null() { + // SAFETY: the event handle is created before the handler is + // registered and is never closed. + unsafe { SetEvent(event) }; + } + TRUE +} + +/// Install a console control handler that records Ctrl-C, Ctrl-Break and +/// console-close events as POSIX signal numbers (INT, QUIT, HUP) instead of +/// letting them terminate this process, and wakes any pending +/// [`ChildExt::wait_or_timeout`] call that was given a `signaled` flag. +/// +/// Use [`last_ctrl_signal`] to read the last event received. Idempotent. +pub fn enable_ctrl_forwarding() -> io::Result<()> { + if !WAKE_EVENT.load(Ordering::Acquire).is_null() { + return Ok(()); + } + // Manual-reset so a wakeup latched before a wait starts is never lost. + // SAFETY: null attributes and name are documented as valid. + let event = unsafe { CreateEventW(ptr::null(), TRUE, FALSE, ptr::null()) }; + if event.is_null() { + return Err(io::Error::last_os_error()); + } + WAKE_EVENT.store(event, Ordering::Release); + // SAFETY: the handler only touches atomics and a live event handle. + if unsafe { SetConsoleCtrlHandler(Some(console_ctrl_handler), TRUE) } == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +/// The POSIX signal number corresponding to the last console control event +/// received since [`enable_ctrl_forwarding`], if any. +pub fn last_ctrl_signal() -> Option { + match LAST_CTRL_SIGNAL.load(Ordering::Acquire) { + 0 => None, + signal => Some(signal as usize), + } +} + +/// Create a one-shot waitable timer that fires after `timeout`. +/// +/// Uses a high-resolution timer (100 ns due-time granularity, not coalesced +/// to the ~15.6 ms scheduler tick) when the OS supports it (Windows 10 1803+), +/// falling back to a standard waitable timer otherwise. +fn create_relative_timer(timeout: Duration) -> io::Result { + // SAFETY: null attributes and name are documented as valid. + let mut raw = unsafe { + CreateWaitableTimerExW( + ptr::null(), + ptr::null(), + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, + TIMER_ALL_ACCESS, + ) + }; + if raw.is_null() { + // Pre-1803 systems reject the high-resolution flag. + // SAFETY: as above. + raw = unsafe { CreateWaitableTimerExW(ptr::null(), ptr::null(), 0, TIMER_ALL_ACCESS) }; + } + if raw.is_null() { + return Err(io::Error::last_os_error()); + } + // SAFETY: `raw` is a valid handle exclusively owned by us. + let timer = unsafe { OwnedHandle::from_raw_handle(raw) }; + + // A negative due time is relative, in 100 ns units. Round up so a + // sub-tick duration never fires early, and clamp huge durations. + let ticks = timeout.as_nanos().div_ceil(100).min(i64::MAX as u128) as i64; + let due_time = -ticks; + // SAFETY: the timer handle is valid; no completion routine is used. + if unsafe { + SetWaitableTimer( + timer.as_raw_handle() as HANDLE, + &raw const due_time, + 0, + None, + ptr::null(), + FALSE, + ) + } == 0 + { + return Err(io::Error::last_os_error()); + } + Ok(timer) +} + +impl ChildExt for Child { + fn send_signal(&mut self, signal: usize) -> io::Result<()> { + send_signal_to_process(self, signal) + } + + fn send_signal_group(&mut self, signal: usize) -> io::Result<()> { + // Unlike unix (which signals the caller's own process group), this + // targets the child's console process group: on Windows only a group + // the child leads can be addressed at all. + let pid = self.id(); + send_signal_to_console_group(pid, signal) + } + + fn wait_or_timeout( + &mut self, + timeout: Duration, + signaled: Option<&AtomicBool>, + ) -> io::Result> { + // The unix implementation drops stdin so the child sees EOF; match it. + drop(self.stdin.take()); + + // A single blocking wait over up to three handles. Ordering matters: + // on simultaneous completion `WaitForMultipleObjects` reports the + // lowest index, so child-exit wins over a console event, which wins + // over timer expiry — matching the unix implementation's races. + let mut handles: [HANDLE; 3] = [ptr::null_mut(); 3]; + let mut count: u32 = 0; + + handles[count as usize] = self.as_raw_handle() as HANDLE; + count += 1; + + let wake_index = if signaled.is_some() { + let event = WAKE_EVENT.load(Ordering::Acquire); + if event.is_null() { + None + } else { + handles[count as usize] = event; + count += 1; + Some(count - 1) + } + } else { + // The caller wants this wait to ignore console events (e.g. the + // kill-after grace period), so the wake event is left out. + None + }; + + // A timeout of zero disables the timeout: no timer handle, so the + // wait below blocks until the child exits (or a console event fires). + let _timer: Option = if timeout.is_zero() { + None + } else { + let timer = create_relative_timer(timeout)?; + handles[count as usize] = timer.as_raw_handle() as HANDLE; + count += 1; + Some(timer) + }; + + // SAFETY: `handles[..count]` are valid, open handles for the whole + // call; INFINITE is safe because at least the process handle (and + // usually the timer) will eventually be signalled. + let result = unsafe { WaitForMultipleObjects(count, handles.as_ptr(), FALSE, INFINITE) }; + let index = result.wrapping_sub(WAIT_OBJECT_0); + if index >= count { + // WAIT_FAILED (or an impossible WAIT_ABANDONED: no mutexes here). + return Err(io::Error::last_os_error()); + } + if index == 0 { + // The child exited; this reaps it without blocking. + return self.wait().map(Some); + } + if Some(index) == wake_index { + if let Some(flag) = signaled { + flag.store(true, Ordering::Relaxed); + } + } + // Console event or timer expiry: the child is still running. + Ok(None) + } +} diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 0bde7d9aa33..1e049bb3ce1 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -392,6 +392,17 @@ pub static ALL_SIGNALS: [&str; 32] = [ "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "PWR", "USR1", "USR2", ]; +// Windows has no POSIX signals, but utilities that emulate signal delivery +// (e.g. `timeout`) accept the POSIX names/numbers so cross-platform scripts +// keep working. The Linux layout is used so that synthesized `128 + n` exit +// codes (130, 137, 143, ...) match what such scripts expect. +#[cfg(windows)] +pub static ALL_SIGNALS: [&str; 32] = [ + "EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV", + "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU", + "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "POLL", "PWR", "SYS", +]; + /// Returns the signal number for a given signal name or value. pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option { let signal_name_upcase = signal_name_or_value.to_uppercase(); diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 4dfffed2995..657205b9edb 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -100,13 +100,16 @@ pub use crate::features::perms; any(feature = "pipes", feature = "buf-copy") ))] pub use crate::features::pipes; -#[cfg(all(unix, feature = "process"))] +#[cfg(all(any(unix, windows), feature = "process"))] pub use crate::features::process; #[cfg(all(unix, feature = "safe-copy"))] pub use crate::features::safe_copy; #[cfg(all(unix, not(target_os = "redox")))] pub use crate::features::safe_traversal; -#[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))] +#[cfg(all( + any(windows, all(unix, not(target_os = "fuchsia"))), + feature = "signals" +))] pub use crate::features::signals; #[cfg(all( unix, From 4cf718fd847f40138bada2bfbe2d5084804cdb37 Mon Sep 17 00:00:00 2001 From: Nikola Lukovic Date: Sat, 11 Jul 2026 16:34:31 +0200 Subject: [PATCH 2/6] timeout: enable on Windows via platform module split --- Cargo.toml | 2 +- src/uu/timeout/Cargo.toml | 2 +- src/uu/timeout/src/platform/mod.rs | 19 +++ src/uu/timeout/src/platform/unix.rs | 158 ++++++++++++++++++++++ src/uu/timeout/src/platform/windows.rs | 100 ++++++++++++++ src/uu/timeout/src/timeout.rs | 176 +++---------------------- 6 files changed, 299 insertions(+), 158 deletions(-) create mode 100644 src/uu/timeout/src/platform/mod.rs create mode 100644 src/uu/timeout/src/platform/unix.rs create mode 100644 src/uu/timeout/src/platform/windows.rs diff --git a/Cargo.toml b/Cargo.toml index 8f2bede1353..1774865046e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -298,7 +298,7 @@ feat_os_unix_musl = [ "feat_require_unix_utmpx", ] # "feat_os_windows" == set of utilities which can be built/run on modern windows platforms -feat_os_windows = ["feat_Tier1", "stdbuf"] +feat_os_windows = ["feat_Tier1", "stdbuf", "timeout"] ## (secondary platforms) feature sets # "feat_os_unix_gnueabihf" == set of utilities which can be built/run on the "arm-unknown-linux-gnueabihf" target (ARMv6 Linux [hardfloat]) feat_os_unix_gnueabihf = [ diff --git a/src/uu/timeout/Cargo.toml b/src/uu/timeout/Cargo.toml index a4cd53a0de4..de6650f552b 100644 --- a/src/uu/timeout/Cargo.toml +++ b/src/uu/timeout/Cargo.toml @@ -21,11 +21,11 @@ doctest = false [dependencies] clap = { workspace = true } -libc = { workspace = true } uucore = { workspace = true, features = ["parser", "process", "signals"] } fluent = { workspace = true } [target.'cfg(unix)'.dependencies] +libc = { workspace = true } rustix = { workspace = true, features = ["process"] } [[bin]] diff --git a/src/uu/timeout/src/platform/mod.rs b/src/uu/timeout/src/platform/mod.rs new file mode 100644 index 00000000000..b8a9d81b230 --- /dev/null +++ b/src/uu/timeout/src/platform/mod.rs @@ -0,0 +1,19 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Platform-specific pieces of `timeout`: process-group / job setup, signal +//! sending and signal-forwarding state. The shared control flow in +//! `timeout.rs` only talks to the facade functions re-exported here, which +//! both submodules provide with identical signatures. + +#[cfg(unix)] +mod unix; +#[cfg(unix)] +pub(crate) use unix::*; + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub(crate) use windows::*; diff --git a/src/uu/timeout/src/platform/unix.rs b/src/uu/timeout/src/platform/unix.rs new file mode 100644 index 00000000000..13e862b4f71 --- /dev/null +++ b/src/uu/timeout/src/platform/unix.rs @@ -0,0 +1,158 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore (ToDO) sigstr setpgid sigchld getpid TTIN TTOU + +use std::os::unix::process::CommandExt; +use std::os::unix::process::ExitStatusExt; +use std::process::Child; +use std::sync::atomic; + +use rustix::process::{Pid, Signal, getpid, kill_process, setpgid}; +use uucore::process::ChildExt; +use uucore::signals::{install_signal_handler, signal_by_name_or_value}; + +/// Install SIGCHLD handler to ensure waiting for child works even if parent ignored SIGCHLD. +fn install_sigchld() { + extern "C" fn chld(_: libc::c_int) {} + let _ = install_signal_handler(Signal::as_raw(Signal::CHILD), chld); +} + +/// Install signal handlers for termination signals. +fn install_signal_handlers(term_signal: usize) { + extern "C" fn handle_signal(sig: libc::c_int) { + crate::SIGNALED.store(true, atomic::Ordering::Relaxed); + crate::RECEIVED_SIGNAL.store(sig, atomic::Ordering::Relaxed); + } + + let sigpipe_ignored = uucore::signals::sigpipe_was_ignored(); + + for sig in [ + Signal::ALARM, + Signal::INT, + Signal::QUIT, + Signal::HUP, + Signal::TERM, + Signal::PIPE, + Signal::USR1, + Signal::USR2, + ] { + if sig == Signal::PIPE && sigpipe_ignored { + continue; // Skip SIGPIPE if it was ignored by parent + } + let _ = install_signal_handler(Signal::as_raw(sig), handle_signal); + } + + if let Some(sig) = signal_from_raw(term_signal as i32) { + let _ = install_signal_handler(Signal::as_raw(sig), handle_signal); + } +} + +fn signal_from_raw(sig: i32) -> Option { + if sig <= 0 { + return None; + } + // Fast path: standard named signals (SIGHUP, SIGTERM, SIGKILL, etc.) + if let Some(s) = Signal::from_named_raw(sig) { + return Some(s); + } + // Slow path: realtime signals (SIGRTMIN..=SIGRTMAX). + #[cfg(target_os = "linux")] + { + let rtmin = libc::SIGRTMIN(); + let rtmax = libc::SIGRTMAX(); + if sig >= rtmin && sig <= rtmax { + return Some(unsafe { Signal::from_raw_unchecked(sig) }); + } + } + + None +} + +/// Configure our own process group, the child's spawn attributes and the +/// signal handlers, right before the child is spawned. +pub(crate) fn prepare(cmd_builder: &mut std::process::Command, foreground: bool, signal: usize) { + if !foreground { + let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0)); + } + + { + #[cfg(target_os = "linux")] + let death_sig = signal_from_raw(signal as i32); + let sigpipe_was_ignored = uucore::signals::sigpipe_was_ignored(); + let stdin_was_closed = uucore::signals::stdin_was_closed(); + + unsafe { + cmd_builder.pre_exec(move || { + // Reset terminal signals to default + let _ = libc::signal(Signal::as_raw(Signal::TTIN), libc::SIG_DFL); + let _ = libc::signal(Signal::as_raw(Signal::TTOU), libc::SIG_DFL); + // Preserve SIGPIPE ignore status if parent had it ignored + if sigpipe_was_ignored { + let _ = libc::signal(Signal::as_raw(Signal::PIPE), libc::SIG_IGN); + } + // If stdin was closed before Rust reopened it as /dev/null, close it in child + if stdin_was_closed { + libc::close(libc::STDIN_FILENO); + } + #[cfg(target_os = "linux")] + let _ = rustix::process::set_parent_process_death_signal(death_sig); + Ok(()) + }); + } + } + + install_sigchld(); + install_signal_handlers(signal); +} + +/// Nothing to do after spawning on unix. +pub(crate) fn post_spawn(_child: &Child, _foreground: bool) {} + +pub(crate) fn send_signal(process: &mut Child, signal: usize, foreground: bool) { + // NOTE: GNU timeout doesn't check for errors of signal. + // The subprocess might have exited just after the timeout. + let _ = process.send_signal(signal); + if signal == 0 || foreground { + return; + } + let _ = process.send_signal_group(signal); + let kill_signal = signal_by_name_or_value("KILL").unwrap(); + let continued_signal = signal_by_name_or_value("CONT").unwrap(); + if signal != kill_signal && signal != continued_signal { + let _ = process.send_signal(continued_signal); + let _ = process.send_signal_group(continued_signal); + } +} + +/// The termination signal received by timeout itself while waiting, if any. +pub(crate) fn external_signal() -> Option { + let received_sig = crate::RECEIVED_SIGNAL.load(atomic::Ordering::Relaxed); + (received_sig > 0 && received_sig != libc::SIGALRM).then_some(received_sig as usize) +} + +/// The signal the child was terminated by, if it was terminated by a signal. +pub(crate) fn status_signal(status: std::process::ExitStatus) -> Option { + status.signal() +} + +pub(crate) fn preserve_signal_info(signal: libc::c_int) -> libc::c_int { + // This is needed because timeout is expected to preserve the exit + // status of its child. It is not the case that utilities have a + // single simple exit code, that's an illusion some shells + // provide. Instead exit status is really two numbers: + // + // - An exit code if the program ran to completion + // + // - A signal number if the program was terminated by a signal + // + // The easiest way to preserve the latter seems to be to kill + // ourselves with whatever signal our child exited with, which is + // what the following is intended to accomplish. + if let Some(sig) = signal_from_raw(signal) { + let _ = kill_process(getpid(), sig); + } + signal +} diff --git a/src/uu/timeout/src/platform/windows.rs b/src/uu/timeout/src/platform/windows.rs new file mode 100644 index 00000000000..df029ff7122 --- /dev/null +++ b/src/uu/timeout/src/platform/windows.rs @@ -0,0 +1,100 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Windows implementation of `timeout`'s platform facade, built on the signal +//! emulation in [`uucore::process`] (which maps POSIX signal numbers to +//! Windows primitives). Non-foreground mode spawns the child in a new console +//! process group tracked by a Job Object, so a lethal signal kills the whole +//! tree and `INT`/`QUIT` arrive as a catchable `CTRL_BREAK_EVENT`. + +use std::process::Child; +use std::sync::OnceLock; + +use uucore::process::{ + Job, configure_process_group, enable_ctrl_forwarding, last_ctrl_signal, + send_signal_to_console_group, send_signal_to_process, send_signal_to_tree, +}; + +const SIGNAL_INT: usize = 2; +const SIGNAL_QUIT: usize = 3; + +/// The job object tracking the child's process tree (non-foreground mode +/// only; `None` also when job assignment failed and we degraded to +/// per-process signalling). +static JOB: OnceLock = OnceLock::new(); + +/// Configure the child's spawn attributes and console-event forwarding, right +/// before the child is spawned. +pub(crate) fn prepare(cmd_builder: &mut std::process::Command, foreground: bool, _signal: usize) { + // Record Ctrl-C/Ctrl-Break/console-close as forwardable signal numbers + // instead of dying to them (the unix code installs signal handlers here). + let _ = enable_ctrl_forwarding(); + + if !foreground { + // The analog of unix setpgid(0, 0): the child leads its own console + // group, so the console's Ctrl-C no longer reaches it directly and + // CTRL_BREAK can target exactly its group. + configure_process_group(cmd_builder); + } +} + +/// Put the freshly spawned child in a job so a lethal signal can terminate +/// its entire process tree. Degrades silently to per-process signalling when +/// jobs are unavailable (mirroring the ignored `setpgid` result on unix). +/// +/// A child that spawns a grandchild before the assignment completes escapes +/// the job; the window is sub-millisecond and unix has the analogous escape +/// (a child can leave the process group via setpgid). +pub(crate) fn post_spawn(child: &Child, foreground: bool) { + if foreground { + return; + } + if let Ok(job) = Job::new() { + if job.assign(child).is_ok() { + let _ = JOB.set(job); + } + } +} + +pub(crate) fn send_signal(process: &mut Child, signal: usize, foreground: bool) { + // GNU timeout ignores signal-send errors (the child may have just exited), + // hence the discarded results below. + if foreground { + if matches!(signal, SIGNAL_INT | SIGNAL_QUIT) && last_ctrl_signal() == Some(signal) { + // A foreground child shares our console group: the console already + // delivered this externally received event to it, and re-sending + // cannot be targeted without hitting ourselves. + return; + } + let _ = send_signal_to_process(process, signal); + } else if matches!(signal, SIGNAL_INT | SIGNAL_QUIT) { + // Deliverable as a catchable CTRL_BREAK to the child's group; without + // a console, fall back to termination so the signal is never lost. + if send_signal_to_console_group(process.id(), signal).is_err() { + let _ = send_signal_to_tree(process, JOB.get(), signal); + } + } else { + let _ = send_signal_to_tree(process, JOB.get(), signal); + } +} + +/// The signal number of the console event received by timeout itself while +/// waiting, if any. +pub(crate) fn external_signal() -> Option { + last_ctrl_signal() +} + +/// Windows exit statuses carry no signal information: terminated children +/// report their forced `128 + signal` value through `status.code()` instead. +pub(crate) fn status_signal(_status: std::process::ExitStatus) -> Option { + None +} + +/// No two-channel exit status exists on Windows; the exit code already +/// carries the signal information (`128 + signal`), so there is nothing to +/// preserve by re-signalling ourselves like the unix implementation does. +pub(crate) fn preserve_signal_info(signal: i32) -> i32 { + signal +} diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 6646540d86e..2d3756db898 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -5,12 +5,12 @@ // spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid sigchld getpid TTIN TTOU +mod platform; mod status; use crate::status::ExitStatus; use clap::{Arg, ArgAction, Command}; use std::io::{ErrorKind, Write}; -use std::os::unix::process::ExitStatusExt; use std::process::{self, Child, Stdio}; use std::sync::atomic::{self, AtomicBool}; use std::time::Duration; @@ -18,12 +18,8 @@ use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError}; use uucore::parser::parse_time; use uucore::process::ChildExt; -use uucore::signals::install_signal_handler; use uucore::translate; -use rustix::process::{Pid, Signal, getpid, kill_process, setpgid}; -#[cfg(unix)] -use std::os::unix::process::CommandExt; use uucore::{ format_usage, signals::{signal_by_name_or_value, signal_list_name_by_value}, @@ -178,46 +174,11 @@ pub fn uu_app() -> Command { .after_help(translate!("timeout-after-help")) } -/// Install SIGCHLD handler to ensure waiting for child works even if parent ignored SIGCHLD. -fn install_sigchld() { - extern "C" fn chld(_: libc::c_int) {} - let _ = install_signal_handler(Signal::as_raw(Signal::CHILD), chld); -} - /// We should terminate child process when receiving termination signals. -static SIGNALED: AtomicBool = AtomicBool::new(false); +pub(crate) static SIGNALED: AtomicBool = AtomicBool::new(false); /// Track which signal was received (0 = none/timeout expired naturally). -static RECEIVED_SIGNAL: atomic::AtomicI32 = atomic::AtomicI32::new(0); - -/// Install signal handlers for termination signals. -fn install_signal_handlers(term_signal: usize) { - extern "C" fn handle_signal(sig: libc::c_int) { - SIGNALED.store(true, atomic::Ordering::Relaxed); - RECEIVED_SIGNAL.store(sig, atomic::Ordering::Relaxed); - } - - let sigpipe_ignored = uucore::signals::sigpipe_was_ignored(); - - for sig in [ - Signal::ALARM, - Signal::INT, - Signal::QUIT, - Signal::HUP, - Signal::TERM, - Signal::PIPE, - Signal::USR1, - Signal::USR2, - ] { - if sig == Signal::PIPE && sigpipe_ignored { - continue; // Skip SIGPIPE if it was ignored by parent - } - let _ = install_signal_handler(Signal::as_raw(sig), handle_signal); - } - - if let Some(sig) = signal_from_raw(term_signal as i32) { - let _ = install_signal_handler(Signal::as_raw(sig), handle_signal); - } -} +#[cfg(unix)] +pub(crate) static RECEIVED_SIGNAL: atomic::AtomicI32 = atomic::AtomicI32::new(0); /// Report that a signal is being sent if the verbose flag is set. fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) { @@ -237,43 +198,6 @@ fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) { } } -fn signal_from_raw(sig: i32) -> Option { - if sig <= 0 { - return None; - } - // Fast path: standard named signals (SIGHUP, SIGTERM, SIGKILL, etc.) - if let Some(s) = Signal::from_named_raw(sig) { - return Some(s); - } - // Slow path: realtime signals (SIGRTMIN..=SIGRTMAX). - #[cfg(target_os = "linux")] - { - let rtmin = libc::SIGRTMIN(); - let rtmax = libc::SIGRTMAX(); - if sig >= rtmin && sig <= rtmax { - return Some(unsafe { Signal::from_raw_unchecked(sig) }); - } - } - - None -} - -fn send_signal(process: &mut Child, signal: usize, foreground: bool) { - // NOTE: GNU timeout doesn't check for errors of signal. - // The subprocess might have exited just after the timeout. - let _ = process.send_signal(signal); - if signal == 0 || foreground { - return; - } - let _ = process.send_signal_group(signal); - let kill_signal = signal_by_name_or_value("KILL").unwrap(); - let continued_signal = signal_by_name_or_value("CONT").unwrap(); - if signal != kill_signal && signal != continued_signal { - let _ = process.send_signal(continued_signal); - let _ = process.send_signal_group(continued_signal); - } -} - /// Wait for a child process and send a kill signal if it does not terminate. /// /// This function waits for the child `process` for the time period @@ -307,7 +231,7 @@ fn wait_or_kill_process( Ok(Some(status)) => { if preserve_status { let exit_code = status.code().unwrap_or_else(|| { - status.signal().unwrap_or_else(|| { + platform::status_signal(status).unwrap_or_else(|| { // Extremely rare: process exited but we have neither exit code nor signal. // This can happen on some platforms or in unusual termination scenarios. ExitStatus::TimeoutFailed.into() @@ -321,7 +245,7 @@ fn wait_or_kill_process( Ok(None) => { let signal = signal_by_name_or_value("KILL").unwrap(); report_if_verbose(signal, cmd, verbose); - send_signal(process, signal, foreground); + platform::send_signal(process, signal, foreground); process.wait()?; Ok(ExitStatus::SignalSent(signal).into()) } @@ -329,32 +253,6 @@ fn wait_or_kill_process( } } -#[cfg(unix)] -fn preserve_signal_info(signal: libc::c_int) -> libc::c_int { - // This is needed because timeout is expected to preserve the exit - // status of its child. It is not the case that utilities have a - // single simple exit code, that's an illusion some shells - // provide. Instead exit status is really two numbers: - // - // - An exit code if the program ran to completion - // - // - A signal number if the program was terminated by a signal - // - // The easiest way to preserve the latter seems to be to kill - // ourselves with whatever signal our child exited with, which is - // what the following is intended to accomplish. - if let Some(sig) = signal_from_raw(signal) { - let _ = kill_process(getpid(), sig); - } - signal -} - -#[cfg(not(unix))] -fn preserve_signal_info(signal: libc::c_int) -> libc::c_int { - // Do nothing - signal -} - fn timeout( cmd: &[String], duration: Duration, @@ -364,10 +262,6 @@ fn timeout( preserve_status: bool, verbose: bool, ) -> UResult<()> { - if !foreground { - let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0)); - } - let mut cmd_builder = process::Command::new(&cmd[0]); cmd_builder .args(&cmd[1..]) @@ -375,35 +269,7 @@ fn timeout( .stdout(Stdio::inherit()) .stderr(Stdio::inherit()); - #[cfg(unix)] - { - #[cfg(target_os = "linux")] - let death_sig = signal_from_raw(signal as i32); - let sigpipe_was_ignored = uucore::signals::sigpipe_was_ignored(); - let stdin_was_closed = uucore::signals::stdin_was_closed(); - - unsafe { - cmd_builder.pre_exec(move || { - // Reset terminal signals to default - let _ = libc::signal(Signal::as_raw(Signal::TTIN), libc::SIG_DFL); - let _ = libc::signal(Signal::as_raw(Signal::TTOU), libc::SIG_DFL); - // Preserve SIGPIPE ignore status if parent had it ignored - if sigpipe_was_ignored { - let _ = libc::signal(Signal::as_raw(Signal::PIPE), libc::SIG_IGN); - } - // If stdin was closed before Rust reopened it as /dev/null, close it in child - if stdin_was_closed { - libc::close(libc::STDIN_FILENO); - } - #[cfg(target_os = "linux")] - let _ = rustix::process::set_parent_process_death_signal(death_sig); - Ok(()) - }); - } - } - - install_sigchld(); - install_signal_handlers(signal); + platform::prepare(&mut cmd_builder, foreground, signal); let process = &mut cmd_builder.spawn().map_err(|err| { let status_code = match err.kind() { @@ -417,6 +283,8 @@ fn timeout( ) })?; + platform::post_spawn(process, foreground); + // Wait for the child process for the specified time period. // // If the process exits within the specified time period (the @@ -431,23 +299,20 @@ fn timeout( match process.wait_or_timeout(duration, Some(&SIGNALED)) { Ok(Some(status)) => { let exit_code = status.code().unwrap_or_else(|| { - status - .signal() - .map_or_else(|| ExitStatus::TimeoutFailed.into(), preserve_signal_info) + platform::status_signal(status).map_or_else( + || ExitStatus::TimeoutFailed.into(), + platform::preserve_signal_info, + ) }); Err(exit_code.into()) } Ok(None) => { - let received_sig = RECEIVED_SIGNAL.load(atomic::Ordering::Relaxed); - let is_external_signal = received_sig > 0 && received_sig != libc::SIGALRM; - let signal_to_send = if is_external_signal { - received_sig as usize - } else { - signal - }; + let external_signal = platform::external_signal(); + let is_external_signal = external_signal.is_some(); + let signal_to_send = external_signal.unwrap_or(signal); report_if_verbose(signal_to_send, &cmd[0], verbose); - send_signal(process, signal_to_send, foreground); + platform::send_signal(process, signal_to_send, foreground); if let Some(kill_after) = kill_after { return match wait_or_kill_process( @@ -468,15 +333,14 @@ fn timeout( let status = process.wait()?; if is_external_signal { - Err(ExitStatus::SignalSent(received_sig as usize).into()) + Err(ExitStatus::SignalSent(signal_to_send).into()) } else if SIGNALED.load(atomic::Ordering::Relaxed) { Err(ExitStatus::CommandTimedOut.into()) } else if preserve_status { Err(status .code() .or_else(|| { - status - .signal() + platform::status_signal(status) .map(|s| ExitStatus::SignalSent(s as usize).into()) }) .unwrap_or(ExitStatus::CommandTimedOut.into()) @@ -488,7 +352,7 @@ fn timeout( Err(_) => { // We're going to return ERR_EXIT_STATUS regardless of // whether `send_signal()` succeeds or fails - send_signal(process, signal, foreground); + platform::send_signal(process, signal, foreground); Err(ExitStatus::TimeoutFailed.into()) } } From 69d0dc0cab7e4202df3be8dec9c50df4dd2c212e Mon Sep 17 00:00:00 2001 From: Nikola Lukovic Date: Sat, 11 Jul 2026 16:34:32 +0200 Subject: [PATCH 3/6] tests/timeout: run on Windows using the multicall binary for child commands --- tests/by-util/test_timeout.rs | 203 +++++++++++++++++++++++++++------- 1 file changed, 164 insertions(+), 39 deletions(-) diff --git a/tests/by-util/test_timeout.rs b/tests/by-util/test_timeout.rs index 5e24fdb1095..431424a612e 100644 --- a/tests/by-util/test_timeout.rs +++ b/tests/by-util/test_timeout.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore dont +// spell-checker:ignore dont SIGBREAK use rstest::rstest; use std::time::Duration; @@ -11,6 +11,15 @@ use uucore::display::Quotable; use uutests::util::TestScenario; use uutests::{new_ucmd, util_name}; +/// A scenario plus the path to the multicall test binary, used to run child +/// commands (`sleep`, `true`, ...) portably: it exists on every test platform, +/// unlike `sh`/`sleep` from `PATH`. +fn scenario_with_bin() -> (TestScenario, String) { + let ts = TestScenario::new(util_name!()); + let bin = ts.bin_path.to_string_lossy().into_owned(); + (ts, bin) +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(125); @@ -18,9 +27,10 @@ fn test_invalid_arg() { #[test] fn test_subcommand_return_code() { - new_ucmd!().arg("1").arg("true").succeeds(); + let (ts, bin) = scenario_with_bin(); + ts.ucmd().args(&["1", &bin, "true"]).succeeds(); - new_ucmd!().arg("1").arg("false").fails_with_code(1); + ts.ucmd().args(&["1", &bin, "false"]).fails_with_code(1); } #[rstest] @@ -43,34 +53,40 @@ fn test_invalid_kill_after() { #[test] fn test_command_with_args() { - new_ucmd!() - .args(&["1700", "echo", "-n", "abcd"]) + let (ts, bin) = scenario_with_bin(); + ts.ucmd() + .args(&["1700", &bin, "echo", "-n", "abcd"]) .succeeds() .stdout_only("abcd"); } #[test] fn test_verbose() { + let (ts, bin) = scenario_with_bin(); for verbose_flag in ["-v", "--verbose"] { - new_ucmd!() - .args(&[verbose_flag, ".1", "sleep", "1"]) + ts.ucmd() + .args(&[verbose_flag, ".1", &bin, "sleep", "1"]) .fails() - .stderr_only("timeout: sending signal TERM to command 'sleep'\n"); - new_ucmd!() - .args(&[verbose_flag, "-s0", "-k.1", ".1", "sleep", "1"]) + .no_stdout() + .stderr_contains("timeout: sending signal TERM to command"); + ts.ucmd() + .args(&[verbose_flag, "-s0", "-k.1", ".1", &bin, "sleep", "1"]) .fails() - .stderr_only("timeout: sending signal 0 to command 'sleep'\ntimeout: sending signal KILL to command 'sleep'\n"); + .no_stdout() + .stderr_contains("timeout: sending signal 0 to command") + .stderr_contains("timeout: sending signal KILL to command"); } } #[test] fn test_zero_timeout() { - new_ucmd!() - .args(&["-v", "0", "sleep", ".1"]) + let (ts, bin) = scenario_with_bin(); + ts.ucmd() + .args(&["-v", "0", &bin, "sleep", ".1"]) .succeeds() .no_output(); - new_ucmd!() - .args(&["-v", "0", "-s0", "-k0", "sleep", ".1"]) + ts.ucmd() + .args(&["-v", "0", "-s0", "-k0", &bin, "sleep", ".1"]) .succeeds() .no_output(); } @@ -85,9 +101,10 @@ fn test_command_empty_args() { #[test] fn test_foreground() { + let (ts, bin) = scenario_with_bin(); for arg in ["-f", "--foreground"] { - new_ucmd!() - .args(&[arg, ".1", "sleep", "10"]) + ts.ucmd() + .args(&[arg, ".1", &bin, "sleep", "10"]) .fails_with_code(124) .no_output(); } @@ -95,9 +112,10 @@ fn test_foreground() { #[test] fn test_preserve_status() { + let (ts, bin) = scenario_with_bin(); for arg in ["-p", "--preserve-status"] { - new_ucmd!() - .args(&[arg, ".1", "sleep", "10"]) + ts.ucmd() + .args(&[arg, ".1", &bin, "sleep", "10"]) // 128 + SIGTERM = 128 + 15 .fails_with_code(128 + 15) .no_output(); @@ -106,18 +124,28 @@ fn test_preserve_status() { #[test] fn test_kill_after_preserves_timeout_exit_without_preserve_status() { - new_ucmd!() - .args(&["-k", "1", "1", "sleep", "10"]) + let (ts, bin) = scenario_with_bin(); + ts.ucmd() + .args(&["-k", "1", "1", &bin, "sleep", "10"]) .fails_with_code(124) .no_output(); } #[test] fn test_preserve_status_even_when_send_signal() { + let (ts, bin) = scenario_with_bin(); // When sending CONT signal, process doesn't get killed or stopped. // So, expected result is success and code 0. for cont_spelling in ["CONT", "cOnT", "SIGcont"] { - new_ucmd!() - .args(&["-s", cont_spelling, "--preserve-status", ".1", "sleep", "1"]) + ts.ucmd() + .args(&[ + "-s", + cont_spelling, + "--preserve-status", + ".1", + &bin, + "sleep", + "1", + ]) .succeeds() .no_output(); } @@ -125,30 +153,32 @@ fn test_preserve_status_even_when_send_signal() { #[test] fn test_dont_overflow() { - new_ucmd!() - .args(&["9223372036854775808d", "sleep", "0"]) + let (ts, bin) = scenario_with_bin(); + ts.ucmd() + .args(&["9223372036854775808d", &bin, "sleep", "0"]) .succeeds() .no_output(); - new_ucmd!() - .args(&["-k", "9223372036854775808d", "10", "sleep", "0"]) + ts.ucmd() + .args(&["-k", "9223372036854775808d", "10", &bin, "sleep", "0"]) .succeeds() .no_output(); } #[test] fn test_dont_underflow() { - new_ucmd!() - .args(&[".0000000001", "sleep", "1"]) + let (ts, bin) = scenario_with_bin(); + ts.ucmd() + .args(&[".0000000001", &bin, "sleep", "1"]) .fails_with_code(124) .no_output(); - new_ucmd!() - .args(&["1e-100", "sleep", "1"]) + ts.ucmd() + .args(&["1e-100", &bin, "sleep", "1"]) .fails_with_code(124) .no_output(); // Unlike GNU coreutils, we underflow to 1ns for very short timeouts. // https://debbugs.gnu.org/cgi/bugreport.cgi?bug=77535 - new_ucmd!() - .args(&["1e-18172487393827593258", "sleep", "1"]) + ts.ucmd() + .args(&["1e-18172487393827593258", &bin, "sleep", "1"]) .fails_with_code(124) .no_output(); } @@ -180,13 +210,15 @@ fn test_invalid_multi_byte_characters() { /// Test that the long form of the `--kill-after` argument is recognized. #[test] fn test_kill_after_long() { - new_ucmd!() - .args(&["--kill-after=1", "1", "sleep", "0"]) + let (ts, bin) = scenario_with_bin(); + ts.ucmd() + .args(&["--kill-after=1", "1", &bin, "sleep", "0"]) .succeeds() .no_output(); } #[test] +#[cfg(unix)] fn test_kill_subprocess() { new_ucmd!() .args(&[ @@ -202,14 +234,16 @@ fn test_kill_subprocess() { #[test] fn test_hex_timeout_ending_with_d() { - new_ucmd!() - .args(&["0x0.1d", "sleep", "10"]) + let (ts, bin) = scenario_with_bin(); + ts.ucmd() + .args(&["0x0.1d", &bin, "sleep", "10"]) .timeout(Duration::from_secs(1)) .fails_with_code(124) .no_output(); } #[test] +#[cfg(unix)] fn test_terminate_child_on_receiving_terminate() { let mut timeout_cmd = new_ucmd!() .args(&[ @@ -292,8 +326,9 @@ fn test_forward_sigint_to_child() { #[test] fn test_foreground_signal0_kill_after() { - new_ucmd!() - .args(&["--foreground", "-s0", "-k.1", ".1", "sleep", "10"]) + let (ts, bin) = scenario_with_bin(); + ts.ucmd() + .args(&["--foreground", "-s0", "-k.1", ".1", &bin, "sleep", "10"]) .fails_with_code(137); } @@ -314,3 +349,93 @@ fn test_realtime_signal_names() { .fails() .stderr_contains("sending signal RTMAX to command"); } + +/// The whole process tree must be timed out in non-foreground mode, not just +/// the direct child: a grandchild sequencer (an inner `cmd.exe`) would +/// otherwise survive the timeout and create the marker file. +#[test] +#[cfg(windows)] +fn test_windows_kills_process_tree() { + let (ts, bin) = scenario_with_bin(); + let at = &ts.fixtures; + at.write( + "tree_grandchild.bat", + &format!("@echo off\r\n\"{bin}\" sleep 1\r\n\"{bin}\" touch tree_marker\r\n"), + ); + // Outer cmd is timeout's direct child; inner cmd (running the batch file) + // is the grandchild doing the work. + ts.ucmd() + .args(&[".3", "cmd", "/c", "cmd", "/c", "tree_grandchild.bat"]) + .fails_with_code(124); + // Give a surviving grandchild ample time to reach the `touch`. + std::thread::sleep(Duration::from_millis(2500)); + assert!( + !at.file_exists("tree_marker"), + "grandchild survived the process-tree kill" + ); +} + +/// `-s INT` is delivered as a CTRL_BREAK to the child's group (or termination +/// without a console); either way the child dies and timeout reports 124. +#[test] +#[cfg(windows)] +fn test_windows_int_signal_kills_child() { + let (ts, bin) = scenario_with_bin(); + ts.ucmd() + .args(&["-v", "-s", "INT", ".1", &bin, "sleep", "10"]) + .fails_with_code(124) + .stderr_contains("sending signal INT to command"); +} + +#[test] +#[cfg(windows)] +fn test_windows_kill_after() { + let (ts, bin) = scenario_with_bin(); + ts.ucmd() + .args(&["-s0", "-k", ".2", ".2", &bin, "sleep", "10"]) + .fails_with_code(137); +} + +/// Terminations use exit code 128 + N, so `--preserve-status` reports the +/// same codes as on unix. +#[test] +#[cfg(windows)] +fn test_windows_preserve_status_signal_codes() { + let (ts, bin) = scenario_with_bin(); + ts.ucmd() + .args(&["-p", "-s", "KILL", ".1", &bin, "sleep", "10"]) + .fails_with_code(128 + 9); + ts.ucmd() + .args(&["-p", "-s", "HUP", ".1", &bin, "sleep", "10"]) + .fails_with_code(128 + 1); +} + +/// POSIX signal names/numbers are accepted; names unix would reject (the +/// CRT-only SIGBREAK) are rejected here too. +#[test] +#[cfg(windows)] +fn test_windows_signal_name_parsing() { + let (ts, bin) = scenario_with_bin(); + ts.ucmd() + .args(&["-s", "SIGBREAK", "1", &bin, "true"]) + .fails_with_code(125) + .usage_error("'SIGBREAK': invalid signal"); + ts.ucmd() + .args(&["-s", "USR1", "1", &bin, "true"]) + .succeeds(); + ts.ucmd().args(&["-s", "9", "1", &bin, "true"]).succeeds(); +} + +/// A file that exists but is not executable yields 126, like unix. +#[test] +#[cfg(windows)] +fn test_windows_command_cannot_invoke() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("not_executable.txt"); + // The explicit path is needed: a bare filename is searched in PATH (not + // the current directory) and would fail with 127 instead. + ts.ucmd() + .args(&["1", ".\\not_executable.txt"]) + .fails_with_code(126); +} From a715cc44f5b833d3af8a236e4aa7b1c18378001d Mon Sep 17 00:00:00 2001 From: Nikola Lukovic Date: Sat, 11 Jul 2026 16:38:13 +0200 Subject: [PATCH 4/6] timeout: add cspell ignore entries for new Windows code --- src/uu/timeout/src/platform/windows.rs | 2 ++ src/uucore/src/lib/features/process/windows.rs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/uu/timeout/src/platform/windows.rs b/src/uu/timeout/src/platform/windows.rs index df029ff7122..db83e5a47c4 100644 --- a/src/uu/timeout/src/platform/windows.rs +++ b/src/uu/timeout/src/platform/windows.rs @@ -3,6 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore catchable forwardable + //! Windows implementation of `timeout`'s platform facade, built on the signal //! emulation in [`uucore::process`] (which maps POSIX signal numbers to //! Windows primitives). Non-foreground mode spawns the child in a new console diff --git a/src/uucore/src/lib/features/process/windows.rs b/src/uucore/src/lib/features/process/windows.rs index a8185a1e7e5..49456646075 100644 --- a/src/uucore/src/lib/features/process/windows.rs +++ b/src/uucore/src/lib/features/process/windows.rs @@ -4,7 +4,8 @@ // file that was distributed with this source code. // spell-checker:ignore (win-api) WAITABLE Waitable HIRES -// spell-checker:ignore (signals) CHLD TSTP TTIN TTOU WINCH +// spell-checker:ignore (signals) CHLD TSTP TTIN TTOU WINCH ESRCH +// spell-checker:ignore catchable targetable wakeup //! Windows emulation of POSIX signal delivery for child processes. //! From 8d0b6f5565a78f3a5d5203d00ccb7d262eb572ad Mon Sep 17 00:00:00 2001 From: Nikola Lukovic Date: Sat, 11 Jul 2026 17:10:43 +0200 Subject: [PATCH 5/6] uucore: confine raw Win32 calls to safe wrappers in process::sys --- .../src/lib/features/process/windows.rs | 533 ++++++++++-------- 1 file changed, 312 insertions(+), 221 deletions(-) diff --git a/src/uucore/src/lib/features/process/windows.rs b/src/uucore/src/lib/features/process/windows.rs index 49456646075..d1cc384a10c 100644 --- a/src/uucore/src/lib/features/process/windows.rs +++ b/src/uucore/src/lib/features/process/windows.rs @@ -3,61 +3,241 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (win-api) WAITABLE Waitable HIRES +// spell-checker:ignore (win-api) WAITABLE Waitable PHANDLER unsignaled // spell-checker:ignore (signals) CHLD TSTP TTIN TTOU WINCH ESRCH // spell-checker:ignore catchable targetable wakeup //! Windows emulation of POSIX signal delivery for child processes. //! -//! Windows has no signals, so this module emulates the POSIX *default -//! dispositions* using native primitives: -//! -//! - Signal numbers follow the Linux layout (the same table -//! `uucore::signals::ALL_SIGNALS` uses on Windows), so `-s HUP`, `-s 9`, -//! etc. keep meaning what cross-platform scripts expect. -//! - "Terminate" signals force-exit the target with exit code `128 + n`, -//! which is what observers of the exit status see on unix when a process -//! dies to signal `n`. -//! - `INT`/`QUIT` can be delivered to a console process group as a -//! `CTRL_BREAK_EVENT`: targetable, catchable by the child, and fatal by -//! default — the closest analog to a catchable SIGINT. (`CTRL_C_EVENT` -//! cannot target a specific group; it would be broadcast to the whole -//! console, including the sender.) -//! - Discard-by-default signals (`CHLD`, `CONT`, `URG`, `WINCH`) and the -//! stop family (`STOP`, `TSTP`, `TTIN`, `TTOU`), which cannot be emulated -//! with documented APIs, are accepted as no-ops. -//! -//! [`Job`] provides process-tree termination (the analog of signalling a -//! process group), and [`enable_ctrl_forwarding`] + [`last_ctrl_signal`] -//! translate console control events (Ctrl-C, Ctrl-Break, console close) into -//! POSIX signal numbers so callers can implement signal forwarding. +//! Windows has no signals, so this module emulates the POSIX default +//! dispositions with native primitives: signal numbers follow the Linux +//! layout (matching `uucore::signals::ALL_SIGNALS`), "terminate" signals +//! force-exit with exit code `128 + n`, and `INT`/`QUIT` map to a +//! `CTRL_BREAK_EVENT` on a console process group. [`Job`] gives process-tree +//! termination, and [`enable_ctrl_forwarding`]/[`last_ctrl_signal`] surface +//! console control events (Ctrl-C, Ctrl-Break, close) as POSIX signal numbers +//! for forwarding. All raw Win32 calls live in the safe [`sys`] wrappers. use std::io; -use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle}; +use std::os::windows::io::{AsHandle, BorrowedHandle, OwnedHandle}; use std::process::{Child, Command, ExitStatus}; -use std::ptr; -use std::sync::atomic::{AtomicBool, AtomicI32, AtomicPtr, Ordering}; +use std::sync::OnceLock; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::time::Duration; -use windows_sys::Win32::Foundation::{ - FALSE, HANDLE, TRUE, WAIT_OBJECT_0, WAIT_TIMEOUT, -}; -use windows_sys::Win32::System::Console::{ - CTRL_BREAK_EVENT, CTRL_C_EVENT, CTRL_CLOSE_EVENT, GenerateConsoleCtrlEvent, - SetConsoleCtrlHandler, -}; -use windows_sys::Win32::System::JobObjects::{ - AssignProcessToJobObject, CreateJobObjectW, TerminateJobObject, -}; -use windows_sys::Win32::System::Threading::{ - CREATE_NEW_PROCESS_GROUP, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, CreateEventW, - CreateWaitableTimerExW, INFINITE, SetEvent, SetWaitableTimer, TIMER_ALL_ACCESS, - TerminateProcess, WaitForMultipleObjects, WaitForSingleObject, -}; +use windows_sys::Win32::System::Console::{CTRL_BREAK_EVENT, CTRL_C_EVENT, CTRL_CLOSE_EVENT}; +use windows_sys::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, INFINITE}; use windows_sys::core::BOOL; use super::ChildExt; +/// Safe wrappers around the raw Win32 calls used for process control: each +/// validates results into [`io::Error`] and takes +/// [`BorrowedHandle`]/[`OwnedHandle`] so callers never touch raw `HANDLE`s. +pub mod sys { + use std::io; + use std::os::windows::io::{AsRawHandle, BorrowedHandle, HandleOrNull, OwnedHandle}; + + use windows_sys::Win32::Foundation::{ + FALSE, HANDLE, TRUE, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT, + }; + use windows_sys::Win32::System::Console::{ + GenerateConsoleCtrlEvent, PHANDLER_ROUTINE, SetConsoleCtrlHandler, + }; + use windows_sys::Win32::System::JobObjects::{ + AssignProcessToJobObject, CreateJobObjectW, TerminateJobObject, + }; + use windows_sys::Win32::System::Threading::{ + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, CreateEventW, CreateWaitableTimerExW, SetEvent, + SetWaitableTimer, TIMER_ALL_ACCESS, TerminateProcess, WaitForMultipleObjects, + WaitForSingleObject, + }; + use windows_sys::core::BOOL; + + /// The outcome of a successful bounded wait. + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum WaitOutcome { + /// The object at this index in the wait set became signaled. + Object(u32), + /// The wait interval elapsed first. + TimedOut, + } + + fn cvt(result: BOOL) -> io::Result<()> { + if result == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } + + /// Take ownership of a `HANDLE` from a Win32 create-function, treating + /// null as failure. + /// + /// # Safety + /// + /// `raw` must be either null or a valid handle exclusively owned by the + /// caller (i.e. freshly returned by a Win32 function that transfers + /// ownership). + unsafe fn cvt_created_handle(raw: HANDLE) -> io::Result { + // SAFETY: per this function's contract, `raw` is null or owned. + OwnedHandle::try_from(unsafe { HandleOrNull::from_raw_handle(raw) }) + .map_err(|_| io::Error::last_os_error()) + } + + /// Create an anonymous job object. + pub fn create_job_object() -> io::Result { + // SAFETY: null attributes and name are documented as valid; the + // returned handle is owned by us. + unsafe { cvt_created_handle(CreateJobObjectW(std::ptr::null(), std::ptr::null())) } + } + + /// Assign the process behind `process` to the job behind `job`. + pub fn assign_process_to_job(job: BorrowedHandle, process: BorrowedHandle) -> io::Result<()> { + // SAFETY: both handles are valid for the duration of the call by + // construction of `BorrowedHandle`. + cvt(unsafe { + AssignProcessToJobObject(job.as_raw_handle() as HANDLE, process.as_raw_handle()) + }) + } + + /// Terminate every process in the job with the given exit code. + pub fn terminate_job_object(job: BorrowedHandle, exit_code: u32) -> io::Result<()> { + // SAFETY: the job handle is valid for the duration of the call. + cvt(unsafe { TerminateJobObject(job.as_raw_handle() as HANDLE, exit_code) }) + } + + /// Terminate the process behind `process` with the given exit code. + pub fn terminate_process(process: BorrowedHandle, exit_code: u32) -> io::Result<()> { + // SAFETY: the process handle is valid for the duration of the call; + // terminating an already-exited process fails cleanly. + cvt(unsafe { TerminateProcess(process.as_raw_handle() as HANDLE, exit_code) }) + } + + /// Create an unnamed manual-reset event, initially unsignaled. + pub fn create_manual_reset_event() -> io::Result { + // SAFETY: null attributes and name are documented as valid; the + // returned handle is owned by us. + unsafe { + cvt_created_handle(CreateEventW( + std::ptr::null(), + TRUE, + FALSE, + std::ptr::null(), + )) + } + } + + /// Signal a (manual-reset or auto-reset) event. + pub fn set_event(event: BorrowedHandle) -> io::Result<()> { + // SAFETY: the event handle is valid for the duration of the call. + cvt(unsafe { SetEvent(event.as_raw_handle() as HANDLE) }) + } + + /// Create a one-shot waitable timer. + /// + /// With `high_resolution`, the timer is not coalesced to the ~15.6 ms + /// scheduler tick (Windows 10 1803+); callers should fall back to a + /// standard timer when the flag is unsupported. + pub fn create_waitable_timer(high_resolution: bool) -> io::Result { + let flags = if high_resolution { + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION + } else { + 0 + }; + // SAFETY: null attributes and name are documented as valid; the + // returned handle is owned by us. + unsafe { + cvt_created_handle(CreateWaitableTimerExW( + std::ptr::null(), + std::ptr::null(), + flags, + TIMER_ALL_ACCESS, + )) + } + } + + /// Arm `timer` to fire once after `ticks_100ns` (in 100 ns units). + pub fn set_relative_timer(timer: BorrowedHandle, ticks_100ns: i64) -> io::Result<()> { + // A negative due time means a relative wait. + let due_time = -ticks_100ns; + // SAFETY: the timer handle is valid, the due-time pointer is valid + // for the duration of the call, and no completion routine is used. + cvt(unsafe { + SetWaitableTimer( + timer.as_raw_handle() as HANDLE, + &raw const due_time, + 0, + None, + std::ptr::null(), + FALSE, + ) + }) + } + + /// Wait until any handle in `handles` is signaled or `timeout_ms` + /// elapses (`INFINITE` for no limit). On simultaneous completion the + /// lowest index wins. + /// + /// Mutexes are not supported (an abandoned-mutex result is reported as + /// an error). + pub fn wait_for_any(handles: &[BorrowedHandle], timeout_ms: u32) -> io::Result { + let count = u32::try_from(handles.len()) + .map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?; + // SAFETY: `BorrowedHandle` is `repr(transparent)` over a raw handle, + // so the slice is layout-compatible with an array of `HANDLE`, and + // every element is a valid open handle for the duration of the call. + let result = + unsafe { WaitForMultipleObjects(count, handles.as_ptr().cast(), FALSE, timeout_ms) }; + wait_outcome(result, count) + } + + /// Wait until `handle` is signaled or `timeout_ms` elapses. A zero + /// timeout makes this a state poll. + pub fn wait_for_one(handle: BorrowedHandle, timeout_ms: u32) -> io::Result { + // SAFETY: the handle is valid for the duration of the call. + let result = unsafe { WaitForSingleObject(handle.as_raw_handle() as HANDLE, timeout_ms) }; + wait_outcome(result, 1) + } + + fn wait_outcome(result: u32, count: u32) -> io::Result { + if result == WAIT_TIMEOUT { + return Ok(WaitOutcome::TimedOut); + } + if result == WAIT_FAILED { + return Err(io::Error::last_os_error()); + } + let index = result.wrapping_sub(WAIT_OBJECT_0); + if index < count { + Ok(WaitOutcome::Object(index)) + } else { + // WAIT_ABANDONED_0..: only possible for mutexes, which this + // module never waits on; surface it instead of guessing. + Err(io::Error::other(format!("unexpected wait result {result}"))) + } + } + + /// Send a console control event (`CTRL_C_EVENT`/`CTRL_BREAK_EVENT`) to + /// the console process group led by `process_group_id`. + pub fn generate_console_ctrl_event(ctrl_event: u32, process_group_id: u32) -> io::Result<()> { + // SAFETY: no pointers involved; fails cleanly without a console. + cvt(unsafe { GenerateConsoleCtrlEvent(ctrl_event, process_group_id) }) + } + + /// Register `handler` to receive console control events. + /// + /// # Safety + /// + /// The OS invokes `handler` on a dedicated thread at any moment, + /// including while other threads run arbitrary code; it must be sound + /// under those conditions (touch only atomics and other thread-safe, + /// non-allocating state). + pub unsafe fn set_console_ctrl_handler(handler: PHANDLER_ROUTINE) -> io::Result<()> { + // SAFETY: the handler contract is upheld by the caller. + cvt(unsafe { SetConsoleCtrlHandler(handler, TRUE) }) + } +} + // POSIX (Linux-layout) signal numbers, matching the Windows `ALL_SIGNALS` // table in `uucore::signals`. Kept local so the `process` feature does not // depend on the `signals` feature. @@ -67,13 +247,11 @@ const SIGNAL_QUIT: i32 = 3; /// What delivering a given POSIX signal number means on Windows. enum Disposition { - /// Signal 0: existence check only. Probe, /// Discarded-by-default and stop signals: accepted, nothing to do. Ignore, /// `INT`/`QUIT`: deliverable to a console process group as CTRL_BREAK. Interrupt, - /// Everything else: forced termination with exit code `128 + n`. Terminate, } @@ -81,47 +259,35 @@ fn disposition(signal: usize) -> io::Result { match signal { 0 => Ok(Disposition::Probe), 2 | 3 => Ok(Disposition::Interrupt), - // CHLD, CONT, URG and WINCH are discarded by default on POSIX; the - // stop family (STOP, TSTP, TTIN, TTOU) cannot be emulated with - // documented APIs. All are accepted as no-ops. + // Discarded-by-default (CHLD, CONT, URG, WINCH) and the stop family + // (STOP, TSTP, TTIN, TTOU): no documented emulation, so no-ops. 17..=23 | 28 => Ok(Disposition::Ignore), 1..=31 => Ok(Disposition::Terminate), _ => Err(io::ErrorKind::InvalidInput.into()), } } -/// Terminate the process behind `handle` so that its exit status becomes -/// `128 + signal`, emulating "killed by signal" for exit-code observers. -fn terminate_with_signal(handle: HANDLE, signal: usize) -> io::Result<()> { - // SAFETY: the handle is a valid process handle; terminating an - // already-exited process fails cleanly with an OS error. - if unsafe { TerminateProcess(handle, (128 + signal) as u32) } == 0 { - return Err(io::Error::last_os_error()); - } - Ok(()) +/// Terminate so the child's exit status becomes `128 + signal`, emulating +/// "killed by signal" for exit-code observers. +fn terminate_with_signal(handle: BorrowedHandle, signal: usize) -> io::Result<()> { + sys::terminate_process(handle, (128 + signal) as u32) } /// Deliver `signal` (POSIX numbering) to the child process only. /// /// A console control event cannot target a single process, so `INT`/`QUIT` -/// fall back to their POSIX default disposition here: termination. Callers -/// that want a catchable interrupt must target a process group via -/// [`send_signal_to_console_group`]. +/// fall back to termination here; callers wanting a catchable interrupt must +/// use [`send_signal_to_console_group`]. pub fn send_signal_to_process(child: &Child, signal: usize) -> io::Result<()> { - let handle = child.as_raw_handle() as HANDLE; match disposition(signal)? { - Disposition::Probe => { - // SAFETY: valid process handle; a zero timeout makes this a poll. - match unsafe { WaitForSingleObject(handle, 0) } { - WAIT_TIMEOUT => Ok(()), - // The process has exited: the POSIX analog is ESRCH. - WAIT_OBJECT_0 => Err(io::ErrorKind::NotFound.into()), - _ => Err(io::Error::last_os_error()), - } - } + Disposition::Probe => match sys::wait_for_one(child.as_handle(), 0)? { + sys::WaitOutcome::TimedOut => Ok(()), + // The process has exited: the POSIX analog is ESRCH. + sys::WaitOutcome::Object(_) => Err(io::ErrorKind::NotFound.into()), + }, Disposition::Ignore => Ok(()), Disposition::Interrupt | Disposition::Terminate => { - terminate_with_signal(handle, signal) + terminate_with_signal(child.as_handle(), signal) } } } @@ -135,23 +301,20 @@ pub fn send_signal_to_process(child: &Child, signal: usize) -> io::Result<()> { pub fn send_signal_to_console_group(pid: u32, signal: usize) -> io::Result<()> { match disposition(signal)? { Disposition::Probe | Disposition::Ignore => Ok(()), - Disposition::Interrupt => { - // SAFETY: no pointers involved; fails cleanly without a console. - if unsafe { GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid) } == 0 { - return Err(io::Error::last_os_error()); - } - Ok(()) - } + // CTRL_C_EVENT cannot target a nonzero group (it broadcasts to the + // whole console, including the sender), so INT/QUIT both use + // CTRL_BREAK: targetable, catchable, fatal by default. + Disposition::Interrupt => sys::generate_console_ctrl_event(CTRL_BREAK_EVENT, pid), Disposition::Terminate => Err(io::ErrorKind::Unsupported.into()), } } -/// Deliver `signal` (POSIX numbering) to the child's whole process tree: -/// terminating signals (including `INT`/`QUIT`, which cannot reach a whole -/// tree as console events) terminate the job with exit code `128 + n`, -/// falling back to the direct child when `job` is `None` or termination via -/// the job fails; probe and ignored signals behave as in -/// [`send_signal_to_process`]. +/// Deliver `signal` (POSIX numbering) to the child's whole process tree. +/// +/// Terminating signals (including `INT`/`QUIT`, which cannot reach a tree as +/// console events) terminate the job with exit code `128 + n`, falling back +/// to the direct child when `job` is `None` or the job terminate fails; +/// probe/ignored signals behave as [`send_signal_to_process`]. pub fn send_signal_to_tree(child: &Child, job: Option<&Job>, signal: usize) -> io::Result<()> { match disposition(signal)? { Disposition::Probe | Disposition::Ignore => send_signal_to_process(child, signal), @@ -161,18 +324,16 @@ pub fn send_signal_to_tree(child: &Child, job: Option<&Job>, signal: usize) -> i return Ok(()); } } - terminate_with_signal(child.as_raw_handle() as HANDLE, signal) + terminate_with_signal(child.as_handle(), signal) } } } /// Make `cmd` spawn its child as the leader of a new console process group, -/// so that `CTRL_BREAK_EVENT` can be targeted at exactly that child's group -/// and the console's own Ctrl-C no longer reaches the child directly (the -/// analog of `setpgid(0, 0)` isolation on unix). +/// so `CTRL_BREAK_EVENT` can target exactly that group and the console's own +/// Ctrl-C no longer reaches the child (the analog of unix `setpgid(0, 0)`). /// -/// Note: this sets the command's creation flags, overwriting any flags set -/// earlier via `CommandExt::creation_flags`. +/// Overwrites any creation flags set earlier via `CommandExt::creation_flags`. pub fn configure_process_group(cmd: &mut Command) { use std::os::windows::process::CommandExt; cmd.creation_flags(CREATE_NEW_PROCESS_GROUP); @@ -190,13 +351,7 @@ pub struct Job(OwnedHandle); impl Job { /// Create a new anonymous job object. pub fn new() -> io::Result { - // SAFETY: null attributes and name are documented as valid. - let raw = unsafe { CreateJobObjectW(ptr::null(), ptr::null()) }; - if raw.is_null() { - return Err(io::Error::last_os_error()); - } - // SAFETY: `raw` is a valid handle exclusively owned by us. - Ok(Self(unsafe { OwnedHandle::from_raw_handle(raw) })) + sys::create_job_object().map(Self) } /// Assign `child` (and, transitively, every process it spawns from then @@ -206,64 +361,45 @@ impl Job { /// current process already runs inside a job; callers should degrade to /// per-process operations in that case. pub fn assign(&self, child: &Child) -> io::Result<()> { - // SAFETY: both handles are valid for the duration of the call. - if unsafe { - AssignProcessToJobObject( - self.0.as_raw_handle() as HANDLE, - child.as_raw_handle() as HANDLE, - ) - } == 0 - { - return Err(io::Error::last_os_error()); - } - Ok(()) + sys::assign_process_to_job(self.0.as_handle(), child.as_handle()) } /// Terminate every process in the job with the given exit code /// (pass `128 + signal` to emulate death by signal). pub fn terminate(&self, exit_code: u32) -> io::Result<()> { - // SAFETY: the job handle is valid for the duration of the call. - if unsafe { TerminateJobObject(self.0.as_raw_handle() as HANDLE, exit_code) } == 0 { - return Err(io::Error::last_os_error()); - } - Ok(()) + sys::terminate_job_object(self.0.as_handle(), exit_code) } } /// Manual-reset event signalled by the console control handler to wake -/// [`ChildExt::wait_or_timeout`]. Null until [`enable_ctrl_forwarding`] runs. -/// Intentionally lives for the rest of the process; the OS reclaims it. -static WAKE_EVENT: AtomicPtr = AtomicPtr::new(ptr::null_mut()); +/// [`ChildExt::wait_or_timeout`]. Unset until [`enable_ctrl_forwarding`] +/// runs; intentionally lives for the rest of the process. +static WAKE_EVENT: OnceLock = OnceLock::new(); /// POSIX signal number of the last console control event received (0 = none). static LAST_CTRL_SIGNAL: AtomicI32 = AtomicI32::new(0); -/// The console control handler runs on a system-spawned thread: it must only -/// touch atomics and signal the pre-created event (no allocation, no locks). +/// Console control handler. Runs on an OS-spawned thread, so it only touches +/// atomics and signals the pre-created event — no allocation, no locks. /// /// # Safety /// -/// Only registered via `SetConsoleCtrlHandler` and called by the system with -/// a valid `ctrl_type`; touches nothing but atomics and a live event handle. +/// Registered only via [`sys::set_console_ctrl_handler`] and invoked by the +/// system with a valid `ctrl_type`. unsafe extern "system" fn console_ctrl_handler(ctrl_type: u32) -> BOOL { let signal = match ctrl_type { CTRL_C_EVENT => SIGNAL_INT, CTRL_BREAK_EVENT => SIGNAL_QUIT, - // The console window is going away; the analog of losing the - // controlling terminal. The system still terminates us after a grace - // period, so the main thread must react promptly. + // Console window closing (analog of losing the controlling terminal); + // the system force-terminates us after a grace period, so react now. CTRL_CLOSE_EVENT => SIGNAL_HUP, - // Logoff/shutdown notifications are only delivered to services; - // leave them to the default handler. - _ => return FALSE, + // Logoff/shutdown reach only services; leave them to the default handler. + _ => return 0, }; LAST_CTRL_SIGNAL.store(signal, Ordering::Release); - let event = WAKE_EVENT.load(Ordering::Acquire); - if !event.is_null() { - // SAFETY: the event handle is created before the handler is - // registered and is never closed. - unsafe { SetEvent(event) }; + if let Some(event) = WAKE_EVENT.get() { + let _ = sys::set_event(event.as_handle()); } - TRUE + 1 } /// Install a console control handler that records Ctrl-C, Ctrl-Break and @@ -273,21 +409,17 @@ unsafe extern "system" fn console_ctrl_handler(ctrl_type: u32) -> BOOL { /// /// Use [`last_ctrl_signal`] to read the last event received. Idempotent. pub fn enable_ctrl_forwarding() -> io::Result<()> { - if !WAKE_EVENT.load(Ordering::Acquire).is_null() { + if WAKE_EVENT.get().is_some() { return Ok(()); } // Manual-reset so a wakeup latched before a wait starts is never lost. - // SAFETY: null attributes and name are documented as valid. - let event = unsafe { CreateEventW(ptr::null(), TRUE, FALSE, ptr::null()) }; - if event.is_null() { - return Err(io::Error::last_os_error()); - } - WAKE_EVENT.store(event, Ordering::Release); - // SAFETY: the handler only touches atomics and a live event handle. - if unsafe { SetConsoleCtrlHandler(Some(console_ctrl_handler), TRUE) } == 0 { - return Err(io::Error::last_os_error()); - } - Ok(()) + let event = sys::create_manual_reset_event()?; + // A racing second caller just drops its event; registering the handler + // twice is harmless. + let _ = WAKE_EVENT.set(event); + // SAFETY: the handler only touches atomics, the immutable `WAKE_EVENT` + // cell and a live event handle — sound for arbitrary-thread invocation. + unsafe { sys::set_console_ctrl_handler(Some(console_ctrl_handler)) } } /// The POSIX signal number corresponding to the last console control event @@ -304,45 +436,14 @@ pub fn last_ctrl_signal() -> Option { /// Uses a high-resolution timer (100 ns due-time granularity, not coalesced /// to the ~15.6 ms scheduler tick) when the OS supports it (Windows 10 1803+), /// falling back to a standard waitable timer otherwise. -fn create_relative_timer(timeout: Duration) -> io::Result { - // SAFETY: null attributes and name are documented as valid. - let mut raw = unsafe { - CreateWaitableTimerExW( - ptr::null(), - ptr::null(), - CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, - TIMER_ALL_ACCESS, - ) - }; - if raw.is_null() { +fn start_relative_timer(timeout: Duration) -> io::Result { + let timer = sys::create_waitable_timer(true) // Pre-1803 systems reject the high-resolution flag. - // SAFETY: as above. - raw = unsafe { CreateWaitableTimerExW(ptr::null(), ptr::null(), 0, TIMER_ALL_ACCESS) }; - } - if raw.is_null() { - return Err(io::Error::last_os_error()); - } - // SAFETY: `raw` is a valid handle exclusively owned by us. - let timer = unsafe { OwnedHandle::from_raw_handle(raw) }; - - // A negative due time is relative, in 100 ns units. Round up so a - // sub-tick duration never fires early, and clamp huge durations. + .or_else(|_| sys::create_waitable_timer(false))?; + // 100 ns ticks: round up so a sub-tick duration never fires early, and + // clamp huge durations. let ticks = timeout.as_nanos().div_ceil(100).min(i64::MAX as u128) as i64; - let due_time = -ticks; - // SAFETY: the timer handle is valid; no completion routine is used. - if unsafe { - SetWaitableTimer( - timer.as_raw_handle() as HANDLE, - &raw const due_time, - 0, - None, - ptr::null(), - FALSE, - ) - } == 0 - { - return Err(io::Error::last_os_error()); - } + sys::set_relative_timer(timer.as_handle(), ticks)?; Ok(timer) } @@ -353,8 +454,8 @@ impl ChildExt for Child { fn send_signal_group(&mut self, signal: usize) -> io::Result<()> { // Unlike unix (which signals the caller's own process group), this - // targets the child's console process group: on Windows only a group - // the child leads can be addressed at all. + // targets the child's console group — the only group Windows lets us + // address at all. let pid = self.id(); send_signal_to_console_group(pid, signal) } @@ -367,53 +468,43 @@ impl ChildExt for Child { // The unix implementation drops stdin so the child sees EOF; match it. drop(self.stdin.take()); - // A single blocking wait over up to three handles. Ordering matters: - // on simultaneous completion `WaitForMultipleObjects` reports the - // lowest index, so child-exit wins over a console event, which wins - // over timer expiry — matching the unix implementation's races. - let mut handles: [HANDLE; 3] = [ptr::null_mut(); 3]; - let mut count: u32 = 0; - - handles[count as usize] = self.as_raw_handle() as HANDLE; - count += 1; - - let wake_index = if signaled.is_some() { - let event = WAKE_EVENT.load(Ordering::Acquire); - if event.is_null() { - None - } else { - handles[count as usize] = event; - count += 1; - Some(count - 1) - } - } else { - // The caller wants this wait to ignore console events (e.g. the - // kill-after grace period), so the wake event is left out. + // Zero disables the timeout: no timer, so the wait blocks until the + // child exits (or a console event fires). + let timer = if timeout.is_zero() { None + } else { + Some(start_relative_timer(timeout)?) }; - - // A timeout of zero disables the timeout: no timer handle, so the - // wait below blocks until the child exits (or a console event fires). - let _timer: Option = if timeout.is_zero() { - None + // The wake event is left out when this wait must ignore console events + // (e.g. the kill-after grace period). + let wake_event = if signaled.is_some() { + WAKE_EVENT.get() } else { - let timer = create_relative_timer(timeout)?; - handles[count as usize] = timer.as_raw_handle() as HANDLE; - count += 1; - Some(timer) + None }; - // SAFETY: `handles[..count]` are valid, open handles for the whole - // call; INFINITE is safe because at least the process handle (and - // usually the timer) will eventually be signalled. - let result = unsafe { WaitForMultipleObjects(count, handles.as_ptr(), FALSE, INFINITE) }; - let index = result.wrapping_sub(WAIT_OBJECT_0); - if index >= count { - // WAIT_FAILED (or an impossible WAIT_ABANDONED: no mutexes here). - return Err(io::Error::last_os_error()); + // A single blocking wait over up to three handles. On simultaneous + // completion the lowest index wins, so child-exit beats a console + // event, which beats timer expiry — matching the unix races. + let mut handles: Vec = Vec::with_capacity(3); + handles.push(self.as_handle()); + let wake_index = wake_event.map(|event| { + handles.push(event.as_handle()); + handles.len() - 1 + }); + if let Some(timer) = &timer { + handles.push(timer.as_handle()); } + + let index = match sys::wait_for_any(&handles, INFINITE)? { + sys::WaitOutcome::Object(index) => index as usize, + // Unreachable with an INFINITE wait; treat as timer expiry. + sys::WaitOutcome::TimedOut => return Ok(None), + }; + drop(handles); + if index == 0 { - // The child exited; this reaps it without blocking. + // Child exited; reap it without blocking. return self.wait().map(Some); } if Some(index) == wake_index { From cde6314309b83db1008af3d8537ff885e4824c0b Mon Sep 17 00:00:00 2001 From: Nikola Lukovic Date: Sat, 11 Jul 2026 17:37:31 +0200 Subject: [PATCH 6/6] timeout: enable benchmarks on Windows with a self-exec child command --- src/uu/timeout/benches/timeout_bench.rs | 69 +++++++++++++++++-- src/uu/timeout/src/platform/windows.rs | 19 +++-- .../src/lib/features/process/windows.rs | 30 ++++---- 3 files changed, 86 insertions(+), 32 deletions(-) diff --git a/src/uu/timeout/benches/timeout_bench.rs b/src/uu/timeout/benches/timeout_bench.rs index 511955f6f0b..36a715ee12b 100644 --- a/src/uu/timeout/benches/timeout_bench.rs +++ b/src/uu/timeout/benches/timeout_bench.rs @@ -3,13 +3,23 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -#[cfg(unix)] use divan::{Bencher, black_box}; -#[cfg(unix)] use uu_timeout::uumain; -#[cfg(unix)] use uucore::benchmark::run_util_function; +/// First-arg marker that re-runs this bench binary as its own child command, +/// a portable `sleep`/`true` stand-in (`cargo bench` builds no other binary +/// and Windows lacks both in `PATH`). +const CHILD_MARKER: &str = "__timeout_bench_child"; + +#[cfg(windows)] +fn self_exe() -> String { + std::env::current_exe() + .unwrap() + .to_string_lossy() + .into_owned() +} + /// Benchmark the fast path where the command exits immediately. #[cfg(unix)] #[divan::bench] @@ -19,6 +29,19 @@ fn timeout_quick_exit(bencher: Bencher) { }); } +/// Benchmark the fast path where the command exits immediately. +#[cfg(windows)] +#[divan::bench] +fn timeout_quick_exit(bencher: Bencher) { + let exe = self_exe(); + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["0.02", &exe, CHILD_MARKER, "0"], + )); + }); +} + /// Benchmark a command that runs longer than the threshold and receives the default signal. #[cfg(unix)] #[divan::bench] @@ -28,7 +51,45 @@ fn timeout_enforced(bencher: Bencher) { }); } +/// Benchmark a command that runs longer than the threshold: timer expiry, +/// job-object tree kill and exit-code plumbing. +#[cfg(windows)] +#[divan::bench] +fn timeout_enforced(bencher: Bencher) { + let exe = self_exe(); + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["0.02", &exe, CHILD_MARKER, "0.2"], + )); + }); +} + +/// Track timeout-firing latency across small durations; a regression from the +/// 100 ns waitable timer to a coarser wait (the 15.6 ms scheduler tick, or +/// polling) shows up as a step change across the arguments. +#[cfg(windows)] +#[divan::bench(args = ["0.001", "0.005", "0.02"])] +fn timer_expiry_latency(bencher: Bencher, duration: &str) { + let exe = self_exe(); + bencher.bench(|| { + black_box(run_util_function( + uumain, + &[duration, &exe, CHILD_MARKER, "5"], + )); + }); +} + fn main() { - #[cfg(unix)] + // When re-invoked with CHILD_MARKER, act as a tiny `sleep` replacement. + let mut args = std::env::args().skip(1); + if args.next().as_deref() == Some(CHILD_MARKER) { + if let Some(secs) = args.next().and_then(|s| s.parse::().ok()) { + if secs > 0.0 { + std::thread::sleep(std::time::Duration::from_secs_f64(secs)); + } + } + return; + } divan::main(); } diff --git a/src/uu/timeout/src/platform/windows.rs b/src/uu/timeout/src/platform/windows.rs index db83e5a47c4..5d7921bec00 100644 --- a/src/uu/timeout/src/platform/windows.rs +++ b/src/uu/timeout/src/platform/windows.rs @@ -12,7 +12,7 @@ //! tree and `INT`/`QUIT` arrive as a catchable `CTRL_BREAK_EVENT`. use std::process::Child; -use std::sync::OnceLock; +use std::sync::Mutex; use uucore::process::{ Job, configure_process_group, enable_ctrl_forwarding, last_ctrl_signal, @@ -24,8 +24,10 @@ const SIGNAL_QUIT: usize = 3; /// The job object tracking the child's process tree (non-foreground mode /// only; `None` also when job assignment failed and we degraded to -/// per-process signalling). -static JOB: OnceLock = OnceLock::new(); +/// per-process signalling). Replaced on every spawn so that repeated +/// in-process invocations of `uumain` (benchmarks, fuzzing) never signal a +/// previous run's job. +static JOB: Mutex> = Mutex::new(None); /// Configure the child's spawn attributes and console-event forwarding, right /// before the child is spawned. @@ -53,11 +55,8 @@ pub(crate) fn post_spawn(child: &Child, foreground: bool) { if foreground { return; } - if let Ok(job) = Job::new() { - if job.assign(child).is_ok() { - let _ = JOB.set(job); - } - } + let job = Job::new().ok().filter(|job| job.assign(child).is_ok()); + *JOB.lock().unwrap() = job; } pub(crate) fn send_signal(process: &mut Child, signal: usize, foreground: bool) { @@ -75,10 +74,10 @@ pub(crate) fn send_signal(process: &mut Child, signal: usize, foreground: bool) // Deliverable as a catchable CTRL_BREAK to the child's group; without // a console, fall back to termination so the signal is never lost. if send_signal_to_console_group(process.id(), signal).is_err() { - let _ = send_signal_to_tree(process, JOB.get(), signal); + let _ = send_signal_to_tree(process, JOB.lock().unwrap().as_ref(), signal); } } else { - let _ = send_signal_to_tree(process, JOB.get(), signal); + let _ = send_signal_to_tree(process, JOB.lock().unwrap().as_ref(), signal); } } diff --git a/src/uucore/src/lib/features/process/windows.rs b/src/uucore/src/lib/features/process/windows.rs index d1cc384a10c..18985f6e107 100644 --- a/src/uucore/src/lib/features/process/windows.rs +++ b/src/uucore/src/lib/features/process/windows.rs @@ -134,17 +134,17 @@ pub mod sys { cvt(unsafe { SetEvent(event.as_raw_handle() as HANDLE) }) } - /// Create a one-shot waitable timer. - /// - /// With `high_resolution`, the timer is not coalesced to the ~15.6 ms - /// scheduler tick (Windows 10 1803+); callers should fall back to a - /// standard timer when the flag is unsupported. - pub fn create_waitable_timer(high_resolution: bool) -> io::Result { - let flags = if high_resolution { - CREATE_WAITABLE_TIMER_HIGH_RESOLUTION - } else { - 0 - }; + /// Create a one-shot waitable timer with the best resolution the OS + /// offers: a high-resolution timer (not coalesced to the ~15.6 ms + /// scheduler tick; Windows 10 1803+) when supported, a standard + /// waitable timer otherwise. + pub fn create_waitable_timer() -> io::Result { + create_waitable_timer_with(CREATE_WAITABLE_TIMER_HIGH_RESOLUTION) + // Pre-1803 systems reject the high-resolution flag. + .or_else(|_| create_waitable_timer_with(0)) + } + + fn create_waitable_timer_with(flags: u32) -> io::Result { // SAFETY: null attributes and name are documented as valid; the // returned handle is owned by us. unsafe { @@ -432,14 +432,8 @@ pub fn last_ctrl_signal() -> Option { } /// Create a one-shot waitable timer that fires after `timeout`. -/// -/// Uses a high-resolution timer (100 ns due-time granularity, not coalesced -/// to the ~15.6 ms scheduler tick) when the OS supports it (Windows 10 1803+), -/// falling back to a standard waitable timer otherwise. fn start_relative_timer(timeout: Duration) -> io::Result { - let timer = sys::create_waitable_timer(true) - // Pre-1803 systems reject the high-resolution flag. - .or_else(|_| sys::create_waitable_timer(false))?; + let timer = sys::create_waitable_timer()?; // 100 ns ticks: round up so a sub-tick duration never fires early, and // clamp huge durations. let ticks = timeout.as_nanos().div_ceil(100).min(i64::MAX as u128) as i64;