Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OsString>) -> 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.
Expand Down Expand Up @@ -846,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<F>(self, f: F) -> Self
where
F: FnMut() -> io::Result<()> + Send + Sync + 'static;
}

impl ExecExt for Exec {
Expand All @@ -863,6 +901,14 @@ pub mod unix {
self.os_options.setpgid = Some(0);
self
}

unsafe fn pre_exec<F>(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`].
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,13 @@ 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;
}

/// Subprocess extensions for Windows platforms.
#[cfg(any(windows, docsrs))]
pub mod windows {
pub use super::exec::windows::*;
pub use super::process::windows::ExitStatusExt;
}
65 changes: 65 additions & 0 deletions src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<i32>;
}

impl ExitStatusExt for ExitStatus {
fn from_raw(raw: i32) -> ExitStatus {
ExitStatus::from_raw(raw)
}

fn into_raw(self) -> Option<i32> {
self.0
}
}
}
}

Expand Down Expand Up @@ -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<u32>;
}

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<u32> {
#[cfg(windows)]
{
self.0
}
#[cfg(not(windows))]
{
unimplemented!()
}
}
}
}
11 changes: 8 additions & 3 deletions src/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,12 @@ fn get_redirection_to_standard_stream(which: StandardStream) -> io::Result<Arc<R
pub(crate) mod os {
use super::*;

#[derive(Clone, Default)]
#[derive(Default)]
pub struct OsOptions {
pub setuid: Option<u32>,
pub setgid: Option<u32>,
pub setpgid: Option<u32>,
pub pre_exec_fns: Vec<Box<dyn FnMut() -> io::Result<()> + Send + Sync>>,
}

impl OsOptions {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -439,7 +440,7 @@ pub(crate) mod os {
Option<Arc<Redirection>>,
),
chdir: Option<impl FnOnce() -> io::Result<()>>,
os_options: &OsOptions,
os_options: OsOptions,
) -> io::Result<()> {
// Called after fork - use ManuallyDrop to prevent deallocation on
// early return via ?.
Expand All @@ -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()?;
Expand All @@ -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()?;
Expand Down
85 changes: 84 additions & 1 deletion src/tests/posix.rs
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -158,6 +158,89 @@ 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 = <ExitStatus as ExitStatusExt>::from_raw(42 << 8);
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]
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]
Expand Down
Loading