Start off on main interface
This commit is contained in:
parent
826dbd8cda
commit
0bb38f40a5
|
@ -2070,6 +2070,7 @@ dependencies = [
|
|||
"color-eyre",
|
||||
"command-group",
|
||||
"dunce",
|
||||
"futures",
|
||||
"miette",
|
||||
"notify 5.0.0-pre.11",
|
||||
"thiserror",
|
||||
|
|
|
@ -20,6 +20,7 @@ notify = "5.0.0-pre.11"
|
|||
thiserror = "1.0.26"
|
||||
tracing = "0.1.26"
|
||||
dunce = "1.0.2"
|
||||
futures = "0.3.16"
|
||||
|
||||
[dependencies.command-group]
|
||||
version = "1.0.5"
|
||||
|
|
|
@ -4,7 +4,11 @@ use tokio::{
|
|||
sync::{mpsc, watch},
|
||||
time::sleep,
|
||||
};
|
||||
use watchexec::{event::{Event, Particle}, fs, signal::{self, Signal}};
|
||||
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.
|
||||
|
@ -26,7 +30,8 @@ async fn main() -> color_eyre::eyre::Result<()> {
|
|||
tracing::info!("event: {:?}", e);
|
||||
|
||||
if e.particulars.contains(&Particle::Signal(Signal::Interrupt))
|
||||
|| e.particulars.contains(&Particle::Signal(Signal::Terminate)) {
|
||||
|| e.particulars.contains(&Particle::Signal(Signal::Terminate))
|
||||
{
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
pub struct Config {
|
||||
pub fs: crate::fs::WorkingData,
|
||||
}
|
|
@ -4,9 +4,15 @@ use std::path::PathBuf;
|
|||
|
||||
use miette::Diagnostic;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::{
|
||||
sync::{mpsc, watch},
|
||||
task::JoinError,
|
||||
};
|
||||
|
||||
use crate::{event::Event, fs::Watcher};
|
||||
use crate::{
|
||||
event::Event,
|
||||
fs::{self, Watcher},
|
||||
};
|
||||
|
||||
/// Errors which are not recoverable and stop watchexec execution.
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
|
@ -21,6 +27,11 @@ pub enum CriticalError {
|
|||
#[error("cannot send internal runtime error: {0}")]
|
||||
#[diagnostic(code(watchexec::critical::error_channel_send))]
|
||||
ErrorChannelSend(#[from] mpsc::error::SendError<RuntimeError>),
|
||||
|
||||
/// Error received when joining the main watchexec task.
|
||||
#[error("main task join: {0}")]
|
||||
#[diagnostic(code(watchexec::critical::main_task_join))]
|
||||
MainTaskJoin(#[source] JoinError),
|
||||
}
|
||||
|
||||
/// Errors which _may_ be recoverable, transient, or only affect a part of the operation, and should
|
||||
|
@ -92,3 +103,13 @@ pub enum RuntimeError {
|
|||
err: mpsc::error::TrySendError<Event>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Errors occurring from reconfigs.
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum ReconfigError {
|
||||
/// Error received when the fs watcher internal state cannot be updated.
|
||||
#[error("reconfig: fs watch: {0}")]
|
||||
#[diagnostic(code(watchexec::reconfig::fs_watch))]
|
||||
FsWatch(#[from] watch::error::SendError<fs::WorkingData>),
|
||||
}
|
||||
|
|
|
@ -29,14 +29,13 @@ impl Default for Watcher {
|
|||
}
|
||||
|
||||
impl Watcher {
|
||||
fn create(self, f: impl notify::EventFn) -> Result<Box<dyn notify::Watcher + Send>, RuntimeError> {
|
||||
fn create(
|
||||
self,
|
||||
f: impl notify::EventFn,
|
||||
) -> Result<Box<dyn notify::Watcher + Send>, RuntimeError> {
|
||||
match self {
|
||||
Self::Native => {
|
||||
notify::RecommendedWatcher::new(f).map(|w| Box::new(w) as _)
|
||||
}
|
||||
Self::Poll => {
|
||||
notify::PollWatcher::new(f).map(|w| Box::new(w) as _)
|
||||
}
|
||||
Self::Native => notify::RecommendedWatcher::new(f).map(|w| Box::new(w) as _),
|
||||
Self::Poll => notify::PollWatcher::new(f).map(|w| Box::new(w) as _),
|
||||
}
|
||||
.map_err(|err| RuntimeError::FsWatcherCreate { kind: self, err })
|
||||
}
|
||||
|
|
|
@ -15,10 +15,20 @@
|
|||
#![warn(clippy::unwrap_used)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
// the toolkit to make your own
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod fs;
|
||||
pub mod shell;
|
||||
pub mod signal;
|
||||
|
||||
// the core experience
|
||||
mod config;
|
||||
mod watchexec;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use config::Config;
|
||||
#[doc(inline)]
|
||||
pub use watchexec::Watchexec;
|
||||
|
||||
// the *action* is debounced, not the events
|
||||
|
|
318
lib/src/shell.rs
318
lib/src/shell.rs
|
@ -10,188 +10,196 @@ use tokio::process::Command;
|
|||
/// other options.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Shell {
|
||||
/// Use no shell, and execute the command directly.
|
||||
None,
|
||||
/// Use no shell, and execute the command directly.
|
||||
None,
|
||||
|
||||
/// Use the given string as a unix shell invocation.
|
||||
///
|
||||
/// This means two things:
|
||||
/// - the program is invoked with `-c` followed by the command, and
|
||||
/// - the string will be split on space, and the resulting vec used as
|
||||
/// execvp(3) arguments: first is the shell program, rest are additional
|
||||
/// arguments (which come before the `-c` mentioned above). This is a very
|
||||
/// simplistic approach deliberately: it will not support quoted
|
||||
/// arguments, for example. Use [`Shell::None`] with a custom command vec
|
||||
/// if you want that.
|
||||
Unix(String),
|
||||
/// Use the given string as a unix shell invocation.
|
||||
///
|
||||
/// This means two things:
|
||||
/// - the program is invoked with `-c` followed by the command, and
|
||||
/// - the string will be split on space, and the resulting vec used as
|
||||
/// execvp(3) arguments: first is the shell program, rest are additional
|
||||
/// arguments (which come before the `-c` mentioned above). This is a very
|
||||
/// simplistic approach deliberately: it will not support quoted
|
||||
/// arguments, for example. Use [`Shell::None`] with a custom command vec
|
||||
/// if you want that.
|
||||
Unix(String),
|
||||
|
||||
/// Use the Windows CMD.EXE shell.
|
||||
///
|
||||
/// This is invoked with `/C` followed by the command.
|
||||
#[cfg(windows)]
|
||||
Cmd,
|
||||
/// Use the Windows CMD.EXE shell.
|
||||
///
|
||||
/// This is invoked with `/C` followed by the command.
|
||||
#[cfg(windows)]
|
||||
Cmd,
|
||||
|
||||
/// Use Powershell, on Windows or elsewhere.
|
||||
///
|
||||
/// This is invoked with `-Command` followed by the command.
|
||||
///
|
||||
/// This is preferred over `Unix("pwsh")`, though that will also work
|
||||
/// on unices due to Powershell supporting the `-c` short option.
|
||||
Powershell,
|
||||
/// Use Powershell, on Windows or elsewhere.
|
||||
///
|
||||
/// This is invoked with `-Command` followed by the command.
|
||||
///
|
||||
/// This is preferred over `Unix("pwsh")`, though that will also work
|
||||
/// on unices due to Powershell supporting the `-c` short option.
|
||||
Powershell,
|
||||
}
|
||||
|
||||
impl Default for Shell {
|
||||
#[cfg(windows)]
|
||||
fn default() -> Self {
|
||||
Self::Powershell
|
||||
}
|
||||
#[cfg(windows)]
|
||||
fn default() -> Self {
|
||||
Self::Powershell
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn default() -> Self {
|
||||
Self::Unix("sh".into())
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
fn default() -> Self {
|
||||
Self::Unix("sh".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
/// Obtain a [`Command`] given the cmd vec from [`Config`][crate::config::Config].
|
||||
///
|
||||
/// Behaves as described in the enum documentation.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - Panics if `cmd` is empty.
|
||||
/// - Panics if the string in the `Unix` variant is empty or only whitespace.
|
||||
pub fn to_command(&self, cmd: &[String]) -> Command {
|
||||
assert!(!cmd.is_empty(), "cmd was empty");
|
||||
/// Obtain a [`Command`] given the cmd vec from [`Config`][crate::config::Config].
|
||||
///
|
||||
/// Behaves as described in the enum documentation.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - Panics if `cmd` is empty.
|
||||
/// - Panics if the string in the `Unix` variant is empty or only whitespace.
|
||||
pub fn to_command(&self, cmd: &[String]) -> Command {
|
||||
assert!(!cmd.is_empty(), "cmd was empty");
|
||||
|
||||
match self {
|
||||
Shell::None => {
|
||||
// UNWRAP: checked by assert
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let (first, rest) = cmd.split_first().unwrap();
|
||||
let mut c = Command::new(first);
|
||||
c.args(rest);
|
||||
c
|
||||
}
|
||||
match self {
|
||||
Shell::None => {
|
||||
// UNWRAP: checked by assert
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let (first, rest) = cmd.split_first().unwrap();
|
||||
let mut c = Command::new(first);
|
||||
c.args(rest);
|
||||
c
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
Shell::Cmd => {
|
||||
let mut c = Command::new("cmd.exe");
|
||||
c.arg("/C").arg(cmd.join(" "));
|
||||
c
|
||||
}
|
||||
#[cfg(windows)]
|
||||
Shell::Cmd => {
|
||||
let mut c = Command::new("cmd.exe");
|
||||
c.arg("/C").arg(cmd.join(" "));
|
||||
c
|
||||
}
|
||||
|
||||
Shell::Powershell if cfg!(windows) => {
|
||||
let mut c = Command::new("powershell.exe");
|
||||
c.arg("-Command").arg(cmd.join(" "));
|
||||
c
|
||||
}
|
||||
Shell::Powershell if cfg!(windows) => {
|
||||
let mut c = Command::new("powershell.exe");
|
||||
c.arg("-Command").arg(cmd.join(" "));
|
||||
c
|
||||
}
|
||||
|
||||
Shell::Powershell => {
|
||||
let mut c = Command::new("pwsh");
|
||||
c.arg("-Command").arg(cmd.join(" "));
|
||||
c
|
||||
}
|
||||
Shell::Powershell => {
|
||||
let mut c = Command::new("pwsh");
|
||||
c.arg("-Command").arg(cmd.join(" "));
|
||||
c
|
||||
}
|
||||
|
||||
Shell::Unix(name) => {
|
||||
assert!(!name.is_empty(), "shell program was empty");
|
||||
let sh = name.split_ascii_whitespace().collect::<Vec<_>>();
|
||||
Shell::Unix(name) => {
|
||||
assert!(!name.is_empty(), "shell program was empty");
|
||||
let sh = name.split_ascii_whitespace().collect::<Vec<_>>();
|
||||
|
||||
// UNWRAP: checked by assert
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let (shprog, shopts) = sh.split_first().unwrap();
|
||||
// UNWRAP: checked by assert
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let (shprog, shopts) = sh.split_first().unwrap();
|
||||
|
||||
let mut c = Command::new(shprog);
|
||||
c.args(shopts);
|
||||
c.arg("-c").arg(cmd.join(" "));
|
||||
c
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut c = Command::new(shprog);
|
||||
c.args(shopts);
|
||||
c.arg("-c").arg(cmd.join(" "));
|
||||
c
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Shell;
|
||||
use command_group::AsyncCommandGroup;
|
||||
use super::Shell;
|
||||
use command_group::AsyncCommandGroup;
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn unix_shell_default() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::default()
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status().await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn unix_shell_default() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::default()
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()
|
||||
.await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn unix_shell_none() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::None
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status().await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn unix_shell_none() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::None
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()
|
||||
.await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn unix_shell_alternate() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Unix("bash".into())
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status().await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn unix_shell_alternate() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Unix("bash".into())
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()
|
||||
.await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn unix_shell_alternate_shopts() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Unix("bash -o errexit".into())
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status().await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn unix_shell_alternate_shopts() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Unix("bash -o errexit".into())
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()
|
||||
.await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(windows)]
|
||||
async fn windows_shell_default() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::default()
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status().await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
#[tokio::test]
|
||||
#[cfg(windows)]
|
||||
async fn windows_shell_default() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::default()
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()
|
||||
.await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(windows)]
|
||||
async fn windows_shell_cmd() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Cmd
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status().await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
#[tokio::test]
|
||||
#[cfg(windows)]
|
||||
async fn windows_shell_cmd() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Cmd
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()
|
||||
.await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(windows)]
|
||||
async fn windows_shell_powershell() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Powershell
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status().await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
#[tokio::test]
|
||||
#[cfg(windows)]
|
||||
async fn windows_shell_powershell() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Powershell
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()
|
||||
.await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(windows)]
|
||||
async fn windows_shell_unix_style_powershell() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Unix("powershell.exe".into())
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status().await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
#[tokio::test]
|
||||
#[cfg(windows)]
|
||||
async fn windows_shell_unix_style_powershell() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Unix("powershell.exe".into())
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()
|
||||
.await?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@ async fn imp_worker(
|
|||
errors: mpsc::Sender<RuntimeError>,
|
||||
events: mpsc::Sender<Event>,
|
||||
) -> Result<(), CriticalError> {
|
||||
use tokio::signal::windows::{ctrl_c, ctrl_break};
|
||||
use tokio::signal::windows::{ctrl_break, ctrl_c};
|
||||
|
||||
debug!("launching windows signal worker");
|
||||
|
||||
|
@ -160,10 +160,17 @@ async fn imp_worker(
|
|||
}
|
||||
}
|
||||
|
||||
async fn send_event(errors: mpsc::Sender<RuntimeError>,
|
||||
events: mpsc::Sender<Event>, sig: Signal) -> Result<(), CriticalError> {
|
||||
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::Source(if sig == Signal::Interrupt {
|
||||
Source::Keyboard
|
||||
} else {
|
||||
Source::Os
|
||||
}),
|
||||
Particle::Signal(sig),
|
||||
];
|
||||
|
||||
|
@ -174,7 +181,12 @@ async fn send_event(errors: mpsc::Sender<RuntimeError>,
|
|||
|
||||
trace!(?event, "processed signal into event");
|
||||
if let Err(err) = events.send(event).await {
|
||||
errors.send(RuntimeError::EventChannelSend { ctx: "signals", err }).await?;
|
||||
errors
|
||||
.send(RuntimeError::EventChannelSend {
|
||||
ctx: "signals",
|
||||
err,
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use futures::FutureExt;
|
||||
use tokio::{
|
||||
spawn,
|
||||
sync::{mpsc, watch, Notify},
|
||||
task::{JoinError, JoinHandle},
|
||||
try_join,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
error::{CriticalError, ReconfigError},
|
||||
fs, signal,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Watchexec {
|
||||
handle: JoinHandle<Result<(), CriticalError>>,
|
||||
start_lock: Arc<Notify>,
|
||||
fs_watch: watch::Sender<fs::WorkingData>,
|
||||
}
|
||||
|
||||
impl Watchexec {
|
||||
pub fn new(config: Config) -> Result<Self, CriticalError> {
|
||||
let (fs_s, fs_r) = watch::channel(config.fs);
|
||||
|
||||
let notify = Arc::new(Notify::new());
|
||||
let start_lock = notify.clone();
|
||||
let handle = spawn(async move {
|
||||
notify.notified().await;
|
||||
|
||||
let (er_s, er_r) = mpsc::channel(64); // TODO: configure?
|
||||
let (ev_s, ev_r) = mpsc::channel(1024); // TODO: configure?
|
||||
|
||||
macro_rules! subtask {
|
||||
($task:expr) => {
|
||||
spawn($task).then(|jr| async { flatten(jr) })
|
||||
};
|
||||
}
|
||||
|
||||
let fs = subtask!(fs::worker(fs_r, er_s.clone(), ev_s.clone()));
|
||||
let signal = subtask!(signal::worker(er_s.clone(), ev_s.clone()));
|
||||
|
||||
try_join!(fs, signal).map(drop)
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
handle,
|
||||
start_lock,
|
||||
fs_watch: fs_s,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reconfig(&self, config: Config) -> Result<(), ReconfigError> {
|
||||
self.fs_watch.send(config.fs)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<(), CriticalError> {
|
||||
self.start_lock.notify_one();
|
||||
(&mut self.handle)
|
||||
.await
|
||||
.map_err(CriticalError::MainTaskJoin)?
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn flatten(join_res: Result<Result<(), CriticalError>, JoinError>) -> Result<(), CriticalError> {
|
||||
join_res
|
||||
.map_err(CriticalError::MainTaskJoin)
|
||||
.and_then(|x| x)
|
||||
}
|
Loading…
Reference in New Issue