use std::collections::HashMap; use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; use ping_async::IcmpEchoRequestor; use serde::Deserialize; use tauri::{AppHandle, Emitter}; use tokio::sync::Mutex; use tokio::task::JoinHandle; use crate::models::monitor::MonitorEntry; struct MonitorState { entries: HashMap, task: Option>, running: bool, } static MONITOR: std::sync::OnceLock>> = std::sync::OnceLock::new(); fn get_monitor() -> Arc> { MONITOR .get_or_init(|| { Arc::new(Mutex::new(MonitorState { entries: HashMap::new(), task: None, running: false, })) }) .clone() } #[derive(Debug, Deserialize)] pub struct MonitorHost { pub ip: String, pub label: Option, } #[tauri::command] pub async fn monitor_add_hosts(hosts: Vec) -> Result<(), String> { let state = get_monitor(); let mut guard = state.lock().await; for host in hosts { let ip: IpAddr = host .ip .parse() .map_err(|e| format!("Invalid IP '{}': {}", host.ip, e))?; guard .entries .entry(ip) .or_insert_with(|| MonitorEntry::new(ip, host.label)); } Ok(()) } #[tauri::command] pub async fn monitor_remove_host(ip: String) -> Result<(), String> { let parsed: IpAddr = ip.parse().map_err(|e| format!("Invalid IP: {}", e))?; let state = get_monitor(); let mut guard = state.lock().await; guard.entries.remove(&parsed); Ok(()) } #[tauri::command] pub async fn monitor_clear() -> Result<(), String> { let state = get_monitor(); let mut guard = state.lock().await; guard.entries.clear(); Ok(()) } #[tauri::command] pub async fn monitor_get_entries() -> Result, String> { let state = get_monitor(); let guard = state.lock().await; let mut entries: Vec = guard.entries.values().cloned().collect(); entries.sort_by(|a, b| a.ip.to_string().cmp(&b.ip.to_string())); Ok(entries) } #[tauri::command] pub async fn monitor_start(app: AppHandle, interval_ms: Option) -> Result<(), String> { let state = get_monitor(); let mut guard = state.lock().await; if guard.running { return Err("Monitor already running".to_string()); } if guard.entries.is_empty() { return Err("No hosts to monitor".to_string()); } guard.running = true; let interval = Duration::from_millis(interval_ms.unwrap_or(2000)); let state2 = get_monitor(); let task = tokio::spawn(async move { loop { { let guard = state2.lock().await; if !guard.running { break; } } // Collect IPs to ping let ips: Vec = { let guard = state2.lock().await; guard.entries.keys().cloned().collect() }; // Ping all hosts concurrently let mut join_set = tokio::task::JoinSet::new(); for ip in ips { join_set.spawn(async move { let timeout = Duration::from_millis(1500); let requestor = match IcmpEchoRequestor::new(ip, None, None, Some(timeout)) { Ok(r) => r, Err(_) => return (ip, None), }; match requestor.send().await { Ok(reply) => match reply.status() { ping_async::IcmpEchoStatus::Success => { let rtt = reply.round_trip_time().as_secs_f64() * 1000.0; (ip, Some(rtt)) } _ => (ip, None), }, Err(_) => (ip, None), } }); } // Process results while let Some(result) = join_set.join_next().await { if let Ok((ip, rtt)) = result { let mut guard = state2.lock().await; if let Some(entry) = guard.entries.get_mut(&ip) { let old_status = entry.status.clone(); match rtt { Some(ms) => entry.record_success(ms), None => entry.record_failure(), } // Emit state change event if entry.status != old_status { let _ = app.emit( "monitor:state-change", serde_json::json!({ "ip": ip.to_string(), "old": format!("{:?}", old_status), "new": format!("{:?}", entry.status), }), ); } let _ = app.emit("monitor:update", &entry.clone()); } } } tokio::time::sleep(interval).await; } }); guard.task = Some(task); Ok(()) } #[tauri::command] pub async fn monitor_stop() -> Result<(), String> { let state = get_monitor(); let mut guard = state.lock().await; guard.running = false; if let Some(task) = guard.task.take() { task.abort(); } Ok(()) } #[tauri::command] pub async fn monitor_update_label(ip: String, label: String) -> Result<(), String> { let parsed: IpAddr = ip.parse().map_err(|e| format!("Invalid IP: {}", e))?; let state = get_monitor(); let mut guard = state.lock().await; if let Some(entry) = guard.entries.get_mut(&parsed) { entry.label = if label.is_empty() { None } else { Some(label) }; } Ok(()) } #[tauri::command] pub async fn monitor_resolve_host(ip: String) -> Result, String> { let parsed: IpAddr = ip.parse().map_err(|e| format!("Invalid IP: {}", e))?; Ok(crate::net::dns::reverse_lookup(parsed).await) } pub fn is_monitor_running() -> bool { let state = get_monitor(); let guard = state.blocking_lock(); guard.running }