Krolyxon
Commit dbc22bf

Branches

Tags

Refs →

remagesrcconfigwatch.rs

143 lines · 4.4 KB · Rust

1//! Live reload: a background thread that reloads the config when a watched file changes, on
2//! SIGUSR1, or on request, and hands the result to the event loop.
3
4use std::collections::HashSet;
5use std::path::{Path, PathBuf};
6use std::time::Duration;
7
8use crossbeam_channel::{Receiver, RecvTimeoutError, Sender};
9use notify::event::{AccessKind, AccessMode};
10use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
11
12use super::{LoadError, Loaded, load};
13
14/// Quiet time before reloading, so editors' multi-step saves reload once.
15const DEBOUNCE: Duration = Duration::from_millis(100);
16
17enum Msg {
18 Fs(notify::Event),
19 Reload,
20}
21
22/// Handle to the reload thread. Dropping it stops nothing; the thread lives with the process.
23#[derive(Clone)]
24pub struct ConfigWatch {
25 tx: Sender<Msg>,
26}
27
28impl ConfigWatch {
29 /// Reload now (the `reload-config` action).
30 pub fn reload(&self) {
31 let _ = self.tx.send(Msg::Reload);
32 }
33}
34
35/// Start watching `watch` (from the initial load); `deliver` receives every reload result.
36pub fn spawn(
37 path: Option<PathBuf>,
38 watch: Vec<PathBuf>,
39 deliver: impl Fn(Result<Loaded, LoadError>) + Send + 'static,
40) -> ConfigWatch {
41 let (tx, rx) = crossbeam_channel::unbounded();
42 let fs_tx = tx.clone();
43 let sig_tx = tx.clone();
44 std::thread::Builder::new()
45 .name("remage-sigusr1".into())
46 .spawn(
47 move || match signal_hook::iterator::Signals::new([signal_hook::consts::SIGUSR1]) {
48 Ok(mut signals) => {
49 for _ in signals.forever() {
50 if sig_tx.send(Msg::Reload).is_err() {
51 break;
52 }
53 }
54 }
55 Err(e) => log::warn!("SIGUSR1 handler: {e}"),
56 },
57 )
58 .ok();
59 std::thread::Builder::new()
60 .name("remage-config".into())
61 .spawn(move || {
62 let watcher = notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
63 if let Ok(ev) = res {
64 let _ = fs_tx.send(Msg::Fs(ev));
65 }
66 });
67 let mut watcher = match watcher {
68 Ok(w) => Some(w),
69 Err(e) => {
70 log::warn!("config watcher: {e}");
71 None
72 }
73 };
74 run(path.as_deref(), watch, &rx, watcher.as_mut(), deliver);
75 })
76 .ok();
77 ConfigWatch { tx }
78}
79
80fn run(
81 path: Option<&Path>,
82 mut files: Vec<PathBuf>,
83 rx: &Receiver<Msg>,
84 mut watcher: Option<&mut RecommendedWatcher>,
85 deliver: impl Fn(Result<Loaded, LoadError>),
86) {
87 let mut dirs = HashSet::new();
88 if let Some(w) = watcher.as_deref_mut() {
89 watch_dirs(w, &mut dirs, &files);
90 }
91 while let Ok(msg) = rx.recv() {
92 if let Msg::Fs(ev) = &msg
93 && !relevant(ev, &files)
94 {
95 continue;
96 }
97 loop {
98 match rx.recv_timeout(DEBOUNCE) {
99 Ok(_) => continue,
100 Err(RecvTimeoutError::Timeout) => break,
101 Err(RecvTimeoutError::Disconnected) => return,
102 }
103 }
104 let result = load(path);
105 files = match &result {
106 Ok(l) => l.watch.clone(),
107 Err(e) => e.watch.clone(),
108 };
109 if let Some(w) = watcher.as_deref_mut() {
110 watch_dirs(w, &mut dirs, &files);
111 }
112 deliver(result);
113 }
114}
115
116/// Watch the parent directory of every file: editors replace files by renaming, which a
117/// watch on the file itself would lose.
118fn watch_dirs(w: &mut RecommendedWatcher, dirs: &mut HashSet<PathBuf>, files: &[PathBuf]) {
119 let wanted: HashSet<PathBuf> = files
120 .iter()
121 .filter_map(|f| f.parent())
122 .filter(|d| d.is_dir())
123 .map(Path::to_path_buf)
124 .collect();
125 for d in dirs.difference(&wanted) {
126 let _ = w.unwatch(d);
127 }
128 for d in wanted.difference(dirs) {
129 if let Err(e) = w.watch(d, RecursiveMode::NonRecursive) {
130 log::warn!("watch {}: {e}", d.display());
131 }
132 }
133 *dirs = wanted;
134}
135
136fn relevant(ev: &notify::Event, files: &[PathBuf]) -> bool {
137 let changes = match ev.kind {
138 EventKind::Access(AccessKind::Close(AccessMode::Write)) => true,
139 EventKind::Access(_) => false,
140 _ => true,
141 };
142 changes && ev.paths.iter().any(|p| files.iter().any(|f| f == p))
143}