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 offset = {
95            let now_rt = get_clock_time(CLOCK_REALTIME)?;
96            let now_mono = get_clock_time(CLOCK_MONOTONIC)?;
97            now_rt.saturating_sub(now_mono)
98        };
99
100        let userspace_usec = self.finish_timestamp_mono.saturating_sub(self.userspace);
101        let kernel_usec = if self.kernel > 0 {
102            let kernel_timestamp_mono = self.kernel.saturating_sub(offset);
103            self.userspace.saturating_sub(kernel_timestamp_mono)
104        } else {
105            0
106        };
107        let loader_usec = if self.loader > 0 {
108            // if self.initrd_timestamp_mono > 0 {
109            //     self.initrd_timestamp_mono.saturating_sub(self.loader)
110            // } else {
111                self.userspace.saturating_sub(self.loader)
112            // }
113        } else {
114            0
115        };
116        let firmware_usec = if self.loader > 0 {
117            self.loader.saturating_sub(self.firmware)
118        } else {
119            0
120        };
121
122        self.firmware = firmware_usec;
123        self.loader = loader_usec;
124        self.kernel = kernel_usec;
125        self.userspace = userspace_usec;
126
127        self.total = firmware_usec + loader_usec + kernel_usec + userspace_usec;
128        Ok(())
129    }
130}
131
132fn get_clock_time(clock_id: i32) -> Result<u64> {
133    let mut tp = MaybeUninit::<timespec>::uninit();
134    let res = unsafe { clock_gettime(clock_id, tp.as_mut_ptr()) };
135    if res == 0 {
136        let tp = unsafe { tp.assume_init() };
137        Ok(tp.tv_sec as u64 * 1_000_000 + (tp.tv_nsec as u64 / 1_000))
138    } else {
139        Err(anyhow!(
140            "Failed to get clock_time: {}",
141            Error::last_os_error()
142        ))
143    }
144}
145
146fn unescape(s: &str) -> String {
147    s.replace("\\x20", " ")
148        .replace("\\x5c", "\\")
149        .replace("\\x2f", "/")
150        .replace("\\x2d", "-")
151}
152
153type ServiceTuple = (
154    String,
155    String,
156    String,
157    String,
158    String,
159    String,
160    OwnedObjectPath,
161    u32,
162    String,
163    OwnedObjectPath,
164);
165
166#[derive(Debug, Serialize, Clone)]
167pub struct ServiceInfo {
168    /// Unit name (e.g. `hibernate.target`)
169    pub name: String,
170
171    /// Unit description (e.g. `System Hibernation`)
172    pub description: String,
173
174    /// Load state
175    pub load_state: LoadState,
176
177    /// Active state
178    pub active_state: ActiveState,
179
180    /// Work state
181    pub work_state: WorkState,
182
183    /// Daemon path
184    pub daemon_path: String,
185
186    /// Job ID
187    pub job_id: u32,
188
189    /// Unit type
190    pub unit_type: UnitType,
191}
192
193impl ToPlainText for ServiceInfo {
194    fn to_plain(&self) -> String {
195        let mut s = format!("\nService \"{}\"\n", &self.name);
196        s += &print_val("Description", &self.description);
197        s += &print_val("Load state", &self.load_state);
198        s += &print_val("Active state", &self.active_state);
199        s += &print_val("Work state", &self.work_state);
200        s += &print_val("Daemon path", &self.daemon_path);
201        s += &print_val("Job ID", &self.job_id);
202        s += &print_val("Unit type", &self.unit_type);
203
204        s
205    }
206}
207
208impl ToJson for ServiceInfo {}
209
210impl From<ServiceTuple> for ServiceInfo {
211    fn from(value: ServiceTuple) -> Self {
212        Self {
213            name: unescape(&value.0),
214            description: unescape(&value.1),
215            load_state: LoadState::from(&value.2),
216            active_state: ActiveState::from(&value.3),
217            work_state: WorkState::from(&value.4),
218            daemon_path: unescape(&value.5),
219            job_id: value.7,
220            unit_type: UnitType::from(&value.8),
221        }
222    }
223}
224
225#[derive(Debug, Serialize, Clone)]
226pub enum LoadState {
227    Loaded,
228    Stub,
229    Masked,
230    NotFound,
231    Unknown(String),
232}
233
234impl Display for LoadState {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        write!(
237            f,
238            "{}",
239            match self {
240                Self::Loaded => "Loaded",
241                Self::Stub => "Stub",
242                Self::Masked => "Masked",
243                Self::NotFound => "Not found",
244                _ => "Unknown",
245            }
246        )
247    }
248}
249
250impl From<&String> for LoadState {
251    fn from(value: &String) -> Self {
252        match value as &str {
253            "loaded" => Self::Loaded,
254            "stub" => Self::Stub,
255            "masked" => Self::Masked,
256            "not-found" => Self::NotFound,
257            _ => Self::Unknown(value.to_string()),
258        }
259    }
260}
261
262#[derive(Debug, Serialize, Clone)]
263pub enum ActiveState {
264    Active,
265    Inactive,
266    Activating,
267    Deactivating,
268    Failed,
269    Unknown(String),
270}
271
272impl Display for ActiveState {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        write!(
275            f,
276            "{}",
277            match self {
278                Self::Active => "Active",
279                Self::Inactive => "Inactive",
280                Self::Activating => "Activating",
281                Self::Deactivating => "Deactivating",
282                Self::Failed => "Failed",
283                _ => "Unknown",
284            }
285        )
286    }
287}
288
289impl From<&String> for ActiveState {
290    fn from(value: &String) -> Self {
291        match value as &str {
292            "active" => Self::Active,
293            "inactive" => Self::Inactive,
294            "activating" => Self::Activating,
295            "deactivating" => Self::Deactivating,
296            "failed" => Self::Failed,
297            _ => Self::Unknown(value.to_string()),
298        }
299    }
300}
301
302#[derive(Debug, Serialize, Clone)]
303pub enum WorkState {
304    Active,
305    Running,
306    Exited,
307    Dead,
308    Mounted,
309    Mounting,
310    Plugged,
311    Listening,
312    Waiting,
313    Failed,
314    Unknown(String),
315}
316
317impl Display for WorkState {
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        write!(
320            f,
321            "{}",
322            match self {
323                Self::Active => "Active",
324                Self::Running => "Running",
325                Self::Exited => "Exited",
326                Self::Dead => "Dead",
327                Self::Mounted => "Mounted",
328                Self::Mounting => "Mounting",
329                Self::Plugged => "Plugged",
330                Self::Listening => "Listening",
331                Self::Waiting => "Waiting",
332                Self::Failed => "Failed",
333                _ => "Unknown",
334            }
335        )
336    }
337}
338
339impl From<&String> for WorkState {
340    fn from(value: &String) -> Self {
341        match value as &str {
342            "active" => Self::Active,
343            "running" => Self::Running,
344            "exited" => Self::Exited,
345            "dead" => Self::Dead,
346            "mounted" => Self::Mounted,
347            "mounting" => Self::Mounting,
348            "plugged" => Self::Plugged,
349            "listening" => Self::Listening,
350            "waiting" => Self::Waiting,
351            "failed" => Self::Failed,
352            _ => Self::Unknown(value.to_string()),
353        }
354    }
355}
356
357#[derive(Debug, Serialize, Clone)]
358pub enum UnitType {
359    Target,
360    Service,
361    Mount,
362    Swap,
363    None,
364    Unknown(String),
365}
366
367impl Display for UnitType {
368    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369        write!(
370            f,
371            "{}",
372            match self {
373                Self::Target => "Target",
374                Self::Service => "Service",
375                Self::Mount => "Mount",
376                Self::Swap => "Swap",
377                Self::None => "None-type",
378                _ => "Unknown",
379            }
380        )
381    }
382}
383
384impl From<&String> for UnitType {
385    fn from(value: &String) -> Self {
386        match value as &str {
387            "target" => Self::Target,
388            "service" => Self::Service,
389            "mount" => Self::Mount,
390            "swap" => Self::Swap,
391            "" => Self::None,
392            _ => Self::Unknown(value.to_string()),
393        }
394    }
395}