watchexec/crates/lib/src/command/process.rs

149 lines
4.1 KiB
Rust

use std::process::ExitStatus;
use command_group::AsyncGroupChild;
use tokio::process::Child;
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 {
Self::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: command_group::Signal) -> Result<(), RuntimeError> {
use command_group::UnixChildExt;
match self {
Self::None | Self::Done(_) => Ok(()),
Self::Grouped(c) => {
debug!(signal=%sig, pgid=?c.id(), "sending signal to process group");
c.signal(sig)
}
Self::Ungrouped(c) => {
debug!(signal=%sig, pid=?c.id(), "sending signal to process");
c.signal(sig)
}
}
.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(()),
Self::Grouped(c) => {
debug!(pgid=?c.id(), "killing process group");
c.kill()
}
Self::Ungrouped(c) => {
debug!(pid=?c.id(), "killing process");
c.kill().await
}
}
.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),
Self::Grouped(c) => c.try_wait().map(|status| {
trace!("try-waiting on process group");
if let Some(status) = status {
trace!(?status, "converting to ::Done");
*self = Self::Done(status);
true
} else {
false
}
}),
Self::Ungrouped(c) => c.try_wait().map(|status| {
trace!("try-waiting on process");
if let Some(status) = status {
trace!(?status, "converting to ::Done");
*self = Self::Done(status);
true
} else {
false
}
}),
}
.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),
Self::Done(status) => Ok(Some(*status)),
Self::Grouped(c) => {
trace!("waiting on process group");
let status = c.wait().await.map_err(|err| RuntimeError::IoError {
about: "waiting on process group",
err,
})?;
trace!(?status, "converting to ::Done");
*self = Self::Done(status);
Ok(Some(status))
}
Self::Ungrouped(c) => {
trace!("waiting on process");
let status = c.wait().await.map_err(|err| RuntimeError::IoError {
about: "waiting on process (ungrouped)",
err,
})?;
trace!(?status, "converting to ::Done");
*self = Self::Done(status);
Ok(Some(status))
}
}
.map_err(RuntimeError::Process)
}
}