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