chj_unix_util/
forking_loop.rs

1use std::fmt::Display;
2
3use anyhow::{bail, Result};
4
5use crate::{
6    backoff::LoopWithBackoff,
7    unix::{easy_fork, waitpid_until_gone, Status},
8};
9
10/// Runs `job` repeatedly forever by forking off a child, running the
11/// `job` in the child once. In the parent, wait for the child to end,
12/// then treat both error returns and crashes / non-0 exits as errors.
13/// Restart but back off before retrying, using the given
14/// `LoopWithBackoff` config. Runs `until` after every run, and
15/// returns if it returns true.
16///
17/// Note: must be run while there are no running threads, panics
18/// otherwise!
19pub fn forking_loop<E: Display, F: FnOnce() -> Result<(), E>>(
20    config: LoopWithBackoff,
21    job: F,
22    until: impl Fn() -> bool,
23) where
24    anyhow::Error: From<E>,
25{
26    let mut perhaps_job = Some(job);
27    config.run(
28        || -> Result<()> {
29            if let Some(pid) = easy_fork()? {
30                // Parent process
31
32                // XXX todo: optionally set up a thread that kills the
33                // pid after a timeout.
34
35                match waitpid_until_gone(pid)? {
36                    Status::Normalexit(code) => {
37                        if code != 0 {
38                            bail!("child {pid} exited with exit code {code}");
39                        }
40                    }
41                    Status::Signalexit(signal) => {
42                        bail!("child {pid} terminated by signal {signal}");
43                    }
44                }
45            } else {
46                // Detach from the parent, type system wise at least,
47                // to allow for `job` to be FnOnce.
48                let mut perhaps_job = unsafe {
49                    // Safety: Uh, not very sure at all. It's as if
50                    // each child created the same state from scratch,
51                    // with everything attached. There is no safe
52                    // shared memory, so no such problems (there are
53                    // *some* safe(?) shared unix resources across
54                    // fork, though, like flock, XX todo.).
55                    // Check:
56                    // https://users.rust-lang.org/t/moving-borrowed-values-into-a-forked-child/28183
57                    ((&mut perhaps_job) as *const Option<F>).read()
58                };
59                let job = perhaps_job.take().expect("only once per child");
60
61                // Child process
62                match job() {
63                    Ok(()) => std::process::exit(0),
64                    Err(e) => {
65                        eprintln!("Error: {e:#}");
66                        std::process::exit(1);
67                    }
68                }
69            }
70            Ok(())
71        },
72        until,
73    )
74}