Make it possible to pass multiple Commands (library) (#320)
This commit is contained in:
parent
361e5530c5
commit
ba86225ad4
|
@ -9,8 +9,9 @@ use notify_rust::Notification;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use watchexec::{
|
use watchexec::{
|
||||||
action::{Action, Outcome, PostSpawn, PreSpawn},
|
action::{Action, Outcome, PostSpawn, PreSpawn},
|
||||||
command::Shell,
|
command::{Command, Shell},
|
||||||
config::RuntimeConfig,
|
config::RuntimeConfig,
|
||||||
|
error::RuntimeError,
|
||||||
event::ProcessEnd,
|
event::ProcessEnd,
|
||||||
fs::Watcher,
|
fs::Watcher,
|
||||||
handler::SyncFnHandler,
|
handler::SyncFnHandler,
|
||||||
|
@ -21,10 +22,7 @@ use watchexec::{
|
||||||
pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
|
pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
|
||||||
let mut config = RuntimeConfig::default();
|
let mut config = RuntimeConfig::default();
|
||||||
|
|
||||||
config.command(
|
config.command(interpret_command_args(args)?);
|
||||||
args.values_of("command")
|
|
||||||
.expect("(clap) Bug: command is not present")
|
|
||||||
);
|
|
||||||
|
|
||||||
config.pathset(match args.values_of_os("paths") {
|
config.pathset(match args.values_of_os("paths") {
|
||||||
Some(paths) => paths.map(|os| Path::new(os).to_owned()).collect(),
|
Some(paths) => paths.map(|os| Path::new(os).to_owned()).collect(),
|
||||||
|
@ -48,22 +46,6 @@ pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
|
||||||
config.command_grouped(false);
|
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 clear = args.is_present("clear");
|
||||||
let notif = args.is_present("notif");
|
let notif = args.is_present("notif");
|
||||||
let mut on_busy = args
|
let mut on_busy = args
|
||||||
|
@ -253,7 +235,7 @@ pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
|
||||||
if notif {
|
if notif {
|
||||||
Notification::new()
|
Notification::new()
|
||||||
.summary("Watchexec: change detected")
|
.summary("Watchexec: change detected")
|
||||||
.body(&format!("Running `{}`", postspawn.command.join(" ")))
|
.body(&format!("Running {}", postspawn.command))
|
||||||
.show()
|
.show()
|
||||||
.map(drop)
|
.map(drop)
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
|
@ -267,6 +249,55 @@ pub fn runtime(args: &ArgMatches) -> Result<RuntimeConfig> {
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn interpret_command_args(args: &ArgMatches) -> Result<Command> {
|
||||||
|
let mut cmd = args
|
||||||
|
.values_of("command")
|
||||||
|
.expect("(clap) Bug: command is not present")
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(if args.is_present("no-shell") {
|
||||||
|
Command::Exec {
|
||||||
|
prog: cmd.remove(0),
|
||||||
|
args: cmd,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let (shell, shopts) = if let Some(s) = args.value_of("shell") {
|
||||||
|
if s.is_empty() {
|
||||||
|
return Err(RuntimeError::CommandShellEmptyShell).into_diagnostic();
|
||||||
|
} else if s.eq_ignore_ascii_case("powershell") {
|
||||||
|
(Shell::Powershell, Vec::new())
|
||||||
|
} else if s.eq_ignore_ascii_case("none") {
|
||||||
|
return Ok(Command::Exec {
|
||||||
|
prog: cmd.remove(0),
|
||||||
|
args: cmd,
|
||||||
|
});
|
||||||
|
} else if s.eq_ignore_ascii_case("cmd") {
|
||||||
|
(cmd_shell(s.into()), Vec::new())
|
||||||
|
} else {
|
||||||
|
let sh = s.split_ascii_whitespace().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// UNWRAP: checked by first if branch
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
let (shprog, shopts) = sh.split_first().unwrap();
|
||||||
|
|
||||||
|
(
|
||||||
|
Shell::Unix(shprog.to_string()),
|
||||||
|
shopts.iter().map(|s| s.to_string()).collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(default_shell(), Vec::new())
|
||||||
|
};
|
||||||
|
|
||||||
|
Command::Shell {
|
||||||
|
shell,
|
||||||
|
args: shopts,
|
||||||
|
command: cmd.join(" "),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// until 2.0, then Powershell
|
// until 2.0, then Powershell
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn default_shell() -> Shell {
|
fn default_shell() -> Shell {
|
||||||
|
|
|
@ -10,6 +10,14 @@ First "stable" release of the library.
|
||||||
- These five new crates live in the watchexec monorepo, rather than being completely separate like `command-group` and `clearscreen`
|
- These five new crates live in the watchexec monorepo, rather than being completely separate like `command-group` and `clearscreen`
|
||||||
- This makes the main library bit less likely to change as often as it did, so it was finally time to release 2.0.0!
|
- This makes the main library bit less likely to change as often as it did, so it was finally time to release 2.0.0!
|
||||||
|
|
||||||
|
- **Change: the Action worker now launches a set of Commands**
|
||||||
|
- A new type `Command` replaces and augments `Shell`, making explicit which style of calling will be used
|
||||||
|
- The action working data now takes a `Vec<Command>`, so multiple commands to be run as a set
|
||||||
|
- Commands in the set are run sequentially, with an error interrupting the sequence
|
||||||
|
- It is thus possible to run both "shelled" and "raw exec" commands in a set
|
||||||
|
- `PreSpawn` and `PostSpawn` handlers are run per Command, not per command set
|
||||||
|
- This new style should be preferred over sending command lines like `cmd1 && cmd2`
|
||||||
|
|
||||||
- **Change: the event queue is now a priority queue**
|
- **Change: the event queue is now a priority queue**
|
||||||
- Shutting down the runtime is faster and more predictable. No more hanging after hitting Ctrl-C if there's tonnes of events coming in!
|
- Shutting down the runtime is faster and more predictable. No more hanging after hitting Ctrl-C if there's tonnes of events coming in!
|
||||||
- Signals sent to the main process have higher priority
|
- Signals sent to the main process have higher priority
|
||||||
|
@ -22,6 +30,7 @@ First "stable" release of the library.
|
||||||
- Improvement: the main subtasks of the runtime are now aborted on error
|
- Improvement: the main subtasks of the runtime are now aborted on error
|
||||||
- Improvement: the event queue is explicitly closed when shutting down
|
- Improvement: the event queue is explicitly closed when shutting down
|
||||||
- Improvement: the action worker will check if the event queue is closed more often, to shutdown early
|
- Improvement: the action worker will check if the event queue is closed more often, to shutdown early
|
||||||
|
- Improvement: `kill_on_drop` is set on Commands, which will be a little more eager to terminate processes when we're done with them
|
||||||
|
|
||||||
Other miscellaneous:
|
Other miscellaneous:
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::time::Duration;
|
||||||
use miette::{IntoDiagnostic, Result};
|
use miette::{IntoDiagnostic, Result};
|
||||||
use watchexec::{
|
use watchexec::{
|
||||||
action::{Action, Outcome},
|
action::{Action, Outcome},
|
||||||
|
command::Command,
|
||||||
config::{InitConfig, RuntimeConfig},
|
config::{InitConfig, RuntimeConfig},
|
||||||
error::ReconfigError,
|
error::ReconfigError,
|
||||||
fs::Watcher,
|
fs::Watcher,
|
||||||
|
@ -23,7 +24,10 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let mut runtime = RuntimeConfig::default();
|
let mut runtime = RuntimeConfig::default();
|
||||||
runtime.pathset(["src", "dontexist", "examples"]);
|
runtime.pathset(["src", "dontexist", "examples"]);
|
||||||
runtime.command(["date"]);
|
runtime.command(Command::Exec {
|
||||||
|
prog: "date".into(),
|
||||||
|
args: Vec::new(),
|
||||||
|
});
|
||||||
|
|
||||||
let wx = Watchexec::new(init, runtime.clone())?;
|
let wx = Watchexec::new(init, runtime.clone())?;
|
||||||
let w = wx.clone();
|
let w = wx.clone();
|
||||||
|
|
|
@ -16,10 +16,9 @@ use crate::{
|
||||||
command::Supervisor,
|
command::Supervisor,
|
||||||
error::RuntimeError,
|
error::RuntimeError,
|
||||||
event::{Event, Priority},
|
event::{Event, Priority},
|
||||||
handler::rte,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{process_holder::ProcessHolder, Outcome, PostSpawn, PreSpawn, WorkingData};
|
use super::{process_holder::ProcessHolder, Outcome, WorkingData};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct OutcomeWorker {
|
pub struct OutcomeWorker {
|
||||||
|
@ -119,50 +118,29 @@ impl OutcomeWorker {
|
||||||
debug!(outcome=?o, "meaningless without a process, not doing anything");
|
debug!(outcome=?o, "meaningless without a process, not doing anything");
|
||||||
}
|
}
|
||||||
(_, Outcome::Start) => {
|
(_, Outcome::Start) => {
|
||||||
let (cmd, shell, grouped, pre_spawn_handler, post_spawn_handler) = {
|
let (cmds, grouped, pre_spawn_handler, post_spawn_handler) = {
|
||||||
let wrk = self.working.borrow();
|
let wrk = self.working.borrow();
|
||||||
(
|
(
|
||||||
wrk.command.clone(),
|
wrk.commands.clone(),
|
||||||
wrk.shell.clone(),
|
|
||||||
wrk.grouped,
|
wrk.grouped,
|
||||||
wrk.pre_spawn_handler.clone(),
|
wrk.pre_spawn_handler.clone(),
|
||||||
wrk.post_spawn_handler.clone(),
|
wrk.post_spawn_handler.clone(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
if cmd.is_empty() {
|
if cmds.is_empty() {
|
||||||
warn!("tried to start a command without anything to run");
|
warn!("tried to start commands without anything to run");
|
||||||
} else {
|
} else {
|
||||||
let command = shell.to_command(&cmd);
|
|
||||||
let (pre_spawn, command) =
|
|
||||||
PreSpawn::new(command, cmd.clone(), self.events.clone());
|
|
||||||
|
|
||||||
debug!("running pre-spawn handler");
|
|
||||||
notry!(pre_spawn_handler.call(pre_spawn))
|
|
||||||
.map_err(|e| rte("action pre-spawn", e))?;
|
|
||||||
|
|
||||||
let mut command = Arc::try_unwrap(command)
|
|
||||||
.map_err(|_| RuntimeError::HandlerLockHeld("pre-spawn"))?
|
|
||||||
.into_inner();
|
|
||||||
|
|
||||||
trace!("spawning supervisor for command");
|
trace!("spawning supervisor for command");
|
||||||
let sup = Supervisor::spawn(
|
let sup = Supervisor::spawn(
|
||||||
self.errors_c.clone(),
|
self.errors_c.clone(),
|
||||||
self.events_c.clone(),
|
self.events_c.clone(),
|
||||||
&mut command,
|
cmds,
|
||||||
grouped,
|
grouped,
|
||||||
|
self.events.clone(),
|
||||||
|
pre_spawn_handler,
|
||||||
|
post_spawn_handler,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
debug!("running post-spawn handler");
|
|
||||||
let post_spawn = PostSpawn {
|
|
||||||
command: cmd.clone(),
|
|
||||||
events: self.events.clone(),
|
|
||||||
id: sup.id(),
|
|
||||||
grouped,
|
|
||||||
};
|
|
||||||
notry!(post_spawn_handler.call(post_spawn))
|
|
||||||
.map_err(|e| rte("action post-spawn", e))?;
|
|
||||||
|
|
||||||
notry!(self.process.replace(sup));
|
notry!(self.process.replace(sup));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,11 @@ use std::{
|
||||||
|
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
process::Command,
|
process::Command as TokioCommand,
|
||||||
sync::{Mutex, OwnedMutexGuard},
|
sync::{Mutex, OwnedMutexGuard},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{command::Shell, event::Event, filter::Filterer, handler::HandlerLock};
|
use crate::{command::Command, event::Event, filter::Filterer, handler::HandlerLock};
|
||||||
|
|
||||||
use super::Outcome;
|
use super::Outcome;
|
||||||
|
|
||||||
|
@ -47,8 +47,8 @@ pub struct WorkingData {
|
||||||
/// A handler triggered before a command is spawned.
|
/// A handler triggered before a command is spawned.
|
||||||
///
|
///
|
||||||
/// This handler is called with the [`PreSpawn`] environment, which provides mutable access to
|
/// This handler is called with the [`PreSpawn`] environment, which provides mutable access to
|
||||||
/// the [`Command`] which is about to be run. See the notes on the [`PreSpawn::command()`]
|
/// the [`Command`](TokioCommand) which is about to be run. See the notes on the
|
||||||
/// method for important information on what you can do with it.
|
/// [`PreSpawn::command()`] method for important information on what you can do with it.
|
||||||
///
|
///
|
||||||
/// Returning an error from the handler will stop the action from processing further, and issue
|
/// Returning an error from the handler will stop the action from processing further, and issue
|
||||||
/// a [`RuntimeError`][crate::error::RuntimeError] to the error channel.
|
/// a [`RuntimeError`][crate::error::RuntimeError] to the error channel.
|
||||||
|
@ -64,13 +64,10 @@ pub struct WorkingData {
|
||||||
/// issue a [`RuntimeError`][crate::error::RuntimeError] to the error channel.
|
/// issue a [`RuntimeError`][crate::error::RuntimeError] to the error channel.
|
||||||
pub post_spawn_handler: HandlerLock<PostSpawn>,
|
pub post_spawn_handler: HandlerLock<PostSpawn>,
|
||||||
|
|
||||||
/// Command to execute.
|
/// Commands to execute.
|
||||||
///
|
///
|
||||||
/// When `shell` is [`Shell::None`], this is expected to be in “execvp(3)” format: first
|
/// These will be run in order, and an error will stop early.
|
||||||
/// program, rest arguments. Otherwise, all elements will be joined together with a single space
|
pub commands: Vec<Command>,
|
||||||
/// and passed to the shell. More control can then be obtained by providing a 1-element vec, and
|
|
||||||
/// doing your own joining and/or escaping there.
|
|
||||||
pub command: Vec<String>,
|
|
||||||
|
|
||||||
/// Whether to use process groups (on Unix) or job control (on Windows) to run the command.
|
/// Whether to use process groups (on Unix) or job control (on Windows) to run the command.
|
||||||
///
|
///
|
||||||
|
@ -81,11 +78,6 @@ pub struct WorkingData {
|
||||||
/// meantime.
|
/// meantime.
|
||||||
pub grouped: bool,
|
pub grouped: bool,
|
||||||
|
|
||||||
/// The shell to use to run the command.
|
|
||||||
///
|
|
||||||
/// See the [`Shell`] enum documentation for more details.
|
|
||||||
pub shell: Shell,
|
|
||||||
|
|
||||||
/// The filterer implementation to use when filtering events.
|
/// The filterer implementation to use when filtering events.
|
||||||
///
|
///
|
||||||
/// The default is a no-op, which will always pass every event.
|
/// The default is a no-op, which will always pass every event.
|
||||||
|
@ -96,8 +88,7 @@ impl fmt::Debug for WorkingData {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("WorkingData")
|
f.debug_struct("WorkingData")
|
||||||
.field("throttle", &self.throttle)
|
.field("throttle", &self.throttle)
|
||||||
.field("shell", &self.shell)
|
.field("commands", &self.commands)
|
||||||
.field("command", &self.command)
|
|
||||||
.field("grouped", &self.grouped)
|
.field("grouped", &self.grouped)
|
||||||
.field("filterer", &self.filterer)
|
.field("filterer", &self.filterer)
|
||||||
.finish_non_exhaustive()
|
.finish_non_exhaustive()
|
||||||
|
@ -107,13 +98,11 @@ impl fmt::Debug for WorkingData {
|
||||||
impl Default for WorkingData {
|
impl Default for WorkingData {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
// set to 50ms here, but will remain 100ms on cli until 2022
|
|
||||||
throttle: Duration::from_millis(50),
|
throttle: Duration::from_millis(50),
|
||||||
action_handler: Default::default(),
|
action_handler: Default::default(),
|
||||||
pre_spawn_handler: Default::default(),
|
pre_spawn_handler: Default::default(),
|
||||||
post_spawn_handler: Default::default(),
|
post_spawn_handler: Default::default(),
|
||||||
command: Vec::new(),
|
commands: Vec::new(),
|
||||||
shell: Shell::default(),
|
|
||||||
grouped: true,
|
grouped: true,
|
||||||
filterer: Arc::new(()),
|
filterer: Arc::new(()),
|
||||||
}
|
}
|
||||||
|
@ -165,28 +154,26 @@ impl Action {
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct PreSpawn {
|
pub struct PreSpawn {
|
||||||
/// The command which is about to be spawned.
|
/// The command which is about to be spawned.
|
||||||
///
|
pub command: Command,
|
||||||
/// This is the final command, after the [`Shell`] has been applied.
|
|
||||||
pub command: Vec<String>,
|
|
||||||
|
|
||||||
/// The collected events which triggered the action this command issues from.
|
/// The collected events which triggered the action this command issues from.
|
||||||
pub events: Arc<[Event]>,
|
pub events: Arc<[Event]>,
|
||||||
|
|
||||||
command_w: Weak<Mutex<Command>>,
|
to_spawn_w: Weak<Mutex<TokioCommand>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreSpawn {
|
impl PreSpawn {
|
||||||
pub(super) fn new(
|
pub(crate) fn new(
|
||||||
command: Command,
|
command: Command,
|
||||||
cmd: Vec<String>,
|
to_spawn: TokioCommand,
|
||||||
events: Arc<[Event]>,
|
events: Arc<[Event]>,
|
||||||
) -> (Self, Arc<Mutex<Command>>) {
|
) -> (Self, Arc<Mutex<TokioCommand>>) {
|
||||||
let arc = Arc::new(Mutex::new(command));
|
let arc = Arc::new(Mutex::new(to_spawn));
|
||||||
(
|
(
|
||||||
Self {
|
Self {
|
||||||
command: cmd,
|
command,
|
||||||
events,
|
events,
|
||||||
command_w: Arc::downgrade(&arc),
|
to_spawn_w: Arc::downgrade(&arc),
|
||||||
},
|
},
|
||||||
arc.clone(),
|
arc.clone(),
|
||||||
)
|
)
|
||||||
|
@ -199,8 +186,8 @@ impl PreSpawn {
|
||||||
/// documentation about handlers for more.
|
/// documentation about handlers for more.
|
||||||
///
|
///
|
||||||
/// This will always return `Some()` under normal circumstances.
|
/// This will always return `Some()` under normal circumstances.
|
||||||
pub async fn command(&self) -> Option<OwnedMutexGuard<Command>> {
|
pub async fn command(&self) -> Option<OwnedMutexGuard<TokioCommand>> {
|
||||||
if let Some(arc) = self.command_w.upgrade() {
|
if let Some(arc) = self.to_spawn_w.upgrade() {
|
||||||
Some(arc.lock_owned().await)
|
Some(arc.lock_owned().await)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -216,8 +203,8 @@ impl PreSpawn {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct PostSpawn {
|
pub struct PostSpawn {
|
||||||
/// The final command the process was spawned with.
|
/// The command the process was spawned with.
|
||||||
pub command: Vec<String>,
|
pub command: Command,
|
||||||
|
|
||||||
/// The collected events which triggered the action the command issues from.
|
/// The collected events which triggered the action the command issues from.
|
||||||
pub events: Arc<[Event]>,
|
pub events: Arc<[Event]>,
|
||||||
|
|
|
@ -1,14 +1,156 @@
|
||||||
//! Command construction, configuration, and tracking.
|
//! Command construction, configuration, and tracking.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use tokio::process::Command as TokioCommand;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
use crate::error::RuntimeError;
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use process::Process;
|
pub use process::Process;
|
||||||
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use shell::Shell;
|
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use supervisor::Supervisor;
|
pub use supervisor::Supervisor;
|
||||||
|
|
||||||
mod process;
|
mod process;
|
||||||
mod shell;
|
|
||||||
mod supervisor;
|
mod supervisor;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
/// A command to execute.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Command {
|
||||||
|
/// A raw command which will be executed as-is.
|
||||||
|
Exec {
|
||||||
|
/// The program to run.
|
||||||
|
prog: String,
|
||||||
|
|
||||||
|
/// The arguments to pass.
|
||||||
|
args: Vec<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A shelled command line.
|
||||||
|
Shell {
|
||||||
|
/// The shell to run.
|
||||||
|
shell: Shell,
|
||||||
|
|
||||||
|
/// Additional options or arguments to pass to the shell.
|
||||||
|
///
|
||||||
|
/// These will be inserted before the `-c` (or equivalent) option immediately preceding the
|
||||||
|
/// command line string.
|
||||||
|
args: Vec<String>,
|
||||||
|
|
||||||
|
/// The command line to pass to the shell.
|
||||||
|
command: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shell to use to run shelled commands.
|
||||||
|
///
|
||||||
|
/// `Cmd` and `Powershell` are special-cased because they have different calling conventions. Also
|
||||||
|
/// `Cmd` is only available in Windows, while `Powershell` is also available on unices (provided the
|
||||||
|
/// end-user has it installed, of course).
|
||||||
|
///
|
||||||
|
/// There is no default implemented: as consumer of this library you are encouraged to set your own
|
||||||
|
/// default as makes sense in your application / for your platform.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Shell {
|
||||||
|
/// Use the given string as a unix shell invocation.
|
||||||
|
///
|
||||||
|
/// This means two things:
|
||||||
|
/// - the program is invoked with `-c` followed by the command, and
|
||||||
|
/// - the string will be split on space, and the resulting vec used as execvp(3) arguments:
|
||||||
|
/// first is the shell program, rest are additional arguments (which come before the `-c`
|
||||||
|
/// mentioned above). This is a very simplistic approach deliberately: it will not support
|
||||||
|
/// quoted arguments, for example. Use [`Shell::None`] with a custom command vec for that.
|
||||||
|
Unix(String),
|
||||||
|
|
||||||
|
/// Use the Windows CMD.EXE shell.
|
||||||
|
///
|
||||||
|
/// This is invoked with `/C` followed by the command.
|
||||||
|
#[cfg(windows)]
|
||||||
|
Cmd,
|
||||||
|
|
||||||
|
/// Use Powershell, on Windows or elsewhere.
|
||||||
|
///
|
||||||
|
/// This is invoked with `-Command` followed by the command.
|
||||||
|
///
|
||||||
|
/// This is preferred over `Unix("pwsh")`, though that will also work on unices due to
|
||||||
|
/// Powershell supporting the `-c` short option.
|
||||||
|
Powershell,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
/// Obtain a [`tokio::process::Command`] from a [`Command`].
|
||||||
|
///
|
||||||
|
/// Behaves as described in the [`Command`] and [`Shell`] documentation.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// - Errors if the `command` of a `Command::Shell` is empty.
|
||||||
|
/// - Errors if the `shell` of a `Shell::Unix(shell)` is empty.
|
||||||
|
pub fn to_spawnable(&self) -> Result<TokioCommand, RuntimeError> {
|
||||||
|
trace!(cmd=?self, "constructing command");
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Command::Exec { prog, args } => {
|
||||||
|
let mut c = TokioCommand::new(prog);
|
||||||
|
c.args(args);
|
||||||
|
Ok(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
Command::Shell {
|
||||||
|
shell,
|
||||||
|
args,
|
||||||
|
command,
|
||||||
|
} => {
|
||||||
|
let (shcmd, shcliopt) = match shell {
|
||||||
|
#[cfg(windows)]
|
||||||
|
Shell::Cmd => ("cmd.exe", "/C"),
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
Shell::Powershell => ("powershell.exe", "-Command"),
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
Shell::Powershell => ("pwsh", "-c"),
|
||||||
|
|
||||||
|
Shell::Unix(cmd) => {
|
||||||
|
if cmd.is_empty() {
|
||||||
|
return Err(RuntimeError::CommandShellEmptyShell);
|
||||||
|
}
|
||||||
|
|
||||||
|
(cmd.as_str(), "-c")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if command.is_empty() {
|
||||||
|
return Err(RuntimeError::CommandShellEmptyCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut c = TokioCommand::new(shcmd);
|
||||||
|
c.args(args);
|
||||||
|
c.arg(shcliopt).arg(command);
|
||||||
|
Ok(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Command {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Command::Exec { prog, args } => {
|
||||||
|
write!(f, "{}", prog)?;
|
||||||
|
for arg in args {
|
||||||
|
write!(f, " {}", arg)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Command::Shell { command, .. } => {
|
||||||
|
write!(f, "{}", command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,201 +0,0 @@
|
||||||
use tokio::process::Command;
|
|
||||||
use tracing::trace;
|
|
||||||
|
|
||||||
/// Shell to use to run commands.
|
|
||||||
///
|
|
||||||
/// `Cmd` and `Powershell` are special-cased because they have different calling conventions. Also
|
|
||||||
/// `Cmd` is only available in Windows, while `Powershell` is also available on unices (provided the
|
|
||||||
/// end-user has it installed, of course).
|
|
||||||
///
|
|
||||||
/// See [`Config.cmd`] for the semantics of `None` vs the other options.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum Shell {
|
|
||||||
/// Use no shell, and execute the command directly.
|
|
||||||
///
|
|
||||||
/// This is the default, however as consumer of this library you are encouraged to set your own
|
|
||||||
/// default as makes sense in your application / for your platform.
|
|
||||||
None,
|
|
||||||
|
|
||||||
/// Use the given string as a unix shell invocation.
|
|
||||||
///
|
|
||||||
/// This means two things:
|
|
||||||
/// - the program is invoked with `-c` followed by the command, and
|
|
||||||
/// - the string will be split on space, and the resulting vec used as execvp(3) arguments:
|
|
||||||
/// first is the shell program, rest are additional arguments (which come before the `-c`
|
|
||||||
/// mentioned above). This is a very simplistic approach deliberately: it will not support
|
|
||||||
/// quoted arguments, for example. Use [`Shell::None`] with a custom command vec for that.
|
|
||||||
Unix(String),
|
|
||||||
|
|
||||||
/// Use the Windows CMD.EXE shell.
|
|
||||||
///
|
|
||||||
/// This is invoked with `/C` followed by the command.
|
|
||||||
#[cfg(windows)]
|
|
||||||
Cmd,
|
|
||||||
|
|
||||||
/// Use Powershell, on Windows or elsewhere.
|
|
||||||
///
|
|
||||||
/// This is invoked with `-Command` followed by the command.
|
|
||||||
///
|
|
||||||
/// This is preferred over `Unix("pwsh")`, though that will also work on unices due to
|
|
||||||
/// Powershell supporting the `-c` short option.
|
|
||||||
Powershell,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Shell {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Shell {
|
|
||||||
/// Obtain a [`Command`] given a list of command parts.
|
|
||||||
///
|
|
||||||
/// Behaves as described in the enum documentation.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// - Panics if `cmd` is empty.
|
|
||||||
/// - Panics if the string in the `Unix` variant is empty or only whitespace.
|
|
||||||
pub fn to_command(&self, cmd: &[String]) -> Command {
|
|
||||||
assert!(!cmd.is_empty(), "cmd was empty");
|
|
||||||
trace!(shell=?self, ?cmd, "constructing command");
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Shell::None => {
|
|
||||||
// UNWRAP: checked by assert
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
let (first, rest) = cmd.split_first().unwrap();
|
|
||||||
let mut c = Command::new(first);
|
|
||||||
c.args(rest);
|
|
||||||
c
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
Shell::Cmd => {
|
|
||||||
let mut c = Command::new("cmd.exe");
|
|
||||||
c.arg("/C").arg(cmd.join(" "));
|
|
||||||
c
|
|
||||||
}
|
|
||||||
|
|
||||||
Shell::Powershell if cfg!(windows) => {
|
|
||||||
let mut c = Command::new("powershell.exe");
|
|
||||||
c.arg("-Command").arg(cmd.join(" "));
|
|
||||||
c
|
|
||||||
}
|
|
||||||
|
|
||||||
Shell::Powershell => {
|
|
||||||
let mut c = Command::new("pwsh");
|
|
||||||
c.arg("-Command").arg(cmd.join(" "));
|
|
||||||
c
|
|
||||||
}
|
|
||||||
|
|
||||||
Shell::Unix(name) => {
|
|
||||||
assert!(!name.is_empty(), "shell program was empty");
|
|
||||||
let sh = name.split_ascii_whitespace().collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// UNWRAP: checked by assert
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
let (shprog, shopts) = sh.split_first().unwrap();
|
|
||||||
|
|
||||||
let mut c = Command::new(shprog);
|
|
||||||
c.args(shopts);
|
|
||||||
c.arg("-c").arg(cmd.join(" "));
|
|
||||||
c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::Shell;
|
|
||||||
use command_group::AsyncCommandGroup;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[cfg(unix)]
|
|
||||||
async fn unix_shell_default() -> Result<(), std::io::Error> {
|
|
||||||
assert!(Shell::default()
|
|
||||||
.to_command(&["echo".into(), "hi".into()])
|
|
||||||
.group_status()
|
|
||||||
.await?
|
|
||||||
.success());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[cfg(unix)]
|
|
||||||
async fn unix_shell_none() -> Result<(), std::io::Error> {
|
|
||||||
assert!(Shell::None
|
|
||||||
.to_command(&["echo".into(), "hi".into()])
|
|
||||||
.group_status()
|
|
||||||
.await?
|
|
||||||
.success());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[cfg(unix)]
|
|
||||||
async fn unix_shell_alternate() -> Result<(), std::io::Error> {
|
|
||||||
assert!(Shell::Unix("bash".into())
|
|
||||||
.to_command(&["echo".into(), "hi".into()])
|
|
||||||
.group_status()
|
|
||||||
.await?
|
|
||||||
.success());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[cfg(unix)]
|
|
||||||
async fn unix_shell_alternate_shopts() -> Result<(), std::io::Error> {
|
|
||||||
assert!(Shell::Unix("bash -o errexit".into())
|
|
||||||
.to_command(&["echo".into(), "hi".into()])
|
|
||||||
.group_status()
|
|
||||||
.await?
|
|
||||||
.success());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[cfg(windows)]
|
|
||||||
async fn windows_shell_default() -> Result<(), std::io::Error> {
|
|
||||||
assert!(Shell::default()
|
|
||||||
.to_command(&["echo".into(), "hi".into()])
|
|
||||||
.group_status()
|
|
||||||
.await?
|
|
||||||
.success());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[cfg(windows)]
|
|
||||||
async fn windows_shell_cmd() -> Result<(), std::io::Error> {
|
|
||||||
assert!(Shell::Cmd
|
|
||||||
.to_command(&["echo".into(), "hi".into()])
|
|
||||||
.group_status()
|
|
||||||
.await?
|
|
||||||
.success());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[cfg(windows)]
|
|
||||||
async fn windows_shell_powershell() -> Result<(), std::io::Error> {
|
|
||||||
assert!(Shell::Powershell
|
|
||||||
.to_command(&["echo".into(), "hi".into()])
|
|
||||||
.group_status()
|
|
||||||
.await?
|
|
||||||
.success());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[cfg(windows)]
|
|
||||||
async fn windows_shell_unix_style_powershell() -> Result<(), std::io::Error> {
|
|
||||||
assert!(Shell::Unix("powershell.exe".into())
|
|
||||||
.to_command(&["echo".into(), "hi".into()])
|
|
||||||
.group_status()
|
|
||||||
.await?
|
|
||||||
.success());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +1,22 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_priority_channel as priority;
|
use async_priority_channel as priority;
|
||||||
use command_group::AsyncCommandGroup;
|
use command_group::AsyncCommandGroup;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
process::Command,
|
|
||||||
select, spawn,
|
select, spawn,
|
||||||
sync::{
|
sync::{
|
||||||
mpsc::{self, Sender},
|
mpsc::{self, Sender},
|
||||||
watch,
|
watch,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tracing::{debug, error, trace};
|
use tracing::{debug, debug_span, error, trace, Span};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
action::{PostSpawn, PreSpawn},
|
||||||
|
command::Command,
|
||||||
error::RuntimeError,
|
error::RuntimeError,
|
||||||
event::{Event, Priority, Source, Tag},
|
event::{Event, Priority, Source, Tag},
|
||||||
|
handler::{rte, HandlerLock},
|
||||||
signal::process::SubSignal,
|
signal::process::SubSignal,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,136 +28,171 @@ enum Intervention {
|
||||||
Signal(SubSignal),
|
Signal(SubSignal),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A task which supervises a process.
|
/// A task which supervises a sequence of processes.
|
||||||
///
|
///
|
||||||
/// This spawns a process from a [`Command`] and waits for it to complete while handling
|
/// This spawns processes from a vec of [`Command`]s in order and waits for each to complete while
|
||||||
/// interventions to it: orders to terminate it, or to send a signal to it. It also immediately
|
/// handling interventions to itself: orders to terminate, or to send a signal to the current
|
||||||
/// issues a [`Tag::ProcessCompletion`] event when the process completes.
|
/// process. It also immediately issues a [`Tag::ProcessCompletion`] event when the set completes.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Supervisor {
|
pub struct Supervisor {
|
||||||
id: u32,
|
|
||||||
intervene: Sender<Intervention>,
|
intervene: Sender<Intervention>,
|
||||||
ongoing: watch::Receiver<bool>,
|
ongoing: watch::Receiver<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Supervisor {
|
impl Supervisor {
|
||||||
/// Spawns the command, the supervision task, and returns a new control object.
|
/// Spawns the command set, the supervision task, and returns a new control object.
|
||||||
pub fn spawn(
|
pub fn spawn(
|
||||||
errors: Sender<RuntimeError>,
|
errors: Sender<RuntimeError>,
|
||||||
events: priority::Sender<Event, Priority>,
|
events: priority::Sender<Event, Priority>,
|
||||||
command: &mut Command,
|
mut commands: Vec<Command>,
|
||||||
grouped: bool,
|
grouped: bool,
|
||||||
|
actioned_events: Arc<[Event]>,
|
||||||
|
pre_spawn_handler: HandlerLock<PreSpawn>,
|
||||||
|
post_spawn_handler: HandlerLock<PostSpawn>,
|
||||||
) -> Result<Self, RuntimeError> {
|
) -> Result<Self, RuntimeError> {
|
||||||
debug!(%grouped, ?command, "spawning command");
|
// get commands in reverse order so pop() returns the next to run
|
||||||
let (process, id) = if grouped {
|
commands.reverse();
|
||||||
let proc = command.group_spawn().map_err(|err| RuntimeError::IoError {
|
let next = commands.pop().ok_or(RuntimeError::NoCommands)?;
|
||||||
about: "spawning process group",
|
|
||||||
err,
|
|
||||||
})?;
|
|
||||||
let id = proc.id().ok_or(RuntimeError::ProcessDeadOnArrival)?;
|
|
||||||
debug!(pgid=%id, "process group spawned");
|
|
||||||
(Process::Grouped(proc), id)
|
|
||||||
} else {
|
|
||||||
let proc = command.spawn().map_err(|err| RuntimeError::IoError {
|
|
||||||
about: "spawning process (ungrouped)",
|
|
||||||
err,
|
|
||||||
})?;
|
|
||||||
let id = proc.id().ok_or(RuntimeError::ProcessDeadOnArrival)?;
|
|
||||||
debug!(pid=%id, "process spawned");
|
|
||||||
(Process::Ungrouped(proc), id)
|
|
||||||
};
|
|
||||||
|
|
||||||
let (notify, waiter) = watch::channel(true);
|
let (notify, waiter) = watch::channel(true);
|
||||||
let (int_s, int_r) = mpsc::channel(8);
|
let (int_s, int_r) = mpsc::channel(8);
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let mut process = process;
|
let span = debug_span!("supervisor");
|
||||||
|
|
||||||
|
let mut next = next;
|
||||||
|
let mut commands = commands;
|
||||||
let mut int = int_r;
|
let mut int = int_r;
|
||||||
|
|
||||||
debug!(?process, "starting task to watch on process");
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
select! {
|
let (mut process, pid) = match spawn_process(
|
||||||
p = process.wait() => {
|
span.clone(),
|
||||||
match p {
|
next,
|
||||||
Ok(_) => break, // deal with it below
|
grouped,
|
||||||
Err(err) => {
|
actioned_events.clone(),
|
||||||
error!(%err, "while waiting on process");
|
pre_spawn_handler.clone(),
|
||||||
errors.send(err).await.ok();
|
post_spawn_handler.clone(),
|
||||||
trace!("marking process as done");
|
)
|
||||||
notify.send(false).unwrap_or_else(|e| trace!(%e, "error sending process complete"));
|
.await
|
||||||
trace!("closing supervisor task early");
|
{
|
||||||
return;
|
Ok(pp) => pp,
|
||||||
}
|
Err(err) => {
|
||||||
}
|
let _enter = span.enter();
|
||||||
},
|
error!(%err, "while spawning process");
|
||||||
Some(int) = int.recv() => {
|
errors.send(err).await.ok();
|
||||||
match int {
|
trace!("marking process as done");
|
||||||
Intervention::Kill => {
|
notify
|
||||||
if let Err(err) = process.kill().await {
|
.send(false)
|
||||||
error!(%err, "while killing process");
|
.unwrap_or_else(|e| trace!(%e, "error sending process complete"));
|
||||||
errors.send(err).await.ok();
|
trace!("closing supervisor task early");
|
||||||
trace!("continuing to watch command");
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
span.in_scope(|| debug!(?process, ?pid, "spawned process"));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
select! {
|
||||||
|
p = process.wait() => {
|
||||||
|
match p {
|
||||||
|
Ok(_) => break, // deal with it below
|
||||||
|
Err(err) => {
|
||||||
|
let _enter = span.enter();
|
||||||
|
error!(%err, "while waiting on process");
|
||||||
|
errors.try_send(err).ok();
|
||||||
|
trace!("marking process as done");
|
||||||
|
notify.send(false).unwrap_or_else(|e| trace!(%e, "error sending process complete"));
|
||||||
|
trace!("closing supervisor task early");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(unix)]
|
},
|
||||||
Intervention::Signal(sig) => {
|
Some(int) = int.recv() => {
|
||||||
if let Some(sig) = sig.to_nix() {
|
match int {
|
||||||
if let Err(err) = process.signal(sig) {
|
Intervention::Kill => {
|
||||||
error!(%err, "while sending signal to process");
|
if let Err(err) = process.kill().await {
|
||||||
errors.send(err).await.ok();
|
let _enter = span.enter();
|
||||||
|
error!(%err, "while killing process");
|
||||||
|
errors.try_send(err).ok();
|
||||||
trace!("continuing to watch command");
|
trace!("continuing to watch command");
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
Intervention::Signal(sig) => {
|
||||||
|
let _enter = span.enter();
|
||||||
|
if let Some(sig) = sig.to_nix() {
|
||||||
|
if let Err(err) = process.signal(sig) {
|
||||||
|
error!(%err, "while sending signal to process");
|
||||||
|
errors.try_send(err).ok();
|
||||||
|
trace!("continuing to watch command");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let err = RuntimeError::UnsupportedSignal(sig);
|
||||||
|
error!(%err, "while sending signal to process");
|
||||||
|
errors.try_send(err).ok();
|
||||||
|
trace!("continuing to watch command");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
Intervention::Signal(sig) => {
|
||||||
|
let _enter = span.enter();
|
||||||
|
// https://github.com/watchexec/watchexec/issues/219
|
||||||
let err = RuntimeError::UnsupportedSignal(sig);
|
let err = RuntimeError::UnsupportedSignal(sig);
|
||||||
error!(%err, "while sending signal to process");
|
error!(%err, "while sending signal to process");
|
||||||
errors.send(err).await.ok();
|
errors.try_send(err).ok();
|
||||||
trace!("continuing to watch command");
|
trace!("continuing to watch command");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
}
|
||||||
Intervention::Signal(sig) => {
|
else => break,
|
||||||
// https://github.com/watchexec/watchexec/issues/219
|
}
|
||||||
let err = RuntimeError::UnsupportedSignal(sig);
|
}
|
||||||
error!(%err, "while sending signal to process");
|
|
||||||
errors.send(err).await.ok();
|
span.in_scope(|| trace!("got out of loop, waiting once more"));
|
||||||
trace!("continuing to watch command");
|
match process.wait().await {
|
||||||
}
|
Err(err) => {
|
||||||
|
let _enter = span.enter();
|
||||||
|
error!(%err, "while waiting on process");
|
||||||
|
errors.try_send(err).ok();
|
||||||
|
}
|
||||||
|
Ok(status) => {
|
||||||
|
let event = span.in_scope(|| {
|
||||||
|
let event = Event {
|
||||||
|
tags: vec![
|
||||||
|
Tag::Source(Source::Internal),
|
||||||
|
Tag::ProcessCompletion(status.map(|s| s.into())),
|
||||||
|
],
|
||||||
|
metadata: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(?event, "creating synthetic process completion event");
|
||||||
|
event
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(err) = events.send(event, Priority::Low).await {
|
||||||
|
let _enter = span.enter();
|
||||||
|
error!(%err, "while sending process completion event");
|
||||||
|
errors
|
||||||
|
.try_send(RuntimeError::EventChannelSend {
|
||||||
|
ctx: "command supervisor",
|
||||||
|
err,
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else => break,
|
}
|
||||||
}
|
|
||||||
}
|
let _enter = span.enter();
|
||||||
|
if let Some(cmd) = commands.pop() {
|
||||||
trace!("got out of loop, waiting once more");
|
debug!(?cmd, "queuing up next command");
|
||||||
match process.wait().await {
|
next = cmd;
|
||||||
Err(err) => {
|
} else {
|
||||||
error!(%err, "while waiting on process");
|
debug!("no more commands to supervise");
|
||||||
errors.send(err).await.ok();
|
break;
|
||||||
}
|
|
||||||
Ok(status) => {
|
|
||||||
let event = Event {
|
|
||||||
tags: vec![
|
|
||||||
Tag::Source(Source::Internal),
|
|
||||||
Tag::ProcessCompletion(status.map(|s| s.into())),
|
|
||||||
],
|
|
||||||
metadata: Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!(?event, "creating synthetic process completion event");
|
|
||||||
if let Err(err) = events.send(event, Priority::Low).await {
|
|
||||||
error!(%err, "while sending process completion event");
|
|
||||||
errors
|
|
||||||
.send(RuntimeError::EventChannelSend {
|
|
||||||
ctx: "command supervisor",
|
|
||||||
err,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _enter = span.enter();
|
||||||
trace!("marking process as done");
|
trace!("marking process as done");
|
||||||
notify
|
notify
|
||||||
.send(false)
|
.send(false)
|
||||||
|
@ -162,21 +201,11 @@ impl Supervisor {
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id,
|
|
||||||
ongoing: waiter,
|
ongoing: waiter,
|
||||||
intervene: int_s,
|
intervene: int_s,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the PID of the process or process group.
|
|
||||||
///
|
|
||||||
/// This always successfully returns a PID, even if the process has already exited, as the PID
|
|
||||||
/// is held as soon as the process spawns. Take care not to use this for process manipulation
|
|
||||||
/// once the process has exited, as the ID may have been reused already.
|
|
||||||
pub fn id(&self) -> u32 {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Issues a signal to the process.
|
/// Issues a signal to the process.
|
||||||
///
|
///
|
||||||
/// On Windows, this currently only supports [`SubSignal::ForceStop`].
|
/// On Windows, this currently only supports [`SubSignal::ForceStop`].
|
||||||
|
@ -239,3 +268,76 @@ impl Supervisor {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn spawn_process(
|
||||||
|
span: Span,
|
||||||
|
command: Command,
|
||||||
|
grouped: bool,
|
||||||
|
actioned_events: Arc<[Event]>,
|
||||||
|
pre_spawn_handler: HandlerLock<PreSpawn>,
|
||||||
|
post_spawn_handler: HandlerLock<PostSpawn>,
|
||||||
|
) -> Result<(Process, u32), RuntimeError> {
|
||||||
|
let (pre_spawn, spawnable) = span.in_scope::<_, Result<_, RuntimeError>>(|| {
|
||||||
|
debug!(%grouped, ?command, "preparing command");
|
||||||
|
let mut spawnable = command.to_spawnable()?;
|
||||||
|
spawnable.kill_on_drop(true);
|
||||||
|
|
||||||
|
debug!("running pre-spawn handler");
|
||||||
|
Ok(PreSpawn::new(
|
||||||
|
command.clone(),
|
||||||
|
spawnable,
|
||||||
|
actioned_events.clone(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
pre_spawn_handler
|
||||||
|
.call(pre_spawn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| rte("action pre-spawn", e))?;
|
||||||
|
|
||||||
|
let (proc, id, post_spawn) = span.in_scope::<_, Result<_, RuntimeError>>(|| {
|
||||||
|
let mut spawnable = Arc::try_unwrap(spawnable)
|
||||||
|
.map_err(|_| RuntimeError::HandlerLockHeld("pre-spawn"))?
|
||||||
|
.into_inner();
|
||||||
|
|
||||||
|
debug!(command=?spawnable, "spawning command");
|
||||||
|
let (proc, id) = if grouped {
|
||||||
|
let proc = spawnable
|
||||||
|
.group_spawn()
|
||||||
|
.map_err(|err| RuntimeError::IoError {
|
||||||
|
about: "spawning process group",
|
||||||
|
err,
|
||||||
|
})?;
|
||||||
|
let id = proc.id().ok_or(RuntimeError::ProcessDeadOnArrival)?;
|
||||||
|
debug!(pgid=%id, "process group spawned");
|
||||||
|
(Process::Grouped(proc), id)
|
||||||
|
} else {
|
||||||
|
let proc = spawnable.spawn().map_err(|err| RuntimeError::IoError {
|
||||||
|
about: "spawning process (ungrouped)",
|
||||||
|
err,
|
||||||
|
})?;
|
||||||
|
let id = proc.id().ok_or(RuntimeError::ProcessDeadOnArrival)?;
|
||||||
|
debug!(pid=%id, "process spawned");
|
||||||
|
(Process::Ungrouped(proc), id)
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("running post-spawn handler");
|
||||||
|
Ok((
|
||||||
|
proc,
|
||||||
|
id,
|
||||||
|
PostSpawn {
|
||||||
|
command: command.clone(),
|
||||||
|
events: actioned_events.clone(),
|
||||||
|
id,
|
||||||
|
grouped,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
post_spawn_handler
|
||||||
|
.call(post_spawn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| rte("action post-spawn", e))?;
|
||||||
|
|
||||||
|
Ok((proc, id))
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
|
||||||
|
use super::{Command, Shell};
|
||||||
|
use command_group::AsyncCommandGroup;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn unix_shell_none() -> Result<(), std::io::Error> {
|
||||||
|
assert!(Command::Exec {
|
||||||
|
prog: "echo".into(),
|
||||||
|
args: vec!["hi".into()]
|
||||||
|
}
|
||||||
|
.to_spawnable()
|
||||||
|
.unwrap()
|
||||||
|
.group_status()
|
||||||
|
.await?
|
||||||
|
.success());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn unix_shell_sh() -> Result<(), std::io::Error> {
|
||||||
|
assert!(Command::Shell {
|
||||||
|
shell: Shell::Unix("sh".into()),
|
||||||
|
args: Vec::new(),
|
||||||
|
command: "echo hi".into()
|
||||||
|
}
|
||||||
|
.to_spawnable()
|
||||||
|
.unwrap()
|
||||||
|
.group_status()
|
||||||
|
.await?
|
||||||
|
.success());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn unix_shell_alternate() -> Result<(), std::io::Error> {
|
||||||
|
assert!(Command::Shell {
|
||||||
|
shell: Shell::Unix("bash".into()),
|
||||||
|
args: Vec::new(),
|
||||||
|
command: "echo hi".into()
|
||||||
|
}
|
||||||
|
.to_spawnable()
|
||||||
|
.unwrap()
|
||||||
|
.group_status()
|
||||||
|
.await?
|
||||||
|
.success());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn unix_shell_alternate_shopts() -> Result<(), std::io::Error> {
|
||||||
|
assert!(Command::Shell {
|
||||||
|
shell: Shell::Unix("bash".into()),
|
||||||
|
args: vec!["-o".into(), "errexit".into()],
|
||||||
|
command: "echo hi".into()
|
||||||
|
}
|
||||||
|
.to_spawnable()
|
||||||
|
.unwrap()
|
||||||
|
.group_status()
|
||||||
|
.await?
|
||||||
|
.success());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[cfg(windows)]
|
||||||
|
async fn windows_shell_none() -> Result<(), std::io::Error> {
|
||||||
|
assert!(Command::Exec {
|
||||||
|
prog: "echo".into(),
|
||||||
|
args: vec!["hi".into()]
|
||||||
|
}
|
||||||
|
.to_spawnable()
|
||||||
|
.unwrap()
|
||||||
|
.group_status()
|
||||||
|
.await?
|
||||||
|
.success());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[cfg(windows)]
|
||||||
|
async fn windows_shell_cmd() -> Result<(), std::io::Error> {
|
||||||
|
assert!(Command::Shell {
|
||||||
|
shell: Shell::Cmd,
|
||||||
|
args: Vec::new(),
|
||||||
|
command: "echo hi".into()
|
||||||
|
}
|
||||||
|
.to_spawnable()
|
||||||
|
.unwrap()
|
||||||
|
.group_status()
|
||||||
|
.await?
|
||||||
|
.success());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[cfg(windows)]
|
||||||
|
async fn windows_shell_powershell() -> Result<(), std::io::Error> {
|
||||||
|
assert!(Command::Shell {
|
||||||
|
shell: Shell::Powershell,
|
||||||
|
args: Vec::new(),
|
||||||
|
command: "echo hi".into()
|
||||||
|
}
|
||||||
|
.to_spawnable()
|
||||||
|
.unwrap()
|
||||||
|
.group_status()
|
||||||
|
.await?
|
||||||
|
.success());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[cfg(windows)]
|
||||||
|
async fn windows_shell_unix_style_powershell() -> Result<(), std::io::Error> {
|
||||||
|
assert!(Command::Shell {
|
||||||
|
shell: Shell::Unix("powershell.exe".into()),
|
||||||
|
args: Vec::new(),
|
||||||
|
command: "echo hi".into()
|
||||||
|
}
|
||||||
|
.to_spawnable()
|
||||||
|
.unwrap()
|
||||||
|
.group_status()
|
||||||
|
.await?
|
||||||
|
.success());
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ use std::{fmt, path::Path, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
action::{Action, PostSpawn, PreSpawn},
|
action::{Action, PostSpawn, PreSpawn},
|
||||||
command::Shell,
|
command::Command,
|
||||||
filter::Filterer,
|
filter::Filterer,
|
||||||
fs::Watcher,
|
fs::Watcher,
|
||||||
handler::{Handler, HandlerLock},
|
handler::{Handler, HandlerLock},
|
||||||
|
@ -61,25 +61,23 @@ impl RuntimeConfig {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the shell to use to invoke commands.
|
|
||||||
pub fn command_shell(&mut self, shell: Shell) -> &mut Self {
|
|
||||||
self.action.shell = shell;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggle whether to use process groups or not.
|
/// Toggle whether to use process groups or not.
|
||||||
pub fn command_grouped(&mut self, grouped: bool) -> &mut Self {
|
pub fn command_grouped(&mut self, grouped: bool) -> &mut Self {
|
||||||
self.action.grouped = grouped;
|
self.action.grouped = grouped;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the command to run on action.
|
/// Set a single command to run on action.
|
||||||
pub fn command<I, S>(&mut self, command: I) -> &mut Self
|
///
|
||||||
where
|
/// This is a convenience for `.commands(vec![Command...])`.
|
||||||
I: IntoIterator<Item = S>,
|
pub fn command(&mut self, command: Command) -> &mut Self {
|
||||||
S: AsRef<str>,
|
self.action.commands = vec![command];
|
||||||
{
|
self
|
||||||
self.action.command = command.into_iter().map(|c| c.as_ref().to_owned()).collect();
|
}
|
||||||
|
|
||||||
|
/// Set the commands to run on action.
|
||||||
|
pub fn commands(&mut self, commands: impl Into<Vec<Command>>) -> &mut Self {
|
||||||
|
self.action.commands = commands.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,6 +121,27 @@ pub enum RuntimeError {
|
||||||
#[diagnostic(code(watchexec::runtime::unsupported_signal))]
|
#[diagnostic(code(watchexec::runtime::unsupported_signal))]
|
||||||
UnsupportedSignal(SubSignal),
|
UnsupportedSignal(SubSignal),
|
||||||
|
|
||||||
|
/// Error received when there are no commands to run.
|
||||||
|
///
|
||||||
|
/// This is generally a programmer error and should be caught earlier.
|
||||||
|
#[error("no commands to run")]
|
||||||
|
#[diagnostic(code(watchexec::runtime::no_commands))]
|
||||||
|
NoCommands,
|
||||||
|
|
||||||
|
/// Error received when trying to render a [`Command::Shell`](crate::command::Command) that has no `command`
|
||||||
|
///
|
||||||
|
/// This is generally a programmer error and should be caught earlier.
|
||||||
|
#[error("empty shelled command")]
|
||||||
|
#[diagnostic(code(watchexec::runtime::command_shell::empty_command))]
|
||||||
|
CommandShellEmptyCommand,
|
||||||
|
|
||||||
|
/// Error received when trying to render a [`Shell::Unix`](crate::command::Shell) with an empty shell
|
||||||
|
///
|
||||||
|
/// This is generally a programmer error and should be caught earlier.
|
||||||
|
#[error("empty shell program")]
|
||||||
|
#[diagnostic(code(watchexec::runtime::command_shell::empty_shell))]
|
||||||
|
CommandShellEmptyShell,
|
||||||
|
|
||||||
/// Error received when clearing the screen.
|
/// Error received when clearing the screen.
|
||||||
#[error("clear screen: {0}")]
|
#[error("clear screen: {0}")]
|
||||||
#[diagnostic(code(watchexec::runtime::clearscreen))]
|
#[diagnostic(code(watchexec::runtime::clearscreen))]
|
||||||
|
@ -128,6 +149,7 @@ pub enum RuntimeError {
|
||||||
|
|
||||||
/// Error received from the [`ignore-files`](ignore_files) crate.
|
/// Error received from the [`ignore-files`](ignore_files) crate.
|
||||||
#[error("ignore files: {0}")]
|
#[error("ignore files: {0}")]
|
||||||
|
#[diagnostic(code(watchexec::runtime::ignore_files))]
|
||||||
IgnoreFiles(
|
IgnoreFiles(
|
||||||
#[diagnostic_source]
|
#[diagnostic_source]
|
||||||
#[from]
|
#[from]
|
||||||
|
|
|
@ -9,7 +9,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use async_priority_channel as priority;
|
use async_priority_channel as priority;
|
||||||
use notify::{Watcher as _, poll::PollWatcherConfig};
|
use notify::{poll::PollWatcherConfig, Watcher as _};
|
||||||
use tokio::sync::{mpsc, watch};
|
use tokio::sync::{mpsc, watch};
|
||||||
use tracing::{debug, error, trace, warn};
|
use tracing::{debug, error, trace, warn};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue