DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/Environment/
FileWatcherProvider.rs1use 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 let state = Arc::new(WatcherState {
110 Entries:Arc::new(StandardMutex::new(HashMap::new())),
111 EventSender:tx,
112 DedupIndex:Arc::new(StandardMutex::new(HashMap::new())),
113 Aliases:Arc::new(StandardMutex::new(HashMap::new())),
114 HandleToPrimary:Arc::new(StandardMutex::new(HashMap::new())),
115 });
116
117 let env_clone = env.clone();
121 let state_clone = state.clone();
122 tokio::spawn(async move {
123 use tauri::Emitter;
124 while let Some(WatchEvent { Handle, Kind, Path }) = rx.recv().await {
125 let ipc_provider:Arc<dyn IPCProvider> = env_clone.Require();
126 let mut Recipients:Vec<String> = vec![Handle.clone()];
131 if let Ok(AliasGuard) = state_clone.Aliases.lock() {
132 if let Some(AliasList) = AliasGuard.get(&Handle) {
133 Recipients.extend(AliasList.iter().cloned());
134 }
135 }
136 for RecipientHandle in Recipients {
137 let payload = json!({
138 "handle": RecipientHandle,
139 "kind": Kind.AsString(),
140 "path": Path.to_string_lossy().to_string(),
141 });
142 if let Err(error) = ipc_provider
143 .SendNotificationToSideCar(
144 "cocoon-main".to_string(),
145 "$fileWatcher:event".to_string(),
146 payload.clone(),
147 )
148 .await
149 {
150 dev_log!(
151 "filewatcher",
152 "warn: [FileWatcherProvider] Failed to forward event handle={} kind={} path={:?}: \
153 {:?}",
154 RecipientHandle,
155 Kind.AsString(),
156 Path,
157 error
158 );
159 }
160 if let Err(Error) =
169 env_clone.ApplicationHandle.emit(SkyEvent::VFSFileChange.AsStr(), &payload)
170 {
171 dev_log!(
172 "filewatcher",
173 "warn: [FileWatcherProvider] sky://vfs/fileChange emit failed: {}",
174 Error
175 );
176 }
177 }
178 }
179 });
180
181 state
182 })
183 .clone()
184 }
185}
186
187fn MapEventKind(raw:&EventKind) -> Option<WatchEventKind> {
188 match raw {
189 EventKind::Create(_) => Some(WatchEventKind::Create),
190
191 EventKind::Modify(_) => Some(WatchEventKind::Change),
192
193 EventKind::Remove(_) => Some(WatchEventKind::Delete),
194
195 _ => None,
197 }
198}
199
200fn CompileGlobToRegex(Pattern:&str) -> Option<regex::Regex> {
206 let mut Regex = String::with_capacity(Pattern.len() * 2 + 4);
207
208 if cfg!(any(target_os = "macos", target_os = "windows")) {
212 Regex.push_str("(?i)");
213 }
214
215 Regex.push('^');
216
217 let mut Chars = Pattern.chars().peekable();
218
219 let mut InClass = false;
220
221 while let Some(C) = Chars.next() {
222 if InClass {
223 if C == ']' {
224 InClass = false;
225 }
226
227 Regex.push(C);
228
229 continue;
230 }
231
232 match C {
233 '*' => {
234 if Chars.peek() == Some(&'*') {
235 Chars.next();
236
237 if Chars.peek() == Some(&'/') {
238 Chars.next();
239
240 Regex.push_str("(?:.*/)?");
241 } else {
242 Regex.push_str(".*");
243 }
244 } else {
245 Regex.push_str("[^/]*");
246 }
247 },
248
249 '?' => Regex.push_str("[^/]"),
250
251 '[' => {
252 Regex.push('[');
253
254 InClass = true;
255 },
256
257 '{' => Regex.push_str("(?:"),
258
259 '}' => Regex.push(')'),
260
261 ',' => Regex.push('|'),
262
263 '.' | '+' | '(' | ')' | '^' | '$' | '|' | '\\' => {
264 Regex.push('\\');
265
266 Regex.push(C);
267 },
268
269 _ => Regex.push(C),
270 }
271 }
272
273 Regex.push('$');
274
275 regex::Regex::new(&Regex).ok()
276}
277
278#[async_trait]
279impl FileWatcherProvider for MountainEnvironment {
280 async fn RegisterWatcher(
281 &self,
282
283 Handle:String,
284
285 Root:PathBuf,
286
287 IsRecursive:bool,
288
289 Pattern:Option<String>,
290 ) -> Result<(), CommonError> {
291 let state = WatcherState::Get(self);
292
293 {
295 let guard = state
296 .Entries
297 .lock()
298 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
299
300 if guard.contains_key(&Handle) {
301 dev_log!(
302 "filewatcher",
303 "[FileWatcherProvider] handle={} already registered; skipping duplicate",
304 Handle
305 );
306
307 return Ok(());
308 }
309 }
310
311 let DedupKeyValue:DedupKey = (Root.clone(), IsRecursive, Pattern.clone());
319
320 {
321 let DedupGuard = state
322 .DedupIndex
323 .lock()
324 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
325
326 if let Some(PrimaryHandle) = DedupGuard.get(&DedupKeyValue).cloned() {
327 drop(DedupGuard);
328
329 let mut AliasGuard = state
330 .Aliases
331 .lock()
332 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
333
334 AliasGuard
335 .entry(PrimaryHandle.clone())
336 .or_insert_with(Vec::new)
337 .push(Handle.clone());
338
339 let mut H2PGuard = state
340 .HandleToPrimary
341 .lock()
342 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
343
344 H2PGuard.insert(Handle.clone(), PrimaryHandle.clone());
345
346 dev_log!(
347 "filewatcher",
348 "[FileWatcherProvider] dedup hit; handle={} aliased to primary={} root={} pattern={:?}",
349 Handle,
350 PrimaryHandle,
351 Root.display(),
352 Pattern
353 );
354
355 return Ok(());
356 }
357 }
358
359 let CompiledPattern = Pattern.as_deref().and_then(CompileGlobToRegex);
365
366 let pattern_for_callback = CompiledPattern.clone();
367
368 let handle_for_callback = Handle.clone();
372
373 let sender = state.EventSender.clone();
374
375 let entries = state.Entries.clone();
376
377 let mut watcher = notify::recommended_watcher(move |event_result:notify::Result<notify::Event>| {
378 let Ok(event) = event_result else { return };
379 let Some(kind) = MapEventKind(&event.kind) else { return };
380 let kind_tag = kind.AsString();
381
382 let matched_paths:Vec<PathBuf> = event
390 .paths
391 .into_iter()
392 .filter(|path| {
393 let PathString = path.to_string_lossy();
394
395 if super::FileWatcherIgnore::Fn(&PathString) {
396 return false;
397 }
398
399 match &pattern_for_callback {
400 Some(re) => re.is_match(&PathString),
401 None => true,
402 }
403 })
404 .collect();
405 if matched_paths.is_empty() {
406 return;
407 }
408
409 let mut final_paths:Vec<PathBuf> = Vec::with_capacity(matched_paths.len());
412 if let Ok(mut guard) = entries.lock() {
413 if let Some(entry) = guard.get_mut(&handle_for_callback) {
414 let now = Instant::now();
415 entry
416 .LastSeen
417 .retain(|_, instant| now.duration_since(*instant) < Duration::from_secs(10));
418 for path in matched_paths {
419 let key = (path.clone(), kind_tag);
420 let keep = match entry.LastSeen.get(&key) {
421 Some(previous) if now.duration_since(*previous) < DebounceWindow => false,
422 _ => {
423 entry.LastSeen.insert(key, now);
424 true
425 },
426 };
427 if keep {
428 final_paths.push(path);
429 }
430 }
431 } else {
432 return;
433 }
434 } else {
435 return;
436 }
437
438 for path in final_paths {
439 let _ = sender.send(WatchEvent { Handle:handle_for_callback.clone(), Kind:kind, Path:path });
440 }
441 })
442 .map_err(|error| CommonError::Unknown { Description:format!("FileWatcher create failed: {}", error) })?;
443
444 let mode = if IsRecursive { RecursiveMode::Recursive } else { RecursiveMode::NonRecursive };
445
446 let WatchResult = watcher.watch(&Root, mode);
457
458 let mut guard = state
459 .Entries
460 .lock()
461 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
462
463 let _ = CompiledPattern;
464
465 match WatchResult {
466 Ok(()) => {
467 guard.insert(Handle.clone(), WatcherEntry { Watcher:watcher, LastSeen:HashMap::new() });
468
469 drop(guard);
473
474 if let Ok(mut DedupGuard) = state.DedupIndex.lock() {
475 DedupGuard.entry(DedupKeyValue.clone()).or_insert_with(|| Handle.clone());
476 }
477
478 dev_log!(
479 "filewatcher",
480 "[FileWatcherProvider] Registered watcher handle={} root={} recursive={} pattern={:?}",
481 Handle,
482 Root.display(),
483 IsRecursive,
484 Pattern
485 );
486
487 return Ok(());
488 },
489
490 Err(error) => {
491 let ErrorString = error.to_string().to_lowercase();
492
493 let IsBenignAbsent = ErrorString.contains("no path was found")
494 || ErrorString.contains("no such file or directory")
495 || ErrorString.contains("entity not found")
496 || ErrorString.contains("path not found")
497 || ErrorString.contains("os error 2")
498 || !Root.exists();
499
500 if IsBenignAbsent {
501 dev_log!(
502 "filewatcher",
503 "[FileWatcherProvider] watch path absent (deferred) handle={} root={} err={}",
504 Handle,
505 Root.display(),
506 error
507 );
508
509 drop(watcher);
513 } else {
514 return Err(CommonError::Unknown {
515 Description:format!("FileWatcher watch failed for {}: {}", Root.display(), error),
516 });
517 }
518 },
519 }
520
521 dev_log!(
522 "filewatcher",
523 "[FileWatcherProvider] Registered watcher handle={} root={} recursive={} pattern={:?}",
524 Handle,
525 Root.display(),
526 IsRecursive,
527 Pattern
528 );
529
530 Ok(())
531 }
532
533 async fn UnregisterWatcher(&self, Handle:String) -> Result<(), CommonError> {
534 let state = WatcherState::Get(self);
535
536 let MaybePrimary = {
540 let mut H2PGuard = state
541 .HandleToPrimary
542 .lock()
543 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
544
545 H2PGuard.remove(&Handle)
546 };
547
548 if let Some(PrimaryHandle) = MaybePrimary {
549 let mut AliasGuard = state
550 .Aliases
551 .lock()
552 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
553
554 if let Some(AliasList) = AliasGuard.get_mut(&PrimaryHandle) {
555 AliasList.retain(|EntryHandle| EntryHandle != &Handle);
556
557 if AliasList.is_empty() {
558 AliasGuard.remove(&PrimaryHandle);
559 }
560 }
561
562 dev_log!(
563 "filewatcher",
564 "[FileWatcherProvider] Unregistered alias handle={} primary={}",
565 Handle,
566 PrimaryHandle
567 );
568
569 return Ok(());
570 }
571
572 let mut Guard = state
578 .Entries
579 .lock()
580 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
581
582 if Guard.remove(&Handle).is_some() {
583 dev_log!("filewatcher", "[FileWatcherProvider] Unregistered watcher handle={}", Handle);
584 }
585
586 drop(Guard);
587
588 let mut DedupGuard = state
592 .DedupIndex
593 .lock()
594 .map_err(|error| CommonError::StateLockPoisoned { Context:error.to_string() })?;
595
596 DedupGuard.retain(|_, PrimaryHandle| PrimaryHandle != &Handle);
597
598 Ok(())
599 }
600}