Skip to main content

pfx_load/scene/
watch.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
6use std::time::{Duration, Instant};
7
8use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
9
10use super::{Scene, SceneDiff, SceneError};
11use crate::sha256;
12
13pub const DEBOUNCE: Duration = Duration::from_millis(100);
14
15#[derive(Clone, Debug)]
16pub struct Reload {
17    pub scene: Scene,
18    pub diff: SceneDiff,
19    pub recovered: bool,
20}
21
22pub struct SceneWatch {
23    path: PathBuf,
24    scene: Scene,
25    watcher: RecommendedWatcher,
26    events: Receiver<Instant>,
27    folders: BTreeSet<PathBuf>,
28    hashes: BTreeMap<PathBuf, Option<[u8; 32]>>,
29    pending: Option<Instant>,
30    debounce: Duration,
31    shared_debounce: Arc<AtomicU64>,
32    broken: Option<SceneError>,
33}
34
35fn counts(event: &notify::Result<notify::Event>) -> bool {
36    matches!(event, Ok(event) if !matches!(event.kind, EventKind::Access(_)))
37}
38
39fn nanos(debounce: Duration) -> u64 {
40    u64::try_from(debounce.as_nanos()).unwrap_or(u64::MAX)
41}
42
43fn waker(
44    signals: Receiver<()>,
45    debounce: Arc<AtomicU64>,
46    wake: Box<dyn Fn() + Send>,
47) -> std::io::Result<()> {
48    std::thread::Builder::new()
49        .name("scene watch waker".into())
50        .spawn(move || {
51            while signals.recv().is_ok() {
52                loop {
53                    let quiet = Duration::from_nanos(debounce.load(Ordering::Relaxed));
54                    match signals.recv_timeout(quiet) {
55                        Ok(()) => {}
56                        Err(RecvTimeoutError::Timeout) => break,
57                        Err(RecvTimeoutError::Disconnected) => return,
58                    }
59                }
60                wake();
61            }
62        })
63        .map(|_| ())
64}
65
66fn folder(file: &Path) -> PathBuf {
67    match file.parent() {
68        Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
69        _ => PathBuf::from("."),
70    }
71}
72
73fn hashes(files: impl IntoIterator<Item = PathBuf>) -> BTreeMap<PathBuf, Option<[u8; 32]>> {
74    files
75        .into_iter()
76        .map(|file| {
77            let hash = std::fs::read(&file).ok().map(|bytes| sha256(&bytes));
78            (file, hash)
79        })
80        .collect()
81}
82
83impl SceneWatch {
84    pub fn open(path: impl AsRef<Path>) -> Result<Self, SceneError> {
85        Self::start(path.as_ref(), None)
86    }
87
88    pub fn open_with(
89        path: impl AsRef<Path>,
90        wake: impl Fn() + Send + 'static,
91    ) -> Result<Self, SceneError> {
92        Self::start(path.as_ref(), Some(Box::new(wake)))
93    }
94
95    fn start(path: &Path, wake: Option<Box<dyn Fn() + Send>>) -> Result<Self, SceneError> {
96        let path = path.to_path_buf();
97        let scene = Scene::open(&path)?;
98        let (sender, events) = channel();
99        let shared_debounce = Arc::new(AtomicU64::new(nanos(DEBOUNCE)));
100        let signals: Option<Sender<()>> = match wake {
101            Some(wake) => {
102                let (signal, signals) = channel();
103                waker(signals, shared_debounce.clone(), wake).map_err(|error| {
104                    SceneError::new(&path, None, format!("cannot start the waker: {error}"))
105                })?;
106                Some(signal)
107            }
108            None => None,
109        };
110        let watcher = notify::recommended_watcher(move |event| {
111            if counts(&event) {
112                let _ = sender.send(Instant::now());
113                if let Some(signal) = &signals {
114                    let _ = signal.send(());
115                }
116            }
117        })
118        .map_err(|error| SceneError::new(&path, None, format!("cannot watch: {error}")))?;
119        let mut watch = Self {
120            hashes: scene
121                .files
122                .iter()
123                .map(|(file, hash)| (file.clone(), Some(*hash)))
124                .collect(),
125            path,
126            scene,
127            watcher,
128            events,
129            folders: BTreeSet::new(),
130            pending: None,
131            debounce: DEBOUNCE,
132            shared_debounce,
133            broken: None,
134        };
135        watch.follow()?;
136        Ok(watch)
137    }
138
139    pub fn path(&self) -> &Path {
140        &self.path
141    }
142
143    pub fn scene(&self) -> &Scene {
144        &self.scene
145    }
146
147    pub fn error(&self) -> Option<&SceneError> {
148        self.broken.as_ref()
149    }
150
151    pub fn set_debounce(&mut self, debounce: Duration) {
152        self.debounce = debounce;
153        self.shared_debounce
154            .store(nanos(debounce), Ordering::Relaxed);
155    }
156
157    fn drain(&mut self) {
158        while let Ok(at) = self.events.try_recv() {
159            self.pending = Some(self.pending.map_or(at, |seen| seen.max(at)));
160        }
161    }
162
163    pub fn pending(&mut self) -> Option<Instant> {
164        self.drain();
165        self.pending.map(|seen| seen + self.debounce)
166    }
167
168    pub fn files(&self) -> impl Iterator<Item = &Path> {
169        self.hashes.keys().map(PathBuf::as_path)
170    }
171
172    fn follow(&mut self) -> Result<(), SceneError> {
173        let wanted: BTreeSet<PathBuf> = self.hashes.keys().map(|file| folder(file)).collect();
174        for gone in self.folders.difference(&wanted) {
175            let _ = self.watcher.unwatch(gone);
176        }
177        for new in wanted.difference(&self.folders) {
178            self.watcher
179                .watch(new, RecursiveMode::NonRecursive)
180                .map_err(|error| {
181                    SceneError::new(
182                        &self.path,
183                        None,
184                        format!("cannot watch {}: {error}", new.display()),
185                    )
186                })?;
187        }
188        self.folders = wanted;
189        Ok(())
190    }
191
192    pub fn poll(&mut self) -> Option<Result<Reload, SceneError>> {
193        self.drain();
194        let since = self.pending?;
195        if since.elapsed() < self.debounce {
196            return None;
197        }
198        self.pending = None;
199        self.check()
200    }
201
202    pub fn check(&mut self) -> Option<Result<Reload, SceneError>> {
203        let now = hashes(self.hashes.keys().cloned());
204        if now == self.hashes {
205            return None;
206        }
207        match Scene::open(&self.path) {
208            Ok(scene) => {
209                let diff = self.scene.diff(&scene);
210                let recovered = self.broken.take().is_some();
211                self.hashes = scene
212                    .files
213                    .iter()
214                    .map(|(file, hash)| (file.clone(), Some(*hash)))
215                    .collect();
216                self.scene = scene.clone();
217                if let Err(error) = self.follow() {
218                    return Some(Err(error));
219                }
220                if diff.is_empty() && !recovered {
221                    return None;
222                }
223                Some(Ok(Reload {
224                    scene,
225                    diff,
226                    recovered,
227                }))
228            }
229            Err(error) => {
230                self.hashes = now;
231                self.broken = Some(error.clone());
232                Some(Err(error))
233            }
234        }
235    }
236}