--emit-events-to (#515)

This commit is contained in:
Félix Saparelli 2023-03-18 21:32:24 +13:00 committed by GitHub
parent 64ad4e31f5
commit 8156864bf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 2841 additions and 883 deletions

View File

@ -16,6 +16,14 @@ updates:
directory: "/crates/lib"
schedule:
interval: "daily"
- package-ecosystem: "cargo"
directory: "/crates/events"
schedule:
interval: "daily"
- package-ecosystem: "cargo"
directory: "/crates/signals"
schedule:
interval: "daily"
- package-ecosystem: "cargo"
directory: "/crates/filterer/ignore"
schedule:

View File

@ -9,12 +9,14 @@ on:
options:
- cli
- lib
- bosion
- events
- ignore-files
- project-origins
- signals
- filterer/globset
- filterer/ignore
- filterer/tagged
- bosion
- ignore-files
- project-origins
version:
description: Version to release
required: true

595
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,8 @@ resolver = "2"
members = [
"crates/lib",
"crates/cli",
"crates/events",
"crates/signals",
"crates/filterer/globset",
"crates/filterer/ignore",
"crates/filterer/tagged",

View File

@ -65,8 +65,7 @@ pub fn gather_to(filename: &str, structname: &str, public: bool) {
.collect::<Vec<_>>()
.join(" ");
let crate_feature_list = crate_features
.join(",");
let crate_feature_list = crate_features.join(",");
let viz = if public { "pub" } else { "pub(crate)" };

View File

@ -23,18 +23,25 @@ path = "src/main.rs"
argfile = "0.1.5"
chrono = "0.4.23"
clap_complete = "4.1.4"
clap_complete_nushell = "0.1.10"
clap_mangen = "0.2.9"
command-group = { version = "2.1.0", features = ["with-tokio"] }
console-subscriber = { version = "0.1.0", optional = true }
dirs = "4.0.0"
futures = "0.3.17"
humantime = "2.1.0"
is-terminal = "0.4.4"
miette = { version = "5.3.0", features = ["fancy"] }
notify-rust = "4.5.2"
serde_json = "1.0.94"
tempfile = "3.4.0"
tracing = "0.1.26"
which = "4.4.0"
clap_complete_nushell = "0.1.10"
[dependencies.command-group]
version = "2.1.0"
features = ["with-tokio"]
[dependencies.console-subscriber]
version = "0.1.0"
optional = true
[dependencies.clap]
version = "4.1.8"
@ -44,6 +51,10 @@ features = ["cargo", "derive", "env", "wrap_help"]
version = "1.1.0"
path = "../ignore-files"
[dependencies.miette]
version = "5.3.0"
features = ["fancy"]
[dependencies.project-origins]
version = "1.2.0"
path = "../project-origins"
@ -52,6 +63,15 @@ path = "../project-origins"
version = "2.1.1"
path = "../lib"
[dependencies.watchexec-events]
version = "1.0.0"
path = "../events"
features = ["serde"]
[dependencies.watchexec-signals]
version = "1.0.0"
path = "../signals"
[dependencies.watchexec-filterer-globset]
version = "1.1.0"
path = "../filterer/globset"

View File

@ -1,7 +1,8 @@
use std::{path::PathBuf, str::FromStr, time::Duration};
use clap::{ArgAction, Parser, ValueEnum, ValueHint};
use watchexec::{paths::PATH_SEPARATOR, signal::process::SubSignal};
use watchexec::paths::PATH_SEPARATOR;
use watchexec_signals::Signal;
const OPTSET_FILTERING: &str = "Filtering";
const OPTSET_COMMAND: &str = "Command";
@ -179,7 +180,7 @@ pub struct Args {
conflicts_with_all = ["restart", "watch_when_idle"],
value_name = "SIGNAL"
)]
pub signal: Option<SubSignal>,
pub signal: Option<Signal>,
/// Hidden legacy shorthand for '--signal=kill'.
#[arg(short, long, hide = true)]
@ -202,7 +203,7 @@ pub struct Args {
/// events. For portability the unix signals "SIGKILL", "SIGINT", "SIGTERM", and "SIGHUP" are
/// respectively mapped to these.
#[arg(long, value_name = "SIGNAL")]
pub stop_signal: Option<SubSignal>,
pub stop_signal: Option<Signal>,
/// Time to wait for the command to exit gracefully
///
@ -274,12 +275,12 @@ pub struct Args {
///
/// Supported project ignore files:
///
/// - Git: .gitignore at project root and child directories, .git/info/exclude, and the file pointed to by `core.excludesFile` in .git/config.
/// - Mercurial: .hgignore at project root and child directories.
/// - Bazaar: .bzrignore at project root.
/// - Darcs: _darcs/prefs/boring
/// - Fossil: .fossil-settings/ignore-glob
/// - Ripgrep/Watchexec/generic: .ignore at project root and child directories.
/// - Git: .gitignore at project root and child directories, .git/info/exclude, and the file pointed to by `core.excludesFile` in .git/config.
/// - Mercurial: .hgignore at project root and child directories.
/// - Bazaar: .bzrignore at project root.
/// - Darcs: _darcs/prefs/boring
/// - Fossil: .fossil-settings/ignore-glob
/// - Ripgrep/Watchexec/generic: .ignore at project root and child directories.
///
/// VCS ignore files (Git, Mercurial, Bazaar, Darcs, Fossil) are only used if the corresponding
/// VCS is discovered to be in use for the project/origin. For example, a .bzrignore in a Git
@ -303,10 +304,10 @@ pub struct Args {
///
/// Supported global ignore files
///
/// - Git (if core.excludesFile is set): the file at that path
/// - Git (otherwise): the first found of $XDG_CONFIG_HOME/git/ignore, %APPDATA%/.gitignore, %USERPROFILE%/.gitignore, $HOME/.config/git/ignore, $HOME/.gitignore.
/// - Bazaar: the first found of %APPDATA%/Bazzar/2.0/ignore, $HOME/.bazaar/ignore.
/// - Watchexec: the first found of $XDG_CONFIG_HOME/watchexec/ignore, %APPDATA%/watchexec/ignore, %USERPROFILE%/.watchexec/ignore, $HOME/.watchexec/ignore.
/// - Git (if core.excludesFile is set): the file at that path
/// - Git (otherwise): the first found of $XDG_CONFIG_HOME/git/ignore, %APPDATA%/.gitignore, %USERPROFILE%/.gitignore, $HOME/.config/git/ignore, $HOME/.gitignore.
/// - Bazaar: the first found of %APPDATA%/Bazzar/2.0/ignore, $HOME/.bazaar/ignore.
/// - Watchexec: the first found of $XDG_CONFIG_HOME/watchexec/ignore, %APPDATA%/watchexec/ignore, %USERPROFILE%/.watchexec/ignore, $HOME/.watchexec/ignore.
///
/// Like for project files, Git and Bazaar global files will only be used for the corresponding
/// VCS as used in the project.
@ -453,7 +454,7 @@ pub struct Args {
/// Watchexec emits event information when running a command, which can be used by the command
/// to target specific changed files.
///
/// One thing to take care of is assuming inherent behaviour where there is only chance.
/// One thing to take care with is assuming inherent behaviour where there is only chance.
/// Notably, it could appear as if the `RENAMED` variable contains both the original and the new
/// path being renamed. In previous versions, it would even appear on some platforms as if the
/// original always came before the new. However, none of this was true. It's impossible to
@ -468,12 +469,12 @@ pub struct Args {
/// $WATCHEXEC_COMMON_PATH is set to the longest common path of all of the below variables,
/// and so should be prepended to each path to obtain the full/real path. Then:
///
/// - $WATCHEXEC_CREATED_PATH is set when files/folders were created
/// - $WATCHEXEC_REMOVED_PATH is set when files/folders were removed
/// - $WATCHEXEC_RENAMED_PATH is set when files/folders were renamed
/// - $WATCHEXEC_WRITTEN_PATH is set when files/folders were modified
/// - $WATCHEXEC_META_CHANGED_PATH is set when files/folders' metadata were modified
/// - $WATCHEXEC_OTHERWISE_CHANGED_PATH is set for every other kind of pathed event
/// - $WATCHEXEC_CREATED_PATH is set when files/folders were created
/// - $WATCHEXEC_REMOVED_PATH is set when files/folders were removed
/// - $WATCHEXEC_RENAMED_PATH is set when files/folders were renamed
/// - $WATCHEXEC_WRITTEN_PATH is set when files/folders were modified
/// - $WATCHEXEC_META_CHANGED_PATH is set when files/folders' metadata were modified
/// - $WATCHEXEC_OTHERWISE_CHANGED_PATH is set for every other kind of pathed event
///
/// Multiple paths are separated by the system path separator, ';' on Windows and ':' on unix.
/// Within each variable, paths are deduplicated and sorted in binary order (i.e. neither
@ -493,51 +494,51 @@ pub struct Args {
/// set of events Watchexec handles. Here's an example of a folder being created on Linux:
///
/// ```json
/// {
/// "tags": [
/// {
/// "kind": "path",
/// "absolute": "/home/user/your/new-folder",
/// "filetype": "dir"
/// },
/// {
/// "kind": "fs",
/// "simple": "create",
/// "full": "Create(Folder)"
/// },
/// {
/// "kind": "source",
/// "source": "filesystem",
/// {
/// "tags": [
/// {
/// "kind": "path",
/// "absolute": "/home/user/your/new-folder",
/// "filetype": "dir"
/// },
/// {
/// "kind": "fs",
/// "simple": "create",
/// "full": "Create(Folder)"
/// },
/// {
/// "kind": "source",
/// "source": "filesystem",
/// }
/// ],
/// "metadata": {
/// "notify-backend": "inotify"
/// }
/// ],
/// "metadata": {
/// "notify-backend": "inotify"
/// }
/// }
/// ```
///
/// The fields are as follows:
///
/// - `tags`, structured event data.
/// - `tags[].kind`, which can be:
/// * 'path', along with:
/// + `absolute`, an absolute path.
/// + `filetype`, a file type if known ('dir', 'file', 'symlink', 'other').
/// * 'fs':
/// + `simple`, the "simple" event type ('access', 'create', 'modify', 'remove', or 'other').
/// + `full`, the "full" event type, which is too complex to fully describe here, but looks like 'General(Precise(Specific))'.
/// * 'source', along with:
/// + `source`, the source of the event ('filesystem', 'keyboard', 'mouse', 'os', 'time', 'internal').
/// * 'keyboard', along with:
/// + `keycode`. Currently only the value 'eof' is supported.
/// * 'process', for events caused by processes:
/// + `pid`, the process ID.
/// * 'signal', for signals sent to Watchexec:
/// + `name`, the normalised signal name ('hangup', 'interrupt', 'quit', 'terminate', 'user1', 'user2').
/// * 'completion', for when a command ends:
/// + `disposition`, the exit disposition ('success', 'error', 'signal', 'stop', 'exception', 'continued').
/// + `code`, the exit, stop, or exception code.
/// + `signal`, the signal name or number if the exit was caused by a signal.
/// - `tags`, structured event data.
/// - `tags[].kind`, which can be:
/// * 'path', along with:
/// + `absolute`, an absolute path.
/// + `filetype`, a file type if known ('dir', 'file', 'symlink', 'other').
/// * 'fs':
/// + `simple`, the "simple" event type ('access', 'create', 'modify', 'remove', or 'other').
/// + `full`, the "full" event type, which is too complex to fully describe here, but looks like 'General(Precise(Specific))'.
/// * 'source', along with:
/// + `source`, the source of the event ('filesystem', 'keyboard', 'mouse', 'os', 'time', 'internal').
/// * 'keyboard', along with:
/// + `keycode`. Currently only the value 'eof' is supported.
/// * 'process', for events caused by processes:
/// + `pid`, the process ID.
/// * 'signal', for signals sent to Watchexec:
/// + `signal`, the normalised signal name ('hangup', 'interrupt', 'quit', 'terminate', 'user1', 'user2').
/// * 'completion', for when a command ends:
/// + `disposition`, the exit disposition ('success', 'error', 'signal', 'stop', 'exception', 'continued').
/// + `code`, the exit, signal, stop, or exception code.
/// - `metadata`, additional information about the event.
///
/// The 'json-stdin' mode will emit JSON events to the standard input of the command, one per
/// line, then close stdin. The 'json-file' mode will create a temporary file, write the
@ -546,7 +547,6 @@ pub struct Args {
///
/// Finally, the special 'none' mode will disable event emission entirely.
#[arg(
hide = true, // until the feature is done
long,
help_heading = OPTSET_COMMAND,
verbatim_doc_comment,
@ -888,7 +888,7 @@ pub fn get_args() -> Args {
let mut args = Args::parse_from(args);
if args.kill {
args.signal = Some(SubSignal::ForceStop);
args.signal = Some(Signal::ForceStop);
}
if args.signal.is_some() {

View File

@ -1,24 +1,26 @@
use std::{collections::HashMap, convert::Infallible, env::current_dir, ffi::OsString};
use std::{
collections::HashMap, convert::Infallible, env::current_dir, ffi::OsString, fs::File,
process::Stdio,
};
use miette::{miette, IntoDiagnostic, Result};
use notify_rust::Notification;
use tracing::{debug, debug_span};
use tracing::{debug, debug_span, error};
use watchexec::{
action::{Action, Outcome, PostSpawn, PreSpawn},
command::{Command, Shell},
config::RuntimeConfig,
error::RuntimeError,
event::{Event, ProcessEnd, Tag},
fs::Watcher,
handler::SyncFnHandler,
keyboard::Keyboard,
paths::summarise_events_to_env,
signal::{process::SubSignal, source::MainSignal},
};
use watchexec_events::{Event, Keyboard, ProcessEnd, Tag};
use watchexec_signals::Signal;
use crate::args::{Args, ClearMode, EmitEvents, OnBusyUpdate};
use crate::state::State;
pub fn runtime(args: &Args) -> Result<RuntimeConfig> {
pub fn runtime(args: &Args, state: &State) -> Result<RuntimeConfig> {
let _span = debug_span!("args-runtime").entered();
let mut config = RuntimeConfig::default();
@ -71,15 +73,15 @@ pub fn runtime(args: &Args) -> Result<RuntimeConfig> {
return fut;
}
let signals: Vec<MainSignal> = action.events.iter().flat_map(Event::signals).collect();
let signals: Vec<Signal> = action.events.iter().flat_map(Event::signals).collect();
let has_paths = action.events.iter().flat_map(Event::paths).next().is_some();
if signals.contains(&MainSignal::Terminate) {
if signals.contains(&Signal::Terminate) {
action.outcome(Outcome::both(Outcome::Stop, Outcome::Exit));
return fut;
}
if signals.contains(&MainSignal::Interrupt) {
if signals.contains(&Signal::Interrupt) {
action.outcome(Outcome::both(Outcome::Stop, Outcome::Exit));
return fut;
}
@ -98,7 +100,7 @@ pub fn runtime(args: &Args) -> Result<RuntimeConfig> {
if !signals.is_empty() {
let mut out = Outcome::DoNothing;
for sig in signals {
out = Outcome::both(out, Outcome::Signal(sig.into()));
out = Outcome::both(out, Outcome::Signal(sig));
}
action.outcome(out);
@ -169,13 +171,13 @@ pub fn runtime(args: &Args) -> Result<RuntimeConfig> {
let when_running = match on_busy {
OnBusyUpdate::Restart => Outcome::both(
Outcome::both(
Outcome::Signal(stop_signal.unwrap_or(SubSignal::Terminate)),
Outcome::Signal(stop_signal.unwrap_or(Signal::Terminate)),
Outcome::both(Outcome::Sleep(stop_timeout), Outcome::Stop),
),
start,
),
OnBusyUpdate::Signal => {
Outcome::Signal(stop_signal.or(signal).unwrap_or(SubSignal::Terminate))
Outcome::Signal(stop_signal.or(signal).unwrap_or(Signal::Terminate))
}
OnBusyUpdate::Queue => Outcome::wait(start),
OnBusyUpdate::DoNothing => Outcome::DoNothing,
@ -187,7 +189,7 @@ pub fn runtime(args: &Args) -> Result<RuntimeConfig> {
});
let mut add_envs = HashMap::new();
// TODO: move to args and use osstrings
// TODO: move to args?
for pair in &args.env {
if let Some((k, v)) = pair.split_once('=') {
add_envs.insert(k.to_owned(), OsString::from(v));
@ -203,27 +205,59 @@ pub fn runtime(args: &Args) -> Result<RuntimeConfig> {
let workdir = args.workdir.clone();
let emit_events_to = args.emit_events_to;
let emit_file = state.emit_file.clone();
config.on_pre_spawn(move |prespawn: PreSpawn| {
use crate::emits::*;
let workdir = workdir.clone();
let mut add_envs = add_envs.clone();
let mut stdin = None;
match emit_events_to {
EmitEvents::Environment => {
add_envs.extend(
summarise_events_to_env(prespawn.events.iter())
.into_iter()
.map(|(k, v)| (format!("WATCHEXEC_{k}_PATH"), v)),
);
add_envs.extend(emits_to_environment(&prespawn.events));
}
EmitEvents::Stdin => todo!(),
EmitEvents::File => todo!(),
EmitEvents::JsonStdin => todo!(),
EmitEvents::JsonFile => todo!(),
EmitEvents::Stdin => match emits_to_file(&emit_file, &prespawn.events)
.and_then(|path| File::open(path).into_diagnostic())
{
Ok(file) => {
stdin.replace(Stdio::from(file));
}
Err(err) => {
error!("Failed to write events to stdin, continuing without it: {err}");
}
},
EmitEvents::File => match emits_to_file(&emit_file, &prespawn.events) {
Ok(path) => {
add_envs.insert("WATCHEXEC_EVENTS_FILE".into(), path.into());
}
Err(err) => {
error!("Failed to write WATCHEXEC_EVENTS_FILE, continuing without it: {err}");
}
},
EmitEvents::JsonStdin => match emits_to_json_file(&emit_file, &prespawn.events)
.and_then(|path| File::open(path).into_diagnostic())
{
Ok(file) => {
stdin.replace(Stdio::from(file));
}
Err(err) => {
error!("Failed to write events to stdin, continuing without it: {err}");
}
},
EmitEvents::JsonFile => match emits_to_json_file(&emit_file, &prespawn.events) {
Ok(path) => {
add_envs.insert("WATCHEXEC_EVENTS_FILE".into(), path.into());
}
Err(err) => {
error!("Failed to write WATCHEXEC_EVENTS_FILE, continuing without it: {err}");
}
},
EmitEvents::None => {}
}
async move {
if !add_envs.is_empty() || workdir.is_some() {
if !add_envs.is_empty() || workdir.is_some() || stdin.is_some() {
if let Some(mut command) = prespawn.command().await {
for (k, v) in add_envs {
debug!(?k, ?v, "inserting environment variable");
@ -234,6 +268,11 @@ pub fn runtime(args: &Args) -> Result<RuntimeConfig> {
debug!(?workdir, "set command workdir");
command.current_dir(workdir);
}
if let Some(stdin) = stdin {
debug!("set command stdin");
command.stdin(stdin);
}
}
}

71
crates/cli/src/emits.rs Normal file
View File

@ -0,0 +1,71 @@
use std::{ffi::OsString, fmt::Write, path::PathBuf};
use miette::{IntoDiagnostic, Result};
use watchexec::paths::summarise_events_to_env;
use watchexec_events::{filekind::FileEventKind, Event, Tag};
use crate::state::RotatingTempFile;
pub fn emits_to_environment(events: &[Event]) -> impl Iterator<Item = (String, OsString)> {
summarise_events_to_env(events.iter())
.into_iter()
.map(|(k, v)| (format!("WATCHEXEC_{k}_PATH"), v))
}
fn events_to_simple_format(events: &[Event]) -> Result<String> {
let mut buf = String::new();
for event in events {
let feks = event
.tags
.iter()
.filter_map(|tag| match tag {
Tag::FileEventKind(kind) => Some(kind),
_ => None,
})
.collect::<Vec<_>>();
for path in event.paths().map(|(p, _)| p) {
if feks.is_empty() {
writeln!(&mut buf, "other:{}", path.to_string_lossy()).into_diagnostic()?;
continue;
}
for fek in &feks {
writeln!(
&mut buf,
"{}:{}",
match fek {
FileEventKind::Any | FileEventKind::Other => "other",
FileEventKind::Access(_) => "access",
FileEventKind::Create(_) => "create",
FileEventKind::Modify(_) => "modify",
FileEventKind::Remove(_) => "remove",
},
path.to_string_lossy()
)
.into_diagnostic()?;
}
}
}
Ok(buf)
}
pub fn emits_to_file(target: &RotatingTempFile, events: &[Event]) -> Result<PathBuf> {
target.rotate()?;
target.write(events_to_simple_format(events)?.as_bytes())?;
Ok(target.path())
}
pub fn emits_to_json_file(target: &RotatingTempFile, events: &[Event]) -> Result<PathBuf> {
target.rotate()?;
for event in events {
if event.is_empty() {
continue;
}
target.write(&serde_json::to_vec(event).into_diagnostic()?)?;
target.write(b"\n")?;
}
Ok(target.path())
}

View File

@ -19,7 +19,9 @@ use watchexec::{
pub mod args;
mod config;
mod emits;
mod filterer;
mod state;
async fn init() -> Result<Args> {
let mut log_on = false;
@ -104,7 +106,9 @@ async fn run_watchexec(args: Args) -> Result<()> {
info!(version=%env!("CARGO_PKG_VERSION"), "constructing Watchexec from CLI");
let init = config::init(&args);
let mut runtime = config::runtime(&args)?;
let state = state::State::new()?;
let mut runtime = config::runtime(&args, &state)?;
runtime.filterer(filterer::globset(&args).await?);
info!("initialising Watchexec runtime");

47
crates/cli/src/state.rs Normal file
View File

@ -0,0 +1,47 @@
use std::{
io::Write,
path::PathBuf,
sync::{Arc, Mutex},
};
use miette::{IntoDiagnostic, Result};
use tempfile::NamedTempFile;
#[derive(Clone, Debug)]
pub struct State {
pub emit_file: RotatingTempFile,
}
impl State {
pub fn new() -> Result<Self> {
let emit_file = RotatingTempFile::new()?;
Ok(Self { emit_file })
}
}
#[derive(Clone, Debug)]
pub struct RotatingTempFile(Arc<Mutex<NamedTempFile>>);
impl RotatingTempFile {
pub fn new() -> Result<Self> {
let file = Arc::new(Mutex::new(NamedTempFile::new().into_diagnostic()?));
Ok(Self(file))
}
pub fn rotate(&self) -> Result<()> {
let mut file = self.0.lock().unwrap();
*file = NamedTempFile::new().into_diagnostic()?;
// implicitly drops the old file
Ok(())
}
pub fn write(&self, data: &[u8]) -> Result<()> {
let mut file = self.0.lock().unwrap();
file.write_all(data).into_diagnostic()?;
Ok(())
}
pub fn path(&self) -> PathBuf {
self.0.lock().unwrap().path().to_owned()
}
}

View File

@ -0,0 +1,6 @@
# Changelog
## Next (YYYY-MM-DD)
- Split off new `watchexec-events` crate (this one), to have a lightweight library that can parse
and generate events and maintain the JSON event format.

43
crates/events/Cargo.toml Normal file
View File

@ -0,0 +1,43 @@
[package]
name = "watchexec-events"
version = "1.0.0"
authors = ["Félix Saparelli <felix@passcod.name>"]
license = "Apache-2.0 OR MIT"
description = "Watchexec's event types"
keywords = ["watchexec", "event", "format", "json"]
documentation = "https://docs.rs/watchexec-events"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
rust-version = "1.61.0"
edition = "2021"
[dependencies.notify]
version = "5.0.0"
optional = true
[dependencies.serde]
version = "1.0.152"
optional = true
features = ["derive"]
[dependencies.watchexec-signals]
version = "1.0.0"
path = "../signals"
default-features = false
[target.'cfg(unix)'.dependencies.nix]
version = "0.26.2"
features = ["signal"]
[dev-dependencies]
watchexec-events = { version = "*", features = ["serde"], path = "." }
snapbox = "0.4.10"
serde_json = "1.0.94"
[features]
default = ["notify"]
notify = ["dep:notify"]
serde = ["dep:serde", "notify?/serde", "watchexec-signals/serde"]

36
crates/events/README.md Normal file
View File

@ -0,0 +1,36 @@
# watchexec-events
_Watchexec's event types._
- **[API documentation][docs]**.
- Licensed under [Apache 2.0][license] or [MIT](https://passcod.mit-license.org).
- Status: maintained.
[docs]: https://docs.rs/watchexec-events
[license]: ../../LICENSE
This is particularly useful if you're building a tool that runs under Watchexec, and want to easily
read its events (with `--emit-events-to=json-file` and `--emit-events-to=json-stdin`).
```rust ,no_run
use std::io::{stdin, Result};
use watchexec_events::Event;
fn main() -> Result<()> {
for line in stdin().lines() {
let event: Event = serde_json::from_str(&line?)?;
dbg!(event);
}
Ok(())
}
```
## Features
- `serde`: enables serde support.
- `notify`: use Notify's file event types (default).
If you disable `notify`, you'll get a leaner dependency tree that's still able to parse the entire
events, but isn't type compatible with Notify. In most deserialisation usecases, this is fine, but
it's not the default to avoid surprises.

View File

@ -0,0 +1,11 @@
use std::io::{stdin, Result};
use watchexec_events::Event;
fn main() -> Result<()> {
for line in stdin().lines() {
let event: Event = serde_json::from_str(&line?)?;
dbg!(event);
}
Ok(())
}

View File

@ -0,0 +1,10 @@
pre-release-commit-message = "release: events v{{version}}"
tag-prefix = "events-"
tag-message = "watchexec-events {{version}}"
[[pre-release-replacements]]
file = "CHANGELOG.md"
search = "^## Next.*$"
replace = "## Next (YYYY-MM-DD)\n\n## v{{version}} ({{date}})"
prerelease = true
max = 1

View File

@ -1,34 +1,20 @@
//! 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 watchexec_signals::Signal;
use crate::keyboard::Keyboard;
use crate::signal::{process::SubSignal, source::MainSignal};
#[cfg(feature = "serde")]
use crate::serde_formats::{SerdeEvent, SerdeTag};
/// 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,
};
}
use crate::{filekind::FileEventKind, FileType, Keyboard, ProcessEnd};
/// An event, as far as watchexec cares about.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(from = "SerdeEvent", into = "SerdeEvent"))]
pub struct Event {
/// Structured, classified information which can be used to filter or classify the event.
pub tags: Vec<Tag>,
@ -39,6 +25,8 @@ pub struct Event {
/// Something which can be used to filter or qualify an event.
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(from = "SerdeTag", into = "SerdeTag"))]
#[non_exhaustive]
pub enum Tag {
/// The event is about a path or file in the filesystem.
@ -56,17 +44,21 @@ pub enum Tag {
/// The general source of the event.
Source(Source),
/// The event was caused by specific keyboard input
/// The event is about a 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),
Signal(Signal),
/// The event is about the subprocess ending.
ProcessCompletion(Option<ProcessEnd>),
#[cfg(feature = "serde")]
/// The event is unknown (or not yet implemented).
Unknown,
}
impl Tag {
@ -81,126 +73,8 @@ impl Tag {
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<std::fs::FileType> 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<ExitStatus> 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())
#[cfg(feature = "serde")]
Self::Unknown => "Unknown",
}
}
}
@ -209,6 +83,8 @@ impl From<ExitStatus> for ProcessEnd {
///
/// This is set by the event source. Note that not all of these are currently used.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
#[non_exhaustive]
pub enum Source {
/// Event comes from a file change.
@ -254,6 +130,8 @@ impl fmt::Display for Source {
/// 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)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum Priority {
/// Low priority
///
@ -310,7 +188,7 @@ impl Event {
}
/// Return all signals in the event's tags.
pub fn signals(&self) -> impl Iterator<Item = MainSignal> + '_ {
pub fn signals(&self) -> impl Iterator<Item = Signal> + '_ {
self.tags.iter().filter_map(|p| match p {
Tag::Signal(s) => Some(*s),
_ => None,
@ -344,6 +222,8 @@ impl fmt::Display for Event {
Tag::Signal(s) => write!(f, " signal={s:?}")?,
Tag::ProcessCompletion(None) => write!(f, " command-completed")?,
Tag::ProcessCompletion(Some(c)) => write!(f, " command-completed({c:?})")?,
#[cfg(feature = "serde")]
Tag::Unknown => write!(f, " unknown")?,
}
}

65
crates/events/src/fs.rs Normal file
View File

@ -0,0 +1,65 @@
use std::fmt;
/// Re-export of the Notify file event types.
#[cfg(feature = "notify")]
pub mod filekind {
pub use notify::event::{
AccessKind, AccessMode, CreateKind, DataChange, EventKind as FileEventKind, MetadataKind,
ModifyKind, RemoveKind, RenameMode,
};
}
/// Pseudo file event types without dependency on Notify.
#[cfg(not(feature = "notify"))]
pub mod filekind {
pub use crate::sans_notify::{
AccessKind, AccessMode, CreateKind, DataChange, EventKind as FileEventKind, MetadataKind,
ModifyKind, RemoveKind, RenameMode,
};
}
/// 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)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum FileType {
/// A regular file.
File,
/// A directory.
Dir,
/// A symbolic link.
Symlink,
/// Something else.
Other,
}
impl From<std::fs::FileType> 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"),
}
}
}

View File

@ -0,0 +1,9 @@
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
#[non_exhaustive]
/// A keyboard input.
pub enum Keyboard {
/// Event representing an 'end of file' on stdin
Eof,
}

30
crates/events/src/lib.rs Normal file
View File

@ -0,0 +1,30 @@
//! 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.
#[doc(inline)]
pub use event::*;
#[doc(inline)]
pub use fs::*;
#[doc(inline)]
pub use keyboard::*;
#[doc(inline)]
pub use process::*;
mod event;
mod fs;
mod keyboard;
mod process;
#[cfg(not(feature = "notify"))]
mod sans_notify;
#[cfg(feature = "serde")]
mod serde_formats;

View File

@ -0,0 +1,90 @@
use std::{
num::{NonZeroI32, NonZeroI64},
process::ExitStatus,
};
use watchexec_signals::Signal;
/// The end status of a process.
///
/// This is a sort-of equivalent of the [`std::process::ExitStatus`] type which, 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 limit the other values.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "disposition", content = "code"))]
pub enum ProcessEnd {
/// The process ended successfully, with exit status = 0.
#[cfg_attr(feature = "serde", serde(rename = "success"))]
Success,
/// The process exited with a non-zero exit status.
#[cfg_attr(feature = "serde", serde(rename = "error"))]
ExitError(NonZeroI64),
/// The process exited due to a signal.
#[cfg_attr(feature = "serde", serde(rename = "signal"))]
ExitSignal(Signal),
/// The process was stopped (but not terminated) (`libc::WIFSTOPPED`).
#[cfg_attr(feature = "serde", serde(rename = "stop"))]
ExitStop(NonZeroI32),
/// The process suffered an unhandled exception or warning (typically Windows only).
#[cfg_attr(feature = "serde", serde(rename = "exception"))]
Exception(NonZeroI32),
/// The process was continued (`libc::WIFCONTINUED`).
#[cfg_attr(feature = "serde", serde(rename = "continued"))]
Continued,
}
impl From<ExitStatus> 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())
}
}
}

View File

@ -0,0 +1,273 @@
// This file is dual-licensed under the Artistic License 2.0 as per the
// LICENSE.ARTISTIC file, and the Creative Commons Zero 1.0 license.
//
// Taken verbatim from the `notify` crate, with the Event types removed.
use std::hash::Hash;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// An event describing open or close operations on files.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum AccessMode {
/// The catch-all case, to be used when the specific kind of event is unknown.
Any,
/// An event emitted when the file is executed, or the folder opened.
Execute,
/// An event emitted when the file is opened for reading.
Read,
/// An event emitted when the file is opened for writing.
Write,
/// An event which specific kind is known but cannot be represented otherwise.
Other,
}
/// An event describing non-mutating access operations on files.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "kind", content = "mode"))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum AccessKind {
/// The catch-all case, to be used when the specific kind of event is unknown.
Any,
/// An event emitted when the file is read.
Read,
/// An event emitted when the file, or a handle to the file, is opened.
Open(AccessMode),
/// An event emitted when the file, or a handle to the file, is closed.
Close(AccessMode),
/// An event which specific kind is known but cannot be represented otherwise.
Other,
}
/// An event describing creation operations on files.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "kind"))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum CreateKind {
/// The catch-all case, to be used when the specific kind of event is unknown.
Any,
/// An event which results in the creation of a file.
File,
/// An event which results in the creation of a folder.
Folder,
/// An event which specific kind is known but cannot be represented otherwise.
Other,
}
/// An event emitted when the data content of a file is changed.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum DataChange {
/// The catch-all case, to be used when the specific kind of event is unknown.
Any,
/// An event emitted when the size of the data is changed.
Size,
/// An event emitted when the content of the data is changed.
Content,
/// An event which specific kind is known but cannot be represented otherwise.
Other,
}
/// An event emitted when the metadata of a file or folder is changed.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum MetadataKind {
/// The catch-all case, to be used when the specific kind of event is unknown.
Any,
/// An event emitted when the access time of the file or folder is changed.
AccessTime,
/// An event emitted when the write or modify time of the file or folder is changed.
WriteTime,
/// An event emitted when the permissions of the file or folder are changed.
Permissions,
/// An event emitted when the ownership of the file or folder is changed.
Ownership,
/// An event emitted when an extended attribute of the file or folder is changed.
///
/// If the extended attribute's name or type is known, it should be provided in the
/// `Info` event attribute.
Extended,
/// An event which specific kind is known but cannot be represented otherwise.
Other,
}
/// An event emitted when the name of a file or folder is changed.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum RenameMode {
/// The catch-all case, to be used when the specific kind of event is unknown.
Any,
/// An event emitted on the file or folder resulting from a rename.
To,
/// An event emitted on the file or folder that was renamed.
From,
/// A single event emitted with both the `From` and `To` paths.
///
/// This event should be emitted when both source and target are known. The paths should be
/// provided in this exact order (from, to).
Both,
/// An event which specific kind is known but cannot be represented otherwise.
Other,
}
/// An event describing mutation of content, name, or metadata.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "kind", content = "mode"))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum ModifyKind {
/// The catch-all case, to be used when the specific kind of event is unknown.
Any,
/// An event emitted when the data content of a file is changed.
Data(DataChange),
/// An event emitted when the metadata of a file or folder is changed.
Metadata(MetadataKind),
/// An event emitted when the name of a file or folder is changed.
#[cfg_attr(feature = "serde", serde(rename = "rename"))]
Name(RenameMode),
/// An event which specific kind is known but cannot be represented otherwise.
Other,
}
/// An event describing removal operations on files.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "kind"))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum RemoveKind {
/// The catch-all case, to be used when the specific kind of event is unknown.
Any,
/// An event emitted when a file is removed.
File,
/// An event emitted when a folder is removed.
Folder,
/// An event which specific kind is known but cannot be represented otherwise.
Other,
}
/// Top-level event kind.
///
/// This is arguably the most important classification for events. All subkinds below this one
/// represent details that may or may not be available for any particular backend, but most tools
/// and Notify systems will only care about which of these four general kinds an event is about.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum EventKind {
/// The catch-all event kind, for unsupported/unknown events.
///
/// This variant should be used as the "else" case when mapping native kernel bitmasks or
/// bitmaps, such that if the mask is ever extended with new event types the backend will not
/// gain bugs due to not matching new unknown event types.
///
/// This variant is also the default variant used when Notify is in "imprecise" mode.
Any,
/// An event describing non-mutating access operations on files.
///
/// This event is about opening and closing file handles, as well as executing files, and any
/// other such event that is about accessing files, folders, or other structures rather than
/// mutating them.
///
/// Only some platforms are capable of generating these.
Access(AccessKind),
/// An event describing creation operations on files.
///
/// This event is about the creation of files, folders, or other structures but not about e.g.
/// writing new content into them.
Create(CreateKind),
/// An event describing mutation of content, name, or metadata.
///
/// This event is about the mutation of files', folders', or other structures' content, name
/// (path), or associated metadata (attributes).
Modify(ModifyKind),
/// An event describing removal operations on files.
///
/// This event is about the removal of files, folders, or other structures but not e.g. erasing
/// content from them. This may also be triggered for renames/moves that move files _out of the
/// watched subpath_.
///
/// Some editors also trigger Remove events when saving files as they may opt for removing (or
/// renaming) the original then creating a new file in-place.
Remove(RemoveKind),
/// An event not fitting in any of the above four categories.
///
/// This may be used for meta-events about the watch itself.
Other,
}
impl EventKind {
/// Indicates whether an event is an Access variant.
pub fn is_access(&self) -> bool {
matches!(self, EventKind::Access(_))
}
/// Indicates whether an event is a Create variant.
pub fn is_create(&self) -> bool {
matches!(self, EventKind::Create(_))
}
/// Indicates whether an event is a Modify variant.
pub fn is_modify(&self) -> bool {
matches!(self, EventKind::Modify(_))
}
/// Indicates whether an event is a Remove variant.
pub fn is_remove(&self) -> bool {
matches!(self, EventKind::Remove(_))
}
/// Indicates whether an event is an Other variant.
pub fn is_other(&self) -> bool {
matches!(self, EventKind::Other)
}
}
impl Default for EventKind {
fn default() -> Self {
EventKind::Any
}
}

View File

@ -0,0 +1,361 @@
use std::{
collections::BTreeMap,
num::{NonZeroI32, NonZeroI64},
path::PathBuf,
};
use serde::{Deserialize, Serialize};
use watchexec_signals::Signal;
use crate::{
fs::filekind::{
AccessKind, AccessMode, CreateKind, DataChange, FileEventKind as EventKind, MetadataKind,
ModifyKind, RemoveKind, RenameMode,
},
Event, FileType, Keyboard, ProcessEnd, Source, Tag,
};
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SerdeTag {
kind: TagKind,
// path
#[serde(default, skip_serializing_if = "Option::is_none")]
absolute: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
filetype: Option<FileType>,
// fs
#[serde(default, skip_serializing_if = "Option::is_none")]
simple: Option<FsEventKind>,
#[serde(default, skip_serializing_if = "Option::is_none")]
full: Option<String>,
// source
#[serde(default, skip_serializing_if = "Option::is_none")]
source: Option<Source>,
// keyboard
#[serde(default, skip_serializing_if = "Option::is_none")]
keycode: Option<Keyboard>,
// process
#[serde(default, skip_serializing_if = "Option::is_none")]
pid: Option<u32>,
// signal
#[serde(default, skip_serializing_if = "Option::is_none")]
signal: Option<Signal>,
// completion
#[serde(default, skip_serializing_if = "Option::is_none")]
disposition: Option<ProcessDisposition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
code: Option<i64>,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum TagKind {
#[default]
None,
Path,
Fs,
Source,
Keyboard,
Process,
Signal,
Completion,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProcessDisposition {
Unknown,
Success,
Error,
Signal,
Stop,
Exception,
Continued,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum FsEventKind {
Access,
Create,
Modify,
Remove,
Other,
}
impl From<EventKind> for FsEventKind {
fn from(value: EventKind) -> Self {
match value {
EventKind::Access(_) => Self::Access,
EventKind::Create(_) => Self::Create,
EventKind::Modify(_) => Self::Modify,
EventKind::Remove(_) => Self::Remove,
EventKind::Any | EventKind::Other => Self::Other,
}
}
}
impl From<Tag> for SerdeTag {
fn from(value: Tag) -> Self {
match value {
Tag::Path { path, file_type } => Self {
kind: TagKind::Path,
absolute: Some(path),
filetype: file_type,
..Default::default()
},
Tag::FileEventKind(fek) => Self {
kind: TagKind::Fs,
full: Some(format!("{:?}", fek)),
simple: Some(fek.into()),
..Default::default()
},
Tag::Source(source) => Self {
kind: TagKind::Source,
source: Some(source),
..Default::default()
},
Tag::Keyboard(keycode) => Self {
kind: TagKind::Keyboard,
keycode: Some(keycode),
..Default::default()
},
Tag::Process(pid) => Self {
kind: TagKind::Process,
pid: Some(pid),
..Default::default()
},
Tag::Signal(signal) => Self {
kind: TagKind::Signal,
signal: Some(signal),
..Default::default()
},
Tag::ProcessCompletion(None) => Self {
kind: TagKind::Completion,
disposition: Some(ProcessDisposition::Unknown),
..Default::default()
},
Tag::ProcessCompletion(Some(end)) => Self {
kind: TagKind::Completion,
code: match &end {
ProcessEnd::Success => None,
ProcessEnd::ExitSignal(_) => None,
ProcessEnd::ExitError(err) => Some(err.get()),
ProcessEnd::ExitStop(code) => Some(code.get().into()),
ProcessEnd::Exception(exc) => Some(exc.get().into()),
ProcessEnd::Continued => None,
},
signal: if let ProcessEnd::ExitSignal(sig) = &end {
Some(*sig)
} else {
None
},
disposition: Some(match end {
ProcessEnd::Success => ProcessDisposition::Success,
ProcessEnd::ExitError(_) => ProcessDisposition::Error,
ProcessEnd::ExitSignal(_) => ProcessDisposition::Signal,
ProcessEnd::ExitStop(_) => ProcessDisposition::Stop,
ProcessEnd::Exception(_) => ProcessDisposition::Exception,
ProcessEnd::Continued => ProcessDisposition::Continued,
}),
..Default::default()
},
Tag::Unknown => Self::default(),
}
}
}
impl From<SerdeTag> for Tag {
fn from(value: SerdeTag) -> Self {
match value {
SerdeTag {
kind: TagKind::Path,
absolute: Some(path),
filetype,
..
} => Self::Path {
path,
file_type: filetype,
},
SerdeTag {
kind: TagKind::Fs,
full: Some(full),
..
} => Self::FileEventKind(match full.as_str() {
"Any" => EventKind::Any,
"Access(Any)" => EventKind::Access(AccessKind::Any),
"Access(Read)" => EventKind::Access(AccessKind::Read),
"Access(Open(Any))" => EventKind::Access(AccessKind::Open(AccessMode::Any)),
"Access(Open(Execute))" => EventKind::Access(AccessKind::Open(AccessMode::Execute)),
"Access(Open(Read))" => EventKind::Access(AccessKind::Open(AccessMode::Read)),
"Access(Open(Write))" => EventKind::Access(AccessKind::Open(AccessMode::Write)),
"Access(Open(Other))" => EventKind::Access(AccessKind::Open(AccessMode::Other)),
"Access(Close(Any))" => EventKind::Access(AccessKind::Close(AccessMode::Any)),
"Access(Close(Execute))" => {
EventKind::Access(AccessKind::Close(AccessMode::Execute))
}
"Access(Close(Read))" => EventKind::Access(AccessKind::Close(AccessMode::Read)),
"Access(Close(Write))" => EventKind::Access(AccessKind::Close(AccessMode::Write)),
"Access(Close(Other))" => EventKind::Access(AccessKind::Close(AccessMode::Other)),
"Access(Other)" => EventKind::Access(AccessKind::Other),
"Create(Any)" => EventKind::Create(CreateKind::Any),
"Create(File)" => EventKind::Create(CreateKind::File),
"Create(Folder)" => EventKind::Create(CreateKind::Folder),
"Create(Other)" => EventKind::Create(CreateKind::Other),
"Modify(Any)" => EventKind::Modify(ModifyKind::Any),
"Modify(Data(Any))" => EventKind::Modify(ModifyKind::Data(DataChange::Any)),
"Modify(Data(Size))" => EventKind::Modify(ModifyKind::Data(DataChange::Size)),
"Modify(Data(Content))" => EventKind::Modify(ModifyKind::Data(DataChange::Content)),
"Modify(Data(Other))" => EventKind::Modify(ModifyKind::Data(DataChange::Other)),
"Modify(Metadata(Any))" => {
EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any))
}
"Modify(Metadata(AccessTime))" => {
EventKind::Modify(ModifyKind::Metadata(MetadataKind::AccessTime))
}
"Modify(Metadata(WriteTime))" => {
EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime))
}
"Modify(Metadata(Permissions))" => {
EventKind::Modify(ModifyKind::Metadata(MetadataKind::Permissions))
}
"Modify(Metadata(Ownership))" => {
EventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership))
}
"Modify(Metadata(Extended))" => {
EventKind::Modify(ModifyKind::Metadata(MetadataKind::Extended))
}
"Modify(Metadata(Other))" => {
EventKind::Modify(ModifyKind::Metadata(MetadataKind::Other))
}
"Modify(Name(Any))" => EventKind::Modify(ModifyKind::Name(RenameMode::Any)),
"Modify(Name(To))" => EventKind::Modify(ModifyKind::Name(RenameMode::To)),
"Modify(Name(From))" => EventKind::Modify(ModifyKind::Name(RenameMode::From)),
"Modify(Name(Both))" => EventKind::Modify(ModifyKind::Name(RenameMode::Both)),
"Modify(Name(Other))" => EventKind::Modify(ModifyKind::Name(RenameMode::Other)),
"Modify(Other)" => EventKind::Modify(ModifyKind::Other),
"Remove(Any)" => EventKind::Remove(RemoveKind::Any),
"Remove(File)" => EventKind::Remove(RemoveKind::File),
"Remove(Folder)" => EventKind::Remove(RemoveKind::Folder),
"Remove(Other)" => EventKind::Remove(RemoveKind::Other),
_ => EventKind::Other, // and literal "Other"
}),
SerdeTag {
kind: TagKind::Fs,
simple: Some(simple),
..
} => Self::FileEventKind(match simple {
FsEventKind::Access => EventKind::Access(AccessKind::Any),
FsEventKind::Create => EventKind::Create(CreateKind::Any),
FsEventKind::Modify => EventKind::Modify(ModifyKind::Any),
FsEventKind::Remove => EventKind::Remove(RemoveKind::Any),
FsEventKind::Other => EventKind::Other,
}),
SerdeTag {
kind: TagKind::Source,
source: Some(source),
..
} => Self::Source(source),
SerdeTag {
kind: TagKind::Keyboard,
keycode: Some(keycode),
..
} => Self::Keyboard(keycode),
SerdeTag {
kind: TagKind::Process,
pid: Some(pid),
..
} => Self::Process(pid),
SerdeTag {
kind: TagKind::Signal,
signal: Some(sig),
..
} => Self::Signal(sig),
SerdeTag {
kind: TagKind::Completion,
disposition: None | Some(ProcessDisposition::Unknown),
..
} => Self::ProcessCompletion(None),
SerdeTag {
kind: TagKind::Completion,
disposition: Some(ProcessDisposition::Success),
..
} => Self::ProcessCompletion(Some(ProcessEnd::Success)),
SerdeTag {
kind: TagKind::Completion,
disposition: Some(ProcessDisposition::Continued),
..
} => Self::ProcessCompletion(Some(ProcessEnd::Continued)),
SerdeTag {
kind: TagKind::Completion,
disposition: Some(ProcessDisposition::Signal),
signal: Some(sig),
..
} => Self::ProcessCompletion(Some(ProcessEnd::ExitSignal(sig))),
SerdeTag {
kind: TagKind::Completion,
disposition: Some(ProcessDisposition::Error),
code: Some(err),
..
} if err != 0 => Self::ProcessCompletion(Some(ProcessEnd::ExitError(unsafe {
NonZeroI64::new_unchecked(err)
}))),
SerdeTag {
kind: TagKind::Completion,
disposition: Some(ProcessDisposition::Stop),
code: Some(code),
..
} if code != 0 && i32::try_from(code).is_ok() => {
Self::ProcessCompletion(Some(ProcessEnd::ExitStop(unsafe {
NonZeroI32::new_unchecked(code.try_into().unwrap())
})))
}
SerdeTag {
kind: TagKind::Completion,
disposition: Some(ProcessDisposition::Exception),
code: Some(exc),
..
} if exc != 0 && i32::try_from(exc).is_ok() => {
Self::ProcessCompletion(Some(ProcessEnd::Exception(unsafe {
NonZeroI32::new_unchecked(exc.try_into().unwrap())
})))
}
_ => Self::Unknown,
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SerdeEvent {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
tags: Vec<Tag>,
// for a consistent serialization order
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
metadata: BTreeMap<String, Vec<String>>,
}
impl From<Event> for SerdeEvent {
fn from(Event { tags, metadata }: Event) -> Self {
Self {
tags,
metadata: metadata.into_iter().collect(),
}
}
}
impl From<SerdeEvent> for Event {
fn from(SerdeEvent { tags, metadata }: SerdeEvent) -> Self {
Self {
tags,
metadata: metadata.into_iter().collect(),
}
}
}

253
crates/events/tests/json.rs Normal file
View File

@ -0,0 +1,253 @@
use std::num::{NonZeroI32, NonZeroI64};
use snapbox::assert_eq_path;
use watchexec_events::{
filekind::{CreateKind, FileEventKind as EventKind, ModifyKind, RemoveKind, RenameMode},
Event, FileType, Keyboard, ProcessEnd, Source, Tag,
};
use watchexec_signals::Signal;
fn parse_file(path: &str) -> Vec<Event> {
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap()
}
#[test]
fn single() {
let single = Event {
tags: vec![Tag::Source(Source::Internal)],
metadata: Default::default(),
};
assert_eq_path(
"tests/snapshots/single.json",
serde_json::to_string_pretty(&single).unwrap(),
);
assert_eq!(
serde_json::from_str::<Event>(
&std::fs::read_to_string("tests/snapshots/single.json").unwrap()
)
.unwrap(),
single
);
}
#[test]
fn array() {
let array = &[
Event {
tags: vec![Tag::Source(Source::Internal)],
metadata: Default::default(),
},
Event {
tags: vec![
Tag::ProcessCompletion(Some(ProcessEnd::Success)),
Tag::Process(123),
],
metadata: Default::default(),
},
Event {
tags: vec![Tag::Keyboard(Keyboard::Eof)],
metadata: Default::default(),
},
];
assert_eq_path(
"tests/snapshots/array.json",
serde_json::to_string_pretty(array).unwrap(),
);
assert_eq!(parse_file("tests/snapshots/array.json"), array);
}
#[test]
fn metadata() {
let metadata = &[Event {
tags: vec![Tag::Source(Source::Internal)],
metadata: [
("Dafan".into(), vec!["Mountain".into()]),
("Lan".into(), vec!["Zhan".into()]),
]
.into(),
}];
assert_eq_path(
"tests/snapshots/metadata.json",
serde_json::to_string_pretty(metadata).unwrap(),
);
assert_eq!(parse_file("tests/snapshots/metadata.json"), metadata);
}
#[test]
fn asymmetric() {
// asymmetric because these have information loss or missing fields
assert_eq!(
parse_file("tests/snapshots/asymmetric.json"),
&[
Event {
tags: vec![
// no filetype field
Tag::Path {
path: "/foo/bar/baz".into(),
file_type: None
},
// fs with only simple reprensentation
Tag::FileEventKind(EventKind::Create(CreateKind::Any)),
// unparseable of a known kind
Tag::Unknown,
],
metadata: Default::default(),
},
Event {
tags: vec![
// no simple field
Tag::FileEventKind(EventKind::Modify(ModifyKind::Other)),
// no disposition field
Tag::ProcessCompletion(None)
],
metadata: Default::default(),
},
]
);
}
#[test]
fn sources() {
let sources = vec![
Event {
tags: vec![
Tag::Source(Source::Filesystem),
Tag::Source(Source::Keyboard),
Tag::Source(Source::Mouse),
],
metadata: Default::default(),
},
Event {
tags: vec![
Tag::Source(Source::Os),
Tag::Source(Source::Time),
Tag::Source(Source::Internal),
],
metadata: Default::default(),
},
];
assert_eq_path(
"tests/snapshots/sources.json",
serde_json::to_string_pretty(&sources).unwrap(),
);
assert_eq!(parse_file("tests/snapshots/sources.json"), sources);
}
#[test]
fn signals() {
let signals = vec![
Event {
tags: vec![
Tag::Signal(Signal::Interrupt),
Tag::Signal(Signal::User1),
Tag::Signal(Signal::ForceStop),
],
metadata: Default::default(),
},
Event {
tags: vec![
Tag::Signal(Signal::Custom(66)),
Tag::Signal(Signal::Custom(0)),
],
metadata: Default::default(),
},
];
assert_eq_path(
"tests/snapshots/signals.json",
serde_json::to_string_pretty(&signals).unwrap(),
);
assert_eq!(parse_file("tests/snapshots/signals.json"), signals);
}
#[test]
fn completions() {
let completions = vec![
Event {
tags: vec![
Tag::ProcessCompletion(None),
Tag::ProcessCompletion(Some(ProcessEnd::Success)),
Tag::ProcessCompletion(Some(ProcessEnd::Continued)),
],
metadata: Default::default(),
},
Event {
tags: vec![
Tag::ProcessCompletion(Some(ProcessEnd::ExitError(NonZeroI64::new(12).unwrap()))),
Tag::ProcessCompletion(Some(ProcessEnd::ExitSignal(Signal::Interrupt))),
Tag::ProcessCompletion(Some(ProcessEnd::ExitSignal(Signal::Custom(34)))),
Tag::ProcessCompletion(Some(ProcessEnd::ExitStop(NonZeroI32::new(56).unwrap()))),
Tag::ProcessCompletion(Some(ProcessEnd::Exception(NonZeroI32::new(78).unwrap()))),
],
metadata: Default::default(),
},
];
assert_eq_path(
"tests/snapshots/completions.json",
serde_json::to_string_pretty(&completions).unwrap(),
);
assert_eq!(parse_file("tests/snapshots/completions.json"), completions);
}
#[test]
fn paths() {
let paths = vec![
Event {
tags: vec![
Tag::Path {
path: "/foo/bar/baz".into(),
file_type: Some(FileType::Symlink),
},
Tag::FileEventKind(EventKind::Create(CreateKind::File)),
],
metadata: Default::default(),
},
Event {
tags: vec![
Tag::Path {
path: "/rename/from/this".into(),
file_type: Some(FileType::File),
},
Tag::Path {
path: "/rename/into/that".into(),
file_type: Some(FileType::Other),
},
Tag::FileEventKind(EventKind::Modify(ModifyKind::Name(RenameMode::Both))),
],
metadata: Default::default(),
},
Event {
tags: vec![
Tag::Path {
path: "/delete/this".into(),
file_type: Some(FileType::Dir),
},
Tag::Path {
path: "/".into(),
file_type: None,
},
Tag::FileEventKind(EventKind::Remove(RemoveKind::Any)),
],
metadata: Default::default(),
},
];
assert_eq_path(
"tests/snapshots/paths.json",
serde_json::to_string_pretty(&paths).unwrap(),
);
assert_eq!(parse_file("tests/snapshots/paths.json"), paths);
}

View File

@ -0,0 +1,30 @@
[
{
"tags": [
{
"kind": "source",
"source": "internal"
}
]
},
{
"tags": [
{
"kind": "completion",
"disposition": "success"
},
{
"kind": "process",
"pid": 123
}
]
},
{
"tags": [
{
"kind": "keyboard",
"keycode": "eof"
}
]
}
]

View File

@ -0,0 +1,28 @@
[
{
"tags": [
{
"kind": "path",
"absolute": "/foo/bar/baz"
},
{
"kind": "fs",
"simple": "create"
},
{
"kind": "fs"
}
]
},
{
"tags": [
{
"kind": "fs",
"full": "Modify(Other)"
},
{
"kind": "completion"
}
]
}
]

View File

@ -0,0 +1,47 @@
[
{
"tags": [
{
"kind": "completion",
"disposition": "unknown"
},
{
"kind": "completion",
"disposition": "success"
},
{
"kind": "completion",
"disposition": "continued"
}
]
},
{
"tags": [
{
"kind": "completion",
"disposition": "error",
"code": 12
},
{
"kind": "completion",
"signal": "SIGINT",
"disposition": "signal"
},
{
"kind": "completion",
"signal": 34,
"disposition": "signal"
},
{
"kind": "completion",
"disposition": "stop",
"code": 56
},
{
"kind": "completion",
"disposition": "exception",
"code": 78
}
]
}
]

View File

@ -0,0 +1,18 @@
[
{
"tags": [
{
"kind": "source",
"source": "internal"
}
],
"metadata": {
"Dafan": [
"Mountain"
],
"Lan": [
"Zhan"
]
}
}
]

View File

@ -0,0 +1,53 @@
[
{
"tags": [
{
"kind": "path",
"absolute": "/foo/bar/baz",
"filetype": "symlink"
},
{
"kind": "fs",
"simple": "create",
"full": "Create(File)"
}
]
},
{
"tags": [
{
"kind": "path",
"absolute": "/rename/from/this",
"filetype": "file"
},
{
"kind": "path",
"absolute": "/rename/into/that",
"filetype": "other"
},
{
"kind": "fs",
"simple": "modify",
"full": "Modify(Name(Both))"
}
]
},
{
"tags": [
{
"kind": "path",
"absolute": "/delete/this",
"filetype": "dir"
},
{
"kind": "path",
"absolute": "/"
},
{
"kind": "fs",
"simple": "remove",
"full": "Remove(Any)"
}
]
}
]

View File

@ -0,0 +1,30 @@
[
{
"tags": [
{
"kind": "signal",
"signal": "SIGINT"
},
{
"kind": "signal",
"signal": "SIGUSR1"
},
{
"kind": "signal",
"signal": "SIGKILL"
}
]
},
{
"tags": [
{
"kind": "signal",
"signal": 66
},
{
"kind": "signal",
"signal": 0
}
]
}
]

View File

@ -0,0 +1,8 @@
{
"tags": [
{
"kind": "source",
"source": "internal"
}
]
}

View File

@ -0,0 +1,34 @@
[
{
"tags": [
{
"kind": "source",
"source": "filesystem"
},
{
"kind": "source",
"source": "keyboard"
},
{
"kind": "source",
"source": "mouse"
}
]
},
{
"tags": [
{
"kind": "source",
"source": "os"
},
{
"kind": "source",
"source": "time"
},
{
"kind": "source",
"source": "internal"
}
]
}
]

View File

@ -43,3 +43,7 @@ features = [
"rt-multi-thread",
"macros",
]
[dependencies.watchexec-signals]
version = "1.0.0"
path = "../../signals"

View File

@ -6,10 +6,9 @@ use watchexec::{
error::RuntimeError,
event::{filekind::FileEventKind, Event, FileType, Priority, ProcessEnd, Source, Tag},
filter::Filterer,
signal::source::MainSignal,
};
use watchexec_filterer_ignore::IgnoreFilterer;
use watchexec_signals::Signal;
pub mod ignore {
pub use super::ig_file as file;
@ -146,11 +145,11 @@ pub trait TaggedHarness {
self.tag_pass(Tag::Process(pid), false);
}
fn signal_does_pass(&self, sig: MainSignal) {
fn signal_does_pass(&self, sig: Signal) {
self.tag_pass(Tag::Signal(sig), true);
}
fn signal_doesnt_pass(&self, sig: MainSignal) {
fn signal_doesnt_pass(&self, sig: Signal) {
self.tag_pass(Tag::Signal(sig), false);
}

View File

@ -44,6 +44,10 @@ path = "../../lib"
version = "1.1.0"
path = "../ignore"
[dependencies.watchexec-signals]
version = "1.0.0"
path = "../../signals"
[dev-dependencies]
tracing-subscriber = "0.3.6"

View File

@ -14,9 +14,9 @@ use watchexec::{
error::RuntimeError,
event::{Event, FileType, Priority, ProcessEnd, Tag},
filter::Filterer,
signal::{process::SubSignal, source::MainSignal},
};
use watchexec_filterer_ignore::IgnoreFilterer;
use watchexec_signals::Signal;
use crate::{swaplock::SwapLock, Filter, Matcher, Op, Pattern, TaggedFiltererError};
@ -321,6 +321,21 @@ impl TaggedFilterer {
// Ok(None) => for some precondition, the match was not done (mismatched tag, out of context, …)
fn match_tag(&self, filter: &Filter, tag: &Tag) -> Result<Option<bool>, TaggedFiltererError> {
trace!(matcher=?filter.on, "matching filter to tag");
fn sig_match(sig: Signal) -> (&'static str, i32) {
match sig {
Signal::Hangup | Signal::Custom(1) => ("HUP", 1),
Signal::ForceStop | Signal::Custom(9) => ("KILL", 9),
Signal::Interrupt | Signal::Custom(2) => ("INT", 2),
Signal::Quit | Signal::Custom(3) => ("QUIT", 3),
Signal::Terminate | Signal::Custom(15) => ("TERM", 15),
Signal::User1 | Signal::Custom(10) => ("USR1", 10),
Signal::User2 | Signal::Custom(12) => ("USR2", 12),
Signal::Custom(n) => ("UNK", n),
_ => ("UNK", 0),
}
}
match (tag, filter.on) {
(tag, Matcher::Tag) => filter.matches(tag.discriminant_name()),
(Tag::Path { path, .. }, Matcher::Path) => {
@ -360,15 +375,7 @@ impl TaggedFilterer {
(Tag::Source(src), Matcher::Source) => filter.matches(src.to_string()),
(Tag::Process(pid), Matcher::Process) => filter.matches(pid.to_string()),
(Tag::Signal(sig), Matcher::Signal) => {
let (text, int) = match sig {
MainSignal::Hangup => ("HUP", 1),
MainSignal::Interrupt => ("INT", 2),
MainSignal::Quit => ("QUIT", 3),
MainSignal::Terminate => ("TERM", 15),
MainSignal::User1 => ("USR1", 10),
MainSignal::User2 => ("USR2", 12),
};
let (text, int) = sig_match(*sig);
Ok(filter.matches(text)?
|| filter.matches(format!("SIG{text}"))?
|| filter.matches(int.to_string())?)
@ -378,17 +385,7 @@ impl TaggedFilterer {
Some(ProcessEnd::Success) => filter.matches("success"),
Some(ProcessEnd::ExitError(int)) => filter.matches(format!("error({int})")),
Some(ProcessEnd::ExitSignal(sig)) => {
let (text, int) = match sig {
SubSignal::Hangup | SubSignal::Custom(1) => ("HUP", 1),
SubSignal::ForceStop | SubSignal::Custom(9) => ("KILL", 9),
SubSignal::Interrupt | SubSignal::Custom(2) => ("INT", 2),
SubSignal::Quit | SubSignal::Custom(3) => ("QUIT", 3),
SubSignal::Terminate | SubSignal::Custom(15) => ("TERM", 15),
SubSignal::User1 | SubSignal::Custom(10) => ("USR1", 10),
SubSignal::User2 | SubSignal::Custom(12) => ("USR2", 12),
SubSignal::Custom(n) => ("UNK", *n),
};
let (text, int) = sig_match(*sig);
Ok(filter.matches(format!("signal({text})"))?
|| filter.matches(format!("signal(SIG{text})"))?
|| filter.matches(format!("signal({int})"))?)

View File

@ -1,7 +1,5 @@
use watchexec::{
event::{filekind::*, ProcessEnd, Source},
signal::source::MainSignal,
};
use watchexec::event::{filekind::*, ProcessEnd, Source};
use watchexec_signals::Signal;
mod helpers;
use helpers::tagged_ff::*;
@ -28,7 +26,7 @@ async fn empty_filter_passes_everything() {
filterer.source_does_pass(Source::Keyboard);
filterer.fek_does_pass(FileEventKind::Create(CreateKind::File));
filterer.pid_does_pass(1234);
filterer.signal_does_pass(MainSignal::User1);
filterer.signal_does_pass(Signal::User1);
filterer.complete_does_pass(None);
filterer.complete_does_pass(Some(ProcessEnd::Success));
}

View File

@ -13,10 +13,10 @@ use watchexec::{
error::RuntimeError,
event::{filekind::FileEventKind, Event, FileType, Priority, ProcessEnd, Source, Tag},
filter::Filterer,
signal::source::MainSignal,
};
use watchexec_filterer_ignore::IgnoreFilterer;
use watchexec_filterer_tagged::{Filter, FilterFile, Matcher, Op, Pattern, TaggedFilterer};
use watchexec_signals::Signal;
pub mod tagged {
pub use super::ig_file as file;
@ -163,11 +163,11 @@ pub trait TaggedHarness {
self.tag_pass(Tag::Process(pid), false);
}
fn signal_does_pass(&self, sig: MainSignal) {
fn signal_does_pass(&self, sig: Signal) {
self.tag_pass(Tag::Signal(sig), true);
}
fn signal_doesnt_pass(&self, sig: MainSignal) {
fn signal_doesnt_pass(&self, sig: Signal) {
self.tag_pass(Tag::Signal(sig), false);
}

View File

@ -1,11 +1,8 @@
use std::num::{NonZeroI32, NonZeroI64};
use watchexec::{
event::{filekind::*, ProcessEnd, Source},
signal::{process::SubSignal, source::MainSignal},
};
use watchexec::event::{filekind::*, ProcessEnd, Source};
use watchexec_filterer_tagged::TaggedFilterer;
use watchexec_signals::Signal;
mod helpers;
use helpers::tagged::*;
@ -17,7 +14,7 @@ async fn empty_filter_passes_everything() {
filterer.source_does_pass(Source::Keyboard);
filterer.fek_does_pass(FileEventKind::Create(CreateKind::File));
filterer.pid_does_pass(1234);
filterer.signal_does_pass(MainSignal::User1);
filterer.signal_does_pass(Signal::User1);
filterer.complete_does_pass(None);
filterer.complete_does_pass(Some(ProcessEnd::Success));
}
@ -183,94 +180,94 @@ async fn signal_set_single_without_sig() {
let filterer = filt(&[f]).await;
filterer.signal_does_pass(MainSignal::Interrupt);
filterer.signal_doesnt_pass(MainSignal::Hangup);
filterer.signal_does_pass(Signal::Interrupt);
filterer.signal_doesnt_pass(Signal::Hangup);
}
#[tokio::test]
async fn signal_set_single_with_sig() {
let filterer = filt(&[filter("signal:=SIGINT")]).await;
filterer.signal_does_pass(MainSignal::Interrupt);
filterer.signal_doesnt_pass(MainSignal::Hangup);
filterer.signal_does_pass(Signal::Interrupt);
filterer.signal_doesnt_pass(Signal::Hangup);
}
#[tokio::test]
async fn signal_set_multiple_without_sig() {
let filterer = filt(&[filter("sig:=INT,TERM")]).await;
filterer.signal_does_pass(MainSignal::Interrupt);
filterer.signal_does_pass(MainSignal::Terminate);
filterer.signal_doesnt_pass(MainSignal::Hangup);
filterer.signal_does_pass(Signal::Interrupt);
filterer.signal_does_pass(Signal::Terminate);
filterer.signal_doesnt_pass(Signal::Hangup);
}
#[tokio::test]
async fn signal_set_multiple_with_sig() {
let filterer = filt(&[filter("signal:=SIGINT,SIGTERM")]).await;
filterer.signal_does_pass(MainSignal::Interrupt);
filterer.signal_does_pass(MainSignal::Terminate);
filterer.signal_doesnt_pass(MainSignal::Hangup);
filterer.signal_does_pass(Signal::Interrupt);
filterer.signal_does_pass(Signal::Terminate);
filterer.signal_doesnt_pass(Signal::Hangup);
}
#[tokio::test]
async fn signal_set_multiple_mixed_sig() {
let filterer = filt(&[filter("sig:=SIGINT,TERM")]).await;
filterer.signal_does_pass(MainSignal::Interrupt);
filterer.signal_does_pass(MainSignal::Terminate);
filterer.signal_doesnt_pass(MainSignal::Hangup);
filterer.signal_does_pass(Signal::Interrupt);
filterer.signal_does_pass(Signal::Terminate);
filterer.signal_doesnt_pass(Signal::Hangup);
}
#[tokio::test]
async fn signal_equals_without_sig() {
let filterer = filt(&[filter("sig==INT")]).await;
filterer.signal_does_pass(MainSignal::Interrupt);
filterer.signal_doesnt_pass(MainSignal::Hangup);
filterer.signal_does_pass(Signal::Interrupt);
filterer.signal_doesnt_pass(Signal::Hangup);
}
#[tokio::test]
async fn signal_equals_with_sig() {
let filterer = filt(&[filter("signal==SIGINT")]).await;
filterer.signal_does_pass(MainSignal::Interrupt);
filterer.signal_doesnt_pass(MainSignal::Hangup);
filterer.signal_does_pass(Signal::Interrupt);
filterer.signal_doesnt_pass(Signal::Hangup);
}
#[tokio::test]
async fn signal_set_single_numbers() {
let filterer = filt(&[filter("signal:=2")]).await;
filterer.signal_does_pass(MainSignal::Interrupt);
filterer.signal_doesnt_pass(MainSignal::Hangup);
filterer.signal_does_pass(Signal::Interrupt);
filterer.signal_doesnt_pass(Signal::Hangup);
}
#[tokio::test]
async fn signal_set_multiple_numbers() {
let filterer = filt(&[filter("sig:=2,15")]).await;
filterer.signal_does_pass(MainSignal::Interrupt);
filterer.signal_does_pass(MainSignal::Terminate);
filterer.signal_doesnt_pass(MainSignal::Hangup);
filterer.signal_does_pass(Signal::Interrupt);
filterer.signal_does_pass(Signal::Terminate);
filterer.signal_doesnt_pass(Signal::Hangup);
}
#[tokio::test]
async fn signal_equals_numbers() {
let filterer = filt(&[filter("sig==2")]).await;
filterer.signal_does_pass(MainSignal::Interrupt);
filterer.signal_doesnt_pass(MainSignal::Hangup);
filterer.signal_does_pass(Signal::Interrupt);
filterer.signal_doesnt_pass(Signal::Hangup);
}
#[tokio::test]
async fn signal_set_all_mixed() {
let filterer = filt(&[filter("signal:=SIGHUP,INT,15")]).await;
filterer.signal_does_pass(MainSignal::Hangup);
filterer.signal_does_pass(MainSignal::Interrupt);
filterer.signal_does_pass(MainSignal::Terminate);
filterer.signal_doesnt_pass(MainSignal::User1);
filterer.signal_does_pass(Signal::Hangup);
filterer.signal_does_pass(Signal::Interrupt);
filterer.signal_does_pass(Signal::Terminate);
filterer.signal_doesnt_pass(Signal::User1);
}
#[tokio::test]
@ -389,7 +386,7 @@ async fn complete_with_any_exception() {
async fn complete_with_specific_signal_with_sig() {
let filterer = filt(&[filter("complete*=signal(SIGINT)")]).await;
filterer.complete_does_pass(Some(ProcessEnd::ExitSignal(SubSignal::Interrupt)));
filterer.complete_does_pass(Some(ProcessEnd::ExitSignal(Signal::Interrupt)));
filterer.complete_doesnt_pass(Some(ProcessEnd::ExitStop(NonZeroI32::new(19).unwrap())));
filterer.complete_doesnt_pass(Some(ProcessEnd::Success));
filterer.complete_doesnt_pass(None);
@ -399,7 +396,7 @@ async fn complete_with_specific_signal_with_sig() {
async fn complete_with_specific_signal_without_sig() {
let filterer = filt(&[filter("complete*=signal(INT)")]).await;
filterer.complete_does_pass(Some(ProcessEnd::ExitSignal(SubSignal::Interrupt)));
filterer.complete_does_pass(Some(ProcessEnd::ExitSignal(Signal::Interrupt)));
filterer.complete_doesnt_pass(Some(ProcessEnd::ExitStop(NonZeroI32::new(19).unwrap())));
filterer.complete_doesnt_pass(Some(ProcessEnd::Success));
filterer.complete_doesnt_pass(None);
@ -409,7 +406,7 @@ async fn complete_with_specific_signal_without_sig() {
async fn complete_with_specific_signal_number() {
let filterer = filt(&[filter("complete*=signal(2)")]).await;
filterer.complete_does_pass(Some(ProcessEnd::ExitSignal(SubSignal::Interrupt)));
filterer.complete_does_pass(Some(ProcessEnd::ExitSignal(Signal::Interrupt)));
filterer.complete_doesnt_pass(Some(ProcessEnd::ExitStop(NonZeroI32::new(19).unwrap())));
filterer.complete_doesnt_pass(Some(ProcessEnd::Success));
filterer.complete_doesnt_pass(None);
@ -419,9 +416,9 @@ async fn complete_with_specific_signal_number() {
async fn complete_with_any_signal() {
let filterer = filt(&[filter("complete*=signal(*)")]).await;
filterer.complete_does_pass(Some(ProcessEnd::ExitSignal(SubSignal::Interrupt)));
filterer.complete_does_pass(Some(ProcessEnd::ExitSignal(SubSignal::Terminate)));
filterer.complete_does_pass(Some(ProcessEnd::ExitSignal(SubSignal::Custom(123))));
filterer.complete_does_pass(Some(ProcessEnd::ExitSignal(Signal::Interrupt)));
filterer.complete_does_pass(Some(ProcessEnd::ExitSignal(Signal::Terminate)));
filterer.complete_does_pass(Some(ProcessEnd::ExitSignal(Signal::Custom(123))));
filterer.complete_doesnt_pass(Some(ProcessEnd::ExitStop(NonZeroI32::new(63).unwrap())));
filterer.complete_doesnt_pass(Some(ProcessEnd::ExitError(NonZeroI64::new(63).unwrap())));
filterer.complete_doesnt_pass(Some(ProcessEnd::Success));

View File

@ -3,6 +3,7 @@
## Next (YYYY-MM-DD)
- Ditch MSRV policy. The `rust-version` indication will remain, for the minimum estimated Rust version for the code features used in the crate's own code, but dependencies may have already moved on. From now on, only latest stable is assumed and tested for. ([#510](https://github.com/watchexec/watchexec/pull/510))
- Split off `watchexec-events` and `watchexec-signals` crates.
## v2.1.1 (2023-02-14)

View File

@ -30,6 +30,14 @@ normalize-path = "0.2.0"
version = "2.0.1"
features = ["with-tokio"]
[dependencies.watchexec-events]
version = "1.0.0"
path = "../events"
[dependencies.watchexec-signals]
version = "1.0.0"
path = "../signals"
[dependencies.ignore-files]
version = "1.1.0"
path = "../ignore-files"

View File

@ -89,7 +89,9 @@ make anything else you may want:
and supervise a process while also listening for & acting on interventions such as sending signals.
- **Event sources**: [Filesystem](https://docs.rs/watchexec/2/watchexec/fs/index.html),
[Signals](https://docs.rs/watchexec/2/watchexec/signal/source/index.html), (more to come).
[Signals](https://docs.rs/watchexec/2/watchexec/signal/index.html),
[Keyboard](https://docs.rs/watchexec/2/watchexec/keyboard/index.html),
(more to come).
- Finding **[a common prefix](https://docs.rs/watchexec/2/watchexec/paths/fn.common_prefix.html)**
of a set of paths.
@ -117,6 +119,11 @@ There are also separate, standalone crates used to build Watchexec which you can
- **[Command Group](https://docs.rs/command-group)** augments the std and tokio `Command` with
support for process groups, portable between Unix and Windows.
- **[Event types](https://docs.rs/watchexec-events)** contains the event types used by Watchexec,
including the JSON format used for passing event data to child processes.
- **[Signal types](https://docs.rs/watchexec-signals)** contains the signal types used by Watchexec.
- **[Ignore files](https://docs.rs/ignore-files)** finds, parses, and interprets ignore files.
- **[Project Origins](https://docs.rs/project-origins)** finds the origin (or root) path of a

View File

@ -8,9 +8,9 @@ use watchexec::{
error::ReconfigError,
event::Event,
fs::Watcher,
signal::source::MainSignal,
ErrorHook, Watchexec,
};
use watchexec_signals::Signal;
// Run with: `env RUST_LOG=debug cargo run --example print_out`
#[tokio::main]
@ -46,13 +46,13 @@ async fn main() -> Result<()> {
.flat_map(Event::signals)
.collect::<Vec<_>>();
if sigs.iter().any(|sig| sig == &MainSignal::Interrupt) {
if sigs.iter().any(|sig| sig == &Signal::Interrupt) {
action.outcome(Outcome::Exit);
} else if sigs.iter().any(|sig| sig == &MainSignal::User1) {
} else if sigs.iter().any(|sig| sig == &Signal::User1) {
eprintln!("Switching to native for funsies");
config.file_watcher(Watcher::Native);
w.reconfigure(config)?;
} else if sigs.iter().any(|sig| sig == &MainSignal::User2) {
} else if sigs.iter().any(|sig| sig == &Signal::User2) {
eprintln!("Switching to polling for funsies");
config.file_watcher(Watcher::Poll(Duration::from_millis(50)));
w.reconfigure(config)?;

View File

@ -5,8 +5,9 @@ use miette::Result;
use tokio::sync::mpsc;
use watchexec::{
event::{Event, Priority, Tag},
signal::{self, source::MainSignal},
signal,
};
use watchexec_signals::Signal;
// Run with: `env RUST_LOG=debug cargo run --example signal`,
// then issue some signals to the printed PID, or hit e.g. Ctrl-C.
@ -22,7 +23,7 @@ async fn main() -> Result<()> {
while let Ok((event, priority)) = ev_r.recv().await {
tracing::info!("event {priority:?}: {event:?}");
if event.tags.contains(&Tag::Signal(MainSignal::Terminate)) {
if event.tags.contains(&Tag::Signal(Signal::Terminate)) {
exit(0);
}
}
@ -35,7 +36,7 @@ async fn main() -> Result<()> {
});
tracing::info!("PID is {}", std::process::id());
signal::source::worker(er_s.clone(), ev_s.clone()).await?;
signal::worker(er_s.clone(), ev_s.clone()).await?;
Ok(())
}

View File

@ -1,6 +1,6 @@
use std::time::Duration;
use crate::signal::process::SubSignal;
use watchexec_signals::Signal;
/// The outcome to execute when an action is triggered.
///
@ -34,7 +34,7 @@ pub enum Outcome {
/// Send this signal to the command.
///
/// This does not wait for the command to complete.
Signal(SubSignal),
Signal(Signal),
/// Clear the (terminal) screen.
Clear,

View File

@ -2,8 +2,9 @@ use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::trace;
use watchexec_signals::Signal;
use crate::{command::Supervisor, error::RuntimeError, signal::process::SubSignal};
use crate::{command::Supervisor, error::RuntimeError};
#[derive(Clone, Debug, Default)]
pub struct ProcessHolder(Arc<RwLock<Option<Supervisor>>>);
@ -36,7 +37,7 @@ impl ProcessHolder {
}
}
pub async fn signal(&self, sig: SubSignal) {
pub async fn signal(&self, sig: Signal) {
if let Some(p) = self.0.read().await.as_ref() {
trace!("signaling supervisor");
p.signal(sig).await;

View File

@ -10,6 +10,7 @@ use tokio::{
},
};
use tracing::{debug, debug_span, error, trace, Span};
use watchexec_signals::Signal;
use crate::{
action::{PostSpawn, PreSpawn},
@ -17,7 +18,6 @@ use crate::{
error::RuntimeError,
event::{Event, Priority, Source, Tag},
handler::{rte, HandlerLock},
signal::process::SubSignal,
};
use super::Process;
@ -25,7 +25,7 @@ use super::Process;
#[derive(Clone, Copy, Debug)]
enum Intervention {
Kill,
Signal(SubSignal),
Signal(Signal),
}
/// A task which supervises a sequence of processes.
@ -208,13 +208,13 @@ impl Supervisor {
/// Issues a signal to the process.
///
/// On Windows, this currently only supports [`SubSignal::ForceStop`].
/// On Windows, this currently only supports [`Signal::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) {
pub async fn signal(&self, signal: Signal) {
if cfg!(windows) {
if signal == SubSignal::ForceStop {
if signal == Signal::ForceStop {
self.intervene.send(Intervention::Kill).await.ok();
}
// else: https://github.com/watchexec/watchexec/issues/219

View File

@ -1,10 +1,10 @@
use miette::Diagnostic;
use thiserror::Error;
use watchexec_signals::Signal;
use crate::{
event::{Event, Priority},
fs::Watcher,
signal::process::SubSignal,
};
/// Errors which _may_ be recoverable, transient, or only affect a part of the operation, and should
@ -122,13 +122,13 @@ pub enum RuntimeError {
#[diagnostic(code(watchexec::runtime::process_doa))]
ProcessDeadOnArrival,
/// Error received when a [`SubSignal`] is unsupported
/// Error received when a [`Signal`] 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),
UnsupportedSignal(Signal),
/// Error received when there are no commands to run.
///

View File

@ -6,6 +6,10 @@ use tokio::sync::watch;
use crate::{action, fs, keyboard};
// compatibility re-export
#[deprecated(note = "use the `watchexec_signals` crate directly instead")]
pub use watchexec_signals::SignalParseError;
/// Errors occurring from reconfigs.
#[derive(Debug, Diagnostic, Error)]
#[non_exhaustive]
@ -27,33 +31,6 @@ pub enum ReconfigError {
KeyboardWatch(#[from] watch::error::SendError<keyboard::WorkingData>),
}
/// Error when parsing a signal from string.
#[derive(Debug, Diagnostic, Error)]
#[error("invalid signal `{src}`: {err}")]
#[diagnostic(code(watchexec::signal::process::parse), url(docsrs))]
pub struct SignalParseError {
// The string that was parsed.
#[source_code]
src: String,
// The error that occurred.
err: String,
// The span of the source which is in error.
#[label = "invalid signal"]
span: (usize, usize),
}
impl SignalParseError {
pub(crate) fn new(src: &str, err: &str) -> Self {
Self {
src: src.to_owned(),
err: err.to_owned(),
span: (0, src.len()),
}
}
}
/// Errors emitted by the filesystem watcher.
#[derive(Debug, Diagnostic, Error)]
#[non_exhaustive]

View File

@ -5,6 +5,7 @@ use tokio::{
sync::{mpsc, oneshot, watch},
};
use tracing::trace;
pub use watchexec_events::Keyboard;
use crate::{
error::{CriticalError, KeyboardWatcherError, RuntimeError},
@ -21,14 +22,6 @@ pub struct WorkingData {
pub eof: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
/// Enumeration of different keyboard events
pub enum Keyboard {
/// Event representing an 'end of file' on stdin
Eof,
}
/// Launch the filesystem event worker.
///
/// While you can run several, you should only have one.

View File

@ -101,7 +101,6 @@
pub mod action;
pub mod command;
pub mod error;
pub mod event;
pub mod filter;
pub mod fs;
pub mod keyboard;
@ -113,6 +112,13 @@ pub mod config;
pub mod handler;
mod watchexec;
// compatibility
#[deprecated(
note = "use the `watchexec-events` crate directly instead",
since = "2.1.0"
)]
pub use watchexec_events as event;
#[doc(inline)]
pub use crate::watchexec::{ErrorHook, Watchexec};

View File

@ -1,4 +1,176 @@
//! Signal handling.
//! Event source for signals / notifications sent to the main process.
pub mod process;
pub mod source;
use async_priority_channel as priority;
use tokio::{select, sync::mpsc};
use tracing::{debug, trace};
use watchexec_signals::Signal;
use crate::{
error::{CriticalError, RuntimeError},
event::{Event, Priority, Source, Tag},
};
/// Compatibility shim for the old `watchexec::signal::process` module.
pub mod process {
#[deprecated(
note = "use the `watchexec-signals` crate directly instead",
since = "2.1.0"
)]
pub use watchexec_signals::Signal as SubSignal;
}
/// Compatibility shim for the old `watchexec::signal::source` module.
pub mod source {
#[deprecated(
note = "use `watchexec::signal::worker` directly instead",
since = "2.1.0"
)]
pub use super::worker;
#[deprecated(
note = "use the `watchexec-signals` crate directly instead",
since = "2.1.0"
)]
pub use watchexec_signals::Signal as MainSignal;
}
/// Launch the signal event worker.
///
/// While you _can_ run several, you **must** only have one. This may be enforced later.
///
/// # Examples
///
/// Direct usage:
///
/// ```no_run
/// use tokio::sync::mpsc;
/// use async_priority_channel as priority;
/// use watchexec::signal::source::worker;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let (ev_s, _) = priority::bounded(1024);
/// let (er_s, _) = mpsc::channel(64);
///
/// worker(er_s, ev_s).await?;
/// Ok(())
/// }
/// ```
pub async fn worker(
errors: mpsc::Sender<RuntimeError>,
events: priority::Sender<Event, Priority>,
) -> Result<(), CriticalError> {
imp_worker(errors, events).await
}
#[cfg(unix)]
async fn imp_worker(
errors: mpsc::Sender<RuntimeError>,
events: priority::Sender<Event, Priority>,
) -> Result<(), CriticalError> {
use tokio::signal::unix::{signal, SignalKind};
debug!("launching unix signal worker");
macro_rules! listen {
($sig:ident) => {{
trace!(kind=%stringify!($sig), "listening for unix signal");
signal(SignalKind::$sig()).map_err(|err| CriticalError::IoError {
about: concat!("setting ", stringify!($sig), " signal listener"), err
})?
}}
}
let mut s_hangup = listen!(hangup);
let mut s_interrupt = listen!(interrupt);
let mut s_quit = listen!(quit);
let mut s_terminate = listen!(terminate);
let mut s_user1 = listen!(user_defined1);
let mut s_user2 = listen!(user_defined2);
loop {
let sig = select!(
_ = s_hangup.recv() => Signal::Hangup,
_ = s_interrupt.recv() => Signal::Interrupt,
_ = s_quit.recv() => Signal::Quit,
_ = s_terminate.recv() => Signal::Terminate,
_ = s_user1.recv() => Signal::User1,
_ = s_user2.recv() => Signal::User2,
);
debug!(?sig, "received unix signal");
send_event(errors.clone(), events.clone(), sig).await?;
}
}
#[cfg(windows)]
async fn imp_worker(
errors: mpsc::Sender<RuntimeError>,
events: priority::Sender<Event, Priority>,
) -> Result<(), CriticalError> {
use tokio::signal::windows::{ctrl_break, ctrl_c};
debug!("launching windows signal worker");
macro_rules! listen {
($sig:ident) => {{
trace!(kind=%stringify!($sig), "listening for windows process notification");
$sig().map_err(|err| CriticalError::IoError {
about: concat!("setting ", stringify!($sig), " signal listener"), err
})?
}}
}
let mut sigint = listen!(ctrl_c);
let mut sigbreak = listen!(ctrl_break);
loop {
let sig = select!(
_ = sigint.recv() => Signal::Interrupt,
_ = sigbreak.recv() => Signal::Terminate,
);
debug!(?sig, "received windows process notification");
send_event(errors.clone(), events.clone(), sig).await?;
}
}
async fn send_event(
errors: mpsc::Sender<RuntimeError>,
events: priority::Sender<Event, Priority>,
sig: Signal,
) -> Result<(), CriticalError> {
let tags = vec![
Tag::Source(if sig == Signal::Interrupt {
Source::Keyboard
} else {
Source::Os
}),
Tag::Signal(sig),
];
let event = Event {
tags,
metadata: Default::default(),
};
trace!(?event, "processed signal into event");
if let Err(err) = events
.send(
event,
match sig {
Signal::Interrupt | Signal::Terminate => Priority::Urgent,
_ => Priority::High,
},
)
.await
{
errors
.send(RuntimeError::EventChannelSend {
ctx: "signals",
err,
})
.await?;
}
Ok(())
}

View File

@ -1,210 +0,0 @@
//! Event source for signals / notifications sent to the main process.
use async_priority_channel as priority;
use tokio::{select, sync::mpsc};
use tracing::{debug, trace};
use crate::{
error::{CriticalError, RuntimeError},
event::{Event, Priority, Source, Tag},
};
/// A notification sent to the main (watchexec) process.
///
/// On Windows, only [`Interrupt`][MainSignal::Interrupt] and [`Terminate`][MainSignal::Terminate]
/// will be produced: they are respectively `Ctrl-C` (SIGINT) and `Ctrl-Break` (SIGBREAK).
/// `Ctrl-Close` (the equivalent of `SIGHUP` on Unix, without the semantics of configuration reload)
/// is not supported, and on console close the process will be terminated by the OS.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MainSignal {
/// Received when the terminal is disconnected.
///
/// On Unix, this is `SIGHUP`. On Windows, it is not produced.
///
/// This signal is available because it is a common signal used to reload configuration files,
/// and it is reasonable that either watchexec could make use of it, or that it should be passed
/// on to a sub process.
Hangup,
/// Received to indicate that the process should stop.
///
/// On Unix, this is `SIGINT`. On Windows, this is `Ctrl+C`.
///
/// This signal is generally produced by the user, so it may be handled differently than a
/// termination.
Interrupt,
/// Received to cause the process to stop and the kernel to dump its core.
///
/// On Unix, this is `SIGQUIT`. On Windows, it is not produced.
///
/// This signal is available because it is reasonable that it could be passed on to a sub
/// process, rather than terminate watchexec itself.
Quit,
/// Received to indicate that the process should stop.
///
/// On Unix, this is `SIGTERM`. On Windows, this is `Ctrl+Break`.
///
/// This signal is available for cleanup, but will generally not be passed on to a sub process
/// with no other consequence: it is expected the main process should terminate.
Terminate,
/// Received for a user or application defined purpose.
///
/// On Unix, this is `SIGUSR1`. On Windows, it is not produced.
///
/// This signal is available because it is expected that it most likely should be passed on to a
/// sub process or trigger a particular action within watchexec.
User1,
/// Received for a user or application defined purpose.
///
/// On Unix, this is `SIGUSR2`. On Windows, it is not produced.
///
/// This signal is available because it is expected that it most likely should be passed on to a
/// sub process or trigger a particular action within watchexec.
User2,
}
/// Launch the signal event worker.
///
/// While you _can_ run several, you **must** only have one. This may be enforced later.
///
/// # Examples
///
/// Direct usage:
///
/// ```no_run
/// use tokio::sync::mpsc;
/// use async_priority_channel as priority;
/// use watchexec::signal::source::worker;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let (ev_s, _) = priority::bounded(1024);
/// let (er_s, _) = mpsc::channel(64);
///
/// worker(er_s, ev_s).await?;
/// Ok(())
/// }
/// ```
pub async fn worker(
errors: mpsc::Sender<RuntimeError>,
events: priority::Sender<Event, Priority>,
) -> Result<(), CriticalError> {
imp_worker(errors, events).await
}
#[cfg(unix)]
async fn imp_worker(
errors: mpsc::Sender<RuntimeError>,
events: priority::Sender<Event, Priority>,
) -> Result<(), CriticalError> {
use tokio::signal::unix::{signal, SignalKind};
debug!("launching unix signal worker");
macro_rules! listen {
($sig:ident) => {{
trace!(kind=%stringify!($sig), "listening for unix signal");
signal(SignalKind::$sig()).map_err(|err| CriticalError::IoError {
about: concat!("setting ", stringify!($sig), " signal listener"), err
})?
}}
}
let mut s_hangup = listen!(hangup);
let mut s_interrupt = listen!(interrupt);
let mut s_quit = listen!(quit);
let mut s_terminate = listen!(terminate);
let mut s_user1 = listen!(user_defined1);
let mut s_user2 = listen!(user_defined2);
loop {
let sig = select!(
_ = s_hangup.recv() => MainSignal::Hangup,
_ = s_interrupt.recv() => MainSignal::Interrupt,
_ = s_quit.recv() => MainSignal::Quit,
_ = s_terminate.recv() => MainSignal::Terminate,
_ = s_user1.recv() => MainSignal::User1,
_ = s_user2.recv() => MainSignal::User2,
);
debug!(?sig, "received unix signal");
send_event(errors.clone(), events.clone(), sig).await?;
}
}
#[cfg(windows)]
async fn imp_worker(
errors: mpsc::Sender<RuntimeError>,
events: priority::Sender<Event, Priority>,
) -> Result<(), CriticalError> {
use tokio::signal::windows::{ctrl_break, ctrl_c};
debug!("launching windows signal worker");
macro_rules! listen {
($sig:ident) => {{
trace!(kind=%stringify!($sig), "listening for windows process notification");
$sig().map_err(|err| CriticalError::IoError {
about: concat!("setting ", stringify!($sig), " signal listener"), err
})?
}}
}
let mut sigint = listen!(ctrl_c);
let mut sigbreak = listen!(ctrl_break);
loop {
let sig = select!(
_ = sigint.recv() => MainSignal::Interrupt,
_ = sigbreak.recv() => MainSignal::Terminate,
);
debug!(?sig, "received windows process notification");
send_event(errors.clone(), events.clone(), sig).await?;
}
}
async fn send_event(
errors: mpsc::Sender<RuntimeError>,
events: priority::Sender<Event, Priority>,
sig: MainSignal,
) -> Result<(), CriticalError> {
let tags = vec![
Tag::Source(if sig == MainSignal::Interrupt {
Source::Keyboard
} else {
Source::Os
}),
Tag::Signal(sig),
];
let event = Event {
tags,
metadata: Default::default(),
};
trace!(?event, "processed signal into event");
if let Err(err) = events
.send(
event,
match sig {
MainSignal::Interrupt | MainSignal::Terminate => Priority::Urgent,
_ => Priority::High,
},
)
.await
{
errors
.send(RuntimeError::EventChannelSend {
ctx: "signals",
err,
})
.await?;
}
Ok(())
}

View File

@ -0,0 +1,6 @@
# Changelog
## Next (YYYY-MM-DD)
- Split off new `watchexec-signals` crate (this one), to have a lightweight library that can parse
and represent signals as handled by Watchexec.

38
crates/signals/Cargo.toml Normal file
View File

@ -0,0 +1,38 @@
[package]
name = "watchexec-signals"
version = "1.0.0"
authors = ["Félix Saparelli <felix@passcod.name>"]
license = "Apache-2.0 OR MIT"
description = "Watchexec's signal types"
keywords = ["watchexec", "signal"]
documentation = "https://docs.rs/watchexec-signals"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
rust-version = "1.61.0"
edition = "2021"
[dependencies.miette]
version = "5.3.0"
optional = true
[dependencies.thiserror]
version = "1.0.26"
optional = true
[dependencies.serde]
version = "1.0.152"
optional = true
features = ["derive"]
[target.'cfg(unix)'.dependencies.nix]
version = "0.26.2"
features = ["signal"]
[features]
default = ["fromstr", "miette"]
fromstr = ["dep:thiserror"]
miette = ["dep:miette"]
serde = ["dep:serde"]

24
crates/signals/README.md Normal file
View File

@ -0,0 +1,24 @@
# watchexec-signals
_Watchexec's signal type._
- **[API documentation][docs]**.
- Licensed under [Apache 2.0][license] or [MIT](https://passcod.mit-license.org).
- Status: maintained.
[docs]: https://docs.rs/watchexec-signals
[license]: ../../LICENSE
```rust
use watchexec_signals::Signal;
fn main() {
assert_eq!(Signal::from_str("SIGINT").unwrap(), Signal::Interrupt);
}
```
## Features
- `serde`: enables serde support.
- `fromstr`: enables `FromStr` support (default).
- `miette`: enables miette (rich diagnostics) support (default).

View File

@ -0,0 +1,10 @@
pre-release-commit-message = "release: signals v{{version}}"
tag-prefix = "signals-"
tag-message = "watchexec-signals {{version}}"
[[pre-release-replacements]]
file = "CHANGELOG.md"
search = "^## Next.*$"
replace = "## Next (YYYY-MM-DD)\n\n## v{{version}} ({{date}})"
prerelease = true
max = 1

View File

@ -1,22 +1,41 @@
//! Types for cross-platform and cross-purpose handling of subprocess signals.
//! Notifications (signals or Windows control events) sent to a process.
//!
//! This signal type in Watchexec is used for any of:
//! - signals sent to the main process by some external actor,
//! - signals received from a sub process by the main process,
//! - signals sent to a sub process by Watchexec.
//!
//! ## Features
//!
//! - `fromstr`: Enables parsing of signals from strings.
//! - `serde`: Enables [`serde`][serde] support. Note that this is stricter than string parsing.
//! - `miette`: Enables [`miette`][miette] support for [`SignalParseError`][SignalParseError].
use std::fmt;
#[cfg(feature = "fromstr")]
use std::str::FromStr;
#[cfg(unix)]
use nix::sys::signal::Signal as NixSignal;
use crate::error::SignalParseError;
use super::source::MainSignal;
/// A notification sent to a subprocess.
/// A notification sent to a process.
///
/// On Windows, only some signals are supported, as described. Others will be ignored.
///
/// On Unix, there are several "first-class" signals which have their own variants, and a generic
/// [`Custom`][SubSignal::Custom] variant which can be used to send arbitrary signals.
/// [`Custom`][Signal::Custom] variant which can be used to send arbitrary signals.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SubSignal {
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(
from = "serde_support::SerdeSignal",
into = "serde_support::SerdeSignal"
)
)]
pub enum Signal {
/// Indicate that the terminal is disconnected.
///
/// On Unix, this is `SIGHUP`. On Windows, this is ignored for now but may be supported in the
@ -79,36 +98,40 @@ pub enum SubSignal {
///
/// Invalid signals on the current platform will be ignored. Does nothing on Windows.
///
/// The special value `0` is used to indicate an unknown signal. That is, a signal was received
/// or parsed, but it is not known which. This is not a usual case, and should in general be
/// ignored rather than hard-erroring.
///
/// # Examples
///
/// ```
/// # #[cfg(unix)]
/// # {
/// use watchexec::signal::process::SubSignal;
/// use nix::sys::signal::Signal;
/// assert_eq!(SubSignal::Custom(6), SubSignal::from(Signal::SIGABRT as i32));
/// use watchexec_signals::Signal;
/// use nix::sys::signal::Signal as NixSignal;
/// assert_eq!(Signal::Custom(6), Signal::from(NixSignal::SIGABRT as i32));
/// # }
/// ```
///
/// On Unix the [`from_nix`][SubSignal::from_nix] method should be preferred if converting from
/// On Unix the [`from_nix`][Signal::from_nix] method should be preferred if converting from
/// Nix's `Signal` type:
///
/// ```
/// # #[cfg(unix)]
/// # {
/// use watchexec::signal::process::SubSignal;
/// use nix::sys::signal::Signal;
/// assert_eq!(SubSignal::Custom(6), SubSignal::from_nix(Signal::SIGABRT));
/// use watchexec_signals::Signal;
/// use nix::sys::signal::Signal as NixSignal;
/// assert_eq!(Signal::Custom(6), Signal::from_nix(NixSignal::SIGABRT));
/// # }
/// ```
Custom(i32),
}
impl SubSignal {
/// Converts to a [`nix::Signal`][command_group::Signal] if possible.
impl Signal {
/// Converts to a [`nix::Signal`][NixSignal] if possible.
///
/// This will return `None` if the signal is not supported on the current platform (only for
/// [`Custom`][SubSignal::Custom], as the first-class ones are always supported).
/// [`Custom`][Signal::Custom], as the first-class ones are always supported).
#[cfg(unix)]
#[must_use]
pub fn to_nix(self) -> Option<NixSignal> {
@ -124,7 +147,7 @@ impl SubSignal {
}
}
/// Converts from a [`nix::Signal`][command_group::Signal].
/// Converts from a [`nix::Signal`][NixSignal].
#[cfg(unix)]
#[allow(clippy::missing_const_for_fn)]
#[must_use]
@ -142,20 +165,7 @@ impl SubSignal {
}
}
impl From<MainSignal> for SubSignal {
fn from(main: MainSignal) -> Self {
match main {
MainSignal::Hangup => Self::Hangup,
MainSignal::Interrupt => Self::Interrupt,
MainSignal::Quit => Self::Quit,
MainSignal::Terminate => Self::Terminate,
MainSignal::User1 => Self::User1,
MainSignal::User2 => Self::User2,
}
}
}
impl From<i32> for SubSignal {
impl From<i32> for Signal {
/// Converts from a raw signal number.
///
/// This uses hardcoded numbers for the first-class signals.
@ -173,7 +183,8 @@ impl From<i32> for SubSignal {
}
}
impl SubSignal {
#[cfg(feature = "fromstr")]
impl Signal {
/// Parse the input as a unix signal.
///
/// This parses the input as a signal name, or a signal number, in a case-insensitive manner.
@ -184,14 +195,14 @@ impl SubSignal {
/// falls back to a hardcoded approximation instead of looking up signal tables (via [`nix`]).
///
/// ```
/// # use watchexec::signal::process::SubSignal;
/// assert_eq!(SubSignal::Hangup, SubSignal::from_unix_str("hup").unwrap());
/// assert_eq!(SubSignal::Interrupt, SubSignal::from_unix_str("SIGINT").unwrap());
/// assert_eq!(SubSignal::ForceStop, SubSignal::from_unix_str("Kill").unwrap());
/// # use watchexec_signals::Signal;
/// assert_eq!(Signal::Hangup, Signal::from_unix_str("hup").unwrap());
/// assert_eq!(Signal::Interrupt, Signal::from_unix_str("SIGINT").unwrap());
/// assert_eq!(Signal::ForceStop, Signal::from_unix_str("Kill").unwrap());
/// ```
///
/// Using [`FromStr`] is recommended for practical use, as it will also parse Windows control
/// events, see [`SubSignal::from_windows_str`].
/// events, see [`Signal::from_windows_str`].
pub fn from_unix_str(s: &str) -> Result<Self, SignalParseError> {
Self::from_unix_str_impl(s)
}
@ -243,10 +254,10 @@ impl SubSignal {
/// - `STOP`, `FORCE-STOP` for a forced stop. This is also mapped to `KILL` and `SIGKILL`.
///
/// ```
/// # use watchexec::signal::process::SubSignal;
/// assert_eq!(SubSignal::Hangup, SubSignal::from_windows_str("ctrl+close").unwrap());
/// assert_eq!(SubSignal::Interrupt, SubSignal::from_windows_str("C").unwrap());
/// assert_eq!(SubSignal::ForceStop, SubSignal::from_windows_str("Stop").unwrap());
/// # use watchexec_signals::Signal;
/// assert_eq!(Signal::Hangup, Signal::from_windows_str("ctrl+close").unwrap());
/// assert_eq!(Signal::Interrupt, Signal::from_windows_str("C").unwrap());
/// assert_eq!(Signal::ForceStop, Signal::from_windows_str("Stop").unwrap());
/// ```
///
/// Using [`FromStr`] is recommended for practical use, as it will fall back to parsing as a
@ -262,10 +273,126 @@ impl SubSignal {
}
}
impl FromStr for SubSignal {
#[cfg(feature = "fromstr")]
impl FromStr for Signal {
type Err = SignalParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_windows_str(s).or_else(|err| Self::from_unix_str(s).map_err(|_| err))
}
}
/// Error when parsing a signal from string.
#[cfg(feature = "fromstr")]
#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
#[derive(Debug, thiserror::Error)]
#[error("invalid signal `{src}`: {err}")]
pub struct SignalParseError {
// The string that was parsed.
#[cfg_attr(feature = "miette", source_code)]
src: String,
// The error that occurred.
err: String,
// The span of the source which is in error.
#[cfg_attr(feature = "miette", label = "invalid signal")]
span: (usize, usize),
}
#[cfg(feature = "fromstr")]
impl SignalParseError {
pub fn new(src: &str, err: &str) -> Self {
Self {
src: src.to_owned(),
err: err.to_owned(),
span: (0, src.len()),
}
}
}
impl fmt::Display for Signal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match (self, cfg!(windows)) {
(Self::Hangup, false) => "SIGHUP",
(Self::Hangup, true) => "CTRL-CLOSE",
(Self::ForceStop, false) => "SIGKILL",
(Self::ForceStop, true) => "STOP",
(Self::Interrupt, false) => "SIGINT",
(Self::Interrupt, true) => "CTRL-C",
(Self::Quit, _) => "SIGQUIT",
(Self::Terminate, false) => "SIGTERM",
(Self::Terminate, true) => "CTRL-BREAK",
(Self::User1, _) => "SIGUSR1",
(Self::User2, _) => "SIGUSR2",
(Self::Custom(n), _) => {
return write!(f, "{}", n);
}
}
)
}
}
#[cfg(feature = "serde")]
mod serde_support {
use super::*;
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum SerdeSignal {
Named(NamedSignal),
Number(i32),
}
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum NamedSignal {
#[serde(rename = "SIGHUP")]
Hangup,
#[serde(rename = "SIGKILL")]
ForceStop,
#[serde(rename = "SIGINT")]
Interrupt,
#[serde(rename = "SIGQUIT")]
Quit,
#[serde(rename = "SIGTERM")]
Terminate,
#[serde(rename = "SIGUSR1")]
User1,
#[serde(rename = "SIGUSR2")]
User2,
}
impl From<Signal> for SerdeSignal {
fn from(signal: Signal) -> Self {
match signal {
Signal::Hangup => Self::Named(NamedSignal::Hangup),
Signal::Interrupt => Self::Named(NamedSignal::Interrupt),
Signal::Quit => Self::Named(NamedSignal::Quit),
Signal::Terminate => Self::Named(NamedSignal::Terminate),
Signal::User1 => Self::Named(NamedSignal::User1),
Signal::User2 => Self::Named(NamedSignal::User2),
Signal::ForceStop => Self::Named(NamedSignal::ForceStop),
Signal::Custom(number) => Self::Number(number),
}
}
}
impl From<SerdeSignal> for Signal {
fn from(signal: SerdeSignal) -> Self {
match signal {
SerdeSignal::Named(NamedSignal::Hangup) => Signal::Hangup,
SerdeSignal::Named(NamedSignal::ForceStop) => Signal::ForceStop,
SerdeSignal::Named(NamedSignal::Interrupt) => Signal::Interrupt,
SerdeSignal::Named(NamedSignal::Quit) => Signal::Quit,
SerdeSignal::Named(NamedSignal::Terminate) => Signal::Terminate,
SerdeSignal::Named(NamedSignal::User1) => Signal::User1,
SerdeSignal::Named(NamedSignal::User2) => Signal::User2,
SerdeSignal::Number(number) => Signal::Custom(number),
}
}
}
}

View File

@ -4,7 +4,7 @@
.SH NAME
watchexec \- Execute commands when watched files change
.SH SYNOPSIS
\fBwatchexec\fR [\fB\-w\fR|\fB\-\-watch\fR] [\fB\-c\fR|\fB\-\-clear\fR] [\fB\-o\fR|\fB\-\-on\-busy\-update\fR] [\fB\-r\fR|\fB\-\-restart\fR] [\fB\-s\fR|\fB\-\-signal\fR] [\fB\-\-stop\-signal\fR] [\fB\-\-stop\-timeout\fR] [\fB\-\-debounce\fR] [\fB\-\-stdin\-quit\fR] [\fB\-\-no\-vcs\-ignore\fR] [\fB\-\-no\-project\-ignore\fR] [\fB\-\-no\-global\-ignore\fR] [\fB\-\-no\-default\-ignore\fR] [\fB\-p\fR|\fB\-\-postpone\fR] [\fB\-\-delay\-run\fR] [\fB\-\-poll\fR] [\fB\-\-shell\fR] [\fB\-n \fR] [\fB\-\-no\-environment\fR] [\fB\-E\fR|\fB\-\-env\fR] [\fB\-\-no\-process\-group\fR] [\fB\-N\fR|\fB\-\-notify\fR] [\fB\-\-project\-origin\fR] [\fB\-\-workdir\fR] [\fB\-e\fR|\fB\-\-exts\fR] [\fB\-f\fR|\fB\-\-filter\fR] [\fB\-\-filter\-file\fR] [\fB\-i\fR|\fB\-\-ignore\fR] [\fB\-\-ignore\-file\fR] [\fB\-\-fs\-events\fR] [\fB\-\-no\-meta\fR] [\fB\-\-print\-events\fR] [\fB\-v\fR|\fB\-\-verbose\fR]... [\fB\-\-log\-file\fR] [\fB\-\-manpage\fR] [\fB\-\-completions\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fICOMMAND\fR]
\fBwatchexec\fR [\fB\-w\fR|\fB\-\-watch\fR] [\fB\-c\fR|\fB\-\-clear\fR] [\fB\-o\fR|\fB\-\-on\-busy\-update\fR] [\fB\-r\fR|\fB\-\-restart\fR] [\fB\-s\fR|\fB\-\-signal\fR] [\fB\-\-stop\-signal\fR] [\fB\-\-stop\-timeout\fR] [\fB\-\-debounce\fR] [\fB\-\-stdin\-quit\fR] [\fB\-\-no\-vcs\-ignore\fR] [\fB\-\-no\-project\-ignore\fR] [\fB\-\-no\-global\-ignore\fR] [\fB\-\-no\-default\-ignore\fR] [\fB\-p\fR|\fB\-\-postpone\fR] [\fB\-\-delay\-run\fR] [\fB\-\-poll\fR] [\fB\-\-shell\fR] [\fB\-n \fR] [\fB\-\-no\-environment\fR] [\fB\-\-emit\-events\-to\fR] [\fB\-E\fR|\fB\-\-env\fR] [\fB\-\-no\-process\-group\fR] [\fB\-N\fR|\fB\-\-notify\fR] [\fB\-\-project\-origin\fR] [\fB\-\-workdir\fR] [\fB\-e\fR|\fB\-\-exts\fR] [\fB\-f\fR|\fB\-\-filter\fR] [\fB\-\-filter\-file\fR] [\fB\-i\fR|\fB\-\-ignore\fR] [\fB\-\-ignore\-file\fR] [\fB\-\-fs\-events\fR] [\fB\-\-no\-meta\fR] [\fB\-\-print\-events\fR] [\fB\-v\fR|\fB\-\-verbose\fR]... [\fB\-\-log\-file\fR] [\fB\-\-manpage\fR] [\fB\-\-completions\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fICOMMAND\fR]
.SH DESCRIPTION
Execute commands when watched files change.
.PP
@ -125,12 +125,12 @@ or user ignore files, like \*(Aq~/.gitignore\*(Aq or \*(Aq~/.config/watchexec/ig
Supported project ignore files:
\- Git: .gitignore at project root and child directories, .git/info/exclude, and the file pointed to by `core.excludesFile` in .git/config.
\- Mercurial: .hgignore at project root and child directories.
\- Bazaar: .bzrignore at project root.
\- Darcs: _darcs/prefs/boring
\- Fossil: .fossil\-settings/ignore\-glob
\- Ripgrep/Watchexec/generic: .ignore at project root and child directories.
\- Git: .gitignore at project root and child directories, .git/info/exclude, and the file pointed to by `core.excludesFile` in .git/config.
\- Mercurial: .hgignore at project root and child directories.
\- Bazaar: .bzrignore at project root.
\- Darcs: _darcs/prefs/boring
\- Fossil: .fossil\-settings/ignore\-glob
\- Ripgrep/Watchexec/generic: .ignore at project root and child directories.
VCS ignore files (Git, Mercurial, Bazaar, Darcs, Fossil) are only used if the corresponding
VCS is discovered to be in use for the project/origin. For example, a .bzrignore in a Git
@ -148,10 +148,10 @@ This disables loading of global or user ignore files, like \*(Aq~/.gitignore\*(A
Supported global ignore files
\- Git (if core.excludesFile is set): the file at that path
\- Git (otherwise): the first found of $XDG_CONFIG_HOME/git/ignore, %APPDATA%/.gitignore, %USERPROFILE%/.gitignore, $HOME/.config/git/ignore, $HOME/.gitignore.
\- Bazaar: the first found of %APPDATA%/Bazzar/2.0/ignore, $HOME/.bazaar/ignore.
\- Watchexec: the first found of $XDG_CONFIG_HOME/watchexec/ignore, %APPDATA%/watchexec/ignore, %USERPROFILE%/.watchexec/ignore, $HOME/.watchexec/ignore.
\- Git (if core.excludesFile is set): the file at that path
\- Git (otherwise): the first found of $XDG_CONFIG_HOME/git/ignore, %APPDATA%/.gitignore, %USERPROFILE%/.gitignore, $HOME/.config/git/ignore, $HOME/.gitignore.
\- Bazaar: the first found of %APPDATA%/Bazzar/2.0/ignore, $HOME/.bazaar/ignore.
\- Watchexec: the first found of $XDG_CONFIG_HOME/watchexec/ignore, %APPDATA%/watchexec/ignore, %USERPROFILE%/.watchexec/ignore, $HOME/.watchexec/ignore.
Like for project files, Git and Bazaar global files will only be used for the corresponding
VCS as used in the project.
@ -229,6 +229,105 @@ Shorthand for \*(Aq\-\-emit\-events=none\*(Aq
This is the old way to disable event emission into the environment. See \*(Aq\-\-emit\-events\*(Aq for more.
.TP
\fB\-\-emit\-events\-to\fR=\fIMODE\fR
Configure event emission
Watchexec emits event information when running a command, which can be used by the command
to target specific changed files.
One thing to take care with is assuming inherent behaviour where there is only chance.
Notably, it could appear as if the `RENAMED` variable contains both the original and the new
path being renamed. In previous versions, it would even appear on some platforms as if the
original always came before the new. However, none of this was true. It\*(Aqs impossible to
reliably and portably know which changed path is the old or new, "half" renames may appear
(only the original, only the new), "unknown" renames may appear (change was a rename, but
whether it was the old or new isn\*(Aqt known), rename events might split across two debouncing
boundaries, and so on.
This option controls where that information is emitted. It defaults to \*(Aqenvironment\*(Aq, which
sets environment variables with the paths of the affected files, for filesystem events:
$WATCHEXEC_COMMON_PATH is set to the longest common path of all of the below variables,
and so should be prepended to each path to obtain the full/real path. Then:
\- $WATCHEXEC_CREATED_PATH is set when files/folders were created
\- $WATCHEXEC_REMOVED_PATH is set when files/folders were removed
\- $WATCHEXEC_RENAMED_PATH is set when files/folders were renamed
\- $WATCHEXEC_WRITTEN_PATH is set when files/folders were modified
\- $WATCHEXEC_META_CHANGED_PATH is set when files/folders\*(Aq metadata were modified
\- $WATCHEXEC_OTHERWISE_CHANGED_PATH is set for every other kind of pathed event
Multiple paths are separated by the system path separator, \*(Aq;\*(Aq on Windows and \*(Aq:\*(Aq on unix.
Within each variable, paths are deduplicated and sorted in binary order (i.e. neither
Unicode nor locale aware).
This is the legacy mode and will be deprecated and removed in the future. The environment of
a process is a very restricted space, while also limited in what it can usefully represent.
Large numbers of files will either cause the environment to be truncated, or may error or
crash the process entirely.
Two new modes are available: \*(Aqstdin\*(Aq writes absolute paths to the stdin of the command,
one per line, each prefixed with `create:`, `remove:`, `rename:`, `modify:`, or `other:`,
then closes the handle; \*(Aqfile\*(Aq writes the same thing to a temporary file, and its path is
given with the $WATCHEXEC_EVENTS_FILE environment variable.
There are also two JSON modes, which are based on JSON objects and can represent the full
set of events Watchexec handles. Here\*(Aqs an example of a folder being created on Linux:
```json
{
"tags": [
{
"kind": "path",
"absolute": "/home/user/your/new\-folder",
"filetype": "dir"
},
{
"kind": "fs",
"simple": "create",
"full": "Create(Folder)"
},
{
"kind": "source",
"source": "filesystem",
}
],
"metadata": {
"notify\-backend": "inotify"
}
}
```
The fields are as follows:
\- `tags`, structured event data.
\- `tags[].kind`, which can be:
* \*(Aqpath\*(Aq, along with:
+ `absolute`, an absolute path.
+ `filetype`, a file type if known (\*(Aqdir\*(Aq, \*(Aqfile\*(Aq, \*(Aqsymlink\*(Aq, \*(Aqother\*(Aq).
* \*(Aqfs\*(Aq:
+ `simple`, the "simple" event type (\*(Aqaccess\*(Aq, \*(Aqcreate\*(Aq, \*(Aqmodify\*(Aq, \*(Aqremove\*(Aq, or \*(Aqother\*(Aq).
+ `full`, the "full" event type, which is too complex to fully describe here, but looks like \*(AqGeneral(Precise(Specific))\*(Aq.
* \*(Aqsource\*(Aq, along with:
+ `source`, the source of the event (\*(Aqfilesystem\*(Aq, \*(Aqkeyboard\*(Aq, \*(Aqmouse\*(Aq, \*(Aqos\*(Aq, \*(Aqtime\*(Aq, \*(Aqinternal\*(Aq).
* \*(Aqkeyboard\*(Aq, along with:
+ `keycode`. Currently only the value \*(Aqeof\*(Aq is supported.
* \*(Aqprocess\*(Aq, for events caused by processes:
+ `pid`, the process ID.
* \*(Aqsignal\*(Aq, for signals sent to Watchexec:
+ `signal`, the normalised signal name (\*(Aqhangup\*(Aq, \*(Aqinterrupt\*(Aq, \*(Aqquit\*(Aq, \*(Aqterminate\*(Aq, \*(Aquser1\*(Aq, \*(Aquser2\*(Aq).
* \*(Aqcompletion\*(Aq, for when a command ends:
+ `disposition`, the exit disposition (\*(Aqsuccess\*(Aq, \*(Aqerror\*(Aq, \*(Aqsignal\*(Aq, \*(Aqstop\*(Aq, \*(Aqexception\*(Aq, \*(Aqcontinued\*(Aq).
+ `code`, the exit, signal, stop, or exception code.
\- `metadata`, additional information about the event.
The \*(Aqjson\-stdin\*(Aq mode will emit JSON events to the standard input of the command, one per
line, then close stdin. The \*(Aqjson\-file\*(Aq mode will create a temporary file, write the
events to it, and provide the path to the file with the $WATCHEXEC_EVENTS_FILE
environment variable.
Finally, the special \*(Aqnone\*(Aq mode will disable event emission entirely.
.TP
\fB\-E\fR, \fB\-\-env\fR=\fIKEY=VALUE\fR
Add env vars to the command

View File

@ -11,10 +11,10 @@ watchexec - Execute commands when watched files change
\[**\--no-project-ignore**\] \[**\--no-global-ignore**\]
\[**\--no-default-ignore**\] \[**-p**\|**\--postpone**\]
\[**\--delay-run**\] \[**\--poll**\] \[**\--shell**\] \[**-n **\]
\[**\--no-environment**\] \[**-E**\|**\--env**\]
\[**\--no-process-group**\] \[**-N**\|**\--notify**\]
\[**\--project-origin**\] \[**\--workdir**\] \[**-e**\|**\--exts**\]
\[**-f**\|**\--filter**\] \[**\--filter-file**\]
\[**\--no-environment**\] \[**\--emit-events-to**\]
\[**-E**\|**\--env**\] \[**\--no-process-group**\]
\[**-N**\|**\--notify**\] \[**\--project-origin**\] \[**\--workdir**\]
\[**-e**\|**\--exts**\] \[**-f**\|**\--filter**\] \[**\--filter-file**\]
\[**-i**\|**\--ignore**\] \[**\--ignore-file**\] \[**\--fs-events**\]
\[**\--no-meta**\] \[**\--print-events**\]
\[**-v**\|**\--verbose**\]\... \[**\--log-file**\] \[**\--manpage**\]
@ -352,6 +352,91 @@ This is a shorthand for \--shell=none.
This is the old way to disable event emission into the environment. See
\--emit-events for more.
**\--emit-events-to**=*MODE*
: Configure event emission
Watchexec emits event information when running a command, which can be
used by the command to target specific changed files.
One thing to take care with is assuming inherent behaviour where there
is only chance. Notably, it could appear as if the \`RENAMED\` variable
contains both the original and the new path being renamed. In previous
versions, it would even appear on some platforms as if the original
always came before the new. However, none of this was true. Its
impossible to reliably and portably know which changed path is the old
or new, \"half\" renames may appear (only the original, only the new),
\"unknown\" renames may appear (change was a rename, but whether it was
the old or new isnt known), rename events might split across two
debouncing boundaries, and so on.
This option controls where that information is emitted. It defaults to
environment, which sets environment variables with the paths of the
affected files, for filesystem events:
\$WATCHEXEC_COMMON_PATH is set to the longest common path of all of the
below variables, and so should be prepended to each path to obtain the
full/real path. Then:
\- \$WATCHEXEC_CREATED_PATH is set when files/folders were created -
\$WATCHEXEC_REMOVED_PATH is set when files/folders were removed -
\$WATCHEXEC_RENAMED_PATH is set when files/folders were renamed -
\$WATCHEXEC_WRITTEN_PATH is set when files/folders were modified -
\$WATCHEXEC_META_CHANGED_PATH is set when files/folders metadata were
modified - \$WATCHEXEC_OTHERWISE_CHANGED_PATH is set for every other
kind of pathed event
Multiple paths are separated by the system path separator, ; on Windows
and : on unix. Within each variable, paths are deduplicated and sorted
in binary order (i.e. neither Unicode nor locale aware).
This is the legacy mode and will be deprecated and removed in the
future. The environment of a process is a very restricted space, while
also limited in what it can usefully represent. Large numbers of files
will either cause the environment to be truncated, or may error or crash
the process entirely.
Two new modes are available: stdin writes absolute paths to the stdin of
the command, one per line, each prefixed with \`create:\`, \`remove:\`,
\`rename:\`, \`modify:\`, or \`other:\`, then closes the handle; file
writes the same thing to a temporary file, and its path is given with
the \$WATCHEXEC_EVENTS_FILE environment variable.
There are also two JSON modes, which are based on JSON objects and can
represent the full set of events Watchexec handles. Heres an example of
a folder being created on Linux:
\`\`\`json { \"tags\": \[ { \"kind\": \"path\", \"absolute\":
\"/home/user/your/new-folder\", \"filetype\": \"dir\" }, { \"kind\":
\"fs\", \"simple\": \"create\", \"full\": \"Create(Folder)\" }, {
\"kind\": \"source\", \"source\": \"filesystem\", } \], \"metadata\": {
\"notify-backend\": \"inotify\" } } \`\`\`
The fields are as follows:
\- \`tags\`, structured event data. - \`tags\[\].kind\`, which can be:
\* path, along with: + \`absolute\`, an absolute path. + \`filetype\`, a
file type if known (dir, file, symlink, other). \* fs: + \`simple\`, the
\"simple\" event type (access, create, modify, remove, or other). +
\`full\`, the \"full\" event type, which is too complex to fully
describe here, but looks like General(Precise(Specific)). \* source,
along with: + \`source\`, the source of the event (filesystem, keyboard,
mouse, os, time, internal). \* keyboard, along with: + \`keycode\`.
Currently only the value eof is supported. \* process, for events caused
by processes: + \`pid\`, the process ID. \* signal, for signals sent to
Watchexec: + \`signal\`, the normalised signal name (hangup, interrupt,
quit, terminate, user1, user2). \* completion, for when a command
ends: + \`disposition\`, the exit disposition (success, error, signal,
stop, exception, continued). + \`code\`, the exit, signal, stop, or
exception code. - \`metadata\`, additional information about the event.
The json-stdin mode will emit JSON events to the standard input of the
command, one per line, then close stdin. The json-file mode will create
a temporary file, write the events to it, and provide the path to the
file with the \$WATCHEXEC_EVENTS_FILE environment variable.
Finally, the special none mode will disable event emission entirely.
**-E**, **\--env**=*KEY=VALUE*
: Add env vars to the command

Binary file not shown.