Crate spirit

source ·
Expand description

A helper to create unix daemons.

When writing a service (in the unix terminology, a daemon), there are two parts of the job. One is the actual functionality of the service, the part that makes it different than all the other services out there. And then there’s the very boring part of turning the prototype implementation into a well-behaved service and handling all the things expected of all of them.

This crate is supposed to help with the latter. Before using, you should know the following:

  • This is an early version and while already (hopefully) useful, it is expected to expand and maybe change a bit in future versions. There are certainly parts of functionality I still haven’t written and expect it to be rough around the edges.
  • It is opinionated ‒ it comes with an idea about how a well-behaved daemon should look like and how to integrate that into your application. I simply haven’t find a way to make it less opinionated yet and this helps to scratch my own itch, so it reflects what I needed. If you have use cases that you think should fall within the responsibilities of this crate and are not handled, you are of course welcome to open an issue (or even better, a pull request) on the repository ‒ it would be great if it scratched not only my own itch.
  • It brings in a lot of dependencies. There will likely be features to turn off the unneeded parts, but for now, nobody made them yet.
  • This supports unix-style daemons only for now. This is because I have no experience in how a service for different OS should look like. However, help in this area would be appreciated ‒ being able to write a single code and compile a cross-platform service with all the needed plumbing would indeed sound very useful.

What the crate does and how

To be honest, the crate doesn’t bring much (or, maybe mostly none) of novelty functionality to the table. It just takes other crates doing something useful and gluing them together to form something most daemons want to do.

Composing these things together the crate allows for cutting down on your own boilerplate code around configuration handling, signal handling, command line arguments.

Using the builder pattern, you create a singleton Spirit object. That one starts a background thread that runs some callbacks configured previously when things happen.

It takes two structs, one for command line arguments (using StructOpt) and another for configuration (implementing serde’s Deserialize, loaded using the config crate). It enriches both to add common options, like configuration overrides on the command line and logging into the configuration.

The background thread listens to certain signals (like SIGHUP) using the signal-hook crate and reloads the configuration when requested. It manages the logging backend to reopen on SIGHUP and reflect changes to the configuration.

Helpers

It brings the idea of helpers. A helper is something that plugs a certain functionality into the main crate, to cut down on some more specific boiler-plate code. These are usually provided by other crates. To list some:

  • spirit-daemonize: Configuration and routines to go into background and be a nice daemon.
  • spirit-log: Configuration of logging.
  • spirit-tokio: Integrates basic tokio primitives ‒ auto-reconfiguration for TCP and UDP sockets and starting the runtime.
  • spirit-hyper: Integrates the hyper web server.

(Others will come over time)

There are some general helpers in the helpers module.

Examples

#[macro_use]
extern crate log;
#[macro_use]
extern crate serde_derive;
extern crate spirit;

use std::time::Duration;
use std::thread;

use spirit::{Empty, Spirit};

#[derive(Debug, Default, Deserialize)]
struct Cfg {
    message: String,
    sleep: u64,
}

static DEFAULT_CFG: &str = r#"
message = "hello"
sleep = 2
"#;

fn main() {
    Spirit::<Empty, Cfg>::new()
        // Provide default values for the configuration
        .config_defaults(DEFAULT_CFG)
        // If the program is passed a directory, load files with these extensions from there
        .config_exts(&["toml", "ini", "json"])
        .on_terminate(|| debug!("Asked to terminate"))
        .on_config(|_opts, cfg| debug!("New config loaded: {:?}", cfg))
        // Run the closure, logging the error nicely if it happens (note: no error happens
        // here)
        .run(|spirit: &_| {
            while !spirit.is_terminated() {
                let cfg = spirit.config(); // Get a new version of config every round
                thread::sleep(Duration::from_secs(cfg.sleep));
                info!("{}", cfg.message);
            }
            Ok(())
        });
}

More complete examples can be found in the repository.

Added configuration and options

Command line options

  • config-override: Override configuration value.

Furthermore, it takes a list of paths ‒ both files and directories. They are loaded as configuration files (the directories are examined and files in them ‒ the ones passing a filter ‒ are also loaded).

Common patterns

TODO

Re-exports

pub use utils::key_val;
pub use utils::log_error;
pub use utils::log_errors;
pub use utils::log_errors_named;
pub use utils::MissingEquals;

Modules

Helpers for integrating common configuration patterns.
Various utilities.
Helpers for configuration validation.

Structs

The builder of Spirit.
A struct that may be used when either configuration or command line options are not needed.
A workaround type for Box<FnOnce() -> Result<(), Error>.
An error returned whenever the user passes something not a file nor a directory as configuration.
Returned if configuration path is missing.
The main manipulation handle/struct of the library.
A wrapper around a body.

Type Definitions

An atomic storage for Arc.