1 | // Take a look at the license at the top of the repository in the LICENSE file. |
2 | |
3 | use crate::sys::component::{self, Component}; |
4 | use crate::sys::cpu::*; |
5 | use crate::sys::disk; |
6 | use crate::sys::process::*; |
7 | use crate::sys::utils::{get_all_data, to_u64}; |
8 | use crate::{ |
9 | CpuRefreshKind, Disk, LoadAvg, Networks, Pid, ProcessRefreshKind, RefreshKind, SystemExt, User, |
10 | }; |
11 | |
12 | use libc::{self, c_char, c_int, sysconf, _SC_CLK_TCK, _SC_HOST_NAME_MAX, _SC_PAGESIZE}; |
13 | use std::cmp::min; |
14 | use std::collections::HashMap; |
15 | use std::fs::File; |
16 | use std::io::{BufRead, BufReader, Read}; |
17 | use std::path::Path; |
18 | use std::str::FromStr; |
19 | use std::sync::{Arc, Mutex}; |
20 | use 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)] |
25 | pub(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 | |
53 | pub(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 | |
68 | fn 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 | |
97 | pub(crate) struct SystemInfo { |
98 | pub(crate) page_size_kb: u64, |
99 | pub(crate) clock_cycle: u64, |
100 | pub(crate) boot_time: u64, |
101 | } |
102 | |
103 | impl 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 | |
115 | declare_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" )] |
152 | pub 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 | |
171 | impl 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 | |
219 | impl 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 | |
610 | fn 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 | |
616 | fn read_table<F>(filename: &str, colsep: char, mut f: F) |
617 | where |
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 | |
635 | impl Default for System { |
636 | fn default() -> System { |
637 | System::new() |
638 | } |
639 | } |
640 | |
641 | #[derive (PartialEq, Eq)] |
642 | enum 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" ))] |
654 | fn 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" )] |
694 | fn 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)] |
724 | mod 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" |
752 | VERSION="20.10 (Groovy Gorilla)" |
753 | ID=ubuntu |
754 | ID_LIKE=debian |
755 | PRETTY_NAME="Ubuntu 20.10" |
756 | VERSION_ID="20.10" |
757 | VERSION_CODENAME=groovy |
758 | UBUNTU_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 |
767 | DISTRIB_RELEASE=20.10 |
768 | DISTRIB_CODENAME=groovy |
769 | DISTRIB_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 | |