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,
mass,
mass_gpu_primary,
} => {
start(
socket,
detectors,
detectors_cli_explicit,
cache_dir,
backend,
request_timeout_secs,
mass,
mass_gpu_primary,
)
.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,
mass: bool,
mass_gpu_primary: bool,
) -> 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),
mass_service: mass,
mass_gpu_primary_required: mass_gpu_primary,
};
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 mut stale = conn.is_stale();
let daemon_version = conn.daemon_version().to_string();
let backend_policy = conn.backend_policy().to_string();
let mass_service = conn.is_mass_service();
let mut stale_reason = conn.stale_reason().map(str::to_string);
let hello_warm_backend = conn
.warm_backend_status()
.context("daemon status: Hello omitted required warm-backend status")?
.clone();
match conn.round_trip(&Request::Health).await? {
Response::Health {
uptime_secs,
scans_served,
active_scans,
detector_count,
backend_recoveries,
last_backend_fault,
warm_backend,
} => {
if warm_backend.daemon_generation != hello_warm_backend.daemon_generation {
anyhow::bail!(
"daemon status: daemon generation changed between Hello ({}) and Health ({}). Retry status; if it recurs, restart with `keyhog daemon stop && keyhog daemon start`.",
hello_warm_backend.daemon_generation,
warm_backend.daemon_generation
);
}
if warm_backend.identity != hello_warm_backend.identity
|| warm_backend.required_backends != hello_warm_backend.required_backends
{
anyhow::bail!(
"daemon status: warm-route identity or required backend set changed within daemon generation {}; restart with `keyhog daemon stop && keyhog daemon start`",
warm_backend.daemon_generation
);
}
let exact_mismatches = client::current_warm_backend_mismatches(&warm_backend)?;
if !exact_mismatches.is_empty() {
stale = true;
let exact_reason = exact_mismatches.join("; ");
stale_reason = Some(match stale_reason.take() {
Some(control_reason) if control_reason != exact_reason => {
format!("{control_reason}; {exact_reason}")
}
Some(control_reason) => control_reason,
None => exact_reason,
});
}
if warm_backend.ready {
println!(
"warm backend: ready · generation {} · engine {} · binary {} · detectors {} · config {} · GPU artifact {}",
warm_backend.daemon_generation,
warm_backend.identity.engine,
warm_backend.identity.binary_sha256,
warm_backend.identity.detector_rules_digest,
warm_backend.identity.config_digest,
match warm_backend.identity.gpu_artifact.as_deref() {
Some(artifact) => artifact,
None => "none",
}
);
} else {
let (reason, repair) = match (
warm_backend.reason.as_deref(),
warm_backend.repair_command.as_deref(),
) {
(Some(reason), Some(repair)) => (reason, repair),
_ => anyhow::bail!(
"daemon status: warm-backend status is internally inconsistent; restart with `keyhog daemon stop && keyhog daemon start`"
),
};
println!(
"warm backend: not ready · generation {} · {reason} · repair `{repair}`",
warm_backend.daemon_generation
);
}
println!(
"keyhog daemon: uptime {}s · {} scans served · {} active · {} detectors",
uptime_secs, scans_served, active_scans, detector_count
);
if mass_service {
println!(
"scan scope: bounded directory, Git, archive, binary, remote, and cloud \
batches via --daemon=mass; warm stdin/single-file requests remain available. \
Baseline, Merkle state, verification, lockdown, and per-request scanner policy \
remain in-process."
);
} else {
println!(
"scan scope: warm stdin/single-file requests only; start with --mass for \
bounded client-acquired source batches."
);
}
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();
let reason = match stale_reason.as_deref() {
Some(reason) => reason,
None => anyhow::bail!(
"daemon status: client marked the daemon stale without an exact readiness or identity mismatch"
),
};
eprintln!(
"{} this daemon's warm-route readiness/identity is not compatible with 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"),
reason,
);
}
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)
),
}
}