1use 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
53const DEFAULT_GRACE_PERIOD: Duration = Duration::from_secs(5);
62
63mod error;
65
66#[derive(Copy, Clone, Eq, PartialEq, Debug)]
72enum Streamed {
73 Cancelled,
75
76 Closed,
78}
79
80#[derive(Clone, Debug, Builder)]
124pub struct Process {
125 output: Output,
127
128 #[builder(default)]
130 cancellation: Cancellation,
131
132 #[builder(default = DEFAULT_GRACE_PERIOD)]
134 grace_period: Duration,
135}
136
137impl Process {
138 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 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 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 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 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 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 #![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 #[cfg(unix)]
460 const SPIN_UNTIL_SIGNALED: &str = "while :; do :; done";
461
462 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 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 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[test]
986 fn trait_send() {
987 fn assert_send<T: Send>() {}
988 assert_send::<Process>();
989 }
990
991 #[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 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 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}