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