@@ -3,10 +3,13 @@ use std::sync::{Arc, OnceLock, RwLock};
33
44use block2:: { Block , RcBlock } ;
55use dispatch2:: { DispatchQueue , DispatchRetained } ;
6+ use tracing:: warn;
67
78use crate :: error:: { Error , Result } ;
89use 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.
1013const NW_PATH_STATUS_SATISFIED : i32 = 1 ;
1114const NW_INTERFACE_TYPE_WIFI : i32 = 1 ;
1215const 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.
4551unsafe impl Send for MacosConnectivityMonitor { }
4652unsafe 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.
4860pub 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> {
6072fn 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
99118fn 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.
103124fn 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 {
133154fn 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