//! P5-49 — Network awareness (Windows connectivity / NCSI). //! //! Proactively detects when the machine gains or loses internet connectivity so //! the web client can surface an offline state and, more importantly, nudge the //! matrix client to retry its backed-off `/sync` the instant the network comes //! back instead of waiting out the sync-loop backoff timer. //! //! Windows: a lightweight background thread polls the Network List Manager //! (`INetworkListManager::IsConnectedToInternet`, the same NCSI signal the shell //! uses) every ~3 seconds. We prefer a robust poll over a COM event sink //! (`INetworkEvents`) — the poll is far simpler to reason about, needs no //! connection-point plumbing, and a 3s cadence is more than responsive enough //! for a "retry sync now" hint. We emit **only on a state transition**, so the //! web side gets one event per change rather than a steady heartbeat. //! //! Other platforms are a no-op: the browser already fires `online`/`offline` //! events, and the desktop shells (macOS/Linux) can adopt their own reachability //! APIs later; the web hook stays unconditional so nothing there needs guarding. use tauri::AppHandle; /// Payload for the `network-changed` DOM event (`{ online: bool }`). #[cfg(target_os = "windows")] #[derive(serde::Serialize)] struct NetworkState { online: bool, } /// Called once from lib.rs `native::setup()`. On Windows, spawns the poll /// thread; elsewhere it does nothing. pub fn setup(app: &AppHandle) -> tauri::Result<()> { #[cfg(target_os = "windows")] { // Own a handle inside the thread so the poll outlives this call and runs // for the lifetime of the app. let app = app.clone(); std::thread::spawn(move || watch_network(app)); } #[cfg(not(target_os = "windows"))] { // No-op on non-Windows platforms (see module docs). Bind the arg so the // signature stays identical cross-platform with no unused warning. let _ = app; } Ok(()) } /// Poll loop, runs on its own thread for the app's lifetime. #[cfg(target_os = "windows")] fn watch_network(app: AppHandle) { use std::time::Duration; use windows::Win32::Networking::NetworkListManager::{INetworkListManager, NetworkListManager}; use windows::Win32::System::Com::{ CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, }; // Initialize COM for this thread in the multithreaded apartment. This is a // dedicated thread, so it should be the first to init and succeed (S_FALSE — // "already initialized, same mode" — also counts as success). If it fails // outright (e.g. RPC_E_CHANGED_MODE) we can't proceed, so bail. // Safety: FFI call; `None` reserved param per the API contract. if unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }.is_err() { return; } // Create the Network List Manager COM object (mirrors the `CoCreateInstance` // idiom in lib.rs `set_badge_count`). If it's unavailable, tear COM back down // and stop the thread cleanly. // Safety: standard COM instantiation; type is inferred from the annotation. let manager: INetworkListManager = match unsafe { CoCreateInstance(&NetworkListManager, None, CLSCTX_INPROC_SERVER) } { Ok(manager) => manager, Err(_) => { // Safety: balances the successful CoInitializeEx above. unsafe { CoUninitialize() }; return; } }; // `None` = unknown; the first successful read is treated as a transition so // the web side always learns the initial connectivity state. let mut last: Option = None; loop { // `IsConnectedToInternet` yields a VARIANT_BOOL (VARIANT_TRUE == -1 when // connected). Skip transient read errors without emitting. // Safety: FFI call on a live COM interface owned by this thread. if let Ok(connected) = unsafe { manager.IsConnectedToInternet() } { let online = connected.as_bool(); if last != Some(online) { last = Some(online); super::emit_to_web( &app, "network-changed", &serde_json::to_string(&NetworkState { online }).unwrap_or_default(), ); } } std::thread::sleep(Duration::from_secs(3)); } // Note: the loop never returns, so we intentionally don't call // `CoUninitialize` here — COM stays initialized for this thread until the // process exits, which is exactly the desired lifetime. }