Start off on main interface

This commit is contained in:
Félix Saparelli 2021-08-19 01:12:50 +12:00
parent 826dbd8cda
commit 0bb38f40a5
No known key found for this signature in database
GPG Key ID: B948C4BAE44FC474
10 changed files with 304 additions and 171 deletions

1
Cargo.lock generated
View File

@ -2070,6 +2070,7 @@ dependencies = [
"color-eyre",
"command-group",
"dunce",
"futures",
"miette",
"notify 5.0.0-pre.11",
"thiserror",

View File

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

View File

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

3
lib/src/config.rs Normal file
View File

@ -0,0 +1,3 @@
pub struct Config {
pub fs: crate::fs::WorkingData,
}

View File

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

View File

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

View File

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

View File

@ -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(())
}
}

View File

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

73
lib/src/watchexec.rs Normal file
View File

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