1use crate::utils::read_to_string;
24use crate::{traits::*, utils::Size};
25use anyhow::{Result, anyhow};
26use serde::{Deserialize, Serialize};
27use std::env::{self, var, vars};
28use std::fmt::Display;
29use std::process::Command;
30
31#[derive(Debug, Serialize)]
34pub struct Sys {
35 pub machine_id: Option<String>,
37
38 pub timezone: Option<String>,
40
41 pub env_vars: Vec<(String, String)>,
43
44 pub uptime: Uptime,
46
47 pub loadavg: LoadAVG,
49
50 pub shells: Shells,
52
53 pub hostname: Option<HostName>,
55 }
58
59impl Sys {
60 pub fn new() -> Result<Self> {
61 Ok(Self {
62 machine_id: read_to_string("/etc/machine-id").ok(),
63 timezone: read_to_string("/etc/timezone").ok(),
64 env_vars: get_env_vars(),
65 uptime: Uptime::new()?,
66 loadavg: LoadAVG::new()?,
67 shells: get_shells()?,
68 hostname: get_hostname(),
69 })
71 }
72
73 pub fn update(&mut self) -> Result<()> {
74 self.uptime = Uptime::new()?;
75 self.loadavg = LoadAVG::new()?;
76 Ok(())
77 }
78}
79
80impl ToJson for Sys {}
81
82#[derive(Debug, Serialize, Clone)]
84pub struct Kernel {
85 pub uname: Option<String>, pub cmdline: Option<String>, pub arch: Option<String>, pub version: Option<String>, pub build_info: Option<String>, pub pid_max: u32, pub threads_max: u32, pub user_events_max: Option<u32>, pub enthropy_avail: Option<u16>, }
112
113impl Kernel {
114 pub fn new() -> Result<Self> {
115 Ok(Self {
116 uname: read_to_string("/proc/version").ok(),
117 cmdline: read_to_string("/proc/cmdline").ok(),
118 arch: read_to_string("/proc/sys/kernel/arch").ok(),
119 version: read_to_string("/proc/sys/kernel/osrelease").ok(),
120 build_info: read_to_string("/proc/sys/kernel/version").ok(),
121 pid_max: read_to_string("/proc/sys/kernel/pid_max")?.parse()?,
122 threads_max: read_to_string("/proc/sys/kernel/threads-max")?.parse()?,
123 user_events_max: match read_to_string("/proc/sys/kernel/user_events_max").ok() {
124 Some(uem) => uem.parse().ok(),
125 None => None,
126 },
127 enthropy_avail: match read_to_string("/proc/sys/kernel/random/entropy_avail").ok() {
128 Some(ea) => ea.parse().ok(),
129 None => None,
130 },
131 })
132 }
133}
134
135impl ToJson for Kernel {}
136
137#[derive(Debug, Serialize, Default, Clone)]
141pub struct OsRelease {
142 pub name: String,
146
147 pub id: Option<String>,
150
151 pub id_like: Option<String>,
154
155 pub pretty_name: Option<String>,
159
160 pub cpe_name: Option<String>,
162
163 pub variant: Option<String>,
166
167 pub variant_id: Option<String>,
170
171 pub version: Option<String>,
175
176 pub version_id: Option<String>,
179
180 pub version_codename: Option<String>,
183
184 pub build_id: Option<String>,
187
188 pub image_id: Option<String>,
193
194 pub image_version: Option<String>,
198
199 pub home_url: Option<String>,
201
202 pub documentation_url: Option<String>,
204
205 pub support_url: Option<String>,
207
208 pub bug_report_url: Option<String>,
210
211 pub privacy_policy_url: Option<String>,
213
214 pub logo: Option<String>,
217
218 pub default_hostname: Option<String>,
221
222 pub sysext_level: Option<String>,
227}
228
229impl OsRelease {
230 pub fn new() -> Result<Self> {
231 let chunks = get_chunks_osrelease(read_to_string("/etc/os-release")?);
232 let mut osr = Self::default();
233 for chunk in chunks {
234 parse_osrelease(&mut osr, chunk);
235 }
236 Ok(osr)
237 }
238}
239
240impl ToJson for OsRelease {}
241
242fn get_chunks_osrelease(contents: String) -> Vec<(Option<String>, Option<String>)> {
243 contents
244 .lines()
245 .map(|item| {
246 let mut items = item.split('=').map(sanitize_str);
247 (items.next(), items.next())
248 })
249 .collect::<Vec<_>>()
250}
251
252fn parse_osrelease(osr: &mut OsRelease, chunk: (Option<String>, Option<String>)) {
253 match chunk {
254 (Some(key), Some(val)) => {
255 let key = &key as &str;
256 match key {
257 "NAME" => osr.name = val.to_string(),
258 "ID" => osr.id = Some(val.to_string()),
259 "ID_LIKE" => osr.id_like = Some(val.to_string()),
260 "PRETTY_NAME" => osr.pretty_name = Some(val.to_string()),
261 "CPE_NAME" => osr.cpe_name = Some(val.to_string()),
262 "VARIANT" => osr.variant = Some(val.to_string()),
263 "VARIANT_ID" => osr.variant_id = Some(val.to_string()),
264 "VERSION" => osr.version = Some(val.to_string()),
265 "VERSION_CODENAME" => osr.version_codename = Some(val.to_string()),
266 "VERSION_ID" => osr.version_id = Some(val.to_string()),
267 "BUILD_ID" => osr.build_id = Some(val.to_string()),
268 "IMAGE_ID" => osr.image_id = Some(val.to_string()),
269 "IMAGE_VERSION" => osr.image_version = Some(val.to_string()),
270 "HOME_URL" => osr.home_url = Some(val.to_string()),
271 "DOCUMENTATION_URL" => osr.documentation_url = Some(val.to_string()),
272 "SUPPORT_URL" => osr.support_url = Some(val.to_string()),
273 "BUG_REPORT_URL" => osr.bug_report_url = Some(val.to_string()),
274 "PRIVACY_POLICY_URL" => osr.privacy_policy_url = Some(val.to_string()),
275 "LOGO" => osr.logo = Some(val.to_string()),
276 "DEFAULT_HOSTNAME" => osr.default_hostname = Some(val.to_string()),
277 "SYSEXT_LEVEL" => osr.sysext_level = Some(val.to_string()),
278 _ => {}
279 }
280 }
281 _ => {}
282 }
283}
284
285#[derive(Debug, Serialize, Deserialize, Clone)]
287pub struct Shell {
288 pub path: String,
289 pub version: String,
290}
291
292impl Shell {
293 pub fn new() -> Result<Self> {
294 let path = env::var("SHELL")?;
295 let version = {
296 let stdout = Command::new(&path).arg("--version").output()?.stdout;
297 String::from_utf8(stdout)?
298 .lines()
299 .next()
300 .unwrap_or("")
301 .to_string()
302 };
303
304 Ok(Self { path, version })
305 }
306}
307
308impl Display for Shell {
309 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310 write!(f, "{} ({})", &self.version, &self.path)
311 }
312}
313
314#[derive(Debug, Serialize, Clone)]
316pub struct Uptime(
317 pub f32,
319 pub f32,
321);
322
323impl Uptime {
324 pub fn new() -> Result<Self> {
325 let data = read_to_string("/proc/uptime")?;
326 let mut chunks = data.split_whitespace();
327 match (chunks.next(), chunks.next()) {
328 (Some(a), Some(b)) => Ok(Self(a.parse()?, b.parse()?)),
329 _ => Err(anyhow!("`/proc/uptime` file format is incorrect!")),
330 }
331 }
332}
333
334impl ToPlainText for Uptime {
335 fn to_plain(&self) -> String {
336 format!(
337 "\nUptime: {} seconds; downtime: {} seconds\n",
338 self.0, self.1
339 )
340 }
341}
342
343#[derive(Debug, Serialize, Clone)]
345pub struct LoadAVG(
346 pub f32,
348 pub f32,
350 pub f32,
352);
353
354impl LoadAVG {
355 pub fn new() -> Result<Self> {
356 let data = read_to_string("/proc/loadavg")?;
357 let mut chunks = data.split_whitespace();
358 match (chunks.next(), chunks.next(), chunks.next()) {
359 (Some(a), Some(b), Some(c)) => Ok(Self(a.parse()?, b.parse()?, c.parse()?)),
360 _ => Err(anyhow!("`/proc/loadavg` file format is incorrect!")),
361 }
362 }
363}
364
365impl ToPlainText for LoadAVG {
366 fn to_plain(&self) -> String {
367 let mut s = format!("\nAverage system load:\n");
368 s += &print_val("1 minute", &self.0);
369 s += &print_val("5 minutes", &self.1);
370 s += &print_val("15 minutes", &self.2);
371
372 s
373 }
374}
375
376#[derive(Debug, Serialize, Clone)]
378pub struct Users {
379 pub users: Vec<User>,
380}
381
382impl ToJson for Users {}
383
384impl Users {
385 pub fn new() -> Result<Self> {
386 let mut users = vec![];
387 for user in read_to_string("/etc/passwd")?.lines() {
388 match User::try_from(user) {
389 Ok(user) => users.push(user),
390 Err(_) => continue,
391 }
392 }
393
394 Ok(Self { users })
395 }
396}
397
398#[derive(Debug, Serialize, Clone)]
400pub struct User {
401 pub name: String,
403
404 pub uid: u32,
413
414 pub gid: u32,
417
418 pub gecos: Option<String>,
427
428 pub home_dir: String,
430
431 pub login_shell: String,
435}
436
437impl TryFrom<&str> for User {
438 type Error = anyhow::Error;
439
440 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
441 let chunks = value
442 .trim()
443 .split(':')
444 .map(sanitize_str)
445 .collect::<Vec<_>>();
446 if chunks.len() != 7 {
447 return Err(anyhow!("Field \"{value}\" is incorrect user entry"));
448 }
449
450 Ok(Self {
451 name: sanitize_str(&chunks[0]),
452 uid: chunks[2].parse()?,
453 gid: chunks[3].parse()?,
454 gecos: match chunks[4].is_empty() {
455 true => None,
456 false => Some(sanitize_str(&chunks[4])),
457 },
458 home_dir: sanitize_str(&chunks[5]),
459 login_shell: sanitize_str(&chunks[6]),
460 })
461 }
462}
463
464impl ToPlainText for User {
465 fn to_plain(&self) -> String {
466 let mut s = format!("\nUser '{}':\n", &self.name);
467 s += &print_val("User ID", &self.uid);
468 s += &print_val("Group ID", &self.gid);
469 s += &print_opt_val("GECOS", &self.gecos);
470 s += &print_val("Home directory", &self.home_dir);
471 s += &print_val("Login shell", &self.login_shell);
472
473 s
474 }
475}
476
477pub fn current_user() -> Option<String> {
479 std::env::var("USER").ok()
480}
481
482#[derive(Debug, Serialize, Clone)]
484pub struct Groups {
485 pub groups: Vec<Group>,
486}
487
488impl ToJson for Groups {}
489
490impl Groups {
491 pub fn new() -> Result<Self> {
492 let mut groups = vec![];
493 for group in read_to_string("/etc/group")?.lines() {
494 match Group::try_from(group) {
495 Ok(group) => groups.push(group),
496 Err(_) => continue,
497 }
498 }
499 Ok(Self { groups })
500 }
501}
502
503#[derive(Debug, Serialize, Clone)]
505pub struct Group {
506 pub name: String,
508
509 pub gid: u32,
511
512 pub users: Vec<String>,
515}
516
517impl TryFrom<&str> for Group {
518 type Error = anyhow::Error;
519
520 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
521 let chunks = value
522 .trim()
523 .split(':')
524 .map(sanitize_str)
525 .collect::<Vec<_>>();
526 if chunks.len() != 4 {
527 return Err(anyhow!("Field \"{value}\" is incorrect group entry"));
528 }
529
530 Ok(Self {
531 name: chunks[0].to_string(),
532 gid: chunks[2].parse()?,
533 users: {
534 let mut users = vec![];
535 for user in chunks[3].split(',') {
536 if !user.is_empty() {
537 users.push(user.to_string());
538 }
539 }
540 users
541 },
542 })
543 }
544}
545
546pub type Shells = Vec<String>;
548
549fn get_shells() -> Result<Shells> {
550 let mut shells = vec![];
551 for shell in read_to_string("/etc/shells")?
552 .lines()
553 .filter(|line| !line.is_empty() && !line.starts_with('#'))
554 {
555 shells.push(shell.to_string());
556 }
557 Ok(shells)
558}
559
560pub type HostName = String;
562
563pub fn get_hostname() -> Option<HostName> {
564 match read_to_string("/etc/hostname") {
565 Ok(s) => Some(sanitize_str(&s)),
566 Err(_) => None,
567 }
568}
569
570#[derive(Debug, Serialize, Clone)]
572pub struct Locale {}
573
574fn sanitize_str(s: &str) -> String {
575 s.trim().replace('"', "").replace('\'', "")
576}
577
578#[derive(Debug, Serialize, Deserialize, Clone)]
580pub struct KModules {
581 pub modules: Vec<Module>,
582}
583
584impl ToJson for KModules {}
585
586impl KModules {
587 pub fn new() -> Result<Self> {
588 let contents = read_to_string("/proc/modules")?;
589 let contents = contents.lines();
590 let mut modules = Vec::new();
591
592 for s in contents {
593 modules.push(Module::try_from(s)?);
594 }
595
596 Ok(Self { modules })
597 }
598}
599
600#[derive(Debug, Serialize, Deserialize, Clone)]
601pub struct Module {
602 pub name: String,
604
605 pub size: Size,
607
608 pub instances: usize,
610
611 pub dependencies: String,
614
615 pub state: String,
617
618 pub memory_addrs: String,
622}
623
624impl TryFrom<&str> for Module {
625 type Error = anyhow::Error;
626 fn try_from(value: &str) -> Result<Self> {
627 let mut ch = value.split_whitespace();
628 match (
629 ch.next(),
630 ch.next(),
631 ch.next(),
632 ch.next(),
633 ch.next(),
634 ch.next(),
635 ) {
636 (
637 Some(name),
638 Some(size),
639 Some(instances),
640 Some(dependencies),
641 Some(state),
642 Some(memory_addrs),
643 ) => {
644 let size = size.parse::<u64>().map_err(|err| anyhow!("{err}"))?;
645 let instances = instances.parse::<usize>().map_err(|err| anyhow!("{err}"))?;
646
647 Ok(Self {
648 name: name.to_string(),
649 size: Size::B(size),
650 instances,
651 dependencies: dependencies.to_string(),
652 state: state.to_string(),
653 memory_addrs: memory_addrs.to_string(),
654 })
655 }
656 _ => Err(anyhow!("Unknown field: \"{value}\"")),
657 }
658 }
659}
660
661pub fn get_current_desktop() -> Option<String> {
662 var("XDG_CURRENT_DESKTOP").ok()
663}
664
665pub fn get_lang() -> Option<String> {
666 let lang = var("LANG").ok();
667 let lc_all = var("LC_ALL").ok();
668 if lang.is_some() { lang } else { lc_all }
669}
670
671pub fn get_env_vars() -> Vec<(String, String)> {
672 let mut vars = vars().collect::<Vec<(String, String)>>();
673 vars.sort_by_key(|v| v.0.clone());
674 vars
675}
676
677pub fn get_machine_id() -> Option<String> {
678 read_to_string("/etc/machine-id").ok()
679}