1use notify::Watcher as _;
8use pushkin_core::envelope::CheckResult;
9use pushkin_core::manifest::Manifest;
10use pushkin_core::pipeline::{check_write, WriteRequest};
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, Mutex, RwLock};
15
16fn fnv1a(bytes: &[u8]) -> u64 {
20 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
21 for byte in bytes {
22 hash ^= u64::from(*byte);
23 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
24 }
25 hash
26}
27
28const MANIFEST_NAME: &str = "pushkin.toml";
31
32type Memo = HashMap<String, (u64, CheckResult)>;
33
34struct Inner {
35 manifest: RwLock<Manifest>,
36 memo: Mutex<Memo>,
37 hits: AtomicU64,
38}
39
40pub struct WarmState {
42 inner: Arc<Inner>,
43}
44
45pub struct WatchGuard {
47 _watcher: notify::RecommendedWatcher,
48}
49
50impl WarmState {
51 #[must_use]
52 pub fn new(manifest: Manifest) -> Self {
53 Self {
54 inner: Arc::new(Inner {
55 manifest: RwLock::new(manifest),
56 memo: Mutex::new(HashMap::new()),
57 hits: AtomicU64::new(0),
58 }),
59 }
60 }
61
62 #[must_use]
65 pub fn share(&self) -> Self {
66 Self {
67 inner: Arc::clone(&self.inner),
68 }
69 }
70
71 #[must_use]
75 pub fn check(&self, request: &WriteRequest) -> CheckResult {
76 let content_hash = fnv1a(request.content.as_bytes());
77 if let Ok(memo) = self.inner.memo.lock() {
78 if let Some((stored_hash, stored)) = memo.get(&request.file_path) {
79 if *stored_hash == content_hash {
80 self.inner.hits.fetch_add(1, Ordering::SeqCst);
81 return stored.clone();
82 }
83 }
84 }
85 let result = match self.inner.manifest.read() {
86 Ok(manifest) => check_write(&manifest, request),
87 Err(poisoned) => check_write(&poisoned.into_inner(), request),
88 };
89 if let Ok(mut memo) = self.inner.memo.lock() {
90 memo.insert(request.file_path.clone(), (content_hash, result.clone()));
91 }
92 result
93 }
94
95 #[must_use]
97 pub fn hits(&self) -> u64 {
98 self.inner.hits.load(Ordering::SeqCst)
99 }
100
101 pub fn swap_manifest(&self, manifest: Manifest) {
104 match self.inner.manifest.write() {
105 Ok(mut slot) => *slot = manifest,
106 Err(poisoned) => *poisoned.into_inner() = manifest,
107 }
108 if let Ok(mut memo) = self.inner.memo.lock() {
109 memo.clear();
110 }
111 }
112
113 pub fn invalidate_path(&self, path: &str) {
115 if let Ok(mut memo) = self.inner.memo.lock() {
116 memo.remove(path);
117 }
118 }
119
120 pub fn watch(&self, root: &Path) -> std::io::Result<WatchGuard> {
129 let governing = root.join(MANIFEST_NAME);
130 self.watch_governing(root, &governing)
131 }
132
133 pub fn watch_governing(&self, root: &Path, governing: &Path) -> std::io::Result<WatchGuard> {
143 let inner = Arc::clone(&self.inner);
144 let root_buf = root.to_path_buf();
145 let governing_buf = canonical_or_owned(governing);
146 let mut watcher =
147 notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| {
148 let Ok(event) = event else { return };
149 for path in event.paths {
150 handle_change(&inner, &root_buf, &governing_buf, &path);
151 }
152 })
153 .map_err(std::io::Error::other)?;
154 watcher
155 .watch(root, notify::RecursiveMode::Recursive)
156 .map_err(std::io::Error::other)?;
157 Ok(WatchGuard { _watcher: watcher })
158 }
159}
160
161fn canonical_or_owned(path: &Path) -> PathBuf {
167 path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
168}
169
170fn handle_change(inner: &Inner, root: &Path, governing: &Path, changed: &Path) {
171 if canonical_or_owned(changed) == governing {
172 reload_manifest(inner, changed);
173 return;
174 }
175 if changed
176 .file_name()
177 .is_some_and(|name| name == MANIFEST_NAME)
178 {
179 eprintln!(
184 "pushkin daemon: declined to reload from {} — it is not the governing \
185 manifest ({}). Only the resolved manifest reloads.",
186 changed.display(),
187 governing.display()
188 );
189 return;
190 }
191 let key = changed
192 .strip_prefix(root)
193 .unwrap_or(changed)
194 .to_string_lossy()
195 .into_owned();
196 if let Ok(mut memo) = inner.memo.lock() {
197 memo.remove(&key);
198 }
199}
200
201fn reload_manifest(inner: &Inner, path: &Path) {
208 let mut outcome = load(path);
214 if outcome.is_err() {
215 std::thread::sleep(std::time::Duration::from_millis(200));
216 outcome = load(path);
217 }
218 let manifest = match outcome {
219 Ok(manifest) => manifest,
220 Err(reason) => {
221 eprintln!(
222 "pushkin daemon: the governing manifest {} no longer parses \
223 ({reason}). Serving STOPPED — stale rules are worse than no \
224 daemon; checks fall back to the cold pipeline until the \
225 manifest is fixed and the daemon restarted.",
226 path.display()
227 );
228 std::process::exit(1);
229 }
230 };
231 match inner.manifest.write() {
232 Ok(mut slot) => *slot = manifest,
233 Err(poisoned) => *poisoned.into_inner() = manifest,
234 }
235 if let Ok(mut memo) = inner.memo.lock() {
237 memo.clear();
238 }
239}
240
241fn load(path: &Path) -> Result<Manifest, String> {
243 std::fs::read_to_string(path)
244 .map_err(|error| error.to_string())
245 .and_then(|text| Manifest::parse(&text).map_err(|error| error.to_string()))
246}