1use std::{
28 collections::HashMap,
29 path::PathBuf,
30 sync::{Arc, Mutex as StandardMutex},
31 time::{Duration, Instant},
32};
33
34use CommonLibrary::{
35 Environment::Requires::Requires,
36 Error::CommonError::CommonError,
37 FileSystem::FileWatcherProvider::{FileWatcherProvider, WatchEvent, WatchEventKind},
38 IPC::{IPCProvider::IPCProvider, SkyEvent::SkyEvent},
39};
40use async_trait::async_trait;
41use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
42use serde_json::json;
43use tokio::sync::mpsc as TokioMPSC;
44
45use super::MountainEnvironment::MountainEnvironment;
46use crate::dev_log;
47
48const DebounceWindow:Duration = Duration::from_millis(100);
51
52pub struct WatcherEntry {
56 #[allow(dead_code)]
57 Watcher:RecommendedWatcher,
58 LastSeen:HashMap<(PathBuf, &'static str), Instant>,
59}
60
61type DedupKey = (PathBuf, bool, Option<String>);
69
70pub struct WatcherState {
74 pub Entries:Arc<StandardMutex<HashMap<String, WatcherEntry>>>,
75 pub EventSender:TokioMPSC::UnboundedSender<WatchEvent>,
76 pub DedupIndex:Arc<StandardMutex<HashMap<DedupKey, String>>>,
81 pub Aliases:Arc<StandardMutex<HashMap<String, Vec<String>>>>,
86 pub HandleToPrimary:Arc<StandardMutex<HashMap<String, String>>>,
90}
91
92impl WatcherState {
93 pub fn Get(env:&MountainEnvironment) -> Arc<WatcherState> {
96 use std::sync::OnceLock;
97
98 static GLOBAL:OnceLock<Arc<WatcherState>> = OnceLock::new();
101 GLOBAL
102 .get_or_init(|| {
103 let (tx, mut rx) = TokioMPSC::unbounded_channel::<WatchEvent>();
104 let state = Arc::new(WatcherState {
105 Entries:Arc::new(StandardMutex::new(HashMap::new())),
106 EventSender:tx,
107 DedupIndex:Arc::new(StandardMutex::new(HashMap::new())),
108 Aliases:Arc::new(StandardMutex::new(HashMap::new())),
109 HandleToPrimary:Arc::new(StandardMutex::new(HashMap::new())),
110 });
111
112 let env_clone = env.clone();
116 let state_clone = state.clone();
117 tokio::spawn(async move {
118 use tauri::Emitter;
119 while let Some(WatchEvent { Handle, Kind, Path }) = rx.recv().await {
120 let ipc_provider:Arc<dyn IPCProvider> = env_clone.Require();
121 let mut Recipients:Vec<String> = vec![Handle.clone()];
126 if let Ok(AliasGuard) = state_clone.Aliases.lock() {
127 if let Some(AliasList) = AliasGuard.get(&Handle) {
128 Recipients.extend(AliasList.iter().cloned());
129 }
130 }
131 for RecipientHandle in Recipients {
132 let payload = json!({
133 "handle": RecipientHandle,
134 "kind": Kind.AsString(),
135 "path": Path.to_string_lossy().to_string(),
136 });
137 if let Err(error) = ipc_provider
138 .SendNotificationToSideCar(
139 "cocoon-main".to_string(),
140 "$fileWatcher:event".to_string(),
141 payload.clone(),
142 )
143 .await
144 {
145 dev_log!(
146 "filewatcher",
147 "warn: [FileWatcherProvider] Failed to forward event handle={} kind={} path={:?}: \
148 {:?}",
149 RecipientHandle,
150 Kind.AsString(),
151 Path,
152 error
153 );
154 }
155 if let Err(Error) =
164 env_clone.ApplicationHandle.emit(SkyEvent::VFSFileChange.AsStr(), &payload)
165 {
166 dev_log!(
167 "filewatcher",
168 "warn: [FileWatcherProvider] sky://vfs/fileChange emit failed: {}",
169 Error
170 );
171 }
172 }
173 }
174 });
175
176 state
177 })
178 .clone()
179 }
180}
181
182fn MapEventKind(raw:&EventKind) -> Option<WatchEventKind> {
183 match raw {
184 EventKind::Create(_) => Some(WatchEventKind::Create),
185 EventKind::Modify(_) => Some(WatchEventKind::Change),
186 EventKind::Remove(_) => Some(WatchEventKind::Delete),
187 _ => None,
189 }
190}
191
192fn CompileGlobToRegex(Pattern:&str) -> Option<regex::Regex> {
198 let mut Regex = String::with_capacity(Pattern.len() * 2 + 4);
199 if cfg!(any(target_os = "macos", target_os = "windows")) {
203 Regex.push_str("(?i)");
204 }
205 Regex.push('^');
206 let mut Chars = Pattern.chars().peekable();
207 let mut InClass = false;
208 while let Some(C) = Chars.next() {
209 if InClass {
210 if C == ']' {
211 InClass = false;
212 }
213 Regex.push(C);
214 continue;
215 }
216 match C {
217 '*' => {
218 if Chars.peek() == Some(&'*') {
219 Chars.next();
220 if Chars.peek() == Some(&'/') {
221 Chars.next();
222 Regex.push_str("(?:.*/)?");
223 } else {
224 Regex.push_str(".*");
225 }
226 } else {
227 Regex.push_str("[^/]*");
228 }
229 },
230 '?' => Regex.push_str("[^/]"),
231 '[' => {
232 Regex.push('[');
233 InClass = true;
234 },
235 '{' => Regex.push_str("(?:"),
236 '}' => Regex.push(')'),
237 ',' => Regex.push('|'),
238 '.' | '+' | '(' | ')' | '^' | '$' | '|' | '\\' => {
239 Regex.push('\\');
240 Regex.push(C);
241 },
242 _ => Regex.push(C),
243 }
244 }
245 Regex.push('$');
246 regex::Regex::new(&Regex).ok()
247}
248
249#[async_trait]
250impl FileWatcherProvider for MountainEnvironment {
251 async fn RegisterWatcher(
252 &self,
253 Handle:String,
254 Root:PathBuf,
255 IsRecursive:bool,
256 Pattern:Option<String>,
257 ) -> Result<(), CommonError> {
258 let state = WatcherState::Get(self);
259
260 {
262 let guard = state
263 .Entries
264 .lock()
265 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
266 if guard.contains_key(&Handle) {
267 dev_log!(
268 "filewatcher",
269 "[FileWatcherProvider] handle={} already registered; skipping duplicate",
270 Handle
271 );
272 return Ok(());
273 }
274 }
275
276 let DedupKeyValue:DedupKey = (Root.clone(), IsRecursive, Pattern.clone());
284 {
285 let DedupGuard = state
286 .DedupIndex
287 .lock()
288 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
289 if let Some(PrimaryHandle) = DedupGuard.get(&DedupKeyValue).cloned() {
290 drop(DedupGuard);
291 let mut AliasGuard = state
292 .Aliases
293 .lock()
294 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
295 AliasGuard
296 .entry(PrimaryHandle.clone())
297 .or_insert_with(Vec::new)
298 .push(Handle.clone());
299 let mut H2PGuard = state
300 .HandleToPrimary
301 .lock()
302 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
303 H2PGuard.insert(Handle.clone(), PrimaryHandle.clone());
304 dev_log!(
305 "filewatcher",
306 "[FileWatcherProvider] dedup hit; handle={} aliased to primary={} root={} pattern={:?}",
307 Handle,
308 PrimaryHandle,
309 Root.display(),
310 Pattern
311 );
312 return Ok(());
313 }
314 }
315 let CompiledPattern = Pattern.as_deref().and_then(CompileGlobToRegex);
321 let pattern_for_callback = CompiledPattern.clone();
322
323 let handle_for_callback = Handle.clone();
327 let sender = state.EventSender.clone();
328 let entries = state.Entries.clone();
329 let mut watcher = notify::recommended_watcher(move |event_result:notify::Result<notify::Event>| {
330 let Ok(event) = event_result else { return };
331 let Some(kind) = MapEventKind(&event.kind) else { return };
332 let kind_tag = kind.AsString();
333
334 let matched_paths:Vec<PathBuf> = event
336 .paths
337 .into_iter()
338 .filter(|path| {
339 match &pattern_for_callback {
340 Some(re) => re.is_match(&path.to_string_lossy()),
341 None => true,
342 }
343 })
344 .collect();
345 if matched_paths.is_empty() {
346 return;
347 }
348
349 let mut final_paths:Vec<PathBuf> = Vec::with_capacity(matched_paths.len());
352 if let Ok(mut guard) = entries.lock() {
353 if let Some(entry) = guard.get_mut(&handle_for_callback) {
354 let now = Instant::now();
355 entry
356 .LastSeen
357 .retain(|_, instant| now.duration_since(*instant) < Duration::from_secs(10));
358 for path in matched_paths {
359 let key = (path.clone(), kind_tag);
360 let keep = match entry.LastSeen.get(&key) {
361 Some(previous) if now.duration_since(*previous) < DebounceWindow => false,
362 _ => {
363 entry.LastSeen.insert(key, now);
364 true
365 },
366 };
367 if keep {
368 final_paths.push(path);
369 }
370 }
371 } else {
372 return;
373 }
374 } else {
375 return;
376 }
377
378 for path in final_paths {
379 let _ = sender.send(WatchEvent { Handle:handle_for_callback.clone(), Kind:kind, Path:path });
380 }
381 })
382 .map_err(|error| CommonError::Unknown { Description:format!("FileWatcher create failed: {}", error) })?;
383
384 let mode = if IsRecursive { RecursiveMode::Recursive } else { RecursiveMode::NonRecursive };
385 let WatchResult = watcher.watch(&Root, mode);
396 let mut guard = state
397 .Entries
398 .lock()
399 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
400 let _ = CompiledPattern;
401 match WatchResult {
402 Ok(()) => {
403 guard.insert(Handle.clone(), WatcherEntry { Watcher:watcher, LastSeen:HashMap::new() });
404 drop(guard);
408 if let Ok(mut DedupGuard) = state.DedupIndex.lock() {
409 DedupGuard.entry(DedupKeyValue.clone()).or_insert_with(|| Handle.clone());
410 }
411 dev_log!(
412 "filewatcher",
413 "[FileWatcherProvider] Registered watcher handle={} root={} recursive={} pattern={:?}",
414 Handle,
415 Root.display(),
416 IsRecursive,
417 Pattern
418 );
419 return Ok(());
420 },
421 Err(error) => {
422 let ErrorString = error.to_string().to_lowercase();
423 let IsBenignAbsent = ErrorString.contains("no path was found")
424 || ErrorString.contains("no such file or directory")
425 || ErrorString.contains("entity not found")
426 || ErrorString.contains("path not found")
427 || ErrorString.contains("os error 2")
428 || !Root.exists();
429 if IsBenignAbsent {
430 dev_log!(
431 "filewatcher",
432 "[FileWatcherProvider] watch path absent (deferred) handle={} root={} err={}",
433 Handle,
434 Root.display(),
435 error
436 );
437 drop(watcher);
441 } else {
442 return Err(CommonError::Unknown {
443 Description:format!("FileWatcher watch failed for {}: {}", Root.display(), error),
444 });
445 }
446 },
447 }
448
449 dev_log!(
450 "filewatcher",
451 "[FileWatcherProvider] Registered watcher handle={} root={} recursive={} pattern={:?}",
452 Handle,
453 Root.display(),
454 IsRecursive,
455 Pattern
456 );
457
458 Ok(())
459 }
460
461 async fn UnregisterWatcher(&self, Handle:String) -> Result<(), CommonError> {
462 let state = WatcherState::Get(self);
463
464 let MaybePrimary = {
468 let mut H2PGuard = state
469 .HandleToPrimary
470 .lock()
471 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
472 H2PGuard.remove(&Handle)
473 };
474 if let Some(PrimaryHandle) = MaybePrimary {
475 let mut AliasGuard = state
476 .Aliases
477 .lock()
478 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
479 if let Some(AliasList) = AliasGuard.get_mut(&PrimaryHandle) {
480 AliasList.retain(|EntryHandle| EntryHandle != &Handle);
481 if AliasList.is_empty() {
482 AliasGuard.remove(&PrimaryHandle);
483 }
484 }
485 dev_log!(
486 "filewatcher",
487 "[FileWatcherProvider] Unregistered alias handle={} primary={}",
488 Handle,
489 PrimaryHandle
490 );
491 return Ok(());
492 }
493
494 let mut Guard = state
500 .Entries
501 .lock()
502 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
503 if Guard.remove(&Handle).is_some() {
504 dev_log!("filewatcher", "[FileWatcherProvider] Unregistered watcher handle={}", Handle);
505 }
506 drop(Guard);
507
508 let mut DedupGuard = state
512 .DedupIndex
513 .lock()
514 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
515 DedupGuard.retain(|_, PrimaryHandle| PrimaryHandle != &Handle);
516 Ok(())
517 }
518}