Skip to main content

coven_foundation/
network_watch.rs

1//! The device's network, as the platform describes it (§6.7, api.md E5).
2//!
3//! Each open store watches it from opening to closing. The platform says
4//! which usable network the device is on, if any, and whether it is metered;
5//! the watch keeps that, and counts each time a usable network appears or
6//! the device moves to another one. Each such count is a network report,
7//! which ends the waits that followed an attempt with no answer from
8//! storage. Losing the network, or a network turning metered or unmetered,
9//! counts nothing. Where the platform can't tell, the watch says so: no
10//! network counts and none is metered. A platform that fails to answer
11//! after the watch started can't tell either, but its network may be
12//! metered, so the watch calls it metered until the platform answers.
13
14// The platforms coven watches; elsewhere it can't tell.
15#[cfg(target_os = "android")]
16#[path = "network_watch_android.rs"]
17mod android;
18#[cfg(target_vendor = "apple")]
19#[path = "network_watch_apple.rs"]
20mod apple;
21#[cfg(any(test, target_os = "android"))]
22#[path = "network_watch_default.rs"]
23mod default_network;
24#[cfg(target_os = "linux")]
25#[path = "network_watch_linux.rs"]
26mod linux;
27#[cfg(windows)]
28#[path = "network_watch_windows.rs"]
29mod winrt;
30
31#[cfg(any(
32    test,
33    feature = "test-utils",
34    target_vendor = "apple",
35    target_os = "android",
36    target_os = "linux",
37    windows
38))]
39use crate::locks::lock_unpoisoned;
40use std::sync::{Arc, Mutex};
41use tokio::sync::watch;
42
43/// What coven knows of the device's network.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct NetworkState {
46    /// Whether the device has a usable network; `None` where the platform
47    /// can't tell.
48    pub usable: Option<bool>,
49    /// Whether coven treats that network as metered: the platform says it
50    /// is, or failed to answer since the watch started, so it may be. False
51    /// with no network, and where the platform never tells.
52    pub metered: bool,
53    /// How many times a usable network has appeared, or the device moved to
54    /// another one, since the watch started, the network it found at the
55    /// start included: each is a network report (§6.7).
56    pub joined: u64,
57}
58
59impl NetworkState {
60    /// The state where the platform can't tell.
61    const UNKNOWN: Self = Self { usable: None, metered: false, joined: 0 };
62}
63
64/// One usable network, as the platform names it.
65// Only the platforms coven watches, and a test's network, say anything.
66#[cfg(any(
67    test,
68    feature = "test-utils",
69    target_vendor = "apple",
70    target_os = "android",
71    target_os = "linux",
72    windows
73))]
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub(crate) struct UsableNetwork {
76    /// The platform's own name for the network: another name is another
77    /// network.
78    pub(crate) id: String,
79    /// Whether the platform says it is metered.
80    pub(crate) metered: bool,
81}
82
83/// Turns what the platform says into the watch's state, counting each
84/// network that appears.
85#[cfg(any(
86    test,
87    feature = "test-utils",
88    target_vendor = "apple",
89    target_os = "android",
90    target_os = "linux",
91    windows
92))]
93pub(crate) struct NetworkSink {
94    /// The usable network the device was last on.
95    current: Option<String>,
96    state: watch::Sender<NetworkState>,
97}
98
99#[cfg(any(
100    test,
101    feature = "test-utils",
102    target_vendor = "apple",
103    target_os = "android",
104    target_os = "linux",
105    windows
106))]
107impl NetworkSink {
108    fn new() -> Self {
109        Self { current: None, state: watch::channel(NetworkState::UNKNOWN).0 }
110    }
111
112    /// The platform says the device is on `network`, or on none.
113    pub(crate) fn observe(&mut self, network: Option<UsableNetwork>) {
114        let mut next = *self.state.borrow();
115        match network {
116            Some(network) => {
117                if self.current.as_ref() != Some(&network.id) {
118                    next.joined += 1;
119                }
120                next.usable = Some(true);
121                next.metered = network.metered;
122                self.current = Some(network.id);
123            }
124            None => {
125                next.usable = Some(false);
126                next.metered = false;
127                self.current = None;
128            }
129        }
130        self.state.send_if_modified(|state| {
131            let changed = *state != next;
132            *state = next;
133            changed
134        });
135    }
136
137    /// The platform failed to say: a read it answered with an error after
138    /// the watch started. Coven can't tell until a read succeeds, and calls
139    /// the network metered meanwhile: it may be, and the setting to move
140    /// files only on unmetered networks moves them only on one known to be
141    /// unmetered. The network the device was last on stays its last, so
142    /// that network read again is no report: nothing said it changed.
143    pub(crate) fn cannot_tell(&mut self) {
144        self.state.send_if_modified(|state| {
145            let next = NetworkState { usable: None, metered: true, joined: state.joined };
146            let changed = *state != next;
147            *state = next;
148            changed
149        });
150    }
151
152    fn subscribe(&self) -> watch::Receiver<NetworkState> {
153        self.state.subscribe()
154    }
155
156    /// The platform says the network the device is on may work again
157    /// though it never changed, as Android before 10 says when Doze ends:
158    /// a network report, if the device is on a usable one.
159    #[cfg(any(test, target_os = "android"))]
160    pub(crate) fn network_returned(&mut self) {
161        if self.current.is_some() {
162            self.state.send_modify(|state| state.joined += 1);
163        }
164    }
165}
166
167/// Run `read`, what a platform's callback does with what the platform says
168/// of the network. A panic unwinding out of a callback the platform calls
169/// would end the process; here it goes to `bugs` (api.md E5, E15).
170#[cfg(any(test, target_vendor = "apple", target_os = "android", windows))]
171pub(crate) fn heard(
172    sink: &Mutex<NetworkSink>,
173    bugs: &dyn crate::task::BugSink,
174    operation: &'static str,
175    read: impl FnOnce(),
176) {
177    if let Err(bug) = crate::task::caught_sync(operation, read) {
178        panicked(sink, bugs, &bug);
179    }
180}
181
182/// The watch's reading of what the platform says panicked with `bug`: it
183/// goes to `bugs`, and the watch can't tell until the platform says again,
184/// calling the network metered meanwhile, as after a failed read.
185#[cfg(any(test, target_vendor = "apple", target_os = "android", target_os = "linux", windows))]
186pub(crate) fn panicked(
187    sink: &Mutex<NetworkSink>,
188    bugs: &dyn crate::task::BugSink,
189    bug: &crate::task::Bug,
190) {
191    lock_unpoisoned(sink).cannot_tell();
192    crate::task::report(bugs, bug);
193}
194
195/// Where the store's watch hears about the network: the platform's own, or
196/// a test's.
197#[derive(Clone)]
198pub enum NetworkSource {
199    /// What the platform this runs on says (api.md E5).
200    Platform,
201    /// A network a test drives (api.md E1 `network`).
202    #[cfg(any(test, feature = "test-utils"))]
203    Test(TestNetwork),
204    /// A platform that refuses the watch with this error.
205    #[cfg(any(test, feature = "test-utils"))]
206    Refused(NetworkWatchError),
207}
208
209/// The platform refused to let coven watch the device's network (api.md
210/// E1, E5), such as an Android app without coven's Android library.
211#[derive(Clone, Debug, thiserror::Error)]
212pub enum NetworkWatchError {
213    /// The platform's path monitor couldn't be created.
214    #[error("the platform's path monitor could not be created")]
215    MonitorUnavailable,
216    /// The platform's path monitor ended before it described the network.
217    #[error("the platform's path monitor ended before describing the network")]
218    MonitorEnded,
219    /// A call to the platform's network interface failed.
220    #[error("coven can't watch the device's network: `{call}` failed")]
221    Platform {
222        /// The call that failed.
223        call: &'static str,
224        /// What the platform answered.
225        #[source]
226        source: std::sync::Arc<dyn std::error::Error + Send + Sync>,
227    },
228}
229
230#[cfg(any(target_os = "android", target_os = "linux", windows))]
231impl NetworkWatchError {
232    /// The platform's answer to a failed `call`.
233    pub(crate) fn platform(
234        call: &'static str,
235        source: impl std::error::Error + Send + Sync + 'static,
236    ) -> Self {
237        Self::Platform { call, source: std::sync::Arc::new(source) }
238    }
239}
240
241/// One open store's watch of the device's network (§6.7): it starts as the
242/// store opens, already holding what the platform says, and stops as it
243/// closes. Each owner that acts on the network holds a view of it.
244pub struct NetworkWatch {
245    state: watch::Receiver<NetworkState>,
246    /// The platform's registration, which ends as the watch drops.
247    #[cfg(target_vendor = "apple")]
248    _platform: Option<apple::PathMonitor>,
249    #[cfg(target_os = "android")]
250    _platform: Option<android::JavaWatch>,
251    #[cfg(target_os = "linux")]
252    _platform: Option<linux::ManagerWatch>,
253    #[cfg(windows)]
254    _platform: Option<winrt::ProfileWatch>,
255}
256
257impl NetworkWatch {
258    /// Start watching, as `source` says. The state already holds what the
259    /// platform says now. A panic in what the platform's callbacks run goes
260    /// to `bugs`.
261    pub fn start(
262        source: &NetworkSource,
263        #[cfg_attr(
264            not(any(
265                target_vendor = "apple",
266                target_os = "android",
267                target_os = "linux",
268                windows
269            )),
270            expect(unused_variables, reason = "where coven asks nothing, nothing calls back")
271        )]
272        bugs: &crate::task::BugSinkRef,
273    ) -> Result<Self, NetworkWatchError> {
274        match source {
275            #[cfg(any(
276                target_vendor = "apple",
277                target_os = "android",
278                target_os = "linux",
279                windows
280            ))]
281            NetworkSource::Platform => {
282                let sink = Arc::new(Mutex::new(NetworkSink::new()));
283                let state = lock_unpoisoned(&sink).subscribe();
284                #[cfg(target_vendor = "apple")]
285                let platform = Some(apple::PathMonitor::start(sink, bugs.clone())?);
286                #[cfg(target_os = "android")]
287                let platform = Some(android::JavaWatch::start(sink, bugs.clone())?);
288                #[cfg(windows)]
289                let platform = Some(winrt::ProfileWatch::start(sink, bugs.clone())?);
290                // None without NetworkManager: the state stays can't tell.
291                #[cfg(target_os = "linux")]
292                let platform = linux::ManagerWatch::start(sink, bugs.clone())?;
293                Ok(Self { state, _platform: platform })
294            }
295            // Elsewhere, as on the BSDs, coven asks nothing and can't tell
296            // (api.md E5).
297            #[cfg(not(any(
298                target_vendor = "apple",
299                target_os = "android",
300                target_os = "linux",
301                windows
302            )))]
303            NetworkSource::Platform => Ok(Self::unwatched()),
304            #[cfg(any(test, feature = "test-utils"))]
305            NetworkSource::Test(network) => Ok(Self::without_platform(network.subscribe())),
306            #[cfg(any(test, feature = "test-utils"))]
307            NetworkSource::Refused(error) => Err(error.clone()),
308        }
309    }
310
311    /// A watch that hears nothing, for work that transfers no file the
312    /// network holds: a read-only handle's, and loading's (api.md E4).
313    pub fn unwatched() -> Self {
314        Self::without_platform(watch::channel(NetworkState::UNKNOWN).1)
315    }
316
317    /// A watch of `state` with no platform registration.
318    fn without_platform(state: watch::Receiver<NetworkState>) -> Self {
319        Self {
320            state,
321            #[cfg(any(
322                target_vendor = "apple",
323                target_os = "android",
324                target_os = "linux",
325                windows
326            ))]
327            _platform: None,
328        }
329    }
330
331    /// A view of the network, for an owner that acts on it.
332    pub fn view(&self) -> NetworkView {
333        NetworkView(self.state.clone())
334    }
335}
336
337/// Hears what the store's watch knows of the network (§6.7).
338#[derive(Clone)]
339pub struct NetworkView(watch::Receiver<NetworkState>);
340
341impl NetworkView {
342    /// What the watch knows now.
343    pub fn current(&self) -> NetworkState {
344        *self.0.borrow()
345    }
346
347    /// Resolves once the state changes after the one last read through this
348    /// view; never once the watch is gone.
349    pub async fn changed(&mut self) {
350        if self.0.changed().await.is_err() {
351            std::future::pending::<()>().await;
352        }
353    }
354
355    /// The state, marking it read: `changed` then waits for a later one.
356    pub fn read(&mut self) -> NetworkState {
357        *self.0.borrow_and_update()
358    }
359}
360
361/// A network a test drives in place of the platform's (api.md E1
362/// `network`): clones drive the same network.
363#[cfg(any(test, feature = "test-utils"))]
364#[derive(Clone)]
365pub struct TestNetwork {
366    state: Arc<Mutex<TestNetworkState>>,
367}
368
369/// What a test's network says, and how many networks it has named.
370#[cfg(any(test, feature = "test-utils"))]
371struct TestNetworkState {
372    sink: NetworkSink,
373    named: u64,
374}
375
376#[cfg(any(test, feature = "test-utils"))]
377impl TestNetwork {
378    /// The network as the platform would describe it as the store opens.
379    pub fn new(usable: bool, metered: bool) -> Self {
380        let network = Self {
381            state: Arc::new(Mutex::new(TestNetworkState { sink: NetworkSink::new(), named: 0 })),
382        };
383        if usable {
384            network.join(metered);
385        } else {
386            network.lose();
387        }
388        network
389    }
390
391    fn subscribe(&self) -> watch::Receiver<NetworkState> {
392        lock_unpoisoned(&self.state).sink.subscribe()
393    }
394
395    /// The device moves to another usable network: a network report.
396    pub fn join(&self, metered: bool) {
397        let mut state = lock_unpoisoned(&self.state);
398        state.named += 1;
399        let id = format!("test network {}", state.named);
400        state.sink.observe(Some(UsableNetwork { id, metered }));
401    }
402
403    /// The device loses its network: no report.
404    pub fn lose(&self) {
405        lock_unpoisoned(&self.state).sink.observe(None);
406    }
407
408    /// The platform fails to say what the network is: coven can't tell, and
409    /// calls the network metered, until the test says again.
410    pub fn cannot_tell(&self) {
411        lock_unpoisoned(&self.state).sink.cannot_tell();
412    }
413
414    /// The same network turns metered or unmetered: no report.
415    pub fn set_metered(&self, metered: bool) {
416        let mut state = lock_unpoisoned(&self.state);
417        let Some(id) = state.sink.current.clone() else {
418            panic!("a test network turns metered only while the device is on one");
419        };
420        state.sink.observe(Some(UsableNetwork { id, metered }));
421    }
422}
423
424#[cfg(test)]
425#[path = "network_watch_tests.rs"]
426mod tests;