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