1use 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, pub scope: UnitScope, pub description: String, pub file_path: Option<Result<String, String>>, pub load_state: String, pub activation_state: String,
24 pub sub_state: String,
26
27 pub enablement_state: Option<String>,
30 }
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub enum UnitScope {
40 Global,
41 User,
42}
43
44#[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 pub fn id(&self) -> UnitId {
78 UnitId { name: self.name.clone(), scope: self.scope }
79 }
80
81 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#[derive(Clone, Copy, Default, Debug)]
111pub enum Scope {
112 Global,
113 User,
114 #[default]
115 All,
116}
117
118pub 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 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 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 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 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 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 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 tokio::select! {
268 _ = cancel_token.cancelled() => {
269 anyhow::bail!("cancelled");
271 }
272 result = restart(service) => {
273 result
274 }
275 }
276}
277
278pub async fn sleep_test(_service: String, cancel_token: CancellationToken) -> Result<()> {
280 tokio::select! {
282 _ = cancel_token.cancelled() => {
283 anyhow::bail!("cancelled");
285 }
286 _ = tokio::time::sleep(std::time::Duration::from_secs(2)) => {
287 Ok(())
288 }
289 }
290}
291
292#[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 #[dbus_proxy(name = "StartUnit")]
303 fn start_unit(&self, name: String, mode: String) -> zbus::Result<zvariant::OwnedObjectPath>;
304
305 #[dbus_proxy(name = "StopUnit")]
307 fn stop_unit(&self, name: String, mode: String) -> zbus::Result<zvariant::OwnedObjectPath>;
308
309 #[dbus_proxy(name = "ReloadUnit")]
311 fn reload_unit(&self, name: String, mode: String) -> zbus::Result<zvariant::OwnedObjectPath>;
312
313 #[dbus_proxy(name = "RestartUnit")]
315 fn restart_unit(&self, name: String, mode: String) -> zbus::Result<zvariant::OwnedObjectPath>;
316
317 #[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 #[dbus_proxy(name = "DisableUnitFiles")]
328 fn disable_unit_files(&self, files: Vec<String>, runtime: bool) -> zbus::Result<Vec<(String, String, String)>>;
329
330 #[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 #[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 #[dbus_proxy(name = "Reload")]
372 fn reload(&self) -> zbus::Result<()>;
373}
374
375#[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 #[dbus_proxy(property)]
386 fn active_state(&self) -> zbus::Result<String>;
387
388 #[dbus_proxy(property)]
390 fn load_state(&self) -> zbus::Result<String>;
391
392 #[dbus_proxy(property)]
394 fn unit_file_state(&self) -> zbus::Result<String>;
395}
396
397#[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 #[dbus_proxy(property, name = "MainPID")]
408 fn main_pid(&self) -> zbus::Result<u32>;
409}
410
411pub 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
432pub 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
453pub 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
469fn 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
482pub fn get_unit_path(full_service_name: &str) -> String {
489 format!("/org/freedesktop/systemd1/unit/{}", encode_as_dbus_object_path(full_service_name))
490}