292 lines
7.0 KiB
Rust
292 lines
7.0 KiB
Rust
use std::{
|
|
collections::HashMap, convert::Infallible, env::current_dir, ffi::OsString, path::Path,
|
|
str::FromStr, time::Duration,
|
|
};
|
|
|
|
use clap::ArgMatches;
|
|
use miette::{miette, IntoDiagnostic, Result};
|
|
use notify_rust::Notification;
|
|
use tracing::debug;
|
|
use watchexec::{
|
|
action::{Action, Outcome, PostSpawn, PreSpawn},
|
|
command::Shell,
|
|
config::RuntimeConfig,
|
|
event::ProcessEnd,
|
|
fs::Watcher,
|
|
handler::SyncFnHandler,
|
|
paths::summarise_events_to_env,
|
|
signal::{process::SubSignal, source::MainSignal},
|
|
};
|
|
|
|
pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
|
|
let mut config = RuntimeConfig::default();
|
|
|
|
config.command(
|
|
args.values_of_lossy("command")
|
|
.expect("(clap) Bug: command is not present")
|
|
.iter(),
|
|
);
|
|
|
|
config.pathset(match args.values_of_os("paths") {
|
|
Some(paths) => paths.map(|os| Path::new(os).to_owned()).collect(),
|
|
None => vec![current_dir().into_diagnostic()?],
|
|
});
|
|
|
|
config.action_throttle(Duration::from_millis(
|
|
args.value_of("debounce")
|
|
.unwrap_or("50")
|
|
.parse()
|
|
.into_diagnostic()?,
|
|
));
|
|
|
|
if let Some(interval) = args.value_of("poll") {
|
|
config.file_watcher(Watcher::Poll(Duration::from_millis(
|
|
interval.parse().into_diagnostic()?,
|
|
)));
|
|
}
|
|
|
|
if args.is_present("no-process-group") {
|
|
config.command_grouped(false);
|
|
}
|
|
|
|
config.command_shell(if args.is_present("no-shell") {
|
|
Shell::None
|
|
} else if let Some(s) = args.value_of("shell") {
|
|
if s.eq_ignore_ascii_case("powershell") {
|
|
Shell::Powershell
|
|
} else if s.eq_ignore_ascii_case("none") {
|
|
Shell::None
|
|
} else if s.eq_ignore_ascii_case("cmd") {
|
|
cmd_shell(s.into())
|
|
} else {
|
|
Shell::Unix(s.into())
|
|
}
|
|
} else {
|
|
default_shell()
|
|
});
|
|
|
|
let clear = args.is_present("clear");
|
|
let notif = args.is_present("notif");
|
|
let mut on_busy = args
|
|
.value_of("on-busy-update")
|
|
.unwrap_or("queue")
|
|
.to_owned();
|
|
|
|
if args.is_present("restart") {
|
|
on_busy = "restart".into();
|
|
}
|
|
|
|
if args.is_present("watch-when-idle") {
|
|
on_busy = "do-nothing".into();
|
|
}
|
|
|
|
let mut signal = args
|
|
.value_of("signal")
|
|
.map(SubSignal::from_str)
|
|
.transpose()
|
|
.into_diagnostic()?
|
|
.unwrap_or(SubSignal::Terminate);
|
|
|
|
if args.is_present("kill") {
|
|
signal = SubSignal::ForceStop;
|
|
}
|
|
|
|
let print_events = args.is_present("print-events");
|
|
let once = args.is_present("once");
|
|
|
|
config.on_action(move |action: Action| {
|
|
let fut = async { Ok::<(), Infallible>(()) };
|
|
|
|
if print_events {
|
|
for (n, event) in action.events.iter().enumerate() {
|
|
eprintln!("[EVENT {}] {}", n, event);
|
|
}
|
|
}
|
|
|
|
if once {
|
|
action.outcome(Outcome::both(Outcome::Start, Outcome::wait(Outcome::Exit)));
|
|
return fut;
|
|
}
|
|
|
|
let signals: Vec<MainSignal> = action.events.iter().flat_map(|e| e.signals()).collect();
|
|
let has_paths = action
|
|
.events
|
|
.iter()
|
|
.flat_map(|e| e.paths())
|
|
.next()
|
|
.is_some();
|
|
|
|
if signals.contains(&MainSignal::Terminate) {
|
|
action.outcome(Outcome::both(Outcome::Stop, Outcome::Exit));
|
|
return fut;
|
|
}
|
|
|
|
if signals.contains(&MainSignal::Interrupt) {
|
|
action.outcome(Outcome::both(Outcome::Stop, Outcome::Exit));
|
|
return fut;
|
|
}
|
|
|
|
if !has_paths {
|
|
if !signals.is_empty() {
|
|
let mut out = Outcome::DoNothing;
|
|
for sig in signals {
|
|
out = Outcome::both(out, Outcome::Signal(sig.into()));
|
|
}
|
|
|
|
action.outcome(out);
|
|
return fut;
|
|
}
|
|
|
|
let completion = action.events.iter().flat_map(|e| e.completions()).next();
|
|
if let Some(status) = completion {
|
|
let (msg, printit) = match status {
|
|
Some(ProcessEnd::ExitError(code)) => {
|
|
(format!("Command exited with {}", code), true)
|
|
}
|
|
Some(ProcessEnd::ExitSignal(sig)) => {
|
|
(format!("Command killed by {:?}", sig), true)
|
|
}
|
|
Some(ProcessEnd::ExitStop(sig)) => {
|
|
(format!("Command stopped by {:?}", sig), true)
|
|
}
|
|
Some(ProcessEnd::Continued) => ("Command continued".to_string(), true),
|
|
Some(ProcessEnd::Exception(ex)) => {
|
|
(format!("Command ended by exception {:#x}", ex), true)
|
|
}
|
|
Some(ProcessEnd::Success) => ("Command was successful".to_string(), false),
|
|
None => ("Command completed".to_string(), false),
|
|
};
|
|
|
|
if printit {
|
|
eprintln!("[[{}]]", msg);
|
|
}
|
|
|
|
if notif {
|
|
Notification::new()
|
|
.summary("Watchexec: command ended")
|
|
.body(&msg)
|
|
.show()
|
|
.map(drop)
|
|
.unwrap_or_else(|err| {
|
|
eprintln!("[[Failed to send desktop notification: {}]]", err);
|
|
});
|
|
}
|
|
|
|
action.outcome(Outcome::DoNothing);
|
|
return fut;
|
|
}
|
|
}
|
|
|
|
let when_running = match (clear, on_busy.as_str()) {
|
|
(_, "do-nothing") => Outcome::DoNothing,
|
|
(true, "restart") => {
|
|
Outcome::both(Outcome::Stop, Outcome::both(Outcome::Clear, Outcome::Start))
|
|
}
|
|
(false, "restart") => Outcome::both(Outcome::Stop, Outcome::Start),
|
|
(_, "signal") => Outcome::Signal(signal),
|
|
(true, "queue") => Outcome::wait(Outcome::both(Outcome::Clear, Outcome::Start)),
|
|
(false, "queue") => Outcome::wait(Outcome::Start),
|
|
_ => Outcome::DoNothing,
|
|
};
|
|
|
|
let when_idle = if clear {
|
|
Outcome::both(Outcome::Clear, Outcome::Start)
|
|
} else {
|
|
Outcome::Start
|
|
};
|
|
|
|
action.outcome(Outcome::if_running(when_running, when_idle));
|
|
|
|
fut
|
|
});
|
|
|
|
let mut add_envs = HashMap::new();
|
|
for pair in args.values_of_lossy("command-env").unwrap_or_default() {
|
|
if let Some((k, v)) = pair.split_once('=') {
|
|
add_envs.insert(k.to_owned(), OsString::from(v));
|
|
} else {
|
|
return Err(miette!("{pair} is not in key=value format"));
|
|
}
|
|
}
|
|
debug!(
|
|
?add_envs,
|
|
"additional environment variables to add to command"
|
|
);
|
|
|
|
let workdir = args
|
|
.value_of_os("command-workdir")
|
|
.map(|wkd| Path::new(wkd).to_owned());
|
|
|
|
let no_env = args.is_present("no-environment");
|
|
config.on_pre_spawn(move |prespawn: PreSpawn| {
|
|
let add_envs = add_envs.clone();
|
|
let workdir = workdir.clone();
|
|
async move {
|
|
if !no_env || !add_envs.is_empty() || workdir.is_some() {
|
|
if let Some(mut command) = prespawn.command().await {
|
|
let mut envs = add_envs.clone();
|
|
|
|
if !no_env {
|
|
envs.extend(
|
|
summarise_events_to_env(prespawn.events.iter())
|
|
.into_iter()
|
|
.map(|(k, v)| (format!("WATCHEXEC_{}_PATH", k), v)),
|
|
);
|
|
}
|
|
|
|
for (k, v) in envs {
|
|
debug!(?k, ?v, "inserting environment variable");
|
|
command.env(k, v);
|
|
}
|
|
|
|
if let Some(ref workdir) = workdir {
|
|
debug!(?workdir, "set command workdir");
|
|
command.current_dir(workdir);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok::<(), Infallible>(())
|
|
}
|
|
});
|
|
|
|
config.on_post_spawn(SyncFnHandler::from(move |postspawn: PostSpawn| {
|
|
if notif {
|
|
Notification::new()
|
|
.summary("Watchexec: change detected")
|
|
.body(&format!("Running `{}`", postspawn.command.join(" ")))
|
|
.show()
|
|
.map(drop)
|
|
.unwrap_or_else(|err| {
|
|
eprintln!("[[Failed to send desktop notification: {}]]", err);
|
|
});
|
|
}
|
|
|
|
Ok::<(), Infallible>(())
|
|
}));
|
|
|
|
Ok(config)
|
|
}
|
|
|
|
// until 2.0, then Powershell
|
|
#[cfg(windows)]
|
|
fn default_shell() -> Shell {
|
|
Shell::Cmd
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
fn default_shell() -> Shell {
|
|
Shell::Unix("sh".to_string())
|
|
}
|
|
|
|
// because Shell::Cmd is only on windows
|
|
#[cfg(windows)]
|
|
fn cmd_shell(_: String) -> Shell {
|
|
Shell::Cmd
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
fn cmd_shell(s: String) -> Shell {
|
|
Shell::Unix(s)
|
|
}
|