diff --git a/.editorconfig b/.editorconfig index 0bdd715..5ffbbb1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[lib/src/*.rs] +[lib/**/*.rs] indent_style = tab [*.toml] diff --git a/lib/examples/fs.rs b/lib/examples/fs.rs index ea4d488..73437e4 100644 --- a/lib/examples/fs.rs +++ b/lib/examples/fs.rs @@ -1,10 +1,10 @@ -use std::time::Duration; +use std::{process::exit, time::Duration}; use tokio::{ sync::{mpsc, watch}, time::sleep, }; -use watchexec::fs; +use watchexec::{event::{Event, Particle}, fs, signal::{self, Signal}}; // Run with: `env RUST_LOG=debug cargo run --example fs`, // then touch some files within the first 15 seconds, and afterwards. @@ -13,7 +13,7 @@ async fn main() -> color_eyre::eyre::Result<()> { tracing_subscriber::fmt::init(); color_eyre::install()?; - let (ev_s, mut ev_r) = mpsc::channel(1024); + let (ev_s, mut ev_r) = mpsc::channel::(1024); let (er_s, mut er_r) = mpsc::channel(64); let (wd_s, wd_r) = watch::channel(fs::WorkingData::default()); @@ -24,6 +24,11 @@ async fn main() -> color_eyre::eyre::Result<()> { tokio::spawn(async move { while let Some(e) = ev_r.recv().await { tracing::info!("event: {:?}", e); + + if e.particulars.contains(&Particle::Signal(Signal::Interrupt)) + || e.particulars.contains(&Particle::Signal(Signal::Terminate)) { + exit(0); + } } }); @@ -33,6 +38,8 @@ async fn main() -> color_eyre::eyre::Result<()> { } }); + tokio::spawn(signal::worker(er_s.clone(), ev_s.clone())); + let wd_sh = tokio::spawn(async move { sleep(Duration::from_secs(15)).await; wkd.pathset = Vec::new(); diff --git a/lib/src/error.rs b/lib/src/error.rs index 2dadbce..92e0577 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -78,6 +78,15 @@ pub enum RuntimeError { #[error("cannot send event from {ctx}: {err}")] #[diagnostic(code(watchexec::runtime::event_channel_send))] EventChannelSend { + ctx: &'static str, + #[source] + err: mpsc::error::SendError, + }, + + /// Error received when an event cannot be sent to the event channel. + #[error("cannot send event from {ctx}: {err}")] + #[diagnostic(code(watchexec::runtime::event_channel_try_send))] + EventChannelTrySend { ctx: &'static str, #[source] err: mpsc::error::TrySendError, diff --git a/lib/src/event.rs b/lib/src/event.rs index 28474c9..647bbc5 100644 --- a/lib/src/event.rs +++ b/lib/src/event.rs @@ -7,6 +7,8 @@ use chrono::{DateTime, Local}; use std::{collections::HashMap, path::PathBuf}; +use crate::signal::Signal; + /// An event, as far as watchexec cares about. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Event { @@ -22,6 +24,7 @@ pub enum Particle { Path(PathBuf), Source(Source), Process(u32), + Signal(Signal), } /// The general origin of the event. @@ -31,5 +34,6 @@ pub enum Source { Filesystem, Keyboard, Mouse, + Os, Time, } diff --git a/lib/src/fs.rs b/lib/src/fs.rs index 8054eea..e699232 100644 --- a/lib/src/fs.rs +++ b/lib/src/fs.rs @@ -213,7 +213,7 @@ fn process_event( trace!(event = ?ev, "processed notify event into watchexec event"); n_events .try_send(ev) - .map_err(|err| RuntimeError::EventChannelSend { + .map_err(|err| RuntimeError::EventChannelTrySend { ctx: "fs watcher", err, })?; diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 59e4a98..381dc48 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -19,5 +19,6 @@ pub mod error; pub mod event; pub mod fs; pub mod shell; +pub mod signal; // the *action* is debounced, not the events diff --git a/lib/src/signal.rs b/lib/src/signal.rs new file mode 100644 index 0000000..58cda0b --- /dev/null +++ b/lib/src/signal.rs @@ -0,0 +1,181 @@ +use tokio::{select, sync::mpsc}; +use tracing::{debug, trace}; + +use crate::{ + error::{CriticalError, RuntimeError}, + event::{Event, Particle, Source}, +}; + +/// A notification sent to the main (watchexec) process. +/// +/// On Windows, only [`Interrupt`][Signal::Interrupt] and [`Terminate`][Signal::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 Signal { + /// 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 watchexec::signal::worker; +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Box> { +/// let (ev_s, _) = mpsc::channel(1024); +/// let (er_s, _) = mpsc::channel(64); +/// +/// worker(er_s, ev_s).await?; +/// Ok(()) +/// } +/// ``` +pub async fn worker( + errors: mpsc::Sender, + events: mpsc::Sender, +) -> Result<(), CriticalError> { + imp_worker(errors, events).await +} + +#[cfg(unix)] +async fn imp_worker( + errors: mpsc::Sender, + events: mpsc::Sender, +) -> 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())? + }} + } + + 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, + events: mpsc::Sender, +) -> Result<(), CriticalError> { + use tokio::signal::windows::{ctrl_c, ctrl_break}; + + debug!("launching windows signal worker"); + + macro_rules! listen { + ($sig:ident) => {{ + trace!(kind=%stringify!($sig), "listening for windows process notification"); + $sig()? + }} + } + + 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, + events: mpsc::Sender, sig: Signal) -> Result<(), CriticalError> { + let particulars = vec![ + Particle::Source(if sig == Signal::Interrupt { Source::Keyboard } else { Source::Os }), + Particle::Signal(sig), + ]; + + let event = Event { + particulars, + metadata: Default::default(), + }; + + trace!(?event, "processed signal into event"); + if let Err(err) = events.send(event).await { + errors.send(RuntimeError::EventChannelSend { ctx: "signals", err }).await?; + } + + Ok(()) +}