Skip to main content

try/
main.rs

1//! Re-run a command until success (ok) or failure (ko) with cooldown.
2//!
3//! # Errors
4//! - Argument parsing or command execution fails.
5#![feature(exit_status_error)]
6
7use core::str::FromStr;
8use std::process::Command;
9use std::process::ExitStatusError;
10use std::time::Duration;
11use std::time::Instant;
12
13use rootcause::prelude::ResultExt;
14use rootcause::report;
15use ytil_sys::cli::Args;
16
17/// Exit condition for retry loop.
18#[cfg_attr(test, derive(Debug))]
19enum ExitCond {
20    /// Exit when the command succeeds.
21    Ok,
22    /// Exit when the command fails.
23    Ko,
24}
25
26impl ExitCond {
27    /// Determines if the loop should break based on the exit condition and command result.
28    pub const fn should_break(&self, cmd_res: Result<(), ExitStatusError>) -> bool {
29        matches!((self, cmd_res), (Self::Ok, Ok(())) | (Self::Ko, Err(_)))
30    }
31}
32
33/// Parses [`ExitCond`] from string.
34impl FromStr for ExitCond {
35    type Err = rootcause::Report;
36
37    fn from_str(s: &str) -> Result<Self, Self::Err> {
38        Ok(match s {
39            "ok" => Self::Ok,
40            "ko" => Self::Ko,
41            unexpected => Err(report!("unexpected exit condition")).attach_with(|| format!("value={unexpected}"))?,
42        })
43    }
44}
45
46/// Re-run a command until success (ok) or failure (ko) with cooldown.
47#[ytil_sys::main]
48fn main() -> rootcause::Result<()> {
49    let args = ytil_sys::cli::get();
50
51    if args.has_help() {
52        println!("{}", include_str!("../help.txt"));
53        return Ok(());
54    }
55
56    let Some((cooldown_secs, args)) = args.split_first() else {
57        return Err(report!("missing cooldown arg")).attach_with(|| format!("args={args:#?}"));
58    };
59    let cooldown = Duration::from_secs(
60        cooldown_secs
61            .parse()
62            .context("invalid cooldown secs")
63            .attach_with(|| format!("value={cooldown_secs}"))?,
64    );
65
66    let Some((exit_cond, args)) = args.split_first() else {
67        return Err(report!("missing exit condition arg")).attach_with(|| format!("args={args:#?}"));
68    };
69    let exit_cond = ExitCond::from_str(exit_cond)
70        .context("invalid exit condition")
71        .attach_with(|| format!("args={args:#?}"))?;
72
73    let Some((program, program_args)) = args.split_first() else {
74        return Err(report!("missing command arg")).attach_with(|| format!("args={args:#?}"));
75    };
76
77    let mut tries = vec![];
78    loop {
79        let now = Instant::now();
80        let output = Command::new(program)
81            .args(program_args)
82            .output()
83            .context("error running cmd")
84            .attach_with(|| format!("program={program:?} args={program_args:?}"))?;
85        tries.push(now.elapsed());
86
87        let terminal_output = if output.status.success() {
88            output.stdout
89        } else {
90            output.stderr
91        };
92        println!("{}", String::from_utf8_lossy(&terminal_output));
93
94        if exit_cond.should_break(output.status.exit_ok()) {
95            break;
96        }
97        std::thread::sleep(cooldown);
98    }
99
100    let tries_count = u32::try_from(tries.len())
101        .context("cannot convert tries len to u32")
102        .attach_with(|| format!("len={}", tries.len()))?;
103    let total_time = tries.iter().fold(Duration::ZERO, |acc, &d| acc.saturating_add(d));
104    let avg_runs_time = if tries_count > 0 {
105        total_time.checked_div(tries_count).unwrap_or(Duration::ZERO)
106    } else {
107        Duration::ZERO
108    };
109    println!("Summary:\n - tries {tries_count}\n - avg time {avg_runs_time:#?}");
110
111    Ok(())
112}
113
114#[cfg(test)]
115mod tests {
116    use core::str::FromStr;
117
118    use super::*;
119
120    #[test]
121    fn exit_cond_from_str_when_ok_returns_ok_variant() {
122        assert2::assert!(let Ok(ExitCond::Ok) = ExitCond::from_str("ok"));
123    }
124
125    #[test]
126    fn exit_cond_from_str_when_ko_returns_ko_variant() {
127        assert2::assert!(let Ok(ExitCond::Ko) = ExitCond::from_str("ko"));
128    }
129
130    #[test]
131    fn exit_cond_from_str_when_invalid_returns_error() {
132        assert2::assert!(let Err(err) = ExitCond::from_str("invalid"));
133        assert!(err.to_string().contains("unexpected exit condition"));
134    }
135
136    #[test]
137    fn should_break_ok_cond_with_success_result_returns_true() {
138        pretty_assertions::assert_eq!(ExitCond::Ok.should_break(Ok(())), true);
139    }
140
141    #[test]
142    fn should_break_ok_cond_with_failure_result_returns_false() {
143        let err_result: Result<(), ExitStatusError> = std::process::Command::new("false").status().unwrap().exit_ok();
144        pretty_assertions::assert_eq!(ExitCond::Ok.should_break(err_result), false);
145    }
146
147    #[test]
148    fn should_break_ko_cond_with_failure_result_returns_true() {
149        let err_result: Result<(), ExitStatusError> = std::process::Command::new("false").status().unwrap().exit_ok();
150        pretty_assertions::assert_eq!(ExitCond::Ko.should_break(err_result), true);
151    }
152
153    #[test]
154    fn should_break_ko_cond_with_success_result_returns_false() {
155        pretty_assertions::assert_eq!(ExitCond::Ko.should_break(Ok(())), false);
156    }
157}