use crate::args::DaemonArgs;
use crate::daemon::client;
use crate::daemon::protocol::{response_kind, Request, Response};
use crate::daemon::server::{self, default_socket_path};
use crate::style;
use anyhow::{Context, Result};
use std::path::PathBuf;
use std::process::ExitCode;
use std::time::Duration;
pub(crate) async fn run(args: DaemonArgs) -> Result<ExitCode> {
match args.action {
crate::args::DaemonAction::Start {
socket,
detectors,
detectors_cli_explicit,
cache_dir,
backend,
request_timeout_secs,
} => {
start(
socket,
detectors,
detectors_cli_explicit,
cache_dir,
backend,
request_timeout_secs,
)
.await
}
crate::args::DaemonAction::Stop { socket } => stop(socket).await,
crate::args::DaemonAction::Status { socket } => status(socket).await,
}
}
async fn start(
socket: Option<PathBuf>,
detectors_dir: PathBuf,
detectors_cli_explicit: bool,
cache_dir: Option<PathBuf>,
backend: Option<String>,
request_timeout_secs: u64,
) -> Result<ExitCode> {
crate::runtime_preflight::validate_scan_runtime_config()?;
crate::orchestrator_config::validate_explicit_detector_path(
&detectors_dir,
detectors_cli_explicit,
)?;
let detectors_dir = crate::orchestrator_config::auto_discover_detectors(&detectors_dir)?;
crate::orchestrator_config::configure_hyperscan_cache_dir(cache_dir)?;
let backend_override = crate::orchestrator_config::parse_backend_override(backend.as_deref())?;
let gpu_policy =
crate::orchestrator_config::gpu_runtime_policy_for_backend_override(backend_override)?;
keyhog_scanner::gpu::set_gpu_runtime_policy(gpu_policy);
if gpu_policy == keyhog_scanner::gpu::GpuRuntimePolicy::Required {
keyhog_scanner::gpu::require_gpu_preflight()
.map_err(crate::orchestrator::daemon_gpu_preflight_failure)?;
}
let socket = socket.unwrap_or_else(default_socket_path); let detectors = crate::orchestrator_config::load_detectors_or_embedded(&detectors_dir)
.with_context(|| {
format!(
"daemon start: load detectors from {}",
detectors_dir.display()
)
})?;
let options = server::ServerOptions {
request_read_timeout: Duration::from_secs(request_timeout_secs),
};
server::run_with_backend_override(socket, detectors, options, backend_override).await?;
Ok(ExitCode::SUCCESS)
}
async fn stop(socket: Option<PathBuf>) -> Result<ExitCode> {
let socket = socket.unwrap_or_else(default_socket_path); let mut conn = client::connect_any_version(&socket)
.await
.with_context(|| {
format!(
"daemon stop: no daemon at {} (already stopped?)",
socket.display()
)
})?;
match conn.round_trip(&Request::Shutdown).await? {
Response::Shutdown => {
eprintln!("keyhog daemon stopped");
Ok(ExitCode::SUCCESS)
}
other => {
anyhow::bail!(
"daemon stop: protocol mismatch (got {}). Shutdown was not confirmed, and the \
incompatible daemon socket was left untouched. Stop the daemon with the matching \
KeyHog version or the service manager that started it before starting a replacement.",
response_kind(&other)
)
}
}
}
async fn status(socket: Option<PathBuf>) -> Result<ExitCode> {
let socket = socket.unwrap_or_else(default_socket_path); let mut conn = client::connect_any_version(&socket)
.await
.with_context(|| {
format!(
"daemon status: no daemon at {} (start one with `keyhog daemon start`)",
socket.display()
)
})?;
let stale = conn.is_stale();
let daemon_version = conn.daemon_version().to_string();
let backend_policy = conn.backend_policy().to_string();
let stale_reason = conn.stale_reason().map(str::to_string);
match conn.round_trip(&Request::Health).await? {
Response::Health {
uptime_secs,
scans_served,
active_scans,
detector_count,
backend_recoveries,
last_backend_fault,
} => {
println!(
"keyhog daemon: uptime {}s · {} scans served · {} active · {} detectors",
uptime_secs, scans_served, active_scans, detector_count
);
println!(
"scan scope: eligible stdin/single-file scans before baseline, Merkle \
skip-cache, and verification; directories, git/remote sources, policy \
changes, baseline, and --verify run in-process."
);
if backend_policy == "autoroute" {
println!("backend policy: autoroute (persisted warm-route evidence)");
} else if backend_policy == "autoroute-recovery" {
println!(
"backend policy: autoroute invalid (scalar correctness recovery; run `keyhog calibrate-autoroute`)"
);
} else if backend_policy == "autoroute-degraded" {
println!(
"backend policy: autoroute degraded (one or more workload routes quarantined; affected requests use scalar correctness recovery; run `keyhog calibrate-autoroute`)"
);
} else {
println!(
"backend policy: forced {backend_policy} (daemon startup diagnostic override)"
);
}
if let Some(fault) = last_backend_fault {
let state = if fault.failed_backend == "autoroute-invalid" {
"Autoroute remains invalid until recalibration."
} else {
"The affected route is quarantined until recalibration."
};
println!(
"backend health: {} recovered request(s); last fault {} recovered {} byte(s) through {}. {}",
backend_recoveries,
fault.failed_backend,
fault.recovered_bytes,
fault.recovery_backend,
state,
);
} else if backend_policy == "autoroute-recovery" {
println!(
"backend health: autoroute evidence invalid; requests will report complete scalar recovery"
);
} else if backend_policy == "autoroute-degraded" {
println!(
"backend health: persisted autoroute quarantine loaded; affected requests will report complete scalar recovery"
);
} else {
println!("backend health: no recovered runtime faults");
}
if stale {
let palette = style::for_stderr();
eprintln!(
"{} this daemon's build/corpus identity does not match the client \
(daemon keyhog {}, client {}; {}): scan connections refuse it; \
`--daemon=auto` runs in process and `--daemon=on` fails until you restart it: \
`keyhog daemon stop && keyhog daemon start`.",
style::warn("WARN", &palette),
daemon_version,
env!("CARGO_PKG_VERSION"),
stale_reason.as_deref().unwrap_or("identity mismatch"), );
}
Ok(ExitCode::SUCCESS)
}
other => anyhow::bail!(
"daemon status: protocol mismatch (got {}). Restart with \
`keyhog daemon stop && keyhog daemon start` to clear stuck state.",
response_kind(&other)
),
}
}