sioul_app/devices.rs
1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! This device's sessions, as your other devices learn them through the
5//! sharing folder (docs/database.md, "Devices"; `sioul_sync::devices`): its
6//! start, its clean close, and between them `working`, up.
7//!
8//! - On a computer, a session is the window's life: up when Sioul starts,
9//! down as the last step of a clean close (the window closed, or the stop
10//! signal: cpp/signals.cpp quits Qt), after the final export (`share::closing`).
11//! - On a phone, a session is the time on the screen: up when Sioul comes
12//! back, down when it goes to the background, after its export (Android may
13//! end a process in the background without a word, so it goes down then).
14//! - A reminder handled while no session is open here (a dose's alarm,
15//! "Taken" pressed on its notification) is a session of its own: up, what
16//! it records, its export, down (`receiver`). Only when the last one ends,
17//! the window's or a receiver's, does it go down.
18//! - Each exchange says what it read and wrote (`exported`), never touching
19//! the session: a phone's background service exporting in its own process
20//! never undoes the window's.
21//!
22//! - A computer also says whether a person is at it (`active`): input within
23//! ten minutes and its session unlocked, as its system says, or a press in
24//! Sioul's window or of do-not-disturb's switch, which counts at once. A
25//! phone's background steps slow down while nobody is at any computer
26//! (`others_in_use`); where the system says nothing of it, Sioul running
27//! counts, as before.
28//!
29//! A crash leaves it up: your other devices then say they cannot tell what
30//! was marked here, until it starts again or you say it is off.
31
32use sioul_sync::devices::{COMPUTER, Entry, PHONE};
33use std::sync::Mutex;
34use std::sync::atomic::{AtomicI64, Ordering};
35
36/// The sessions open in this process: the window's, and receivers at work.
37struct Sessions {
38 window: bool,
39 receivers: u32,
40}
41
42static SESSIONS: Mutex<Sessions> = Mutex::new(Sessions { window: false, receivers: 0 });
43
44/// When this process last recorded something of the doses (a mark, a record,
45/// a reminder), and when its last export read the files (milliseconds, this
46/// device's clock). A session goes down only once an export read the files
47/// after the last of them: else an answer captured here may not have gone
48/// out, and saying "closed" would make the others sure of what they lack.
49static RECORDED: AtomicI64 = AtomicI64::new(0);
50static EXPORTED: AtomicI64 = AtomicI64::new(0);
51
52/// Something of the doses recorded here (`health::change`, `change_records`):
53/// owed to the others until an export reads it.
54pub(crate) fn recorded() {
55 RECORDED.fetch_max(jiff::Timestamp::now().as_millisecond(), Ordering::SeqCst);
56}
57
58/// Whether everything recorded here went out with an export since.
59fn all_out() -> bool {
60 went_out(RECORDED.load(Ordering::SeqCst), EXPORTED.load(Ordering::SeqCst))
61}
62
63/// Whether what was recorded (`RECORDED`) went out with an export since
64/// (`EXPORTED`): both 0 in a process just started, which knows nothing of an
65/// earlier one's.
66fn went_out(recorded: i64, exported: i64) -> bool {
67 recorded < exported || recorded == 0
68}
69
70fn sessions() -> std::sync::MutexGuard<'static, Sessions> {
71 SESSIONS.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
72}
73
74fn now() -> i64 {
75 jiff::Timestamp::now().as_second()
76}
77
78/// This device's name, to say where: a computer's host name; a phone's
79/// model (its host name is "localhost"), said "the phone" by the others.
80pub(crate) fn name() -> String {
81 static NAME: std::sync::OnceLock<String> = std::sync::OnceLock::new();
82 NAME.get_or_init(|| {
83 if cfg!(target_os = "android") {
84 let model = std::process::Command::new("/system/bin/getprop").arg("ro.product.model").output().ok().map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string());
85 model.filter(|m| !m.is_empty()).unwrap_or_else(|| PHONE.to_string())
86 } else {
87 sioul_sync::lease::host_name()
88 }
89 })
90 .clone()
91}
92
93/// A phone, or a computer.
94pub(crate) fn kind() -> &'static str {
95 if cfg!(target_os = "android") { PHONE } else { COMPUTER }
96}
97
98/// This process's opening, its entry's `started`, once written into the
99/// folder (Unix seconds); 0 while not (sharing off, the key not read, the
100/// folder not writable). Until every phone said it read it, this computer
101/// is never a dose's first reminder (`health::first_here`).
102static PUBLISHED: AtomicI64 = AtomicI64::new(0);
103
104/// This process's opening as written into the folder; 0 while it is not.
105pub(crate) fn published_start() -> i64 {
106 PUBLISHED.load(Ordering::SeqCst)
107}
108
109/// This device's entry changed by `change`, its identity set: written here,
110/// and into the folder when sharing is on. Never fails loudly: the doses of
111/// the others say what they cannot know when it is not written. Whether it
112/// went into the folder.
113fn write(change: impl FnOnce(&mut Entry)) -> bool {
114 let Some((id, vault)) = crate::share::vault() else { return false };
115 let path = sioul_sync::devices::own_path(&sioul_core::config::state_dir());
116 // Its doses said known only while their part travels: held under a newer
117 // format (`share::holds`), they are not, whatever this process recorded.
118 let doses = doses_said(crate::share::shares("health"), crate::share::holds_doses());
119 // A phone's notification access, as Android says it now: what it may hold of other apps.
120 let notifications = crate::appnotes::access_here();
121 // A phone's word on each computer's opening it read (none on a computer: `health::acknowledged_openings`).
122 let acked = crate::health::acknowledged_openings();
123 let written = sioul_sync::devices::change(&path, vault.as_ref().map(|(folder, key)| (folder.as_path(), key)), |entry| {
124 entry.id = id;
125 entry.name = name();
126 entry.kind = kind().into();
127 // The build that writes it (docs/database.md, "Devices"): which code each device runs.
128 entry.version = sioul_core::build::VERSION.into();
129 entry.commit = sioul_core::build::COMMIT.into();
130 entry.format = sioul_sync::share::FORMAT;
131 // How it keeps the sealed files no record points to any more: the others keep theirs two weeks once every device says so.
132 entry.keeping = sioul_sync::blobs::KEEPING;
133 entry.doses = doses;
134 if notifications.is_some() {
135 entry.notifications = notifications;
136 }
137 entry.acked = acked;
138 change(entry);
139 });
140 match written {
141 Ok(entry) if vault.is_some() => {
142 // Its opening, as the folder now says it: up since then, or down.
143 PUBLISHED.store(if entry.working { entry.started } else { 0 }, Ordering::SeqCst);
144 true
145 }
146 Ok(_) => false,
147 Err(e) => {
148 eprintln!("sioul: devices: {e}");
149 false
150 }
151 }
152}
153
154/// The window opened, or a phone's Sioul back on the screen: up. Found up
155/// already (the last session ended without closing: a crash), what it left
156/// is owed an export before the next close.
157pub(crate) fn window_opened() {
158 let mut open = sessions();
159 open.window = true;
160 raise();
161}
162
163/// Up, said; a session found up already owes an export. A computer says
164/// too whether someone is at it (`active_here`).
165fn raise() {
166 let active = active_here();
167 // Not written into the folder: no opening of this process said there yet.
168 if !write(|entry| {
169 if entry.working {
170 recorded();
171 }
172 entry.start(now());
173 entry.active = active;
174 }) {
175 PUBLISHED.store(0, Ordering::SeqCst);
176 }
177}
178
179/// This computer came back after a gap in its own running (asleep, the
180/// machine frozen): a new opening, as a start, so that every phone's word on
181/// it is asked anew before it is a dose's first (`health::tick`; the seventh
182/// review's C1).
183pub(crate) fn resumed() {
184 raise();
185}
186
187/// When a press was last made in Sioul on this computer: in its window, or
188/// of do-not-disturb's switch (Unix seconds; 0, none in this process).
189static PRESSED: AtomicI64 = AtomicI64::new(0);
190
191/// A press in Sioul's window (cpp/application.cpp), or of do-not-disturb's
192/// switch (`everywhere::press`), on this computer: a person is at it, whatever
193/// its system says of input, for ten minutes, and whatever the session's
194/// last reading said of its lock when the press is newer than it
195/// (`sioul_sync::power::present_since`). Kept for the next export, at the
196/// minute's cadence: never an exchange of its own, which would tell the
197/// server when someone came back to this computer (review of battery part C, G2).
198pub(crate) fn pressed() {
199 PRESSED.fetch_max(now(), Ordering::SeqCst);
200}
201
202/// A press in Sioul's window, from its event filter (cpp/application.cpp),
203/// a few seconds apart at most: `pressed`.
204// SAFETY: no other symbol of the program has this name.
205#[unsafe(no_mangle)]
206pub extern "C" fn sioul_window_pressed() {
207 let _ = std::panic::catch_unwind(pressed);
208}
209
210/// Whether a person is at this device now, for its entry (`Entry::active`):
211/// on a phone, unsaid (in use only while Sioul is on its screen); on a
212/// computer, as `active_of` reads the session's last reading and the last
213/// press in Sioul. The system is read on a thread of its own every thirty
214/// seconds (`watch_system`): an exchange, a start or a close only read the
215/// value kept, and never wait on the system's bus (a bus that never answers
216/// once held the exchanges for good: review of battery part C, F3 and G1).
217/// No reading yet, or one two minutes old: unsaid (Sioul running counts), a
218/// recent press aside.
219fn active_here() -> Option<bool> {
220 if cfg!(target_os = "android") {
221 return None;
222 }
223 watch_system();
224 active_of(false, sioul_sync::power::kept_session(), PRESSED.load(Ordering::SeqCst), now())
225}
226
227/// The system's reading thread started and asked for readings
228/// (`sioul_sync::power::watch`): this computer's session, its power supply,
229/// its connection's cost, read on connections of its own and kept; the one
230/// way this program has them read. In this crate's tests, on a stand-in bus
231/// of their own that never answers (`tests::stand_in_bus`): never the
232/// session or the system of whoever runs them.
233pub(crate) fn watch_system() {
234 #[cfg(test)]
235 tests::stand_in_bus();
236 sioul_sync::power::watch();
237}
238
239/// Every fact of this computer's (`sioul_sync::power::read`, through
240/// `watch_system`): may wait for a fresh reading ten seconds at most, so only
241/// on a thread of its own (the spam filter's training by itself), never in
242/// an exchange (`share::metered` reads what is kept). A computer's alone: a
243/// phone never trains.
244#[cfg(not(target_os = "android"))]
245pub(crate) fn power_now() -> sioul_sync::power::Power {
246 watch_system();
247 sioul_sync::power::read()
248}
249
250/// `active_here`, from what was read: a phone; the session's last reading
251/// and how long ago it was made (none: none fresh); when Sioul was last
252/// pressed here (Unix seconds, 0 never); now.
253fn active_of(phone: bool, reading: Option<(sioul_sync::power::Session, std::time::Duration)>, pressed: i64, now: i64) -> Option<bool> {
254 if phone {
255 return None;
256 }
257 let ago = (pressed > 0).then(|| std::time::Duration::from_secs(now.saturating_sub(pressed).max(0) as u64));
258 sioul_sync::power::present_since(reading, ago)
259}
260
261/// Down, said, the last step of a session, once all it recorded went out;
262/// else it stays up, and the others say what they cannot know until the
263/// next session's export.
264fn lower() -> bool {
265 if !may_lower(all_out(), crate::share::holds_doses()) {
266 eprintln!("sioul: devices: what was recorded here has not gone out; this device stays up");
267 return false;
268 }
269 write(|entry| entry.close(now()));
270 true
271}
272
273/// Whether this device's entry may say it shares its doses: it shares the
274/// health part, and that part is not held here under a newer format (its
275/// answers then wait unsent, and the others must not count them as known).
276fn doses_said(shares: bool, held: bool) -> bool {
277 shares && !held
278}
279
280/// Whether a session may go down (closed: everything it captured went out):
281/// all it recorded went out, and the doses' part is not held here, which
282/// this process's own counts cannot tell after a restart.
283fn may_lower(all_out: bool, held: bool) -> bool {
284 all_out && !held
285}
286
287/// The window's session ended, its final export done (`share::closing`):
288/// down, unless a receiver is still at work, or `ended` says it is not over
289/// after all (a phone's Sioul brought back meanwhile).
290pub(crate) fn window_closed(ended: impl Fn() -> bool) {
291 let mut open = sessions();
292 if !ended() {
293 return;
294 }
295 open.window = false;
296 if open.receivers == 0 {
297 lower();
298 }
299}
300
301/// `work` (what a reminder records, then its export) as a session of its own
302/// when none is open here: up first, so that a process ended in its midst
303/// leaves the others in doubt rather than sure; down after, then the sync app
304/// asked to carry it (`share::nudge_carriers`): the lowered entry goes up,
305/// not the raised one.
306pub(crate) fn receiver<T>(work: impl FnOnce() -> T) -> T {
307 {
308 let mut open = sessions();
309 if !open.window && open.receivers == 0 {
310 raise();
311 }
312 open.receivers += 1;
313 }
314 let out = work();
315 let lowered = {
316 let mut open = sessions();
317 open.receivers = open.receivers.saturating_sub(1);
318 !open.window && open.receivers == 0 && lower()
319 };
320 if lowered {
321 crate::share::nudge_carriers();
322 }
323 out
324}
325
326/// Whether a reminder's own session is at work in this process (no window open):
327/// the sync app is then asked once it is down, not by each exchange inside it.
328pub(crate) fn in_receiver() -> bool {
329 let open = sessions();
330 !open.window && open.receivers > 0
331}
332
333/// An exchange done: what it read and wrote, said; the session as it is.
334/// The first export of a session is carried at once (a phone's sync app
335/// asked to look): the others see this device in use with fresh news, not
336/// with what it last shared before it was put away.
337pub(crate) fn exported(outcome: &sioul_sync::share::Outcome) {
338 if outcome.looked <= 0 {
339 return;
340 }
341 EXPORTED.fetch_max(outcome.looked, Ordering::SeqCst);
342 // A computer says whether someone is at it now (sealed in its entry, written at
343 // the same cadence either way): while you sleep, a phone steps slower when nobody is.
344 let active = active_here();
345 let first = std::sync::atomic::AtomicBool::new(false);
346 write(|entry| {
347 first.store(entry.working && entry.exported < entry.started, std::sync::atomic::Ordering::Relaxed);
348 entry.exported(outcome.looked, outcome.wrote, now());
349 entry.active = active;
350 });
351 if first.load(std::sync::atomic::Ordering::Relaxed) && !in_receiver() {
352 crate::share::nudge_carriers();
353 }
354}
355
356/// This device replaces another (a phone that will never share again: dead,
357/// lost), as you said when it joined (`join::replace`): written into its
358/// entry, sealed, for every device to read (`Entry::replaces`), what that
359/// one last said with it. Whether it went into the folder.
360pub(crate) fn replace(replaced: sioul_sync::devices::Replaced) -> bool {
361 write(|entry| {
362 entry.replaces.retain(|r| r.id != replaced.id);
363 entry.replaces.push(replaced);
364 })
365}
366
367/// Sharing stopped here: down, and said to count no more.
368pub(crate) fn left() {
369 write(|entry| {
370 entry.close(now());
371 entry.left = true;
372 });
373}
374
375/// This device's id and every device's entry, as the folder and the last
376/// pull's copy hold them (`sioul_sync::devices::all`); none while sharing is off.
377pub(crate) fn entries() -> Option<(String, Vec<Entry>)> {
378 let Some((id, Some((folder, key)))) = crate::share::vault() else { return None };
379 Some((id, sioul_sync::devices::all(&folder, &key).0))
380}
381
382/// Whether another device says it is in use, with an export dated lately
383/// (`FRESH` and `SKEW`), and, while you sleep (`asleep`), a computer only with
384/// someone at it (`sioul_sync::devices::others_in_use`): for the rhythm of a
385/// phone's background service only; the doses' knowing is `health::know`'s.
386pub(crate) fn others_in_use(asleep: bool) -> bool {
387 entries().is_some_and(|(id, entries)| sioul_sync::devices::others_in_use(&entries, &id, now(), asleep))
388}
389
390/// A device's kind ("phone", "computer") and name, from its entry in the
391/// folder; none when it has none (an older Sioul, or not arrived yet).
392pub(crate) fn kind_and_name(id: &str) -> Option<(String, String)> {
393 let (_, Some((folder, key))) = crate::share::vault()? else { return None };
394 sioul_sync::devices::all(&folder, &key).0.into_iter().find(|e| e.id == id).map(|e| (e.kind, e.name))
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 /// A process started anew (an app restart; on Android, the background
402 /// process killed and started again) knows nothing of what an earlier one
403 /// recorded: its counts say all went out. While the doses' part is held
404 /// under a newer format (`share::holds`, read from the disk), its entry
405 /// still says its doses are not known, and its session never goes down as
406 /// if they were: the others doubt, never take a dose as not taken
407 /// (docs/health.md, "A newer Sioul's doses").
408 #[test]
409 fn a_held_doses_part_claims_no_knowledge_after_a_restart() {
410 // The statics as a new process has them: nothing recorded, nothing exported.
411 let (recorded, exported) = (0, 0);
412 assert!(went_out(recorded, exported), "a new process: all went out, by its own counts");
413 assert!(!may_lower(went_out(recorded, exported), true), "held: never down as if all went out");
414 assert!(!doses_said(true, true), "held: its doses said not shared");
415 assert!(may_lower(went_out(recorded, exported), false) && doses_said(true, false), "not held: as before");
416 assert!(!may_lower(went_out(5, 3), false), "an answer recorded and not exported keeps it up, as before");
417 }
418
419 /// A press in Sioul (its window, do-not-disturb's switch) makes this
420 /// computer active at once, whatever its system says of input, for ten
421 /// minutes; its session locked, nobody; its system saying nothing,
422 /// unsaid (Sioul running counts, as before); a phone says nothing of it.
423 #[test]
424 fn a_press_makes_this_computer_active_at_once() {
425 use sioul_sync::power::Session;
426 use std::time::Duration;
427 let now = 1_800_000_000;
428 // The session as read on its own thread, ten seconds ago.
429 let read = |session: Session| Some((session, Duration::from_secs(10)));
430 let idle = Session { since_input: Some(Duration::from_secs(2 * 3600)), locked: false, disputed: false };
431 assert_eq!(active_of(false, read(idle), 0, now), Some(false), "two hours without input: nobody");
432 assert_eq!(active_of(false, read(idle), now - 2, now), Some(true), "a press two seconds ago: someone, at once");
433 assert_eq!(active_of(false, read(idle), now - 600, now), Some(false), "ten minutes on: the system's word again");
434 assert_eq!(active_of(false, read(Session { since_input: Some(Duration::from_secs(30)), ..idle }), 0, now), Some(true));
435 // Locked when read: nobody; a press made since the reading wins over its lock (review of
436 // battery part C, F4: someone unlocked the computer to make it).
437 let locked = Session { locked: true, ..idle };
438 assert_eq!(active_of(false, read(locked), now - 60, now), Some(false), "pressed before the reading that says locked");
439 assert_eq!(active_of(false, read(locked), now - 2, now), Some(true), "pressed after it: someone");
440 // No reading kept (none yet, or it stopped coming): unsaid, Sioul running counts; a recent press still says someone.
441 assert_eq!(active_of(false, None, 0, now), None, "nothing read: unsaid");
442 assert_eq!(active_of(false, None, now - 30, now), Some(true));
443 assert_eq!(active_of(false, read(Session::default()), 0, now), None, "the system says nothing: unsaid");
444 assert_eq!(active_of(true, read(idle), now, now), None, "a phone says nothing of it");
445 // `pressed` keeps the time, nothing else: no exchange of its own, which
446 // would tell the server when someone came back (review of battery part C, G2).
447 pressed();
448 assert!(super::now() - PRESSED.load(Ordering::SeqCst) < 5);
449 let source = |path: &str| std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(path)).unwrap();
450 let devices = source("src/devices.rs");
451 let press = devices.split("pub(crate) fn pressed() {").nth(1).and_then(|rest| rest.split("\n}\n").next()).unwrap();
452 assert_eq!(press.trim(), "PRESSED.fetch_max(now(), Ordering::SeqCst);", "a press asks no exchange");
453 assert!(!source("src/share.rs").contains("fn exchange_soon"), "nothing left to ask one");
454 // Do-not-disturb's switch and the window's presses both say it (read from the sources).
455 let everywhere = source("src/everywhere.rs");
456 let press_via = everywhere.split("fn press_via(").nth(1).and_then(|rest| rest.split("\n}\n").next()).unwrap();
457 assert!(press_via.contains("crate::devices::pressed();"), "a press of the switch counts at once");
458 let application = source("cpp/application.cpp");
459 assert!(application.contains("installEventFilter(new Presses(application))") && application.contains("sioul_window_pressed();"), "a press in the window counts at once");
460 }
461
462 /// A bus for this crate's tests that takes connections and never answers
463 /// (a Linux abstract socket of their own, its listener holding each
464 /// connection, nothing written to disk): the system's reading thread,
465 /// started on it for both buses (`watch_system`), is held in its first
466 /// connection, as by a wedged bus, and never reads the session or the
467 /// system of whoever runs the tests. Elsewhere, no bus at all (Windows and
468 /// macOS read no D-Bus).
469 pub(super) fn stand_in_bus() {
470 static MADE: std::sync::Once = std::sync::Once::new();
471 MADE.call_once(|| {
472 let address = silent_bus();
473 sioul_sync::power::watch_on(address.clone(), address);
474 });
475 }
476
477 /// `stand_in_bus`'s socket: its D-Bus address, none where there is none.
478 fn silent_bus() -> Option<String> {
479 #[cfg(target_os = "linux")]
480 {
481 use std::os::linux::net::SocketAddrExt;
482 let name = format!("sioul-app-tests-{}", std::process::id());
483 let address = std::os::unix::net::SocketAddr::from_abstract_name(name.as_bytes()).ok()?;
484 let listener = std::os::unix::net::UnixListener::bind_addr(&address).ok()?;
485 std::thread::spawn(move || {
486 let mut held = Vec::new();
487 for stream in listener.incoming().flatten() {
488 held.push(stream);
489 }
490 });
491 Some(format!("unix:abstract={name}"))
492 }
493 #[cfg(not(target_os = "linux"))]
494 None
495 }
496
497 /// An exchange, a start or a close never wait on the system's bus: this
498 /// computer's activity is read from the value the system's own thread
499 /// keeps, at once, whatever the bus does (review of battery part C, F3
500 /// and G1: a bus that never answered once held the exchanges for good).
501 /// Here that thread is held for good by a stand-in bus that never answers
502 /// (`stand_in_bus`), as by a wedged one, and the activity comes at once,
503 /// unsaid; never read from the real session or system of whoever runs it.
504 #[test]
505 fn the_activity_is_read_without_waiting_on_the_bus() {
506 let _ = active_here();
507 std::thread::sleep(std::time::Duration::from_millis(300));
508 let start = std::time::Instant::now();
509 for _ in 0..100 {
510 let _ = active_here();
511 }
512 assert!(start.elapsed() < std::time::Duration::from_millis(200), "{:?}", start.elapsed());
513 assert!(sioul_sync::power::kept_session().is_none(), "the stand-in never answers: nothing read");
514 let source = |path: &str| std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(path)).unwrap();
515 let devices = source("src/devices.rs");
516 let body = devices.split("fn active_here() -> Option<bool> {").nth(1).and_then(|rest| rest.split("\n}\n").next()).unwrap();
517 assert!(body.contains("watch_system();") && body.contains("kept_session()"), "the kept reading, never one of its own");
518 let watch = devices.split("pub(crate) fn watch_system() {").nth(1).and_then(|rest| rest.split("\n}\n").next()).unwrap();
519 assert!(watch.contains("#[cfg(test)]\n tests::stand_in_bus();\n sioul_sync::power::watch();"), "tests start it on their stand-in first");
520 // The system read through `watch_system` alone, and an exchange reads what is kept.
521 for file in std::fs::read_dir(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src")).unwrap().flatten() {
522 let path = file.path();
523 if path.extension().is_some_and(|e| e == "rs") && !path.ends_with("devices.rs") {
524 let text = std::fs::read_to_string(&path).unwrap();
525 for direct in ["power::read(", "power::supply(", "power::watch(", "power::watch_on("] {
526 assert!(!text.contains(direct), "{}: {direct} (through devices::watch_system)", path.display());
527 }
528 }
529 }
530 let share = source("src/share.rs");
531 let metered = share.split("fn metered() -> bool {").nth(1).and_then(|rest| rest.split("\n}\n").next()).unwrap();
532 assert!(metered.contains("kept_power()") && metered.contains("watch_system()"), "the cost of the connection as kept");
533 }
534}