1#![deny(rustdoc::missing_crate_level_docs)]
21#![warn(missing_docs)]
22#![doc(test(attr(deny(warnings))))]
24#![deny(clippy::all)]
25#![allow(clippy::collapsible_else_if)]
26#![allow(clippy::result_large_err)]
27
28pub mod fs {
32 use displaydoc::Display;
33
34 use std::path::PathBuf;
35
36 pub(crate) trait PathWrapper {
38 fn into_path_buf(self) -> PathBuf;
40 }
41
42 #[derive(Debug, Display, Clone)]
46 #[ignore_extra_doc_attributes]
47 pub struct File(pub PathBuf);
48
49 impl PathWrapper for File {
50 fn into_path_buf(self) -> PathBuf {
51 let Self(path) = self;
52 path
53 }
54 }
55
56 #[derive(Debug, Display, Clone)]
60 #[ignore_extra_doc_attributes]
61 pub struct Directory(pub PathBuf);
62
63 impl PathWrapper for Directory {
64 fn into_path_buf(self) -> PathBuf {
65 let Self(path) = self;
66 path
67 }
68 }
69}
70
71pub mod exe {
74 use super::fs::{self, PathWrapper};
75
76 use displaydoc::Display;
77 use indexmap::IndexMap;
78 use once_cell::sync::Lazy;
79 use signal_hook::consts::{signal::*, TERM_SIGNALS};
80 use thiserror::Error;
81
82 use std::{
83 collections::VecDeque,
84 ffi::{OsStr, OsString},
85 io, iter,
86 os::unix::process::ExitStatusExt,
87 path::{Path, PathBuf},
88 process, str,
89 };
90
91 #[derive(Debug, Display, Clone)]
95 #[ignore_extra_doc_attributes]
96 pub struct Exe(pub fs::File);
97
98 impl<R: AsRef<OsStr>> From<&R> for Exe {
99 fn from(value: &R) -> Self {
100 let p = Path::new(value);
101 let f = fs::File(p.to_path_buf());
102 Self(f)
103 }
104 }
105
106 impl Default for Exe {
107 fn default() -> Self { Self(fs::File(PathBuf::default())) }
108 }
109
110 impl Exe {
111 pub fn is_empty(&self) -> bool {
115 let Self(fs::File(exe)) = self;
116 exe.as_os_str().is_empty()
117 }
118 }
119
120 impl PathWrapper for Exe {
121 fn into_path_buf(self) -> PathBuf {
122 let Self(exe) = self;
123 exe.into_path_buf()
124 }
125 }
126
127 #[derive(Debug, Display, Clone, Default)]
133 #[ignore_extra_doc_attributes]
134 pub struct Argv(pub VecDeque<OsString>);
135
136 impl<R: AsRef<OsStr>, I: iter::IntoIterator<Item=R>> From<I> for Argv {
137 fn from(value: I) -> Self {
138 let argv: VecDeque<OsString> = value
139 .into_iter()
140 .map(|s| {
141 let s: &OsStr = s.as_ref();
142 s.to_os_string()
143 })
144 .collect();
145 Self(argv)
146 }
147 }
148
149 impl Argv {
150 pub fn trailing_args(mut self) -> Self {
152 if self.0.is_empty() {
153 Self(VecDeque::new())
154 } else {
155 self.unshift("--".into());
156 self
157 }
158 }
159
160 pub fn unshift(&mut self, leftmost_arg: OsString) {
162 let Self(ref mut argv) = self;
163 argv.push_front(leftmost_arg);
164 }
165 }
166
167 #[derive(Debug, Display, Clone, Default)]
171 #[ignore_extra_doc_attributes]
172 pub struct EnvModifications(pub IndexMap<OsString, OsString>);
173
174 impl<R: AsRef<OsStr>, I: iter::IntoIterator<Item=(R, R)>> From<I> for EnvModifications {
175 fn from(value: I) -> Self {
176 let env: IndexMap<OsString, OsString> = value
177 .into_iter()
178 .map(|(k, v)| {
179 let k: &OsStr = k.as_ref();
180 let v: &OsStr = v.as_ref();
181 (k.to_os_string(), v.to_os_string())
182 })
183 .collect();
184 Self(env)
185 }
186 }
187
188 #[derive(Debug, Display, Clone, Default)]
193 #[ignore_extra_doc_attributes]
194 pub struct Command {
195 pub exe: Exe,
197 pub wd: Option<fs::Directory>,
200 pub argv: Argv,
203 pub env: EnvModifications,
206 }
207
208 impl Command {
209 pub(crate) fn command(self) -> async_process::Command {
210 dbg!(&self);
211 let Self {
212 exe,
213 wd,
214 argv,
215 env: EnvModifications(env),
216 } = self;
217 if exe.is_empty() {
218 unreachable!(
219 "command was executed before .exe was set; this can only occur using ::default()"
220 );
221 }
222 let mut command = async_process::Command::new(exe.into_path_buf());
223 if let Some(wd) = wd {
224 command.current_dir(wd.into_path_buf());
225 }
226 command.args(argv.0);
227 for (var, val) in env.into_iter() {
228 command.env(&var, &val);
229 }
230 command
231 }
232
233 pub fn unshift_new_exe(&mut self, new_exe: Exe) {
236 if new_exe.is_empty() {
237 unreachable!("new_exe is an empty string!! self was: {:?}", self);
238 }
239
240 let mut argv = self.argv.clone();
241 if !self.exe.is_empty() {
242 argv.unshift(self.exe.clone().into_path_buf().as_os_str().to_os_string());
243 }
244
245 self.argv = argv;
246 self.exe = new_exe;
247 }
248
249 pub(crate) fn unshift_shell_script(&mut self, script_path: Exe) {
250 self.unshift_new_exe(script_path);
251 self.unshift_new_exe(Exe(fs::File(PathBuf::from("sh"))));
252 }
253 }
254
255 #[derive(Debug, Display, Error)]
257 pub enum CommandError {
258 NonZeroExit(i32),
260 ProcessTerminated(i32, &'static str),
262 ProcessKilled(i32, &'static str),
264 Io(#[from] io::Error),
266 Utf8(#[from] str::Utf8Error),
268 }
269
270 macro_rules! signal_pairs {
271 ($($name:ident),+) => {
272 [$(($name, stringify!($name))),+]
273 }
274 }
275
276 static SIGNAL_NAMES: Lazy<IndexMap<i32, &'static str>> = Lazy::new(|| {
277 signal_pairs![
278 SIGABRT, SIGALRM, SIGBUS, SIGCHLD, SIGCONT, SIGFPE, SIGHUP, SIGILL, SIGINT, SIGIO, SIGKILL,
279 SIGPIPE, SIGPROF, SIGQUIT, SIGSEGV, SIGSTOP, SIGSYS, SIGTERM, SIGTRAP, SIGTSTP, SIGTTIN,
280 SIGTTOU, SIGURG, SIGUSR1, SIGUSR2, SIGVTALRM, SIGWINCH, SIGXCPU, SIGXFSZ
281 ]
282 .into()
283 });
284
285 impl CommandError {
286 pub fn analyze_exit_status(status: process::ExitStatus) -> Result<(), Self> {
288 if let Some(code) = status.code() {
289 if code == 0 {
290 Ok(())
291 } else {
292 Err(Self::NonZeroExit(code))
293 }
294 } else if let Some(signal) = status.signal() {
295 let name = SIGNAL_NAMES.get(&signal).unwrap();
296 Err(if TERM_SIGNALS.contains(&signal) {
297 Self::ProcessTerminated(signal, name)
298 } else {
299 Self::ProcessKilled(signal, name)
300 })
301 } else {
302 unreachable!("status {:?} had no exit code or signal", status)
303 }
304 }
305
306 pub(crate) fn command_with_context(
307 self,
308 command: Command,
309 context: String,
310 ) -> CommandErrorWrapper {
311 CommandErrorWrapper {
312 command,
313 context,
314 error: self,
315 }
316 }
317 }
318
319 #[derive(Debug, Display, Error)]
321 pub struct CommandErrorWrapper {
322 pub command: Command,
324 pub context: String,
326 #[source]
328 pub error: CommandError,
329 }
330}
331
332pub mod base {
334 use super::*;
335
336 use async_trait::async_trait;
337 use displaydoc::Display;
338 use thiserror::Error;
339
340 use std::io;
341
342 #[derive(Debug, Display, Error)]
345 pub enum SetupError {
346 Inner(#[source] Box<SetupErrorWrapper>),
348 Io(#[from] io::Error),
350 }
351
352 impl SetupError {
353 pub fn with_context(self, context: String) -> SetupErrorWrapper {
355 SetupErrorWrapper {
356 context,
357 error: self,
358 }
359 }
360 }
361
362 #[derive(Debug, Display, Error)]
364 pub struct SetupErrorWrapper {
365 pub context: String,
367 #[source]
369 pub error: SetupError,
370 }
371
372 #[async_trait]
375 pub trait CommandBase {
376 async fn setup_command(self) -> Result<exe::Command, SetupError>;
378 }
379}
380
381pub mod sync {
406 use super::exe;
407
408 use async_trait::async_trait;
409
410 use std::{process, str};
411
412 #[derive(Debug, Clone)]
414 #[allow(missing_docs)]
415 pub struct RawOutput {
416 pub stdout: Vec<u8>,
417 pub stderr: Vec<u8>,
418 }
419
420 impl RawOutput {
421 pub fn extract(
424 command: exe::Command,
425 output: process::Output,
426 ) -> Result<Self, exe::CommandErrorWrapper> {
427 let process::Output {
428 status,
429 stdout,
430 stderr,
431 } = output;
432
433 let output = Self { stdout, stderr };
434 if let Err(e) = exe::CommandError::analyze_exit_status(status) {
435 let output_msg: String = match output.decode(command.clone()) {
436 Ok(decoded) => format!("(utf-8 decoded) {:?}", decoded),
437 Err(_) => format!("(could not decode) {:?}", &output),
438 };
439 return Err(e.command_with_context(
440 command,
441 format!("when analyzing exit status for output {}", output_msg),
442 ));
443 }
444
445 Ok(output)
446 }
447
448 pub fn decode(
451 &self,
452 command: exe::Command,
453 ) -> Result<DecodedOutput<'_>, exe::CommandErrorWrapper> {
454 let Self { stdout, stderr } = self;
455
456 let stdout =
457 str::from_utf8(stdout)
458 .map_err(|e| e.into())
459 .map_err(|e: exe::CommandError| {
460 e.command_with_context(
461 command.clone(),
462 format!("when decoding stdout from {:?}", &self),
463 )
464 })?;
465 let stderr =
466 str::from_utf8(stderr)
467 .map_err(|e| e.into())
468 .map_err(|e: exe::CommandError| {
469 e.command_with_context(command, format!("when decoding stderr from {:?}", &self))
470 })?;
471
472 Ok(DecodedOutput { stdout, stderr })
473 }
474 }
475
476 #[derive(Debug, Clone)]
479 #[allow(missing_docs)]
480 pub struct DecodedOutput<'a> {
481 pub stdout: &'a str,
482 pub stderr: &'a str,
483 }
484
485 #[async_trait]
487 pub trait SyncInvocable {
488 async fn invoke(self) -> Result<RawOutput, exe::CommandErrorWrapper>;
491 }
492
493 #[async_trait]
494 impl SyncInvocable for exe::Command {
495 async fn invoke(self) -> Result<RawOutput, exe::CommandErrorWrapper> {
496 let mut command = self.clone().command();
497 let output =
498 command
499 .output()
500 .await
501 .map_err(|e| e.into())
502 .map_err(|e: exe::CommandError| {
503 e.command_with_context(self.clone(), "waiting for output".to_string())
504 })?;
505 let output = RawOutput::extract(self, output)?;
506 Ok(output)
507 }
508 }
509}
510
511pub mod stream {
520 use super::exe;
521
522 use async_process::{self, Child, Stdio};
523
524 pub struct Streaming {
526 pub child: Child,
529 pub command: exe::Command,
531 }
532
533 impl Streaming {
534 pub async fn wait(self) -> Result<(), exe::CommandErrorWrapper> {
537 let Self { mut child, command } = self;
538
539 let status = child.status().await.map_err(|e| {
540 let e: exe::CommandError = e.into();
541 e.command_with_context(command.clone(), "merging async streams".to_string())
542 })?;
543 exe::CommandError::analyze_exit_status(status)
544 .map_err(|e| e.command_with_context(command, "checking async exit status".to_string()))?;
545
546 Ok(())
547 }
548 }
549
550 pub trait Streamable {
552 fn invoke_streaming(self) -> Result<Streaming, exe::CommandErrorWrapper>;
554 }
555
556 impl Streamable for exe::Command {
557 fn invoke_streaming(self) -> Result<Streaming, exe::CommandErrorWrapper> {
558 let mut command = self.clone().command();
559 let child = command
560 .stdout(Stdio::inherit())
561 .stderr(Stdio::inherit())
562 .spawn()
563 .map_err(|e| e.into())
564 .map_err(|e: exe::CommandError| {
565 e.command_with_context(self.clone(), "spawning async process".to_string())
566 })?;
567 Ok(Streaming {
568 child,
569 command: self,
570 })
571 }
572 }
573}
574
575pub mod sh {
577 use super::{
578 base::{self, CommandBase},
579 exe, fs,
580 sync::SyncInvocable,
581 };
582
583 use async_trait::async_trait;
584 use displaydoc::Display;
585 use indexmap::IndexMap;
586 use tempfile::{NamedTempFile, TempPath};
587 use thiserror::Error;
588
589 use std::{
590 ffi::OsString,
591 io::{self, BufRead, Write},
592 str,
593 };
594
595 #[derive(Debug, Display, Error)]
597 pub enum ShellError {
598 Setup(#[from] base::SetupErrorWrapper),
600 Command(#[from] exe::CommandErrorWrapper),
602 Io(#[from] io::Error),
604 Utf8(#[from] str::Utf8Error),
606 }
607
608 impl ShellError {
609 pub fn with_context(self, context: String) -> ShellErrorWrapper {
611 ShellErrorWrapper {
612 context,
613 error: self,
614 }
615 }
616 }
617
618 #[derive(Debug, Display, Error)]
620 pub struct ShellErrorWrapper {
621 pub context: String,
623 #[source]
625 pub error: ShellError,
626 }
627
628 #[derive(Debug, Clone)]
646 pub struct ShellSource {
647 pub contents: Vec<u8>,
649 }
650
651 impl ShellSource {
652 fn write_to_temp_path(self) -> io::Result<TempPath> {
653 let (mut script_file, script_path) = NamedTempFile::new()?.into_parts();
655 let Self { contents } = self;
656 script_file.write_all(&contents)?;
657 script_file.sync_all()?;
658 Ok(script_path)
660 }
661
662 pub async fn into_script(self) -> Result<ShellScript, ShellError> {
667 let script_path = self.write_to_temp_path()?;
668
669 let script_path = exe::Exe(fs::File(
671 script_path
672 .keep()
673 .expect("should never be any error keeping the shell script path"),
674 ));
675
676 Ok(ShellScript { script_path })
677 }
678 }
679
680 #[derive(Debug, Clone)]
697 pub struct EnvAfterScript {
698 pub source: ShellSource,
700 }
701
702 impl EnvAfterScript {
703 fn into_source(self) -> ShellSource {
704 let Self {
705 source: ShellSource { mut contents },
706 } = self;
707 contents.extend_from_slice(b"\n\nexec env");
708 ShellSource { contents }
709 }
710
711 async fn into_command(self) -> Result<exe::Command, ShellErrorWrapper> {
712 let source = self.into_source();
714 let script = source
715 .into_script()
716 .await
717 .map_err(|e| e.with_context("when writing env script to file".to_string()))?;
718 let sh = script.with_command(exe::Command::default());
720 let command = sh
721 .setup_command()
722 .await
723 .map_err(|e| {
724 e.with_context("when setting up the shell command".to_string())
725 .into()
726 })
727 .map_err(|e: ShellError| {
728 e.with_context("when setting up the shell command, again".to_string())
729 })?;
730 Ok(command)
731 }
732
733 async fn extract_stdout(self) -> Result<Vec<u8>, ShellErrorWrapper> {
734 let command = self.into_command().await?;
736
737 let output = command
739 .invoke()
740 .await
741 .map_err(|e| e.into())
742 .map_err(|e: ShellError| e.with_context("when extracting env bindings".to_string()))?;
743
744 Ok(output.stdout)
745 }
746
747 pub async fn extract_env_bindings(self) -> Result<exe::EnvModifications, ShellErrorWrapper> {
750 let stdout = self.extract_stdout().await?;
751
752 let mut env_map: IndexMap<OsString, OsString> = IndexMap::new();
754 for line in stdout.lines() {
755 let line = line
756 .map_err(|e| e.into())
757 .map_err(|e: ShellError| e.with_context("when extracting stdout line".to_string()))?;
758 if let Some(equals_index) = line.find('=') {
760 let key = &line[..equals_index];
761 let value = &line[equals_index + 1..];
762 env_map.insert(key.into(), value.into());
763 }
764 }
765
766 Ok(exe::EnvModifications(env_map))
767 }
768 }
769
770 #[derive(Debug, Clone)]
795 pub struct ShellScript {
796 pub script_path: exe::Exe,
798 }
799
800 impl ShellScript {
801 pub fn with_command(self, base: exe::Command) -> ShellScriptInvocation {
803 ShellScriptInvocation { script: self, base }
804 }
805 }
806
807 #[derive(Debug, Clone)]
809 pub struct ShellScriptInvocation {
810 pub script: ShellScript,
812 pub base: exe::Command,
814 }
815
816 #[async_trait]
817 impl CommandBase for ShellScriptInvocation {
818 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
819 let Self {
820 script: ShellScript { script_path },
821 mut base,
822 } = self;
823 base.unshift_shell_script(script_path);
824 Ok(base)
825 }
826 }
827}