use anyhow::{Context, Result}; use std::fs; /// Extracts a unique hardware identifier from the system /// This function reads /proc/cpuinfo to find a stable CPU serial number pub fn get_hardware_id() -> Result { // Try to read /proc/cpuinfo first (common on Raspberry Pi and other ARM systems) if let Ok(hardware_id) = read_cpu_serial() { return Ok(hardware_id); } // Fallback: try to read machine-id if let Ok(machine_id) = read_machine_id() { return Ok(machine_id); } // Last resort: generate a warning and use hostname + MAC address hash eprintln!("Warning: Could not read CPU serial or machine-id, using fallback method"); get_fallback_id() } /// Reads CPU serial number from /proc/cpuinfo /// This is the most reliable method on Raspberry Pi systems fn read_cpu_serial() -> Result { let cpuinfo = fs::read_to_string("/proc/cpuinfo") .context("Failed to read /proc/cpuinfo")?; for line in cpuinfo.lines() { if line.starts_with("Serial") { if let Some(serial) = line.split(':').nth(1) { let serial = serial.trim(); if !serial.is_empty() && serial != "0000000000000000" { return Ok(format!("CPU_{}", serial)); } } } } anyhow::bail!("No valid CPU serial found in /proc/cpuinfo") } /// Reads machine ID from /etc/machine-id (systemd systems) fn read_machine_id() -> Result { let machine_id = fs::read_to_string("/etc/machine-id") .context("Failed to read /etc/machine-id")?; let machine_id = machine_id.trim(); if machine_id.len() >= 8 { Ok(format!("MACHINE_{}", &machine_id[..16])) } else { anyhow::bail!("Invalid machine-id format") } } /// Fallback method to generate a hardware ID /// Uses hostname + network interface information fn get_fallback_id() -> Result { use std::process::Command; // Get hostname let hostname_output = Command::new("hostname") .output() .context("Failed to execute hostname command")?; let hostname = String::from_utf8_lossy(&hostname_output.stdout) .trim() .to_string(); // Try to get MAC address from network interfaces if let Ok(mac) = get_primary_mac_address() { return Ok(format!("FALLBACK_{}_{}", hostname, mac)); } // Very last resort: just use hostname with timestamp let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs(); Ok(format!("FALLBACK_{}_{}", hostname, timestamp)) } /// Attempts to get the MAC address of the primary network interface fn get_primary_mac_address() -> Result { let interfaces_dir = "/sys/class/net"; let entries = fs::read_dir(interfaces_dir) .context("Failed to read network interfaces directory")?; for entry in entries { let entry = entry?; let interface_name = entry.file_name(); let interface_name = interface_name.to_string_lossy(); // Skip loopback and common virtual interfaces if interface_name == "lo" || interface_name.starts_with("docker") || interface_name.starts_with("veth") { continue; } let mac_path = format!("{}/{}/address", interfaces_dir, interface_name); if let Ok(mac) = fs::read_to_string(&mac_path) { let mac = mac.trim().replace(':', ""); if mac.len() == 12 && mac != "000000000000" { return Ok(mac.to_uppercase()); } } } anyhow::bail!("No valid MAC address found") } #[cfg(test)] mod tests { use super::*; #[test] fn test_hardware_id_format() { // Test that we can get some kind of hardware ID // The exact format will depend on the system, but it should not be empty let result = get_hardware_id(); match result { Ok(id) => { assert!(!id.is_empty(), "Hardware ID should not be empty"); assert!(id.len() >= 8, "Hardware ID should be at least 8 characters"); println!("Hardware ID: {}", id); } Err(e) => { println!("Could not get hardware ID: {}", e); // On systems without the expected files, this might fail // That's okay for testing purposes } } } #[test] fn test_fallback_id() { let result = get_fallback_id(); assert!(result.is_ok(), "Fallback ID generation should always work"); let id = result.unwrap(); assert!(id.starts_with("FALLBACK_"), "Fallback ID should have correct prefix"); assert!(id.len() > 10, "Fallback ID should be reasonably long"); } }