#![doc(test(attr(deny(warnings))))]
#![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::de::DeserializeOwned;
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, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub struct Daemonize {
pub daemonize: bool,
pub pid_file: Option<PathBuf>,
}
impl Daemonize {
pub unsafe fn daemonize(&self) -> Result<(), AnyError> {
if self.daemonize {
trace!("Redirecting stdio");
let devnull = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open("/dev/null")
.context("Failed to open /dev/null")?;
for fd in &[0, 1, 2] {
unistd::dup2(devnull.as_raw_fd(), *fd)
.with_context(|_| format!("Failed to redirect FD {}", fd))?;
}
trace!("Doing double fork");
if let ForkResult::Parent { .. } = unistd::fork().context("Failed to fork")? {
process::exit(0);
}
unistd::setsid()?;
if let ForkResult::Parent { .. } = unistd::fork().context("Failed to fork")? {
process::exit(0);
}
} else {
trace!("Not going to background");
}
if let Some(file) = self.pid_file.as_ref() {
let mut f = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o644)
.open(file)
.with_context(|_| format!("Failed to write PID file {}", file.display()))?;
writeln!(f, "{}", unistd::getpid())?;
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "cfg-help", derive(StructDoc))]
#[serde(untagged)]
#[non_exhaustive]
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")]
#[non_exhaustive]
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,
}
impl Daemon {
pub fn prepare(&self) -> Result<Daemonize, 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)
.with_context(|_| format!("Failed to switch to workdir {}", workdir.display()))?;
match self.group {
SecId::Id(id) => {
unistd::setgid(Gid::from_raw(id)).context("Failed to change the group")?
}
SecId::Name(ref name) => privdrop::PrivDrop::default()
.group(&name)
.apply()
.context("Failed to change the group")?,
SecId::Nothing => (),
}
match self.user {
SecId::Id(id) => {
unistd::setuid(Uid::from_raw(id)).context("Failed to change the user")?
}
SecId::Name(ref name) => privdrop::PrivDrop::default()
.user(&name)
.apply()
.context("Failed to change the user")?,
SecId::Nothing => (),
}
if let Some(file) = self.pid_file.as_ref() {
let _ = OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.mode(0o644)
.open(file)
.with_context(|_| format!("Writing the PID file {}", file.display()))?;
}
Ok(Daemonize {
daemonize: self.daemonize,
pid_file: self.pid_file.clone(),
})
}
pub unsafe fn daemonize(&self) -> Result<(), AnyError> {
self.prepare()?.daemonize()?;
Ok(())
}
}
impl From<UserDaemon> for Daemon {
fn from(ud: UserDaemon) -> Daemon {
Daemon {
pid_file: ud.pid_file,
workdir: ud.workdir,
daemonize: ud.daemonize,
..Daemon::default()
}
}
}
impl From<(Daemon, Opts)> for Daemon {
fn from((d, o): (Daemon, Opts)) -> Self {
o.transform(d)
}
}
impl From<(UserDaemon, Opts)> for Daemon {
fn from((d, o): (UserDaemon, Opts)) -> Self {
o.transform(d.into())
}
}
#[cfg_attr(not(doc), allow(missing_docs))]
#[cfg_attr(
doc,
doc = r#"
Command line options fragment.
This adds the `-d` (`--daemonize`) and `-f` (`--foreground`) flag to command line. These
override whatever is written in configuration (if merged together with the configuration).
This can be used to transform the [`Daemon`] before daemonization.
See the [`extension`] for how to use it.
"#
)]
#[derive(Clone, Debug, StructOpt)]
#[non_exhaustive]
pub struct Opts {
#[structopt(short, long)]
pub daemonize: bool,
#[structopt(short, long)]
pub 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")]
#[non_exhaustive]
pub struct UserDaemon {
#[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,
}
impl UserDaemon {
pub fn into_daemon(self) -> Daemon {
self.into()
}
}
pub unsafe fn extension<E, C, D>(extractor: C) -> impl Extension<E>
where
E: Extensible<Ok = E>,
E::Config: DeserializeOwned + Send + Sync + 'static,
E::Opts: StructOpt + Send + Sync + 'static,
C: Fn(&E::Config, &E::Opts) -> D + Send + Sync + 'static,
D: Into<Daemon>,
{
move |e: E| {
let init = move |_: &_, cfg: &Arc<_>, opts: &_| -> Result<Action, AnyError> {
let d = extractor(cfg, opts);
d.into().daemonize()?;
Ok(Action::new())
};
e.config_validator(init)
}
}