aboutsummaryrefslogtreecommitdiff
path: root/src/watcher.rs
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-02-21 10:45:03 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-02-21 10:45:03 +0400
commit9e60b344d06979ade51e49eaa766198e3133b909 (patch)
treef57d794a14dcc23ff15967b75ec7b11faa584366 /src/watcher.rs
parent2c01cde9beaacd8abf5f5b3117b9e006349bd633 (diff)
downloadniri-9e60b344d06979ade51e49eaa766198e3133b909.tar.gz
niri-9e60b344d06979ade51e49eaa766198e3133b909.tar.bz2
niri-9e60b344d06979ade51e49eaa766198e3133b909.zip
Move watcher to utils
Diffstat (limited to 'src/watcher.rs')
-rw-r--r--src/watcher.rs348
1 files changed, 0 insertions, 348 deletions
diff --git a/src/watcher.rs b/src/watcher.rs
deleted file mode 100644
index 84fdab5c..00000000
--- a/src/watcher.rs
+++ /dev/null
@@ -1,348 +0,0 @@
-//! File modification watcher.
-
-use std::path::PathBuf;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{mpsc, Arc};
-use std::thread;
-use std::time::Duration;
-
-use smithay::reexports::calloop::channel::SyncSender;
-
-pub struct Watcher {
- should_stop: Arc<AtomicBool>,
-}
-
-impl Drop for Watcher {
- fn drop(&mut self) {
- self.should_stop.store(true, Ordering::SeqCst);
- }
-}
-
-impl Watcher {
- pub fn new(path: PathBuf, changed: SyncSender<()>) -> Self {
- Self::with_start_notification(path, changed, None)
- }
-
- pub fn with_start_notification(
- path: PathBuf,
- changed: SyncSender<()>,
- started: Option<mpsc::SyncSender<()>>,
- ) -> Self {
- let should_stop = Arc::new(AtomicBool::new(false));
-
- {
- let should_stop = should_stop.clone();
- thread::Builder::new()
- .name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
- .spawn(move || {
- // this "should" be as simple as mtime, but it does not quite work in practice;
- // it doesn't work if the config is a symlink, and its target changes but the
- // new target and old target have identical mtimes.
- //
- // in practice, this does not occur on any systems other than nix.
- // because, on nix practically everything is a symlink to /nix/store
- // and due to reproducibility, /nix/store keeps no mtime (= 1970-01-01)
- // so, symlink targets change frequently when mtime doesn't.
- let mut last_props = path
- .canonicalize()
- .and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
- .ok();
-
- if let Some(started) = started {
- let _ = started.send(());
- }
-
- loop {
- thread::sleep(Duration::from_millis(500));
-
- if should_stop.load(Ordering::SeqCst) {
- break;
- }
-
- if let Ok(new_props) = path
- .canonicalize()
- .and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
- {
- if last_props.as_ref() != Some(&new_props) {
- trace!("file changed: {}", path.to_string_lossy());
-
- if let Err(err) = changed.send(()) {
- warn!("error sending change notification: {err:?}");
- break;
- }
-
- last_props = Some(new_props);
- }
- }
- }
-
- debug!("exiting watcher thread for {}", path.to_string_lossy());
- })
- .unwrap();
- }
-
- Self { should_stop }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::error::Error;
- use std::fs::File;
- use std::io::Write;
- use std::sync::atomic::AtomicU8;
-
- use calloop::channel::sync_channel;
- use calloop::EventLoop;
- use smithay::reexports::rustix::fs::{futimens, Timestamps};
- use smithay::reexports::rustix::time::Timespec;
- use xshell::{cmd, Shell};
-
- use super::*;
-
- fn check(
- setup: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>,
- change: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>,
- ) {
- let sh = Shell::new().unwrap();
- let temp_dir = sh.create_temp_dir().unwrap();
- sh.change_dir(temp_dir.path());
- // let dir = sh.create_dir("xshell").unwrap();
- // sh.change_dir(dir);
-
- let mut config_path = sh.current_dir();
- config_path.push("niri");
- config_path.push("config.kdl");
-
- setup(&sh).unwrap();
-
- let changed = AtomicU8::new(0);
-
- let mut event_loop = EventLoop::try_new().unwrap();
- let loop_handle = event_loop.handle();
-
- let (tx, rx) = sync_channel(1);
- let (started_tx, started_rx) = mpsc::sync_channel(1);
- let _watcher = Watcher::with_start_notification(config_path.clone(), tx, Some(started_tx));
- loop_handle
- .insert_source(rx, |_, _, _| {
- changed.fetch_add(1, Ordering::SeqCst);
- })
- .unwrap();
- started_rx.recv().unwrap();
-
- // HACK: if we don't sleep, files might have the same mtime.
- thread::sleep(Duration::from_millis(100));
-
- change(&sh).unwrap();
-
- event_loop
- .dispatch(Duration::from_millis(750), &mut ())
- .unwrap();
-
- assert_eq!(changed.load(Ordering::SeqCst), 1);
-
- // Verify that the watcher didn't break.
- sh.write_file(&config_path, "c").unwrap();
-
- event_loop
- .dispatch(Duration::from_millis(750), &mut ())
- .unwrap();
-
- assert_eq!(changed.load(Ordering::SeqCst), 2);
- }
-
- #[test]
- fn change_file() {
- check(
- |sh| {
- sh.write_file("niri/config.kdl", "a")?;
- Ok(())
- },
- |sh| {
- sh.write_file("niri/config.kdl", "b")?;
- Ok(())
- },
- );
- }
-
- #[test]
- fn create_file() {
- check(
- |sh| {
- sh.create_dir("niri")?;
- Ok(())
- },
- |sh| {
- sh.write_file("niri/config.kdl", "a")?;
- Ok(())
- },
- );
- }
-
- #[test]
- fn create_dir_and_file() {
- check(
- |_sh| Ok(()),
- |sh| {
- sh.write_file("niri/config.kdl", "a")?;
- Ok(())
- },
- );
- }
-
- #[test]
- fn change_linked_file() {
- check(
- |sh| {
- sh.write_file("niri/config2.kdl", "a")?;
- cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?;
- Ok(())
- },
- |sh| {
- sh.write_file("niri/config2.kdl", "b")?;
- Ok(())
- },
- );
- }
-
- #[test]
- fn change_file_in_linked_dir() {
- check(
- |sh| {
- sh.write_file("niri2/config.kdl", "a")?;
- cmd!(sh, "ln -s niri2 niri").run()?;
- Ok(())
- },
- |sh| {
- sh.write_file("niri2/config.kdl", "b")?;
- Ok(())
- },
- );
- }
-
- #[test]
- fn recreate_file() {
- check(
- |sh| {
- sh.write_file("niri/config.kdl", "a")?;
- Ok(())
- },
- |sh| {
- sh.remove_path("niri/config.kdl")?;
- sh.write_file("niri/config.kdl", "b")?;
- Ok(())
- },
- );
- }
-
- #[test]
- fn recreate_dir() {
- check(
- |sh| {
- sh.write_file("niri/config.kdl", "a")?;
- Ok(())
- },
- |sh| {
- sh.remove_path("niri")?;
- sh.write_file("niri/config.kdl", "b")?;
- Ok(())
- },
- );
- }
-
- #[test]
- fn swap_dir() {
- check(
- |sh| {
- sh.write_file("niri/config.kdl", "a")?;
- Ok(())
- },
- |sh| {
- sh.write_file("niri2/config.kdl", "b")?;
- sh.remove_path("niri")?;
- cmd!(sh, "mv niri2 niri").run()?;
- Ok(())
- },
- );
- }
-
- #[test]
- fn swap_just_link() {
- // NixOS setup: link path changes, mtime stays constant.
- check(
- |sh| {
- let mut dir = sh.current_dir();
- dir.push("niri");
- sh.create_dir(&dir)?;
-
- let mut d2 = dir.clone();
- d2.push("config2.kdl");
- let mut c2 = File::create(d2).unwrap();
- write!(c2, "a")?;
- c2.flush()?;
- futimens(
- &c2,
- &Timestamps {
- last_access: Timespec {
- tv_sec: 0,
- tv_nsec: 0,
- },
- last_modification: Timespec {
- tv_sec: 0,
- tv_nsec: 0,
- },
- },
- )?;
- c2.sync_all()?;
- drop(c2);
-
- let mut d3 = dir.clone();
- d3.push("config3.kdl");
- let mut c3 = File::create(d3).unwrap();
- write!(c3, "b")?;
- c3.flush()?;
- futimens(
- &c3,
- &Timestamps {
- last_access: Timespec {
- tv_sec: 0,
- tv_nsec: 0,
- },
- last_modification: Timespec {
- tv_sec: 0,
- tv_nsec: 0,
- },
- },
- )?;
- c3.sync_all()?;
- drop(c3);
-
- cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?;
- Ok(())
- },
- |sh| {
- cmd!(sh, "unlink niri/config.kdl").run()?;
- cmd!(sh, "ln -s config3.kdl niri/config.kdl").run()?;
- Ok(())
- },
- );
- }
-
- #[test]
- fn swap_dir_link() {
- check(
- |sh| {
- sh.write_file("niri2/config.kdl", "a")?;
- cmd!(sh, "ln -s niri2 niri").run()?;
- Ok(())
- },
- |sh| {
- sh.write_file("niri3/config.kdl", "b")?;
- cmd!(sh, "unlink niri").run()?;
- cmd!(sh, "ln -s niri3 niri").run()?;
- Ok(())
- },
- );
- }
-}