Skip to main content

systemd_transient/
manager.rs

1use std::time::Duration;
2
3use futures_util::StreamExt;
4use tracing::warn;
5use zbus::{Connection, ObjectPath, OwnedObjectPath, Variant, proxy::CacheProperties};
6
7use crate::{
8  JobMode, ReusableTransientUnit, TransientUnitHandle, UnitExit,
9  dbus::{
10    manager::{JobRemovedStream, ManagerProxy},
11    unit::UnitProxy,
12    unit_object_path,
13  },
14  error::SystemdTransientError,
15  name::unit_name,
16  props::UnitProperty,
17  watch::{ExitWatch, exit_of},
18};
19
20const UNIT_UNLOAD_TIMEOUT: Duration = Duration::from_secs(5);
21const ERR_NO_SUCH_UNIT: &str = "org.freedesktop.systemd1.NoSuchUnit";
22
23#[derive(Clone, Debug)]
24pub struct SystemdTransientManager {
25  conn: Connection,
26  /// Shared by all calls. Property caching is off: this crate reads no
27  /// Manager properties, and a cache costs a GetAll plus a match rule.
28  proxy: ManagerProxy<'static>,
29}
30
31impl SystemdTransientManager {
32  pub async fn system() -> Result<Self, SystemdTransientError> {
33    Self::from_connection(Connection::system().await?).await
34  }
35
36  pub async fn session() -> Result<Self, SystemdTransientError> {
37    Self::from_connection(Connection::session().await?).await
38  }
39
40  async fn from_connection(conn: Connection) -> Result<Self, SystemdTransientError> {
41    let proxy = ManagerProxy::builder(&conn)
42      .cache_properties(CacheProperties::No)
43      .build()
44      .await?;
45    proxy.subscribe().await?;
46
47    Ok(Self { conn, proxy })
48  }
49
50  /// See [`start`](Self::start) for `name` and `props`.
51  pub fn new_reusable(
52    &self,
53    name: impl Into<String>,
54    props: impl IntoIterator<Item = UnitProperty>,
55  ) -> ReusableTransientUnit {
56    ReusableTransientUnit {
57      name: name.into(),
58      props: props.into_iter().collect(),
59      unit_handle: None,
60      systemd_manager: self.clone(),
61    }
62  }
63
64  /// Starts `name` as a transient unit and returns once its start job
65  /// finished.
66  ///
67  /// `name` keeps a unit type suffix (`.slice`, `.socket`, `.scope`, ...);
68  /// any other name gets `.service` appended. A slice that other units name
69  /// in `Slice=` before it is started is created implicitly and is not
70  /// transient, so start a transient slice before the units in it.
71  pub async fn start(
72    &self,
73    name: &str,
74    props: impl IntoIterator<Item = UnitProperty>,
75  ) -> Result<TransientUnitHandle, SystemdTransientError> {
76    let name = unit_name(name);
77
78    self.prepare_name(&name).await?;
79
80    // Pin the unit to this connection, so it stays loaded after it exits.
81    // Appended last, so it wins over a caller's AddRef.
82    let pairs: Vec<_> = props
83      .into_iter()
84      .chain([UnitProperty::AddRef(true)])
85      .map(UnitProperty::into_pair)
86      .collect();
87    let tuples: Vec<(&str, &Variant<'_>)> = pairs.iter().map(|(k, v)| (k.as_ref(), v)).collect();
88    let refs: Vec<&(&str, &Variant<'_>)> = tuples.iter().collect();
89
90    let unit = UnitProxy::builder(&self.conn)
91      .path(unit_object_path(&name)?)
92      .cache_properties(CacheProperties::No)
93      .build()
94      .await?;
95
96    // Once this returns Ok, the unit exists and holds our reference, which a
97    // later failure must drop.
98    let jobs = self.proxy.receive_job_removed().await?;
99    let job = self
100      .proxy
101      .start_transient_unit(&name, JobMode::Replace.as_str(), &refs, &[])
102      .await?;
103
104    let started = async {
105      wait_for_job(jobs, job.as_ref()).await?;
106      let watch = ExitWatch::subscribe(&self.conn, &unit).await?;
107      // Identifies this run; a later start of the same unit gets a new one.
108      let invocation_id = unit.invocation_id().await?;
109      Ok::<_, SystemdTransientError>((watch, invocation_id))
110    }
111    .await;
112
113    let (watch, invocation_id) = match started {
114      Ok(started) => started,
115      Err(err) => {
116        // Let systemd collect the unit this call created.
117        if let Err(unref_err) = unit.unref().await {
118          warn!("releasing unit {} failed: {}", name, unref_err);
119        }
120        return Err(err);
121      }
122    };
123
124    Ok(TransientUnitHandle {
125      name,
126      manager: self.clone(),
127      unit,
128      invocation_id,
129      watch,
130      released: false,
131    })
132  }
133
134  /// Takes over the transient unit `name`, for example one that an earlier
135  /// process started and did not stop. The handle holds its own reference
136  /// and works like one from [`start`](Self::start); the unit keeps the
137  /// properties it was started with. `name` is completed as for `start`.
138  ///
139  /// Running and exited units both qualify, as long as systemd still has
140  /// them loaded: a failed unit stays loaded until it is reset, an inactive
141  /// one only while something references it. Returns `None` if no unit of
142  /// that name is loaded, and `UnitNotTransient` for a unit from a unit file.
143  pub async fn adopt(
144    &self,
145    name: &str,
146  ) -> Result<Option<TransientUnitHandle>, SystemdTransientError> {
147    let name = unit_name(name);
148
149    let Some(path) = get_unit(&self.proxy, &name).await? else {
150      return Ok(None);
151    };
152    let unit = UnitProxy::builder(&self.conn)
153      .path(path)
154      .cache_properties(CacheProperties::No)
155      .build()
156      .await?;
157
158    // Reference first, so the unit cannot be collected while we look at it.
159    // If it was collected right after GetUnit, this call loads a stub of the
160    // name instead, which the LoadState check catches.
161    unit.ref_().await?;
162
163    let adopted = async {
164      if unit.load_state().await? != "loaded" {
165        return Ok(None);
166      }
167      if !unit.transient().await? {
168        return Err(SystemdTransientError::UnitNotTransient { name: name.clone() });
169      }
170      let watch = ExitWatch::subscribe(&self.conn, &unit).await?;
171      let invocation_id = unit.invocation_id().await?;
172      Ok(Some((watch, invocation_id)))
173    }
174    .await;
175
176    match adopted {
177      Ok(Some((watch, invocation_id))) => Ok(Some(TransientUnitHandle {
178        name,
179        manager: self.clone(),
180        unit,
181        invocation_id,
182        watch,
183        released: false,
184      })),
185      other => {
186        if let Err(err) = unit.unref().await {
187          warn!("releasing unit {} failed: {}", name, err);
188        }
189        other.map(|_| None)
190      }
191    }
192  }
193
194  /// Loaded units whose names match one of `patterns`, shell globs as for
195  /// `systemctl list-units`. No patterns match every loaded unit.
196  pub async fn list_units(
197    &self,
198    patterns: &[&str],
199  ) -> Result<Vec<ListedUnit>, SystemdTransientError> {
200    let units = self.proxy.list_units_by_patterns(&[], patterns).await?;
201    Ok(
202      units
203        .into_iter()
204        .map(
205          |(name, _description, load_state, active_state, sub_state, ..)| ListedUnit {
206            name,
207            load_state,
208            active_state,
209            sub_state,
210          },
211        )
212        .collect(),
213    )
214  }
215
216  /// Makes `name` free for `StartTransientUnit`.
217  ///
218  /// - No unit loaded: nothing to do.
219  /// - Unit failed: reset it, then wait until systemd garbage-collects it.
220  /// - Unit inactive: wait for collection.
221  ///
222  /// Anything else (active, activating, deactivating, reloading, ...) is an error.
223  async fn prepare_name(&self, name: &str) -> Result<(), SystemdTransientError> {
224    let proxy = &self.proxy;
225    // Subscribe before the first lookup, so a removal in between is not missed.
226    let mut removed = proxy.receive_unit_removed().await?;
227
228    let Some(path) = get_unit(proxy, name).await? else {
229      return Ok(());
230    };
231
232    let unit = UnitProxy::builder(&self.conn)
233      .path(path)
234      .cache_properties(CacheProperties::No)
235      .build()
236      .await?;
237
238    if !unit.transient().await? {
239      return Err(SystemdTransientError::UnitNotTransient {
240        name: name.to_owned(),
241      });
242    }
243
244    match exit_of(&unit.active_state().await?) {
245      Some(UnitExit::Failed) => proxy.reset_failed_unit(name).await?,
246      Some(UnitExit::Inactive) => {}
247      None => {
248        return Err(SystemdTransientError::UnitAlreadyRunning {
249          name: name.to_owned(),
250        });
251      }
252    }
253
254    let wait = async {
255      while get_unit(proxy, name).await?.is_some() {
256        loop {
257          let signal = removed
258            .next()
259            .await
260            .ok_or(SystemdTransientError::UnitStreamEnded)?;
261          match signal.args() {
262            Ok(args) if *args.id() == name => break,
263            Ok(_) => {}
264            Err(err) => warn!("unparsable UnitRemoved signal: {}", err),
265          }
266        }
267      }
268      Ok::<_, SystemdTransientError>(())
269    };
270
271    tokio::time::timeout(UNIT_UNLOAD_TIMEOUT, wait)
272      .await
273      .map_err(|_| SystemdTransientError::UnitStillLoaded {
274        name: name.to_owned(),
275      })?
276  }
277
278  /// Stops a unit by full name (e.g. `foo.slice`) and waits for the job.
279  /// A unit systemd does not know counts as stopped.
280  pub async fn stop_unit(&self, name: &str) -> Result<(), SystemdTransientError> {
281    match self.stop_job(name).await {
282      Err(SystemdTransientError::Zbus(zbus::Error::MethodError(err, _, _)))
283        if err.as_str() == ERR_NO_SUCH_UNIT =>
284      {
285        Ok(())
286      }
287      other => other,
288    }
289  }
290
291  /// Queues a stop job for `name` and waits for it.
292  ///
293  /// There is no client-side timeout. systemd enforces the unit's job
294  /// timeouts itself (`TimeoutStopUSec`, `JobTimeoutUSec`) and reports an
295  /// expiry as a `JobRemoved` result of `timeout`.
296  pub(crate) async fn stop_job(&self, name: &str) -> Result<(), SystemdTransientError> {
297    let jobs = self.proxy.receive_job_removed().await?;
298    let job = self
299      .proxy
300      .stop_unit(name, JobMode::Replace.as_str())
301      .await?;
302    wait_for_job(jobs, job.as_ref()).await
303  }
304}
305
306/// `GetUnit`, with `NoSuchUnit` mapped to `None`. Unlike `LoadUnit`, this
307/// does not create a stub unit.
308async fn get_unit(
309  proxy: &ManagerProxy<'_>,
310  name: &str,
311) -> Result<Option<OwnedObjectPath>, SystemdTransientError> {
312  match proxy.get_unit(name).await {
313    Ok(path) => Ok(Some(path)),
314    Err(zbus::Error::MethodError(err, _, _)) if err.as_str() == ERR_NO_SUCH_UNIT => Ok(None),
315    Err(err) => Err(err.into()),
316  }
317}
318
319async fn wait_for_job(
320  mut stream: JobRemovedStream,
321  job_path: ObjectPath<'_>,
322) -> Result<(), SystemdTransientError> {
323  while let Some(signal) = stream.next().await {
324    let args = match signal.args() {
325      Ok(args) => args,
326      Err(err) => {
327        warn!("unparsable JobRemoved signal: {}", err);
328        continue;
329      }
330    };
331
332    if *args.job() != job_path {
333      continue;
334    }
335
336    return match *args.result() {
337      "done" => Ok(()),
338      r => Err(SystemdTransientError::JobWaitFailed {
339        unit: args.unit().to_string(),
340        reason: r.to_owned(),
341      }),
342    };
343  }
344
345  Err(SystemdTransientError::JobStreamEnded)
346}
347
348/// A unit as [`SystemdTransientManager::list_units`] reports it.
349#[derive(Debug, Clone, PartialEq, Eq)]
350#[non_exhaustive]
351pub struct ListedUnit {
352  pub name: String,
353  pub load_state: String,
354  pub active_state: String,
355  pub sub_state: String,
356}
357
358impl ListedUnit {
359  /// The state the unit ended in, or `None` while it is active or changing
360  /// state.
361  pub fn exit(&self) -> Option<UnitExit> {
362    exit_of(&self.active_state)
363  }
364}