Skip to main content

clawless_core/process/
mod.rs

1//! Running external programs from a command
2//!
3//! Most command-line applications drive other programs. A release tool calls
4//! `git`, a build tool calls `cargo`, and a deployment tool calls a provider's
5//! own client. This module defines [`Process`], the interface that runs such a
6//! program and reports it through the event system while it runs.
7//!
8//! A run reports itself as [`ProcessEvent`]s: one for the start, one for every
9//! line that the program writes, and one for the end. A presenter therefore
10//! shows a long build as it happens, and a TUI can keep the last few lines of
11//! the program in a corner of the screen. The command's own code is the same in
12//! both cases.
13//!
14//! The description of a command is an [`Invocation`], and the result of a run
15//! is an [`Execution`]. Both come from the `kawauso-process` crate, which owns
16//! the mechanics of starting a program and reading its streams. This module
17//! re-exports what a command names, so that an application needs Clawless
18//! alone.
19//!
20//! # Examples
21//!
22//! ```no_run
23//! use clawless_core::process::{Invocation, Process};
24//!
25//! # async fn example(process: Process) -> Result<(), Box<dyn std::error::Error>> {
26//! let execution = process
27//!     .run(Invocation::new("git").arg("status").arg("--short"))
28//!     .await?
29//!     .require_success()?;
30//!
31//! let status = execution.stdout().to_string_lossy();
32//! # Ok(())
33//! # }
34//! ```
35//!
36//! [`ProcessEvent`]: crate::event::process::ProcessEvent
37
38use std::time::{Duration, Instant};
39
40use bon::Builder;
41pub use kawauso_process::error::{RequireSuccessError, RunCommandError};
42pub use kawauso_process::execution::Capture;
43pub use kawauso_process::invocation::{Argument, Program, WorkingDirectory};
44pub use kawauso_process::run::{Line, Stream, Text};
45pub use kawauso_process::{Execution, Invocation, ProcessId, Run};
46
47pub use self::error::RunProcessError;
48use crate::cancellation::Cancellation;
49use crate::event::SendError;
50use crate::event::process::{Outcome, ProcessEvent, RunId};
51use crate::output::Output;
52
53/// The time a program gets to end itself before Clawless kills it
54///
55/// A program that answers the request costs what it takes to answer, so this
56/// period is only ever paid in full by a program that ignores it. Five seconds
57/// is therefore generous where it costs nothing and still short enough that a
58/// user who pressed Ctrl+C on such a program does not think the application
59/// ignored them. A program that cleans up for longer says so through the
60/// builder.
61const DEFAULT_GRACE_PERIOD: Duration = Duration::from_secs(5);
62
63/// The error of running an external program from a command
64mod error;
65
66/// The way in which the reading of the output of a program ended
67///
68/// A read that ends because the program closed its streams still owes the
69/// caller the exit status, and a read that ends because cancellation stopped
70/// the run owes nothing. The two ends therefore travel apart.
71#[derive(Copy, Clone, Eq, PartialEq, Debug)]
72enum Streamed {
73    /// Cancellation stopped the run
74    Cancelled,
75
76    /// Both output streams of the program reached their end
77    Closed,
78}
79
80/// Runs external programs and reports them through the event system
81///
82/// `Process` holds the two things that a run needs: the [`Output`] that carries
83/// its events, and the [`Cancellation`] that stops it. [`Context::process`]
84/// builds one from the context of a command, which is how a command reaches
85/// this interface.
86///
87/// A run reports every line that the program writes as it arrives, so a
88/// presenter can render the progress of a program that takes minutes. The same
89/// lines also reach the caller in the [`Execution`], which means that a command
90/// can show the output and read it afterwards without running the program
91/// twice.
92///
93/// Cancellation ends the program. A command that observes its cancellation token
94/// therefore does not leave a build running after the user pressed Ctrl+C, and
95/// it needs no code of its own to achieve that. The program is asked to end
96/// first and killed only if it does not, so a build tool gets the moment it
97/// needs to remove its lock file. The [builder] names how long that moment is.
98///
99/// [builder]: Process::builder
100///
101/// `Process` is cheaply clonable. Cloning produces another handle to the same
102/// event channel and the same cancellation token.
103///
104/// # Examples
105///
106/// ```no_run
107/// use clawless_core::process::{Invocation, Process};
108///
109/// # async fn example(process: Process) -> Result<(), Box<dyn std::error::Error>> {
110/// let execution = process.run(Invocation::new("cargo").arg("build")).await?;
111///
112/// let succeeded = execution.status().success();
113/// # Ok(())
114/// # }
115/// ```
116///
117/// [`Context::process`]: crate::context::Context::process
118// r[impl process.new]
119// r[impl process.new.grace]
120// r[impl process.safety.clone]
121// r[impl process.safety.send]
122// r[impl process.safety.sync]
123#[derive(Clone, Debug, Builder)]
124pub struct Process {
125    /// The channel that carries the events of a run
126    output: Output,
127
128    /// The token that stops a run
129    #[builder(default)]
130    cancellation: Cancellation,
131
132    /// The time a program gets to end itself before Clawless kills it
133    #[builder(default = DEFAULT_GRACE_PERIOD)]
134    grace_period: Duration,
135}
136
137impl Process {
138    /// Runs a program and reports its output while it runs
139    ///
140    /// The method starts the program, sends every line that it writes into the
141    /// event channel as the line arrives, waits for the program to end, and
142    /// returns what the program produced. The [`Execution`] holds the exit
143    /// status, both output streams as the program wrote them, and the time that
144    /// the run took.
145    ///
146    /// A program that ends without success is not a failure of this method. The
147    /// status travels in the [`Execution`], and
148    /// [`require_success`][success] turns a status that the caller cannot
149    /// accept into an error.
150    ///
151    /// Cancellation ends the program: it is asked to end, and killed only if it
152    /// does not answer within the grace period. The run then returns
153    /// [`CancelledRun`][cancelled], and the events of the run end with an
154    /// [`Outcome::Cancelled`].
155    ///
156    /// The program starts with a standard input that is null, so a program that
157    /// asks for a password ends instead of waiting for an answer. It inherits
158    /// the environment of the application, and the operating system resolves
159    /// its name.
160    ///
161    /// The future of this method owns the program. A caller that drops it, in a
162    /// timeout of its own for example, kills the program with it and no event
163    /// reports the end of the run. Stop a run through the cancellation token
164    /// instead, which asks the program to end and closes its events.
165    ///
166    /// # Errors
167    ///
168    /// Returns [`UnrunnableCommand`][unrunnable] when the program does not
169    /// start or the run produces no result, [`CancelledRun`][cancelled] when
170    /// cancellation stopped the program, and
171    /// [`UnreportableOutput`][unreportable] when the presenter stopped
172    /// listening while the program ran.
173    ///
174    /// # Panics
175    ///
176    /// Panics when no Tokio runtime drives the future. Commands always run
177    /// under one.
178    ///
179    /// # Examples
180    ///
181    /// ```no_run
182    /// use clawless_core::process::{Invocation, Process};
183    ///
184    /// # async fn example(process: Process) -> Result<(), Box<dyn std::error::Error>> {
185    /// let execution = process
186    ///     .run(Invocation::new("git").arg("rev-parse").arg("HEAD"))
187    ///     .await?
188    ///     .require_success()?;
189    ///
190    /// let commit = execution.stdout().to_string_lossy().trim().to_owned();
191    /// # Ok(())
192    /// # }
193    /// ```
194    ///
195    /// [`Outcome::Cancelled`]: crate::event::process::Outcome::Cancelled
196    /// [cancelled]: RunProcessError::CancelledRun
197    /// [success]: Execution::require_success
198    /// [unreportable]: RunProcessError::UnreportableOutput
199    /// [unrunnable]: RunProcessError::UnrunnableCommand
200    // r[impl process.run]
201    // r[impl process.run.capture]
202    // r[impl process.run.stream]
203    pub async fn run(&self, invocation: Invocation) -> Result<Execution, RunProcessError> {
204        let started = Instant::now();
205        let id = RunId::next();
206
207        let mut run = invocation
208            .start()
209            .map_err(|source| RunProcessError::UnrunnableCommand { source })?;
210
211        let event = ProcessEvent::Started {
212            id,
213            invocation: invocation.clone(),
214            process_id: run.id(),
215        };
216        self.report(&invocation, event).await?;
217
218        let streamed = self.stream(id, &invocation, &mut run).await;
219
220        match streamed {
221            Ok(Streamed::Closed) => self.complete(id, invocation, run, started).await,
222            Ok(Streamed::Cancelled) => Err(self.cancel(id, invocation, run, started).await),
223            Err(error) => {
224                drop(run.stop(self.grace_period).await);
225                drop(
226                    self.end(id, &invocation, Outcome::Incomplete, started)
227                        .await,
228                );
229
230                Err(error)
231            }
232        }
233    }
234
235    /// Ends a run that cancellation stopped and reports it
236    ///
237    /// The program is asked to end and killed only if it does not answer within
238    /// the grace period, so a build tool that holds a lock file gets the moment
239    /// it needs to remove it.
240    ///
241    /// What the program writes while it ends reaches the capture of the run and
242    /// not the event channel, because the ending owns the streams. A consumer
243    /// therefore sees the lines up to the cancellation and then the end of the
244    /// run.
245    ///
246    /// The grace period bounds the whole ending, so a program that ignores the
247    /// request costs that period and a program that answers costs what it takes
248    /// to answer.
249    ///
250    /// The result of the ending is dropped. The caller is cancelling, so it
251    /// discards the output of the run either way, and a program that could not
252    /// be waited for is no longer this application's concern.
253    // r[impl process.run.cancel.grace]
254    // r[impl process.run.cancel.bound]
255    async fn cancel(
256        &self,
257        id: RunId,
258        invocation: Invocation,
259        run: Run,
260        started: Instant,
261    ) -> RunProcessError {
262        drop(run.stop(self.grace_period).await);
263        drop(self.end(id, &invocation, Outcome::Cancelled, started).await);
264
265        RunProcessError::CancelledRun { invocation }
266    }
267
268    /// Reports every line of the program until it stops writing
269    ///
270    /// The read and the cancellation token race on every line, so a program
271    /// that writes without end still stops when the user asks for it. The
272    /// cancellation branch comes first, which means that a token that is
273    /// already cancelled ends the run instead of reporting one more line.
274    ///
275    /// # Errors
276    ///
277    /// Returns [`UnrunnableCommand`][unrunnable] when a stream of the program
278    /// cannot be read, and [`UnreportableOutput`][unreportable] when a line
279    /// cannot be reported.
280    ///
281    /// [unreportable]: RunProcessError::UnreportableOutput
282    /// [unrunnable]: RunProcessError::UnrunnableCommand
283    // r[impl process.run.cancel]
284    async fn stream(
285        &self,
286        id: RunId,
287        invocation: &Invocation,
288        run: &mut Run,
289    ) -> Result<Streamed, RunProcessError> {
290        loop {
291            let line = tokio::select! {
292                biased;
293
294                () = self.cancellation.cancelled() => return Ok(Streamed::Cancelled),
295                line = run.next_line() => line,
296            };
297
298            let line = line.map_err(|source| RunProcessError::UnrunnableCommand { source })?;
299
300            let Some(line) = line else {
301                return Ok(Streamed::Closed);
302            };
303
304            self.report(invocation, ProcessEvent::Line { id, line })
305                .await?;
306        }
307    }
308
309    /// Waits for a program that stopped writing and reports how it ended
310    ///
311    /// A program can close both of its output streams and keep running, so the
312    /// wait races the cancellation token as the read does.
313    ///
314    /// The wait leaves the handle where it is, so cancellation here ends the
315    /// program the same way it does while the program is writing: a request,
316    /// the grace period, and a kill only if the program does not answer.
317    ///
318    /// # Errors
319    ///
320    /// Returns [`CancelledRun`][cancelled] when cancellation stopped the
321    /// program, [`UnrunnableCommand`][unrunnable] when the end of the program
322    /// cannot be waited for, and [`UnreportableOutput`][unreportable] when the
323    /// end of the run cannot be reported.
324    ///
325    /// [cancelled]: RunProcessError::CancelledRun
326    /// [unreportable]: RunProcessError::UnreportableOutput
327    /// [unrunnable]: RunProcessError::UnrunnableCommand
328    // r[impl process.run.cancel]
329    async fn complete(
330        &self,
331        id: RunId,
332        invocation: Invocation,
333        mut run: Run,
334        started: Instant,
335    ) -> Result<Execution, RunProcessError> {
336        let ended = tokio::select! {
337            biased;
338
339            () = self.cancellation.cancelled() => None,
340            ended = run.wait_for_end() => Some(ended),
341        };
342
343        let Some(ended) = ended else {
344            return Err(self.cancel(id, invocation, run, started).await);
345        };
346
347        let waited = match ended {
348            Ok(()) => run.wait().await,
349            Err(source) => {
350                drop(run.stop(self.grace_period).await);
351
352                Err(source)
353            }
354        };
355
356        let execution = match waited {
357            Ok(execution) => execution,
358            Err(source) => {
359                drop(
360                    self.end(id, &invocation, Outcome::Incomplete, started)
361                        .await,
362                );
363
364                return Err(RunProcessError::UnrunnableCommand { source });
365            }
366        };
367
368        let outcome = Outcome::Exited(execution.status());
369
370        self.end(id, &invocation, outcome, started)
371            .await
372            .map_err(|source| RunProcessError::UnreportableOutput { invocation, source })?;
373
374        Ok(execution)
375    }
376
377    /// Reports the end of a run
378    ///
379    /// A caller that already has an error to report drops the result of this
380    /// method. The consumer that would have read the event is the one that is
381    /// gone, so a second failure says nothing that the first one does not.
382    ///
383    /// # Errors
384    ///
385    /// Returns [`SendError`] if the consumer of the events has stopped
386    /// listening.
387    // r[impl process.run.lifecycle]
388    async fn end(
389        &self,
390        id: RunId,
391        invocation: &Invocation,
392        outcome: Outcome,
393        started: Instant,
394    ) -> Result<(), SendError> {
395        let event = ProcessEvent::Finished {
396            id,
397            invocation: invocation.clone(),
398            outcome,
399            duration: started.elapsed(),
400        };
401
402        self.output.process_event(event).await
403    }
404
405    /// Sends one event of a run, naming the command if the send fails
406    ///
407    /// # Errors
408    ///
409    /// Returns [`UnreportableOutput`][unreportable] if the consumer of the
410    /// events has stopped listening.
411    ///
412    /// [unreportable]: RunProcessError::UnreportableOutput
413    async fn report(
414        &self,
415        invocation: &Invocation,
416        event: ProcessEvent,
417    ) -> Result<(), RunProcessError> {
418        self.output.process_event(event).await.map_err(|source| {
419            RunProcessError::UnreportableOutput {
420                invocation: invocation.clone(),
421                source,
422            }
423        })
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    // An assertion in a test panics by design. A `# Panics` section on every test
430    // would repeat that and give the reader no information.
431    #![allow(clippy::missing_panics_doc)]
432
433    #[cfg(unix)]
434    use std::fs::read_to_string;
435    use std::time::Duration;
436
437    use kawauso_process::run::Stream;
438    #[cfg(unix)]
439    use tempfile::TempDir;
440    use tokio::time::{sleep, timeout};
441
442    use super::*;
443    use crate::event::{Event, EventReceiver, event_channel};
444
445    /// The command that keeps a shell alive until a signal ends it
446    ///
447    /// A test that cancels a run needs a shell that answers `SIGTERM` as soon
448    /// as the signal arrives. A shell that waits for a background program does
449    /// not, because the signal can reach it while it enters that wait. The
450    /// shell then either records the signal and runs the trap when the wait
451    /// ends, which is long after the grace period, or it stops with a
452    /// segmentation fault, because it runs the trap in the handler of the
453    /// signal.
454    ///
455    /// A loop of builtins has no such moment. The shell runs a recorded trap
456    /// on every turn of the loop, and it runs it outside the handler of the
457    /// signal. An answer therefore costs one builtin, wherever the signal
458    /// found the shell.
459    #[cfg(unix)]
460    const SPIN_UNTIL_SIGNALED: &str = "while :; do :; done";
461
462    /// Returns the events of every run that the channel holds
463    ///
464    /// The channel closes when the test drops the handle that sent the events,
465    /// so the drain reads to the end and needs no timeout of its own.
466    async fn drain(mut receiver: EventReceiver) -> Vec<ProcessEvent> {
467        let mut events = Vec::new();
468
469        while let Some(event) = receiver.recv().await {
470            match event {
471                Event::Process(event) => events.push(*event),
472                Event::Message(_) | Event::Detail(_) | Event::Artifact(_) | Event::Prompt(_) => {}
473            }
474        }
475
476        events
477    }
478
479    /// Returns whether the error reports a run that cancellation stopped
480    fn is_cancelled(error: &RunProcessError) -> bool {
481        match error {
482            RunProcessError::CancelledRun { .. } => true,
483            RunProcessError::UnrunnableCommand { .. } => false,
484            RunProcessError::UnreportableOutput { .. } => false,
485        }
486    }
487
488    /// Returns every line of output that the events carry, in the order of arrival
489    fn lines(events: &[ProcessEvent]) -> Vec<(Stream, String)> {
490        events
491            .iter()
492            .filter_map(|event| match event {
493                ProcessEvent::Line { line, .. } => {
494                    Some((line.stream(), line.text().get().to_owned()))
495                }
496                ProcessEvent::Started { .. } => None,
497                ProcessEvent::Finished { .. } => None,
498            })
499            .collect()
500    }
501
502    /// Returns the outcome of the run that the events describe
503    fn outcome(events: &[ProcessEvent]) -> Option<Outcome> {
504        events.iter().find_map(|event| match event {
505            ProcessEvent::Finished { outcome, .. } => Some(outcome.clone()),
506            ProcessEvent::Started { .. } => None,
507            ProcessEvent::Line { .. } => None,
508        })
509    }
510
511    /// Returns an invocation that gives the commands to the shell of the platform
512    ///
513    /// A test needs a program that writes what the test expects, and the shell
514    /// is the program that every machine has. The separator between two
515    /// commands differs between the platforms, so the helper joins them for the
516    /// shell that runs them.
517    fn shell(commands: &[&str]) -> Invocation {
518        if cfg!(windows) {
519            Invocation::new("cmd").arg("/C").arg(commands.join(" & "))
520        } else {
521            Invocation::new("sh").arg("-c").arg(commands.join("; "))
522        }
523    }
524
525    // r[verify process.new]
526    // r[verify process.safety.clone]
527    #[tokio::test]
528    async fn clone_reports_through_the_same_channel() {
529        let (sender, receiver) = event_channel();
530        let process = Process::builder().output(Output::new(sender)).build();
531        let clone = process.clone();
532
533        clone.run(shell(&["exit 0"])).await.expect("should run");
534        drop(process);
535        drop(clone);
536
537        assert_eq!(drain(receiver).await.len(), 2);
538    }
539
540    // r[verify process.event.finished]
541    #[tokio::test]
542    async fn run_reports_the_command_and_the_time_of_the_run() {
543        let (sender, receiver) = event_channel();
544        let process = Process::builder().output(Output::new(sender)).build();
545        let invocation = shell(&["exit 0"]);
546
547        process.run(invocation.clone()).await.expect("should run");
548        drop(process);
549
550        assert_eq!(
551            drain(receiver).await.iter().find_map(|event| match event {
552                ProcessEvent::Finished {
553                    invocation,
554                    duration,
555                    ..
556                } => Some((invocation.to_string(), *duration > Duration::ZERO)),
557                ProcessEvent::Started { .. } => None,
558                ProcessEvent::Line { .. } => None,
559            }),
560            Some((invocation.to_string(), true))
561        );
562    }
563
564    // r[verify process.event.lifecycle]
565    // r[verify process.run.lifecycle]
566    #[tokio::test]
567    async fn run_reports_the_lifecycle_of_the_program() {
568        let (sender, receiver) = event_channel();
569        let process = Process::builder().output(Output::new(sender)).build();
570
571        process
572            .run(shell(&["echo only"]))
573            .await
574            .expect("should run");
575        drop(process);
576
577        assert_eq!(
578            drain(receiver)
579                .await
580                .iter()
581                .map(|event| match event {
582                    ProcessEvent::Started { .. } => "started",
583                    ProcessEvent::Line { .. } => "line",
584                    ProcessEvent::Finished { .. } => "finished",
585                })
586                .collect::<Vec<_>>(),
587            vec!["started", "line", "finished"]
588        );
589    }
590
591    // r[verify process.event.line]
592    // r[verify process.run]
593    // r[verify process.run.stream]
594    #[tokio::test]
595    async fn run_reports_the_lines_of_the_program() {
596        let (sender, receiver) = event_channel();
597        let process = Process::builder().output(Output::new(sender)).build();
598
599        process
600            .run(shell(&["echo one", "echo two"]))
601            .await
602            .expect("should run");
603        drop(process);
604
605        assert_eq!(
606            lines(&drain(receiver).await),
607            vec![
608                (Stream::StandardOutput, "one".to_owned()),
609                (Stream::StandardOutput, "two".to_owned()),
610            ]
611        );
612    }
613
614    // r[verify process.event.outcome]
615    // r[verify process.event.outcome.exited]
616    #[tokio::test]
617    async fn run_reports_the_outcome_of_the_program() {
618        let (sender, receiver) = event_channel();
619        let process = Process::builder().output(Output::new(sender)).build();
620
621        let execution = process.run(shell(&["exit 0"])).await.expect("should run");
622        drop(process);
623
624        assert_eq!(
625            outcome(&drain(receiver).await),
626            Some(Outcome::Exited(execution.status()))
627        );
628    }
629
630    // r[verify process.event.started.identifier]
631    #[tokio::test]
632    async fn run_reports_the_identifier_of_the_program() {
633        let (sender, receiver) = event_channel();
634        let process = Process::builder().output(Output::new(sender)).build();
635
636        process.run(shell(&["exit 0"])).await.expect("should run");
637        drop(process);
638
639        assert_eq!(
640            drain(receiver).await.iter().find_map(|event| match event {
641                ProcessEvent::Started { process_id, .. } => Some(process_id.is_some()),
642                ProcessEvent::Line { .. } => None,
643                ProcessEvent::Finished { .. } => None,
644            }),
645            Some(true)
646        );
647    }
648
649    // r[verify process.event.started]
650    #[tokio::test]
651    async fn run_reports_the_start_of_the_program() {
652        let (sender, receiver) = event_channel();
653        let process = Process::builder().output(Output::new(sender)).build();
654        let invocation = shell(&["exit 0"]);
655
656        process.run(invocation.clone()).await.expect("should run");
657        drop(process);
658
659        assert_eq!(
660            drain(receiver).await.first().map(ProcessEvent::to_string),
661            Some(format!("$ {invocation}"))
662        );
663    }
664
665    // r[verify process.run.capture]
666    #[tokio::test]
667    async fn run_returns_the_capture_of_the_program() {
668        let (sender, _receiver) = event_channel();
669        let process = Process::builder().output(Output::new(sender)).build();
670
671        let execution = process
672            .run(shell(&["echo hello"]))
673            .await
674            .expect("should run");
675
676        assert_eq!(execution.stdout().to_string_lossy().trim(), "hello");
677    }
678
679    // r[verify process.event.line]
680    #[tokio::test]
681    async fn run_separates_the_streams_of_the_program() {
682        let (sender, receiver) = event_channel();
683        let process = Process::builder().output(Output::new(sender)).build();
684
685        process
686            .run(shell(&["echo out", "echo err 1>&2"]))
687            .await
688            .expect("should run");
689        drop(process);
690
691        let mut found = lines(&drain(receiver).await);
692        found.sort();
693
694        assert_eq!(
695            found,
696            vec![
697                (Stream::StandardError, "err".to_owned()),
698                (Stream::StandardOutput, "out".to_owned()),
699            ]
700        );
701    }
702
703    // r[verify process.run.cancel.grace]
704    #[cfg(unix)]
705    #[tokio::test]
706    async fn run_with_a_cancelled_token_lets_the_program_end_itself() {
707        let directory = TempDir::new().expect("should create a directory");
708        let marker = directory.path().join("cleaned");
709        let cancellation = Cancellation::new();
710        let (sender, mut receiver) = event_channel();
711        let process = Process::builder()
712            .output(Output::new(sender))
713            .cancellation(cancellation.clone())
714            .build();
715        let invocation = shell(&[
716            &format!("trap 'touch \"{}\"; exit 0' TERM", marker.display()),
717            "echo ready",
718            SPIN_UNTIL_SIGNALED,
719        ]);
720
721        let handle = tokio::spawn(async move { process.run(invocation).await });
722        while let Some(event) = receiver.recv().await {
723            let ready = match event {
724                Event::Process(event) => match *event {
725                    ProcessEvent::Line { .. } => true,
726                    ProcessEvent::Started { .. } => false,
727                    ProcessEvent::Finished { .. } => false,
728                },
729                Event::Message(_) | Event::Detail(_) | Event::Artifact(_) | Event::Prompt(_) => {
730                    false
731                }
732            };
733
734            if ready {
735                break;
736            }
737        }
738        cancellation.cancel();
739        timeout(Duration::from_secs(20), handle)
740            .await
741            .expect("should not time out")
742            .expect("should join")
743            .expect_err("should fail");
744
745        assert!(marker.exists());
746    }
747
748    // r[verify process.run.cancel]
749    #[cfg(unix)]
750    #[tokio::test]
751    async fn run_with_a_cancelled_token_kills_the_program() {
752        let directory = TempDir::new().expect("should create a directory");
753        let beats = directory.path().join("beats");
754        let cancellation = Cancellation::new();
755        let (sender, _receiver) = event_channel();
756        let process = Process::builder()
757            .output(Output::new(sender))
758            .cancellation(cancellation.clone())
759            .build();
760        let invocation = shell(&[&format!(
761            "while true; do echo beat >> '{}'; sleep 0.05; done",
762            beats.display()
763        )]);
764
765        let handle = tokio::spawn(async move { process.run(invocation).await });
766        sleep(Duration::from_millis(300)).await;
767        cancellation.cancel();
768        timeout(Duration::from_secs(30), handle)
769            .await
770            .expect("should not time out")
771            .expect("should join")
772            .expect_err("should fail");
773        let written = read_to_string(&beats).expect("should read the beats");
774        written
775            .lines()
776            .next()
777            .expect("the program should have written a beat");
778        sleep(Duration::from_millis(400)).await;
779
780        assert_eq!(
781            read_to_string(&beats).expect("should read the beats"),
782            written
783        );
784    }
785
786    // r[verify process.event.outcome.cancelled]
787    #[tokio::test]
788    async fn run_with_a_cancelled_token_reports_the_cancellation() {
789        let cancellation = Cancellation::new();
790        let (sender, receiver) = event_channel();
791        let process = Process::builder()
792            .output(Output::new(sender))
793            .cancellation(cancellation.clone())
794            .build();
795        cancellation.cancel();
796
797        process
798            .run(shell(&["sleep 30"]))
799            .await
800            .expect_err("should fail");
801        drop(process);
802
803        assert_eq!(outcome(&drain(receiver).await), Some(Outcome::Cancelled));
804    }
805
806    // r[verify process.run.cancel.error]
807    #[tokio::test]
808    async fn run_with_a_cancelled_token_returns_an_error() {
809        let cancellation = Cancellation::new();
810        let (sender, _receiver) = event_channel();
811        let process = Process::builder()
812            .output(Output::new(sender))
813            .cancellation(cancellation.clone())
814            .build();
815        cancellation.cancel();
816
817        let error = process
818            .run(shell(&["sleep 30"]))
819            .await
820            .expect_err("should fail");
821
822        assert!(is_cancelled(&error));
823    }
824
825    // r[verify process.run.report.error]
826    #[tokio::test]
827    async fn run_with_a_closed_channel_returns_an_error() {
828        let (sender, receiver) = event_channel();
829        let process = Process::builder().output(Output::new(sender)).build();
830        drop(receiver);
831
832        let error = process
833            .run(shell(&["exit 0"]))
834            .await
835            .expect_err("should fail");
836
837        assert!(matches_unreportable_output(&error));
838    }
839
840    // r[verify process.run.error]
841    #[tokio::test]
842    async fn run_with_a_missing_program_returns_an_error() {
843        let (sender, _receiver) = event_channel();
844        let process = Process::builder().output(Output::new(sender)).build();
845
846        let error = process
847            .run(Invocation::new("clawless-no-such-program"))
848            .await
849            .expect_err("should fail");
850
851        assert!(matches_unrunnable_command(&error));
852    }
853
854    // r[verify process.run.cancel.grace]
855    #[cfg(unix)]
856    #[tokio::test]
857    async fn run_with_a_program_that_closed_its_streams_ends_itself_on_cancellation() {
858        let directory = TempDir::new().expect("should create a directory");
859        let marker = directory.path().join("cleaned");
860        let cancellation = Cancellation::new();
861        let (sender, _receiver) = event_channel();
862        let process = Process::builder()
863            .output(Output::new(sender))
864            .cancellation(cancellation.clone())
865            .build();
866        let invocation = shell(&[
867            &format!("trap 'touch \"{}\"; exit 0' TERM", marker.display()),
868            "exec 1>&- 2>&-",
869            SPIN_UNTIL_SIGNALED,
870        ]);
871
872        let handle = tokio::spawn(async move { process.run(invocation).await });
873        sleep(Duration::from_millis(250)).await;
874        cancellation.cancel();
875        timeout(Duration::from_secs(30), handle)
876            .await
877            .expect("should not time out")
878            .expect("should join")
879            .expect_err("should fail");
880
881        assert!(marker.exists());
882    }
883
884    // r[verify process.run.cancel]
885    #[cfg(unix)]
886    #[tokio::test]
887    async fn run_with_a_program_that_closed_its_streams_stops_on_cancellation() {
888        let cancellation = Cancellation::new();
889        let (sender, _receiver) = event_channel();
890        let process = Process::builder()
891            .output(Output::new(sender))
892            .cancellation(cancellation.clone())
893            .build();
894        let invocation = shell(&["exec 1>&- 2>&-", "sleep 30"]);
895
896        let handle = tokio::spawn(async move { process.run(invocation).await });
897        sleep(Duration::from_millis(250)).await;
898        cancellation.cancel();
899        let error = timeout(Duration::from_secs(30), handle)
900            .await
901            .expect("should not time out")
902            .expect("should join")
903            .expect_err("should fail");
904
905        assert!(is_cancelled(&error));
906    }
907
908    // r[verify process.new.grace]
909    // r[verify process.run.cancel.bound]
910    #[cfg(unix)]
911    #[tokio::test]
912    async fn run_with_a_program_that_leaves_a_child_returns_within_the_grace_period() {
913        let cancellation = Cancellation::new();
914        let (sender, _receiver) = event_channel();
915        let process = Process::builder()
916            .output(Output::new(sender))
917            .cancellation(cancellation.clone())
918            .grace_period(Duration::from_millis(200))
919            .build();
920        let invocation = shell(&["echo ready", "sleep 20"]);
921
922        let handle = tokio::spawn(async move { process.run(invocation).await });
923        sleep(Duration::from_millis(200)).await;
924        cancellation.cancel();
925        let started = Instant::now();
926        timeout(Duration::from_secs(20), handle)
927            .await
928            .expect("should not time out")
929            .expect("should join")
930            .expect_err("should fail");
931
932        assert!(started.elapsed() < Duration::from_secs(5));
933    }
934
935    #[tokio::test]
936    async fn run_with_an_unsuccessful_program_returns_the_status() {
937        let (sender, _receiver) = event_channel();
938        let process = Process::builder().output(Output::new(sender)).build();
939
940        let execution = process.run(shell(&["exit 3"])).await.expect("should run");
941
942        assert_eq!(execution.status().code(), Some(3));
943    }
944
945    // r[verify process.run.cancel]
946    #[tokio::test]
947    async fn run_with_cancellation_while_the_program_runs_returns_an_error() {
948        let cancellation = Cancellation::new();
949        let (sender, mut receiver) = event_channel();
950        let process = Process::builder()
951            .output(Output::new(sender))
952            .cancellation(cancellation.clone())
953            .grace_period(Duration::from_millis(200))
954            .build();
955        let invocation = shell(&["echo ready", "sleep 30"]);
956
957        let handle = tokio::spawn(async move { process.run(invocation).await });
958        while let Some(event) = receiver.recv().await {
959            let ready = match event {
960                Event::Process(event) => match *event {
961                    ProcessEvent::Line { .. } => true,
962                    ProcessEvent::Started { .. } => false,
963                    ProcessEvent::Finished { .. } => false,
964                },
965                Event::Message(_) | Event::Detail(_) | Event::Artifact(_) | Event::Prompt(_) => {
966                    false
967                }
968            };
969
970            if ready {
971                break;
972            }
973        }
974        cancellation.cancel();
975        let error = timeout(Duration::from_secs(30), handle)
976            .await
977            .expect("should not time out")
978            .expect("should join")
979            .expect_err("should fail");
980
981        assert!(is_cancelled(&error));
982    }
983
984    // r[verify process.safety.send]
985    #[test]
986    fn trait_send() {
987        fn assert_send<T: Send>() {}
988        assert_send::<Process>();
989    }
990
991    // r[verify process.safety.sync]
992    #[test]
993    fn trait_sync() {
994        fn assert_sync<T: Sync>() {}
995        assert_sync::<Process>();
996    }
997
998    #[test]
999    fn trait_unpin() {
1000        fn assert_unpin<T: Unpin>() {}
1001        assert_unpin::<Process>();
1002    }
1003
1004    /// Returns whether the error reports output that could not be reported
1005    fn matches_unreportable_output(error: &RunProcessError) -> bool {
1006        match error {
1007            RunProcessError::UnreportableOutput { .. } => true,
1008            RunProcessError::CancelledRun { .. } => false,
1009            RunProcessError::UnrunnableCommand { .. } => false,
1010        }
1011    }
1012
1013    /// Returns whether the error reports a program that could not be run
1014    fn matches_unrunnable_command(error: &RunProcessError) -> bool {
1015        match error {
1016            RunProcessError::UnrunnableCommand { .. } => true,
1017            RunProcessError::CancelledRun { .. } => false,
1018            RunProcessError::UnreportableOutput { .. } => false,
1019        }
1020    }
1021}