#![deny(warnings, missing_docs, rust_2018_idioms, clippy::pedantic)]
#![cfg(target_family = "unix")]
pub mod options;
pub use options::Stdio;
type InvocationResult<T = ()> = Result<T, Error>;
type DaemonResult<T = ()> = Result<T, (DaemonError, nix::Error)>;
#[derive(thiserror::Error, Debug, PartialEq, Eq, Clone, Copy)]
pub enum Error {
#[error("Daemon pid file already exists")]
DaemonAlreadyRunning,
#[error("Failed to close file descriptors: {0}")]
CloseDescriptors(nix::Error),
#[error("Failed to fetch open file descriptors: {0}")]
ListOpenDescriptors(nix::Error),
#[error("Failed to reset signal handlers: {0}")]
ResetSignals(nix::Error),
#[error("Failed to block signals: {0}")]
BlockSignals(nix::Error),
#[error("Failed to create status reporting pipe: {0}")]
CreatePipe(nix::Error),
#[error("Failed to fork daemon process: {0}")]
Fork(nix::Error),
#[error("Failed to receive daemon status report: {0}")]
ReadStatus(nix::Error),
#[error("Daemon failed to initialize: {error}: {cause}")]
Daemon {
error: DaemonError,
cause: nix::Error,
},
#[error("Daemon failed to initialize: {code}")]
Initialization {
code: ErrorCode,
},
}
impl Error {
fn daemon(error: DaemonError, cause: nix::Error) -> Self {
Self::Daemon { error, cause }
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct ErrorCode(pub i32);
impl<I: Into<i32>> From<I> for ErrorCode {
fn from(code: I) -> Self {
Self(code.into())
}
}
impl std::fmt::Display for ErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(thiserror::Error, Debug, Copy, Clone, Eq, PartialEq)]
#[repr(u8)]
pub enum DaemonError {
#[error("Failed to detach from session")]
Setsid = 1,
#[error("Failed to double fork daemon")]
Fork = 2,
#[error("Failed to change root directory")]
ChangeRoot = 3,
#[error("Failed to set user")]
SetUser = 4,
#[error("Failed to set group")]
SetGroup = 5,
#[error("Failed to unblock signals")]
UnblockSignals = 6,
#[error("Failed to close file descriptors")]
CloseDescriptors = 7,
#[error("Failed to fetch open file descriptors")]
ListOpenDescriptors = 8,
#[error("Failed to reset signal handlers")]
ResetSignals = 9,
#[error("Failed to redirect stdin")]
RedirectStdin = 10,
#[error("Failed to redirect stdout")]
RedirectStdout = 11,
#[error("Failed to redirect stderr")]
RedirectStderr = 12,
#[error("Failed initialize daemon after forking")]
Initialization = 13,
}
macro_rules! from_num {
($name: ident, $num: ty, $($variant: ident),*) => {
impl $name {
#[inline]
const fn from_num(num: $num) -> Option<Self> {
fn _check(num: $name) {
match num {
$($name::$variant => (),)*
}
}
match num {
$(_ if Self::$variant as $num == num => Some(Self::$variant),)*
_ => None,
}
}
}
};
}
from_num!(
DaemonError,
u8,
Setsid,
Fork,
ChangeRoot,
SetUser,
SetGroup,
UnblockSignals,
CloseDescriptors,
ListOpenDescriptors,
ResetSignals,
RedirectStdin,
RedirectStdout,
RedirectStderr,
Initialization
);
#[derive(Debug)]
enum ForkResult {
Invoker(Pipe, nix::unistd::Pid),
Daemon(Pipe),
}
pub fn daemonize() -> InvocationResult {
daemonize_inner(options::Options::new(), || Ok(()))
}
pub fn daemonize_with_init<F, R>(initialization: F) -> InvocationResult<R>
where
F: FnOnce() -> Result<R, ErrorCode>,
{
daemonize_inner(options::Options::new(), initialization)
}
#[must_use]
pub fn with_options() -> options::Options {
options::Options::new()
}
fn daemonize_inner<F, R>(options: options::Options, initialization: F) -> InvocationResult<R>
where
F: FnOnce() -> Result<R, ErrorCode>,
{
close_descriptors()?;
reset_signals()?;
block_signals()?;
let pipe = Pipe::new()?;
match fork(pipe)? {
ForkResult::Invoker(pipe, child) => {
finalize_invoker(pipe, child)?;
std::process::exit(0);
}
ForkResult::Daemon(pipe) => {
if let Err((error, cause)) = finalize_daemon(options) {
exit_error(pipe, error, cause);
} else {
match initialization() {
Ok(r) => {
pipe.ok();
Ok(r)
}
Err(e) => exit_error(pipe, DaemonError::Initialization, e),
}
}
}
}
}
fn close_descriptors() -> InvocationResult {
#[allow(clippy::needless_pass_by_value)]
fn file_to_fd(entry: std::fs::DirEntry) -> Option<i32> {
entry
.file_name()
.to_str()
.and_then(|name| name.parse().ok())
}
#[allow(clippy::needless_pass_by_value)]
fn err_list(err: std::io::Error) -> Error {
Error::ListOpenDescriptors(
err.raw_os_error()
.map_or_else(nix::errno::Errno::last, nix::errno::from_i32),
)
}
std::path::PathBuf::from("/dev/fd/")
.read_dir()
.map_err(err_list)?
.into_iter()
.filter_map(Result::ok)
.filter_map(file_to_fd)
.filter(|fd| *fd > 2)
.collect::<Vec<_>>()
.into_iter()
.map(nix::unistd::close)
.filter_map(Result::err)
.filter(|e| nix::Error::EBADF.ne(e))
.map(Error::CloseDescriptors)
.try_fold((), |_, e| Err(e))
}
fn reset_signals() -> InvocationResult {
use nix::sys::signal as nix;
nix::Signal::iterator()
.filter(|signal| signal != &nix::SIGKILL && signal != &nix::SIGSTOP)
.map(|signal| unsafe { nix::signal(signal, nix::SigHandler::SigDfl) })
.find_map(Result::err)
.map_or(Ok(()), |err| Err(Error::ResetSignals(err)))
}
fn block_signals() -> InvocationResult {
use nix::sys::signal as nix;
let mask = nix::SigmaskHow::SIG_BLOCK;
let sigset = nix::SigSet::all();
nix::sigprocmask(mask, Some(&sigset), None).map_err(Error::BlockSignals)
}
fn unblock_signals() -> nix::Result<()> {
use nix::sys::signal as nix;
let mask = nix::SigmaskHow::SIG_UNBLOCK;
let sigset = nix::SigSet::all();
nix::sigprocmask(mask, Some(&sigset), None)
}
fn fork(pipe: Pipe) -> InvocationResult<ForkResult> {
use nix::unistd;
match unsafe { unistd::fork() } {
Err(err) => Err(Error::Fork(err)),
Ok(unistd::ForkResult::Parent { child }) => Ok(ForkResult::Invoker(pipe, child)),
Ok(unistd::ForkResult::Child) => match unistd::setsid() {
Err(err) => exit_error(pipe, DaemonError::Setsid, err),
Ok(_) => match unsafe { nix::unistd::fork() } {
Err(err) => exit_error(pipe, DaemonError::Fork, err),
Ok(unistd::ForkResult::Parent { child: _ }) => exit_success(pipe),
Ok(unistd::ForkResult::Child) => Ok(ForkResult::Daemon(pipe)),
},
},
}
}
fn finalize_invoker(pipe: Pipe, child: nix::unistd::Pid) -> InvocationResult {
let _ = unblock_signals();
let _ = nix::sys::wait::waitpid(child, None);
pipe.read().and_then(Pipe::read).map(|_| ())
}
fn finalize_daemon(options: options::Options) -> DaemonResult {
unblock_signals().map_err(|err| (DaemonError::UnblockSignals, err))?;
nix::unistd::chdir(&options.root).map_err(|err| (DaemonError::ChangeRoot, err))?;
nix::sys::stat::umask(nix::sys::stat::Mode::empty());
change_user(options.user, options.group)?;
redirect_streams(options.stdin, options.stdout, options.stderr)
}
fn change_user(user: Option<nix::unistd::User>, group: Option<nix::unistd::Group>) -> DaemonResult {
if let Some(user) = user {
nix::unistd::setuid(user.uid).map_err(|err| (DaemonError::SetUser, err))?;
}
if let Some(group) = group {
nix::unistd::setgid(group.gid).map_err(|err| (DaemonError::SetGroup, err))?;
}
Ok(())
}
fn redirect_streams(
stdin: Option<Stdio<options::Input>>,
stdout: Option<Stdio<options::Output>>,
stderr: Option<Stdio<options::Output>>,
) -> DaemonResult {
use nix::libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO};
use std::os::unix::io::{AsRawFd, RawFd};
use DaemonError::{RedirectStderr, RedirectStdin, RedirectStdout};
fn redirect_stream<D, F>(
devnull_fd: &mut D,
stdio: Option<Stdio<F>>,
fd: RawFd,
error: DaemonError,
) -> DaemonResult
where
D: FnMut(DaemonError) -> DaemonResult<RawFd>,
F: options::File,
{
if let Some(stdio) = stdio {
nix::unistd::close(fd).map_err(|err| (error, err))?;
let new_fd = match stdio {
Stdio::Null => devnull_fd(error)?,
Stdio::Fd(fd) => fd,
Stdio::File(file) => {
let open_file = file.open().map_err(|_| (error, nix::Error::last()))?;
let raw_fd = open_file.as_raw_fd();
std::mem::forget(open_file);
raw_fd
}
};
nix::unistd::dup2(new_fd, fd).map_err(|err| (error, err))?;
}
Ok(())
}
let mut devnull = None::<std::fs::File>;
let mut devnull_fd = |error: DaemonError| -> DaemonResult<RawFd> {
if devnull.is_none() {
devnull = Some(
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/null")
.map_err(|_| (error, nix::Error::last()))?,
);
}
Ok(devnull.as_ref().unwrap().as_raw_fd())
};
redirect_stream(&mut devnull_fd, stdin, STDIN_FILENO, RedirectStdin)?;
redirect_stream(&mut devnull_fd, stdout, STDOUT_FILENO, RedirectStdout)?;
redirect_stream(&mut devnull_fd, stderr, STDERR_FILENO, RedirectStderr)
}
#[derive(Debug)]
struct Pipe {
reader: std::os::unix::io::RawFd,
writer: std::os::unix::io::RawFd,
}
impl std::ops::Drop for Pipe {
fn drop(&mut self) {
let _ = nix::unistd::close(self.reader);
let _ = nix::unistd::close(self.writer);
}
}
impl Pipe {
fn new() -> InvocationResult<Self> {
nix::unistd::pipe()
.map(|(reader, writer)| Self { reader, writer })
.map_err(Error::CreatePipe)
}
fn read(self) -> InvocationResult<Self> {
let mut status = [0_u8];
nix::unistd::read(self.reader, &mut status).map_err(Error::ReadStatus)?;
if status[0] == 0 {
Ok(self)
} else {
let mut error = [0_u8; 4];
nix::unistd::read(self.reader, &mut error).map_err(Error::ReadStatus)?;
let error = i32::from_be_bytes(error);
match DaemonError::from_num(status[0]) {
Some(DaemonError::Initialization) | None => {
Err(Error::Initialization { code: error.into() })
}
Some(e) => Err(Error::daemon(e, nix::errno::from_i32(error))),
}
}
}
fn ok(self) {
let _ = nix::unistd::write(self.writer, &[0]);
}
fn error(self, error: DaemonError, errno: i32) {
let errno_ptr = errno.to_be_bytes();
let _ = nix::unistd::write(self.writer, &[error as u8]);
let _ = nix::unistd::write(self.writer, &errno_ptr);
}
}
fn exit_success(pipe: Pipe) -> ! {
pipe.ok();
std::process::exit(0);
}
fn exit_error(pipe: Pipe, error: DaemonError, cause: impl Cause) -> ! {
let cause = cause.into_i32();
pipe.error(error, cause);
std::process::exit(cause);
}
trait Cause {
fn into_i32(self) -> i32;
}
impl Cause for nix::Error {
fn into_i32(self) -> i32 {
self as i32
}
}
impl Cause for ErrorCode {
fn into_i32(self) -> i32 {
self.0
}
}
#[cfg(test)]
mod test {
#[test]
fn to_big_endian() {
let int: i32 = 0x09ab_cdef;
let array = int.to_be_bytes();
assert_eq!(array, [0x09, 0xab, 0xcd, 0xef]);
}
#[test]
fn short_circuit_err() {
let list = [Ok(6), Ok(4), Ok(3), Err(5), Ok(1), Err(7), Ok(2), Ok(8)];
let mut count = 0;
let err = list
.into_iter()
.map(|i| {
count += 1;
i
})
.filter_map(Result::err)
.filter(|e| *e > 5)
.try_fold((), |_, e| Err(e));
assert_eq!(Err(7), err);
assert_eq!(6, count);
}
#[test]
fn short_circuit_no_err() {
let list = [Ok(6), Ok(4), Ok(3), Err(5), Ok(1), Ok(7), Ok(2), Ok(8)];
let mut count = 0;
let err = list
.into_iter()
.map(|i| {
count += 1;
i
})
.filter_map(Result::err)
.filter(|e| *e > 5)
.try_fold((), |_, e| Err(e));
assert_eq!(Ok(()), err);
assert_eq!(8, count);
}
}