Merge pull request #43 from chr4/signal_flag

Signal flag
This commit is contained in:
Matt Green 2017-04-03 12:04:52 -04:00 committed by GitHub
commit 00a339a2f1
10 changed files with 304 additions and 185 deletions

View File

@ -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:

View File

@ -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 <SIGKILL|SIGHUP|...>\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

View File

@ -88,7 +88,7 @@
<dt class="flush"><var>command</var></dt><dd><p>Command to run when watched files are modified, and at startup, unless <code>--postpone</code> is specified. All <var>argument</var>s are passed to <var>command</var>.</p></dd>
<dt><code>-e</code>, <code>--exts</code> <var>extensions</var></dt><dd><p>Comma-separated list of file extensions to filter by. Leading dots are allowed (.rs) are allowed. (This is a shorthand for <code>-f</code>).</p></dd>
<dt><code>-f</code>, <code>--filter</code> <var>pattern</var></dt><dd><p>Ignores modifications from paths that do not match <var>pattern</var>. This option can be specified multiple times, where a match on any given pattern causes the path to trigger <var>command</var>.</p></dd>
<dt><code>-k</code>, <code>--kill</code></dt><dd><p>Send <code>SIGKILL</code> to the child process group instead of <code>SIGTERM</code>.</p></dd>
<dt><code>-s</code>, <code>--signal</code> <var>SIGNAL</var></dt><dd><p> Sends the specified signal (e.g. <code>SIGKILl</code>) to the child process. Defaults to <code>SIGTERM</code>.</p></dd>
<dt><code>-i</code>, <code>--ignore</code> <var>pattern</var></dt><dd><p>Ignores modifications from paths that match <var>pattern</var>. This option can be specified multiple times, and a match on any pattern causes the path to be ignored.</p></dd>
<dt><code>-w</code>, <code>--watch</code> <var>path</var></dt><dd><p>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 <var>command</var> to be executed.</p></dd>
<dt><code>-r</code>, <code>--restart</code></dt><dd><p>Terminates the child process group if it is still running when subsequent file modifications are detected. By default, sends <code>SIGTERM</code>; use <code>--kill</code> to send <code>SIGKILL</code>.</p></dd>

View File

@ -22,8 +22,8 @@ Comma-separated list of file extensions to filter by. Leading dots are allowed (
* `-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`:
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.

View File

@ -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<String>,
pub ignores: Vec<String>,
pub clear_screen: bool,
pub kill: bool,
pub signal: Option<String>,
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<String>
};
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"),

View File

@ -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<Pattern> {
contents.iter()
contents
.iter()
.filter(|l| !l.is_empty())
.filter(|l| !l.starts_with('#'))
.map(|l| Pattern::parse(l))

View File

@ -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<RwLock<Option<Process>>> = 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<PathBuf> = 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<Event>, filter: &NotificationFilter) -> Vec<PathBuf> {
paths
}
fn wait_process(process: &RwLock<Option<Process>>, 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<Option<Process>>, signal: Option<Signal>, 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();
}
}
}

View File

@ -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 {

View File

@ -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);
}

View File

@ -4,12 +4,70 @@ lazy_static! {
static ref CLEANUP: Mutex<Option<Box<Fn(self::Signal) + Send>>> = 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<String>) -> Option<Signal> {
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<F>(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<F>(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<F>(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
}