hatchery_node/
machine_env.rs1use hatchery_core::{MachineEnvironment, ProviderCliReport};
4use std::fs;
5use std::process::Command;
6
7const PROVIDERS: &[&str] = &["claude", "codex", "grok", "kimi", "kimi-cli", "bash"];
8
9pub fn collect() -> MachineEnvironment {
11 let (os, kernel, arch) = uname();
12 let hostname = fs::read_to_string("/etc/hostname")
13 .or_else(|_| fs::read_to_string("/proc/sys/kernel/hostname"))
14 .map(|s| s.trim().to_owned())
15 .unwrap_or_else(|_| "unknown".into());
16 let uptime_secs = fs::read_to_string("/proc/uptime")
17 .ok()
18 .and_then(|s| s.split_whitespace().next()?.parse::<f64>().ok())
19 .map(|s| s as u64)
20 .unwrap_or(0);
21 let cpu_cores = std::thread::available_parallelism()
22 .map(|n| n.get() as u32)
23 .unwrap_or(1);
24 let cpu_model = cpu_model();
25 let (ram_total_bytes, ram_available_bytes) = meminfo();
26 let (disk_total_bytes, disk_available_bytes) = disk_root();
27 let provider_clis = PROVIDERS
28 .iter()
29 .filter_map(|id| probe_cli(id))
30 .collect();
31 MachineEnvironment {
32 os,
33 kernel,
34 arch,
35 hostname,
36 uptime_secs,
37 cpu_cores,
38 cpu_model,
39 ram_total_bytes,
40 ram_available_bytes,
41 disk_total_bytes,
42 disk_available_bytes,
43 provider_clis,
44 }
45}
46
47fn uname() -> (String, String, String) {
48 let out = Command::new("uname").args(["-srm"]).output().ok();
49 let text = out
50 .and_then(|o| String::from_utf8(o.stdout).ok())
51 .unwrap_or_default();
52 let mut parts = text.split_whitespace();
53 let os = parts.next().unwrap_or(std::env::consts::OS).to_owned();
54 let kernel = parts.next().unwrap_or("unknown").to_owned();
55 let arch = parts
56 .next()
57 .unwrap_or(std::env::consts::ARCH)
58 .to_owned();
59 (os, kernel, arch)
60}
61
62fn cpu_model() -> Option<String> {
63 let text = fs::read_to_string("/proc/cpuinfo").ok()?;
64 for line in text.lines() {
65 if let Some(rest) = line.strip_prefix("model name") {
66 return Some(rest.trim_start_matches(|c: char| c == ':' || c.is_whitespace()).to_owned());
67 }
68 }
69 None
70}
71
72fn meminfo() -> (u64, u64) {
73 let text = match fs::read_to_string("/proc/meminfo") {
74 Ok(t) => t,
75 Err(_) => return (0, 0),
76 };
77 let mut total_kb = 0u64;
78 let mut avail_kb = 0u64;
79 for line in text.lines() {
80 if let Some(v) = line.strip_prefix("MemTotal:") {
81 total_kb = parse_kb(v);
82 } else if let Some(v) = line.strip_prefix("MemAvailable:") {
83 avail_kb = parse_kb(v);
84 }
85 }
86 (total_kb.saturating_mul(1024), avail_kb.saturating_mul(1024))
87}
88
89fn parse_kb(v: &str) -> u64 {
90 v.split_whitespace()
91 .next()
92 .and_then(|n| n.parse().ok())
93 .unwrap_or(0)
94}
95
96fn disk_root() -> (u64, u64) {
97 let out = Command::new("df").args(["-B1", "/"]).output().ok();
99 let text = out.and_then(|o| String::from_utf8(o.stdout).ok()).unwrap_or_default();
100 for line in text.lines().skip(1) {
101 let cols: Vec<_> = line.split_whitespace().collect();
102 if cols.len() >= 4 {
103 let total = cols[1].parse().unwrap_or(0);
104 let avail = cols[3].parse().unwrap_or(0);
105 return (total, avail);
106 }
107 }
108 (0, 0)
109}
110
111fn probe_cli(id: &str) -> Option<ProviderCliReport> {
112 let path = Command::new("sh")
113 .args(["-c", &format!("command -v {id}")])
114 .output()
115 .ok()
116 .and_then(|o| {
117 if o.status.success() {
118 String::from_utf8(o.stdout).ok().map(|s| s.trim().to_owned())
119 } else {
120 None
121 }
122 })
123 .filter(|s| !s.is_empty())?;
124 let version = Command::new(&path)
125 .arg("--version")
126 .output()
127 .ok()
128 .and_then(|o| {
129 let mut text = String::from_utf8_lossy(&o.stdout).to_string();
130 if text.trim().is_empty() {
131 text = String::from_utf8_lossy(&o.stderr).to_string();
132 }
133 let line = text.lines().next()?.trim().to_owned();
134 if line.is_empty() { None } else { Some(line) }
135 });
136 Some(ProviderCliReport {
137 id: id.to_owned(),
138 version,
139 path: Some(path),
140 })
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn collect_returns_nonzero_cores_on_linux() {
149 let env = collect();
150 assert!(env.cpu_cores >= 1);
151 assert!(!env.os.is_empty());
152 assert!(!env.arch.is_empty());
153 }
154}