diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-02-21 10:45:03 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-02-21 10:45:03 +0400 |
| commit | 9e60b344d06979ade51e49eaa766198e3133b909 (patch) | |
| tree | f57d794a14dcc23ff15967b75ec7b11faa584366 /src/watcher.rs | |
| parent | 2c01cde9beaacd8abf5f5b3117b9e006349bd633 (diff) | |
| download | niri-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.rs | 348 |
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(()) - }, - ); - } -} |
