use async_trait::async_trait;
use dashmap::DashMap;
use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use parking_lot::Mutex;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use tracing::{error, info, warn};
use windows::Win32::UI::Shell::{
FOLDERID_CommonStartMenu, FOLDERID_StartMenu, SHGetKnownFolderPath, KF_FLAG_DEFAULT,
};
use zerolaunch_plugin_api::host::HostApiError;
use zerolaunch_plugin_api::services::installation_monitor::{
InstallationCallback, InstallationEvent, InstallationEventKind, InstallationMonitor,
};
const DEFAULT_DEBOUNCE_MS: u64 = 5000;
struct CallbackRegistration {
pub callback: InstallationCallback,
}
pub struct WindowsInstallationMonitor {
watcher: Mutex<Option<RecommendedWatcher>>,
is_watching: AtomicBool,
callbacks: Arc<DashMap<String, CallbackRegistration>>,
watch_paths: Mutex<Vec<String>>,
debounce_ms: Arc<AtomicU64>,
}
impl WindowsInstallationMonitor {
pub fn new() -> Self {
Self {
watcher: Mutex::new(None),
is_watching: AtomicBool::new(false),
callbacks: Arc::new(DashMap::new()),
watch_paths: Mutex::new(Vec::new()),
debounce_ms: Arc::new(AtomicU64::new(DEFAULT_DEBOUNCE_MS)),
}
}
fn convert_event(event: notify::Event) -> InstallationEvent {
let kind = match event.kind {
EventKind::Create(_) => InstallationEventKind::Created,
EventKind::Modify(_) => InstallationEventKind::Modified,
EventKind::Remove(_) => InstallationEventKind::Removed,
_ => InstallationEventKind::Other,
};
InstallationEvent {
changed_paths: event
.paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect(),
kind,
}
}
}
fn default_watch_paths() -> Vec<String> {
let mut paths = Vec::new();
unsafe {
for folder_id in [&FOLDERID_CommonStartMenu, &FOLDERID_StartMenu] {
if let Ok(pwstr) = SHGetKnownFolderPath(folder_id, KF_FLAG_DEFAULT, None) {
if let Ok(s) = pwstr.to_string() {
if !s.is_empty() {
paths.push(s);
}
}
}
}
}
if paths.is_empty() {
if let Ok(program_data) = std::env::var("ProgramData") {
paths.push(format!(r"{}\Microsoft\Windows\Start Menu", program_data));
}
if let Ok(appdata) = std::env::var("APPDATA") {
paths.push(format!(r"{}\Microsoft\Windows\Start Menu", appdata));
}
}
paths
}
impl Default for WindowsInstallationMonitor {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl InstallationMonitor for WindowsInstallationMonitor {
async fn start_watching(&self) -> Result<(), HostApiError> {
if self.is_watching.load(Ordering::Relaxed) {
info!("Installation monitor is already watching");
return Ok(());
}
let (notify_tx, notify_rx) = std::sync::mpsc::channel();
let mut watcher =
RecommendedWatcher::new(notify_tx, notify::Config::default()).map_err(|e| {
HostApiError::ExecutionFailed {
service: "installation_monitor".to_string(),
reason: format!("创建文件监控器失败: {:?}", e),
}
})?;
let paths = {
let configured = self.watch_paths.lock();
if configured.is_empty() {
info!("未配置监控路径,使用平台默认路径(开始菜单)");
default_watch_paths()
} else {
configured.clone()
}
};
for path in &paths {
if PathBuf::from(path).exists() {
if let Err(e) = watcher.watch(&PathBuf::from(path), RecursiveMode::Recursive) {
warn!("Failed to watch path {:?}: {:?}", path, e);
} else {
info!("Started watching path: {}", path);
}
} else {
warn!("Watch path does not exist, skipping: {}", path);
}
}
*self.watcher.lock() = Some(watcher);
self.is_watching.store(true, Ordering::Relaxed);
let callbacks = self.callbacks.clone();
let debounce_ms = self.debounce_ms.clone();
thread::spawn(move || {
let mut pending: Option<InstallationEvent> = None;
let mut last_event_at: Option<Instant> = None;
loop {
let debounce = Duration::from_millis(debounce_ms.load(Ordering::Relaxed));
if pending.is_some() {
let wait = match last_event_at {
Some(t) => debounce.saturating_sub(t.elapsed()),
None => debounce,
};
match notify_rx.recv_timeout(wait) {
Ok(Ok(event)) => {
pending = Some(WindowsInstallationMonitor::convert_event(event));
last_event_at = Some(Instant::now());
}
Ok(Err(e)) => {
error!("Watch error: {:?}", e);
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
if let Some(event) = pending.take() {
for entry in callbacks.iter() {
(entry.value().callback)(event.clone());
}
last_event_at = None;
}
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
info!("Installation monitor channel closed, stopping...");
break;
}
}
} else {
match notify_rx.recv() {
Ok(Ok(event)) => {
pending = Some(WindowsInstallationMonitor::convert_event(event));
last_event_at = Some(Instant::now());
}
Ok(Err(e)) => {
error!("Watch error: {:?}", e);
}
Err(_) => {
info!("Installation monitor channel closed, stopping...");
break;
}
}
}
}
});
Ok(())
}
async fn stop_watching(&self) -> Result<(), HostApiError> {
if !self.is_watching.load(Ordering::Relaxed) {
return Ok(());
}
info!("Stopping installation monitor...");
*self.watcher.lock() = None;
self.is_watching.store(false, Ordering::Relaxed);
Ok(())
}
fn is_watching(&self) -> bool {
self.is_watching.load(Ordering::Relaxed)
}
fn register_callback(&self, id: &str, callback: InstallationCallback) {
self.callbacks
.insert(id.to_string(), CallbackRegistration { callback });
}
fn unregister_callback(&self, id: &str) {
self.callbacks.remove(id);
}
fn update_watch_paths(&self, paths: Vec<String>) {
*self.watch_paths.lock() = paths;
}
fn update_debounce_secs(&self, secs: f64) {
let ms = if secs > 0.0 {
(secs * 1000.0) as u64
} else {
0
};
self.debounce_ms.store(ms, Ordering::Relaxed);
}
}