use std::collections::HashMap;
use std::sync::Mutex;
use tauri::{AppHandle, Emitter, Manager, Runtime};
use crate::DeviceInfoExt;
mod native;
mod polling;
pub(crate) const WATCH_KINDS: &[&str] = &["battery", "network", "storage", "display", "device"];
pub(crate) trait MonitorHandle: Send {
fn stop(self: Box<Self>);
}
struct Subscription {
handle: Box<dyn MonitorHandle>,
refs: usize,
}
#[derive(Default)]
pub(crate) struct WatcherState {
subs: Mutex<HashMap<String, Subscription>>,
}
pub(crate) fn event_name(kind: &str) -> String {
format!("device-info://{kind}-changed")
}
pub(crate) fn read_snapshot<R: Runtime>(
app: &AppHandle<R>,
kind: &str,
) -> crate::Result<serde_json::Value> {
let di = app.device_info();
let value = match kind {
"battery" => serde_json::to_value(di.get_battery_info()?),
"network" => serde_json::to_value(di.get_network_info()?),
"storage" => serde_json::to_value(di.get_storage_info()?),
"display" => serde_json::to_value(di.get_display_info()?),
"device" => serde_json::to_value(di.get_device_info()?),
other => {
return Err(crate::Error::DeviceInfo(format!(
"unknown watch kind: {other}"
)))
}
};
value.map_err(|e| crate::Error::DeviceInfo(e.to_string()))
}
pub(crate) fn emit_if_changed<R: Runtime>(
app: &AppHandle<R>,
event: &str,
kind: &str,
last: &mut Option<serde_json::Value>,
) {
if let Ok(snapshot) = read_snapshot(app, kind) {
if last.as_ref() != Some(&snapshot) {
let _ = app.emit(event, &snapshot);
*last = Some(snapshot);
}
}
}
pub(crate) fn start<R: Runtime>(
app: &AppHandle<R>,
kind: &str,
interval_ms: Option<u64>,
) -> crate::Result<()> {
if !WATCH_KINDS.contains(&kind) {
return Err(crate::Error::DeviceInfo(format!(
"unknown watch kind: {kind}"
)));
}
let state = app.state::<WatcherState>();
let mut subs = state.subs.lock().map_err(poisoned)?;
if let Some(sub) = subs.get_mut(kind) {
sub.refs += 1;
return Ok(());
}
let handle = match native::try_spawn(app, kind)? {
Some(handle) => handle,
None => polling::spawn(app, kind, interval_ms),
};
subs.insert(kind.to_string(), Subscription { handle, refs: 1 });
Ok(())
}
pub(crate) fn stop<R: Runtime>(app: &AppHandle<R>, kind: &str) -> crate::Result<()> {
let state = app.state::<WatcherState>();
let handle = {
let mut subs = state.subs.lock().map_err(poisoned)?;
match subs.get_mut(kind) {
Some(sub) => {
sub.refs = sub.refs.saturating_sub(1);
if sub.refs == 0 {
subs.remove(kind).map(|sub| sub.handle)
} else {
None
}
}
None => None,
}
};
if let Some(handle) = handle {
handle.stop();
}
Ok(())
}
fn poisoned<E: std::fmt::Display>(e: E) -> crate::Error {
crate::Error::DeviceInfo(format!("watcher state poisoned: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_name_follows_convention() {
assert_eq!(event_name("battery"), "device-info://battery-changed");
assert_eq!(event_name("network"), "device-info://network-changed");
}
#[test]
fn watch_kinds_cover_all_getters() {
for kind in ["battery", "network", "storage", "display", "device"] {
assert!(WATCH_KINDS.contains(&kind), "missing watch kind: {kind}");
}
}
#[cfg(not(target_os = "windows"))]
#[test]
fn unknown_kind_is_rejected() {
let app = mock_app();
let err = start(app.handle(), "gpu", None).unwrap_err();
assert!(err.to_string().contains("unknown watch kind"));
}
#[cfg(not(target_os = "windows"))]
#[test]
fn start_and_stop_every_kind_does_not_crash() {
let app = mock_app();
for kind in WATCH_KINDS {
start(app.handle(), kind, Some(250)).unwrap();
std::thread::sleep(std::time::Duration::from_millis(60));
stop(app.handle(), kind).unwrap();
}
}
#[cfg(not(target_os = "windows"))]
#[test]
fn reference_counting_keeps_monitor_until_last_unsubscribe() {
let app = mock_app();
start(app.handle(), "battery", None).unwrap();
start(app.handle(), "battery", None).unwrap();
stop(app.handle(), "battery").unwrap(); stop(app.handle(), "battery").unwrap(); stop(app.handle(), "battery").unwrap();
}
#[cfg(not(target_os = "windows"))]
fn mock_app() -> tauri::App<tauri::test::MockRuntime> {
tauri::test::mock_builder()
.plugin(crate::init())
.build(tauri::test::mock_context(tauri::test::noop_assets()))
.expect("failed to build mock app")
}
}