Skip to main content

podbox/
process.rs

1use std::ffi::OsString;
2use std::io::{self, IoSlice, IoSliceMut};
3use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
4use std::os::unix::net::UnixStream;
5use std::os::unix::process::CommandExt;
6use std::process::{Command, ExitStatus, Output};
7use std::time::Duration;
8
9use nix::sys::signal::{Signal, kill};
10use nix::sys::socket::{ControlMessage, ControlMessageOwned, MsgFlags, recvmsg, sendmsg};
11use nix::unistd::Pid;
12
13/// Build a `Vec<OsString>` from a slice of `&str`/`&String` literals.
14pub fn args<S: AsRef<str>>(items: &[S]) -> Vec<OsString> {
15    items.iter().map(|s| OsString::from(s.as_ref())).collect()
16}
17
18/// Replace the current process with the given binary and arguments.
19///
20/// Uses `CommandExt::exec()` so the shell gets a real TTY.
21/// On success this function never returns; on failure it returns an error.
22pub fn exec_replace(bin: &str, args: &[OsString]) -> anyhow::Error {
23    let mut cmd = Command::new(bin);
24    cmd.args(args);
25    let err = cmd.exec();
26    anyhow::Error::from(err).context(format!("failed to exec {}", bin))
27}
28
29/// Run a command, capturing stdout and stderr.
30pub fn run_piped(bin: &str, args: &[OsString]) -> anyhow::Result<Output> {
31    let output = Command::new(bin)
32        .args(args)
33        .stdout(std::process::Stdio::piped())
34        .stderr(std::process::Stdio::piped())
35        .spawn()?
36        .wait_with_output()?;
37    Ok(output)
38}
39
40/// Spawn a command attached to the current terminal.
41pub fn spawn_interactive(bin: &str, args: &[OsString]) -> anyhow::Result<ExitStatus> {
42    let status = Command::new(bin).args(args).status()?;
43    Ok(status)
44}
45
46/// Run a command with a timeout, capturing stdout and stderr.
47///
48/// The child process receives SIGKILL after `timeout` if it has not exited.
49pub fn run_piped_timeout(
50    bin: &str,
51    args: &[OsString],
52    timeout: Duration,
53) -> anyhow::Result<Output> {
54    let child = Command::new(bin)
55        .args(args)
56        .stdout(std::process::Stdio::piped())
57        .stderr(std::process::Stdio::piped())
58        .spawn()?;
59    wait_child_timeout(child, timeout)
60}
61
62/// Run an interactive command with a timeout (SIGTERM, then SIGKILL).
63///
64/// Unlike `run_piped_timeout`, this sends SIGTERM first with a 5-second
65/// grace period before SIGKILL, giving well-behaved processes a chance
66/// to clean up.
67pub fn spawn_interactive_timeout(
68    bin: &str,
69    args: &[OsString],
70    timeout: Duration,
71) -> anyhow::Result<ExitStatus> {
72    let mut child = Command::new(bin).args(args).spawn()?;
73    let pid = Pid::from_raw(child.id().cast_signed());
74    let (tx, rx) = std::sync::mpsc::channel();
75    std::thread::spawn(move || {
76        if rx.recv_timeout(timeout).is_err() {
77            let _ = kill(pid, Signal::SIGTERM);
78            std::thread::sleep(Duration::from_secs(5));
79            let _ = kill(pid, Signal::SIGKILL);
80        }
81    });
82    let status = child.wait()?;
83    let _ = tx.send(());
84    Ok(status)
85}
86
87/// Wait for a child process to complete, enforcing a timeout via SIGKILL.
88pub fn wait_child_timeout(child: std::process::Child, timeout: Duration) -> anyhow::Result<Output> {
89    let pid = Pid::from_raw(child.id().cast_signed());
90    let (tx, rx) = std::sync::mpsc::channel();
91    std::thread::spawn(move || {
92        if rx.recv_timeout(timeout).is_err() {
93            let _ = kill(pid, Signal::SIGKILL);
94        }
95    });
96    let output = child.wait_with_output()?;
97    let _ = tx.send(());
98    Ok(output)
99}
100
101/// Open a pidfd for a given PID (Linux 5.3+).
102///
103/// Returns `Err` on old kernels or when the PID does not exist.
104pub fn open_pidfd(pid: i32) -> io::Result<OwnedFd> {
105    let ret = unsafe { nix::libc::syscall(nix::libc::SYS_pidfd_open, pid, 0) };
106    if ret < 0 {
107        Err(io::Error::last_os_error())
108    } else {
109        let fd = i32::try_from(ret).map_err(|_| {
110            io::Error::new(
111                io::ErrorKind::InvalidInput,
112                "pidfd_open returned invalid fd",
113            )
114        })?;
115        // SAFETY: fd is a non-negative fd returned by the kernel.
116        Ok(unsafe { OwnedFd::from_raw_fd(fd) })
117    }
118}
119
120/// Send a raw file descriptor over a connected Unix stream via `SCM_RIGHTS`.
121///
122/// Sends one dummy byte alongside the descriptor so the receiver can detect EOF.
123/// Retries on `EINTR` to prevent spurious session drops.
124pub fn send_fd(stream: &UnixStream, fd: RawFd) -> io::Result<()> {
125    let raw_fd = stream.as_raw_fd();
126    let cmsg = ControlMessage::ScmRights(&[fd]);
127    let iov = [IoSlice::new(&[0u8])];
128    loop {
129        match sendmsg::<()>(raw_fd, &iov, &[cmsg], MsgFlags::empty(), None) {
130            Ok(_) => return Ok(()),
131            Err(nix::errno::Errno::EINTR) => {}
132            Err(e) => return Err(io::Error::from(e)),
133        }
134    }
135}
136
137/// Receive a raw file descriptor from a connected Unix stream via `SCM_RIGHTS`.
138///
139/// Returns `None` when the sender has closed the connection (EOF).
140/// Retries on `EINTR` to prevent spurious session drops.
141pub fn recv_fd(stream: &UnixStream) -> io::Result<Option<RawFd>> {
142    let raw_fd = stream.as_raw_fd();
143    let mut buf = [0u8; 1];
144    let mut iov = [IoSliceMut::new(&mut buf)];
145    let mut cmsg_buf = vec![0u8; 256];
146    let msg = loop {
147        match recvmsg::<()>(raw_fd, &mut iov, Some(&mut cmsg_buf), MsgFlags::empty()) {
148            Ok(m) => break m,
149            Err(nix::errno::Errno::EINTR) => {}
150            Err(e) => return Err(io::Error::from(e)),
151        }
152    };
153
154    if msg.bytes == 0 {
155        return Ok(None);
156    }
157
158    if let Ok(cmsgs) = msg.cmsgs() {
159        for cmsg in cmsgs {
160            if let ControlMessageOwned::ScmRights(fds) = cmsg {
161                if let Some(&fd) = fds.first() {
162                    return Ok(Some(fd));
163                }
164            }
165        }
166    }
167    Ok(None)
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn args_builds_osstring_vec() {
176        let v = args(&["foo", "bar", "baz"]);
177        assert_eq!(v.len(), 3);
178        assert_eq!(v[0], "foo");
179        assert_eq!(v[1], "bar");
180        assert_eq!(v[2], "baz");
181    }
182
183    #[test]
184    fn args_accepts_mixed_types() {
185        let s = String::from("hello");
186        let v = args(&["a", &s, "c"]);
187        assert_eq!(v[1], "hello");
188    }
189}