Skip to main content

ferrix_lib/
init.rs

1/* init.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 `systemd` services
22//! 
23//! ## Usage
24//! ```no-test
25//! use ferrix_lib::init::SystemdServices;
26//! use zbus::Connection;
27//! 
28//! let mut conn = Connection::system().await.unwrap();
29//! let systemd = SystemdServices::new_from_connection(&conn)
30//!     .await
31//!     .unwrap();
32//! 
33//! dbg!(systemd);
34//! ```
35
36use anyhow::{Result, anyhow};
37use libc::{CLOCK_MONOTONIC, CLOCK_REALTIME, clock_gettime, timespec};
38use serde::Serialize;
39use std::{fmt::Display, io::Error, mem::MaybeUninit};
40pub use zbus::{Connection, zvariant::OwnedObjectPath};
41use zbus_systemd::systemd1::ManagerProxy;
42
43use crate::traits::*;
44
45/// A structure containing information about `systemd` services
46#[derive(Debug, Serialize, Clone)]
47pub struct SystemdServices {
48    pub timestamps: BootTimestamps,
49    pub units: Vec<ServiceInfo>,
50}
51
52impl SystemdServices {
53    /// Get current systemd services
54    /// 
55    /// ## Usage
56    /// ```no-test
57    /// use ferrix_lib::init::SystemdServices;
58    /// use zbus::Connection;
59    /// 
60    /// let mut conn = Connection::system().await.unwrap();
61    /// let sysd = SystemdServices::new_from_connection(&conn)
62    ///     .await
63    ///     .unwrap();
64    /// dbg!(&sysd.units);
65    /// ```
66    pub async fn new_from_connection(conn: &Connection) -> Result<Self> {
67        let mgr = ManagerProxy::new(conn).await?;
68        let mut units = vec![];
69        for unit in mgr.list_units().await? {
70            units.push(ServiceInfo::from(unit));
71        }
72        let timestamps = BootTimestamps::get().await?;
73        Ok(Self { timestamps, units })
74    }
75}
76
77impl ToJson for SystemdServices {}
78
79impl ToPlainText for SystemdServices {
80    fn to_plain(&self) -> String {
81        let mut s = format!("\nSystemd services list:");
82        for service in &self.units {
83            s += &service.to_plain();
84        }
85
86        s
87    }
88}
89
90#[derive(Debug, Clone, Copy, Serialize, Default)]
91pub struct BootTimestamps {
92    pub firmware: u64,
93    pub loader: u64,
94    pub kernel: u64,
95    pub initrd_timestamp_mono: u64,
96    pub userspace: u64,
97    pub finish_timestamp_mono: u64,
98    pub total: u64,
99}
100
101impl BootTimestamps {
102    pub async fn get<'a>() -> Result<Self> {
103        let conn = zbus::Connection::system().await?;
104        let mgr = ManagerProxy::new(&conn).await?;
105        Ok(Self {
106            firmware: mgr.cached_firmware_timestamp_monotonic()?.unwrap_or(0),
107            loader: mgr.loader_timestamp_monotonic().await?,
108            kernel: mgr.kernel_timestamp().await?,
109            initrd_timestamp_mono: mgr.init_rd_timestamp_monotonic().await?,
110            userspace: mgr.userspace_timestamp_monotonic().await?,
111            finish_timestamp_mono: mgr.finish_timestamp_monotonic().await?,
112            total: 0,
113        })
114    }
115
116    pub fn calc_boot_time(&mut self) -> Result<()> {
117        if self.userspace == 0 || self.finish_timestamp_mono == 0 {
118            return Err(anyhow!("Failed to get system load time: not enough data"));
119        }
120        let offset = {
121            let now_rt = get_clock_time(CLOCK_REALTIME)?;
122            let now_mono = get_clock_time(CLOCK_MONOTONIC)?;
123            now_rt.saturating_sub(now_mono)
124        };
125
126        let userspace_usec = self.finish_timestamp_mono.saturating_sub(self.userspace);
127        let kernel_usec = if self.kernel > 0 {
128            let kernel_timestamp_mono = self.kernel.saturating_sub(offset);
129            self.userspace.saturating_sub(kernel_timestamp_mono)
130        } else {
131            0
132        };
133        let loader_usec = if self.loader > 0 {
134            // if self.initrd_timestamp_mono > 0 {
135            //     self.initrd_timestamp_mono.saturating_sub(self.loader)
136            // } else {
137                self.userspace.saturating_sub(self.loader)
138            // }
139        } else {
140            0
141        };
142        let firmware_usec = if self.loader > 0 {
143            self.loader.saturating_sub(self.firmware)
144        } else {
145            0
146        };
147
148        self.firmware = firmware_usec;
149        self.loader = loader_usec;
150        self.kernel = kernel_usec;
151        self.userspace = userspace_usec;
152
153        self.total = firmware_usec + loader_usec + kernel_usec + userspace_usec;
154        Ok(())
155    }
156}
157
158fn get_clock_time(clock_id: i32) -> Result<u64> {
159    let mut tp = MaybeUninit::<timespec>::uninit();
160    let res = unsafe { clock_gettime(clock_id, tp.as_mut_ptr()) };
161    if res == 0 {
162        let tp = unsafe { tp.assume_init() };
163        Ok(tp.tv_sec as u64 * 1_000_000 + (tp.tv_nsec as u64 / 1_000))
164    } else {
165        Err(anyhow!(
166            "Failed to get clock_time: {}",
167            Error::last_os_error()
168        ))
169    }
170}
171
172fn unescape(s: &str) -> String {
173    s.replace("\\x20", " ")
174        .replace("\\x5c", "\\")
175        .replace("\\x2f", "/")
176        .replace("\\x2d", "-")
177}
178
179type ServiceTuple = (
180    String,
181    String,
182    String,
183    String,
184    String,
185    String,
186    OwnedObjectPath,
187    u32,
188    String,
189    OwnedObjectPath,
190);
191
192#[derive(Debug, Serialize, Clone)]
193pub struct ServiceInfo {
194    /// Unit name (e.g. `hibernate.target`)
195    pub name: String,
196
197    /// Unit description (e.g. `System Hibernation`)
198    pub description: String,
199
200    /// Load state
201    pub load_state: LoadState,
202
203    /// Active state
204    pub active_state: ActiveState,
205
206    /// Work state
207    pub work_state: WorkState,
208
209    /// Daemon path
210    pub daemon_path: String,
211
212    /// Job ID
213    pub job_id: u32,
214
215    /// Unit type
216    pub unit_type: UnitType,
217}
218
219impl ToPlainText for ServiceInfo {
220    fn to_plain(&self) -> String {
221        let mut s = format!("\nService \"{}\"\n", &self.name);
222        s += &print_val("Description", &self.description);
223        s += &print_val("Load state", &self.load_state);
224        s += &print_val("Active state", &self.active_state);
225        s += &print_val("Work state", &self.work_state);
226        s += &print_val("Daemon path", &self.daemon_path);
227        s += &print_val("Job ID", &self.job_id);
228        s += &print_val("Unit type", &self.unit_type);
229
230        s
231    }
232}
233
234impl ToJson for ServiceInfo {}
235
236impl From<ServiceTuple> for ServiceInfo {
237    fn from(value: ServiceTuple) -> Self {
238        Self {
239            name: unescape(&value.0),
240            description: unescape(&value.1),
241            load_state: LoadState::from(&value.2),
242            active_state: ActiveState::from(&value.3),
243            work_state: WorkState::from(&value.4),
244            daemon_path: unescape(&value.5),
245            job_id: value.7,
246            unit_type: UnitType::from(&value.8),
247        }
248    }
249}
250
251#[derive(Debug, Serialize, Clone)]
252pub enum LoadState {
253    Loaded,
254    Stub,
255    Masked,
256    NotFound,
257    Unknown(String),
258}
259
260impl Display for LoadState {
261    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262        write!(
263            f,
264            "{}",
265            match self {
266                Self::Loaded => "Loaded",
267                Self::Stub => "Stub",
268                Self::Masked => "Masked",
269                Self::NotFound => "Not found",
270                _ => "Unknown",
271            }
272        )
273    }
274}
275
276impl From<&String> for LoadState {
277    fn from(value: &String) -> Self {
278        match value as &str {
279            "loaded" => Self::Loaded,
280            "stub" => Self::Stub,
281            "masked" => Self::Masked,
282            "not-found" => Self::NotFound,
283            _ => Self::Unknown(value.to_string()),
284        }
285    }
286}
287
288#[derive(Debug, Serialize, Clone)]
289pub enum ActiveState {
290    Active,
291    Inactive,
292    Activating,
293    Deactivating,
294    Failed,
295    Unknown(String),
296}
297
298impl Display for ActiveState {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        write!(
301            f,
302            "{}",
303            match self {
304                Self::Active => "Active",
305                Self::Inactive => "Inactive",
306                Self::Activating => "Activating",
307                Self::Deactivating => "Deactivating",
308                Self::Failed => "Failed",
309                _ => "Unknown",
310            }
311        )
312    }
313}
314
315impl From<&String> for ActiveState {
316    fn from(value: &String) -> Self {
317        match value as &str {
318            "active" => Self::Active,
319            "inactive" => Self::Inactive,
320            "activating" => Self::Activating,
321            "deactivating" => Self::Deactivating,
322            "failed" => Self::Failed,
323            _ => Self::Unknown(value.to_string()),
324        }
325    }
326}
327
328#[derive(Debug, Serialize, Clone)]
329pub enum WorkState {
330    Active,
331    Running,
332    Exited,
333    Dead,
334    Mounted,
335    Mounting,
336    Plugged,
337    Listening,
338    Waiting,
339    Failed,
340    Unknown(String),
341}
342
343impl Display for WorkState {
344    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345        write!(
346            f,
347            "{}",
348            match self {
349                Self::Active => "Active",
350                Self::Running => "Running",
351                Self::Exited => "Exited",
352                Self::Dead => "Dead",
353                Self::Mounted => "Mounted",
354                Self::Mounting => "Mounting",
355                Self::Plugged => "Plugged",
356                Self::Listening => "Listening",
357                Self::Waiting => "Waiting",
358                Self::Failed => "Failed",
359                _ => "Unknown",
360            }
361        )
362    }
363}
364
365impl From<&String> for WorkState {
366    fn from(value: &String) -> Self {
367        match value as &str {
368            "active" => Self::Active,
369            "running" => Self::Running,
370            "exited" => Self::Exited,
371            "dead" => Self::Dead,
372            "mounted" => Self::Mounted,
373            "mounting" => Self::Mounting,
374            "plugged" => Self::Plugged,
375            "listening" => Self::Listening,
376            "waiting" => Self::Waiting,
377            "failed" => Self::Failed,
378            _ => Self::Unknown(value.to_string()),
379        }
380    }
381}
382
383#[derive(Debug, Serialize, Clone)]
384pub enum UnitType {
385    Target,
386    Service,
387    Mount,
388    Swap,
389    None,
390    Unknown(String),
391}
392
393impl Display for UnitType {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        write!(
396            f,
397            "{}",
398            match self {
399                Self::Target => "Target",
400                Self::Service => "Service",
401                Self::Mount => "Mount",
402                Self::Swap => "Swap",
403                Self::None => "None-type",
404                _ => "Unknown",
405            }
406        )
407    }
408}
409
410impl From<&String> for UnitType {
411    fn from(value: &String) -> Self {
412        match value as &str {
413            "target" => Self::Target,
414            "service" => Self::Service,
415            "mount" => Self::Mount,
416            "swap" => Self::Swap,
417            "" => Self::None,
418            _ => Self::Unknown(value.to_string()),
419        }
420    }
421}