mod args;
mod cancel;
mod commands;
mod error;
mod exit;
mod locate;
mod log;
#[cfg(test)]
mod reference;
mod term;
#[cfg(test)]
mod testing;
mod text;
mod values;
use std::process::ExitCode;
use std::sync::Arc;
use clap::Parser;
use isothrum_runner::{Runner, RunnerConfig, SystemProcesses};
use crate::args::Cli;
use crate::error::{Error, Result};
use crate::exit::Verdict;
use crate::term::{Term, Trail, failure};
use crate::values::Var;
#[tokio::main]
async fn main() -> ExitCode {
let term = Term::std();
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => return refused(&e, &term),
};
let var = |name: &str| std::env::var_os(name);
if let Err(e) = log::from_env(&var) {
term.err(&format!("warning: {e}"));
}
let trail = Trail::default();
let processes = SystemProcesses::default();
let outcome = tokio::select! {
outcome = start(&cli, &processes, &term, &trail, &var) => outcome,
signal = cancelled(&term) => Err(Error::Cancelled(signal)),
};
tools_stopped(&processes, &term).await;
let outcome = outcome.and_then(|verdict| term.finish().map(|()| verdict));
if let Err(e) = &outcome {
report(e, &cli, &term, &trail);
}
exit::code(&outcome)
}
async fn start(cli: &Cli, processes: &SystemProcesses, term: &Term, trail: &Trail, var: &Var<'_>) -> Result<Verdict> {
let runner = || -> Result<Runner> {
let home = locate::home(cli.home.clone(), var("ISOTHRUM_HOME"), std::env::home_dir())?;
let mut config = RunnerConfig::new(home);
config.processes = Arc::new(processes.clone());
Ok(Runner::new(config)?)
};
commands::run(cli, runner, term, trail, var).await
}
async fn tools_stopped(processes: &SystemProcesses, term: &Term) {
if !processes.is_stopping() {
return;
}
term.err("Waiting for the tools to stop (Ctrl-C again to end them now)…");
tokio::select! {
() = processes.stopped() => {}
Ok(_) = cancel::requested() => {
processes.end_stopping();
processes.stopped().await;
}
}
}
async fn cancelled(term: &Term) -> cancel::Signal {
match cancel::requested().await {
Ok(signal) => signal,
Err(e) => {
term.err(&format!("warning: Ctrl-C and SIGTERM won't stop the command's tools: {e}"));
std::future::pending().await
}
}
}
fn report(e: &Error, cli: &Cli, term: &Term, trail: &Trail) {
if trail.ended_in_error() {
return;
}
if !cli.json {
term.err(&format!("error: {e}"));
return;
}
let kind = failure(e.code(), e.to_string());
let run = commands::run_named(&cli.command).map(str::to_owned);
let event = trail.next(run, commands::op_of(&cli.command), kind);
term.json(&event);
}
fn refused(e: &clap::Error, term: &Term) -> ExitCode {
let json = std::env::args_os().any(|a| a == "--json");
if !json || !e.use_stderr() {
e.exit()
}
let kind = failure(isothrum_runner::ErrorCode::Usage, e.render().to_string().trim_end().to_owned());
term.json(&Trail::default().next(None, None, kind));
ExitCode::from(exit::USAGE)
}