Skip to main content

crabshell_services/
process.rs

1//! Programs a config runs, as Quickshell's `Process`: what they print,
2//! whole or line by line, and how they ended.
3
4#[cfg(test)]
5mod tests;
6
7use std::cell::RefCell;
8use std::ffi::OsStr;
9use std::io::{ErrorKind, PipeReader, PipeWriter, Read, Write};
10use std::os::fd::AsFd;
11use std::os::unix::process::CommandExt;
12use std::panic::{self, AssertUnwindSafe, Location};
13use std::process::{Child, Command, ExitStatus, Stdio};
14use std::rc::{Rc, Weak};
15use std::sync::Once;
16use std::time::Instant;
17
18use calloop::timer::TimeoutAction;
19use calloop::{Interest, RegistrationToken};
20use crabshell_reactive::{ReadSignal, Signal, untrack};
21use crabshell_ui::Cx;
22use rustix::process::{Pid, PidfdFlags, Signal as Kill, kill_process_group, pidfd_open};
23
24use crate::lines::{Lines, panic_message};
25use crate::{Shared, spawn};
26
27/// The most of each output kept in [`Process::stdout`] and
28/// [`Process::stderr`]. Lines still reach `on_line` past it.
29pub(crate) const MAX_OUTPUT: usize = 1024 * 1024;
30
31/// The most bytes left to write to a program's input before it is closed,
32/// so a program that reads nothing can't grow the shell without end.
33pub(crate) const MAX_INPUT: usize = 4 * 1024 * 1024;
34
35/// What `cx.process` and `cx.exec_detached` need from the shell: the loop.
36pub(crate) struct Processes {
37  shared: Rc<Shared>,
38  /// Every `cx.process`, so the shell can stop them all before it exits.
39  live: RefCell<Vec<Weak<Inner>>>,
40}
41
42impl Processes {
43  pub(crate) fn new(shared: Rc<Shared>) -> Processes {
44    Processes {
45      shared,
46      live: RefCell::default(),
47    }
48  }
49}
50
51/// Stops every program `cx.process` started, as the shell does before it
52/// exits: `exit` skips the drops that would, and each program has a
53/// process group of its own, which nothing else signals.
54pub(crate) fn stop_all(cx: &Cx) {
55  let Some(processes) = cx.service::<Processes>() else {
56    return;
57  };
58  let live: Vec<Rc<Inner>> = processes
59    .live
60    .borrow()
61    .iter()
62    .filter_map(Weak::upgrade)
63    .collect();
64  for inner in live {
65    inner.stop();
66  }
67}
68
69/// What `cx.process` and `cx.exec_detached` run: a [`Command`], or the
70/// program and its arguments as a list, such as `["sleep", "30"]`, as
71/// Quickshell's `command` is.
72pub trait IntoCommand {
73  /// The command, or `None` for an empty list.
74  fn into_command(self) -> Option<Command>;
75}
76
77impl IntoCommand for Command {
78  fn into_command(self) -> Option<Command> {
79    Some(self)
80  }
81}
82
83impl<S: AsRef<OsStr>> IntoCommand for Vec<S> {
84  fn into_command(self) -> Option<Command> {
85    let mut words = self.into_iter();
86    let mut command = Command::new(words.next()?);
87    command.args(words);
88    Some(command)
89  }
90}
91
92impl<S: AsRef<OsStr>, const N: usize> IntoCommand for [S; N] {
93  fn into_command(self) -> Option<Command> {
94    Vec::from(self).into_command()
95  }
96}
97
98/// A program the shell runs, from `cx.process(command)`. A clone is cheap
99/// and shares it.
100///
101/// It starts on the loop's next turn, so `on_line` and `with_stdin` set
102/// right after `cx.process` apply to it. It runs in a process group of its
103/// own, and that group gets `SIGTERM` when the context that started it
104/// goes or the command starts again. Nothing polls: the shell wakes when
105/// the program prints, takes input or ends.
106#[derive(Clone)]
107pub struct Process {
108  inner: Rc<Inner>,
109}
110
111struct Inner {
112  /// `None` when the shell provided no services.
113  processes: Option<Rc<Shared>>,
114  /// These live under the starting context's owner.
115  running: Signal<bool>,
116  status: Signal<Option<ExitStatus>>,
117  stdout: Signal<String>,
118  stderr: Signal<String>,
119  /// Read by the effect that builds the command, so bumping it starts
120  /// the command again.
121  restarts: Signal<u64>,
122  state: RefCell<State>,
123}
124
125#[derive(Default)]
126struct State {
127  /// The command to start at the loop's next turn, and the timer that
128  /// will.
129  pending: Option<Command>,
130  starter: Option<RegistrationToken>,
131  run: Option<Run>,
132  keep_stdin: bool,
133  /// Written before the program started, for its input.
134  input: Vec<u8>,
135  /// The config closed the input: it closes once what was written is
136  /// sent.
137  closing: bool,
138  /// A run ended, failed to start or was killed before it started, and
139  /// nothing is to start: writes and closes have no run to wait for.
140  ran: bool,
141  on_line: Option<OnLine>,
142  /// Where `on_line` was set, for the log if it panics.
143  at: Option<&'static Location<'static>>,
144}
145
146/// What a config does with each line.
147type OnLine = Rc<dyn Fn(&str)>;
148
149/// The program running now.
150struct Run {
151  child: Child,
152  program: String,
153  exit: Option<RegistrationToken>,
154  out: Output,
155  err: Output,
156  stdin: Option<PipeWriter>,
157  outbox: Vec<u8>,
158  writer: Option<RegistrationToken>,
159  lines: Lines,
160}
161
162/// One of a program's outputs, read as it comes.
163#[derive(Default)]
164struct Output {
165  stream: Option<(PipeReader, RegistrationToken)>,
166  /// The start of a character a read cut off.
167  partial: Vec<u8>,
168  /// How much is in the signal, and whether the cap was logged.
169  kept: usize,
170  capped: bool,
171}
172
173#[derive(Clone, Copy, PartialEq)]
174enum Which {
175  Out,
176  Err,
177}
178
179/// `cx.process(command)`.
180#[track_caller]
181pub(crate) fn process<C: IntoCommand>(cx: &Cx, command: impl Fn() -> C + 'static) -> Process {
182  let at = Location::caller();
183  let service = cx.service::<Processes>();
184  let processes = service
185    .as_ref()
186    .map(|processes| Rc::clone(&processes.shared));
187  if processes.is_none() {
188    static LOGGED: Once = Once::new();
189    LOGGED.call_once(|| {
190      tracing::warn!("cx.process() needs the services the shell provides; nothing runs");
191    });
192  }
193  let inner = Rc::new(Inner {
194    processes,
195    // Running from the start: the effect below starts it at the next
196    // flush.
197    running: cx.signal(true),
198    status: cx.signal(None),
199    stdout: cx.signal(String::new()),
200    stderr: cx.signal(String::new()),
201    restarts: cx.signal(0),
202    state: RefCell::default(),
203  });
204  // Dropped with the starting context's owner, which stops the program.
205  cx.owner().signal(StopOnDrop(Rc::clone(&inner)));
206  if let Some(service) = service {
207    let mut live = service.live.borrow_mut();
208    live.retain(|inner| inner.strong_count() > 0);
209    live.push(Rc::downgrade(&inner));
210  }
211  let weak = Rc::downgrade(&inner);
212  let restarts = inner.restarts;
213  // The command is the config's: built in an effect, so it is built again
214  // when a signal it read changes. A panic stops the run before and is
215  // logged; the next change tries again.
216  cx.effect(move || {
217    restarts.get();
218    let built = panic::catch_unwind(AssertUnwindSafe(|| command().into_command()));
219    let Some(inner) = weak.upgrade() else {
220      return;
221    };
222    match built {
223      Ok(next) => untrack(|| inner.start(next)),
224      Err(payload) => {
225        let message = panic_message(payload.as_ref());
226        tracing::error!("cx.process (called at {at}) panicked building its command: {message}");
227        untrack(|| inner.cancel());
228      }
229    }
230  });
231  Process { inner }
232}
233
234/// `cx.exec_detached(command)`.
235pub(crate) fn exec_detached(cx: &Cx, command: impl IntoCommand) {
236  let Some(processes) = cx.service::<Processes>() else {
237    tracing::warn!("cx.exec_detached() needs the services the shell provides; nothing runs");
238    return;
239  };
240  let Some(mut command) = command.into_command() else {
241    return tracing::warn!("cx.exec_detached() got an empty command; nothing runs");
242  };
243  // Logged there.
244  let _ = spawn::spawn(&processes.shared, &mut command);
245}
246
247/// Stops the program when the starting context's owner drops it.
248struct StopOnDrop(Rc<Inner>);
249
250impl Drop for StopOnDrop {
251  fn drop(&mut self) {
252    self.0.stop();
253  }
254}
255
256impl Process {
257  /// `true` from `cx.process` or a restart until the program ends or
258  /// can't start.
259  pub fn running(&self) -> ReadSignal<bool> {
260    self.inner.running.read_only()
261  }
262
263  /// How the last run ended: `None` while it runs, or if it couldn't
264  /// start.
265  pub fn exit_status(&self) -> ReadSignal<Option<ExitStatus>> {
266    self.inner.status.read_only()
267  }
268
269  /// Everything it printed on its standard output this run, up to 1 MiB,
270  /// decoded as UTF-8 with bad bytes replaced. Complete once
271  /// [`exit_status`](Self::exit_status) is set.
272  pub fn stdout(&self) -> ReadSignal<String> {
273    self.inner.stdout.read_only()
274  }
275
276  /// Like [`stdout`](Self::stdout), for its standard error.
277  pub fn stderr(&self) -> ReadSignal<String> {
278    self.inner.stderr.read_only()
279  }
280
281  /// Calls `on_line` with each line of its standard output, without its
282  /// `\n`, decoded as UTF-8 with bad bytes replaced, as it comes. What is
283  /// left when it ends counts as a last line. A line over 1 MiB is
284  /// dropped and logged.
285  ///
286  /// It runs on the main thread. If it panics, that is logged with the
287  /// program and where `on_line` was called, and it isn't called again.
288  #[track_caller]
289  pub fn on_line(self, on_line: impl Fn(&str) + 'static) -> Process {
290    let mut state = self.inner.state.borrow_mut();
291    state.on_line = Some(Rc::new(on_line));
292    state.at = Some(Location::caller());
293    drop(state);
294    self
295  }
296
297  /// Gives the program an input that [`write`](Self::write) feeds, open
298  /// until [`close_stdin`](Self::close_stdin). Without it the program's
299  /// input is empty. It applies from the next start, so call it right
300  /// after `cx.process`.
301  pub fn with_stdin(self) -> Process {
302    self.inner.state.borrow_mut().keep_stdin = true;
303    self
304  }
305
306  /// Sends `data` to the program's input, as it takes it; written before
307  /// the program started, it waits for the start. Without
308  /// [`with_stdin`](Self::with_stdin), or once the input is closed, `data`
309  /// is dropped and that is logged. If more than 4 MiB wait because the
310  /// program reads nothing, its input is closed.
311  pub fn write(&self, data: impl AsRef<[u8]>) {
312    self.inner.write(data.as_ref());
313  }
314
315  /// Closes the program's input once what was written is sent, so it
316  /// reads the end of it.
317  pub fn close_stdin(&self) {
318    {
319      let mut state = self.inner.state.borrow_mut();
320      // Nothing runs or is to start: the next run's input starts open.
321      if state.run.is_none() && state.ran {
322        return;
323      }
324      state.closing = true;
325    }
326    self.inner.flush();
327  }
328
329  /// Sends `SIGTERM` to the program's process group. Its exit status
330  /// comes when it ends.
331  /// Before it started, it doesn't start.
332  pub fn kill(&self) {
333    if let Some(run) = &self.inner.state.borrow().run {
334      return terminate(&run.child);
335    }
336    self.inner.cancel();
337  }
338
339  /// Stops the program if it runs and starts the command again, as when
340  /// a signal the command read changed.
341  pub fn restart(&self) {
342    self.inner.restarts.try_update(|count| *count += 1);
343  }
344}
345
346/// Sends `SIGTERM` to `child`'s process group.
347fn terminate(child: &Child) {
348  // The group is the child's own, from `process_group(0)`.
349  if let Err(error) = kill_process_group(Pid::from_child(child), Kill::TERM) {
350    tracing::debug!(%error, "couldn't stop a program; it may have ended");
351  }
352}
353
354impl Inner {
355  fn shared(&self) -> Option<&Rc<Shared>> {
356    self.processes.as_ref()
357  }
358
359  fn event_loop(&self) -> Option<Box<dyn crate::EventLoop>> {
360    self.shared()?.event_loop()
361  }
362
363  /// Stops the program running now, if one is, and starts `command` at
364  /// the loop's next turn. An empty command is logged and runs nothing.
365  fn start(self: &Rc<Self>, command: Option<Command>) {
366    self.stop_run();
367    let Some(command) = command else {
368      tracing::warn!("cx.process() got an empty command; nothing runs");
369      set(self.running, false);
370      return;
371    };
372    let Some(event_loop) = self.event_loop() else {
373      return;
374    };
375    let mut state = self.state.borrow_mut();
376    state.pending = Some(command);
377    state.ran = false;
378    if state.starter.is_none() {
379      let inner = Rc::clone(self);
380      state.starter = event_loop.insert_timer(
381        Instant::now(),
382        Box::new(move || {
383          inner.spawn_pending();
384          TimeoutAction::Drop
385        }),
386      );
387    }
388    drop(state);
389    set(self.running, true);
390    set(self.status, None);
391    set(self.stdout, String::new());
392    set(self.stderr, String::new());
393  }
394
395  fn spawn_pending(self: &Rc<Self>) {
396    let Some(event_loop) = self.event_loop() else {
397      return;
398    };
399    let (command, keep_stdin, input) = {
400      let mut state = self.state.borrow_mut();
401      state.starter = None;
402      let input = std::mem::take(&mut state.input);
403      (state.pending.take(), state.keep_stdin, input)
404    };
405    let Some(mut command) = command else {
406      return;
407    };
408    let program = command.get_program().to_string_lossy().into_owned();
409    let spawned = spawn_piped(&mut command, keep_stdin);
410    // The command holds the program's ends of the pipes. They must close
411    // here, or its outputs would never end.
412    drop(command);
413    let (child, out, err, stdin) = match spawned {
414      Ok(spawned) => spawned,
415      Err(error) => {
416        tracing::warn!(program, %error, "couldn't start the program");
417        let mut state = self.state.borrow_mut();
418        state.ran = true;
419        state.closing = false;
420        drop(state);
421        set(self.running, false);
422        return;
423      }
424    };
425    let mut run = Run {
426      child,
427      program,
428      exit: None,
429      out: Output::default(),
430      err: Output::default(),
431      stdin,
432      outbox: input,
433      writer: None,
434      lines: Lines::default(),
435    };
436    for (which, stream) in [(Which::Out, out), (Which::Err, err)] {
437      let Ok(fd) = stream.as_fd().try_clone_to_owned() else {
438        continue;
439      };
440      let inner = Rc::clone(self);
441      let read = Box::new(move || inner.on_output(which));
442      if let Some(token) = event_loop.insert_fd(fd, Interest::READ, read) {
443        run.output(which).stream = Some((stream, token));
444      }
445    }
446    match pidfd_open(Pid::from_child(&run.child), PidfdFlags::NONBLOCK) {
447      Ok(fd) => {
448        let inner = Rc::clone(self);
449        let ended = Box::new(move || inner.on_exit());
450        run.exit = event_loop.insert_fd(fd, Interest::READ, ended);
451      }
452      Err(error) => tracing::warn!(%error, "couldn't follow a program to its end"),
453    }
454    self.state.borrow_mut().run = Some(run);
455    // Whatever was written before it started.
456    self.flush();
457  }
458
459  /// Reads what the program printed on one output.
460  fn on_output(&self, which: Which) {
461    let mut chunk = [0; 64 * 1024];
462    let mut state = self.state.borrow_mut();
463    let Some(run) = state.run.as_mut() else {
464      return;
465    };
466    let Some((stream, _)) = run.output(which).stream.as_mut() else {
467      return;
468    };
469    let read = match stream.read(&mut chunk) {
470      Ok(read) => read,
471      Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {
472        return;
473      }
474      Err(_) => 0,
475    };
476    let (text, lines) = run.take(which, &chunk[..read]);
477    let program = run.program.clone();
478    if read == 0 {
479      // It closed this output; its exit is still to come.
480      let token = run.output(which).stream.take().map(|(_, token)| token);
481      if let (Some(token), Some(event_loop)) = (token, self.event_loop()) {
482        event_loop.remove(token);
483      }
484    }
485    drop(state);
486    self.show(which, text);
487    self.deliver(lines, &program);
488  }
489
490  /// The program ended: reads what is left of its outputs, then shows how
491  /// it ended.
492  fn on_exit(&self) {
493    let mut state = self.state.borrow_mut();
494    let Some(run) = state.run.as_mut() else {
495      return;
496    };
497    let status = match run.child.try_wait() {
498      Ok(Some(status)) => Some(status),
499      Ok(None) => return,
500      // Reaped behind the shell's back: its end is all there is to know.
501      Err(error) => {
502        tracing::warn!(%error, program = run.program, "lost track of the program");
503        None
504      }
505    };
506    let mut run = state.run.take().expect("invariant: matched just above");
507    // The next run's input starts open.
508    state.closing = false;
509    state.ran = true;
510    drop(state);
511    // What it printed before it ended is in the sockets now. A program it
512    // started may hold them open, so read only what is there.
513    // A program it started that keeps printing would keep this going, so
514    // it stops after what the signal keeps.
515    let mut chunk = [0; 64 * 1024];
516    for which in [Which::Out, Which::Err] {
517      let mut budget = MAX_OUTPUT;
518      while let Some((stream, _)) = run.output(which).stream.as_mut()
519        && budget > 0
520      {
521        let Ok(read @ 1..) = stream.read(&mut chunk) else {
522          break;
523        };
524        budget = budget.saturating_sub(read);
525        let (text, lines) = run.take(which, &chunk[..read]);
526        self.show(which, text);
527        self.deliver(lines, &run.program);
528      }
529      let (text, lines) = run.take(which, &[]);
530      self.show(which, text);
531      self.deliver(lines, &run.program);
532    }
533    let last = run.lines.finish();
534    self.deliver(last.into_iter().collect(), &run.program);
535    self.remove_sources(&mut run);
536    set(self.status, status);
537    set(self.running, false);
538  }
539
540  /// Adds `text` to the output's signal.
541  fn show(&self, which: Which, text: String) {
542    if text.is_empty() {
543      return;
544    }
545    let signal = match which {
546      Which::Out => self.stdout,
547      Which::Err => self.stderr,
548    };
549    signal.try_update(|all| all.push_str(&text));
550  }
551
552  /// Calls `on_line` with each line, with nothing borrowed.
553  fn deliver(&self, lines: Vec<String>, program: &str) {
554    if lines.is_empty() {
555      return;
556    }
557    let (on_line, at) = {
558      let state = self.state.borrow();
559      (state.on_line.clone(), state.at)
560    };
561    let Some(on_line) = on_line else {
562      return;
563    };
564    for line in lines {
565      // Not once the context went, or `on_line` was replaced.
566      let current = self.state.borrow().on_line.clone();
567      if !current.is_some_and(|now| Rc::ptr_eq(&now, &on_line)) {
568        return;
569      }
570      if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(|| on_line(&line))) {
571        let at = at.map_or_else(String::new, |at| format!(" (called at {at})"));
572        let message = panic_message(payload.as_ref());
573        tracing::error!("process {program} on_line{at} panicked: {message}");
574        let mut state = self.state.borrow_mut();
575        // Unless it set a new one meanwhile.
576        if state
577          .on_line
578          .as_ref()
579          .is_some_and(|now| Rc::ptr_eq(now, &on_line))
580        {
581          state.on_line = None;
582        }
583        return;
584      }
585    }
586  }
587
588  fn write(self: &Rc<Self>, data: &[u8]) {
589    let mut state = self.state.borrow_mut();
590    if state.closing {
591      return tracing::warn!("the program's input is closed, so a write was dropped");
592    }
593    // Not started yet, so kept for the start.
594    let waiting = state.keep_stdin && state.run.is_none() && !state.ran;
595    match state.run.as_mut() {
596      Some(run) if run.stdin.is_some() => run.outbox.extend_from_slice(data),
597      _ if waiting => {
598        if state.input.len() + data.len() > MAX_INPUT {
599          return tracing::warn!("over 4 MiB written before the program started; dropped");
600        }
601        return state.input.extend_from_slice(data);
602      }
603      _ => return tracing::warn!("the program takes no input now, so a write was dropped"),
604    }
605    drop(state);
606    self.flush();
607  }
608
609  /// Writes what the program's input takes, and waits for room for the
610  /// rest.
611  fn flush(self: &Rc<Self>) {
612    let Some(event_loop) = self.event_loop() else {
613      return;
614    };
615    let mut state = self.state.borrow_mut();
616    let closing = state.closing;
617    let Some(run) = state.run.as_mut() else {
618      return;
619    };
620    let Some(stdin) = run.stdin.as_mut() else {
621      return;
622    };
623    let mut broken = false;
624    while !run.outbox.is_empty() {
625      match stdin.write(&run.outbox) {
626        Ok(written) => {
627          run.outbox.drain(..written);
628        }
629        Err(error) if error.kind() == ErrorKind::Interrupted => {}
630        Err(error) if error.kind() == ErrorKind::WouldBlock => break,
631        Err(_) => {
632          broken = true;
633          break;
634        }
635      }
636    }
637    if run.outbox.len() > MAX_INPUT {
638      let program = &run.program;
639      tracing::warn!(
640        program,
641        "the program reads nothing it is sent; closing its input"
642      );
643      broken = true;
644    }
645    if broken {
646      drop(state);
647      return self.drop_stdin();
648    }
649    if run.outbox.is_empty() {
650      if let Some(writer) = run.writer.take() {
651        event_loop.remove(writer);
652      }
653      if closing {
654        // All sent: the program reads the end.
655        run.stdin = None;
656      }
657    } else if run.writer.is_none()
658      && let Ok(fd) = stdin.as_fd().try_clone_to_owned()
659    {
660      let inner = Rc::clone(self);
661      run.writer = event_loop.insert_fd(fd, Interest::WRITE, Box::new(move || inner.flush()));
662    }
663  }
664
665  /// Closes the program's input now, dropping what wasn't sent.
666  fn drop_stdin(&self) {
667    let writer = {
668      let mut state = self.state.borrow_mut();
669      state.input.clear();
670      let Some(run) = state.run.as_mut() else {
671        return;
672      };
673      run.stdin = None;
674      run.outbox.clear();
675      run.writer.take()
676    };
677    if let (Some(writer), Some(event_loop)) = (writer, self.event_loop()) {
678      event_loop.remove(writer);
679    }
680  }
681
682  /// Stops the program running now, if one is: `SIGTERM` to its group,
683  /// its outputs closed, and it is left to be reaped.
684  fn stop_run(&self) {
685    let run = {
686      let mut state = self.state.borrow_mut();
687      let run = state.run.take();
688      // The next run's input starts open.
689      if run.is_some() {
690        state.closing = false;
691      }
692      run
693    };
694    let Some(mut run) = run else {
695      return;
696    };
697    terminate(&run.child);
698    self.remove_sources(&mut run);
699    if let Some(shared) = &self.processes {
700      spawn::reap(shared, run.child, run.program);
701    }
702  }
703
704  /// Stops everything, for good, as the starting context went.
705  fn stop(&self) {
706    let (starter, on_line) = {
707      let mut state = self.state.borrow_mut();
708      state.pending = None;
709      (state.starter.take(), state.on_line.take())
710    };
711    // Dropped outside the borrow: it may hold this process, and its drop
712    // may use it.
713    drop(on_line);
714    if let (Some(starter), Some(event_loop)) = (starter, self.event_loop()) {
715      event_loop.remove(starter);
716    }
717    self.stop_run();
718  }
719
720  /// Stops the run and drops a start still to come, as when the command
721  /// panicked or the config killed it before it started.
722  fn cancel(&self) {
723    let starter = {
724      let mut state = self.state.borrow_mut();
725      state.pending = None;
726      state.ran = true;
727      state.closing = false;
728      state.starter.take()
729    };
730    if let (Some(starter), Some(event_loop)) = (starter, self.event_loop()) {
731      event_loop.remove(starter);
732    }
733    self.stop_run();
734    let ran = self.state.borrow().run.is_none();
735    if ran {
736      set(self.running, false);
737    }
738  }
739
740  fn remove_sources(&self, run: &mut Run) {
741    let tokens = [
742      run.exit.take(),
743      run.writer.take(),
744      run.out.stream.take().map(|(_, token)| token),
745      run.err.stream.take().map(|(_, token)| token),
746    ];
747    run.stdin = None;
748    if let Some(event_loop) = self.event_loop() {
749      for token in tokens.into_iter().flatten() {
750        event_loop.remove(token);
751      }
752    }
753  }
754}
755
756/// The program's outputs, and its input if wanted, through pipes, whose
757/// ends here are non-blocking. Pipes, not socket pairs: a program can
758/// open `/dev/stdout` on a pipe.
759#[allow(
760  clippy::type_complexity,
761  reason = "the four parts of one spawn, used once"
762)]
763fn spawn_piped(
764  command: &mut Command,
765  keep_stdin: bool,
766) -> std::io::Result<(Child, PipeReader, PipeReader, Option<PipeWriter>)> {
767  let (out, child_out) = std::io::pipe()?;
768  let (err, child_err) = std::io::pipe()?;
769  rustix::io::ioctl_fionbio(&out, true)?;
770  rustix::io::ioctl_fionbio(&err, true)?;
771  let (stdin, child_in) = if keep_stdin {
772    let (theirs, ours) = std::io::pipe()?;
773    rustix::io::ioctl_fionbio(&ours, true)?;
774    (Some(ours), Stdio::from(theirs))
775  } else {
776    (None, Stdio::null())
777  };
778  spawn::untune(command);
779  let child = command
780    .stdout(child_out)
781    .stderr(child_err)
782    .stdin(child_in)
783    .process_group(0)
784    .spawn()?;
785  Ok((child, out, err, stdin))
786}
787
788impl Run {
789  fn output(&mut self, which: Which) -> &mut Output {
790    match which {
791      Which::Out => &mut self.out,
792      Which::Err => &mut self.err,
793    }
794  }
795
796  /// `bytes` read from one output: the text to add to its signal, and for
797  /// standard output the whole lines it completes. Empty `bytes` means
798  /// the output ended, which lets a cut-off character go.
799  fn take(&mut self, which: Which, bytes: &[u8]) -> (String, Vec<String>) {
800    let lines = if which == Which::Out && !bytes.is_empty() {
801      self.lines.push(bytes, &self.program)
802    } else {
803      Vec::new()
804    };
805    let program = self.program.clone();
806    let output = self.output(which);
807    output.partial.extend_from_slice(bytes);
808    let mut text = if bytes.is_empty() {
809      String::from_utf8_lossy(&std::mem::take(&mut output.partial)).into_owned()
810    } else {
811      decode(&mut output.partial)
812    };
813    // Once a character didn't fit, nothing after it is kept either.
814    if output.capped {
815      text.clear();
816    }
817    let room = MAX_OUTPUT.saturating_sub(output.kept);
818    if text.len() > room {
819      if !std::mem::replace(&mut output.capped, true) {
820        tracing::warn!(program, "a program printed over 1 MiB; the rest isn't kept");
821      }
822      let mut end = room;
823      while !text.is_char_boundary(end) {
824        end -= 1;
825      }
826      text.truncate(end);
827    }
828    output.kept += text.len();
829    (text, lines)
830  }
831}
832
833/// The text `bytes` holds, with bad bytes replaced, leaving in `bytes`
834/// the start of a character cut off at its end.
835fn decode(bytes: &mut Vec<u8>) -> String {
836  let mut text = String::new();
837  let mut rest: &[u8] = bytes;
838  loop {
839    match std::str::from_utf8(rest) {
840      Ok(valid) => {
841        text.push_str(valid);
842        rest = &[];
843        break;
844      }
845      Err(error) => {
846        let (valid, after) = rest.split_at(error.valid_up_to());
847        text.push_str(&String::from_utf8_lossy(valid));
848        match error.error_len() {
849          Some(bad) => {
850            text.push('\u{fffd}');
851            rest = &after[bad..];
852          }
853          None => {
854            rest = after;
855            break;
856          }
857        }
858      }
859    }
860  }
861  *bytes = rest.to_vec();
862  text
863}
864
865/// Sets `signal` unless it holds `value` or is gone. The read is
866/// untracked, so an effect that starts a process doesn't run again for it.
867fn set<T: PartialEq + 'static>(signal: Signal<T>, value: T) {
868  if untrack(|| signal.try_with(|old| *old != value)) == Some(true) {
869    signal.try_set(value);
870  }
871}