diff --git a/src/cli.rs b/src/cli.rs index d1c343b..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}; -#[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,10 +31,20 @@ 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 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") .arg(Arg::with_name("command") @@ -117,11 +126,19 @@ 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)); - 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 get_args() -> 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 get_args() -> 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 get_args() -> Args { .exit(); } - Args { + Ok(Args { cmd: cmd, paths: paths, filters: filters, @@ -201,5 +218,5 @@ pub fn get_args() -> Args { once: args.is_present("once"), poll: args.occurrences_of("poll") > 0, poll_interval: poll_interval, - } + }) } 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/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..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()) + 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 302b08f..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) -> Result<()> { - 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) -> Result<()> { } if fallback { - return Watcher::new(tx, &paths, true, poll_interval); + return Watcher::new(tx, &args.paths, true, poll_interval); } } @@ -93,23 +106,71 @@ pub fn run(args: cli::Args) -> Result<()> { warn!("Polling for changes every {} ms", args.poll_interval); } - // Start child process initially, if necessary + // Call handler initially, if necessary if args.run_initially && !args.once { if args.clear_screen { - cli::clear_screen(); + clear_screen(); } - let mut guard = child_process.write().unwrap(); - *guard = Some(process::spawn(&args.cmd, vec![], args.no_shell)); + if !handler.on_manual()? { + return Ok(()); + } } 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); + 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 @@ -117,74 +178,78 @@ pub fn run(args: cli::Args) -> Result<()> { // 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()); + 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(&child_process, signal, true); + signal_process(&self.child_process, self.signal, true); // Launch child process - if args.clear_screen { - cli::clear_screen(); + if self.args.clear_screen { + clear_screen(); } debug!("Launching child process"); { - let mut guard = child_process.write().unwrap(); - *guard = Some(process::spawn(&args.cmd, paths, args.no_shell)); + 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".to_owned())); - signal_process(&child_process, sigterm, true); + let sigterm = signal::new(Some("SIGTERM".into())); + signal_process(&self.child_process, sigterm, true); // Launch child process - if args.clear_screen { - cli::clear_screen(); + if self.args.clear_screen { + clear_screen(); } debug!("Launching child process"); { - let mut guard = child_process.write().unwrap(); - *guard = Some(process::spawn(&args.cmd, paths, args.no_shell)); + 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(&child_process, signal, false), + (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(&child_process, None, true); + signal_process(&self.child_process, None, true); // Launch child process - if args.clear_screen { - cli::clear_screen(); + if self.args.clear_screen { + clear_screen(); } debug!("Launching child process"); { - let mut guard = child_process.write().unwrap(); - *guard = Some(process::spawn(&args.cmd, paths, args.no_shell)); + 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 args.once { - signal_process(&child_process, signal, false); - break; + if self.args.once { + signal_process(&self.child_process, self.signal, false); + return Ok(false); } - } - Ok(()) + Ok(true) + } +} + +pub fn run(args: Args) -> Result<()> { + watch::(args) } fn wait_fs(rx: &Receiver, filter: &NotificationFilter, debounce: u64) -> Vec {