use crate::args::DoctorArgs;
use crate::installer::scan_engine_self_test;
use crate::style::Palette;
use anyhow::Result;
use keyhog_scanner::hw_probe::probe_hardware;
use std::process::ExitCode;
const EXIT_DOCTOR_UNHEALTHY: u8 = 4;
pub fn run(_args: DoctorArgs) -> Result<ExitCode> {
let mut healthy = true;
let mut warned = false;
let Palette {
green,
red,
yellow,
dim,
bold,
reset,
} = Palette::for_stdout();
println!("{bold}keyhog doctor{reset} v{}", env!("CARGO_PKG_VERSION"));
let hw = probe_hardware();
let simd = if hw.has_avx512 {
"AVX-512"
} else if hw.has_avx2 {
"AVX2"
} else if hw.has_neon {
"NEON"
} else {
"scalar"
};
println!("\n{bold}host{reset}");
println!(
" os/arch {} / {}",
std::env::consts::OS,
std::env::consts::ARCH
);
println!(
" cpu {} physical / {} logical cores",
hw.physical_cores, hw.logical_cores
);
println!(" simd {simd}");
let gpu = if !hw.gpu_available {
format!("{dim}not detected (CPU/SIMD path){reset}")
} else if hw.gpu_is_software {
format!("{yellow}software renderer (disabled for scans){reset}")
} else {
format!("{green}{}{reset}", hw.gpu_name.as_deref().unwrap_or("yes"))
};
println!(" gpu {gpu}");
println!(
" hyperscan {}",
if hw.hyperscan_available {
format!("{green}compiled-in{reset}")
} else {
format!("{dim}absent (regex fallback){reset}")
}
);
println!("\n{bold}install{reset}");
match std::env::current_exe() {
Ok(exe) => {
println!(" binary {}", exe.display());
if let Some(dir) = exe.parent() {
let on_path = std::env::var_os("PATH")
.map(|p| std::env::split_paths(&p).any(|d| d == dir))
.unwrap_or(false);
if on_path {
println!(" on PATH {green}yes{reset}");
} else {
warned = true;
println!(
" on PATH {yellow}no{reset} {dim}add: export PATH=\"{}:$PATH\"{reset}",
dir.display()
);
}
}
}
Err(e) => {
warned = true;
println!(" binary {yellow}unknown ({e}){reset}");
}
}
println!(" version v{}", env!("CARGO_PKG_VERSION"));
let exe_name = if cfg!(windows) {
"keyhog.exe"
} else {
"keyhog"
};
let mut on_path: Vec<std::path::PathBuf> = Vec::new();
if let Some(pathvar) = std::env::var_os("PATH") {
for dir in std::env::split_paths(&pathvar) {
let cand = dir.join(exe_name);
if cand.is_file() {
let canon = std::fs::canonicalize(&cand).unwrap_or(cand);
if !on_path.contains(&canon) {
on_path.push(canon);
}
}
}
}
let running = std::env::current_exe()
.ok()
.and_then(|p| std::fs::canonicalize(&p).ok());
match on_path.len() {
0 => println!(
" resolves {dim}not on PATH (invoke by full path or add its dir){reset}"
),
1 => println!(" resolves {green}one keyhog on PATH{reset}"),
n => {
warned = true;
println!(
" resolves {yellow}{n} keyhog binaries on PATH - possible shadowing:{reset}"
);
for p in &on_path {
println!(" {dim}{}{reset}", p.display());
}
}
}
if let (Some(run), Some(first)) = (&running, on_path.first()) {
if run != first {
warned = true;
println!(
" {yellow}shadowed{reset} PATH resolves keyhog to {} but THIS binary is {}.\n {dim}An older install is ahead on PATH; remove it or fix PATH order.{reset}",
first.display(),
run.display()
);
}
}
println!("\n{bold}detectors{reset}");
let embedded = keyhog_core::embedded_detector_count();
if embedded > 0 {
println!(" embedded {green}{embedded}{reset} service detectors");
} else {
healthy = false;
println!(" embedded {red}0 - corpus missing from binary{reset}");
}
println!("\n{bold}self-test{reset}");
match scan_engine_self_test() {
Ok(true) => println!(
" scan engine {green}PASS{reset} {dim}planted secret detected end-to-end{reset}"
),
Ok(false) => {
healthy = false;
println!(" scan engine {red}FAIL{reset} planted secret was NOT detected");
}
Err(e) => {
healthy = false;
println!(" scan engine {red}FAIL{reset} {e}");
}
}
if hw.gpu_available && !hw.gpu_is_software {
match keyhog_scanner::gpu::vyre_ac_kernel_self_test() {
Ok(report) => println!(
" gpu scan path {green}PASS{reset} {dim}AC kernel matches={}, backend={}{reset}",
report.matches, report.backend_id
),
Err(e) => {
warned = true;
println!(
" gpu scan path {yellow}DEGRADED{reset} GPU AC kernel self-test failed; scans fall back to SIMD/CPU (recall preserved, large-file GPU acceleration lost).\n {dim}{e}{reset}\n {dim}run `keyhog backend --self-test` for the full GPU diagnostic{reset}"
);
}
}
}
println!();
if healthy && !warned {
println!("{green}{bold}✓ keyhog is healthy.{reset}");
Ok(ExitCode::SUCCESS)
} else if healthy {
println!("{yellow}{bold}keyhog works, with warnings above.{reset}");
Ok(ExitCode::SUCCESS)
} else {
eprintln!("{red}{bold}✗ keyhog is unhealthy - see failures above.{reset}");
Ok(ExitCode::from(EXIT_DOCTOR_UNHEALTHY))
}
}