Handle signals into events

This commit is contained in:
Félix Saparelli 2021-08-17 21:41:13 +12:00
parent f5e19a6e5f
commit 0237a568df
No known key found for this signature in database
GPG Key ID: B948C4BAE44FC474
7 changed files with 207 additions and 5 deletions

View File

@ -8,7 +8,7 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[lib/src/*.rs]
[lib/**/*.rs]
indent_style = tab
[*.toml]

View File

@ -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::<Event>(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();

View File

@ -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<Event>,
},
/// 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<Event>,

View File

@ -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,
}

View File

@ -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,
})?;

View File

@ -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

181
lib/src/signal.rs Normal file
View File

@ -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<dyn std::error::Error>> {
/// 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<RuntimeError>,
events: mpsc::Sender<Event>,
) -> Result<(), CriticalError> {
imp_worker(errors, events).await
}
#[cfg(unix)]
async fn imp_worker(
errors: mpsc::Sender<RuntimeError>,
events: mpsc::Sender<Event>,
) -> 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<RuntimeError>,
events: mpsc::Sender<Event>,
) -> 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<RuntimeError>,
events: mpsc::Sender<Event>, 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(())
}