diff --git a/Cargo.lock b/Cargo.lock index 1ad2844..32f8788 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,9 @@ name = "anyhow" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +dependencies = [ + "backtrace", +] [[package]] name = "arbitrary" @@ -2137,13 +2140,16 @@ dependencies = [ name = "hardwareinfo" version = "0.1.0" dependencies = [ + "anyhow", "core-foundation", "directories", + "glob", "indexmap 2.3.0", "libc", "log", "netdev", "nvml-wrapper", + "regex", "serde", "serde_json", "starship-battery", diff --git a/platforms/unix/hardwareinfo/Cargo.toml b/platforms/unix/hardwareinfo/Cargo.toml index 8f482a2..c17b45e 100644 --- a/platforms/unix/hardwareinfo/Cargo.toml +++ b/platforms/unix/hardwareinfo/Cargo.toml @@ -18,3 +18,8 @@ directories = "4.0.1" [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9.4" libc = "0.2.155" + +[target.'cfg(target_os = "linux")'.dependencies] +anyhow = { version = "1.0.86", features = ["backtrace"] } +regex = "1.10.5" +glob = "0.3.1" diff --git a/platforms/unix/hardwareinfo/src/lib.rs b/platforms/unix/hardwareinfo/src/lib.rs index 5550b51..5f871e6 100644 --- a/platforms/unix/hardwareinfo/src/lib.rs +++ b/platforms/unix/hardwareinfo/src/lib.rs @@ -12,6 +12,8 @@ use std::{ pub use nvml_wrapper::Nvml; pub use sysinfo::{Components, Disks, Networks, System, MINIMUM_CPU_UPDATE_INTERVAL}; +#[cfg(target_os = "linux")] +pub mod linux; #[cfg(target_os = "macos")] pub mod mac; pub mod settings; @@ -86,7 +88,8 @@ pub struct CoresGPU { pub struct CoresRAMInfo { pub manufacturer_name: String, pub configured_speed: u32, - // TODO + pub configured_voltage: f32, + pub size: u64, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -103,7 +106,7 @@ pub struct CoresOS { #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] -pub struct CoresDisks { +pub struct CoresDisk { pub name: String, pub total_space: u64, pub free_space: u64, @@ -115,7 +118,7 @@ pub struct CoresDisks { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct CoresStorage { - pub disks: Vec, + pub disks: Vec, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -312,14 +315,14 @@ pub fn refresh_hardware_info(data: &mut Data) { } // RAM Info - let total_memory = data.sys.total_memory() as f64 / gb; - let used_memory = data.sys.used_memory() as f64 / gb; - let total_swap = data.sys.total_swap() as f64 / gb; - let used_swap = data.sys.used_swap() as f64 / gb; - let ram_used = (used_memory / total_memory) * 100.0; - let swap_used = (used_swap / total_swap) * 100.0; - let memory_available = total_memory - used_memory; - let virtual_memory_available = total_swap - used_swap; + let total_memory = (data.sys.total_memory() as f64 / gb).fmt_num(); + let used_memory = (data.sys.used_memory() as f64 / gb).fmt_num(); + let total_swap = (data.sys.total_swap() as f64 / gb).fmt_num(); + let used_swap = (data.sys.used_swap() as f64 / gb).fmt_num(); + let ram_used = ((used_memory / total_memory) * 100.0).fmt_num(); + let swap_used = ((used_swap / total_swap) * 100.0).fmt_num(); + let memory_available = (total_memory - used_memory).fmt_num(); + let virtual_memory_available = (total_swap - used_swap).fmt_num(); let mut mem_map = IndexMap::::new(); mem_map.insert("Memory Used".to_string(), used_memory); @@ -368,14 +371,14 @@ pub fn refresh_hardware_info(data: &mut Data) { if data.first_run { data.hw_info.cpu.load.push(CoresSensor { - name: format!("{} #{}", brand, cpu_count), + name: format!("Core #{}", cpu_count), value: cpu.cpu_usage() as f64, min: cpu.cpu_usage() as f64, max: cpu.cpu_usage() as f64, }); data.hw_info.cpu.clock.push(CoresSensor { - name: format!("{} #{}", brand, cpu_count), + name: format!("Core #{}", cpu_count), value: cpu.frequency() as f64, min: cpu.frequency() as f64, max: cpu.frequency() as f64, @@ -510,24 +513,24 @@ pub fn refresh_hardware_info(data: &mut Data) { // for (_pid, process) in data.sys.processes() {} // Disks - if data.first_run { - let disks = Disks::new_with_refreshed_list(); - for disk in disks.list() { - let free_space = disk.available_space() as f64 / gb; - let total_space = disk.total_space() as f64 / gb; - let name = disk.name().to_str().unwrap().to_string(); - - data.hw_info.system.storage.disks.push(CoresDisks { - name: name.clone(), - total_space: total_space as u64, - free_space: free_space as u64, - throughput_read: 0.0, - throughput_write: 0.0, - temperature: CoresSensor::default(), - health: "N/A".to_string(), - }); - } - } + // if data.first_run { + // let disks = Disks::new_with_refreshed_list(); + // for disk in disks.list() { + // let free_space = disk.available_space() as f64 / gb; + // let total_space = disk.total_space() as f64 / gb; + // let name = disk.name().to_str().unwrap().to_string(); + + // data.hw_info.system.storage.disks.push(CoresDisk { + // name: name.clone(), + // total_space: total_space as u64, + // free_space: free_space as u64, + // throughput_read: 0.0, + // throughput_write: 0.0, + // temperature: CoresSensor::default(), + // health: "N/A".to_string(), + // }); + // } + // } // Network info match get_default_interface() { @@ -596,10 +599,14 @@ pub fn refresh_hardware_info(data: &mut Data) { } }; - // MAC os + // macOS #[cfg(target_os = "macos")] mac::macos_hardware_info(data); + // Linux + #[cfg(target_os = "linux")] + linux::linux_hardware_info(data); + // END data.first_run = false; diff --git a/platforms/unix/hardwareinfo/src/linux/cpu.rs b/platforms/unix/hardwareinfo/src/linux/cpu.rs new file mode 100644 index 0000000..85ba443 --- /dev/null +++ b/platforms/unix/hardwareinfo/src/linux/cpu.rs @@ -0,0 +1,303 @@ +use anyhow::{anyhow, bail, Context, Result}; +use glob::glob; +use log::{debug, warn}; +use regex::Regex; +use std::{ + path::{Path, PathBuf}, + sync::LazyLock, +}; + +const KNOWN_HWMONS: &[&str] = &["zenpower", "coretemp", "k10temp"]; + +const KNOWN_THERMAL_ZONES: &[&str] = &["x86_pkg_temp", "acpitz"]; + +static RE_LSCPU_MODEL_NAME: LazyLock = + LazyLock::new(|| Regex::new(r"Model name:\s*(.*)").unwrap()); + +static RE_LSCPU_ARCHITECTURE: LazyLock = + LazyLock::new(|| Regex::new(r"Architecture:\s*(.*)").unwrap()); + +static RE_LSCPU_CPUS: LazyLock = LazyLock::new(|| Regex::new(r"CPU\(s\):\s*(.*)").unwrap()); + +static RE_LSCPU_SOCKETS: LazyLock = + LazyLock::new(|| Regex::new(r"Socket\(s\):\s*(.*)").unwrap()); + +static RE_LSCPU_CORES: LazyLock = + LazyLock::new(|| Regex::new(r"Core\(s\) per socket:\s*(.*)").unwrap()); + +static RE_LSCPU_VIRTUALIZATION: LazyLock = + LazyLock::new(|| Regex::new(r"Virtualization:\s*(.*)").unwrap()); + +static RE_LSCPU_MAX_MHZ: LazyLock = + LazyLock::new(|| Regex::new(r"CPU max MHz:\s*(.*)").unwrap()); + +static RE_PROC_STAT: LazyLock = LazyLock::new(|| { + Regex::new(r"cpu[0-9]* *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*)").unwrap() +}); + +static CPU_TEMPERATURE_PATH: LazyLock> = LazyLock::new(|| { + let cpu_temperature_path = + search_for_hwmons(KNOWN_HWMONS).or_else(|| search_for_thermal_zones(KNOWN_THERMAL_ZONES)); + + if let Some((sensor, path)) = &cpu_temperature_path { + debug!( + "CPU temperature sensor located at {} ({sensor})", + path.display() + ); + } else { + warn!("No sensor for CPU temperature found!"); + } + + cpu_temperature_path.map(|(_, path)| path) +}); + +/// Looks for hwmons with the given names. +/// This function is a bit inefficient since the `names` array is considered to be ordered by priority. +fn search_for_hwmons(names: &[&'static str]) -> Option<(&'static str, PathBuf)> { + for temp_name in names { + for path in (glob("/sys/class/hwmon/hwmon*").unwrap()).flatten() { + if let Ok(read_name) = std::fs::read_to_string(path.join("name")) { + if &read_name.trim_end() == temp_name { + return Some((temp_name, path.join("temp1_input"))); + } + } + } + } + + None +} + +/// Looks for thermal zones with the given types. +/// This function is a bit inefficient since the `types` array is considered to be ordered by priority. +fn search_for_thermal_zones(types: &[&'static str]) -> Option<(&'static str, PathBuf)> { + for temp_type in types { + for path in (glob("/sys/class/thermal/thermal_zone*").unwrap()).flatten() { + if let Ok(read_type) = std::fs::read_to_string(path.join("type")) { + if &read_type.trim_end() == temp_type { + return Some((temp_type, path.join("temp"))); + } + } + } + } + + None +} + +pub struct CpuData { + pub new_total_usage: (u64, u64), + pub new_thread_usages: Vec<(u64, u64)>, + pub temperature: Result, + pub frequencies: Vec>, +} + +impl CpuData { + pub fn new(logical_cpus: usize) -> Self { + let new_total_usage = get_cpu_usage(None).unwrap_or((0, 0)); + + let temperature = get_temperature(); + + let mut frequencies = Vec::with_capacity(logical_cpus); + let mut new_thread_usages = Vec::with_capacity(logical_cpus); + + for i in 0..logical_cpus { + let smth = get_cpu_usage(Some(i)).unwrap_or((0, 0)); + new_thread_usages.push(smth); + + let freq = get_cpu_freq(i); + frequencies.push(freq.ok()); + } + + Self { + new_total_usage, + new_thread_usages, + temperature, + frequencies, + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct CpuInfo { + pub model_name: Option, + pub architecture: Option, + pub logical_cpus: Option, + pub physical_cpus: Option, + pub sockets: Option, + pub virtualization: Option, + pub max_speed: Option, +} + +fn trade_mark_symbols>(s: S) -> String { + s.as_ref() + .replace("(R)", "®") + .replace("(tm)", "™") + .replace("(TM)", "™") +} + +/// Returns a `CPUInfo` struct populated with values gathered from `lscpu`. +/// +/// # Errors +/// +/// Will return `Err` if the are problems during reading or parsing +/// of the `lscpu` command +pub fn cpu_info() -> Result { + let lscpu_output = String::from_utf8( + std::process::Command::new("lscpu") + .env("LC_ALL", "C") + .output() + .context("unable to run lscpu, is util-linux installed?")? + .stdout, + ) + .context("unable to parse lscpu output to UTF-8")?; + + let model_name = RE_LSCPU_MODEL_NAME + .captures(&lscpu_output) + .and_then(|captures| { + captures + .get(1) + .map(|capture| trade_mark_symbols(capture.as_str())) + }); + + let architecture = RE_LSCPU_ARCHITECTURE + .captures(&lscpu_output) + .and_then(|captures| captures.get(1).map(|capture| capture.as_str().into())); + + let sockets = RE_LSCPU_SOCKETS + .captures(&lscpu_output) + .and_then(|captures| { + captures + .get(1) + .and_then(|capture| capture.as_str().parse().ok()) + }); + + let logical_cpus = RE_LSCPU_CPUS.captures(&lscpu_output).and_then(|captures| { + captures + .get(1) + .and_then(|capture| capture.as_str().parse().ok()) + }); + + let physical_cpus = RE_LSCPU_CORES.captures(&lscpu_output).and_then(|captures| { + captures + .get(1) + .and_then(|capture| capture.as_str().parse::().ok()) + .map(|int| int * sockets.unwrap_or(1)) + }); + + let virtualization = RE_LSCPU_VIRTUALIZATION + .captures(&lscpu_output) + .and_then(|captures| captures.get(1).map(|capture| capture.as_str().into())); + + let max_speed = RE_LSCPU_MAX_MHZ + .captures(&lscpu_output) + .and_then(|captures| { + captures.get(1).and_then(|capture| { + capture + .as_str() + .parse::() + .ok() + .map(|float| float * 1_000_000.0) + }) + }); + + Ok(CpuInfo { + model_name, + architecture, + logical_cpus, + physical_cpus, + sockets, + virtualization, + max_speed, + }) +} + +/// Returns the frequency of the given CPU `core` +/// +/// # Errors +/// +/// Will return `Err` if the are problems during reading or parsing +/// of the corresponding file in sysfs +pub fn get_cpu_freq(core: usize) -> Result { + std::fs::read_to_string(format!( + "/sys/devices/system/cpu/cpu{core}/cpufreq/scaling_cur_freq" + )) + .with_context(|| format!("unable to read scaling_cur_freq for core {core}"))? + .replace('\n', "") + .parse::() + .context("can't parse scaling_cur_freq to usize") + .map(|x| x * 1000) +} + +fn parse_proc_stat_line>(line: S) -> Result<(u64, u64)> { + let captures = RE_PROC_STAT + .captures(line.as_ref()) + .ok_or_else(|| anyhow!("using regex to parse /proc/stat failed"))?; + let idle_time = captures + .name("idle") + .and_then(|x| x.as_str().parse::().ok()) + .ok_or_else(|| anyhow!("unable to get idle time"))? + + captures + .name("iowait") + .and_then(|x| x.as_str().parse::().ok()) + .ok_or_else(|| anyhow!("unable to get iowait time"))?; + let sum = captures + .iter() + .skip(1) + .flat_map(|cap| { + cap.and_then(|x| x.as_str().parse::().ok()) + .ok_or_else(|| anyhow!("unable to sum CPU times from /proc/stat")) + }) + .sum(); + Ok((idle_time, sum)) +} + +fn get_proc_stat(core: Option) -> Result { + // the combined stats are in line 0, the other cores are in the following lines, + // since our `core` argument starts with 0, we must add 1 to it if it's not `None`. + let selected_line_number = core.map_or(0, |x| x + 1); + let proc_stat_raw = + std::fs::read_to_string("/proc/stat").context("unable to read /proc/stat")?; + let mut proc_stat = proc_stat_raw.split('\n').collect::>(); + proc_stat.retain(|x| x.starts_with("cpu")); + // return an `Error` if `core` is greater than the number of cores + if selected_line_number >= proc_stat.len() { + bail!("`core` argument greater than amount of cores") + } + Ok(proc_stat[selected_line_number].to_string()) +} + +/// Returns the CPU usage of either all cores combined (if supplied argument is `None`), +/// or of a specific thread (taken from the supplied argument starting at 0) +/// Please keep in mind that this is the total CPU time since boot, you have to do delta +/// calculations yourself. The tuple's layout is: `(idle_time, total_time)` +/// +/// # Errors +/// +/// Will return `Err` if the are problems during reading or parsing +/// of /proc/stat +pub fn get_cpu_usage(core: Option) -> Result<(u64, u64)> { + parse_proc_stat_line(get_proc_stat(core)?) +} + +/// Returns the CPU temperature. +/// +/// # Errors +/// +/// Will return `Err` if there was no way to read the CPU temperature. +pub fn get_temperature() -> Result { + if let Some(path) = CPU_TEMPERATURE_PATH.as_ref() { + read_sysfs_thermal(path) + } else { + bail!("no CPU temperature sensor found") + } +} + +fn read_sysfs_thermal>(path: P) -> Result { + let path = path.as_ref(); + let temp_string = std::fs::read_to_string(path) + .with_context(|| format!("unable to read {}", path.display()))?; + temp_string + .replace('\n', "") + .parse::() + .with_context(|| format!("unable to parse {}", path.display())) + .map(|t| t / 1000f32) +} diff --git a/platforms/unix/hardwareinfo/src/linux/drive.rs b/platforms/unix/hardwareinfo/src/linux/drive.rs new file mode 100644 index 0000000..8a54fd6 --- /dev/null +++ b/platforms/unix/hardwareinfo/src/linux/drive.rs @@ -0,0 +1,291 @@ +use anyhow::{Context, Result}; +use regex::Regex; +use std::{ + collections::HashMap, + fmt::Display, + path::{Path, PathBuf}, + sync::LazyLock, +}; + +const SYS_STATS: &str = r" *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*) *(?P[0-9]*)"; + +static RE_DRIVE: LazyLock = LazyLock::new(|| Regex::new(SYS_STATS).unwrap()); + +#[derive(Debug)] +pub struct DriveData { + pub inner: Drive, + pub is_virtual: bool, + pub writable: Result, + pub removable: Result, + pub disk_stats: HashMap, + pub capacity: Result, +} + +impl DriveData { + pub fn new(path: &Path) -> Self { + let inner = Drive::from_sysfs(path); + let is_virtual = inner.is_virtual(); + let writable = inner.writable(); + let removable = inner.removable(); + let disk_stats = inner.sys_stats().unwrap_or_default(); + let capacity = inner.capacity(); + + Self { + inner, + is_virtual, + writable, + removable, + disk_stats, + capacity, + } + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +pub enum DriveType { + CdDvdBluray, + Emmc, + Flash, + Floppy, + Hdd, + LoopDevice, + MappedDevice, + Nvme, + Raid, + RamDisk, + Ssd, + ZfsVolume, + Zram, + #[default] + Unknown, +} + +#[derive(Debug, Clone, Default, Eq)] +pub struct Drive { + pub model: Option, + pub drive_type: DriveType, + pub block_device: String, + pub sysfs_path: PathBuf, +} + +impl Display for DriveType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + DriveType::CdDvdBluray => "CD/DVD/Blu-ray Drive", + DriveType::Emmc => "eMMC Storage", + DriveType::Flash => "Flash Storage", + DriveType::Floppy => "Floppy Drive", + DriveType::Hdd => "Hard Disk Drive", + DriveType::LoopDevice => "Loop Device", + DriveType::MappedDevice => "Mapped Device", + DriveType::Nvme => "NVMe Drive", + DriveType::Unknown => "N/A", + DriveType::Raid => "Software Raid", + DriveType::RamDisk => "RAM Disk", + DriveType::Ssd => "Solid State Drive", + DriveType::ZfsVolume => "ZFS Volume", + DriveType::Zram => "Compressed RAM Disk (zram)", + } + ) + } +} + +impl PartialEq for Drive { + fn eq(&self, other: &Self) -> bool { + self.block_device == other.block_device + } +} + +impl Drive { + /// Creates a `Drive` using a SysFS Path + /// + /// # Errors + /// + /// Will return `Err` if the are errors during + /// reading or parsing + pub fn from_sysfs>(sysfs_path: P) -> Drive { + let path = sysfs_path.as_ref().to_path_buf(); + let block_device = path + .file_name() + .expect("sysfs path ends with \"..\"?") + .to_string_lossy() + .to_string(); + + let mut drive = Self::default(); + drive.sysfs_path = path; + drive.block_device = block_device; + drive.model = drive.model().ok().map(|model| model.trim().to_string()); + drive.drive_type = drive.drive_type().unwrap_or_default(); + drive + } + + /// Returns the SysFS Paths of possible drives + /// + /// # Errors + /// + /// Will return `Err` if the are errors during + /// reading or parsing + pub fn get_sysfs_paths() -> Result> { + let mut list = Vec::new(); + let entries = std::fs::read_dir("/sys/block")?; + for entry in entries { + let entry = entry?; + let block_device = entry.file_name().to_string_lossy().to_string(); + if block_device.is_empty() { + continue; + } + list.push(entry.path()); + } + Ok(list) + } + + pub fn display_name(&self) -> String { + return String::from("N/A"); + } + + /// Returns the current SysFS stats for the drive + /// + /// # Errors + /// + /// Will return `Err` if the are errors during + /// reading or parsing + pub fn sys_stats(&self) -> Result> { + let stat = std::fs::read_to_string(self.sysfs_path.join("stat")) + .with_context(|| format!("unable to read /sys/block/{}/stat", self.block_device))?; + + let captures = RE_DRIVE + .captures(&stat) + .with_context(|| format!("unable to parse /sys/block/{}/stat", self.block_device))?; + + Ok(RE_DRIVE + .capture_names() + .flatten() + .filter_map(|named_capture| { + Some(( + named_capture.to_string(), + captures.name(named_capture)?.as_str().parse().ok()?, + )) + }) + .collect()) + } + + fn drive_type(&self) -> Result { + if self.block_device.starts_with("nvme") { + Ok(DriveType::Nvme) + } else if self.block_device.starts_with("mmc") { + Ok(DriveType::Emmc) + } else if self.block_device.starts_with("fd") { + Ok(DriveType::Floppy) + } else if self.block_device.starts_with("sr") { + Ok(DriveType::CdDvdBluray) + } else if self.block_device.starts_with("zram") { + Ok(DriveType::Zram) + } else if self.block_device.starts_with("md") { + Ok(DriveType::Raid) + } else if self.block_device.starts_with("loop") { + Ok(DriveType::LoopDevice) + } else if self.block_device.starts_with("dm") { + Ok(DriveType::MappedDevice) + } else if self.block_device.starts_with("ram") { + Ok(DriveType::RamDisk) + } else if self.block_device.starts_with("zd") { + Ok(DriveType::ZfsVolume) + } else if let Ok(rotational) = + std::fs::read_to_string(self.sysfs_path.join("queue/rotational")) + { + // turn rot into a boolean + let rotational = rotational + .replace('\n', "") + .parse::() + .map(|rot| rot != 0)?; + if rotational { + Ok(DriveType::Hdd) + } else if self.removable()? { + Ok(DriveType::Flash) + } else { + Ok(DriveType::Ssd) + } + } else { + Ok(DriveType::Unknown) + } + } + + /// Returns, whether the drive is removable + /// + /// # Errors + /// + /// Will return `Err` if the are errors during + /// reading or parsing + pub fn removable(&self) -> Result { + std::fs::read_to_string(self.sysfs_path.join("removable"))? + .replace('\n', "") + .parse::() + .map(|rem| rem != 0) + .context("unable to parse removable sysfs file") + } + + /// Returns, whether the drive is writable + /// + /// # Errors + /// + /// Will return `Err` if the are errors during + /// reading or parsing + pub fn writable(&self) -> Result { + std::fs::read_to_string(self.sysfs_path.join("ro"))? + .replace('\n', "") + .parse::() + .map(|ro| ro == 0) + .context("unable to parse ro sysfs file") + } + + /// Returns the capacity of the drive **in bytes** + /// + /// # Errors + /// + /// Will return `Err` if the are errors during + /// reading or parsing + pub fn capacity(&self) -> Result { + std::fs::read_to_string(self.sysfs_path.join("size"))? + .replace('\n', "") + .parse::() + .map(|sectors| sectors * 512) + .context("unable to parse size sysfs file") + } + + /// Returns the model information of the drive + /// + /// # Errors + /// + /// Will return `Err` if the are errors during + /// reading or parsing + pub fn model(&self) -> Result { + std::fs::read_to_string(self.sysfs_path.join("device/model")) + .context("unable to parse model sysfs file") + } + + /// Returns the World-Wide Identification of the drive + /// + /// # Errors + /// + /// Will return `Err` if the are errors during + /// reading or parsing + pub fn wwid(&self) -> Result { + std::fs::read_to_string(self.sysfs_path.join("device/wwid")) + .context("unable to parse wwid sysfs file") + } + + pub fn is_virtual(&self) -> bool { + match self.drive_type { + DriveType::LoopDevice + | DriveType::MappedDevice + | DriveType::Raid + | DriveType::RamDisk + | DriveType::ZfsVolume + | DriveType::Zram => true, + _ => self.capacity().unwrap_or(0) == 0, + } + } +} diff --git a/platforms/unix/hardwareinfo/src/linux/memory.rs b/platforms/unix/hardwareinfo/src/linux/memory.rs new file mode 100644 index 0000000..d9f7412 --- /dev/null +++ b/platforms/unix/hardwareinfo/src/linux/memory.rs @@ -0,0 +1,354 @@ +use std::{process::Command, sync::LazyLock}; + +use anyhow::{bail, Context, Result}; +use log::debug; +use regex::Regex; + +use super::{FLATPAK_APP_PATH, FLATPAK_SPAWN, IS_FLATPAK}; + +const TEMPLATE_RE_PRESENT: &str = r"MEMORY_DEVICE_%_PRESENT=(\d)"; + +const TEMPLATE_RE_CONFIGURED_SPEED_MTS: &str = r"MEMORY_DEVICE_%_CONFIGURED_SPEED_MTS=(\d*)"; + +const TEMPLATE_RE_SPEED_MTS: &str = r"MEMORY_DEVICE_%_SPEED_MTS=(\d*)"; + +const TEMPLATE_RE_FORM_FACTOR: &str = r"MEMORY_DEVICE_%_FORM_FACTOR=(.*)"; + +const TEMPLATE_RE_TYPE: &str = r"MEMORY_DEVICE_%_TYPE=(.*)"; + +const TEMPLATE_RE_TYPE_DETAIL: &str = r"MEMORY_DEVICE_%_TYPE_DETAIL=(.*)"; + +const TEMPLATE_RE_SIZE: &str = r"MEMORY_DEVICE_%_SIZE=(\d*)"; + +const TEMPLATE_RE_MANUFACTURER: &str = r"MEMORY_DEVICE_%_MANUFACTURER=(.*)"; + +const TEMPLATE_RE_CONFIGURED_VOLTAGE: &str = r"MEMORY_DEVICE_%_CONFIGURED_VOLTAGE=(\d*)"; + +const BYTES_IN_GIB: u64 = 1_073_741_824; // 1024 * 1024 * 1024 + +static RE_CONFIGURED_SPEED: LazyLock = + LazyLock::new(|| Regex::new(r"Configured Memory Speed: (\d+) MT/s").unwrap()); + +static RE_SPEED: LazyLock = LazyLock::new(|| Regex::new(r"Speed: (\d+) MT/s").unwrap()); + +static RE_MANUFACTURER: LazyLock = + LazyLock::new(|| Regex::new(r"Manufacturer: (.+)").unwrap()); + +static RE_CONFIGURED_VOLTAGE: LazyLock = + LazyLock::new(|| Regex::new(r"Configured Voltage: (\d+) V").unwrap()); + +static RE_FORMFACTOR: LazyLock = LazyLock::new(|| Regex::new(r"Form Factor: (.+)").unwrap()); + +static RE_TYPE: LazyLock = LazyLock::new(|| Regex::new(r"Type: (.+)").unwrap()); + +static RE_TYPE_DETAIL: LazyLock = + LazyLock::new(|| Regex::new(r"Type Detail: (.+)").unwrap()); + +static RE_SIZE: LazyLock = LazyLock::new(|| Regex::new(r"Size: (\d+) GB").unwrap()); + +static RE_MEM_TOTAL: LazyLock = + LazyLock::new(|| Regex::new(r"MemTotal:\s*(\d*) kB").unwrap()); + +static RE_MEM_AVAILABLE: LazyLock = + LazyLock::new(|| Regex::new(r"MemAvailable:\s*(\d*) kB").unwrap()); + +static RE_SWAP_TOTAL: LazyLock = + LazyLock::new(|| Regex::new(r"SwapTotal:\s*(\d*) kB").unwrap()); + +static RE_SWAP_FREE: LazyLock = + LazyLock::new(|| Regex::new(r"SwapFree:\s*(\d*) kB").unwrap()); + +static RE_NUM_MEMORY_DEVICES: LazyLock = + LazyLock::new(|| Regex::new(r"MEMORY_ARRAY_NUM_DEVICES=(\d*)").unwrap()); + +#[derive(Debug, Clone, Copy)] +pub struct MemoryData { + pub total_mem: usize, + pub available_mem: usize, + pub total_swap: usize, + pub free_swap: usize, +} + +impl MemoryData { + pub fn new() -> Result { + let proc_mem = + std::fs::read_to_string("/proc/meminfo").context("unable to read /proc/meminfo")?; + + let total_mem = RE_MEM_TOTAL + .captures(&proc_mem) + .context("RE_MEM_TOTAL no captures") + .and_then(|captures| { + captures + .get(1) + .context("RE_MEM_TOTAL not enough captures") + .and_then(|capture| { + capture + .as_str() + .parse::() + .context("unable to parse MemTotal") + .map(|int| int * 1024) + }) + })?; + + let available_mem = RE_MEM_AVAILABLE + .captures(&proc_mem) + .context("RE_MEM_AVAILABLE no captures") + .and_then(|captures| { + captures + .get(1) + .context("RE_MEM_AVAILABLE not enough captures") + .and_then(|capture| { + capture + .as_str() + .parse::() + .context("unable to parse MemAvailable") + .map(|int| int * 1024) + }) + })?; + + let total_swap = RE_SWAP_TOTAL + .captures(&proc_mem) + .context("RE_SWAP_TOTAL no captures") + .and_then(|captures| { + captures + .get(1) + .context("RE_SWAP_TOTAL not enough captures") + .and_then(|capture| { + capture + .as_str() + .parse::() + .context("unable to parse SwapTotal") + .map(|int| int * 1024) + }) + })?; + + let free_swap = RE_SWAP_FREE + .captures(&proc_mem) + .context("RE_SWAP_FREE no captures") + .and_then(|captures| { + captures + .get(1) + .context("RE_SWAP_FREE not enough captures") + .and_then(|capture| { + capture + .as_str() + .parse::() + .context("unable to parse SwapFree") + .map(|int| int * 1024) + }) + })?; + + Ok(Self { + total_mem, + available_mem, + total_swap, + free_swap, + }) + } +} + +#[derive(Debug, Clone, Default)] +pub struct MemoryDevice { + pub manufacturer: Option, + pub speed_mts: Option, + pub configured_voltage: Option, + pub form_factor: Option, + pub r#type: Option, + pub type_detail: Option, + pub size: Option, + pub installed: bool, +} + +fn parse_dmidecode>(dmi: S) -> Vec { + let mut devices = Vec::new(); + + let device_strings = dmi.as_ref().split("\n\n"); + + for device_string in device_strings { + if device_string.is_empty() { + continue; + } + let memory_device = MemoryDevice { + speed_mts: RE_CONFIGURED_SPEED + .captures(device_string) + .or_else(|| RE_SPEED.captures(device_string)) + .map(|x| x[1].parse().unwrap()), + form_factor: RE_FORMFACTOR + .captures(device_string) + .map(|x| x[1].to_string()), + r#type: RE_TYPE.captures(device_string).map(|x| x[1].to_string()), + type_detail: RE_TYPE_DETAIL + .captures(device_string) + .map(|x| x[1].to_string()), + size: RE_SIZE + .captures(device_string) + .map(|x| x[1].parse::().unwrap() * BYTES_IN_GIB), + installed: RE_SPEED + .captures(device_string) + .map(|x| x[1].to_string()) + .is_some(), + manufacturer: RE_MANUFACTURER + .captures(device_string) + .map(|x| x[1].to_string()), + configured_voltage: RE_CONFIGURED_VOLTAGE + .captures(device_string) + .map(|x| x[1].parse().unwrap()), + }; + + devices.push(memory_device); + } + + devices +} + +fn virtual_dmi() -> Vec { + let command = if *IS_FLATPAK { + Command::new(FLATPAK_SPAWN) + .args([ + "--host", + "udevadm", + "info", + "-p", + "/sys/devices/virtual/dmi/id", + ]) + .output() + } else { + Command::new("udevadm") + .args(["info", "-p", "/sys/devices/virtual/dmi/id"]) + .output() + }; + + let virtual_dmi_output = command + .context("unable to execute udevadm") + .and_then(|output| { + String::from_utf8(output.stdout).context("unable to parse stdout of udevadm to UTF-8") + }) + .unwrap_or_default(); + + parse_virtual_dmi(virtual_dmi_output) +} + +fn parse_virtual_dmi>(dmi: S) -> Vec { + let dmi = dmi.as_ref(); + + let devices_amount: usize = RE_NUM_MEMORY_DEVICES + .captures(dmi) + .and_then(|captures| captures.get(1)) + .and_then(|capture| capture.as_str().parse().ok()) + .unwrap_or(0); + + let mut devices = Vec::with_capacity(devices_amount); + + for i in 0..devices_amount { + let i = i.to_string(); + + let speed = Regex::new(&TEMPLATE_RE_CONFIGURED_SPEED_MTS.replace('%', &i)) + .ok() + .and_then(|regex| regex.captures(dmi)) + .or_else(|| { + Regex::new(&TEMPLATE_RE_SPEED_MTS.replace('%', &i.to_string())) + .ok() + .and_then(|regex| regex.captures(dmi)) + }) + .and_then(|captures| captures.get(1)) + .and_then(|capture| capture.as_str().parse().ok()); + + let form_factor = Regex::new(&TEMPLATE_RE_FORM_FACTOR.replace('%', &i)) + .ok() + .and_then(|regex| regex.captures(dmi)) + .and_then(|captures| captures.get(1)) + .map(|capture| capture.as_str().to_string()); + + let r#type = Regex::new(&TEMPLATE_RE_TYPE.replace('%', &i)) + .ok() + .and_then(|regex| regex.captures(dmi)) + .and_then(|captures| captures.get(1)) + .map(|capture| capture.as_str().to_string()) + .filter(|capture| capture != ""); + + let type_detail = Regex::new(&TEMPLATE_RE_TYPE_DETAIL.replace('%', &i)) + .ok() + .and_then(|regex| regex.captures(dmi)) + .and_then(|captures| captures.get(1)) + .map(|capture| capture.as_str().to_string()); + + let size = Regex::new(&TEMPLATE_RE_SIZE.replace('%', &i)) + .ok() + .and_then(|regex| regex.captures(dmi)) + .and_then(|captures| captures.get(1)) + .and_then(|capture| capture.as_str().parse().ok()); + + let installed = Regex::new(&TEMPLATE_RE_PRESENT.replace('%', &i)) + .ok() + .and_then(|regex| regex.captures(dmi)) + .and_then(|captures| captures.get(1)) + .and_then(|capture| capture.as_str().parse::().ok()) + .map_or(true, |int| int != 0); + + let manufacturer = Regex::new(&TEMPLATE_RE_MANUFACTURER.replace('%', &i)) + .ok() + .and_then(|regex| regex.captures(dmi)) + .and_then(|captures| captures.get(1)) + .map(|capture| capture.as_str().to_string()); + + let configured_voltage = Regex::new(&TEMPLATE_RE_CONFIGURED_VOLTAGE.replace('%', &i)) + .ok() + .and_then(|regex| regex.captures(dmi)) + .and_then(|captures| captures.get(1)) + .and_then(|capture| capture.as_str().parse().ok()); + + devices.push(MemoryDevice { + speed_mts: speed, + form_factor, + r#type, + type_detail, + size, + installed, + manufacturer, + configured_voltage, + }); + } + + devices +} + +pub fn get_memory_devices() -> Result> { + let virtual_dmi = virtual_dmi(); + if virtual_dmi.is_empty() { + let output = Command::new("dmidecode") + .args(["-t", "17", "-q"]) + .output()?; + if output.status.code().unwrap_or(1) == 1 { + debug!("Unable to get memory information without elevated privileges"); + bail!("no permission") + } + debug!("Memory information obtained using dmidecode (unprivileged)"); + Ok(parse_dmidecode(String::from_utf8(output.stdout)?)) + } else { + debug!("Memory information obtained using udevadm"); + Ok(virtual_dmi) + } +} + +pub fn pkexec_dmidecode() -> Result> { + debug!("Using pkexec to get memory information (dmidecode)…"); + let output = if *IS_FLATPAK { + Command::new(FLATPAK_SPAWN) + .args([ + "--host", + "/usr/bin/pkexec", + "--disable-internal-agent", + &format!("{}/bin/dmidecode", FLATPAK_APP_PATH.as_str()), + "-t", + "17", + "-q", + ]) + .output()? + } else { + Command::new("pkexec") + .args(["--disable-internal-agent", "dmidecode", "-t", "17", "-q"]) + .output()? + }; + debug!("Memory information obtained using dmidecode (privileged)"); + Ok(parse_dmidecode(String::from_utf8(output.stdout)?.as_str())) +} diff --git a/platforms/unix/hardwareinfo/src/linux/mod.rs b/platforms/unix/hardwareinfo/src/linux/mod.rs new file mode 100644 index 0000000..61765f6 --- /dev/null +++ b/platforms/unix/hardwareinfo/src/linux/mod.rs @@ -0,0 +1,98 @@ +use std::sync::LazyLock; + +use log::{debug, info}; + +use crate::{compare_sensor, CoresDisk, CoresRAMInfo, CoresSensor, Data}; + +pub mod cpu; +pub mod drive; +pub mod memory; + +const FLATPAK_SPAWN: &str = "/usr/bin/flatpak-spawn"; +static FLATPAK_APP_PATH: LazyLock = LazyLock::new(|| String::new()); +pub static IS_FLATPAK: LazyLock = LazyLock::new(|| { + let is_flatpak = std::path::Path::new("/.flatpak-info").exists(); + + if is_flatpak { + debug!("Running as Flatpak"); + } else { + debug!("Not running as Flatpak"); + } + + is_flatpak +}); + +pub fn linux_hardware_info(data: &mut Data) { + let drive_paths = drive::Drive::get_sysfs_paths().unwrap_or_default(); + const SECTOR_SIZE: usize = 512; + + if data.first_run { + // Memory + let mem = memory::get_memory_devices(); + + if let Ok(mem) = mem { + for mem_device in mem { + data.hw_info.ram.info.push(CoresRAMInfo { + manufacturer_name: mem_device.manufacturer.unwrap_or("N/A".to_string()), + configured_speed: mem_device.speed_mts.unwrap_or(0), + configured_voltage: mem_device.configured_voltage.unwrap_or(0.0) * 1000.0, + size: mem_device.size.unwrap_or(1) / 1024 / 1024, + }); + } + } + + // CPU + let logical_cpus = data.sys.cpus().len(); + let cpu_info = cpu::cpu_info().unwrap(); + + data.hw_info.cpu.info[0].max_speed = cpu_info.max_speed.unwrap_or(1.0) / 1000.0 / 1000.0; + + info!("{cpu_info:?}"); + + let cpu_data = cpu::CpuData::new(logical_cpus); + if let Ok(temp) = cpu_data.temperature { + data.hw_info.cpu.temperature.push(CoresSensor { + name: "CPU".to_string(), + value: temp as f64, + min: temp as f64, + max: temp as f64, + }); + } + + let mut drive_data = Vec::with_capacity(drive_paths.len()); + for path in &drive_paths { + let d = drive::DriveData::new(path); + + if !d.is_virtual { + let inner = &d.inner; + + data.hw_info.system.storage.disks.push(CoresDisk { + name: inner.clone().model.unwrap_or("N/A".to_string()), + total_space: inner.clone().capacity().unwrap_or(1) / 1000 / 1000 / 1000, + free_space: 0, + throughput_read: 0.0, + throughput_write: 0.0, + temperature: CoresSensor::default(), + health: "N/A".to_string(), + }); + drive_data.push(drive::DriveData::new(path)); + } + } + } else { + // CPU + let logical_cpus = data.sys.cpus().len(); + let prev_temp = data.hw_info.cpu.temperature[0].clone(); + + let cpu_data = cpu::CpuData::new(logical_cpus); + if let Ok(temp) = cpu_data.temperature { + data.hw_info.cpu.temperature[0] = compare_sensor(&prev_temp, temp as f64); + } + } + + // let disks = &data.hw_info.system.storage.disks; + // for disk in disks { + // let inner = &disk.inner; + // let time_passed = 5.0; + // let delta_write_sectors = inner.clone().sys_stats().unwrap().get("write_sectors").unwrap_or(&0).saturating_sub(rhs); + // } +}