sysinfo/unix/linux/
system.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use crate::sys::cpu::{get_physical_core_count, CpusWrapper};
4use crate::sys::process::{compute_cpu_usage, refresh_procs};
5use crate::sys::utils::{get_all_utf8_data, to_u64};
6use crate::{
7    Cpu, CpuRefreshKind, LoadAvg, MemoryRefreshKind, Pid, Process, ProcessRefreshKind,
8    ProcessesToUpdate,
9};
10
11use libc::{self, c_char, sysconf, _SC_CLK_TCK, _SC_HOST_NAME_MAX, _SC_PAGESIZE};
12
13use std::cmp::min;
14use std::collections::HashMap;
15use std::ffi::CStr;
16use std::fs::File;
17use std::io::Read;
18use std::path::Path;
19use std::str::FromStr;
20use std::sync::{atomic::AtomicIsize, OnceLock};
21use std::time::Duration;
22
23// This whole thing is to prevent having too many files open at once. It could be problematic
24// for processes using a lot of files and using sysinfo at the same time.
25pub(crate) fn remaining_files() -> &'static AtomicIsize {
26    static REMAINING_FILES: OnceLock<AtomicIsize> = OnceLock::new();
27    REMAINING_FILES.get_or_init(|| unsafe {
28        let mut limits = libc::rlimit {
29            rlim_cur: 0,
30            rlim_max: 0,
31        };
32        if libc::getrlimit(libc::RLIMIT_NOFILE, &mut limits) != 0 {
33            // Most Linux system now defaults to 1024.
34            return AtomicIsize::new(1024 / 2);
35        }
36        // We save the value in case the update fails.
37        let current = limits.rlim_cur;
38
39        // The set the soft limit to the hard one.
40        limits.rlim_cur = limits.rlim_max;
41        // In this part, we leave minimum 50% of the available file descriptors to the process
42        // using sysinfo.
43        AtomicIsize::new(if libc::setrlimit(libc::RLIMIT_NOFILE, &limits) == 0 {
44            limits.rlim_cur / 2
45        } else {
46            current / 2
47        } as _)
48    })
49}
50
51declare_signals! {
52    libc::c_int,
53    Signal::Hangup => libc::SIGHUP,
54    Signal::Interrupt => libc::SIGINT,
55    Signal::Quit => libc::SIGQUIT,
56    Signal::Illegal => libc::SIGILL,
57    Signal::Trap => libc::SIGTRAP,
58    Signal::Abort => libc::SIGABRT,
59    Signal::IOT => libc::SIGIOT,
60    Signal::Bus => libc::SIGBUS,
61    Signal::FloatingPointException => libc::SIGFPE,
62    Signal::Kill => libc::SIGKILL,
63    Signal::User1 => libc::SIGUSR1,
64    Signal::Segv => libc::SIGSEGV,
65    Signal::User2 => libc::SIGUSR2,
66    Signal::Pipe => libc::SIGPIPE,
67    Signal::Alarm => libc::SIGALRM,
68    Signal::Term => libc::SIGTERM,
69    Signal::Child => libc::SIGCHLD,
70    Signal::Continue => libc::SIGCONT,
71    Signal::Stop => libc::SIGSTOP,
72    Signal::TSTP => libc::SIGTSTP,
73    Signal::TTIN => libc::SIGTTIN,
74    Signal::TTOU => libc::SIGTTOU,
75    Signal::Urgent => libc::SIGURG,
76    Signal::XCPU => libc::SIGXCPU,
77    Signal::XFSZ => libc::SIGXFSZ,
78    Signal::VirtualAlarm => libc::SIGVTALRM,
79    Signal::Profiling => libc::SIGPROF,
80    Signal::Winch => libc::SIGWINCH,
81    Signal::IO => libc::SIGIO,
82    Signal::Poll => libc::SIGPOLL,
83    Signal::Power => libc::SIGPWR,
84    Signal::Sys => libc::SIGSYS,
85}
86
87#[doc = include_str!("../../../md_doc/supported_signals.md")]
88pub const SUPPORTED_SIGNALS: &[crate::Signal] = supported_signals();
89#[doc = include_str!("../../../md_doc/minimum_cpu_update_interval.md")]
90pub const MINIMUM_CPU_UPDATE_INTERVAL: Duration = Duration::from_millis(200);
91
92pub(crate) fn get_max_nb_fds() -> isize {
93    unsafe {
94        let mut limits = libc::rlimit {
95            rlim_cur: 0,
96            rlim_max: 0,
97        };
98        if libc::getrlimit(libc::RLIMIT_NOFILE, &mut limits) != 0 {
99            // Most Linux system now defaults to 1024.
100            1024 / 2
101        } else {
102            limits.rlim_max as isize / 2
103        }
104    }
105}
106
107fn boot_time() -> u64 {
108    if let Ok(buf) = File::open("/proc/stat").and_then(|mut f| {
109        let mut buf = Vec::new();
110        f.read_to_end(&mut buf)?;
111        Ok(buf)
112    }) {
113        let line = buf.split(|c| *c == b'\n').find(|l| l.starts_with(b"btime"));
114
115        if let Some(line) = line {
116            return line
117                .split(|x| *x == b' ')
118                .filter(|s| !s.is_empty())
119                .nth(1)
120                .map(to_u64)
121                .unwrap_or(0);
122        }
123    }
124    // Either we didn't find "btime" or "/proc/stat" wasn't available for some reason...
125    unsafe {
126        let mut up: libc::timespec = std::mem::zeroed();
127        if libc::clock_gettime(libc::CLOCK_BOOTTIME, &mut up) == 0 {
128            up.tv_sec as u64
129        } else {
130            sysinfo_debug!("clock_gettime failed: boot time cannot be retrieve...");
131            0
132        }
133    }
134}
135
136pub(crate) struct SystemInfo {
137    pub(crate) page_size_b: u64,
138    pub(crate) clock_cycle: u64,
139    pub(crate) boot_time: u64,
140}
141
142impl SystemInfo {
143    fn new() -> Self {
144        unsafe {
145            Self {
146                page_size_b: sysconf(_SC_PAGESIZE) as _,
147                clock_cycle: sysconf(_SC_CLK_TCK) as _,
148                boot_time: boot_time(),
149            }
150        }
151    }
152}
153
154pub(crate) struct SystemInner {
155    process_list: HashMap<Pid, Process>,
156    mem_total: u64,
157    mem_free: u64,
158    mem_available: u64,
159    mem_buffers: u64,
160    mem_page_cache: u64,
161    mem_shmem: u64,
162    mem_slab_reclaimable: u64,
163    swap_total: u64,
164    swap_free: u64,
165    info: SystemInfo,
166    cpus: CpusWrapper,
167}
168
169impl SystemInner {
170    /// It is sometime possible that a CPU usage computation is bigger than
171    /// `"number of CPUs" * 100`.
172    ///
173    /// To prevent that, we compute ahead of time this maximum value and ensure that processes'
174    /// CPU usage don't go over it.
175    fn get_max_process_cpu_usage(&self) -> f32 {
176        self.cpus.len() as f32 * 100.
177    }
178
179    fn update_procs_cpu(&mut self, refresh_kind: ProcessRefreshKind) {
180        if !refresh_kind.cpu() {
181            return;
182        }
183        self.cpus
184            .refresh_if_needed(true, CpuRefreshKind::nothing().with_cpu_usage());
185
186        if self.cpus.is_empty() {
187            sysinfo_debug!("cannot compute processes CPU usage: no CPU found...");
188            return;
189        }
190        let (new, old) = self.cpus.get_global_raw_times();
191        let total_time = if old > new { 1 } else { new - old };
192        let total_time = total_time as f32 / self.cpus.len() as f32;
193        let max_value = self.get_max_process_cpu_usage();
194
195        for proc_ in self.process_list.values_mut() {
196            compute_cpu_usage(&mut proc_.inner, total_time, max_value);
197        }
198    }
199
200    fn refresh_cpus(&mut self, only_update_global_cpu: bool, refresh_kind: CpuRefreshKind) {
201        self.cpus.refresh(only_update_global_cpu, refresh_kind);
202    }
203}
204
205impl SystemInner {
206    pub(crate) fn new() -> Self {
207        Self {
208            process_list: HashMap::new(),
209            mem_total: 0,
210            mem_free: 0,
211            mem_available: 0,
212            mem_buffers: 0,
213            mem_page_cache: 0,
214            mem_shmem: 0,
215            mem_slab_reclaimable: 0,
216            swap_total: 0,
217            swap_free: 0,
218            cpus: CpusWrapper::new(),
219            info: SystemInfo::new(),
220        }
221    }
222
223    pub(crate) fn refresh_memory_specifics(&mut self, refresh_kind: MemoryRefreshKind) {
224        if !refresh_kind.ram() && !refresh_kind.swap() {
225            return;
226        }
227        let mut mem_available_found = false;
228        read_table("/proc/meminfo", ':', |key, value_kib| {
229            let field = match key {
230                "MemTotal" => &mut self.mem_total,
231                "MemFree" => &mut self.mem_free,
232                "MemAvailable" => {
233                    mem_available_found = true;
234                    &mut self.mem_available
235                }
236                "Buffers" => &mut self.mem_buffers,
237                "Cached" => &mut self.mem_page_cache,
238                "Shmem" => &mut self.mem_shmem,
239                "SReclaimable" => &mut self.mem_slab_reclaimable,
240                "SwapTotal" => &mut self.swap_total,
241                "SwapFree" => &mut self.swap_free,
242                _ => return,
243            };
244            // /proc/meminfo reports KiB, though it says "kB". Convert it.
245            *field = value_kib.saturating_mul(1_024);
246        });
247
248        // Linux < 3.14 may not have MemAvailable in /proc/meminfo
249        // So it should fallback to the old way of estimating available memory
250        // https://github.com/KittyKatt/screenFetch/issues/386#issuecomment-249312716
251        if !mem_available_found {
252            self.mem_available = self
253                .mem_free
254                .saturating_add(self.mem_buffers)
255                .saturating_add(self.mem_page_cache)
256                .saturating_add(self.mem_slab_reclaimable)
257                .saturating_sub(self.mem_shmem);
258        }
259    }
260
261    pub(crate) fn cgroup_limits(&self) -> Option<crate::CGroupLimits> {
262        crate::CGroupLimits::new(self)
263    }
264
265    pub(crate) fn refresh_cpu_specifics(&mut self, refresh_kind: CpuRefreshKind) {
266        self.refresh_cpus(false, refresh_kind);
267    }
268
269    pub(crate) fn refresh_processes_specifics(
270        &mut self,
271        processes_to_update: ProcessesToUpdate<'_>,
272        refresh_kind: ProcessRefreshKind,
273    ) -> usize {
274        let uptime = Self::uptime();
275        let nb_updated = refresh_procs(
276            &mut self.process_list,
277            Path::new("/proc"),
278            uptime,
279            &self.info,
280            processes_to_update,
281            refresh_kind,
282        );
283        self.update_procs_cpu(refresh_kind);
284        nb_updated
285    }
286
287    // COMMON PART
288    //
289    // Need to be moved into a "common" file to avoid duplication.
290
291    pub(crate) fn processes(&self) -> &HashMap<Pid, Process> {
292        &self.process_list
293    }
294
295    pub(crate) fn processes_mut(&mut self) -> &mut HashMap<Pid, Process> {
296        &mut self.process_list
297    }
298
299    pub(crate) fn process(&self, pid: Pid) -> Option<&Process> {
300        self.process_list.get(&pid)
301    }
302
303    pub(crate) fn global_cpu_usage(&self) -> f32 {
304        self.cpus.global_cpu.usage()
305    }
306
307    pub(crate) fn cpus(&self) -> &[Cpu] {
308        &self.cpus.cpus
309    }
310
311    pub(crate) fn physical_core_count(&self) -> Option<usize> {
312        get_physical_core_count()
313    }
314
315    pub(crate) fn total_memory(&self) -> u64 {
316        self.mem_total
317    }
318
319    pub(crate) fn free_memory(&self) -> u64 {
320        self.mem_free
321    }
322
323    pub(crate) fn available_memory(&self) -> u64 {
324        self.mem_available
325    }
326
327    pub(crate) fn used_memory(&self) -> u64 {
328        self.mem_total - self.mem_available
329    }
330
331    pub(crate) fn total_swap(&self) -> u64 {
332        self.swap_total
333    }
334
335    pub(crate) fn free_swap(&self) -> u64 {
336        self.swap_free
337    }
338
339    // need to be checked
340    pub(crate) fn used_swap(&self) -> u64 {
341        self.swap_total - self.swap_free
342    }
343
344    pub(crate) fn uptime() -> u64 {
345        let content = get_all_utf8_data("/proc/uptime", 50).unwrap_or_default();
346        content
347            .split('.')
348            .next()
349            .and_then(|t| t.parse().ok())
350            .unwrap_or_default()
351    }
352
353    pub(crate) fn boot_time() -> u64 {
354        boot_time()
355    }
356
357    pub(crate) fn load_average() -> LoadAvg {
358        let mut s = String::new();
359        if File::open("/proc/loadavg")
360            .and_then(|mut f| f.read_to_string(&mut s))
361            .is_err()
362        {
363            return LoadAvg::default();
364        }
365        let loads = s
366            .trim()
367            .split(' ')
368            .take(3)
369            .map(|val| val.parse::<f64>().unwrap())
370            .collect::<Vec<f64>>();
371        LoadAvg {
372            one: loads[0],
373            five: loads[1],
374            fifteen: loads[2],
375        }
376    }
377
378    #[cfg(not(target_os = "android"))]
379    pub(crate) fn name() -> Option<String> {
380        get_system_info_linux(
381            InfoType::Name,
382            Path::new("/etc/os-release"),
383            Path::new("/etc/lsb-release"),
384        )
385    }
386
387    #[cfg(target_os = "android")]
388    pub(crate) fn name() -> Option<String> {
389        get_system_info_android(InfoType::Name)
390    }
391
392    #[cfg(not(target_os = "android"))]
393    pub(crate) fn long_os_version() -> Option<String> {
394        let mut long_name = "Linux".to_owned();
395
396        let distro_name = Self::name();
397        let distro_version = Self::os_version();
398        if let Some(distro_version) = &distro_version {
399            // "Linux (Ubuntu 24.04)"
400            long_name.push_str(" (");
401            long_name.push_str(distro_name.as_deref().unwrap_or("unknown"));
402            long_name.push(' ');
403            long_name.push_str(distro_version);
404            long_name.push(')');
405        } else if let Some(distro_name) = &distro_name {
406            // "Linux (Ubuntu)"
407            long_name.push_str(" (");
408            long_name.push_str(distro_name);
409            long_name.push(')');
410        }
411
412        Some(long_name)
413    }
414
415    #[cfg(target_os = "android")]
416    pub(crate) fn long_os_version() -> Option<String> {
417        let mut long_name = "Android".to_owned();
418
419        if let Some(os_version) = Self::os_version() {
420            long_name.push(' ');
421            long_name.push_str(&os_version);
422        }
423
424        // Android's name() is extracted from the system property "ro.product.model"
425        // which is documented as "The end-user-visible name for the end product."
426        // So this produces a long_os_version like "Android 15 on Pixel 9 Pro".
427        if let Some(product_name) = Self::name() {
428            long_name.push_str(" on ");
429            long_name.push_str(&product_name);
430        }
431
432        Some(long_name)
433    }
434
435    pub(crate) fn host_name() -> Option<String> {
436        unsafe {
437            let hostname_max = sysconf(_SC_HOST_NAME_MAX);
438            let mut buffer = vec![0_u8; hostname_max as usize];
439            if libc::gethostname(buffer.as_mut_ptr() as *mut c_char, buffer.len()) == 0 {
440                if let Some(pos) = buffer.iter().position(|x| *x == 0) {
441                    // Shrink buffer to terminate the null bytes
442                    buffer.resize(pos, 0);
443                }
444                String::from_utf8(buffer).ok()
445            } else {
446                sysinfo_debug!("gethostname failed: hostname cannot be retrieved...");
447                None
448            }
449        }
450    }
451
452    pub(crate) fn kernel_version() -> Option<String> {
453        let mut raw = std::mem::MaybeUninit::<libc::utsname>::zeroed();
454
455        unsafe {
456            if libc::uname(raw.as_mut_ptr()) == 0 {
457                let info = raw.assume_init();
458
459                let release = info
460                    .release
461                    .iter()
462                    .filter(|c| **c != 0)
463                    .map(|c| *c as u8 as char)
464                    .collect::<String>();
465
466                Some(release)
467            } else {
468                None
469            }
470        }
471    }
472
473    #[cfg(not(target_os = "android"))]
474    pub(crate) fn os_version() -> Option<String> {
475        get_system_info_linux(
476            InfoType::OsVersion,
477            Path::new("/etc/os-release"),
478            Path::new("/etc/lsb-release"),
479        )
480    }
481
482    #[cfg(target_os = "android")]
483    pub(crate) fn os_version() -> Option<String> {
484        get_system_info_android(InfoType::OsVersion)
485    }
486
487    #[cfg(not(target_os = "android"))]
488    pub(crate) fn distribution_id() -> String {
489        get_system_info_linux(
490            InfoType::DistributionID,
491            Path::new("/etc/os-release"),
492            Path::new(""),
493        )
494        .unwrap_or_else(|| std::env::consts::OS.to_owned())
495    }
496
497    #[cfg(target_os = "android")]
498    pub(crate) fn distribution_id() -> String {
499        // Currently get_system_info_android doesn't support InfoType::DistributionID and always
500        // returns None. This call is done anyway for consistency with non-Android implementation
501        // and to suppress dead-code warning for DistributionID on Android.
502        get_system_info_android(InfoType::DistributionID)
503            .unwrap_or_else(|| std::env::consts::OS.to_owned())
504    }
505
506    pub(crate) fn cpu_arch() -> Option<String> {
507        let mut raw = std::mem::MaybeUninit::<libc::utsname>::uninit();
508
509        unsafe {
510            if libc::uname(raw.as_mut_ptr()) != 0 {
511                return None;
512            }
513            let info = raw.assume_init();
514            // Converting `&[i8]` to `&[u8]`.
515            let machine: &[u8] =
516                std::slice::from_raw_parts(info.machine.as_ptr() as *const _, info.machine.len());
517
518            CStr::from_bytes_until_nul(machine)
519                .ok()
520                .and_then(|res| match res.to_str() {
521                    Ok(arch) => Some(arch.to_string()),
522                    Err(_) => None,
523                })
524        }
525    }
526
527    pub(crate) fn refresh_cpu_list(&mut self, refresh_kind: CpuRefreshKind) {
528        self.cpus = CpusWrapper::new();
529        self.refresh_cpu_specifics(refresh_kind);
530    }
531}
532
533fn read_u64(filename: &str) -> Option<u64> {
534    get_all_utf8_data(filename, 16_635)
535        .ok()
536        .and_then(|d| u64::from_str(d.trim()).ok())
537}
538
539fn read_table<F>(filename: &str, colsep: char, mut f: F)
540where
541    F: FnMut(&str, u64),
542{
543    if let Ok(content) = get_all_utf8_data(filename, 16_635) {
544        content
545            .split('\n')
546            .flat_map(|line| {
547                let mut split = line.split(colsep);
548                let key = split.next()?;
549                let value = split.next()?;
550                let value0 = value.trim_start().split(' ').next()?;
551                let value0_u64 = u64::from_str(value0).ok()?;
552                Some((key, value0_u64))
553            })
554            .for_each(|(k, v)| f(k, v));
555    }
556}
557
558fn read_table_key(filename: &str, target_key: &str, colsep: char) -> Option<u64> {
559    if let Ok(content) = get_all_utf8_data(filename, 16_635) {
560        return content.split('\n').find_map(|line| {
561            let mut split = line.split(colsep);
562            let key = split.next()?;
563            if key != target_key {
564                return None;
565            }
566
567            let value = split.next()?;
568            let value0 = value.trim_start().split(' ').next()?;
569            u64::from_str(value0).ok()
570        });
571    }
572
573    None
574}
575
576impl crate::CGroupLimits {
577    fn new(sys: &SystemInner) -> Option<Self> {
578        assert!(
579            sys.mem_total != 0,
580            "You need to call System::refresh_memory before trying to get cgroup limits!",
581        );
582        if let (Some(mem_cur), Some(mem_max), Some(mem_rss)) = (
583            // cgroups v2
584            read_u64("/sys/fs/cgroup/memory.current"),
585            read_u64("/sys/fs/cgroup/memory.max"),
586            read_table_key("/sys/fs/cgroup/memory.stat", "anon", ' '),
587        ) {
588            let mut limits = Self {
589                total_memory: sys.mem_total,
590                free_memory: sys.mem_free,
591                free_swap: sys.swap_free,
592                rss: mem_rss,
593            };
594
595            limits.total_memory = min(mem_max, sys.mem_total);
596            limits.free_memory = limits.total_memory.saturating_sub(mem_cur);
597
598            if let Some(swap_cur) = read_u64("/sys/fs/cgroup/memory.swap.current") {
599                limits.free_swap = sys.swap_total.saturating_sub(swap_cur);
600            }
601
602            Some(limits)
603        } else if let (Some(mem_cur), Some(mem_max), Some(mem_rss)) = (
604            // cgroups v1
605            read_u64("/sys/fs/cgroup/memory/memory.usage_in_bytes"),
606            read_u64("/sys/fs/cgroup/memory/memory.limit_in_bytes"),
607            read_table_key("/sys/fs/cgroup/memory/memory.stat", "total_rss", ' '),
608        ) {
609            let mut limits = Self {
610                total_memory: sys.mem_total,
611                free_memory: sys.mem_free,
612                free_swap: sys.swap_free,
613                rss: mem_rss,
614            };
615
616            limits.total_memory = min(mem_max, sys.mem_total);
617            limits.free_memory = limits.total_memory.saturating_sub(mem_cur);
618
619            Some(limits)
620        } else {
621            None
622        }
623    }
624}
625
626#[derive(PartialEq, Eq)]
627enum InfoType {
628    /// The end-user friendly name of:
629    /// - Android: The device model
630    /// - Linux: The distributions name
631    Name,
632    OsVersion,
633    /// Machine-parseable ID of a distribution, see
634    /// https://www.freedesktop.org/software/systemd/man/os-release.html#ID=
635    DistributionID,
636}
637
638#[cfg(not(target_os = "android"))]
639fn get_system_info_linux(info: InfoType, path: &Path, fallback_path: &Path) -> Option<String> {
640    if let Ok(buf) = File::open(path).and_then(|mut f| {
641        let mut buf = String::new();
642        f.read_to_string(&mut buf)?;
643        Ok(buf)
644    }) {
645        let info_str = match info {
646            InfoType::Name => "NAME=",
647            InfoType::OsVersion => "VERSION_ID=",
648            InfoType::DistributionID => "ID=",
649        };
650
651        for line in buf.lines() {
652            if let Some(stripped) = line.strip_prefix(info_str) {
653                return Some(stripped.replace('"', ""));
654            }
655        }
656    }
657
658    // Fallback to `/etc/lsb-release` file for systems where VERSION_ID is not included.
659    // VERSION_ID is not required in the `/etc/os-release` file
660    // per https://www.linux.org/docs/man5/os-release.html
661    // If this fails for some reason, fallback to None
662    let buf = File::open(fallback_path)
663        .and_then(|mut f| {
664            let mut buf = String::new();
665            f.read_to_string(&mut buf)?;
666            Ok(buf)
667        })
668        .ok()?;
669
670    let info_str = match info {
671        InfoType::OsVersion => "DISTRIB_RELEASE=",
672        InfoType::Name => "DISTRIB_ID=",
673        InfoType::DistributionID => {
674            // lsb-release is inconsistent with os-release and unsupported.
675            return None;
676        }
677    };
678    for line in buf.lines() {
679        if let Some(stripped) = line.strip_prefix(info_str) {
680            return Some(stripped.replace('"', ""));
681        }
682    }
683    None
684}
685
686#[cfg(target_os = "android")]
687fn get_system_info_android(info: InfoType) -> Option<String> {
688    // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/os/Build.java#58
689    let name: &'static [u8] = match info {
690        InfoType::Name => b"ro.product.model\0",
691        InfoType::OsVersion => b"ro.build.version.release\0",
692        InfoType::DistributionID => {
693            // Not supported.
694            return None;
695        }
696    };
697
698    let mut value_buffer = vec![0u8; libc::PROP_VALUE_MAX as usize];
699    unsafe {
700        let len = libc::__system_property_get(
701            name.as_ptr() as *const c_char,
702            value_buffer.as_mut_ptr() as *mut c_char,
703        );
704
705        if len != 0 {
706            if let Some(pos) = value_buffer.iter().position(|c| *c == 0) {
707                value_buffer.resize(pos, 0);
708            }
709            String::from_utf8(value_buffer).ok()
710        } else {
711            None
712        }
713    }
714}
715
716#[cfg(test)]
717mod test {
718    #[cfg(target_os = "android")]
719    use super::get_system_info_android;
720    #[cfg(not(target_os = "android"))]
721    use super::get_system_info_linux;
722    use super::read_table;
723    use super::read_table_key;
724    use super::InfoType;
725    use std::collections::HashMap;
726    use std::io::Write;
727    use tempfile::NamedTempFile;
728
729    #[test]
730    fn test_read_table() {
731        // Create a temporary file with test content
732        let mut file = NamedTempFile::new().unwrap();
733        writeln!(file, "KEY1:100 kB").unwrap();
734        writeln!(file, "KEY2:200 kB").unwrap();
735        writeln!(file, "KEY3:300 kB").unwrap();
736        writeln!(file, "KEY4:invalid").unwrap();
737
738        let file_path = file.path().to_str().unwrap();
739
740        // Test reading the table
741        let mut result = HashMap::new();
742        read_table(file_path, ':', |key, value| {
743            result.insert(key.to_string(), value);
744        });
745
746        assert_eq!(result.get("KEY1"), Some(&100));
747        assert_eq!(result.get("KEY2"), Some(&200));
748        assert_eq!(result.get("KEY3"), Some(&300));
749        assert_eq!(result.get("KEY4"), None);
750
751        // Test with different separator and units
752        let mut file = NamedTempFile::new().unwrap();
753        writeln!(file, "KEY1 400 MB").unwrap();
754        writeln!(file, "KEY2 500 GB").unwrap();
755        writeln!(file, "KEY3 600").unwrap();
756
757        let file_path = file.path().to_str().unwrap();
758
759        let mut result = HashMap::new();
760        read_table(file_path, ' ', |key, value| {
761            result.insert(key.to_string(), value);
762        });
763
764        assert_eq!(result.get("KEY1"), Some(&400));
765        assert_eq!(result.get("KEY2"), Some(&500));
766        assert_eq!(result.get("KEY3"), Some(&600));
767
768        // Test with empty file
769        let file = NamedTempFile::new().unwrap();
770        let file_path = file.path().to_str().unwrap();
771
772        let mut result = HashMap::new();
773        read_table(file_path, ':', |key, value| {
774            result.insert(key.to_string(), value);
775        });
776
777        assert!(result.is_empty());
778
779        // Test with non-existent file
780        let mut result = HashMap::new();
781        read_table("/nonexistent/file", ':', |key, value| {
782            result.insert(key.to_string(), value);
783        });
784
785        assert!(result.is_empty());
786    }
787
788    #[test]
789    fn test_read_table_key() {
790        // Create a temporary file with test content
791        let mut file = NamedTempFile::new().unwrap();
792        writeln!(file, "KEY1:100 kB").unwrap();
793        writeln!(file, "KEY2:200 kB").unwrap();
794        writeln!(file, "KEY3:300 kB").unwrap();
795
796        let file_path = file.path().to_str().unwrap();
797
798        // Test existing keys
799        assert_eq!(read_table_key(file_path, "KEY1", ':'), Some(100));
800        assert_eq!(read_table_key(file_path, "KEY2", ':'), Some(200));
801        assert_eq!(read_table_key(file_path, "KEY3", ':'), Some(300));
802
803        // Test non-existent key
804        assert_eq!(read_table_key(file_path, "KEY4", ':'), None);
805
806        // Test with different separator
807        let mut file = NamedTempFile::new().unwrap();
808        writeln!(file, "KEY1 400 kB").unwrap();
809        writeln!(file, "KEY2 500 kB").unwrap();
810
811        let file_path = file.path().to_str().unwrap();
812
813        assert_eq!(read_table_key(file_path, "KEY1", ' '), Some(400));
814        assert_eq!(read_table_key(file_path, "KEY2", ' '), Some(500));
815
816        // Test with invalid file
817        assert_eq!(read_table_key("/nonexistent/file", "KEY1", ':'), None);
818    }
819
820    #[test]
821    #[cfg(target_os = "android")]
822    fn lsb_release_fallback_android() {
823        assert!(get_system_info_android(InfoType::OsVersion).is_some());
824        assert!(get_system_info_android(InfoType::Name).is_some());
825        assert!(get_system_info_android(InfoType::DistributionID).is_none());
826    }
827
828    #[test]
829    #[cfg(not(target_os = "android"))]
830    fn lsb_release_fallback_not_android() {
831        use std::path::Path;
832
833        let dir = tempfile::tempdir().expect("failed to create temporary directory");
834        let tmp1 = dir.path().join("tmp1");
835        let tmp2 = dir.path().join("tmp2");
836
837        // /etc/os-release
838        std::fs::write(
839            &tmp1,
840            r#"NAME="Ubuntu"
841VERSION="20.10 (Groovy Gorilla)"
842ID=ubuntu
843ID_LIKE=debian
844PRETTY_NAME="Ubuntu 20.10"
845VERSION_ID="20.10"
846VERSION_CODENAME=groovy
847UBUNTU_CODENAME=groovy
848"#,
849        )
850        .expect("Failed to create tmp1");
851
852        // /etc/lsb-release
853        std::fs::write(
854            &tmp2,
855            r#"DISTRIB_ID=Ubuntu
856DISTRIB_RELEASE=20.10
857DISTRIB_CODENAME=groovy
858DISTRIB_DESCRIPTION="Ubuntu 20.10"
859"#,
860        )
861        .expect("Failed to create tmp2");
862
863        // Check for the "normal" path: "/etc/os-release"
864        assert_eq!(
865            get_system_info_linux(InfoType::OsVersion, &tmp1, Path::new("")),
866            Some("20.10".to_owned())
867        );
868        assert_eq!(
869            get_system_info_linux(InfoType::Name, &tmp1, Path::new("")),
870            Some("Ubuntu".to_owned())
871        );
872        assert_eq!(
873            get_system_info_linux(InfoType::DistributionID, &tmp1, Path::new("")),
874            Some("ubuntu".to_owned())
875        );
876
877        // Check for the "fallback" path: "/etc/lsb-release"
878        assert_eq!(
879            get_system_info_linux(InfoType::OsVersion, Path::new(""), &tmp2),
880            Some("20.10".to_owned())
881        );
882        assert_eq!(
883            get_system_info_linux(InfoType::Name, Path::new(""), &tmp2),
884            Some("Ubuntu".to_owned())
885        );
886        assert_eq!(
887            get_system_info_linux(InfoType::DistributionID, Path::new(""), &tmp2),
888            None
889        );
890    }
891}