use keyhog_scanner::hw_probe::HardwareCaps;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct AutorouteHostProfile {
pub(super) os: String,
pub(super) arch: String,
pub(super) cpu_model: Option<String>,
pub(super) physical_cores: usize,
pub(super) logical_cores: usize,
pub(super) has_avx2: bool,
pub(super) has_avx512: bool,
pub(super) has_neon: bool,
pub(super) hyperscan_available: bool,
pub(super) hyperscan_runtime_identity: Option<String>,
pub(super) gpu_name: Option<String>,
pub(super) gpu_runtime_backend: Option<String>,
pub(super) gpu_driver_runtime_identity: Option<String>,
pub(super) gpu_batch_input_limit_bytes: Option<u64>,
pub(super) gpu_is_software: bool,
pub(super) total_memory_mb: Option<u64>,
pub(super) eligible_backends: Vec<String>,
}
impl AutorouteHostProfile {
pub(super) fn with_live_hyperscan(mut self, available: bool) -> Self {
self.hyperscan_available = available;
self.hyperscan_runtime_identity = available
.then(keyhog_scanner::hw_probe::hyperscan_runtime_identity)
.flatten();
self
}
pub(super) fn from_caps(
caps: &HardwareCaps,
gpu_peer_identity: Option<&str>,
gpu_participates: bool,
eligible_backends: Vec<String>,
) -> Self {
let acquired_peer_present = gpu_peer_identity.is_some();
let gpu_device_identity =
(gpu_participates && (caps.gpu_available || acquired_peer_present)).then(|| {
if caps.gpu_is_software && acquired_peer_present {
let Some(identity) = gpu_peer_identity else {
return String::new();
};
identity.to_string()
} else {
caps.gpu_name
.clone()
.or_else(|| gpu_peer_identity.map(str::to_string))
.unwrap_or_default() }
});
Self {
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
cpu_model: detect_cpu_model(),
physical_cores: caps.physical_cores,
logical_cores: caps.logical_cores,
has_avx2: caps.has_avx2,
has_avx512: caps.has_avx512,
has_neon: caps.has_neon,
hyperscan_available: caps.hyperscan_available,
hyperscan_runtime_identity: if caps.hyperscan_available {
caps.hyperscan_runtime_identity
.clone()
.or_else(keyhog_scanner::hw_probe::hyperscan_runtime_identity)
} else {
None
},
gpu_name: gpu_device_identity,
gpu_runtime_backend: (gpu_participates && acquired_peer_present)
.then(|| gpu_peer_identity.map(str::to_string))
.flatten(),
gpu_driver_runtime_identity: (gpu_participates && acquired_peer_present)
.then(|| gpu_peer_identity.map(str::to_string))
.flatten(),
gpu_batch_input_limit_bytes: (gpu_participates && acquired_peer_present)
.then(|| keyhog_scanner::gpu_batch_input_limit() as u64),
gpu_is_software: gpu_participates && caps.gpu_is_software && !acquired_peer_present,
total_memory_mb: caps.total_memory_mb,
eligible_backends,
}
}
pub(super) fn require_exact_identity(&self) -> Result<(), &'static str> {
if !field_is_present_nonblank(&self.cpu_model) {
return Err("CPU model string is unavailable");
}
if self.physical_cores == 0
|| self.logical_cores == 0
|| self.logical_cores < self.physical_cores
{
return Err("CPU core topology is unavailable");
}
match self.total_memory_mb {
Some(memory_mb) if memory_mb > 0 => {}
_ => return Err("system memory size is unavailable"),
}
self.require_exact_candidate_census()?;
if self.hyperscan_available {
if !field_is_present_nonblank(&self.hyperscan_runtime_identity) {
return Err("Hyperscan runtime identity is unavailable");
}
} else if self.hyperscan_runtime_identity.is_some() {
return Err("Hyperscan runtime identity is present while the backend is unavailable");
}
let gpu_name_present = field_is_present_nonblank(&self.gpu_name);
if self.gpu_name.is_some() && !gpu_name_present {
return Err("GPU device identity is unavailable");
}
let hardware_gpu_present = gpu_name_present && !self.gpu_is_software;
let gpu_runtime_backend_present = field_is_present_nonblank(&self.gpu_runtime_backend);
if self.gpu_runtime_backend.is_some() && !gpu_runtime_backend_present {
return Err("GPU runtime backend identity is unavailable");
}
if gpu_runtime_backend_present && self.gpu_name.is_none() {
return Err("GPU runtime backend is present without GPU device identity");
}
if hardware_gpu_present && !gpu_runtime_backend_present {
return Err("GPU runtime backend identity is unavailable");
}
if (hardware_gpu_present || gpu_runtime_backend_present)
&& !field_is_present_nonblank(&self.gpu_driver_runtime_identity)
{
return Err("GPU driver/runtime identity is unavailable");
}
let gpu_candidate_present = self.eligible_backends.iter().any(|label| {
keyhog_scanner::hw_probe::parse_backend_str(label)
.is_some_and(|backend| backend.is_gpu())
});
if hardware_gpu_present != gpu_candidate_present {
return Err("GPU identity and eligible backend census disagree");
}
match (gpu_candidate_present, self.gpu_batch_input_limit_bytes) {
(true, Some(limit)) if limit > 0 => {}
(true, _) => return Err("GPU batch input limit identity is unavailable"),
(false, None) => {}
(false, Some(_)) => {
return Err("GPU batch input limit is present without an eligible GPU backend")
}
}
Ok(())
}
pub(super) fn candidate_backend_set(&self) -> Result<BTreeSet<String>, &'static str> {
self.require_exact_candidate_census()?;
Ok(self.eligible_backends.iter().cloned().collect())
}
fn require_exact_candidate_census(&self) -> Result<(), &'static str> {
if self.eligible_backends.is_empty() {
return Err("eligible backend census is unavailable");
}
if !self
.eligible_backends
.windows(2)
.all(|pair| pair[0] < pair[1])
{
return Err("eligible backend census is not unique canonical order");
}
let mut has_simd = false;
let mut has_scalar = false;
for label in &self.eligible_backends {
let Some(backend) = keyhog_scanner::hw_probe::parse_backend_str(label) else {
return Err("eligible backend census contains an unsupported backend");
};
if backend.label() != label {
return Err("eligible backend census contains a non-canonical backend label");
}
has_simd |= backend == keyhog_scanner::ScanBackend::SimdCpu;
has_scalar |= backend == keyhog_scanner::ScanBackend::CpuFallback;
}
if !has_scalar {
return Err("eligible backend census omits the required scalar CPU peer");
}
if has_simd != self.hyperscan_available {
return Err("eligible backend census disagrees with live Hyperscan availability");
}
Ok(())
}
}
pub(super) fn render_host_profile(host: &AutorouteHostProfile) -> String {
let simd = if host.has_avx512 {
"AVX-512"
} else if host.has_avx2 {
"AVX2"
} else if host.has_neon {
"NEON"
} else {
"scalar"
};
format!(
"{}/{} {} | {}p/{}l cores | {} | hyperscan={} | hyperscan_runtime={} | gpu={} | gpu_peers={} | gpu_driver={} | gpu_batch_input_limit={} | candidates={}",
host.os,
host.arch,
host.cpu_model.as_deref().unwrap_or("unknown-cpu"), host.physical_cores,
host.logical_cores,
simd,
if host.hyperscan_available {
"yes"
} else {
"no"
},
host.hyperscan_runtime_identity.as_deref().unwrap_or("none"),
host.gpu_name.as_deref().unwrap_or("none"), host.gpu_runtime_backend.as_deref().unwrap_or("none"), host.gpu_driver_runtime_identity
.as_deref()
.unwrap_or("none"), host.gpu_batch_input_limit_bytes
.map_or_else(|| "none".to_string(), |bytes| bytes.to_string()),
host.eligible_backends.join(","),
)
}
pub(super) fn host_identity_digest(host: &AutorouteHostProfile) -> String {
let mut hasher = crate::stable_hash::StableHasher::new("autoroute-host-identity-v1");
hasher
.field_str("os", &host.os)
.field_str("arch", &host.arch)
.field_option_str("cpu_model", host.cpu_model.as_deref())
.field_usize("physical_cores", host.physical_cores)
.field_usize("logical_cores", host.logical_cores)
.field_bool("has_avx2", host.has_avx2)
.field_bool("has_avx512", host.has_avx512)
.field_bool("has_neon", host.has_neon)
.field_bool("hyperscan_available", host.hyperscan_available)
.field_option_str(
"hyperscan_runtime_identity",
host.hyperscan_runtime_identity.as_deref(),
)
.field_option_str("gpu_name", host.gpu_name.as_deref())
.field_option_str("gpu_runtime_backend", host.gpu_runtime_backend.as_deref())
.field_option_str(
"gpu_driver_runtime_identity",
host.gpu_driver_runtime_identity.as_deref(),
)
.field_option_u64(
"gpu_batch_input_limit_bytes",
host.gpu_batch_input_limit_bytes,
)
.field_bool("gpu_is_software", host.gpu_is_software)
.field_option_u64("total_memory_mb", host.total_memory_mb)
.field_usize("eligible_backend_count", host.eligible_backends.len());
for backend in &host.eligible_backends {
hasher.field_str("eligible_backend", backend);
}
blake3::Hash::from_bytes(hasher.finish_256())
.to_hex()
.to_string()
}
fn field_is_present_nonblank(field: &Option<String>) -> bool {
field
.as_deref()
.map(str::trim)
.is_some_and(|value| !value.is_empty())
}
fn detect_cpu_model() -> Option<String> {
#[cfg(target_os = "linux")]
{
if let Some(model) = linux_cpu_model() {
return Some(model);
}
}
#[cfg(target_os = "macos")]
{
let sysctl = keyhog_core::resolve_safe_bin("sysctl")?;
if let Some(model) =
command_first_nonempty_stdout_line(&sysctl, &["-n", "machdep.cpu.brand_string"])
{
return Some(model);
}
}
#[cfg(target_os = "windows")]
{
if let Some(model) = windows_cpu_model() {
return Some(model);
}
}
None
}
#[cfg(target_os = "linux")]
fn linux_cpu_model() -> Option<String> {
let content = std::fs::read_to_string("/proc/cpuinfo").ok()?; parse_cpuinfo_model(&content)
}
#[cfg(target_os = "linux")]
pub(super) fn parse_cpuinfo_model(content: &str) -> Option<String> {
for line in content.lines() {
let Some((key, value)) = line.split_once(':') else {
continue;
};
let key = key.trim().to_ascii_lowercase();
if matches!(key.as_str(), "model name" | "hardware" | "processor") {
let value = value.trim();
if key == "processor" && value.parse::<usize>().is_ok() {
continue;
}
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
#[cfg(target_os = "windows")]
fn windows_cpu_model() -> Option<String> {
keyhog_core::resolve_safe_bin("powershell")
.and_then(|ps| {
command_first_nonempty_stdout_line(
&ps,
&[
"-NoProfile",
"-Command",
"(Get-CimInstance Win32_Processor | Select-Object -First 1 -ExpandProperty Name)",
],
)
})
.or_else(|| {
let wmic = keyhog_core::resolve_safe_bin("wmic")?;
command_first_nonempty_stdout_line(&wmic, &["cpu", "get", "Name", "/value"]).and_then(
|line| {
line.strip_prefix("Name=")
.map(str::trim)
.map(str::to_string)
},
)
})
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
fn command_first_nonempty_stdout_line(bin: &std::path::Path, args: &[&str]) -> Option<String> {
let output = std::process::Command::new(bin).args(args).output().ok()?; String::from_utf8_lossy(&output.stdout)
.lines()
.map(str::trim)
.find(|line| !line.is_empty())
.map(str::to_string)
}