Skip to main content

kinetic_config/
watcher.rs

1//! Hot-reload watcher for configuration files.
2
3use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
4use snafu::Snafu;
5use std::path::{Path, PathBuf};
6use tokio::sync::mpsc;
7use tracing::{error, info};
8
9#[derive(Debug, Snafu)]
10pub enum Error {
11    #[snafu(display("Failed to initialize file watcher: {}", source))]
12    WatcherInit { source: notify::Error },
13
14    #[snafu(display("Failed to watch path {}: {}", path.display(), source))]
15    WatchPath {
16        path: PathBuf,
17        source: notify::Error,
18    },
19}
20
21pub type Result<T, E = Error> = std::result::Result<T, E>;
22
23/// Watches a configuration file for changes and emits events when it's modified.
24pub struct ConfigWatcher {
25    _watcher: RecommendedWatcher,
26    pub rx: mpsc::Receiver<()>,
27}
28
29impl ConfigWatcher {
30    /// Starts watching the specified configuration file path.
31    /// Returns a `ConfigWatcher` which contains a receiver channel that yields
32    /// `()` every time the file is modified.
33    pub fn new(path: impl AsRef<Path>) -> Result<Self> {
34        let path = path.as_ref().to_path_buf();
35        let (tx, rx) = mpsc::channel(1);
36
37        let mut watcher =
38            notify::recommended_watcher(move |res: std::result::Result<Event, notify::Error>| {
39                match res {
40                    Ok(event) => {
41                        // Some editors write to a temp file and then rename/replace.
42                        // We listen for modify, create, and rename events.
43                        if event.kind.is_modify()
44                            || event.kind.is_create()
45                            || event.kind.is_access()
46                        {
47                            info!("Configuration file change detected: {:?}", event.kind);
48                            // Try to send signal. If the channel is full, we don't care
49                            // because one reload signal is enough.
50                            let _ = tx.try_send(());
51                        }
52                    }
53                    Err(e) => error!("Watch error: {:?}", e),
54                }
55            })
56            .map_err(|e| Error::WatcherInit { source: e })?;
57
58        watcher
59            .watch(&path, RecursiveMode::NonRecursive)
60            .map_err(|e| Error::WatchPath {
61                path: path.clone(),
62                source: e,
63            })?;
64
65        info!("Started watching config file: {}", path.display());
66
67        Ok(Self {
68            _watcher: watcher,
69            rx,
70        })
71    }
72}