1use std::path::{Path, PathBuf};
7use std::sync::mpsc;
8use std::time::{Duration, Instant};
9
10use anyhow::{Context, Result};
11use notify::RecursiveMode;
12use notify_debouncer_full::{new_debouncer, DebouncedEvent, DebounceEventResult};
13
14use crate::ingest::scanner::NOISE_DIRS;
15use crate::ingest::parser::SUPPORTED_EXTENSIONS;
16
17pub const COOLDOWN_QUIET_MS: u64 = 2000;
23pub const COOLDOWN_DEADLINE_MS: u64 = 5000;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ChangeKind {
28 Created,
30 Modified,
32 Deleted,
34}
35
36#[derive(Debug, Clone)]
38pub struct WatchEvent {
39 pub paths: Vec<PathBuf>,
40 pub kind: ChangeKind,
41}
42
43pub fn run_watch_loop(
51 root: &Path,
52 stop_flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
53 on_change: impl Fn(Vec<WatchEvent>) + Send + 'static,
54) -> Result<()> {
55 let include_exts = supported_exts();
56
57 let (tx, rx) = mpsc::channel::<DebounceEventResult>();
58
59 let mut debouncer = new_debouncer(
60 Duration::from_millis(300),
61 None,
62 move |result: DebounceEventResult| {
63 let _ = tx.send(result);
67 },
68 )
69 .with_context(|| "创建文件防抖监听器失败")?;
70
71 let watch_roots = vec![root.to_path_buf()];
74 for watch_root in &watch_roots {
75 if !watch_root.exists() {
76 tracing::warn!("监听根不存在,跳过: {}", watch_root.display());
77 continue;
78 }
79 debouncer
82 .watch(watch_root.as_path(), RecursiveMode::Recursive)
83 .with_context(|| format!("监听目录失败: {}", watch_root.display()))?;
84 }
85
86 tracing::info!(
87 "文件监听已启动(阻塞模式): {}",
88 watch_roots
89 .iter()
90 .map(|p| p.display().to_string())
91 .collect::<Vec<_>>()
92 .join(", ")
93 );
94
95 use std::sync::atomic::Ordering;
102 let mut pending: Vec<(PathBuf, ChangeKind)> = Vec::new();
103 let mut pending_first_at: Option<Instant> = None;
104 let mut quiet_since: Option<Instant> = None;
105 loop {
106 if stop_flag.load(Ordering::Relaxed) {
107 tracing::info!("收到停止信号,文件监听退出");
110 return Ok(());
111 }
112 match rx.recv_timeout(Duration::from_millis(500)) {
113 Ok(Ok(events)) => {
114 let watch_events = process_batch(&events, &include_exts);
117 if !watch_events.is_empty() {
118 apply_batch(&mut pending, &watch_events);
124 let now = Instant::now();
125 pending_first_at.get_or_insert(now);
126 quiet_since = Some(now);
127 }
128 }
129 Ok(Err(errors)) => {
130 for e in &errors {
131 tracing::warn!("文件监听错误: {:?}", e);
132 }
133 }
134 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
137 if should_flush_now(&pending, &mut pending_first_at, &mut quiet_since) {
138 on_change(flush_events(&std::mem::take(&mut pending)));
139 }
140 }
141 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
143 }
144 if let Some(first_at) = pending_first_at {
150 let total = Instant::now().saturating_duration_since(first_at);
151 if should_flush(Duration::ZERO, total) {
152 on_change(flush_events(&std::mem::take(&mut pending)));
153 pending_first_at = None;
154 quiet_since = None;
155 }
156 }
157 }
158 Ok(())
159}
160
161fn should_flush(quiet_elapsed: Duration, total_elapsed: Duration) -> bool {
165 quiet_elapsed >= Duration::from_millis(COOLDOWN_QUIET_MS)
166 || total_elapsed >= Duration::from_millis(COOLDOWN_DEADLINE_MS)
167}
168
169fn should_flush_now(
171 pending: &[(PathBuf, ChangeKind)],
172 pending_first_at: &mut Option<Instant>,
173 quiet_since: &mut Option<Instant>,
174) -> bool {
175 if pending.is_empty() {
176 return false;
177 }
178 let now = Instant::now();
179 let quiet_elapsed = quiet_since
180 .map(|q| now.saturating_duration_since(q))
181 .unwrap_or(Duration::ZERO);
182 let total_elapsed = pending_first_at
183 .map(|f| now.saturating_duration_since(f))
184 .unwrap_or(Duration::ZERO);
185 let flush = should_flush(quiet_elapsed, total_elapsed);
186 if flush {
187 *pending_first_at = None;
188 *quiet_since = None;
189 }
190 flush
191}
192
193fn apply_batch(pending: &mut Vec<(PathBuf, ChangeKind)>, events: &[WatchEvent]) {
197 for event in events {
198 for p in &event.paths {
199 match pending.iter_mut().find(|(path, _)| path == p) {
200 Some(entry) => entry.1 = event.kind,
201 None => pending.push((p.clone(), event.kind)),
202 }
203 }
204 }
205}
206
207fn flush_events(pending: &[(PathBuf, ChangeKind)]) -> Vec<WatchEvent> {
210 let mut out: Vec<WatchEvent> = Vec::new();
211 for (path, kind) in pending {
212 match out.iter_mut().find(|e| e.kind == *kind) {
213 Some(ev) => ev.paths.push(path.clone()),
214 None => out.push(WatchEvent {
215 paths: vec![path.clone()],
216 kind: *kind,
217 }),
218 }
219 }
220 out
221}
222
223pub fn process_batch(events: &[DebouncedEvent], include_exts: &[String]) -> Vec<WatchEvent> {
228 fold_events(aggregate_events(events, include_exts))
229}
230
231fn fold_events(events: Vec<WatchEvent>) -> Vec<WatchEvent> {
239 let mut out: Vec<WatchEvent> = Vec::new();
240 for event in &events {
241 let paths: Vec<PathBuf> = event
242 .paths
243 .iter()
244 .filter(|p| {
245 if event.kind != ChangeKind::Deleted && has_path(&events, ChangeKind::Deleted, p) {
247 return false;
248 }
249 if event.kind == ChangeKind::Created && has_path(&events, ChangeKind::Modified, p) {
251 return false;
252 }
253 true
254 })
255 .cloned()
256 .collect();
257 if !paths.is_empty() {
258 out.push(WatchEvent { paths, kind: event.kind });
259 }
260 }
261 out
262}
263
264fn has_path(events: &[WatchEvent], kind: ChangeKind, path: &Path) -> bool {
266 events
267 .iter()
268 .any(|e| e.kind == kind && e.paths.iter().any(|p| p == path))
269}
270fn aggregate_events(events: &[DebouncedEvent], include_exts: &[String]) -> Vec<WatchEvent> {
274 let mut out: Vec<WatchEvent> = Vec::new();
275 for debounced in events {
276 let kind = change_kind_of(&debounced.event.kind);
277 for p in &debounced.event.paths {
278 if !should_report(p, include_exts) {
279 continue;
280 }
281 match out.iter_mut().find(|e| e.kind == kind) {
282 Some(ev) if !ev.paths.contains(p) => ev.paths.push(p.clone()),
284 Some(_) => {}
286 None => out.push(WatchEvent { paths: vec![p.clone()], kind }),
288 }
289 }
290 }
291 out
292}
293
294fn change_kind_of(kind: ¬ify::EventKind) -> ChangeKind {
299 match kind {
300 notify::EventKind::Create(_) => ChangeKind::Created,
301 notify::EventKind::Remove(_) => ChangeKind::Deleted,
302 _ => ChangeKind::Modified,
303 }
304}
305
306fn supported_exts() -> Vec<String> {
309 SUPPORTED_EXTENSIONS
310 .iter()
311 .map(|e| e.trim_start_matches('.').to_string())
312 .collect()
313}
314
315fn should_report(path: &Path, include_exts: &[String]) -> bool {
317 !should_ignore(path) && matches_include(path, include_exts)
318}
319
320fn should_ignore(path: &Path) -> bool {
323 path.components().any(|c| {
324 if let Some(s) = c.as_os_str().to_str() {
325 NOISE_DIRS.contains(&s)
326 } else {
327 false
328 }
329 })
330}
331
332fn matches_include(path: &Path, include_exts: &[String]) -> bool {
334 if include_exts.is_empty() {
335 return true;
336 }
337 match path.extension() {
338 Some(ext) => include_exts.iter().any(|e| ext == e.as_str()),
339 None => false,
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn test_should_ignore_target_dir() {
349 let p = Path::new("/repo/target/debug/main.rs");
350 assert!(should_ignore(p));
351 }
352
353 #[test]
354 fn test_should_ignore_git_dir() {
355 let p = Path::new("/repo/.git/HEAD");
356 assert!(should_ignore(p));
357 }
358
359 #[test]
360 fn test_should_ignore_node_modules() {
361 let p = Path::new("/repo/node_modules/foo/index.js");
362 assert!(should_ignore(p));
363 }
364
365 #[test]
366 fn test_should_ignore_dist_and_venv() {
367 assert!(should_ignore(Path::new("/repo/dist/bundle.js")));
368 assert!(should_ignore(Path::new("/repo/.venv/lib/py.py")));
369 }
370
371 #[test]
372 fn test_should_not_ignore_src_dir() {
373 let p = Path::new("/repo/src/main.rs");
374 assert!(!should_ignore(p));
375 }
376
377 #[test]
378 fn test_matches_include_with_matching_ext() {
379 let exts = vec!["rs".to_string(), "tsx".to_string()];
380 assert!(matches_include(Path::new("main.rs"), &exts));
381 assert!(matches_include(Path::new("comp.tsx"), &exts));
382 }
383
384 #[test]
385 fn test_matches_include_with_mismatch_ext() {
386 let exts = vec!["rs".to_string()];
387 assert!(!matches_include(Path::new("main.js"), &exts));
388 assert!(!matches_include(Path::new("no_ext"), &exts));
389 }
390
391 #[test]
393 fn test_supported_exts_cover_all_parsers() {
394 let exts = supported_exts();
395 for expected in ["rs", "ts", "tsx", "py", "go", "js", "jsx", "mjs", "cjs", "cs", "java"] {
396 assert!(exts.contains(&expected.to_string()), "缺少 {expected}");
397 }
398 }
399
400 #[test]
402 fn test_should_report_filters_ignored_and_mismatched() {
403 let exts = vec!["rs".to_string()];
404 assert!(should_report(Path::new("/repo/src/main.rs"), &exts));
405 assert!(!should_report(Path::new("/repo/target/main.rs"), &exts));
406 assert!(!should_report(Path::new("/repo/src/main.js"), &exts));
407 assert!(!should_report(Path::new("/repo/src/no_ext"), &exts));
408 }
409
410 #[test]
413 fn test_watch_event_kind_preserved() {
414 use notify::event::{DataChange, ModifyKind, RemoveKind};
415 use notify::{Event, EventKind};
416
417 let mk = || {
418 let mut e = Event::new(EventKind::Modify(ModifyKind::Data(DataChange::Content)));
419 e.paths = vec![PathBuf::from("src/a.rs")];
420 DebouncedEvent::new(e, std::time::Instant::now())
421 };
422 let mut removed = Event::new(EventKind::Remove(RemoveKind::File));
423 removed.paths = vec![PathBuf::from("src/b.rs")];
424 let events = vec![
425 mk(),
426 DebouncedEvent::new(removed, std::time::Instant::now()),
427 ];
428 let exts = vec!["rs".to_string()];
429 let aggregated = aggregate_events(&events, &exts);
430
431 assert_eq!(aggregated.len(), 2, "Modify 与 Remove 应各自聚合成独立事件");
432 let modified = aggregated
433 .iter()
434 .find(|e| e.kind == ChangeKind::Modified)
435 .expect("应存在 Modified 事件");
436 assert_eq!(modified.paths, vec![PathBuf::from("src/a.rs")]);
437 let deleted = aggregated
438 .iter()
439 .find(|e| e.kind == ChangeKind::Deleted)
440 .expect("应存在 Deleted 事件");
441 assert_eq!(deleted.paths, vec![PathBuf::from("src/b.rs")]);
442 }
443
444 #[test]
446 fn test_aggregate_events_dedups_same_kind_paths() {
447 use notify::event::{DataChange, ModifyKind};
448 use notify::{Event, EventKind};
449
450 let mk = || {
451 let mut e = Event::new(EventKind::Modify(ModifyKind::Data(DataChange::Content)));
452 e.paths = vec![PathBuf::from("src/a.rs")];
453 DebouncedEvent::new(e, std::time::Instant::now())
454 };
455 let exts = vec!["rs".to_string()];
456 let aggregated = aggregate_events(&[mk(), mk()], &exts);
457 assert_eq!(aggregated.len(), 1);
458 assert_eq!(aggregated[0].paths, vec![PathBuf::from("src/a.rs")]);
459 }
460
461 fn make_debounced(kind: notify::EventKind, path: &str) -> DebouncedEvent {
467 let mut e = notify::Event::new(kind);
468 e.paths = vec![PathBuf::from(path)];
469 DebouncedEvent::new(e, std::time::Instant::now())
470 }
471
472 #[test]
474 fn test_fold_modified_deleted() {
475 use notify::event::{DataChange, ModifyKind, RemoveKind};
476 let exts = vec!["rs".to_string()];
477 let events = vec![
478 make_debounced(notify::EventKind::Modify(ModifyKind::Data(DataChange::Content)), "src/a.rs"),
479 make_debounced(notify::EventKind::Remove(RemoveKind::File), "src/a.rs"),
480 ];
481 let folded = process_batch(&events, &exts);
482 assert_eq!(folded.len(), 1, "同路径 Modified+Deleted 应折叠为单事件");
483 assert_eq!(folded[0].kind, ChangeKind::Deleted);
484 assert_eq!(folded[0].paths, vec![PathBuf::from("src/a.rs")]);
485 }
486
487 #[test]
489 fn test_fold_created_deleted() {
490 use notify::event::{CreateKind, RemoveKind};
491 let exts = vec!["rs".to_string()];
492 let events = vec![
493 make_debounced(notify::EventKind::Create(CreateKind::File), "src/a.rs"),
494 make_debounced(notify::EventKind::Remove(RemoveKind::File), "src/a.rs"),
495 ];
496 let folded = process_batch(&events, &exts);
497 assert_eq!(folded.len(), 1);
498 assert_eq!(folded[0].kind, ChangeKind::Deleted);
499 }
500
501 #[test]
503 fn test_fold_created_modified() {
504 use notify::event::{CreateKind, DataChange, ModifyKind};
505 let exts = vec!["rs".to_string()];
506 let events = vec![
507 make_debounced(notify::EventKind::Create(CreateKind::File), "src/a.rs"),
508 make_debounced(notify::EventKind::Modify(ModifyKind::Data(DataChange::Content)), "src/a.rs"),
509 ];
510 let folded = process_batch(&events, &exts);
511 assert_eq!(folded.len(), 1);
512 assert_eq!(folded[0].kind, ChangeKind::Modified);
513 assert_eq!(folded[0].paths, vec![PathBuf::from("src/a.rs")]);
514 }
515
516 #[test]
519 fn test_aggregate_events_same_path_cross_kind() {
520 use notify::event::{DataChange, ModifyKind, RemoveKind};
521 let exts = vec!["rs".to_string()];
522 let events = vec![
523 make_debounced(notify::EventKind::Modify(ModifyKind::Data(DataChange::Content)), "src/a.rs"),
524 make_debounced(notify::EventKind::Remove(RemoveKind::File), "src/a.rs"),
525 ];
526 let folded = process_batch(&events, &exts);
527 assert_eq!(folded.len(), 1);
528 assert_eq!(folded[0].kind, ChangeKind::Deleted);
529 }
530
531 #[test]
534 fn test_aggregate_events_preserves_distinct_paths() {
535 use notify::event::{DataChange, ModifyKind, RemoveKind};
536 let exts = vec!["rs".to_string()];
537 let events = vec![
538 make_debounced(notify::EventKind::Modify(ModifyKind::Data(DataChange::Content)), "src/a.rs"),
539 make_debounced(notify::EventKind::Remove(RemoveKind::File), "src/a.rs"),
540 make_debounced(notify::EventKind::Modify(ModifyKind::Data(DataChange::Content)), "src/b.rs"),
541 ];
542 let folded = process_batch(&events, &exts);
543 assert_eq!(folded.len(), 2, "a 折叠为 Deleted、b 独立 Modified,共 2 事件");
544 let deleted = folded
545 .iter()
546 .find(|e| e.kind == ChangeKind::Deleted)
547 .expect("应存在 Deleted 事件");
548 assert_eq!(deleted.paths, vec![PathBuf::from("src/a.rs")]);
549 let modified = folded
550 .iter()
551 .find(|e| e.kind == ChangeKind::Modified)
552 .expect("应存在 Modified 事件");
553 assert_eq!(modified.paths, vec![PathBuf::from("src/b.rs")]);
554 }
555
556 #[test]
560 fn test_watch_loop_exits_on_pre_set_stop_flag() {
561 let dir = std::env::temp_dir().join(format!("code_repo_wiki_watch_stop_{}", std::process::id()));
562 let _ = std::fs::remove_dir_all(&dir);
563 std::fs::create_dir_all(&dir).unwrap();
564 let stop_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
566 let start = std::time::Instant::now();
567 let result = run_watch_loop(&dir, stop_flag, |_| panic!("不应触发回调"));
568 assert!(result.is_ok(), "优雅退出应返回 Ok: {result:?}");
569 assert!(
570 start.elapsed() < std::time::Duration::from_secs(5),
571 "预置停止标记应在监听启动后立即退出(无需等待事件)"
572 );
573 let _ = std::fs::remove_dir_all(&dir);
574 }
575
576 #[test]
580 fn test_should_flush_quiet_elapsed_reaches_threshold() {
581 assert!(
582 should_flush(
583 Duration::from_millis(COOLDOWN_QUIET_MS),
584 Duration::from_millis(500)
585 ),
586 "安静 2s 应触发(尾沿)"
587 );
588 assert!(
589 should_flush(
590 Duration::from_millis(3000),
591 Duration::from_millis(3000)
592 ),
593 "安静 3s 应触发"
594 );
595 }
596
597 #[test]
599 fn test_should_flush_deadline_forced() {
600 assert!(
601 should_flush(
602 Duration::from_millis(300),
603 Duration::from_millis(COOLDOWN_DEADLINE_MS)
604 ),
605 "总时长 5s 应强制触发(编辑未停也触发)"
606 );
607 }
608
609 #[test]
611 fn test_should_flush_within_cooldown_does_not_trigger() {
612 assert!(
613 !should_flush(
614 Duration::from_millis(1500),
615 Duration::from_millis(1500)
616 ),
617 "编辑未停且未到 5s 上限不应触发"
618 );
619 assert!(
620 !should_flush(Duration::ZERO, Duration::ZERO),
621 "刚收到事件不应触发"
622 );
623 }
624
625 #[test]
627 fn test_apply_batch_dedups_and_combines() {
628 let mut pending: Vec<(PathBuf, ChangeKind)> = Vec::new();
629 let batch1 = vec![WatchEvent {
630 paths: vec![PathBuf::from("src/a.rs")],
631 kind: ChangeKind::Modified,
632 }];
633 let batch2 = vec![
634 WatchEvent {
635 paths: vec![PathBuf::from("src/a.rs"), PathBuf::from("src/b.rs")],
636 kind: ChangeKind::Modified,
637 },
638 WatchEvent {
639 paths: vec![PathBuf::from("src/c.rs")],
640 kind: ChangeKind::Deleted,
641 },
642 ];
643 apply_batch(&mut pending, &batch1);
644 apply_batch(&mut pending, &batch2);
645 let flushed = flush_events(&pending);
646 assert_eq!(flushed.len(), 2, "同 kind 合并为 1 组 + Deleted 1 组");
647 let modified = flushed
648 .iter()
649 .find(|e| e.kind == ChangeKind::Modified)
650 .expect("应存在 Modified 组");
651 assert_eq!(
652 modified.paths,
653 vec![PathBuf::from("src/a.rs"), PathBuf::from("src/b.rs")],
654 "a 去重、b 追加"
655 );
656 assert!(flushed.iter().any(|e| e.kind == ChangeKind::Deleted));
657 }
658
659 #[test]
662 fn test_apply_batch_later_kind_overwrites() {
663 let mut pending: Vec<(PathBuf, ChangeKind)> = Vec::new();
664 apply_batch(
665 &mut pending,
666 &[WatchEvent {
667 paths: vec![PathBuf::from("src/a.rs")],
668 kind: ChangeKind::Modified,
669 }],
670 );
671 apply_batch(
672 &mut pending,
673 &[WatchEvent {
674 paths: vec![PathBuf::from("src/a.rs"), PathBuf::from("src/b.rs")],
675 kind: ChangeKind::Deleted,
676 }],
677 );
678 let flushed = flush_events(&pending);
679 assert_eq!(flushed.len(), 1, "跨批 Modified+Deleted 收敛为单个事件");
680 assert_eq!(flushed[0].kind, ChangeKind::Deleted);
681 assert_eq!(
682 flushed[0].paths,
683 vec![PathBuf::from("src/a.rs"), PathBuf::from("src/b.rs")]
684 );
685 }
686
687 #[test]
691 fn test_apply_batch_delete_then_recreate_keeps_created() {
692 let mut pending: Vec<(PathBuf, ChangeKind)> = Vec::new();
693 apply_batch(
694 &mut pending,
695 &[WatchEvent {
696 paths: vec![PathBuf::from("src/a.rs")],
697 kind: ChangeKind::Deleted,
698 }],
699 );
700 apply_batch(
701 &mut pending,
702 &[WatchEvent {
703 paths: vec![PathBuf::from("src/a.rs")],
704 kind: ChangeKind::Created,
705 }],
706 );
707 let flushed = flush_events(&pending);
708 assert_eq!(flushed.len(), 1);
709 assert_eq!(
710 flushed[0].kind,
711 ChangeKind::Created,
712 "删除重建必须收敛为 Created(文件最终存在),否则下游误删产物页"
713 );
714 assert_eq!(flushed[0].paths, vec![PathBuf::from("src/a.rs")]);
715 }
716
717 #[test]
719 fn test_apply_batch_accumulates_across_batches() {
720 let mut pending: Vec<(PathBuf, ChangeKind)> = Vec::new();
721 apply_batch(
722 &mut pending,
723 &[WatchEvent {
724 paths: vec![PathBuf::from("src/a.rs")],
725 kind: ChangeKind::Modified,
726 }],
727 );
728 apply_batch(
729 &mut pending,
730 &[WatchEvent {
731 paths: vec![PathBuf::from("src/b.rs")],
732 kind: ChangeKind::Modified,
733 }],
734 );
735 apply_batch(
736 &mut pending,
737 &[WatchEvent {
738 paths: vec![PathBuf::from("src/c.rs")],
739 kind: ChangeKind::Modified,
740 }],
741 );
742 let flushed = flush_events(&pending);
743 assert_eq!(flushed.len(), 1);
744 assert_eq!(
745 flushed[0].paths,
746 vec![
747 PathBuf::from("src/a.rs"),
748 PathBuf::from("src/b.rs"),
749 PathBuf::from("src/c.rs")
750 ],
751 "三批同 kind 路径应全部累积"
752 );
753 }
754}