use daemonix::{Daemonize, Outcome, Stdio};
use log::{debug, error, info};
use pingora_error::{Error, ErrorType, OrErr, Result};
use std::ffi::CString;
use std::fs::{self, OpenOptions};
use std::os::unix::prelude::OpenOptionsExt;
use std::path::Path;
use std::process;
use std::thread;
use std::time::{Duration, Instant};
use crate::server::configuration::ServerConf;
#[derive(Debug)]
pub(crate) enum SignalError {
PermissionDenied,
OtherSignalError(i32),
}
impl std::fmt::Display for SignalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SignalError::PermissionDenied => write!(f, "permission denied (EPERM)"),
SignalError::OtherSignalError(errno) => {
write!(f, "kill failed with errno {errno}")
}
}
}
}
fn send_signal(pid: libc::pid_t, signal: libc::c_int) -> Result<(), SignalError> {
let ret = unsafe { libc::kill(pid, signal) };
if ret == 0 {
return Ok(());
}
let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(-1);
if errno == libc::EPERM {
Err(SignalError::PermissionDenied)
} else {
Err(SignalError::OtherSignalError(errno))
}
}
const DEFAULT_DAEMON_READY_TIMEOUT: Duration = Duration::from_secs(600);
const NOTIFY_RETRY_INTERVAL: Duration = Duration::from_millis(100);
const LIVENESS_CHECK_INTERVAL: Duration = Duration::from_millis(100);
fn move_old_pid(path: &str) {
if !Path::new(path).exists() {
debug!("Old pid file does not exist");
return;
}
let new_path = format!("{path}.old");
match fs::rename(path, &new_path) {
Ok(()) => {
debug!("Old pid file renamed");
}
Err(e) => {
error!(
"failed to rename pid file from {} to {}: {}",
path, new_path, e
);
}
}
}
unsafe fn gid_for_username(name: &CString) -> Option<libc::gid_t> {
let passwd = libc::getpwnam(name.as_ptr() as *const libc::c_char);
if !passwd.is_null() {
return Some((*passwd).pw_gid);
}
None
}
fn drop_privileges_in_parent(conf: &ServerConf) -> Result<()> {
let Some(user) = conf.user.as_ref() else {
return Ok(());
};
let user_cstr = CString::new(user.as_str()).or_err_with(ErrorType::Custom("Daemon"), || {
format!("drop_privileges_in_parent: user '{user}' invalid")
})?;
let passwd = unsafe { libc::getpwnam(user_cstr.as_ptr() as *const libc::c_char) };
if passwd.is_null() {
return Error::e_explain(
ErrorType::Custom("Daemon"),
format!("drop_privileges_in_parent: user '{user}' not found"),
);
}
let uid = unsafe { (*passwd).pw_uid };
let ret = unsafe { libc::setuid(uid) };
if ret == 0 {
Ok(())
} else {
Error::e_explain(
ErrorType::Custom("Daemon"),
format!(
"drop_privileges_in_parent: setuid({uid}) failed: {}",
std::io::Error::last_os_error()
),
)
}
}
pub struct DaemonizeResult {
pub notify_parent_pid: Option<u32>,
}
pub fn daemonize(conf: &ServerConf) -> DaemonizeResult {
let parent_pid = if conf.daemon_wait_for_ready {
Some(process::id())
} else {
None
};
move_old_pid(&conf.pid_file);
match build_daemonize(conf).execute() {
Outcome::Parent(result) => {
result.unwrap_or_else(|e| panic!("Daemonize failed: {e}"));
}
Outcome::Child(result) => {
result.unwrap_or_else(|e| panic!("Daemonize child setup failed: {e}"));
return DaemonizeResult {
notify_parent_pid: parent_pid,
};
}
}
if conf.daemon_wait_for_ready {
if let Err(e) = drop_privileges_in_parent(conf) {
error!("drop_privileges_in_parent failed: {e}");
process::exit(1);
}
let timeout = conf
.daemon_ready_timeout_seconds
.map(|n| Duration::from_secs(n.get()))
.unwrap_or(DEFAULT_DAEMON_READY_TIMEOUT);
info!(
"Waiting up to {:?} for daemon to signal readiness via SIGUSR1",
timeout
);
wait_for_ready_or_exit(&conf.pid_file, timeout);
}
process::exit(0);
}
fn build_parent_runtime() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to build tokio runtime for parent signal wait")
}
fn wait_for_ready_or_exit(pid_file: &str, timeout: Duration) {
let rt = build_parent_runtime();
let pid_file = pid_file.to_owned();
rt.block_on(async move {
use tokio::signal::unix::{signal, SignalKind};
use tokio::time::{interval, timeout as tokio_timeout};
let mut sigusr1 =
signal(SignalKind::user_defined1()).expect("failed to register SIGUSR1 listener");
let mut liveness_check = interval(LIVENESS_CHECK_INTERVAL);
let mut daemon_pid: Option<libc::pid_t> = None;
let result = tokio_timeout(timeout, async {
loop {
tokio::select! {
_ = sigusr1.recv() => {
info!("Daemon signaled readiness, parent exiting");
return;
}
_ = liveness_check.tick() => {
if daemon_pid.is_none() {
daemon_pid = try_read_pid_file(&pid_file);
}
if let Some(pid) = daemon_pid {
if !process_is_running(pid) {
error!(
"Daemon process (pid {pid}) is no longer running \
before signaling readiness, aborting"
);
process::exit(1);
}
}
}
}
}
})
.await;
if result.is_err() {
error!("Daemon did not signal readiness within {timeout:?}, aborting");
process::exit(1);
}
});
}
pub fn notify_parent_ready_for_fds(parent_pid: u32, notify_timeout: Duration) {
let parent_pid = parent_pid as libc::pid_t;
info!(
"Sending SIGUSR1 to parent process (pid {}) to signal daemon readiness",
parent_pid
);
let start = Instant::now();
while start.elapsed() < notify_timeout {
match send_signal(parent_pid, libc::SIGUSR1) {
Ok(()) => return,
Err(SignalError::PermissionDenied) => {
debug!(
"Permission denied sending SIGUSR1 to parent (pid {}), retrying in {:?}",
parent_pid, NOTIFY_RETRY_INTERVAL
);
thread::sleep(NOTIFY_RETRY_INTERVAL);
}
Err(SignalError::OtherSignalError(errno)) => {
error!(
"Failed to send SIGUSR1 to parent (pid {}): errno {errno}",
parent_pid
);
return;
}
}
}
error!(
"Permission denied sending SIGUSR1 to parent (pid {}), giving up after {:?}",
parent_pid, notify_timeout
);
}
fn try_read_pid_file(pid_file: &str) -> Option<libc::pid_t> {
fs::read_to_string(pid_file)
.ok()
.and_then(|c| c.trim().parse().ok())
}
fn process_is_running(pid: libc::pid_t) -> bool {
send_signal(pid, 0).is_ok()
}
fn build_daemonize(conf: &ServerConf) -> Daemonize<()> {
let daemonize = Daemonize::new()
.umask(0o007) .pid_file(&conf.pid_file);
let daemonize = if let Some(working_directory) = conf.working_directory.as_ref() {
daemonize.working_directory(working_directory)
} else {
daemonize
};
let daemonize = if let Some(error_log) = conf.error_log.as_ref() {
let err = OpenOptions::new()
.append(true)
.create(true)
.read(true)
.custom_flags(libc::O_NONBLOCK)
.open(error_log)
.unwrap();
daemonize.stderr(err)
} else {
daemonize.stdout(Stdio::keep()).stderr(Stdio::keep())
};
let daemonize = match conf.user.as_ref() {
Some(user) => {
let user_cstr = CString::new(user.as_str()).unwrap();
#[cfg(target_os = "macos")]
let group_id = unsafe { gid_for_username(&user_cstr).map(|gid| gid as i32) };
#[cfg(target_os = "freebsd")]
let group_id = unsafe { gid_for_username(&user_cstr).map(|gid| gid as u32) };
#[cfg(target_os = "linux")]
let group_id = unsafe { gid_for_username(&user_cstr) };
daemonize
.privileged_action(move || {
if let Some(gid) = group_id {
unsafe {
libc::initgroups(user_cstr.as_ptr() as *const libc::c_char, gid);
}
}
})
.user(user.as_str())
.chown_pid_file(true)
}
None => daemonize,
};
match conf.group.as_ref() {
Some(group) => daemonize.group(group.as_str()),
None => daemonize,
}
}