systemctl_tui/
systemd.rs

1// File initially taken from https://github.com/servicer-labs/servicer/blob/master/src/utils/systemd.rs, since modified
2
3use core::str;
4use std::process::Command;
5
6use anyhow::{bail, Context, Result};
7use log::error;
8use tokio_util::sync::CancellationToken;
9use tracing::info;
10use zbus::{proxy, zvariant, Connection};
11
12#[derive(Debug, Clone)]
13pub struct UnitWithStatus {
14  pub name: String,                              // The primary unit name as string
15  pub scope: UnitScope,                          // System or user?
16  pub description: String,                       // The human readable description string
17  pub file_path: Option<Result<String, String>>, // The unit file path - populated later on demand
18
19  pub load_state: String, // The load state (i.e. whether the unit file has been loaded successfully)
20
21  // Some comments re: state from this helpful comment: https://www.reddit.com/r/linuxquestions/comments/r58dvz/comment/hmlemfk/
22  /// One state, called the "activation state", essentially describes what the unit is doing now. The two most common values for this state are active and inactive, though there are a few other possibilities. (Each unit type has its own set of "substates" that map to these activation states. For instance, service units can be running or stopped. Again, there's a variety of other substates, and the list differs for each unit type.)
23  pub activation_state: String,
24  /// The sub state (a more fine-grained version of the active state that is specific to the unit type, which the active state is not)
25  pub sub_state: String,
26
27  /// The other state all units have is called the "enablement state". It describes how the unit might be automatically started in the future. A unit is enabled if it has been added to the requirements list of any other unit though symlinks in the filesystem. The set of symlinks to be created when enabling a unit is described by the unit's [Install] section. A unit is disabled if no symlinks are present. Again there's a variety of other values other than these two (e.g. not all units even have [Install] sections).
28  /// Only populated when needed b/c this is much slower to get
29  pub enablement_state: Option<String>,
30  // We don't use any of these right now, might as well skip'em so there's less data to clone
31  // pub followed: String, // A unit that is being followed in its state by this unit, if there is any, otherwise the empty string.
32  // pub path: String,     // The unit object path
33  // pub job_id: u32,      // If there is a job queued for the job unit the numeric job id, 0 otherwise
34  // pub job_type: String, // The job type as string
35  // pub job_path: String, // The job object path
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub enum UnitScope {
40  Global,
41  User,
42}
43
44/// Just enough info to fully identify a unit
45#[derive(Debug, Clone, PartialEq, Eq, Hash)]
46pub struct UnitId {
47  pub name: String,
48  pub scope: UnitScope,
49}
50
51impl UnitWithStatus {
52  pub fn is_active(&self) -> bool {
53    self.activation_state == "active"
54  }
55
56  pub fn is_failed(&self) -> bool {
57    self.activation_state == "failed"
58  }
59
60  pub fn is_not_found(&self) -> bool {
61    self.load_state == "not-found"
62  }
63
64  pub fn is_enabled(&self) -> bool {
65    self.load_state == "loaded" && self.activation_state == "active"
66  }
67
68  pub fn short_name(&self) -> &str {
69    if self.name.ends_with(".service") {
70      &self.name[..self.name.len() - 8]
71    } else {
72      &self.name
73    }
74  }
75
76  // TODO: should we have a non-allocating version of this?
77  pub fn id(&self) -> UnitId {
78    UnitId { name: self.name.clone(), scope: self.scope }
79  }
80
81  // useful for updating without wiping out the file path
82  pub fn update(&mut self, other: UnitWithStatus) {
83    self.description = other.description;
84    self.load_state = other.load_state;
85    self.activation_state = other.activation_state;
86    self.sub_state = other.sub_state;
87  }
88}
89
90type RawUnit =
91  (String, String, String, String, String, String, zvariant::OwnedObjectPath, u32, String, zvariant::OwnedObjectPath);
92
93fn to_unit_status(raw_unit: RawUnit, scope: UnitScope) -> UnitWithStatus {
94  let (name, description, load_state, active_state, sub_state, _followed, _path, _job_id, _job_type, _job_path) =
95    raw_unit;
96
97  UnitWithStatus {
98    name,
99    scope,
100    description,
101    file_path: None,
102    enablement_state: None,
103    load_state,
104    activation_state: active_state,
105    sub_state,
106  }
107}
108
109// Different from UnitScope in that this is not for 1 specific unit (i.e. it can include multiple scopes)
110#[derive(Clone, Copy, Default, Debug)]
111pub enum Scope {
112  Global,
113  User,
114  #[default]
115  All,
116}
117
118// this takes like 5-10 ms on 13th gen Intel i7 (scope=all)
119pub async fn get_all_services(scope: Scope, services: &[String]) -> Result<Vec<UnitWithStatus>> {
120  let start = std::time::Instant::now();
121
122  let mut units = vec![];
123
124  let is_root = nix::unistd::geteuid().is_root();
125
126  match scope {
127    Scope::Global => {
128      let system_units = get_services(UnitScope::Global, services).await?;
129      units.extend(system_units);
130    },
131    Scope::User => {
132      let user_units = get_services(UnitScope::User, services).await?;
133      units.extend(user_units);
134    },
135    Scope::All => {
136      let (system_units, user_units) =
137        tokio::join!(get_services(UnitScope::Global, services), get_services(UnitScope::User, services));
138      units.extend(system_units?);
139
140      // Should always be able to get user units, but it may fail when running as root
141      if let Ok(user_units) = user_units {
142        units.extend(user_units);
143      } else if is_root {
144        error!("Failed to get user units, ignoring because we're running as root")
145      } else {
146        user_units?;
147      }
148    },
149  }
150
151  // sort by name case-insensitive
152  units.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
153
154  info!("Loaded systemd services in {:?}", start.elapsed());
155
156  Ok(units)
157}
158
159async fn get_services(scope: UnitScope, services: &[String]) -> Result<Vec<UnitWithStatus>, anyhow::Error> {
160  let connection = get_connection(scope).await?;
161  let manager_proxy = ManagerProxy::new(&connection).await?;
162  let units = manager_proxy.list_units_by_patterns(vec![], services.to_vec()).await?;
163  let units: Vec<_> = units.into_iter().map(|u| to_unit_status(u, scope)).collect();
164  Ok(units)
165}
166
167pub fn get_unit_file_location(service: &UnitId) -> Result<String> {
168  // show -P FragmentPath reitunes.service
169  let mut args = vec!["--quiet", "show", "-P", "FragmentPath"];
170  args.push(&service.name);
171
172  if service.scope == UnitScope::User {
173    args.insert(0, "--user");
174  }
175
176  let output = Command::new("systemctl").args(&args).output()?;
177
178  if output.status.success() {
179    let path = str::from_utf8(&output.stdout)?.trim();
180    if path.is_empty() {
181      bail!("No unit file found for {}", service.name);
182    }
183    Ok(path.trim().to_string())
184  } else {
185    let stderr = String::from_utf8(output.stderr)?;
186    bail!(stderr);
187  }
188}
189
190pub async fn start_service(service: UnitId, cancel_token: CancellationToken) -> Result<()> {
191  async fn start_service(service: UnitId) -> Result<()> {
192    let connection = get_connection(service.scope).await?;
193    let manager_proxy = ManagerProxy::new(&connection).await?;
194    manager_proxy.start_unit(service.name.clone(), "replace".into()).await?;
195    Ok(())
196  }
197
198  // god these select macros are ugly, is there really no better way to select?
199  tokio::select! {
200    _ = cancel_token.cancelled() => {
201        anyhow::bail!("cancelled");
202    }
203    result = start_service(service) => {
204        result
205    }
206  }
207}
208
209pub async fn stop_service(service: UnitId, cancel_token: CancellationToken) -> Result<()> {
210  async fn stop_service(service: UnitId) -> Result<()> {
211    let connection = get_connection(service.scope).await?;
212    let manager_proxy = ManagerProxy::new(&connection).await?;
213    manager_proxy.stop_unit(service.name, "replace".into()).await?;
214    Ok(())
215  }
216
217  // god these select macros are ugly, is there really no better way to select?
218  tokio::select! {
219    _ = cancel_token.cancelled() => {
220        anyhow::bail!("cancelled");
221    }
222    result = stop_service(service) => {
223        result
224    }
225  }
226}
227
228pub async fn reload(scope: UnitScope, cancel_token: CancellationToken) -> Result<()> {
229  async fn reload_(scope: UnitScope) -> Result<()> {
230    let connection = get_connection(scope).await?;
231    let manager_proxy: ManagerProxy<'_> = ManagerProxy::new(&connection).await?;
232    let error_message = match scope {
233      UnitScope::Global => "Failed to reload units, probably because superuser permissions are needed. Try running `sudo systemctl daemon-reload`",
234      UnitScope::User => "Failed to reload units. Try running `systemctl --user daemon-reload`",
235    };
236    manager_proxy.reload().await.context(error_message)?;
237    Ok(())
238  }
239
240  // god these select macros are ugly, is there really no better way to select?
241  tokio::select! {
242    _ = cancel_token.cancelled() => {
243        anyhow::bail!("cancelled");
244    }
245    result = reload_(scope) => {
246        result
247    }
248  }
249}
250
251async fn get_connection(scope: UnitScope) -> Result<Connection, anyhow::Error> {
252  match scope {
253    UnitScope::Global => Ok(Connection::system().await?),
254    UnitScope::User => Ok(Connection::session().await?),
255  }
256}
257
258pub async fn restart_service(service: UnitId, cancel_token: CancellationToken) -> Result<()> {
259  async fn restart(service: UnitId) -> Result<()> {
260    let connection = get_connection(service.scope).await?;
261    let manager_proxy = ManagerProxy::new(&connection).await?;
262    manager_proxy.restart_unit(service.name, "replace".into()).await?;
263    Ok(())
264  }
265
266  // god these select macros are ugly, is there really no better way to select?
267  tokio::select! {
268    _ = cancel_token.cancelled() => {
269        // The token was cancelled
270        anyhow::bail!("cancelled");
271    }
272    result = restart(service) => {
273        result
274    }
275  }
276}
277
278// useless function only added to test that cancellation works
279pub async fn sleep_test(_service: String, cancel_token: CancellationToken) -> Result<()> {
280  // god these select macros are ugly, is there really no better way to select?
281  tokio::select! {
282      _ = cancel_token.cancelled() => {
283          // The token was cancelled
284          anyhow::bail!("cancelled");
285      }
286      _ = tokio::time::sleep(std::time::Duration::from_secs(2)) => {
287          Ok(())
288      }
289  }
290}
291
292/// Proxy object for `org.freedesktop.systemd1.Manager`.
293/// Partially taken from https://github.com/lucab/zbus_systemd/blob/main/src/systemd1/generated.rs
294#[proxy(
295  interface = "org.freedesktop.systemd1.Manager",
296  default_service = "org.freedesktop.systemd1",
297  default_path = "/org/freedesktop/systemd1",
298  gen_blocking = false
299)]
300pub trait Manager {
301  /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#StartUnit()) Call interface method `StartUnit`.
302  #[dbus_proxy(name = "StartUnit")]
303  fn start_unit(&self, name: String, mode: String) -> zbus::Result<zvariant::OwnedObjectPath>;
304
305  /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#StopUnit()) Call interface method `StopUnit`.
306  #[dbus_proxy(name = "StopUnit")]
307  fn stop_unit(&self, name: String, mode: String) -> zbus::Result<zvariant::OwnedObjectPath>;
308
309  /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#ReloadUnit()) Call interface method `ReloadUnit`.
310  #[dbus_proxy(name = "ReloadUnit")]
311  fn reload_unit(&self, name: String, mode: String) -> zbus::Result<zvariant::OwnedObjectPath>;
312
313  /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#RestartUnit()) Call interface method `RestartUnit`.
314  #[dbus_proxy(name = "RestartUnit")]
315  fn restart_unit(&self, name: String, mode: String) -> zbus::Result<zvariant::OwnedObjectPath>;
316
317  /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#EnableUnitFiles()) Call interface method `EnableUnitFiles`.
318  #[dbus_proxy(name = "EnableUnitFiles")]
319  fn enable_unit_files(
320    &self,
321    files: Vec<String>,
322    runtime: bool,
323    force: bool,
324  ) -> zbus::Result<(bool, Vec<(String, String, String)>)>;
325
326  /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#DisableUnitFiles()) Call interface method `DisableUnitFiles`.
327  #[dbus_proxy(name = "DisableUnitFiles")]
328  fn disable_unit_files(&self, files: Vec<String>, runtime: bool) -> zbus::Result<Vec<(String, String, String)>>;
329
330  /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#ListUnits()) Call interface method `ListUnits`.
331  #[dbus_proxy(name = "ListUnits")]
332  fn list_units(
333    &self,
334  ) -> zbus::Result<
335    Vec<(
336      String,
337      String,
338      String,
339      String,
340      String,
341      String,
342      zvariant::OwnedObjectPath,
343      u32,
344      String,
345      zvariant::OwnedObjectPath,
346    )>,
347  >;
348
349  /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#ListUnitsByPatterns()) Call interface method `ListUnitsByPatterns`.
350  #[dbus_proxy(name = "ListUnitsByPatterns")]
351  fn list_units_by_patterns(
352    &self,
353    states: Vec<String>,
354    patterns: Vec<String>,
355  ) -> zbus::Result<
356    Vec<(
357      String,
358      String,
359      String,
360      String,
361      String,
362      String,
363      zvariant::OwnedObjectPath,
364      u32,
365      String,
366      zvariant::OwnedObjectPath,
367    )>,
368  >;
369
370  /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#Reload()) Call interface method `Reload`.
371  #[dbus_proxy(name = "Reload")]
372  fn reload(&self) -> zbus::Result<()>;
373}
374
375/// Proxy object for `org.freedesktop.systemd1.Unit`.
376/// Taken from https://github.com/lucab/zbus_systemd/blob/main/src/systemd1/generated.rs
377#[proxy(
378  interface = "org.freedesktop.systemd1.Unit",
379  default_service = "org.freedesktop.systemd1",
380  assume_defaults = false,
381  gen_blocking = false
382)]
383pub trait Unit {
384  /// Get property `ActiveState`.
385  #[dbus_proxy(property)]
386  fn active_state(&self) -> zbus::Result<String>;
387
388  /// Get property `LoadState`.
389  #[dbus_proxy(property)]
390  fn load_state(&self) -> zbus::Result<String>;
391
392  /// Get property `UnitFileState`.
393  #[dbus_proxy(property)]
394  fn unit_file_state(&self) -> zbus::Result<String>;
395}
396
397/// Proxy object for `org.freedesktop.systemd1.Service`.
398/// Taken from https://github.com/lucab/zbus_systemd/blob/main/src/systemd1/generated.rs
399#[proxy(
400  interface = "org.freedesktop.systemd1.Service",
401  default_service = "org.freedesktop.systemd1",
402  assume_defaults = false,
403  gen_blocking = false
404)]
405trait Service {
406  /// Get property `MainPID`.
407  #[dbus_proxy(property, name = "MainPID")]
408  fn main_pid(&self) -> zbus::Result<u32>;
409}
410
411/// Returns the load state of a systemd unit
412///
413/// Returns `invalid-unit-path` if the path is invalid
414///
415/// # Arguments
416///
417/// * `connection`: zbus connection
418/// * `full_service_name`: Full name of the service name with '.service' in the end
419///
420pub async fn get_active_state(connection: &Connection, full_service_name: &str) -> String {
421  let object_path = get_unit_path(full_service_name);
422
423  match zvariant::ObjectPath::try_from(object_path) {
424    Ok(path) => {
425      let unit_proxy = UnitProxy::new(connection, path).await.unwrap();
426      unit_proxy.active_state().await.unwrap_or("invalid-unit-path".into())
427    },
428    Err(_) => "invalid-unit-path".to_string(),
429  }
430}
431
432/// Returns the unit file state of a systemd unit. If the state is `enabled`, the unit loads on every boot
433///
434/// Returns `invalid-unit-path` if the path is invalid
435///
436/// # Arguments
437///
438/// * `connection`: zbus connection
439/// * `full_service_name`: Full name of the service name with '.service' in the end
440///
441pub async fn get_unit_file_state(connection: &Connection, full_service_name: &str) -> String {
442  let object_path = get_unit_path(full_service_name);
443
444  match zvariant::ObjectPath::try_from(object_path) {
445    Ok(path) => {
446      let unit_proxy = UnitProxy::new(connection, path).await.unwrap();
447      unit_proxy.unit_file_state().await.unwrap_or("invalid-unit-path".into())
448    },
449    Err(_) => "invalid-unit-path".to_string(),
450  }
451}
452
453/// Returns the PID of a systemd service
454///
455/// # Arguments
456///
457/// * `connection`: zbus connection
458/// * `full_service_name`: Full name of the service name with '.service' in the end
459///
460pub async fn get_main_pid(connection: &Connection, full_service_name: &str) -> Result<u32, zbus::Error> {
461  let object_path = get_unit_path(full_service_name);
462
463  let validated_object_path = zvariant::ObjectPath::try_from(object_path).unwrap();
464
465  let service_proxy = ServiceProxy::new(connection, validated_object_path).await.unwrap();
466  service_proxy.main_pid().await
467}
468
469/// Encode into a valid dbus string
470///
471/// # Arguments
472///
473/// * `input_string`
474///
475fn encode_as_dbus_object_path(input_string: &str) -> String {
476  input_string
477    .chars()
478    .map(|c| if c.is_ascii_alphanumeric() || c == '/' || c == '_' { c.to_string() } else { format!("_{:x}", c as u32) })
479    .collect()
480}
481
482/// Unit file path for a service
483///
484/// # Arguments
485///
486/// * `full_service_name`
487///
488pub fn get_unit_path(full_service_name: &str) -> String {
489  format!("/org/freedesktop/systemd1/unit/{}", encode_as_dbus_object_path(full_service_name))
490}