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