code_moniker_workspace/live/
watcher.rs1use std::path::PathBuf;
2use std::sync::mpsc;
3use std::thread::{self, JoinHandle};
4use std::time::Duration;
5
6use notify::{Config, Event, RecursiveMode, Watcher};
7
8use super::model::{WorkspaceLiveEvent, WorkspaceWatchRoot};
9use super::roots::{WorkspaceEventClassifier, watch_paths_for};
10
11pub struct LiveWorkspaceWatcher {
12 _watcher: WorkspaceWatcherBackend,
13 _worker: JoinHandle<()>,
14 watched_paths: usize,
15 warnings: Vec<String>,
16}
17
18const LIVE_EVENT_DEBOUNCE: Duration = Duration::from_millis(50);
19
20impl LiveWorkspaceWatcher {
21 pub fn start<F>(roots: Vec<WorkspaceWatchRoot>, publish: F) -> anyhow::Result<Self>
22 where
23 F: Fn(WorkspaceLiveEvent) + Send + 'static,
24 {
25 Self::start_with_backend(roots, WorkspaceWatcherBackendKind::Recommended, publish)
26 }
27
28 pub fn start_polling<F>(roots: Vec<WorkspaceWatchRoot>, publish: F) -> anyhow::Result<Self>
29 where
30 F: Fn(WorkspaceLiveEvent) + Send + 'static,
31 {
32 Self::start_with_backend(roots, WorkspaceWatcherBackendKind::Polling, publish)
33 }
34
35 fn start_with_backend<F>(
36 roots: Vec<WorkspaceWatchRoot>,
37 backend: WorkspaceWatcherBackendKind,
38 publish: F,
39 ) -> anyhow::Result<Self>
40 where
41 F: Fn(WorkspaceLiveEvent) + Send + 'static,
42 {
43 let watch_targets = watch_paths_for(&roots);
44 let classifier = WorkspaceEventClassifier::new(roots);
45 let (tx, worker) = watcher_event_channel(publish);
46 let mut watcher = new_watcher(backend, classifier.clone(), tx)?;
47 let (watched_paths, warnings) = watch_target_paths(&mut watcher, &watch_targets);
48
49 Ok(Self {
50 _watcher: watcher,
51 _worker: worker,
52 watched_paths,
53 warnings,
54 })
55 }
56
57 pub fn status(&self) -> Option<String> {
58 if self.watched_paths == 0 {
59 if !self.warnings.is_empty() {
60 return Some(format!(
61 "live store disabled: no source path could be watched ({})",
62 self.warnings.join("; ")
63 ));
64 }
65 return Some("live store disabled: no source path could be watched".to_string());
66 }
67 if self.warnings.is_empty() {
68 return Some(format!(
69 "live store watching {} path(s)",
70 self.watched_paths
71 ));
72 }
73 Some(format!(
74 "live store watching {} path(s), {} warning(s)",
75 self.watched_paths,
76 self.warnings.len()
77 ))
78 }
79}
80
81enum WorkspaceWatcherBackend {
82 Recommended(notify::RecommendedWatcher),
83 Polling(notify::PollWatcher),
84}
85
86impl WorkspaceWatcherBackend {
87 fn watch(&mut self, path: &std::path::Path, mode: RecursiveMode) -> notify::Result<()> {
88 match self {
89 Self::Recommended(watcher) => watcher.watch(path, mode),
90 Self::Polling(watcher) => watcher.watch(path, mode),
91 }
92 }
93}
94
95#[derive(Clone, Copy)]
96enum WorkspaceWatcherBackendKind {
97 Recommended,
98 Polling,
99}
100
101fn watcher_event_channel<F>(publish: F) -> (mpsc::Sender<WorkspaceLiveEvent>, JoinHandle<()>)
102where
103 F: Fn(WorkspaceLiveEvent) + Send + 'static,
104{
105 let (tx, rx) = mpsc::channel();
106 let worker = thread::spawn(move || publish_coalesced_events(rx, publish));
107 (tx, worker)
108}
109
110fn new_watcher(
111 backend: WorkspaceWatcherBackendKind,
112 classifier: WorkspaceEventClassifier,
113 tx: mpsc::Sender<WorkspaceLiveEvent>,
114) -> anyhow::Result<WorkspaceWatcherBackend> {
115 match backend {
116 WorkspaceWatcherBackendKind::Recommended => Ok(WorkspaceWatcherBackend::Recommended(
117 notify::RecommendedWatcher::new(
118 move |event| publish_classified_event(&classifier, &tx, event),
119 Config::default(),
120 )?,
121 )),
122 WorkspaceWatcherBackendKind::Polling => {
123 Ok(WorkspaceWatcherBackend::Polling(notify::PollWatcher::new(
124 move |event| publish_classified_event(&classifier, &tx, event),
125 polling_watcher_config(),
126 )?))
127 }
128 }
129}
130
131fn publish_classified_event(
132 classifier: &WorkspaceEventClassifier,
133 tx: &mpsc::Sender<WorkspaceLiveEvent>,
134 event: notify::Result<Event>,
135) {
136 let Ok(event) = event else {
137 return;
138 };
139 if let Some(store_event) = classifier.classify_event(&event) {
140 let _ = tx.send(store_event);
141 }
142}
143
144fn polling_watcher_config() -> Config {
145 Config::default()
146 .with_poll_interval(Duration::from_millis(50))
147 .with_compare_contents(true)
148}
149
150fn watch_target_paths(
151 watcher: &mut WorkspaceWatcherBackend,
152 targets: &[PathBuf],
153) -> (usize, Vec<String>) {
154 let mut warnings = Vec::new();
155 let mut watched_paths = 0;
156 for path in targets {
157 match watcher.watch(path.as_path(), RecursiveMode::Recursive) {
158 Ok(()) => watched_paths += 1,
159 Err(error) => warnings.push(format!("{}: {error}", path.display())),
160 }
161 }
162 (watched_paths, warnings)
163}
164
165fn publish_coalesced_events<F>(rx: mpsc::Receiver<WorkspaceLiveEvent>, publish: F)
166where
167 F: Fn(WorkspaceLiveEvent),
168{
169 while let Ok(first) = rx.recv() {
170 let mut event = first;
171 while let Ok(next) = rx.recv_timeout(LIVE_EVENT_DEBOUNCE) {
172 event = event.coalesce(next);
173 }
174 publish(event);
175 }
176}