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 Watcher:RecommendedWatcher,
57
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
76 pub EventSender:TokioMPSC::UnboundedSender<WatchEvent>,
77
78 pub DedupIndex:Arc<StandardMutex<HashMap<DedupKey, String>>>,
83
84 pub Aliases:Arc<StandardMutex<HashMap<String, Vec<String>>>>,
89
90 pub HandleToPrimary:Arc<StandardMutex<HashMap<String, String>>>,
94}
95
96impl WatcherState {
97 pub fn Get(env:&MountainEnvironment) -> Arc<WatcherState> {
100 use std::sync::OnceLock;
101
102 static GLOBAL:OnceLock<Arc<WatcherState>> = OnceLock::new();
105
106 GLOBAL
107 .get_or_init(|| {
108 let (tx, mut rx) = TokioMPSC::unbounded_channel::<WatchEvent>();
109
110 let state = Arc::new(WatcherState {
111 Entries:Arc::new(StandardMutex::new(HashMap::new())),
112 EventSender:tx,
113 DedupIndex:Arc::new(StandardMutex::new(HashMap::new())),
114 Aliases:Arc::new(StandardMutex::new(HashMap::new())),
115 HandleToPrimary:Arc::new(StandardMutex::new(HashMap::new())),
116 });
117
118 let env_clone = env.clone();
122
123 let state_clone = state.clone();
124
125 tokio::spawn(async move {
126 use tauri::Emitter;
127
128 while let Some(WatchEvent { Handle, Kind, Path }) = rx.recv().await {
129 let ipc_provider:Arc<dyn IPCProvider> = env_clone.Require();
130
131 let mut Recipients:Vec<String> = vec![Handle.clone()];
136
137 if let Ok(AliasGuard) = state_clone.Aliases.lock() {
138 if let Some(AliasList) = AliasGuard.get(&Handle) {
139 Recipients.extend(AliasList.iter().cloned());
140 }
141 }
142
143 for RecipientHandle in Recipients {
144 let payload = json!({
145 "handle": RecipientHandle,
146 "kind": Kind.AsString(),
147 "path": Path.to_string_lossy().to_string(),
148 });
149
150 if let Err(error) = ipc_provider
151 .SendNotificationToSideCar(
152 "cocoon-main".to_string(),
153 "$fileWatcher:event".to_string(),
154 payload.clone(),
155 )
156 .await
157 {
158 dev_log!(
159 "filewatcher",
160 "warn: [FileWatcherProvider] Failed to forward event handle={} kind={} path={:?}: \
161 {:?}",
162 RecipientHandle,
163 Kind.AsString(),
164 Path,
165 error
166 );
167 }
168
169 if let Err(Error) =
178 env_clone.ApplicationHandle.emit(SkyEvent::VFSFileChange.AsStr(), &payload)
179 {
180 dev_log!(
181 "filewatcher",
182 "warn: [FileWatcherProvider] sky://vfs/fileChange emit failed: {}",
183 Error
184 );
185 }
186 }
187 }
188 });
189
190 state
191 })
192 .clone()
193 }
194}
195
196fn MapEventKind(raw:&EventKind) -> Option<WatchEventKind> {
197 match raw {
198 EventKind::Create(_) => Some(WatchEventKind::Create),
199
200 EventKind::Modify(_) => Some(WatchEventKind::Change),
201
202 EventKind::Remove(_) => Some(WatchEventKind::Delete),
203
204 _ => None,
206 }
207}
208
209fn CompileGlobToRegex(Pattern:&str) -> Option<regex::Regex> {
215 let mut Regex = String::with_capacity(Pattern.len() * 2 + 4);
216
217 if cfg!(any(target_os = "macos", target_os = "windows")) {
221 Regex.push_str("(?i)");
222 }
223
224 Regex.push('^');
225
226 let mut Chars = Pattern.chars().peekable();
227
228 let mut InClass = false;
229
230 while let Some(C) = Chars.next() {
231 if InClass {
232 if C == ']' {
233 InClass = false;
234 }
235
236 Regex.push(C);
237
238 continue;
239 }
240
241 match C {
242 '*' => {
243 if Chars.peek() == Some(&'*') {
244 Chars.next();
245
246 if Chars.peek() == Some(&'/') {
247 Chars.next();
248
249 Regex.push_str("(?:.*/)?");
250 } else {
251 Regex.push_str(".*");
252 }
253 } else {
254 Regex.push_str("[^/]*");
255 }
256 },
257
258 '?' => Regex.push_str("[^/]"),
259
260 '[' => {
261 Regex.push('[');
262
263 InClass = true;
264 },
265
266 '{' => Regex.push_str("(?:"),
267
268 '}' => Regex.push(')'),
269
270 ',' => Regex.push('|'),
271
272 '.' | '+' | '(' | ')' | '^' | '$' | '|' | '\\' => {
273 Regex.push('\\');
274
275 Regex.push(C);
276 },
277
278 _ => Regex.push(C),
279 }
280 }
281
282 Regex.push('$');
283
284 regex::Regex::new(&Regex).ok()
285}
286
287#[async_trait]
288impl FileWatcherProvider for MountainEnvironment {
289 async fn RegisterWatcher(
290 &self,
291
292 Handle:String,
293
294 Root:PathBuf,
295
296 IsRecursive:bool,
297
298 Pattern:Option<String>,
299 ) -> Result<(), CommonError> {
300 let state = WatcherState::Get(self);
301
302 {
304 let guard = state
305 .Entries
306 .lock()
307 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
308
309 if guard.contains_key(&Handle) {
310 dev_log!(
311 "filewatcher",
312 "[FileWatcherProvider] handle={} already registered; skipping duplicate",
313 Handle
314 );
315
316 return Ok(());
317 }
318 }
319
320 let DedupKeyValue:DedupKey = (Root.clone(), IsRecursive, Pattern.clone());
328
329 {
330 let DedupGuard = state
331 .DedupIndex
332 .lock()
333 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
334
335 if let Some(PrimaryHandle) = DedupGuard.get(&DedupKeyValue).cloned() {
336 drop(DedupGuard);
337
338 let mut AliasGuard = state
339 .Aliases
340 .lock()
341 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
342
343 AliasGuard
344 .entry(PrimaryHandle.clone())
345 .or_insert_with(Vec::new)
346 .push(Handle.clone());
347
348 let mut H2PGuard = state
349 .HandleToPrimary
350 .lock()
351 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
352
353 H2PGuard.insert(Handle.clone(), PrimaryHandle.clone());
354
355 dev_log!(
356 "filewatcher",
357 "[FileWatcherProvider] dedup hit; handle={} aliased to primary={} root={} pattern={:?}",
358 Handle,
359 PrimaryHandle,
360 Root.display(),
361 Pattern
362 );
363
364 return Ok(());
365 }
366 }
367
368 let CompiledPattern = Pattern.as_deref().and_then(CompileGlobToRegex);
374
375 let pattern_for_callback = CompiledPattern.clone();
376
377 let handle_for_callback = Handle.clone();
381
382 let sender = state.EventSender.clone();
383
384 let entries = state.Entries.clone();
385
386 let mut watcher = notify::recommended_watcher(move |event_result:notify::Result<notify::Event>| {
387 let Ok(event) = event_result else { return };
388
389 let Some(kind) = MapEventKind(&event.kind) else { return };
390
391 let kind_tag = kind.AsString();
392
393 let matched_paths:Vec<PathBuf> = event
401 .paths
402 .into_iter()
403 .filter(|path| {
404 let PathString = path.to_string_lossy();
405
406 if super::FileWatcherIgnore::Fn(&PathString) {
407 return false;
408 }
409
410 match &pattern_for_callback {
411 Some(re) => re.is_match(&PathString),
412 None => true,
413 }
414 })
415 .collect();
416
417 if matched_paths.is_empty() {
418 return;
419 }
420
421 let mut final_paths:Vec<PathBuf> = Vec::with_capacity(matched_paths.len());
424
425 if let Ok(mut guard) = entries.lock() {
426 if let Some(entry) = guard.get_mut(&handle_for_callback) {
427 let now = Instant::now();
428
429 entry
430 .LastSeen
431 .retain(|_, instant| now.duration_since(*instant) < Duration::from_secs(10));
432
433 for path in matched_paths {
434 let key = (path.clone(), kind_tag);
435
436 let keep = match entry.LastSeen.get(&key) {
437 Some(previous) if now.duration_since(*previous) < DebounceWindow => false,
438 _ => {
439 entry.LastSeen.insert(key, now);
440
441 true
442 },
443 };
444
445 if keep {
446 final_paths.push(path);
447 }
448 }
449 } else {
450 return;
451 }
452 } else {
453 return;
454 }
455
456 for path in final_paths {
457 let _ = sender.send(WatchEvent { Handle:handle_for_callback.clone(), Kind:kind, Path:path });
458 }
459 })
460 .map_err(|error| CommonError::Unknown { Description:format!("FileWatcher create failed: {}", error) })?;
461
462 let mode = if IsRecursive { RecursiveMode::Recursive } else { RecursiveMode::NonRecursive };
463
464 let WatchResult = watcher.watch(&Root, mode);
475
476 let mut guard = state
477 .Entries
478 .lock()
479 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
480
481 let _ = CompiledPattern;
482
483 match WatchResult {
484 Ok(()) => {
485 guard.insert(Handle.clone(), WatcherEntry { Watcher:watcher, LastSeen:HashMap::new() });
486
487 drop(guard);
491
492 if let Ok(mut DedupGuard) = state.DedupIndex.lock() {
493 DedupGuard.entry(DedupKeyValue.clone()).or_insert_with(|| Handle.clone());
494 }
495
496 dev_log!(
497 "filewatcher",
498 "[FileWatcherProvider] Registered watcher handle={} root={} recursive={} pattern={:?}",
499 Handle,
500 Root.display(),
501 IsRecursive,
502 Pattern
503 );
504
505 return Ok(());
506 },
507
508 Err(error) => {
509 let ErrorString = error.to_string().to_lowercase();
510
511 let IsBenignAbsent = ErrorString.contains("no path was found")
512 || ErrorString.contains("no such file or directory")
513 || ErrorString.contains("entity not found")
514 || ErrorString.contains("path not found")
515 || ErrorString.contains("os error 2")
516 || !Root.exists();
517
518 if IsBenignAbsent {
519 dev_log!(
520 "filewatcher",
521 "[FileWatcherProvider] watch path absent (deferred) handle={} root={} err={}",
522 Handle,
523 Root.display(),
524 error
525 );
526
527 drop(watcher);
531 } else {
532 return Err(CommonError::Unknown {
533 Description:format!("FileWatcher watch failed for {}: {}", Root.display(), error),
534 });
535 }
536 },
537 }
538
539 dev_log!(
540 "filewatcher",
541 "[FileWatcherProvider] Registered watcher handle={} root={} recursive={} pattern={:?}",
542 Handle,
543 Root.display(),
544 IsRecursive,
545 Pattern
546 );
547
548 Ok(())
549 }
550
551 async fn UnregisterWatcher(&self, Handle:String) -> Result<(), CommonError> {
552 let state = WatcherState::Get(self);
553
554 let MaybePrimary = {
558 let mut H2PGuard = state
559 .HandleToPrimary
560 .lock()
561 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
562
563 H2PGuard.remove(&Handle)
564 };
565
566 if let Some(PrimaryHandle) = MaybePrimary {
567 let mut AliasGuard = state
568 .Aliases
569 .lock()
570 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
571
572 if let Some(AliasList) = AliasGuard.get_mut(&PrimaryHandle) {
573 AliasList.retain(|EntryHandle| EntryHandle != &Handle);
574
575 if AliasList.is_empty() {
576 AliasGuard.remove(&PrimaryHandle);
577 }
578 }
579
580 dev_log!(
581 "filewatcher",
582 "[FileWatcherProvider] Unregistered alias handle={} primary={}",
583 Handle,
584 PrimaryHandle
585 );
586
587 return Ok(());
588 }
589
590 let mut Guard = state
596 .Entries
597 .lock()
598 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
599
600 if Guard.remove(&Handle).is_some() {
601 dev_log!("filewatcher", "[FileWatcherProvider] Unregistered watcher handle={}", Handle);
602 }
603
604 drop(Guard);
605
606 let mut DedupGuard = state
610 .DedupIndex
611 .lock()
612 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
613
614 DedupGuard.retain(|_, PrimaryHandle| PrimaryHandle != &Handle);
615
616 Ok(())
617 }
618}