use std::ffi::OsStr;
use std::io;
use std::path::Path;
use std::process::{Child, ExitStatus, Output, Stdio};
use std::time::Instant;
use super::record::{Record, Stream};
use super::sink;
#[cfg(unix)]
fn exit_signal(status: &ExitStatus) -> Option<i32> {
use std::os::unix::process::ExitStatusExt;
status.signal()
}
fn reads_streams(jsonl: bool, transcribed: bool, transcribing: bool) -> bool {
jsonl || (transcribed && transcribing)
}
pub struct Command {
inner: std::process::Command,
stdout_set: bool,
stderr_set: bool,
transcribed: bool,
}
impl Command {
pub(crate) fn new(program: impl AsRef<OsStr>) -> Self {
Self {
inner: std::process::Command::new(program),
stdout_set: false,
stderr_set: false,
transcribed: false,
}
}
pub fn transcribed(&mut self) -> &mut Self {
self.transcribed = true;
self
}
pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
self.inner.arg(arg);
self
}
pub fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.inner.args(args);
self
}
pub fn env(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
self.inner.env(key, value);
self
}
pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.inner.envs(vars);
self
}
pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
self.inner.env_remove(key);
self
}
pub fn configured_env(&self) -> Vec<(String, Option<String>)> {
self.inner
.get_envs()
.map(|(name, value)| {
(
name.to_string_lossy().into_owned(),
value.map(|v| v.to_string_lossy().into_owned()),
)
})
.collect()
}
pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
self.inner.current_dir(dir);
self
}
pub fn stdin(&mut self, cfg: impl Into<Stdio>) -> &mut Self {
self.inner.stdin(cfg);
self
}
pub fn stdout(&mut self, cfg: impl Into<Stdio>) -> &mut Self {
self.stdout_set = true;
self.inner.stdout(cfg);
self
}
pub fn stderr(&mut self, cfg: impl Into<Stdio>) -> &mut Self {
self.stderr_set = true;
self.inner.stderr(cfg);
self
}
#[cfg(unix)]
pub unsafe fn pre_exec<F>(&mut self, f: F) -> &mut Self
where
F: FnMut() -> io::Result<()> + Send + Sync + 'static,
{
use std::os::unix::process::CommandExt;
self.inner.pre_exec(f);
self
}
#[cfg(unix)]
pub fn in_its_own_group(&mut self) -> &mut Self {
use std::os::unix::process::CommandExt;
unsafe {
self.inner.pre_exec(|| {
if libc::setsid() == -1 {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
self
}
#[cfg(windows)]
pub fn in_its_own_group(&mut self) -> &mut Self {
use std::os::windows::process::CommandExt;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
self.inner.creation_flags(CREATE_NEW_PROCESS_GROUP);
self
}
#[cfg(unix)]
pub fn exec(&mut self) -> io::Error {
if !sink::is_jsonl() {
use std::os::unix::process::CommandExt;
return self.inner.exec();
}
match self.status() {
Ok(status) => {
let code = status.code().unwrap_or_else(|| {
128 + exit_signal(&status).unwrap_or(0)
});
std::process::exit(sink::finish(code));
}
Err(error) => error,
}
}
pub fn get_envs(&self) -> std::process::CommandEnvs<'_> {
self.inner.get_envs()
}
pub fn argv(&self) -> Vec<String> {
let mut argv = vec![self.inner.get_program().to_string_lossy().into_owned()];
argv.extend(
self.inner
.get_args()
.map(|arg| arg.to_string_lossy().into_owned()),
);
argv
}
fn cwd(&self) -> Option<String> {
self.inner
.get_current_dir()
.map(|dir| dir.display().to_string())
}
#[cfg(unix)]
pub fn detached(&mut self) -> &mut Self {
use std::os::unix::process::CommandExt;
unsafe {
self.inner.pre_exec(|| {
if libc::setsid() == -1 {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
self
}
#[cfg(windows)]
pub fn detached(&mut self) -> &mut Self {
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x0000_0008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
self.inner
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
self
}
pub fn status(&mut self) -> io::Result<ExitStatus> {
let jsonl = sink::is_jsonl();
if !reads_streams(jsonl, self.transcribed, sink::transcribing()) {
return self.inner.status();
}
let echo = !jsonl;
let id = sink::next_subprocess_id();
let argv = self.argv();
let cwd = self.cwd();
let started = Instant::now();
let capture_stdout = !self.stdout_set;
let capture_stderr = !self.stderr_set;
if capture_stdout {
self.inner.stdout(Stdio::piped());
}
if capture_stderr {
self.inner.stderr(Stdio::piped());
}
let mut child = self.inner.spawn()?;
sink::emit(Record::SubprocessStarted {
id,
argv,
cwd,
pid: child.id(),
});
let out_reader = capture_stdout.then(|| {
let pipe = child.stdout.take().expect("stdout was configured as piped");
std::thread::spawn(move || sink::pump(id, Stream::Stdout, pipe, echo))
});
let err_reader = capture_stderr.then(|| {
let pipe = child.stderr.take().expect("stderr was configured as piped");
std::thread::spawn(move || sink::pump(id, Stream::Stderr, pipe, echo))
});
let status = child.wait()?;
if let Some(reader) = out_reader {
let _ = reader.join();
}
if let Some(reader) = err_reader {
let _ = reader.join();
}
sink::finished_child(id, &status, started);
Ok(status)
}
pub fn output(&mut self) -> io::Result<Output> {
if !sink::is_jsonl() {
return self.inner.output();
}
let id = sink::next_subprocess_id();
let started = Instant::now();
self.inner.stdin(Stdio::null());
self.inner.stdout(Stdio::piped());
self.inner.stderr(Stdio::piped());
let child = self.inner.spawn()?;
sink::emit(Record::SubprocessStarted {
id,
argv: self.argv(),
cwd: self.cwd(),
pid: child.id(),
});
let result = child.wait_with_output();
if let Ok(output) = &result {
sink::finished_child(id, &output.status, started);
}
result
}
pub fn spawn(&mut self) -> io::Result<Child> {
if !sink::is_jsonl() {
return self.inner.spawn();
}
let id = sink::next_subprocess_id();
let child = self.inner.spawn()?;
sink::emit(Record::SubprocessStarted {
id,
argv: self.argv(),
cwd: self.cwd(),
pid: child.id(),
});
Ok(child)
}
pub fn spawn_just_written(&mut self) -> io::Result<Child> {
for attempt in 0..5 {
match self.spawn() {
Err(busy) if busy.kind() == io::ErrorKind::ExecutableFileBusy => {
std::thread::sleep(std::time::Duration::from_millis(2 << attempt));
}
other => return other,
}
}
self.spawn()
}
}
#[cfg(test)]
mod spawn_tests {
use super::*;
#[test]
fn test12322_a_transcribed_child_is_read_into_the_log() {
assert!(reads_streams(false, true, true), "a transcribed child with a log is read");
assert!(!reads_streams(false, false, true), "an untranscribed child keeps the terminal");
assert!(!reads_streams(false, true, false), "nothing to record into: left on the terminal");
assert!(reads_streams(true, false, false), "JSONL reads every child");
}
#[cfg(unix)]
#[test]
fn test11530_a_busy_executable_is_waited_for() {
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("a scratch directory");
let program = dir.path().join("program");
let mut written = std::fs::File::create(&program).expect("the file is created");
written.write_all(b"#!/bin/sh\nexit 0\n").expect("written");
written.flush().expect("flushed");
std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755))
.expect("executable");
let arranged = Command::new(&program).spawn(); match arranged {
Err(busy) if busy.kind() == std::io::ErrorKind::ExecutableFileBusy => {}
Ok(mut ran) => {
let _ = ran.wait();
return;
}
Err(other) => panic!(
"the fixture failed for a reason that is not the one under test: {other}"
),
}
let holder = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(15));
drop(written);
});
let mut child = Command::new(&program) .spawn_just_written()
.expect("a busy executable is waited for, not refused");
assert!(child.wait().expect("it finishes").success());
holder.join().expect("the descriptor is released");
}
#[test]
fn test11531_a_refusal_that_is_not_busy_is_returned_immediately() {
let dir = tempfile::tempdir().expect("a scratch directory");
let missing = dir.path().join("not-here");
let started = std::time::Instant::now();
let error = Command::new(&missing) .spawn_just_written()
.map(|_| ())
.expect_err("a program that does not exist cannot be spawned");
assert_eq!(error.kind(), std::io::ErrorKind::NotFound, "{error}");
assert!(
started.elapsed() < std::time::Duration::from_millis(50),
"a refusal that will never change was retried for {:?}",
started.elapsed()
);
}
}