Docs: command

This commit is contained in:
Félix Saparelli 2021-10-17 02:24:36 +13:00
parent fcf6a2154a
commit 17b83fda08
No known key found for this signature in database
GPG Key ID: B948C4BAE44FC474
4 changed files with 105 additions and 18 deletions

View File

@ -235,16 +235,7 @@ async fn apply_outcome(
}
(Some(p), Outcome::Signal(sig)) => {
#[cfg(unix)]
if let Some(sig) = sig.to_nix() {
p.signal(sig).await;
}
#[cfg(windows)]
if let SubSignal::Terminate = sig {
p.kill().await;
}
// else: https://github.com/watchexec/watchexec/issues/219
p.signal(sig).await;
}
(Some(p), Outcome::Wait) => {

View File

@ -6,21 +6,33 @@ use tracing::{debug, trace};
use crate::error::RuntimeError;
/// Low-level wrapper around a process child, be it grouped or ungrouped.
#[derive(Debug)]
pub enum Process {
/// The initial state of the process, before it's spawned.
None,
/// A grouped process that's been spawned.
Grouped(AsyncGroupChild),
/// An ungrouped process that's been spawned.
Ungrouped(Child),
/// The cached exit status of the process.
Done(ExitStatus),
}
impl Default for Process {
/// Returns [`Process::None`].
fn default() -> Self {
Process::None
}
}
impl Process {
/// Sends a Unix signal to the process.
///
/// Does nothing if the process is not running.
#[cfg(unix)]
pub fn signal(&mut self, sig: Signal) -> Result<(), RuntimeError> {
use command_group::UnixChildExt;
@ -39,6 +51,12 @@ impl Process {
.map_err(RuntimeError::Process)
}
/// Kills the process.
///
/// Does nothing if the process is not running.
///
/// Note that this has different behaviour for grouped and ungrouped processes due to Tokio's
/// API: it waits on ungrouped processes, but not for grouped processes.
pub async fn kill(&mut self) -> Result<(), RuntimeError> {
match self {
Self::None | Self::Done(_) => Ok(()),
@ -54,6 +72,15 @@ impl Process {
.map_err(RuntimeError::Process)
}
/// Checks the status of the process.
///
/// Returns `true` if the process is still running.
///
/// This takes `&mut self` as it transitions the [`Process`] state to [`Process::Done`] if it
/// finds the process has ended, such that it will cache the exit status. Otherwise that status
/// would be lost.
///
/// Does nothing and returns `false` immediately if the `Process` is `Done` or `None`.
pub fn is_running(&mut self) -> Result<bool, RuntimeError> {
match self {
Self::None | Self::Done(_) => Ok(false),
@ -81,6 +108,16 @@ impl Process {
.map_err(RuntimeError::Process)
}
/// Waits for the process to exit, and returns its exit status.
///
/// This takes `&mut self` as it transitions the [`Process`] state to [`Process::Done`] if it
/// finds the process has ended, such that it will cache the exit status.
///
/// This makes it possible to call `wait` on a process multiple times, without losing the exit
/// status.
///
/// Returns immediately with the cached exit status if the `Process` is `Done`, and with `None`
/// if the `Process` is `None`.
pub async fn wait(&mut self) -> Result<Option<ExitStatus>, RuntimeError> {
match self {
Self::None => Ok(None),

View File

@ -3,7 +3,7 @@ use std::sync::{
Arc,
};
use command_group::{AsyncCommandGroup, Signal};
use command_group::AsyncCommandGroup;
use tokio::{
process::Command,
select, spawn,
@ -18,6 +18,7 @@ use tracing::{debug, error, trace};
use crate::{
error::RuntimeError,
event::{Event, Source, Tag},
signal::process::SubSignal,
};
use super::Process;
@ -25,10 +26,14 @@ use super::Process;
#[derive(Clone, Copy, Debug)]
enum Intervention {
Kill,
#[cfg(unix)]
Signal(Signal),
Signal(SubSignal),
}
/// A task which supervises a process.
///
/// This spawns a process from a [`Command`] and waits for it to complete while handling
/// interventions to it: orders to terminate it, or to send a signal to it. It also immediately
/// issues a [`Tag::ProcessCompletion`] event when the process completes.
#[derive(Debug)]
pub struct Supervisor {
id: u32,
@ -43,6 +48,7 @@ pub struct Supervisor {
}
impl Supervisor {
/// Spawns the command, the supervision task, and returns a new control object.
pub fn spawn(
errors: Sender<RuntimeError>,
events: Sender<Event>,
@ -100,12 +106,27 @@ impl Supervisor {
}
#[cfg(unix)]
Intervention::Signal(sig) => {
if let Err(err) = process.signal(sig) {
if let Some(sig) = sig.to_nix() {
if let Err(err) = process.signal(sig) {
error!(%err, "while sending signal to process");
errors.send(err).await.ok();
trace!("continuing to watch command");
}
} else {
let err = RuntimeError::UnsupportedSignal(sig);
error!(%err, "while sending signal to process");
errors.send(err).await.ok();
trace!("continuing to watch command");
}
}
#[cfg(windows)]
Intervention::Signal(sig) => {
// https://github.com/watchexec/watchexec/issues/219
let err = RuntimeError::UnsupportedSignal(sig);
error!(%err, "while sending signal to process");
errors.send(err).await.ok();
trace!("continuing to watch command");
}
}
}
else => break,
@ -156,29 +177,58 @@ impl Supervisor {
})
}
/// 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
}
#[cfg(unix)]
pub async fn signal(&self, signal: Signal) {
trace!(?signal, "sending signal intervention");
self.intervene.send(Intervention::Signal(signal)).await.ok();
/// Issues a signal to the process.
///
/// On Windows, this currently only supports [`SubSignal::ForceStop`].
///
/// While this is async, it returns once the signal intervention has been sent internally, not
/// when the signal has been delivered.
pub async fn signal(&self, signal: SubSignal) {
if cfg!(windows) {
if let SubSignal::ForceStop = signal {
self.intervene.send(Intervention::Kill).await.ok();
}
// else: https://github.com/watchexec/watchexec/issues/219
} else {
trace!(?signal, "sending signal intervention");
self.intervene.send(Intervention::Signal(signal)).await.ok();
}
// only errors on channel closed, and that only happens if the process is dead
}
/// Stops the process.
///
/// While this is async, it returns once the signal intervention has been sent internally, not
/// when the signal has been delivered.
pub async fn kill(&self) {
trace!("sending kill intervention");
self.intervene.send(Intervention::Kill).await.ok();
// only errors on channel closed, and that only happens if the process is dead
}
/// Returns true if the supervisor is still running.
///
/// This is almost always equivalent to whether the _process_ is still running, but may not be
/// 100% in sync.
pub fn is_running(&self) -> bool {
let ongoing = self.ongoing.load(Ordering::SeqCst);
trace!(?ongoing, "supervisor state");
ongoing
}
/// Returns only when the supervisor completes.
///
/// This is almost always equivalent to waiting for the _process_ to complete, but may not be
/// 100% in sync.
pub async fn wait(&mut self) -> Result<(), RuntimeError> {
if !self.ongoing.load(Ordering::SeqCst) {
trace!("supervisor already completed");

View File

@ -13,6 +13,7 @@ use crate::{
action,
event::Event,
fs::{self, Watcher},
signal::process::SubSignal,
};
/// Errors which are not recoverable and stop watchexec execution.
@ -169,6 +170,14 @@ pub enum RuntimeError {
#[diagnostic(code(watchexec::runtime::process_doa))]
ProcessDeadOnArrival,
/// Error received when a [`SubSignal`] is unsupported
///
/// This may happen if the signal is not supported on the current platform, or if Watchexec
/// doesn't support sending the signal.
#[error("unsupported signal: {0:?}")]
#[diagnostic(code(watchexec::runtime::unsupported_signal))]
UnsupportedSignal(SubSignal),
/// Error received when clearing the screen.
#[error("clear screen: {0}")]
#[diagnostic(code(watchexec::runtime::clearscreen))]