use keyhog::args::Command;
use keyhog::orchestrator::EXIT_SCANNER_PANIC;
use keyhog::{subcommands, FINDINGS_COUNT, SCANNED_CHUNKS, SCANNER_PANICKED, TOTAL_CHUNKS};
use std::io::IsTerminal;
use std::process::ExitCode;
use std::sync::atomic::Ordering;
const EXIT_USER_ERROR: u8 = 2;
const EXIT_SYSTEM_ERROR: u8 = 3;
#[cfg(unix)]
fn reset_sigpipe() {
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
}
#[cfg(not(unix))]
fn reset_sigpipe() {}
#[tokio::main(flavor = "current_thread")]
async fn main() -> ExitCode {
reset_sigpipe();
let is_version = std::env::args_os().any(|a| {
a.to_str()
.map(|s| s == "-V" || s == "--version")
.unwrap_or(false)
});
if is_version {
print_version_info();
return ExitCode::SUCCESS;
}
tokio::spawn(async move {
if let Ok(()) = tokio::signal::ctrl_c().await {
let scanned = SCANNED_CHUNKS.load(std::sync::atomic::Ordering::SeqCst);
let total = TOTAL_CHUNKS.load(std::sync::atomic::Ordering::SeqCst);
let findings = FINDINGS_COUNT.load(std::sync::atomic::Ordering::SeqCst);
eprintln!("\nScan interrupted. {scanned}/{total} files scanned. {findings} findings.");
std::process::exit(130);
}
});
let log_ansi = {
use std::io::IsTerminal;
std::io::stderr().is_terminal() && std::env::var_os("NO_COLOR").is_none()
};
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_ansi(log_ansi)
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env().add_directive(
"keyhog=warn".parse().unwrap_or_else(|_| {
tracing_subscriber::filter::Directive::from(tracing::Level::INFO)
}),
),
)
.with_target(false)
.init();
let cli = keyhog::args::parse();
if cli.version {
print_version_info();
return ExitCode::SUCCESS;
}
let command_outcome = match cli.command {
Some(Command::Scan(args)) => subcommands::scan::run(*args).await,
Some(Command::Hook { command }) => subcommands::hook::run(command),
Some(Command::Detectors(args)) => subcommands::detectors::run(args),
Some(Command::Explain(args)) => subcommands::explain::run(args).map(|()| ExitCode::SUCCESS),
Some(Command::Diff(args)) => subcommands::diff::run(args),
Some(Command::Calibrate(args)) => {
subcommands::calibrate::run(args).map(|()| ExitCode::SUCCESS)
}
Some(Command::Watch(args)) => subcommands::watch::run(args).map(|()| ExitCode::SUCCESS),
Some(Command::Completion(args)) => {
subcommands::completion::run(args);
return ExitCode::SUCCESS;
}
Some(Command::Backend(args)) => subcommands::backend::run(args),
Some(Command::Doctor(args)) => subcommands::doctor::run(args),
Some(Command::Update(args)) => subcommands::update::run(args).await,
Some(Command::Repair(args)) => subcommands::repair::run(args).await,
Some(Command::Uninstall(args)) => subcommands::uninstall::run(args),
Some(Command::ScanSystem(args)) => subcommands::scan_system::run(args),
#[cfg(unix)]
Some(Command::Daemon(args)) => subcommands::daemon::run(args).await,
#[cfg(not(unix))]
Some(Command::Daemon(_args)) => Err(anyhow::anyhow!(
"`keyhog daemon` is a unix-only command (it serves scans over a \
Unix-domain socket). On Windows, run scans in-process: \
`keyhog scan <path>`. Daemon-mode Windows support (via named \
pipes) is tracked but not yet implemented."
)),
#[cfg(feature = "tui")]
Some(Command::Tui(args)) => subcommands::tui::run(args),
None => {
let mut cmd = keyhog::args::command();
let _ = cmd.print_help();
return ExitCode::from(0);
}
};
match command_outcome {
Ok(outcome) => {
if SCANNER_PANICKED.load(Ordering::Relaxed) {
ExitCode::from(EXIT_SCANNER_PANIC)
} else {
outcome
}
}
Err(error) => {
eprintln!("error: {error:#}");
let code = if keyhog::SCANNER_PANICKED.load(std::sync::atomic::Ordering::SeqCst) {
EXIT_SCANNER_PANIC
} else if error.chain().any(is_user_io_error) {
EXIT_USER_ERROR
} else if error.chain().any(|e| e.is::<std::io::Error>()) {
EXIT_SYSTEM_ERROR
} else {
EXIT_USER_ERROR
};
ExitCode::from(code)
}
}
}
fn is_user_io_error(error: &(dyn std::error::Error + 'static)) -> bool {
let Some(io) = error.downcast_ref::<std::io::Error>() else {
return false;
};
matches!(
io.kind(),
std::io::ErrorKind::NotFound
| std::io::ErrorKind::PermissionDenied
| std::io::ErrorKind::ConnectionRefused
| std::io::ErrorKind::InvalidInput
| std::io::ErrorKind::InvalidData
| std::io::ErrorKind::AlreadyExists
)
}
fn print_version_info() {
println!("KeyHog v{}", env!("CARGO_PKG_VERSION"));
println!(
"Build Target: {}-{}",
std::env::consts::ARCH,
std::env::consts::OS
);
println!(
"ML Model Version: {}",
keyhog_scanner::ml_scorer::model_version()
);
if std::env::var_os("KEYHOG_VERSION_FULL").is_none() {
return;
}
let hw = keyhog_scanner::hw_probe::probe_hardware();
if hw.gpu_available {
println!(
"GPU Acceleration: {}{}",
hw.gpu_name.as_deref().unwrap_or("available"),
hw.gpu_vram_mb
.map(|mb| {
if mb >= 1024 {
format!(" (max buffer {} GB)", mb / 1024)
} else {
format!(" (max buffer {mb} MB)")
}
})
.unwrap_or_default()
);
} else {
println!("GPU Acceleration: not detected");
}
if hw.hyperscan_available {
println!("SIMD Regex: vectorscan/hyperscan (active)");
} else if hw.has_avx512 || hw.has_avx2 || hw.has_neon {
let simd = if hw.has_avx512 {
"AVX-512"
} else if hw.has_avx2 {
"AVX2"
} else {
"NEON"
};
println!("SIMD Regex: {simd} (no Hyperscan)");
} else {
println!("SIMD Regex: not available");
}
if hw.io_uring_available {
println!("io_uring: available");
}
}
pub fn print_banner(detector_count: usize) {
if !std::io::stderr().is_terminal() {
return;
}
let mut stderr = std::io::stderr();
let _ = keyhog_core::banner::print_banner(&mut stderr, true, true, detector_count);
eprintln!();
}