diff --git a/Cargo.lock b/Cargo.lock index 025a355..d1636e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1033,9 +1033,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" +checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce" [[package]] name = "libgit2-sys" @@ -2619,6 +2619,7 @@ dependencies = [ "git2", "globset", "ignore", + "libc", "miette", "nom 7.0.0", "notify", diff --git a/lib/Cargo.toml b/lib/Cargo.toml index b460c7f..c53c723 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -58,5 +58,8 @@ features = [ version = "0.1.7" features = ["fs"] +[target.'cfg(unix)'.dependencies] +libc = "0.2.104" + [dev-dependencies] tracing-subscriber = "0.2.19" diff --git a/lib/src/command/supervisor.rs b/lib/src/command/supervisor.rs index 62f6ab7..7e57b60 100644 --- a/lib/src/command/supervisor.rs +++ b/lib/src/command/supervisor.rs @@ -143,7 +143,7 @@ impl Supervisor { let event = Event { tags: vec![ Tag::Source(Source::Internal), - Tag::ProcessCompletion(status), + Tag::ProcessCompletion(status.map(|s| s.into())), ], metadata: Default::default(), }; diff --git a/lib/src/event.rs b/lib/src/event.rs index 9f628de..0e14a0b 100644 --- a/lib/src/event.rs +++ b/lib/src/event.rs @@ -9,13 +9,14 @@ use std::{ collections::HashMap, fmt, + num::{NonZeroI32, NonZeroI64}, path::{Path, PathBuf}, process::ExitStatus, }; use notify::EventKind; -use crate::signal::source::MainSignal; +use crate::signal::{process::SubSignal, source::MainSignal}; /// An event, as far as watchexec cares about. #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -53,8 +54,7 @@ pub enum Tag { Signal(MainSignal), /// The event is about the subprocess exiting. - // TODO: replace ExitStatus with something we can de/serialize. - ProcessCompletion(Option), + ProcessCompletion(Option), } impl Tag { @@ -76,7 +76,6 @@ impl Tag { /// This is a simplification of the [`std::fs::FileType`] type, which is not constructable and may /// differ on different platforms. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[non_exhaustive] pub enum FileType { /// A regular file. File, @@ -116,6 +115,103 @@ impl fmt::Display for FileType { } } +/// The end status of a process. +/// +/// This is a sort-of equivalent of the [`std::process::ExitStatus`] type, which is while +/// constructable, differs on various platforms. The native type is an integer that is interpreted +/// either through convention or via platform-dependent libc or kernel calls; our type is a more +/// structured representation for the purpose of being clearer and transportable. +/// +/// On Unix, one can tell whether a process dumped core from the exit status; this is not replicated +/// in this structure; if that's desirable you can obtain it manually via `libc::WCOREDUMP` and the +/// `ExitSignal` variant. +/// +/// On Unix and Windows, the exit status is a 32-bit integer; on Fuchsia it's a 64-bit integer. For +/// portability, we use `i64`. On all platforms, the "success" value is zero, so we special-case +/// that as a variant and use `NonZeroI*` to niche the other values. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProcessEnd { + /// The process ended successfully, with exit status = 0. + Success, + + /// The process exited with a non-zero exit status. + ExitError(NonZeroI64), + + /// The process exited due to a signal. + ExitSignal(SubSignal), + + /// The process was stopped (but not terminated) (`libc::WIFSTOPPED`). + ExitStop(NonZeroI32), + + /// The process suffered an unhandled exception or warning (typically Windows only). + Exception(NonZeroI32), + + /// The process was continued (`libc::WIFCONTINUED`). + Continued, +} + +impl From for ProcessEnd { + #[cfg(target_os = "fuchsia")] + fn from(es: ExitStatus) -> Self { + // Once https://github.com/rust-lang/rust/pull/88300 (unix_process_wait_more) lands, use + // that API instead of doing the transmute, and clean up the forbid condition at crate root. + let raw: i64 = unsafe { std::mem::transmute(es) }; + NonZeroI64::try_from(raw) + .map(Self::ExitError) + .unwrap_or(Self::Success) + } + + #[cfg(all(unix, not(target_os = "fuchsia")))] + fn from(es: ExitStatus) -> Self { + use std::os::unix::process::ExitStatusExt; + match (es.code(), es.signal()) { + (Some(_), Some(_)) => { + unreachable!("exitstatus cannot both be code and signal?!") + } + (Some(code), None) => match NonZeroI64::try_from(i64::from(code)) { + Ok(code) => Self::ExitError(code), + Err(_) if cfg!(debug_assertions) => { + unreachable!("exitstatus code cannot be zero?!") + } + Err(_) => Self::Success, + }, + // TODO: once unix_process_wait_more lands, use stopped_signal() instead and clear the libc dep + (None, Some(signal)) if libc::WIFSTOPPED(-signal) => { + match NonZeroI32::try_from(libc::WSTOPSIG(-signal)) { + Ok(signal) => Self::ExitStop(signal), + Err(_) if cfg!(debug_assertions) => { + unreachable!("exitsignal code cannot be zero?!") + } + Err(_) => Self::Success, + } + } + // TODO: once unix_process_wait_more lands, use continued() instead and clear the libc dep + #[cfg(not(target_os = "vxworks"))] + (None, Some(signal)) if libc::WIFCONTINUED(-signal) => Self::Continued, + (None, Some(signal)) => Self::ExitSignal(signal.into()), + (None, None) => Self::Success, + } + } + + #[cfg(windows)] + fn from(es: ExitStatus) -> Self { + match es.code().map(NonZeroI32::try_from) { + None | Some(Err(_)) => Self::Success, + Some(Ok(code)) if code & 0x80000000 != 0 => Self::Exception(code), + Some(Ok(code)) => Self::ExitError(code.into()), + } + } + + #[cfg(not(any(unix, windows)))] + fn from(es: ExitStatus) -> Self { + if es.success() { + Self::Success + } else { + Self::ExitError(NonZeroI64::new(1).unwrap()) + } + } +} + /// The general origin of the event. /// /// This is set by the event source. Note that not all of these are currently used. @@ -188,7 +284,7 @@ impl Event { } /// Return all process completions in the event's tags. - pub fn completions(&self) -> impl Iterator> + '_ { + pub fn completions(&self) -> impl Iterator> + '_ { self.tags.iter().filter_map(|p| match p { Tag::ProcessCompletion(s) => Some(*s), _ => None, @@ -212,7 +308,7 @@ impl fmt::Display for Event { Tag::Process(p) => write!(f, " process={}", p)?, Tag::Signal(s) => write!(f, " signal={:?}", s)?, Tag::ProcessCompletion(None) => write!(f, " command-completed")?, - Tag::ProcessCompletion(Some(c)) => write!(f, " command-completed({})", c)?, + Tag::ProcessCompletion(Some(c)) => write!(f, " command-completed({:?})", c)?, } } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index a0af184..e4247fb 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -95,7 +95,8 @@ #![doc(html_favicon_url = "https://watchexec.github.io/logo:watchexec.svg")] #![doc(html_logo_url = "https://watchexec.github.io/logo:watchexec.svg")] #![warn(clippy::unwrap_used, missing_docs)] -#![forbid(unsafe_code)] +#![cfg_attr(not(target_os = "fuchsia"), forbid(unsafe_code))] +// see event::ProcessEnd for why this is disabled on fuchsia // the toolkit to make your own pub mod action;