pub(crate) mod budget;
pub(crate) mod condition;
#[cfg(test)]
#[path = "tests.rs"]
mod tests;
use anyhow::Context as _;
use crate::cli::{WaitArgs, WaitFor};
use budget::Budget;
use condition::{Condition, Poll};
pub(crate) const EXIT_MET: i32 = 0;
pub(crate) const EXIT_TIMEOUT: i32 = 1;
pub(crate) const EXIT_ERROR: i32 = 2;
pub(crate) const EXIT_PENDING: i32 = 75;
const MAX_CONSECUTIVE_PROBE_ERRORS: u32 = 3;
const ABANDON_AFTER_SECS: u64 = 600;
pub(crate) trait Clock {
fn now_unix(&self) -> u64;
fn sleep(&self, secs: u64);
}
pub(crate) struct SystemClock;
impl Clock for SystemClock {
fn now_unix(&self) -> u64 {
budget::now_unix()
}
fn sleep(&self, secs: u64) {
std::thread::sleep(std::time::Duration::from_secs(secs));
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Plan {
pub(crate) timeout_s: u64,
pub(crate) interval_s: u64,
pub(crate) slice_s: u64,
}
const SLICE_MIN: u64 = 5;
const SLICE_MAX: u64 = 500;
const TIMEOUT_MAX: u64 = 86_400;
impl WaitFor {
fn default_interval(self) -> u64 {
match self {
WaitFor::Run | WaitFor::File => 5,
WaitFor::Check => 20,
}
}
fn interval_floor(self) -> u64 {
match self {
WaitFor::Run | WaitFor::File => 1,
WaitFor::Check => 10,
}
}
fn as_str(self) -> &'static str {
match self {
WaitFor::Run => "run",
WaitFor::File => "file",
WaitFor::Check => "check",
}
}
}
pub(crate) fn resolve_plan(args: &WaitArgs) -> anyhow::Result<Plan> {
anyhow::ensure!(args.timeout > 0, "--timeout must be at least 1 second");
let timeout_s = args.timeout.min(TIMEOUT_MAX);
let interval_s = args
.interval
.unwrap_or_else(|| args.condition.default_interval())
.max(args.condition.interval_floor());
let slice_s = args.slice.clamp(SLICE_MIN, SLICE_MAX).min(timeout_s);
Ok(Plan {
timeout_s,
interval_s,
slice_s,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Status {
Met,
Pending,
Timeout,
Error,
}
impl Status {
pub(crate) fn as_str(self) -> &'static str {
match self {
Status::Met => "met",
Status::Pending => "pending",
Status::Timeout => "timeout",
Status::Error => "error",
}
}
pub(crate) fn exit_code(self) -> i32 {
match self {
Status::Met => EXIT_MET,
Status::Pending => EXIT_PENDING,
Status::Timeout => EXIT_TIMEOUT,
Status::Error => EXIT_ERROR,
}
}
pub(crate) fn is_terminal(self) -> bool {
!matches!(self, Status::Pending)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Outcome {
pub(crate) status: Status,
pub(crate) detail: String,
}
pub(crate) fn poll_until<C: Condition + ?Sized, K: Clock>(
cond: &C,
plan: &Plan,
budget: &mut Budget,
clock: &K,
) -> anyhow::Result<Outcome> {
let slice_end = clock.now_unix().saturating_add(plan.slice_s);
let mut consecutive_errors = 0u32;
let mut detail: String;
loop {
budget.polls += 1;
budget.updated_at = clock.now_unix();
match cond.poll() {
Ok(Poll::Met(d)) => {
return Ok(Outcome {
status: Status::Met,
detail: d,
});
}
Ok(Poll::Pending(d)) => {
consecutive_errors = 0;
detail = d;
}
Err(e) => {
consecutive_errors += 1;
if consecutive_errors > MAX_CONSECUTIVE_PROBE_ERRORS {
return Err(e.context(format!(
"condition probe failed {consecutive_errors} times in a row"
)));
}
detail = format!("probe error {consecutive_errors}, retrying: {e}");
}
}
let now = clock.now_unix();
if budget.expired(now) {
return Ok(Outcome {
status: Status::Timeout,
detail,
});
}
if now >= slice_end {
return Ok(Outcome {
status: Status::Pending,
detail,
});
}
let nap = plan
.interval_s
.min(budget.remaining(now))
.min(slice_end.saturating_sub(now))
.max(1);
clock.sleep(nap);
}
}
pub(crate) fn status_line(
outcome: &Outcome,
verb: WaitFor,
budget: &Budget,
now: u64,
rerun: &str,
) -> String {
let detail = outcome.detail.replace('"', "'").replace('\n', " ");
let mut line = format!(
"tm-wait status={} for={} elapsed={} remaining={} polls={} detail=\"{}\"",
outcome.status.as_str(),
verb.as_str(),
budget.elapsed(now),
budget.remaining(now),
budget.polls,
detail
);
if outcome.status == Status::Pending {
line.push_str(&format!(" rerun=\"{rerun}\""));
}
line
}
pub(crate) fn rerun_command(args: &WaitArgs) -> String {
let mut parts = vec![
"tm wait".to_string(),
format!("--for {}", args.condition.as_str()),
];
if let Some(pid) = args.pid {
parts.push(format!("--pid {pid}"));
}
if let Some(h) = &args.handle {
parts.push(format!("--handle {}", h.display()));
}
if let Some(p) = &args.path {
parts.push(format!("--path {}", p.display()));
}
if let Some(c) = &args.contains {
parts.push(format!("--contains {c:?}"));
}
if let Some(pr) = args.pr {
parts.push(format!("--pr {pr}"));
}
if let Some(r) = &args.repo {
parts.push(format!("--repo {r}"));
}
if args.allow_empty_checks {
parts.push("--allow-empty-checks".to_string());
}
parts.push(format!("--timeout {}", args.timeout));
if let Some(i) = args.interval {
parts.push(format!("--interval {i}"));
}
parts.push(format!("--slice {}", args.slice));
if let Some(d) = &args.state_dir {
parts.push(format!("--state-dir {}", d.display()));
}
parts.join(" ")
}
pub(crate) fn run(args: WaitArgs) -> ! {
let code = match run_inner(&args) {
Ok(code) => code,
Err(e) => {
let outcome = Outcome {
status: Status::Error,
detail: format!("{e:#}"),
};
let spent = Budget {
spec: condition::spec(&args),
started_at: 0,
updated_at: 0,
polls: 0,
timeout_s: 0,
};
println!("{}", status_line(&outcome, args.condition, &spent, 0, ""));
eprintln!("tm wait: {e:#}");
EXIT_ERROR
}
};
std::process::exit(code)
}
fn run_inner(args: &WaitArgs) -> anyhow::Result<i32> {
let plan = resolve_plan(args)?;
let cond = condition::build(args)?;
let spec = condition::spec(args);
let dir = args
.state_dir
.clone()
.unwrap_or_else(budget::default_state_dir);
let state_path = budget::path_for(&dir, &spec);
if args.reset {
budget::clear(&state_path);
}
let clock = SystemClock;
let now = clock.now_unix();
let abandon_after = ABANDON_AFTER_SECS.max(plan.interval_s.saturating_mul(10));
let mut budget = budget::load_or_start(&state_path, &spec, plan.timeout_s, now, abandon_after);
let outcome = poll_until(cond.as_ref(), &plan, &mut budget, &clock)?;
let end = clock.now_unix();
if outcome.status.is_terminal() {
budget::clear(&state_path);
} else {
budget::save(&state_path, &budget).context("cannot persist the wait budget")?;
}
println!(
"{}",
status_line(&outcome, args.condition, &budget, end, &rerun_command(args))
);
Ok(outcome.status.exit_code())
}