xand_utils/
procutils.rs

1//! Unix specific code for process cleanup
2
3use nix::{sys::signal::Signal, unistd::Pid};
4use std::{
5    io,
6    os::unix::process::CommandExt,
7    process::{Child, Command, ExitStatus},
8};
9
10pub struct KillChildOnDrop {
11    pub command: Command,
12    kill_mode: ChildKillMode,
13    child: Option<Child>,
14}
15
16pub enum ChildKillMode {
17    /// Sends the SIGTERM signal, and then waits for the process to terminate.
18    /// Note that this may block indefinitely if the process does not respect
19    /// SIGTERM.
20    SIGTERM,
21    /// Sends the SIGKILL signal, which forcefully kills the process. This is
22    /// the default mode.
23    SIGKILL,
24}
25
26impl Default for ChildKillMode {
27    fn default() -> Self {
28        ChildKillMode::SIGKILL
29    }
30}
31
32impl KillChildOnDrop {
33    /// Creates a new KillChildOnDrop with the default ChildKillMode: ChildKillMode::SIGKILL.
34    pub fn new(command: Command) -> Self {
35        KillChildOnDrop {
36            command,
37            kill_mode: ChildKillMode::default(),
38            child: None,
39        }
40    }
41
42    /// Creates a new KillChildOnDrop with the specified ChildKillMode.
43    ///
44    /// After sending the appropriate termination signal, the process will wait
45    /// for termination. See the documentation for the relevant ChildKillMode
46    /// for details.
47    pub fn with_kill_mode(command: Command, kill_mode: ChildKillMode) -> Self {
48        KillChildOnDrop {
49            command,
50            kill_mode,
51            child: None,
52        }
53    }
54
55    pub fn spawn(&mut self) -> io::Result<&mut Self> {
56        self.child = Some(
57            // Spoooooky -- pre_exec() is unsafe. Yarn in particular has forced our hand here by
58            // being absolutely crap at forwarding signals to children, but it is useful for
59            // any processes that might "go rogue"
60            //
61            // Rationale: `pre_exec` is `unsafe`. The closure passed to `pre_exec` only calls
62            // `setsid`, which abides by the safety requirements of `pre_exec`.
63            #[allow(unsafe_code)]
64            unsafe {
65                self.command
66                    .pre_exec(|| {
67                        // Force the processes to run in a new process group so they can be murdered
68                        nix::unistd::setsid().unwrap();
69                        Ok(())
70                    })
71                    .spawn()?
72            },
73        );
74        Ok(self)
75    }
76
77    pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
78        match self.child.as_mut() {
79            Some(ch) => ch.try_wait(),
80            None => Err(io::Error::new(
81                io::ErrorKind::NotFound,
82                "child not yet initialized",
83            )),
84        }
85    }
86
87    pub fn kill(&mut self) -> io::Result<()> {
88        match self.child.as_mut() {
89            Some(c) => c.kill(),
90            None => Ok(()),
91        }
92    }
93
94    pub fn request_terminate(&mut self) -> io::Result<()> {
95        match &mut self.child {
96            Some(child) => {
97                let pid = Pid::from_raw(child.id() as i32);
98                match nix::unistd::getpgid(Some(pid)) {
99                    // politely terminate the process group
100                    Ok(pgroup) => nix::sys::signal::killpg(pgroup, Signal::SIGTERM)
101                        .map_err(|e| io::Error::new(io::ErrorKind::Other, e)),
102                    // no such process: nothing to do
103                    Err(nix::Error::Sys(nix::errno::Errno::ESRCH)) => Ok(()),
104                    Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)),
105                }
106            }
107            // child not yet initialized: nothing to do
108            None => Ok(()),
109        }
110    }
111}
112
113impl Drop for KillChildOnDrop {
114    fn drop(&mut self) {
115        self.request_terminate()
116            .expect("process group termination failed");
117
118        if let Some(ref mut child) = &mut self.child {
119            if let ChildKillMode::SIGKILL = self.kill_mode {
120                let _ = child.kill();
121            }
122
123            // If wait isn't called we can leave zombie processes behind
124            let _ = child.wait();
125        }
126    }
127}
128
129#[cfg(test)]
130mod test {
131    use super::KillChildOnDrop;
132
133    use std::{
134        process::{Command, ExitStatus},
135        thread,
136        time::{Duration, Instant},
137    };
138
139    #[test]
140    fn terminate_long_command_before_finished() {
141        let cmd = {
142            let mut cmd = Command::new("sleep");
143            cmd.arg("1h");
144            cmd
145        };
146        let mut dropme = KillChildOnDrop::new(cmd);
147        let process = dropme.spawn().expect("unable to spawn process");
148
149        thread::sleep(Duration::new(1, 0));
150        match process.try_wait() {
151            Ok(Some(_)) => panic!("process unexpectedly terminated early"),
152            Ok(None) => {
153                // process is still running as expected
154            }
155            Err(e) => panic!("Error: {:?}", e),
156        }
157
158        process
159            .request_terminate()
160            .expect("process group termination failed");
161
162        thread::sleep(Duration::new(1, 0));
163        match process.try_wait() {
164            Ok(Some(s)) => assert!(!s.success()),
165            Ok(None) => panic!("process unexpectedly still running after SIGTERM"),
166            Err(e) => panic!("Error: {:?}", e),
167        }
168    }
169
170    #[test]
171    fn capture_successful_exit_status_from_quick_command() {
172        let cmd = Command::new("true");
173        let status = run_command(cmd, default_timeout());
174        assert!(status.success());
175    }
176
177    #[test]
178    fn capture_unsuccessful_exit_status_from_quick_command() {
179        let cmd = Command::new("false");
180        let status = run_command(cmd, default_timeout());
181        assert!(!status.success());
182    }
183
184    fn default_timeout() -> Duration {
185        Duration::new(1, 0)
186    }
187
188    fn run_command(cmd: Command, timeout: Duration) -> ExitStatus {
189        let start_time = Instant::now();
190
191        let mut dropme = KillChildOnDrop::new(cmd);
192        let process = dropme.spawn().expect("unable to spawn process");
193
194        loop {
195            match process.try_wait() {
196                Ok(None) => {
197                    if Instant::now().duration_since(start_time) > timeout {
198                        panic!("process timed out");
199                    } else {
200                        thread::sleep(Duration::new(1, 0));
201                    }
202                }
203                Ok(Some(s)) => return s,
204                Err(e) => panic!("unexpected error while waiting on process: {}", e),
205            }
206        }
207    }
208}