dbus_launch/
lib.rs

1//! A D-Bus daemon launcher.
2//!
3//! A tool for starting an new isolated instance of a dbus-daemon or a
4//! dbus-broker, with option to configure and start services using D-Bus
5//! activation.
6//!
7//! Intended for the use in integration tests of D-Bus services and utilities.
8//!
9//! # Examples
10//!
11//! ## Launching a dbus-daemon process
12//!
13//! ```no_run
14//! // Start the dbus-daemon.
15//! let daemon = dbus_launch::Launcher::daemon()
16//!     .launch()
17//!     .expect("failed to launch dbus-daemon");
18//!
19//! // Use dbus-daemon by connecting to `daemon.address()`.
20//!
21//! // Stop the dbus-daemon process by dropping it.
22//! drop(daemon);
23//! ```
24//!
25//! ## Starting custom services using D-Bus activation
26//!
27//! ```no_run
28//! use std::path::Path;
29//!
30//! let daemon = dbus_launch::Launcher::daemon()
31//!     .service("com.example.Test", Path::new("/usr/lib/test-service"))
32//!     .launch()
33//!     .expect("failed to launch dbus-daemon");
34//!
35//! // Use com.example.Test service by connecting to `daemon.address()`.
36//!
37//! ```
38
39use crate::process::Process;
40use crate::xml::XmlWriter;
41use std::ffi::{OsStr, OsString};
42use std::fs;
43use std::io;
44use std::os::unix::ffi::*;
45use std::os::unix::io::AsRawFd;
46use std::os::unix::net::UnixListener;
47use std::path::{Path, PathBuf};
48use std::time::Duration;
49
50mod pipe;
51mod process;
52mod sys;
53mod xml;
54
55/// A D-Bus daemon launcher.
56#[derive(Clone, Debug)]
57pub struct Launcher {
58    program: Option<OsString>,
59    daemon_type: DaemonType,
60    config: Config,
61    services: Vec<Service>,
62}
63
64#[derive(Clone, Debug, Default)]
65struct Config {
66    bus_type: Option<BusType>,
67    allow_anonymous: bool,
68    listen: Vec<String>,
69    auth: Vec<Auth>,
70    service_dirs: Vec<PathBuf>,
71}
72
73
74/// A type of a D-Bus daemon.
75#[non_exhaustive]
76#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
77pub enum DaemonType {
78    /// A dbus-daemon from the reference implementation.
79    DBusDaemon,
80    /// A dbus-broker.
81    DBusBroker,
82}
83
84#[derive(Clone, Debug)]
85struct Service {
86    name: String,
87    exec: PathBuf,
88}
89
90/// A running D-Bus daemon process.
91///
92/// The process is killed on drop.
93#[derive(Debug)]
94pub struct Daemon {
95    address: String,
96    tmp_dir: tempfile::TempDir,
97    process: Process,
98}
99
100/// An authentication mechanism.
101#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
102pub enum Auth {
103    Anonymous,
104    External,
105    DBusCookieSha1,
106}
107
108/// A well-known message bus type.
109#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
110pub enum BusType {
111    Session,
112    System,
113}
114
115impl Launcher {
116    /// Returns a new launcher for given type of D-Bus daemon.
117    pub fn new(daemon_type: DaemonType) -> Launcher {
118        Launcher {
119            program: None,
120            daemon_type,
121            config: Config::default(),
122            services: Vec::default(),
123        }
124    }
125
126    /// Returns a new launcher for dbus-daemon.
127    pub fn daemon() -> Launcher {
128        Self::new(DaemonType::DBusDaemon)
129    }
130
131    /// Returns a new launcher for dbus-broker.
132    pub fn broker() -> Launcher {
133        Self::new(DaemonType::DBusBroker)
134    }
135
136    /// The well-known type of the message bus.
137    pub fn bus_type(&mut self, bus_type: BusType) -> &mut Self {
138        self.config.bus_type = Some(bus_type);
139        self
140    }
141
142    /// Listen on an additional address.
143    ///
144    /// By default daemon will listen on a Unix domain socket in a temporary
145    /// directory.
146    ///
147    /// # Examples
148    ///
149    /// ```no_run
150    /// let mut launcher = dbus_launch::Launcher::daemon();
151    /// launcher.listen("tcp:host=localhost");
152    /// launcher.listen("unix:abstract=");
153    /// ```
154    pub fn listen(&mut self, listen: &str) -> &mut Self {
155        self.config.listen.push(listen.to_owned());
156        self
157    }
158
159    /// Authorize connections using anonymous mechanism.
160    ///
161    /// This option has no practical effect unless the anonymous mechanism is
162    /// also enabled.
163    pub fn allow_anonymous(&mut self) -> &mut Self {
164        self.config.allow_anonymous = true;
165        self
166    }
167
168    /// Allow authorization mechanism.
169    ///
170    /// By default all known mechanisms are allowed.
171    ///
172    /// # Examples
173    ///
174    /// ```no_run
175    /// let mut launcher = dbus_launch::Launcher::daemon();
176    /// launcher.auth(dbus_launch::Auth::External);
177    /// ```
178    pub fn auth(&mut self, auth: Auth) -> &mut Self {
179        self.config.auth.push(auth);
180        self
181    }
182
183    /// Adds a directory to search for .service files.
184    pub fn service_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
185        let path = path.as_ref().to_path_buf();
186        self.config.service_dirs.push(path);
187        self
188    }
189
190    /// Adds a service file with given name and executable path.
191    pub fn service<P: AsRef<Path>>(&mut self, name: &str, exec: P) -> &mut Self {
192        let name = name.to_string();
193        let exec = exec.as_ref().to_path_buf();
194        self.services.push(Service { name, exec });
195        self
196    }
197
198    #[doc(hidden)]
199    pub fn program(&mut self, program: &OsStr) -> &mut Self {
200        self.program = Some(program.to_owned());
201        self
202    }
203
204    /// Starts the dbus-daemon process.
205    pub fn launch(&self) -> io::Result<Daemon> {
206        let mut config = self.config.clone();
207
208        // Create temporary dir for configuration files.
209        let tmp_dir = tempfile::Builder::new()
210            .prefix("dbus-daemon-rs-")
211            .tempdir()?;
212
213        if DaemonType::DBusDaemon == self.daemon_type && config.listen.is_empty() {
214            // We use unix:dir instead of unix:tmpdir to avoid using abstract
215            // sockets on Linux which are currently poorly supported in Rust
216            // ecosystem.
217            let path = escape_path(&tmp_dir.path());
218            let address = format!("unix:dir={}", &path);
219            config.listen.push(address);
220        }
221
222        // Write service files.
223        if !self.services.is_empty() {
224            config.service_dirs.push(tmp_dir.path().to_owned());
225            for service in &self.services {
226                let file = format!("{}.service", service.name);
227                let path = tmp_dir.path().join(&file);
228                let contents = format!(
229                    "[D-BUS Service]\nName={}\nExec={}\n",
230                    service.name,
231                    service.exec.display()
232                );
233                fs::write(path, contents)?;
234            }
235        }
236
237        // Write daemon config file.
238        let config_file = tmp_dir.path().join("daemon.conf");
239        fs::write(&config_file, config.to_xml().as_bytes())?;
240
241        let program = self.program.as_deref();
242        match self.daemon_type {
243            DaemonType::DBusDaemon => {
244                let (process, address) =
245                    Process::spawn_dbus_daemon(program, &config_file)?;
246                Ok(Daemon {
247                    address,
248                    tmp_dir,
249                    process,
250                })
251            }
252            DaemonType::DBusBroker => {
253                let path = tmp_dir.path().join("socket");
254                let address = format!("unix:path={}", escape_path(&path));
255                let socket = UnixListener::bind(&path)?;
256                let process = Process::spawn_dbus_broker(
257                    program,
258                    &config_file,
259                    socket.as_raw_fd(),
260                )?;
261                Ok(Daemon {
262                    address,
263                    tmp_dir,
264                    process,
265                })
266            }
267        }
268    }
269}
270
271fn escape_path(path: &Path) -> String {
272    use std::fmt::Write;
273
274    let mut escaped = String::new();
275    for b in path.as_os_str().as_bytes().iter().cloned() {
276        match b {
277            b'-'
278            | b'0'..=b'9'
279            | b'A'..=b'Z'
280            | b'a'..=b'z'
281            | b'_'
282            | b'/'
283            | b'.'
284            | b'\\' => {
285                escaped.push(b.into());
286            }
287            _ => {
288                write!(&mut escaped, "%{0:2x}", b).unwrap();
289            }
290        }
291    }
292
293    escaped
294}
295
296impl Daemon {
297    /// Returns the address of the message bus.
298    pub fn address(&self) -> &str {
299        &self.address
300    }
301
302    /// Returns the path to daemon configuration directory.
303    ///
304    /// The directory is temporary and removed after daemon is dropped.
305    pub fn config_dir(&self) -> &Path {
306        self.tmp_dir.path()
307    }
308
309    /// Returns the PID of the daemon process.
310    pub fn pid(&self) -> libc::pid_t {
311        self.process.pid()
312    }
313}
314
315impl Drop for Daemon {
316    fn drop(&mut self) {
317        let _ = self.process.kill(libc::SIGTERM);
318        let _ = self.process.try_wait_timeout(Duration::from_secs(10));
319        let _ = self.process.kill(libc::SIGKILL);
320        let _ = self.process.wait();
321    }
322}
323
324impl Config {
325    fn to_xml(&self) -> String {
326        const DOCTYPE: &str = r#"<!DOCTYPE busconfig PUBLIC
327 "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
328 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">"#;
329
330        let mut s = String::new();
331        s.push_str(DOCTYPE);
332        s.push_str("\n");
333
334        let mut xml = XmlWriter::new(&mut s);
335        xml.start_tag("busconfig");
336
337        if let Some(bus_type) = self.bus_type {
338            xml.tag_with_text(
339                "type",
340                match bus_type {
341                    BusType::Session => "session",
342                    BusType::System => "system",
343                },
344            );
345        }
346
347        if self.allow_anonymous {
348            xml.start_tag("allow_anonymous");
349            xml.end_tag("allow_anonymous");
350        }
351
352        for listen in &self.listen {
353            xml.tag_with_text("listen", listen);
354        }
355
356        for auth in &self.auth {
357            xml.tag_with_text(
358                "auth",
359                match auth {
360                    Auth::Anonymous => "ANONYMOUS",
361                    Auth::External => "EXTERNAL",
362                    Auth::DBusCookieSha1 => "DBUS_COOKIE_SHA1",
363                },
364            );
365        }
366
367        for dir in &self.service_dirs {
368            let dir = dir.to_str().expect("servicedir is not valid UTF-8");
369            xml.tag_with_text("servicedir", dir);
370        }
371
372        xml.start_tag("policy");
373        xml.attr("context", "default");
374
375        xml.start_tag("allow");
376        xml.attr("receive_requested_reply", "true");
377        xml.end_tag("allow");
378
379        xml.start_tag("allow");
380        xml.attr("send_destination", "*");
381        xml.end_tag("allow");
382
383        xml.start_tag("allow");
384        xml.attr("own", "*");
385        xml.end_tag("allow");
386
387        xml.end_tag("policy");
388
389        xml.end_tag("busconfig");
390
391        s
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    /// Verify xml config serialization.
400    #[test]
401    fn to_xml() {
402        let mut c = Config::default();
403        c.bus_type = Some(BusType::Session);
404        c.listen.push("unix:tmpdir=/tmp".into());
405        c.auth.push(Auth::Anonymous);
406        c.auth.push(Auth::External);
407        c.auth.push(Auth::DBusCookieSha1);
408        c.service_dirs.push("/tmp/servicedir".into());
409
410        let actual = c.to_xml();
411        let expected = r#"<!DOCTYPE busconfig PUBLIC
412 "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
413 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
414<busconfig>
415  <type>session</type>
416  <listen>unix:tmpdir=/tmp</listen>
417  <auth>ANONYMOUS</auth>
418  <auth>EXTERNAL</auth>
419  <auth>DBUS_COOKIE_SHA1</auth>
420  <servicedir>/tmp/servicedir</servicedir>
421  <policy context="default">
422    <allow receive_requested_reply="true"/>
423    <allow send_destination="*"/>
424    <allow own="*"/>
425  </policy>
426</busconfig>
427"#;
428
429        assert_eq!(expected, actual, "\n\n{}.\n\n{}.", expected, actual);
430    }
431
432    #[test]
433    fn escape() {
434        assert_eq!("/", &escape_path(Path::new("/")));
435        assert_eq!("/tmp/a%23b", &escape_path(Path::new("/tmp/a#b")));
436    }
437}