1use crate::proto::{send_msg, Out};
10use crate::store::expand;
11use serde_json::{json, Value};
12use std::collections::HashMap;
13use std::fs::File;
14use std::io::{Read, Seek, SeekFrom};
15use std::path::{Path, PathBuf};
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::Arc;
18use std::thread::JoinHandle;
19use std::time::{Duration, UNIX_EPOCH};
20
21const MAX_ENTRIES: usize = 20_000;
23const MAX_LINE: usize = 1 << 20;
25
26pub struct Watcher {
28 stop: Arc<AtomicBool>,
29 handle: Option<JoinHandle<()>>,
30}
31
32impl Drop for Watcher {
33 fn drop(&mut self) {
34 self.stop.store(true, Ordering::Relaxed);
35 if let Some(h) = self.handle.take() {
36 let _ = h.join();
37 }
38 }
39}
40
41impl Watcher {
42 pub fn fs_watch(out: &Out, req: &Value, id: String) -> Watcher {
45 let path = expand(req["path"].as_str().unwrap_or("."));
46 let recursive = req["recursive"].as_bool().unwrap_or(false);
47 let interval = Duration::from_millis(req["interval_ms"].as_u64().unwrap_or(1000).max(100));
48 Self::spawn(out, move |out, stop| {
49 watch_loop(out, stop, &path, recursive, interval, &id)
50 })
51 }
52
53 pub fn fs_tail(out: &Out, req: &Value, id: String) -> Watcher {
56 let path = expand(req["path"].as_str().unwrap_or("."));
57 let from_end = req["from"].as_str() != Some("start");
58 let interval = Duration::from_millis(req["interval_ms"].as_u64().unwrap_or(300).max(50));
59 Self::spawn(out, move |out, stop| {
60 tail_loop(out, stop, &path, from_end, interval, &id)
61 })
62 }
63
64 pub fn meter_stream(out: &Out, req: &Value, id: String) -> Watcher {
68 let path = expand(req["path"].as_str().unwrap_or("."));
69 let interval = Duration::from_millis(req["interval_ms"].as_u64().unwrap_or(33).max(16));
70 Self::spawn(out, move |out, stop| {
71 meter_loop(out, stop, &path, interval, &id)
72 })
73 }
74
75 fn spawn(out: &Out, body: impl FnOnce(&Out, &AtomicBool) + Send + 'static) -> Watcher {
76 let stop = Arc::new(AtomicBool::new(false));
77 let (s2, o) = (stop.clone(), out.clone());
78 let handle = std::thread::spawn(move || body(&o, &s2));
79 Watcher {
80 stop,
81 handle: Some(handle),
82 }
83 }
84}
85
86fn sleep_sliced(stop: &AtomicBool, dur: Duration) {
88 let mut slept = Duration::ZERO;
89 while slept < dur && !stop.load(Ordering::Relaxed) {
90 let slice = Duration::from_millis(100).min(dur - slept);
91 std::thread::sleep(slice);
92 slept += slice;
93 }
94}
95
96fn with_id(mut ev: Value, id: &str) -> Value {
97 if !id.is_empty() {
98 ev["id"] = json!(id);
99 }
100 ev
101}
102
103fn meter_loop(out: &Out, stop: &AtomicBool, path: &Path, interval: Duration, id: &str) {
106 let mut last = 0u64;
107 while !stop.load(Ordering::Relaxed) {
108 if let Some(mt) = mtime_ms(path) {
109 if mt != last {
110 last = mt;
111 if let Ok(bytes) = std::fs::read(path) {
112 if let Ok(text) = String::from_utf8(bytes) {
113 let frame = with_id(json!({ "ev": "meter", "text": text }), id);
114 if send_msg(out, &frame).is_err() {
115 break;
116 }
117 }
118 }
119 }
120 }
121 sleep_sliced(stop, interval);
122 }
123}
124
125fn mtime_ms(p: &Path) -> Option<u64> {
126 std::fs::metadata(p)
127 .ok()?
128 .modified()
129 .ok()?
130 .duration_since(UNIX_EPOCH)
131 .ok()
132 .map(|d| d.as_millis() as u64)
133}
134
135fn snapshot(root: &Path, recursive: bool) -> HashMap<PathBuf, u64> {
137 let mut out = HashMap::new();
138 if root.is_file() {
139 if let Some(m) = mtime_ms(root) {
140 out.insert(root.to_path_buf(), m);
141 }
142 return out;
143 }
144 let mut stack = vec![(root.to_path_buf(), 0usize)];
145 while let Some((dir, depth)) = stack.pop() {
146 let Ok(rd) = std::fs::read_dir(&dir) else {
147 continue;
148 };
149 for e in rd.flatten() {
150 let p = e.path();
151 let is_dir = e.file_type().map(|t| t.is_dir()).unwrap_or(false);
152 if let Some(m) = mtime_ms(&p) {
153 out.insert(p.clone(), m);
154 }
155 if is_dir && recursive && depth < 32 {
156 stack.push((p, depth + 1));
157 }
158 if out.len() >= MAX_ENTRIES {
159 return out;
160 }
161 }
162 }
163 out
164}
165
166fn watch_loop(
167 out: &Out,
168 stop: &AtomicBool,
169 root: &Path,
170 recursive: bool,
171 interval: Duration,
172 id: &str,
173) {
174 let emit = |kind: &str, p: &Path| -> bool {
175 let frame = with_id(
176 json!({"ev": "fs", "kind": kind, "path": p.to_string_lossy()}),
177 id,
178 );
179 send_msg(out, &frame).is_ok()
180 };
181 let mut prev = snapshot(root, recursive);
182 while !stop.load(Ordering::Relaxed) {
183 sleep_sliced(stop, interval);
184 if stop.load(Ordering::Relaxed) {
185 break;
186 }
187 let cur = snapshot(root, recursive);
188 for (p, m) in &cur {
189 let changed = match prev.get(p) {
190 None => emit("created", p),
191 Some(pm) if pm != m => emit("modified", p),
192 _ => true,
193 };
194 if !changed {
195 return; }
197 }
198 for p in prev.keys() {
199 if !cur.contains_key(p) && !emit("removed", p) {
200 return;
201 }
202 }
203 prev = cur;
204 }
205}
206
207fn tail_loop(
208 out: &Out,
209 stop: &AtomicBool,
210 path: &Path,
211 from_end: bool,
212 interval: Duration,
213 id: &str,
214) {
215 let mut pos: u64 = if from_end {
216 std::fs::metadata(path).map(|m| m.len()).unwrap_or(0)
217 } else {
218 0
219 };
220 let mut carry = String::new();
221 while !stop.load(Ordering::Relaxed) {
222 if let Ok(mut f) = File::open(path) {
223 let len = f.metadata().map(|m| m.len()).unwrap_or(0);
224 if len < pos {
225 pos = 0;
227 carry.clear();
228 }
229 if len > pos && f.seek(SeekFrom::Start(pos)).is_ok() {
230 let mut buf = Vec::new();
231 if f.take(len - pos).read_to_end(&mut buf).is_ok() {
232 pos += buf.len() as u64;
233 carry.push_str(&String::from_utf8_lossy(&buf));
234 if !flush_lines(out, &mut carry, id) {
235 return; }
237 }
238 }
239 }
240 sleep_sliced(stop, interval);
241 }
242}
243
244fn flush_lines(out: &Out, carry: &mut String, id: &str) -> bool {
246 while let Some(idx) = carry.find('\n') {
247 let line: String = carry.drain(..=idx).collect();
248 let line = line.trim_end_matches(['\r', '\n']);
249 if !send_line(out, line, id) {
250 return false;
251 }
252 }
253 if carry.len() > MAX_LINE {
254 let line = std::mem::take(carry);
255 return send_line(out, &line, id);
256 }
257 true
258}
259
260fn send_line(out: &Out, line: &str, id: &str) -> bool {
261 send_msg(out, &with_id(json!({"ev": "line", "data": line}), id)).is_ok()
262}