mirror of
https://github.com/watchexec/watchexec.git
synced 2024-09-07 20:11:29 +02:00
228 lines
8.5 KiB
Markdown
228 lines
8.5 KiB
Markdown
[![Crates.io page](https://badgen.net/crates/v/watchexec)](https://crates.io/crates/watchexec)
|
|
[![API Docs](https://docs.rs/watchexec/badge.svg)][docs]
|
|
[![Crate license: Apache 2.0](https://badgen.net/badge/license/Apache%202.0)][license]
|
|
[![CI status](https://github.com/watchexec/watchexec/actions/workflows/check.yml/badge.svg)](https://github.com/watchexec/watchexec/actions/workflows/check.yml)
|
|
|
|
# Watchexec library
|
|
|
|
_The library which powers [Watchexec CLI](https://watchexec.github.io) and other tools._
|
|
|
|
- **[API documentation][docs]**.
|
|
- Licensed under [Apache 2.0][license].
|
|
- Status: maintained.
|
|
|
|
[docs]: https://docs.rs/watchexec
|
|
[license]: ../../LICENSE
|
|
|
|
|
|
## Examples
|
|
|
|
Here's a complete example showing some of the library's features:
|
|
|
|
```rust ,no_run
|
|
use miette::{IntoDiagnostic, Result};
|
|
use std::{
|
|
sync::{Arc, Mutex},
|
|
time::Duration,
|
|
};
|
|
use watchexec::{
|
|
command::{Command, Program, Shell},
|
|
job::CommandState,
|
|
Watchexec,
|
|
};
|
|
use watchexec_events::{Event, Priority};
|
|
use watchexec_signals::Signal;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
// this is okay to start with, but Watchexec logs a LOT of data,
|
|
// even at error level. you will quickly want to filter it down.
|
|
tracing_subscriber::fmt()
|
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
|
.init();
|
|
|
|
// initialise Watchexec with a simple initial action handler
|
|
let job = Arc::new(Mutex::new(None));
|
|
let wx = Watchexec::new({
|
|
let outerjob = job.clone();
|
|
move |mut action| {
|
|
let (_, job) = action.create_job(Arc::new(Command {
|
|
program: Program::Shell {
|
|
shell: Shell::new("bash"),
|
|
command: "
|
|
echo 'Hello world'
|
|
trap 'echo Not quitting yet!' TERM
|
|
read
|
|
"
|
|
.into(),
|
|
args: Vec::new(),
|
|
},
|
|
options: Default::default(),
|
|
}));
|
|
|
|
// store the job outside this closure too
|
|
*outerjob.lock().unwrap() = Some(job.clone());
|
|
|
|
// block SIGINT
|
|
#[cfg(unix)]
|
|
job.set_spawn_hook(|cmd, _| {
|
|
use nix::sys::signal::{sigprocmask, SigSet, SigmaskHow, Signal};
|
|
unsafe {
|
|
cmd.command_mut().pre_exec(|| {
|
|
let mut newset = SigSet::empty();
|
|
newset.add(Signal::SIGINT);
|
|
sigprocmask(SigmaskHow::SIG_BLOCK, Some(&newset), None)?;
|
|
Ok(())
|
|
});
|
|
}
|
|
});
|
|
|
|
// start the command
|
|
job.start();
|
|
|
|
action
|
|
}
|
|
})?;
|
|
|
|
// start the engine
|
|
let main = wx.main();
|
|
|
|
// send an event to start
|
|
wx.send_event(Event::default(), Priority::Urgent)
|
|
.await
|
|
.unwrap();
|
|
// ^ this will cause the action handler we've defined above to run,
|
|
// creating and starting our little bash program, and storing it in the mutex
|
|
|
|
// spin until we've got the job
|
|
while job.lock().unwrap().is_none() {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
|
|
// watch the job and restart it when it exits
|
|
let job = job.lock().unwrap().clone().unwrap();
|
|
let auto_restart = tokio::spawn(async move {
|
|
loop {
|
|
job.to_wait().await;
|
|
job.run(|context| {
|
|
if let CommandState::Finished {
|
|
status,
|
|
started,
|
|
finished,
|
|
} = context.current
|
|
{
|
|
let duration = *finished - *started;
|
|
eprintln!("[Program stopped with {status:?}; ran for {duration:?}]")
|
|
}
|
|
})
|
|
.await;
|
|
|
|
eprintln!("[Restarting...]");
|
|
job.start().await;
|
|
}
|
|
});
|
|
|
|
// now we change what the action does:
|
|
let auto_restart_abort = auto_restart.abort_handle();
|
|
wx.config.on_action(move |mut action| {
|
|
// if we get Ctrl-C on the Watchexec instance, we quit
|
|
if action.signals().any(|sig| sig == Signal::Interrupt) {
|
|
eprintln!("[Quitting...]");
|
|
auto_restart_abort.abort();
|
|
action.quit_gracefully(Signal::ForceStop, Duration::ZERO);
|
|
return action;
|
|
}
|
|
|
|
// if the action was triggered by file events, gracefully stop the program
|
|
if action.paths().next().is_some() {
|
|
// watchexec can manage ("supervise") more than one program;
|
|
// here we only have one but we don't know its Id so we grab it out of the iterator
|
|
if let Some(job) = action.list_jobs().next().map(|(_, job)| job.clone()) {
|
|
eprintln!("[Asking program to stop...]");
|
|
job.stop_with_signal(Signal::Terminate, Duration::from_secs(5));
|
|
}
|
|
}
|
|
|
|
action
|
|
});
|
|
|
|
// and watch all files in the current directory:
|
|
wx.config.pathset(["."]);
|
|
|
|
// then keep running until Watchexec quits!
|
|
let _ = main.await.into_diagnostic()?;
|
|
auto_restart.abort();
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
Other examples:
|
|
- [Only Commands](./examples/only_commands.rs): skip watching files, only use the supervisor.
|
|
- [Only Events](./examples/only_events.rs): never start any processes, only print events.
|
|
- [Restart `cargo run` only when `cargo build` succeeds](./examples/restart_run_on_successful_build.rs)
|
|
|
|
|
|
## Kitchen sink
|
|
|
|
Though not its primary usecase, the library exposes most of its relatively standalone components,
|
|
available to make other tools that are not Watchexec-shaped:
|
|
|
|
- **Event sources**: [Filesystem](https://docs.rs/watchexec/3/watchexec/sources/fs/index.html),
|
|
[Signals](https://docs.rs/watchexec/3/watchexec/sources/signal/index.html),
|
|
[Keyboard](https://docs.rs/watchexec/3/watchexec/sources/keyboard/index.html).
|
|
|
|
- Finding **[a common prefix](https://docs.rs/watchexec/3/watchexec/paths/fn.common_prefix.html)**
|
|
of a set of paths.
|
|
|
|
- A **[Changeable](https://docs.rs/watchexec/3/watchexec/changeable/index.html)** type, which
|
|
powers the "live" configuration system.
|
|
|
|
- And [more][docs]!
|
|
|
|
Filterers are split into their own crates, so they can be evolved independently:
|
|
|
|
- The **[Globset](https://docs.rs/watchexec-filterer-globset) filterer** implements the default
|
|
Watchexec CLI filtering, based on the regex crate's ignore mechanisms.
|
|
|
|
- ~~The **[Tagged](https://docs.rs/watchexec-filterer-tagged) filterer**~~ was an experiment in
|
|
creating a more powerful filtering solution, which could operate on every part of events, not
|
|
just their paths, using a custom syntax. It is no longer maintained.
|
|
|
|
- The **[Ignore](https://docs.rs/watchexec-filterer-ignore) filterer** implements ignore-file
|
|
semantics, and especially supports _trees_ of ignore files. It is used as a subfilterer in both
|
|
of the main filterers above.
|
|
|
|
There are also separate, standalone crates used to build Watchexec which you can tap into:
|
|
|
|
- **[Supervisor](https://docs.rs/watchexec-supervisor)** is Watchexec's process supervisor and
|
|
command abstraction.
|
|
|
|
- **[ClearScreen](https://docs.rs/clearscreen)** makes clearing the terminal screen in a
|
|
cross-platform way easy by default, and provides advanced options to fit your usecase.
|
|
|
|
- **[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
|
|
project, and what kind of project it is.
|
|
|
|
## Rust version (MSRV)
|
|
|
|
Due to the unpredictability of dependencies changing their MSRV, this library no longer tries to
|
|
keep to a minimum supported Rust version behind stable. Instead, it is assumed that developers use
|
|
the latest stable at all times.
|
|
|
|
Applications that wish to support lower-than-stable Rust (such as the Watchexec CLI does) should:
|
|
- use a lock file
|
|
- recommend the use of `--locked` when installing from source
|
|
- provide pre-built binaries (and [Binstall](https://github.com/cargo-bins/cargo-binstall) support) for non-distro users
|
|
- avoid using newer features until some time has passed, to let distro users catch up
|
|
- consider recommending that distro-Rust users switch to distro `rustup` where available
|