use std::path::Path;
use std::process::ExitCode;
use std::time::Duration;
use clap::{CommandFactory, Parser};
use clap_complete::env::{Bash, CompleteEnv, Fish, Shells, Zsh};
use overbrainer::cli::{self, Cli};
use overbrainer::config::{DOTENV_FILE, DotenvKeys};
fn main() -> ExitCode {
CompleteEnv::with_factory(Cli::command)
.shells(Shells(&[&Bash, &Zsh, &Fish]))
.complete();
let mut cli = Cli::parse();
match cli::wizard(&mut cli) {
Ok(true) => {},
Ok(false) => return ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e:#}");
return ExitCode::FAILURE;
},
}
let needs_env = !matches!(cli.command, cli::Command::Skill { .. });
let dotenv = if needs_env {
let path = cli.project_dir.join(DOTENV_FILE);
let loaded = DotenvKeys::record(&path, process_vars())
.map_err(|error| error.to_string())
.and_then(|keys| load_dotenv(&path).map(|()| keys));
match loaded {
Ok(keys) => keys,
Err(message) => {
eprintln!("error: {message}");
return ExitCode::FAILURE;
},
}
} else {
DotenvKeys::default()
};
let logs = cli.command.log_mode();
overbrainer::logging::init(&logs);
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(e) => {
eprintln!("error: cannot start async runtime: {e}");
return ExitCode::FAILURE;
},
};
let result = runtime.block_on(cli::run(cli, logs, dotenv));
shut_down(runtime);
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e:#}");
ExitCode::FAILURE
},
}
}
const SHUTDOWN_GRACE: Duration = Duration::from_millis(500);
fn shut_down(runtime: tokio::runtime::Runtime) {
runtime.shutdown_timeout(SHUTDOWN_GRACE);
}
fn process_vars() -> impl Iterator<Item = (String, String)> {
std::env::vars_os().filter_map(|(key, value)| {
Some((
key.into_string().ok()?,
value.to_string_lossy().into_owned(),
))
})
}
fn load_dotenv(path: &Path) -> Result<(), String> {
match dotenvy::from_path(path) {
Ok(()) => Ok(()),
Err(dotenvy::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(dotenvy::Error::Io(e)) => Err(format!("cannot load .env: {e}")),
Err(_) => Err("cannot load .env".to_string()),
}
}
#[cfg(test)]
mod tests {
use std::time::Instant;
use super::*;
#[test]
fn a_stuck_blocking_task_holds_the_exit_at_most_the_grace() -> std::io::Result<()> {
let runtime = tokio::runtime::Builder::new_multi_thread().build()?;
let (started, running) = std::sync::mpsc::channel();
runtime.spawn_blocking(move || {
started.send(()).ok();
std::thread::sleep(Duration::from_secs(10));
});
running
.recv_timeout(Duration::from_secs(5))
.map_err(std::io::Error::other)?;
let start = Instant::now();
shut_down(runtime);
let took = start.elapsed();
assert!(
took >= SHUTDOWN_GRACE && took < SHUTDOWN_GRACE + Duration::from_secs(3),
"{took:?}"
);
Ok(())
}
}