1use anyhow::{Context as _, Result};
2use std::process::Stdio;
3
4pub struct Child {
13 process: smol::process::Child,
14 #[cfg(windows)]
15 job: Option<windows_job::JobObject>,
16}
17
18impl std::ops::Deref for Child {
19 type Target = smol::process::Child;
20
21 fn deref(&self) -> &Self::Target {
22 &self.process
23 }
24}
25
26impl std::ops::DerefMut for Child {
27 fn deref_mut(&mut self) -> &mut Self::Target {
28 &mut self.process
29 }
30}
31
32impl Child {
33 #[cfg(not(windows))]
34 pub fn spawn(
35 mut command: std::process::Command,
36 stdin: Stdio,
37 stdout: Stdio,
38 stderr: Stdio,
39 ) -> Result<Self> {
40 crate::set_pre_exec_to_start_new_session(&mut command);
41 let mut command = smol::process::Command::from(command);
42 let process = command
43 .stdin(stdin)
44 .stdout(stdout)
45 .stderr(stderr)
46 .spawn()
47 .with_context(|| {
48 format!(
49 "failed to spawn command {}",
50 crate::redact::redact_command(&format!("{command:?}"))
51 )
52 })?;
53 Ok(Self { process })
54 }
55
56 #[cfg(windows)]
57 pub fn spawn(
58 command: std::process::Command,
59 stdin: Stdio,
60 stdout: Stdio,
61 stderr: Stdio,
62 ) -> Result<Self> {
63 let mut command = smol::process::Command::from(command);
64 let process = command
65 .stdin(stdin)
66 .stdout(stdout)
67 .stderr(stderr)
68 .spawn()
69 .with_context(|| {
70 format!(
71 "failed to spawn command {}",
72 crate::redact::redact_command(&format!("{command:?}"))
73 )
74 })?;
75
76 let job = windows_job::JobObject::new()
92 .and_then(|job| {
93 job.assign_process(process.id())?;
94 Ok(job)
95 })
96 .map_err(|error| {
97 log::error!("failed to assign spawned process to a job object: {error:#}");
98 })
99 .ok();
100
101 Ok(Self { process, job })
102 }
103
104 pub async fn output(self) -> Result<std::process::Output> {
107 Ok(self.process.output().await?)
112 }
113
114 #[cfg(not(windows))]
115 pub fn kill(&mut self) -> Result<()> {
116 let pid = self.process.id();
117 unsafe {
118 libc::killpg(pid as i32, libc::SIGKILL);
119 }
120 Ok(())
121 }
122
123 #[cfg(windows)]
124 pub fn kill(&mut self) -> Result<()> {
125 if let Some(job) = &self.job {
126 job.terminate()
127 } else {
128 self.process.kill()?;
129 Ok(())
130 }
131 }
132}
133
134#[cfg(windows)]
135mod windows_job {
136 use crate::ResultExt as _;
137 use anyhow::{Context as _, Result};
138 use windows::Win32::{
139 Foundation::{CloseHandle, HANDLE},
140 System::{
141 JobObjects::{
142 AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
143 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
144 SetInformationJobObject, TerminateJobObject,
145 },
146 Threading::{OpenProcess, PROCESS_SET_QUOTA, PROCESS_TERMINATE},
147 },
148 };
149
150 pub(crate) struct JobObject(HANDLE);
156
157 unsafe impl Send for JobObject {}
159 unsafe impl Sync for JobObject {}
160
161 impl JobObject {
162 pub(crate) fn new() -> Result<Self> {
163 unsafe {
164 let job =
165 Self(CreateJobObjectW(None, None).context("failed to create job object")?);
166 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
167 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
168 SetInformationJobObject(
169 job.0,
170 JobObjectExtendedLimitInformation,
171 &info as *const _ as *const _,
172 size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
173 )
174 .context("failed to set job object limits")?;
175 Ok(job)
176 }
177 }
178
179 pub(crate) fn assign_process(&self, pid: u32) -> Result<()> {
180 unsafe {
181 let process = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, false, pid)
182 .context("failed to open process")?;
183 let result = AssignProcessToJobObject(self.0, process)
184 .context("failed to assign process to job object");
185 CloseHandle(process).log_err();
186 result
187 }
188 }
189
190 pub(crate) fn terminate(&self) -> Result<()> {
191 unsafe { TerminateJobObject(self.0, 1).context("failed to terminate job object") }
192 }
193 }
194
195 impl Drop for JobObject {
196 fn drop(&mut self) {
197 unsafe {
198 CloseHandle(self.0).log_err();
199 }
200 }
201 }
202}
203
204#[cfg(all(test, windows))]
205mod windows_tests {
206 use super::*;
207 use std::time::{Duration, Instant};
208
209 fn spawn_process_tree(temp_dir: &std::path::Path) -> (Child, u32) {
212 let pid_file = temp_dir.join("grandchild_pid");
213 let mut command = std::process::Command::new("powershell.exe");
214 command.args(["-NoProfile", "-Command"]).arg(format!(
215 "$p = Start-Process -FilePath ping.exe -ArgumentList @('-n','60','127.0.0.1') -PassThru -WindowStyle Hidden; \
216 Set-Content -LiteralPath '{}' -Value $p.Id; \
217 Wait-Process -Id $p.Id",
218 pid_file.display()
219 ));
220 let child = Child::spawn(command, Stdio::null(), Stdio::null(), Stdio::null())
221 .expect("failed to spawn powershell");
222
223 let deadline = Instant::now() + Duration::from_secs(5);
224 let grandchild_pid = loop {
225 if let Ok(contents) = std::fs::read_to_string(&pid_file)
226 && let Ok(pid) = contents.trim().parse::<u32>()
227 {
228 break pid;
229 }
230 assert!(
231 Instant::now() < deadline,
232 "timed out waiting for grandchild pid file"
233 );
234 std::thread::sleep(Duration::from_millis(50));
235 };
236 assert!(
237 process_is_alive(grandchild_pid),
238 "grandchild should be alive after spawning"
239 );
240 (child, grandchild_pid)
241 }
242
243 fn process_is_alive(pid: u32) -> bool {
244 use windows::Win32::{
245 Foundation::{CloseHandle, STILL_ACTIVE},
246 System::Threading::{
247 GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
248 },
249 };
250
251 unsafe {
252 let Ok(handle) = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) else {
253 return false;
254 };
255 let mut exit_code = 0u32;
256 let alive = GetExitCodeProcess(handle, &mut exit_code).is_ok()
257 && exit_code == STILL_ACTIVE.0 as u32;
258 CloseHandle(handle).expect("failed to close process handle");
259 alive
260 }
261 }
262
263 fn assert_process_exits(pid: u32, message: &str) {
264 let deadline = Instant::now() + Duration::from_secs(2);
265 while process_is_alive(pid) {
266 assert!(Instant::now() < deadline, "{message} (pid {pid})");
267 std::thread::sleep(Duration::from_millis(100));
268 }
269 }
270
271 #[test]
272 fn test_kill_terminates_grandchildren() {
273 let temp_dir = tempfile::tempdir().unwrap();
274 let (mut child, grandchild_pid) = spawn_process_tree(temp_dir.path());
275
276 child.kill().expect("failed to kill child");
277
278 assert_process_exits(
279 grandchild_pid,
280 "grandchild should be terminated after killing the child",
281 );
282 }
283
284 #[test]
285 fn test_drop_terminates_grandchildren() {
286 let temp_dir = tempfile::tempdir().unwrap();
287 let (child, grandchild_pid) = spawn_process_tree(temp_dir.path());
288
289 drop(child);
290
291 assert_process_exits(
292 grandchild_pid,
293 "grandchild should be terminated after dropping the child",
294 );
295 }
296}