Skip to main content

git_policy_cli/
systemd_dbus.rs

1//! What:
2//!  A minimal D-Bus client for the user session bus: authenticate as the current user, say `Hello`, and call methods of
3//!  `org.freedesktop.systemd1.Manager` with string arguments, each bounded by one deadline for the whole exchange.
4//! Why:
5//!  The wrapper provisions socket activation itself, and may start no program to do it (`systemctl` would be one). The
6//!  supported surface of the user manager is its D-Bus API (`org.freedesktop.systemd1(5)`), reached through the session
7//!  bus; the manager's own private socket is documented as private to systemd. Only `Reload`, `ResetFailedUnit`,
8//!  `StartUnit`, `StopUnit` and the state query (`GetUnit`, then a property read) are needed, all with at most two string
9//!  arguments, so the writer and the reader below cover exactly the types those calls and the bus's replies use (`s`, `o`,
10//!  `g`, `u`, and a variant holding `s`) and stop at anything else. The reader is total over arbitrary bytes (a
11//!  fuzz target holds it to that). The comparison with a D-Bus crate is in `doc/planning/cli-git-resident-service.md`,
12//!  "Provisioning".
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! const bus = await connect(address, 5000); await bus.startUnit('cli-git.socket', 'replace'); // throws with the error name
17//! ```
18
19use std::io::{Read, Write};
20use std::os::unix::net::UnixStream;
21use std::path::Path;
22use std::time::{Duration, Instant};
23
24/// The manager's object path.
25pub const MANAGER_PATH: &str = "/org/freedesktop/systemd1";
26
27/// The manager's interface.
28pub const MANAGER_INTERFACE: &str = "org.freedesktop.systemd1.Manager";
29
30/// How long the whole exchange (connect, authenticate, say hello, make the calls) may take; a stuck bus never holds a
31///  commit longer.
32pub const EXCHANGE_WAIT: Duration = Duration::from_secs(20);
33
34/// The bus daemon's name, object and interface, for `Hello`.
35const BUS_NAME: &str = "org.freedesktop.DBus";
36const BUS_PATH: &str = "/org/freedesktop/DBus";
37
38/// The manager's bus name.
39pub const MANAGER_NAME: &str = "org.freedesktop.systemd1";
40
41/// The most bytes of one reply this client accepts.
42const MAX_REPLY_BYTES: usize = 1 << 20;
43
44/// Message type of a method call.
45const METHOD_CALL: u8 = 1;
46
47/// Message type of a successful reply.
48const METHOD_RETURN: u8 = 2;
49
50/// Message type of an error reply.
51const ERROR: u8 = 3;
52
53/// Header field code of the number of the call a reply answers.
54const FIELD_REPLY_SERIAL: u8 = 5;
55
56/// Header field codes.
57const FIELD_PATH: u8 = 1;
58const FIELD_INTERFACE: u8 = 2;
59const FIELD_MEMBER: u8 = 3;
60const FIELD_ERROR_NAME: u8 = 4;
61const FIELD_DESTINATION: u8 = 6;
62const FIELD_SIGNATURE: u8 = 8;
63
64/// Why a call did not succeed.
65#[derive(Clone, Debug, Eq, PartialEq)]
66pub enum CallError {
67    /// The socket could not be reached or the connection broke.
68    Connection(String),
69    /// The manager refused the authentication.
70    Authentication(String),
71    /// The bus did not answer within the time the exchange was given, which says nothing about whether the manager is there.
72    TimedOut,
73    /// The manager answered with an error.
74    Manager {
75        /// The error's name, for example `org.freedesktop.systemd1.NoSuchUnit`.
76        name: String,
77        /// The error's text, when it gave one.
78        text: String,
79    },
80    /// The reply was not a message this client reads.
81    Malformed(String),
82}
83
84/// Appends values in D-Bus little-endian wire form.
85struct Writer {
86    bytes: Vec<u8>,
87}
88
89impl Writer {
90    fn align(&mut self, boundary: usize) {
91        while self.bytes.len() % boundary != 0 {
92            self.bytes.push(0);
93        }
94    }
95
96    fn unsigned(&mut self, value: u32) {
97        self.align(4);
98        self.bytes.extend(value.to_le_bytes());
99    }
100
101    fn text(&mut self, value: &str) {
102        self.unsigned(u32::try_from(value.len()).unwrap_or(u32::MAX));
103        self.bytes.extend(value.as_bytes());
104        self.bytes.push(0);
105    }
106
107    fn signature(&mut self, value: &str) {
108        self.bytes.push(u8::try_from(value.len()).unwrap_or(u8::MAX));
109        self.bytes.extend(value.as_bytes());
110        self.bytes.push(0);
111    }
112
113    /// One header field: its code, then a variant holding a string-like or signature value.
114    fn field(&mut self, code: u8, kind: char, value: &str) {
115        self.align(8);
116        self.bytes.push(code);
117        self.signature(&kind.to_string());
118        if kind == 'g' {
119            self.signature(value);
120        } else {
121            self.text(value);
122        }
123    }
124}
125
126/// Where a method call goes.
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub struct Target<'a> {
129    /// The bus name of the receiver.
130    pub destination: &'a str,
131    /// The object path.
132    pub path: &'a str,
133    /// The interface.
134    pub interface: &'a str,
135    /// The method.
136    pub member: &'a str,
137}
138
139/// `Hello` on the bus daemon, the first call of every connection.
140pub const HELLO: Target<'static> = Target { destination: BUS_NAME, path: BUS_PATH, interface: BUS_NAME, member: "Hello" };
141
142/// `Reload` on the user manager.
143pub const RELOAD: Target<'static> = Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "Reload" };
144
145/// `StartUnit` on the user manager.
146pub const START_UNIT: Target<'static> = Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "StartUnit" };
147
148/// `StopUnit` on the user manager.
149pub const STOP_UNIT: Target<'static> = Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "StopUnit" };
150
151/// `ResetFailedUnit` on the user manager.
152pub const RESET_FAILED_UNIT: Target<'static> =
153    Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "ResetFailedUnit" };
154
155/// `GetUnit` on the user manager: the object path of a loaded unit.
156pub const GET_UNIT: Target<'static> = Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "GetUnit" };
157
158/// The interface whose `Get` reads one property of an object.
159const PROPERTIES_INTERFACE: &str = "org.freedesktop.DBus.Properties";
160
161/// The interface that holds a unit's `ActiveState`.
162const UNIT_INTERFACE: &str = "org.freedesktop.systemd1.Unit";
163
164/// The error name of a unit the manager does not hold (never loaded, or unloaded after it stopped).
165pub const NO_SUCH_UNIT: &str = "org.freedesktop.systemd1.NoSuchUnit";
166
167/// The error name of an object that is gone by the time a property is read.
168const UNKNOWN_OBJECT: &str = "org.freedesktop.DBus.Error.UnknownObject";
169
170/// What:
171///  The bytes of a method call with string arguments.
172/// Why:
173///   Kept apart from the socket so a test can read them back and compare them with the specification's layout.
174///
175/// In TS you'd write (pseudocode):
176/// ```ts
177/// encodeCall(serial, START_UNIT, ['cli-git.socket', 'replace'])
178/// ```
179pub fn encode_call(serial: u32, target: &Target<'_>, arguments: &[&str]) -> Vec<u8> {
180    let mut body: Writer = Writer { bytes: Vec::new() };
181    for argument in arguments {
182        body.text(argument);
183    }
184    let mut fields: Writer = Writer { bytes: vec![0; 16] };
185    fields.field(FIELD_PATH, 'o', target.path);
186    fields.field(FIELD_DESTINATION, 's', target.destination);
187    fields.field(FIELD_INTERFACE, 's', target.interface);
188    fields.field(FIELD_MEMBER, 's', target.member);
189    if !arguments.is_empty() {
190        fields.field(FIELD_SIGNATURE, 'g', &"s".repeat(arguments.len()));
191    }
192    // The first sixteen bytes are the fixed header and the array length, filled in below.
193    let array_length: u32 = u32::try_from(fields.bytes.len() - 16).unwrap_or(u32::MAX);
194    fields.align(8);
195    let mut message: Vec<u8> = fields.bytes;
196    message[0] = b'l';
197    message[1] = METHOD_CALL;
198    message[2] = 0;
199    message[3] = 1;
200    message[4..8].copy_from_slice(&u32::try_from(body.bytes.len()).unwrap_or(u32::MAX).to_le_bytes());
201    message[8..12].copy_from_slice(&serial.to_le_bytes());
202    message[12..16].copy_from_slice(&array_length.to_le_bytes());
203    message.extend(body.bytes);
204    return message;
205}
206
207/// A reply: its type, the error name its header carries, and the first string of its body.
208#[derive(Clone, Debug, Eq, PartialEq)]
209pub struct Reply {
210    /// Message type: 2 for a return, 3 for an error, 4 for a signal.
211    pub kind: u8,
212    /// The number of the call this message answers.
213    pub reply_serial: Option<u32>,
214    /// The error name of an error reply.
215    pub error_name: Option<String>,
216    /// The first string argument of the body, which an error reply uses for its text.
217    pub first_text: Option<String>,
218}
219
220fn read_u32(bytes: &[u8], at: usize) -> Option<u32> {
221    return bytes.get(at..at.checked_add(4)?).map(|slice| { return u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]); });
222}
223
224/// The string at `at` and the offset after its end byte, or nothing when it is cut short or the byte after the text is not
225///  the end byte a D-Bus string carries. Callers pass only the bytes of the region the string must lie in, so an end byte
226///  beyond that region is not found.
227fn read_text(bytes: &[u8], at: usize) -> Option<(String, usize)> {
228    let length: usize = usize::try_from(read_u32(bytes, at)?).ok()?;
229    let start: usize = at.checked_add(4)?;
230    let end: usize = start.checked_add(length)?;
231    let text: &[u8] = bytes.get(start..end)?;
232    if *bytes.get(end)? != 0 {
233        return None;
234    }
235    return Some((String::from_utf8_lossy(text).into_owned(), end.saturating_add(1)));
236}
237
238/// The string a variant at `at` holds, or nothing when it holds another type, is cut short, or has a signature that does
239///  not end with its end byte.
240fn read_variant_text(bytes: &[u8], at: usize) -> Option<String> {
241    let length: usize = usize::from(*bytes.get(at)?);
242    let inner_start: usize = at.checked_add(1)?;
243    let inner_end: usize = inner_start.checked_add(length)?;
244    let inner: &[u8] = bytes.get(inner_start..inner_end)?;
245    if inner != b"s" || *bytes.get(inner_end)? != 0 {
246        return None;
247    }
248    // The signature's end byte is checked; the string follows on a four-byte boundary.
249    return read_text(bytes, aligned(inner_end.checked_add(1)?, 4)).map(|(text, _)| { return text; });
250}
251
252fn aligned(offset: usize, boundary: usize) -> usize {
253    return offset.checked_add(boundary - 1).map_or(usize::MAX, |sum| { return sum / boundary * boundary; });
254}
255
256/// What:
257///  Reads a complete reply message.
258/// Why:
259///   Only the type, the error name and the first string of the body matter; a header field of a kind this reader does not
260///  know ends the walk of the header, which is safe because the fields it needs come first in what the manager sends or
261///  are looked for before any unknown kind.
262///
263/// In TS you'd write (pseudocode):
264/// ```ts
265/// decodeReply(bytes)
266/// ```
267pub fn decode_reply(bytes: &[u8]) -> Result<Reply, CallError> {
268    let malformed = |what: &str| { return CallError::Malformed(what.to_string()); };
269    if bytes.len() < 16 || bytes[0] != b'l' {
270        return Err(malformed("the reply is not a little-endian message"));
271    }
272    let kind: u8 = bytes[1];
273    let body_length: usize = usize::try_from(read_u32(bytes, 4).ok_or_else(|| { return malformed("no body length"); })?).unwrap_or(usize::MAX);
274    let fields_length: usize = usize::try_from(read_u32(bytes, 12).ok_or_else(|| { return malformed("no header length"); })?).unwrap_or(usize::MAX);
275    let fields_end: usize = 16usize.saturating_add(fields_length);
276    let body_start: usize = aligned(fields_end, 8);
277    if bytes.len() < body_start.saturating_add(body_length) || fields_end > bytes.len() {
278        return Err(malformed("the reply is shorter than it says"));
279    }
280    let mut error_name: Option<String> = None;
281    let mut reply_serial: Option<u32> = None;
282    let mut signature: String = String::new();
283    // A header field lies within the declared header and a body value within the declared body, whatever follows them.
284    let header: &[u8] = bytes.get(..fields_end).unwrap_or_default();
285    let body: &[u8] = bytes.get(..body_start.saturating_add(body_length)).unwrap_or_default();
286    let mut at: usize = 16;
287    while at < fields_end {
288        at = aligned(at, 8);
289        let (Some(&code), Some(&signature_length)) = (header.get(at), header.get(at.saturating_add(1))) else { break };
290        let kind_at: usize = at.saturating_add(2);
291        let Some(&value_kind) = header.get(kind_at) else { break };
292        let value_at: usize = kind_at.saturating_add(usize::from(signature_length)).saturating_add(1);
293        match (value_kind, code) {
294            (b's' | b'o', _) => {
295                let Some((text, next)) = read_text(header, aligned(value_at, 4)) else { break };
296                if code == FIELD_ERROR_NAME {
297                    error_name = Some(text);
298                }
299                at = next;
300            }
301            (b'g', _) => {
302                let length: usize = usize::from(*header.get(value_at).unwrap_or(&0));
303                if code == FIELD_SIGNATURE {
304                    let start: usize = value_at.saturating_add(1);
305                    // A signature whose end byte is missing is not read, so the body is not interpreted by it.
306                    if header.get(start.saturating_add(length)) == Some(&0) {
307                        signature = String::from_utf8_lossy(header.get(start..start.saturating_add(length)).unwrap_or_default()).into_owned();
308                    }
309                }
310                at = value_at.saturating_add(length).saturating_add(2);
311            }
312            (b'u', _) => {
313                let number_at: usize = aligned(value_at, 4);
314                if code == FIELD_REPLY_SERIAL {
315                    reply_serial = read_u32(header, number_at);
316                }
317                at = number_at.saturating_add(4);
318            }
319            _ => break,
320        }
321    }
322    // A string and an object path share one wire form; a variant holding a string is its own signature, then that form.
323    let first_text: Option<String> = if signature.starts_with('s') || signature.starts_with('o') {
324        read_text(body, aligned(body_start, 4)).map(|(text, _)| { return text; })
325    } else if signature == "v" {
326        read_variant_text(body, body_start)
327    } else {
328        None
329    };
330    return Ok(Reply { kind, reply_serial, error_name, first_text });
331}
332
333/// The user id as the ASCII digits the `EXTERNAL` mechanism sends, written in hexadecimal.
334fn external_identity() -> String {
335    // SAFETY: `geteuid` takes no arguments and cannot fail.
336    let user: u32 = unsafe { libc::geteuid() };
337    return user.to_string().bytes().map(|byte| { return format!("{byte:02x}"); }).collect();
338}
339
340/// The time left of the exchange, or an error when it ran out.
341fn remaining(deadline: Instant) -> Result<Duration, CallError> {
342    let left: Duration = deadline.saturating_duration_since(Instant::now());
343    return if left.is_zero() { Err(CallError::TimedOut) } else { Ok(left) };
344}
345
346/// A socket failure as the call's error: running out of the read or write time is a timeout, anything else a broken connection.
347fn socket_failure(error: &std::io::Error) -> CallError {
348    return match error.kind() {
349        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => CallError::TimedOut,
350        _ => CallError::Connection(error.to_string()),
351    };
352}
353
354fn read_line(stream: &mut UnixStream, deadline: Instant) -> Result<String, CallError> {
355    let mut line: Vec<u8> = Vec::new();
356    let mut one: [u8; 1] = [0];
357    while line.len() < 4096 {
358        stream.set_read_timeout(Some(remaining(deadline)?)).map_err(|error| { return CallError::Connection(error.to_string()); })?;
359        stream.read_exact(&mut one).map_err(|error| {
360            return match socket_failure(&error) {
361                CallError::TimedOut => CallError::TimedOut,
362                _ => CallError::Authentication(error.to_string()),
363            };
364        })?;
365        if one[0] == b'\n' {
366            return Ok(String::from_utf8_lossy(line.as_slice()).trim_end_matches('\r').to_string());
367        }
368        line.push(one[0]);
369    }
370    return Err(CallError::Authentication(String::from("the answer is too long")));
371}
372
373/// Reads exactly one message from the stream.
374fn read_message(stream: &mut UnixStream, deadline: Instant) -> Result<Vec<u8>, CallError> {
375    let mut header: Vec<u8> = vec![0; 16];
376    stream.set_read_timeout(Some(remaining(deadline)?)).map_err(|error| { return CallError::Connection(error.to_string()); })?;
377    stream.read_exact(header.as_mut_slice()).map_err(|error| { return socket_failure(&error); })?;
378    let body_length: usize = usize::try_from(read_u32(header.as_slice(), 4).unwrap_or(0)).unwrap_or(usize::MAX);
379    let fields_length: usize = usize::try_from(read_u32(header.as_slice(), 12).unwrap_or(0)).unwrap_or(usize::MAX);
380    let rest: usize = aligned(16usize.saturating_add(fields_length), 8).saturating_add(body_length).saturating_sub(16);
381    if rest > MAX_REPLY_BYTES {
382        return Err(CallError::Malformed(String::from("the reply is larger than this client reads")));
383    }
384    let mut remainder: Vec<u8> = vec![0; rest];
385    stream.set_read_timeout(Some(remaining(deadline)?)).map_err(|error| { return CallError::Connection(error.to_string()); })?;
386    stream.read_exact(remainder.as_mut_slice()).map_err(|error| { return socket_failure(&error); })?;
387    header.extend(remainder);
388    return Ok(header);
389}
390
391/// The path of the session bus socket: the `unix:path=` entry of the address variable, else `$XDG_RUNTIME_DIR/bus`.
392pub fn session_bus_path(address: Option<&str>, runtime_dir: Option<&Path>) -> Option<std::path::PathBuf> {
393    let from_address: Option<std::path::PathBuf> = address.and_then(|list| {
394        return list.split(';').find_map(|entry| {
395            let rest: &str = entry.strip_prefix("unix:")?;
396            let path: &str = rest.split(',').find_map(|option| { return option.strip_prefix("path="); })?;
397            // Escapes (`%xx`) in a path are not read; a path that uses them falls back to the default location.
398            return if path.contains('%') || path.is_empty() { None } else { Some(std::path::PathBuf::from(path)) };
399        });
400    });
401    return from_address.or_else(|| { return runtime_dir.map(|directory| { return directory.join("bus"); }); });
402}
403
404/// An authenticated connection to the session bus, past `Hello`.
405pub struct Bus {
406    stream: UnixStream,
407    deadline: Instant,
408    serial: u32,
409}
410
411/// Methods over a connection.
412impl Bus {
413    /// What:
414    ///  Connects to the bus socket, authenticates as the current user and says `Hello`, all within `wait`.
415    /// Why:
416    ///   The deadline of the whole exchange starts here, so every later call shares what is left of it.
417    ///
418    /// In TS you'd write (pseudocode):
419    /// ```ts
420    /// const bus = await Bus.connect(path, 20000)
421    /// ```
422    pub fn connect(socket: &Path, wait: Duration) -> Result<Bus, CallError> {
423        let deadline: Instant = Instant::now() + wait;
424        let mut stream: UnixStream = UnixStream::connect(socket).map_err(|error| { return CallError::Connection(error.to_string()); })?;
425        stream.set_write_timeout(Some(wait)).map_err(|error| { return CallError::Connection(error.to_string()); })?;
426        let mut hello: Vec<u8> = vec![0];
427        hello.extend(format!("AUTH EXTERNAL {}\r\n", external_identity()).as_bytes());
428        stream.write_all(hello.as_slice()).map_err(|error| { return CallError::Authentication(error.to_string()); })?;
429        let answer: String = read_line(&mut stream, deadline)?;
430        if !answer.starts_with("OK ") {
431            return Err(CallError::Authentication(answer));
432        }
433        stream.write_all(b"BEGIN\r\n").map_err(|error| { return CallError::Authentication(error.to_string()); })?;
434        let mut bus: Bus = Bus { stream, deadline, serial: 0 };
435        bus.call(&HELLO, &[])?;
436        return Ok(bus);
437    }
438
439    /// What:
440    ///  Gives the calls that follow `wait` from now, replacing what was left of the time the connection was given.
441    /// Why:
442    ///   The time of the connection bounds a stuck bus, but a caller that spends time between calls (waiting for a service to
443    ///  stop) must bound each phase by its own budget: a slow phase must not make the next one fail, and a query must not run
444    ///  past the phase it belongs to.
445    ///
446    /// In TS you'd write (pseudocode):
447    /// ```ts
448    /// bus.within(10000) // the next calls may take ten seconds in all
449    /// ```
450    pub fn within(&mut self, wait: Duration) {
451        self.deadline = Instant::now() + wait;
452    }
453
454    /// What:
455    ///  Makes one method call and waits for the reply that answers it, skipping the signals the bus sends meanwhile.
456    /// Why:
457    ///   A return is the receiver's confirmation (a reload returns after the units are read); an error reply comes back
458    ///  with its name and text so the caller can say what to do.
459    ///
460    /// In TS you'd write (pseudocode):
461    /// ```ts
462    /// await bus.call(RELOAD, [])
463    /// ```
464    pub fn call(&mut self, target: &Target<'_>, arguments: &[&str]) -> Result<Reply, CallError> {
465        self.serial += 1;
466        let serial: u32 = self.serial;
467        self.stream.write_all(encode_call(serial, target, arguments).as_slice()).map_err(|error| { return socket_failure(&error); })?;
468        loop {
469            let reply: Reply = decode_reply(read_message(&mut self.stream, self.deadline)?.as_slice())?;
470            if reply.reply_serial != Some(serial) || !matches!(reply.kind, METHOD_RETURN | ERROR) {
471                continue;
472            }
473            return match (reply.kind, reply.error_name.clone()) {
474                (METHOD_RETURN, _) => Ok(reply),
475                (_, Some(name)) => Err(CallError::Manager { name, text: reply.first_text.unwrap_or_default() }),
476                _ => Err(CallError::Malformed(String::from("an error reply without a name"))),
477            };
478        }
479    }
480
481    /// Reloads the user manager's unit files.
482    pub fn reload(&mut self) -> Result<(), CallError> {
483        return self.call(&RELOAD, &[]).map(drop);
484    }
485
486    /// Clears a unit's failed state and its start-rate counter, so a start is not refused as "repeated too quickly".
487    pub fn reset_failed(&mut self, name: &str) -> Result<(), CallError> {
488        return self.call(&RESET_FAILED_UNIT, &[name]).map(drop);
489    }
490
491    /// Stops a unit, replacing a queued job for it.
492    pub fn stop_unit(&mut self, name: &str) -> Result<(), CallError> {
493        return self.call(&STOP_UNIT, &[name, "replace"]).map(drop);
494    }
495
496    /// What:
497    ///  The unit's `ActiveState` (`active`, `activating`, `deactivating`, `inactive`, `failed`), or nothing when the manager
498    ///  does not hold the unit.
499    /// Why:
500    ///   A stop call returns when the job is queued, so only this state says the unit and its processes are gone. `GetUnit`
501    ///  answers the unit's object path and the property read answers a variant holding the state text. A unit that is not
502    ///  loaded, or that unloads between the two calls, is not running.
503    ///
504    /// In TS you'd write (pseudocode):
505    /// ```ts
506    /// await bus.activeState('cli-git.service') // 'deactivating' | 'inactive' | undefined
507    /// ```
508    pub fn active_state(&mut self, name: &str) -> Result<Option<String>, CallError> {
509        let path: String = match self.call(&GET_UNIT, &[name]) {
510            Ok(reply) => reply.first_text.ok_or_else(|| { return CallError::Malformed(String::from("GetUnit answered without a path")); })?,
511            Err(CallError::Manager { name: error, .. }) if error == NO_SUCH_UNIT => return Ok(None),
512            Err(error) => return Err(error),
513        };
514        let property: Target<'_> = Target { destination: MANAGER_NAME, path: path.as_str(), interface: PROPERTIES_INTERFACE, member: "Get" };
515        return match self.call(&property, &[UNIT_INTERFACE, "ActiveState"]) {
516            Ok(reply) => reply
517                .first_text
518                .map(Some)
519                .ok_or_else(|| { return CallError::Malformed(String::from("the state property answered without text")); }),
520            Err(CallError::Manager { name: error, .. }) if error == NO_SUCH_UNIT || error == UNKNOWN_OBJECT => Ok(None),
521            Err(error) => Err(error),
522        };
523    }
524
525    /// Starts a unit, replacing a queued job for it.
526    pub fn start_unit(&mut self, name: &str) -> Result<(), CallError> {
527        return self.call(&START_UNIT, &[name, "replace"]).map(drop);
528    }
529}
530
531/// Client controls stay out of the release executable.
532#[cfg(test)]
533#[path = "systemd_dbus_tests.rs"]
534mod tests;