ferrix_lib/
sys.rs

1/* sys.rs
2 *
3 * Copyright 2025 Michail Krasnov <mskrasnov07@ya.ru>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21//! Get information about installed system
22
23use crate::utils::read_to_string;
24use crate::{traits::*, utils::Size};
25use anyhow::{Result, anyhow};
26use serde::{Deserialize, Serialize};
27use std::env::{var, vars};
28
29/// A structure containing all collected information about
30/// installed system
31#[derive(Debug, Serialize)]
32pub struct Sys {
33    /// Machine ID
34    pub machine_id: Option<String>,
35
36    /// Timezone
37    pub timezone: Option<String>,
38
39    /// Environment variables for current user
40    pub env_vars: Vec<(String, String)>,
41
42    /// Uptime
43    pub uptime: Uptime,
44
45    /// System load (average)
46    pub loadavg: LoadAVG,
47
48    /// List of installed shells
49    pub shells: Shells,
50
51    /// Host name
52    pub hostname: Option<HostName>,
53    // /// Current locale
54    // pub locale: Locale,
55}
56
57impl Sys {
58    pub fn new() -> Result<Self> {
59        Ok(Self {
60            machine_id: read_to_string("/etc/machine-id").ok(),
61            timezone: read_to_string("/etc/timezone").ok(),
62            env_vars: get_env_vars(),
63            uptime: Uptime::new()?,
64            loadavg: LoadAVG::new()?,
65            shells: get_shells()?,
66            hostname: get_hostname(),
67            // locale: todo!(),
68        })
69    }
70
71    pub fn update(&mut self) -> Result<()> {
72        self.uptime = Uptime::new()?;
73        self.loadavg = LoadAVG::new()?;
74        Ok(())
75    }
76}
77
78impl ToJson for Sys {}
79
80/// Information about Linux kernel
81#[derive(Debug, Serialize, Clone)]
82pub struct Kernel {
83    /// All data about kernel
84    pub uname: Option<String>, // /proc/version
85
86    /// Kernel command line
87    pub cmdline: Option<String>, // /proc/cmdline
88
89    /// Kernel architecture
90    pub arch: Option<String>, // /proc/sys/kernel/arch
91
92    /// Kernel version
93    pub version: Option<String>, // /proc/sys/kernel/osrelease
94
95    /// Kernel build info
96    pub build_info: Option<String>, // /proc/sys/kernel/version
97
98    /// Max processes count
99    pub pid_max: u32, // /proc/sys/kernel/pid_max
100
101    /// Max threads count
102    pub threads_max: u32, // /proc/sys/kernel/threads-max
103
104    /// Max user events
105    pub user_events_max: Option<u32>, // /proc/sys/kernel/user_events_max
106
107    /// Available enthropy
108    pub enthropy_avail: Option<u16>, // /proc/sys/kernel/random/entropy_avail
109}
110
111impl Kernel {
112    pub fn new() -> Result<Self> {
113        Ok(Self {
114            uname: read_to_string("/proc/version").ok(),
115            cmdline: read_to_string("/proc/cmdline").ok(),
116            arch: read_to_string("/proc/sys/kernel/arch").ok(),
117            version: read_to_string("/proc/sys/kernel/osrelease").ok(),
118            build_info: read_to_string("/proc/sys/kernel/version").ok(),
119            pid_max: read_to_string("/proc/sys/kernel/pid_max")?.parse()?,
120            threads_max: read_to_string("/proc/sys/kernel/threads-max")?.parse()?,
121            user_events_max: match read_to_string("/proc/sys/kernel/user_events_max").ok() {
122                Some(uem) => uem.parse().ok(),
123                None => None,
124            },
125            enthropy_avail: match read_to_string("/proc/sys/kernel/random/entropy_avail").ok() {
126                Some(ea) => ea.parse().ok(),
127                None => None,
128            },
129        })
130    }
131}
132
133impl ToJson for Kernel {}
134
135/// Information about installed distro from `/etc/os-release`
136///
137/// > Information from *[freedesktop](https://www.freedesktop.org/software/systemd/man/249/os-release.html)* portal.
138#[derive(Debug, Serialize, Default, Clone)]
139pub struct OsRelease {
140    /// The operating system name without a version component
141    ///
142    /// If not set, a default `Linux` value may be used
143    pub name: String,
144
145    /// A lower-case string identifying the OS, excluding any
146    /// version information
147    pub id: Option<String>,
148
149    /// A space-separated list of operating system identifiers in the
150    /// same syntax as the `id` param.
151    pub id_like: Option<String>,
152
153    /// A pretty OS name in a format suitable for presentation to
154    /// the user. May or may not contain a release code or OS version
155    /// of some kind, as suitable
156    pub pretty_name: Option<String>,
157
158    /// A CPE name for the OS, in URI binding syntax
159    pub cpe_name: Option<String>,
160
161    /// Specific variant or edition of the OS suitable for
162    /// presentation to the user
163    pub variant: Option<String>,
164
165    /// Lower-case string identifying a specific variant or edition
166    /// of the OS
167    pub variant_id: Option<String>,
168
169    /// The OS version, excluding any OS name information, possibly
170    /// including a release code name, and suitable for presentation
171    /// to the user
172    pub version: Option<String>,
173
174    /// A lower-case string identifying the OS version, excluding any
175    /// OS name information or release code name
176    pub version_id: Option<String>,
177
178    /// A lower-case string identifying the OS release code name,
179    /// excluding any OS name information or release version
180    pub version_codename: Option<String>,
181
182    /// A string uniquely identifying the system image originally
183    /// used as the installation base
184    pub build_id: Option<String>,
185
186    /// A lower-case string, identifying a specific image of the OS.
187    /// This is supposed to be used for envs where OS images are
188    /// prepared, built, shipped and updated as comprehensive,
189    /// consistent OS images
190    pub image_id: Option<String>,
191
192    /// A lower-case string identifying the OS image version. This is
193    /// supposed to be used together with `image_id` describes above,
194    /// to discern different versions of the same image
195    pub image_version: Option<String>,
196
197    /// Home URL of installed OS
198    pub home_url: Option<String>,
199
200    /// Documentation URL of installed OS
201    pub documentation_url: Option<String>,
202
203    /// Support URL of installed OS
204    pub support_url: Option<String>,
205
206    /// URL for bug reports
207    pub bug_report_url: Option<String>,
208
209    /// URL with information about privacy policy of the installed OS
210    pub privacy_policy_url: Option<String>,
211
212    /// A string, specifying the name of an icon as defined by
213    /// [freedesktop.org Icon Theme Specification](http://standards.freedesktop.org/icon-theme-spec/latest)
214    pub logo: Option<String>,
215
216    /// Default hostname if `hostname(5)` isn't present and no other
217    /// configuration source specifies the hostname
218    pub default_hostname: Option<String>,
219
220    /// A lower-case string identifying the OS extensions support
221    /// level, to indicate which extension images are supported.
222    ///
223    /// See [systemd-sysext(8)](https://www.freedesktop.org/software/systemd/man/249/systemd-sysext.html#) for more information
224    pub sysext_level: Option<String>,
225}
226
227impl OsRelease {
228    pub fn new() -> Result<Self> {
229        let chunks = get_chunks_osrelease(read_to_string("/etc/os-release")?);
230        let mut osr = Self::default();
231        for chunk in chunks {
232            parse_osrelease(&mut osr, chunk);
233        }
234        Ok(osr)
235    }
236}
237
238impl ToJson for OsRelease {}
239
240fn get_chunks_osrelease(contents: String) -> Vec<(Option<String>, Option<String>)> {
241    contents
242        .lines()
243        .map(|item| {
244            let mut items = item.split('=').map(sanitize_str);
245            (items.next(), items.next())
246        })
247        .collect::<Vec<_>>()
248}
249
250fn parse_osrelease(osr: &mut OsRelease, chunk: (Option<String>, Option<String>)) {
251    match chunk {
252        (Some(key), Some(val)) => {
253            let key = &key as &str;
254            match key {
255                "NAME" => osr.name = val.to_string(),
256                "ID" => osr.id = Some(val.to_string()),
257                "ID_LIKE" => osr.id_like = Some(val.to_string()),
258                "PRETTY_NAME" => osr.pretty_name = Some(val.to_string()),
259                "CPE_NAME" => osr.cpe_name = Some(val.to_string()),
260                "VARIANT" => osr.variant = Some(val.to_string()),
261                "VARIANT_ID" => osr.variant_id = Some(val.to_string()),
262                "VERSION" => osr.version = Some(val.to_string()),
263                "VERSION_CODENAME" => osr.version_codename = Some(val.to_string()),
264                "VERSION_ID" => osr.version_id = Some(val.to_string()),
265                "BUILD_ID" => osr.build_id = Some(val.to_string()),
266                "IMAGE_ID" => osr.image_id = Some(val.to_string()),
267                "IMAGE_VERSION" => osr.image_version = Some(val.to_string()),
268                "HOME_URL" => osr.home_url = Some(val.to_string()),
269                "DOCUMENTATION_URL" => osr.documentation_url = Some(val.to_string()),
270                "SUPPORT_URL" => osr.support_url = Some(val.to_string()),
271                "BUG_REPORT_URL" => osr.bug_report_url = Some(val.to_string()),
272                "PRIVACY_POLICY_URL" => osr.privacy_policy_url = Some(val.to_string()),
273                "LOGO" => osr.logo = Some(val.to_string()),
274                "DEFAULT_HOSTNAME" => osr.default_hostname = Some(val.to_string()),
275                "SYSEXT_LEVEL" => osr.sysext_level = Some(val.to_string()),
276                _ => {}
277            }
278        }
279        _ => {}
280    }
281}
282
283/// System uptime
284#[derive(Debug, Serialize, Clone)]
285pub struct Uptime(
286    /// Uptime
287    pub f32,
288    /// Downtime
289    pub f32,
290);
291
292impl Uptime {
293    pub fn new() -> Result<Self> {
294        let data = read_to_string("/proc/uptime")?;
295        let mut chunks = data.split_whitespace();
296        match (chunks.next(), chunks.next()) {
297            (Some(a), Some(b)) => Ok(Self(a.parse()?, b.parse()?)),
298            _ => Err(anyhow!("`/proc/uptime` file format is incorrect!")),
299        }
300    }
301}
302
303impl ToPlainText for Uptime {
304    fn to_plain(&self) -> String {
305        format!(
306            "\nUptime: {} seconds; downtime: {} seconds\n",
307            self.0, self.1
308        )
309    }
310}
311
312/// System load (average)
313#[derive(Debug, Serialize, Clone)]
314pub struct LoadAVG(
315    /// 1minute
316    pub f32,
317    /// 5minutes
318    pub f32,
319    /// 15minutes
320    pub f32,
321);
322
323impl LoadAVG {
324    pub fn new() -> Result<Self> {
325        let data = read_to_string("/proc/loadavg")?;
326        let mut chunks = data.split_whitespace();
327        match (chunks.next(), chunks.next(), chunks.next()) {
328            (Some(a), Some(b), Some(c)) => Ok(Self(a.parse()?, b.parse()?, c.parse()?)),
329            _ => Err(anyhow!("`/proc/loadavg` file format is incorrect!")),
330        }
331    }
332}
333
334impl ToPlainText for LoadAVG {
335    fn to_plain(&self) -> String {
336        let mut s = format!("\nAverage system load:\n");
337        s += &print_val("1 minute", &self.0);
338        s += &print_val("5 minutes", &self.1);
339        s += &print_val("15 minutes", &self.2);
340
341        s
342    }
343}
344
345/// Information about users
346#[derive(Debug, Serialize, Clone)]
347pub struct Users {
348    pub users: Vec<User>,
349}
350
351impl ToJson for Users {}
352
353impl Users {
354    pub fn new() -> Result<Self> {
355        let mut users = vec![];
356        for user in read_to_string("/etc/passwd")?.lines() {
357            match User::try_from(user) {
358                Ok(user) => users.push(user),
359                Err(_) => continue,
360            }
361        }
362
363        Ok(Self { users })
364    }
365}
366
367/// Information about followed user
368#[derive(Debug, Serialize, Clone)]
369pub struct User {
370    /// User's login name (case-sensitive, 1-32 characters)
371    pub name: String,
372
373    /// User ID
374    ///
375    /// ## Examples
376    /// | UID   | User name     |
377    /// |:-----:|---------------|
378    /// | 0     | `root`        |
379    /// | 1-999 | System users  |
380    /// | 1000+ | Regular users |
381    pub uid: u32,
382
383    /// Group ID links to `/etc/group` ([`Groups`]). Defines default
384    /// group ownership for new files
385    pub gid: u32,
386
387    /// Optional comment field (traditionally for user info). Often
388    /// holds:
389    ///
390    /// - Full name;
391    /// - Room number;
392    /// - Contact info;
393    ///
394    ///  Multiple entries comma-separated.
395    pub gecos: Option<String>,
396
397    /// Absolute path to the user's home directory
398    pub home_dir: String,
399
400    /// Absolute path to the user's default shell (e.g., `/bin/bash`).
401    /// If set to `/usr/sbin/nologin` or `/bin/false`, the user cannot
402    /// log in
403    pub login_shell: String,
404}
405
406impl TryFrom<&str> for User {
407    type Error = anyhow::Error;
408
409    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
410        let chunks = value
411            .trim()
412            .split(':')
413            .map(sanitize_str)
414            .collect::<Vec<_>>();
415        if chunks.len() != 7 {
416            return Err(anyhow!("Field \"{value}\" is incorrect user entry"));
417        }
418
419        Ok(Self {
420            name: sanitize_str(&chunks[0]),
421            uid: chunks[2].parse()?,
422            gid: chunks[3].parse()?,
423            gecos: match chunks[4].is_empty() {
424                true => None,
425                false => Some(sanitize_str(&chunks[4])),
426            },
427            home_dir: sanitize_str(&chunks[5]),
428            login_shell: sanitize_str(&chunks[6]),
429        })
430    }
431}
432
433impl ToPlainText for User {
434    fn to_plain(&self) -> String {
435        let mut s = format!("\nUser '{}':\n", &self.name);
436        s += &print_val("User ID", &self.uid);
437        s += &print_val("Group ID", &self.gid);
438        s += &print_opt_val("GECOS", &self.gecos);
439        s += &print_val("Home directory", &self.home_dir);
440        s += &print_val("Login shell", &self.login_shell);
441
442        s
443    }
444}
445
446/// Information about groups
447#[derive(Debug, Serialize, Clone)]
448pub struct Groups {
449    pub groups: Vec<Group>,
450}
451
452impl ToJson for Groups {}
453
454impl Groups {
455    pub fn new() -> Result<Self> {
456        let mut groups = vec![];
457        for group in read_to_string("/etc/group")?.lines() {
458            match Group::try_from(group) {
459                Ok(group) => groups.push(group),
460                Err(_) => continue,
461            }
462        }
463        Ok(Self { groups })
464    }
465}
466
467/// Information about followed group
468#[derive(Debug, Serialize, Clone)]
469pub struct Group {
470    /// Group name
471    pub name: String,
472
473    /// Group ID
474    pub gid: u32,
475
476    /// List of users (links to `/etc/passwd` ([`Users`])) in this
477    /// group
478    pub users: Vec<String>,
479}
480
481impl TryFrom<&str> for Group {
482    type Error = anyhow::Error;
483
484    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
485        let chunks = value
486            .trim()
487            .split(':')
488            .map(sanitize_str)
489            .collect::<Vec<_>>();
490        if chunks.len() != 4 {
491            return Err(anyhow!("Field \"{value}\" is incorrect group entry"));
492        }
493
494        Ok(Self {
495            name: chunks[0].to_string(),
496            gid: chunks[2].parse()?,
497            users: {
498                let mut users = vec![];
499                for user in chunks[3].split(',') {
500                    if !user.is_empty() {
501                        users.push(user.to_string());
502                    }
503                }
504                users
505            },
506        })
507    }
508}
509
510/// List of installed console shells
511pub type Shells = Vec<String>;
512
513fn get_shells() -> Result<Shells> {
514    let mut shells = vec![];
515    for shell in read_to_string("/etc/shells")?
516        .lines()
517        .filter(|line| !line.is_empty() && !line.starts_with('#'))
518    {
519        shells.push(shell.to_string());
520    }
521    Ok(shells)
522}
523
524/// Host name
525pub type HostName = String;
526
527pub fn get_hostname() -> Option<HostName> {
528    match read_to_string("/etc/hostname") {
529        Ok(s) => Some(sanitize_str(&s)),
530        Err(_) => None,
531    }
532}
533
534/// Information about current locale
535#[derive(Debug, Serialize, Clone)]
536pub struct Locale {}
537
538fn sanitize_str(s: &str) -> String {
539    s.trim().replace('"', "").replace('\'', "")
540}
541
542/// Linux kernel modules list
543#[derive(Debug, Serialize, Deserialize, Clone)]
544pub struct KModules {
545    pub modules: Vec<Module>,
546}
547
548impl KModules {
549    pub fn new() -> Result<Self> {
550        let contents = read_to_string("/proc/modules")?;
551        let contents = contents.lines();
552        let mut modules = Vec::new();
553
554        for s in contents {
555            modules.push(Module::try_from(s)?);
556        }
557
558        Ok(Self { modules })
559    }
560}
561
562#[derive(Debug, Serialize, Deserialize, Clone)]
563pub struct Module {
564    /// The name of the loaded kernel module
565    pub name: String,
566
567    /// Size of the module
568    pub size: Size,
569
570    /// Number of times the module is currently in use or loaded
571    pub instances: usize,
572
573    /// A comma-separated list of other modules that this module
574    /// depends on
575    pub dependencies: String,
576
577    /// The current state of the module
578    pub state: String,
579
580    /// The memory addresses where the module is loaded (may not
581    /// always be present or fully detailed depending on the kernel
582    /// version and configuration)
583    pub memory_addrs: String,
584}
585
586impl TryFrom<&str> for Module {
587    type Error = anyhow::Error;
588    fn try_from(value: &str) -> Result<Self> {
589        let mut ch = value.split_whitespace();
590        match (
591            ch.next(),
592            ch.next(),
593            ch.next(),
594            ch.next(),
595            ch.next(),
596            ch.next(),
597        ) {
598            (
599                Some(name),
600                Some(size),
601                Some(instances),
602                Some(dependencies),
603                Some(state),
604                Some(memory_addrs),
605            ) => {
606                let size = size.parse::<usize>().map_err(|err| anyhow!("{err}"))?;
607                let instances = instances.parse::<usize>().map_err(|err| anyhow!("{err}"))?;
608
609                Ok(Self {
610                    name: name.to_string(),
611                    size: Size::B(size),
612                    instances,
613                    dependencies: dependencies.to_string(),
614                    state: state.to_string(),
615                    memory_addrs: memory_addrs.to_string(),
616                })
617            }
618            _ => Err(anyhow!("Unknown field: \"{value}\"")),
619        }
620    }
621}
622
623pub fn get_current_desktop() -> Option<String> {
624    var("XDG_CURRENT_DESKTOP").ok()
625}
626
627pub fn get_lang() -> Option<String> {
628    let lang = var("LANG").ok();
629    let lc_all = var("LC_ALL").ok();
630    if lang.is_some() { lang } else { lc_all }
631}
632
633pub fn get_env_vars() -> Vec<(String, String)> {
634    let mut vars = vars().collect::<Vec<(String, String)>>();
635    vars.sort_by_key(|v| v.0.clone());
636    vars
637}