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