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 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 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 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 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 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 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 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 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 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 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 async fn prepare_name(&self, name: &str) -> Result<(), SystemdTransientError> {
224 let proxy = &self.proxy;
225 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 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 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
306async 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#[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 pub fn exit(&self) -> Option<UnitExit> {
362 exit_of(&self.active_state)
363 }
364}