fmf_core\engine/watch.rs
1//! Scope-mode change source (ADR-0024), the `JournalSource` second
2//! implementation that pairs with the folder-walk scanner.
3//!
4//! **Phase 1 (this file): a no-op journal.** The walk builds the index once
5//! and it stays static until the app restarts (the user's manual re-index).
6//! `read_blocking` never reports a change — it parks briefly and returns a
7//! benign empty batch so the worker can re-check its stop flag, exactly like
8//! the idle path of the USN journal. The walk snapshot is stamped with
9//! `journal_id == 0`, which `snapshot_decision` always restores (no USN
10//! cursor to validate), so a restart reloads the snapshot rather than
11//! re-walking.
12//!
13//! **Phase 2 (planned):** replace `read_blocking` with one overlapped
14//! `ReadDirectoryChangesW` handle per root behind a single IOCP, translating
15//! `FILE_NOTIFY_INFORMATION` into synthesized `UsnRecord`s (path → synthetic
16//! FRN via `scan::walk_id`), with buffer-overflow → re-walk and a periodic
17//! re-walk fallback for network/cloud roots.
18
19use std::time::Duration;
20
21use crate::usn::{ReadOutcome, StatFetcher, UsnError};
22
23use super::seams::{JournalSource, JournalView};
24
25/// Synthetic journal identity for a walk snapshot. `0` makes
26/// `snapshot_decision` always restore a loaded walk snapshot (there is no USN
27/// retention window to fall outside of).
28const WALK_JOURNAL_ID: u64 = 0;
29
30/// How long the no-op `read_blocking` parks between benign wakeups. Bounds
31/// shutdown latency (the worker re-checks `stop` on each wakeup) at the cost
32/// of one cheap timer per scope slot; Phase 2 replaces the park with an IOCP
33/// wait that returns the instant a watched root changes.
34const IDLE_PARK: Duration = Duration::from_millis(250);
35
36/// Non-elevated change source for scope mode. Holds the configured roots for
37/// Phase 2's watchers; Phase 1 keeps them only to log/identify the session.
38pub(super) struct WatcherJournalSource {
39 #[allow(dead_code)] // Phase 2: one ReadDirectoryChangesW handle per root.
40 roots: Vec<String>,
41}
42
43impl WatcherJournalSource {
44 pub(super) const fn new(roots: Vec<String>) -> Self {
45 Self { roots }
46 }
47}
48
49impl JournalSource for WatcherJournalSource {
50 fn open(&mut self) -> Result<(), UsnError> {
51 Ok(())
52 }
53
54 fn query(&mut self) -> Result<JournalView, UsnError> {
55 Ok(JournalView::scope())
56 }
57
58 fn read_blocking(&mut self, _buf: &mut Vec<u8>) -> Result<ReadOutcome, UsnError> {
59 // Phase 1: the index is static. A periodic empty batch is the benign
60 // wakeup the worker treats as "nothing to apply, re-check stop".
61 std::thread::sleep(IDLE_PARK);
62 Ok(ReadOutcome::Records {
63 records: Vec::new(),
64 truncated: false,
65 })
66 }
67
68 fn journal_id(&self) -> u64 {
69 WALK_JOURNAL_ID
70 }
71
72 fn next_usn(&self) -> i64 {
73 0
74 }
75
76 fn set_next_usn(&mut self, _usn: i64) {}
77
78 fn open_stat_fetcher(&self) -> Result<Box<dyn StatFetcher>, UsnError> {
79 // Never consulted in Phase 1 (no records are ever applied); Phase 2's
80 // watcher pre-stats changed paths into a per-batch map instead.
81 Ok(Box::new(NullStatFetcher))
82 }
83}
84
85/// A stat fetcher that knows nothing — apply never calls it in Phase 1.
86struct NullStatFetcher;
87
88impl StatFetcher for NullStatFetcher {
89 fn stat(&self, _frn: u64) -> Option<(u64, i64)> {
90 None
91 }
92}