commit
00a339a2f1
|
@ -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:
|
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:
|
Run `make` when any file changes, using the `.gitignore` file in the current directory to filter:
|
||||||
|
|
||||||
|
|
|
@ -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\.
|
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
|
.TP
|
||||||
\fB\-k\fR, \fB\-\-kill\fR
|
\fB\-s\fR, \fB\-\-signal <SIGKILL|SIGHUP|...>\fR
|
||||||
Send \fBSIGKILL\fR to the child process group instead of \fBSIGTERM\fR\.
|
Sends the specified signal (e\.g\. \fBSIGKILL\fR) to the child process\. Defaults to \fBSIGTERM\fR\.
|
||||||
.
|
.
|
||||||
.TP
|
.TP
|
||||||
\fB\-i\fR, \fB\-\-ignore\fR \fIpattern\fR
|
\fB\-i\fR, \fB\-\-ignore\fR \fIpattern\fR
|
||||||
|
|
|
@ -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 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>-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>-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>-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>-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>
|
<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>
|
||||||
|
|
|
@ -22,8 +22,8 @@ Comma-separated list of file extensions to filter by. Leading dots are allowed (
|
||||||
* `-f`, `--filter` <pattern>:
|
* `-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>.
|
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`:
|
* `-s`, `--signal`:
|
||||||
Send `SIGKILL` to the child process group instead of `SIGTERM`.
|
Sends the specified signal (e.g. `SIGKILL`) to the child process. Defaults to `SIGTERM`.
|
||||||
|
|
||||||
* `-i`, `--ignore` <pattern>:
|
* `-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.
|
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.
|
||||||
|
|
149
src/cli.rs
149
src/cli.rs
|
@ -1,7 +1,7 @@
|
||||||
use std::path::MAIN_SEPARATOR;
|
use std::path::MAIN_SEPARATOR;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use clap::{App, Arg};
|
use clap::{App, Arg, Error};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
@ -10,7 +10,7 @@ pub struct Args {
|
||||||
pub filters: Vec<String>,
|
pub filters: Vec<String>,
|
||||||
pub ignores: Vec<String>,
|
pub ignores: Vec<String>,
|
||||||
pub clear_screen: bool,
|
pub clear_screen: bool,
|
||||||
pub kill: bool,
|
pub signal: Option<String>,
|
||||||
pub restart: bool,
|
pub restart: bool,
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
pub run_initially: bool,
|
pub run_initially: bool,
|
||||||
|
@ -37,79 +37,92 @@ pub fn get_args() -> Args {
|
||||||
.version(crate_version!())
|
.version(crate_version!())
|
||||||
.about("Execute commands when watched files change")
|
.about("Execute commands when watched files change")
|
||||||
.arg(Arg::with_name("command")
|
.arg(Arg::with_name("command")
|
||||||
.help("Command to execute")
|
.help("Command to execute")
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.required(true))
|
.required(true))
|
||||||
.arg(Arg::with_name("extensions")
|
.arg(Arg::with_name("extensions")
|
||||||
.help("Comma-separated list of file extensions to watch (js,css,html)")
|
.help("Comma-separated list of file extensions to watch (js,css,html)")
|
||||||
.short("e")
|
.short("e")
|
||||||
.long("exts")
|
.long("exts")
|
||||||
.takes_value(true))
|
.takes_value(true))
|
||||||
.arg(Arg::with_name("path")
|
.arg(Arg::with_name("path")
|
||||||
.help("Watch a specific directory")
|
.help("Watch a specific directory")
|
||||||
.short("w")
|
.short("w")
|
||||||
.long("watch")
|
.long("watch")
|
||||||
.number_of_values(1)
|
.number_of_values(1)
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.takes_value(true))
|
.takes_value(true))
|
||||||
.arg(Arg::with_name("clear")
|
.arg(Arg::with_name("clear")
|
||||||
.help("Clear screen before executing command")
|
.help("Clear screen before executing command")
|
||||||
.short("c")
|
.short("c")
|
||||||
.long("clear"))
|
.long("clear"))
|
||||||
.arg(Arg::with_name("restart")
|
.arg(Arg::with_name("restart")
|
||||||
.help("Restart the process if it's still running")
|
.help("Restart the process if it's still running")
|
||||||
.short("r")
|
.short("r")
|
||||||
.long("restart"))
|
.long("restart"))
|
||||||
.arg(Arg::with_name("debug")
|
.arg(Arg::with_name("signal")
|
||||||
.help("Print debugging messages to stderr")
|
.help("Send signal to process upon changes, e.g. SIGHUP")
|
||||||
.short("d")
|
.short("s")
|
||||||
.long("debug"))
|
.long("signal")
|
||||||
.arg(Arg::with_name("filter")
|
.takes_value(true)
|
||||||
.help("Ignore all modifications except those matching the pattern")
|
.number_of_values(1)
|
||||||
.short("f")
|
.value_name("signal"))
|
||||||
.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("kill")
|
.arg(Arg::with_name("kill")
|
||||||
.help("Send SIGKILL to child processes")
|
.help("Send SIGKILL to child processes (deprecated, use -s SIGKILL instead)")
|
||||||
.short("k")
|
.short("k")
|
||||||
.long("kill"))
|
.long("kill"))
|
||||||
.arg(Arg::with_name("once")
|
.arg(Arg::with_name("debug")
|
||||||
.short("1")
|
.help("Print debugging messages to stderr")
|
||||||
.hidden(true))
|
.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();
|
.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(".")]);
|
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![]);
|
let mut filters = values_t!(args.values_of("filter"), String).unwrap_or(vec![]);
|
||||||
|
|
||||||
if let Some(extensions) = args.values_of("extensions") {
|
if let Some(extensions) = args.values_of("extensions") {
|
||||||
for exts in extensions {
|
for exts in extensions {
|
||||||
filters.extend(exts.split(',')
|
filters.extend(exts.split(',')
|
||||||
.filter(|ext| !ext.is_empty())
|
.filter(|ext| !ext.is_empty())
|
||||||
.map(|ext| format!("*.{}", ext.replace(".", ""))));
|
.map(|ext| format!("*.{}", ext.replace(".", ""))));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,13 +142,25 @@ pub fn get_args() -> Args {
|
||||||
1000
|
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 {
|
Args {
|
||||||
cmd: cmd,
|
cmd: cmd,
|
||||||
paths: paths,
|
paths: paths,
|
||||||
filters: filters,
|
filters: filters,
|
||||||
ignores: ignores,
|
ignores: ignores,
|
||||||
|
signal: signal,
|
||||||
clear_screen: args.is_present("clear"),
|
clear_screen: args.is_present("clear"),
|
||||||
kill: args.is_present("kill"),
|
|
||||||
restart: args.is_present("restart"),
|
restart: args.is_present("restart"),
|
||||||
debug: args.is_present("debug"),
|
debug: args.is_present("debug"),
|
||||||
run_initially: !args.is_present("postpone"),
|
run_initially: !args.is_present("postpone"),
|
||||||
|
|
|
@ -137,19 +137,17 @@ impl GitignoreFile {
|
||||||
pat = pat + "/**";
|
pat = pat + "/**";
|
||||||
}
|
}
|
||||||
|
|
||||||
let glob = try!(GlobBuilder::new(&pat)
|
let glob = try!(GlobBuilder::new(&pat).literal_separator(true).build());
|
||||||
.literal_separator(true)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
builder.add(glob);
|
builder.add(glob);
|
||||||
patterns.push(p);
|
patterns.push(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(GitignoreFile {
|
Ok(GitignoreFile {
|
||||||
set: try!(builder.build()),
|
set: try!(builder.build()),
|
||||||
patterns: patterns,
|
patterns: patterns,
|
||||||
root: root.to_owned(),
|
root: root.to_owned(),
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,9 +167,9 @@ impl GitignoreFile {
|
||||||
for &i in matches.iter().rev() {
|
for &i in matches.iter().rev() {
|
||||||
let pattern = &self.patterns[i];
|
let pattern = &self.patterns[i];
|
||||||
return match pattern.pattern_type {
|
return match pattern.pattern_type {
|
||||||
PatternType::Whitelist => MatchResult::Whitelist,
|
PatternType::Whitelist => MatchResult::Whitelist,
|
||||||
PatternType::Ignore => MatchResult::Ignore,
|
PatternType::Ignore => MatchResult::Ignore,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
MatchResult::None
|
MatchResult::None
|
||||||
|
@ -182,7 +180,8 @@ impl GitignoreFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(contents: Vec<&str>) -> Vec<Pattern> {
|
fn parse(contents: Vec<&str>) -> Vec<Pattern> {
|
||||||
contents.iter()
|
contents
|
||||||
|
.iter()
|
||||||
.filter(|l| !l.is_empty())
|
.filter(|l| !l.is_empty())
|
||||||
.filter(|l| !l.starts_with('#'))
|
.filter(|l| !l.starts_with('#'))
|
||||||
.map(|l| Pattern::parse(l))
|
.map(|l| Pattern::parse(l))
|
||||||
|
|
126
src/main.rs
126
src/main.rs
|
@ -45,7 +45,8 @@ fn init_logger(debug: bool) {
|
||||||
log::LogLevelFilter::Warn
|
log::LogLevelFilter::Warn
|
||||||
};
|
};
|
||||||
|
|
||||||
log_builder.format(|r| format!("*** {}", r.args()))
|
log_builder
|
||||||
|
.format(|r| format!("*** {}", r.args()))
|
||||||
.filter(None, level);
|
.filter(None, level);
|
||||||
log_builder.init().expect("unable to initialize logger");
|
log_builder.init().expect("unable to initialize logger");
|
||||||
}
|
}
|
||||||
|
@ -54,23 +55,17 @@ fn main() {
|
||||||
let args = cli::get_args();
|
let args = cli::get_args();
|
||||||
let child_process: Arc<RwLock<Option<Process>>> = Arc::new(RwLock::new(None));
|
let child_process: Arc<RwLock<Option<Process>>> = Arc::new(RwLock::new(None));
|
||||||
let weak_child = Arc::downgrade(&child_process);
|
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| {
|
signal::install_handler(move |sig: Signal| {
|
||||||
if let Some(lock) = weak_child.upgrade() {
|
if let Some(lock) = weak_child.upgrade() {
|
||||||
let strong = lock.read().unwrap();
|
let strong = lock.read().unwrap();
|
||||||
if let Some(ref child) = *strong {
|
if let Some(ref child) = *strong {
|
||||||
match sig {
|
match sig {
|
||||||
Signal::Terminate => {
|
Signal::SIGCHLD => child.reap(), // SIGCHLD is special, initiate reap()
|
||||||
if kill {
|
_ => child.signal(sig),
|
||||||
child.kill();
|
|
||||||
} else {
|
|
||||||
child.terminate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Signal::Stop => child.pause(),
|
|
||||||
Signal::Continue => child.resume(),
|
|
||||||
Signal::ChildExit => child.reap(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,11 +76,11 @@ fn main() {
|
||||||
let paths: Vec<PathBuf> = args.paths
|
let paths: Vec<PathBuf> = args.paths
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| {
|
.map(|p| {
|
||||||
Path::new(&p)
|
Path::new(&p)
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.expect(&format!("unable to canonicalize \"{}\"", &p))
|
.expect(&format!("unable to canonicalize \"{}\"", &p))
|
||||||
.to_owned()
|
.to_owned()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let gitignore = if !args.no_vcs_ignore {
|
let gitignore = if !args.no_vcs_ignore {
|
||||||
|
@ -98,8 +93,8 @@ fn main() {
|
||||||
.expect("unable to create notification filter");
|
.expect("unable to create notification filter");
|
||||||
|
|
||||||
let (tx, rx) = channel();
|
let (tx, rx) = channel();
|
||||||
let watcher = Watcher::new(tx, &paths, args.poll, args.poll_interval)
|
let watcher =
|
||||||
.expect("unable to create watcher");
|
Watcher::new(tx, &paths, args.poll, args.poll_interval).expect("unable to create watcher");
|
||||||
|
|
||||||
if watcher.is_polling() {
|
if watcher.is_polling() {
|
||||||
warn!("Polling for changes every {} ms", args.poll_interval);
|
warn!("Polling for changes every {} ms", args.poll_interval);
|
||||||
|
@ -122,23 +117,76 @@ fn main() {
|
||||||
debug!("Path updated: {:?}", path);
|
debug!("Path updated: {:?}", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for current child process to exit
|
// We have three scenarios here:
|
||||||
wait_process(&child_process, kill, args.restart);
|
//
|
||||||
|
// 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
|
match scenario {
|
||||||
if args.clear_screen {
|
// Custom restart behaviour (--restart was given, and --signal specified):
|
||||||
cli::clear_screen();
|
// 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");
|
// Launch child process
|
||||||
{
|
if args.clear_screen {
|
||||||
let mut guard = child_process.write().unwrap();
|
cli::clear_screen();
|
||||||
*guard = Some(process::spawn(&args.cmd, paths));
|
}
|
||||||
|
|
||||||
|
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
|
// Handle once option for integration testing
|
||||||
if args.once {
|
if args.once {
|
||||||
wait_process(&child_process, kill, false);
|
signal_process(&child_process, signal, false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -188,20 +236,18 @@ fn wait_fs(rx: &Receiver<Event>, filter: &NotificationFilter) -> Vec<PathBuf> {
|
||||||
paths
|
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();
|
let guard = process.read().unwrap();
|
||||||
|
|
||||||
if let Some(ref child) = *guard {
|
if let Some(ref child) = *guard {
|
||||||
if restart {
|
if let Some(s) = signal {
|
||||||
debug!("Stopping child process");
|
child.signal(s);
|
||||||
if kill {
|
|
||||||
child.kill();
|
|
||||||
} else {
|
|
||||||
child.terminate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Waiting for process to exit...");
|
if wait {
|
||||||
child.wait();
|
debug!("Waiting for process to exit...");
|
||||||
|
child.wait();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,11 +44,11 @@ impl NotificationFilter {
|
||||||
let ignore_set = try!(ignore_set_builder.build());
|
let ignore_set = try!(ignore_set_builder.build());
|
||||||
|
|
||||||
Ok(NotificationFilter {
|
Ok(NotificationFilter {
|
||||||
filters: filter_set,
|
filters: filter_set,
|
||||||
filter_count: filters.len(),
|
filter_count: filters.len(),
|
||||||
ignores: ignore_set,
|
ignores: ignore_set,
|
||||||
ignore_files: ignore_files,
|
ignore_files: ignore_files,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_excluded(&self, path: &Path) -> bool {
|
pub fn is_excluded(&self, path: &Path) -> bool {
|
||||||
|
|
|
@ -13,6 +13,7 @@ mod imp {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::*;
|
use std::sync::*;
|
||||||
|
use signal::Signal;
|
||||||
|
|
||||||
pub struct Process {
|
pub struct Process {
|
||||||
pgid: pid_t,
|
pgid: pid_t,
|
||||||
|
@ -40,23 +41,16 @@ mod imp {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Until process_exec lands in stable, handle fork/exec ourselves
|
// 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()
|
.spawn()
|
||||||
.and_then(|p| {
|
.and_then(|p| {
|
||||||
Ok(Process {
|
Ok(Process {
|
||||||
pgid: p.id() as i32,
|
pgid: p.id() as i32,
|
||||||
lock: Mutex::new(false),
|
lock: Mutex::new(false),
|
||||||
cvar: Condvar::new(),
|
cvar: Condvar::new(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
pub fn kill(&self) {
|
|
||||||
self.signal(SIGKILL);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pause(&self) {
|
|
||||||
self.signal(SIGTSTP);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reap(&self) {
|
pub fn reap(&self) {
|
||||||
|
@ -82,11 +76,15 @@ mod imp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resume(&self) {
|
pub fn signal(&self, signal: Signal) {
|
||||||
self.signal(SIGCONT);
|
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" {
|
extern "C" {
|
||||||
fn killpg(pgrp: pid_t, sig: c_int) -> c_int;
|
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) {
|
pub fn wait(&self) {
|
||||||
let mut done = self.lock.lock().unwrap();
|
let mut done = self.lock.lock().unwrap();
|
||||||
while !*done {
|
while !*done {
|
||||||
|
@ -119,6 +113,7 @@ mod imp {
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use kernel32::*;
|
use kernel32::*;
|
||||||
use winapi::*;
|
use winapi::*;
|
||||||
|
use signal::Signal;
|
||||||
|
|
||||||
pub struct Process {
|
pub struct Process {
|
||||||
job: HANDLE,
|
job: HANDLE,
|
||||||
|
@ -160,27 +155,22 @@ mod imp {
|
||||||
command.env("WATCHEXEC_COMMON_PATH", common_path);
|
command.env("WATCHEXEC_COMMON_PATH", common_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
command.spawn().and_then(|p| {
|
command
|
||||||
let r = unsafe { AssignProcessToJobObject(job, p.into_raw_handle()) };
|
.spawn()
|
||||||
if r == 0 {
|
.and_then(|p| {
|
||||||
panic!("failed to add to job object: {}", last_err());
|
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 reap(&self) {}
|
||||||
|
|
||||||
pub fn resume(&self) {}
|
pub fn signal(&self, signal: Signal) {
|
||||||
|
debug!("Ignoring signal {:?} (not supported by Windows)", signal);
|
||||||
pub fn terminate(&self) {
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let _ = TerminateJobObject(self.job, 1);
|
let _ = TerminateJobObject(self.job, 1);
|
||||||
}
|
}
|
||||||
|
|
101
src/signal.rs
101
src/signal.rs
|
@ -4,12 +4,70 @@ lazy_static! {
|
||||||
static ref CLEANUP: Mutex<Option<Box<Fn(self::Signal) + Send>>> = Mutex::new(None);
|
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 {
|
pub enum Signal {
|
||||||
Terminate,
|
SIGKILL,
|
||||||
Stop,
|
SIGTERM,
|
||||||
Continue,
|
SIGINT,
|
||||||
ChildExit,
|
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)]
|
#[cfg(unix)]
|
||||||
|
@ -23,12 +81,17 @@ pub fn install_handler<F>(handler: F)
|
||||||
// Mask all signals interesting to us. The mask propagates
|
// Mask all signals interesting to us. The mask propagates
|
||||||
// to all threads started after this point.
|
// to all threads started after this point.
|
||||||
let mut mask = SigSet::empty();
|
let mut mask = SigSet::empty();
|
||||||
|
mask.add(SIGKILL);
|
||||||
mask.add(SIGTERM);
|
mask.add(SIGTERM);
|
||||||
mask.add(SIGINT);
|
mask.add(SIGINT);
|
||||||
mask.add(SIGTSTP);
|
mask.add(SIGHUP);
|
||||||
|
mask.add(SIGSTOP);
|
||||||
mask.add(SIGCONT);
|
mask.add(SIGCONT);
|
||||||
mask.add(SIGCHLD);
|
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);
|
set_handler(handler);
|
||||||
|
|
||||||
|
@ -45,36 +108,28 @@ pub fn install_handler<F>(handler: F)
|
||||||
// Spawn a thread to catch these signals
|
// Spawn a thread to catch these signals
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
loop {
|
loop {
|
||||||
let raw_signal = mask.wait().expect("unable to sigwait");
|
let signal = mask.wait().expect("Unable to sigwait");
|
||||||
debug!("Received {:?}", raw_signal);
|
debug!("Received {:?}", signal);
|
||||||
|
|
||||||
let sig = match raw_signal {
|
|
||||||
SIGTERM | SIGINT => self::Signal::Terminate,
|
|
||||||
SIGTSTP => self::Signal::Stop,
|
|
||||||
SIGCONT => self::Signal::Continue,
|
|
||||||
SIGCHLD => self::Signal::ChildExit,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Invoke closure
|
// Invoke closure
|
||||||
invoke(sig);
|
invoke(signal);
|
||||||
|
|
||||||
// Restore default behavior for received signal and unmask it
|
// Restore default behavior for received signal and unmask it
|
||||||
if raw_signal != SIGCHLD {
|
if signal != SIGCHLD {
|
||||||
let default_action =
|
let default_action =
|
||||||
SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty());
|
SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty());
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let _ = sigaction(raw_signal, &default_action);
|
let _ = sigaction(signal, &default_action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut new_mask = SigSet::empty();
|
let mut new_mask = SigSet::empty();
|
||||||
new_mask.add(raw_signal);
|
new_mask.add(signal);
|
||||||
|
|
||||||
// Re-raise with signal unmasked
|
// Re-raise with signal unmasked
|
||||||
let _ = new_mask.thread_unblock();
|
let _ = new_mask.thread_unblock();
|
||||||
let _ = raise(raw_signal);
|
let _ = raise(signal);
|
||||||
let _ = new_mask.thread_block();
|
let _ = new_mask.thread_block();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -88,7 +143,7 @@ pub fn install_handler<F>(handler: F)
|
||||||
use winapi::{BOOL, DWORD, FALSE, TRUE};
|
use winapi::{BOOL, DWORD, FALSE, TRUE};
|
||||||
|
|
||||||
pub unsafe extern "system" fn ctrl_handler(_: DWORD) -> BOOL {
|
pub unsafe extern "system" fn ctrl_handler(_: DWORD) -> BOOL {
|
||||||
invoke(self::Signal::Terminate);
|
invoke(self::Signal::SIGTERM);
|
||||||
|
|
||||||
FALSE
|
FALSE
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue