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;