From b70c808d603ea167c5982e143f9a7b1d982f846c Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 5 Oct 2026 08:23:34 -0400 Subject: [PATCH] FreeBSD: system info reports memory, CPU model and OS version sysctl hw.physmem / hw.pagesize / vm free+inactive page counts, hw.model, and freebsd-version, with a unit test for the sysctl parser. The FreeBSD CI workflow itself landed with #69. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/sysinfo.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/engine/src/sysinfo.rs b/crates/engine/src/sysinfo.rs index 8e26e70a..0debec95 100644 --- a/crates/engine/src/sysinfo.rs +++ b/crates/engine/src/sysinfo.rs @@ -101,6 +101,16 @@ pub fn parse_vm_stat(text: &str, total: u64) -> Option { Some(SysMemory { total, available: (free * page).min(total) }) } +/// Parse `sysctl -n hw.physmem hw.pagesize vm.stats.vm.v_free_count vm.stats.vm.v_inactive_count` +/// (FreeBSD: one number per line, in that order). +pub fn parse_freebsd_sysctl(text: &str) -> Option { + let mut it = text.split_whitespace().map(|v| v.parse::().ok()); + let total = it.next()??; + let page = it.next()??; + let free = it.next()??.saturating_add(it.next()??); + Some(SysMemory { total, available: free.saturating_mul(page).min(total) }) +} + /// Total and available physical memory, when the system reports them. pub fn memory() -> Option { #[cfg(target_os = "linux")] @@ -112,6 +122,10 @@ pub fn memory() -> Option { let total: u64 = run("sysctl", &["-n", "hw.memsize"])?.trim().parse().ok()?; return parse_vm_stat(&run("vm_stat", &[])?, total).or(Some(SysMemory { total, available: total })); } + #[cfg(target_os = "freebsd")] + { + return parse_freebsd_sysctl(&run("sysctl", &["-n", "hw.physmem", "hw.pagesize", "vm.stats.vm.v_free_count", "vm.stats.vm.v_inactive_count"])?); + } #[cfg(target_os = "windows")] { let text = run( @@ -136,6 +150,10 @@ pub fn cpu_name() -> Option { { return run("sysctl", &["-n", "machdep.cpu.brand_string"]).map(|s| s.trim().to_string()).filter(|s| !s.is_empty()); } + #[cfg(target_os = "freebsd")] + { + return run("sysctl", &["-n", "hw.model"]).map(|s| s.trim().to_string()).filter(|s| !s.is_empty()); + } #[cfg(target_os = "windows")] { return std::env::var("PROCESSOR_IDENTIFIER").ok(); @@ -156,6 +174,10 @@ pub fn os_version() -> String { { return n.trim_matches('"').to_string(); } + #[cfg(target_os = "freebsd")] + if let Some(v) = run("freebsd-version", &[]) { + return format!("FreeBSD {}", v.trim()); + } #[cfg(target_os = "windows")] if let Some(v) = run("cmd", &["/C", "ver"]) { return v.trim().to_string(); @@ -172,6 +194,18 @@ pub fn cpu_cores() -> usize { mod tests { use super::*; + #[test] + fn freebsd_sysctl_memory_parses() { + let m = parse_freebsd_sysctl("17179869184\n4096\n1000000\n500000\n").expect("parses"); + assert_eq!(m.total, 17_179_869_184); + assert_eq!(m.available, 1_500_000 * 4096); + assert!(parse_freebsd_sysctl("17179869184\n4096\n").is_none()); + assert!(parse_freebsd_sysctl("garbage").is_none()); + // Available never exceeds total, even with nonsense counts. + let m = parse_freebsd_sysctl("1000\n4096\n999999999\n999999999").expect("parses"); + assert_eq!(m.available, 1000); + } + #[test] fn parses_proc_meminfo_and_vm_stat() { let m = parse_meminfo("MemTotal: 16000000 kB\nMemFree: 1000 kB\nMemAvailable: 8000000 kB\n").unwrap();