use std::io;
use std::process::{Child, Command, ExitStatus, Output};
pub fn spawn(command: &mut Command) -> io::Result<Child> {
command.spawn()
}
pub fn status(command: &mut Command) -> io::Result<ExitStatus> {
command.status()
}
pub fn output(command: &mut Command) -> io::Result<Output> {
command.output()
}
#[cfg(unix)]
pub fn exec(command: &mut Command) -> io::Error {
use std::os::unix::process::CommandExt;
command.exec()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spawn_preserves_native_missing_program_error() {
let directory = tempfile::tempdir().expect("private fixture directory");
let mut command = Command::new(directory.path().join("absent-executable"));
let error = spawn(&mut command).expect_err("no child should be created");
assert_eq!(error.kind(), io::ErrorKind::NotFound);
}
#[cfg(unix)]
#[test]
fn spawn_transfers_child_and_preserves_configured_output() {
use std::io::{Read, Seek};
use std::process::Stdio;
use std::time::{Duration, Instant};
let mut destination = tempfile::tempfile().expect("output file");
let mut command = Command::new("/bin/sh");
command
.args(["-c", "printf '%s' \"$MARKER\"; exit 7"])
.env("MARKER", "caller-configured-output")
.stdin(Stdio::null())
.stderr(Stdio::null())
.stdout(Stdio::from(destination.try_clone().unwrap()));
let mut child = spawn(&mut command).expect("foreground spawn");
assert!(
child.stdout.is_none(),
"file binding must not become a capture pipe"
);
let deadline = Instant::now() + Duration::from_secs(5);
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break status,
Ok(None) if Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(10));
}
outcome => {
let _ = child.kill();
let _ = child.wait();
panic!("foreground fixture did not complete: {outcome:?}");
}
}
};
assert_eq!(status.code(), Some(7));
destination.rewind().unwrap();
let mut bytes = Vec::new();
destination.read_to_end(&mut bytes).unwrap();
assert_eq!(bytes, b"caller-configured-output");
}
#[cfg(unix)]
#[test]
fn exec_returns_native_missing_image_error() {
let directory = tempfile::tempdir().expect("private fixture directory");
let mut command = Command::new(directory.path().join("absent-executable"));
let error = exec(&mut command);
assert_eq!(error.kind(), io::ErrorKind::NotFound);
assert_eq!(error.raw_os_error(), Some(libc::ENOENT));
}
#[cfg(unix)]
#[test]
fn output_preserves_cwd_environment_literal_argv_and_native_nonzero() {
let directory = tempfile::tempdir().expect("fixture cwd");
let cwd = directory.path().canonicalize().expect("physical cwd");
let mut command = Command::new("/bin/sh");
command.args(["-c", "[ \"$#\" = 2 ] && [ -z \"$2\" ] || exit 9; printf '%s\\n%s\\n%s\\n' \"$PWD\" \"$MARKER\" \"$1\"; exit 7", "foreground", "$literal; not a command", ""]);
command.current_dir(&cwd).env("MARKER", "foreground-env");
let output = output(&mut command).expect("foreground output");
assert_eq!(output.status.code(), Some(7));
let expected = format!(
"{}\nforeground-env\n$literal; not a command\n",
cwd.display()
);
assert_eq!(output.stdout, expected.as_bytes());
assert!(output.stderr.is_empty());
}
#[cfg(windows)]
#[test]
fn output_preserves_windows_environment_cwd_and_nonzero_status() {
let directory = tempfile::tempdir().expect("fixture cwd");
let mut command = Command::new("cmd.exe");
command.args(["/D", "/C", "cd & echo %MARKER% & exit /b 7"]);
command
.current_dir(directory.path())
.env("MARKER", "foreground-env");
let captured = output(&mut command).expect("foreground output");
assert_eq!(captured.status.code(), Some(7));
let text = String::from_utf8_lossy(&captured.stdout);
let mut lines = text.lines();
let actual = std::path::Path::new(lines.next().expect("cwd line"));
assert_eq!(
actual.canonicalize().unwrap(),
directory.path().canonicalize().unwrap()
);
assert_eq!(
lines.next().expect("environment line").trim(),
"foreground-env"
);
}
#[cfg(target_os = "linux")]
#[test]
fn foreground_keeps_parent_process_group_and_session() {
let parent = std::fs::read_to_string("/proc/self/stat").expect("parent stat");
let mut command = Command::new("/bin/sh");
command.args(["-c", "cut -d' ' -f5,6 /proc/self/stat"]);
let output = output(&mut command).expect("foreground child");
let parent_fields: Vec<_> = parent
.rsplit_once(") ")
.expect("stat")
.1
.split_whitespace()
.collect();
let child = String::from_utf8_lossy(&output.stdout);
assert_eq!(
child.trim(),
format!("{} {}", parent_fields[2], parent_fields[3])
);
}
#[cfg(unix)]
#[test]
fn both_execution_forms_preserve_explicit_stdio_overrides() {
use std::io::{Read, Seek, Write};
use std::process::Stdio;
for capture in [false, true] {
let mut input = tempfile::tempfile().unwrap();
input.write_all(b"caller-controlled input").unwrap();
input.rewind().unwrap();
let mut destination = tempfile::tempfile().unwrap();
let mut command = Command::new("/bin/sh");
command
.args(["-c", "cat; exit 7"])
.stdin(Stdio::from(input))
.stdout(Stdio::from(destination.try_clone().unwrap()))
.stderr(Stdio::null());
if capture {
let captured = output(&mut command).unwrap();
assert_eq!(captured.status.code(), Some(7));
assert!(captured.stdout.is_empty());
assert!(captured.stderr.is_empty());
} else {
assert_eq!(status(&mut command).unwrap().code(), Some(7));
}
destination.rewind().unwrap();
let mut bytes = Vec::new();
destination.read_to_end(&mut bytes).unwrap();
assert_eq!(bytes, b"caller-controlled input");
}
}
#[cfg(target_os = "linux")]
#[test]
fn foreground_does_not_close_an_inheritable_high_descriptor() {
use std::io::{Seek, Write};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
let mut file = tempfile::tempfile().expect("fixture file");
file.write_all(b"inherited-resource").unwrap();
file.rewind().unwrap();
let descriptor = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_DUPFD, 64) };
assert!(descriptor >= 64);
let descriptor = unsafe { OwnedFd::from_raw_fd(descriptor) };
let mut command = Command::new("/bin/sh");
command.args([
"-c",
"cat \"/proc/self/fd/$1\"",
"foreground",
&descriptor.as_raw_fd().to_string(),
]);
let captured = output(&mut command).expect("inherited descriptor read");
assert!(captured.status.success());
assert_eq!(captured.stdout, b"inherited-resource");
}
}