Skip to main content

rig_tools/
process.rs

1//! Child processes started in a session of their own, such as `shell`
2//! commands, so they never read the terminal and are killed as a group, and
3//! the [`Launcher`] that says how every tool starts them. A [`Process`] it
4//! [spawns](Launcher::spawn) takes its input, hands its output over line
5//! by line, and is stopped gently or killed with its whole group.
6
7use std::ffi::{OsStr, OsString};
8use std::io::{self, Read, Write as _};
9use std::path::PathBuf;
10use std::process::{Child, Command, ExitStatus, Output, Stdio};
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU32, Ordering};
13use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
14use std::thread;
15use std::time::{Duration, Instant};
16
17/// How tools start processes: under a wrapper program (a sandbox such as
18/// `bwrap … --`, `nice -n 10`, a container's `exec`), in a directory, with
19/// variables set and removed. Every command it builds is [`detach`]ed. A
20/// host that hands one launcher to every tool that starts processes
21/// ([`Shell`](crate::Shell), a background job, a formatter) puts them all
22/// under the same policy.
23///
24/// ```
25/// use rig_tools::process::Launcher;
26///
27/// let launcher = Launcher::default()
28///     .wrapped(["nice", "-n", "10"])
29///     .in_dir("/srv/project")
30///     .with_env("CI", "1")
31///     .without_env(["AGENT_TOKEN"]);
32/// let mut command = launcher.command("sh");
33/// command.args(["-c", "make test"]);
34/// assert_eq!(command.get_program(), "nice");
35/// let args: Vec<_> = command.get_args().collect();
36/// assert_eq!(args, ["-n", "10", "sh", "-c", "make test"]);
37/// assert_eq!(command.get_current_dir(), Some(std::path::Path::new("/srv/project")));
38/// ```
39#[derive(Clone, Debug, Default, PartialEq, Eq)]
40pub struct Launcher {
41    /// The program and arguments every command runs under; empty runs the
42    /// command itself.
43    pub prefix: Vec<OsString>,
44    /// The directory commands start in; `None` is this process's.
45    pub cwd: Option<PathBuf>,
46    /// Variables set for every command.
47    pub env: Vec<(OsString, OsString)>,
48    /// Variables no command inherits, such as those that make a program
49    /// act as the agent that runs it.
50    pub unset_env: Vec<OsString>,
51}
52
53impl Launcher {
54    /// The same launcher, running every command under `prefix`.
55    pub fn wrapped(mut self, prefix: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
56        self.prefix = prefix.into_iter().map(Into::into).collect();
57        self
58    }
59
60    /// The same launcher, starting commands in `dir`.
61    pub fn in_dir(mut self, dir: impl Into<PathBuf>) -> Self {
62        self.cwd = Some(dir.into());
63        self
64    }
65
66    /// The same launcher, setting `name` to `value` for every command.
67    pub fn with_env(mut self, name: impl Into<OsString>, value: impl Into<OsString>) -> Self {
68        self.env.push((name.into(), value.into()));
69        self
70    }
71
72    /// The same launcher, keeping `names` from every command.
73    pub fn without_env(mut self, names: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
74        self.unset_env.extend(names.into_iter().map(Into::into));
75        self
76    }
77
78    /// A detached command that runs `program` under this launcher's policy;
79    /// add the program's arguments to it.
80    pub fn command(&self, program: impl AsRef<OsStr>) -> Command {
81        let mut command = match self.prefix.split_first() {
82            Some((wrapper, arguments)) => {
83                let mut command = Command::new(wrapper);
84                command.args(arguments).arg(program);
85                command
86            }
87            None => Command::new(program),
88        };
89        if let Some(dir) = &self.cwd {
90            command.current_dir(dir);
91        }
92        for name in &self.unset_env {
93            command.env_remove(name);
94        }
95        command.envs(self.env.iter().map(|(name, value)| (name, value)));
96        detach(&mut command);
97        command
98    }
99
100    /// Starts `program` with `args` under this launcher's policy, as
101    /// [`command`](Self::command) builds it, with stdin and output as `how`
102    /// says.
103    ///
104    /// ```
105    /// # #[cfg(unix)] {
106    /// use rig_tools::process::{Launcher, OutputLine, Spawn};
107    ///
108    /// let how = Spawn { stdin: Some(b"hello\n".to_vec()), ..Spawn::default() };
109    /// let mut process = Launcher::default().spawn("cat", ["-"], how)?;
110    /// let status = process.wait_for(std::time::Duration::from_secs(5))?;
111    /// assert!(status.is_some_and(|status| status.success()));
112    /// let lines: Vec<OutputLine> = process.lines().iter().collect();
113    /// assert_eq!(lines, [OutputLine::Out("hello\n".to_owned())]);
114    /// # }
115    /// # Ok::<(), std::io::Error>(())
116    /// ```
117    pub fn spawn(
118        &self,
119        program: impl AsRef<OsStr>,
120        args: impl IntoIterator<Item = impl AsRef<OsStr>>,
121        how: Spawn,
122    ) -> io::Result<Process> {
123        let mut command = self.command(program);
124        command.args(args).stdin(match how.stdin {
125            Some(_) => Stdio::piped(),
126            None => Stdio::null(),
127        });
128        let (sender, lines) = channel();
129        let mut pipes: Vec<(Box<dyn Read + Send>, Tag)> = Vec::new();
130        if how.merge_stderr {
131            // One pipe for both keeps their order.
132            let (reader, writer) = io::pipe()?;
133            command.stdout(writer.try_clone()?).stderr(writer);
134            pipes.push((Box::new(reader), OutputLine::Out));
135        } else {
136            command.stdout(Stdio::piped()).stderr(Stdio::piped());
137        }
138        let spawned = command.spawn();
139        // The parent's copies of a shared pipe's write end must close, or
140        // reading never ends.
141        drop(command);
142        let child = spawned?;
143        let group = Group(Arc::new(AtomicU32::new(child.id())));
144        // From here on, an error drops the process, which kills it.
145        let mut process = Process {
146            child,
147            lines,
148            group,
149            remote: None,
150            forced: false,
151        };
152        let (stdout, stderr) = (process.child.stdout.take(), process.child.stderr.take());
153        #[cfg(unix)]
154        let (stdout, stderr) = match (stdout, stderr) {
155            (Some(stdout), Some(stderr)) => {
156                let sender = sender.clone();
157                thread::Builder::new()
158                    .name("rig-process-output".to_owned())
159                    .spawn(move || forward_both(stdout, stderr, &sender))?;
160                (None, None)
161            }
162            apart => apart,
163        };
164        if let Some(stdout) = stdout {
165            pipes.push((Box::new(stdout), OutputLine::Out));
166        }
167        if let Some(stderr) = stderr {
168            pipes.push((Box::new(stderr), OutputLine::Err));
169        }
170        for (pipe, tag) in pipes {
171            let sender = sender.clone();
172            thread::Builder::new()
173                .name("rig-process-output".to_owned())
174                .spawn(move || forward(pipe, tag, &sender))?;
175        }
176        if let (Some(mut stdin), Some(bytes)) = (process.child.stdin.take(), how.stdin) {
177            // Written on a thread of its own, so a child that reads slowly,
178            // or not at all, never blocks the caller; closed once written.
179            thread::Builder::new()
180                .name("rig-process-input".to_owned())
181                .spawn(move || {
182                    stdin.write_all(&bytes).ok();
183                })?;
184        }
185        Ok(process)
186    }
187}
188
189/// How [`Launcher::spawn`] connects a process.
190#[derive(Clone, Debug, Default, PartialEq, Eq)]
191pub struct Spawn {
192    /// What the process reads on stdin, which is closed after it; `None`
193    /// gives it an empty stdin.
194    pub stdin: Option<Vec<u8>>,
195    /// Whether stderr goes into stdout's pipe, so the two keep their order
196    /// and every line arrives as [`OutputLine::Out`].
197    pub merge_stderr: bool,
198}
199
200/// A piece of a process's output, as [`Process::lines`] hands it over: a
201/// line with its `\n`, or the end of the output without one. A line longer
202/// than 64 KB comes in pieces. Bytes that are not UTF-8 are replaced.
203#[derive(Clone, Debug, PartialEq, Eq)]
204pub enum OutputLine {
205    /// From stdout.
206    Out(String),
207    /// From stderr.
208    Err(String),
209}
210
211impl OutputLine {
212    /// The text, whichever stream it came from.
213    pub fn text(&self) -> &str {
214        match self {
215            Self::Out(text) | Self::Err(text) => text,
216        }
217    }
218}
219
220type Tag = fn(String) -> OutputLine;
221
222/// How often a waiting [`Process`] is checked.
223const POLL: Duration = Duration::from_millis(10);
224
225/// How long [`Process::run`] reads on after the process exited, for output
226/// something it left running still writes, and again after a kill.
227const PIPE_GRACE: Duration = Duration::from_millis(100);
228
229/// How long a process asked to stop (`SIGTERM`) has to end before it is
230/// killed: time for a transport that hangs up the command it runs on
231/// another machine when it is stopped to do so.
232pub const STOP_GRACE: Duration = Duration::from_secs(2);
233
234/// The longest piece of a line [`forward`] hands over at once.
235const MAX_PIECE: usize = 64 * 1024;
236
237/// Reads `pipe` until every writer closed it, or the receiver is gone,
238/// and sends what it reads on line by line.
239///
240/// When the receiver stops listening while a process that left the group
241/// still holds the pipe, this thread stays blocked in `read` until that
242/// process writes again or exits; its next write then finds the receiver
243/// gone, the read end is dropped, and the process gets `EPIPE` or
244/// `SIGPIPE`, as after any closed pipe.
245fn forward(mut pipe: impl Read, tag: Tag, lines: &Sender<OutputLine>) {
246    let mut pending = Vec::new();
247    let mut buffer = [0; 8192];
248    loop {
249        let read = match pipe.read(&mut buffer) {
250            Ok(0) => break,
251            Ok(read) => read,
252            Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
253            Err(_) => break,
254        };
255        pending.extend_from_slice(buffer.get(..read).unwrap_or_default());
256        let mut start = 0;
257        while let Some(end) = pending.get(start..).and_then(piece_end) {
258            let piece = pending.get(start..start + end).unwrap_or_default();
259            if lines
260                .send(tag(String::from_utf8_lossy(piece).into_owned()))
261                .is_err()
262            {
263                return;
264            }
265            start += end;
266        }
267        pending.drain(..start);
268    }
269    if !pending.is_empty() {
270        lines
271            .send(tag(String::from_utf8_lossy(&pending).into_owned()))
272            .ok();
273    }
274}
275
276/// Reads a process's stdout and stderr on one thread, whichever has data
277/// first, stdout first when both have, and sends each on line by line as
278/// [`forward`] does: so the lines of the two keep the order they were
279/// written in, as far as one reader can tell, and a line one writes in
280/// pieces is never split by the other's.
281#[cfg(unix)]
282fn forward_both(
283    stdout: std::process::ChildStdout,
284    stderr: std::process::ChildStderr,
285    lines: &Sender<OutputLine>,
286) {
287    use std::os::fd::AsRawFd;
288
289    let fds = [stdout.as_raw_fd(), stderr.as_raw_fd()];
290    let mut pipes: [(Box<dyn Read>, Tag, Vec<u8>, bool); 2] = [
291        (Box::new(stdout), OutputLine::Out, Vec::new(), true),
292        (Box::new(stderr), OutputLine::Err, Vec::new(), true),
293    ];
294    let mut buffer = [0; 8192];
295    while pipes.iter().any(|(.., open)| *open) {
296        let mut polled = fds.map(|fd| libc::pollfd {
297            fd,
298            events: libc::POLLIN,
299            revents: 0,
300        });
301        for (poll, (.., open)) in polled.iter_mut().zip(&pipes) {
302            if !*open {
303                poll.fd = -1;
304            }
305        }
306        // SAFETY: `polled` is an array of two initialized `pollfd`s that
307        // outlives the call, and its length is passed with it.
308        let ready = unsafe { libc::poll(polled.as_mut_ptr(), 2, -1) };
309        if ready < 0 {
310            if io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
311                continue;
312            }
313            break;
314        }
315        for (poll, (pipe, tag, pending, open)) in polled.iter().zip(&mut pipes) {
316            if !*open || poll.revents == 0 {
317                continue;
318            }
319            let read = match pipe.read(&mut buffer) {
320                Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
321                Ok(0) | Err(_) => None,
322                Ok(read) => Some(read),
323            };
324            let Some(read) = read else {
325                *open = false;
326                if !pending.is_empty() {
327                    let text = String::from_utf8_lossy(pending).into_owned();
328                    pending.clear();
329                    if lines.send(tag(text)).is_err() {
330                        return;
331                    }
332                }
333                continue;
334            };
335            pending.extend_from_slice(buffer.get(..read).unwrap_or_default());
336            let mut start = 0;
337            while let Some(end) = pending.get(start..).and_then(piece_end) {
338                let piece = pending.get(start..start + end).unwrap_or_default();
339                if lines
340                    .send(tag(String::from_utf8_lossy(piece).into_owned()))
341                    .is_err()
342                {
343                    return;
344                }
345                start += end;
346            }
347            pending.drain(..start);
348        }
349    }
350}
351
352/// Where the first piece of `bytes` ends: after its first `\n`, else after
353/// [`MAX_PIECE`] bytes, not inside a character; `None` while it is shorter.
354fn piece_end(bytes: &[u8]) -> Option<usize> {
355    if let Some(at) = bytes.iter().position(|&byte| byte == b'\n') {
356        return Some(at + 1);
357    }
358    let head = bytes.get(..MAX_PIECE)?;
359    match std::str::from_utf8(head) {
360        Err(error) if error.error_len().is_none() && error.valid_up_to() > 0 => {
361            Some(error.valid_up_to())
362        }
363        _ => Some(head.len()),
364    }
365}
366
367/// A process [`Launcher::spawn`] started, the leader of a process group of
368/// its own. Dropping it while it runs kills the group and reaps it.
369pub struct Process {
370    child: Child,
371    lines: Receiver<OutputLine>,
372    group: Group,
373    /// The machine it is the local end of a transport to, if it is one.
374    remote: Option<String>,
375    /// Whether the last stop had to kill it.
376    forced: bool,
377}
378
379impl Process {
380    /// The process's id, which is also its group's.
381    pub fn id(&self) -> u32 {
382        self.child.id()
383    }
384
385    /// The same process, marked as the local end of a transport that runs
386    /// the command on the machine `host`: stopping it stops the transport,
387    /// and what that does there is the transport's to say.
388    pub fn through(mut self, host: impl Into<String>) -> Self {
389        self.remote = Some(host.into());
390        self
391    }
392
393    /// The machine it runs the command on through a transport, if it was
394    /// marked so ([`through`](Self::through)).
395    pub fn remote(&self) -> Option<&str> {
396        self.remote.as_deref()
397    }
398
399    /// Whether the last [`stop`](Self::stop) had to kill it: it was still
400    /// running its grace period after `SIGTERM`.
401    pub fn forced(&self) -> bool {
402        self.forced
403    }
404
405    /// Its output, line by line, as it comes. Lines wait here until they
406    /// are received; the channel disconnects once every process holding
407    /// the output closed it.
408    pub fn lines(&self) -> &Receiver<OutputLine> {
409        &self.lines
410    }
411
412    /// A handle that stops its group from another thread, such as the one
413    /// dropping the call that waits for it.
414    pub fn group(&self) -> Group {
415        self.group.clone()
416    }
417
418    /// How it ended, if it has.
419    pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
420        let status = self.child.try_wait();
421        if !matches!(status, Ok(None)) {
422            // Reaped, or about to be: its id may be reused.
423            self.group.0.store(0, Ordering::SeqCst);
424        }
425        status
426    }
427
428    /// Waits at most `timeout` for it to end, and says how it ended if it
429    /// did. Its output keeps arriving at [`lines`](Self::lines) meanwhile.
430    /// A timeout too long for the clock, such as `Duration::MAX`, waits
431    /// until it ends.
432    pub fn wait_for(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
433        let deadline = Instant::now().checked_add(timeout);
434        // A process whose output just closed is usually a moment from
435        // ending, so the first naps are short.
436        let mut nap = Duration::from_millis(1);
437        loop {
438            if let Some(status) = self.try_wait()? {
439                return Ok(Some(status));
440            }
441            let left = deadline.map_or(POLL, |deadline| {
442                deadline.saturating_duration_since(Instant::now())
443            });
444            if left.is_zero() {
445                return Ok(None);
446            }
447            thread::sleep(left.min(nap));
448            nap = (nap * 2).min(POLL);
449        }
450    }
451
452    /// Runs it to its end, at most `timeout` (`None` waits as long as it
453    /// takes), and returns what it printed on stdout and on stderr (with
454    /// [`Spawn::merge_stderr`], all of it on stdout) and how it ended. A
455    /// blocking call: run it on a thread of its own, such as inside
456    /// [`blocking`](crate::blocking). Past `timeout` the error is
457    /// [`io::ErrorKind::TimedOut`]; otherwise as [`run`](Self::run).
458    ///
459    /// ```
460    /// # #[cfg(unix)] {
461    /// use rig_tools::process::{Launcher, Spawn};
462    ///
463    /// let process = Launcher::default().spawn("sh", ["-c", "echo hi"], Spawn::default())?;
464    /// let output = process.output(Some(std::time::Duration::from_secs(5)))?;
465    /// assert_eq!(output.stdout, b"hi\n");
466    /// assert!(output.status.success());
467    /// # }
468    /// # Ok::<(), std::io::Error>(())
469    /// ```
470    pub fn output(mut self, timeout: Option<Duration>) -> io::Result<Output> {
471        let deadline = timeout.and_then(|timeout| Instant::now().checked_add(timeout));
472        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
473        let ended = self.run(deadline, None, |line| {
474            match line {
475                OutputLine::Out(text) => stdout.extend(text.into_bytes()),
476                OutputLine::Err(text) => stderr.extend(text.into_bytes()),
477            }
478            true
479        })?;
480        match ended {
481            Ended::Exited(status) => Ok(Output {
482                status,
483                stdout,
484                stderr,
485            }),
486            Ended::TimedOut | Ended::Quiet | Ended::Stopped => Err(io::Error::new(
487                io::ErrorKind::TimedOut,
488                "the process ran past its timeout and was killed",
489            )),
490        }
491    }
492
493    /// Waits for it to end, at most until `deadline` (`None` waits as long
494    /// as it takes) and, with `quiet`, while it printed something within
495    /// that long, handing each piece of its output to `sink`; `sink`
496    /// returning `false` stops the wait. A blocking call.
497    ///
498    /// After an exit, something it left running that still holds its
499    /// output gets a moment (100 ms) to finish, then the rest of its group
500    /// is killed; past the deadline or stopped, the group is
501    /// [stopped](Self::stop) with [`STOP_GRACE`]. After a kill, reading
502    /// goes on for that moment: a process that left the group survives the
503    /// kill and may hold the pipe for good.
504    pub fn run(
505        &mut self,
506        deadline: Option<Instant>,
507        quiet: Option<Duration>,
508        mut sink: impl FnMut(OutputLine) -> bool,
509    ) -> io::Result<Ended> {
510        let mut more = true;
511        let mut heard = Instant::now();
512        let ended = loop {
513            if let Some(status) = self.try_wait()? {
514                break Ended::Exited(status);
515            }
516            if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
517                break Ended::TimedOut;
518            }
519            if quiet.is_some_and(|quiet| heard.elapsed() >= quiet) {
520                break Ended::Quiet;
521            }
522            if !more {
523                break Ended::Stopped;
524            }
525            match self.lines.recv_timeout(POLL) {
526                Ok(line) => {
527                    heard = Instant::now();
528                    more = sink(line);
529                }
530                Err(RecvTimeoutError::Timeout) => {}
531                // It closed its output but still runs.
532                Err(RecvTimeoutError::Disconnected) => {
533                    self.wait_for(POLL)?;
534                }
535            }
536        };
537        if !matches!(ended, Ended::Exited(_)) {
538            self.stop(STOP_GRACE).ok();
539            self.read_rest(&mut sink);
540        } else if !self.read_rest(&mut sink) {
541            self.kill().ok();
542            self.read_rest(&mut sink);
543        }
544        Ok(ended)
545    }
546
547    /// Hands output to `sink` until the pipe's end, for at most
548    /// [`PIPE_GRACE`]; whether the end came.
549    fn read_rest(&self, sink: &mut impl FnMut(OutputLine) -> bool) -> bool {
550        let deadline = Instant::now() + PIPE_GRACE;
551        loop {
552            match self
553                .lines
554                .recv_timeout(deadline.saturating_duration_since(Instant::now()))
555            {
556                Ok(line) => {
557                    sink(line);
558                }
559                Err(RecvTimeoutError::Disconnected) => return true,
560                Err(RecvTimeoutError::Timeout) => return false,
561            }
562        }
563    }
564
565    /// Asks its group to stop (`SIGTERM`), waits at most `grace` for it to
566    /// end, then [kills](Self::kill) the group. Elsewhere than unix it is
567    /// killed at once.
568    pub fn stop(&mut self, grace: Duration) -> io::Result<ExitStatus> {
569        self.forced = false;
570        if self.try_wait()?.is_none() {
571            self.group.terminate();
572            self.forced = self.wait_for(grace)?.is_none();
573        }
574        self.kill()
575    }
576
577    /// Kills its group (`SIGKILL`), also what is left of it after the
578    /// process itself ended, and says how the process ended. Kill within
579    /// moments of the end: once the group is empty its id is free again.
580    pub fn kill(&mut self) -> io::Result<ExitStatus> {
581        self.group.0.store(0, Ordering::SeqCst);
582        #[cfg(unix)]
583        kill_group_of(self.child.id());
584        #[cfg(not(unix))]
585        if self.child.try_wait()?.is_none() {
586            self.child.kill()?;
587        }
588        self.child.wait()
589    }
590}
591
592/// How [`Process::run`]'s wait ended.
593#[derive(Clone, Copy, Debug, PartialEq, Eq)]
594pub enum Ended {
595    /// It exited.
596    Exited(ExitStatus),
597    /// It ran past the deadline, and its group was stopped.
598    TimedOut,
599    /// It printed nothing for the `quiet` time, and its group was stopped.
600    Quiet,
601    /// The sink stopped the wait, and its group was stopped.
602    Stopped,
603}
604
605impl Drop for Process {
606    fn drop(&mut self) {
607        if self.group.0.load(Ordering::SeqCst) != 0 {
608            self.kill().ok();
609        }
610    }
611}
612
613/// A [`Process`]'s group, to stop from another thread. It acts only while
614/// the process runs: once it was reaped, its id may name another process.
615#[derive(Clone, Debug)]
616pub struct Group(Arc<AtomicU32>);
617
618impl Group {
619    /// Kills the group (`SIGKILL`); elsewhere than unix, nothing.
620    pub fn kill(&self) {
621        self.signal(Signal::Kill);
622    }
623
624    /// Asks the group to stop (`SIGTERM`); elsewhere than unix, nothing.
625    pub fn terminate(&self) {
626        self.signal(Signal::Terminate);
627    }
628
629    fn signal(&self, signal: Signal) {
630        let leader = self.0.load(Ordering::SeqCst);
631        #[cfg(unix)]
632        if leader != 0 {
633            signal_group(leader, signal);
634        }
635        #[cfg(not(unix))]
636        let _ = (leader, signal);
637    }
638}
639
640/// Stops a [`Process`]'s group when dropped: kept in a future that waits
641/// for the process on another thread ([`blocking`](crate::blocking)), it
642/// asks the group to stop (`SIGTERM`) the moment the future is dropped,
643/// such as when a call is interrupted or the app quits, and kills it
644/// [`STOP_GRACE`] later if it still runs. The thread waiting for the
645/// process must go on reaping it meanwhile, as [`Process::run`] does.
646pub struct KillOnDrop(pub Group);
647
648impl Drop for KillOnDrop {
649    fn drop(&mut self) {
650        self.0.terminate();
651        let group = self.0.clone();
652        let later = thread::Builder::new()
653            .name("rig-process-stop".to_owned())
654            .spawn(move || {
655                thread::sleep(STOP_GRACE);
656                group.kill();
657            });
658        if later.is_err() {
659            self.0.kill();
660        }
661    }
662}
663
664/// The signals a [`Group`] sends.
665#[derive(Clone, Copy)]
666enum Signal {
667    Terminate,
668    Kill,
669}
670
671/// Starts `command` in a new session, which also makes it the leader of a
672/// new process group. With no controlling terminal, a program that opens
673/// `/dev/tty` for a prompt (ssh, git, sudo) fails at once instead of
674/// drawing over the view and stopping on a terminal read. Git is also told
675/// not to prompt.
676pub fn detach(command: &mut Command) {
677    command.env("GIT_TERMINAL_PROMPT", "0");
678    #[cfg(unix)]
679    {
680        use std::os::unix::process::CommandExt;
681        // SAFETY: the closure runs in the forked child before `exec` and
682        // only calls `setsid`, which is async-signal-safe.
683        unsafe {
684            command.pre_exec(|| {
685                if libc::setsid() == -1 {
686                    Err(std::io::Error::last_os_error())
687                } else {
688                    Ok(())
689                }
690            });
691        }
692    }
693}
694
695/// Kills the process group `child` leads, created by [`detach`];
696/// elsewhere, the child alone.
697#[cfg(unix)]
698pub fn kill_group(child: &mut Child) {
699    kill_group_of(child.id());
700}
701
702/// Kills the process group led by the process `leader`. Its id names that
703/// group while the leader is not reaped or any member lives: the kernel
704/// does not reuse the id of a process group that exists. Once the leader
705/// was reaped and the group emptied, the id is free again, so callers kill
706/// within moments of the reap.
707#[cfg(unix)]
708pub fn kill_group_of(leader: u32) {
709    signal_group(leader, Signal::Kill);
710}
711
712/// Sends `signal` to the process group led by the process `leader`.
713#[cfg(unix)]
714fn signal_group(leader: u32, signal: Signal) {
715    let signal = match signal {
716        Signal::Terminate => libc::SIGTERM,
717        Signal::Kill => libc::SIGKILL,
718    };
719    if let Ok(group) = i32::try_from(leader) {
720        // SAFETY: `kill` takes plain integers; a negative pid names the
721        // process group the child leads, created by `detach`.
722        unsafe {
723            libc::kill(-group, signal);
724        }
725    }
726}
727
728#[cfg(not(unix))]
729pub fn kill_group(child: &mut Child) {
730    child.kill().ok();
731}
732
733#[cfg(test)]
734mod tests;