Skip to content

Commit 312e8e6

Browse files
committed
fix: log macOS connectivity failures and document monitor invariants
1 parent 7a8527d commit 312e8e6

2 files changed

Lines changed: 70 additions & 7 deletions

File tree

src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
5353
app.manage(Connectivity);
5454
#[cfg(target_os = "macos")]
5555
{
56-
// Start the path monitor early so the first frontend call can read a warm cache.
56+
// Start the path monitor early. Its first update arrives
57+
// asynchronously, so this does not guarantee a populated cache on
58+
// return — it ensures the update has normally landed long before
59+
// the webview loads and the frontend makes its first call. Until
60+
// then, reads report disconnected (see `platform::macos`).
5761
let _ = platform::connection_status();
5862
}
5963
Ok(())

src/platform/macos.rs

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ use std::sync::{Arc, OnceLock, RwLock};
33

44
use block2::{Block, RcBlock};
55
use dispatch2::{DispatchQueue, DispatchRetained};
6+
use tracing::warn;
67

78
use crate::error::{Error, Result};
89
use crate::types::{ConnectionStatus, ConnectionType};
910

11+
// Values mirror Apple's `nw_path_status_t` and `nw_interface_type_t` enums
12+
// from the Network framework headers.
1013
const NW_PATH_STATUS_SATISFIED: i32 = 1;
1114
const NW_INTERFACE_TYPE_WIFI: i32 = 1;
1215
const NW_INTERFACE_TYPE_CELLULAR: i32 = 2;
@@ -42,9 +45,18 @@ struct MacosConnectivityMonitor {
4245
status: Arc<RwLock<Option<ConnectionStatus>>>,
4346
}
4447

48+
// SAFETY: Rust code only ever reads `status` (already `Send + Sync`); the
49+
// remaining fields are only used by the Network framework on its own queue,
50+
// and the value lives in a static so it is never dropped.
4551
unsafe impl Send for MacosConnectivityMonitor {}
4652
unsafe impl Sync for MacosConnectivityMonitor {}
4753

54+
/// Returns the connection status last reported by the path monitor.
55+
///
56+
/// The monitor delivers path updates asynchronously on its dispatch queue,
57+
/// starting with an initial update shortly after `nw_path_monitor_start`.
58+
/// Until that first update lands, the cache is empty and this reports disconnected.
59+
/// Either way, re-check at the decision point rather than relying on earlier results.
4860
pub fn connection_status() -> Result<ConnectionStatus> {
4961
let monitor = MONITOR.get_or_init(create_monitor);
5062

@@ -60,16 +72,20 @@ pub fn connection_status() -> Result<ConnectionStatus> {
6072
fn create_monitor() -> Option<MacosConnectivityMonitor> {
6173
let monitor = unsafe { nw_path_monitor_create() };
6274
if monitor.is_null() {
75+
warn!("failed to create macOS path monitor");
6376
return None;
6477
}
6578

6679
let queue = DispatchQueue::new("tauri.plugin.connectivity.path", None);
6780
let status = Arc::new(RwLock::new(None));
6881
let handler_status = Arc::clone(&status);
69-
let handler = RcBlock::new(move |path: NwPath| {
70-
if let Ok(mut status) = handler_status.write() {
82+
let handler = RcBlock::new(move |path: NwPath| match handler_status.write() {
83+
Ok(mut status) => {
7184
*status = Some(read_status(path));
7285
}
86+
Err(error) => {
87+
warn!(%error, "failed to update macOS connection status cache");
88+
}
7389
});
7490

7591
unsafe {
@@ -92,14 +108,19 @@ impl MacosConnectivityMonitor {
92108
.status
93109
.read()
94110
.map(|status| cached_status(status.clone()))
95-
.unwrap_or_else(|_| ConnectionStatus::disconnected())
111+
.unwrap_or_else(|error| {
112+
warn!(%error, "failed to read macOS connection status cache");
113+
ConnectionStatus::disconnected()
114+
})
96115
}
97116
}
98117

99118
fn cached_status(status: Option<ConnectionStatus>) -> ConnectionStatus {
100119
status.unwrap_or_else(ConnectionStatus::disconnected)
101120
}
102121

122+
/// Other interface types (loopback, `nw_interface_type_other`) intentionally
123+
/// map to `Unknown`: the plugin only distinguishes transports callers can act on.
103124
fn resolve_connection_type(wifi: bool, wired: bool, cellular: bool) -> ConnectionType {
104125
if wifi {
105126
ConnectionType::Wifi
@@ -133,17 +154,34 @@ fn is_connected_status(status: i32) -> bool {
133154
fn read_status(path: NwPath) -> ConnectionStatus {
134155
let connected = is_connected_status(unsafe { nw_path_get_status(path) });
135156
if !connected {
136-
return ConnectionStatus::disconnected();
157+
return assemble_status(false, false, false, false, false, false);
137158
}
138159

160+
let metered = unsafe { nw_path_is_expensive(path) };
161+
let constrained = unsafe { nw_path_is_constrained(path) };
139162
let wifi = unsafe { nw_path_uses_interface_type(path, NW_INTERFACE_TYPE_WIFI) };
140163
let wired = unsafe { nw_path_uses_interface_type(path, NW_INTERFACE_TYPE_WIRED) };
141164
let cellular = unsafe { nw_path_uses_interface_type(path, NW_INTERFACE_TYPE_CELLULAR) };
142165

166+
assemble_status(true, metered, constrained, wifi, wired, cellular)
167+
}
168+
169+
fn assemble_status(
170+
connected: bool,
171+
metered: bool,
172+
constrained: bool,
173+
wifi: bool,
174+
wired: bool,
175+
cellular: bool,
176+
) -> ConnectionStatus {
177+
if !connected {
178+
return ConnectionStatus::disconnected();
179+
}
180+
143181
ConnectionStatus {
144182
connected: true,
145-
metered: unsafe { nw_path_is_expensive(path) },
146-
constrained: unsafe { nw_path_is_constrained(path) },
183+
metered,
184+
constrained,
147185
connection_type: resolve_connection_type(wifi, wired, cellular),
148186
}
149187
}
@@ -185,6 +223,27 @@ mod tests {
185223
assert_eq!(cached_status(None), ConnectionStatus::disconnected());
186224
}
187225

226+
#[test]
227+
fn disconnected_assembled_status_uses_disconnected_defaults() {
228+
assert_eq!(
229+
assemble_status(false, true, true, true, true, true),
230+
ConnectionStatus::disconnected()
231+
);
232+
}
233+
234+
#[test]
235+
fn connected_assembled_status_preserves_policy_flags_and_connection_type() {
236+
assert_eq!(
237+
assemble_status(true, true, true, false, false, true),
238+
ConnectionStatus {
239+
connected: true,
240+
metered: true,
241+
constrained: true,
242+
connection_type: ConnectionType::Cellular,
243+
}
244+
);
245+
}
246+
188247
#[test]
189248
fn only_satisfied_status_is_connected() {
190249
assert!(is_connected_status(NW_PATH_STATUS_SATISFIED));

0 commit comments

Comments
 (0)