Skip to main content

sioul_sync/
bus.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! Every D-Bus connection Sioul makes on Linux and the BSDs, made here alone:
5//! the system's reading (power.rs), the notifications (notify.rs), the
6//! desktop's do-not-disturb (dnd.rs), colord's word on the screens
7//! (sioul-app's colour.rs).
8//!
9//! libdbus's own ways to reach a bus (`dbus_bus_get`, `dbus_bus_get_private`,
10//! `dbus_bus_register`, behind the `dbus` crate's `new_session`, `new_system`
11//! and `new_address`) hold its process-wide locks across the connection's
12//! `Hello`, which they wait for without limit: a bus that takes a connection
13//! and never answers held every other D-Bus connection of the process with
14//! it, being made or closed, and Qt's own at Sioul's exit (review of battery
15//! part C, G1). Here the connection is opened as a plain one
16//! (`dbus_connection_open_private`, which takes none of those locks), its
17//! `Hello` said by Sioul, and the whole setup (the socket, the
18//! authentication, the `Hello`'s answer) waited for on a thread of its own,
19//! `SETUP` at most: such a bus then holds that one thread, never the caller
20//! and never another connection. While a setup to a bus has been under way
21//! longer than its limit, none more is tried there: the next is refused at
22//! once, until the bus answers it, or another bus answers at the same place
23//! (a socket file made anew: `socket_of`).
24//!
25//! The connection made is the same as before in every other way: private,
26//! not closing the program when the bus goes, its name given by the bus.
27//! Only libdbus's own record of that name is not kept (`unique_name` says
28//! none), which nothing in Sioul reads.
29
30use dbus::Message;
31use dbus::channel::Channel;
32use std::sync::Mutex;
33use std::sync::atomic::{AtomicU64, Ordering};
34use std::time::{Duration, Instant};
35
36/// How long a connection's setup may take: its socket, its authentication
37/// and its `Hello`'s answer.
38pub const SETUP: Duration = Duration::from_secs(5);
39
40/// The setups under way: the bus's address, when it began, its limit, and its number.
41static UNDER_WAY: Mutex<Vec<Setup>> = Mutex::new(Vec::new());
42
43/// A setup under way (`UNDER_WAY`).
44#[derive(Debug, Clone)]
45struct Setup {
46    address: String,
47    /// The socket file it went to, as it was then (`socket_of`): another bus
48    /// started at the same place is another socket file.
49    socket: Option<(u64, u64)>,
50    began: Instant,
51    limit: Duration,
52    number: u64,
53}
54
55/// Whether a new setup to `address`, whose socket file is `socket` now, is
56/// refused at `now`: one under way to the same bus (the same address, the
57/// same socket file) has been so longer than its limit (the bus takes
58/// connections and does not answer them). A bus started again at that path,
59/// the old one hung but alive, is another bus, tried at once.
60fn refused(under_way: &[Setup], address: &str, socket: Option<(u64, u64)>, now: Instant) -> bool {
61    under_way.iter().any(|setup| setup.address == address && setup.socket == socket && now.saturating_duration_since(setup.began) >= setup.limit)
62}
63
64/// The socket file of a `unix:path=` address (its first one), as it is now:
65/// its device and inode; none for another kind of address (an abstract
66/// socket's name, TCP), or no file there.
67fn socket_of(address: &str) -> Option<(u64, u64)> {
68    use std::os::unix::fs::MetadataExt;
69    let found = std::fs::metadata(unix_path(address)?).ok()?;
70    Some((found.dev(), found.ino()))
71}
72
73/// The path a `unix:path=` address names, its escapes undone (`escaped`).
74pub(crate) fn unix_path(address: &str) -> Option<std::path::PathBuf> {
75    use std::os::unix::ffi::OsStringExt;
76    let first = address.split(';').next()?.strip_prefix("unix:")?;
77    let value = first.split(',').find_map(|pair| pair.strip_prefix("path="))?;
78    let mut bytes = Vec::with_capacity(value.len());
79    let mut rest = value.as_bytes();
80    while let Some((&byte, after)) = rest.split_first() {
81        if byte == b'%' {
82            let hex = std::str::from_utf8(after.get(..2)?).ok()?;
83            bytes.push(u8::from_str_radix(hex, 16).ok()?);
84            rest = &after[2..];
85        } else {
86            bytes.push(byte);
87            rest = after;
88        }
89    }
90    Some(std::path::PathBuf::from(std::ffi::OsString::from_vec(bytes)))
91}
92
93/// A connection to the bus at `address` (a D-Bus address, as
94/// `DBUS_SESSION_BUS_ADDRESS` gives one), made within `SETUP`.
95pub fn connect(address: &str) -> Result<Channel, dbus::Error> {
96    connect_within(address, SETUP)
97}
98
99/// `connect`, made within `limit`: on a thread of its own, its answer waited
100/// for `limit` at most; refused at once while an earlier setup to the same
101/// bus has been under way longer than its own limit (`refused`).
102pub fn connect_within(address: &str, limit: Duration) -> Result<Channel, dbus::Error> {
103    static NEXT: AtomicU64 = AtomicU64::new(0);
104    let number = NEXT.fetch_add(1, Ordering::Relaxed);
105    let socket = socket_of(address);
106    {
107        let mut under_way = UNDER_WAY.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
108        if refused(&under_way, address, socket, Instant::now()) {
109            return Err(dbus::Error::new_failed("the bus did not answer a connection in time"));
110        }
111        under_way.push(Setup { address: address.to_string(), socket, began: Instant::now(), limit, number });
112    }
113    let (sender, answer) = std::sync::mpsc::channel();
114    let target = address.to_string();
115    let started = std::thread::Builder::new().name("sioul-bus-setup".into()).spawn(move || {
116        let made = set_up(&target, limit);
117        finished(number);
118        // Given up meanwhile: dropped here, and the connection with it.
119        let _ = sender.send(made);
120    });
121    if started.is_err() {
122        finished(number);
123        return Err(dbus::Error::new_failed("no thread for the connection"));
124    }
125    answer.recv_timeout(limit).unwrap_or_else(|_| Err(dbus::Error::new_failed("the bus did not answer the connection in time")))
126}
127
128/// Setup `number` no longer under way.
129fn finished(number: u64) {
130    UNDER_WAY.lock().unwrap_or_else(std::sync::PoisonError::into_inner).retain(|setup| setup.number != number);
131}
132
133/// The setup itself: the socket, then this connection's `Hello`, said here
134/// and its answer waited for `limit` at most once it went out (it goes out
135/// only once the bus answered the authentication).
136fn set_up(address: &str, limit: Duration) -> Result<Channel, dbus::Error> {
137    let channel = Channel::open_private(address)?;
138    let hello = Message::new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "Hello").map_err(|e| dbus::Error::new_failed(&e))?;
139    channel.send_with_reply_and_block(hello, limit)?;
140    Ok(channel)
141}
142
143/// A connection to the session's bus (`session_address`), made within `SETUP`.
144pub fn session() -> Result<Channel, dbus::Error> {
145    let address = session_address().ok_or_else(|| dbus::Error::new_failed("no session bus"))?;
146    connect(&address)
147}
148
149/// A connection to the system's bus (`system_address`), made within `SETUP`.
150pub fn system() -> Result<Channel, dbus::Error> {
151    connect(&system_address())
152}
153
154/// The session's bus as libdbus finds it: `DBUS_SESSION_BUS_ADDRESS`, else
155/// the user's bus in `XDG_RUNTIME_DIR` when there is one; none, no session bus.
156pub fn session_address() -> Option<String> {
157    if let Some(address) = std::env::var("DBUS_SESSION_BUS_ADDRESS").ok().filter(|address| !address.is_empty()) {
158        return Some(address);
159    }
160    let bus = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR")?).join("bus");
161    bus.exists().then(|| format!("unix:path={}", escaped(&bus)))
162}
163
164/// The system's bus as libdbus finds it: `DBUS_SYSTEM_BUS_ADDRESS`, else its usual socket.
165pub fn system_address() -> String {
166    std::env::var("DBUS_SYSTEM_BUS_ADDRESS").ok().filter(|address| !address.is_empty()).unwrap_or_else(|| "unix:path=/var/run/dbus/system_bus_socket".to_string())
167}
168
169/// A path as a D-Bus address writes it: each byte but letters, digits and
170/// `-_/.\*` as `%` and its two hex digits.
171pub fn escaped(path: &std::path::Path) -> String {
172    use std::os::unix::ffi::OsStrExt;
173    path.as_os_str().as_bytes().iter().map(|&byte| if byte.is_ascii_alphanumeric() || b"-_/.\\*".contains(&byte) { char::from(byte).to_string() } else { format!("%{byte:02x}") }).collect()
174}
175
176/// The tests that stand in for the session's notification server, on the
177/// test's own bus (dbus-run-session), take turns: one name, one owner.
178#[cfg(test)]
179pub(crate) static NOTIFICATION_SERVER: Mutex<()> = Mutex::new(());
180
181/// Two stand-in buses for tests: private `dbus-daemon`s in a scratch folder,
182/// never the session or the system of whoever runs them; ended and removed
183/// however the test ends.
184#[cfg(test)]
185pub(crate) struct StandIns {
186    dir: std::path::PathBuf,
187    daemons: Vec<std::process::Child>,
188}
189
190#[cfg(test)]
191impl StandIns {
192    /// A scratch folder named after `name` and this process.
193    pub(crate) fn new(name: &str) -> StandIns {
194        let dir = std::env::temp_dir().join(format!("sioul-{name}-{}", std::process::id()));
195        let _ = std::fs::remove_dir_all(&dir);
196        std::fs::create_dir_all(&dir).unwrap();
197        StandIns { dir, daemons: Vec::new() }
198    }
199
200    /// A private bus named `name`: its pid and address; none of the system's
201    /// services on it, nothing started for a name asked, as the session's own
202    /// policy otherwise (session.conf).
203    pub(crate) fn start(&mut self, name: &str) -> (u32, String) {
204        use std::process::{Command, Stdio};
205        let socket = self.dir.join(name);
206        let _ = std::fs::remove_file(&socket);
207        let address = format!("unix:path={}", escaped(&socket));
208        let config = self.dir.join(format!("{name}.conf"));
209        let policy = "<allow send_destination=\"*\" eavesdrop=\"true\"/><allow eavesdrop=\"true\"/><allow own=\"*\"/>";
210        std::fs::write(&config, format!("<!DOCTYPE busconfig PUBLIC \"-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN\" \"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd\">\n<busconfig><type>session</type><listen>{address}</listen><auth>EXTERNAL</auth><policy context=\"default\">{policy}</policy></busconfig>\n")).unwrap();
211        let daemon = Command::new("dbus-daemon").arg(format!("--config-file={}", config.display())).arg("--nofork").stdout(Stdio::null()).stderr(Stdio::null()).spawn().expect("dbus-daemon");
212        let pid = daemon.id();
213        self.daemons.push(daemon);
214        let until = Instant::now() + Duration::from_secs(5);
215        while !socket.exists() && Instant::now() < until {
216            std::thread::sleep(Duration::from_millis(20));
217        }
218        assert!(socket.exists(), "{name}: no socket");
219        (pid, address)
220    }
221
222    /// Sends a signal ("-STOP", "-CONT", "-KILL") to a stand-in.
223    pub(crate) fn signal(pid: u32, signal: &str) {
224        let sent = std::process::Command::new("kill").args([signal, &pid.to_string()]).status().is_ok_and(|status| status.success());
225        assert!(sent, "kill {signal} {pid}");
226    }
227}
228
229#[cfg(test)]
230impl Drop for StandIns {
231    fn drop(&mut self) {
232        for daemon in &mut self.daemons {
233            let _ = daemon.kill();
234            let _ = daemon.wait();
235        }
236        let _ = std::fs::remove_dir_all(&self.dir);
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    /// A setup under way longer than its limit refuses the next ones to the
245    /// same bus at once (the bus takes connections and does not answer them:
246    /// one thread held, never one more); within its limit, to another bus, or
247    /// to another bus started at the same path (another socket file), they go
248    /// ahead (review of battery part C, L1).
249    #[test]
250    fn a_setup_stuck_past_its_limit_refuses_the_next_ones() {
251        let began = Instant::now();
252        let old = Some((44, 1001));
253        let stuck = Setup { address: "unix:path=/run/a".into(), socket: old, began, limit: SETUP, number: 1 };
254        let under_way = [stuck];
255        assert!(!refused(&under_way, "unix:path=/run/a", old, began + SETUP - Duration::from_millis(1)), "within its limit: another may go");
256        assert!(refused(&under_way, "unix:path=/run/a", old, began + SETUP), "past it: refused");
257        assert!(!refused(&under_way, "unix:path=/run/b", None, began + SETUP * 3), "another bus goes ahead");
258        assert!(!refused(&under_way, "unix:path=/run/a", Some((44, 2002)), began + SETUP * 3), "a bus started again at the same path: tried");
259        assert!(!refused(&under_way, "unix:path=/run/a", None, began + SETUP * 3), "no socket there now: tried (and fails at once)");
260        assert!(!refused(&[], "unix:path=/run/a", old, began));
261        // An address without a file (an abstract socket): refused while stuck, as before.
262        let abstract_bus = [Setup { address: "unix:abstract=sioul".into(), socket: None, began, limit: SETUP, number: 2 }];
263        assert!(refused(&abstract_bus, "unix:abstract=sioul", None, began + SETUP));
264    }
265
266    /// The buses' addresses as libdbus finds them, a path written as an
267    /// address writes it, and read back from one.
268    #[test]
269    fn addresses_are_written_as_d_bus_writes_them() {
270        assert_eq!(escaped(std::path::Path::new("/run/user/1000/bus")), "/run/user/1000/bus");
271        assert_eq!(escaped(std::path::Path::new("/tmp/a b,c;é")), "/tmp/a%20b%2cc%3b%c3%a9");
272        let odd = std::path::Path::new("/tmp/a b,c;é");
273        assert_eq!(unix_path(&format!("unix:path={}", escaped(odd))).as_deref(), Some(odd));
274        assert_eq!(unix_path("unix:path=/run/user/1000/bus,guid=0123abcd;unix:path=/other").as_deref(), Some(std::path::Path::new("/run/user/1000/bus")));
275        assert_eq!(unix_path("unix:abstract=/tmp/dbus-abc"), None);
276        assert_eq!(unix_path("tcp:host=localhost,port=4444"), None);
277        assert_eq!(unix_path("unix:path=/tmp/broken%2"), None, "a broken escape reads as no path");
278    }
279
280    /// A bus that does not exist is refused at once, nothing held.
281    #[test]
282    fn a_bus_that_is_not_there_is_refused_at_once() {
283        let start = Instant::now();
284        assert!(connect("unix:path=/nonexistent/sioul-no-bus").is_err());
285        assert!(start.elapsed() < Duration::from_secs(2), "{:?}", start.elapsed());
286        assert!(UNDER_WAY.lock().unwrap().iter().all(|setup| setup.address != "unix:path=/nonexistent/sioul-no-bus"), "nothing left under way");
287    }
288
289    /// Whether `text` names `name` as code does: a whole word, not a part of
290    /// a longer name, wherever it stands (a call, a function used as a value,
291    /// a space before its parenthesis, a comment).
292    fn named(text: &str, name: &str) -> bool {
293        let part = |c: char| c.is_alphanumeric() || c == '_';
294        text.match_indices(name).any(|(at, _)| !text[..at].chars().next_back().is_some_and(part) && !text[at + name.len()..].chars().next().is_some_and(part))
295    }
296
297    /// Whether `text` reaches `name` as a method or a path's last part
298    /// (`.name`, `::name`, spaces allowed before it).
299    fn reached(text: &str, name: &str) -> bool {
300        let part = |c: char| c.is_alphanumeric() || c == '_';
301        text.match_indices(name).any(|(at, _)| {
302            let before = text[..at].trim_end();
303            !text[at + name.len()..].chars().next().is_some_and(part) && (before.ends_with('.') || before.ends_with("::"))
304        })
305    }
306
307    /// No source of Sioul's outside this file reaches a bus through libdbus's
308    /// own registration, nor opens a connection itself: no file under crates/
309    /// (whatever its kind, folders named "target" there included; Qt's
310    /// compiled resources aside) names the `dbus` crate's `new_session`,
311    /// `new_system`, `new_address`, `get_private`, `open_private` or calls a
312    /// `register`, nor libdbus's own functions, nor the older `ffidisp` side,
313    /// nor another way to a bus (`zbus`, the Secret Service's crates: the
314    /// keyring reaches it inside the `keyring` crate alone, as it does until
315    /// the owner decides: docs/android.md), nor notify-rust (which registers
316    /// so) but notify.rs, in any way of writing them; no C or C++ uses Qt's
317    /// D-Bus, and no build script asks for its module. The manifests, read
318    /// as TOML (tables, renamed packages, targets): notify-rust for Windows
319    /// and macOS alone, the `dbus` crate in sioul-sync alone, no other D-Bus
320    /// crate; and the lock has no other crate reaching libdbus than those and
321    /// the keyring's (review of battery part C, L2 of the third and fourth
322    /// reviews).
323    #[test]
324    fn nothing_but_this_file_reaches_a_bus() {
325        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
326        let (mut files, mut manifests) = (Vec::new(), Vec::new());
327        let mut folders = vec![root.join("crates")];
328        while let Some(folder) = folders.pop() {
329            for entry in std::fs::read_dir(&folder).unwrap().flatten() {
330                let path = entry.path();
331                if path.is_dir() {
332                    folders.push(path);
333                } else if path.ends_with("Cargo.toml") {
334                    manifests.push(path);
335                } else if !path.to_string_lossy().ends_with("_rc.py") {
336                    files.push(path);
337                }
338            }
339        }
340        assert!(files.len() > 500 && manifests.len() >= 5, "{} files, {} manifests", files.len(), manifests.len());
341        let words = ["new_session", "new_system", "new_address", "get_private", "open_private", "dbus_bus_get", "dbus_bus_get_private", "dbus_bus_register", "dbus_connection_open", "dbus_connection_open_private", "ffidisp", "libdbus_sys", "dbus_secret_service", "secret_service", "zbus"];
342        // Pictures and fonts: no code. Everything else read as bytes, so that a
343        // file not in UTF-8 (a C file with a Latin-1 comment) is read all the
344        // same (the fifth review's L5).
345        let binary = ["ttf", "otf", "woff", "woff2", "png", "jpg", "jpeg", "gif", "ico", "icns", "qsb", "qm", "bin", "gz", "zst"];
346        for file in &files {
347            if file.ends_with("sioul-sync/src/bus.rs") || file.extension().and_then(|e| e.to_str()).is_some_and(|e| binary.contains(&e)) {
348                continue;
349            }
350            let bytes = std::fs::read(file).unwrap();
351            let text = String::from_utf8_lossy(&bytes);
352            for word in words {
353                assert!(!named(&text, word), "{}: {word} (connections through sioul_sync::bus alone)", file.display());
354            }
355            assert!(!reached(&text, "register"), "{}: a register call (libdbus's own registration)", file.display());
356            if !file.ends_with("sioul-sync/src/notify.rs") {
357                assert!(!named(&text, "notify_rust"), "{}: notify-rust (registers through libdbus)", file.display());
358            }
359            // Qt's own D-Bus, in C++ or through a cxx bridge in Rust (`include!("QtDBus/…")`).
360            assert!(!text.contains("QDBus") && !text.contains("QtDBus"), "{}: Qt's D-Bus", file.display());
361            if file.ends_with("build.rs") {
362                assert!(!text.contains("\"DBus\""), "{}: Qt's D-Bus module", file.display());
363            }
364        }
365        // The manifests, as TOML: each dependency by its package's name, wherever it is declared.
366        manifests.push(root.join("Cargo.toml"));
367        for manifest in &manifests {
368            let table: toml::Table = std::fs::read_to_string(manifest).unwrap().parse().unwrap();
369            let own = manifest.ends_with("sioul-sync/Cargo.toml");
370            let workspace = *manifest == root.join("Cargo.toml");
371            let mut declared: Vec<(String, String)> = Vec::new();
372            let mut sections: Vec<(String, &toml::Table)> = Vec::new();
373            for kind in ["dependencies", "dev-dependencies", "build-dependencies"] {
374                if let Some(deps) = table.get(kind).and_then(toml::Value::as_table) {
375                    sections.push((format!("[{kind}]"), deps));
376                }
377                if let Some(deps) = table.get("workspace").and_then(|w| w.get(kind)).and_then(toml::Value::as_table) {
378                    sections.push((format!("[workspace.{kind}]"), deps));
379                }
380            }
381            if let Some(targets) = table.get("target").and_then(toml::Value::as_table) {
382                for (target, inner) in targets {
383                    for kind in ["dependencies", "dev-dependencies", "build-dependencies"] {
384                        if let Some(deps) = inner.get(kind).and_then(toml::Value::as_table) {
385                            sections.push((format!("[target.'{target}'.{kind}]"), deps));
386                        }
387                    }
388                }
389            }
390            for (section, deps) in sections {
391                for (name, spec) in deps {
392                    let package = spec.get("package").and_then(toml::Value::as_str).unwrap_or(name);
393                    declared.push((section.clone(), package.to_string()));
394                }
395            }
396            for (section, package) in &declared {
397                match package.as_str() {
398                    "notify-rust" => assert!((own && section == "[target.'cfg(any(windows, target_os = \"macos\"))'.dependencies]") || (workspace && section == "[workspace.dependencies]"), "{}: notify-rust under {section}", manifest.display()),
399                    "dbus" => assert!(own && section.contains("not(any(target_os = \"macos\", target_os = \"android\"))"), "{}: dbus under {section}", manifest.display()),
400                    "zbus" | "secret-service" | "dbus-secret-service" | "libdbus-sys" | "dbus-tokio" | "dbus-crossroads" | "zbus_names" | "ashpd" => panic!("{}: {package} under {section} (connections through sioul_sync::bus alone)", manifest.display()),
401                    _ => {}
402                }
403            }
404        }
405        // The lock: who reaches libdbus's crates, and the other ways to a bus.
406        let lock = std::fs::read_to_string(root.join("Cargo.lock")).unwrap();
407        let mut reaching: Vec<(String, String)> = Vec::new();
408        for package in lock.split("[[package]]").skip(1) {
409            let name = package.lines().find_map(|line| line.strip_prefix("name = \"")).and_then(|rest| rest.strip_suffix('"')).unwrap_or_default().to_string();
410            for line in package.lines() {
411                let dependency = line.trim().trim_end_matches(',').trim_matches('"');
412                let dependency = dependency.split(' ').next().unwrap_or_default();
413                if ["dbus", "libdbus-sys"].contains(&dependency) {
414                    reaching.push((dependency.to_string(), name.clone()));
415                }
416            }
417        }
418        reaching.sort();
419        let expected: Vec<(String, String)> = [("dbus", "dbus-secret-service"), ("dbus", "notify-rust"), ("dbus", "sioul-sync"), ("libdbus-sys", "dbus")].iter().map(|(a, b)| (a.to_string(), b.to_string())).collect();
420        assert_eq!(reaching, expected, "the crates that reach libdbus (the keyring's, notify-rust's for Windows and macOS, and this one)");
421        // Who reaches zbus or dbus at all, through any other crate: today's
422        // list, and no other (a new dependency bringing an XDG portal, `rfd`
423        // through `ashpd`, would open a bus of its own: the fifth review's L5).
424        let mut depends: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
425        let lock: toml::Table = lock.parse().unwrap();
426        for package in lock.get("package").and_then(toml::Value::as_array).unwrap() {
427            let name = package.get("name").and_then(toml::Value::as_str).unwrap_or_default().to_string();
428            let on: Vec<String> = package.get("dependencies").and_then(toml::Value::as_array).map(|all| all.iter().filter_map(toml::Value::as_str).map(|d| d.split(' ').next().unwrap_or_default().to_string()).collect()).unwrap_or_default();
429            depends.entry(name).or_default().extend(on);
430        }
431        let reach = |target: &str| -> Vec<String> {
432            let mut found: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
433            loop {
434                let more: Vec<String> = depends.iter().filter(|(name, on)| !found.contains(*name) && *name != target && on.iter().any(|d| d == target || found.contains(d))).map(|(name, _)| name.clone()).collect();
435                if more.is_empty() {
436                    break;
437                }
438                found.extend(more);
439            }
440            found.into_iter().collect()
441        };
442        assert_eq!(reach("zbus"), ["android-keyring", "keyring", "secret-service", "sioul-app", "sioul-cli", "sioul-learn", "sioul-sync"], "who reaches zbus");
443        assert_eq!(reach("dbus"), ["android-keyring", "dbus-secret-service", "keyring", "notify-rust", "sioul-app", "sioul-cli", "sioul-learn", "sioul-sync"], "who reaches dbus");
444    }
445
446    /// A setup stuck on a bus that hangs alive (its socket open, never
447    /// answering) refuses the next ones; another bus started at the same
448    /// path (the socket file made anew) is tried, and answers, while the old
449    /// one still hangs (review of battery part C, L1). Starts `dbus-daemon`:
450    /// run alone, by name:
451    /// `cargo test --release -p sioul-sync bus::tests::a_bus_started_again_at_the_same_path_is_tried -- --ignored --exact`.
452    #[test]
453    #[ignore = "starts a stand-in dbus-daemon: run alone, by name"]
454    fn a_bus_started_again_at_the_same_path_is_tried() {
455        let mut stand_ins = StandIns::new("stand-in-again");
456        let path = stand_ins.dir.join("bus");
457        let address = format!("unix:path={}", escaped(&path));
458        // The old bus: takes connections, never answers, stays alive.
459        let listener = std::os::unix::net::UnixListener::bind(&path).unwrap();
460        let held: std::sync::Arc<Mutex<Vec<std::os::unix::net::UnixStream>>> = std::sync::Arc::default();
461        let keep = std::sync::Arc::clone(&held);
462        std::thread::spawn(move || {
463            for stream in listener.incoming().flatten() {
464                keep.lock().unwrap().push(stream);
465            }
466        });
467        let limit = Duration::from_secs(1);
468        assert!(connect_within(&address, limit).is_err(), "the old bus never answers");
469        assert!(connect_within(&address, limit).is_err(), "and, its setup stuck past its limit, refuses the next at once");
470        // Another bus at the same path (its file made anew), the old one still hanging.
471        std::fs::remove_file(&path).unwrap();
472        let (_, again) = stand_ins.start("bus");
473        assert_eq!(again, address);
474        let start = Instant::now();
475        let made = connect_within(&address, limit);
476        assert!(made.is_ok(), "the bus started again is tried, and answers: {:?}", made.err());
477        assert!(start.elapsed() < limit, "{:?}", start.elapsed());
478        held.lock().unwrap().clear();
479    }
480
481    /// Two stand-in buses (`StandIns`), the session's stopped (SIGSTOP)
482    /// before anything connected to it, as a bus that takes connections and
483    /// never answers (review of battery part C, G1):
484    /// - a connection to it comes back refused within its limit, its thread
485    ///   left held there alone;
486    /// - meanwhile, a connection to the other bus through libdbus's own
487    ///   registration (its process-wide locks) is made and dropped at once;
488    /// - past the limit, the next connection to the stopped bus is refused at
489    ///   once, no thread more;
490    /// - once it answers again, connections to it are made again.
491    /// Starts `dbus-daemon`: run alone, by name:
492    /// `cargo test --release -p sioul-sync bus::tests::a_bus_stuck_at_setup_holds_nothing_else -- --ignored --exact`.
493    #[test]
494    #[ignore = "starts two stand-in dbus-daemons: run alone, by name"]
495    fn a_bus_stuck_at_setup_holds_nothing_else() {
496        use dbus::blocking::Connection;
497        let mut stand_ins = StandIns::new("stand-in-setup");
498        let (stopped_pid, stopped) = stand_ins.start("stopped");
499        let (_, other) = stand_ins.start("other");
500        StandIns::signal(stopped_pid, "-STOP");
501        let watched = |limit: Duration, work: Box<dyn FnOnce() + Send>| -> Option<Duration> {
502            let (sender, came) = std::sync::mpsc::channel();
503            let start = Instant::now();
504            std::thread::spawn(move || {
505                work();
506                let _ = sender.send(start.elapsed());
507            });
508            came.recv_timeout(limit).ok()
509        };
510        // A connection to the stopped bus, on a thread, and libdbus's own to the other meanwhile.
511        let limit = Duration::from_secs(2);
512        let target = stopped.clone();
513        let first = std::thread::spawn(move || {
514            let start = Instant::now();
515            let made = connect_within(&target, limit);
516            (made.is_ok(), start.elapsed())
517        });
518        std::thread::sleep(Duration::from_millis(500));
519        let elsewhere = other.clone();
520        let registered = watched(Duration::from_secs(3), Box::new(move || drop(Connection::new_address(&elsewhere).unwrap())));
521        let (made, took) = first.join().unwrap();
522        assert!(registered.is_some(), "libdbus's own connection to the other bus waited behind the stopped one");
523        assert!(!made && took < limit + Duration::from_secs(1), "refused within its limit: {made} {took:?}");
524        // Past its limit, still under way: the next is refused at once.
525        let start = Instant::now();
526        assert!(connect_within(&stopped, limit).is_err());
527        assert!(start.elapsed() < Duration::from_millis(200), "{:?}", start.elapsed());
528        // The bus answers again: what was under way ends, and connections are made again.
529        StandIns::signal(stopped_pid, "-CONT");
530        let until = Instant::now() + Duration::from_secs(5);
531        while UNDER_WAY.lock().unwrap().iter().any(|setup| setup.address == stopped) && Instant::now() < until {
532            std::thread::sleep(Duration::from_millis(50));
533        }
534        let again = connect(&stopped).map(Connection::from).expect("made again");
535        let (names,): (Vec<String>,) = again.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", Duration::from_secs(2)).method_call("org.freedesktop.DBus", "ListNames", ()).unwrap();
536        assert!(names.iter().any(|name| name.starts_with(':')), "{names:?}");
537    }
538}