chiark / gitweb /
c2b39a4ebd17158c671b9387c58a59a4edc33899
[hippotat.git] / server / daemon.rs
1 // Copyright 2021-2022 Ian Jackson and contributors to Hippotat
2 // SPDX-License-Identifier: GPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 use std::convert::TryInto;
6 use std::ffi::{c_int, CStr};
7 use std::io::IoSlice;
8 use std::os::unix::io::RawFd;
9 use std::slice;
10 use std::str;
11 use std::thread::panicking;
12
13 use extend::ext;
14
15 use nix::errno::*;
16 use nix::fcntl::*;
17 use nix::unistd::*;
18 use nix::sys::stat::*;
19 use nix::sys::signal::*;
20 use nix::sys::uio::*;
21 use nix::sys::wait::*;
22
23 use hippotat::prelude as prelude;
24 use prelude::default;
25
26 pub struct Daemoniser {
27   drop_bomb: Option<()>,
28   intermediate_pid: Pid,
29   null_fd: RawFd,
30   st_wfd: RawFd,
31 }
32
33 fn crashv(ms: &[IoSlice<'_>]) -> ! {
34   unsafe {
35     let _ = writev(2, ms);
36     libc::_exit(18);
37   }
38 }
39   
40 macro_rules! crashv { { $( $m:expr ),* $(,)? } => {
41   match [
42     "hippotatd: ",
43     $( $m, )*
44     "\n",
45   ] {
46     ms => {
47       let ms = ms.map(|m| IoSlice::new(m.as_bytes()));
48       crashv(&ms)
49     }
50   }
51 } }
52
53 macro_rules! cstr { { $b:tt } => {
54   CStr::from_bytes_with_nul($b)
55     .unwrap_or_else(|_| crashm("cstr not nul terminated?! bug!"))
56 } }
57
58 fn crashm(m: &str) -> ! {
59   crashv!(m)
60 }
61 fn crashe(m: &str, e: Errno) -> ! {
62   crashv!(m, ": ", e.desc())
63 }
64
65 #[ext]
66 impl<T> nix::Result<T> {
67   fn context(self, m: &str) -> T {
68     match self {
69       Ok(y) => y,
70       Err(e) => crashe(m, e),
71     }
72   }
73 }
74
75 const ITOA_BUFL: usize = 12;
76 fn c_itoa(value: c_int, buf: &mut [u8; ITOA_BUFL]) -> &str {
77   unsafe {
78     *buf = [b'.'; ITOA_BUFL];
79     libc::snprintf({ let buf: *mut u8 = buf.as_mut_ptr(); buf as *mut i8 },
80                    ITOA_BUFL-2,
81                    cstr!(b"%x\0").as_ptr(),
82                    value);
83   }
84   let s = buf.splitn(2, |&c| c == b'\0').next()
85     .unwrap_or_else(|| crashm("splitn no next"));
86   str::from_utf8(s).unwrap_or_else(|_| crashm("non-utf-8 from snprintf!"))
87 }
88
89 unsafe fn mdup2(oldfd: RawFd, newfd: RawFd, what: &str) {
90   match dup2(oldfd, newfd) {
91     Ok(got) if got == newfd => { },
92     Ok(_) => crashm("dup2 gave wrong return value"),
93     Err(e) => crashv!("dup2 ", what, ": ", e.desc()),
94   }
95 }
96
97 unsafe fn write_status(st_wfd: RawFd, estatus: u8) {
98   match write(st_wfd, slice::from_ref(&estatus)) {
99     Ok(1) => {}
100     Ok(_) => crashm("write child startup exit status: short write"),
101     Err(e) => crashe("write child startup exit status", e),
102   }
103 }
104
105 unsafe fn parent(st_rfd: RawFd) -> ! {
106   let mut exitstatus = 0u8;
107   loop {
108     match read(st_rfd, slice::from_mut(&mut exitstatus)) {
109       Ok(0) => crashm("startup/daemonisation failed"),
110       Ok(1) => libc::_exit(exitstatus.into()),
111       Ok(_) => crashm("read startup: excess read!"),
112       Err(e) if e == Errno::EINTR => continue,
113       Err(e) => crashe("read startup signal pipe", e),
114     }
115   }
116 }
117
118 unsafe fn intermediate(child: Pid, st_wfd: RawFd) -> ! {
119   let mut wstatus: c_int = 0;
120
121   let r = libc::waitpid(child.as_raw(), &mut wstatus, 0);
122   if r == -1 { crashe("await child startup status",
123                       nix::errno::from_i32(errno())) }
124   if r != child.as_raw() { crashm("await child startup status: wrong pid") }
125
126   let cooked = WaitStatus::from_raw(child, wstatus)
127     .context("await child startup status: convert wait status");
128   match cooked {
129     WaitStatus::Exited(_, estatus) => {
130       let estatus: u8 = estatus.try_into()
131         .unwrap_or_else(|_| crashm(
132           "await child startup status: exit status out of range!"));
133       write_status(st_wfd, estatus);
134       libc::_exit(0);
135     }
136
137     WaitStatus::Signaled(_, signal, coredump) => {
138       crashv!("startup failed: died due to signal: ", signal.as_str(),
139               if coredump { " (core dumped)" } else { "" });
140     },
141
142     _ => {
143       crashv!("child startup exit status was strange!  0x",
144               c_itoa(wstatus, &mut default()))
145     }
146   }
147 }
148
149 impl Daemoniser {
150   /// Start daemonising - call before any threads created!
151   pub fn phase1() -> Self {
152     unsafe {
153       let null_fd = open(cstr!(b"/dev/null\0"), OFlag::O_RDWR, Mode::empty())
154         .context("open /dev/null");
155       mdup2(null_fd, 0, "null onto stdin");
156
157       let (st_rfd, st_wfd) = pipe().context("pipe");
158
159       match fork().context("fork (1)") {
160         ForkResult::Child => { }
161         ForkResult::Parent { child: _ } => {
162           close(st_wfd).context("close st_wfd pipe");
163           parent(st_rfd)
164         },
165       }
166
167       close(st_rfd).context("close st_rfd pipe");
168       setsid().context("setsid");
169       let intermediate_pid = Pid::this();
170
171       match fork().context("fork (2)") {
172         ForkResult::Child => { }
173         ForkResult::Parent { child } => {
174           intermediate(child, st_wfd)
175         },
176       }
177
178       Daemoniser {
179         drop_bomb: Some(()),
180         intermediate_pid,
181         null_fd,
182         st_wfd,
183       }
184     }
185   }
186
187   pub fn complete(mut self) {
188     unsafe {
189       mdup2(self.null_fd, 1, "null over stdin");
190
191       if Pid::parent() != self.intermediate_pid {
192         crashm(
193           "startup complete, but our parent is no longer the intermediate?");
194       }
195       kill(self.intermediate_pid, Some(Signal::SIGKILL))
196         .context("kill intermediate (after startup complete)");
197
198       write_status(self.st_wfd, 0);
199       mdup2(self.null_fd, 2, "null over stderrr");
200
201       self.drop_bomb.take();
202     }
203   }
204 }
205
206 impl Drop for Daemoniser {
207   fn drop(&mut self) {
208     if let Some(()) = self.drop_bomb.take() {
209       if panicking() {
210         // We will crash in due course, having printed some messages
211         // to stderr, presumably.
212         return
213       } else {
214         panic!("Daemonizer object dropped unexpectedly, startup failed");
215       }
216     }
217   }
218 }