From 9e60b344d06979ade51e49eaa766198e3133b909 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Wed, 21 Feb 2024 10:45:03 +0400 Subject: Move watcher to utils --- src/lib.rs | 1 - src/main.rs | 2 +- src/utils/mod.rs | 1 + src/utils/watcher.rs | 348 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/watcher.rs | 348 --------------------------------------------------- 5 files changed, 350 insertions(+), 350 deletions(-) create mode 100644 src/utils/watcher.rs delete mode 100644 src/watcher.rs (limited to 'src') diff --git a/src/lib.rs b/src/lib.rs index b332dc5e..68bcbc36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,6 @@ pub mod protocols; pub mod render_helpers; pub mod screenshot_ui; pub mod utils; -pub mod watcher; #[cfg(not(feature = "xdp-gnome-screencast"))] pub mod dummy_pw_utils; diff --git a/src/main.rs b/src/main.rs index ce1208c5..5f475993 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,8 +16,8 @@ use niri::dbus; use niri::ipc::client::handle_msg; use niri::niri::State; use niri::utils::spawning::{spawn, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE}; +use niri::utils::watcher::Watcher; use niri::utils::{cause_panic, version, IS_SYSTEMD_SERVICE}; -use niri::watcher::Watcher; use niri_config::Config; use portable_atomic::Ordering; use sd_notify::NotifyState; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index e273fb72..e83378ae 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -15,6 +15,7 @@ use smithay::reexports::rustix::time::{clock_gettime, ClockId}; use smithay::utils::{Logical, Point, Rectangle, Size}; pub mod spawning; +pub mod watcher; pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false); diff --git a/src/utils/watcher.rs b/src/utils/watcher.rs new file mode 100644 index 00000000..84fdab5c --- /dev/null +++ b/src/utils/watcher.rs @@ -0,0 +1,348 @@ +//! 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, +} + +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>, + ) -> 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>, + change: impl FnOnce(&Shell) -> Result<(), Box>, + ) { + 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(()) + }, + ); + } +} 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, -} - -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>, - ) -> 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>, - change: impl FnOnce(&Shell) -> Result<(), Box>, - ) { - 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(()) - }, - ); - } -} -- cgit