sysinfo/linux/
system.rs

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