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: rlimit = libc::rlimit {
29 rlim_cur: 0,
30 rlim_max: 0,
31 };
32 if libc::getrlimit(resource:libc::RLIMIT_NOFILE, &mut limits) != 0 {
33 // Most Linux system now defaults to 1024.
34 return Arc::new(data:Mutex::new(1024 / 2));
35 }
36 // We save the value in case the update fails.
37 let current: u64 = 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(data:Mutex::new(
44 if libc::setrlimit(resource: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: rlimit = libc::rlimit {
56 rlim_cur: 0,
57 rlim_max: 0,
58 };
59 if libc::getrlimit(resource: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_dataOption(file_path:filename, size:16_635)
612 .ok()
613 .and_then(|d: String| 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: String) = get_all_data(file_path:filename, size:16_635) {
621 contentimpl Iterator
622 .split('\n')
623 .flat_map(|line: &str| {
624 let mut split: Split<'_, char> = line.split(colsep);
625 let key: &str = split.next()?;
626 let value: &str = split.next()?;
627 let value0: &str = value.trim_start().split(' ').next()?;
628 let value0_u64: u64 = u64::from_str(value0).ok()?;
629 Some((key, value0_u64))
630 })
631 .for_each(|(k: &str, v: u64)| 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}
803