Skip to content

Commit dd02701

Browse files
feat: use watchable instead of callbacks for netmon changes (#25)
## Description <!-- A summary of what this pull request achieves and a rough list of changes. --> ## Breaking Changes <!-- Optional, if there are any breaking changes document them, including how to migrate older code. --> ## Notes & open questions <!-- Any notes, remarks or open questions you have to make about the PR. --> ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented.
1 parent c17eb28 commit dd02701

File tree

12 files changed

+128
-201
lines changed

12 files changed

+128
-201
lines changed

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

netwatch/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ workspace = true
1919
atomic-waker = "1.1.2"
2020
bytes = "1.7"
2121
n0-future = "0.1.3"
22+
n0-watcher = "0.1"
2223
nested_enum_utils = "0.2.0"
2324
snafu = "0.8.5"
2425
time = "0.3.20"

netwatch/src/interfaces.rs

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@ use self::bsd::default_route;
2929
use self::linux::default_route;
3030
#[cfg(target_os = "windows")]
3131
use self::windows::default_route;
32+
#[cfg(not(wasm_browser))]
33+
use crate::ip::is_link_local;
3234
use crate::ip::{is_private_v6, is_up};
35+
#[cfg(not(wasm_browser))]
36+
use crate::netmon::is_interesting_interface;
3337

3438
/// Represents a network interface.
35-
#[derive(Debug)]
39+
#[derive(Debug, Clone)]
3640
pub struct Interface {
3741
iface: netdev::interface::Interface,
3842
}
@@ -61,12 +65,12 @@ impl Eq for Interface {}
6165

6266
impl Interface {
6367
/// Is this interface up?
64-
pub(crate) fn is_up(&self) -> bool {
68+
pub fn is_up(&self) -> bool {
6569
is_up(&self.iface)
6670
}
6771

6872
/// The name of the interface.
69-
pub(crate) fn name(&self) -> &str {
73+
pub fn name(&self) -> &str {
7074
&self.iface.name
7175
}
7276

@@ -153,7 +157,7 @@ impl IpNet {
153157

154158
/// Intended to store the state of the machine's network interfaces, routing table, and
155159
/// other network configuration. For now it's pretty basic.
156-
#[derive(Debug, PartialEq, Eq)]
160+
#[derive(Debug, PartialEq, Eq, Clone)]
157161
pub struct State {
158162
/// Maps from an interface name interface.
159163
pub interfaces: HashMap<String, Interface>,
@@ -165,22 +169,16 @@ pub struct State {
165169
/// Whether the machine has some non-localhost, non-link-local IPv4 address.
166170
pub have_v4: bool,
167171

168-
//// Whether the current network interface is considered "expensive", which currently means LTE/etc
172+
/// Whether the current network interface is considered "expensive", which currently means LTE/etc
169173
/// instead of Wifi. This field is not populated by `get_state`.
170-
pub(crate) is_expensive: bool,
174+
pub is_expensive: bool,
171175

172176
/// The interface name for the machine's default route.
173177
///
174178
/// It is not yet populated on all OSes.
175179
///
176180
/// When set, its value is the map key into `interface` and `interface_ips`.
177-
pub(crate) default_route_interface: Option<String>,
178-
179-
/// The HTTP proxy to use, if any.
180-
pub(crate) http_proxy: Option<String>,
181-
182-
/// The URL to the Proxy Autoconfig URL, if applicable.
183-
pub(crate) pac: Option<String>,
181+
pub default_route_interface: Option<String>,
184182
}
185183

186184
impl fmt::Display for State {
@@ -241,8 +239,6 @@ impl State {
241239
have_v6,
242240
is_expensive: false,
243241
default_route_interface,
244-
http_proxy: None,
245-
pac: None,
246242
}
247243
}
248244

@@ -258,10 +254,42 @@ impl State {
258254
have_v4: true,
259255
is_expensive: false,
260256
default_route_interface: Some(ifname),
261-
http_proxy: None,
262-
pac: None,
263257
}
264258
}
259+
260+
/// Is this a major change compared to the `old` one?.
261+
#[cfg(wasm_browser)]
262+
pub fn is_major_change(&self, old: &State) -> bool {
263+
// All changes are major.
264+
// In the browser, there only are changes from online to offline
265+
self != old
266+
}
267+
268+
/// Is this a major change compared to the `old` one?.
269+
#[cfg(not(wasm_browser))]
270+
pub fn is_major_change(&self, old: &State) -> bool {
271+
if self.have_v6 != old.have_v6
272+
|| self.have_v4 != old.have_v4
273+
|| self.is_expensive != old.is_expensive
274+
|| self.default_route_interface != old.default_route_interface
275+
{
276+
return true;
277+
}
278+
279+
for (iname, i) in &old.interfaces {
280+
if !is_interesting_interface(i.name()) {
281+
continue;
282+
}
283+
let Some(i2) = self.interfaces.get(iname) else {
284+
return true;
285+
};
286+
if i != i2 || !prefixes_major_equal(i.addrs(), i2.addrs()) {
287+
return true;
288+
}
289+
}
290+
291+
false
292+
}
265293
}
266294

267295
/// Reports whether ip is a usable IPv4 address which should have Internet connectivity.
@@ -373,6 +401,30 @@ impl HomeRouter {
373401
}
374402
}
375403

404+
/// Checks whether `a` and `b` are equal after ignoring uninteresting
405+
/// things, like link-local, loopback and multicast addresses.
406+
#[cfg(not(wasm_browser))]
407+
fn prefixes_major_equal(a: impl Iterator<Item = IpNet>, b: impl Iterator<Item = IpNet>) -> bool {
408+
fn is_interesting(p: &IpNet) -> bool {
409+
let a = p.addr();
410+
if is_link_local(a) || a.is_loopback() || a.is_multicast() {
411+
return false;
412+
}
413+
true
414+
}
415+
416+
let a = a.filter(is_interesting);
417+
let b = b.filter(is_interesting);
418+
419+
for (a, b) in a.zip(b) {
420+
if a != b {
421+
return false;
422+
}
423+
}
424+
425+
true
426+
}
427+
376428
#[cfg(test)]
377429
mod tests {
378430
use std::net::Ipv6Addr;

netwatch/src/interfaces/wasm_browser.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use js_sys::{JsString, Reflect};
55
pub const BROWSER_INTERFACE: &str = "browserif";
66

77
/// Represents a network interface.
8-
#[derive(Debug, PartialEq, Eq)]
8+
#[derive(Clone, Debug, PartialEq, Eq)]
99
pub struct Interface {
1010
is_up: bool,
1111
}
@@ -45,7 +45,7 @@ impl Interface {
4545

4646
/// Intended to store the state of the machine's network interfaces, routing table, and
4747
/// other network configuration. For now it's pretty basic.
48-
#[derive(Debug, PartialEq, Eq)]
48+
#[derive(Clone, Debug, PartialEq, Eq)]
4949
pub struct State {
5050
/// Maps from an interface name interface.
5151
pub interfaces: HashMap<String, Interface>,

netwatch/src/netmon.rs

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
//! Monitoring of networking interfaces and route changes.
22
3-
use n0_future::{
4-
boxed::BoxFuture,
5-
task::{self, AbortOnDropHandle},
6-
};
3+
use n0_future::task::{self, AbortOnDropHandle};
4+
use n0_watcher::Watchable;
75
use nested_enum_utils::common_fields;
86
use snafu::{Backtrace, ResultExt, Snafu};
97
use tokio::sync::{mpsc, oneshot};
@@ -26,15 +24,18 @@ mod wasm_browser;
2624
#[cfg(target_os = "windows")]
2725
mod windows;
2826

29-
pub use self::actor::CallbackToken;
27+
#[cfg(not(wasm_browser))]
28+
pub(crate) use self::actor::is_interesting_interface;
3029
use self::actor::{Actor, ActorMessage};
30+
pub use crate::interfaces::State;
3131

3232
/// Monitors networking interface and route changes.
3333
#[derive(Debug)]
3434
pub struct Monitor {
3535
/// Task handle for the monitor task.
3636
_handle: AbortOnDropHandle<()>,
3737
actor_tx: mpsc::Sender<ActorMessage>,
38+
interface_state: Watchable<State>,
3839
}
3940

4041
#[common_fields({
@@ -66,6 +67,7 @@ impl Monitor {
6667
pub async fn new() -> Result<Self, Error> {
6768
let actor = Actor::new().await.context(ActorSnafu)?;
6869
let actor_tx = actor.subscribe();
70+
let interface_state = actor.state().clone();
6971

7072
let handle = task::spawn(async move {
7173
actor.run().await;
@@ -74,30 +76,13 @@ impl Monitor {
7476
Ok(Monitor {
7577
_handle: AbortOnDropHandle::new(handle),
7678
actor_tx,
79+
interface_state,
7780
})
7881
}
7982

8083
/// Subscribe to network changes.
81-
pub async fn subscribe<F>(&self, callback: F) -> Result<CallbackToken, Error>
82-
where
83-
F: Fn(bool) -> BoxFuture<()> + 'static + Sync + Send,
84-
{
85-
let (s, r) = oneshot::channel();
86-
self.actor_tx
87-
.send(ActorMessage::Subscribe(Box::new(callback), s))
88-
.await?;
89-
let token = r.await?;
90-
Ok(token)
91-
}
92-
93-
/// Unsubscribe a callback from network changes, using the provided token.
94-
pub async fn unsubscribe(&self, token: CallbackToken) -> Result<(), Error> {
95-
let (s, r) = oneshot::channel();
96-
self.actor_tx
97-
.send(ActorMessage::Unsubscribe(token, s))
98-
.await?;
99-
r.await?;
100-
Ok(())
84+
pub fn interface_state(&self) -> n0_watcher::Direct<State> {
85+
self.interface_state.watch()
10186
}
10287

10388
/// Potential change detected outside
@@ -109,23 +94,16 @@ impl Monitor {
10994

11095
#[cfg(test)]
11196
mod tests {
112-
use n0_future::future::FutureExt;
97+
use n0_watcher::Watcher as _;
11398

11499
use super::*;
115100

116101
#[tokio::test]
117102
async fn test_smoke_monitor() {
118103
let mon = Monitor::new().await.unwrap();
119-
let _token = mon
120-
.subscribe(|is_major| {
121-
async move {
122-
println!("CHANGE DETECTED: {}", is_major);
123-
}
124-
.boxed()
125-
})
126-
.await
127-
.unwrap();
128-
129-
tokio::time::sleep(std::time::Duration::from_secs(15)).await;
104+
let sub = mon.interface_state();
105+
106+
let current = sub.get().unwrap();
107+
println!("current state: {}", current);
130108
}
131109
}

0 commit comments

Comments
 (0)