1use 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 SIGTERM,
21 SIGKILL,
24}
25
26impl Default for ChildKillMode {
27 fn default() -> Self {
28 ChildKillMode::SIGKILL
29 }
30}
31
32impl KillChildOnDrop {
33 pub fn new(command: Command) -> Self {
35 KillChildOnDrop {
36 command,
37 kill_mode: ChildKillMode::default(),
38 child: None,
39 }
40 }
41
42 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 #[allow(unsafe_code)]
64 unsafe {
65 self.command
66 .pre_exec(|| {
67 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 Ok(pgroup) => nix::sys::signal::killpg(pgroup, Signal::SIGTERM)
101 .map_err(|e| io::Error::new(io::ErrorKind::Other, e)),
102 Err(nix::Error::Sys(nix::errno::Errno::ESRCH)) => Ok(()),
104 Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)),
105 }
106 }
107 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 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 }
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}