From ec92bcec33364180606673d310195a15bd01b000 Mon Sep 17 00:00:00 2001 From: Hrvoje Niksic Date: Thu, 16 Apr 2026 22:43:26 +0200 Subject: [PATCH 1/3] Add Exec::arg0() to override argv[0] --- src/exec.rs | 13 +++++++++++++ src/tests/posix.rs | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/exec.rs b/src/exec.rs index 0e91394..0d6f1e7 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -303,6 +303,19 @@ impl Exec { self } + /// Overrides the first process argument, `argv[0]`. + /// + /// By default `argv[0]` is set to the command passed to [`Exec::cmd`]. This method + /// allows setting it to an arbitrary value. + pub fn arg0(mut self, arg: impl Into) -> Exec { + if self.executable.is_none() { + self.executable = Some(std::mem::replace(&mut self.command, arg.into())); + } else { + self.command = arg.into(); + } + self + } + /// Specifies the current working directory of the child process. /// /// If unspecified, the current working directory is inherited from the parent. diff --git a/src/tests/posix.rs b/src/tests/posix.rs index 646e672..2943468 100644 --- a/src/tests/posix.rs +++ b/src/tests/posix.rs @@ -158,6 +158,19 @@ fn exit_status_display() { assert_eq!(ExitStatus::from_raw(9).to_string(), "signal 9"); } +// --- arg0 tests --- + +#[test] +fn arg0_override() { + let out = Exec::cmd("sh") + .arg0("custom-name") + .args(&["-c", "echo $0"]) + .capture() + .unwrap() + .stdout_str(); + assert_eq!(out.trim(), "custom-name"); +} + // --- JobExt tests --- #[test] From 5cc376fd85f95056ca3709c00ea4e49f10b55fec Mon Sep 17 00:00:00 2001 From: Hrvoje Niksic Date: Thu, 16 Apr 2026 22:54:59 +0200 Subject: [PATCH 2/3] Add ExitStatusExt for constructing and inspecting ExitStatus Mirrors std: unix::ExitStatusExt takes i32 (the waitpid() status), windows::ExitStatusExt takes u32 (the GetExitCodeProcess() code). --- src/lib.rs | 2 ++ src/process.rs | 65 ++++++++++++++++++++++++++++++++++++++++++++++ src/tests/posix.rs | 10 ++++++- 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 294b711..a679336 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,6 +87,7 @@ pub use process::Process; pub mod unix { pub use super::exec::unix::JobExt; pub use super::exec::unix::PipelineExt; + pub use super::process::ExitStatusExt; pub use super::process::ProcessExt; } @@ -94,4 +95,5 @@ pub mod unix { #[cfg(any(windows, docsrs))] pub mod windows { pub use super::exec::windows::*; + pub use super::process::windows::ExitStatusExt; } diff --git a/src/process.rs b/src/process.rs index 2a2fe75..49a7bb6 100644 --- a/src/process.rs +++ b/src/process.rs @@ -463,6 +463,29 @@ mod os { self.0.send_signal_group(signal) } } + + /// Unix-specific extension methods for [`ExitStatus`]. + pub trait ExitStatusExt { + /// Constructs an `ExitStatus` from the raw status returned by `waitpid()`. + /// + /// The value encodes both normal exit codes and signal deaths in the format + /// documented by `wait(2)`. + fn from_raw(raw: i32) -> ExitStatus; + + /// Returns the raw `waitpid()` status, or `None` if the exit status could not + /// be determined (e.g. because someone else waited for the child). + fn into_raw(self) -> Option; + } + + impl ExitStatusExt for ExitStatus { + fn from_raw(raw: i32) -> ExitStatus { + ExitStatus::from_raw(raw) + } + + fn into_raw(self) -> Option { + self.0 + } + } } } @@ -594,3 +617,45 @@ mod os { pub(crate) use os::ExtProcessState; #[cfg(unix)] pub use os::ext::*; + +/// Windows-specific extensions. +#[cfg(any(windows, docsrs))] +pub mod windows { + use super::ExitStatus; + + /// Windows-specific extension methods for [`ExitStatus`]. + pub trait ExitStatusExt { + /// Constructs an `ExitStatus` from the raw exit code returned by + /// `GetExitCodeProcess()`. + fn from_raw(raw: u32) -> ExitStatus; + + /// Returns the raw `GetExitCodeProcess()` value, or `None` if the exit status + /// could not be determined. + fn into_raw(self) -> Option; + } + + impl ExitStatusExt for ExitStatus { + fn from_raw(raw: u32) -> ExitStatus { + #[cfg(windows)] + { + ExitStatus::from_raw(raw) + } + #[cfg(not(windows))] + { + let _ = raw; + unimplemented!() + } + } + + fn into_raw(self) -> Option { + #[cfg(windows)] + { + self.0 + } + #[cfg(not(windows))] + { + unimplemented!() + } + } + } +} diff --git a/src/tests/posix.rs b/src/tests/posix.rs index 2943468..0accc15 100644 --- a/src/tests/posix.rs +++ b/src/tests/posix.rs @@ -1,7 +1,7 @@ use std::time::{Duration, Instant}; use super::exec_signal_delay; -use crate::unix::{JobExt, PipelineExt}; +use crate::unix::{ExitStatusExt, JobExt, PipelineExt}; use crate::{Exec, ExecExt, ExitStatus, Redirection}; #[test] @@ -158,6 +158,14 @@ fn exit_status_display() { assert_eq!(ExitStatus::from_raw(9).to_string(), "signal 9"); } +// --- ExitStatusExt tests --- + +#[test] +fn exit_status_ext_round_trip() { + let status = ::from_raw(42 << 8); + assert_eq!(status.into_raw(), Some(42 << 8)); +} + // --- arg0 tests --- #[test] From ec2bb2b302af5dbc11c34b795430ff37278c5bf0 Mon Sep 17 00:00:00 2001 From: Hrvoje Niksic Date: Thu, 16 Apr 2026 22:46:39 +0200 Subject: [PATCH 3/3] Add unix ExecExt::pre_exec() to run closures between fork and exec Closures run after the other builder-configured setup (setuid/setgid/setpgid) and immediately before exec. They must be async-signal-safe. --- src/exec.rs | 33 ++++++++++++++++++++++++ src/spawn.rs | 11 +++++--- src/tests/posix.rs | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/src/exec.rs b/src/exec.rs index 0d6f1e7..0526ac8 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -859,6 +859,31 @@ pub mod unix { /// [`ProcessExt::send_signal_group`]: crate::unix::ProcessExt::send_signal_group /// [`PipelineExt::setpgid`]: PipelineExt::setpgid fn setpgid(self) -> Self; + + /// Schedule a closure to run in the child process between `fork()` and `exec()`. + /// + /// This is useful for performing setup that must happen in the child, such as + /// setting resource limits, calling `setsid()`, or closing extra file + /// descriptors. + /// + /// Multiple closures can be registered and they will run in the order they were + /// added. If any closure returns an error, the child process will exit and the + /// error will be reported to the parent. + /// + /// Closures run after all builder-configured setup and immediately before the + /// `exec()`. To run code with the parent's original privileges, omit the + /// corresponding builder methods and drop privileges manually at the end of the + /// closure. + /// + /// # Safety + /// + /// The closure runs after `fork()` in the child process. It must only call + /// async-signal-safe functions. In particular, it must not allocate, acquire + /// locks, or call into code that does so. Violating this may cause deadlocks or + /// undefined behavior in the child process. + unsafe fn pre_exec(self, f: F) -> Self + where + F: FnMut() -> io::Result<()> + Send + Sync + 'static; } impl ExecExt for Exec { @@ -876,6 +901,14 @@ pub mod unix { self.os_options.setpgid = Some(0); self } + + unsafe fn pre_exec(mut self, f: F) -> Exec + where + F: FnMut() -> io::Result<()> + Send + Sync + 'static, + { + self.os_options.pre_exec_fns.push(Box::new(f)); + self + } } /// Unix-specific extension methods for [`Pipeline`]. diff --git a/src/spawn.rs b/src/spawn.rs index 84ca21d..b3b0214 100644 --- a/src/spawn.rs +++ b/src/spawn.rs @@ -272,11 +272,12 @@ fn get_redirection_to_standard_stream(which: StandardStream) -> io::Result, pub setgid: Option, pub setpgid: Option, + pub pre_exec_fns: Vec io::Result<()> + Send + Sync>>, } impl OsOptions { @@ -350,7 +351,7 @@ pub(crate) mod os { } None => { drop(exec_fail_pipe.0); - let result = do_exec(just_exec, child_ends, do_chdir, &os_options); + let result = do_exec(just_exec, child_ends, do_chdir, os_options); let error_code = match result { Ok(()) => unreachable!(), Err(e) => e.raw_os_error().unwrap_or(-1), @@ -439,7 +440,7 @@ pub(crate) mod os { Option>, ), chdir: Option io::Result<()>>, - os_options: &OsOptions, + os_options: OsOptions, ) -> io::Result<()> { // Called after fork - use ManuallyDrop to prevent deallocation on // early return via ?. @@ -448,6 +449,7 @@ pub(crate) mod os { let mut stdout = std::mem::ManuallyDrop::new(stdout); let mut stderr = std::mem::ManuallyDrop::new(stderr); let mut just_exec = std::mem::ManuallyDrop::new(just_exec); + let mut os_options = std::mem::ManuallyDrop::new(os_options); if let Some(chdir) = chdir { chdir()?; @@ -467,6 +469,9 @@ pub(crate) mod os { if let Some(pgid) = os_options.setpgid { posix::setpgid(0, pgid)?; } + for f in &mut os_options.pre_exec_fns { + f()?; + } // SAFETY: just_exec is taken exactly once and not accessed afterward. let just_exec = unsafe { std::mem::ManuallyDrop::take(&mut just_exec) }; just_exec()?; diff --git a/src/tests/posix.rs b/src/tests/posix.rs index 0accc15..3fa219e 100644 --- a/src/tests/posix.rs +++ b/src/tests/posix.rs @@ -166,6 +166,68 @@ fn exit_status_ext_round_trip() { assert_eq!(status.into_raw(), Some(42 << 8)); } +// --- pre_exec tests --- + +#[test] +fn pre_exec_runs() { + // pre_exec calls _exit(42) directly; the child never reaches exec, and the parent + // observes the exit code. + let job = unsafe { + Exec::cmd("true") + .pre_exec(|| libc::_exit(42)) + .start() + .unwrap() + }; + let status = job.wait().unwrap(); + assert_eq!(status.code(), Some(42)); +} + +#[test] +fn pre_exec_error_reported() { + // A pre_exec closure that returns an error should cause start() to fail. + let result = unsafe { + Exec::cmd("true") + .pre_exec(|| Err(std::io::Error::from_raw_os_error(libc::EACCES))) + .start() + }; + let err = result.unwrap_err(); + assert_eq!(err.raw_os_error(), Some(libc::EACCES)); +} + +#[test] +fn pre_exec_multiple() { + // Each closure writes a distinct byte to a pipe the parent holds open; the parent + // reads back the bytes to verify both closures ran in registration order. + use std::io::Read; + use std::os::fd::AsRawFd; + let (mut read_end, write_end) = crate::posix::pipe().unwrap(); + let fd = write_end.as_raw_fd(); + let job = unsafe { + Exec::cmd("true") + .pre_exec(move || { + let n = libc::write(fd, b"1".as_ptr().cast(), 1); + if n != 1 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }) + .pre_exec(move || { + let n = libc::write(fd, b"2".as_ptr().cast(), 1); + if n != 1 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }) + .start() + .unwrap() + }; + drop(write_end); + let mut buf = [0u8; 2]; + read_end.read_exact(&mut buf).unwrap(); + assert_eq!(&buf, b"12"); + job.wait().unwrap(); +} + // --- arg0 tests --- #[test]