fork_rs/
lib.rs

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    // SAFETY: libc call
52    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    // SAFETY: libc call
63    if cvt::cvt_r(|| unsafe { libc::poll(&raw mut poll_fd, 1, timeout_ms.into()) })? == 0 {
64        // didn't fail within `timeout_ms`
65        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    // SAFETY: libc call
77    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    // SAFETY: libc call
112    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    // SAFETY: libc call
121    cvt::cvt_r(|| unsafe { libc::dup2(from, to) }).map(|_| ())
122}
123
124fn fork() -> Result<Fork, std::io::Error> {
125    // we're not capturing `EAGAIN` here, as the errors
126    // described there aren't resolvable by themselves
127    // SAFETY: libc call
128    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    // SAFETY: libc call
141    let sid = unsafe { libc::setsid() };
142
143    cvt::cvt(sid).map(|_| ())
144}
145
146/// Daemonizes the process
147///
148/// # Errors
149///
150/// * When the `fork` fails in the original process calling `daemonize()`
151pub 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    /// Daemonizes the process
179    ///
180    /// # Errors
181    ///
182    /// * When the `fork` fails in the original process calling `daemonize()`
183    pub fn daemonize(self) -> Result<Identity, std::io::Error> {
184        // fork() so the parent can exit, this returns control to the command line or shell invoking your program. This step is required so that the new process is guaranteed not to be a process group leader. The next step, setsid(), fails if you're a process group leader.
185        match fork() {
186            Ok(Fork::Child) => {
187                // we're the child
188            },
189            Ok(Fork::Parent { child: child_pid }) => {
190                // We're in the parent
191                return wait_for_success(child_pid).map(|()| Identity::Original);
192            },
193            Err(error) => {
194                // We're still in the parent
195                return Err(error);
196            },
197        }
198
199        // setsid() to become a process group and session group leader. Since a controlling terminal is associated with a session, and this new session has not yet acquired a controlling terminal our process now has no controlling terminal, which is a Good Thing for daemons.
200        if let Err(_err) = setsid() {
201            // TODO define exit code
202            process::exit(ExitCodes::ChildSetsidFailed.into());
203        }
204
205        // we're now the session group leader
206
207        // fork() again so the parent, (the session group leader), can exit. This means that we, as a non-session group leader, can never regain a controlling terminal.
208        match fork() {
209            Ok(Fork::Child) => {
210                // We're the grand-child, continue
211            },
212            Ok(Fork::Parent { child: child_pid }) => {
213                // let duration = Duration::from_secs(1);
214
215                // We're in the child (i.e. the session group leader)
216                match wait_for_failure(child_pid, self.timeout_ms.unwrap_or(1000)) {
217                    Ok(()) => {
218                        // stop child (us) gracefully
219                        process::exit(ExitCodes::Ok.into())
220                    },
221                    Err(_err) => {
222                        process::exit(ExitCodes::GrandchildFailedTooSoon.into());
223                    },
224                }
225            },
226            Err(_err) => {
227                // We're still in the child, but fork failed
228                process::exit(ExitCodes::ChildFailedToFork.into());
229            },
230        }
231
232        // we're now in the grand-child
233
234        // chdir("/") to ensure that our process doesn't keep any directory in use.
235        // Failure to do this could make it so that an administrator couldn't unmount a filesystem, because it was our current directory.
236        // [Equivalently, we could change to any directory containing files important to the daemon's operation.]
237        if let Err(_err) = set_current_dir("/") {
238            // Couldn't chdir to "/", which shouldn't fail
239            process::exit(ExitCodes::GrandchildChdirFailed.into());
240        }
241
242        // umask(0) so that we have complete control over the permissions of anything we write. We don't know what umask we may have inherited.
243        // [This step is optional]
244        // SAFETY: libc call
245        let _previous_mask = unsafe { libc::umask(0) };
246
247        // close() fds 0, 1, and 2. This releases the standard in, out, and error we inherited from our parent process.
248        // We have no way of knowing where these fds might have been redirected to.
249        // Note that many daemons use sysconf() to determine the limit _SC_OPEN_MAX. _SC_OPEN_MAX tells you the maximun open files/process.
250        // Then in a loop, the daemon can close all possible file descriptors. You have to decide if you need to do this or not.
251        // If you think that there might be file-descriptors open you should close them, since there's a limit on number of concurrent file descriptors.
252
253        // Establish new open descriptors for stdin, stdout and stderr. Even if you don't plan to use them, it is still a good idea to have them open.
254        // The precise handling of these is a matter of taste; if you have a logfile, for example, you might wish to open it as stdout or stderr, and open `/dev/null' as stdin; alternatively, you could open `/dev/console' as stderr and/or stdout, and `/dev/null' as stdin, or any other combination that makes sense for your particular daemon.
255
256        // we're doing both the closing and establishing new descriptors with a dup2 call instead of close and re-open (and hoping we get 0, 1 & 2)
257        let fd = match OpenOptions::new().read(true).write(true).open("/dev/null") {
258            Ok(file) => file.into_raw_fd(),
259            Err(_err) => {
260                // couldn't open /dev/null?
261                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            // fd is not one of the pre-defined ones, let's close it
271            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}