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 fn spawn(out: &Out, body: impl FnOnce(&Out, &AtomicBool) + Send + 'static) -> Watcher {
65 let stop = Arc::new(AtomicBool::new(false));
66 let (s2, o) = (stop.clone(), out.clone());
67 let handle = std::thread::spawn(move || body(&o, &s2));
68 Watcher {
69 stop,
70 handle: Some(handle),
71 }
72 }
73}
74
75fn sleep_sliced(stop: &AtomicBool, dur: Duration) {
77 let mut slept = Duration::ZERO;
78 while slept < dur && !stop.load(Ordering::Relaxed) {
79 let slice = Duration::from_millis(100).min(dur - slept);
80 std::thread::sleep(slice);
81 slept += slice;
82 }
83}
84
85fn with_id(mut ev: Value, id: &str) -> Value {
86 if !id.is_empty() {
87 ev["id"] = json!(id);
88 }
89 ev
90}
91
92fn mtime_ms(p: &Path) -> Option<u64> {
93 std::fs::metadata(p)
94 .ok()?
95 .modified()
96 .ok()?
97 .duration_since(UNIX_EPOCH)
98 .ok()
99 .map(|d| d.as_millis() as u64)
100}
101
102fn snapshot(root: &Path, recursive: bool) -> HashMap<PathBuf, u64> {
104 let mut out = HashMap::new();
105 if root.is_file() {
106 if let Some(m) = mtime_ms(root) {
107 out.insert(root.to_path_buf(), m);
108 }
109 return out;
110 }
111 let mut stack = vec![(root.to_path_buf(), 0usize)];
112 while let Some((dir, depth)) = stack.pop() {
113 let Ok(rd) = std::fs::read_dir(&dir) else {
114 continue;
115 };
116 for e in rd.flatten() {
117 let p = e.path();
118 let is_dir = e.file_type().map(|t| t.is_dir()).unwrap_or(false);
119 if let Some(m) = mtime_ms(&p) {
120 out.insert(p.clone(), m);
121 }
122 if is_dir && recursive && depth < 32 {
123 stack.push((p, depth + 1));
124 }
125 if out.len() >= MAX_ENTRIES {
126 return out;
127 }
128 }
129 }
130 out
131}
132
133fn watch_loop(
134 out: &Out,
135 stop: &AtomicBool,
136 root: &Path,
137 recursive: bool,
138 interval: Duration,
139 id: &str,
140) {
141 let emit = |kind: &str, p: &Path| -> bool {
142 let frame = with_id(
143 json!({"ev": "fs", "kind": kind, "path": p.to_string_lossy()}),
144 id,
145 );
146 send_msg(out, &frame).is_ok()
147 };
148 let mut prev = snapshot(root, recursive);
149 while !stop.load(Ordering::Relaxed) {
150 sleep_sliced(stop, interval);
151 if stop.load(Ordering::Relaxed) {
152 break;
153 }
154 let cur = snapshot(root, recursive);
155 for (p, m) in &cur {
156 let changed = match prev.get(p) {
157 None => emit("created", p),
158 Some(pm) if pm != m => emit("modified", p),
159 _ => true,
160 };
161 if !changed {
162 return; }
164 }
165 for p in prev.keys() {
166 if !cur.contains_key(p) && !emit("removed", p) {
167 return;
168 }
169 }
170 prev = cur;
171 }
172}
173
174fn tail_loop(
175 out: &Out,
176 stop: &AtomicBool,
177 path: &Path,
178 from_end: bool,
179 interval: Duration,
180 id: &str,
181) {
182 let mut pos: u64 = if from_end {
183 std::fs::metadata(path).map(|m| m.len()).unwrap_or(0)
184 } else {
185 0
186 };
187 let mut carry = String::new();
188 while !stop.load(Ordering::Relaxed) {
189 if let Ok(mut f) = File::open(path) {
190 let len = f.metadata().map(|m| m.len()).unwrap_or(0);
191 if len < pos {
192 pos = 0;
194 carry.clear();
195 }
196 if len > pos && f.seek(SeekFrom::Start(pos)).is_ok() {
197 let mut buf = Vec::new();
198 if f.take(len - pos).read_to_end(&mut buf).is_ok() {
199 pos += buf.len() as u64;
200 carry.push_str(&String::from_utf8_lossy(&buf));
201 if !flush_lines(out, &mut carry, id) {
202 return; }
204 }
205 }
206 }
207 sleep_sliced(stop, interval);
208 }
209}
210
211fn flush_lines(out: &Out, carry: &mut String, id: &str) -> bool {
213 while let Some(idx) = carry.find('\n') {
214 let line: String = carry.drain(..=idx).collect();
215 let line = line.trim_end_matches(['\r', '\n']);
216 if !send_line(out, line, id) {
217 return false;
218 }
219 }
220 if carry.len() > MAX_LINE {
221 let line = std::mem::take(carry);
222 return send_line(out, &line, id);
223 }
224 true
225}
226
227fn send_line(out: &Out, line: &str, id: &str) -> bool {
228 send_msg(out, &with_id(json!({"ev": "line", "data": line}), id)).is_ok()
229}