Skip to main content

ferrix_lib/
sys.rs

1/* sys.rs
2 *
3 * Copyright 2025-2026 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::{self, var, vars};
28use std::fmt::Display;
29use std::process::Command;
30
31/// A structure containing all collected information about
32/// installed system
33#[derive(Debug, Serialize)]
34pub struct Sys {
35    /// Machine ID
36    pub machine_id: Option<String>,
37
38    /// Timezone
39    pub timezone: Option<String>,
40
41    /// Environment variables for current user
42    pub env_vars: Vec<(String, String)>,
43
44    /// Uptime
45    pub uptime: Uptime,
46
47    /// System load (average)
48    pub loadavg: LoadAVG,
49
50    /// List of installed shells
51    pub shells: Shells,
52
53    /// Host name
54    pub hostname: Option<HostName>,
55    // /// Current locale
56    // pub locale: Locale,
57}
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            // locale: todo!(),
70        })
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/// Information about Linux kernel
83#[derive(Debug, Serialize, Clone)]
84pub struct Kernel {
85    /// All data about kernel
86    pub uname: Option<String>, // /proc/version
87
88    /// Kernel command line
89    pub cmdline: Option<String>, // /proc/cmdline
90
91    /// Kernel architecture
92    pub arch: Option<String>, // /proc/sys/kernel/arch
93
94    /// Kernel version
95    pub version: Option<String>, // /proc/sys/kernel/osrelease
96
97    /// Kernel build info
98    pub build_info: Option<String>, // /proc/sys/kernel/version
99
100    /// Max processes count
101    pub pid_max: u32, // /proc/sys/kernel/pid_max
102
103    /// Max threads count
104    pub threads_max: u32, // /proc/sys/kernel/threads-max
105
106    /// Max user events
107    pub user_events_max: Option<u32>, // /proc/sys/kernel/user_events_max
108
109    /// Available enthropy
110    pub enthropy_avail: Option<u16>, // /proc/sys/kernel/random/entropy_avail
111}
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/// Information about installed distro from `/etc/os-release`
138///
139/// > Information from *[freedesktop](https://www.freedesktop.org/software/systemd/man/249/os-release.html)* portal.
140#[derive(Debug, Serialize, Default, Clone)]
141pub struct OsRelease {
142    /// The operating system name without a version component
143    ///
144    /// If not set, a default `Linux` value may be used
145    pub name: String,
146
147    /// A lower-case string identifying the OS, excluding any
148    /// version information
149    pub id: Option<String>,
150
151    /// A space-separated list of operating system identifiers in the
152    /// same syntax as the `id` param.
153    pub id_like: Option<String>,
154
155    /// A pretty OS name in a format suitable for presentation to
156    /// the user. May or may not contain a release code or OS version
157    /// of some kind, as suitable
158    pub pretty_name: Option<String>,
159
160    /// A CPE name for the OS, in URI binding syntax
161    pub cpe_name: Option<String>,
162
163    /// Specific variant or edition of the OS suitable for
164    /// presentation to the user
165    pub variant: Option<String>,
166
167    /// Lower-case string identifying a specific variant or edition
168    /// of the OS
169    pub variant_id: Option<String>,
170
171    /// The OS version, excluding any OS name information, possibly
172    /// including a release code name, and suitable for presentation
173    /// to the user
174    pub version: Option<String>,
175
176    /// A lower-case string identifying the OS version, excluding any
177    /// OS name information or release code name
178    pub version_id: Option<String>,
179
180    /// A lower-case string identifying the OS release code name,
181    /// excluding any OS name information or release version
182    pub version_codename: Option<String>,
183
184    /// A string uniquely identifying the system image originally
185    /// used as the installation base
186    pub build_id: Option<String>,
187
188    /// A lower-case string, identifying a specific image of the OS.
189    /// This is supposed to be used for envs where OS images are
190    /// prepared, built, shipped and updated as comprehensive,
191    /// consistent OS images
192    pub image_id: Option<String>,
193
194    /// A lower-case string identifying the OS image version. This is
195    /// supposed to be used together with `image_id` describes above,
196    /// to discern different versions of the same image
197    pub image_version: Option<String>,
198
199    /// Home URL of installed OS
200    pub home_url: Option<String>,
201
202    /// Documentation URL of installed OS
203    pub documentation_url: Option<String>,
204
205    /// Support URL of installed OS
206    pub support_url: Option<String>,
207
208    /// URL for bug reports
209    pub bug_report_url: Option<String>,
210
211    /// URL with information about privacy policy of the installed OS
212    pub privacy_policy_url: Option<String>,
213
214    /// A string, specifying the name of an icon as defined by
215    /// [freedesktop.org Icon Theme Specification](http://standards.freedesktop.org/icon-theme-spec/latest)
216    pub logo: Option<String>,
217
218    /// Default hostname if `hostname(5)` isn't present and no other
219    /// configuration source specifies the hostname
220    pub default_hostname: Option<String>,
221
222    /// A lower-case string identifying the OS extensions support
223    /// level, to indicate which extension images are supported.
224    ///
225    /// See [systemd-sysext(8)](https://www.freedesktop.org/software/systemd/man/249/systemd-sysext.html#) for more information
226    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/// System command shell
286#[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/// System uptime
315#[derive(Debug, Serialize, Clone)]
316pub struct Uptime(
317    /// Uptime
318    pub f32,
319    /// Downtime
320    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/// System load (average)
344#[derive(Debug, Serialize, Clone)]
345pub struct LoadAVG(
346    /// 1minute
347    pub f32,
348    /// 5minutes
349    pub f32,
350    /// 15minutes
351    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/// Information about users
377#[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/// Information about followed user
399#[derive(Debug, Serialize, Clone)]
400pub struct User {
401    /// User's login name (case-sensitive, 1-32 characters)
402    pub name: String,
403
404    /// User ID
405    ///
406    /// ## Examples
407    /// | UID   | User name     |
408    /// |:-----:|---------------|
409    /// | 0     | `root`        |
410    /// | 1-999 | System users  |
411    /// | 1000+ | Regular users |
412    pub uid: u32,
413
414    /// Group ID links to `/etc/group` ([`Groups`]). Defines default
415    /// group ownership for new files
416    pub gid: u32,
417
418    /// Optional comment field (traditionally for user info). Often
419    /// holds:
420    ///
421    /// - Full name;
422    /// - Room number;
423    /// - Contact info;
424    ///
425    ///  Multiple entries comma-separated.
426    pub gecos: Option<String>,
427
428    /// Absolute path to the user's home directory
429    pub home_dir: String,
430
431    /// Absolute path to the user's default shell (e.g., `/bin/bash`).
432    /// If set to `/usr/sbin/nologin` or `/bin/false`, the user cannot
433    /// log in
434    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
477/// Get current user string (user name)
478pub fn current_user() -> Option<String> {
479    std::env::var("USER").ok()
480}
481
482/// Information about groups
483#[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/// Information about followed group
504#[derive(Debug, Serialize, Clone)]
505pub struct Group {
506    /// Group name
507    pub name: String,
508
509    /// Group ID
510    pub gid: u32,
511
512    /// List of users (links to `/etc/passwd` ([`Users`])) in this
513    /// group
514    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
546/// List of installed console shells
547pub 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
560/// Host name
561pub 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/// Information about current locale
571#[derive(Debug, Serialize, Clone)]
572pub struct Locale {}
573
574fn sanitize_str(s: &str) -> String {
575    s.trim().replace('"', "").replace('\'', "")
576}
577
578/// Linux kernel modules list
579#[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    /// The name of the loaded kernel module
603    pub name: String,
604
605    /// Size of the module
606    pub size: Size,
607
608    /// Number of times the module is currently in use or loaded
609    pub instances: usize,
610
611    /// A comma-separated list of other modules that this module
612    /// depends on
613    pub dependencies: String,
614
615    /// The current state of the module
616    pub state: String,
617
618    /// The memory addresses where the module is loaded (may not
619    /// always be present or fully detailed depending on the kernel
620    /// version and configuration)
621    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}