From 8bd9bb3c259d8eef58c6da4f0c211adaa49a8bba Mon Sep 17 00:00:00 2001 From: James Kominick Date: Wed, 6 Sep 2017 23:53:41 -0400 Subject: [PATCH] detailed update information issue #59 - Keep track of `notify::op::Op`s associated with each updated path - Collect paths into `notify::op::Op` categories and pass them on as environment vars - Set a COMMON_PATH and use relative paths if more than one unique path was touched --- src/lib.rs | 1 + src/main.rs | 1 + src/pathop.rs | 44 ++++++++++++++++++++ src/process.rs | 106 ++++++++++++++++++++++++++++++++++++++++--------- src/run.rs | 18 +++++---- 5 files changed, 143 insertions(+), 27 deletions(-) create mode 100644 src/pathop.rs diff --git a/src/lib.rs b/src/lib.rs index 613f24a..d3f9a3a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,5 +25,6 @@ mod process; pub mod run; mod signal; mod watcher; +mod pathop; pub use run::run; diff --git a/src/main.rs b/src/main.rs index 9ee0cbf..6398ea3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,7 @@ mod process; mod run; mod signal; mod watcher; +mod pathop; fn main() { let args = cli::get_args(); diff --git a/src/pathop.rs b/src/pathop.rs new file mode 100644 index 0000000..96d0f7f --- /dev/null +++ b/src/pathop.rs @@ -0,0 +1,44 @@ +use notify::op; +use std::path::{Path, PathBuf}; + + +/// Info about a path and its corresponding `notify` event +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct PathOp { + pub path: PathBuf, + pub op: Option, + pub cookie: Option, +} + +impl PathOp { + pub fn new(path: &Path, op: Option, cookie: Option) -> PathOp { + PathOp { + path: path.to_path_buf(), + op: op, + cookie: cookie, + } + } + + pub fn is_create(op_: op::Op) -> bool { + op_.contains(op::CREATE) + } + + pub fn is_remove(op_: op::Op) -> bool { + op_.contains(op::REMOVE) + } + + pub fn is_rename(op_: op::Op) -> bool { + op_.contains(op::RENAME) + } + + pub fn is_write(op_: op::Op) -> bool { + let mut write_or_close_write = op::WRITE; + write_or_close_write.toggle(op::CLOSE_WRITE); + op_.intersects(write_or_close_write) + } + + pub fn is_meta(op_: op::Op) -> bool { + op_.contains(op::CHMOD) + } +} + diff --git a/src/process.rs b/src/process.rs index bdeec9d..365c382 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; +use std::collections::{HashMap, HashSet}; +use pathop::PathOp; -pub fn spawn(cmd: &str, updated_paths: Vec, no_shell: bool) -> Process { +pub fn spawn(cmd: &str, updated_paths: Vec, no_shell: bool) -> Process { self::imp::Process::new(cmd, updated_paths, no_shell).expect("unable to spawn process") } @@ -11,10 +13,10 @@ mod imp { use nix::{self, Error}; use nix::libc::*; use std::io::{self, Result}; - use std::path::PathBuf; use std::process::Command; use std::sync::*; use signal::Signal; + use pathop::PathOp; pub struct Process { pgid: pid_t, @@ -33,7 +35,7 @@ mod imp { #[allow(unknown_lints)] #[allow(mutex_atomic)] impl Process { - pub fn new(cmd: &str, updated_paths: Vec, no_shell: bool) -> Result { + pub fn new(cmd: &str, updated_paths: Vec, no_shell: bool) -> Result { use nix::unistd::*; use std::os::unix::process::CommandExt; @@ -55,12 +57,9 @@ mod imp { debug!("Assembled command {:?}", command); - if let Some(single_path) = super::get_single_updated_path(&updated_paths) { - command.env("WATCHEXEC_UPDATED_PATH", single_path); - } - - if let Some(common_path) = super::get_longest_common_path(&updated_paths) { - command.env("WATCHEXEC_COMMON_PATH", common_path); + let command_envs = super::collect_path_env_vars(&updated_paths); + for &(ref name, ref val) in &command_envs { + command.env(name, val); } command @@ -133,12 +132,12 @@ mod imp { use std::io; use std::io::Result; use std::mem; - use std::path::PathBuf; use std::process::Command; use std::ptr; use kernel32::*; use winapi::*; use signal::Signal; + use pathop::PathOp; pub struct Process { job: HANDLE, @@ -152,7 +151,7 @@ mod imp { } impl Process { - pub fn new(cmd: &str, updated_paths: Vec, no_shell: bool) -> Result { + pub fn new(cmd: &str, updated_paths: Vec, no_shell: bool) -> Result { use std::os::windows::io::IntoRawHandle; use std::os::windows::process::CommandExt; @@ -215,12 +214,9 @@ mod imp { command.creation_flags(CREATE_SUSPENDED); debug!("Assembled command {:?}", command); - if let Some(single_path) = super::get_single_updated_path(&updated_paths) { - command.env("WATCHEXEC_UPDATED_PATH", single_path); - } - - if let Some(common_path) = super::get_longest_common_path(&updated_paths) { - command.env("WATCHEXEC_COMMON_PATH", common_path); + let command_envs = super::collect_path_env_vars(&updated_paths); + for &(ref name, ref val) in &command_envs { + command.env(name, val); } command @@ -298,10 +294,62 @@ mod imp { } } -fn get_single_updated_path(paths: &[PathBuf]) -> Option<&str> { - paths.get(0).and_then(|p| p.to_str()) + +/// Collect `PathOp` details into op-categories to pass onto the exec'd command as env-vars +/// +/// WRITTEN -> notify::ops::WRITE, notify::ops::CLOSE_WRITE +/// META_CHANGED -> notify::ops::CHMOD +/// REMOVED -> notify::ops::REMOVE +/// CREATED -> notify::ops::CREATE +/// RENAMED -> notify::ops::RENAME +fn collect_path_env_vars(pathops: &[PathOp]) -> Vec<(String, String)> { + #[cfg(target_family = "unix")] + const ENV_SEP: &'static str = ":"; + #[cfg(not(target_family = "unix"))] + const ENV_SEP: &'static str = ";"; + + let mut by_op = HashMap::new(); // Paths as `String`s collected by `notify::op` + let mut all_pathbufs = HashSet::new(); // All unique `PathBuf`s + for pathop in pathops { + if let Some(op) = pathop.op { // ignore pathops that don't have a `notify::op` set + if let Some(s) = pathop.path.to_str() { // ignore invalid utf8 paths + all_pathbufs.insert(pathop.path.clone()); + let e = by_op.entry(op).or_insert(vec![]); + e.push(s.to_owned()); + } + } + } + + let mut vars = vec![]; + // Only break off a common path if we have more than one unique path, + // otherwise we end up with a `COMMON_PATH` being set and other vars + // being present but empty. + let common_path = if all_pathbufs.len() > 1 { + let all_pathbufs: Vec = all_pathbufs.into_iter().collect(); + get_longest_common_path(&all_pathbufs) + } else { None }; + if let Some(ref common_path) = common_path { + vars.push(("WATCHEXEC_COMMON_PATH".to_string(), common_path.to_string())); + } + for (op, paths) in by_op.into_iter() { + let key = match op { + op if PathOp::is_create(op) => "WATCHEXEC_CREATED_PATH", + op if PathOp::is_remove(op) => "WATCHEXEC_REMOVED_PATH", + op if PathOp::is_rename(op) => "WATCHEXEC_RENAMED_PATH", + op if PathOp::is_write(op) => "WATCHEXEC_WRITTEN_PATH", + op if PathOp::is_meta(op) => "WATCHEXEC_META_CHANGED_PATH", + _ => continue, // ignore `notify::op::RESCAN`s + }; + + let paths = if let Some(ref common_path) = common_path { + paths.iter().map(|path_str| path_str.trim_left_matches(common_path).to_string()).collect::>() + } else { paths }; + vars.push((key.to_string(), paths.as_slice().join(ENV_SEP))); + } + vars } + fn get_longest_common_path(paths: &[PathBuf]) -> Option { match paths.len() { 0 => return None, @@ -339,9 +387,13 @@ fn get_longest_common_path(paths: &[PathBuf]) -> Option { #[cfg(target_family = "unix")] mod tests { use std::path::PathBuf; + use std::collections::HashSet; + use notify; + use pathop::PathOp; use super::spawn; use super::get_longest_common_path; + use super::collect_path_env_vars; #[test] fn test_start() { @@ -375,5 +427,21 @@ mod tests { let uneven_result = get_longest_common_path(&uneven_paths).unwrap(); assert_eq!(uneven_result, "/tmp/logs"); } + + #[test] + fn pathops_collect_to_env_vars() { + let pathops = vec![ + PathOp::new(&PathBuf::from("/tmp/logs/hi"), Some(notify::op::CREATE), None), + PathOp::new(&PathBuf::from("/tmp/logs/hey/there"), Some(notify::op::CREATE), None), + PathOp::new(&PathBuf::from("/tmp/logs/bye"), Some(notify::op::REMOVE), None), + ]; + let expected_vars = vec![ + ("WATCHEXEC_COMMON_PATH".to_string(), "/tmp/logs".to_string()), + ("WATCHEXEC_REMOVED_PATH".to_string(), "/bye".to_string()), + ("WATCHEXEC_CREATED_PATH".to_string(), "/hi:/hey/there".to_string()), + ]; + let vars = collect_path_env_vars(&pathops); + assert_eq!(vars.iter().collect::>(), expected_vars.iter().collect::>()); + } } diff --git a/src/run.rs b/src/run.rs index f68b2ff..71c8999 100644 --- a/src/run.rs +++ b/src/run.rs @@ -12,6 +12,7 @@ use notification_filter::NotificationFilter; use process::{self, Process}; use signal::{self, Signal}; use watcher::{Event, Watcher}; +use pathop::PathOp; fn init_logger(debug: bool) { let mut log_builder = env_logger::LogBuilder::new(); @@ -167,7 +168,7 @@ pub fn run(args: cli::Args) { } } -fn wait_fs(rx: &Receiver, filter: &NotificationFilter, debounce: u64) -> Vec { +fn wait_fs(rx: &Receiver, filter: &NotificationFilter, debounce: u64) -> Vec { let mut paths = vec![]; let mut cache = HashMap::new(); @@ -175,15 +176,16 @@ fn wait_fs(rx: &Receiver, filter: &NotificationFilter, debounce: u64) -> let e = rx.recv().expect("error when reading event"); if let Some(ref path) = e.path { + let pathop = PathOp::new(&path, e.op.ok(), e.cookie); // Ignore cache for the initial file. Otherwise, in // debug mode it's hard to track what's going on let excluded = filter.is_excluded(path); - if !cache.contains_key(path) { - cache.insert(path.to_owned(), excluded); + if !cache.contains_key(&pathop) { + cache.insert(pathop.clone(), excluded); } if !excluded { - paths.push(path.to_owned()); + paths.push(pathop); break; } } @@ -193,17 +195,17 @@ fn wait_fs(rx: &Receiver, filter: &NotificationFilter, debounce: u64) -> let timeout = Duration::from_millis(debounce); while let Ok(e) = rx.recv_timeout(timeout) { if let Some(ref path) = e.path { - if cache.contains_key(path) { + let pathop = PathOp::new(&path, e.op.ok(), e.cookie); + if cache.contains_key(&pathop) { continue; } let excluded = filter.is_excluded(path); - let p = path.to_owned(); - cache.insert(p.clone(), excluded); + cache.insert(pathop.clone(), excluded); if !excluded { - paths.push(p); + paths.push(pathop); } } }