watchctl 0.3.0

Process supervisor with wait, watch, and retry phases
mod check;
mod cli;
mod config;
mod duration;
mod error;
mod process;
mod retry;
mod wait;
mod watch;

use config::Config;
use error::Result;
use process::Process;
use retry::RetryState;
use std::fs::File;
use std::process::ExitCode;
use tracing::{error, info};
use tracing_subscriber::EnvFilter;
use tracing_subscriber::fmt::writer::BoxMakeWriter;
use watch::WatchResult;

#[tokio::main]
async fn main() -> ExitCode {
    let args = cli::parse();
    let log_file = args.log.clone();

    init_logging(log_file.as_deref());

    match run(args).await {
        Ok(code) => code,
        Err(e) => {
            error!("{e}");
            ExitCode::FAILURE
        }
    }
}

fn init_logging(log_file: Option<&str>) {
    let Some(path) = log_file else {
        return;
    };

    let file = File::create(path).expect("failed to create log file");
    let writer = BoxMakeWriter::new(file);

    tracing_subscriber::fmt()
        .with_env_filter(
            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
        )
        .with_target(false)
        .with_ansi(false)
        .with_writer(writer)
        .init();
}

async fn run(args: cli::Args) -> Result<ExitCode> {
    let config = Config::from_args(args)?;

    let mut retry_state = RetryState::new(&config.retry);
    let mut run_wait = true;

    loop {
        if run_wait && let Err(e) = wait::run_wait_phase(&config.wait).await {
            error!("wait phase failed: {e}");
            return Err(e);
        }

        info!("starting command: {:?}", config.command);
        let process = Process::spawn(&config.command)?;

        let result = watch::run_watch_phase(&config.watch, process).await?;

        let exit_status = match result {
            WatchResult::ProcessExited(status) => Some(status),
            WatchResult::HealthCheckFailed(_) | WatchResult::Timeout => {
                return Ok(ExitCode::FAILURE);
            }
        };

        if !retry_state.should_retry(&config.retry, exit_status) {
            return Ok(exit_code_from_status(exit_status.unwrap()));
        }

        retry_state.wait_before_retry(&config.retry).await;
        run_wait = config.retry.with_wait;
    }
}

fn exit_code_from_status(status: std::process::ExitStatus) -> ExitCode {
    match status.code() {
        Some(0) => ExitCode::SUCCESS,
        Some(code) => ExitCode::from(code.clamp(1, 255) as u8),
        None => ExitCode::FAILURE,
    }
}