1use std::env::set_current_dir;
2use std::fs::OpenOptions;
3use std::os::fd::{IntoRawFd as _, RawFd};
4use std::process;
5
6#[derive(Clone, Copy)]
7#[repr(i32)]
8enum ExitCodes {
9 Ok = 0,
10 ChildFailedToFork,
11 ChildSetsidFailed,
12 GrandchildChdirFailed,
13 GrandchildOpenDevNullFailed,
14 GrandchildFailedTooSoon,
15}
16
17impl From<ExitCodes> for i32 {
18 fn from(val: ExitCodes) -> Self {
19 val as i32
20 }
21}
22
23impl TryFrom<i32> for ExitCodes {
24 type Error = &'static str;
25
26 fn try_from(value: i32) -> Result<Self, Self::Error> {
27 match value {
28 0 => Ok(ExitCodes::Ok),
29 1 => Ok(ExitCodes::ChildFailedToFork),
30 2 => Ok(ExitCodes::ChildSetsidFailed),
31 3 => Ok(ExitCodes::GrandchildChdirFailed),
32 4 => Ok(ExitCodes::GrandchildOpenDevNullFailed),
33 5 => Ok(ExitCodes::GrandchildFailedTooSoon),
34 _ => Err("Unknown exitcode"),
35 }
36 }
37}
38
39#[derive(Debug)]
40pub enum Identity {
41 Original,
42 Daemon,
43}
44
45enum Fork {
46 Parent { child: i32 },
47 Child,
48}
49
50fn wait_for_failure(pid: i32, timeout_ms: u16) -> Result<(), std::io::Error> {
51 let pid_fd: libc::c_int = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) }
53 .try_into()
54 .expect("File descriptors always fit in `c_int`");
55
56 let mut poll_fd = libc::pollfd {
57 fd: pid_fd,
58 events: libc::POLLIN,
59 revents: 0,
60 };
61
62 if cvt::cvt_r(|| unsafe { libc::poll(&raw mut poll_fd, 1, timeout_ms.into()) })? == 0 {
64 Ok(())
66 } else {
67 Err(std::io::Error::other(
68 "Grandchild died before `timeout_ms` expiration",
69 ))
70 }
71}
72
73fn wait_for_success(pid: i32) -> Result<(), std::io::Error> {
74 let mut status = 0;
75
76 let changed_pid = cvt::cvt_r(|| unsafe { libc::waitpid(pid, &raw mut status, 0) })?;
78
79 if pid != changed_pid {
80 return Err(std::io::Error::other(
81 "changed pid does not match awaited pit",
82 ));
83 }
84
85 let status = libc::WEXITSTATUS(status);
86
87 match ExitCodes::try_from(status) {
88 Ok(ExitCodes::Ok) => Ok(()),
89 Ok(ExitCodes::ChildFailedToFork) => {
90 Err(std::io::Error::other("Child did not launch correctly"))
91 },
92
93 Ok(ExitCodes::ChildSetsidFailed) => Err(std::io::Error::other("Child setsid failed")),
94 Ok(ExitCodes::GrandchildChdirFailed) => {
95 Err(std::io::Error::other("GrandChild chdir failed"))
96 },
97 Ok(ExitCodes::GrandchildOpenDevNullFailed) => {
98 Err(std::io::Error::other("GrandChild open /dev/null failed"))
99 },
100 Ok(ExitCodes::GrandchildFailedTooSoon) => {
101 Err(std::io::Error::other("GrandChild failed too soon"))
102 },
103 Err(err) => Err(std::io::Error::other(format!(
104 "Unspecified error code: {}",
105 err
106 ))),
107 }
108}
109
110fn close(fd: i32) -> Result<(), std::io::Error> {
111 let res = unsafe { libc::close(fd) };
113
114 let _: i32 = cvt::cvt(res)?;
115
116 Ok(())
117}
118
119fn dup2(from: RawFd, to: RawFd) -> Result<(), std::io::Error> {
120 cvt::cvt_r(|| unsafe { libc::dup2(from, to) }).map(|_| ())
122}
123
124fn fork() -> Result<Fork, std::io::Error> {
125 let pid = unsafe { libc::fork() };
129
130 let pid = cvt::cvt(pid)?;
131
132 if pid == 0 {
133 Ok(Fork::Child)
134 } else {
135 Ok(Fork::Parent { child: pid })
136 }
137}
138
139fn setsid() -> Result<(), std::io::Error> {
140 let sid = unsafe { libc::setsid() };
142
143 cvt::cvt(sid).map(|_| ())
144}
145
146pub fn daemonize() -> Result<Identity, std::io::Error> {
152 DaemonizeOptions::new().daemonize()
153}
154
155pub struct DaemonizeOptions {
156 timeout_ms: Option<u16>,
157}
158
159impl Default for DaemonizeOptions {
160 fn default() -> Self {
161 DaemonizeOptions::new()
162 }
163}
164
165impl DaemonizeOptions {
166 #[must_use]
167 pub fn new() -> Self {
168 Self { timeout_ms: None }
169 }
170
171 #[must_use]
172 pub fn set_timeout_ms(mut self, timeout_ms: u16) -> Self {
173 self.timeout_ms = Some(timeout_ms);
174
175 self
176 }
177
178 pub fn daemonize(self) -> Result<Identity, std::io::Error> {
184 match fork() {
186 Ok(Fork::Child) => {
187 },
189 Ok(Fork::Parent { child: child_pid }) => {
190 return wait_for_success(child_pid).map(|()| Identity::Original);
192 },
193 Err(error) => {
194 return Err(error);
196 },
197 }
198
199 if let Err(_err) = setsid() {
201 process::exit(ExitCodes::ChildSetsidFailed.into());
203 }
204
205 match fork() {
209 Ok(Fork::Child) => {
210 },
212 Ok(Fork::Parent { child: child_pid }) => {
213 match wait_for_failure(child_pid, self.timeout_ms.unwrap_or(1000)) {
217 Ok(()) => {
218 process::exit(ExitCodes::Ok.into())
220 },
221 Err(_err) => {
222 process::exit(ExitCodes::GrandchildFailedTooSoon.into());
223 },
224 }
225 },
226 Err(_err) => {
227 process::exit(ExitCodes::ChildFailedToFork.into());
229 },
230 }
231
232 if let Err(_err) = set_current_dir("/") {
238 process::exit(ExitCodes::GrandchildChdirFailed.into());
240 }
241
242 let _previous_mask = unsafe { libc::umask(0) };
246
247 let fd = match OpenOptions::new().read(true).write(true).open("/dev/null") {
258 Ok(file) => file.into_raw_fd(),
259 Err(_err) => {
260 process::exit(ExitCodes::GrandchildOpenDevNullFailed.into());
262 },
263 };
264
265 let _r = dup2(fd, libc::STDIN_FILENO);
266 let _r = dup2(fd, libc::STDOUT_FILENO);
267 let _r = dup2(fd, libc::STDERR_FILENO);
268
269 if fd > 2 {
270 let _r = close(fd);
272 }
273
274 Ok(Identity::Daemon)
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use std::thread;
281 use std::time::Duration;
282
283 use crate::{Identity, daemonize};
284
285 #[test]
286 fn child_1() {
287 let result = match daemonize() {
288 Ok(Identity::Original) => Ok(()),
289 Ok(Identity::Daemon) => {
290 thread::sleep(Duration::from_secs(2));
291 Ok(())
292 },
293 Err(err) => Err(err),
294 };
295
296 assert!(matches!(result, Ok(())));
297 }
298}