diff --git a/README.md b/README.md index 2f45fc8..4db681c 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,11 @@ Call/restart `python server.py` when any Python file in the current directory (a Call/restart `my_server` when any file in the current directory (and all subdirectories) changes, sending `SIGKILL` to stop the child process: - $ watchexec -r -k my_server + $ watchexec -r -s SIGKILL my_server + +Send a SIGHUP to the child process upon changes: + + $ watchexec -s SIGHUP my_server Run `make` when any file changes, using the `.gitignore` file in the current directory to filter: diff --git a/doc/watchexec.1 b/doc/watchexec.1 index ac2c7eb..c95a17d 100644 --- a/doc/watchexec.1 +++ b/doc/watchexec.1 @@ -30,8 +30,8 @@ Comma\-separated list of file extensions to filter by\. Leading dots are allowed Ignores modifications from paths that do not match \fIpattern\fR\. This option can be specified multiple times, where a match on any given pattern causes the path to trigger \fIcommand\fR\. . .TP -\fB\-k\fR, \fB\-\-kill\fR -Send \fBSIGKILL\fR to the child process group instead of \fBSIGTERM\fR\. +\fB\-s\fR, \fB\-\-signal \fR +Sends the specified signal (e\.g\. \fBSIGKILL\fR) to the child process\. Defaults to \fBSIGTERM\fR\. . .TP \fB\-i\fR, \fB\-\-ignore\fR \fIpattern\fR diff --git a/doc/watchexec.1.html b/doc/watchexec.1.html index ed03541..765f5cc 100644 --- a/doc/watchexec.1.html +++ b/doc/watchexec.1.html @@ -88,7 +88,7 @@
command

Command to run when watched files are modified, and at startup, unless --postpone is specified. All arguments are passed to command.

-e, --exts extensions

Comma-separated list of file extensions to filter by. Leading dots are allowed (.rs) are allowed. (This is a shorthand for -f).

-f, --filter pattern

Ignores modifications from paths that do not match pattern. This option can be specified multiple times, where a match on any given pattern causes the path to trigger command.

-
-k, --kill

Send SIGKILL to the child process group instead of SIGTERM.

+
-s, --signal SIGNAL

Sends the specified signal (e.g. SIGKILl) to the child process. Defaults to SIGTERM.

-i, --ignore pattern

Ignores modifications from paths that match pattern. This option can be specified multiple times, and a match on any pattern causes the path to be ignored.

-w, --watch path

Monitor a specific path for changes. By default, the current working directory is watched. This may be specified multiple times, where a change in any watched directory (and subdirectories) causes command to be executed.

-r, --restart

Terminates the child process group if it is still running when subsequent file modifications are detected. By default, sends SIGTERM; use --kill to send SIGKILL.

diff --git a/doc/watchexec.1.ronn b/doc/watchexec.1.ronn index 4fa6cc0..1b8bbe3 100644 --- a/doc/watchexec.1.ronn +++ b/doc/watchexec.1.ronn @@ -22,8 +22,8 @@ Comma-separated list of file extensions to filter by. Leading dots are allowed ( * `-f`, `--filter` : Ignores modifications from paths that do not match . This option can be specified multiple times, where a match on any given pattern causes the path to trigger . -* `-k`, `--kill`: -Send `SIGKILL` to the child process group instead of `SIGTERM`. +* `-s`, `--signal`: +Sends the specified signal (e.g. `SIGKILL`) to the child process. Defaults to `SIGTERM`. * `-i`, `--ignore` : Ignores modifications from paths that match . This option can be specified multiple times, and a match on any pattern causes the path to be ignored. diff --git a/src/cli.rs b/src/cli.rs index 3e9e906..3ef1a7f 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}; +use clap::{App, Arg, Error}; #[derive(Debug)] pub struct Args { @@ -10,7 +10,7 @@ pub struct Args { pub filters: Vec, pub ignores: Vec, pub clear_screen: bool, - pub kill: bool, + pub signal: Option, pub restart: bool, pub debug: bool, pub run_initially: bool, @@ -37,79 +37,92 @@ pub fn get_args() -> Args { .version(crate_version!()) .about("Execute commands when watched files change") .arg(Arg::with_name("command") - .help("Command to execute") - .multiple(true) - .required(true)) + .help("Command to execute") + .multiple(true) + .required(true)) .arg(Arg::with_name("extensions") - .help("Comma-separated list of file extensions to watch (js,css,html)") - .short("e") - .long("exts") - .takes_value(true)) + .help("Comma-separated list of file extensions to watch (js,css,html)") + .short("e") + .long("exts") + .takes_value(true)) .arg(Arg::with_name("path") - .help("Watch a specific directory") - .short("w") - .long("watch") - .number_of_values(1) - .multiple(true) - .takes_value(true)) + .help("Watch a specific directory") + .short("w") + .long("watch") + .number_of_values(1) + .multiple(true) + .takes_value(true)) .arg(Arg::with_name("clear") - .help("Clear screen before executing command") - .short("c") - .long("clear")) + .help("Clear screen before executing command") + .short("c") + .long("clear")) .arg(Arg::with_name("restart") - .help("Restart the process if it's still running") - .short("r") - .long("restart")) - .arg(Arg::with_name("debug") - .help("Print debugging messages to stderr") - .short("d") - .long("debug")) - .arg(Arg::with_name("filter") - .help("Ignore all modifications except those matching the pattern") - .short("f") - .long("filter") - .number_of_values(1) - .multiple(true) - .takes_value(true) - .value_name("pattern")) - .arg(Arg::with_name("ignore") - .help("Ignore modifications to paths matching the pattern") - .short("i") - .long("ignore") - .number_of_values(1) - .multiple(true) - .takes_value(true) - .value_name("pattern")) - .arg(Arg::with_name("no-vcs-ignore") - .help("Skip auto-loading of .gitignore files for filtering") - .long("no-vcs-ignore")) - .arg(Arg::with_name("postpone") - .help("Wait until first change to execute command") - .short("p") - .long("postpone")) - .arg(Arg::with_name("poll") - .help("Forces polling mode") - .long("force-poll") - .value_name("interval")) + .help("Restart the process if it's still running") + .short("r") + .long("restart")) + .arg(Arg::with_name("signal") + .help("Send signal to process upon changes, e.g. SIGHUP") + .short("s") + .long("signal") + .takes_value(true) + .number_of_values(1) + .value_name("signal")) .arg(Arg::with_name("kill") - .help("Send SIGKILL to child processes") - .short("k") - .long("kill")) - .arg(Arg::with_name("once") - .short("1") - .hidden(true)) + .help("Send SIGKILL to child processes (deprecated, use -s SIGKILL instead)") + .short("k") + .long("kill")) + .arg(Arg::with_name("debug") + .help("Print debugging messages to stderr") + .short("d") + .long("debug")) + .arg(Arg::with_name("filter") + .help("Ignore all modifications except those matching the pattern") + .short("f") + .long("filter") + .number_of_values(1) + .multiple(true) + .takes_value(true) + .value_name("pattern")) + .arg(Arg::with_name("ignore") + .help("Ignore modifications to paths matching the pattern") + .short("i") + .long("ignore") + .number_of_values(1) + .multiple(true) + .takes_value(true) + .value_name("pattern")) + .arg(Arg::with_name("no-vcs-ignore") + .help("Skip auto-loading of .gitignore files for filtering") + .long("no-vcs-ignore")) + .arg(Arg::with_name("postpone") + .help("Wait until first change to execute command") + .short("p") + .long("postpone")) + .arg(Arg::with_name("poll") + .help("Forces polling mode") + .long("force-poll") + .value_name("interval")) + .arg(Arg::with_name("once").short("1").hidden(true)) .get_matches(); - let cmd = values_t!(args.values_of("command"), String).unwrap().join(" "); + let cmd = values_t!(args.values_of("command"), String) + .unwrap() + .join(" "); let paths = values_t!(args.values_of("path"), String).unwrap_or(vec![String::from(".")]); + // Treat --kill as --signal SIGKILL (for compatibility with older syntax) + let signal = match args.is_present("kill") { + true => Some("SIGKILL".to_string()), + false => args.value_of("signal").map(str::to_string), // Convert Option<&str> to Option + }; + let mut filters = values_t!(args.values_of("filter"), String).unwrap_or(vec![]); if let Some(extensions) = args.values_of("extensions") { for exts in extensions { filters.extend(exts.split(',') - .filter(|ext| !ext.is_empty()) - .map(|ext| format!("*.{}", ext.replace(".", "")))); + .filter(|ext| !ext.is_empty()) + .map(|ext| format!("*.{}", ext.replace(".", "")))); } } @@ -129,13 +142,25 @@ pub fn get_args() -> Args { 1000 }; + if signal.is_some() && args.is_present("postpone") { + // TODO: Error::argument_conflict() might be the better fit, usage was unclear, though + Error::value_validation_auto(format!("--postpone and --signal are mutually exclusive")) + .exit(); + } + + if signal.is_some() && args.is_present("kill") { + // TODO: Error::argument_conflict() might be the better fit, usage was unclear, though + Error::value_validation_auto(format!("--kill and --signal is ambiguous.\n Hint: Use only '--signal SIGKILL' without --kill")) + .exit(); + } + Args { cmd: cmd, paths: paths, filters: filters, ignores: ignores, + signal: signal, clear_screen: args.is_present("clear"), - kill: args.is_present("kill"), restart: args.is_present("restart"), debug: args.is_present("debug"), run_initially: !args.is_present("postpone"), diff --git a/src/gitignore.rs b/src/gitignore.rs index 35de420..473b06e 100644 --- a/src/gitignore.rs +++ b/src/gitignore.rs @@ -137,19 +137,17 @@ impl GitignoreFile { pat = pat + "/**"; } - let glob = try!(GlobBuilder::new(&pat) - .literal_separator(true) - .build()); + let glob = try!(GlobBuilder::new(&pat).literal_separator(true).build()); builder.add(glob); patterns.push(p); } Ok(GitignoreFile { - set: try!(builder.build()), - patterns: patterns, - root: root.to_owned(), - }) + set: try!(builder.build()), + patterns: patterns, + root: root.to_owned(), + }) } @@ -169,9 +167,9 @@ impl GitignoreFile { for &i in matches.iter().rev() { let pattern = &self.patterns[i]; return match pattern.pattern_type { - PatternType::Whitelist => MatchResult::Whitelist, - PatternType::Ignore => MatchResult::Ignore, - }; + PatternType::Whitelist => MatchResult::Whitelist, + PatternType::Ignore => MatchResult::Ignore, + }; } MatchResult::None @@ -182,7 +180,8 @@ impl GitignoreFile { } fn parse(contents: Vec<&str>) -> Vec { - contents.iter() + contents + .iter() .filter(|l| !l.is_empty()) .filter(|l| !l.starts_with('#')) .map(|l| Pattern::parse(l)) diff --git a/src/main.rs b/src/main.rs index 2690062..ed1ec3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,7 +45,8 @@ fn init_logger(debug: bool) { log::LogLevelFilter::Warn }; - log_builder.format(|r| format!("*** {}", r.args())) + log_builder + .format(|r| format!("*** {}", r.args())) .filter(None, level); log_builder.init().expect("unable to initialize logger"); } @@ -54,23 +55,17 @@ fn main() { let args = cli::get_args(); let child_process: Arc>> = Arc::new(RwLock::new(None)); let weak_child = Arc::downgrade(&child_process); - let kill = args.kill; + + // Convert signal string to the corresponding integer + let signal = signal::new(args.signal); 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::Terminate => { - if kill { - child.kill(); - } else { - child.terminate(); - } - } - Signal::Stop => child.pause(), - Signal::Continue => child.resume(), - Signal::ChildExit => child.reap(), + Signal::SIGCHLD => child.reap(), // SIGCHLD is special, initiate reap() + _ => child.signal(sig), } } } @@ -81,11 +76,11 @@ fn main() { let paths: Vec = args.paths .iter() .map(|p| { - Path::new(&p) - .canonicalize() - .expect(&format!("unable to canonicalize \"{}\"", &p)) - .to_owned() - }) + Path::new(&p) + .canonicalize() + .expect(&format!("unable to canonicalize \"{}\"", &p)) + .to_owned() + }) .collect(); let gitignore = if !args.no_vcs_ignore { @@ -98,8 +93,8 @@ fn main() { .expect("unable to create notification filter"); let (tx, rx) = channel(); - let watcher = Watcher::new(tx, &paths, args.poll, args.poll_interval) - .expect("unable to create watcher"); + let watcher = + Watcher::new(tx, &paths, args.poll, args.poll_interval).expect("unable to create watcher"); if watcher.is_polling() { warn!("Polling for changes every {} ms", args.poll_interval); @@ -122,23 +117,76 @@ fn main() { debug!("Path updated: {:?}", path); } - // Wait for current child process to exit - wait_process(&child_process, kill, args.restart); + // 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()); - // Launch child process - if args.clear_screen { - cli::clear_screen(); - } + 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); - debug!("Launching child process"); - { - let mut guard = child_process.write().unwrap(); - *guard = Some(process::spawn(&args.cmd, paths)); + // 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)); + } + } + + // 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)); + } + } + + // 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)); + } + } } // Handle once option for integration testing if args.once { - wait_process(&child_process, kill, false); + signal_process(&child_process, signal, false); break; } } @@ -188,20 +236,18 @@ fn wait_fs(rx: &Receiver, filter: &NotificationFilter) -> Vec { paths } -fn wait_process(process: &RwLock>, kill: bool, restart: bool) { +// signal_process sends signal to process. It waits for the process to exit if wait is true +fn signal_process(process: &RwLock>, signal: Option, wait: bool) { let guard = process.read().unwrap(); if let Some(ref child) = *guard { - if restart { - debug!("Stopping child process"); - if kill { - child.kill(); - } else { - child.terminate(); - } + if let Some(s) = signal { + child.signal(s); } - debug!("Waiting for process to exit..."); - child.wait(); + if wait { + debug!("Waiting for process to exit..."); + child.wait(); + } } } diff --git a/src/notification_filter.rs b/src/notification_filter.rs index f0a0302..8c0c607 100644 --- a/src/notification_filter.rs +++ b/src/notification_filter.rs @@ -44,11 +44,11 @@ impl NotificationFilter { let ignore_set = try!(ignore_set_builder.build()); Ok(NotificationFilter { - filters: filter_set, - filter_count: filters.len(), - ignores: ignore_set, - ignore_files: ignore_files, - }) + filters: filter_set, + filter_count: filters.len(), + ignores: ignore_set, + ignore_files: ignore_files, + }) } pub fn is_excluded(&self, path: &Path) -> bool { diff --git a/src/process.rs b/src/process.rs index 2cef8f7..b0b7ff9 100644 --- a/src/process.rs +++ b/src/process.rs @@ -13,6 +13,7 @@ mod imp { use std::path::PathBuf; use std::process::Command; use std::sync::*; + use signal::Signal; pub struct Process { pgid: pid_t, @@ -40,23 +41,16 @@ mod imp { } // Until process_exec lands in stable, handle fork/exec ourselves - command.before_exec(|| setpgid(0, 0).map_err(io::Error::from)) + command + .before_exec(|| setpgid(0, 0).map_err(io::Error::from)) .spawn() .and_then(|p| { - Ok(Process { - pgid: p.id() as i32, - lock: Mutex::new(false), - cvar: Condvar::new(), - }) - }) - } - - pub fn kill(&self) { - self.signal(SIGKILL); - } - - pub fn pause(&self) { - self.signal(SIGTSTP); + Ok(Process { + pgid: p.id() as i32, + lock: Mutex::new(false), + cvar: Condvar::new(), + }) + }) } pub fn reap(&self) { @@ -82,11 +76,15 @@ mod imp { } } - pub fn resume(&self) { - self.signal(SIGCONT); + pub fn signal(&self, signal: Signal) { + use signal::ConvertToLibc; + + let signo = signal.convert_to_libc(); + debug!("Sending {:?} (int: {}) to child process", signal, signo); + self.c_signal(signo); } - fn signal(&self, sig: c_int) { + fn c_signal(&self, sig: c_int) { extern "C" { fn killpg(pgrp: pid_t, sig: c_int) -> c_int; } @@ -97,10 +95,6 @@ mod imp { } - pub fn terminate(&self) { - self.signal(SIGTERM); - } - pub fn wait(&self) { let mut done = self.lock.lock().unwrap(); while !*done { @@ -119,6 +113,7 @@ mod imp { use std::process::Command; use kernel32::*; use winapi::*; + use signal::Signal; pub struct Process { job: HANDLE, @@ -160,27 +155,22 @@ mod imp { command.env("WATCHEXEC_COMMON_PATH", common_path); } - command.spawn().and_then(|p| { - let r = unsafe { AssignProcessToJobObject(job, p.into_raw_handle()) }; - if r == 0 { - panic!("failed to add to job object: {}", last_err()); - } + command + .spawn() + .and_then(|p| { + let r = unsafe { AssignProcessToJobObject(job, p.into_raw_handle()) }; + if r == 0 { + panic!("failed to add to job object: {}", last_err()); + } - Ok(Process { job: job }) - }) + Ok(Process { job: job }) + }) } - pub fn kill(&self) { - self.terminate(); - } - - pub fn pause(&self) {} - pub fn reap(&self) {} - pub fn resume(&self) {} - - pub fn terminate(&self) { + pub fn signal(&self, signal: Signal) { + debug!("Ignoring signal {:?} (not supported by Windows)", signal); unsafe { let _ = TerminateJobObject(self.job, 1); } diff --git a/src/signal.rs b/src/signal.rs index be6c7c9..ea1c99e 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -4,12 +4,70 @@ lazy_static! { static ref CLEANUP: Mutex>> = Mutex::new(None); } -#[allow(dead_code)] +#[cfg(unix)] +pub use nix::sys::signal::Signal; + +// This is a dummy enum for Windows +#[cfg(windows)] +#[derive(Debug, Copy, Clone)] pub enum Signal { - Terminate, - Stop, - Continue, - ChildExit, + SIGKILL, + SIGTERM, + SIGINT, + SIGHUP, + SIGSTOP, + SIGCONT, + SIGCHLD, + SIGUSR1, + SIGUSR2, +} + +#[cfg(unix)] +use libc::*; + +#[cfg(unix)] +pub trait ConvertToLibc { + fn convert_to_libc(self) -> c_int; +} + +#[cfg(unix)] +impl ConvertToLibc for Signal { + fn convert_to_libc(self) -> c_int { + // Convert from signal::Signal enum to libc::* c_int constants + match self { + Signal::SIGKILL => SIGKILL, + Signal::SIGTERM => SIGTERM, + Signal::SIGINT => SIGINT, + Signal::SIGHUP => SIGHUP, + Signal::SIGSTOP => SIGSTOP, + Signal::SIGCONT => SIGCONT, + Signal::SIGCHLD => SIGCHLD, + Signal::SIGUSR1 => SIGUSR1, + Signal::SIGUSR2 => SIGUSR2, + _ => panic!("unsupported signal: {:?}", self), + } + } +} + +pub fn new(signal_name: Option) -> Option { + if let Some(signame) = signal_name { + let signal = match signame.as_ref() { + "SIGKILL" | "KILL" => Signal::SIGKILL, + "SIGTERM" | "TERM" => Signal::SIGTERM, + "SIGINT" | "INT" => Signal::SIGINT, + "SIGHUP" | "HUP" => Signal::SIGHUP, + "SIGSTOP" | "STOP" => Signal::SIGSTOP, + "SIGCONT" | "CONT" => Signal::SIGCONT, + "SIGCHLD" | "CHLD" => Signal::SIGCHLD, + "SIGUSR1" | "USR1" => Signal::SIGUSR1, + "SIGUSR2" | "USR2" => Signal::SIGUSR2, + _ => panic!("unsupported signal: {}", signame), + }; + + Some(signal) + } else { + None + } } #[cfg(unix)] @@ -23,12 +81,17 @@ pub fn install_handler(handler: F) // Mask all signals interesting to us. The mask propagates // to all threads started after this point. let mut mask = SigSet::empty(); + mask.add(SIGKILL); mask.add(SIGTERM); mask.add(SIGINT); - mask.add(SIGTSTP); + mask.add(SIGHUP); + mask.add(SIGSTOP); mask.add(SIGCONT); mask.add(SIGCHLD); - mask.thread_set_mask().expect("unable to set signal mask"); + mask.add(SIGUSR1); + mask.add(SIGUSR2); + mask.thread_set_mask() + .expect("unable to set signal mask"); set_handler(handler); @@ -45,36 +108,28 @@ pub fn install_handler(handler: F) // Spawn a thread to catch these signals thread::spawn(move || { loop { - let raw_signal = mask.wait().expect("unable to sigwait"); - debug!("Received {:?}", raw_signal); - - let sig = match raw_signal { - SIGTERM | SIGINT => self::Signal::Terminate, - SIGTSTP => self::Signal::Stop, - SIGCONT => self::Signal::Continue, - SIGCHLD => self::Signal::ChildExit, - _ => unreachable!(), - }; + let signal = mask.wait().expect("Unable to sigwait"); + debug!("Received {:?}", signal); // Invoke closure - invoke(sig); + invoke(signal); // Restore default behavior for received signal and unmask it - if raw_signal != SIGCHLD { + if signal != SIGCHLD { let default_action = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty()); unsafe { - let _ = sigaction(raw_signal, &default_action); + let _ = sigaction(signal, &default_action); } } let mut new_mask = SigSet::empty(); - new_mask.add(raw_signal); + new_mask.add(signal); // Re-raise with signal unmasked let _ = new_mask.thread_unblock(); - let _ = raise(raw_signal); + let _ = raise(signal); let _ = new_mask.thread_block(); } }); @@ -88,7 +143,7 @@ pub fn install_handler(handler: F) use winapi::{BOOL, DWORD, FALSE, TRUE}; pub unsafe extern "system" fn ctrl_handler(_: DWORD) -> BOOL { - invoke(self::Signal::Terminate); + invoke(self::Signal::SIGTERM); FALSE }