use std::process::{Child, Command, Stdio};
pub(crate) struct ChildGuard {
label: &'static str,
child: Option<Child>,
}
impl ChildGuard {
pub(crate) fn spawn(label: &'static str, command: &mut Command) -> std::io::Result<Self> {
let child = command.spawn()?;
Ok(Self {
label,
child: Some(child),
})
}
pub(crate) fn id(&self) -> Option<u32> {
self.child.as_ref().map(Child::id)
}
pub(crate) fn exited(&mut self) -> Option<std::process::ExitStatus> {
self.child.as_mut()?.try_wait().ok().flatten()
}
pub(crate) fn stop(&mut self) {
let pid = self.id();
let Some(mut child) = self.child.take() else {
return;
};
let _ = child.kill();
if let Err(error) = child.wait() {
let pid = pid.map_or_else(|| String::from("unknown"), |pid| pid.to_string());
eprintln!(
"arc dev: could not reap the {} process (pid {pid}): {error}. \
It is still holding an IPC endpoint; kill it before starting `arc dev` again.",
self.label
);
}
}
}
impl Drop for ChildGuard {
fn drop(&mut self) {
self.stop();
}
}
pub(crate) fn inherited(program: &str) -> Command {
let mut command = Command::new(program);
command.stdout(Stdio::inherit()).stderr(Stdio::inherit());
command.stdin(Stdio::piped());
command
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_guard_reports_the_pid_of_a_live_child() {
let mut command = if cfg!(windows) {
let mut c = Command::new("cmd");
c.args(["/C", "exit 0"]);
c
} else {
let mut c = Command::new("sh");
c.args(["-c", "exit 0"]);
c
};
command.stdout(Stdio::null()).stderr(Stdio::null());
let mut guard = ChildGuard::spawn("test", &mut command).expect("spawn should succeed");
assert!(guard.id().is_some());
guard.stop();
assert!(guard.id().is_none());
}
#[test]
fn a_child_is_given_a_stdin_the_supervisor_holds_open() {
let mut command = inherited(if cfg!(windows) { "cmd" } else { "sh" });
command.args(if cfg!(windows) {
["/C", "exit 0"]
} else {
["-c", "exit 0"]
});
command.stdout(Stdio::null()).stderr(Stdio::null());
let mut guard = ChildGuard::spawn("test", &mut command).expect("spawn should succeed");
assert!(
guard.child.as_ref().is_some_and(|c| c.stdin.is_some()),
"the write end must stay with the guard; dropping it early would \
tell the child it was orphaned while the supervisor is alive"
);
guard.stop();
}
#[test]
fn stopping_twice_is_not_an_error() {
let mut command = if cfg!(windows) {
let mut c = Command::new("cmd");
c.args(["/C", "exit 0"]);
c
} else {
let mut c = Command::new("sh");
c.args(["-c", "exit 0"]);
c
};
command.stdout(Stdio::null()).stderr(Stdio::null());
let mut guard = ChildGuard::spawn("test", &mut command).expect("spawn should succeed");
guard.stop();
guard.stop();
}
#[test]
fn spawning_a_program_that_is_not_installed_is_an_error() {
let mut command = Command::new("arcature-no-such-program-exists");
assert!(ChildGuard::spawn("test", &mut command).is_err());
}
}