#![doc(
html_root_url = "https://docs.rs/spirit-daemonize/0.3.1/spirit_daemonize/",
test(attr(deny(warnings)))
)]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
use std::env;
use std::fs::OpenOptions;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::process;
use std::sync::Arc;
use err_context::prelude::*;
use log::{debug, trace, warn};
use nix::sys::stat::{self, Mode};
use nix::unistd::{self, ForkResult, Gid, Uid};
use serde::{Deserialize, Serialize};
use spirit::extension::{Extensible, Extension};
use spirit::validation::Action;
use spirit::AnyError;
use structdoc::StructDoc;
#[cfg(feature = "cfg-help")]
use structopt::StructOpt;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "cfg-help", derive(StructDoc))]
#[serde(untagged)]
pub enum SecId {
Name(String),
Id(u32),
#[serde(skip)]
Nothing,
}
impl SecId {
fn is_nothing(&self) -> bool {
self == &SecId::Nothing
}
}
impl Default for SecId {
fn default() -> Self {
SecId::Nothing
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "cfg-help", derive(StructDoc))]
#[serde(rename_all = "kebab-case")]
pub struct Daemon {
#[serde(default, skip_serializing_if = "SecId::is_nothing")]
pub user: SecId,
#[serde(default, skip_serializing_if = "SecId::is_nothing")]
pub group: SecId,
#[serde(skip_serializing_if = "Option::is_none")]
pub pid_file: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workdir: Option<PathBuf>,
#[serde(default)]
pub daemonize: bool,
#[serde(default, skip)]
sentinel: (),
}
impl Daemon {
pub fn daemonize(&self) -> Result<(), AnyError> {
debug!("Preparing to daemonize with {:?}", self);
stat::umask(Mode::empty()); let workdir = self
.workdir
.as_ref()
.map(|pb| pb as &Path)
.unwrap_or_else(|| Path::new("/"));
trace!("Changing working directory to {:?}", workdir);
env::set_current_dir(workdir)?;
if self.daemonize {
trace!("Redirecting stdio");
let devnull = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open("/dev/null")?;
for fd in &[0, 1, 2] {
unistd::dup2(devnull.as_raw_fd(), *fd)?;
}
trace!("Doing double fork");
if let ForkResult::Parent { .. } = unistd::fork()? {
process::exit(0);
}
unistd::setsid()?;
if let ForkResult::Parent { .. } = unistd::fork()? {
process::exit(0);
}
} else {
trace!("Not going to background");
}
if let Some(ref file) = self.pid_file {
let mut f = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o644)
.open(file)?;
writeln!(f, "{}", unistd::getpid())?;
}
match self.group {
SecId::Id(id) => unistd::setgid(Gid::from_raw(id))?,
SecId::Name(ref name) => privdrop::PrivDrop::default().group(&name).apply()?,
SecId::Nothing => (),
}
match self.user {
SecId::Id(id) => unistd::setuid(Uid::from_raw(id))?,
SecId::Name(ref name) => privdrop::PrivDrop::default().user(&name).apply()?,
SecId::Nothing => (),
}
Ok(())
}
pub fn extension<E, F>(extractor: F) -> impl Extension<E>
where
E: Extensible<Ok = E>,
F: Fn(&E::Config, &E::Opts) -> Self + Send + 'static,
{
let mut previous_daemon = None;
let validator_hook =
move |_: &_, cfg: &Arc<E::Config>, opts: &_| -> Result<Action, AnyError> {
let daemon = extractor(cfg, opts);
if let Some(previous) = previous_daemon.as_ref() {
if previous != &daemon {
warn!("Can't change daemon config at runtime");
}
return Ok(Action::new());
}
daemon.daemonize().context("Failed to daemonize")?;
previous_daemon = Some(daemon);
Ok(Action::new())
};
move |e: E| e.config_validator(validator_hook)
}
}
impl From<UserDaemon> for Daemon {
fn from(ud: UserDaemon) -> Daemon {
Daemon {
pid_file: ud.pid_file,
workdir: ud.workdir,
daemonize: ud.daemonize,
..Daemon::default()
}
}
}
#[derive(Clone, Debug, StructOpt)]
pub struct Opts {
#[structopt(short = "d", long = "daemonize")]
daemonize: bool,
#[structopt(short = "f", long = "foreground")]
foreground: bool,
}
impl Opts {
pub fn daemonize(&self) -> bool {
self.daemonize && !self.foreground
}
pub fn transform(&self, daemon: Daemon) -> Daemon {
Daemon {
daemonize: self.daemonize(),
..daemon
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "cfg-help", derive(StructDoc))]
#[serde(rename_all = "kebab-case")]
pub struct UserDaemon {
#[serde(skip_serializing_if = "Option::is_none")]
pid_file: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
workdir: Option<PathBuf>,
#[serde(default)]
daemonize: bool,
}
impl UserDaemon {
pub fn into_daemon(self) -> Daemon {
self.into()
}
}