Skip to main content

tauri_plugin_network_manager/
lib.rs

1use commands::{
2    connect_to_wifi, connect_vpn, create_vpn_profile, delete_vpn_profile, delete_wifi_connection,
3    disconnect_from_wifi, disconnect_vpn, get_network_state, get_vpn_status,
4    get_saved_wifi_networks, list_wifi_networks, rescan_wifi, toggle_network_state,
5    get_wireless_enabled, list_vpn_profiles, set_wireless_enabled, is_wireless_available,
6    update_vpn_profile, get_network_stats, get_network_interfaces
7};
8pub use models::{
9    NetworkInfo, VpnConnectionState, VpnCreateConfig, VpnEventPayload, VpnProfile, VpnStatus,
10    VpnUpdateConfig, WiFiConnectionConfig, WiFiSecurityType,
11};
12use std::result::Result;
13use std::sync::{Arc, RwLock};
14use std::time::{Duration, Instant};
15use tauri::{
16    plugin::{Builder, TauriPlugin},
17    AppHandle, Emitter, Manager, Runtime,
18};
19
20#[cfg(desktop)]
21pub mod desktop;
22
23mod commands;
24pub mod error;
25pub mod models;
26mod nm_constants;
27mod nm_helpers;
28mod network_stats;
29
30pub use crate::error::{NetworkError, Result as NetworkResult};
31
32pub struct NetworkManagerState<R: Runtime> {
33    pub manager: Arc<RwLock<Option<crate::models::VSKNetworkManager<'static, R>>>>,
34    pub stats_tracker: Arc<RwLock<Option<crate::network_stats::NetworkStatsTracker>>>,
35    pub wifi_networks_cache: Arc<RwLock<Option<WifiNetworksCache>>>,
36}
37
38pub struct WifiNetworksCache {
39    pub data: Vec<NetworkInfo>,
40    pub fetched_at: Instant,
41}
42
43impl<R: Runtime> Default for NetworkManagerState<R> {
44    fn default() -> Self {
45        Self {
46            manager: Arc::new(RwLock::new(None)),
47            stats_tracker: Arc::new(RwLock::new(None)),
48            wifi_networks_cache: Arc::new(RwLock::new(None)),
49        }
50    }
51}
52
53pub fn spawn_network_change_emitter<R: tauri::Runtime>(
54    app: AppHandle<R>,
55    network_manager: crate::models::VSKNetworkManager<'static, R>,
56) {
57    let rx = match network_manager.listen_network_changes() {
58        Ok(rx) => rx,
59        Err(e) => {
60            eprintln!("No se pudo escuchar cambios de red: {:?}", e);
61            return;
62        }
63    };
64
65    std::thread::spawn(move || {
66        use std::time::{Duration, Instant};
67        use std::sync::mpsc::RecvTimeoutError;
68
69        let mut pending_event: Option<crate::models::NetworkInfo> = None;
70        let mut pending_vpn_status: Option<crate::models::VpnStatus> = None;
71        let mut last_vpn_status: Option<crate::models::VpnStatus> = None;
72        let mut debounce_deadline: Option<Instant> = None;
73        let debounce_duration = Duration::from_millis(250);
74
75        loop {
76            if let Some(deadline) = debounce_deadline {
77                let now = Instant::now();
78                if now >= deadline {
79                    // Timeout reached, emit valid pending event
80                    if let Some(info) = pending_event.take() {
81                        let _ = app.emit("network-changed", &info);
82                    }
83                    if let Some(vpn_status) = pending_vpn_status.take() {
84                        emit_vpn_events(&app, &network_manager, &mut last_vpn_status, vpn_status);
85                    }
86                    debounce_deadline = None;
87                } else {
88                    // Wait for remaining time or new event
89                    let timeout = deadline - now;
90                    match rx.recv_timeout(timeout) {
91                        Ok(info) => {
92                            // New event during debounce window: update pending and extend deadline
93                            pending_event = Some(info);
94                            if let Ok(vpn_status) = network_manager.get_vpn_status() {
95                                pending_vpn_status = Some(vpn_status);
96                            }
97                            debounce_deadline = Some(Instant::now() + debounce_duration);
98                        }
99                        Err(RecvTimeoutError::Timeout) => {
100                            // Loop will handle emission
101                        }
102                        Err(RecvTimeoutError::Disconnected) => break,
103                    }
104                }
105            } else {
106                // No pending event, block until one arrives
107                match rx.recv() {
108                    Ok(info) => {
109                        // Leading emission improves perceived latency for UI updates.
110                        let _ = app.emit("network-changed", &info);
111                        if let Ok(vpn_status) = network_manager.get_vpn_status() {
112                            emit_vpn_events(&app, &network_manager, &mut last_vpn_status, vpn_status);
113                        }
114                        pending_event = None;
115                        pending_vpn_status = None;
116                        debounce_deadline = Some(Instant::now() + debounce_duration);
117                    }
118                    Err(_) => break,
119                }
120            }
121        }
122    });
123}
124
125fn resolve_active_vpn_profile<R: tauri::Runtime>(
126    network_manager: &crate::models::VSKNetworkManager<'static, R>,
127    status: &VpnStatus,
128) -> Option<VpnProfile> {
129    let active_uuid = status.active_profile_uuid.as_deref()?;
130    let profiles = network_manager.list_vpn_profiles().ok()?;
131    profiles.into_iter().find(|profile| profile.uuid == active_uuid)
132}
133
134fn emit_vpn_events<R: tauri::Runtime>(
135    app: &AppHandle<R>,
136    network_manager: &crate::models::VSKNetworkManager<'static, R>,
137    last_vpn_status: &mut Option<VpnStatus>,
138    status: VpnStatus,
139) {
140    let previous = last_vpn_status.clone();
141    if previous.as_ref() == Some(&status) {
142        return;
143    }
144
145    let profile = resolve_active_vpn_profile(network_manager, &status);
146    let payload = VpnEventPayload {
147        status: status.clone(),
148        profile,
149        reason: None,
150    };
151
152    let _ = app.emit("vpn-changed", &payload);
153
154    let previous_state = previous
155        .as_ref()
156        .map(|s| s.state.clone())
157        .unwrap_or(VpnConnectionState::Unknown);
158
159    match status.state {
160        VpnConnectionState::Connected => {
161            if previous_state != VpnConnectionState::Connected {
162                let _ = app.emit("vpn-connected", &payload);
163            }
164        }
165        VpnConnectionState::Disconnected => {
166            if previous_state == VpnConnectionState::Connected
167                || previous_state == VpnConnectionState::Disconnecting
168            {
169                let _ = app.emit("vpn-disconnected", &payload);
170            }
171        }
172        VpnConnectionState::Failed => {
173            let failed_payload = VpnEventPayload {
174                reason: Some("vpn-connection-failed".to_string()),
175                ..payload.clone()
176            };
177            let _ = app.emit("vpn-failed", &failed_payload);
178        }
179        _ => {}
180    }
181
182    *last_vpn_status = Some(status);
183}
184
185impl<R: Runtime> NetworkManagerState<R> {
186    pub fn new(manager: Option<crate::models::VSKNetworkManager<'static, R>>) -> Self {
187        Self {
188            manager: Arc::new(RwLock::new(manager)),
189            stats_tracker: Arc::new(RwLock::new(None)),
190            wifi_networks_cache: Arc::new(RwLock::new(None)),
191        }
192    }
193
194    pub fn list_wifi_networks(
195        &self,
196        force_refresh: bool,
197        ttl_ms: Option<u64>,
198    ) -> Result<Vec<NetworkInfo>, NetworkError> {
199        let ttl = Duration::from_millis(ttl_ms.unwrap_or(3000).clamp(250, 30000));
200
201        if !force_refresh {
202            let cache = self
203                .wifi_networks_cache
204                .read()
205                .map_err(|_| NetworkError::LockError)?;
206            if let Some(cache_entry) = cache.as_ref() {
207                if cache_entry.fetched_at.elapsed() <= ttl {
208                    return Ok(cache_entry.data.clone());
209                }
210            }
211        }
212
213        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
214        let networks = match manager.as_ref() {
215            Some(manager) => manager.list_wifi_networks(),
216            _none => Err(NetworkError::NotInitialized),
217        }?;
218
219        let mut cache = self
220            .wifi_networks_cache
221            .write()
222            .map_err(|_| NetworkError::LockError)?;
223        *cache = Some(WifiNetworksCache {
224            data: networks.clone(),
225            fetched_at: Instant::now(),
226        });
227
228        Ok(networks)
229    }
230
231    pub fn invalidate_wifi_networks_cache(&self) -> Result<(), NetworkError> {
232        let mut cache = self
233            .wifi_networks_cache
234            .write()
235            .map_err(|_| NetworkError::LockError)?;
236        *cache = None;
237        Ok(())
238    }
239
240    pub fn rescan_wifi(&self) -> Result<Vec<NetworkInfo>, NetworkError> {
241        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
242        match manager.as_ref() {
243            Some(manager) => manager.rescan_wifi()?,
244            _none => return Err(NetworkError::NotInitialized),
245        };
246        drop(manager);
247
248        self.invalidate_wifi_networks_cache()?;
249        self.list_wifi_networks(true, None)
250    }
251
252    pub fn connect_to_wifi(&self, config: WiFiConnectionConfig) -> Result<(), NetworkError> {
253        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
254        match manager.as_ref() {
255            Some(manager) => manager.connect_to_wifi(config),
256            _none => Err(NetworkError::NotInitialized),
257        }
258    }
259
260    pub fn disconnect_from_wifi(&self) -> Result<(), NetworkError> {
261        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
262        match manager.as_ref() {
263            Some(manager) => manager.disconnect_from_wifi(),
264            _none => Err(NetworkError::NotInitialized),
265        }
266    }
267
268    pub fn get_saved_wifi_networks(&self) -> Result<Vec<NetworkInfo>, NetworkError> {
269        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
270        match manager.as_ref() {
271            Some(manager) => manager.get_saved_wifi_networks(),
272            _none => Err(NetworkError::NotInitialized),
273        }
274    }
275
276    pub fn delete_wifi_connection(&self, ssid: &str) -> Result<bool, NetworkError> {
277        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
278        match manager.as_ref() {
279            Some(manager) => manager.delete_wifi_connection(ssid),
280            _none => Err(NetworkError::NotInitialized),
281        }
282    }
283
284    pub fn toggle_network_state(&self, enabled: bool) -> Result<bool, NetworkError> {
285        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
286        match manager.as_ref() {
287            Some(manager) => manager.toggle_network_state(enabled),
288            _none => Err(NetworkError::NotInitialized),
289        }
290    }
291
292    pub fn get_wireless_enabled(&self) -> Result<bool, NetworkError> {
293        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
294        match manager.as_ref() {
295            Some(manager) => manager.get_wireless_enabled().map_err(|e| NetworkError::from(e)),
296            _none => Err(NetworkError::NotInitialized),
297        }
298    }
299
300    pub fn set_wireless_enabled(&self, enabled: bool) -> Result<(), NetworkError> {
301        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
302        match manager.as_ref() {
303             Some(manager) => manager.set_wireless_enabled(enabled).map_err(|e| NetworkError::from(e)),
304            _none => Err(NetworkError::NotInitialized),
305        }
306    }
307
308    pub fn is_wireless_available(&self) -> Result<bool, NetworkError> {
309        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
310        match manager.as_ref() {
311            Some(manager) => manager.is_wireless_available().map_err(|e| NetworkError::from(e)),
312            _none => Err(NetworkError::NotInitialized),
313        }
314    }
315
316    pub fn get_network_stats(&self) -> Result<crate::models::NetworkStats, NetworkError> {
317        let mut tracker = self.stats_tracker.write().map_err(|_| NetworkError::LockError)?;
318        
319        // Initialize tracker if not already initialized
320        if tracker.is_none() {
321            // Get active interface from network manager
322            let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
323            if let Some(manager) = manager.as_ref() {
324                let network_state = manager.get_current_network_state()
325                    .map_err(|e| NetworkError::OperationError(e.to_string()))?;
326                
327                // Try to determine interface name from connection type
328                let interface = if network_state.connection_type == "WiFi" {
329                    // Try common WiFi interface names
330                    crate::network_stats::get_network_interfaces()
331                        .ok()
332                        .and_then(|interfaces| {
333                            interfaces.into_iter()
334                                .find(|i| i.starts_with("wl") || i.starts_with("wlan"))
335                        })
336                        .unwrap_or_else(|| "wlan0".to_string())
337                } else {
338                    // Try common ethernet interface names
339                    crate::network_stats::get_network_interfaces()
340                        .ok()
341                        .and_then(|interfaces| {
342                            interfaces.into_iter()
343                                .find(|i| i.starts_with("en") || i.starts_with("eth"))
344                        })
345                        .unwrap_or_else(|| "eth0".to_string())
346                };
347                
348                *tracker = crate::network_stats::NetworkStatsTracker::new(interface).ok();
349            }
350        }
351        
352        // Get stats from tracker
353        match tracker.as_mut() {
354            Some(t) => t.get_stats().map_err(|e| NetworkError::from(e)),
355            None => Err(NetworkError::OperationError("Stats tracker not initialized".to_string())),
356        }
357    }
358
359    pub fn list_vpn_profiles(&self) -> Result<Vec<VpnProfile>, NetworkError> {
360        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
361        match manager.as_ref() {
362            Some(manager) => manager.list_vpn_profiles(),
363            _none => Err(NetworkError::NotInitialized),
364        }
365    }
366
367    pub fn get_vpn_status(&self) -> Result<VpnStatus, NetworkError> {
368        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
369        match manager.as_ref() {
370            Some(manager) => manager.get_vpn_status(),
371            _none => Err(NetworkError::NotInitialized),
372        }
373    }
374
375    pub fn connect_vpn(&self, uuid: String) -> Result<(), NetworkError> {
376        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
377        match manager.as_ref() {
378            Some(manager) => manager.connect_vpn(uuid),
379            _none => Err(NetworkError::NotInitialized),
380        }
381    }
382
383    pub fn disconnect_vpn(&self, uuid: Option<String>) -> Result<(), NetworkError> {
384        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
385        match manager.as_ref() {
386            Some(manager) => manager.disconnect_vpn(uuid),
387            _none => Err(NetworkError::NotInitialized),
388        }
389    }
390
391    pub fn create_vpn_profile(&self, config: VpnCreateConfig) -> Result<VpnProfile, NetworkError> {
392        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
393        match manager.as_ref() {
394            Some(manager) => manager.create_vpn_profile(config),
395            _none => Err(NetworkError::NotInitialized),
396        }
397    }
398
399    pub fn update_vpn_profile(&self, config: VpnUpdateConfig) -> Result<VpnProfile, NetworkError> {
400        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
401        match manager.as_ref() {
402            Some(manager) => manager.update_vpn_profile(config),
403            _none => Err(NetworkError::NotInitialized),
404        }
405    }
406
407    pub fn delete_vpn_profile(&self, uuid: String) -> Result<(), NetworkError> {
408        let manager = self.manager.read().map_err(|_| NetworkError::LockError)?;
409        match manager.as_ref() {
410            Some(manager) => manager.delete_vpn_profile(uuid),
411            _none => Err(NetworkError::NotInitialized),
412        }
413    }
414}
415
416/// Initializes the plugin.
417pub fn init() -> TauriPlugin<tauri::Wry> {
418    Builder::new("network-manager")
419        .invoke_handler(tauri::generate_handler![
420            get_network_state,
421            list_wifi_networks,
422            connect_to_wifi,
423            disconnect_from_wifi,
424            get_saved_wifi_networks,
425            rescan_wifi,
426            delete_wifi_connection,
427            toggle_network_state,
428            get_wireless_enabled,
429            set_wireless_enabled,
430            is_wireless_available,
431            get_network_stats,
432            get_network_interfaces,
433            list_vpn_profiles,
434            get_vpn_status,
435            connect_vpn,
436            disconnect_vpn,
437            create_vpn_profile,
438            update_vpn_profile,
439            delete_vpn_profile,
440        ])
441        .setup(|app, _api| -> Result<(), Box<dyn std::error::Error>> {
442            #[cfg(desktop)]
443            // Removed tokio runtime initialization
444            let rt = tokio::runtime::Builder::new_multi_thread()
445                .enable_all()
446                .build()?;
447            let network_manager = rt.block_on(async { crate::desktop::init(&app, _api).await })?;
448
449            app.manage(NetworkManagerState::<tauri::Wry>::new(Some(
450                network_manager,
451            )));
452
453            app.state::<NetworkManagerState<tauri::Wry>>()
454                .manager
455                .read()
456                .map_err(|_| NetworkError::LockError)?
457                .as_ref()
458                .map(|manager| {
459                    // Clone the manager with a 'static lifetime
460                    let manager_static: crate::models::VSKNetworkManager<'static, tauri::Wry> =
461                        crate::models::VSKNetworkManager {
462                            connection: manager.connection.clone(),
463                            proxy: manager.proxy.clone(),
464                            app: app.clone(),
465                        };
466                    spawn_network_change_emitter(app.clone(), manager_static);
467                });
468
469            Ok(())
470        })
471        .build()
472}
473
474/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the network-manager APIs.
475pub trait NetworkManagerExt<R: Runtime> {
476    fn network_manager(&self) -> Option<crate::models::VSKNetworkManager<'static, R>>;
477}
478
479impl<R: Runtime + Clone, T: Manager<R>> NetworkManagerExt<R> for T {
480    fn network_manager(&self) -> Option<crate::models::VSKNetworkManager<'static, R>> {
481        self.try_state::<NetworkManagerState<R>>()
482            .and_then(|state| {
483                state.manager.read().ok().and_then(|m| {
484                    m.as_ref().map(|x| crate::models::VSKNetworkManager {
485                        connection: x.connection.clone(),
486                        proxy: x.proxy.clone(),
487                        app: self.app_handle().clone(),
488                    })
489                })
490            })
491    }
492}