2023-11-25 21:33:44 +01:00
|
|
|
use std::{
|
|
|
|
borrow::Cow,
|
|
|
|
collections::HashMap,
|
|
|
|
env::current_dir,
|
|
|
|
ffi::{OsStr, OsString},
|
|
|
|
fs::File,
|
2023-12-09 11:52:40 +01:00
|
|
|
io::{IsTerminal, Write},
|
2023-11-25 21:33:44 +01:00
|
|
|
path::Path,
|
|
|
|
process::Stdio,
|
2023-12-11 02:21:57 +01:00
|
|
|
sync::{
|
2023-12-19 12:22:59 +01:00
|
|
|
atomic::{AtomicBool, AtomicU8, Ordering},
|
2023-12-11 02:21:57 +01:00
|
|
|
Arc,
|
|
|
|
},
|
|
|
|
time::Duration,
|
2023-11-25 21:33:44 +01:00
|
|
|
};
|
2021-08-24 12:45:31 +02:00
|
|
|
|
2023-11-25 21:33:44 +01:00
|
|
|
use clearscreen::ClearScreen;
|
|
|
|
use miette::{miette, IntoDiagnostic, Report, Result};
|
|
|
|
use notify_rust::Notification;
|
2023-11-27 13:12:51 +01:00
|
|
|
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
|
2023-11-25 21:33:44 +01:00
|
|
|
use tokio::{process::Command as TokioCommand, time::sleep};
|
2023-12-09 10:10:11 +01:00
|
|
|
use tracing::{debug, debug_span, error, instrument, trace, trace_span, Instrument};
|
2023-11-25 21:33:44 +01:00
|
|
|
use watchexec::{
|
2023-12-11 02:21:57 +01:00
|
|
|
action::ActionHandler,
|
2023-11-25 21:33:44 +01:00
|
|
|
command::{Command, Program, Shell, SpawnOptions},
|
|
|
|
error::RuntimeError,
|
|
|
|
job::{CommandState, Job},
|
|
|
|
sources::fs::Watcher,
|
|
|
|
Config, ErrorHook, Id,
|
|
|
|
};
|
|
|
|
use watchexec_events::{Event, Keyboard, ProcessEnd, Tag};
|
|
|
|
use watchexec_signals::Signal;
|
|
|
|
|
|
|
|
use crate::{
|
2023-12-09 10:30:58 +01:00
|
|
|
args::{Args, ClearMode, ColourMode, EmitEvents, OnBusyUpdate, SignalMapping},
|
2023-11-25 21:33:44 +01:00
|
|
|
state::RotatingTempFile,
|
|
|
|
};
|
2023-11-27 13:12:51 +01:00
|
|
|
use crate::{emits::events_to_simple_format, state::State};
|
|
|
|
|
2023-12-09 10:10:11 +01:00
|
|
|
#[derive(Clone, Copy, Debug)]
|
2023-11-27 13:12:51 +01:00
|
|
|
struct OutputFlags {
|
|
|
|
quiet: bool,
|
|
|
|
colour: ColorChoice,
|
|
|
|
timings: bool,
|
|
|
|
bell: bool,
|
|
|
|
toast: bool,
|
|
|
|
}
|
2023-11-25 21:33:44 +01:00
|
|
|
|
|
|
|
pub fn make_config(args: &Args, state: &State) -> Result<Config> {
|
|
|
|
let _span = debug_span!("args-runtime").entered();
|
|
|
|
let config = Config::default();
|
|
|
|
config.on_error(|err: ErrorHook| {
|
|
|
|
if let RuntimeError::IoError {
|
|
|
|
about: "waiting on process group",
|
|
|
|
..
|
|
|
|
} = err.error
|
|
|
|
{
|
|
|
|
// "No child processes" and such
|
|
|
|
// these are often spurious, so condemn them to -v only
|
|
|
|
error!("{}", err.error);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfg!(debug_assertions) {
|
|
|
|
eprintln!("[[{:?}]]", err.error);
|
|
|
|
}
|
|
|
|
|
|
|
|
eprintln!("[[Error (not fatal)]]\n{}", Report::new(err.error));
|
|
|
|
});
|
|
|
|
|
|
|
|
config.pathset(if args.paths.is_empty() {
|
|
|
|
vec![current_dir().into_diagnostic()?]
|
|
|
|
} else if args.paths.len() == 1
|
|
|
|
&& args
|
|
|
|
.paths
|
|
|
|
.first()
|
|
|
|
.map_or(false, |p| p == Path::new("/dev/null"))
|
|
|
|
{
|
|
|
|
// special case: /dev/null means "don't start the fs event source"
|
|
|
|
Vec::new()
|
|
|
|
} else {
|
|
|
|
args.paths.clone()
|
|
|
|
});
|
|
|
|
|
|
|
|
config.throttle(args.debounce.0);
|
|
|
|
config.keyboard_events(args.stdin_quit);
|
|
|
|
|
|
|
|
if let Some(interval) = args.poll {
|
|
|
|
config.file_watcher(Watcher::Poll(interval.0));
|
|
|
|
}
|
|
|
|
|
2023-11-27 11:29:55 +01:00
|
|
|
let once = args.once;
|
2023-11-25 21:33:44 +01:00
|
|
|
let clear = args.screen_clear;
|
2023-11-27 11:29:55 +01:00
|
|
|
|
|
|
|
let emit_events_to = args.emit_events_to;
|
|
|
|
let emit_file = state.emit_file.clone();
|
|
|
|
|
|
|
|
if args.only_emit_events {
|
|
|
|
config.on_action(move |mut action| {
|
|
|
|
// if we got a terminate or interrupt signal, quit
|
2023-11-27 13:12:51 +01:00
|
|
|
if action
|
|
|
|
.signals()
|
|
|
|
.any(|sig| sig == Signal::Terminate || sig == Signal::Interrupt)
|
|
|
|
{
|
2023-12-11 02:21:57 +01:00
|
|
|
// no need to be graceful as there's no commands
|
2023-11-27 11:29:55 +01:00
|
|
|
action.quit();
|
|
|
|
return action;
|
|
|
|
}
|
|
|
|
|
|
|
|
// clear the screen before printing events
|
|
|
|
if let Some(mode) = clear {
|
|
|
|
match mode {
|
|
|
|
ClearMode::Clear => {
|
|
|
|
clearscreen::clear().ok();
|
|
|
|
}
|
|
|
|
ClearMode::Reset => {
|
|
|
|
for cs in [
|
|
|
|
ClearScreen::WindowsCooked,
|
|
|
|
ClearScreen::WindowsVt,
|
|
|
|
ClearScreen::VtLeaveAlt,
|
|
|
|
ClearScreen::VtWellDone,
|
|
|
|
ClearScreen::default(),
|
|
|
|
] {
|
|
|
|
cs.clear().ok();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
match emit_events_to {
|
2023-11-28 12:30:33 +01:00
|
|
|
EmitEvents::Stdio => {
|
2023-11-27 13:12:51 +01:00
|
|
|
println!(
|
|
|
|
"{}",
|
|
|
|
events_to_simple_format(action.events.as_ref()).unwrap_or_default()
|
|
|
|
);
|
2023-11-27 11:29:55 +01:00
|
|
|
}
|
2023-11-28 12:30:33 +01:00
|
|
|
EmitEvents::JsonStdio => {
|
2023-11-27 11:29:55 +01:00
|
|
|
for event in action.events.iter().filter(|e| !e.is_empty()) {
|
|
|
|
println!("{}", serde_json::to_string(event).unwrap_or_default());
|
|
|
|
}
|
|
|
|
}
|
2023-11-27 13:12:51 +01:00
|
|
|
other => unreachable!(
|
|
|
|
"emit_events_to should have been validated earlier: {:?}",
|
|
|
|
other
|
|
|
|
),
|
2023-11-27 11:29:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
action
|
|
|
|
});
|
|
|
|
|
|
|
|
return Ok(config);
|
|
|
|
}
|
|
|
|
|
2023-11-25 21:33:44 +01:00
|
|
|
let delay_run = args.delay_run.map(|ts| ts.0);
|
|
|
|
let on_busy = args.on_busy_update;
|
2023-12-09 11:52:40 +01:00
|
|
|
let stdin_quit = args.stdin_quit;
|
2023-11-25 21:33:44 +01:00
|
|
|
|
|
|
|
let signal = args.signal;
|
|
|
|
let stop_signal = args.stop_signal;
|
|
|
|
let stop_timeout = args.stop_timeout.0;
|
|
|
|
|
|
|
|
let print_events = args.print_events;
|
2023-11-27 13:12:51 +01:00
|
|
|
let outflags = OutputFlags {
|
|
|
|
quiet: args.quiet,
|
|
|
|
colour: match args.color {
|
|
|
|
ColourMode::Auto if !std::io::stdin().is_terminal() => ColorChoice::Never,
|
|
|
|
ColourMode::Auto => ColorChoice::Auto,
|
|
|
|
ColourMode::Always => ColorChoice::Always,
|
|
|
|
ColourMode::Never => ColorChoice::Never,
|
|
|
|
},
|
|
|
|
timings: args.timings,
|
|
|
|
bell: args.bell,
|
|
|
|
toast: args.notify,
|
|
|
|
};
|
2023-11-25 21:33:44 +01:00
|
|
|
|
|
|
|
let workdir = Arc::new(args.workdir.clone());
|
|
|
|
|
|
|
|
let mut add_envs = HashMap::new();
|
|
|
|
for pair in &args.env {
|
|
|
|
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 id = Id::default();
|
|
|
|
let command = interpret_command_args(args)?;
|
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
let signal_map: Arc<HashMap<Signal, Option<Signal>>> = Arc::new(
|
|
|
|
args.signal_map
|
|
|
|
.iter()
|
|
|
|
.copied()
|
|
|
|
.map(|SignalMapping { from, to }| (from, to))
|
|
|
|
.collect(),
|
|
|
|
);
|
|
|
|
|
2023-12-19 12:22:59 +01:00
|
|
|
let queued = Arc::new(AtomicBool::new(false));
|
2023-12-11 02:21:57 +01:00
|
|
|
let quit_again = Arc::new(AtomicU8::new(0));
|
|
|
|
|
2023-11-25 21:33:44 +01:00
|
|
|
config.on_action_async(move |mut action| {
|
|
|
|
let add_envs = add_envs.clone();
|
|
|
|
let command = command.clone();
|
|
|
|
let emit_file = emit_file.clone();
|
2023-12-19 12:22:59 +01:00
|
|
|
let queued = queued.clone();
|
2023-12-11 02:21:57 +01:00
|
|
|
let quit_again = quit_again.clone();
|
2023-12-09 10:30:58 +01:00
|
|
|
let signal_map = signal_map.clone();
|
2023-11-25 21:33:44 +01:00
|
|
|
let workdir = workdir.clone();
|
2023-12-09 10:10:11 +01:00
|
|
|
Box::new(
|
|
|
|
async move {
|
|
|
|
trace!(events=?action.events, "handling action");
|
|
|
|
|
2023-11-25 21:33:44 +01:00
|
|
|
let add_envs = add_envs.clone();
|
2023-12-09 10:30:58 +01:00
|
|
|
let command = command.clone();
|
2023-11-25 21:33:44 +01:00
|
|
|
let emit_file = emit_file.clone();
|
2023-12-19 12:22:59 +01:00
|
|
|
let queued = queued.clone();
|
2023-12-11 02:21:57 +01:00
|
|
|
let quit_again = quit_again.clone();
|
2023-12-09 10:30:58 +01:00
|
|
|
let signal_map = signal_map.clone();
|
|
|
|
let workdir = workdir.clone();
|
2023-11-25 21:33:44 +01:00
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
trace!("set spawn hook for workdir and environment variables");
|
|
|
|
let job = action.get_or_create_job(id, move || command.clone());
|
|
|
|
let events = action.events.clone();
|
|
|
|
job.set_spawn_hook(move |command, _| {
|
|
|
|
let add_envs = add_envs.clone();
|
|
|
|
let emit_file = emit_file.clone();
|
|
|
|
let events = events.clone();
|
|
|
|
|
|
|
|
if let Some(ref workdir) = workdir.as_ref() {
|
|
|
|
debug!(?workdir, "set command workdir");
|
|
|
|
command.current_dir(workdir);
|
|
|
|
}
|
2023-11-25 21:33:44 +01:00
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
emit_events_to_command(command, events, emit_file, emit_events_to, add_envs);
|
|
|
|
});
|
2023-11-25 21:33:44 +01:00
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
let show_events = || {
|
|
|
|
if print_events {
|
2023-12-09 10:10:11 +01:00
|
|
|
trace!("print events to stderr");
|
2023-12-09 10:30:58 +01:00
|
|
|
for (n, event) in action.events.iter().enumerate() {
|
|
|
|
eprintln!("[EVENT {n}] {event}");
|
|
|
|
}
|
2023-11-25 21:33:44 +01:00
|
|
|
}
|
2023-12-09 10:30:58 +01:00
|
|
|
};
|
2023-11-25 21:33:44 +01:00
|
|
|
|
2023-12-11 02:21:57 +01:00
|
|
|
let quit = |mut action: ActionHandler| {
|
|
|
|
match quit_again.fetch_add(1, Ordering::Relaxed) {
|
|
|
|
0 => {
|
|
|
|
eprintln!("[Waiting {stop_timeout:?} for processes to exit before stopping...]");
|
|
|
|
// eprintln!("[Waiting {stop_timeout:?} for processes to exit before stopping... Ctrl-C again to exit faster]");
|
|
|
|
// see TODO in action/worker.rs
|
|
|
|
action.quit_gracefully(
|
|
|
|
stop_signal.unwrap_or(Signal::Terminate),
|
|
|
|
stop_timeout,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
1 => {
|
|
|
|
action.quit_gracefully(Signal::ForceStop, Duration::ZERO);
|
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
action.quit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
action
|
|
|
|
};
|
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
if once {
|
2023-12-09 10:10:11 +01:00
|
|
|
debug!("debug mode: run once and quit");
|
2023-12-09 10:30:58 +01:00
|
|
|
show_events();
|
|
|
|
|
|
|
|
if let Some(delay) = delay_run {
|
|
|
|
job.run_async(move |_| {
|
|
|
|
Box::new(async move {
|
|
|
|
sleep(delay).await;
|
|
|
|
})
|
|
|
|
});
|
|
|
|
}
|
2023-11-25 21:33:44 +01:00
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
// this blocks the event loop, but also this is a debug feature so i don't care
|
|
|
|
job.start().await;
|
|
|
|
job.to_wait().await;
|
2023-12-11 02:21:57 +01:00
|
|
|
return quit(action);
|
2023-11-25 21:33:44 +01:00
|
|
|
}
|
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
let is_keyboard_eof = action
|
|
|
|
.events
|
|
|
|
.iter()
|
|
|
|
.any(|e| e.tags.contains(&Tag::Keyboard(Keyboard::Eof)));
|
2023-12-09 11:52:40 +01:00
|
|
|
if stdin_quit && is_keyboard_eof {
|
2023-12-09 10:30:58 +01:00
|
|
|
debug!("keyboard EOF, quit");
|
|
|
|
show_events();
|
2023-12-11 02:21:57 +01:00
|
|
|
return quit(action);
|
2023-12-09 10:30:58 +01:00
|
|
|
}
|
2023-11-25 21:33:44 +01:00
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
let signals: Vec<Signal> = action.signals().collect();
|
2023-12-09 10:10:11 +01:00
|
|
|
trace!(?signals, "received some signals");
|
2023-11-25 21:33:44 +01:00
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
// if we got a terminate or interrupt signal and they're not mapped, quit
|
|
|
|
if (signals.contains(&Signal::Terminate)
|
|
|
|
&& !signal_map.contains_key(&Signal::Terminate))
|
|
|
|
|| (signals.contains(&Signal::Interrupt)
|
|
|
|
&& !signal_map.contains_key(&Signal::Interrupt))
|
|
|
|
{
|
|
|
|
debug!("unmapped terminate or interrupt signal, quit");
|
|
|
|
show_events();
|
2023-12-11 02:21:57 +01:00
|
|
|
return quit(action);
|
2023-12-09 10:30:58 +01:00
|
|
|
}
|
2023-11-25 21:33:44 +01:00
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
// pass all other signals on
|
|
|
|
for signal in signals {
|
|
|
|
match signal_map.get(&signal) {
|
|
|
|
Some(Some(mapped)) => {
|
|
|
|
debug!(?signal, ?mapped, "passing mapped signal");
|
|
|
|
job.signal(*mapped);
|
|
|
|
}
|
|
|
|
Some(None) => {
|
|
|
|
debug!(?signal, "discarding signal");
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
debug!(?signal, "passing signal on");
|
|
|
|
job.signal(signal);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-11-25 21:33:44 +01:00
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
// only filesystem events below here (or empty synthetic events)
|
|
|
|
if action.paths().next().is_none() && !action.events.iter().any(|e| e.is_empty()) {
|
|
|
|
debug!("no filesystem or synthetic events, skip without doing more");
|
|
|
|
show_events();
|
|
|
|
return action;
|
|
|
|
}
|
|
|
|
|
|
|
|
// clear the screen before printing events
|
|
|
|
if let Some(mode) = clear {
|
|
|
|
match mode {
|
|
|
|
ClearMode::Clear => {
|
|
|
|
clearscreen::clear().ok();
|
2023-12-09 10:10:11 +01:00
|
|
|
debug!("cleared screen");
|
2023-11-25 21:33:44 +01:00
|
|
|
}
|
2023-12-09 10:30:58 +01:00
|
|
|
ClearMode::Reset => {
|
|
|
|
for cs in [
|
|
|
|
ClearScreen::WindowsCooked,
|
|
|
|
ClearScreen::WindowsVt,
|
|
|
|
ClearScreen::VtLeaveAlt,
|
|
|
|
ClearScreen::VtWellDone,
|
|
|
|
ClearScreen::default(),
|
|
|
|
] {
|
|
|
|
cs.clear().ok();
|
|
|
|
}
|
2023-12-09 10:10:11 +01:00
|
|
|
debug!("hard-reset screen");
|
2023-12-09 10:30:58 +01:00
|
|
|
}
|
2023-11-25 21:33:44 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
show_events();
|
2023-11-25 21:33:44 +01:00
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
if let Some(delay) = delay_run {
|
2023-12-09 10:10:11 +01:00
|
|
|
trace!("delaying run by sleeping inside the job");
|
2023-12-09 10:30:58 +01:00
|
|
|
job.run_async(move |_| {
|
|
|
|
Box::new(async move {
|
|
|
|
sleep(delay).await;
|
|
|
|
})
|
|
|
|
});
|
|
|
|
}
|
2023-11-25 21:33:44 +01:00
|
|
|
|
2023-12-09 10:10:11 +01:00
|
|
|
trace!("querying job state via run_async");
|
2023-12-09 10:30:58 +01:00
|
|
|
job.run_async({
|
2023-11-25 21:33:44 +01:00
|
|
|
let job = job.clone();
|
2023-12-09 10:30:58 +01:00
|
|
|
move |context| {
|
|
|
|
let job = job.clone();
|
|
|
|
let is_running = matches!(context.current, CommandState::Running { .. });
|
|
|
|
Box::new(async move {
|
|
|
|
let innerjob = job.clone();
|
|
|
|
if is_running {
|
2023-12-09 10:10:11 +01:00
|
|
|
trace!(?on_busy, "job is running, decide what to do");
|
2023-12-09 10:30:58 +01:00
|
|
|
match on_busy {
|
|
|
|
OnBusyUpdate::DoNothing => {}
|
|
|
|
OnBusyUpdate::Signal => {
|
|
|
|
job.signal(if cfg!(windows) {
|
|
|
|
Signal::ForceStop
|
|
|
|
} else {
|
|
|
|
stop_signal.or(signal).unwrap_or(Signal::Terminate)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
OnBusyUpdate::Restart if cfg!(windows) => {
|
|
|
|
job.restart();
|
|
|
|
job.run(move |context| {
|
|
|
|
setup_process(
|
|
|
|
innerjob.clone(),
|
|
|
|
context.command.clone(),
|
|
|
|
outflags,
|
|
|
|
)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
OnBusyUpdate::Restart => {
|
|
|
|
job.restart_with_signal(
|
|
|
|
stop_signal.unwrap_or(Signal::Terminate),
|
|
|
|
stop_timeout,
|
|
|
|
);
|
2023-11-25 21:33:44 +01:00
|
|
|
job.run(move |context| {
|
|
|
|
setup_process(
|
|
|
|
innerjob.clone(),
|
|
|
|
context.command.clone(),
|
2023-11-27 13:12:51 +01:00
|
|
|
outflags,
|
2023-11-25 21:33:44 +01:00
|
|
|
)
|
|
|
|
});
|
2023-12-09 10:30:58 +01:00
|
|
|
}
|
|
|
|
OnBusyUpdate::Queue => {
|
|
|
|
let job = job.clone();
|
2023-12-19 12:22:59 +01:00
|
|
|
let already_queued =
|
|
|
|
queued.fetch_or(true, Ordering::SeqCst);
|
|
|
|
if already_queued {
|
|
|
|
debug!("next start is already queued, do nothing");
|
|
|
|
} else {
|
|
|
|
debug!("queueing next start of job");
|
|
|
|
tokio::spawn({
|
|
|
|
let queued = queued.clone();
|
|
|
|
async move {
|
|
|
|
trace!("waiting for job to finish");
|
|
|
|
job.to_wait().await;
|
|
|
|
trace!("job finished, starting queued");
|
|
|
|
job.start();
|
|
|
|
job.run(move |context| {
|
|
|
|
setup_process(
|
|
|
|
innerjob.clone(),
|
|
|
|
context.command.clone(),
|
|
|
|
outflags,
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.await;
|
|
|
|
trace!("resetting queued state");
|
|
|
|
queued.store(false, Ordering::SeqCst);
|
|
|
|
}
|
2023-12-09 10:30:58 +01:00
|
|
|
});
|
2023-12-19 12:22:59 +01:00
|
|
|
}
|
2023-12-09 10:30:58 +01:00
|
|
|
}
|
2023-11-25 21:33:44 +01:00
|
|
|
}
|
2023-12-09 10:30:58 +01:00
|
|
|
} else {
|
2023-12-09 10:10:11 +01:00
|
|
|
trace!("job is not running, start it");
|
2023-12-09 10:30:58 +01:00
|
|
|
job.start();
|
|
|
|
job.run(move |context| {
|
2023-12-09 10:10:11 +01:00
|
|
|
setup_process(
|
|
|
|
innerjob.clone(),
|
|
|
|
context.command.clone(),
|
|
|
|
outflags,
|
|
|
|
)
|
2023-12-09 10:30:58 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
});
|
2023-11-25 21:33:44 +01:00
|
|
|
|
2023-12-09 10:30:58 +01:00
|
|
|
action
|
2023-12-09 10:10:11 +01:00
|
|
|
}
|
|
|
|
.instrument(trace_span!("action handler")),
|
|
|
|
)
|
2023-11-25 21:33:44 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
Ok(config)
|
|
|
|
}
|
|
|
|
|
2023-12-09 10:10:11 +01:00
|
|
|
#[instrument(level = "debug")]
|
2023-11-25 21:33:44 +01:00
|
|
|
fn interpret_command_args(args: &Args) -> Result<Arc<Command>> {
|
|
|
|
let mut cmd = args.command.clone();
|
|
|
|
if cmd.is_empty() {
|
|
|
|
panic!("(clap) Bug: command is not present");
|
|
|
|
}
|
|
|
|
|
|
|
|
let shell = match if args.no_shell || args.no_shell_long {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
args.shell.as_deref().or(Some("default"))
|
|
|
|
} {
|
|
|
|
Some("") => return Err(RuntimeError::CommandShellEmptyShell).into_diagnostic(),
|
|
|
|
|
|
|
|
Some("none") | None => None,
|
|
|
|
|
|
|
|
#[cfg(windows)]
|
|
|
|
Some("default") | Some("cmd") | Some("cmd.exe") | Some("CMD") | Some("CMD.EXE") => {
|
|
|
|
Some(Shell::cmd())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(not(windows))]
|
|
|
|
Some("default") => Some(Shell::new("sh")),
|
|
|
|
|
|
|
|
Some(other) => {
|
|
|
|
let sh = other.split_ascii_whitespace().collect::<Vec<_>>();
|
|
|
|
|
|
|
|
// UNWRAP: checked by Some("")
|
|
|
|
#[allow(clippy::unwrap_used)]
|
|
|
|
let (shprog, shopts) = sh.split_first().unwrap();
|
|
|
|
|
|
|
|
Some(Shell {
|
|
|
|
prog: shprog.into(),
|
|
|
|
options: shopts.iter().map(|s| (*s).to_string()).collect(),
|
|
|
|
program_option: Some(Cow::Borrowed(OsStr::new("-c"))),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let program = if let Some(shell) = shell {
|
|
|
|
Program::Shell {
|
|
|
|
shell,
|
|
|
|
command: cmd.join(" "),
|
|
|
|
args: Vec::new(),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Program::Exec {
|
|
|
|
prog: cmd.remove(0).into(),
|
|
|
|
args: cmd,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(Arc::new(Command {
|
|
|
|
program,
|
|
|
|
options: SpawnOptions {
|
|
|
|
grouped: !args.no_process_group,
|
|
|
|
..Default::default()
|
|
|
|
},
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
2023-12-09 10:10:11 +01:00
|
|
|
#[instrument(level = "trace")]
|
2023-11-27 13:12:51 +01:00
|
|
|
fn setup_process(job: Job, command: Arc<Command>, outflags: OutputFlags) {
|
|
|
|
if outflags.toast {
|
2023-11-25 21:33:44 +01:00
|
|
|
Notification::new()
|
|
|
|
.summary("Watchexec: change detected")
|
|
|
|
.body(&format!("Running {command}"))
|
|
|
|
.show()
|
|
|
|
.map_or_else(
|
|
|
|
|err| {
|
|
|
|
eprintln!("[[Failed to send desktop notification: {err}]]");
|
|
|
|
},
|
|
|
|
drop,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-11-27 13:12:51 +01:00
|
|
|
if !outflags.quiet {
|
|
|
|
let mut stderr = StandardStream::stderr(outflags.colour);
|
|
|
|
stderr.reset().ok();
|
|
|
|
stderr
|
|
|
|
.set_color(ColorSpec::new().set_fg(Some(Color::Green)))
|
|
|
|
.ok();
|
|
|
|
writeln!(&mut stderr, "[Running: {command}]").ok();
|
|
|
|
stderr.reset().ok();
|
|
|
|
}
|
|
|
|
|
2023-11-25 21:33:44 +01:00
|
|
|
tokio::spawn(async move {
|
|
|
|
job.to_wait().await;
|
2023-11-27 13:12:51 +01:00
|
|
|
job.run(move |context| end_of_process(context.current, outflags));
|
2023-11-25 21:33:44 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-12-09 10:10:11 +01:00
|
|
|
#[instrument(level = "trace")]
|
2023-11-27 13:12:51 +01:00
|
|
|
fn end_of_process(state: &CommandState, outflags: OutputFlags) {
|
2023-11-25 21:33:44 +01:00
|
|
|
let CommandState::Finished {
|
|
|
|
status,
|
|
|
|
started,
|
|
|
|
finished,
|
|
|
|
} = state
|
|
|
|
else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
let duration = *finished - *started;
|
2023-11-27 13:12:51 +01:00
|
|
|
let timing = if outflags.timings {
|
|
|
|
format!(", lasted {duration:?}")
|
|
|
|
} else {
|
|
|
|
String::new()
|
|
|
|
};
|
|
|
|
let (msg, fg) = match status {
|
|
|
|
ProcessEnd::ExitError(code) => (format!("Command exited with {code}{timing}"), Color::Red),
|
2023-11-25 21:33:44 +01:00
|
|
|
ProcessEnd::ExitSignal(sig) => {
|
2023-11-27 13:12:51 +01:00
|
|
|
(format!("Command killed by {sig:?}{timing}"), Color::Magenta)
|
2023-11-25 21:33:44 +01:00
|
|
|
}
|
2023-11-27 13:12:51 +01:00
|
|
|
ProcessEnd::ExitStop(sig) => (format!("Command stopped by {sig:?}{timing}"), Color::Blue),
|
|
|
|
ProcessEnd::Continued => (format!("Command continued{timing}"), Color::Cyan),
|
|
|
|
ProcessEnd::Exception(ex) => (
|
|
|
|
format!("Command ended by exception {ex:#x}{timing}"),
|
|
|
|
Color::Yellow,
|
|
|
|
),
|
|
|
|
ProcessEnd::Success => (format!("Command was successful{timing}"), Color::Green),
|
2023-11-25 21:33:44 +01:00
|
|
|
};
|
|
|
|
|
2023-11-27 13:12:51 +01:00
|
|
|
if outflags.toast {
|
2023-11-25 21:33:44 +01:00
|
|
|
Notification::new()
|
|
|
|
.summary("Watchexec: command ended")
|
|
|
|
.body(&msg)
|
|
|
|
.show()
|
|
|
|
.map_or_else(
|
|
|
|
|err| {
|
|
|
|
eprintln!("[[Failed to send desktop notification: {err}]]");
|
|
|
|
},
|
|
|
|
drop,
|
|
|
|
);
|
|
|
|
}
|
2023-11-27 13:12:51 +01:00
|
|
|
|
|
|
|
if !outflags.quiet {
|
|
|
|
let mut stderr = StandardStream::stderr(outflags.colour);
|
|
|
|
stderr.reset().ok();
|
|
|
|
stderr.set_color(ColorSpec::new().set_fg(Some(fg))).ok();
|
|
|
|
writeln!(&mut stderr, "[{msg}]").ok();
|
|
|
|
stderr.reset().ok();
|
|
|
|
}
|
|
|
|
|
|
|
|
if outflags.bell {
|
2023-12-09 11:52:40 +01:00
|
|
|
let mut stdout = std::io::stdout();
|
|
|
|
stdout.write_all(b"\x07").ok();
|
|
|
|
stdout.flush().ok();
|
2023-11-27 13:12:51 +01:00
|
|
|
}
|
2023-11-25 21:33:44 +01:00
|
|
|
}
|
|
|
|
|
2023-12-09 10:10:11 +01:00
|
|
|
#[instrument(level = "trace")]
|
2023-11-25 21:33:44 +01:00
|
|
|
fn emit_events_to_command(
|
|
|
|
command: &mut TokioCommand,
|
|
|
|
events: Arc<[Event]>,
|
|
|
|
emit_file: RotatingTempFile,
|
|
|
|
emit_events_to: EmitEvents,
|
|
|
|
mut add_envs: HashMap<String, OsString>,
|
|
|
|
) {
|
|
|
|
use crate::emits::*;
|
|
|
|
|
|
|
|
let mut stdin = None;
|
|
|
|
|
|
|
|
match emit_events_to {
|
|
|
|
EmitEvents::Environment => {
|
|
|
|
add_envs.extend(emits_to_environment(&events));
|
|
|
|
}
|
2023-11-28 12:30:33 +01:00
|
|
|
EmitEvents::Stdio => match emits_to_file(&emit_file, &events)
|
2023-11-25 21:33:44 +01:00
|
|
|
.and_then(|path| File::open(path).into_diagnostic())
|
|
|
|
{
|
|
|
|
Ok(file) => {
|
|
|
|
stdin.replace(Stdio::from(file));
|
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
error!("Failed to write events to stdin, continuing without it: {err}");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
EmitEvents::File => match emits_to_file(&emit_file, &events) {
|
|
|
|
Ok(path) => {
|
|
|
|
add_envs.insert("WATCHEXEC_EVENTS_FILE".into(), path.into());
|
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
error!("Failed to write WATCHEXEC_EVENTS_FILE, continuing without it: {err}");
|
|
|
|
}
|
|
|
|
},
|
2023-11-28 12:30:33 +01:00
|
|
|
EmitEvents::JsonStdio => match emits_to_json_file(&emit_file, &events)
|
2023-11-25 21:33:44 +01:00
|
|
|
.and_then(|path| File::open(path).into_diagnostic())
|
|
|
|
{
|
|
|
|
Ok(file) => {
|
|
|
|
stdin.replace(Stdio::from(file));
|
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
error!("Failed to write events to stdin, continuing without it: {err}");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
EmitEvents::JsonFile => match emits_to_json_file(&emit_file, &events) {
|
|
|
|
Ok(path) => {
|
|
|
|
add_envs.insert("WATCHEXEC_EVENTS_FILE".into(), path.into());
|
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
error!("Failed to write WATCHEXEC_EVENTS_FILE, continuing without it: {err}");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
EmitEvents::None => {}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (k, v) in add_envs {
|
|
|
|
debug!(?k, ?v, "inserting environment variable");
|
|
|
|
command.env(k, v);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(stdin) = stdin {
|
|
|
|
debug!("set command stdin");
|
|
|
|
command.stdin(stdin);
|
|
|
|
}
|
|
|
|
}
|