try/
main.rs

1//! Re-run a command until success (ok) or failure (ko) with cooldown.
2//!
3//! # Arguments
4//! - `cooldown_secs` Seconds to sleep between tries.
5//! - `exit_condition` `ok` (stop on success) or `ko` (stop on failure).
6//! - `command...` Remainder joined and executed via `sh -c`.
7//!
8//! # Usage
9//! ```bash
10//! try 2 ok cargo test # rerun every 2s until tests pass
11//! try 5 ko curl -f localhost:8080 # rerun until it fails (e.g. service goes down)
12//! ```
13//!
14//! # Errors
15//! - Cooldown seconds parse fails.
16//! - Exit condition parse fails.
17//! - Spawning or executing command fails.
18//! - UTF-8 conversion for output or error context fails.
19#![feature(exit_status_error)]
20
21use core::str::FromStr;
22use std::process::Command;
23use std::process::ExitStatusError;
24use std::time::Duration;
25use std::time::Instant;
26
27use color_eyre::eyre;
28use color_eyre::eyre::WrapErr;
29use color_eyre::eyre::bail;
30use itertools::Itertools;
31use ytil_sys::cli::Args;
32
33/// Exit condition for retry loop.
34enum ExitCond {
35    /// Exit when the command succeeds.
36    Ok,
37    /// Exit when the command fails.
38    Ko,
39}
40
41impl ExitCond {
42    /// Determines if the loop should break.
43    #[allow(clippy::suspicious_operation_groupings)]
44    pub const fn should_break(&self, cmd_res: Result<(), ExitStatusError>) -> bool {
45        self.is_ok() && cmd_res.is_ok() || !self.is_ok() && cmd_res.is_err()
46    }
47
48    /// Checks if this represents success.
49    const fn is_ok(&self) -> bool {
50        match self {
51            Self::Ok => true,
52            Self::Ko => false,
53        }
54    }
55}
56
57/// Parses [`ExitCond`] from string.
58impl FromStr for ExitCond {
59    type Err = eyre::Error;
60
61    fn from_str(s: &str) -> Result<Self, Self::Err> {
62        Ok(match s {
63            "ok" => Self::Ok,
64            "ko" => Self::Ko,
65            unexpected => bail!("unexpected exit condition | value={unexpected}"),
66        })
67    }
68}
69
70/// Re-run a command until success (ok) or failure (ko) with cooldown.
71fn main() -> color_eyre::Result<()> {
72    color_eyre::install()?;
73
74    let args = ytil_sys::cli::get();
75
76    if args.has_help() {
77        println!("{}", include_str!("../help.txt"));
78        return Ok(());
79    }
80
81    let Some((cooldown_secs, args)) = args.split_first() else {
82        bail!("missing cooldown arg | args={args:#?}");
83    };
84    let cooldown = Duration::from_secs(
85        cooldown_secs
86            .parse()
87            .with_context(|| format!("invalid cooldown secs | value={cooldown_secs}"))?,
88    );
89
90    let Some((exit_cond, args)) = args.split_first() else {
91        bail!("missing exit condition arg | args={args:#?}");
92    };
93    let exit_cond =
94        ExitCond::from_str(exit_cond).with_context(|| format!("invalid exit condition | args={args:#?}"))?;
95
96    let cmd = args.iter().join(" ");
97
98    let mut tries = vec![];
99    loop {
100        let now = Instant::now();
101        let output = Command::new("sh")
102            .arg("-c")
103            .arg(&cmd)
104            .output()
105            .with_context(|| format!("error running cmd | cmd={cmd:?}"))?;
106        tries.push(now.elapsed());
107
108        let terminal_output = if output.status.success() {
109            output.stdout
110        } else {
111            output.stderr
112        };
113        println!("{}", String::from_utf8_lossy(&terminal_output));
114
115        if exit_cond.should_break(output.status.exit_ok()) {
116            break;
117        }
118        std::thread::sleep(cooldown);
119    }
120
121    let tries_count =
122        u32::try_from(tries.len()).with_context(|| format!("cannot convert tries len to u32 | len={}", tries.len()))?;
123    let total_time = tries.iter().fold(Duration::ZERO, |acc, &d| acc.saturating_add(d));
124    let avg_runs_time = if tries_count > 0 {
125        total_time.checked_div(tries_count).unwrap_or(Duration::ZERO)
126    } else {
127        Duration::ZERO
128    };
129    println!("Summary:\n - tries {tries_count}\n - avg time {avg_runs_time:#?}");
130
131    Ok(())
132}