watchexec/crates/lib/README.md
Félix Saparelli a13bc429eb
Watchexec lib v3 (#601)
Co-authored-by: emilHof <95590295+emilHof@users.noreply.github.com>
2023-11-25 20:33:44 +00:00

8.5 KiB

Crates.io page API Docs Crate license: Apache 2.0 CI status

Watchexec library

The library which powers Watchexec CLI and other tools.

Examples

Here's a complete example showing some of the library's features:

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.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:

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:

Filterers are split into their own crates, so they can be evolved independently:

  • The Globset filterer implements the default Watchexec CLI filtering, based on the regex crate's ignore mechanisms.

  • The 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 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 is Watchexec's process supervisor and command abstraction.

  • ClearScreen makes clearing the terminal screen in a cross-platform way easy by default, and provides advanced options to fit your usecase.

  • Command Group augments the std and tokio Command with support for process groups, portable between Unix and Windows.

  • Event types contains the event types used by Watchexec, including the JSON format used for passing event data to child processes.

  • Signal types contains the signal types used by Watchexec.

  • Ignore files finds, parses, and interprets ignore files.

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