204 lines
6.2 KiB
Rust
204 lines
6.2 KiB
Rust
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<IpAddr, MonitorEntry>,
|
|
task: Option<JoinHandle<()>>,
|
|
running: bool,
|
|
}
|
|
|
|
static MONITOR: std::sync::OnceLock<Arc<Mutex<MonitorState>>> = std::sync::OnceLock::new();
|
|
|
|
fn get_monitor() -> Arc<Mutex<MonitorState>> {
|
|
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<String>,
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn monitor_add_hosts(hosts: Vec<MonitorHost>) -> 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<Vec<MonitorEntry>, String> {
|
|
let state = get_monitor();
|
|
let guard = state.lock().await;
|
|
let mut entries: Vec<MonitorEntry> = 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<u64>) -> 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<IpAddr> = {
|
|
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<Option<String>, 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
|
|
}
|