//! Synthetic event type, derived from inputs, triggers actions. //! //! Fundamentally, events in watchexec have three purposes: //! //! 1. To trigger the launch, restart, or other interruption of a process; //! 2. To be filtered upon according to whatever set of criteria is desired; //! 3. To carry information about what caused the event, which may be provided to the process. use std::{ collections::HashMap, fmt, num::{NonZeroI32, NonZeroI64}, path::{Path, PathBuf}, process::ExitStatus, }; use filekind::FileEventKind; use crate::keyboard::Keyboard; use crate::signal::{process::SubSignal, source::MainSignal}; /// Re-export of the Notify file event types. pub mod filekind { pub use notify::event::{ AccessKind, AccessMode, CreateKind, DataChange, EventKind as FileEventKind, MetadataKind, ModifyKind, RemoveKind, RenameMode, }; } /// An event, as far as watchexec cares about. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Event { /// Structured, classified information which can be used to filter or classify the event. pub tags: Vec, /// Arbitrary other information, cannot be used for filtering. pub metadata: HashMap>, } /// Something which can be used to filter or qualify an event. #[derive(Clone, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum Tag { /// The event is about a path or file in the filesystem. Path { /// Path to the file or directory. path: PathBuf, /// Optional file type, if known. file_type: Option, }, /// Kind of a filesystem event (create, remove, modify, etc). FileEventKind(FileEventKind), /// The general source of the event. Source(Source), /// The event was caused by specific keyboard input Keyboard(Keyboard), /// The event was caused by a particular process. Process(u32), /// The event is about a signal being delivered to the main process. Signal(MainSignal), /// The event is about the subprocess ending. ProcessCompletion(Option), } impl Tag { /// The name of the variant. #[must_use] pub const fn discriminant_name(&self) -> &'static str { match self { Self::Path { .. } => "Path", Self::FileEventKind(_) => "FileEventKind", Self::Source(_) => "Source", Self::Keyboard(_) => "Keyboard", Self::Process(_) => "Process", Self::Signal(_) => "Signal", Self::ProcessCompletion(_) => "ProcessCompletion", } } } /// The type of a file. /// /// 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)] pub enum FileType { /// A regular file. File, /// A directory. Dir, /// A symbolic link. Symlink, /// Something else. Other, } impl From for FileType { fn from(ft: std::fs::FileType) -> Self { if ft.is_file() { Self::File } else if ft.is_dir() { Self::Dir } else if ft.is_symlink() { Self::Symlink } else { Self::Other } } } impl fmt::Display for FileType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::File => write!(f, "file"), Self::Dir => write!(f, "dir"), Self::Symlink => write!(f, "symlink"), Self::Other => write!(f, "other"), } } } /// 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(unix)] fn from(es: ExitStatus) -> Self { use std::os::unix::process::ExitStatusExt; match (es.code(), es.signal(), es.stopped_signal()) { (Some(_), Some(_), _) => { unreachable!("exitstatus cannot both be code and signal?!") } (Some(code), None, _) => { NonZeroI64::try_from(i64::from(code)).map_or(Self::Success, Self::ExitError) } (None, Some(_), Some(stopsig)) => { NonZeroI32::try_from(stopsig).map_or(Self::Success, Self::ExitStop) } #[cfg(not(target_os = "vxworks"))] (None, Some(_), _) if es.continued() => 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.get() < 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. #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum Source { /// Event comes from a file change. Filesystem, /// Event comes from a keyboard input. Keyboard, /// Event comes from a mouse click. Mouse, /// Event comes from the OS. Os, /// Event is time based. Time, /// Event is internal to Watchexec. Internal, } impl fmt::Display for Source { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", match self { Self::Filesystem => "filesystem", Self::Keyboard => "keyboard", Self::Mouse => "mouse", Self::Os => "os", Self::Time => "time", Self::Internal => "internal", } ) } } /// The priority of the event in the queue. /// /// In the event queue, events are inserted with a priority, such that more important events are /// delivered ahead of others. This is especially important when there is a large amount of events /// generated and relatively slow filtering, as events can become noticeably delayed, and may give /// the impression of stalling. #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] pub enum Priority { /// Low priority /// /// Used for: /// - process completion events Low, /// Normal priority /// /// Used for: /// - filesystem events Normal, /// High priority /// /// Used for: /// - signals to main process, except Interrupt and Terminate High, /// Urgent events bypass filtering entirely. /// /// Used for: /// - Interrupt and Terminate signals to main process Urgent, } impl Default for Priority { fn default() -> Self { Self::Normal } } impl Event { /// Returns true if the event has an Internal source tag. #[must_use] pub fn is_internal(&self) -> bool { self.tags .iter() .any(|tag| matches!(tag, Tag::Source(Source::Internal))) } /// Returns true if the event has no tags. #[must_use] pub fn is_empty(&self) -> bool { self.tags.is_empty() } /// Return all paths in the event's tags. pub fn paths(&self) -> impl Iterator)> { self.tags.iter().filter_map(|p| match p { Tag::Path { path, file_type } => Some((path.as_path(), file_type.as_ref())), _ => None, }) } /// Return all signals in the event's tags. pub fn signals(&self) -> impl Iterator + '_ { self.tags.iter().filter_map(|p| match p { Tag::Signal(s) => Some(*s), _ => None, }) } /// Return all process completions in the event's tags. pub fn completions(&self) -> impl Iterator> + '_ { self.tags.iter().filter_map(|p| match p { Tag::ProcessCompletion(s) => Some(*s), _ => None, }) } } impl fmt::Display for Event { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Event")?; for p in &self.tags { match p { Tag::Path { path, file_type } => { write!(f, " path={}", path.display())?; if let Some(ft) = file_type { write!(f, " filetype={ft}")?; } } Tag::FileEventKind(kind) => write!(f, " kind={kind:?}")?, Tag::Source(s) => write!(f, " source={s:?}")?, Tag::Keyboard(k) => write!(f, " keyboard={k:?}")?, 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:?})")?, } } if !self.metadata.is_empty() { write!(f, " meta: {:?}", self.metadata)?; } Ok(()) } }