4 stable releases

new 2.0.0 Apr 20, 2024
1.0.3 Dec 19, 2023
1.0.2 Dec 9, 2023
1.0.1 Nov 29, 2023
1.0.0 Nov 26, 2023

#48 in macOS and iOS APIs

Download history 9675/week @ 2024-01-05 14987/week @ 2024-01-12 17608/week @ 2024-01-19 27570/week @ 2024-01-26 29951/week @ 2024-02-02 30326/week @ 2024-02-09 28975/week @ 2024-02-16 31830/week @ 2024-02-23 31459/week @ 2024-03-01 34125/week @ 2024-03-08 36387/week @ 2024-03-15 37624/week @ 2024-03-22 37020/week @ 2024-03-29 37654/week @ 2024-04-05 38778/week @ 2024-04-12 30762/week @ 2024-04-19

149,934 downloads per month
Used in 9 crates (via watchexec)

Apache-2.0 OR MIT

135KB
3K SLoC

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

Supervisor

Watchexec's process supervisor.


lib.rs:

Watchexec's process supervisor.

This crate implements the process supervisor for Watchexec. It is responsible for spawning and managing processes, and for sending events to them.

You may use this crate to implement your own process supervisor, but keep in mind its direction will always primarily be driven by the needs of Watchexec itself.

Usage

There is no struct or implementation of a single supervisor, as the particular needs of the application will dictate how that is designed. Instead, this crate provides a Job construct, which is a handle to a single Command, and manages its lifecycle. The Job API has been modeled after the systemctl set of commands for service control, with operations for starting, stopping, restarting, sending signals, waiting for the process to complete, etc.

There are also methods for running hooks within the job's runtime task, and for handling errors.

Theory of Operation

A Job is, properly speaking, a handle which lets one control a Tokio task. That task is spawned on the Tokio runtime, and so runs in the background. A Job takes as input a Command, which describes how to start a single process, through either a shell command or a direct executable invocation, and if the process should be grouped (using command-group) or not.

The job's task runs an event loop on two sources: the process's wait() (i.e. when the process ends) and the job's control queue. The control queue is a hybrid MPSC queue, with three priority levels and a timer. When the timer is active, the lowest ("Normal") priority queue is disabled. This is an internal detail which serves to implement graceful stops and restarts. The internals of the job's task are not available to the API user, actions and queries are performed by sending messages on this control queue.

The control queue is executed in priority and in order within priorities. Sending a control to the task returns a Ticket, which is a future that resolves when the control has been processed. Dropping the ticket will not cancel the control. This provides two complementary ways to orchestrate actions: queueing controls in the desired order if there is no need for branching flow or for signaling, and sending controls or performing other actions after awaiting tickets.

Do note that both of these can be used together. There is no need for the below pattern:

#
#
job.start().await;
job.signal(Signal::User1).await;
job.stop().await;

Because of ordering, it behaves the same as this:

#
#
job.start();
job.signal(Signal::User1);
job.stop().await; // here, all of start(), signal(), and stop() will have run in order

However, this is a different program:

#
#
job.start().await;
println!("program started!");
sleep(Duration::from_secs(5)).await; // wait until program is fully started

job.signal(Signal::User1).await;
sleep(Duration::from_millis(150)).await; // wait until program has dumped stats
println!("program stats dumped via USR1 signal!");

job.stop().await;
println!("program stopped");
#

Example

use watchexec_supervisor::Signal;
use watchexec_supervisor::command::{Command, Program};
use watchexec_supervisor::job::{CommandState, start_job};

let (job, task) = start_job(Arc::new(Command {
    program: Program::Exec {
        prog: "/bin/date".into(),
        args: Vec::new(),
    }.into(),
    options: Default::default(),
}));

job.start().await;
job.signal(Signal::User1).await;
job.stop().await;

job.delete_now().await;

task.await; // make sure the task is fully cleaned up

Dependencies

~6–18MB
~197K SLoC