From 7b3daeef9c9906e3ee74e30fe707c2e2e447c759 Mon Sep 17 00:00:00 2001 From: David Ziegler Date: Wed, 23 Jan 2019 18:56:09 +0100 Subject: [PATCH 1/5] Major changes for c bindings integration as third party crate (see project: github/InfinityMod/watchexec_c). Changed version to 1.9.3 for third party crate compatibility detection. --- .gitignore | 1 + Cargo.toml | 2 +- src/cli.rs | 21 ++++--- src/lib.rs | 2 +- src/main.rs | 2 +- src/pathop.rs | 3 + src/run.rs | 156 +++++++++++++++++++++++++++----------------------- 7 files changed, 106 insertions(+), 81 deletions(-) diff --git a/.gitignore b/.gitignore index c2c87bf..a3aff5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target *.tar.gz *.zip +*.vscode \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index f6198bc..957ccb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "watchexec" -version = "1.9.2" +version = "1.9.3" authors = ["Matt Green "] description = "Executes commands in response to file modifications" documentation = "https://github.com/watchexec/watchexec" diff --git a/src/cli.rs b/src/cli.rs index d1c343b..b690a2b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,7 @@ use std::path::MAIN_SEPARATOR; use std::process::Command; -use clap::{App, Arg, Error}; +use clap::{App, Arg, Error, ArgMatches}; #[derive(Debug)] pub struct Args { @@ -32,10 +32,8 @@ pub fn clear_screen() { let _ = Command::new("tput").arg("reset").status(); } -#[allow(unknown_lints)] -#[allow(or_fun_call)] -pub fn get_args() -> Args { - let args = App::new("watchexec") +pub fn init_app<'a, 'b>() -> App<'a, 'b> { + let app = App::new("watchexec") .version(crate_version!()) .about("Execute commands when watched files change") .arg(Arg::with_name("command") @@ -117,9 +115,11 @@ pub fn get_args() -> Args { .help("Do not wrap command in 'sh -c' resp. 'cmd.exe /C'") .short("n") .long("no-shell")) - .arg(Arg::with_name("once").short("1").hidden(true)) - .get_matches(); + .arg(Arg::with_name("once").short("1").hidden(true)); + app +} +pub fn process_args(args: ArgMatches) -> Args{ let cmd: Vec = values_t!(args.values_of("command"), String).unwrap(); let paths = values_t!(args.values_of("path"), String).unwrap_or(vec![String::from(".")]); @@ -203,3 +203,10 @@ pub fn get_args() -> Args { poll_interval: poll_interval, } } + +#[allow(unknown_lints)] +#[allow(or_fun_call)] +pub fn get_args() -> Args { + let args = init_app().get_matches(); + process_args(args) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 6cdd807..cf28533 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,7 @@ pub mod cli; pub mod error; mod gitignore; mod notification_filter; -mod pathop; +pub mod pathop; mod process; pub mod run; mod signal; diff --git a/src/main.rs b/src/main.rs index bb30a23..955bccd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,5 +2,5 @@ extern crate watchexec; use watchexec::{cli, error, run}; fn main() -> error::Result<()> { - run(cli::get_args()) + run(cli::get_args(), None::) } diff --git a/src/pathop.rs b/src/pathop.rs index 76d987a..41747cb 100644 --- a/src/pathop.rs +++ b/src/pathop.rs @@ -1,5 +1,6 @@ use notify::op; use std::path::{Path, PathBuf}; +use std::time::{SystemTime}; /// Info about a path and its corresponding `notify` event #[derive(Debug, Clone, Hash, Eq, PartialEq)] @@ -7,6 +8,7 @@ pub struct PathOp { pub path: PathBuf, pub op: Option, pub cookie: Option, + pub time: SystemTime, } impl PathOp { @@ -15,6 +17,7 @@ impl PathOp { path: path.to_path_buf(), op: op, cookie: cookie, + time: SystemTime::now(), } } diff --git a/src/run.rs b/src/run.rs index 302b08f..d4465e4 100644 --- a/src/run.rs +++ b/src/run.rs @@ -32,7 +32,7 @@ fn init_logger(debug: bool) { .init(); } -pub fn run(args: cli::Args) -> Result<()> { +pub fn run(args: cli::Args, cb: Option) -> Result<()> where F: Fn(Vec) { let child_process: Arc>> = Arc::new(RwLock::new(None)); let weak_child = Arc::downgrade(&child_process); @@ -103,87 +103,101 @@ pub fn run(args: cli::Args) -> Result<()> { *guard = Some(process::spawn(&args.cmd, vec![], args.no_shell)); } - loop { - debug!("Waiting for filesystem activity"); - let paths = wait_fs(&rx, &filter, args.debounce); - if let Some(path) = paths.get(0) { - debug!("Path updated: {:?}", path); + //Decide if callback cb function or direct execution should be used + let has_cb: bool = cb.is_some(); + if has_cb { + let fcb = cb.unwrap(); + loop { + debug!("Waiting for filesystem activity"); + let paths = wait_fs(&rx, &filter, args.debounce); + if let Some(path) = paths.get(0) { + debug!("Path updated: {:?}", path); + } + //Execute callback + fcb(paths); } + }else{ + loop { + debug!("Waiting for filesystem activity"); + let paths = wait_fs(&rx, &filter, args.debounce); + if let Some(path) = paths.get(0) { + debug!("Path updated: {:?}", path); + } - // We have three scenarios here: - // - // 1. Make sure the previous run was ended, then run the command again - // 2. Just send a specified signal to the child, do nothing more - // 3. Send SIGTERM to the child, wait for it to exit, then run the command again - // 4. Send a specified signal to the child, wait for it to exit, then run the command again - // - let scenario = (args.restart, signal.is_some()); + // We have three scenarios here: + // + // 1. Make sure the previous run was ended, then run the command again + // 2. Just send a specified signal to the child, do nothing more + // 3. Send SIGTERM to the child, wait for it to exit, then run the command again + // 4. Send a specified signal to the child, wait for it to exit, then run the command again + // + let scenario = (args.restart, signal.is_some()); - match scenario { - // Custom restart behaviour (--restart was given, and --signal specified): - // Send specified signal to the child, wait for it to exit, then run the command again - (true, true) => { - signal_process(&child_process, signal, true); + match scenario { + // Custom restart behaviour (--restart was given, and --signal specified): + // Send specified signal to the child, wait for it to exit, then run the command again + (true, true) => { + signal_process(&child_process, signal, true); - // Launch child process - if args.clear_screen { - cli::clear_screen(); + // Launch child process + if args.clear_screen { + cli::clear_screen(); + } + + debug!("Launching child process"); + { + let mut guard = child_process.write().unwrap(); + *guard = Some(process::spawn(&args.cmd, paths, args.no_shell)); + } } - debug!("Launching child process"); - { - let mut guard = child_process.write().unwrap(); - *guard = Some(process::spawn(&args.cmd, paths, args.no_shell)); + // Default restart behaviour (--restart was given, but --signal wasn't specified): + // Send SIGTERM to the child, wait for it to exit, then run the command again + (true, false) => { + let sigterm = signal::new(Some("SIGTERM".to_owned())); + signal_process(&child_process, sigterm, true); + + // Launch child process + if args.clear_screen { + cli::clear_screen(); + } + + debug!("Launching child process"); + { + let mut guard = child_process.write().unwrap(); + *guard = Some(process::spawn(&args.cmd, paths, args.no_shell)); + } + } + + // SIGHUP scenario: --signal was given, but --restart was not + // Just send a signal (e.g. SIGHUP) to the child, do nothing more + (false, true) => signal_process(&child_process, signal, false), + + // Default behaviour (neither --signal nor --restart specified): + // Make sure the previous run was ended, then run the command again + (false, false) => { + signal_process(&child_process, None, true); + + // Launch child process + if args.clear_screen { + cli::clear_screen(); + } + + debug!("Launching child process"); + { + let mut guard = child_process.write().unwrap(); + *guard = Some(process::spawn(&args.cmd, paths, args.no_shell)); + } } } - // Default restart behaviour (--restart was given, but --signal wasn't specified): - // Send SIGTERM to the child, wait for it to exit, then run the command again - (true, false) => { - let sigterm = signal::new(Some("SIGTERM".to_owned())); - signal_process(&child_process, sigterm, true); - - // Launch child process - if args.clear_screen { - cli::clear_screen(); - } - - debug!("Launching child process"); - { - let mut guard = child_process.write().unwrap(); - *guard = Some(process::spawn(&args.cmd, paths, args.no_shell)); - } + // Handle once option for integration testing + if args.once { + signal_process(&child_process, signal, false); + break; } - - // SIGHUP scenario: --signal was given, but --restart was not - // Just send a signal (e.g. SIGHUP) to the child, do nothing more - (false, true) => signal_process(&child_process, signal, false), - - // Default behaviour (neither --signal nor --restart specified): - // Make sure the previous run was ended, then run the command again - (false, false) => { - signal_process(&child_process, None, true); - - // Launch child process - if args.clear_screen { - cli::clear_screen(); - } - - debug!("Launching child process"); - { - let mut guard = child_process.write().unwrap(); - *guard = Some(process::spawn(&args.cmd, paths, args.no_shell)); - } - } - } - - // Handle once option for integration testing - if args.once { - signal_process(&child_process, signal, false); - break; - } + } } - Ok(()) } From 65db0dfae15c0204c7cccd57777d2bfd936c89b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Sat, 26 Jan 2019 09:16:02 +1300 Subject: [PATCH 2/5] [meta] Revert version bump --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 957ccb8..f6198bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "watchexec" -version = "1.9.3" +version = "1.9.2" authors = ["Matt Green "] description = "Executes commands in response to file modifications" documentation = "https://github.com/watchexec/watchexec" From b49bf74dd63b34c598876b393a07838f3c2c3140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Sat, 26 Jan 2019 09:16:43 +1300 Subject: [PATCH 3/5] [meta] Revert .gitignore change --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index a3aff5e..c2c87bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ target *.tar.gz *.zip -*.vscode \ No newline at end of file From ac3a4f0717a7fd86f026f600dc25702100ceeac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Sat, 26 Jan 2019 14:40:30 +1300 Subject: [PATCH 4/5] [pathop] Revert changes adding a time field --- src/pathop.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pathop.rs b/src/pathop.rs index 41747cb..76d987a 100644 --- a/src/pathop.rs +++ b/src/pathop.rs @@ -1,6 +1,5 @@ use notify::op; use std::path::{Path, PathBuf}; -use std::time::{SystemTime}; /// Info about a path and its corresponding `notify` event #[derive(Debug, Clone, Hash, Eq, PartialEq)] @@ -8,7 +7,6 @@ pub struct PathOp { pub path: PathBuf, pub op: Option, pub cookie: Option, - pub time: SystemTime, } impl PathOp { @@ -17,7 +15,6 @@ impl PathOp { path: path.to_path_buf(), op: op, cookie: cookie, - time: SystemTime::now(), } } From aae5a216b0b7d381b7c8af2d032f0b959757efbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Sat, 26 Jan 2019 14:45:08 +1300 Subject: [PATCH 5/5] [api] Make watchexec take a Handler rather than a callback Instead of special-casing the callback, which is the path least-taken, switch the internals to a Handler model, where the default behaviour is an implementation of a Handler, and external callers can implement their own Handlers and pass them in. While doing so, change all unwraps in run::run to returning Errs, and expand the watchexec Error enum to accommodate. That should make it easier to use as a library. Also, differentiate between "manual" and "on update" runs. For now the only manual run is the initial run, but this paves the way for e.g. keyboard- or signal- triggered runs. --- src/cli.rs | 56 ++++--- src/error.rs | 19 ++- src/main.rs | 2 +- src/notification_filter.rs | 28 ++-- src/run.rs | 301 ++++++++++++++++++++++--------------- 5 files changed, 242 insertions(+), 164 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index b690a2b..e02aee8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,12 +1,11 @@ -use std::path::MAIN_SEPARATOR; -use std::process::Command; +use clap::{App, Arg, Error}; +use error; +use std::{ffi::OsString, fs::canonicalize, path::{MAIN_SEPARATOR, PathBuf}, process::Command}; -use clap::{App, Arg, Error, ArgMatches}; - -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Args { pub cmd: Vec, - pub paths: Vec, + pub paths: Vec, pub filters: Vec, pub ignores: Vec, pub clear_screen: bool, @@ -32,7 +31,19 @@ pub fn clear_screen() { let _ = Command::new("tput").arg("reset").status(); } -pub fn init_app<'a, 'b>() -> App<'a, 'b> { +pub fn get_args() -> error::Result { + get_args_impl(None::<&[&str]>) +} + +pub fn get_args_from(from: I) -> error::Result +where I: IntoIterator, T: Into + Clone +{ + get_args_impl(Some(from)) +} + +fn get_args_impl(from: Option) -> error::Result +where I: IntoIterator, T: Into + Clone +{ let app = App::new("watchexec") .version(crate_version!()) .about("Execute commands when watched files change") @@ -116,12 +127,18 @@ pub fn init_app<'a, 'b>() -> App<'a, 'b> { .short("n") .long("no-shell")) .arg(Arg::with_name("once").short("1").hidden(true)); - app -} -pub fn process_args(args: ArgMatches) -> Args{ - let cmd: Vec = values_t!(args.values_of("command"), String).unwrap(); - let paths = values_t!(args.values_of("path"), String).unwrap_or(vec![String::from(".")]); + let args = match from { + None => app.get_matches(), + Some(i) => app.get_matches_from(i) + }; + + let cmd: Vec = values_t!(args.values_of("command"), String)?; + let str_paths = values_t!(args.values_of("path"), String).unwrap_or(vec![".".into()]); + let mut paths = vec![]; + for path in str_paths { + paths.push(canonicalize(&path).map_err(|e| error::Error::Canonicalization(path, e))?); + } // Treat --kill as --signal SIGKILL (for compatibility with older syntax) let signal = if args.is_present("kill") { @@ -131,7 +148,7 @@ pub fn process_args(args: ArgMatches) -> Args{ args.value_of("signal").map(str::to_string) }; - let mut filters = values_t!(args.values_of("filter"), String).unwrap_or(vec![]); + let mut filters = values_t!(args.values_of("filter"), String).unwrap_or(Vec::new()); if let Some(extensions) = args.values_of("extensions") { for exts in extensions { @@ -159,7 +176,7 @@ pub fn process_args(args: ArgMatches) -> Args{ if args.occurrences_of("no-default-ignore") == 0 { ignores.extend(default_ignores) }; - ignores.extend(values_t!(args.values_of("ignore"), String).unwrap_or(vec![])); + ignores.extend(values_t!(args.values_of("ignore"), String).unwrap_or(Vec::new())); let poll_interval = if args.occurrences_of("poll") > 0 { value_t!(args.value_of("poll"), u32).unwrap_or_else(|e| e.exit()) @@ -185,7 +202,7 @@ pub fn process_args(args: ArgMatches) -> Args{ .exit(); } - Args { + Ok(Args { cmd: cmd, paths: paths, filters: filters, @@ -201,12 +218,5 @@ pub fn process_args(args: ArgMatches) -> Args{ once: args.is_present("once"), poll: args.occurrences_of("poll") > 0, poll_interval: poll_interval, - } + }) } - -#[allow(unknown_lints)] -#[allow(or_fun_call)] -pub fn get_args() -> Args { - let args = init_app().get_matches(); - process_args(args) -} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index dd7edd3..8185d57 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,14 +1,17 @@ +use clap; use globset; use notify; -use std::{error::Error as StdError, fmt, io}; +use std::{error::Error as StdError, fmt, io, sync::PoisonError}; pub type Result = ::std::result::Result; pub enum Error { Canonicalization(String, io::Error), + Clap(clap::Error), Glob(globset::Error), Io(io::Error), Notify(notify::Error), + PoisonedLock, } impl StdError for Error { @@ -19,6 +22,12 @@ impl StdError for Error { } } +impl From for Error { + fn from(err: clap::Error) -> Self { + Error::Clap(err) + } +} + impl From for Error { fn from(err: globset::Error) -> Self { Error::Glob(err) @@ -40,6 +49,12 @@ impl From for Error { } } +impl<'a, T> From> for Error { + fn from(_err: PoisonError) -> Self { + Error::PoisonedLock + } +} + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( @@ -47,9 +62,11 @@ impl fmt::Display for Error { "{} error: {}", match self { Error::Canonicalization(_, _) => "Path", + Error::Clap(_) => "Argument", Error::Glob(_) => "Globset", Error::Io(_) => "I/O", Error::Notify(_) => "Notify", + Error::PoisonedLock => "Internal", }, match self { Error::Canonicalization(path, err) => { diff --git a/src/main.rs b/src/main.rs index 955bccd..f6f70ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,5 +2,5 @@ extern crate watchexec; use watchexec::{cli, error, run}; fn main() -> error::Result<()> { - run(cli::get_args(), None::) + run(cli::get_args()?) } diff --git a/src/notification_filter.rs b/src/notification_filter.rs index e2e57df..3268c82 100644 --- a/src/notification_filter.rs +++ b/src/notification_filter.rs @@ -14,18 +14,18 @@ pub struct NotificationFilter { impl NotificationFilter { pub fn new( - filters: Vec, - ignores: Vec, + filters: &[String], + ignores: &[String], ignore_files: Gitignore, ) -> error::Result { let mut filter_set_builder = GlobSetBuilder::new(); - for f in &filters { + for f in filters { filter_set_builder.add(Glob::new(f)?); debug!("Adding filter: \"{}\"", f); } let mut ignore_set_builder = GlobSetBuilder::new(); - for i in &ignores { + for i in ignores { let mut ignore_path = Path::new(i).to_path_buf(); if ignore_path.is_relative() && !i.starts_with("*") { ignore_path = Path::new("**").join(&ignore_path); @@ -74,7 +74,7 @@ mod tests { #[test] fn test_allows_everything_by_default() { - let filter = NotificationFilter::new(vec![], vec![], gitignore::load(&vec![])).unwrap(); + let filter = NotificationFilter::new(&[], &[], gitignore::load(&[])).unwrap(); assert!(!filter.is_excluded(&Path::new("foo"))); } @@ -82,9 +82,9 @@ mod tests { #[test] fn test_filename() { let filter = NotificationFilter::new( - vec![], - vec![String::from("test.json")], - gitignore::load(&vec![]), + &[], + &["test.json".into()], + gitignore::load(&[]), ).unwrap(); assert!(filter.is_excluded(&Path::new("/path/to/test.json"))); @@ -93,8 +93,8 @@ mod tests { #[test] fn test_multiple_filters() { - let filters = vec![String::from("*.rs"), String::from("*.toml")]; - let filter = NotificationFilter::new(filters, vec![], gitignore::load(&vec![])).unwrap(); + let filters = &["*.rs".into(), "*.toml".into()]; + let filter = NotificationFilter::new(filters, &[], gitignore::load(&[])).unwrap(); assert!(!filter.is_excluded(&Path::new("hello.rs"))); assert!(!filter.is_excluded(&Path::new("Cargo.toml"))); @@ -103,8 +103,8 @@ mod tests { #[test] fn test_multiple_ignores() { - let ignores = vec![String::from("*.rs"), String::from("*.toml")]; - let filter = NotificationFilter::new(vec![], ignores, gitignore::load(&vec![])).unwrap(); + let ignores = &["*.rs".into(), "*.toml".into()]; + let filter = NotificationFilter::new(&[], ignores, gitignore::load(&vec![])).unwrap(); assert!(filter.is_excluded(&Path::new("hello.rs"))); assert!(filter.is_excluded(&Path::new("Cargo.toml"))); @@ -113,9 +113,9 @@ mod tests { #[test] fn test_ignores_take_precedence() { - let ignores = vec![String::from("*.rs"), String::from("*.toml")]; + let ignores = &["*.rs".into(), "*.toml".into()]; let filter = - NotificationFilter::new(ignores.clone(), ignores, gitignore::load(&vec![])).unwrap(); + NotificationFilter::new(ignores, ignores, gitignore::load(&[])).unwrap(); assert!(filter.is_excluded(&Path::new("hello.rs"))); assert!(filter.is_excluded(&Path::new("Cargo.toml"))); diff --git a/src/run.rs b/src/run.rs index d4465e4..fa8048a 100644 --- a/src/run.rs +++ b/src/run.rs @@ -5,7 +5,7 @@ use std::sync::mpsc::{channel, Receiver}; use std::sync::{Arc, RwLock}; use std::time::Duration; -use cli; +use cli::{Args, clear_screen}; use env_logger; use error::{Error, Result}; use gitignore; @@ -32,40 +32,53 @@ fn init_logger(debug: bool) { .init(); } -pub fn run(args: cli::Args, cb: Option) -> Result<()> where F: Fn(Vec) { - let child_process: Arc>> = Arc::new(RwLock::new(None)); - let weak_child = Arc::downgrade(&child_process); +pub trait Handler { + /// Initialises the `Handler` with a copy of the arguments. + fn new(args: Args) -> Result where Self: Sized; - // Convert signal string to the corresponding integer - let signal = signal::new(args.signal); + /// Called through a manual request, such as an initial run. + /// + /// # Returns + /// + /// A `Result` which means: + /// + /// - `Err`: an error has occurred while processing, quit. + /// - `Ok(false)`: everything is fine and the loop can continue. + /// - `Ok(true)`: everything is fine but we should gracefully stop. + fn on_manual(&mut self) -> Result; - signal::install_handler(move |sig: Signal| { - if let Some(lock) = weak_child.upgrade() { - let strong = lock.read().unwrap(); - if let Some(ref child) = *strong { - match sig { - Signal::SIGCHLD => child.reap(), // SIGCHLD is special, initiate reap() - _ => child.signal(sig), - } - } - } - }); + /// Called through a file-update request. + /// + /// # Parameters + /// + /// - `ops`: The list of events that triggered this update. + /// + /// # Returns + /// + /// A `Result` which means: + /// + /// - `Err`: an error has occurred while processing, quit. + /// - `Ok(true)`: everything is fine and the loop can continue. + /// - `Ok(false)`: everything is fine but we should gracefully stop. + fn on_update(&mut self, ops: Vec) -> Result; +} +/// Starts watching, and calls a handler when something happens. +/// +/// Given an argument structure and a `Handler` type, starts the watcher +/// loop (blocking until done). +pub fn watch(args: Args) -> Result<()> where H: Handler { init_logger(args.debug); + let mut handler = H::new(args.clone())?; - let mut paths = vec![]; - for path in args.paths { - paths.push(canonicalize(&path).map_err(|e| Error::Canonicalization(path, e))?); - } - - let gitignore = gitignore::load(if args.no_vcs_ignore { &[] } else { &paths }); - let filter = NotificationFilter::new(args.filters, args.ignores, gitignore)?; + let gitignore = gitignore::load(if args.no_vcs_ignore { &[] } else { &args.paths }); + let filter = NotificationFilter::new(&args.filters, &args.ignores, gitignore)?; let (tx, rx) = channel(); let poll = args.poll.clone(); #[cfg(target_os = "linux")] let poll_interval = args.poll_interval.clone(); - let watcher = Watcher::new(tx.clone(), &paths, args.poll, args.poll_interval).or_else(|err| { + let watcher = Watcher::new(tx.clone(), &args.paths, args.poll, args.poll_interval).or_else(|err| { if poll { return Err(err); } @@ -82,7 +95,7 @@ pub fn run(args: cli::Args, cb: Option) -> Result<()> where F: Fn(Vec(args: cli::Args, cb: Option) -> Result<()> where F: Fn(Vec { - signal_process(&child_process, signal, true); - - // Launch child process - if args.clear_screen { - cli::clear_screen(); - } - - debug!("Launching child process"); - { - let mut guard = child_process.write().unwrap(); - *guard = Some(process::spawn(&args.cmd, paths, args.no_shell)); - } - } - - // Default restart behaviour (--restart was given, but --signal wasn't specified): - // Send SIGTERM to the child, wait for it to exit, then run the command again - (true, false) => { - let sigterm = signal::new(Some("SIGTERM".to_owned())); - signal_process(&child_process, sigterm, true); - - // Launch child process - if args.clear_screen { - cli::clear_screen(); - } - - debug!("Launching child process"); - { - let mut guard = child_process.write().unwrap(); - *guard = Some(process::spawn(&args.cmd, paths, args.no_shell)); - } - } - - // SIGHUP scenario: --signal was given, but --restart was not - // Just send a signal (e.g. SIGHUP) to the child, do nothing more - (false, true) => signal_process(&child_process, signal, false), - - // Default behaviour (neither --signal nor --restart specified): - // Make sure the previous run was ended, then run the command again - (false, false) => { - signal_process(&child_process, None, true); - - // Launch child process - if args.clear_screen { - cli::clear_screen(); - } - - debug!("Launching child process"); - { - let mut guard = child_process.write().unwrap(); - *guard = Some(process::spawn(&args.cmd, paths, args.no_shell)); - } - } - } - - // Handle once option for integration testing - if args.once { - signal_process(&child_process, signal, false); - break; - } - } } + + loop { + debug!("Waiting for filesystem activity"); + let paths = wait_fs(&rx, &filter, args.debounce); + debug!("Paths updated: {:?}", paths); + + if args.clear_screen { + clear_screen(); + } + + if !handler.on_update(paths)? { + break; + } + } + Ok(()) } +pub struct ExecHandler { + args: Args, + signal: Option, + child_process: Arc>>, +} + +impl Handler for ExecHandler { + fn new(args: Args) -> Result { + let child_process: Arc>> = Arc::new(RwLock::new(None)); + let weak_child = Arc::downgrade(&child_process); + + // Convert signal string to the corresponding integer + let signal = signal::new(args.signal.clone()); + + signal::install_handler(move |sig: Signal| { + if let Some(lock) = weak_child.upgrade() { + let strong = lock.read().unwrap(); + if let Some(ref child) = *strong { + match sig { + Signal::SIGCHLD => child.reap(), // SIGCHLD is special, initiate reap() + _ => child.signal(sig), + } + } + } + }); + + Ok(Self { args, signal, child_process }) + } + + fn on_manual(&mut self) -> Result { + let mut guard = self.child_process.write()?; + *guard = Some(process::spawn(&self.args.cmd, Vec::new(), self.args.no_shell)); + + Ok(true) + } + + fn on_update(&mut self, ops: Vec) -> Result { + // We have three scenarios here: + // + // 1. Make sure the previous run was ended, then run the command again + // 2. Just send a specified signal to the child, do nothing more + // 3. Send SIGTERM to the child, wait for it to exit, then run the command again + // 4. Send a specified signal to the child, wait for it to exit, then run the command again + // + let scenario = (self.args.restart, self.signal.is_some()); + + match scenario { + // Custom restart behaviour (--restart was given, and --signal specified): + // Send specified signal to the child, wait for it to exit, then run the command again + (true, true) => { + signal_process(&self.child_process, self.signal, true); + + // Launch child process + if self.args.clear_screen { + clear_screen(); + } + + debug!("Launching child process"); + { + let mut guard = self.child_process.write()?; + *guard = Some(process::spawn(&self.args.cmd, ops, self.args.no_shell)); + } + } + + // Default restart behaviour (--restart was given, but --signal wasn't specified): + // Send SIGTERM to the child, wait for it to exit, then run the command again + (true, false) => { + let sigterm = signal::new(Some("SIGTERM".into())); + signal_process(&self.child_process, sigterm, true); + + // Launch child process + if self.args.clear_screen { + clear_screen(); + } + + debug!("Launching child process"); + { + let mut guard = self.child_process.write()?; + *guard = Some(process::spawn(&self.args.cmd, ops, self.args.no_shell)); + } + } + + // SIGHUP scenario: --signal was given, but --restart was not + // Just send a signal (e.g. SIGHUP) to the child, do nothing more + (false, true) => signal_process(&self.child_process, self.signal, false), + + // Default behaviour (neither --signal nor --restart specified): + // Make sure the previous run was ended, then run the command again + (false, false) => { + signal_process(&self.child_process, None, true); + + // Launch child process + if self.args.clear_screen { + clear_screen(); + } + + debug!("Launching child process"); + { + let mut guard = self.child_process.write()?; + *guard = Some(process::spawn(&self.args.cmd, ops, self.args.no_shell)); + } + } + } + + // Handle once option for integration testing + if self.args.once { + signal_process(&self.child_process, self.signal, false); + return Ok(false); + } + + Ok(true) + } +} + +pub fn run(args: Args) -> Result<()> { + watch::(args) +} + fn wait_fs(rx: &Receiver, filter: &NotificationFilter, debounce: u64) -> Vec { let mut paths = vec![]; let mut cache = HashMap::new();