Skip to content

Commit

Permalink
Create proper errors for the lib in the type DaemonError, also no mor…
Browse files Browse the repository at this point in the history
…e unwraps on anywhere the code may fail on the main library code
  • Loading branch information
Matheus Xavier committed Jun 28, 2020
1 parent 2ee4463 commit 15b6441
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 55 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
[package]
name = "daemonize-me"
version = "0.2.1-pre"
version = "0.2.1"
authors = ["Matheus Xavier <mxavier@mail.cmdforge.com>"]
edition = "2018"
license = "BSD-3-Clause/Apache-2.0"
repository = "https://github.com/CardinalBytes/daemonize-me"
description = "Rust library to ease the task of creating daemons on unix-like systems"
readme = "README.md"
keywords = ["daemon", "daemonize", "daemonize-me", "unix"]
categories = ["os::unix-apis"]

Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ fn main() {
}
```

# OS support
## OS support
I will try to keep support for linux, freebsd and macos

| os | tier |
Expand All @@ -43,7 +43,16 @@ I will try to keep support for linux, freebsd and macos

For tier 1 any code that breaks the tests and or ci/cd is blocking for a release,
tier 2 compilation errors are release blocking, tier 3 are supported on a best effort basis,
and build failure as well as test failures are not blocking.
and build failure as well as test failures are not blocking.

## Supported Versions

Version support is as follows during this highly volatile initial development period:

| Version | Supported |
| ------- | ------------------ |
| master | Master is bleeding edge and thus inherently unstable |
| tagged versions | a tagged version until a stable release due to code volatility unless stated otherwise is unsupported |

# License

Expand Down
184 changes: 132 additions & 52 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@
mod ffi;
mod util;

extern crate anyhow;
extern crate libc;
extern crate nix;
use anyhow::{anyhow, Result};
use ffi::{GroupRecord, PasswdRecord};
use nix::fcntl::{open, OFlag};
use nix::sys::stat::{umask, Mode};
Expand All @@ -44,13 +42,88 @@ use nix::unistd::{
chdir, chown, close, dup2, fork, getpid, setgid, setsid, setuid, ForkResult, Gid, Pid, Uid,
};
use std::convert::TryFrom;
use std::error::Error;
use std::ffi::CString;
use std::fmt;
use std::fmt::{Debug, Display};
use std::fs::File;
use std::io::prelude::*;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::process::exit;

#[derive(Debug, PartialOrd, PartialEq, Clone)]
pub enum DaemonError {
/// Unable to fork
Fork,
/// Failed to chdir
ChDir,
/// Failed to open dev null
OpenDevNull,
/// Failed to close the file pointer of a stdio stream
CloseFp,
/// Invalid or nonexistent user
InvalidUser,
/// Invalid or nonexistent group
InvalidGroup,
/// Either group or user was specified but no the other
InvalidUserGroupPair,
/// Failed to execute initgroups
InitGroups,
/// Failed to set uid
SetUid,
/// Failed to set gid
SetGid,
/// Failed to chown the pid file
ChownPid,
/// Failed to create the pid file
OpenPid,
/// Failed to write to the pid file
WritePid,
/// Failed to redirect the standard streams
RedirectStream,
/// Umask bits are invalid
InvalidUmaskBits,
/// Failed to set sid
SetSid,
#[doc(hidden)]
__Nonexhaustive,
}

impl DaemonError {
fn __description(&self) -> &str {
match *self {
DaemonError::Fork => "Unable to fork",
DaemonError::ChDir => "Failed to chdir",
DaemonError::OpenDevNull => "Failed to open dev null",
DaemonError::CloseFp => "Failed to close the file pointer of a stdio stream",
DaemonError::InvalidUser => "Invalid or nonexistent user",
DaemonError::InvalidGroup => "Invalid or nonexistent group",
DaemonError::InvalidUserGroupPair => {
"Either group or user was specified but no the other"
}
DaemonError::InitGroups => "Failed to execute initgroups",
DaemonError::SetUid => "Failed to set uid",
DaemonError::SetGid => "Failed to set gid",
DaemonError::ChownPid => "Failed to chown the pid file",
DaemonError::OpenPid => "Failed to create the pid file",
DaemonError::WritePid => "Failed to write to the pid file",
DaemonError::RedirectStream => "Failed to redirect the standard streams",
DaemonError::InvalidUmaskBits => "Umask bits are invalid",
DaemonError::SetSid => "Failed to set sid",
DaemonError::__Nonexhaustive => unreachable!(),
}
}
}

impl Display for DaemonError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
std::fmt::Debug::fmt(&self.__description(), f)
}
}
impl Error for DaemonError {}
pub type Result<T> = std::result::Result<T, DaemonError>;

/// Expects: either the username or the uid
/// if the name is provided it will be resolved to an id
#[derive(Debug, Ord, PartialOrd, PartialEq, Eq, Clone)]
Expand All @@ -59,23 +132,23 @@ pub enum User {
}

impl<'uname> TryFrom<&'uname str> for User {
type Error = &'static str;
type Error = DaemonError;

fn try_from(uname: &'uname str) -> Result<User, Self::Error> {
fn try_from(uname: &'uname str) -> Result<User> {
match PasswdRecord::get_record_by_name(uname) {
Ok(record) => Ok(User::Id(record.pw_uid)),
Err(_) => Err("Could not retrieve uid from username"),
Err(_) => Err(DaemonError::InvalidUser),
}
}
}

impl TryFrom<String> for User {
type Error = &'static str;
type Error = DaemonError;

fn try_from(uname: String) -> Result<User, Self::Error> {
fn try_from(uname: String) -> Result<User> {
match PasswdRecord::get_record_by_name(uname.as_str()) {
Ok(record) => Ok(User::Id(record.pw_uid)),
Err(_) => Err("Could not retrieve uid from username"),
Err(_) => Err(DaemonError::InvalidUser),
}
}
}
Expand All @@ -94,23 +167,23 @@ pub enum Group {
}

impl<'uname> TryFrom<&'uname str> for Group {
type Error = &'static str;
type Error = DaemonError;

fn try_from(gname: &'uname str) -> Result<Group, Self::Error> {
fn try_from(gname: &'uname str) -> Result<Group> {
match GroupRecord::get_record_by_name(gname) {
Ok(record) => Ok(Group::Id(record.gr_gid)),
Err(_) => Err("Could not retrieve group id from name"),
Err(_) => Err(DaemonError::InvalidGroup),
}
}
}

impl TryFrom<String> for Group {
type Error = &'static str;
type Error = DaemonError;

fn try_from(gname: String) -> Result<Group, Self::Error> {
fn try_from(gname: String) -> Result<Group> {
match GroupRecord::get_record_by_name(gname.as_str()) {
Ok(record) => Ok(Group::Id(record.gr_gid)),
Err(_) => Err("Could not retrieve group id from name"),
Err(_) => Err(DaemonError::InvalidGroup),
}
}
}
Expand Down Expand Up @@ -181,26 +254,29 @@ pub struct Daemon {
}

fn redirect_stdio(stdin: &Stdio, stdout: &Stdio, stderr: &Stdio) -> Result<()> {
let devnull_fd = open(
let devnull_fd = match open(
Path::new("/dev/null"),
OFlag::O_APPEND,
Mode::from_bits(OFlag::O_RDWR.bits() as _).unwrap(),
)?;
) {
Ok(fd) => fd,
Err(_) => return Err(DaemonError::OpenDevNull),
};
let proc_stream = |fd, stdio: &Stdio| {
match close(fd) {
Ok(_) => (),
Err(_) => return Err(anyhow!("Failed to close stdio stream")),
Err(_) => return Err(DaemonError::CloseFp),
};
return match &stdio.inner {
StdioImp::Devnull => match dup2(devnull_fd, fd) {
Ok(_) => Ok(()),
Err(_) => Err(anyhow!("Failed to redirect stream to /dev/null")),
Err(_) => Err(DaemonError::RedirectStream),
},
StdioImp::RedirectToFile(file) => {
let raw_fd = file.as_raw_fd();
match dup2(raw_fd, fd) {
Ok(_) => Ok(()),
Err(_) => Err(anyhow!("Failed to redirect stream to file")),
Err(_) => Err(DaemonError::RedirectStream),
}
}
};
Expand Down Expand Up @@ -285,89 +361,93 @@ impl Daemon {
// Set up stream redirection as early as possible
redirect_stdio(&self.stdin, &self.stdout, &self.stderr)?;
if self.chown_pid_file && (self.user.is_none() || self.group.is_none()) {
return Err(anyhow!(
"You can't chmod the pid file without providing user and group"
));
return Err(DaemonError::InvalidUserGroupPair);
} else if (self.user.is_some() || self.group.is_some())
&& (self.user.is_none() || self.group.is_none())
{
return Err(anyhow!(
"If you provide a user or group the other must be provided too"
));
return Err(DaemonError::InvalidUserGroupPair);
}
// Fork and if the process is the parent exit gracefully
// if the process is the child just continue execution
match fork() {
Ok(ForkResult::Parent { child: _ }) => exit(0),
Ok(ForkResult::Child) => (),
Err(_) => return Err(anyhow!("Failed to fork")),
Err(_) => return Err(DaemonError::Fork),
}
// Set the umask either to 0o027 (rwxr-x---) or provided value
let umask_mode = match Mode::from_bits(self.umask as _) {
Some(mode) => mode,
None => return Err(anyhow!("umask is invalid")),
None => return Err(DaemonError::InvalidUmaskBits),
};
umask(umask_mode);
// Set the sid so the process isn't session orphan
setsid().expect("failed to setsid");
if let Err(_) = setsid() {
return Err(DaemonError::SetSid);
};
if let Err(_) = chdir::<Path>(self.chdir.as_path()) {
return Err(anyhow!("failed to chdir"));
return Err(DaemonError::ChDir);
};
pid = getpid();
// create pid file and if configured to, chmod it
if has_pid_file {
// chmod of the pid file is deferred to after checking for the presence of the user and group
let pid_file = &pid_file_path;
File::create(pid_file)?.write_all(pid.to_string().as_ref())?;
match File::create(pid_file) {
Ok(mut fp) => {
if let Err(_) = fp.write_all(pid.to_string().as_ref()) {
return Err(DaemonError::WritePid);
}
}
Err(_) => return Err(DaemonError::WritePid),
};
}
// Drop privileges and chown the requested files
if self.user.is_some() && self.group.is_some() {
let user = match self.user.unwrap() {
User::Id(id) => Uid::from_raw(id),
};

let uname = PasswdRecord::get_record_by_id(user.as_raw())?.pw_name;
let uname = match PasswdRecord::get_record_by_id(user.as_raw()) {
Ok(record) => record.pw_name,
Err(_) => return Err(DaemonError::InvalidUser),
};

let gr = match self.group.unwrap() {
Group::Id(id) => Gid::from_raw(id),
};

if self.chown_pid_file && has_pid_file {
chown(&pid_file_path, Some(user), Some(gr))?;
match chown(&pid_file_path, Some(user), Some(gr)) {
Ok(_) => (),
Err(_) => return Err(DaemonError::ChownPid),
};
}

match setgid(gr) {
Ok(_) => (),
Err(e) => return Err(anyhow!("failed to setgid to {} with error {}", &gr, e)),
Err(_) => return Err(DaemonError::SetGid),
};
#[cfg(not(target_os = "macos"))]
match initgroups(CString::new(uname)?.as_ref(), gr) {
Ok(_) => (),
Err(e) => {
return Err(anyhow!(
"failed to initgroups for user: {} and group: {} with error {}",
user,
gr,
e
))
}
};
{
let u_cstr = match CString::new(uname) {
Ok(cstr) => cstr,
Err(_) => return Err(DaemonError::SetGid),
};
match initgroups(&u_cstr, gr) {
Ok(_) => (),
Err(_) => return Err(DaemonError::InitGroups),
};
}
match setuid(user) {
Ok(_) => (),
Err(e) => return Err(anyhow!("failed to setuid to {} with error {}", user, e)),
Err(_) => return Err(DaemonError::SetUid),
}
}
// chdir
let chdir_path = self.chdir.to_owned();
match chdir::<Path>(chdir_path.as_ref()) {
Ok(_) => (),
Err(e) => {
return Err(anyhow!(
"Failed to chdir to {} with error {}",
chdir_path.as_path().display(),
e
))
}
Err(_) => return Err(DaemonError::ChDir),
};
// Now this process should be a daemon, return
Ok(())
Expand Down

0 comments on commit 15b6441

Please sign in to comment.