Crate spirit[][src]

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 and daemonization.

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-tokio: Integrates basic tokio primitives ‒ auto-reconfiguration for TCP and UDP sockets and starting the runtime.

(Others will come over time)

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, _>::new(Cfg::default())
        // 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(|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

  • daemonize: When this is set, the program becomes instead of staying in the foreground. It closes stdio.
  • config-override: Override configuration value.
  • log: In addition to the logging in configuration file, also log with the given severity to stderr.
  • log-module: Override the stderr log level of the given module.

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

./program --log info --log-module program=trace --config-override ui.message=something

Configuration options

logging

It is an array of logging destinations. No matter where the logging is sent to, these options are valid for all:

  • level: The log level to use. Valid options are OFF, ERROR, WARN, INFO, DEBUG and TRACE.
  • per-module: A map, setting log level overrides for specific modules (logging targets). This one is optional.
  • type: Specifies the type of logger destination. Some of them allow specifying other options.

The allowed types are:

  • stdout: The logs are sent to standard output. There are no additional options.
  • stderr: The logs are sent to standard error output. There are no additional options.
  • file: Logs are written to a file. The file is reopened every time a configuration is re-read (therefore every time the application gets SIGHUP), which makes it work with logrotate.
    • filename: The path to the file where to put the logs.
  • network: The application connects to a given host and port over TCP and sends logs there.
    • host: The hostname (or IP address) to connect to.
    • port: The port to use.
  • syslog: Sends the logs to syslog.

daemon

Influences how daemonization is done.

  • user: The user to become. Either a numeric ID or name. If not present, it doesn't change the user.
  • group: Similar as user, but with group.
  • pid_file: A pid file to write on startup. If not present, nothing is stored.
  • workdir: A working directory it'll switch into. If not set, defaults to /.

Multithreaded applications

As daemonization is done by using fork, you should start any threads after you initialize the spirit. Otherwise you'll lose the threads (and further bad things will happen).

Common patterns

TODO

Modules

helpers

Helpers for integrating common configuration patterns.

validation

Helpers for configuration validation.

Structs

Builder

The builder of Spirit.

Empty

A struct that may be used when either configuration or command line options are not needed.

InnerBody

A workaround type for Box<FnOnce() -> Result<(), Error>.

InvalidFileType

An error returned whenever the user passes something not a file nor a directory as configuration.

MissingEquals

An error returned when the user passes a key-value option without equal sign.

Spirit

The main manipulation handle/struct of the library.

SyslogError

This error can be returned when initialization of logging to syslog fails.

WrapBody

A wrapper around a body.

Functions

log_errors

A wrapper around a fallible function that logs any returned errors, with all the causes and optionally the backtrace.

Type Definitions

ArcSwap

An atomic storage for Arc.

SpiritInner

A type alias for a Spirit with configuration held inside.