1use std::{io, ops::Deref, process::Stdio, time::Duration};
2
3use once_cell::sync::Lazy;
4use tokio::process::Command;
5
6use crate::{Env, Location, Result, RunningProcess};
7
8#[derive(Clone)]
10pub struct Cmd<Loc> {
11 pub exe: String,
13 pub env: Env,
15 pub pwd: Loc,
17 pub msg: Option<String>,
19}
20
21impl<Loc> Cmd<Loc>
22where
23 Loc: Location,
24{
25 pub fn exe(&self) -> &str {
27 &self.exe
28 }
29
30 pub fn env(&self) -> &Env {
32 &self.env
33 }
34
35 pub fn pwd(&self) -> &Loc {
37 &self.pwd
38 }
39
40 pub fn msg(&self) -> Option<&String> {
42 self.msg.as_ref()
43 }
44}
45
46#[derive(Clone, Debug)]
52pub struct KillTimeout(Duration);
53
54impl KillTimeout {
55 pub fn new(duration: Duration) -> Self {
57 Self(duration)
58 }
59
60 pub fn duration(&self) -> Duration {
62 self.0
63 }
64}
65
66static DEFAULT_KILL_TIMEOUT: Lazy<Duration> = Lazy::new(|| {
67 let default = Duration::from_secs(10);
68 match std::env::var("PROCESS_TIMEOUT") {
69 Err(_) => default,
70 Ok(timeout) => match timeout.parse::<u64>() {
71 Ok(x) => Duration::from_secs(x),
72 Err(_) => {
73 eprintln!(
74 "⚠️ TIMEOUT variable is not a valid int: {}. Using default: {}",
75 timeout,
76 default.as_secs()
77 );
78 default
79 }
80 },
81 }
82});
83
84impl Default for KillTimeout {
85 fn default() -> Self {
86 Self(*DEFAULT_KILL_TIMEOUT)
87 }
88}
89
90impl Deref for KillTimeout {
91 type Target = Duration;
92
93 fn deref(&self) -> &Self::Target {
94 &self.0
95 }
96}
97
98impl From<Duration> for KillTimeout {
99 fn from(value: Duration) -> Self {
100 Self(value)
101 }
102}
103
104pub struct SpawnOptions {
106 pub stdout: Stdio,
108 pub stderr: Stdio,
110 pub timeout: KillTimeout,
112 pub group: bool,
115}
116
117impl Default for SpawnOptions {
118 fn default() -> Self {
119 Self {
120 stdout: Stdio::inherit(),
121 stderr: Stdio::inherit(),
122 timeout: KillTimeout::default(),
123 group: false,
124 }
125 }
126}
127
128pub struct Output(Vec<u8>);
130
131impl Output {
132 pub fn bytes(self) -> Vec<u8> {
136 self.0
137 }
138
139 pub fn as_string(self) -> Result<String> {
141 let bytes = self.bytes();
142 let string = String::from_utf8(bytes)?;
143 Ok(string)
144 }
145}
146
147impl<Loc> Cmd<Loc>
148where
149 Loc: Location,
150{
151 #[cfg(unix)]
152 pub(crate) const SHELL: &'static str = "/bin/sh";
153
154 #[cfg(windows)]
155 pub(crate) const SHELL: &'static str = "cmd";
156
157 #[cfg(unix)]
158 pub(crate) fn shelled(cmd: &str) -> Vec<&str> {
159 vec!["-c", cmd]
160 }
161
162 #[cfg(windows)]
163 pub(crate) fn shelled(cmd: &str) -> Vec<&str> {
164 vec!["/c", cmd]
165 }
166
167 pub async fn run(&self) -> Result<()> {
169 eprintln!("{}", crate::headline!(self));
170
171 let opts = SpawnOptions {
172 stdout: Stdio::inherit(),
173 stderr: Stdio::inherit(),
174 ..Default::default()
175 };
176
177 self.spawn(opts)?.wait().await?;
178
179 Ok(())
180 }
181
182 pub async fn silent(&self) -> Result<()> {
184 let opts = SpawnOptions {
185 stdout: Stdio::null(),
186 stderr: Stdio::null(),
187 ..Default::default()
188 };
189
190 self.spawn(opts)?.wait().await?;
191
192 Ok(())
193 }
194
195 pub async fn output(&self) -> Result<Output> {
197 let opts = SpawnOptions {
198 stdout: Stdio::piped(),
199 stderr: Stdio::piped(),
200 ..Default::default()
201 };
202
203 let res = self.spawn(opts)?.wait().await?;
204
205 Ok(Output(res.stdout))
206 }
207
208 #[cfg(unix)]
210 pub fn spawn(&self, opts: SpawnOptions) -> io::Result<RunningProcess> {
211 let cmd = self;
212
213 let SpawnOptions {
214 stdout,
215 stderr,
216 timeout,
217 group,
218 } = opts;
219
220 let mut command = Command::new(Cmd::<Loc>::SHELL);
221 command
222 .args(Cmd::<Loc>::shelled(&cmd.exe))
223 .envs(cmd.env.to_owned())
224 .current_dir(cmd.pwd.as_path())
225 .stdout(stdout)
226 .stderr(stderr);
227
228 if group {
229 command.process_group(0);
230 }
231
232 let process = command.spawn()?;
233
234 Ok(RunningProcess {
235 process,
236 timeout,
237 group,
238 })
239 }
240
241 #[cfg(windows)]
243 pub fn spawn(&self, opts: SpawnOptions) -> io::Result<RunningProcess> {
244 let cmd = self;
245
246 let SpawnOptions {
247 stdout,
248 stderr,
249 timeout,
250 group,
251 } = opts;
252
253 let mut command = Command::new(Cmd::<Loc>::SHELL);
254 command
255 .args(Cmd::<Loc>::shelled(&cmd.exe))
256 .envs(cmd.env.to_owned())
257 .current_dir(cmd.pwd.as_path())
258 .stdout(stdout)
259 .stderr(stderr);
260
261 let process = command.spawn()?;
262
263 Ok(RunningProcess {
264 process,
265 timeout,
266 group,
267 })
268 }
269}
270
271#[macro_export]
312macro_rules! cmd {
313 {
314 $exe:literal,
315 env: $env:expr,
316 pwd: $pwd:expr,
317 msg: $msg:literal$(,)?
318 } => {
319 $crate::Cmd {
320 exe: $exe.to_string(),
321 env: $env,
322 pwd: $pwd,
323 msg: Some($msg.to_string()),
324 }
325 };
326 {
327 exe: $exe:literal,
328 env: $env:expr,
329 pwd: $pwd:expr,
330 msg: $msg:literal$(,)?
331 } => {
332 $crate::Cmd {
333 exe: $exe.to_string(),
334 env: $env,
335 pwd: $pwd,
336 msg: Some($msg.to_string()),
337 }
338 };
339 {
340 $exe:literal,
341 env: $env:expr,
342 pwd: $pwd:expr,
343 msg: Some($msg:expr)$(,)?
344 } => {
345 $crate::Cmd {
346 exe: $exe.to_string(),
347 env: $env,
348 pwd: $pwd,
349 msg: Some($msg),
350 }
351 };
352 {
353 exe: $exe:literal,
354 env: $env:expr,
355 pwd: $pwd:expr,
356 msg: Some($msg:expr)$(,)?
357 } => {
358 $crate::Cmd {
359 exe: $exe.to_string(),
360 env: $env,
361 pwd: $pwd,
362 msg: Some($msg),
363 }
364 };
365 {
366 $exe:literal,
367 env: $env:expr,
368 pwd: $pwd:expr,
369 msg: None$(,)?
370 } => {
371 $crate::Cmd {
372 exe: $exe.to_string(),
373 env: $env,
374 pwd: $pwd,
375 msg: None,
376 }
377 };
378 {
379 exe: $exe:literal,
380 env: $env:expr,
381 pwd: $pwd:expr,
382 msg: None$(,)?
383 } => {
384 $crate::Cmd {
385 exe: $exe.to_string(),
386 env: $env,
387 pwd: $pwd,
388 msg: None,
389 }
390 };
391 {
392 $exe:literal,
393 env: $env:expr,
394 pwd: $pwd:expr,
395 msg: $msg:expr$(,)?
396 } => {
397 $crate::Cmd {
398 exe: $exe.to_string(),
399 env: $env,
400 pwd: $pwd,
401 msg: Some($msg),
402 }
403 };
404 {
405 exe: $exe:literal,
406 env: $env:expr,
407 pwd: $pwd:expr,
408 msg: $msg:expr$(,)?
409 } => {
410 $crate::Cmd {
411 exe: $exe.to_string(),
412 env: $env,
413 pwd: $pwd,
414 msg: Some($msg),
415 }
416 };
417 {
418 $exe:expr,
419 env: $env:expr,
420 pwd: $pwd:expr,
421 msg: $msg:literal$(,)?
422 } => {
423 $crate::Cmd {
424 exe: $exe,
425 env: $env,
426 pwd: $pwd,
427 msg: Some($msg.to_string()),
428 }
429 };
430 {
431 exe: $exe:expr,
432 env: $env:expr,
433 pwd: $pwd:expr,
434 msg: $msg:literal$(,)?
435 } => {
436 $crate::Cmd {
437 exe: $exe,
438 env: $env,
439 pwd: $pwd,
440 msg: Some($msg.to_string()),
441 }
442 };
443 {
444 $exe:expr,
445 env: $env:expr,
446 pwd: $pwd:expr,
447 msg: Some($msg:expr)$(,)?
448 } => {
449 $crate::Cmd {
450 exe: $exe,
451 env: $env,
452 pwd: $pwd,
453 msg: Some($msg),
454 }
455 };
456 {
457 exe: $exe:expr,
458 env: $env:expr,
459 pwd: $pwd:expr,
460 msg: Some($msg:expr)$(,)?
461 } => {
462 $crate::Cmd {
463 exe: $exe,
464 env: $env,
465 pwd: $pwd,
466 msg: Some($msg),
467 }
468 };
469 {
470 $exe:expr,
471 env: $env:expr,
472 pwd: $pwd:expr,
473 msg: None$(,)?
474 } => {
475 $crate::Cmd {
476 exe: $exe,
477 env: $env,
478 pwd: $pwd,
479 msg: None,
480 }
481 };
482 {
483 exe: $exe:expr,
484 env: $env:expr,
485 pwd: $pwd:expr,
486 msg: None$(,)?
487 } => {
488 $crate::Cmd {
489 exe: $exe,
490 env: $env,
491 pwd: $pwd,
492 msg: None,
493 }
494 };
495 {
496 $exe:expr,
497 env: $env:expr,
498 pwd: $pwd:expr,
499 msg: $msg:expr$(,)?
500 } => {
501 $crate::Cmd {
502 exe: $exe,
503 env: $env,
504 pwd: $pwd,
505 msg: Some($msg),
506 }
507 };
508 {
509 exe: $exe:expr,
510 env: $env:expr,
511 pwd: $pwd:expr,
512 msg: $msg:expr$(,)?
513 } => {
514 $crate::Cmd {
515 exe: $exe,
516 env: $env,
517 pwd: $pwd,
518 msg: Some($msg),
519 }
520 };
521 {
522 $exe:literal,
523 env: $env:expr,
524 pwd: $pwd:expr$(,)?
525 } => {
526 $crate::Cmd {
527 exe: $exe.to_string(),
528 env: $env,
529 pwd: $pwd,
530 msg: None,
531 }
532 };
533 {
534 exe: $exe:literal,
535 env: $env:expr,
536 pwd: $pwd:expr$(,)?
537 } => {
538 $crate::Cmd {
539 exe: $exe.to_string(),
540 env: $env,
541 pwd: $pwd,
542 msg: None,
543 }
544 };
545 {
546 $exe:expr,
547 env: $env:expr,
548 pwd: $pwd:expr$(,)?
549 } => {
550 $crate::Cmd {
551 exe: $exe,
552 env: $env,
553 pwd: $pwd,
554 msg: None,
555 }
556 };
557 {
558 exe: $exe:expr,
559 env: $env:expr,
560 pwd: $pwd:expr$(,)?
561 } => {
562 $crate::Cmd {
563 exe: $exe,
564 env: $env,
565 pwd: $pwd,
566 msg: None,
567 }
568 };
569}
570
571#[cfg(test)]
572mod tests {
573 use crate::{Cmd, Env, Location};
574
575 #[allow(dead_code)]
576 fn cmd_macro_unlabeled_exe_literal_msg_literal<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
577 cmd! {
578 "ls",
579 env: env,
580 pwd: loc,
581 msg: "!",
582 }
583 }
584
585 #[allow(dead_code)]
586 fn cmd_macro_labeled_exe_literal_msg_literal<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
587 cmd! {
588 exe: "ls",
589 env: env,
590 pwd: loc,
591 msg: "!",
592 }
593 }
594
595 #[allow(dead_code)]
596 fn cmd_macro_unlabeled_exe_expr_msg_literal<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
597 cmd! {
598 format!("ls {}", "."),
599 env: env,
600 pwd: loc,
601 msg: "!",
602 }
603 }
604
605 #[allow(dead_code)]
606 fn cmd_macro_labeled_exe_expr_msg_literal<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
607 cmd! {
608 exe: format!("ls {}", "."),
609 env: env,
610 pwd: loc,
611 msg: "!",
612 }
613 }
614
615 #[allow(dead_code)]
616 fn cmd_macro_unlabeled_exe_expr_msg_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
617 cmd! {
618 format!("ls {}", "."),
619 env: env,
620 pwd: loc,
621 msg: format!("!"),
622 }
623 }
624
625 #[allow(dead_code)]
626 fn cmd_macro_labeled_exe_expr_msg_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
627 cmd! {
628 exe: format!("ls {}", "."),
629 env: env,
630 pwd: loc,
631 msg: format!("!"),
632 }
633 }
634
635 #[allow(dead_code)]
636 fn cmd_macro_unlabeled_exe_literal_msg_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
637 cmd! {
638 "ls",
639 env: env,
640 pwd: loc,
641 msg: format!("!"),
642 }
643 }
644
645 #[allow(dead_code)]
646 fn cmd_macro_labeled_exe_literal_msg_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
647 cmd! {
648 exe: "ls",
649 env: env,
650 pwd: loc,
651 msg: format!("!"),
652 }
653 }
654
655 #[allow(dead_code)]
656 fn cmd_macro_unlabeled_exe_literal_msg_some_expr<Loc: Location>(
657 env: Env,
658 loc: Loc,
659 ) -> Cmd<Loc> {
660 cmd! {
661 "ls",
662 env: env,
663 pwd: loc,
664 msg: Some(format!("!")),
665 }
666 }
667
668 #[allow(dead_code)]
669 fn cmd_macro_labeled_exe_literal_msg_some_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
670 cmd! {
671 exe: "ls",
672 env: env,
673 pwd: loc,
674 msg: Some(format!("!")),
675 }
676 }
677
678 #[allow(dead_code)]
679 fn cmd_macro_unlabeled_exe_expr_msg_some_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
680 cmd! {
681 format!("ls {}", "."),
682 env: env,
683 pwd: loc,
684 msg: Some(format!("!")),
685 }
686 }
687
688 #[allow(dead_code)]
689 fn cmd_macro_labeled_exe_expr_msg_some_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
690 cmd! {
691 exe: format!("ls {}", "."),
692 env: env,
693 pwd: loc,
694 msg: Some(format!("!")),
695 }
696 }
697
698 #[allow(dead_code)]
699 fn cmd_macro_unlabeled_exe_literal_msg_none<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
700 cmd! {
701 "ls",
702 env: env,
703 pwd: loc,
704 msg: None,
705 }
706 }
707
708 #[allow(dead_code)]
709 fn cmd_macro_labeled_exe_literal_msg_none<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
710 cmd! {
711 exe: "ls",
712 env: env,
713 pwd: loc,
714 msg: None,
715 }
716 }
717
718 #[allow(dead_code)]
719 fn cmd_macro_unlabeled_exe_expr_msg_none<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
720 cmd! {
721 format!("ls {}", "."),
722 env: env,
723 pwd: loc,
724 msg: None,
725 }
726 }
727
728 #[allow(dead_code)]
729 fn cmd_macro_labeled_exe_expr_msg_none<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
730 cmd! {
731 exe: format!("ls {}", "."),
732 env: env,
733 pwd: loc,
734 msg: None,
735 }
736 }
737
738 #[allow(dead_code)]
739 fn cmd_macro_unlabeled_exe_literal_no_msg<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
740 cmd! {
741 "ls",
742 env: env,
743 pwd: loc,
744 }
745 }
746
747 #[allow(dead_code)]
748 fn cmd_macro_labeled_exe_literal_no_msg<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
749 cmd! {
750 exe: "ls",
751 env: env,
752 pwd: loc,
753 }
754 }
755
756 #[allow(dead_code)]
757 fn cmd_macro_unlabeled_exe_expr_no_msg<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
758 cmd! {
759 format!("ls {}", "."),
760 env: env,
761 pwd: loc,
762 }
763 }
764
765 #[allow(dead_code)]
766 fn cmd_macro_labeled_exe_expr_no_msg<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
767 cmd! {
768 exe: format!("ls {}", "."),
769 env: env,
770 pwd: loc,
771 }
772 }
773
774 #[allow(dead_code)]
775 fn cmd_macro_unlabeled_exe_no_trailing_comma<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
776 cmd! { "ls", env: env, pwd: loc }
777 }
778
779 #[allow(dead_code)]
780 fn cmd_macro_labeled_exe_no_trailing_comma<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
781 cmd! { exe: "ls", env: env, pwd: loc }
782 }
783}