This commit is contained in:
7
controller/Cargo.lock
generated
Normal file
7
controller/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 = "intake-controller"
|
||||
version = "1.7.0"
|
||||
5
controller/Cargo.toml
Normal file
5
controller/Cargo.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[package]
|
||||
name = "intake-controller"
|
||||
version = "1.7.0"
|
||||
edition = "2021"
|
||||
[dependencies]
|
||||
194
controller/src/extended.rs
Normal file
194
controller/src/extended.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
//! Optional technician workstations; no write access to internal devices.
|
||||
use std::{fs, io::{self, Write}, path::Path, process::Command, time::{SystemTime, UNIX_EPOCH}};
|
||||
|
||||
fn ask(prompt: &str) -> String {
|
||||
print!("{}", prompt);
|
||||
let _ = io::stdout().flush();
|
||||
let mut answer = String::new();
|
||||
if io::stdin().read_line(&mut answer).is_err() { return String::new(); }
|
||||
answer.trim().to_string()
|
||||
}
|
||||
fn read_trim(path: &str) -> Option<String> {
|
||||
fs::read_to_string(path).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
|
||||
}
|
||||
fn output(exe: &str, args: &[&str]) -> Option<String> {
|
||||
let r = Command::new(exe).args(args).output().ok()?;
|
||||
if r.status.success() { Some(String::from_utf8_lossy(&r.stdout).trim().to_owned()) } else { None }
|
||||
}
|
||||
fn escape_json(s: &str) -> String {
|
||||
let mut result = String::from("\"");
|
||||
for c in s.chars() { match c {
|
||||
'"' => result.push_str("\\\""), '\\' => result.push_str("\\\\"),
|
||||
'\n' => result.push_str("\\n"), '\r' => result.push_str("\\r"), '\t' => result.push_str("\\t"),
|
||||
c if c.is_control() => result.push_str(&format!("\\u{:04x}", c as u32)),
|
||||
c => result.push(c),
|
||||
}}
|
||||
result.push('"'); result
|
||||
}
|
||||
fn valid_asset(id: &str) -> bool {
|
||||
!id.is_empty() && id.len() <= 64 && id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
|
||||
}
|
||||
fn normalize_hostname(model: &str, asset: &str) -> String {
|
||||
let source = format!("{}-{}", model, asset).to_ascii_lowercase();
|
||||
let mut result = String::new();
|
||||
let mut last_dash = false;
|
||||
for c in source.chars() {
|
||||
if c.is_ascii_alphanumeric() { result.push(c); last_dash = false; }
|
||||
else if !last_dash && !result.is_empty() { result.push('-'); last_dash = true; }
|
||||
}
|
||||
result.trim_matches('-').chars().take(63).collect::<String>().trim_end_matches('-').to_owned()
|
||||
}
|
||||
fn net_interfaces() -> Vec<(String, String, String, String)> {
|
||||
let mut list = Vec::new();
|
||||
if let Ok(entries) = fs::read_dir("/sys/class/net") {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if name == "lo" { continue; }
|
||||
let base = format!("/sys/class/net/{name}");
|
||||
let kind = if Path::new(&format!("{base}/wireless")).exists() { "Wi-Fi" } else { "LAN/other" };
|
||||
let carrier = read_trim(&format!("{base}/carrier")).unwrap_or_else(|| "unknown".into());
|
||||
let mac = read_trim(&format!("{base}/address")).unwrap_or_else(|| "unknown".into());
|
||||
list.push((name, kind.to_string(), carrier, mac));
|
||||
}
|
||||
}
|
||||
list.sort(); list
|
||||
}
|
||||
fn ethernet_connected() -> bool { net_interfaces().iter().any(|(_,kind,carrier,_)| kind == "LAN/other" && carrier == "1") }
|
||||
fn memory_dmi() -> (String, String) {
|
||||
let text = match output("dmidecode", &["-t", "memory"]) {
|
||||
Some(s) => s, None => return ("UNKNOWN (dmidecode unavailable or permissions missing)".into(), "UNKNOWN".into()),
|
||||
};
|
||||
let mut slots = 0usize;
|
||||
let mut occupied = 0usize;
|
||||
let mut max_cap = String::from("UNKNOWN");
|
||||
let mut in_device = false;
|
||||
for line in text.lines() {
|
||||
let t = line.trim();
|
||||
if t == "Memory Device" { slots += 1; in_device = true; continue; }
|
||||
if t == "Physical Memory Array" { in_device = false; continue; }
|
||||
if t.starts_with("Maximum Capacity:") && max_cap == "UNKNOWN" { max_cap = t.trim_start_matches("Maximum Capacity:").trim().into(); }
|
||||
if in_device && t.starts_with("Size:") && !t.contains("No Module Installed") && !t.ends_with("Unknown") { occupied += 1; in_device = false; }
|
||||
}
|
||||
let summary = if slots == 0 { "UNKNOWN".into() } else { format!("{occupied}/{slots} occupied; {} free", slots.saturating_sub(occupied)) };
|
||||
(summary, max_cap)
|
||||
}
|
||||
fn display_info() -> String {
|
||||
let mut displays = Vec::new();
|
||||
if let Ok(entries) = fs::read_dir("/sys/class/drm") {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.join("status").is_file() { continue; }
|
||||
let status = fs::read_to_string(path.join("status")).unwrap_or_default();
|
||||
if status.trim() != "connected" { continue; }
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let modes = fs::read_to_string(path.join("modes")).unwrap_or_default();
|
||||
let mode = modes.lines().next().unwrap_or("resolution unknown");
|
||||
let size = fs::read(path.join("edid")).ok().and_then(|e| {
|
||||
if e.len() >= 23 && e[21] > 0 && e[22] > 0 {
|
||||
let diag = ((e[21] as f64).powi(2) + (e[22] as f64).powi(2)).sqrt() / 2.54;
|
||||
Some(format!("{diag:.1} inch (EDID estimate)"))
|
||||
} else { None }
|
||||
}).unwrap_or_else(|| "size UNKNOWN".into());
|
||||
displays.push(format!("{name}: {mode}, {size}"));
|
||||
}
|
||||
}
|
||||
if displays.is_empty() { "UNKNOWN (no active DRM connector)".into() } else { displays.join("; ") }
|
||||
}
|
||||
fn save(data: &Path, asset: &str, stem: &str, content: &str) -> Result<(),String> {
|
||||
let folder = data.join("reports").join(asset).join("supplemental");
|
||||
fs::create_dir_all(&folder).map_err(|e| e.to_string())?;
|
||||
let path = folder.join(stem);
|
||||
fs::write(&path, content).map_err(|e|e.to_string())?;
|
||||
println!("Saved to {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
pub fn device_details(data: &Path) {
|
||||
let asset = ask("Asset ID / Geraetenummer: ");
|
||||
if !valid_asset(&asset) { println!("Invalid asset ID"); return; }
|
||||
let manufacturer = read_trim("/sys/class/dmi/id/sys_vendor").unwrap_or_else(|| "UNKNOWN".into());
|
||||
let model = read_trim("/sys/class/dmi/id/product_name").unwrap_or_else(|| "UNKNOWN".into());
|
||||
let uuid = read_trim("/sys/class/dmi/id/product_uuid").unwrap_or_else(|| "UNKNOWN".into());
|
||||
let cpu = read_trim("/proc/cpuinfo").and_then(|s|s.lines().find_map(|line|line.strip_prefix("model name").and_then(|v|v.split_once(':').map(|(_,v)|v.trim().to_owned())))).unwrap_or_else(|| "UNKNOWN".into());
|
||||
let (slots, max_ram) = memory_dmi();
|
||||
let display = display_info();
|
||||
let host = normalize_hostname(&model, &asset);
|
||||
let connections = net_interfaces();
|
||||
println!("\nManufacturer: {manufacturer}\nModel: {model}\nCPU: {cpu}\nUUID: {uuid}\nRAM slots: {slots}\nMax RAM (BIOS estimate): {max_ram}\nDisplay: {display}\nProposed hostname: {host}");
|
||||
for (name,kind,carrier,mac) in &connections { println!("Interface {name}: {kind}, carrier={carrier}, MAC={mac}"); }
|
||||
println!("Storage sizes and device type are collected by the main scanner; don't include the boot USB.");
|
||||
let mut json = format!("{{\"asset_id\":{},\"manufacturer\":{},\"model\":{},\"cpu\":{},\"uuid\":{},\"ram_slots\":{},\"ram_max\":{},\"display\":{},\"proposed_hostname\":{},\"network\":[",escape_json(&asset),escape_json(&manufacturer),escape_json(&model),escape_json(&cpu),escape_json(&uuid),escape_json(&slots),escape_json(&max_ram),escape_json(&display),escape_json(&host));
|
||||
for (i,(name,kind,carrier,mac)) in connections.iter().enumerate() {
|
||||
if i>0 { json.push(','); }
|
||||
json.push_str(&format!("{{\"interface\":{},\"type\":{},\"carrier\":{},\"mac\":{}}}",escape_json(name),escape_json(kind),escape_json(carrier),escape_json(mac)));
|
||||
}
|
||||
json.push_str("]}\n");
|
||||
if let Err(e)=save(data,&asset,"hardware-details.json",&json){ eprintln!("Save failed: {e}"); }
|
||||
}
|
||||
fn check_prompt(label: &str) -> &'static str {
|
||||
println!("{label}");
|
||||
loop { match ask("Result [p]ass / [f]ail / [u]ntested / [n]ot applicable: ").to_lowercase().as_str() {
|
||||
"p" => return "PASS", "f" => return "FAIL", "u" | "" => return "NOT_TESTED", "n" => return "NOT_APPLICABLE", _ => println!("Use p/f/u/n"),
|
||||
}}
|
||||
}
|
||||
pub fn guided_tests(data: &Path) {
|
||||
let asset=ask("Asset ID: "); if !valid_asset(&asset) { println!("Invalid asset ID"); return; }
|
||||
println!("\nNETWORK PRECHECK: Before Wi-Fi test, unplug Ethernet if one is connected.");
|
||||
if ethernet_connected() {
|
||||
println!("WARNING: Wired network link detected. Unplug LAN cable before Wi-Fi test.");
|
||||
let _=ask("After unplugging LAN press Enter to recheck...");
|
||||
if ethernet_connected() { println!("LAN link remains active: Wi-Fi connectivity cannot be confirmed until disconnected."); }
|
||||
} else { println!("No active wired link detected. Wireless testing may proceed."); }
|
||||
let wifi = if ethernet_connected() { "NOT_TESTED" } else {
|
||||
println!("Wi-Fi interfaces: {:?}",net_interfaces().iter().filter(|x|x.1=="Wi-Fi").map(|x|&x.0).collect::<Vec<_>>());
|
||||
println!("Connect using your network manager and verify actual traffic over Wi-Fi. Interface detection alone is not enough.");
|
||||
check_prompt("Wi-Fi connection and data transfer verified?")
|
||||
};
|
||||
println!("\nAUDIO: We'll attempt a stereo test sound if speaker-test exists. Confirm left/right audibly.");
|
||||
if ask("Run short speaker test? [y/N] ").eq_ignore_ascii_case("y") {
|
||||
let result=Command::new("speaker-test").args(["-c","2","-t","wav","-l","1"]).status();
|
||||
println!("speaker-test outcome: {:?} (not proof that sound was audible)",result);
|
||||
}
|
||||
let audio=check_prompt("Speakers: both channels audible, without distortion?");
|
||||
println!("\nMICROPHONE: make a short recording then play it back, confirming sound clarity.");
|
||||
let mic=check_prompt("Microphone recorded and replayed clearly?");
|
||||
println!("\nCAMERA: look for /dev/video*, and preview live picture if a desktop is running.");
|
||||
if let Ok(devices)=fs::read_dir("/dev") {
|
||||
for dev in devices.flatten() { let n=dev.file_name().to_string_lossy().to_string(); if n.starts_with("video") {println!("Found /dev/{n}");} }
|
||||
}
|
||||
let webcam=check_prompt("Webcam picture inspected and working?");
|
||||
let mut json=format!("{{\"asset_id\":{},\"timestamp\":{},\"wifi\":{},\"audio\":{},\"microphone\":{},\"webcam\":{}}}\n",escape_json(&asset),SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),escape_json(wifi),escape_json(audio),escape_json(mic),escape_json(webcam));
|
||||
if let Err(e)=save(data,&asset,"guided-tests.json",&json){eprintln!("Save failed: {e}");}
|
||||
json.clear();
|
||||
}
|
||||
pub fn maintenance_guidance() {
|
||||
println!("\nMAINTENANCE OF INSTALLED OPERATING SYSTEM");
|
||||
println!("The live USB does NOT automatically run updates on an internal disk. Only run maintenance after booting the installed system and confirming its Linux distribution.");
|
||||
println!("For Debian/Ubuntu/Mint installed systems:");
|
||||
println!(" sudo apt update && sudo apt upgrade && sudo apt autoremove && sudo apt clean");
|
||||
println!("For Arch Linux installed systems:");
|
||||
println!(" sudo pacman -Syu");
|
||||
println!("Do not chroot or rename another machine's OS without selecting and validating its target disk. Proposed hostname is stored in hardware-details.json for controlled provisioning.");
|
||||
let _=ask("Press Enter to return to menu...");
|
||||
}
|
||||
#[cfg(test)] mod tests {
|
||||
use super::*;
|
||||
#[test] fn valid_asset_ids(){assert!(valid_asset("PC-0042")); assert!(!valid_asset("../evil"));}
|
||||
#[test] fn hostnames(){assert_eq!(normalize_hostname("EliteBook 745 G6","PC-0001"),"elitebook-745-g6-pc-0001");}
|
||||
#[test] fn escape(){assert_eq!(escape_json("a\"b"),"\"a\\\"b\"");}
|
||||
}
|
||||
/// Read-only CPU stress plus allocated userspace memory checks. Not a full RAM test.
|
||||
pub fn stress_test(data: &Path) {
|
||||
let asset=ask("Asset ID: "); if !valid_asset(&asset) { println!("Invalid asset ID"); return; }
|
||||
println!("\nThis uses stress-ng (120 seconds) and memtester on up to 50% of MemAvailable, capped at 8 GiB.");
|
||||
println!("Do not use on a machine with unstable cooling. This cannot test all physical RAM or prove absence of faults.");
|
||||
if !ask("Run stress tests now? Type YES: ").eq("YES") {println!("Cancelled");return;}
|
||||
let cpu=Command::new("stress-ng").args(["--cpu","0","--timeout","120s","--metrics-brief"]).status();
|
||||
println!("CPU stress result: {cpu:?}");
|
||||
let mem_kib=read_trim("/proc/meminfo").and_then(|t|t.lines().find(|l|l.starts_with("MemAvailable:")).and_then(|line| line.split_whitespace().nth(1)).and_then(|n|n.parse::<u64>().ok())).unwrap_or(0);
|
||||
let mem_mib=std::cmp::min(mem_kib/2048,8192);
|
||||
let ram=if mem_mib>=128 { Command::new("memtester").args([&format!("{mem_mib}M"),"1"]).status() } else { println!("Not enough free RAM for safe test");return; };
|
||||
println!("RAM stress result: {ram:?}");
|
||||
let status=|r: &io::Result<std::process::ExitStatus>|match r {Ok(s) if s.success()=>"PASS",Ok(_)=>"FAIL",Err(_)=>"UNAVAILABLE"};
|
||||
let report=format!("{{\"asset_id\":{},\"cpu_stress\":{},\"ram_stress\":{},\"ram_test_mib\":{},\"coverage\":\"PARTIAL_USERSPACE\"}}\n",escape_json(&asset),escape_json(status(&cpu)),escape_json(status(&ram)),mem_mib);
|
||||
if let Err(e)=save(data,&asset,"stress-test.json",&report){eprintln!("Save failed: {e}");}
|
||||
}
|
||||
28
controller/src/main.rs
Normal file
28
controller/src/main.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
mod extended;
|
||||
use std::{env, fs, io::{self, Write}, path::{Path, PathBuf}, process::Command, time::{SystemTime, UNIX_EPOCH}};
|
||||
const DATA: &str = "/mnt/intake-data";
|
||||
fn prompt(q: &str) -> String { print!("{}",q); let _=io::stdout().flush(); let mut s=String::new(); let _=io::stdin().read_line(&mut s); s.trim().to_string() }
|
||||
fn run(program:&str,args:&[&str])->Result<(),String>{let st=Command::new(program).args(args).status().map_err(|e|format!("{}: {}",program,e))?;if st.success(){Ok(())}else{Err(format!("{} exited with {}",program,st))}}
|
||||
fn capture(program:&str,args:&[&str])->Result<String,String>{let o=Command::new(program).args(args).output().map_err(|e|e.to_string())?;if !o.status.success(){return Err(String::from_utf8_lossy(&o.stderr).to_string())}Ok(String::from_utf8_lossy(&o.stdout).trim().to_string())}
|
||||
fn sqlquote(s:&str)->String{format!("'{}'",s.replace('\'',"''"))}
|
||||
fn db_path(data:&Path)->PathBuf{data.join("database/intake.sqlite3")}
|
||||
fn sql(data:&Path,statement:&str)->Result<String,String>{capture("sqlite3",&[db_path(data).to_str().ok_or("Invalid DB path")?,statement])}
|
||||
fn init(data:&Path)->Result<(),String>{for part in ["database","reports","checklists","config"]{fs::create_dir_all(data.join(part)).map_err(|e|e.to_string())?;}let probe=data.join(".write-test");fs::write(&probe,b"ok").map_err(|e|format!("Persistent partition not writable: {}",e))?;let _=fs::remove_file(probe);sql(data,"PRAGMA journal_mode=DELETE; CREATE TABLE IF NOT EXISTS scans (id INTEGER PRIMARY KEY AUTOINCREMENT, asset_id TEXT NOT NULL, timestamp INTEGER NOT NULL, report_path TEXT NOT NULL UNIQUE, inspection_path TEXT, inspection_status TEXT NOT NULL DEFAULT 'NOT_TESTED', sync_status TEXT NOT NULL DEFAULT 'PENDING', synced_inventory_id TEXT);")?;Ok(())}
|
||||
fn valid_id(s:&str)->bool{!s.is_empty()&&s.len()<=64&&s.bytes().all(|b|b.is_ascii_alphanumeric()||b==b'-'||b==b'_')}
|
||||
fn latest(data:&Path,id:&str)->Option<PathBuf>{let root=data.join("reports").join(id);let mut matches=Vec::new();for d in fs::read_dir(root).ok()?.flatten(){let p=d.path().join("report.json");if p.is_file(){let mt=p.metadata().ok()?.modified().ok()?;matches.push((mt,p));}}matches.sort_by_key(|(m,_)|*m);matches.pop().map(|(_,p)|p)}
|
||||
fn get_time()->u64{SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()}
|
||||
fn scan(data:&Path,quick:bool){let id=prompt("Asset ID: ");if !valid_id(&id){println!("Invalid ID (letters, digits, underscore and hyphen only)");return;}let out=data.join("reports");let outstr=out.to_string_lossy().to_string();let mut cmd=Command::new("/usr/local/bin/pc-intake");cmd.args(["--asset-id",&id,"--output",&outstr]);if quick{cmd.arg("--quick");}else{cmd.arg("--compile");}let checklist=data.join("checklists").join(format!("{}.txt",id));if checklist.exists(){cmd.args(["--checklist",&checklist.to_string_lossy()]);}println!("Running {} scan...",if quick{"quick"}else{"full"});match cmd.status(){Ok(status) if status.success()=>{if let Some(report)=latest(data,&id){let result=sql(data,&format!("INSERT OR IGNORE INTO scans(asset_id,timestamp,report_path) VALUES({},{},{});",sqlquote(&id),get_time(),sqlquote(&report.to_string_lossy())));match result{Ok(_)=>{println!("Saved: {}",report.display());println!("Inventory sync: PENDING (not uploaded)");},Err(e)=>println!("Database write failed: {}. JSON report remains saved.",e)}}else{println!("Scanner exited successfully, but no report found");}},Ok(status)=>println!("Scanner failed: {}",status),Err(e)=>println!("Could not launch scanner: {}",e)}}
|
||||
const CHECKS:&[(&str,&str)]=&[("display","Show colors/brightness and inspect for dead pixels"),("keyboard","Check every key, including modifiers and function keys"),("touchpad","Verify movement, taps, buttons and scrolling; N/A for desktop"),("wifi","Connect to a network and open a local or remote test service"),("audio","Play left/right sound and check for distortion"),("microphone","Record voice and play it back"),("webcam","Preview image and verify focus and quality; N/A if absent"),("usb_ports","Insert a known-good test device in each physical port"),("charging","Verify AC adapter, charging status, battery runtime; N/A for desktop"),("thermals","Verify fans, heat and sustained load"),("stability","Check for crashes, unexpected resets or kernel errors")];
|
||||
fn technician(data:&Path){let id=prompt("Asset ID for inspection: ");if !valid_id(&id){println!("Invalid ID");return;}let mut rows=Vec::new();for (key,desc) in CHECKS{println!("\n{}: {}",key,desc);if *key=="audio"{println!("Optional speaker test (if installed): speaker-test -c 2 -t wav");}if *key=="webcam"{println!("Optional camera detection: ls /dev/video*; preview requires graphical session");}loop{let ans=prompt("[p]ass / [f]ail / [u]ntested / [n]ot applicable: ").to_lowercase();if let Some(st)=match ans.as_str(){"p"=>Some("PASS"),"f"=>Some("FAIL"),"u"|""=>Some("NOT_TESTED"),"n"=>Some("NOT_APPLICABLE"),_=>None}{rows.push((key.to_string(),st.to_string()));break;}println!("Enter p, f, u or n");}}
|
||||
let target=data.join("checklists").join(format!("{}.txt",id));let mut raw=String::new();for (k,v) in &rows{raw.push_str(&format!("{}={}\n",k,v));}if let Err(e)=fs::write(&target,raw){println!("Could not save checklist: {}",e);return;}let fail=rows.iter().any(|(_,s)|s=="FAIL");let unknown=rows.iter().any(|(_,s)|s=="NOT_TESTED");let verdict=if fail{"FAIL"}else if unknown{"INCOMPLETE"}else{"COMPLETED"};let _=sql(data,&format!("UPDATE scans SET inspection_path={},inspection_status={} WHERE id=(SELECT MAX(id) FROM scans WHERE asset_id={});",sqlquote(&target.to_string_lossy()),sqlquote(verdict),sqlquote(&id)));println!("Inspection {}. Saved {}; re-scan to embed checklist in hardware report.",verdict,target.display());}
|
||||
fn recent(data: &Path) {
|
||||
let query = "SELECT id || ' | ' || asset_id || ' | ' || inspection_status || ' | ' || sync_status || ' | ' || report_path FROM scans ORDER BY id DESC LIMIT 20;";
|
||||
match sql(data, query) {
|
||||
Ok(rows) if rows.is_empty() => println!("No scans yet."),
|
||||
Ok(rows) => println!("{}", rows),
|
||||
Err(err) => println!("Database error: {}", err),
|
||||
}
|
||||
}
|
||||
fn help(){println!("PC Intake AIO v1.7.0 — offline-first technician controller");println!("Usage: intake-controller [--data PATH] [--self-check]");}
|
||||
fn main(){let args:Vec<String>=env::args().collect();if args.iter().any(|x|x=="--help"){help();return;}if args.iter().any(|x|x=="--self-check"){for tool in ["sqlite3","smartctl","ffmpeg","mpv","glmark2","stress-ng","memtester","speaker-test"]{println!("{}: {}",tool,if Command::new("sh").args(["-c",&format!("command -v {} >/dev/null",tool)]).status().map(|s|s.success()).unwrap_or(false){"FOUND"}else{"MISSING"});}return;}let data=if let Some(p)=args.iter().position(|x|x=="--data"){PathBuf::from(args.get(p+1).unwrap_or(&DATA.to_owned()))}else{PathBuf::from(DATA)};if let Err(e)=init(&data){eprintln!("FATAL: {} — refusing to scan without persistent storage",e);std::process::exit(1)}loop{println!("\n==== PC INTAKE AIO v1.7.0 ====\nStorage: {}\n1 Full scan and local queue\n2 Quick inventory\n3 Guided technician checklist\n4 Recent scans and sync states\n5 Check diagnostic tools\n6 Safely power off\n7 Hardware details (UUID / MAC / RAM slots / screen)\n8 Guided audio / webcam / Wi-Fi test\n9 Installed-system maintenance instructions\n10 CPU and RAM stress test (opt-in)\n",data.display());match prompt("Select: ").as_str(){"1"=>scan(&data,false),"2"=>scan(&data,true),"3"=>technician(&data),"4"=>recent(&data),"5"=>{let _=Command::new(env::current_exe().unwrap_or_else(|_|PathBuf::from("intake-controller"))).arg("--self-check").status();},"6"=>{let _=run("sync",&[]);let _=run("systemctl",&["poweroff"]);break;},"7"=>extended::device_details(&data),"8"=>extended::guided_tests(&data),"9"=>extended::maintenance_guidance(),"10"=>extended::stress_test(&data),_=>println!("Unknown selection")}}}
|
||||
#[cfg(test)]mod tests{use super::*;#[test]fn id_validation(){assert!(valid_id("PC-0123"));assert!(!valid_id("../../evil"));}#[test]fn sql_escape(){assert_eq!(sqlquote("O'Reilly"),"'O''Reilly'");}}
|
||||
Reference in New Issue
Block a user