This commit is contained in:
7
scanner/Cargo.lock
generated
Normal file
7
scanner/Cargo.lock
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "pc-intake"
|
||||
version = "1.3.0"
|
||||
11
scanner/Cargo.toml
Normal file
11
scanner/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "pc-intake"
|
||||
version = "1.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
628
scanner/src/main.rs
Normal file
628
scanner/src/main.rs
Normal file
@@ -0,0 +1,628 @@
|
||||
//! Offline Linux intake. No crates, shell commands, network, or write benchmarks.
|
||||
use std::{env, fs, hint::black_box, path::{Path, PathBuf}, process::{self, Command}, thread, time::{Instant, SystemTime, UNIX_EPOCH}};
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const WORK: u64 = 4_000_000;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Disk { name: String, gib: u64, rotational: Option<bool>, removable: bool, model: String, health: String, smart_details: String }
|
||||
#[derive(Debug)]
|
||||
struct Battery { name: String, capacity_pct: Option<u64>, health_pct: Option<u64>, status: String }
|
||||
#[derive(Debug)]
|
||||
struct Bench { single_ms: f64, multi_ms: f64, threads: usize, single_runs_ms: Vec<f64>, parallel_runs_ms: Vec<f64> }
|
||||
#[derive(Debug)]
|
||||
struct CompileBench { compiler: String, median_ms: f64, runs_ms: Vec<f64>, source_functions: usize, status: String }
|
||||
#[derive(Debug)]
|
||||
struct DevBench { median_ms: f64, runs_ms: Vec<f64>, records: usize, checksum: u64 }
|
||||
#[derive(Debug)]
|
||||
struct MemoryBench { median_mib_s: f64, runs_mib_s: Vec<f64>, buffer_mib: usize, checksum: u64 }
|
||||
#[derive(Debug)]
|
||||
struct VideoBench { status: String, source: String, codec: String, width: u32, height: u32, frames: u64, seconds: f64, fps: f64, realtime_factor: f64, note: String }
|
||||
#[derive(Debug)]
|
||||
struct GraphicsBench { status: String, score: Option<u64>, note: String }
|
||||
#[derive(Debug)]
|
||||
struct PlaybackBench { status: String, elapsed_seconds: f64, note: String }
|
||||
#[derive(Debug)]
|
||||
struct Role { name: &'static str, verdict: &'static str, score: Option<u8>, reason: String }
|
||||
#[derive(Debug)]
|
||||
struct Report { playback: Option<PlaybackBench>, video: Option<VideoBench>, graphics: Option<GraphicsBench>, compilation: Option<CompileBench>, memory_bench: Option<MemoryBench>, developer: Option<DevBench>, sensors: Vec<(String, f64)>, network: Vec<(String,String)>, installed_ram_gib: Option<f64>, checklist: Vec<(String,String)>, asset: String, timestamp: u64, distro: String, kernel: String, vendor: String, model: String, cpu: String, threads: usize, mem_gib: f64, gpus: Vec<String>, disks: Vec<Disk>, batteries: Vec<Battery>, bench: Option<Bench>, roles: Vec<Role>, concerns: Vec<String>, health_gate: &'static str }
|
||||
|
||||
fn read(path: impl AsRef<Path>) -> String { fs::read_to_string(path).unwrap_or_default().trim().to_string() }
|
||||
fn dmi_identity(key: &str) -> String { let value = read(format!("/sys/class/dmi/id/{key}")); if value.is_empty() { read(format!("/run/csr-intake/{key}")) } else { value } }
|
||||
fn number(path: impl AsRef<Path>) -> Option<u64> { read(path).parse().ok() }
|
||||
fn clean(s: &str) -> String { s.replace(|c: char| matches!(c, '\r' | '\n' | '\t'), " ").trim().to_string() }
|
||||
fn esc(s: &str) -> String { let mut out = String::new(); for c in s.chars() { match c { '&' => out.push_str("&"), '<' => out.push_str("<"), '>' => out.push_str(">"), '"' => out.push_str("""), '\'' => out.push_str("'"), _ => out.push(c) } } out }
|
||||
fn json(s: &str) -> String { let mut out=String::from("\""); for c in s.chars() { match c { '"'=>out.push_str("\\\""), '\\'=>out.push_str("\\\\"), '\n'=>out.push_str("\\n"), '\r'=>out.push_str("\\r"), '\t'=>out.push_str("\\t"), c if c < ' '=>out.push_str(&format!("\\u{:04x}",c as u32)), _=>out.push(c) } } out.push('"'); out }
|
||||
fn csv(s: &str) -> String { format!("\"{}\"", s.replace('"', "\"\"")) }
|
||||
fn distro() -> String { for line in read("/etc/os-release").lines() { if let Some(x)=line.strip_prefix("PRETTY_NAME=") { return x.trim_matches('"').to_owned(); } } "Unknown".into() }
|
||||
fn cpu() -> String { for line in read("/proc/cpuinfo").lines() { if let Some((k,v))=line.split_once(':') { if k.trim()=="model name" || k.trim()=="Processor" { return clean(v); } } } "Unknown".into() }
|
||||
fn mem_gib() -> f64 { for line in read("/proc/meminfo").lines() { if let Some(x)=line.strip_prefix("MemTotal:") { return x.split_whitespace().next().and_then(|v|v.parse::<f64>().ok()).unwrap_or(0.0)/1048576.0; } } 0.0 }
|
||||
// DMI type 17 is optional and often root-only; do not mistake MemTotal for installed RAM.
|
||||
fn installed_ram_gib() -> Option<f64> {
|
||||
let bytes=fs::read("/sys/firmware/dmi/tables/DMI").or_else(|_| fs::read("/run/csr-intake/DMI")).ok()?;
|
||||
let mut at=0usize; let mut total_mib=0u64; let mut count=0u64;
|
||||
while at+4<=bytes.len() {
|
||||
let kind=bytes[at];let len=bytes[at+1] as usize;
|
||||
if len<4 || at+len>bytes.len(){break;}
|
||||
if kind==17 && len>=14 {
|
||||
let size=u16::from_le_bytes([bytes[at+12],bytes[at+13]]);
|
||||
let mib=match size {
|
||||
0 | 0xffff => 0,
|
||||
0x7fff if len>=32 => u32::from_le_bytes([bytes[at+28],bytes[at+29],bytes[at+30],bytes[at+31]]) as u64,
|
||||
0x7fff => 0,
|
||||
n if n & 0x8000 != 0 => ((n & 0x7fff) as u64)/1024,
|
||||
n => n as u64,
|
||||
};
|
||||
if mib>0 {total_mib+=mib;count+=1;}
|
||||
}
|
||||
let mut next=at+len;
|
||||
while next+1<bytes.len() && !(bytes[next]==0 && bytes[next+1]==0){next+=1;}
|
||||
if next+1>=bytes.len(){break;}
|
||||
at=next+2;
|
||||
if kind==127 {break;}
|
||||
}
|
||||
if count>0 {Some(total_mib as f64/1024.0)} else {None}
|
||||
}
|
||||
const CHECK_NAMES: [&str; 11] = ["display","keyboard","touchpad","wifi","audio","microphone","webcam","usb_ports","charging","thermals","stability"];
|
||||
fn checklist(path:Option<&Path>) -> Result<Vec<(String,String)>,String> {
|
||||
let mut result:Vec<(String,String)>=CHECK_NAMES.iter().map(|n|(n.to_string(),"UNKNOWN".into())).collect();
|
||||
if let Some(path)=path {
|
||||
for (i,line) in fs::read_to_string(path).map_err(|e|format!("Checklist: {e}"))?.lines().enumerate() {
|
||||
let line=line.trim(); if line.is_empty() || line.starts_with('#'){continue;}
|
||||
let (name,value)=line.split_once('=').ok_or_else(||format!("Checklist line {}: expected name=PASS/FAIL/UNKNOWN",i+1))?;
|
||||
let name=name.trim();let value=value.trim().to_uppercase();
|
||||
if !["PASS","FAIL","UNKNOWN","NOT_TESTED","NOT_APPLICABLE","UNAVAILABLE","SETUP_PROBLEM"].contains(&value.as_str()){return Err(format!("Checklist line {}: invalid value",i+1));}
|
||||
let slot=result.iter_mut().find(|(n,_)|n==name).ok_or_else(||format!("Checklist line {}: unknown check {}",i+1,name))?;
|
||||
slot.1=if ["NOT_TESTED","UNAVAILABLE","SETUP_PROBLEM"].contains(&value.as_str()) {"UNKNOWN".into()} else {value};
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
fn gpu() -> Vec<String> {
|
||||
let mut v=vec![];
|
||||
if let Ok(entries)=fs::read_dir("/sys/bus/pci/devices") {
|
||||
for e in entries.flatten() { let p=e.path(); let class=read(p.join("class"));
|
||||
if class.starts_with("0x03") { let vendor=read(p.join("vendor")); let device=read(p.join("device")); let driver=fs::read_link(p.join("driver")).ok().and_then(|x|x.file_name().map(|s|s.to_string_lossy().into_owned())).unwrap_or_else(||"unbound".into());
|
||||
v.push(format!("PCI {}:{} driver={}",vendor,device,driver));
|
||||
}
|
||||
}
|
||||
}
|
||||
v.sort(); v
|
||||
}
|
||||
fn disks() -> Vec<Disk> {
|
||||
let mut found=vec![];
|
||||
if let Ok(entries)=fs::read_dir("/sys/block") { for e in entries.flatten() {
|
||||
let name=e.file_name().to_string_lossy().into_owned();
|
||||
if !(name.starts_with("sd") || name.starts_with("nvme") || name.starts_with("vd") || name.starts_with("mmcblk")) {continue;}
|
||||
let p=e.path(); let sectors=number(p.join("size")).unwrap_or(0);
|
||||
if sectors==0 {continue;}
|
||||
let rot=number(p.join("queue/rotational")).map(|x|x!=0);
|
||||
let model=read(p.join("device/model"));
|
||||
found.push(Disk {name, gib:sectors.saturating_mul(512)/1073741824, rotational:rot, removable:number(p.join("removable")).unwrap_or(0)!=0, model, health:"UNKNOWN".into(), smart_details:"Not assessed".into()});
|
||||
}}
|
||||
found.sort_by(|a,b|a.name.cmp(&b.name)); found
|
||||
}
|
||||
|
||||
// Linux NVMe Get Log Page, SMART/Health Information (log identifier 0x02).
|
||||
// No writes to the SSD. ioctl may require root/CAP_SYS_ADMIN; failures stay UNKNOWN.
|
||||
// SATA, USB, and virtual disks are not falsely marked healthy.
|
||||
#[cfg(target_os="linux")]
|
||||
mod native_nvme {
|
||||
use std::{fs::OpenOptions, os::{fd::AsRawFd, raw::{c_int,c_ulong}}, path::Path};
|
||||
#[repr(C)]
|
||||
#[derive(Default)]
|
||||
struct NvmeAdminCmd {
|
||||
opcode:u8, flags:u8, rsvd1:u16, nsid:u32, cdw2:u32, cdw3:u32,
|
||||
metadata:u64, addr:u64, metadata_len:u32, data_len:u32,
|
||||
cdw10:u32, cdw11:u32, cdw12:u32, cdw13:u32, cdw14:u32, cdw15:u32,
|
||||
timeout_ms:u32, result:u32,
|
||||
}
|
||||
unsafe extern "C" {fn ioctl(fd:c_int, request:c_ulong, ...) -> c_int;}
|
||||
const NVME_IOCTL_ADMIN_CMD:c_ulong=0xC0484E41;
|
||||
fn le128(data:&[u8])->u128 {let mut bytes=[0u8;16];bytes.copy_from_slice(data);u128::from_le_bytes(bytes)}
|
||||
pub fn read_health(name:&str)->Result<(String,String),String> {
|
||||
let controller=name.get(..name[4..].find('n').map(|i|i+4).unwrap_or(name.len())).unwrap_or(name); // nvme0n1 -> nvme0
|
||||
if !controller.starts_with("nvme") || !controller[4..].chars().all(|c|c.is_ascii_digit()) {
|
||||
return Err("Not an NVMe namespace".into());
|
||||
}
|
||||
let device=format!("/dev/{controller}");
|
||||
if !Path::new(&device).exists(){return Err(format!("{device} unavailable"));}
|
||||
let file=OpenOptions::new().read(true).open(&device).map_err(|e|format!("open {device}: {e}"))?;
|
||||
let mut buffer=[0u8;512];
|
||||
let mut command=NvmeAdminCmd {opcode:0x02,nsid:0xffffffff,addr:buffer.as_mut_ptr() as u64,
|
||||
data_len:512,cdw10:((128-1)<<16)|0x02,timeout_ms:5000,..Default::default()};
|
||||
let result=unsafe {ioctl(file.as_raw_fd(),NVME_IOCTL_ADMIN_CMD,&mut command)};
|
||||
if result!=0 {return Err(format!("NVMe admin ioctl returned {result} (may need root/permissions or unsupported device)"));}
|
||||
let warnings=buffer[0];
|
||||
let temp_k=u16::from_le_bytes([buffer[1],buffer[2]]);
|
||||
let spare=buffer[3];let threshold=buffer[4];let used=buffer[5];
|
||||
let media_errors=le128(&buffer[160..176]);
|
||||
let status=if warnings!=0 || spare<threshold || media_errors>0 {"WARN"} else if used>=100 {"WARN"} else {"PASS"};
|
||||
let temp=if temp_k>0 {format!("{} C",i32::from(temp_k)-273)} else {"UNKNOWN".into()};
|
||||
let details=format!("NVMe SMART: critical_warning=0x{warnings:02x}; temperature={temp}; available_spare={spare}%; spare_threshold={threshold}%; percentage_used={used}%; media_errors={media_errors}. WARN requires review, not automatic failure.");
|
||||
Ok((status.into(),details))
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os="linux"))]
|
||||
mod native_nvme {pub fn read_health(_: &str)->Result<(String,String),String>{Err("Linux only".into())}}
|
||||
// Optional read-only smartctl transport for SATA/USB/NVMe. Never infer PASS from an exit code
|
||||
// alone; smartctl exits nonzero for some warning conditions, so parse both stdout and stderr.
|
||||
fn smartctl_health(name: &str) -> Result<(String,String),String> {
|
||||
let path=format!("/dev/{name}");
|
||||
let output=Command::new("smartctl").args(["-H","-A",&path]).output()
|
||||
.map_err(|e|format!("smartctl unavailable ({e})"))?;
|
||||
let content=String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr=String::from_utf8_lossy(&output.stderr);
|
||||
if content.trim().is_empty() {return Err(format!("smartctl gave no output: {}",stderr.chars().take(160).collect::<String>()));}
|
||||
let upper=content.to_ascii_uppercase();
|
||||
// smartctl health strings differ for ATA and SCSI; do not guess on unsupported bridges.
|
||||
let passed=upper.contains("SMART OVERALL-HEALTH SELF-ASSESSMENT TEST RESULT: PASSED")
|
||||
|| upper.contains("SMART HEALTH STATUS: OK")
|
||||
|| upper.contains("SMART OVERALL-HEALTH SELF-ASSESSMENT TEST RESULT: OK");
|
||||
let failed=upper.contains("SMART OVERALL-HEALTH SELF-ASSESSMENT TEST RESULT: FAILED")
|
||||
|| upper.contains("SMART HEALTH STATUS: BAD")
|
||||
|| upper.contains("SMART HEALTH STATUS: FAILED");
|
||||
let interesting:Vec<String>=content.lines().filter(|l| {
|
||||
let x=l.to_ascii_lowercase();
|
||||
["overall-health","smart health status","reallocated_sector_ct","current_pending_sector","offline_uncorrectable","reported_uncorrect","percentage used","media and data integrity errors","critical warning"].iter().any(|w|x.contains(w))
|
||||
}).take(14).map(|l|clean(l)).collect();
|
||||
let details=format!("smartctl (-H -A) exit={}; {}",output.status,interesting.join(" | "));
|
||||
// An overall PASSED flag is not full disk certification. Inspect problem attributes.
|
||||
let bad_attr=content.lines().any(|line| {
|
||||
let x=line.to_ascii_lowercase();
|
||||
if ["reallocated_sector_ct","current_pending_sector","offline_uncorrectable","reported_uncorrect"].iter().any(|t|x.contains(t)) {
|
||||
line.split_whitespace().last().and_then(|v|v.parse::<u64>().ok()).is_some_and(|n|n>0)
|
||||
} else {false}
|
||||
});
|
||||
if failed || bad_attr {Ok(("WARN".into(),details))}
|
||||
else if passed {Ok(("PASS".into(),details))}
|
||||
else {Ok(("UNKNOWN".into(),details))}
|
||||
}
|
||||
|
||||
fn assess_disks(disks:&mut [Disk]) {
|
||||
for disk in disks {
|
||||
if disk.name.starts_with("nvme") {
|
||||
match native_nvme::read_health(&disk.name) {
|
||||
Ok((state,details))=>{disk.health=state;disk.smart_details=details;},
|
||||
Err(e)=>{disk.smart_details=format!("NVMe SMART unavailable: {e}");}
|
||||
}
|
||||
} else {
|
||||
match smartctl_health(&disk.name) {
|
||||
Ok((state,details))=>{disk.health=state;disk.smart_details=details;},
|
||||
Err(e)=>{disk.smart_details=format!("SATA/USB SMART unavailable: {e}");}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Actual FFmpeg file decode (not a display/playback or hardware acceleration test).
|
||||
// Video test uses a user-provided real file; requires ffmpeg on the target machine.
|
||||
fn ffmpeg_video(path:&Path) -> VideoBench {
|
||||
let source=path.display().to_string();
|
||||
let unknown=|status:String|VideoBench{status,source:source.clone(),codec:"UNKNOWN".into(),width:0,height:0,frames:0,seconds:0.0,fps:0.0,realtime_factor:0.0,note:"Real input decoded to null output; measures decode throughput, not dropped display frames".into()};
|
||||
if !path.is_file() {return unknown("UNAVAILABLE (video file does not exist)".into());}
|
||||
let start=Instant::now();
|
||||
let output=Command::new("ffmpeg").args(["-hide_banner","-nostdin","-v","info","-i"]).arg(path)
|
||||
.args(["-map","0:v:0","-an","-sn","-f","null","-"]).output();
|
||||
let output=match output {Ok(o)=>o,Err(e)=>return unknown(format!("UNAVAILABLE (ffmpeg: {e})"))};
|
||||
let log=String::from_utf8_lossy(&output.stderr);
|
||||
if !output.status.success() {
|
||||
let detail = log.lines().last().unwrap_or("unknown error");
|
||||
return unknown(format!(
|
||||
"FAIL (ffmpeg exited {}; {})",
|
||||
output.status,
|
||||
detail.chars().take(160).collect::<String>()
|
||||
));
|
||||
}
|
||||
// Extract frame count from the final ffmpeg progress line (informational only).
|
||||
let progress=log.lines().rev().find(|l|l.contains("frame=")).unwrap_or("");
|
||||
let frames=progress.split("frame=").nth(1).and_then(|x|x.trim_start().split_whitespace().next()).and_then(|x|x.parse::<u64>().ok()).unwrap_or(0);
|
||||
let video_line=log.lines().find(|l|l.contains("Video:")).unwrap_or("");
|
||||
let codec=video_line.split("Video:").nth(1).and_then(|x|x.split(',').next()).unwrap_or("UNKNOWN").trim().to_string();
|
||||
let dimensions=video_line.split(|c:char| !c.is_ascii_alphanumeric() && c!='x').find(|x| {let mut it=x.split('x');it.next().and_then(|n|n.parse::<u32>().ok()).is_some() && it.next().and_then(|n|n.parse::<u32>().ok()).is_some()}).unwrap_or("");
|
||||
let mut pair=dimensions.split('x');let width=pair.next().and_then(|x|x.parse().ok()).unwrap_or(0);let height=pair.next().and_then(|x|x.parse().ok()).unwrap_or(0);
|
||||
let seconds=start.elapsed().as_secs_f64();
|
||||
let fps=if seconds>0.0 {frames as f64/seconds} else {0.0};
|
||||
// Estimated real-time factor based on reported input fps if known; not displayed playback.
|
||||
let nominal=video_line.split(',').find_map(|x|{let x=x.trim();x.strip_suffix(" fps").and_then(|n|n.trim().parse::<f64>().ok())}).unwrap_or(0.0);
|
||||
VideoBench{status:if frames>0 {"PASS".into()} else {"UNKNOWN (ffmpeg did not report frame count)".into()},source,codec,width,height,frames,seconds,fps,realtime_factor:if nominal>0.0 {fps/nominal} else {0.0},note:"Real media decoded by FFmpeg to null output; excludes display scheduling, sound, hardware acceleration validation and dropped frames".into()}
|
||||
}
|
||||
// On-screen playback check via mpv, when a graphical session and mpv are installed.
|
||||
// A successful process exit means playback completed, not that there were no dropped frames.
|
||||
fn mpv_playback(path:&Path)->PlaybackBench {
|
||||
if !path.is_file() {return PlaybackBench{status:"UNAVAILABLE".into(),elapsed_seconds:0.0,note:"File missing".into()};}
|
||||
let start=Instant::now();
|
||||
let output=Command::new("mpv").args(["--no-config","--no-audio","--no-osc","--frames=300","--hwdec=auto","--vo=gpu","--really-quiet","--"]).arg(path).output();
|
||||
match output {
|
||||
Err(e)=>PlaybackBench{status:"UNAVAILABLE".into(),elapsed_seconds:0.0,note:format!("mpv unavailable: {e}")},
|
||||
Ok(o)=>PlaybackBench{status:if o.status.success(){"COMPLETED"}else{"FAILED"}.into(),elapsed_seconds:start.elapsed().as_secs_f64(),note:format!("300-frame on-screen mpv run; exit={}. Dropped frames and display smoothness still require observation",o.status)}
|
||||
}
|
||||
}
|
||||
// Optional benchmark, not a gaming FPS measurement. glmark2 requires usable display session.
|
||||
fn graphics_benchmark()->GraphicsBench {
|
||||
let output=Command::new("glmark2").arg("--off-screen").output();
|
||||
match output {
|
||||
Err(e)=>GraphicsBench{status:"UNAVAILABLE".into(),score:None,note:format!("glmark2 not installed or could not launch: {e}")},
|
||||
Ok(o)=>{
|
||||
let log=String::from_utf8_lossy(&o.stdout);
|
||||
let score=log.lines().find_map(|line|line.trim().strip_prefix("glmark2 Score:").and_then(|v|v.trim().parse::<u64>().ok()));
|
||||
GraphicsBench{status:if o.status.success()&&score.is_some(){"PASS"}else{"UNKNOWN"}.into(),score,note:format!("Off-screen OpenGL benchmark (not game FPS), process status {}",o.status)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn median(values:&mut [f64])->f64 {
|
||||
values.sort_by(|a,b|a.total_cmp(b));
|
||||
let m=values.len()/2;
|
||||
if values.len()%2==1 {values[m]} else {(values[m-1]+values[m])/2.0}
|
||||
}
|
||||
// Read thermal sensors from hwmon. Labels are untrusted hardware strings and escaped in HTML.
|
||||
fn temperatures() -> Vec<(String,f64)> {
|
||||
let mut result=Vec::new();
|
||||
if let Ok(chips)=fs::read_dir("/sys/class/hwmon") { for chip in chips.flatten() {
|
||||
let p=chip.path(); let chipname=read(p.join("name"));
|
||||
if let Ok(files)=fs::read_dir(&p) {for file in files.flatten() {
|
||||
let filename=file.file_name().to_string_lossy().into_owned();
|
||||
if !filename.starts_with("temp") || !filename.ends_with("_input") {continue;}
|
||||
let stem=filename.trim_end_matches("_input");
|
||||
if let Some(millidegrees)=number(file.path()) {
|
||||
if millidegrees>180_000 {continue;}
|
||||
let label=read(p.join(format!("{stem}_label")));
|
||||
let name=if label.is_empty(){format!("{chipname}:{stem}")} else {format!("{chipname}:{label}")};
|
||||
result.push((name,millidegrees as f64/1000.0));
|
||||
}
|
||||
}}
|
||||
}}
|
||||
result.sort_by(|a,b|a.0.cmp(&b.0));result
|
||||
}
|
||||
// Detect network devices and their Linux link state (does not imply internet connectivity).
|
||||
fn network_devices() -> Vec<(String,String)> {
|
||||
let mut result=Vec::new();
|
||||
if let Ok(entries)=fs::read_dir("/sys/class/net") {for e in entries.flatten() {
|
||||
let name=e.file_name().to_string_lossy().into_owned();
|
||||
if name=="lo" {continue;}
|
||||
let p=e.path();let link=read(p.join("operstate"));
|
||||
let kind=if p.join("wireless").exists() || p.join("phy80211").exists() {"Wi-Fi"} else {"Network"};
|
||||
result.push((name,format!("{kind}, link={link}; connectivity not tested")));
|
||||
}}result.sort_by(|a,b|a.0.cmp(&b.0));result
|
||||
}
|
||||
fn batteries() -> Vec<Battery> {
|
||||
let mut found=vec![];
|
||||
if let Ok(entries)=fs::read_dir("/sys/class/power_supply") { for e in entries.flatten() {
|
||||
let p=e.path(); if read(p.join("type"))!="Battery" {continue;}
|
||||
let name=e.file_name().to_string_lossy().into_owned();
|
||||
let full=number(p.join("energy_full")).or_else(||number(p.join("charge_full")));
|
||||
let design=number(p.join("energy_full_design")).or_else(||number(p.join("charge_full_design")));
|
||||
let health=full.zip(design).and_then(|(f,d)|if d>0 {Some((f.saturating_mul(100)/d).min(150))} else {None});
|
||||
found.push(Battery{name,capacity_pct:number(p.join("capacity")),health_pct:health,status:read(p.join("status"))});
|
||||
}} found
|
||||
}
|
||||
// Fixed-workload integer mixing (throughput proxy, NOT a standardized CPU benchmark).
|
||||
fn cpu_work(iterations:u64) -> u64 { let mut x=0x243f6a8885a308d3u64; for i in 0..iterations {x=x.wrapping_add(i ^ 0x9e3779b97f4a7c15); x^=x>>27; x=x.wrapping_mul(0x3c79ac492ba7b653); x^=x>>33;} black_box(x) }
|
||||
fn benchmark() -> Bench {
|
||||
let threads=thread::available_parallelism().map(|n|n.get()).unwrap_or(1).min(16);
|
||||
// Warm up the workload once to reduce first-run noise.
|
||||
black_box(cpu_work(WORK / 4));
|
||||
let mut singles=Vec::new();let mut parallels=Vec::new();
|
||||
for _ in 0..5 {
|
||||
let t=Instant::now();black_box(cpu_work(WORK));singles.push(t.elapsed().as_secs_f64()*1000.0);
|
||||
let t=Instant::now();let hs:Vec<_>=(0..threads).map(|_|thread::spawn(||cpu_work(WORK))).collect();
|
||||
for h in hs {let _=black_box(h.join());}
|
||||
parallels.push(t.elapsed().as_secs_f64()*1000.0);
|
||||
}
|
||||
let mut s=singles.clone();let mut p=parallels.clone();
|
||||
Bench{single_ms:median(&mut s),multi_ms:median(&mut p),threads,single_runs_ms:singles,parallel_runs_ms:parallels}
|
||||
}
|
||||
// Deterministic source-text scanning/indexing proxy; this is NOT a compilation benchmark.
|
||||
// No compiler or external executable is needed on refurbished devices.
|
||||
fn source_index_work(records: usize) -> u64 {
|
||||
let mut checksum = 0xcbf29ce484222325u64;
|
||||
let keywords = ["fn", "let", "impl", "struct", "pub", "match"];
|
||||
for n in 0..records {
|
||||
let line = format!("pub fn function_{n}(value: u64) -> u64 {{ let output = value.wrapping_mul({}); output }}", n % 7919 + 1);
|
||||
for token in line.split(|c: char| !c.is_ascii_alphanumeric() && c != '_') {
|
||||
if token.is_empty() { continue; }
|
||||
let is_keyword = keywords.contains(&token);
|
||||
let mut hash = 0xcbf29ce484222325u64;
|
||||
for b in token.bytes() { hash = (hash ^ b as u64).wrapping_mul(0x100000001b3); }
|
||||
checksum = checksum.rotate_left(7) ^ hash ^ u64::from(is_keyword);
|
||||
}
|
||||
}
|
||||
black_box(checksum)
|
||||
}
|
||||
fn developer_benchmark() -> DevBench {
|
||||
const RECORDS: usize = 16_384;
|
||||
black_box(source_index_work(256));
|
||||
let mut runs = Vec::new();
|
||||
let mut checksum = 0;
|
||||
for _ in 0..3 {
|
||||
let t = Instant::now();
|
||||
checksum = source_index_work(RECORDS);
|
||||
runs.push(t.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
let mut sorted = runs.clone();
|
||||
DevBench { median_ms: median(&mut sorted), runs_ms: runs, records: RECORDS, checksum }
|
||||
}
|
||||
// Allocates 64 MiB; read-only timed passes over filled memory. No disk writes.
|
||||
// This is a single-thread sequential READ proxy, not a standardized DRAM benchmark.
|
||||
fn memory_benchmark() -> MemoryBench {
|
||||
const MIB: usize = 1024 * 1024;
|
||||
const BYTES: usize = 64 * MIB;
|
||||
let elements = BYTES / std::mem::size_of::<u64>();
|
||||
let data: Vec<u64> = (0..elements).map(|i| (i as u64).wrapping_mul(0x9e3779b97f4a7c15)).collect();
|
||||
let mut speeds = Vec::new();
|
||||
let mut checksum = 0u64;
|
||||
for _ in 0..5 {
|
||||
let start = Instant::now();
|
||||
let sum = data.iter().fold(0u64, |sum, item| sum.wrapping_add(black_box(*item)));
|
||||
black_box(sum);
|
||||
checksum = sum;
|
||||
let secs = start.elapsed().as_secs_f64().max(0.000_001);
|
||||
speeds.push(64.0 / secs);
|
||||
}
|
||||
let mut sorted = speeds.clone();
|
||||
MemoryBench { median_mib_s: median(&mut sorted), runs_mib_s: speeds, buffer_mib: 64, checksum }
|
||||
}
|
||||
fn cpu_variation_pct(b: &Bench) -> f64 {
|
||||
if b.single_ms <= 0.0 { return 0.0; }
|
||||
let lo = b.single_runs_ms.iter().copied().fold(f64::INFINITY, f64::min);
|
||||
let hi = b.single_runs_ms.iter().copied().fold(0.0_f64, f64::max);
|
||||
(hi - lo) / b.single_ms * 100.0
|
||||
}
|
||||
// Provisional fixed anchor points for the existing v1 synthetic workloads.
|
||||
// Scores are NOT percentiles, validated application benchmarks, or donation approvals.
|
||||
fn higher_is_better(value: f64, low: f64, high: f64) -> u8 {
|
||||
if !value.is_finite() || value <= 0.0 { return 0; }
|
||||
((100.0 * (value - low) / (high - low)).clamp(0.0, 100.0)).round() as u8
|
||||
}
|
||||
fn lower_is_better(value: f64, best: f64, worst: f64) -> u8 {
|
||||
if !value.is_finite() || value <= 0.0 { return 0; }
|
||||
((100.0 * (worst - value) / (worst - best)).clamp(0.0, 100.0)).round() as u8
|
||||
}
|
||||
// Optional real C compilation benchmark. The tool itself has no external runtime dependencies.
|
||||
// Uses a compiler only when explicitly requested. All generated files are confined to a fresh
|
||||
// temporary directory and removed even if the compiler fails. No generated code is executed.
|
||||
fn compilation_benchmark() -> CompileBench {
|
||||
const FUNCTIONS: usize = 320;
|
||||
let compiler = ["cc", "gcc", "clang"].iter().find(|name|
|
||||
Command::new(name).arg("--version").output().is_ok()
|
||||
).map(|x|x.to_string());
|
||||
let Some(compiler) = compiler else { return CompileBench { compiler:"none".into(), median_ms:0.0, runs_ms:vec![], source_functions:FUNCTIONS, status:"UNAVAILABLE (C compiler not installed)".into() }; };
|
||||
let folder = env::temp_dir().join(format!("pc-intake-compile-{}-{}",process::id(),SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos()));
|
||||
if let Err(e) = fs::create_dir(&folder) {return CompileBench {compiler,median_ms:0.0,runs_ms:vec![],source_functions:FUNCTIONS,status:format!("UNAVAILABLE (temporary directory: {e})")};}
|
||||
let result = (|| -> Result<Vec<f64>,String> {
|
||||
let source=folder.join("workload.c");
|
||||
let mut contents=String::from("#include <stdint.h>\n");
|
||||
for n in 0..FUNCTIONS { contents.push_str(&format!("uint64_t fn{n}(uint64_t x) {{ for (uint64_t j=0;j<96;j++) x=(x*6364136223846793005ULL+1442695040888963407ULL)^(x>>13); return x+{n}ULL; }}\n")); }
|
||||
fs::write(&source,contents).map_err(|e|e.to_string())?;
|
||||
let mut times=vec![];
|
||||
for iteration in 0..3 {
|
||||
let object=folder.join(format!("workload-{iteration}.o"));
|
||||
let start=Instant::now();
|
||||
let output=Command::new(&compiler).arg("-std=c11").arg("-O2").arg("-c").arg(&source).arg("-o").arg(&object).output().map_err(|e|e.to_string())?;
|
||||
if !output.status.success() {return Err(format!("compiler exited {}: {}",output.status, String::from_utf8_lossy(&output.stderr).chars().take(240).collect::<String>()));}
|
||||
let metadata=fs::metadata(&object).map_err(|e|e.to_string())?;
|
||||
if metadata.len()==0 {return Err("empty compiler output".into());}
|
||||
times.push(start.elapsed().as_secs_f64()*1000.0);
|
||||
}
|
||||
Ok(times)
|
||||
})();
|
||||
let _=fs::remove_dir_all(&folder);
|
||||
match result {
|
||||
Ok(mut runs) => {let med=median(&mut runs);CompileBench{compiler,median_ms:med,runs_ms:runs,source_functions:FUNCTIONS,status:"PASS".into()}},
|
||||
Err(e) => CompileBench{compiler,median_ms:0.0,runs_ms:vec![],source_functions:FUNCTIONS,status:format!("UNAVAILABLE ({e})")},
|
||||
}
|
||||
}
|
||||
|
||||
fn grade_with_evidence(cpu: Option<&Bench>, dev: Option<&DevBench>, ram: Option<&MemoryBench>) -> Vec<Role> {
|
||||
let Some(b) = cpu else {return ["Office","Student","Developer","Multimedia playback","Video creation","Gaming"].iter().map(|name|Role{name:*name,verdict:"NOT_ASSESSED",score:None,reason:"Benchmarks skipped (--quick) or unavailable".into()}).collect()};
|
||||
let s=lower_is_better(b.single_ms, 12.0, 85.0);
|
||||
let m=lower_is_better(b.multi_ms, 18.0, 450.0);
|
||||
let office=(s as u16 * 7 + m as u16 * 3)/10;
|
||||
let mut r=vec![Role{name:"Office",verdict:if office>=50 {"PROVISIONAL_CANDIDATE"} else if office>=25 {"LIMITED"} else {"NOT_RECOMMENDED"},score:Some(office as u8),reason:"Fixed-anchor CPU proxy score; verify real browser/office responsiveness".into()}];
|
||||
r.push(Role{name:"Student",verdict:"NEEDS_VIDEO_CALL_TEST",score:None,reason:"CPU proxy available, but video calls/playback and peripherals untested".into()});
|
||||
if let (Some(d),Some(mem))=(dev,ram) {
|
||||
let ds=lower_is_better(d.median_ms, 15.0, 250.0);
|
||||
let rs=higher_is_better(mem.median_mib_s, 1500.0, 20000.0);
|
||||
let composite=(s as u32*30 + m as u32*30 + ds as u32*30 + rs as u32*10)/100;
|
||||
r.push(Role{name:"Developer",verdict:if composite>=65 {"PROVISIONAL_STRONG_CANDIDATE"} else if composite>=40 {"PROVISIONAL_BASIC_CANDIDATE"} else {"LIMITED"},score:Some(composite as u8),reason:format!("CPU single {s}/100, parallel {m}/100, source-index proxy {ds}/100, memory-read proxy {rs}/100. Not real compilation; thresholds need fleet calibration")});
|
||||
} else {
|
||||
r.push(Role{name:"Developer",verdict:"NOT_ASSESSED",score:None,reason:"Source-index and memory measurements are required".into()});
|
||||
}
|
||||
for (name,reason) in [
|
||||
("Multimedia playback","No decoded frames, dropped-frame or codec playback measurements"),
|
||||
("Video creation","No verified software/hardware video encoding workload"),
|
||||
("Gaming","No actual GPU frame-rate or game benchmark")
|
||||
] {r.push(Role{name,verdict:"NOT_ASSESSED",score:None,reason:reason.into()});}
|
||||
r
|
||||
}
|
||||
// Keep legacy gate-style grading API testable while replacing ungrounded decisions.
|
||||
fn grade(single_ms:f64,multi_ms:f64,threads:usize)->Vec<Role>{
|
||||
let b=Bench{single_ms,multi_ms,threads,single_runs_ms:vec![single_ms],parallel_runs_ms:vec![multi_ms]};
|
||||
grade_with_evidence(Some(&b),None,None)
|
||||
}
|
||||
fn collect(asset:String, quick:bool, do_compile:bool, video_path:Option<&Path>, do_graphics:bool, do_playback:bool, checklist: Vec<(String,String)>) -> Report {
|
||||
let threads=thread::available_parallelism().map(|n|n.get()).unwrap_or(1);
|
||||
let mut disk=disks(); assess_disks(&mut disk); let bat=batteries(); let sensors=temperatures();let network=network_devices(); let mem=mem_gib(); let installed=installed_ram_gib();
|
||||
let mut concerns=vec![];
|
||||
if let Some(physical)=installed { if physical < 14.0 { concerns.push(format!("DMI reports {:.1} GiB installed; verify standardized 16 GB RAM", physical)); } }
|
||||
if disk.is_empty() {concerns.push("No local block devices detected".into());}
|
||||
if disk.iter().any(|d|d.rotational==Some(true)) {concerns.push("Rotating HDD detected; expected SSD".into());}
|
||||
for (name,temp) in &sensors {if *temp>=90.0 {concerns.push(format!("High sensor reading {name}: {temp:.1} C; investigate cooling"));}}
|
||||
for b in &bat {if let Some(h)=b.health_pct {if h<70 {concerns.push(format!("Battery {} reported {}% of design capacity",b.name,h));}}}
|
||||
for d in &disk {if d.health!="PASS" {concerns.push(format!("Storage {} health {}: {}",d.name,d.health,d.smart_details));}}
|
||||
for (name,state) in &checklist {if state!="PASS" && state!="NOT_APPLICABLE" {concerns.push(format!("Technician check {name}: {state}"));}}
|
||||
if disk.is_empty() {concerns.push("No SSD available to certify".into());}
|
||||
let bench=if quick {None} else {Some(benchmark())};
|
||||
let developer=if quick {None} else {Some(developer_benchmark())};
|
||||
let memory_bench=if quick {None} else {Some(memory_benchmark())};
|
||||
if let Some(b)=&bench { if cpu_variation_pct(b)>35.0 {concerns.push(format!("CPU benchmark variation {:.1}% across runs; repeat after checking thermal/power conditions",cpu_variation_pct(b)));} }
|
||||
let compilation=if !quick && do_compile {Some(compilation_benchmark())} else {None};
|
||||
if let Some(c)=&compilation {if c.status!="PASS" {concerns.push(format!("Optional compilation benchmark: {}",c.status));}}
|
||||
let video=video_path.map(ffmpeg_video);
|
||||
let playback=if do_playback {video_path.map(mpv_playback)} else {None};
|
||||
let graphics=if do_graphics {Some(graphics_benchmark())} else {None};
|
||||
if let Some(v)=&video {if v.status!="PASS" {concerns.push(format!("Video decode test: {}",v.status));}}
|
||||
let mut roles=grade_with_evidence(bench.as_ref(),developer.as_ref(),memory_bench.as_ref());
|
||||
if let Some(c)=&compilation {if c.status=="PASS" {if let Some(dev)=roles.iter_mut().find(|r|r.name=="Developer") {dev.reason.push_str(&format!("; real C compile: {:.1} ms ({} functions, 3 runs, compiler {})",c.median_ms,c.source_functions,c.compiler));}}}
|
||||
if let Some(v)=&video {if let Some(role)=roles.iter_mut().find(|r|r.name=="Multimedia playback") {role.verdict=if v.status=="PASS" && v.realtime_factor>=1.2 {"DECODE_CANDIDATE"} else {"DISPLAY_PLAYBACK_NOT_ASSESSED"};role.reason=format!("FFmpeg real-file decode: {} frames, {:.1} fps, {:.2}x realtime. Actual display playback and dropped frames not measured",v.frames,v.fps,v.realtime_factor);}}
|
||||
if let Some(g)=&graphics {if let Some(role)=roles.iter_mut().find(|r|r.name=="Gaming") {role.reason=format!("OpenGL glmark2: {} score={:?}; real game FPS untested",g.status,g.score);}}
|
||||
// Unknown SMART or missing functional checks must never mean 'pass'.
|
||||
let gate=if checklist.iter().any(|(_,s)|s=="FAIL") || disk.iter().any(|d|d.health=="WARN") || disk.iter().any(|d|d.rotational==Some(true)) || bat.iter().any(|b|b.health_pct.is_some_and(|h|h<70)) || sensors.iter().any(|(_,t)|*t>=90.0) {
|
||||
"REPAIR_OR_MANUAL_REVIEW_REQUIRED"
|
||||
} else if disk.is_empty() || disk.iter().any(|d|d.health!="PASS") || checklist.iter().any(|(_,s)|s!="PASS" && s!="NOT_APPLICABLE") || installed.is_some_and(|g|g<14.0) {
|
||||
"TECHNICIAN_AND_SMART_REVIEW_REQUIRED"
|
||||
} else {"READY_FOR_FINAL_APPROVAL"};
|
||||
Report{playback,video,graphics,compilation,memory_bench,developer,sensors,network,installed_ram_gib:installed,checklist,asset,timestamp:SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),distro:distro(),kernel:read("/proc/sys/kernel/osrelease"),vendor:read("/sys/class/dmi/id/sys_vendor"),model:read("/sys/class/dmi/id/product_name"),cpu:cpu(),threads,mem_gib:mem,gpus:gpu(),disks:disk,batteries:bat,bench,roles,concerns,health_gate:gate}
|
||||
}
|
||||
fn json_report(r:&Report) -> String {
|
||||
let mut s=format!("{{\n \"schema_version\": 1,\n \"tool_version\": {},\n \"asset_id\": {},\n \"timestamp_unix\": {},\n \"distro\": {},\n \"kernel\": {},\n \"manufacturer\": {},\n \"model\": {},\n \"cpu\": {},\n \"logical_threads\": {},\n \"ram_gib\": {:.3},\n \"health_gate\": {},\n",json(VERSION),json(&r.asset),r.timestamp,json(&r.distro),json(&r.kernel),json(&r.vendor),json(&r.model),json(&r.cpu),r.threads,r.mem_gib,json(r.health_gate));
|
||||
s.push_str(&format!(" \"serial_number\": {},\n \"system_uuid\": {},\n",json(&dmi_identity("product_serial")),json(&dmi_identity("product_uuid"))));
|
||||
s.push_str(&format!(" \"chassis_type\": {},\n", json(&read("/sys/class/dmi/id/chassis_type"))));
|
||||
let touchpad=fs::read_to_string("/proc/bus/input/devices").ok().map(|v|v.to_lowercase().contains("touchpad"));
|
||||
let webcam=fs::read_dir("/sys/class/video4linux").ok().map(|mut e|e.next().is_some());
|
||||
s.push_str(&format!(" \"capabilities\": {{\"touchpad\":{},\"webcam\":{}}},\n",touchpad.map(|v|v.to_string()).unwrap_or("null".into()),webcam.map(|v|v.to_string()).unwrap_or("null".into())));
|
||||
|
||||
s.push_str(" \"gpus\": [");for (i,g) in r.gpus.iter().enumerate(){if i>0{s.push(',');}s.push_str(&json(g));}s.push_str("],\n \"disks\": [");
|
||||
for (i,d) in r.disks.iter().enumerate(){if i>0{s.push(',');}s.push_str(&format!("{{\"name\":{},\"size_gib\":{},\"rotational\":{},\"removable\":{},\"model\":{},\"smart_health\":{},\"smart_details\":{}}}",json(&d.name),d.gib,d.rotational.map(|x|x.to_string()).unwrap_or("null".into()),d.removable,json(&d.model),json(&d.health),json(&d.smart_details)));}
|
||||
s.push_str("],\n \"temperatures_c\": [");
|
||||
for (i,(name,value)) in r.sensors.iter().enumerate() {if i>0{s.push(',');}s.push_str(&format!("{{\"sensor\":{},\"celsius\":{:.2}}}",json(name),value));}
|
||||
s.push_str("],\n \"network_devices\": [");
|
||||
for (i,(name,status)) in r.network.iter().enumerate(){if i>0{s.push(',');}s.push_str(&format!("{{\"interface\":{},\"status\":{}}}",json(name),json(status)));}
|
||||
s.push_str("],\n \"batteries\": [");for (i,b) in r.batteries.iter().enumerate(){if i>0{s.push(',');}s.push_str(&format!("{{\"name\":{},\"charge_percent\":{},\"health_percent\":{},\"status\":{}}}",json(&b.name),b.capacity_pct.map(|x|x.to_string()).unwrap_or("null".into()),b.health_pct.map(|x|x.to_string()).unwrap_or("null".into()),json(&b.status)));}
|
||||
s.push_str("],\n \"benchmarks\": "); if let Some(b)=&r.bench {s.push_str(&format!("{{\"type\":\"rust_integer_mix_v1\",\"work_per_thread\":{},\"single_ms\":{:.4},\"parallel_ms\":{:.4},\"parallel_threads\":{}}}",WORK,b.single_ms,b.multi_ms,b.threads));
|
||||
s.pop();s.push_str(",\"single_runs_ms\":[");for(i,v)in b.single_runs_ms.iter().enumerate(){if i>0{s.push(',');}s.push_str(&format!("{v:.4}"));}
|
||||
s.push_str("],\"parallel_runs_ms\":[");for(i,v)in b.parallel_runs_ms.iter().enumerate(){if i>0{s.push(',');}s.push_str(&format!("{v:.4}"));}s.push_str("]}");
|
||||
}else{s.push_str("null");}
|
||||
s.push_str(",\n \"memory_benchmark\": ");
|
||||
if let Some(b)=&r.memory_bench {
|
||||
s.push_str(&format!("{{\"type\":\"sequential_read_v1\",\"buffer_mib\":{},\"median_mib_s\":{:.2},\"checksum\":{},\"runs_mib_s\":[", b.buffer_mib,b.median_mib_s,b.checksum));
|
||||
for (i,v) in b.runs_mib_s.iter().enumerate(){if i>0{s.push(',');}s.push_str(&format!("{v:.2}"));}
|
||||
s.push_str("]}");
|
||||
} else {s.push_str("null");}
|
||||
s.push_str(",\n \"developer_benchmark\": ");
|
||||
if let Some(d)=&r.developer {
|
||||
s.push_str(&format!("{{\"type\":\"source_index_v1\",\"records\":{},\"median_ms\":{:.4},\"checksum\":{},\"runs_ms\":[",d.records,d.median_ms,d.checksum));
|
||||
for (i,v) in d.runs_ms.iter().enumerate() {if i>0{s.push(',');}s.push_str(&format!("{v:.4}"));}
|
||||
s.push_str("]}");
|
||||
} else {s.push_str("null");}
|
||||
s.push_str(",\n \"compilation_benchmark\": ");
|
||||
if let Some(c)=&r.compilation {s.push_str(&format!("{{\"status\":{},\"compiler\":{},\"median_ms\":{:.3},\"source_functions\":{},\"runs_ms\":[",json(&c.status),json(&c.compiler),c.median_ms,c.source_functions));for(i,v)in c.runs_ms.iter().enumerate(){if i>0{s.push(',');}s.push_str(&format!("{v:.3}"));}s.push_str("]}");} else {s.push_str("null");}
|
||||
s.push_str(",\n \"playback_check\": ");
|
||||
if let Some(v)=&r.playback {s.push_str(&format!("{{\"status\":{},\"elapsed_seconds\":{:.3},\"note\":{}}}",json(&v.status),v.elapsed_seconds,json(&v.note)));} else {s.push_str("null");}
|
||||
s.push_str(",\n \"video_benchmark\": ");
|
||||
if let Some(v)=&r.video {s.push_str(&format!("{{\"status\":{},\"source\":{},\"codec\":{},\"width\":{},\"height\":{},\"frames\":{},\"elapsed_seconds\":{:.3},\"decode_fps\":{:.2},\"realtime_factor\":{:.3},\"note\":{}}}",json(&v.status),json(&v.source),json(&v.codec),v.width,v.height,v.frames,v.seconds,v.fps,v.realtime_factor,json(&v.note)));} else {s.push_str("null");}
|
||||
s.push_str(",\n \"graphics_benchmark\": ");
|
||||
if let Some(g)=&r.graphics {s.push_str(&format!("{{\"status\":{},\"score\":{},\"note\":{}}}",json(&g.status),g.score.map(|v|v.to_string()).unwrap_or("null".into()),json(&g.note)));}else{s.push_str("null");}
|
||||
s.push_str(",\n \"roles\": [");for (i,role) in r.roles.iter().enumerate(){if i>0{s.push(',');}s.push_str(&format!("{{\"name\":{},\"verdict\":{},\"score\":{},\"reason\":{}}}",json(role.name),json(role.verdict),role.score.map(|v|v.to_string()).unwrap_or("null".into()),json(&role.reason)));}
|
||||
s.push_str("],\n \"concerns\": [");for (i,c) in r.concerns.iter().enumerate(){if i>0{s.push(',');}s.push_str(&json(c));}s.push_str("],\n \"installed_ram_gib\": ");
|
||||
s.push_str(&r.installed_ram_gib.map(|x|format!("{:.3}",x)).unwrap_or("null".into()));
|
||||
s.push_str(",\n \"technician_checks\": {");
|
||||
for (i,(name,value)) in r.checklist.iter().enumerate(){if i>0{s.push(',');}s.push_str(&format!("{}:{}",json(name),json(value)));}
|
||||
s.push_str("}\n}\n");s
|
||||
}
|
||||
fn html(r:&Report) -> String {
|
||||
let mut s=format!("<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><title>PC Intake {}</title><style>body{{font:16px system-ui;max-width:850px;margin:3rem auto;padding:0 1rem;line-height:1.5}}table{{border-collapse:collapse;width:100%}}th,td{{text-align:left;border-bottom:1px solid #ddd;padding:.5rem}}.warning{{padding:1rem;background:#fff2dd}}</style><h1>PC Intake: {}</h1><p><strong>Scanner version:</strong> {}</p><p><strong>Release gate:</strong> {}</p><p>{} · {} · {:.1} GiB RAM</p><p><strong>CPU:</strong> {}</p><p><strong>OS:</strong> {} (kernel {})</p>",esc(&r.asset),esc(&r.asset),esc(VERSION),esc(r.health_gate),esc(&r.vendor),esc(&r.model),r.mem_gib,esc(&r.cpu),esc(&r.distro),esc(&r.kernel));
|
||||
s.push_str("<h2>Disk health (native NVMe SMART where available)</h2><table><tr><th>Device</th><th>Capacity</th><th>Type</th><th>Health</th></tr>");for d in &r.disks{s.push_str(&format!("<tr><td>{}</td><td>{} GiB</td><td>{}</td><td>{}: {}</td></tr>",esc(&d.name),d.gib,if d.rotational==Some(true){"HDD"}else if d.rotational==Some(false){"SSD / non-rotating"}else{"Unknown"},esc(&d.health),esc(&d.smart_details)));}s.push_str("</table><h2>Graphics devices</h2><ul>");for g in &r.gpus{s.push_str(&format!("<li>{}</li>",esc(g)));}s.push_str("</ul><h2>Batteries</h2><ul>");for b in &r.batteries{s.push_str(&format!("<li>{}: charge {:?}%, capacity vs design {:?}%, status {}</li>",esc(&b.name),b.capacity_pct,b.health_pct,esc(&b.status)));}s.push_str("</ul><h2>CPU proxy benchmark</h2>");if let Some(b)=&r.bench{s.push_str(&format!("<p>Integer mix v1 median of five runs: single {:.2} ms, parallel {:.2} ms ({} threads). Lower is better; this is not an application benchmark.</p>",b.single_ms,b.multi_ms,b.threads));}else{s.push_str("<p>Not run (--quick)</p>");}
|
||||
s.push_str("<h2>Memory bandwidth (single-thread read proxy)</h2>");
|
||||
if let Some(b)=&r.memory_bench {s.push_str(&format!("<p>{:.0} MiB/s median of 5 runs reading {} MiB of RAM. Higher is better; not a full memory bandwidth test.</p>",b.median_mib_s,b.buffer_mib));}else{s.push_str("<p>Not run (--quick).</p>");}
|
||||
s.push_str("<h2>Developer workload (source-indexing proxy)</h2>");
|
||||
if let Some(d)=&r.developer {s.push_str(&format!("<p>{} generated source records scanned in {:.2} ms (median of 3). Not a compilation or IDE benchmark.</p>",d.records,d.median_ms));}else{s.push_str("<p>Not run (--quick).</p>");}
|
||||
s.push_str("<h2>On-screen playback check (optional)</h2>");
|
||||
if let Some(v)=&r.playback {s.push_str(&format!("<p>{} in {:.1}s. {}</p>",esc(&v.status),v.elapsed_seconds,esc(&v.note)));}else{s.push_str("<p>Not requested (--playback with --video-file PATH).</p>");}
|
||||
s.push_str("<h2>Real media decode (optional)</h2>");
|
||||
if let Some(v)=&r.video {s.push_str(&format!("<p>Status: {}. File: {}. Codec: {}. Resolution: {}x{}. {} frames in {:.2}s = {:.1} decode FPS ({:.2}x nominal real time). {}</p>",esc(&v.status),esc(&v.source),esc(&v.codec),v.width,v.height,v.frames,v.seconds,v.fps,v.realtime_factor,esc(&v.note)));}else{s.push_str("<p>Not requested (--video-file PATH).</p>");}
|
||||
s.push_str("<h2>OpenGL benchmark (optional)</h2>");
|
||||
if let Some(g)=&r.graphics {s.push_str(&format!("<p>{}, score: {:?}. {}</p>",esc(&g.status),g.score,esc(&g.note)));}else{s.push_str("<p>Not requested (--graphics).</p>");}
|
||||
s.push_str("<h2>Optional real C compilation benchmark</h2>");
|
||||
if let Some(c)=&r.compilation {s.push_str(&format!("<p>Status: {}. Compiler: {}. {} functions, median {:.2} ms over {} runs. Lower is better. Compiler version and system load affect comparability.</p>",esc(&c.status),esc(&c.compiler),c.source_functions,c.median_ms,c.runs_ms.len()));} else {s.push_str("<p>Not requested (add --compile). An installed C compiler is required only for this optional test.</p>");}
|
||||
s.push_str(&format!("<h2>Memory</h2><p>Usable Linux RAM: {:.2} GiB. Installed DMI RAM: {}. Usable RAM is not the same as physically installed RAM.</p>",r.mem_gib,r.installed_ram_gib.map(|x|format!("{x:.2} GiB")).unwrap_or("UNKNOWN (DMI unavailable)".into())));
|
||||
s.push_str("<h2>Temperature sensors (snapshot)</h2><table><tr><th>Sensor</th><th>Temperature</th></tr>");
|
||||
for (name,temp) in &r.sensors {s.push_str(&format!("<tr><td>{}</td><td>{:.1} °C</td></tr>",esc(name),temp));}s.push_str("</table><p>A single idle snapshot is not a load/thermal-throttling test.</p>");
|
||||
s.push_str("<h2>Network interfaces</h2><table><tr><th>Interface</th><th>Link state</th></tr>");
|
||||
for (name,status) in &r.network {s.push_str(&format!("<tr><td>{}</td><td>{}</td></tr>",esc(name),esc(status)));}s.push_str("</table>");
|
||||
s.push_str("<h2>Technician checklist</h2><table><tr><th>Check</th><th>Status</th></tr>");for (name,value) in &r.checklist{s.push_str(&format!("<tr><td>{}</td><td>{}</td></tr>",esc(name),esc(value)));}s.push_str("</table>");
|
||||
s.push_str("<h2>Provisional role guidance</h2><p>Scores are uncalibrated workload proxies, not a decision to donate. Empty scores mean not assessed.</p><table><tr><th>Use</th><th>Assessment</th><th>Proxy score</th><th>Limitations</th></tr>");for role in &r.roles{s.push_str(&format!("<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>",esc(role.name),esc(role.verdict),role.score.map(|v|format!("{v}/100")).unwrap_or("—".into()),esc(&role.reason)));}s.push_str("</table><h2>Concerns</h2><div class=\"warning\"><ul>");for c in &r.concerns{s.push_str(&format!("<li>{}</li>",esc(c)));}s.push_str("</ul></div><p>READY_FOR_FINAL_APPROVAL still requires an authorized human decision.</p></html>");s
|
||||
}
|
||||
fn csv_report(r:&Report) -> String {
|
||||
let single=r.bench.as_ref().map(|b|format!("{:.4}",b.single_ms)).unwrap_or_default();
|
||||
let multi=r.bench.as_ref().map(|b|format!("{:.4}",b.multi_ms)).unwrap_or_default();
|
||||
let disks=r.disks.iter().map(|d|format!("{}:{}GiB",d.name,d.gib)).collect::<Vec<_>>().join(";");
|
||||
let gpus=r.gpus.join(";");
|
||||
let concerns=r.concerns.join(";");
|
||||
format!("tool_version,asset_id,timestamp_unix,manufacturer,model,cpu,ram_gib,disks,gpus,cpu_single_ms,cpu_parallel_ms,health_gate,concerns,installed_ram_gib,technician_failed,technician_unknown,developer_index_ms,cpu_single_variation_pct,memory_read_mib_s,office_proxy_score,developer_proxy_score,compile_median_ms,compile_status\n{}\n",[csv(VERSION),csv(&r.asset),r.timestamp.to_string(),csv(&r.vendor),csv(&r.model),csv(&r.cpu),format!("{:.3}",r.mem_gib),csv(&disks),csv(&gpus),single,multi,csv(r.health_gate),csv(&concerns),r.installed_ram_gib.map(|v|format!("{v:.3}")).unwrap_or_default(),r.checklist.iter().filter(|(_,s)|s=="FAIL").count().to_string(),r.checklist.iter().filter(|(_,s)|s=="UNKNOWN").count().to_string(),r.developer.as_ref().map(|d|format!("{:.4}",d.median_ms)).unwrap_or_default(),r.bench.as_ref().map(|b|format!("{:.1}",cpu_variation_pct(b))).unwrap_or_default(),r.memory_bench.as_ref().map(|b|format!("{:.2}",b.median_mib_s)).unwrap_or_default(),r.roles.iter().find(|x|x.name=="Office").and_then(|x|x.score).map(|v|v.to_string()).unwrap_or_default(),r.roles.iter().find(|x|x.name=="Developer").and_then(|x|x.score).map(|v|v.to_string()).unwrap_or_default(),r.compilation.as_ref().filter(|c|c.status=="PASS").map(|c|format!("{:.3}",c.median_ms)).unwrap_or_default(),csv(r.compilation.as_ref().map(|c|c.status.as_str()).unwrap_or("NOT_REQUESTED"))].join(","))
|
||||
}
|
||||
fn tool_status(name: &str) -> String {
|
||||
let found = env::var_os("PATH").and_then(|paths| {
|
||||
env::split_paths(&paths).find_map(|dir| {
|
||||
let candidate=dir.join(name);
|
||||
if candidate.is_file() { Some(candidate) } else { None }
|
||||
})
|
||||
});
|
||||
match found {Some(p)=>format!("FOUND ({})",p.display()),None=>"NOT FOUND".into()}
|
||||
}
|
||||
fn self_check() {
|
||||
println!("PC Intake v{VERSION} — environment diagnostics");
|
||||
println!("Platform: {} / {}", env::consts::OS, env::consts::ARCH);
|
||||
println!("DMI access: {}", if Path::new("/sys/class/dmi/id").exists(){"available"}else{"unavailable"});
|
||||
println!("Block devices: {}", if Path::new("/sys/block").exists(){"available"}else{"unavailable"});
|
||||
for (tool, purpose) in [("smartctl","SATA/USB SMART"),("ffmpeg","video decoding"),("mpv","on-screen playback"),("glmark2","OpenGL benchmark"),("cc","C compilation"),("gcc","C compilation"),("clang","C compilation")] {
|
||||
println!("{tool}: {} — {purpose}", tool_status(tool));
|
||||
}
|
||||
println!("NOTE: FOUND does not prove a utility works or has permissions to access the device.");
|
||||
println!("Run the binary from this release, not an older ./pc-intake elsewhere.");
|
||||
}
|
||||
fn usage() {println!("pc-intake v{VERSION}\nUsage: pc-intake --asset-id ID [--quick] [--output DIR] [--checklist FILE] [--compile] [--video-file PATH] [--playback] [--graphics]\n pc-intake --version | --self-check | --help\nOffline Linux intake; no third-party runtime dependencies.\n--quick: skip CPU work; disk SMART collection still attempted. No GPU/game or video playback benchmark is performed.\n--compile: opt-in C compilation test; requires cc/gcc/clang installed.");}
|
||||
fn run() -> Result<(),String> {
|
||||
let mut args=env::args().skip(1);let mut id=None;let mut out=PathBuf::from("reports");let mut quick=false;let mut do_compile=false;let mut do_graphics=false;let mut do_playback=false;let mut video_path:Option<PathBuf>=None;let mut checks_path:Option<PathBuf>=None;
|
||||
while let Some(a)=args.next(){match a.as_str(){"--asset-id"=>id=Some(args.next().ok_or("--asset-id needs a value")?),"--output"=>out=PathBuf::from(args.next().ok_or("--output needs a directory")?),"--quick"=>quick=true,"--compile"=>do_compile=true,"--graphics"=>do_graphics=true,"--playback"=>do_playback=true,"--video-file"=>video_path=Some(PathBuf::from(args.next().ok_or("--video-file needs a file")?)),"--checklist"=>checks_path=Some(PathBuf::from(args.next().ok_or("--checklist needs a file")?)),"--version"|"-V"=>{println!("pc-intake v{VERSION}");return Ok(());},"--self-check"=>{self_check();return Ok(());},"--help"|"-h"=>{usage();return Ok(());},_=>return Err(format!("Unknown argument: {a}"))}}
|
||||
if do_playback && video_path.is_none() {return Err("--playback requires --video-file PATH".into());}
|
||||
let id=id.ok_or("Provide --asset-id (for example NPC-0042)")?;
|
||||
if id.is_empty()||id.len()>64||!id.chars().all(|c|c.is_ascii_alphanumeric()||c=='-'||c=='_') {return Err("Asset ID must be 1-64 ASCII letters, digits, '-' or '_'".into());}
|
||||
let checks=checklist(checks_path.as_deref())?;let report=collect(id.clone(),quick,do_compile,video_path.as_deref(),do_graphics,do_playback,checks);let folder=out.join(&id).join(format!("{}-{}",report.timestamp,process::id()));
|
||||
fs::create_dir_all(&folder).map_err(|e|e.to_string())?;
|
||||
fs::write(folder.join("report.json"),json_report(&report)).map_err(|e|e.to_string())?;
|
||||
fs::write(folder.join("report.html"),html(&report)).map_err(|e|e.to_string())?;
|
||||
fs::write(folder.join("summary.csv"),csv_report(&report)).map_err(|e|e.to_string())?;
|
||||
println!("PC Intake v{VERSION}");
|
||||
println!("Asset: {} | CPU: {} | RAM {:.1} GiB",id,report.cpu,report.mem_gib);
|
||||
for d in &report.disks { println!("Disk {} ({} GiB): SMART {} — {}",d.name,d.gib,d.health,d.smart_details); }
|
||||
println!("Release: {}",report.health_gate);
|
||||
println!("Installed RAM (DMI): {}",report.installed_ram_gib.map(|x|format!("{x:.2} GiB")).unwrap_or("UNKNOWN".into()));
|
||||
println!("Reports: {}",folder.display());Ok(())
|
||||
}
|
||||
fn main(){if !cfg!(target_os="linux"){eprintln!("Warning: Linux-only hardware inventory");}if let Err(e)=run(){eprintln!("Error: {e}");usage();process::exit(2)}}
|
||||
#[cfg(test)] mod tests {use super::*;#[test]fn escapes(){assert_eq!(json("x\"\\\n"),"\"x\\\"\\\\\\n\"");assert_eq!(esc("<x&"),"<x&");}#[test]fn tests_role_gates(){let roles=grade(10.0,40.0,8);assert_eq!(roles[2].verdict,"NOT_ASSESSED");assert!(roles[0].score.is_some());assert_eq!(roles[5].verdict,"NOT_ASSESSED");}#[test]fn checklist_defaults_unknown(){let c=checklist(None).unwrap();assert_eq!(c.len(),11);assert!(c.iter().all(|(_,v)|v=="UNKNOWN"));}#[test]fn cpu_deterministic(){assert_eq!(cpu_work(100),cpu_work(100));}
|
||||
#[test]fn source_index_deterministic(){assert_eq!(source_index_work(200),source_index_work(200));}
|
||||
#[test]fn source_index_nonempty(){assert_ne!(source_index_work(200),0);}}
|
||||
|
||||
#[cfg(test)] mod new_tests {
|
||||
use super::*;
|
||||
#[test] fn score_anchors_are_bounded(){assert_eq!(lower_is_better(12.0,12.0,85.0),100);assert_eq!(lower_is_better(85.0,12.0,85.0),0);assert_eq!(higher_is_better(1500.0,1500.0,20000.0),0);assert_eq!(higher_is_better(20000.0,1500.0,20000.0),100);}
|
||||
#[test] fn quick_mode_never_scores(){assert!(grade_with_evidence(None,None,None).iter().all(|r|r.score.is_none()));}
|
||||
#[test] fn unmeasured_gpu_roles_have_no_score(){let b=Bench{single_ms:20.0,multi_ms:40.0,threads:8,single_runs_ms:vec![],parallel_runs_ms:vec![]};let r=grade_with_evidence(Some(&b),None,None);assert!(r.iter().filter(|x|matches!(x.name,"Gaming"|"Multimedia playback"|"Video creation")).all(|x|x.score.is_none()));}
|
||||
#[test] fn median_works(){let mut x=[7.0,1.0,4.0,3.0,5.0];assert_eq!(median(&mut x),4.0);}
|
||||
#[test] fn no_false_sata_pass(){let d=vec![Disk{name:"sda".into(),gib:128,rotational:Some(false),removable:false,model:"sample".into(),health:"UNKNOWN".into(),smart_details:String::new()}];assert_eq!(d[0].health,"UNKNOWN");}
|
||||
}
|
||||
|
||||
#[cfg(test)] mod feature_tests {
|
||||
use super::*;
|
||||
#[test] fn temperatures_are_reasonable(){assert!(temperatures().iter().all(|(_,t)|*t>=0.0&&*t<=180.0));}
|
||||
#[test] fn network_excludes_loopback(){assert!(!network_devices().iter().any(|(name,_)|name=="lo"));}
|
||||
}
|
||||
|
||||
#[cfg(test)] mod v07_tests {
|
||||
use super::*;
|
||||
#[test] fn memory_work_has_data() { let m=memory_benchmark(); assert_eq!(m.runs_mib_s.len(),5); assert!(m.median_mib_s>0.0); assert_eq!(m.buffer_mib,64); }
|
||||
#[test] fn physical_memory_is_not_usable_memory() { let r=mem_gib(); assert!(r>=0.0); }
|
||||
}
|
||||
|
||||
#[cfg(test)] mod workflow_compatibility_tests {
|
||||
use super::*;
|
||||
#[test] fn accepts_gui_statuses_without_treating_unavailable_as_failure() {
|
||||
let path=std::env::temp_dir().join(format!("csr-checklist-{}-{}",process::id(),SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()));
|
||||
fs::write(&path,"display=PASS\nkeyboard=FAIL\ntouchpad=NOT_APPLICABLE\nwifi=SETUP_PROBLEM\nmicrophone=UNAVAILABLE\nwebcam=NOT_TESTED\n").unwrap();
|
||||
let checks=checklist(Some(&path)).unwrap();fs::remove_file(path).unwrap();
|
||||
let get=|key:&str|checks.iter().find(|(k,_)|k==key).unwrap().1.as_str();
|
||||
assert_eq!(get("display"),"PASS");assert_eq!(get("keyboard"),"FAIL");assert_eq!(get("touchpad"),"NOT_APPLICABLE");assert_eq!(get("wifi"),"UNKNOWN");assert_eq!(get("microphone"),"UNKNOWN");assert_eq!(get("webcam"),"UNKNOWN");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user