hardware_query/platform/
linux.rs

1/// Enhanced platform-specific hardware detection for Linux
2use crate::{HardwareQueryError, Result};
3use std::collections::HashMap;
4use std::fs;
5use std::process::Command;
6
7/// Linux-specific CPU information
8#[derive(Debug, Clone)]
9pub struct LinuxCPUInfo {
10    pub vendor_id: String,
11    pub model_name: String,
12    pub cpu_family: Option<u32>,
13    pub model: Option<u32>,
14    pub stepping: Option<u32>,
15    pub microcode: Option<String>,
16    pub cpu_cores: u32,
17    pub siblings: u32,
18    pub core_id: Vec<u32>,
19    pub apicid: Vec<u32>,
20    pub initial_apicid: Vec<u32>,
21    pub cpu_mhz: f32,
22    pub cache_size: u32,
23    pub physical_id: Vec<u32>,
24    pub flags: Vec<String>,
25    pub bogomips: f32,
26    pub clflush_size: Option<u32>,
27    pub cache_alignment: Option<u32>,
28    pub address_sizes: Option<String>,
29    pub power_management: Option<String>,
30    pub vulnerabilities: Vec<String>,
31}
32
33impl LinuxCPUInfo {
34    /// Query detailed CPU information from Linux /proc and /sys
35    pub fn query() -> Result<Self> {
36        let cpuinfo_content = fs::read_to_string("/proc/cpuinfo").map_err(|e| {
37            HardwareQueryError::system_info_unavailable(format!("Cannot read /proc/cpuinfo: {}", e))
38        })?;
39
40        let mut cpu_info = LinuxCPUInfo {
41            vendor_id: String::new(),
42            model_name: String::new(),
43            cpu_family: None,
44            model: None,
45            stepping: None,
46            microcode: None,
47            cpu_cores: 0,
48            siblings: 0,
49            core_id: Vec::new(),
50            apicid: Vec::new(),
51            initial_apicid: Vec::new(),
52            cpu_mhz: 0.0,
53            cache_size: 0,
54            physical_id: Vec::new(),
55            flags: Vec::new(),
56            bogomips: 0.0,
57            clflush_size: None,
58            cache_alignment: None,
59            address_sizes: None,
60            power_management: None,
61            vulnerabilities: Vec::new(),
62        };
63
64        // Parse /proc/cpuinfo
65        for line in cpuinfo_content.lines() {
66            if let Some((key, value)) = line.split_once(':') {
67                let key = key.trim();
68                let value = value.trim();
69
70                match key {
71                    "vendor_id" => cpu_info.vendor_id = value.to_string(),
72                    "model name" => cpu_info.model_name = value.to_string(),
73                    "cpu family" => cpu_info.cpu_family = value.parse().ok(),
74                    "model" => cpu_info.model = value.parse().ok(),
75                    "stepping" => cpu_info.stepping = value.parse().ok(),
76                    "microcode" => cpu_info.microcode = Some(value.to_string()),
77                    "cpu cores" => cpu_info.cpu_cores = value.parse().unwrap_or(0),
78                    "siblings" => cpu_info.siblings = value.parse().unwrap_or(0),
79                    "core id" => cpu_info.core_id.push(value.parse().unwrap_or(0)),
80                    "apicid" => cpu_info.apicid.push(value.parse().unwrap_or(0)),
81                    "initial apicid" => cpu_info.initial_apicid.push(value.parse().unwrap_or(0)),
82                    "cpu MHz" => cpu_info.cpu_mhz = value.parse().unwrap_or(0.0),
83                    "cache size" => {
84                        // Parse cache size like "8192 KB"
85                        if let Some(size_str) = value.split_whitespace().next() {
86                            cpu_info.cache_size = size_str.parse().unwrap_or(0);
87                        }
88                    }
89                    "physical id" => cpu_info.physical_id.push(value.parse().unwrap_or(0)),
90                    "flags" => {
91                        cpu_info.flags = value.split_whitespace().map(|s| s.to_string()).collect();
92                    }
93                    "bogomips" => cpu_info.bogomips = value.parse().unwrap_or(0.0),
94                    "clflush size" => cpu_info.clflush_size = value.parse().ok(),
95                    "cache_alignment" => cpu_info.cache_alignment = value.parse().ok(),
96                    "address sizes" => cpu_info.address_sizes = Some(value.to_string()),
97                    "power management" => cpu_info.power_management = Some(value.to_string()),
98                    _ => {}
99                }
100            }
101        }
102
103        // Get vulnerabilities
104        cpu_info.vulnerabilities = Self::get_vulnerabilities()?;
105
106        Ok(cpu_info)
107    }
108
109    /// Get CPU vulnerabilities from /sys/devices/system/cpu/vulnerabilities/
110    pub fn get_vulnerabilities() -> Result<Vec<String>> {
111        let mut vulnerabilities = Vec::new();
112
113        if let Ok(entries) = fs::read_dir("/sys/devices/system/cpu/vulnerabilities") {
114            for entry in entries.flatten() {
115                if let Ok(file_type) = entry.file_type() {
116                    if file_type.is_file() {
117                        if let Some(vuln_name) = entry.file_name().to_str() {
118                            if let Ok(status) = fs::read_to_string(entry.path()) {
119                                let status = status.trim();
120                                if !status.starts_with("Not affected")
121                                    && !status.starts_with("Mitigation")
122                                {
123                                    vulnerabilities.push(format!("{}: {}", vuln_name, status));
124                                }
125                            }
126                        }
127                    }
128                }
129            }
130        }
131
132        Ok(vulnerabilities)
133    }
134
135    /// Get cache information from /sys/devices/system/cpu/cpu0/cache/
136    pub fn get_cache_info() -> Result<HashMap<String, u32>> {
137        let mut cache_info = HashMap::new();
138
139        for level in 1..=3 {
140            for cache_type in &["data", "instruction", "unified"] {
141                let cache_path = format!("/sys/devices/system/cpu/cpu0/cache/index{}/", level);
142
143                // Check if this cache level exists
144                if let Ok(type_content) = fs::read_to_string(format!("{}type", cache_path)) {
145                    if type_content.trim() == *cache_type || type_content.trim() == "Unified" {
146                        if let Ok(size_content) = fs::read_to_string(format!("{}size", cache_path))
147                        {
148                            let size_str = size_content.trim().replace("K", "");
149                            if let Ok(size) = size_str.parse::<u32>() {
150                                let key = format!("L{} {}", level, cache_type);
151                                cache_info.insert(key, size);
152                            }
153                        }
154                    }
155                }
156            }
157        }
158
159        Ok(cache_info)
160    }
161
162    /// Get CPU frequency scaling information
163    pub fn get_frequency_info() -> Result<HashMap<String, u32>> {
164        let mut freq_info = HashMap::new();
165
166        // Try to get scaling frequencies
167        if let Ok(min_freq) =
168            fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq")
169        {
170            if let Ok(freq) = min_freq.trim().parse::<u32>() {
171                freq_info.insert("min_frequency_khz".to_string(), freq);
172            }
173        }
174
175        if let Ok(max_freq) =
176            fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq")
177        {
178            if let Ok(freq) = max_freq.trim().parse::<u32>() {
179                freq_info.insert("max_frequency_khz".to_string(), freq);
180            }
181        }
182
183        if let Ok(cur_freq) =
184            fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq")
185        {
186            if let Ok(freq) = cur_freq.trim().parse::<u32>() {
187                freq_info.insert("current_frequency_khz".to_string(), freq);
188            }
189        }
190
191        Ok(freq_info)
192    }
193
194    /// Get CPU temperature from sensors
195    pub fn get_temperature() -> Result<Option<f32>> {
196        // Try different thermal sensor locations
197        let thermal_paths = [
198            "/sys/class/thermal/thermal_zone0/temp",
199            "/sys/class/hwmon/hwmon0/temp1_input",
200            "/sys/class/hwmon/hwmon1/temp1_input",
201        ];
202
203        for path in &thermal_paths {
204            if let Ok(temp_str) = fs::read_to_string(path) {
205                if let Ok(temp) = temp_str.trim().parse::<i32>() {
206                    // Convert from millidegrees to degrees
207                    return Ok(Some(temp as f32 / 1000.0));
208                }
209            }
210        }
211
212        // Try using lm-sensors
213        if let Ok(output) = Command::new("sensors").arg("-A").arg("-u").output() {
214            let output_str = String::from_utf8_lossy(&output.stdout);
215            for line in output_str.lines() {
216                if line.contains("temp1_input") || line.contains("Core 0") {
217                    if let Some(temp_str) = line.split(':').nth(1) {
218                        if let Ok(temp) = temp_str.trim().parse::<f32>() {
219                            return Ok(Some(temp));
220                        }
221                    }
222                }
223            }
224        }
225
226        Ok(None)
227    }
228}
229
230/// Linux-specific GPU information
231#[derive(Debug, Clone)]
232pub struct LinuxGPUInfo {
233    pub device_name: String,
234    pub vendor_name: String,
235    pub driver: String,
236    pub pci_id: String,
237    pub memory_info: Option<String>,
238    pub driver_version: Option<String>,
239}
240
241impl LinuxGPUInfo {
242    /// Query GPU information from Linux /sys and lspci
243    pub fn query_all() -> Result<Vec<Self>> {
244        let mut gpus = Vec::new();
245
246        // Use lspci to get GPU information
247        if let Ok(output) = Command::new("lspci")
248            .args(["-v", "-s", "$(lspci | grep VGA | cut -d' ' -f1)"])
249            .output()
250        {
251            let output_str = String::from_utf8_lossy(&output.stdout);
252            // Parse lspci output - this is a simplified implementation
253            // A full implementation would parse the complete lspci output
254        }
255
256        // Try to detect NVIDIA GPUs using nvidia-smi
257        if let Ok(nvidia_gpus) = Self::query_nvidia_gpus() {
258            gpus.extend(nvidia_gpus);
259        }
260
261        // Try to detect AMD GPUs using rocm-smi
262        if let Ok(amd_gpus) = Self::query_amd_gpus() {
263            gpus.extend(amd_gpus);
264        }
265
266        Ok(gpus)
267    }
268
269    /// Query NVIDIA GPU information using nvidia-smi
270    fn query_nvidia_gpus() -> Result<Vec<Self>> {
271        let mut gpus = Vec::new();
272
273        if let Ok(output) = Command::new("nvidia-smi")
274            .args([
275                "--query-gpu=name,driver_version,memory.total,pci.bus_id",
276                "--format=csv,noheader,nounits",
277            ])
278            .output()
279        {
280            let output_str = String::from_utf8_lossy(&output.stdout);
281            for line in output_str.lines() {
282                let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
283                if parts.len() >= 4 {
284                    gpus.push(Self {
285                        device_name: parts[0].to_string(),
286                        vendor_name: "NVIDIA".to_string(),
287                        driver: "nvidia".to_string(),
288                        pci_id: parts[3].to_string(),
289                        memory_info: Some(format!("{} MB", parts[2])),
290                        driver_version: Some(parts[1].to_string()),
291                    });
292                }
293            }
294        }
295
296        Ok(gpus)
297    }
298
299    /// Query AMD GPU information using rocm-smi
300    fn query_amd_gpus() -> Result<Vec<Self>> {
301        let mut gpus = Vec::new();
302
303        if let Ok(output) = Command::new("rocm-smi")
304            .args(["--showproductname", "--showdriverversion"])
305            .output()
306        {
307            let output_str = String::from_utf8_lossy(&output.stdout);
308            // Parse rocm-smi output - simplified implementation
309            for line in output_str.lines() {
310                if line.contains("Card series") {
311                    // Extract GPU information from rocm-smi output
312                    // This would need more sophisticated parsing
313                }
314            }
315        }
316
317        Ok(gpus)
318    }
319}
320
321/// Linux-specific memory information
322#[derive(Debug, Clone)]
323pub struct LinuxMemoryInfo {
324    pub mem_total_kb: u64,
325    pub mem_free_kb: u64,
326    pub mem_available_kb: u64,
327    pub buffers_kb: u64,
328    pub cached_kb: u64,
329    pub swap_total_kb: u64,
330    pub swap_free_kb: u64,
331    pub modules: Vec<LinuxMemoryModule>,
332}
333
334#[derive(Debug, Clone)]
335pub struct LinuxMemoryModule {
336    pub size_mb: u64,
337    pub speed_mhz: Option<u32>,
338    pub memory_type: String,
339    pub locator: String,
340}
341
342impl LinuxMemoryInfo {
343    /// Query memory information from Linux /proc/meminfo and dmidecode
344    pub fn query() -> Result<Self> {
345        let meminfo_content = fs::read_to_string("/proc/meminfo").map_err(|e| {
346            HardwareQueryError::system_info_unavailable(format!("Cannot read /proc/meminfo: {}", e))
347        })?;
348
349        let mut mem_info = LinuxMemoryInfo {
350            mem_total_kb: 0,
351            mem_free_kb: 0,
352            mem_available_kb: 0,
353            buffers_kb: 0,
354            cached_kb: 0,
355            swap_total_kb: 0,
356            swap_free_kb: 0,
357            modules: Vec::new(),
358        };
359
360        // Parse /proc/meminfo
361        for line in meminfo_content.lines() {
362            if let Some((key, value)) = line.split_once(':') {
363                let key = key.trim();
364                let value = value.trim().replace(" kB", "");
365
366                if let Ok(val) = value.parse::<u64>() {
367                    match key {
368                        "MemTotal" => mem_info.mem_total_kb = val,
369                        "MemFree" => mem_info.mem_free_kb = val,
370                        "MemAvailable" => mem_info.mem_available_kb = val,
371                        "Buffers" => mem_info.buffers_kb = val,
372                        "Cached" => mem_info.cached_kb = val,
373                        "SwapTotal" => mem_info.swap_total_kb = val,
374                        "SwapFree" => mem_info.swap_free_kb = val,
375                        _ => {}
376                    }
377                }
378            }
379        }
380
381        // Get memory module information using dmidecode
382        mem_info.modules = Self::get_memory_modules()?;
383
384        Ok(mem_info)
385    }
386
387    /// Get memory module information using dmidecode
388    fn get_memory_modules() -> Result<Vec<LinuxMemoryModule>> {
389        let mut modules = Vec::new();
390
391        if let Ok(output) = Command::new("dmidecode").args(["-t", "memory"]).output() {
392            let output_str = String::from_utf8_lossy(&output.stdout);
393            let mut current_module: Option<LinuxMemoryModule> = None;
394
395            for line in output_str.lines() {
396                let line = line.trim();
397
398                if line.starts_with("Memory Device") {
399                    if let Some(module) = current_module.take() {
400                        modules.push(module);
401                    }
402                    current_module = Some(LinuxMemoryModule {
403                        size_mb: 0,
404                        speed_mhz: None,
405                        memory_type: String::new(),
406                        locator: String::new(),
407                    });
408                } else if let Some(ref mut module) = current_module {
409                    if let Some((key, value)) = line.split_once(':') {
410                        let key = key.trim();
411                        let value = value.trim();
412
413                        match key {
414                            "Size" => {
415                                if value.contains("MB") {
416                                    let size_str = value.replace(" MB", "");
417                                    module.size_mb = size_str.parse().unwrap_or(0);
418                                } else if value.contains("GB") {
419                                    let size_str = value.replace(" GB", "");
420                                    if let Ok(size_gb) = size_str.parse::<u64>() {
421                                        module.size_mb = size_gb * 1024;
422                                    }
423                                }
424                            }
425                            "Speed" => {
426                                let speed_str = value.replace(" MHz", "");
427                                module.speed_mhz = speed_str.parse().ok();
428                            }
429                            "Type" => module.memory_type = value.to_string(),
430                            "Locator" => module.locator = value.to_string(),
431                            _ => {}
432                        }
433                    }
434                }
435            }
436
437            if let Some(module) = current_module {
438                modules.push(module);
439            }
440        }
441
442        // Filter out empty modules
443        modules.retain(|m| m.size_mb > 0);
444
445        Ok(modules)
446    }
447}