Skip to main content

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::{CpusWrapper, get_physical_core_count};
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, _SC_CLK_TCK, _SC_HOST_NAME_MAX, _SC_PAGESIZE, c_char, sysconf};
12
13use std::collections::HashMap;
14use std::ffi::CStr;
15use std::fs::File;
16use std::io::Read;
17use std::mem::MaybeUninit;
18use std::path::Path;
19use std::str::FromStr;
20use std::sync::{OnceLock, atomic::AtomicIsize};
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 unsafe { 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::sys::cgroup::limits_for_system()
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        if cfg!(not(target_os = "android"))
351            && let Ok(content) = get_all_utf8_data("/proc/uptime", 50)
352            && let Some(uptime) = content.split('.').next().and_then(|t| t.parse().ok())
353        {
354            return uptime;
355        }
356        Self::uptime_with_sysinfo()
357    }
358
359    fn uptime_with_sysinfo() -> u64 {
360        unsafe {
361            let mut s = MaybeUninit::<libc::sysinfo>::uninit();
362            if libc::sysinfo(s.as_mut_ptr()) != 0 {
363                return 0;
364            }
365            let s = s.assume_init();
366            if s.uptime < 1 { 0 } else { s.uptime as u64 }
367        }
368    }
369
370    pub(crate) fn boot_time() -> u64 {
371        boot_time()
372    }
373
374    pub(crate) fn load_average() -> LoadAvg {
375        let mut s = String::new();
376        if File::open("/proc/loadavg")
377            .and_then(|mut f| f.read_to_string(&mut s))
378            .is_err()
379        {
380            return LoadAvg::default();
381        }
382        let loads = s
383            .trim()
384            .split(' ')
385            .take(3)
386            .filter_map(|val| val.parse::<f64>().ok())
387            .collect::<Vec<f64>>();
388        match *loads.as_slice() {
389            [one, five, fifteen, ..] => LoadAvg { one, five, fifteen },
390            [one, five] => LoadAvg {
391                one,
392                five,
393                fifteen: 0.,
394            },
395            [one] => LoadAvg {
396                one,
397                five: 0.,
398                fifteen: 0.,
399            },
400            [] => LoadAvg {
401                one: 0.,
402                five: 0.,
403                fifteen: 0.,
404            },
405        }
406    }
407
408    #[cfg(not(target_os = "android"))]
409    pub(crate) fn name() -> Option<String> {
410        get_system_info_linux(
411            InfoType::Name,
412            Path::new("/etc/os-release"),
413            Path::new("/etc/lsb-release"),
414        )
415    }
416
417    #[cfg(target_os = "android")]
418    pub(crate) fn name() -> Option<String> {
419        get_system_info_android(InfoType::Name)
420    }
421
422    #[cfg(not(target_os = "android"))]
423    pub(crate) fn long_os_version() -> Option<String> {
424        let mut long_name = "Linux".to_owned();
425
426        let distro_name = Self::name();
427        let distro_version = Self::os_version();
428        if let Some(distro_version) = &distro_version {
429            // "Linux (Ubuntu 24.04)"
430            long_name.push_str(" (");
431            long_name.push_str(distro_name.as_deref().unwrap_or("unknown"));
432            long_name.push(' ');
433            long_name.push_str(distro_version);
434            long_name.push(')');
435        } else if let Some(distro_name) = &distro_name {
436            // "Linux (Ubuntu)"
437            long_name.push_str(" (");
438            long_name.push_str(distro_name);
439            long_name.push(')');
440        }
441
442        Some(long_name)
443    }
444
445    #[cfg(target_os = "android")]
446    pub(crate) fn long_os_version() -> Option<String> {
447        let mut long_name = "Android".to_owned();
448
449        if let Some(os_version) = Self::os_version() {
450            long_name.push(' ');
451            long_name.push_str(&os_version);
452        }
453
454        // Android's name() is extracted from the system property "ro.product.model"
455        // which is documented as "The end-user-visible name for the end product."
456        // So this produces a long_os_version like "Android 15 on Pixel 9 Pro".
457        if let Some(product_name) = Self::name() {
458            long_name.push_str(" on ");
459            long_name.push_str(&product_name);
460        }
461
462        Some(long_name)
463    }
464
465    pub(crate) fn host_name() -> Option<String> {
466        unsafe {
467            let hostname_max = sysconf(_SC_HOST_NAME_MAX);
468            let mut buffer = vec![0_u8; hostname_max as usize];
469            if libc::gethostname(buffer.as_mut_ptr() as *mut c_char, buffer.len()) == 0 {
470                if let Some(pos) = buffer.iter().position(|x| *x == 0) {
471                    // Shrink buffer to terminate the null bytes
472                    buffer.resize(pos, 0);
473                }
474                String::from_utf8(buffer).ok()
475            } else {
476                sysinfo_debug!("gethostname failed: hostname cannot be retrieved...");
477                None
478            }
479        }
480    }
481
482    pub(crate) fn kernel_version() -> Option<String> {
483        let mut raw = MaybeUninit::<libc::utsname>::zeroed();
484
485        unsafe {
486            if libc::uname(raw.as_mut_ptr()) == 0 {
487                let info = raw.assume_init();
488
489                let release = info
490                    .release
491                    .iter()
492                    .filter(|c| **c != 0)
493                    .map(|c| *c as u8 as char)
494                    .collect::<String>();
495
496                Some(release)
497            } else {
498                None
499            }
500        }
501    }
502
503    #[cfg(not(target_os = "android"))]
504    pub(crate) fn os_version() -> Option<String> {
505        get_system_info_linux(
506            InfoType::OsVersion,
507            Path::new("/etc/os-release"),
508            Path::new("/etc/lsb-release"),
509        )
510    }
511
512    #[cfg(target_os = "android")]
513    pub(crate) fn os_version() -> Option<String> {
514        get_system_info_android(InfoType::OsVersion)
515    }
516
517    #[cfg(not(target_os = "android"))]
518    pub(crate) fn distribution_id() -> String {
519        get_system_info_linux(
520            InfoType::DistributionID,
521            Path::new("/etc/os-release"),
522            Path::new(""),
523        )
524        .unwrap_or_else(|| std::env::consts::OS.to_owned())
525    }
526
527    #[cfg(target_os = "android")]
528    pub(crate) fn distribution_id() -> String {
529        // Currently get_system_info_android doesn't support InfoType::DistributionID and always
530        // returns None. This call is done anyway for consistency with non-Android implementation
531        // and to suppress dead-code warning for DistributionID on Android.
532        get_system_info_android(InfoType::DistributionID)
533            .unwrap_or_else(|| std::env::consts::OS.to_owned())
534    }
535
536    #[cfg(not(target_os = "android"))]
537    pub(crate) fn distribution_id_like() -> Vec<String> {
538        system_info_as_list(get_system_info_linux(
539            InfoType::DistributionIDLike,
540            Path::new("/etc/os-release"),
541            Path::new(""),
542        ))
543    }
544
545    #[cfg(target_os = "android")]
546    pub(crate) fn distribution_id_like() -> Vec<String> {
547        // Currently get_system_info_android doesn't support InfoType::DistributionIDLike and always
548        // returns None. This call is done anyway for consistency with non-Android implementation
549        // and to suppress dead-code warning for DistributionIDLike on Android.
550        system_info_as_list(get_system_info_android(InfoType::DistributionIDLike))
551    }
552
553    #[cfg(not(target_os = "android"))]
554    pub(crate) fn kernel_name() -> Option<&'static str> {
555        Some("Linux")
556    }
557
558    #[cfg(target_os = "android")]
559    pub(crate) fn kernel_name() -> Option<&'static str> {
560        Some("Android kernel")
561    }
562
563    pub(crate) fn cpu_arch() -> Option<String> {
564        let mut raw = MaybeUninit::<libc::utsname>::uninit();
565
566        unsafe {
567            if libc::uname(raw.as_mut_ptr()) != 0 {
568                return None;
569            }
570            let info = raw.assume_init();
571            // Converting `&[i8]` to `&[u8]`.
572            let machine: &[u8] =
573                std::slice::from_raw_parts(info.machine.as_ptr() as *const _, info.machine.len());
574
575            CStr::from_bytes_until_nul(machine)
576                .ok()
577                .and_then(|res| match res.to_str() {
578                    Ok(arch) => Some(arch.to_string()),
579                    Err(_) => None,
580                })
581        }
582    }
583
584    pub(crate) fn physical_core_count() -> Option<usize> {
585        get_physical_core_count()
586    }
587
588    pub(crate) fn refresh_cpu_list(&mut self, refresh_kind: CpuRefreshKind) {
589        self.cpus = CpusWrapper::new();
590        self.refresh_cpu_specifics(refresh_kind);
591    }
592
593    pub(crate) fn open_files_limit() -> Option<usize> {
594        unsafe {
595            match getrlimit() {
596                Some(limits) => Some(limits.rlim_cur as _),
597                None => {
598                    sysinfo_debug!("getrlimit failed");
599                    None
600                }
601            }
602        }
603    }
604}
605
606fn read_table<F>(filename: &str, colsep: char, mut f: F)
607where
608    F: FnMut(&str, u64),
609{
610    if let Ok(content) = get_all_utf8_data(filename, 16_635) {
611        content
612            .split('\n')
613            .flat_map(|line| {
614                let mut split = line.split(colsep);
615                let key = split.next()?;
616                let value = split.next()?;
617                let value0 = value.trim_start().split(' ').next()?;
618                let value0_u64 = u64::from_str(value0).ok()?;
619                Some((key, value0_u64))
620            })
621            .for_each(|(k, v)| f(k, v));
622    }
623}
624
625#[derive(PartialEq, Eq)]
626enum InfoType {
627    /// The end-user friendly name of:
628    /// - Android: The device model
629    /// - Linux: The distributions name
630    Name,
631    OsVersion,
632    /// Machine-parseable ID of a distribution, see
633    /// https://www.freedesktop.org/software/systemd/man/os-release.html#ID=
634    DistributionID,
635    /// Machine-parseable ID_LIKE of related distributions, see
636    /// <https://www.freedesktop.org/software/systemd/man/latest/os-release.html#ID_LIKE=>
637    DistributionIDLike,
638}
639
640#[cfg(not(target_os = "android"))]
641fn get_system_info_linux(info: InfoType, path: &Path, fallback_path: &Path) -> Option<String> {
642    if let Ok(buf) = File::open(path).and_then(|mut f| {
643        let mut buf = String::new();
644        f.read_to_string(&mut buf)?;
645        Ok(buf)
646    }) {
647        let info_str = match info {
648            InfoType::Name => "NAME=",
649            InfoType::OsVersion => "VERSION_ID=",
650            InfoType::DistributionID => "ID=",
651            InfoType::DistributionIDLike => "ID_LIKE=",
652        };
653
654        for line in buf.lines() {
655            if let Some(stripped) = line.strip_prefix(info_str) {
656                return Some(stripped.replace('"', ""));
657            }
658        }
659    }
660
661    // Fallback to `/etc/lsb-release` file for systems where VERSION_ID is not included.
662    // VERSION_ID is not required in the `/etc/os-release` file
663    // per https://www.linux.org/docs/man5/os-release.html
664    // If this fails for some reason, fallback to None
665    let buf = File::open(fallback_path)
666        .and_then(|mut f| {
667            let mut buf = String::new();
668            f.read_to_string(&mut buf)?;
669            Ok(buf)
670        })
671        .ok()?;
672
673    let info_str = match info {
674        InfoType::OsVersion => "DISTRIB_RELEASE=",
675        InfoType::Name => "DISTRIB_ID=",
676        InfoType::DistributionID => {
677            // lsb-release is inconsistent with os-release and unsupported.
678            return None;
679        }
680        InfoType::DistributionIDLike => {
681            // lsb-release doesn't support ID_LIKE.
682            return None;
683        }
684    };
685    for line in buf.lines() {
686        if let Some(stripped) = line.strip_prefix(info_str) {
687            return Some(stripped.replace('"', ""));
688        }
689    }
690    None
691}
692
693/// Returns a system info value as a list of strings.
694/// Absence of a value is treated as an empty list.
695fn system_info_as_list(sysinfo: Option<String>) -> Vec<String> {
696    match sysinfo {
697        Some(value) => value.split_ascii_whitespace().map(String::from).collect(),
698        // For list fields absence of a field is equivalent to an empty list.
699        None => Vec::new(),
700    }
701}
702
703#[cfg(target_os = "android")]
704fn get_system_info_android(info: InfoType) -> Option<String> {
705    // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/os/Build.java#58
706    let name: &'static [u8] = match info {
707        InfoType::Name => b"ro.product.model\0",
708        InfoType::OsVersion => b"ro.build.version.release\0",
709        InfoType::DistributionID => {
710            // Not supported.
711            return None;
712        }
713        InfoType::DistributionIDLike => {
714            // Not supported.
715            return None;
716        }
717    };
718
719    let mut value_buffer = vec![0u8; libc::PROP_VALUE_MAX as usize];
720    unsafe {
721        let len = libc::__system_property_get(
722            name.as_ptr() as *const c_char,
723            value_buffer.as_mut_ptr() as *mut c_char,
724        );
725
726        if len != 0 {
727            if let Some(pos) = value_buffer.iter().position(|c| *c == 0) {
728                value_buffer.resize(pos, 0);
729            }
730            String::from_utf8(value_buffer).ok()
731        } else {
732            None
733        }
734    }
735}
736
737#[cfg(test)]
738mod test {
739    use super::InfoType;
740    #[cfg(target_os = "android")]
741    use super::get_system_info_android;
742    #[cfg(not(target_os = "android"))]
743    use super::get_system_info_linux;
744    use super::read_table;
745    use super::system_info_as_list;
746    use std::collections::HashMap;
747    use std::io::Write;
748    use tempfile::NamedTempFile;
749
750    #[test]
751    fn test_read_table() {
752        // Create a temporary file with test content
753        let mut file = NamedTempFile::new().unwrap();
754        writeln!(file, "KEY1:100 kB").unwrap();
755        writeln!(file, "KEY2:200 kB").unwrap();
756        writeln!(file, "KEY3:300 kB").unwrap();
757        writeln!(file, "KEY4:invalid").unwrap();
758
759        let file_path = file.path().to_str().unwrap();
760
761        // Test reading the table
762        let mut result = HashMap::new();
763        read_table(file_path, ':', |key, value| {
764            result.insert(key.to_string(), value);
765        });
766
767        assert_eq!(result.get("KEY1"), Some(&100));
768        assert_eq!(result.get("KEY2"), Some(&200));
769        assert_eq!(result.get("KEY3"), Some(&300));
770        assert_eq!(result.get("KEY4"), None);
771
772        // Test with different separator and units
773        let mut file = NamedTempFile::new().unwrap();
774        writeln!(file, "KEY1 400 MB").unwrap();
775        writeln!(file, "KEY2 500 GB").unwrap();
776        writeln!(file, "KEY3 600").unwrap();
777
778        let file_path = file.path().to_str().unwrap();
779
780        let mut result = HashMap::new();
781        read_table(file_path, ' ', |key, value| {
782            result.insert(key.to_string(), value);
783        });
784
785        assert_eq!(result.get("KEY1"), Some(&400));
786        assert_eq!(result.get("KEY2"), Some(&500));
787        assert_eq!(result.get("KEY3"), Some(&600));
788
789        // Test with empty file
790        let file = NamedTempFile::new().unwrap();
791        let file_path = file.path().to_str().unwrap();
792
793        let mut result = HashMap::new();
794        read_table(file_path, ':', |key, value| {
795            result.insert(key.to_string(), value);
796        });
797
798        assert!(result.is_empty());
799
800        // Test with non-existent file
801        let mut result = HashMap::new();
802        read_table("/nonexistent/file", ':', |key, value| {
803            result.insert(key.to_string(), value);
804        });
805
806        assert!(result.is_empty());
807    }
808
809    #[test]
810    #[cfg(target_os = "android")]
811    fn lsb_release_fallback_android() {
812        assert!(get_system_info_android(InfoType::OsVersion).is_some());
813        assert!(get_system_info_android(InfoType::Name).is_some());
814        assert!(get_system_info_android(InfoType::DistributionID).is_none());
815        assert!(get_system_info_android(InfoType::DistributionIDLike).is_none());
816    }
817
818    #[test]
819    #[cfg(not(target_os = "android"))]
820    fn lsb_release_fallback_not_android() {
821        use std::path::Path;
822
823        let dir = tempfile::tempdir().expect("failed to create temporary directory");
824        let tmp1 = dir.path().join("tmp1");
825        let tmp2 = dir.path().join("tmp2");
826
827        // /etc/os-release
828        std::fs::write(
829            &tmp1,
830            r#"NAME="Ubuntu"
831VERSION="20.10 (Groovy Gorilla)"
832ID=ubuntu
833ID_LIKE=debian
834PRETTY_NAME="Ubuntu 20.10"
835VERSION_ID="20.10"
836VERSION_CODENAME=groovy
837UBUNTU_CODENAME=groovy
838"#,
839        )
840        .expect("Failed to create tmp1");
841
842        // /etc/lsb-release
843        std::fs::write(
844            &tmp2,
845            r#"DISTRIB_ID=Ubuntu
846DISTRIB_RELEASE=20.10
847DISTRIB_CODENAME=groovy
848DISTRIB_DESCRIPTION="Ubuntu 20.10"
849"#,
850        )
851        .expect("Failed to create tmp2");
852
853        // Check for the "normal" path: "/etc/os-release"
854        assert_eq!(
855            get_system_info_linux(InfoType::OsVersion, &tmp1, Path::new("")),
856            Some("20.10".to_owned())
857        );
858        assert_eq!(
859            get_system_info_linux(InfoType::Name, &tmp1, Path::new("")),
860            Some("Ubuntu".to_owned())
861        );
862        assert_eq!(
863            get_system_info_linux(InfoType::DistributionID, &tmp1, Path::new("")),
864            Some("ubuntu".to_owned())
865        );
866        assert_eq!(
867            get_system_info_linux(InfoType::DistributionIDLike, &tmp1, Path::new("")),
868            Some("debian".to_owned())
869        );
870
871        // Check for the "fallback" path: "/etc/lsb-release"
872        assert_eq!(
873            get_system_info_linux(InfoType::OsVersion, Path::new(""), &tmp2),
874            Some("20.10".to_owned())
875        );
876        assert_eq!(
877            get_system_info_linux(InfoType::Name, Path::new(""), &tmp2),
878            Some("Ubuntu".to_owned())
879        );
880        assert_eq!(
881            get_system_info_linux(InfoType::DistributionID, Path::new(""), &tmp2),
882            None
883        );
884        assert_eq!(
885            get_system_info_linux(InfoType::DistributionIDLike, Path::new(""), &tmp2),
886            None
887        );
888    }
889
890    #[test]
891    fn test_system_info_as_list() {
892        // No value.
893        assert_eq!(system_info_as_list(None), Vec::<String>::new());
894        // Empty value.
895        assert_eq!(
896            system_info_as_list(Some("".to_string())),
897            Vec::<String>::new(),
898        );
899        // Whitespaces only.
900        assert_eq!(
901            system_info_as_list(Some(" ".to_string())),
902            Vec::<String>::new(),
903        );
904        // Single value.
905        assert_eq!(
906            system_info_as_list(Some("debian".to_string())),
907            vec!["debian".to_string()],
908        );
909        // Multiple values.
910        assert_eq!(
911            system_info_as_list(Some("rhel fedora".to_string())),
912            vec!["rhel".to_string(), "fedora".to_string()],
913        );
914        // Multiple spaces.
915        assert_eq!(
916            system_info_as_list(Some("rhel        fedora".to_string())),
917            vec!["rhel".to_string(), "fedora".to_string()],
918        );
919    }
920}