// Copyright 2021-2022 Ian Jackson and contributors to Hippotat // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-Hippotat-OpenSSL-Exception // There is NO WARRANTY. use std::convert::TryInto; use std::ffi::CStr; use std::io::IoSlice; use std::os::raw::{c_char, c_int}; use std::os::unix::io::RawFd; use std::slice; use std::str; use std::thread::panicking; use easy_ext::ext; use nix::errno::*; use nix::fcntl::*; use nix::unistd::*; use nix::sys::stat::*; use nix::sys::signal::*; use nix::sys::wait::*; use hippotat::prelude as prelude; use prelude::{compat, default}; pub struct Daemoniser { drop_bomb: Option<()>, intermediate_pid: Pid, null_fd: RawFd, st_wfd: RawFd, } fn crashv(ms: &[IoSlice<'_>]) -> ! { unsafe { let _ = compat::writev(2, ms); libc::_exit(18); } } macro_rules! crashv { { $( $m:expr ),* $(,)? } => { match [ "hippotatd: ", $( $m, )* "\n", ] { ms => { let ms = ms.map(|m| IoSlice::new(m.as_bytes())); crashv(&ms) } } } } macro_rules! cstr { { $b:tt } => { CStr::from_bytes_with_nul($b) .unwrap_or_else(|_| crashm("cstr not nul terminated?! bug!")) } } fn crashm(m: &str) -> ! { crashv!(m) } fn crashe(m: &str, e: Errno) -> ! { crashv!(m, ": ", e.desc()) } #[ext] impl nix::Result { fn context(self, m: &str) -> T { match self { Ok(y) => y, Err(e) => crashe(m, e), } } } const ITOA_BUFL: usize = 12; fn c_itoa(value: c_int, buf: &mut [u8; ITOA_BUFL]) -> &str { unsafe { *buf = [b'.'; ITOA_BUFL]; libc::snprintf({ let buf: *mut u8 = buf.as_mut_ptr(); buf as *mut c_char }, ITOA_BUFL-2, cstr!(b"%x\0").as_ptr(), value); } let s = buf.splitn(2, |&c| c == b'\0').next() .unwrap_or_else(|| crashm("splitn no next")); str::from_utf8(s).unwrap_or_else(|_| crashm("non-utf-8 from snprintf!")) } unsafe fn mdup2(oldfd: RawFd, newfd: RawFd, what: &str) { match dup2(oldfd, newfd) { Ok(got) if got == newfd => { }, Ok(_) => crashm("dup2 gave wrong return value"), Err(e) => crashv!("dup2 ", what, ": ", e.desc()), } } unsafe fn write_status(st_wfd: RawFd, estatus: u8) { match compat::write(st_wfd, slice::from_ref(&estatus)) { Ok(1) => {} Ok(_) => crashm("write child startup exit status: short write"), Err(e) => crashe("write child startup exit status", e), } } unsafe fn parent(st_rfd: RawFd) -> ! { let mut exitstatus = 0u8; loop { match read(st_rfd, slice::from_mut(&mut exitstatus)) { Ok(0) => crashm("startup/daemonisation failed"), Ok(1) => libc::_exit(exitstatus.into()), Ok(_) => crashm("read startup: excess read!"), Err(e) if e == Errno::EINTR => continue, Err(e) => crashe("read startup signal pipe", e), } } } unsafe fn intermediate(child: Pid, st_wfd: RawFd) -> ! { let mut wstatus: c_int = 0; let r = libc::waitpid(child.as_raw(), &mut wstatus, 0); if r == -1 { crashe("await child startup status", compat::nix_last_errno()) } if r != child.as_raw() { crashm("await child startup status: wrong pid") } let cooked = WaitStatus::from_raw(child, wstatus) .context("await child startup status: convert wait status"); match cooked { WaitStatus::Exited(_, estatus) => { let estatus: u8 = estatus.try_into() .unwrap_or_else(|_| crashm( "await child startup status: exit status out of range!")); write_status(st_wfd, estatus); libc::_exit(0); } WaitStatus::Signaled(_, signal, coredump) => { crashv!("startup failed: died due to signal: ", signal.as_str(), if coredump { " (core dumped)" } else { "" }); }, _ => { crashv!("child startup exit status was strange! 0x", c_itoa(wstatus, &mut default())) } } } impl Daemoniser { /// Start daemonising - call before any threads created! pub fn phase1() -> Self { unsafe { let null_fd = open(cstr!(b"/dev/null\0"), OFlag::O_RDWR, Mode::empty()) .context("open /dev/null"); mdup2(null_fd, 0, "null onto stdin"); let (st_rfd, st_wfd) = compat::pipe().context("pipe"); match fork().context("fork (1)") { ForkResult::Child => { } ForkResult::Parent { child: _ } => { close(st_wfd).context("close st_wfd pipe"); parent(st_rfd) }, } close(st_rfd).context("close st_rfd pipe"); setsid().context("setsid"); let intermediate_pid = Pid::this(); match fork().context("fork (2)") { ForkResult::Child => { } ForkResult::Parent { child } => { intermediate(child, st_wfd) }, } Daemoniser { drop_bomb: Some(()), intermediate_pid, null_fd, st_wfd, } } } pub fn complete(mut self) { unsafe { mdup2(self.null_fd, 1, "null over stdin"); if Pid::parent() != self.intermediate_pid { crashm( "startup complete, but our parent is no longer the intermediate?"); } kill(self.intermediate_pid, Some(Signal::SIGKILL)) .context("kill intermediate (after startup complete)"); write_status(self.st_wfd, 0); mdup2(self.null_fd, 2, "null over stderrr"); self.drop_bomb.take(); } } } impl Drop for Daemoniser { fn drop(&mut self) { if let Some(()) = self.drop_bomb.take() { if panicking() { // We will crash in due course, having printed some messages // to stderr, presumably. return } else { panic!("Daemonizer object dropped unexpectedly, startup failed"); } } } }