use std::process::ExitCode;
use std::sync::atomic::{AtomicU8, Ordering};
use youtube_legend_cli::cli::{load_config, parse_with_overrides};
use youtube_legend_cli::config;
use youtube_legend_cli::error::AppError;
use youtube_legend_cli::i18n::{t, Message};
use youtube_legend_cli::io::is_broken_pipe;
use youtube_legend_cli::logging::init_tracing;
use youtube_legend_cli::run;
const EXIT_SIGINT: u8 = 130;
const EXIT_SIGTERM: u8 = 143;
const EXIT_BROKEN_PIPE: u8 = 141;
const EXIT_USAGE: u8 = 2;
const DEFAULT_WORKER_THREADS_MIN: usize = 2;
const DEFAULT_WORKER_THREADS_MAX: usize = 8;
const WORKER_THREADS_FALLBACK: usize = 4;
const WORKER_THREADS_HARD_MAX: usize = 1_024;
static SIGNAL_EXIT: AtomicU8 = AtomicU8::new(0);
use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
fn main() -> ExitCode {
let (mut cli, cli_overrides) = match parse_with_overrides() {
Ok(pair) => pair,
Err(e) => return report_clap_error(&e),
};
let config_path = cli.config.clone().or_else(config::discover);
if let Some(path) = config_path {
match youtube_legend_cli::config::ConfigStore::load_from(&path) {
Ok(store) => config::install_tuning(store.table().clone()),
Err(e) => {
eprintln!("{e}");
return ExitCode::from(e.exit_code());
}
}
match load_config(&path) {
Ok(overrides) => cli.apply_config_overrides(overrides, &cli_overrides),
Err(e) => {
eprintln!("{e}");
return ExitCode::from(e.exit_code());
}
}
}
cli.apply_overrides();
if let Err(e) = init_tracing(
cli.effective_log_level(),
cli.effective_log_format(),
cli.color,
cli.quiet,
cli.json,
) {
eprintln!("{}: {e}", t(Message::ErrTracingInit));
return ExitCode::from(e.exit_code());
}
let shutdown_token = tokio_util::sync::CancellationToken::new();
let worker_threads =
resolve_worker_threads(std::thread::available_parallelism().ok().map(|n| n.get()));
let runtime = match tokio::runtime::Builder::new_multi_thread()
.worker_threads(worker_threads)
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => {
tracing::error!(error = %e, "failed to start tokio runtime");
return ExitCode::from(AppError::Internal(format!("tokio runtime: {e}")).exit_code());
}
};
let exit_code = runtime.block_on(async move {
let signal_watcher = tokio::spawn(install_signal_handler(shutdown_token.clone()));
let result = tokio::select! {
biased;
result = run(cli) => result,
_ = shutdown_token.cancelled() => {
tracing::warn!("cancellation requested before completion");
let code = match SIGNAL_EXIT.load(Ordering::SeqCst) {
0 => EXIT_SIGINT,
seen => seen,
};
Ok(ExitCode::from(code))
}
};
signal_watcher.abort();
result
});
match exit_code {
Ok(code) => code,
Err(e) if is_broken_pipe(&e) => ExitCode::from(EXIT_BROKEN_PIPE),
Err(e) => ExitCode::from(e.exit_code()),
}
}
fn json_requested() -> bool {
std::env::args_os().any(|arg| arg == "--json")
}
fn report_clap_error(err: &clap::Error) -> ExitCode {
use clap::error::ErrorKind;
if matches!(
err.kind(),
ErrorKind::DisplayHelp
| ErrorKind::DisplayVersion
| ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
) {
let _ = err.print();
return ExitCode::SUCCESS;
}
let code = u8::try_from(err.exit_code()).unwrap_or(EXIT_USAGE);
if json_requested() {
let envelope = serde_json::json!({
"error": true,
"code": code,
"message": err.render().to_string().trim_end(),
"kind": "invalid_usage",
"retryable": false,
});
println!("{envelope}");
} else {
let _ = err.print();
}
ExitCode::from(code)
}
fn order_worker_thread_bounds(low: usize, high: usize) -> (usize, usize) {
if low > high {
tracing::warn!(
low,
high,
"cli.worker_threads_min is above cli.worker_threads_max; treating the pair as swapped"
);
return (high, low);
}
(low, high)
}
fn resolve_worker_threads(available: Option<usize>) -> usize {
let low = config::tuning_usize_in_range(
"cli.worker_threads_min",
DEFAULT_WORKER_THREADS_MIN,
1,
WORKER_THREADS_HARD_MAX,
);
let high = config::tuning_usize_in_range(
"cli.worker_threads_max",
DEFAULT_WORKER_THREADS_MAX,
1,
WORKER_THREADS_HARD_MAX,
);
let (low, high) = order_worker_thread_bounds(low, high);
available
.unwrap_or(WORKER_THREADS_FALLBACK)
.clamp(low, high)
}
#[cfg(unix)]
async fn install_signal_handler(token: tokio_util::sync::CancellationToken) {
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = match signal(SignalKind::terminate()) {
Ok(s) => s,
Err(e) => {
tracing::warn!(error = %e, "could not install SIGTERM handler");
return;
}
};
let mut sigint = match signal(SignalKind::interrupt()) {
Ok(s) => s,
Err(e) => {
tracing::warn!(error = %e, "could not install SIGINT handler");
return;
}
};
tracing::info!(
target: "events",
event = "signal_handler_installed",
"signal handlers installed"
);
let mut first = true;
loop {
let code = tokio::select! {
biased;
_ = sigterm.recv() => EXIT_SIGTERM,
_ = sigint.recv() => EXIT_SIGINT,
};
if first {
SIGNAL_EXIT.store(code, Ordering::SeqCst);
tracing::info!(target: "events", event = "signal", exit_code = code, "shutdown requested");
token.cancel();
first = false;
} else {
tracing::warn!(target: "events", event = "signal", "second signal received; forcing immediate exit");
std::process::exit(i32::from(code));
}
}
}
#[cfg(not(unix))]
async fn install_signal_handler(token: tokio_util::sync::CancellationToken) {
let mut first = true;
loop {
if let Err(e) = tokio::signal::ctrl_c().await {
tracing::warn!(error = %e, "could not install SIGINT handler");
return;
}
if first {
SIGNAL_EXIT.store(EXIT_SIGINT, Ordering::SeqCst);
tracing::info!(target: "events", event = "signal", "shutdown requested");
token.cancel();
first = false;
} else {
tracing::warn!(target: "events", event = "signal", "second signal received; forcing immediate exit");
std::process::exit(i32::from(EXIT_SIGINT));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inverted_worker_thread_bounds_do_not_panic() {
let mut cli_table = toml::Table::new();
cli_table.insert("worker_threads_min".to_string(), toml::Value::Integer(8));
cli_table.insert("worker_threads_max".to_string(), toml::Value::Integer(2));
let mut table = toml::Table::new();
table.insert("cli".to_string(), toml::Value::Table(cli_table));
config::install_tuning(table);
assert_eq!(order_worker_thread_bounds(8, 2), (2, 8));
assert_eq!(order_worker_thread_bounds(2, 8), (2, 8));
assert_eq!(resolve_worker_threads(Some(1)), 2);
assert_eq!(resolve_worker_threads(Some(4)), 4);
assert_eq!(resolve_worker_threads(Some(64)), 8);
assert_eq!(resolve_worker_threads(None), WORKER_THREADS_FALLBACK);
}
}