[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.
This commit is contained in:
Félix Saparelli 2019-01-26 14:45:08 +13:00
parent ac3a4f0717
commit aae5a216b0
5 changed files with 242 additions and 164 deletions

View File

@ -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<String>,
pub paths: Vec<String>,
pub paths: Vec<PathBuf>,
pub filters: Vec<String>,
pub ignores: Vec<String>,
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<Args> {
get_args_impl(None::<&[&str]>)
}
pub fn get_args_from<I, T>(from: I) -> error::Result<Args>
where I: IntoIterator<Item=T>, T: Into<OsString> + Clone
{
get_args_impl(Some(from))
}
fn get_args_impl<I, T>(from: Option<I>) -> error::Result<Args>
where I: IntoIterator<Item=T>, T: Into<OsString> + 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<String> = 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<String> = 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)
}

View File

@ -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<T> = ::std::result::Result<T, Error>;
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<clap::Error> for Error {
fn from(err: clap::Error) -> Self {
Error::Clap(err)
}
}
impl From<globset::Error> for Error {
fn from(err: globset::Error) -> Self {
Error::Glob(err)
@ -40,6 +49,12 @@ impl From<notify::Error> for Error {
}
}
impl<'a, T> From<PoisonError<T>> for Error {
fn from(_err: PoisonError<T>) -> 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) => {

View File

@ -2,5 +2,5 @@ extern crate watchexec;
use watchexec::{cli, error, run};
fn main() -> error::Result<()> {
run(cli::get_args(), None::<fn(_)>)
run(cli::get_args()?)
}

View File

@ -14,18 +14,18 @@ pub struct NotificationFilter {
impl NotificationFilter {
pub fn new(
filters: Vec<String>,
ignores: Vec<String>,
filters: &[String],
ignores: &[String],
ignore_files: Gitignore,
) -> error::Result<NotificationFilter> {
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")));

View File

@ -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<F>(args: cli::Args, cb: Option<F>) -> Result<()> where F: Fn(Vec<PathOp>) {
let child_process: Arc<RwLock<Option<Process>>> = 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<Self> 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<bool>;
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<PathOp>) -> Result<bool>;
}
/// 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<H>(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<F>(args: cli::Args, cb: Option<F>) -> Result<()> where F: Fn(Vec<Path
}
if fallback {
return Watcher::new(tx, &paths, true, poll_interval);
return Watcher::new(tx, &args.paths, true, poll_interval);
}
}
@ -93,114 +106,152 @@ pub fn run<F>(args: cli::Args, cb: Option<F>) -> Result<()> where F: Fn(Vec<Path
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));
}
//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);
if !handler.on_manual()? {
return Ok(());
}
}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());
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();
}
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<Signal>,
child_process: Arc<RwLock<Option<Process>>>,
}
impl Handler for ExecHandler {
fn new(args: Args) -> Result<Self> {
let child_process: Arc<RwLock<Option<Process>>> = 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<bool> {
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<PathOp>) -> Result<bool> {
// 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::<ExecHandler>(args)
}
fn wait_fs(rx: &Receiver<Event>, filter: &NotificationFilter, debounce: u64) -> Vec<PathOp> {
let mut paths = vec![];
let mut cache = HashMap::new();