diff --git a/Cargo.lock b/Cargo.lock index 2dca5ff2c7..bab9f8f5b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -886,21 +886,11 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "fsevent" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" -dependencies = [ - "bitflags", - "fsevent-sys", -] - [[package]] name = "fsevent-sys" -version = "2.0.1" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" dependencies = [ "libc", ] @@ -1480,6 +1470,7 @@ dependencies = [ "log 0.4.17", "log4rs", "mio 0.8.5", + "multimap", "nix 0.26.1", "notify", "num_cpus", @@ -1720,9 +1711,9 @@ dependencies = [ [[package]] name = "inotify" -version = "0.7.1" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" dependencies = [ "bitflags", "inotify-sys", @@ -1840,6 +1831,26 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "kqueue" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6112e8f37b59803ac47a42d14f1f3a59bbf72fc6857ffc5be455e28a691f8e" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1858,12 +1869,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.138" @@ -2110,18 +2115,6 @@ dependencies = [ "windows-sys 0.42.0", ] -[[package]] -name = "mio-extras" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" -dependencies = [ - "lazycell", - "log 0.4.17", - "mio 0.6.23", - "slab", -] - [[package]] name = "miow" version = "0.2.2" @@ -2148,6 +2141,9 @@ name = "multimap" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +dependencies = [ + "serde", +] [[package]] name = "native-tls" @@ -2216,18 +2212,18 @@ dependencies = [ [[package]] name = "notify" -version = "4.0.17" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257" +checksum = "ed2c66da08abae1c024c01d635253e402341b4060a12e99b31c7594063bf490a" dependencies = [ "bitflags", + "crossbeam-channel 0.5.1", "filetime", - "fsevent", "fsevent-sys", "inotify", + "kqueue", "libc", - "mio 0.6.23", - "mio-extras", + "mio 0.8.5", "walkdir", "winapi 0.3.9", ] diff --git a/components/sup/Cargo.toml b/components/sup/Cargo.toml index bde640559f..d12d3a4af0 100644 --- a/components/sup/Cargo.toml +++ b/components/sup/Cargo.toml @@ -38,6 +38,7 @@ lazy_static = "*" libc = "*" log = "*" log4rs = "*" +multimap = "*" notify = "*" num_cpus = "*" parking_lot = "*" diff --git a/components/sup/src/manager.rs b/components/sup/src/manager.rs index efa24832a4..6a1f3f35a6 100644 --- a/components/sup/src/manager.rs +++ b/components/sup/src/manager.rs @@ -1,11 +1,9 @@ pub(crate) mod action; -pub mod service; -#[macro_use] -mod debug; pub mod commands; mod file_watcher; mod peer_watcher; mod self_updater; +pub mod service; mod service_updater; mod spec_dir; mod spec_watcher; diff --git a/components/sup/src/manager/debug.rs b/components/sup/src/manager/debug.rs deleted file mode 100644 index 169cdc6770..0000000000 --- a/components/sup/src/manager/debug.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::{collections::{HashMap, - HashSet}, - ffi::OsString, - fmt::{Debug, - Write}, - path::PathBuf}; - -pub trait IndentedToString { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String; -} - -// indented to string -macro_rules! its( - {$value:expr, $spaces:expr, $repeat:expr} => { - { - $value.indented_to_string($spaces, $repeat) - } - }; -); - -// default indented to string -macro_rules! dits( - {$value:expr} => { - its!($value, " ", 0) - }; -); - -pub struct IndentedStructFormatter { - name: String, - fields: Vec<(String, String)>, - spaces: String, - repeat: usize, -} - -impl IndentedStructFormatter { - pub fn new(name: &str, spaces: &str, repeat: usize) -> Self { - Self { name: name.to_string(), - fields: Vec::new(), - spaces: spaces.to_string(), - repeat } - } - - pub fn add_string(&mut self, field_name: &str, field_value: String) { - self.fields.push((field_name.to_string(), field_value)); - } - - pub fn add_debug(&mut self, field_name: &str, field_value: &T) { - self.add_string(field_name, format!("{:?}", field_value)); - } - - pub fn add(&mut self, field_name: &str, field_value: &T) { - let spaces = self.spaces.to_string(); - let repeat = self.repeat + 1; - self.add_string(field_name, its!(field_value, &spaces, repeat)); - } - - pub fn fmt(&mut self) -> String { - let indent = self.spaces.repeat(self.repeat); - let field_indent = self.spaces.repeat(self.repeat + 1); - // 5 - space between name and opening brace, opening brace, newline - // after opening brace, closing brace, terminating zero - let mut capacity = self.name.len() + 5 + indent.len(); - for pair in &self.fields { - // 4 - colon after name, space, comma, newline after value - capacity += field_indent.len() + pair.0.len() + 4 + pair.1.len(); - } - let mut str = String::with_capacity(capacity); - let _ = writeln!(str, "{} {{", self.name,); - for pair in &self.fields { - let _ = writeln!(str, "{}{}: {},", field_indent, pair.0, pair.1,); - } - let _ = write!(str, "{}}}", indent); - str - } -} - -impl IndentedToString for u32 { - fn indented_to_string(&self, _: &str, _: usize) -> String { self.to_string() } -} - -impl IndentedToString for PathBuf { - fn indented_to_string(&self, _: &str, _: usize) -> String { self.display().to_string() } -} - -impl IndentedToString for OsString { - fn indented_to_string(&self, _: &str, _: usize) -> String { self.to_string_lossy().to_string() } -} - -impl IndentedToString for Option { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - match self { - Some(ref v) => format!("Some({})", its!(v, spaces, repeat)), - None => "None".to_string(), - } - } -} - -impl IndentedToString for HashMap { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - let mut paths = self.keys().collect::>(); - paths.sort(); - let indent = spaces.repeat(repeat + 1); - let mut str = String::new(); - str.push_str("{\n"); - for path in paths { - let _ = writeln!(str, - "{}{}: {},", - indent, - path.display(), - its!(self.get(path).unwrap(), spaces, repeat + 1),); - } - let _ = write!(str, "{}}}", spaces.repeat(repeat)); - str - } -} - -impl IndentedToString for HashSet { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - let mut paths = self.iter().collect::>(); - paths.sort(); - let indent = spaces.repeat(repeat + 1); - let mut str = String::new(); - str.push_str("{\n"); - for path in paths { - let _ = writeln!(str, "{}{},", indent, path.display(),); - } - let _ = write!(str, "{}}}", spaces.repeat(repeat)); - str - } -} diff --git a/components/sup/src/manager/file_watcher.rs b/components/sup/src/manager/file_watcher.rs index 12a1e214d2..d145b6e716 100644 --- a/components/sup/src/manager/file_watcher.rs +++ b/components/sup/src/manager/file_watcher.rs @@ -1,22 +1,27 @@ -use super::sup_watcher::SupWatcher; -use crate::{error::{Error, - Result}, - manager::debug::{IndentedStructFormatter, - IndentedToString}}; +use crate::error::{Error, + Result}; use habitat_common::liveliness_checker; use log::{debug, - error}; + trace}; use notify::{self, - DebouncedEvent, + event::{AccessKind, + AccessMode, + CreateKind, + DataChange, + EventKind, + MetadataKind, + ModifyKind, + RemoveKind, + RenameMode}, + Config, + Event, RecursiveMode, Watcher}; -use std::{collections::{hash_map::Entry, - HashMap, - HashSet, - VecDeque}, - env, - ffi::OsString, - mem::swap, + +use std::{cell::RefCell, + collections::HashSet, + fmt::Debug, + os::unix::prelude::OsStrExt, path::{Component, Path, PathBuf}, @@ -26,3439 +31,1461 @@ use std::{collections::{hash_map::Entry, thread, time::Duration}; -pub const WATCHER_DELAY_MS: u64 = 2_000; - -/// A set of callbacks for the watched file events. -pub trait Callbacks { - /// A function that gets called when the watched file shows up. - /// - /// `real_path` will contain a path to the watched file, which may - /// be different from the one that was passed to `FileWatcher`, - /// because of symlinks. - fn file_appeared(&mut self, real_path: &Path); - /// A function that gets called when the watched file is written to. - /// - /// Note that this is called only when the real file is - /// modified. In case when some symlink in the watched paths is - /// atomically changed to point to something else, - /// `file_disappeared` followed by `file_appeared` will be - /// actually called. - /// - /// `real_path` will contain a path to the watched file, which may - /// be different from the one that was passed to `FileWatcher`, - /// because of symlinks. - fn file_modified(&mut self, real_path: &Path); - /// A function that gets called when the watched file goes away. - /// - /// `real_path` will contain a path to the watched file, which may - /// be different from the one that was passed to `FileWatcher`, - /// because of symlinks. - fn file_disappeared(&mut self, real_path: &Path); - /// A function that gets called every time an event with paths - /// happens. - /// - /// `paths` contains a list of paths the recent event is related - /// to. Usually it will be just one, but in case of renames it may - /// be two. - // Keep the variable name for documentation purposes, so silence - // compiler's complaints about unused variable. - #[allow(unused_variables)] - fn event_in_directories(&mut self, paths: &[PathBuf]) {} -} - -// Essentially a pair of dirname and basename. -#[derive(Clone, Debug, Default)] -struct DirFileName { - directory: PathBuf, - file_name: OsString, -} - -impl DirFileName { - // split_path separates the dirname from the basename. - fn split_path(path: &Path) -> Option { - let parent = match path.parent() { - None => return None, - Some(p) => p, - }; - let file_name = match path.file_name() { - None => return None, - Some(f) => f, - }; - Some(Self { directory: parent.to_owned(), - file_name: file_name.to_owned(), }) - } - - fn as_path(&self) -> PathBuf { self.directory.join(&self.file_name) } -} - -impl IndentedToString for DirFileName { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - let mut formatter = IndentedStructFormatter::new("DirFileName", spaces, repeat); - formatter.add("directory", &self.directory); - formatter.add("file_name", &self.file_name); - formatter.fmt() - } -} - -// TODO: handle mount events, we could use libc crate to get the -// select function that we could use to watch /proc/self/mountinfo for -// exceptional events - such event means that something was mounted or -// unmounted. For this to work, we would need to keep a mount state of -// the directories we are interested in and compare it to the current -// status in mountinfo, when some change there happens. - -// Similar to DirFileName, but the file_name part is optional. -struct SplitPath { - directory: PathBuf, - file_name: Option, -} - -impl SplitPath { - fn push(&mut self, path: OsString) -> DirFileName { - if let Some(ref file_name) = self.file_name { - self.directory.push(file_name); - } - self.file_name = Some(path.clone()); - - DirFileName { directory: self.directory.clone(), - file_name: path, } - } -} - -impl IndentedToString for SplitPath { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - let mut formatter = IndentedStructFormatter::new("SplitPath", spaces, repeat); - formatter.add("directory", &self.directory); - formatter.add("file_name", &self.file_name); - formatter.fmt() - } -} - -// Contents of this type are for `process_path`. -#[derive(Debug)] -struct ProcessPathArgs { - // The beginning of the patch to process (very often it is root, - // but not always). - path: PathBuf, - // The rest of the path as components ([habitat-operator, peers]) - path_rest: VecDeque, - // Describes the position in the chain, used for determining from - // where we should start reprocessing the path in case of some - // events. - // - // What is called "chain" here is a list of items we end up - // watching. - // - // TODO(krnowak): Check if we can just remove it. - index: u32, - // Previous path in chain, usually a parent directory, but in case - // of symlinks it becomes a bit more complicated. - // - // Item: directory, file or symlink. - prev: Option, -} - -impl IndentedToString for ProcessPathArgs { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - let mut formatter = IndentedStructFormatter::new("ProcessPathArgs", spaces, repeat); - formatter.add("path", &self.path); - formatter.add_debug("path_rest", &self.path_rest); - formatter.add_string("index", self.index.to_string()); - formatter.add("prev", &self.prev); - formatter.fmt() - } -} - -// This struct tells that for `path` the previous item in chain is -// `prev`. -struct ChainLinkInfo { - path: PathBuf, - prev: Option, -} +use super::sup_watcher::SupWatcher; -impl IndentedToString for ChainLinkInfo { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - let mut formatter = IndentedStructFormatter::new("ChainLinkInfo", spaces, repeat); - formatter.add("path", &self.path); - formatter.add("prev", &self.prev); - formatter.fmt() - } -} +pub const WATCHER_DELAY_MS: u64 = 2_000; -// This struct is passed together with some event actions. -#[derive(Debug)] -struct PathsActionData { - dir_file_name: DirFileName, - args: ProcessPathArgs, +#[derive(Clone, Debug, PartialEq)] +/// WatchedFileState is used to indicate first if a WatchedFile is valid or not. The Invalid +/// variant signals that a file is Invalid and contains a string that provides the reasoning. All +/// other variants are understood to be "valid paths" to a file where valid is understood to mean +/// that to the best of the codes ability the path looks like it name a file that could exist. +/// Beyond "validity" that the other variants indicate that +/// * the file, and thus its enclosing directory, exist on the file system +/// * the file does not exist the file system but enclosing directory does exist +/// * neither the file nor its enclosing directory exists +enum WatchedFileState { + /// Indicates that both the file and enclosing directory exists and are visible. + ExistentFile, + /// Indicates that while the file does not exist on the file system the enclosing directory + /// does exist and since we've passed the watchability checks we should be able to trust that + /// the path is valid. What's tricky about this state is that you have to remember even though + /// we are concerned with watching files (existent or nonexistent) the details of using the + /// notify crate well means that we might sometime need to watch the enclosing directory we're + /// still watching files so the path should indicate a single file that we are watching to see + /// if it comes into existence. + ExistentDirectory, + /// Indicates that neither the file nor the enclosing directory exists but to the best of our + /// ability we believe that the path is valid for the system and indicate a file that could + /// exist on the platform's file system. This also implies that the path of the enclosing + /// directory would be valid path if it were to exist. + NonExistent, + /// Invalid indicates that the path is something that we have determined cannot indicate a file + /// on the system. The path may be wholly malformed or it might indicate a directory instead of + /// a file. The associated String will contain an explanation of why the path is valid + Invalid(String), + // I feel as though I should be making a Schrodinger's WatchedFile joke here... or not. } -impl IndentedToString for PathsActionData { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - let mut formatter = IndentedStructFormatter::new("PathsActionData", spaces, repeat); - formatter.add("dir_file_name", &self.dir_file_name); - formatter.add("args", &self.args); - formatter.fmt() +mod errmsg { + use super::*; + pub fn invalid_ending(path: &Path) -> String { + format!("{:?} appears invalid because it ends with {:?} thus appearing to be a directory \ + instead of a file", + path, + std::path::MAIN_SEPARATOR) } -} - -// This is stores information about the watched item -// -// TODO(krnowak): Rename it. -struct Common { - // TODO: maybe drop this? we could also use dir_file_name.as_path() - path: PathBuf, - dir_file_name: DirFileName, - // Previous watched item in chain, is None for the first watched - // item. - prev: Option, - // Next watched item in chain, is None for the last watched item. - next: Option, - // That is needed to make sure that the generated process args - // with lower index will overwrite the generated process args with - // higher index. Several generated process args can be generated - // when several files or directories are removed. - // - // TODO(krnowak): Not sure if we need it anymore, since we have a - // simple chain of watches, so the most recent removal event - // should happen for the element in chain with lowest index. - index: u32, - // This is the rest of the components that were left to process at - // the moment we were processing this path. Useful for - // reprocessing the path, when the next item in the list was - // removed/replaced. - path_rest: VecDeque, -} - -impl Common { - fn get_process_path_args(&self) -> ProcessPathArgs { - let mut path_rest = VecDeque::new(); - path_rest.push_back(self.dir_file_name.file_name.clone()); - path_rest.extend(self.path_rest.iter().cloned()); - ProcessPathArgs { path: self.dir_file_name.directory.clone(), - path_rest, - index: self.index, - prev: self.prev.clone() } + pub fn invalid_root(path: &Path) -> String { + format!("{:?} appears invalid as the root is different than expected for the platform \ + we're running on.", + path) } - - fn get_chain_link_info(&self) -> ChainLinkInfo { - ChainLinkInfo { path: self.path.clone(), - prev: self.prev.clone(), } + pub fn is_directory(path: &Path) -> String { + format!("{:?} is a directory instead of a file.", path) } - - fn get_paths_action_data(&self) -> PathsActionData { - PathsActionData { dir_file_name: self.dir_file_name.clone(), - args: self.get_process_path_args(), } + pub fn file_not_found(path: &Path) -> String { + format!("Neither File nor Enclosing Directory Found: {:?}", path) } - - // The path is at the end of the chain, which means we expect it - // to be a file. This is because we always expect the watcher to - // be started on a file. - fn is_leaf(&self) -> bool { self.path_rest.is_empty() } } -impl IndentedToString for Common { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - let mut formatter = IndentedStructFormatter::new("Common", spaces, repeat); - formatter.add("path", &self.path); - formatter.add("dir_file_name", &self.dir_file_name); - formatter.add("prev", &self.prev); - formatter.add("next", &self.next); - formatter.add_string("index", self.index.to_string()); - formatter.add_debug("path_rest", &self.path_rest); - formatter.fmt() - } -} - -// This is only used to generate `Common` for each item we have in the -// path, so for `/h-o/peers`, it will generate `Common` instance for -// `/h-o` and then for `/h-o/peers` subsequently. -// -// TODO(krnowak): Rename it. -struct CommonGenerator { - // Usually describes what was the last item we processed. Bit more - // complicated in case of symlinks. - prev: Option, - // Normally `prev` is just a parent directory (previous processed - // item), but this is not the case when we are dealing with - // symlinks. `old_prev` is used then to override `prev` to the - // previous processed item on next iteration. - old_prev: Option, - // The path for the generated `Common`. - // - // TODO(krnowak): drop it? we likely have this information stored - // in split_path, we could add a as_path() function to SplitPath. - path: PathBuf, - // Same as path, but splitted into dirname and basename - split_path: SplitPath, - // Index in the chain. - // - // TODO(krnowak): Check if this can be dropped. - index: u32, - // The rest of the path as components to be processed. - path_rest: VecDeque, -} - -impl CommonGenerator { - fn new(args: ProcessPathArgs) -> Self { - let split_path = SplitPath { directory: args.path.clone(), - file_name: None, }; - let s = Self { prev: args.prev, - old_prev: None, - path: args.path, - split_path, - index: args.index, - path_rest: args.path_rest }; - debug!("common generator created: {}", dits!(s)); - s - } - - fn revert_previous(&mut self) { self.prev = self.old_prev.clone(); } - - fn set_path(&mut self, path: PathBuf) { - self.path = path; - self.split_path = SplitPath { directory: self.path.clone(), - file_name: None, }; - debug!("new path in generator: {:?}", self.path); - } - - fn prepend_to_path_rest(&mut self, mut path_rest: VecDeque) { - path_rest.extend(self.path_rest.drain(..)); - self.path_rest = path_rest; - debug!("new path rest in generator: {:?}", self.path_rest); - } - - // Extract a new component from the `path_rest` vec and create a - // new `Common` instance. If there are not components left in - // `path_rest`, returns `None`. - fn get_new_common(&mut self) -> Option { - debug!("common generator before new Common: {}", dits!(self)); - let c = if let Some(component) = self.path_rest.pop_front() { - self.path.push(&component); - let path = self.path.clone(); - let dir_file_name = self.split_path.push(component); - let prev = self.prev.clone(); - - // This is only used for symlinks. We want to make sure - // that the previous item for the symlink's target is - // either the symlink or target's parent directory if we - // didn't watch the directory before. - // - // An example: we watch `/a/b/c`, `c` is a symlink to - // `/a/x/c`, so after processing `/a/b/c` we want to have - // a chain like `/a, `/a/b`, `/a/x`, `/a/x/c`. - // - // So we use `old_prev` to make sure that the proper - // previous item in chain will be set for the paths coming - // after following the symlink. - swap(&mut self.old_prev, &mut self.prev); - self.prev = Some(self.path.clone()); - - let index = self.index; - self.index += 1; - Some(Common { path, - dir_file_name, - prev, - next: None, - index, - path_rest: self.path_rest.clone() }) - } else { - None - }; - debug!("generated common: {}", dits!(c)); - debug!("common generator after new Common: {}", dits!(self)); - c - } -} - -impl IndentedToString for CommonGenerator { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - let mut formatter = IndentedStructFormatter::new("CommonGenerator", spaces, repeat); - formatter.add("prev", &self.prev); - formatter.add("old_prev", &self.old_prev); - formatter.add("path", &self.path); - formatter.add("split_path", &self.split_path); - formatter.add_string("index", self.index.to_string()); - formatter.add_debug("path_rest", &self.path_rest); - formatter.fmt() - } -} - -// An item we are interested in. -enum WatchedFile { - Regular(Common), - MissingRegular(Common), - Symlink(Common), - Directory(Common), - MissingDirectory(Common), +#[derive(Clone, Debug)] +/// WatchedFile is a struct bundling the essential data together for a file that +/// is to be watched. It may or may not exist when file begins to be watched +/// and it may come into or go out of existence while the watch is in effect. +struct WatchedFile { + /// Owned PathBuf that is the WatchedFile's path + path: PathBuf, + /// This is the enclosing directory broken out for easier reference + directory: PathBuf, + /// Used to signal if an initial Callbacks::file_appeared should be sent + /// when creating the watched file. The file must exist on the file system + /// and this must be set true. After the first execution this value will + /// set back to false as the intial true has fulfilled its purpose. + send_initial_event: bool, + /// trackes the WatchedFileState of the WatchedFile + state: WatchedFileState, } impl WatchedFile { - fn get_common(&self) -> &Common { - match self { - WatchedFile::Regular(ref c) - | WatchedFile::MissingRegular(ref c) - | WatchedFile::Symlink(ref c) - | WatchedFile::Directory(ref c) - | WatchedFile::MissingDirectory(ref c) => c, - } - } - - fn get_mut_common(&mut self) -> &mut Common { - match self { - WatchedFile::Regular(ref mut c) - | WatchedFile::MissingRegular(ref mut c) - | WatchedFile::Symlink(ref mut c) - | WatchedFile::Directory(ref mut c) - | WatchedFile::MissingDirectory(ref mut c) => c, - } - } - - fn steal_common(self) -> Common { - match self { - WatchedFile::Regular(c) - | WatchedFile::MissingRegular(c) - | WatchedFile::Symlink(c) - | WatchedFile::Directory(c) - | WatchedFile::MissingDirectory(c) => c, - } - } -} - -impl IndentedToString for WatchedFile { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - let name = match self { - WatchedFile::Regular(_) => "Regular", - WatchedFile::MissingRegular(_) => "MissingRegular", - WatchedFile::Symlink(_) => "Symlink", - WatchedFile::Directory(_) => "Directory", - WatchedFile::MissingDirectory(_) => "MissingDirectory", - }; - format!("{}({})", name, its!(self.get_common(), spaces, repeat)) - } -} - -// Similar to std::fs::canonicalize, but without resolving symlinks. -// -// I'm not sure if this is entirely correct, consider: -// -// pwd # displays /some/abs/path -// mkdir -p foo/bar -// ln -s foo/bar baz -// realpath baz/.. # displays /some/abs/path/foo -// cd baz/.. # stays in the same directory instead of going to foo -// -// Basically, realpath says that "baz/.." == "foo" and cd says that -// "baz/.." == ".". -// -// I went here with the "cd" way. Likely less surprising. -fn simplify_abs_path(abs_path: &Path) -> PathBuf { - let mut simple = PathBuf::new(); - for c in abs_path.components() { - match c { - Component::CurDir => (), - Component::ParentDir => { - simple.pop(); + fn new

(path: P, send_initial_event: bool) -> Result + where P: AsRef + Debug + { + trace!("WatchedFile::new({:?},{:?})", path, send_initial_event); + let path = path.as_ref(); + let state = Self::discern_state_of(path); + let enclosing_directory = match state { + WatchedFileState::ExistentFile + | WatchedFileState::ExistentDirectory + | WatchedFileState::NonExistent => Self::get_enclosing_directory(path), + WatchedFileState::Invalid(_) => { + let s = path.as_os_str().to_string_lossy().to_string(); + return Err(Error::FileNotFound(s)); } - _ => simple.push(c.as_os_str()), }; - } - simple -} - -// `EventAction`s are high-level actions to be performed in response to -// filesystem events. -// -// We translate `DebouncedEvent` to `EventAction`, and `EventAction` -// to a list of `PathsAction`s. -#[derive(Debug)] -enum EventAction { - Ignore, - PlainChange(PathBuf), - RestartWatching, - AddRegular(PathsActionData), - DropRegular(PathsActionData), - AddDirectory(PathsActionData), - DropDirectory(PathsActionData), - RewireSymlink(PathsActionData), - DropSymlink(PathsActionData), - SettlePath(PathBuf), -} - -impl IndentedToString for EventAction { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - match self { - EventAction::Ignore => "Ignore".to_string(), - EventAction::PlainChange(ref p) => format!("PlainChange({})", its!(p, spaces, repeat)), - EventAction::RestartWatching => "RestartWatching".to_string(), - EventAction::AddRegular(ref pad) => { - format!("AddRegular({})", its!(pad, spaces, repeat)) - } - EventAction::DropRegular(ref pad) => { - format!("DropRegular({})", its!(pad, spaces, repeat)) - } - EventAction::AddDirectory(ref pad) => { - format!("AddDirectory({})", its!(pad, spaces, repeat)) - } - EventAction::DropDirectory(ref pad) => { - format!("DropDirectory({})", its!(pad, spaces, repeat)) - } - EventAction::RewireSymlink(ref pad) => { - format!("RewireSymlink({})", its!(pad, spaces, repeat)) - } - EventAction::DropSymlink(ref pad) => { - format!("DropSymlink({})", its!(pad, spaces, repeat)) - } - EventAction::SettlePath(ref p) => format!("SettlePath({})", its!(p, spaces, repeat)), - } - } -} - -// Lower-level actions, created to execute `EventAction`s. -#[derive(Debug)] -enum PathsAction { - NotifyFileAppeared(PathBuf), - NotifyFileModified(PathBuf), - NotifyFileDisappeared(PathBuf), - DropWatch(PathBuf), - AddPathToSettle(PathBuf), - SettlePath(PathBuf), - ProcessPathAfterSettle(ProcessPathArgs), - RestartWatching, -} - -impl IndentedToString for PathsAction { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - match self { - PathsAction::NotifyFileAppeared(ref p) => { - format!("NotifyFileAppeared({})", its!(p, spaces, repeat + 1)) - } - PathsAction::NotifyFileModified(ref p) => { - format!("NotifyFileModified({})", its!(p, spaces, repeat + 1)) - } - PathsAction::NotifyFileDisappeared(ref p) => { - format!("NotifyFileDisappeared({})", its!(p, spaces, repeat + 1)) - } - PathsAction::DropWatch(ref p) => format!("DropWatch({})", its!(p, spaces, repeat + 1)), - PathsAction::AddPathToSettle(ref p) => { - format!("AddPathToSettle({})", its!(p, spaces, repeat + 1)) - } - PathsAction::SettlePath(ref p) => { - format!("SettlePath({})", its!(p, spaces, repeat + 1)) - } - PathsAction::ProcessPathAfterSettle(ref a) => { - format!("ProcessPathAfterSettle({})", its!(a, spaces, repeat + 1)) - } - PathsAction::RestartWatching => "RestartWatching".to_string(), - } - } -} - -// Both branch result and leaf result are about the status of adding -// new path to be watched. Branch is about symlinks and directories, -// leaves - about regular files, missing regular files and missing -// directories. -// -// TODO(asymmetric): This could be renamed to `BranchStatus`. -enum BranchResult { - // The path already existed - may happen when dealing with - // symlinks in the path. - AlreadyExists, - // New path in a known directory. - NewInOldDirectory(ChainLinkInfo), - // New path in an unknown directory. - NewInNewDirectory(ChainLinkInfo, PathBuf), -} - -impl IndentedToString for BranchResult { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - match self { - BranchResult::AlreadyExists => "AlreadyExists".to_string(), - BranchResult::NewInOldDirectory(ref i) => { - format!("NewInOldDirectory({})", its!(i, spaces, repeat)) - } - BranchResult::NewInNewDirectory(ref i, ref p) => { - format!("NewInNewDirectory({}, {})", - its!(i, spaces, repeat), - p.to_string_lossy()) + let directory = match enclosing_directory { + Some(s) => s, + None => { + let s = path.as_os_str().to_string_lossy().to_string(); + return Err(Error::FileNotFound(s)); } - } - } -} - -// See `BranchResult`. -enum LeafResult { - // New path in a known directory. - NewInOldDirectory(ChainLinkInfo), - // New path in an unknown directory. - NewInNewDirectory(ChainLinkInfo, PathBuf), -} - -impl IndentedToString for LeafResult { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - match self { - LeafResult::NewInOldDirectory(ref i) => { - format!("NewInOldDirectory({})", its!(i, spaces, repeat)) - } - LeafResult::NewInNewDirectory(ref i, ref p) => { - format!("NewInNewDirectory({}, {})", - its!(i, spaces, repeat), - p.to_string_lossy()) - } - } - } -} - -// Used when we settle a path, so we know if we processed a path -// because all the paths were already settled, or not if there were -// still some left to settle. -enum ProcessPathStatus { - // Holds a vector of new directories to watch (a result of - // `process_path` function) - Executed(Vec), - NotExecuted(ProcessPathArgs), -} - -// Paths holds the state with regards to watching. -struct Paths { - // A map of paths to file info of items. If something happens to - // them, we react. - paths: HashMap, - // A map of directories to use count we actually watch with the - // OS-specific watcher. These are parent directories of the items - // in `paths`. Use count can be greater than 1 in case of - // symlinks, for example when we watch `/a`, which is a symlink - // that points to `b`, so we end up with two paths (`/a` and - // `/b`), but only one directory (`/`) to watch with use count - // being two. - dirs: HashMap, - // A path we are interested in. - start_path: PathBuf, - // A map used to detect symlink loops. + }; + debug!("WatchedFile::path = {:?}", path); + debug!("WatchedFile::state = {:?}", state); + debug!("WatchedFile::directory= {:?}", directory); + debug!("WatchedFile::send_initial_event = {:?}", send_initial_event); + let path = path.to_path_buf(); + Ok(Self { path, + directory, + send_initial_event, + state }) + } + + // For clarity and testing this was extracted from new() and its not really intended to be used + // stand alone. Its also written such that you should know the path you're passing in is valid + // and trustworthy meaning whatever path you pass in should be one that would be valid according + // to the new method + fn get_enclosing_directory(path: &Path) -> Option { + trace!("WatchedFile::get_enclosing_directory({:?})", path); + match path.parent() { + Some(d) => Some(PathBuf::from(d)), + None => { + trace!("None Match in WatchedFile::get_enclosing_directory({:?})", + path); + path.components() + .next() + .map(|c| PathBuf::from(c.as_os_str())) + } + } + } + + // First thing we do is call is_file which will resolve symlinks and tell if a file exists on + // the file system. If true, we have an existent file. Second thing we do is call is_dir() and + // if true we return an error because FileWatcher watched individual files, not directories. // - // TODO(krnowak): Figure out if we can perform loop detection - // without this hash map, but only using whatever data we have in - // `Paths`. - symlink_loop_catcher: HashMap, - // A real path to the file from `start_path`. - real_file: Option, - // A set of paths to settle after the items there got removed. - paths_to_settle: HashSet, - // These args are used to pass them to `process_path`, when - // `paths_to_settle` becomes empty. - process_args_after_settle: Option, -} - -impl IndentedToString for Paths { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - let mut formatter = IndentedStructFormatter::new("Paths", spaces, repeat); - formatter.add("paths", &self.paths); - formatter.add("dirs", &self.dirs); - formatter.add("start_path", &self.start_path); - formatter.add("symlink_loop_catcher", &self.symlink_loop_catcher); - formatter.add("real_file", &self.real_file); - formatter.add("paths_to_settle", &self.paths_to_settle); - formatter.add("process_args_after_settle", &self.process_args_after_settle); - formatter.fmt() - } -} - -impl Paths { - fn new(simplified_abs_path: &Path) -> Self { - Self { paths: HashMap::new(), - dirs: HashMap::new(), - start_path: simplified_abs_path.to_path_buf(), - symlink_loop_catcher: HashMap::new(), - real_file: None, - paths_to_settle: HashSet::new(), - process_args_after_settle: None, } - } - - // Get a list of paths to watch, based on the configured - // `start_path`. - fn generate_watch_paths(&mut self) -> Vec { - let process_args = Self::path_for_processing(&self.start_path); - - self.process_path(process_args) - } - - // Separate the root from the `simplified_abs_path` rest and store - // them in a `ProcessPathArgs` instance. - fn path_for_processing(simplified_abs_path: &Path) -> ProcessPathArgs { - // Holds the `/` component of a path, or the path prefix and - // root on Windows (e.g. `C:\`). - let mut path = PathBuf::new(); - // Holds all other components of a path. - let mut path_rest = VecDeque::new(); - - // components are substrings between path separators ('/' or '\') - for component in simplified_abs_path.components() { - match component { - Component::Prefix(_) | Component::RootDir => { - path.push(component.as_os_str()); - } - Component::Normal(c) => path_rest.push_back(c.to_owned()), - // Respectively the `.`. and `..` components of a path. - Component::CurDir | Component::ParentDir => panic!("the path should be simplified"), - }; - } - - ProcessPathArgs { path, - path_rest, - index: 0, - prev: None } - } - - // Navigates through each of the components of the watched paths, - // deciding what action to take for each of them. - fn process_path(&mut self, args: ProcessPathArgs) -> Vec { - let mut common_generator = CommonGenerator::new(args); - let mut new_watches = Vec::new(); - - self.real_file = None; - - while let Some(common) = common_generator.get_new_common() { - let dir_file_name = common.dir_file_name.clone(); - - match common.path.symlink_metadata() { - // The error can be triggered when the underlying path - // does not exist, or when the user lacks permission - // to access the metadata. - // - // TODO(asymmetric): should we handle this last case as well? - // TODO(krnowak): see TODO in get_event_actions for Chmod event. - Err(_) => { - let leaf_result = if common.is_leaf() { - debug!("add missing regular because error and is a leaf"); - self.add_missing_regular(common) - } else { - debug!("add missing directory because error and is not a leaf"); - self.add_missing_directory(common) - }; - - self.handle_leaf_result(leaf_result, &mut new_watches); - - break; - } - Ok(metadata) => { - let file_type = metadata.file_type(); - - if !file_type.is_symlink() { - if common.is_leaf() { - let leaf_result = if file_type.is_file() { - debug!("add regular because is not a symlink, is a file and is a \ - leaf"); - self.real_file = Some(common.path.clone()); - self.add_regular(common) - } else { - debug!("{}{}", - "add missing regular because is not a symlink,", - " not a file and is a leaf",); - // It is not a symlink nor a file, so - // it is either a directory or - // something OS-specific. We expected - // a file here (common is a leaf - has - // an empty path rest). - self.add_missing_regular(common) - }; - - self.handle_leaf_result(leaf_result, &mut new_watches); - // TODO(krnowak): restructure the code, so - // the function has a simpler code - // flow. The break is needed to avoid - // running the rest of the code. - break; - } - - if file_type.is_dir() { - debug!("add directory because is not a symlink and is not a leaf"); - let branch_result = self.get_or_add_directory(common); - - self.handle_branch_result(&mut common_generator, - branch_result, - &mut new_watches); - - continue; - } - - // Not a symlink, not a dir, and not a last - // component - this means that we got some - // file in the middle of the path. For example - // when we watch for `/a/b/c` and apparently - // `/a/b` is a file, not a directory. So add a - // missing directory item here and stop - // processing the rest of the path - we need - // to wait for the directory to show up first. - debug!("{}{}", - "add missing directory because is not a symlink ", - "and is not a dir and is not a leaf",); - let leaf_result = self.add_missing_directory(common); - - self.handle_leaf_result(leaf_result, &mut new_watches); - break; - } else { - let target = match dir_file_name.as_path().read_link() { - Ok(target) => target, - Err(_) => { - // The path does not exist. - let leaf_result = if common.is_leaf() { - debug!("{}{}", - "add missing regular because is a symlink and", - " is a leaf, but failed to read link",); - self.add_missing_regular(common) - } else { - debug!("{}{}", - "add missing directory because is a symlink", - " and is not a leaf, but failed to read link",); - self.add_missing_directory(common) - }; - - self.handle_leaf_result(leaf_result, &mut new_watches); - - break; - } - }; - - debug!("target: {:?}", &target); - let target_path = if target.is_absolute() { - target - } else { - debug!("directory for target: {:?}", &dir_file_name.directory); - dir_file_name.directory.join(target) - }; - - let simplified_target = simplify_abs_path(&target_path); - debug!("simplified target: {:?}", &simplified_target); - let process_args = Self::path_for_processing(&simplified_target); - - if self.symlink_loop(&common.path, - &common.path_rest, - &process_args.path, - &process_args.path_rest) - { - debug!("symlink loop"); - // Symlink loop, nothing to watch here - hopefully - // later some symlink will be rewired to the real - // file. - break; - } - - debug!("add symlink"); - let branch_result = self.get_or_add_symlink(common); - - self.handle_branch_result(&mut common_generator, - branch_result, - &mut new_watches); - - common_generator.set_path(process_args.path); - common_generator.prepend_to_path_rest(process_args.path_rest); - } - } - } - } - - new_watches - } - - fn handle_leaf_result(&mut self, leaf_result: LeafResult, new_watches: &mut Vec) { - debug!("handle leaf result: {}", dits!(leaf_result)); - match leaf_result { - LeafResult::NewInNewDirectory(chain_link_info, directory) => { - new_watches.push(directory); - self.setup_chain_link(chain_link_info); - } - LeafResult::NewInOldDirectory(chain_link_info) => { - self.setup_chain_link(chain_link_info); - } - } - } - - fn handle_branch_result(&mut self, - common_generator: &mut CommonGenerator, - branch_result: BranchResult, - new_watches: &mut Vec) { - debug!("handle branch result: {}", dits!(branch_result)); - match branch_result { - // This is a part of the symlink handling, where we want - // to establish a chain link between a symlink and the - // first not-yet-watched item in the path the symlink is - // pointing to. - BranchResult::AlreadyExists => { - common_generator.revert_previous(); - } - BranchResult::NewInNewDirectory(chain_link_info, directory) => { - new_watches.push(directory); - self.setup_chain_link(chain_link_info); - } - BranchResult::NewInOldDirectory(chain_link_info) => { - self.setup_chain_link(chain_link_info); - } - } - } - - // Adds a new directory/file to a chain of watched paths. - fn setup_chain_link(&mut self, chain_link_info: ChainLinkInfo) { - if let Some(previous) = chain_link_info.prev { - match self.get_mut_watched_file(&previous) { - Some(previous_watched_file) => { - let previous_common = previous_watched_file.get_mut_common(); - previous_common.next = Some(chain_link_info.path); - } - None => { - error!("Paths inconsistency in setup_chain_link, expect strange results"); - } - } - } - } - - // Checks whether the path is present in the list of watched paths. - fn get_watched_file(&self, path: &Path) -> Option<&WatchedFile> { self.paths.get(path) } - - fn get_mut_watched_file(&mut self, path: &Path) -> Option<&mut WatchedFile> { - self.paths.get_mut(path) - } - - fn get_directory(&self, path: &Path) -> Option<&u32> { self.dirs.get(path) } - - fn get_mut_directory(&mut self, path: &Path) -> Option<&mut u32> { self.dirs.get_mut(path) } - - fn drop_watch(&mut self, path: &Path) -> Option { - if let Some(watched_file) = self.paths.remove(path) { - let common = watched_file.steal_common(); - let dir_path = common.dir_file_name.directory; - let unwatch_directory = match self.get_mut_directory(&dir_path) { - Some(count) => { - *count -= 1; - *count == 0 - } - None => { - error!("Dirs inconsistency in drop_watch, expect strange results"); - false - } - }; - self.symlink_loop_catcher.remove(path); - let dir_to_unwatch = if unwatch_directory { - self.dirs.remove(&dir_path); - - Some(dir_path) - } else { - None - }; - // any watch drop means that we don't see the real file anymore - self.real_file = None; - if let Some(prev) = common.prev { - // The `prev` patch may not be watched anymore when we - // stop watching a bunch of files in a batch (like - // when we move the watched directory away, so we stop - // watching everything inside it). - if let Some(watched_prev) = self.get_mut_watched_file(&prev) { - watched_prev.get_mut_common().next = None; - } + // Now since both is_file() and is_dir() traverse symlinks its reasonable to assume that if + // we're still looking then we're at this point we're being asked to look for a file that + // doesn't exist yet. So if the path is not an existent file and its not a existent directory + // then the question we're left with is "does this path look like it could be a file doesn't + // exist yet?" In order to determine that we're going to do two things. + // + // First, we're going to see if the path begins with an appropriate looking "path prefix". That + // will eliminate things which couldn't possibly a valid path. Testing the prefix is requires + // different consideration on unix vs windows but Rust provides std::path::Prefix really helps + // check what we need to check on windows and unix just begins with a std::path::MAIN_SEPARATOR + // character. + // + // Second, we're going to check that it does not end with a platform specific path separator + // which would indicate a directory if the path existed on the file system and we're going to + // error again as we did previously. Since we're testing with std::path::MAIN_SEPARATOR this + // will hopefully prove to be platform agnostic but in testing. + // + // There is also the possibility that we're being asked to watch for something that exists + // but for which there is an ownership or permission issue. Ownership and permission checking + // is a something that the previous versions of FileWatcher didn't try to solve. It's also + // something that the notify crate backends have bail on trying to report. So ownership and + // permission are really not problems we want to take on either so effectively they will present + // as nonexistent files. Which... close enough. :) + // + // Supporting reasoning for these decision is if the path was an existent file it would have + // platform appropriate "beginnings and endings", that is whatever prefix is expected for the + // and it wouldn't end in the platforms component separator. As both are true in the case of + // existent files we shouldn't have different expectations for files that exist when we begin + // watching vs when we're being asked to watch for a file that doesn't currently exist. + // + // Finally, in the documentation for PeerWatcher the sample path '/path/to/file' is + // used so a user might reasonably expect that a file extension isn't required. I think that + // prevents a good test around presences of the file extension in the case of NonExistent files. + fn discern_state_of(path: &Path) -> WatchedFileState { + trace!("WatchedFile::discern_state_of({:?})", path); + if path.is_file() { + // if this is file existing on the file system then everything is as good as we could + // hope. This is a condition we can expect to see during construction or when assessing + // during runtime after construction + WatchedFileState::ExistentFile + } else if path.is_dir() { + // if this is directory existing on the file system then its invalid because we watch + // files, not directories. We should only encounter this during construction. + WatchedFileState::Invalid(errmsg::is_directory(path)) + } else if Self::ends_with_a_path_separator(path) { + // if the file ends in a path separator then its appears to be a directory and we watch + // files, not directories. We should only encounter this during construction. + WatchedFileState::Invalid(errmsg::invalid_ending(path)) + } else if !Self::begins_with_a_root_dir_or_prefix(path) { + // if the file doesn't being appropriately for the platform we're running on then this + // can't be a valid path to a file. We should only encounter this during construction. + WatchedFileState::Invalid(errmsg::invalid_root(path)) + } else if let Some(d) = Self::get_enclosing_directory(path) { + // however, if we have still something that appears as though it could be a file... + if d.is_dir() { + // and the enclosing directory exists then we're in a state where the enclosing + // directory exists but not the file + WatchedFileState::ExistentDirectory } else { - error!("Prev path inconsistency in drop_watch, expect strange results"); + // or the enclosing directory does not exist and and we really don't have anything + // that we can can watch. + WatchedFileState::NonExistent } - dir_to_unwatch } else { - error!("Paths inconsistency in drop_watch, expect strange results"); - None + // if we've fallen through to here then whatever the path we've been provided is isn't + // valid. Whatever the path is it doesn't actually exist on the file system as a file + // and based on our checks it doesn't appear to be a potentially valid path to a file + // where the enclosing directory exists on the file system. + WatchedFileState::Invalid(errmsg::file_not_found(path)) } } - fn symlink_loop(&mut self, - path: &Path, - path_rest: &VecDeque, - new_path: &Path, - new_path_rest: &VecDeque) - -> bool { - let mut merged_path_rest = new_path_rest.clone(); - merged_path_rest.extend(path_rest.iter().cloned()); - let mut merged_path = new_path.to_path_buf(); - merged_path.extend(merged_path_rest); - match self.symlink_loop_catcher.entry(path.to_path_buf()) { - Entry::Occupied(o) => *o.get() == merged_path, - Entry::Vacant(v) => { - v.insert(merged_path); - false - } - } + pub fn assess_state(&self) -> WatchedFileState { + trace!("WatchedFile::assess_state()"); + let cs = Self::discern_state_of(&self.path); + debug!("CURRENT STATE = {:?}", cs); + cs } - fn add_regular(&mut self, common: Common) -> LeafResult { - self.add_leaf_watched_file(WatchedFile::Regular(common)) - } - - // TODO(asymmetric): could these 2 functions be unified? - // TODO(krnowak): Maybe replace `MissingRegular` and - // `MissingDirectory` with just one (`Missing`)? - fn add_missing_regular(&mut self, common: Common) -> LeafResult { - self.add_leaf_watched_file(WatchedFile::MissingRegular(common)) - } - - fn add_missing_directory(&mut self, common: Common) -> LeafResult { - self.add_leaf_watched_file(WatchedFile::MissingDirectory(common)) - } - - fn add_leaf_watched_file(&mut self, watched_file: WatchedFile) -> LeafResult { - debug!("add leaf file: {}", dits!(watched_file)); - let dir_file_name = watched_file.get_common().dir_file_name.clone(); - let needs_watch = self.add_dir(&dir_file_name); - let chain_link_info = match self.paths.entry(dir_file_name.as_path()) { - Entry::Occupied(o) => { - error!("Paths inconsistency in add_leaf_watched_file, expect strange results"); - o.get().get_common().get_chain_link_info() - } - Entry::Vacant(v) => v.insert(watched_file).get_common().get_chain_link_info(), - }; - if needs_watch { - LeafResult::NewInNewDirectory(chain_link_info, dir_file_name.directory) - } else { - LeafResult::NewInOldDirectory(chain_link_info) - } - } - - fn get_or_add_directory(&mut self, common: Common) -> BranchResult { - self.get_or_add_branch_watched_file(WatchedFile::Directory(common)) - } - - fn get_or_add_symlink(&mut self, common: Common) -> BranchResult { - self.get_or_add_branch_watched_file(WatchedFile::Symlink(common)) - } - - fn get_or_add_branch_watched_file(&mut self, watched_file: WatchedFile) -> BranchResult { - debug!("get or add branch file: {}", dits!(watched_file)); - if self.paths.contains_key(&watched_file.get_common().path) { - return BranchResult::AlreadyExists; - } - - let dir_file_name = watched_file.get_common().dir_file_name.clone(); - - let needs_watch = self.add_dir(&dir_file_name); - let chain_link_info = watched_file.get_common().get_chain_link_info(); - debug!("chain link info: {:?}", dits!(chain_link_info)); - self.paths.insert(dir_file_name.as_path(), watched_file); - - if needs_watch { - // The directory where the new file is located was not being watched. - BranchResult::NewInNewDirectory(chain_link_info, dir_file_name.directory) - } else { - // The file is new, the directory isn't. - BranchResult::NewInOldDirectory(chain_link_info) - } - } - - // Updates the counter for a directory in the `dirs` HashMap, - // returning whether the directory has been added for the first - // time. - fn add_dir(&mut self, dir_file_name: &DirFileName) -> bool { - match self.dirs.entry(dir_file_name.directory.clone()) { - Entry::Occupied(mut o) => { - *o.get_mut() += 1; - - false - } - Entry::Vacant(v) => { - v.insert(1); - - true - } - } - } - - fn add_path_to_settle(&mut self, path: PathBuf) { self.paths_to_settle.insert(path); } - - fn settle_path(&mut self, path: &Path) { self.paths_to_settle.remove(path); } - - fn set_process_args(&mut self, args: ProcessPathArgs) { - if match self.process_args_after_settle { - Some(ref old_args) => args.index < old_args.index, - None => true, - } { - self.process_args_after_settle = Some(args) - } - } - - fn process_path_or_defer_if_unsettled(&mut self) -> Option> { - let mut process_args = None; - swap(&mut process_args, &mut self.process_args_after_settle); - let (directories, new_args) = match process_args { - Some(args) => { - match self.process_path_if_settled(args) { - ProcessPathStatus::Executed(v) => (Some(v), None), - ProcessPathStatus::NotExecuted(a) => (None, Some(a)), - } - } - None => (None, None), - }; - self.process_args_after_settle = new_args; - directories - } - - fn process_path_if_settled(&mut self, args: ProcessPathArgs) -> ProcessPathStatus { - if self.paths_to_settle.is_empty() { - ProcessPathStatus::Executed(self.process_path(args)) - } else { - ProcessPathStatus::NotExecuted(args) + // JAH: Altering the code this way I believe that this is now platform agnostic. However, it + // will require tests that at least windows specific and I'm not sure if it won't require tests + // for unix to be run on unix only. + fn begins_with_a_root_dir_or_prefix(path: &Path) -> bool { + trace!("WatchedFile::begins_with_a_root_dir_or_prefix({:?})", path); + match path.components().next() { + Some(Component::Prefix(_)) => true, + Some(Component::RootDir) => true, + Some(Component::CurDir) => false, + Some(Component::ParentDir) => false, + Some(Component::Normal(_)) => false, + None => false, } } - fn reset(&mut self) -> Vec { - self.paths.clear(); - let mut dirs_to_unwatch = Vec::new(); - dirs_to_unwatch.extend(self.dirs.drain().map(|i| i.0)); - self.paths_to_settle.clear(); - self.process_args_after_settle = None; - self.symlink_loop_catcher.clear(); - dirs_to_unwatch + // JAH: this might be platform agnostic but will need to confirm + fn ends_with_a_path_separator(path: &Path) -> bool { + trace!("WatchedFile::ends_with_a_path_separator({:?})", path); + let last_byte: u8 = *(path.as_os_str().as_bytes().last().take().unwrap_or(&0)); + let main_separator_byte: u8 = std::path::MAIN_SEPARATOR.to_string().as_bytes()[0]; + last_byte == main_separator_byte } } -/// A regular file watcher. -/// -/// This type watches for a regular file at any path. The file does -/// not need to exist even - `FileWatcher` will track all the -/// directories and symlinks from the root directory up to the -/// file. If the file or any intermediate directory is missing, then -/// FileWatcher will wait for it to show up. -/// -/// `FileWatcher` will use callbacks to notify the user about events -/// that happened to the regular file at the desired path. Note that -/// it will call the file_appeared callback in the first iteration if -/// the file existed when the watcher was created. -pub struct FileWatcher { - callbacks: C, - // The watcher itself. - watcher: W, - // A channel for receiving events. - rx: Receiver, - // The paths to watch. - paths: Paths, - // Path to the file if it existed when we created the watcher. - initial_real_file: Option, -} - -/// Convenience function for returning a new file watcher that matches -/// the platform. -pub fn create_file_watcher(path: P, - callbacks: C, - ignore_initial: bool) - -> Result> - where P: Into, - C: Callbacks -{ - FileWatcher::::create(path, callbacks, ignore_initial) -} - -impl FileWatcher { - /// Creates a new `FileWatcher`. +/// A set of callbacks for the watched file events. +pub trait Callbacks { + /// A function that gets called when the watched file shows up. /// - /// This will create an instance of `W` and start watching the - /// paths. When looping the file watcher, it will emit an initial - /// "file appeared" event if ignore_initial is Some(false) and the - /// watched file existed when the file watcher was created. - /// Will return `Error::NotifyCreateError` if creating the watcher - /// fails. In case of watching errors, it returns - /// `Error::NotifyError`. - pub fn create

(path: P, callbacks: C, ignore_initial: bool) -> Result - where P: Into - { - Self::create_instance(path, callbacks, ignore_initial) - } - - // Creates an instance of the FileWatcher. - fn create_instance

(path: P, callbacks: C, send_initial_event: bool) -> Result - where P: Into - { - let (tx, rx) = channel(); - let mut watcher = - W::new(tx, Duration::from_millis(WATCHER_DELAY_MS)).map_err(|err| { - Error::NotifyCreateError(err) - })?; - let start_path = Self::watcher_path(path.into())?; - // Initialize the Paths struct, which will hold all state - // relative to file watching. - let mut paths = Paths::new(&start_path); - - // Generate list of paths to watch. - let directories = paths.generate_watch_paths(); - - // Start watcher on each path. - for directory in directories { - watcher.watch(&directory, RecursiveMode::NonRecursive) - .map_err(Error::NotifyError)?; - } - let initial_real_file = if send_initial_event { - paths.real_file.clone() - } else { - None - }; - - Ok(Self { callbacks, - watcher, - rx, - paths, - initial_real_file }) - } - - /// Get the reference to callbacks. - #[allow(dead_code)] - pub fn get_callbacks(&self) -> &C { &self.callbacks } - - /// Get the mutable reference to callbacks. - #[allow(dead_code)] - pub fn get_mut_callbacks(&mut self) -> &mut C { &mut self.callbacks } - - /// Get the reference to the underlying watcher. - #[allow(dead_code)] - pub fn get_underlying_watcher(&self) -> &W { &self.watcher } - - /// Get the mutable reference to the underlying watcher. - #[allow(dead_code)] - pub fn get_mut_underlying_watcher(&mut self) -> &mut W { &mut self.watcher } - - // Turns given path to a simplified absolute path. - // - // Simplified means that it is without `.` and `..`. - fn watcher_path(p: PathBuf) -> Result { - let abs_path = if p.is_absolute() { - p - } else { - let cwd = env::current_dir().map_err(Error::Io)?; - cwd.join(p) - }; - let simplified_abs_path = simplify_abs_path(&abs_path); - match DirFileName::split_path(&simplified_abs_path) { - Some(_) => Ok(simplified_abs_path), - None => Err(Error::FileWatcherFileIsRoot), - } - } - - pub fn run(&mut self) -> Result<()> { - let loop_value: liveliness_checker::ThreadUnregistered<_, _> = loop { - let checked_thread = liveliness_checker::mark_thread_alive(); - if let result @ Err(_) = self.single_iteration() { - break checked_thread.unregister(result); - } - thread::sleep(Duration::from_secs(1)); - }; - loop_value.into_result() - } - - pub fn single_iteration(&mut self) -> Result<()> { - if let Some(ref real_file) = self.initial_real_file { - self.callbacks.file_appeared(real_file); - } - - self.initial_real_file = None; - - match self.rx.try_recv() { - Ok(e) => self.handle_event(e), - Err(TryRecvError::Empty) => Ok(()), - Err(e) => Err(e.into()), - } - } - - fn handle_event(&mut self, event: DebouncedEvent) -> Result<()> { - let mut actions = VecDeque::new(); - debug!("in handle_event fn"); - debug!("got debounced event: {:?}", event); - - self.emit_directories_for_event(&event); - - // Gather the high-level actions. - actions.extend(Self::get_paths_actions(&self.paths, event)); - - debug!("paths: {}", dits!(self.paths)); - debug!("actions: {:?}", actions); - // Perform lower-level actions. - while let Some(action) = actions.pop_front() { - debug!("action {}", dits!(action)); - match action { - PathsAction::NotifyFileAppeared(p) => { - self.callbacks.file_appeared(p.as_path()); - } - PathsAction::NotifyFileModified(p) => { - self.callbacks.file_modified(p.as_path()); - } - PathsAction::NotifyFileDisappeared(p) => { - self.callbacks.file_disappeared(p.as_path()); - } - PathsAction::DropWatch(p) => { - if let Some(dir_path) = self.paths.drop_watch(&p) { - match self.watcher.unwatch(dir_path) { - Ok(_) => (), - // These probably may happen when the - // directory was removed. Ignore them, as - // we wanted to drop the watch anyway. - Err(notify::Error::PathNotFound) - | Err(notify::Error::WatchNotFound) => (), - Err(e) => return Err(Error::NotifyError(e)), - } - } - } - PathsAction::AddPathToSettle(p) => { - self.paths.add_path_to_settle(p); - } - PathsAction::SettlePath(p) => { - self.paths.settle_path(&p); - actions.extend(self.handle_process_path()?); - } - PathsAction::ProcessPathAfterSettle(args) => { - self.paths.set_process_args(args); - actions.extend(self.handle_process_path()?); - } - PathsAction::RestartWatching => { - actions.clear(); - if let Some(ref path) = self.paths.real_file { - actions.push_back(PathsAction::NotifyFileDisappeared(path.clone())); - } - for directory in self.paths.reset() { - match self.watcher.unwatch(directory) { - Ok(_) => (), - // These probably may happen when the - // directory was removed. Ignore them, as - // we wanted to drop the watch anyway. - Err(notify::Error::PathNotFound) - | Err(notify::Error::WatchNotFound) => (), - Err(e) => return Err(Error::NotifyError(e)), - } - } - let process_args = Paths::path_for_processing(&self.paths.start_path); - actions.push_back(PathsAction::ProcessPathAfterSettle(process_args)); - } - } - } - Ok(()) - } - - fn emit_directories_for_event(&mut self, event: &DebouncedEvent) { - let paths = match event { - DebouncedEvent::NoticeWrite(ref p) - | DebouncedEvent::Write(ref p) - | DebouncedEvent::Chmod(ref p) - | DebouncedEvent::NoticeRemove(ref p) - | DebouncedEvent::Remove(ref p) - | DebouncedEvent::Create(ref p) => vec![p], - DebouncedEvent::Rename(ref from, ref to) => vec![from, to], - DebouncedEvent::Rescan => vec![], - DebouncedEvent::Error(_, ref o) => { - match o { - Some(ref p) => vec![p], - None => vec![], - } - } - }; - let mut dirs = vec![]; - for path in paths { - if let Some(wf) = self.paths.get_watched_file(path) { - dirs.push(wf.get_common().dir_file_name.directory.clone()); - } else if self.paths.get_directory(path).is_some() { - dirs.push(path.clone()); - } else if let Some(df) = DirFileName::split_path(path) { - if self.paths.get_directory(&df.directory).is_some() { - dirs.push(df.directory.clone()); - } - } - } - debug!("event in dirs: {:?}", dirs); - self.callbacks.event_in_directories(&dirs); - } - - fn handle_process_path(&mut self) -> Result> { - let mut actions = Vec::new(); - match self.paths.process_path_or_defer_if_unsettled() { - None => (), - Some(directories) => { - for directory in directories { - self.watcher - .watch(&directory, RecursiveMode::NonRecursive)?; - } - if let Some(ref path) = self.paths.real_file { - actions.push(PathsAction::NotifyFileAppeared(path.clone())); - } - } - } - Ok(actions) - } - - // Maps `EventAction`s to one or more lower-level `PathsAction`s. - fn get_paths_actions(paths: &Paths, event: DebouncedEvent) -> Vec { - let mut actions = Vec::new(); - for event_action in Self::get_event_actions(paths, event) { - debug!("event_action: {}", dits!(event_action)); - let mut tmp_actions = Vec::new(); - match event_action { - EventAction::Ignore => (), - EventAction::PlainChange(p) => { - tmp_actions.push(PathsAction::NotifyFileModified(p)); - } - EventAction::RestartWatching => { - tmp_actions.push(PathsAction::RestartWatching); - } - EventAction::AddRegular(pad) => { - let path = pad.dir_file_name.as_path(); - tmp_actions.push(PathsAction::DropWatch(path.clone())); - tmp_actions.push(PathsAction::ProcessPathAfterSettle(pad.args)); - } - EventAction::DropRegular(pad) => { - tmp_actions.extend(Self::drop_common(paths, pad)); - } - EventAction::AddDirectory(pad) => { - let path = pad.dir_file_name.as_path(); - tmp_actions.push(PathsAction::DropWatch(path.clone())); - tmp_actions.push(PathsAction::ProcessPathAfterSettle(pad.args)); - } - EventAction::DropDirectory(pad) => { - tmp_actions.extend(Self::drop_common(paths, pad)); - } - EventAction::RewireSymlink(pad) => { - let path = pad.dir_file_name.as_path(); - tmp_actions.extend(Self::drop_common(paths, pad)); - tmp_actions.push(PathsAction::SettlePath(path)); - } - EventAction::DropSymlink(pad) => { - tmp_actions.extend(Self::drop_common(paths, pad)); - } - EventAction::SettlePath(p) => { - tmp_actions.push(PathsAction::SettlePath(p)); - } - }; - debug!("translated to {:?}", tmp_actions); - actions.extend(tmp_actions); - } - debug!("all actions: {:?}", actions); - actions - } - - fn drop_common(paths: &Paths, pad: PathsActionData) -> Vec { - let mut actions = Vec::new(); - let path = pad.dir_file_name.as_path(); - actions.push(PathsAction::AddPathToSettle(path.clone())); - let mut path_to_drop = Some(path); - while let Some(path) = path_to_drop { - let maybe_watched_file = paths.get_watched_file(&path); - actions.push(PathsAction::DropWatch(path)); - path_to_drop = if let Some(watched_file) = maybe_watched_file { - watched_file.get_common().next.clone() - } else { - None - }; - } - if let Some(ref path) = paths.real_file { - actions.push(PathsAction::NotifyFileDisappeared(path.clone())); - } - actions.push(PathsAction::ProcessPathAfterSettle(pad.args)); - actions - } - - // Maps filesystem events to high-level actions. - fn get_event_actions(paths: &Paths, event: DebouncedEvent) -> Vec { - // Usual actions on files and resulting events (assuming that - // a and b are in the same directory which we watch) - // touch a - Create(a) - // ln -sf foo a (does not matter if symlink a did exist before)- Create(a) - // mkdir a - Create(a) - // mv a b (does not matter if b existed or not) - NoticeRemove(a), Rename(a, b) - // mv ../a . - Create(a) - // mv a .. - NoticeRemove(a), Remove(a) - // rm a - NoticeRemove(a), Remove(a) - // echo foo >a (assuming a did exist earlier) - NoticeWrite(a), Write(a) - let event_action = match event { - // `Write` event will handle that. - DebouncedEvent::NoticeWrite(_) => EventAction::Ignore, - // These happen for regular files, just check if it - // affects the file we are watching. - // - // TODO(krnowak): I wonder if we should watch `Chmod` - // events for directories too. Maybe some permission - // changes can cause the directory to be unwatchable. Or - // watchable again for that matter. - DebouncedEvent::Write(ref p) | DebouncedEvent::Chmod(ref p) => { - match paths.get_watched_file(p) { - Some(&WatchedFile::Regular(_)) => EventAction::PlainChange(p.clone()), - _ => EventAction::Ignore, - } - } - DebouncedEvent::NoticeRemove(ref p) => Self::handle_notice_remove_event(paths, p), - DebouncedEvent::Remove(p) => Self::handle_remove_event(paths, p), - DebouncedEvent::Create(ref p) => { - match paths.get_watched_file(p) { - None => EventAction::Ignore, - Some(&WatchedFile::MissingRegular(ref c)) => { - EventAction::AddRegular(c.get_paths_action_data()) - } - // Create event for an already existing file or - // directory should not happen, restart watching. - Some(&WatchedFile::Regular(_)) | Some(&WatchedFile::Directory(_)) => { - EventAction::RestartWatching - } - Some(&WatchedFile::Symlink(ref c)) => { - EventAction::RewireSymlink(c.get_paths_action_data()) - } - Some(&WatchedFile::MissingDirectory(ref c)) => { - EventAction::AddDirectory(c.get_paths_action_data()) - } - } - } - DebouncedEvent::Rename(from, to) => { - let events = vec![Self::handle_notice_remove_event(paths, &to), - EventAction::SettlePath(to), - Self::handle_remove_event(paths, from)]; - // Rename is annoying in that it does not come - // together with `NoticeRemove` of the destination - // file (it is preceded with `NoticeRemove` of the - // source file only), so we just going to emulate it - // and then settle the destination path. - debug!("translated to {:?}", events); - return events; - } - DebouncedEvent::Rescan => EventAction::RestartWatching, - DebouncedEvent::Error(..) => EventAction::RestartWatching, - }; - debug!("translated to single {}", dits!(event_action)); - vec![event_action] - } - - fn handle_notice_remove_event(paths: &Paths, p: &Path) -> EventAction { - match paths.get_watched_file(p) { - None => EventAction::Ignore, - // Our directory was removed, moved elsewhere or - // replaced. I discovered replacement scenario while - // working on this code. Consider: - // - // mkdir a - // touch a/foo - // mkdir -p test/a - // mv a test - // - // This will replace the empty directory `yest/a` with - // `a`, so the file `foo` will be now in `test/a/foo`. - Some(&WatchedFile::Directory(ref c)) => { - EventAction::DropDirectory(c.get_paths_action_data()) - } - // This happens when we expected `p` to be a file, but it - // was something else and that thing just got removed. - Some(&WatchedFile::MissingRegular(_)) => EventAction::Ignore, - Some(&WatchedFile::Regular(ref c)) => { - EventAction::DropRegular(c.get_paths_action_data()) - } - Some(&WatchedFile::Symlink(ref c)) => { - EventAction::DropSymlink(c.get_paths_action_data()) - } - // This happens when we expected `p` to be a directory, - // but it was something else and that thing just got - // removed. - Some(&WatchedFile::MissingDirectory(_)) => EventAction::Ignore, - } - } - - fn handle_remove_event(paths: &Paths, path: PathBuf) -> EventAction { - match paths.get_watched_file(&path) { - // We should have dropped the watch of this file in - // `NoticeRemove`, so this should not happen - restart - // watching. - Some(&WatchedFile::Symlink(_)) - | Some(&WatchedFile::Directory(_)) - | Some(&WatchedFile::Regular(_)) => EventAction::RestartWatching, - // Possibly `path` is something that used to be - // interesting to us and got removed, try to settle it. If - // it was not that, then nothing will happen. - _ => EventAction::SettlePath(path), - } - } -} - -// For now it's unix only, as we are only testing one k8s related -// scenario, that involves symlinks. -#[cfg(all(unix, test))] -mod tests { - use crate::manager::sup_watcher::SupWatcher; - use habitat_core::locked_env_var; - use log::debug; - use std::{collections::{HashMap, - HashSet, - VecDeque}, - ffi::OsString, - fmt::{Display, - Error, - Formatter}, - fs::{self, - File}, - io::ErrorKind, - os::unix::fs as unix_fs, - path::{Component, - Path, - PathBuf}, - sync::mpsc::Sender, - thread, - time::Duration}; - - use notify::{self, - DebouncedEvent, - RawEvent, - RecursiveMode, - Watcher}; - - use tempfile::TempDir; - - use super::{Callbacks, - FileWatcher, - IndentedStructFormatter, - IndentedToString, - WatchedFile}; - - // Convenient macro for inline creation of hashmaps. - macro_rules! hm( - {$($key:expr => $value:expr),+} => { - { - [ - $( - ($key, $value), - )+ - ].iter().cloned().collect::>() - } - }; - // This form of the macro is to allow the leading comma. - { $($key:expr => $value:expr),+, } => { - hm!{ $($key => $value),+ } - }; - ); - - // Convenient macro for creating PathBufs. - macro_rules! pb( - {$str:expr} => { - PathBuf::from($str) - }; - ); - - // Convenient macro for creating OsStrings. - macro_rules! os( - {$str:expr} => { - OsString::from($str) - }; - ); - - locked_env_var!(HAB_STUDIO_HOST_ARCH, lock_env_var); - - // Add new test cases here. - fn get_test_cases() -> Vec { - vec![TestCase { name: "Basic add/remove directories/files", - init: Init { path: Some(pb!("/a/b/c")), - commands: vec![], - initial_file: None, }, - steps: vec![Step { action: StepAction::Nop, - dirs: hm! { - pb!("/") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::MissingDirectory, - path_rest: vec![os!("b"), os!("c")], - prev: None, - next: None, - }, - }, - events: vec![], }, - Step { action: StepAction::MkdirP(pb!("/a")), - dirs: hm! { - pb!("/") => 1, - pb!("/a") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::MissingDirectory, - path_rest: vec![os!("c")], - prev: Some(pb!("/a")), - next: None, - }, - }, - events: vec![], }, - Step { action: StepAction::MkdirP(pb!("/a/b")), - dirs: hm! { - pb!("/") => 1, - pb!("/a") => 1, - pb!("/a/b") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("c")], - prev: Some(pb!("/a")), - next: Some(pb!("/a/b/c")), - }, - pb!("/a/b/c") => PathState { - kind: PathKind::MissingRegular, - path_rest: vec![], - prev: Some(pb!("/a/b")), - next: None, - }, - }, - events: vec![], }, - Step { action: StepAction::Touch(pb!("/a/b/c")), - dirs: hm! { - pb!("/") => 1, - pb!("/a") => 1, - pb!("/a/b") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("c")], - prev: Some(pb!("/a")), - next: Some(pb!("/a/b/c")), - }, - pb!("/a/b/c") => PathState { - kind: PathKind::Regular, - path_rest: vec![], - prev: Some(pb!("/a/b")), - next: None, - }, - }, - events: vec![NotifyEvent::appeared(pb!("/a/b/c"))], }, - Step { action: StepAction::RmRF(pb!("/a/b/c")), - dirs: hm! { - pb!("/") => 1, - pb!("/a") => 1, - pb!("/a/b") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("c")], - prev: Some(pb!("/a")), - next: Some(pb!("/a/b/c")), - }, - pb!("/a/b/c") => PathState { - kind: PathKind::MissingRegular, - path_rest: vec![], - prev: Some(pb!("/a/b")), - next: None, - }, - }, - events: vec![NotifyEvent::disappeared(pb!("/a/b/c"))], }, - Step { action: StepAction::RmRF(pb!("/a/b")), - dirs: hm! { - pb!("/") => 1, - pb!("/a") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::MissingDirectory, - path_rest: vec![os!("c")], - prev: Some(pb!("/a")), - next: None, - }, - }, - events: vec![], }, - Step { action: StepAction::RmRF(pb!("/a")), - dirs: hm! { - pb!("/") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::MissingDirectory, - path_rest: vec![os!("b"), os!("c")], - prev: None, - next: None, - }, - }, - events: vec![], },], }, - TestCase { name: "Quick remove directories/files", - init: Init { path: Some(pb!("/a/b/c")), - commands: vec![InitCommand::MkdirP(pb!("/a/b")), - InitCommand::Touch(pb!("/a/b/c")),], - initial_file: Some(pb!("/a/b/c")), }, - steps: vec![Step { action: StepAction::Nop, - dirs: hm! { - pb!("/") => 1, - pb!("/a") => 1, - pb!("/a/b") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("c")], - prev: Some(pb!("/a")), - next: Some(pb!("/a/b/c")), - }, - pb!("/a/b/c") => PathState { - kind: PathKind::Regular, - path_rest: vec![], - prev: Some(pb!("/a/b")), - next: None, - }, - }, - events: vec![], }, - Step { action: StepAction::RmRF(pb!("/a")), - dirs: hm! { - pb!("/") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::MissingDirectory, - path_rest: vec![os!("b"), os!("c")], - prev: None, - next: None, - }, - }, - events: vec![NotifyEvent::disappeared(pb!("/a/b/c"))], },], }, - TestCase { name: "Basic symlink tracking", - init: Init { path: Some(pb!("/a")), - commands: vec![InitCommand::Touch(pb!("/1")), - InitCommand::Touch(pb!("/2")), - InitCommand::Touch(pb!("/3")), - InitCommand::LnS(pb!("s2"), pb!("/s1")), - InitCommand::LnS(pb!("/s3"), pb!("/s2")), - InitCommand::LnS(pb!("/1"), pb!("/s3")), - InitCommand::LnS(pb!("s1"), pb!("/a")), - InitCommand::MkdirP(pb!("/tmp")), - InitCommand::LnS(pb!("3"), - pb!("/tmp/link")),], - initial_file: Some(pb!("/1")), }, - steps: vec![Step { action: StepAction::Nop, - dirs: hm! { - pb!("/") => 5, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Symlink, - path_rest: vec![], - prev: None, - next: Some(pb!("/s1")), - }, - pb!("/s1") => PathState { - kind: PathKind::Symlink, - path_rest: vec![], - prev: Some(pb!("/a")), - next: Some(pb!("/s2")), - }, - pb!("/s2") => PathState { - kind: PathKind::Symlink, - path_rest: vec![], - prev: Some(pb!("/s1")), - next: Some(pb!("/s3")), - }, - pb!("/s3") => PathState { - kind: PathKind::Symlink, - path_rest: vec![], - prev: Some(pb!("/s2")), - next: Some(pb!("/1")), - }, - pb!("/1") => PathState { - kind: PathKind::Regular, - path_rest: vec![], - prev: Some(pb!("/s3")), - next: None, - }, - }, - events: vec![], }, - Step { action: StepAction::RmRF(pb!("/s2")), - dirs: hm! { - pb!("/") => 3, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Symlink, - path_rest: vec![], - prev: None, - next: Some(pb!("/s1")), - }, - pb!("/s1") => PathState { - kind: PathKind::Symlink, - path_rest: vec![], - prev: Some(pb!("/a")), - next: Some(pb!("/s2")), - }, - pb!("/s2") => PathState { - kind: PathKind::MissingRegular, - path_rest: vec![], - prev: Some(pb!("/s1")), - next: None, - }, - }, - events: vec![NotifyEvent::disappeared(pb!("/1"))], }, - Step { action: StepAction::LnS(pb!("/2"), pb!("/s2")), - dirs: hm! { - pb!("/") => 4, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Symlink, - path_rest: vec![], - prev: None, - next: Some(pb!("/s1")), - }, - pb!("/s1") => PathState { - kind: PathKind::Symlink, - path_rest: vec![], - prev: Some(pb!("/a")), - next: Some(pb!("/s2")), - }, - pb!("/s2") => PathState { - kind: PathKind::Symlink, - path_rest: vec![], - prev: Some(pb!("/s1")), - next: Some(pb!("/2")), - }, - pb!("/2") => PathState { - kind: PathKind::Regular, - path_rest: vec![], - prev: Some(pb!("/s2")), - next: None, - }, - }, - events: vec![NotifyEvent::appeared(pb!("/2"))], }, - Step { action: StepAction::Mv(pb!("/tmp/link"), pb!("/s1")), - dirs: hm! { - pb!("/") => 3, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Symlink, - path_rest: vec![], - prev: None, - next: Some(pb!("/s1")), - }, - pb!("/s1") => PathState { - kind: PathKind::Symlink, - path_rest: vec![], - prev: Some(pb!("/a")), - next: Some(pb!("/3")), - }, - pb!("/3") => PathState { - kind: PathKind::Regular, - path_rest: vec![], - prev: Some(pb!("/s1")), - next: None, - }, - }, - events: vec![NotifyEvent::disappeared(pb!("/2")), - NotifyEvent::appeared(pb!("/3")),], },], }, - TestCase { name: "Emulate Kubernetes ConfigMap", - init: Init { path: Some(pb!("/a")), - commands: vec![InitCommand::MkdirP(pb!("/old")), - InitCommand::MkdirP(pb!("/new")), - InitCommand::Touch(pb!("/old/a")), - InitCommand::Touch(pb!("/new/a")), - InitCommand::LnS(pb!("old"), - pb!("/data")), - InitCommand::LnS(pb!("data/a"), - pb!("/a")), - InitCommand::MkdirP(pb!("/tmp")), - InitCommand::LnS(pb!("new"), - pb!("/tmp/link")),], - initial_file: Some(pb!("/old/a")), }, - steps: vec![Step { action: StepAction::Nop, - dirs: hm! { - pb!("/") => 3, - pb!("/old") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Symlink, - path_rest: vec![], - prev: None, - next: Some(pb!("/data")), - }, - pb!("/data") => PathState { - kind: PathKind::Symlink, - path_rest: vec![os!("a")], - prev: Some(pb!("/a")), - next: Some(pb!("/old")), - }, - pb!("/old") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("a")], - prev: Some(pb!("/data")), - next: Some(pb!("/old/a")), - }, - pb!("/old/a") => PathState { - kind: PathKind::Regular, - path_rest: vec![], - prev: Some(pb!("/old")), - next: None, - }, - }, - events: vec![], }, - Step { action: StepAction::Mv(pb!("/tmp/link"), pb!("/data")), - dirs: hm! { - pb!("/") => 3, - pb!("/new") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Symlink, - path_rest: vec![], - prev: None, - next: Some(pb!("/data")), - }, - pb!("/data") => PathState { - kind: PathKind::Symlink, - path_rest: vec![os!("a")], - prev: Some(pb!("/a")), - next: Some(pb!("/new")), - }, - pb!("/new") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("a")], - prev: Some(pb!("/data")), - next: Some(pb!("/new/a")), - }, - pb!("/new/a") => PathState { - kind: PathKind::Regular, - path_rest: vec![], - prev: Some(pb!("/new")), - next: None, - }, - }, - events: vec![NotifyEvent::disappeared(pb!("/old/a")), - NotifyEvent::appeared(pb!("/new/a")),], },], }, - TestCase { name: "Symlink loop, pointing to itself", - init: Init { path: Some(pb!("/a/b/c")), - commands: vec![InitCommand::MkdirP(pb!("/a")), - InitCommand::LnS(pb!("b"), pb!("/a/b")),], - initial_file: None, }, - steps: vec![Step { action: StepAction::Nop, - dirs: hm! { - pb!("/") => 1, - pb!("/a") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::Symlink, - path_rest: vec![os!("c")], - prev: Some(pb!("/a")), - next: None, - }, - }, - events: vec![], }, - Step { action: StepAction::RmRF(pb!("/a/b")), - dirs: hm! { - pb!("/") => 1, - pb!("/a") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::MissingDirectory, - path_rest: vec![os!("c")], - prev: Some(pb!("/a")), - next: None, - }, - }, - events: vec![], },], }, - TestCase { name: "Dropping looping symlink and adding a new one instead", - init: Init { path: Some(pb!("/a/b/c/d")), - commands: vec![InitCommand::MkdirP(pb!("/a")), - InitCommand::MkdirP(pb!("/x")), - InitCommand::LnS(pb!("/x"), pb!("/a/b")), - InitCommand::LnS(pb!("/a/b/c"), - pb!("/x/c")),], - initial_file: None, }, - steps: vec![Step { action: StepAction::Nop, - dirs: hm! { - pb!("/") => 2, - pb!("/a") => 1, - pb!("/x") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c"), os!("d")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::Symlink, - path_rest: vec![os!("c"), os!("d")], - prev: Some(pb!("/a")), - next: Some(pb!("/x")), - }, - pb!("/x") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("c"), os!("d")], - prev: Some(pb!("/a/b")), - next: Some(pb!("/x/c")), - }, - pb!("/x/c") => PathState { - kind: PathKind::Symlink, - path_rest: vec![os!("d")], - prev: Some(pb!("/x")), - next: None, - }, - }, - events: vec![], }, - Step { action: StepAction::RmRF(pb!("/x/c")), - dirs: hm! { - pb!("/") => 2, - pb!("/a") => 1, - pb!("/x") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c"), os!("d")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::Symlink, - path_rest: vec![os!("c"), os!("d")], - prev: Some(pb!("/a")), - next: Some(pb!("/x")), - }, - pb!("/x") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("c"), os!("d")], - prev: Some(pb!("/a/b")), - next: Some(pb!("/x/c")), - }, - pb!("/x/c") => PathState { - kind: PathKind::MissingDirectory, - path_rest: vec![os!("d")], - prev: Some(pb!("/x")), - next: None, - }, - }, - events: vec![], }, - Step { action: StepAction::Touch(pb!("/x/d")), - dirs: hm! { - pb!("/") => 2, - pb!("/a") => 1, - pb!("/x") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c"), os!("d")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::Symlink, - path_rest: vec![os!("c"), os!("d")], - prev: Some(pb!("/a")), - next: Some(pb!("/x")), - }, - pb!("/x") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("c"), os!("d")], - prev: Some(pb!("/a/b")), - next: Some(pb!("/x/c")), - }, - pb!("/x/c") => PathState { - kind: PathKind::MissingDirectory, - path_rest: vec![os!("d")], - prev: Some(pb!("/x")), - next: None, - }, - }, - events: vec![], }, - Step { action: StepAction::LnS(pb!("."), pb!("/x/c")), - dirs: hm! { - pb!("/") => 2, - pb!("/a") => 1, - pb!("/x") => 2, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c"), os!("d")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::Symlink, - path_rest: vec![os!("c"), os!("d")], - prev: Some(pb!("/a")), - next: Some(pb!("/x")), - }, - pb!("/x") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("c"), os!("d")], - prev: Some(pb!("/a/b")), - next: Some(pb!("/x/c")), - }, - pb!("/x/c") => PathState { - kind: PathKind::Symlink, - path_rest: vec![os!("d")], - prev: Some(pb!("/x")), - next: Some(pb!("/x/d")), - }, - pb!("/x/d") => PathState { - kind: PathKind::Regular, - path_rest: vec![], - prev: Some(pb!("/x/c")), - next: None, - }, - }, - events: vec![NotifyEvent::appeared(pb!("/x/d"))], },], }, - TestCase { name: "Rewiring symlink loop", - init: Init { path: Some(pb!("/a/b/c/d")), - commands: vec![InitCommand::MkdirP(pb!("/a")), - InitCommand::MkdirP(pb!("/x")), - InitCommand::LnS(pb!("/x"), pb!("/a/b")), - InitCommand::LnS(pb!("/a/b/c"), - pb!("/x/c")), - InitCommand::Touch(pb!("/x/d")), - InitCommand::MkdirP(pb!("/tmp")), - InitCommand::LnS(pb!("."), - pb!("/tmp/link")),], - initial_file: None, }, - steps: vec![Step { action: StepAction::Nop, - dirs: hm! { - pb!("/") => 2, - pb!("/a") => 1, - pb!("/x") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c"), os!("d")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::Symlink, - path_rest: vec![os!("c"), os!("d")], - prev: Some(pb!("/a")), - next: Some(pb!("/x")), - }, - pb!("/x") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("c"), os!("d")], - prev: Some(pb!("/a/b")), - next: Some(pb!("/x/c")), - }, - pb!("/x/c") => PathState { - kind: PathKind::Symlink, - path_rest: vec![os!("d")], - prev: Some(pb!("/x")), - next: None, - }, - }, - events: vec![], }, - Step { action: StepAction::Mv(pb!("/tmp/link"), pb!("/x/c")), - dirs: hm! { - pb!("/") => 2, - pb!("/a") => 1, - pb!("/x") => 2, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c"), os!("d")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::Symlink, - path_rest: vec![os!("c"), os!("d")], - prev: Some(pb!("/a")), - next: Some(pb!("/x")), - }, - pb!("/x") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("c"), os!("d")], - prev: Some(pb!("/a/b")), - next: Some(pb!("/x/c")), - }, - pb!("/x/c") => PathState { - kind: PathKind::Symlink, - path_rest: vec![os!("d")], - prev: Some(pb!("/x")), - next: Some(pb!("/x/d")), - }, - pb!("/x/d") => PathState { - kind: PathKind::Regular, - path_rest: vec![], - prev: Some(pb!("/x/c")), - next: None, - }, - }, - events: vec![NotifyEvent::appeared(pb!("/x/d"))], },], }, - TestCase { name: "Moving a directory", - init: Init { path: Some(pb!("/a/b/c")), - commands: vec![InitCommand::MkdirP(pb!("/a/b")), - InitCommand::Touch(pb!("/a/b/c")),], - initial_file: Some(pb!("/a/b/c")), }, - steps: vec![Step { action: StepAction::Nop, - dirs: hm! { - pb!("/") => 1, - pb!("/a") => 1, - pb!("/a/b") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("c")], - prev: Some(pb!("/a")), - next: Some(pb!("/a/b/c")), - }, - pb!("/a/b/c") => PathState { - kind: PathKind::Regular, - path_rest: vec![], - prev: Some(pb!("/a/b")), - next: None, - }, - }, - events: vec![], }, - Step { action: StepAction::Mv(pb!("/a/b"), pb!("/a/d")), - dirs: hm! { - pb!("/") => 1, - pb!("/a") => 1, - }, - paths: hm! { - pb!("/a") => PathState { - kind: PathKind::Directory, - path_rest: vec![os!("b"), os!("c")], - prev: None, - next: Some(pb!("/a/b")), - }, - pb!("/a/b") => PathState { - kind: PathKind::MissingDirectory, - path_rest: vec![os!("c")], - prev: Some(pb!("/a")), - next: None, - }, - }, - events: vec![NotifyEvent::disappeared(pb!("/a/b/c"))], },], },] - } - - fn run_test_case(tc: &TestCase) { - let mut runner = TestCaseRunner::new(); - runner.debug_info.add(format!("test case: {}", tc.name)); - runner.run_init_commands(&tc.init.commands); - let setup = runner.prepare_watcher(&tc.init.path); - runner.run_steps(setup, &tc.init.initial_file, &tc.steps); - } - - #[test] - fn file_watcher() { - let lock = lock_env_var(); - lock.unset(); - - for tc in get_test_cases() { - run_test_case(&tc); - } - } - - #[test] - fn polling_file_watcher() { - let lock = lock_env_var(); - lock.set("aarch64-darwin"); - - // When using the PollWatcher variant of SupWatcher, the - // behavior is different than the NotifyWatcher with respect to the - // the timing of generated events as well as the number of events - // generated. In the first two test cases, there were extraneous - // events generated that are not generated by the NotifyWatcher and - // these needed to be ignored. In the later test cases, the PollWatcher - // failed to generate the expected events and did not pass regardless of - // the timing or number of iterations. Since these later test cases are - // beyond the scope of our watchers, they were skipped for the PollWatcher. - // Also note that the criteria for passing was based on the original - // NotifyWatcher where these events were generated as expected. - - let cases = get_test_cases(); - let polling_cases = &cases[0..2]; - for tc in polling_cases { - run_test_case(tc); - } - lock.unset(); - } - - // Commands that can be executed at the test case init. - // - // Tests may come and go, so some of the variants may be unused. - #[allow(dead_code)] - enum InitCommand { - MkdirP(PathBuf), - Touch(PathBuf), - LnS(PathBuf, PathBuf), - } - - // Description of the init phase for test case. - struct Init { - // The path to the file that will be watched. - path: Option, - // Commands to be executed before executing the steps. - commands: Vec, - // Optional file to the real file if it exists after - // performing the initial commands. - initial_file: Option, - } - - // Commands executed as a part of the test case step. - // Tests may come and go, so some of the variants may be unused. - #[allow(dead_code)] - #[derive(Debug)] - enum StepAction { - LnS(PathBuf, PathBuf), - MkdirP(PathBuf), - Touch(PathBuf), - Mv(PathBuf, PathBuf), - RmRF(PathBuf), - Nop, - } - - #[derive(Clone, Copy, Debug, PartialEq)] - enum PathKind { - Symlink, - Regular, - MissingRegular, - Directory, - MissingDirectory, - } - - // Simplified description of the WatchedFile's Common struct. - #[derive(Clone)] - struct PathState { - kind: PathKind, - path_rest: Vec, - prev: Option, - next: Option, - } - - impl IndentedToString for PathState { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - let mut formatter = IndentedStructFormatter::new("PathState", spaces, repeat); - formatter.add_debug("kind", &self.kind); - formatter.add_debug("path_rest", &self.path_rest); - formatter.add("prev", &self.prev); - formatter.add("next", &self.next); - formatter.fmt() - } - } - - #[derive(Clone, Copy, Debug, PartialEq)] - enum NotifyEventKind { - Appeared, - Modified, - Disappeared, - } - - #[derive(Clone, Debug, PartialEq)] - struct NotifyEvent { - path: PathBuf, - kind: NotifyEventKind, - } - - impl NotifyEvent { - fn new(path: PathBuf, kind: NotifyEventKind) -> Self { Self { path, kind } } - - fn appeared(path: PathBuf) -> Self { Self::new(path, NotifyEventKind::Appeared) } - - fn modified(path: PathBuf) -> Self { Self::new(path, NotifyEventKind::Modified) } - - fn disappeared(path: PathBuf) -> Self { Self::new(path, NotifyEventKind::Disappeared) } - } - - // A description of the single step in the test case. - struct Step { - // Action to execute at the beginning of the step. - action: StepAction, - // Expected watched directories together with the use count, - // similar to `dirs` field in Paths. - dirs: HashMap, - // Expected watched items, similar to `paths` field in Paths, - // but a bit simplified. - paths: HashMap, - // Expected events that happened when executing the step - // command. The events map to the `file_*` functions in - // `Callbacks` trait. - events: Vec, - } - - impl IndentedToString for Step { - fn indented_to_string(&self, spaces: &str, repeat: usize) -> String { - let mut formatter = IndentedStructFormatter::new("Step", spaces, repeat); - formatter.add_debug("action", &self.action); - formatter.add("dirs", &self.dirs); - formatter.add("paths", &self.paths); - formatter.add_debug("events", &self.events); - formatter.fmt() - } - } + /// `real_path` will contain a path to the watched file, which may + /// be different from the one that was passed to `FileWatcher`, + /// because of symlinks. + fn file_appeared(&mut self, real_path: &Path); + /// A function that gets called when the watched file is written to. + /// + /// Note that this is called only when the real file is + /// modified. In case when some symlink in the watched paths is + /// atomically changed to point to something else, + /// `file_disappeared` followed by `file_appeared` will be + /// actually called. + /// + /// `real_path` will contain a path to the watched file, which may + /// be different from the one that was passed to `FileWatcher`, + /// because of symlinks. + fn file_modified(&mut self, real_path: &Path); + /// A function that gets called when the watched file goes away. + /// + /// `real_path` will contain a path to the watched file, which may + /// be different from the one that was passed to `FileWatcher`, + /// because of symlinks. + fn file_disappeared(&mut self, real_path: &Path); +} - struct TestCase { - // Not used directly, but describes the test. Can be used - // later for debugging. - #[allow(dead_code)] - name: &'static str, - init: Init, - steps: Vec, - } +/// FileWatcher watches for a regular file at any path. The file does not have to exist when the +/// FileWatcher is created. +/// +/// FileWatcher uses callbacks to notify about events occuring to the watched file. Note that it +/// will call file_appeared callback during its first run if the file existed when the watcher was +/// created and ignore_initial is set false. +#[derive(Debug)] +pub struct FileWatcher { + /// The Callback implementation to be used by this FileWatcher + callbacks: C, + /// The Watcher implementation to be used by this FileWatcher. + watcher: RefCell, + /// An std::sync::mpsc::channel used to receive events. + rx: Receiver>, + /// The actual file to be watched. + watched_file: WatchedFile, + active_watches: RefCell>, +} - // The implementation of `Callbacks` trait for testing purposes. - #[derive(Default)] - struct TestCallbacks { - // A list of events that happened when executing the step. - events: Vec, - // A set of ignored directories. Usually it is just `/` and - // `/tmp`. - ignored_dirs: HashSet, - // Whether this single iteration should be ignored, because it - // happened in one of the ignored directories. - ignore: bool, - } +/// Convenience function for returning a new FileWatcher +pub fn create_file_watcher(path: P, + callbacks: C, + ignore_initial: bool) + -> Result> + where P: Into, + C: Callbacks +{ + trace!("create_file_watcher(path,callbacks,{:?})", ignore_initial); + FileWatcher::::new(path, callbacks, ignore_initial) +} - impl TestCallbacks { - fn new(ignored_dirs: &[PathBuf]) -> Self { - let mut cb = Self::default(); - cb.ignored_dirs.extend(ignored_dirs.iter().cloned()); - cb - } +impl FileWatcher { + /// Creates an instance of `W` and creates a WatchedFile using the supplied path. If + /// ignore_initial is passed as false and the WatchedFile exists when this is called then the + /// first loop will emit an initial "file_appeared" event. + fn new

(path: P, callbacks: C, ignore_initial: bool) -> Result + where P: Into + { + trace!("FileWatcher::new(path,callbacks,{:?})", ignore_initial); + let (tx, rx) = channel::>(); + let config = Config::default().with_poll_interval(Duration::from_millis(WATCHER_DELAY_MS)); + let watcher = RefCell::new(W::new(tx, config).map_err(Error::NotifyCreateError)?); + let p = path.into(); + let watched_file = WatchedFile::new(&p, !ignore_initial)?; + let active_watches = RefCell::new(HashSet::new()); + let mut file_watcher = Self { callbacks, + watcher, + rx, + watched_file, + active_watches }; + file_watcher.manage_watches()?; + Ok(file_watcher) + } + + /// manage_watches() wraps up the essential functions around "the watch" for the WatchedFile + /// being watched by the FileWatcher. Its really meant to be called in two places. First, + /// during construction of FileWatcher instances. Second, as late as possible in the event + /// handling process. Its called during construction because we need to assess "the state of + /// the world" when the watch of the file begins so that we know how to respond to the next + /// event received. We then defer calling this again until as late as possible in the process + /// of responding to an event because our response needs to be within the context of the last + /// action we took. + fn manage_watches(&mut self) -> Result<()> { + trace!("FileWatcher::manage_watches()"); + self.clear_active_watches()?; + self.watched_file.state = self.watched_file.assess_state(); + self.set_new_watches() + } + + fn set_new_watches(&self) -> Result<()> { + trace!("FileWatcher::set_new_watches()"); + match &self.watched_file.state { + WatchedFileState::ExistentFile => { + Ok(self.watch(&self.watched_file.path) + .and(self.watch(&self.watched_file.directory))?) + } + WatchedFileState::ExistentDirectory => Ok(self.watch(&self.watched_file.directory)?), + WatchedFileState::NonExistent => Ok(()), + WatchedFileState::Invalid(_) => { + let s = self.watched_file + .path + .as_os_str() + .to_string_lossy() + .to_string(); + Err(Error::FileNotFound(s)) + } + } + } + + fn watch(&self, path: &Path) -> Result<()> { + trace!("FileWatcher::watch({:?})", path); + self.active_watches.borrow_mut().insert(path.to_path_buf()); + self.watcher + .borrow_mut() + .watch(path, RecursiveMode::NonRecursive) + .map_err(Error::NotifyError) + } + + fn clear_active_watches(&self) -> Result<()> { + trace!("FileWatcher::clear_active_watches()"); + let mut laws = self.active_watches.borrow_mut(); + for aw in laws.iter() { + if laws.contains(aw) { + self.unwatch(aw.as_ref())?; + } + } + laws.clear(); + Ok(()) } - impl Callbacks for TestCallbacks { - fn file_appeared(&mut self, real_path: &Path) { - self.events - .push(NotifyEvent::appeared(real_path.to_owned())); - } - - fn file_modified(&mut self, real_path: &Path) { - self.events - .push(NotifyEvent::modified(real_path.to_owned())); - } - - fn file_disappeared(&mut self, real_path: &Path) { - self.events - .push(NotifyEvent::disappeared(real_path.to_owned())); - } - - fn event_in_directories(&mut self, paths: &[PathBuf]) { - for path in paths { - if self.ignored_dirs.contains(path) { - debug!("got event in ignored dirs"); - self.ignore = true; - break; + fn unwatch(&self, path: &Path) -> Result<()> { + trace!("FileWatcher::unwatch({:?})", path); + let r = self.watcher.borrow_mut().unwatch(path); + match r { + Ok(_) => Ok(()), + Err(e) => { + match e.kind { + notify::ErrorKind::WatchNotFound => Ok(()), + _ => Err(Error::NotifyError(e)), } } } } - // The implementation of notify::Watcher trait for testing - // purposes. - struct TestWatcher { - // The real watcher that does the grunt work. - real_watcher: SupWatcher, - // A set of watched dirs. We will use these to correctly - // compute the number of iterations to perform after executing - // the step action. - watched_dirs: HashSet, + pub fn run(&mut self) -> Result<()> { + let loop_value: liveliness_checker::ThreadUnregistered<_, _> = loop { + let checked_thread = liveliness_checker::mark_thread_alive(); + if let result @ Err(_) = self.single_iteration() { + break checked_thread.unregister(result); + } + thread::sleep(Duration::from_secs(1)); + }; + loop_value.into_result() } - impl Watcher for TestWatcher { - fn new_raw(tx: Sender) -> notify::Result { - Ok(TestWatcher { real_watcher: SupWatcher::new_raw(tx)?, - watched_dirs: HashSet::new(), }) + pub fn single_iteration(&mut self) -> Result<()> { + trace!("FileWatcher::single_iteration()"); + + if self.watched_file.send_initial_event { + self.callbacks.file_appeared(&self.watched_file.path); + self.watched_file.send_initial_event = false; + // this should only ever trigger the one time + } + + if self.watched_file.state == WatchedFileState::NonExistent { + // if we've found ourselves with a WatchedFile value that has passed our validation but + // doesn't seem to exist then we we have something to watch for but nothing that we can + // pass to the notify crate to notify about. So, here we assess the current state to + // see if that has changed. + let previous_state = self.watched_file.state.clone(); // JAH: unnecessary clone? + self.watched_file.state = self.watched_file.assess_state(); + match self.watched_file.state { + WatchedFileState::ExistentFile => { + self.set_new_watches()?; + if previous_state == WatchedFileState::NonExistent { + self.callbacks.file_appeared(&self.watched_file.path); + } + return Ok(()); + } + WatchedFileState::ExistentDirectory => { + self.set_new_watches()?; + return Ok(()); + } + WatchedFileState::NonExistent => return Ok(()), + WatchedFileState::Invalid(_) => { + let s = self.watched_file + .path + .as_os_str() + .to_string_lossy() + .to_string(); + return Err(Error::FileNotFound(s)); // JAH: Think about how we would get here + } + } } - fn new(tx: Sender, d: Duration) -> notify::Result { - Ok(TestWatcher { real_watcher: SupWatcher::new(tx, d)?, - watched_dirs: HashSet::new(), }) + match self.rx.try_recv() { + Ok(notify_result) => self.handle_event(¬ify_result?), + Err(TryRecvError::Empty) => Ok(()), + Err(e) => Err(e.into()), } + } - fn watch>(&mut self, path: P, mode: RecursiveMode) -> notify::Result<()> { - if !self.watched_dirs.insert(path.as_ref().to_owned()) { - panic!("Trying to watch a path {} we are already watching", - path.as_ref().display(),); - } - if mode == RecursiveMode::Recursive { - panic!("Recursive watch should not ever happen"); + fn handle_event(&mut self, notify_event: &Event) -> Result<()> { + debug!("FileWatcher::handle_event(..)\n{:?}\n", notify_event); + if notify_event.need_rescan() { + // JAH: think about rescans some more + self.handle_file_modification(notify_event) + } else { + // ModifyKind below doesn't derive Copy which is what drives this call to clone. It + // seems like the easiest decision to make that solves several clippy warnings because + // if the notify project ever does derive copy then we can just remove this. + let notify_event_kind = notify_event.kind.clone(); + match notify_event_kind { + EventKind::Any => { + // v5.0.0: debounced, FSEvent, and Windows backends + self.log_ignored_event(notify_event); + // debounced: debounced mode in v5 seems to throw away all event kind info + // FSEvents: used in "imprecise mode" which may be debounced mode + // Windows: Used as a default value and then appears to be reliably changed to + // the "real kind" before the event is used. + } + EventKind::Access(access_kind) => { + self.handle_access_event(access_kind, notify_event) + } + EventKind::Create(create_kind) => { + self.handle_create_event(create_kind, notify_event); + } + EventKind::Modify(modify_kind) => { + self.handle_modify_event(&modify_kind, notify_event) + } + EventKind::Remove(remove_kind) => { + self.handle_remove_event(remove_kind, notify_event) + } + EventKind::Other => { + // v5.0.0: FSEvents, inotify, and kqueue + self.log_ignored_event(notify_event); + // FSEvents: used in conjunction with need_rescan() to signal that FSEvents is + // signaling to rescan subdirectories so will be covered by need_rescan flag + // inotify: Used in conjunction with need_rescan to signal event queue overflow + // and I believe that it will also be covered by need rescan check above. + // kqueue: used as a catch because "on different BSD variants, different extra + // events may be present" + } } - self.real_watcher.watch(path, mode) } + Ok(()) + } - fn unwatch>(&mut self, path: P) -> notify::Result<()> { - if !self.watched_dirs.remove(&path.as_ref().to_owned()) { - panic!("Trying to unwatch a path {} we were not watching", - path.as_ref().display(),); + fn handle_access_event(&mut self, access_kind: AccessKind, notify_event: &Event) { + trace!("FileWatch::handle_access_event"); + match access_kind { + AccessKind::Close(AccessMode::Write) => { + // v5.0.0: inotify + self.handle_file_modification(notify_event); } - self.real_watcher.unwatch(path) + _ => self.log_ignored_event(notify_event), + // We really don't care about mere file access unless its a write on file close. + // There's AccessKind::Open(AccessMode::Write) covered in this but as of v5.0.0 its not + // in use and I'm not sure we would need to handle opening for write anyway } } - struct DebugInfo { - logs_per_level: Vec>, - } - - impl DebugInfo { - fn new() -> Self { Self { logs_per_level: vec![Vec::new()], } } - - fn push_level(&mut self) { self.logs_per_level.push(Vec::new()); } - - fn pop_level(&mut self) { - self.logs_per_level.pop(); - assert!(!self.logs_per_level.is_empty(), - "too many pops on DebugInfo"); + fn handle_create_event(&mut self, create_kind: CreateKind, notify_event: &Event) { + trace!("FileWatch::handle_create_event"); + match create_kind { + CreateKind::Any // v5.0.0: FSEvent, poll, and windows + | CreateKind::File => { // v5.0.0: FSEvent and inotify + self.handle_file_appearance(notify_event); + // JAH: Will "Any" require special handling on poll and windows to account for + // directories? + } + CreateKind::Folder // v5.0.0: FSEvent and inotify + | CreateKind::Other => { // v5.0.0: FSEvent + self.log_ignored_event(notify_event); + // We're ignoring folder creation because we're watching files and a requirement + // of FileWatcher is that either the file or its enclosing folder exists. So if the + // folder creation doesn't matter in that case. + } } - - fn add(&mut self, str: String) { self.logs_per_level.last_mut().unwrap().push(str); } } - impl Display for DebugInfo { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { - writeln!(f, "----------------")?; - for level_logs in &self.logs_per_level { - for entry in level_logs { - writeln!(f, "{}\n----------------", entry)?; - } + fn handle_data_change_event(&mut self, data_change: DataChange, notify_event: &Event) { + trace!("FileWatch::handle_data_change_event"); + match data_change { + DataChange::Any => { // v5.0.0: inotify, kqueue, and poll + // This case is also trigged by being called in the following cases + // - ModifyKind::Any + // - ModifyKind::Metadata(MetadataKind::WriteTime) + self.handle_file_modification(notify_event); + // If we know a modification has happened but don't know exactly what happened then + // we will notify of a file modification. Similarly, if we are notified that a file + // write time was modified (which is how the poll backend works) then we will notify + // of a file modification. + } + DataChange::Size // v5.0.0: kqueue + | DataChange::Content // v5.0.0: FSEvent + | DataChange::Other => { // v5.0.0: not implemented by any backend + self.log_ignored_event(notify_event); + // These are platforms we don't target today or unimplemented in notify } - Ok(()) } } - struct WatcherSetup { - init_path: PathBuf, - watcher: FileWatcher, - } - - // Structure used for executing the initial commands and step - // actions. - struct FsOps<'a> { - debug_info: &'a mut DebugInfo, - root: &'a PathBuf, - watched_dirs: Option<&'a HashSet>, - } - - impl<'a> FsOps<'a> { - fn ln_s(&self, target: &Path, path: &Path) -> u32 { - let pp = self.prepend_root(path); - let tt = if target.is_absolute() { - self.prepend_root(target) - } else { - target.to_path_buf() - }; - unix_fs::symlink(&tt, &pp).unwrap_or_else(|_| { - panic!("could not create symlink at {} pointing to {}, \ - debug info:\n{}", - pp.display(), - tt.display(), - self.debug_info,) - }); - if self.parent_is_watched(&pp) { - // One event - create. - 1 - } else { - // No events. - 0 + fn handle_modify_event(&mut self, modify_kind: &ModifyKind, notify_event: &Event) { + trace!("FileWatch::handle_modify_event"); + match modify_kind { + ModifyKind::Any // v5.0.0: kqueue and windows (winnt::FILE_ACTION_MODIFIED) + | ModifyKind::Metadata(MetadataKind::WriteTime) => { // v5.0.0: poll (file was written) + self.handle_data_change_event(DataChange::Any, notify_event) } - } - - fn mkdir_p(&self, path: &Path) -> u32 { - let full_path = self.prepend_root(path); - match self.watched_dirs { - Some(wd) => { - let mut test_path = full_path.clone(); - while !test_path.exists() { - test_path = self.get_parent(&test_path); - } - self.real_mkdir(&full_path); - if wd.contains(&test_path) { - // One event - create. - 1 - } else { - // No events. - 0 - } + ModifyKind::Data(data_change) => { + self.handle_data_change_event(*data_change, notify_event) + } + ModifyKind::Name(rename_mode) => { + self.handle_rename_event(*rename_mode, notify_event) + } + ModifyKind::Metadata(MetadataKind::Any) + | ModifyKind::Metadata(MetadataKind::Ownership) + | ModifyKind::Metadata(MetadataKind::Permissions) => { + self.log_ignored_event(notify_event) + // Changes to ownership and permissions can make files and directories appear or + // disappear. However, in v5.0.0 Ownership is only used for FSEvents and Permissions + // isn't implemented by anything only being found in a single test. + // + // This section also account for MetadataKind::Any because in notify v5.0.0 all uses + // of MetadataKind::Any seem to be related to metadata/attrs broadly but in an way + // that we can't know if its ownership, permissions, or something else without going + // deeper than we probably want to. + // - FSEVents: StreamFlags::INODE_META_MOD 'only described as "metadata changed"' + // - inotify: EventMask::ATTRIB which might be permissions and ownership + // - kqueue: kqueue::Vnode::Attrib might also be permissions and ownership + } + ModifyKind::Metadata(MetadataKind::AccessTime) // we don't care about mere file access + | ModifyKind::Metadata(MetadataKind::Extended) // FSEvents: macOS Extended Attributes + | ModifyKind::Metadata(MetadataKind::Other) // FSEvents: Finder info + | ModifyKind::Other => { // v5.0.0 - Not Implemented + self.log_ignored_event(notify_event) + } + } + } + + fn handle_remove_event(&mut self, remove_kind: RemoveKind, notify_event: &Event) { + trace!("FileWatch::handle_remove_event"); + match remove_kind { + RemoveKind::Any // v5.0.0: FSEvent, poll, and windows + // JAH: Does Any require special handling on poll or windows? + | RemoveKind::File => { // v5.0.0: FSEvent and inotify + self.handle_file_disappearance(notify_event); + } + RemoveKind::Folder => { // v5.0.0: FSEvent and inotify + self.handle_folder_disappearance(notify_event); + } + RemoveKind::Other => { // v5.0.0: FSEvent + self.log_ignored_event(notify_event); + } + } + } + + fn handle_rename_event(&mut self, rename_mode: RenameMode, notify_event: &Event) { + trace!("FileWatch::handle_rename_event"); + match rename_mode { + RenameMode::To => { // v5.0.0: inotify and windows + // Per notify crate doc "An event emitted on the file or folder resulting from a + // rename." This case aligns well with the handle_file_appearance requirements. + self.handle_file_appearance(notify_event); + } + RenameMode::From => { // v5.0.0: FSEvents, inotify, and windows + // Per notify crate doc "An event emitted on the file or folder that was renamed." + // This case aligns well with handle_file_disappearance + self.handle_file_disappearance(notify_event); + } + RenameMode::Both => { // v5.0.0: inotify + // Per notify crate doc "A single event emitted with both the From and To paths. + // This event should be emitted when both source and target are known. The paths + // should be provided in this exact order (from, to)." We should be able to leverage + // handle_file_appearance and handle_file_disappearance here. Its a bit hackish + // and wasteful but given that what is known about the state of everything we can + // just call handle_file_appearance and handle_file_disappearance back to back with + // the same argument because its reasonable to assume that the values in the paths + // Vec are unique paths so only one of them should trigger. + self.handle_file_appearance(notify_event); + self.handle_file_disappearance(notify_event); + } + RenameMode::Any // v5.0.0: FSEvent and kqueue + | RenameMode::Other => { // v5.0.0: not implemented + self.log_ignored_event(notify_event); + } + } + } + + /// File Appearance means + /// * a valid WatchedFile + /// * in WatchedFileState::ExistentDirectory + /// * receiving an creation or rename event matching one of the events we watch + /// * where WatchedFile.path matches one of the paths in the event + fn handle_file_appearance(&mut self, notify_event: &Event) { + trace!("FileWatcher::handle_file_appearance\n-=> {:?}\n-=> {:?}\n", + notify_event, + self.watched_file); + for path_buf in notify_event.paths.iter() { + let pb = match path_buf.canonicalize() { + Ok(v) => v, + Err(e) => { + debug!("failed canonicalization: {:?}", e); + path_buf.to_path_buf() } - None => { - self.real_mkdir(&full_path); - 0 + }; + if self.watched_file.state == WatchedFileState::ExistentDirectory { + if self.watched_file.path == pb || self.watched_file.directory == pb { + match self.manage_watches() { + Ok(_) => { + self.callbacks.file_appeared(&self.watched_file.path); + break; + } + Err(e) => { + debug!("While managing watches: {:?}", e); + self.log_ignored_path(notify_event, path_buf) + } + } + } else { + debug!("fell through"); + self.log_ignored_path(notify_event, path_buf); } } } + } - fn real_mkdir(&self, real_path: &Path) { - fs::create_dir_all(&real_path).unwrap_or_else(|_| { - panic!("could not create directories up to {}, \ - debug info:\n{}", - real_path.display(), - self.debug_info,) - }); - } - - fn touch(&self, path: &Path) -> u32 { - let pp = self.prepend_root(path); - File::create(&pp).unwrap_or_else(|_| { - panic!("could not create file {}, debug info:\n{}", - pp.display(), - self.debug_info,) - }); - if self.parent_is_watched(&pp) { - // One event - create. - 1 - } else { - // No events. - 0 - } - } - - fn mv(&self, from: &Path, to: &Path) -> u32 { - let ff = self.prepend_root(from); - let tt = self.prepend_root(to); - fs::rename(&ff, &tt).unwrap_or_else(|_| { - panic!("could not move from {} to {}, debug info:\n{}", - ff.display(), - tt.display(), - self.debug_info,) - }); - match (self.parent_is_watched(&ff), self.parent_is_watched(&tt)) { - (true, true) | (true, false) => { - if self.path_is_watched(&ff) { - // Since we are watching both moved path and - // its parent, we are going to receive double - // notice remove event followed by the rename - // event. - 3 - } else { - // Two events - notice remove, and rename or - // remove. - 2 - } - } - (false, true) => { - // One event - create. - 1 + /// File Modification means + /// * a valid WatchedFile + /// * in WatchedFileState::ExistentFile + /// * receiving an event matching one of the various modification events we watch + /// * where WatchedFile.path matches one of the paths in the event + fn handle_file_modification(&mut self, notify_event: &Event) { + trace!("FileWatcher::handle_file_modification\n-=> {:?}\n-=> {:?}\n", + notify_event, + self.watched_file); + for path_buf in notify_event.paths.iter() { + let pb = match path_buf.canonicalize() { + Ok(v) => v, + Err(e) => { + debug!("failed canonicalization: {:?}", e); + path_buf.to_path_buf() } - (false, false) => { - // No events. - 0 + }; + if self.watched_file.state == WatchedFileState::ExistentFile { + if self.watched_file.path == pb || self.watched_file.directory == pb { + match self.manage_watches() { + Ok(_) => { + self.callbacks.file_modified(&self.watched_file.path); + break; + } + Err(e) => { + debug!("While managing watches: {:?}", e); + self.log_ignored_path(notify_event, path_buf) + } + } + } else { + debug!("fell through"); + self.log_ignored_path(notify_event, path_buf); } } } + } - fn rm_rf(&mut self, path: &Path) -> u32 { - let pp = self.prepend_root(path); - let metadata = match pp.symlink_metadata() { - Ok(m) => m, + /// File Disappearance means + /// * a valid WatchedFile + /// * in WatchedFileState::ExistentFile + /// * receiving an remove or rename event matching one of the events we watch + /// * where WatchedFile.path matches one of the paths in the event + fn handle_file_disappearance(&mut self, notify_event: &Event) { + trace!("FileWatcher::handle_file_disappearance\n-=> {:?}\n-=> {:?}\n", + notify_event, + self.watched_file); + for path_buf in notify_event.paths.iter() { + let pb = match path_buf.canonicalize() { + Ok(v) => v, Err(e) => { - match e.kind() { - ErrorKind::NotFound => return 0, - _ => { - panic!("Failed to stat {}: {}, debug info:\n{}", - pp.display(), - e, - self.debug_info,) + debug!("failed canonicalization: {:?}", e); + path_buf.to_path_buf() + } + }; + if self.watched_file.state == WatchedFileState::ExistentFile { + if self.watched_file.path == pb || self.watched_file.directory == pb { + match self.manage_watches() { + Ok(_) => { + self.callbacks.file_disappeared(&self.watched_file.path); + break; + } + Err(e) => { + debug!("While managing watches: {:?}", e); + self.log_ignored_path(notify_event, path_buf) } } + } else { + debug!("fell through"); + self.log_ignored_path(notify_event, path_buf); } - }; - let event_count = self.get_event_count_on_rm_rf(&pp); - if metadata.is_dir() { - fs::remove_dir_all(&pp).unwrap_or_else(|err| { - panic!("failed to remove directory {}: {}, debug \ - info:\n{}", - pp.display(), - err, - self.debug_info,) - }); - } else { - fs::remove_file(&pp).unwrap_or_else(|err| { - panic!("failed to remove file {}: {}, debug info:\n{}", - pp.display(), - err, - self.debug_info,) - }); } - event_count } + } - fn get_event_count_on_rm_rf(&self, top_path: &Path) -> u32 { - let mut queue = VecDeque::new(); - queue.push_back(top_path.to_path_buf()); - let mut event_count = 0; - while let Some(path) = queue.pop_front() { - if !self.parent_is_watched(&path) { - continue; - } - // Two events for each deletion in the watched path - - // remove notice and remove. - event_count += 2; - let metadata = match path.symlink_metadata() { - Ok(m) => m, - Err(err) => { - panic!("Failed to stat {}: {}, debug info:\n{}", - path.display(), - err, - self.debug_info,) + fn handle_folder_disappearance(&mut self, notify_event: &Event) { + trace!("FileWatcher::handle_folder_disappearance\n-=> {:?}\n-=> {:?}\n", + notify_event, + self.watched_file); + if self.watched_file.state != WatchedFileState::NonExistent { + for path_buf in notify_event.paths.iter() { + let pb = match path_buf.canonicalize() { + Ok(v) => v, + Err(e) => { + debug!("failed canonicalization: {:?}", e); + path_buf.to_path_buf() } }; - if !metadata.is_dir() { - continue; + if self.watched_file.path == pb || self.watched_file.directory == pb { + match self.manage_watches() { + Ok(_) => { + break; + } + Err(e) => { + debug!("While managing watches: {:?}", e); + self.log_ignored_path(notify_event, path_buf) + } + } + } else { + debug!("fell through"); + self.log_ignored_path(notify_event, path_buf); } - queue.extend(self.get_dir_contents(&path)); } - event_count } + } + + // Logging that a particular event was ignored. Ignoring an event may be + // the correct thing to do so the purpose of logging that a particular event + // was ignored event is to highlight the fact that an event was ignored in + // case the decision made today to ignore that event needs to be changed + // due to design changes, lessons learned (bugs) or notify crate updates. + fn log_ignored_event(&mut self, notify_event: &Event) { trace!("IGNORING {:?}", notify_event) } + + // Logging the fact that a particular path was ignored. Ignoring a given + // path may be the correct thing to do so the purpose of logging that a + // particular path was ignored is to highlight that the path was ignored in + // case the decision made today to ignore the event needs to be changed + // due to design changes, lessons learned (bugs) or notify crate updates. + fn log_ignored_path(&mut self, notify_event: &Event, path_buf: &Path) { + trace!("IGNORING {:?} of {:?}", path_buf, notify_event) + } +} + +// scenario, that involves symlinks. +#[cfg(test)] +mod tests { - fn get_dir_contents(&self, path: &Path) -> Vec { - fs::read_dir(&path).unwrap_or_else(|err| { - panic!("failed to read directory {}: {}, debug info:\n{}", - path.display(), - err, - self.debug_info,) - }) - .map(|rde| { - rde.unwrap_or_else(|err| { - panic!("failed to get entry for {}: {}, debug info:\n{}", - path.display(), - err, - self.debug_info,) - }) - .path() - }) - .collect() + use super::*; + use std::path::{Path, + PathBuf}; + + use log::trace; + use multimap::MultiMap; + use tempfile::tempdir; + + use crate::error::Result; + + use super::Callbacks; + + mod watched_file { + + use super::*; + + #[test] + fn get_enclosing_directory() { + // logger::init(); + + // testing a possible file that has a file extension + let mut path = Path::new("/some/path/foo.txt"); + let mut expectation = Some(PathBuf::from("/some/path")); + let mut result = WatchedFile::get_enclosing_directory(path); + assert_eq!(result, expectation); + + // testing a possible file that does not have a file extension + path = Path::new("/some/path/foo"); + expectation = Some(PathBuf::from("/some/path")); + result = WatchedFile::get_enclosing_directory(path); + assert_eq!(result, expectation); + + // testing a directory path. While correct behavior this is sketchy usage. Study the + // function we're testing before you leverage it elsewhere in the code base. + path = Path::new("/some/path/"); + expectation = Some(PathBuf::from("/some")); + result = WatchedFile::get_enclosing_directory(path); + assert_eq!(result, expectation); + + // this is testing the None case where root is passed in. + path = Path::new("/"); + expectation = Some(PathBuf::from("/")); + result = WatchedFile::get_enclosing_directory(path); + assert_eq!(result, expectation); + + // testing a pathological case + path = Path::new(""); + expectation = None; + result = WatchedFile::get_enclosing_directory(path); + assert_eq!(result, expectation); + } + + #[test] + fn discern_state_of() -> Result<()> { + // crate::logger::init(); + + // what to test + // (1a) existing file with extension + // (1b) existing file without extension + // (2) existent directory / is_dir() violated + // (3a) ends with a path separator violation + // (3b) ends with a path separator acceptance (file without extension) + // (4) begins with a root dir or prefix violation + // (5a) parsable enclosing directory that exists + // (5b) parsable enclosing directory that does not exist + // (6) valid path but neither file nor directory exist + // (7) else case: pathlogical case of empty string passed in + // (7x) else case: xxx + + // TESTING (4) + let mut p = Path::new("path/without/root/foo.txt"); + assert_eq!(WatchedFile::discern_state_of(p), + WatchedFileState::Invalid(errmsg::invalid_root(p))); + + // TESTING (3a) + p = Path::new("/path/ending/in/file/separator/"); + assert_eq!(WatchedFile::discern_state_of(p), + WatchedFileState::Invalid(errmsg::invalid_ending(p))); + + let tempdir = tempdir()?; + let dir_a = tempdir.path().join("a"); + fs::create_dir(&dir_a)?; + + // TESTING (2) + assert_eq!(WatchedFile::discern_state_of(dir_a.as_path()), + WatchedFileState::Invalid(errmsg::is_directory(dir_a.as_path()))); + + // TESTING(5a) + let file_foo_txt = dir_a.join("foo.txt"); + let mut state = WatchedFile::discern_state_of(file_foo_txt.as_path()); + assert_eq!(state, WatchedFileState::ExistentDirectory); + + // TESTING (1a) + fs::create_file(&file_foo_txt)?; + state = WatchedFile::discern_state_of(file_foo_txt.as_path()); + assert_eq!(state, WatchedFileState::ExistentFile); + + // TESTING (5b), (6) + let dir_b = tempdir.path().join("b"); + let file_bar = dir_b.join("bar"); + state = WatchedFile::discern_state_of(file_bar.as_path()); + assert_eq!(state, WatchedFileState::NonExistent); + + // TESTING (1b), (3b) + fs::create_dir(&dir_b)?; + fs::create_file(&file_bar)?; + state = WatchedFile::discern_state_of(file_bar.as_path()); + assert_eq!(state, WatchedFileState::ExistentFile); + + // TESTING: (7) + p = Path::new(""); + state = WatchedFile::discern_state_of(p); + assert_eq!(state, WatchedFileState::Invalid(errmsg::invalid_root(p))); + + Ok(()) } - fn parent_is_watched(&self, path: &Path) -> bool { - self.path_is_watched(&self.get_parent(path)) + #[test] + #[cfg(not(windows))] + fn begins_with_a_root_dir_or_prefix() { + let a = Path::new("/path/beginning/with/separator/"); + assert!(WatchedFile::begins_with_a_root_dir_or_prefix(a)); + + let b = Path::new("path/not/beginning/with/separator/"); + assert!(!WatchedFile::begins_with_a_root_dir_or_prefix(b)); + + let c = Path::new("/path/beginning/with/separator/but/not/ending/with/one"); + assert!(WatchedFile::begins_with_a_root_dir_or_prefix(c)); + + let d = Path::new("path/not/beginning/with/separator/and/not/ending/with/one"); + assert!(!WatchedFile::begins_with_a_root_dir_or_prefix(d)); } - fn path_is_watched(&self, path: &Path) -> bool { - match self.watched_dirs { - Some(wd) => wd.contains(path), - None => false, - } + #[test] + #[cfg(windows)] + fn windows_begins_with_a_root_dir_or_prefix() { + // These test are modelled after and extend from the Example section in + // https://doc.rust-lang.org/stable/std/path/enum.Prefix.html + assert!(WatchedFile::begins_with_a_root_dir_or_prefix(Path::new(r"C:\Users\Rust\Pictures\Ferris"))); + assert!(WatchedFile::begins_with_a_root_dir_or_prefix(Path::new(r"\\server\share"))); + assert!(WatchedFile::begins_with_a_root_dir_or_prefix(Path::new(r"\\?\pictures\kittens"))); + assert!(WatchedFile::begins_with_a_root_dir_or_prefix(Path::new(r"\\?\pictures\kittens"))); + assert!(WatchedFile::begins_with_a_root_dir_or_prefix(Path::new(r"\\?\UNC\server\share"))); + assert!(WatchedFile::begins_with_a_root_dir_or_prefix(Path::new(r"\\?\c:\"))); + assert!(WatchedFile::begins_with_a_root_dir_or_prefix(Path::new(r"\\.\BrainInterface"))); + assert!(WatchedFile::begins_with_a_root_dir_or_prefix(Path::new(r"\\server\share"))); + // windows style backslash with and with out trailing slash but lacking preceding \ + assert!(!WatchedFile::begins_with_a_root_dir_or_prefix(Path::new(r"Users\Rust\Pictures\Ferris"))); + assert!(!WatchedFile::begins_with_a_root_dir_or_prefix(Path::new(r"Users\Rust\Pictures\Ferris\"))); + // windows style backslash with and with out trailing slash with preceding \ + assert!(!WatchedFile::begins_with_a_root_dir_or_prefix(Path::new(r"\Users\Rust\Pictures\Ferris"))); + assert!(!WatchedFile::begins_with_a_root_dir_or_prefix(Path::new(r"\Users\Rust\Pictures\Ferris\"))); } - fn get_parent(&self, path: &Path) -> PathBuf { - path.parent() - .unwrap_or_else(|| { - panic!("path {} has no parent, debug info:\n{}", - path.display(), - self.debug_info) - }) - .to_owned() + #[test] + #[cfg(not(windows))] + fn ends_with_a_path_separator() { + let a = Path::new("/path/ending/with/separator/"); + assert!(WatchedFile::ends_with_a_path_separator(a)); + + let b = Path::new("/path/not/ending/with/separator"); + assert!(!WatchedFile::ends_with_a_path_separator(b)); + + let c = Path::new("path/not/beginning/with/separator/but/ending/with/one/"); + assert!(WatchedFile::ends_with_a_path_separator(c)); + + let d = Path::new("path/not/beginning/with/separator/and/not/ending/with/one"); + assert!(!WatchedFile::ends_with_a_path_separator(d)); } - fn prepend_root(&self, p: &Path) -> PathBuf { - prepend_root_impl(self.root, p, self.debug_info) + #[test] + #[cfg(windows)] + fn windows_ends_with_a_path_separator() { + let a = Path::new(r"\\server\share\path\ending\with\separator\"); + assert!(WatchedFile::ends_with_a_path_separator(a)); + + let b = Path::new(r"\\server\share\path\not\ending\with\separator"); + assert!(!WatchedFile::ends_with_a_path_separator(b)); } } - struct TestCaseRunner { - debug_info: DebugInfo, - // We don't use this field anywhere, but it will drop the temp - // dir when TestCaseRunner goes away. - #[allow(dead_code)] - tmp_dir: TempDir, - root: PathBuf, - } + mod file_watcher { - impl TestCaseRunner { - fn new() -> Self { - let tmp_dir = - TempDir::new().unwrap_or_else(|_| panic!("couldn't create temporary directory",)); - let root = tmp_dir.path().to_owned(); - Self { debug_info: DebugInfo::new(), - tmp_dir, - root } - } + use super::*; - fn run_init_commands(&mut self, commands: &[InitCommand]) { - let fs_ops = self.get_fs_ops_init(); - for command in commands { - match command { - InitCommand::MkdirP(ref path) => { - fs_ops.mkdir_p(path); - } - InitCommand::Touch(ref path) => { - fs_ops.touch(path); - } - InitCommand::LnS(ref target, ref path) => { - fs_ops.ln_s(target, path); - } - } - } - } + #[test] + #[allow(unused_must_use)] // Just testing instantion ability so this is OK + fn create_file_watcher() -> Result<()> { + crate::logger::init(); - fn prepare_watcher(&mut self, tc_init_path: &Option) -> WatcherSetup { - let init_path = tc_init_path.clone().unwrap_or_else(|| pb!("/a/b/c/d/e/f")); - let additional_dirs = self.get_additional_directories_from_root(); - let callbacks = TestCallbacks::new(&additional_dirs); - let watcher = FileWatcher::<_, TestWatcher>::create(self.prepend_root(&init_path), - callbacks, - true).unwrap_or_else(|_| { - panic!("failed to create \ - watcher, debug \ - info:\n{}", - self.debug_info,) - }); - WatcherSetup { init_path, watcher } - } + // testing the case that neither the file nor the enclosing directory exists + let p = Path::new("/some/thing/that/does/not/exist.txt"); + let mut fw = super::create_file_watcher(&p, TestCallbacks::default(), true)?; + assert_eq!(fw.watched_file.state, WatchedFileState::NonExistent); - fn run_steps(&mut self, - mut setup: WatcherSetup, - tc_initial_file: &Option, - steps: &[Step]) { - let mut initial_file = tc_initial_file.clone(); - let mut actual_initial_file = setup.watcher.initial_real_file.clone(); + let tempdir = tempdir()?; + let pb = PathBuf::new(); + let tdpath = tempdir.path(); + let dir_a = pb.join(tdpath).join("a"); + let file_foo_txt = dir_a.join("foo.txt"); + let file_foo = dir_a.join("foo"); - let is_poll_watcher = match &setup.watcher.get_mut_underlying_watcher().real_watcher { - SupWatcher::Native(_watcher) => false, - SupWatcher::Fallback(_watcher) => true, - }; + fs::create_dir(dir_a.as_ref()); - for (step_idx, step) in steps.iter().enumerate() { - self.debug_info.push_level(); - self.debug_info - .add(format!("step {}:\n{}", step_idx, dits!(step))); - let expected_event_count = self.execute_step_action(&mut setup, &step.action); - self.spin_watcher(&mut setup, expected_event_count); - - self.test_dirs(&step.dirs, &setup.watcher.paths.dirs); - self.test_paths(&step.paths, &setup.init_path, &setup.watcher.paths.paths); - let real_initial_file = self.test_initial_file(expected_event_count, - &mut initial_file, - &mut actual_initial_file); - - if is_poll_watcher { - self.test_events_polling(real_initial_file, - &step.events, - &mut setup.watcher.get_mut_callbacks().events); - } else { - self.test_events(real_initial_file, - &step.events, - &mut setup.watcher.get_mut_callbacks().events); - } - self.debug_info.pop_level(); - debug!("\n\n\n++++++++++++++++\n++++STEP+END++++\n++++++++++++++++\n\n\n"); - } + // existing directory, non-existent file with extension, false ignore_initial + fw = super::create_file_watcher(&file_foo_txt, TestCallbacks::default(), false)?; + assert_eq!(fw.watched_file.state, WatchedFileState::ExistentDirectory); - debug!("\n\n\n================\n=TEST=CASE=ENDS=\n================\n\n\n"); - } + // existing directory, non-existent file with extension, true ignore_initial + fw = super::create_file_watcher(&file_foo_txt, TestCallbacks::default(), true)?; + assert_eq!(fw.watched_file.state, WatchedFileState::ExistentDirectory); - fn execute_step_action(&mut self, setup: &mut WatcherSetup, action: &StepAction) -> u32 { - let tw = setup.watcher.get_mut_underlying_watcher(); - let iterations = { - let mut fs_ops = self.get_fs_ops_with_dirs(&tw.watched_dirs); - - match action { - StepAction::LnS(ref target, ref path) => fs_ops.ln_s(target, path), - StepAction::MkdirP(ref path) => fs_ops.mkdir_p(path), - StepAction::Touch(ref path) => fs_ops.touch(path), - StepAction::Mv(ref from, ref to) => fs_ops.mv(from, to), - StepAction::RmRF(ref path) => fs_ops.rm_rf(path), - StepAction::Nop => 0, - } - }; - self.debug_info - .add(format!("expected iterations: {}", iterations)); - iterations - } + // existing directory, non-existent file lacking an extension, false ignore_initial + fw = super::create_file_watcher(&file_foo, TestCallbacks::default(), false)?; + assert_eq!(fw.watched_file.state, WatchedFileState::ExistentDirectory); - fn spin_watcher(&self, setup: &mut WatcherSetup, expected_event_count: u32) { - let mut iteration = 0; + // existing directory, non-existent file lacking an extension, true ignore_initial + fw = super::create_file_watcher(&file_foo, TestCallbacks::default(), true)?; + assert_eq!(fw.watched_file.state, WatchedFileState::ExistentDirectory); - let is_poll_watcher = match &setup.watcher.get_mut_underlying_watcher().real_watcher { - SupWatcher::Native(_watcher) => false, - SupWatcher::Fallback(_watcher) => true, - }; + fs::create_file(&file_foo); - let mut iterations = expected_event_count; - - // Through experimentation it was determined that the PollWatcher is less responsive - // and emits more events than the NotifyWatcher. The initial sleep used in - // NotifyWatcher was not adequate to pass the tests and was increased as a - // result. Also, the number of iterations required is larger for the - // PollWatcher case as there were intermediate events observed that would - // lead to test case failure with the original iteration count used. - // The test cases will fail if the desired events are not emitted, so the iteration - // count was increased to allow for that. - if is_poll_watcher { - thread::sleep(Duration::from_secs(5)); - iterations *= 3; - } else { - thread::sleep(Duration::from_secs(3)); - } + // existing directory, existing file lacking an extension, false ignore_initial + fw = super::create_file_watcher(&file_foo, TestCallbacks::default(), false)?; + assert_eq!(fw.watched_file.state, WatchedFileState::ExistentFile); - while iteration < iterations { - setup.watcher - .single_iteration() - .unwrap_or_else(|_| { - panic!("iteration failed, debug info:\n{}", self.debug_info,) - }); - let cb = setup.watcher.get_mut_callbacks(); - if cb.ignore { - debug!("got event below tmpdir, not increasing the iteration counter"); - cb.ignore = false; - } else { - iteration += 1; - } - } - } + // existing directory, existing file lacking an extension, true ignore_initial + fw = super::create_file_watcher(&file_foo, TestCallbacks::default(), true)?; + assert_eq!(fw.watched_file.state, WatchedFileState::ExistentFile); + + fs::create_file(&file_foo_txt); - fn test_dirs(&mut self, - step_dirs: &HashMap, - actual_dirs: &HashMap) { - let expected_dirs = self.fixup_expected_dirs(step_dirs); - self.debug_info - .add(format!("fixed up expected dirs:\n{}", dits!(expected_dirs),)); - assert_eq!(&expected_dirs, actual_dirs, - "comparing watched directories, debug info:\n{}", - self.debug_info,); - } + // existing directory, existing file with an extension, false ignore_initial + fw = super::create_file_watcher(&file_foo_txt, TestCallbacks::default(), false)?; + assert_eq!(fw.watched_file.state, WatchedFileState::ExistentFile); + + // existing directory, existing file with an extension, true ignore_initial + fw = super::create_file_watcher(&file_foo_txt, TestCallbacks::default(), true)?; + assert_eq!(fw.watched_file.state, WatchedFileState::ExistentFile); - fn test_paths(&mut self, - step_paths: &HashMap, - init_path: &Path, - actual_paths: &HashMap) { - let expected_paths = self.fixup_expected_paths(step_paths, init_path); - self.debug_info - .add(format!("fixed up expected paths:\n{}", dits!(expected_paths),)); - self.compare_paths(expected_paths, actual_paths); - } + // while the previous test included the ignore_initial in the permutations the following + // tests explicitly test the behavior for correctness - fn test_initial_file(&mut self, - iterations: u32, - initial_file: &mut Option, - actual_initial_file: &mut Option) - -> Option { - if iterations > 0 { - let expected_initial_file = - initial_file.take().map(|path| self.prepend_root(&path)); - self.debug_info - .add(format!("fixed up initial file: {:?}", expected_initial_file)); - assert_eq!(expected_initial_file, - actual_initial_file.take(), - "comparing initial path, debug info:\n{}", - self.debug_info,); - expected_initial_file - } else { - None - } - } + // If ignore_initial is passed as false and the WatchedFile exists when this is called + // then the first loop will emit an initial "file_appeared" event. - // For the poll watcher there are more than one Debounced event received - // and this test will fail. Instead we can look at the last event and - // ensure that it is correct, as it will be the last entry that will - // determine the final state of the watched. - fn test_events_polling(&mut self, - real_initial_file: Option, - step_events: &[NotifyEvent], - actual_events: &mut Vec) { - let expected_events = self.fixup_expected_events(step_events, real_initial_file); - self.debug_info - .add(format!("fixed up expected events: {:?}", expected_events)); - assert_eq!(expected_events.last(), - actual_events.last(), - "comparing expected events, debug info:\n{}", - self.debug_info,); - actual_events.clear(); - } + // let tempdir = tempdir()?; + // let dir_a = tempdir.path().join("a"); + // fs::create_dir(&dir_a)?; + // let file_foo = dir_a.join("foo.txt"); + // fs::create_file(&file_foo)?; - fn test_events(&mut self, - real_initial_file: Option, - step_events: &[NotifyEvent], - actual_events: &mut Vec) { - let expected_events = self.fixup_expected_events(step_events, real_initial_file); - self.debug_info - .add(format!("fixed up expected events: {:?}", expected_events)); - assert_eq!(&expected_events, actual_events, - "comparing expected events, debug info:\n{}", - self.debug_info,); - actual_events.clear(); - } + // Test the case that the file exists, false is passed, and the event is sent + let mut tc = TestCallbacks::default(); + fw = super::create_file_watcher(&file_foo, tc, false)?; + fw.single_iteration()?; + assert_eq!(1, fw.callbacks.all_callbacks_count()); + assert_eq!(1, fw.callbacks.path_instance_count(&file_foo)); - // For performing initial setup of watcher directories. This occurs - // before creation of the watcher. - fn get_fs_ops_init(&mut self) -> FsOps<'_> { - FsOps { debug_info: &mut self.debug_info, - root: &self.root, - watched_dirs: None, } - } + // Test the case that the file exists, true is passed, and the event is not sent + tc = TestCallbacks::default(); + fw = super::create_file_watcher(&file_foo, tc, true)?; + fw.single_iteration()?; + assert_eq!(0, fw.callbacks.all_callbacks_count()); + assert_eq!(0, fw.callbacks.path_instance_count(&file_foo)); - // For running watcher tests, this requires a WatcherType so we can - // delineate between the NotifyWatcher and PollWatcher specific behaviors. - fn get_fs_ops_with_dirs<'a>(&'a mut self, watched_dirs: &'a HashSet) -> FsOps<'a> { - let mut fs_ops = self.get_fs_ops_init(); - fs_ops.watched_dirs = Some(watched_dirs); - fs_ops - } + // JAH: do something to test that improved error message again + // the reasoning for the following is that I found that the messages were janky as I + // transition to Sup's FileNotFound error. This tests a sound use of the message in + // at least one case and while not exhaustive will hopefully serve to keep things sane. + // let s = format!("File not found at: {}", p.display()); + // assert_eq!(r.unwrap_err().to_string(), s); - fn fixup_expected_dirs(&self, dirs: &HashMap) -> HashMap { - let mut expected_dirs = dirs.iter() - .map(|(p, c)| (self.prepend_root(p), *c)) - .collect::>(); - let additional_dirs = self.get_additional_directories_from_root(); - expected_dirs.extend(additional_dirs.iter().cloned().map(|d| (d, 1))); - expected_dirs + Ok(()) } - // Get vector for extending dirs. For the root directory like - // /tmp/foo, the vector will be [`/`, `/tmp`]. - // - // All this is because we run the tests in a temporary - // directory (let's say `/tmp/foo`) and if in test case we - // specify that we expect directories `/` and `/a` to be - // watched, then in reality these will be `/`, `/tmp`, - // `/tmp/foo`, `/tmp/foo/a`. - fn get_additional_directories_from_root(&self) -> Vec { - let mut tmp_path = PathBuf::new(); - let mut additional_dirs = Vec::new(); - - for component in self.root.components() { - match component { - Component::Prefix(p) => tmp_path.push(p.as_os_str()), - Component::RootDir | Component::Normal(_) => { - tmp_path.push(component.as_os_str()); - additional_dirs.push(tmp_path.clone()); - } - // Respectively the `.`. and `..` components of a path. - Component::CurDir | Component::ParentDir => { - panic!("the path should be simplified") - } - }; - } - // Pop the last directory (like `/tmp/foo`), so it will not - // overwrite an already fixed-up directory in expected_dirs. - additional_dirs.pop(); + #[test] + fn handle_create_event() -> Result<()> { + // TEST HARNESS + // crate::logger::init(); + let tempdir = tempdir()?; + let dir_a = tempdir.path().join("a"); + let file_foo_txt = dir_a.join("foo.txt"); + let mut tc = TestCallbacks::default(); + let mut fw = super::create_file_watcher(&file_foo_txt, tc, true)?; + assert_eq!(fw.watched_file.state, WatchedFileState::NonExistent); + + // TESTING: directory creation triggering ExistentDirectory from NonExistent + tc = TestCallbacks::default(); + fw = super::create_file_watcher(&file_foo_txt, tc, true)?; + fs::create_dir(&dir_a)?; + while fw.watched_file.state == WatchedFileState::NonExistent { + fw.single_iteration()?; + thread::sleep(Duration::from_millis(1000)); + } + assert_eq!(fw.watched_file.state, WatchedFileState::ExistentDirectory); + assert_eq!(fw.callbacks.callback_type_count(CallbackType::Appeared), 0); + assert_eq!(fw.callbacks.path_instance_count(&file_foo_txt), 0); + + // TESTING: file creation triggering ExistentFile from ExistentDirectory + tc = TestCallbacks::default(); + fw = super::create_file_watcher(&file_foo_txt, tc, true)?; + fs::create_file(&file_foo_txt)?; + while fw.callbacks.all_callbacks_count() < 1 { + fw.single_iteration()?; + thread::sleep(Duration::from_millis(1000)); + } + assert_eq!(fw.watched_file.state, WatchedFileState::ExistentFile); + assert_eq!(fw.callbacks.callback_type_count(CallbackType::Appeared), 1); + assert_eq!(fw.callbacks.path_instance_count(&file_foo_txt), 1); - additional_dirs + Ok(()) } - fn fixup_expected_paths(&self, - paths: &HashMap, - init_path: &Path) - -> HashMap { - let expected_paths = self.get_initial_expected_paths(paths); - let real_first_expected = self.get_real_first_expected_path(init_path, &expected_paths); - let additional_paths = self.get_additional_paths(&real_first_expected, - // The existence of this path in the - // map is checked in - // get_real_first_expected_path. - &expected_paths[&real_first_expected]); - - self.link_expected_paths_with_additional_ones(expected_paths, - additional_paths, - real_first_expected) - } + #[test] + fn handle_modify_event() -> Result<()> { + // TEST HARNESS + // crate::logger::init(); + let tempdir = tempdir()?; + let dir_a = tempdir.path().join("a"); + let file_foo = dir_a.join("foo"); + + fs::create_dir(&dir_a)?; + fs::create_file(&file_foo)?; + + let tc = TestCallbacks::default(); + let mut fw = super::create_file_watcher(&file_foo, tc, true)?; + assert_eq!(WatchedFileState::ExistentFile, fw.watched_file.state); - fn compare_paths(&mut self, - expected_paths: HashMap, - actual_paths: &HashMap) { - self.compare_path_keys(&expected_paths, actual_paths); - - for (path, path_state) in expected_paths { - // This should not panic - we tested the equality of - // paths in compare_path_keys. - let watched_file = actual_paths.get(&path).unwrap(); - - self.debug_info.push_level(); - self.debug_info.add(format!("path: {}", path.display())); - self.debug_info - .add(format!("watched file:\n{}", dits!(watched_file))); - self.debug_info - .add(format!("path state:\n{}", dits!(path_state))); - self.compare_path_state_with_watched_file(&path_state, watched_file); - self.debug_info.pop_level(); + // TEST EXECUTION + fs::write_to_file(&file_foo)?; + while fw.callbacks.all_callbacks_count() < 1 { + fw.single_iteration()?; + thread::sleep(Duration::from_millis(1000)); } - } - fn fixup_expected_events(&self, - events: &[NotifyEvent], - real_initial_file: Option) - -> Vec { - let mut expected_events = match real_initial_file { - Some(path) => vec![NotifyEvent::appeared(path)], - None => Vec::new(), - }; - expected_events.extend(events.iter() - .map(|e| { - NotifyEvent::new(self.prepend_root(&e.path), e.kind) - })); - expected_events - } + // TEST EVALUATION + assert_eq!(1, fw.callbacks.callback_type_count(CallbackType::Modified)); + assert_eq!(1, fw.callbacks.path_instance_count(&file_foo)); + assert_eq!(WatchedFileState::ExistentFile, fw.watched_file.state); - fn get_initial_expected_paths(&self, - paths: &HashMap) - -> HashMap { - paths.iter() - .map(|(p, s)| { - (self.prepend_root(p), - PathState { kind: s.kind, - path_rest: s.path_rest.clone(), - prev: s.prev.as_ref().map(|p| self.prepend_root(p)), - next: s.next.as_ref().map(|p| self.prepend_root(p)), }) - }) - .collect() + Ok(()) } - fn get_real_first_expected_path(&self, - init_path: &Path, - expected_paths: &HashMap) - -> PathBuf { - let first_expected = get_first_item(init_path); - let real_first_expected = self.prepend_root(&first_expected); - let first_item = expected_paths.get(&real_first_expected).unwrap_or_else(|| { - panic!( - "expected watched item for {} (real: {}), it is an error in the test case", - first_expected.display(), - real_first_expected.display(), - ) - }); - - assert_eq!(first_item.prev, - None, - "expected prev member of first expected path ({}, real: {}) to be None, {}", - first_expected.display(), - real_first_expected.display(), - "it is an error in the test case",); - - real_first_expected - } + #[test] + fn handle_remove_event() -> Result<()> { + // TEST HARNESS + // crate::logger::init(); + let tempdir = tempdir()?; + let dir_a = tempdir.path().join("a"); + let file_foo = dir_a.join("foo"); - fn prepend_root(&self, p: &Path) -> PathBuf { - prepend_root_impl(&self.root, p, &self.debug_info) - } + fs::create_dir(&dir_a)?; + fs::create_file(&file_foo)?; - fn get_additional_paths(&self, - real_first_expected: &Path, - first_item: &PathState) - -> Vec<(PathBuf, PathState)> { - let mut ap_vec = Vec::new(); - // empty additional path states - for path in self.get_additional_paths_from_root() { - ap_vec.push((path, - PathState { kind: PathKind::Directory, - path_rest: Vec::new(), - prev: None, - next: None, })); - } - let ap_len = ap_vec.len(); - // Link the path states - set the prev and next - // members. The first path state will have None `prev` and - // the last path state will have None `next`. - for idx in (0..ap_len).skip(1) { - ap_vec[idx].1.prev = Some(ap_vec[idx - 1].0.clone()); - ap_vec[ap_len - 1 - idx].1.next = Some(ap_vec[ap_len - idx].0.clone()) - } - // Fill path rests in additional path states. - let real_first_expected_path_rest = to_path_rest(real_first_expected); - for (idx, ap_vec_item) in ap_vec.iter_mut().enumerate() { - let path_rest = &mut ap_vec_item.1.path_rest; - // If root/temporary directory is `/tmp/foo/bar` then - // `/tmp` will have path rest `[foo, bar, ]`, `/tmp/foo` - `[bar, ]`, and `/tmp/foo/bar` - `[]`. - path_rest.extend(real_first_expected_path_rest.iter().skip(idx + 1).cloned()); - // Here add the `` part. - path_rest.extend(first_item.path_rest.iter().cloned()); + let tc = TestCallbacks::default(); + let mut fw = super::create_file_watcher(&file_foo, tc, true)?; + assert_eq!(WatchedFileState::ExistentFile, fw.watched_file.state); + + // TEST EXECUTION + fs::remove(&file_foo)?; + + while fw.callbacks.all_callbacks_count() < 1 { + fw.single_iteration()?; + thread::sleep(Duration::from_millis(1000)); } + // Event { kind: Modify(Metadata(Any)), paths: ["/tmp/.tempDir/a/foo"], + // attr:tracker: None, attr:flag: None, attr:info: None, attr:source: None } + // will be ignored + // Event { kind: Remove(File), paths: ["/tmp/.tempDir/a/foo"], attr:tracker: None, + // attr:flag: None, attr:info: None, attr:source: None } + // will be fired + assert_eq!(1, + fw.callbacks.callback_type_count(CallbackType::Disappeared)); + assert_eq!(1, fw.callbacks.path_instance_count(&file_foo)); + assert_eq!(WatchedFileState::ExistentDirectory, fw.watched_file.state); - ap_vec + Ok(()) } + } - fn link_expected_paths_with_additional_ones(&self, - mut expected_paths: HashMap, - mut additional_paths: Vec<(PathBuf, - PathState)>, - real_first_expected: PathBuf) - -> HashMap { - // link last additional path state with the first expected one - if let Some(last) = additional_paths.last_mut() { - // The existence of this path in the map is checked in - // get_real_first_expected_path. - let mut first_item = expected_paths.get_mut(&real_first_expected).unwrap(); - - last.1.next = Some(real_first_expected); - first_item.prev = Some(last.0.clone()); - } + #[derive(Debug, Default)] + struct TestCallbacks { + type_occurrences: MultiMap, + path_occurrences: MultiMap, + } + + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + enum CallbackType { + Appeared, + Modified, + Disappeared, + } - expected_paths.extend(additional_paths); - expected_paths + impl Callbacks for TestCallbacks { + fn file_appeared(&mut self, real_path: &Path) { + trace!("TestCallbacks::file_appeared({:?})", real_path); + self.capture_callback(CallbackType::Appeared, real_path) } - fn compare_path_keys(&self, - expected_paths: &HashMap, - actual_paths: &HashMap) { - let mut expected_paths_keys: Vec<&PathBuf> = expected_paths.keys().collect(); - expected_paths_keys.sort(); - let mut actual_paths_keys: Vec<&PathBuf> = actual_paths.keys().collect(); - actual_paths_keys.sort(); - - assert_eq!(expected_paths_keys, actual_paths_keys, - "comparing paths, debug info:\n{}", - self.debug_info,); + fn file_modified(&mut self, real_path: &Path) { + trace!("TestCallbacks::file_modified({:?})", real_path); + self.capture_callback(CallbackType::Modified, real_path) } - fn compare_path_state_with_watched_file(&self, - path_state: &PathState, - watched_file: &WatchedFile) { - let common = watched_file.get_common(); - let expected_kind = match watched_file { - WatchedFile::Regular(_) => PathKind::Regular, - WatchedFile::MissingRegular(_) => PathKind::MissingRegular, - WatchedFile::Symlink(_) => PathKind::Symlink, - WatchedFile::Directory(_) => PathKind::Directory, - WatchedFile::MissingDirectory(_) => PathKind::MissingDirectory, - }; - let path_rest: Vec = common.path_rest.iter().cloned().collect(); - - assert_eq!(path_state.kind, expected_kind, - "ensuring proper watched file kind, debug info:\n{}", - self.debug_info,); - assert_eq!(path_state.path_rest, path_rest, - "comparing path rest, debug info:\n{}", - self.debug_info,); - assert_eq!(path_state.prev, common.prev, - "comparing prev member, debug info:\n{}", - self.debug_info,); - assert_eq!(path_state.next, common.next, - "comparing next member, debug info:\n{}", - self.debug_info,); + fn file_disappeared(&mut self, real_path: &Path) { + trace!("TestCallbacks::file_disappeared({:?})", real_path); + self.capture_callback(CallbackType::Disappeared, real_path) } + } - // Get vector for extending paths. For the root directory like - // /tmp/foo, the vector will be [`/tmp/`, `/tmp/foo`]. - // - // All this is because we run the tests in a temporary - // directory (let's say `/tmp/foo`) and if in test case we - // specify that we expect the watched items to be `/a` and - // `/a/b`, then in reality these will be `/tmp`, `/tmp/foo`, - // `/tmp/foo/a` and `/tmp/foo/a/b`. - fn get_additional_paths_from_root(&self) -> Vec { - let mut tmp_path = PathBuf::new(); - let mut for_paths = Vec::new(); - - for component in self.root.components() { - match component { - Component::Prefix(_) | Component::RootDir => { - tmp_path.push(component.as_os_str()) - } - Component::Normal(c) => { - tmp_path.push(c); - for_paths.push(tmp_path.to_owned()); - } - // Respectively the `.`. and `..` components of a path. - Component::CurDir | Component::ParentDir => { - panic!("the path should be simplified") - } - }; + impl TestCallbacks { + fn capture_callback

(&mut self, callback_type: CallbackType, path: P) + where P: AsRef + { + self.type_occurrences + .insert(callback_type, path.as_ref().to_path_buf()); + self.path_occurrences + .insert(path.as_ref().to_path_buf(), callback_type); + } + + fn all_callbacks_count(&self) -> usize { + #[allow(unused_variables)] // k isn't used below + self.type_occurrences + .iter_all() + .fold(0, |acc, (k, v)| acc + v.len()) + } + + fn callback_type_count(&self, callback_type: CallbackType) -> usize { + self.type_occurrences.get(&callback_type).iter().count() + } + + fn path_instance_count

(&self, path: P) -> usize + where P: AsRef + { + self.path_occurrences + .get(&path.as_ref().to_path_buf()) + .iter() + .count() + } + + fn _path_count(&self) -> usize { + #[allow(unused_variables)] // k isn't used below + self.path_occurrences + .iter_all() + .fold(0, |acc, (k, v)| acc + v.len()) + } + } + + mod fs { + use log::trace; + use std::{fmt::Debug, + fs::File, + io, + io::Write, + path::Path}; + + /// Creates a file, writes some "Lorem ipsem", and syncs the file. You should create the + /// containing directory before calling it + // pub fn create_file

(path: P) -> io::Result + // where P: AsRef + Debug + pub fn create_file(path: &Path) -> io::Result { + trace!("fs::create_file({:?})", path); + let mut file = std::fs::File::create(&path)?; + file.write_all(b"Lorem ipsum dolor sit amet,....")?; + file.sync_all()?; + Ok(file) + } + + /// Create a directory. This is assumes "a complete path" from root to final directory. + /// This function creates intermediate directories. + // pub fn create_dir(path: T) -> io::Result<()> + // where T: AsRef + Debug + pub fn create_dir(path: &Path) -> io::Result<()> { + trace!("fs::create_dir({:?})", path); + std::fs::create_dir_all(path) + } + + /// if the path passed in + pub fn remove(path: T) -> io::Result<()> + where T: AsRef + Debug + { + trace!("fs::remove({:?})", path); + let p = path.as_ref(); + if p.is_dir() { + std::fs::remove_dir_all(&path) + // Errors when path doesn’t exist, isn’t a directory, we lack permissions, + // or directory isn’t empty. + } else { + std::fs::remove_file(&path) + // Errors when file is a dir, doesn't exist, or we lack permissions to delete. } - - for_paths } - } - fn prepend_root_impl(root: &Path, p: &Path, debug_info: &DebugInfo) -> PathBuf { - if !p.is_absolute() { - panic!("expected path {} to be absolute, debug info:\n{}", - p.display(), - debug_info,); + pub fn write_to_file(path: &Path) -> io::Result { + trace!("fs::write_to_file({:?})", path); + let mut file = std::fs::OpenOptions::new().write(true) + .create(true) + .open(path)?; + file.write_all(b"Lorem ipsum dolor sit amet,....")?; + file.sync_all()?; + Ok(file) } - root.join(strip_prefix_and_root(p)) + + // pub fn mv(from: T, to: T) -> io::Result<()> + // where T: AsRef + Debug + // { + // trace!("fs::mv {:?} {:?} ", from, to); + // std::fs::rename(from, to) + // } + + // pub fn ln_s(target: T, link: T) -> io::Result<()> + // where T: AsRef + Debug + // { + // trace!("fs::ln_s {:?} {:?} ", target, link); + // std::os::unix::fs::symlink(&target, &link) + // } } +} - fn get_first_item(path: &Path) -> PathBuf { - let mut first = PathBuf::new(); +// These are all of the various scenarios I could think of that could impact file appearance and +// disappearance from the command line. captured using unix commands I think they all have windows +// equivalents but I'm not as comfortable "thinking windows" as I am "thinking unix". An entry with +// a + is one that I'm going to try and test and one with a - means that testing will be skipped +// because of knowledge of what the notify crate does. Currently that means no testing of ownership +// or permissions changes because they aren't exposed well in the notify crate because due to +// implementation issues with. Also, neither concept seems to have been a concern in the previous +// version of the code so this massive update shouldn't be breaking anything as regards permissions +// and ownership that wasn't broken before. +// +// single file +// + creation (touch foo) +// + deletion (rm foo) +// + rename (mv foo bar) +// + move (mv foo ../a/foo) +// + move and rename (mv foo ../a/bar) +// multiple file +// + creation (touch foo bar baz) +// + deletion (rm foo bar baz) +// + move (mv foo bar baz ../dir/) +// single directory +// + create (mkdir a/) +// + deletion (rmdir a/) +// + rename (mv a/ b/) +// + move (mv a/ ../b/) +// + move and rename (mv a/ ../b/c) +// multiple directories +// + creation (mkdir a/ b/ c/) +// + deletion (rmdir a/ b/ c/) +// + rename (mv a/ b/) +// + move (mv a/ ../b/) +// + move and rename (mv a/ ../b/c) +// links +// + file symlink creation (ln -s foo bar) +// + file symlink deletion (rm bar@) +// + file symlink move (mv @bar baz) +// + file hard link creation (ln foo baz) +// + file hard link deletion (rm baz) +// + file hard link move (mv baz qux) +// permissions +// - permissions changed to make file visible +// - permissions changed to make file invisible +// ownership +// - ownership changed to make file visible +// - ownership changed to make file invisible +// +// Besides file appearance and disappearance we are concerned with file modifications which are well +// covered by the notify crate. At the time of this commenting what we should test can be reviewed +// by considering the follow cases in the code. +// * need_rescan() +// * AccessKind::Close(AccessMode::Write) +// * DataChange events as documented in handle_data_change_events +// +// One final thought is that the previous version of the code used "#[cfg(all(unix, test))]" to +// begin its test section so this version of the code will be explicitly testing windows for the +// first time since whenever that was added and may uncover more things. Also, it would be ideal to +// add explicit testing for the polling back end alongside the windows one. +// +// More information may be found in in the comments embedded inline with the code. +// +// Some random bits of doc collected along the way +// https://man7.org/linux/man-pages/man7/inotify.7.html +// https://www.freebsd.org/cgi/man.cgi?kqueue +// +// What Watches What Using FileWatcher +// 1) UserConfigWatch watches individual files with names of the general form +// '/hab/user/myservice/config/user.toml' +// 2) PeerWatcher watches a file for the purposes of "connecting to the ring" +// with an expected naming convention like peer_watch_file = "/path/to/file". +// +// What about SupWatcher and SpecWatcher? +// SpecWatcher users SuperWatch which uses the notify crate to watch files in +// SpecDir. The spec directory is /hab/sup/default/specs and this is a distinct +// use of the notify crate separate from the usage in the FileWatcher hierarchy. +// +// Things to consider: +// 1) Is the file valid? If its TOML, YAML, JSON, etc. we could validate before passing on a notice +// - for component in path.components() { - match component { - Component::Prefix(p) => first.push(p.as_os_str()), - Component::RootDir => first.push(component.as_os_str()), - Component::Normal(c) => { - first.push(c); - break; - } - // Respectively the `.`. and `..` components of a path. - Component::CurDir | Component::ParentDir => panic!("the path should be simplified"), - } - } +// What follows are tests that may be completed or tossed depending on exactly what test +// implementation strategy shakes out as this goes along - first - } +// // #[test] +// // fn file_appeared_due_to_directory_create() {} +// // doesn't make sense in practice, directory has to be generating its +// // own event and then files are created in the directory with their +// // own events - fn to_path_rest(path: &Path) -> Vec { - let mut path_rest = Vec::new(); +// #[test] +// fn file_appeared_due_to_symlink_create() {} - for component in path.components() { - match component { - Component::Prefix(_) => (), - Component::RootDir => (), - Component::Normal(c) => path_rest.push(c.to_owned()), - // Respectively the `.`. and `..` components of a path. - Component::CurDir | Component::ParentDir => panic!("the path should be simplified"), - } - } +// #[test] +// fn file_appeared_due_to_permissions_change() {} - path_rest - } +// #[test] +// fn file_appeared_due_to_ownership_change() {} - fn strip_prefix_and_root(path: &Path) -> PathBuf { - let mut stripped = PathBuf::new(); +// #[test] +// fn file_appeared_due_to_file_rename() {} - for component in path.components() { - match component { - Component::Prefix(_) | Component::RootDir => (), - Component::Normal(_) | Component::CurDir | Component::ParentDir => { - stripped.push(component.as_os_str()) - } - } - } +// #[test] +// fn file_disappeared_due_to_file_delete() {} - stripped - } -} +// #[test] +// fn file_disappeared_due_to_directory_delete() {} + +// #[test] +// fn file_disappeared_due_to_symlink_delete() {} + +// #[test] +// fn file_disappeared_due_to_permissions_change() {} + +// // fn file_disappeared_due_to_ownership_change() {} + +// #[test] +// fn file_disappeared_due_to_file_rename() {} + +// #[test] +// fn file_appeared_and_file_disappeared_on_symlink_move() {} +// // think this one through +// // is it enough to have the separate tests above? + +// #[test] +// fn file_modified_due_to_file_write() {} + +// #[test] +// fn file_modified_due_to_() {} + +// #[test] +// fn file_modified_due_to_xxx() {} +// +// #[test] +// `mv foo.txt foo.txt` results in "mv: 'foo.txt' and 'foo.txt' are the same file" but let's +// explicitly test this to make sure that it works across platforms, etc. + +// Things to Test: +// Q: what happens when you have a properly constructed WatchedFile instance and the file is +// deleted? A: This feels like normal usage and should be covered but think about it a moment longer +// later +// +// Q: what happens when you have a properly constructed WatchedFile instance and the file and +// enclosing directory are deleted? A: This feels like an exceptional path. Currently I think this +// would througn an invalid file exception but this needs to be worked through. diff --git a/components/sup/src/manager/spec_watcher.rs b/components/sup/src/manager/spec_watcher.rs index 6668fd2bc5..0618c199fb 100644 --- a/components/sup/src/manager/spec_watcher.rs +++ b/components/sup/src/manager/spec_watcher.rs @@ -8,7 +8,8 @@ use crate::{error::{Error, sup_watcher::SupWatcher}}; use log::{error, trace}; -use notify::{DebouncedEvent, +use notify::{Config, + Event, RecursiveMode, Watcher}; use std::{sync::mpsc::{self, @@ -45,7 +46,7 @@ pub struct SpecWatcher { // purposes (`Drop` kills the threads that the watcher spawns to do // its work). _watcher: SupWatcher, - channel: Receiver, + channel: Receiver>, } impl SpecWatcher { @@ -89,8 +90,9 @@ impl SpecWatcher { fn new(spec_dir: &SpecDir) -> Result { let (tx, rx) = mpsc::channel(); let delay = SpecWatcherDelay::configured_value(); - let mut watcher = SupWatcher::new(tx, delay.0)?; - watcher.watch(spec_dir, RecursiveMode::NonRecursive)?; + let config = Config::default().with_poll_interval(delay.0); + let mut watcher = SupWatcher::new(tx, config)?; + watcher.watch(spec_dir.as_ref(), RecursiveMode::NonRecursive)?; Ok(SpecWatcher { _watcher: watcher, channel: rx, }) } diff --git a/components/sup/src/manager/sup_watcher.rs b/components/sup/src/manager/sup_watcher.rs index 8d0fc235d2..e0762b07ab 100644 --- a/components/sup/src/manager/sup_watcher.rs +++ b/components/sup/src/manager/sup_watcher.rs @@ -1,63 +1,70 @@ //! Watcher interface implementation for Habitat Supervisor. - use habitat_core::package::target::{PackageTarget, AARCH64_DARWIN}; -use log::debug; +use log::{debug, + warn}; use notify::{poll::PollWatcher, - DebouncedEvent, + Config, + EventHandler, RecommendedWatcher, RecursiveMode, Result, - Watcher}; + Watcher, + WatcherKind}; use std::{env, path::Path, - str::FromStr, - sync::mpsc::Sender, - time::Duration}; + str::FromStr}; +#[derive(Debug)] pub enum SupWatcher { Native(RecommendedWatcher), Fallback(PollWatcher), } impl Watcher for SupWatcher { - fn new_raw(tx: Sender) -> Result { - let target = PackageTarget::from_str(&env::var("HAB_STUDIO_HOST_ARCH"). - unwrap_or_default()). - unwrap_or_else(|_| PackageTarget::active_target()); - if target == AARCH64_DARWIN { - Ok(SupWatcher::Fallback(PollWatcher::new_raw(tx).unwrap())) - } else { - Ok(SupWatcher::Native(RecommendedWatcher::new_raw(tx).unwrap())) - } - } - - fn new(tx: Sender, delay: Duration) -> Result { + fn new(event_handler: F, config: Config) -> Result { let target = PackageTarget::from_str(&env::var("HAB_STUDIO_HOST_ARCH"). unwrap_or_default()). unwrap_or_else(|_| PackageTarget::active_target()); if target == AARCH64_DARWIN { debug!("Using pollwatcher"); - Ok(SupWatcher::Fallback(PollWatcher::new(tx, delay).unwrap())) + Ok(SupWatcher::Fallback(PollWatcher::new(event_handler, config).unwrap())) } else { debug!("Using native watcher"); - Ok(SupWatcher::Native(RecommendedWatcher::new(tx, delay).unwrap())) + Ok(SupWatcher::Native(RecommendedWatcher::new(event_handler, config).unwrap())) } } - fn watch>(&mut self, path: P, recursive_mode: RecursiveMode) -> Result<()> { + fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { match self { SupWatcher::Native(watcher) => watcher.watch(path, recursive_mode), SupWatcher::Fallback(watcher) => watcher.watch(path, recursive_mode), } } - fn unwatch>(&mut self, path: P) -> Result<()> { + fn unwatch(&mut self, path: &Path) -> Result<()> { match self { SupWatcher::Native(watcher) => watcher.unwatch(path), SupWatcher::Fallback(watcher) => watcher.unwatch(path), } } + + // For now we are using the default implementation of configure() provided + // by the notify crate which returns Ok(false) signalling that runtime + // configuration is not supported. + + fn kind() -> WatcherKind + where Self: Sized + { + // https://github.com/notify-rs/notify/pull/441#discussion_r961970946 + // Lacking a self reference I don't see how it isn't a mistake to include this method in the + // trait. Trying to come up with an implementation I went and searched the notify-rs github + // repo an found the discussion linked above. That's my best indicator that the maintainers + // agree with me. So to satify this API I'm doing the following which is ugly and feels + // horrible so please do suggest or implement something better if you have something. + warn!("This implementation of kind() LIES to you. See comment in source code."); + WatcherKind::NullWatcher + } } #[cfg(test)] @@ -73,11 +80,12 @@ mod test { fn sup_watcher_constructor_test_polling() { let (sender, _) = channel(); let delay = Duration::from_millis(1000); + let config = Config::default().with_poll_interval(delay); let lock = lock_env_var(); lock.set("aarch64-darwin"); - let _sup_watcher = SupWatcher::new(sender, delay); + let _sup_watcher = SupWatcher::new(sender, config); let watcher_type = match _sup_watcher { Ok(SupWatcher::Native(_sup_watcher)) => "Native", Ok(SupWatcher::Fallback(_sup_watcher)) => "Fallback", @@ -93,11 +101,12 @@ mod test { fn sup_watcher_constructor_test_notify() { let (sender, _) = channel(); let delay = Duration::from_millis(1000); + let config = Config::default().with_poll_interval(delay); let lock = lock_env_var(); lock.unset(); - let _sup_watcher = SupWatcher::new(sender, delay); + let _sup_watcher = SupWatcher::new(sender, config); let watcher_type = match _sup_watcher { Ok(SupWatcher::Native(_sup_watcher)) => "Native", Ok(SupWatcher::Fallback(_sup_watcher)) => "Fallback",