mirror of https://github.com/passcod/cargo-watch
399 lines
9.7 KiB
Rust
399 lines
9.7 KiB
Rust
use std::{
|
|
convert::Infallible,
|
|
env,
|
|
path::PathBuf,
|
|
sync::{
|
|
atomic::{AtomicU8, Ordering},
|
|
Arc,
|
|
},
|
|
time::Duration,
|
|
};
|
|
|
|
use miette::{miette, IntoDiagnostic, Report, Result};
|
|
use tracing::{debug, info};
|
|
use watchexec::{
|
|
action::{Action, Outcome, PostSpawn, PreSpawn},
|
|
command::{Command, Shell},
|
|
config::RuntimeConfig,
|
|
event::ProcessEnd,
|
|
fs::Watcher,
|
|
handler::SyncFnHandler,
|
|
paths::summarise_events_to_env,
|
|
signal::source::MainSignal,
|
|
};
|
|
|
|
#[cfg(not(target_os = "freebsd"))]
|
|
use notify_rust::Notification;
|
|
|
|
use crate::args::Args;
|
|
|
|
pub fn runtime(args: &Args, command_order: Vec<&'static str>) -> Result<RuntimeConfig> {
|
|
let mut config = RuntimeConfig::default();
|
|
|
|
let mut pathset = args.watch.clone();
|
|
if pathset.is_empty() {
|
|
pathset = vec![PathBuf::from(".")];
|
|
}
|
|
config.pathset(&pathset);
|
|
|
|
let features = if args.features.is_empty() {
|
|
None
|
|
} else {
|
|
Some(args.features.join(","))
|
|
};
|
|
info!(?features, "features");
|
|
|
|
let mut used_shell = if args.use_shell.len() == 1 && command_order.last() == Some(&"use-shell")
|
|
{
|
|
args.use_shell.first().cloned()
|
|
} else {
|
|
None
|
|
};
|
|
debug!(?used_shell, "initial used shell");
|
|
|
|
if !args.cmd_trail.is_empty() {
|
|
info!(trailing=?args.cmd_trail, "use the trailing command");
|
|
let mut args = args.cmd_trail.clone();
|
|
config.command(Command::Exec {
|
|
prog: args.remove(0),
|
|
args,
|
|
});
|
|
} else if args.cmd_cargo.is_empty() && args.cmd_shell.is_empty() {
|
|
info!("use the default command");
|
|
config.command(cargo_command("check", &features)?);
|
|
} else {
|
|
info!("use the optioned commands");
|
|
let mut commands = Vec::with_capacity(args.cmd_cargo.len() + args.cmd_shell.len());
|
|
|
|
let mut cargos = args.cmd_cargo.iter();
|
|
let mut shells = args.cmd_shell.iter();
|
|
let mut use_shells = args.use_shell.iter();
|
|
|
|
for c in command_order {
|
|
match c {
|
|
"cargo" => {
|
|
commands.push(cargo_command(
|
|
cargos
|
|
.next()
|
|
.ok_or_else(|| miette!("Argument-order mismatch, this is a bug"))?,
|
|
&features,
|
|
)?);
|
|
}
|
|
"shell" => {
|
|
commands.push(shell_command(
|
|
shells
|
|
.next()
|
|
.ok_or_else(|| miette!("Argument-order mismatch, this is a bug"))?,
|
|
used_shell.as_ref(),
|
|
)?);
|
|
}
|
|
"use-shell" => {
|
|
used_shell.replace(
|
|
use_shells
|
|
.next()
|
|
.ok_or_else(|| miette!("Argument-order mismatch, this is a bug"))?
|
|
.clone(),
|
|
);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
config.commands(commands);
|
|
}
|
|
|
|
if let Some(delay) = &args.delay {
|
|
let delay = if delay.ends_with("ms") {
|
|
let d: u64 = delay.trim_end_matches("ms").parse().into_diagnostic()?;
|
|
Duration::from_millis(d)
|
|
} else {
|
|
let d: f64 = delay.parse().into_diagnostic()?;
|
|
let delay = (d * 1000.0).round();
|
|
if delay.is_infinite() || delay.is_nan() || delay.is_sign_negative() {
|
|
return Err(Report::msg("delay must be finite and non-negative"));
|
|
}
|
|
if delay >= 1000.0 {
|
|
return Err(Report::msg("delay must be less than 1000 seconds"));
|
|
}
|
|
|
|
// SAFETY: delay is finite, not nan, non-negative, and less than 1000
|
|
Duration::from_millis(unsafe { delay.to_int_unchecked() })
|
|
};
|
|
|
|
config.action_throttle(delay);
|
|
}
|
|
|
|
if args.poll {
|
|
config.file_watcher(Watcher::Poll(config.action.throttle));
|
|
}
|
|
|
|
// config.command_grouped(args.process_group);
|
|
|
|
let quiet = args.quiet;
|
|
let clear = args.clear;
|
|
let notif = args.notif;
|
|
let on_busy = if args.restart {
|
|
"restart"
|
|
} else {
|
|
"do-nothing"
|
|
};
|
|
|
|
// TODO: add using SubSignal in Args directly
|
|
// let mut signal = args
|
|
// .signal
|
|
// .map(SubSignal::from_str)
|
|
// .transpose()
|
|
// .into_diagnostic()?
|
|
// .unwrap_or(SubSignal::Terminate);
|
|
|
|
let print_events = args.why;
|
|
let quit_after_n = args.quit_after_n.map(|n| Arc::new(AtomicU8::new(n)));
|
|
let delay_run = args.delay_run.map(Duration::from_secs);
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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(), !quiet),
|
|
None => ("Command completed".to_string(), false),
|
|
};
|
|
|
|
if printit {
|
|
eprintln!("[[{}]]", msg);
|
|
}
|
|
|
|
#[cfg(not(target_os = "freebsd"))]
|
|
if notif {
|
|
Notification::new()
|
|
.summary("Watchexec: command ended")
|
|
.body(&msg)
|
|
.show()
|
|
.map(drop)
|
|
.unwrap_or_else(|err| {
|
|
eprintln!("[[Failed to send desktop notification: {}]]", err);
|
|
});
|
|
}
|
|
|
|
if let Some(runs) = quit_after_n.clone() {
|
|
if runs.load(Ordering::SeqCst) == 0 {
|
|
eprintln!("[[--quit after n--]]");
|
|
action.outcome(Outcome::Exit);
|
|
return fut;
|
|
}
|
|
}
|
|
|
|
action.outcome(Outcome::DoNothing);
|
|
return fut;
|
|
}
|
|
}
|
|
|
|
if let Some(runs) = quit_after_n.clone() {
|
|
if runs.load(Ordering::SeqCst) == 0 {
|
|
debug!("quitting after n triggers");
|
|
action.outcome(Outcome::Exit);
|
|
return fut;
|
|
}
|
|
}
|
|
|
|
let start = if clear {
|
|
Outcome::both(Outcome::Clear, Outcome::Start)
|
|
} else {
|
|
Outcome::Start
|
|
};
|
|
|
|
let start = if let Some(delay) = &delay_run {
|
|
Outcome::both(Outcome::Sleep(*delay), start)
|
|
} else {
|
|
start
|
|
};
|
|
|
|
let when_idle = start.clone();
|
|
let when_running = match on_busy {
|
|
"do-nothing" => Outcome::DoNothing,
|
|
"restart" => Outcome::both(Outcome::Stop, start),
|
|
// "signal" => Outcome::Signal(signal),
|
|
_ => Outcome::DoNothing,
|
|
};
|
|
|
|
if let Some(runs) = quit_after_n.clone() {
|
|
let remaining = runs.fetch_sub(1, Ordering::SeqCst);
|
|
if remaining > 0 {
|
|
debug!(?remaining, "getting closer to quitting");
|
|
}
|
|
}
|
|
|
|
action.outcome(Outcome::if_running(when_running, when_idle));
|
|
|
|
fut
|
|
});
|
|
|
|
let no_env = args.no_auto_env;
|
|
config.on_pre_spawn(move |prespawn: PreSpawn| async move {
|
|
if !no_env {
|
|
let envs = summarise_events_to_env(prespawn.events.iter());
|
|
if let Some(mut command) = prespawn.command().await {
|
|
for (k, v) in envs {
|
|
command.env(format!("CARGO_WATCH_{}_PATH", k), v);
|
|
}
|
|
}
|
|
}
|
|
|
|
if !quiet {
|
|
eprintln!("[[Running `{}`]]", prespawn.command);
|
|
}
|
|
|
|
Ok::<(), Infallible>(())
|
|
});
|
|
|
|
config.on_post_spawn(SyncFnHandler::from(move |postspawn: PostSpawn| {
|
|
#[cfg(not(target_os = "freebsd"))]
|
|
if notif {
|
|
Notification::new()
|
|
.summary("Cargo Watch: change detected")
|
|
.body(&format!("Running `{}`", postspawn.command))
|
|
.show()?;
|
|
}
|
|
|
|
#[cfg(target_os = "freebsd")]
|
|
return Ok::<(), Infallible>(());
|
|
#[cfg(not(target_os = "freebsd"))]
|
|
return Ok::<(), notify_rust::error::Error>(());
|
|
}));
|
|
|
|
Ok(config)
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn default_shell() -> Shell {
|
|
Shell::Powershell
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
fn default_shell() -> Shell {
|
|
Shell::Unix(env::var("SHELL").unwrap_or_else(|_| String::from("sh")))
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
fn cargo_command(arg: &str, features: &Option<String>) -> Result<Command> {
|
|
debug!(command=?arg, ?features, "building a cargo command");
|
|
|
|
let mut lexed = shlex::split(arg).ok_or_else(|| miette!("Command is not valid: {:?}", arg))?;
|
|
let subc = lexed
|
|
.get(0)
|
|
.ok_or_else(|| miette!("Cargo command needs at least one word"))?
|
|
.clone();
|
|
|
|
if let Some(features) = features.as_ref() {
|
|
if subc.starts_with('b')
|
|
|| subc == "check"
|
|
|| subc == "doc"
|
|
|| subc.starts_with('r')
|
|
|| subc == "test"
|
|
|| subc == "install"
|
|
{
|
|
lexed.insert(1, "--features".into());
|
|
lexed.insert(2, features.into());
|
|
}
|
|
}
|
|
|
|
Ok(Command::Exec {
|
|
prog: "cargo".into(),
|
|
args: lexed,
|
|
})
|
|
}
|
|
|
|
fn shell_command(arg: &str, use_shell: Option<&String>) -> Result<Command> {
|
|
debug!(command=?arg, ?use_shell, "building a shelled command");
|
|
|
|
let (shell, shell_args) = if let Some(sh) = use_shell {
|
|
let mut lexed_shell = shlex::split(&sh)
|
|
.ok_or_else(|| miette!("Shell invocation syntax is invalid: {:?}", sh))?;
|
|
let shell_prog = lexed_shell.remove(0);
|
|
|
|
(
|
|
if shell_prog.eq_ignore_ascii_case("powershell") {
|
|
Shell::Powershell
|
|
} else if shell_prog.eq_ignore_ascii_case("none") {
|
|
// for now, silently discard any shell arguments provided
|
|
let mut lexed =
|
|
shlex::split(arg).ok_or_else(|| miette!("Command is not valid: {:?}", arg))?;
|
|
let prog = lexed.remove(0);
|
|
return Ok(Command::Exec { prog, args: lexed });
|
|
} else if shell_prog.eq_ignore_ascii_case("cmd") {
|
|
cmd_shell(shell_prog.into())
|
|
} else {
|
|
Shell::Unix(shell_prog.into())
|
|
},
|
|
lexed_shell,
|
|
)
|
|
} else {
|
|
(default_shell(), Vec::new())
|
|
};
|
|
|
|
Ok(Command::Shell {
|
|
shell,
|
|
args: shell_args,
|
|
command: arg.into(),
|
|
})
|
|
}
|