open_gpui_util/
process.rs1use 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()
77 .and_then(|job| {
78 job.assign_process(process.id())?;
79 Ok(job)
80 })
81 .map_err(|error| {
82 log::error!("failed to assign spawned process to a job object: {error:#}");
83 })
84 .ok();
85
86 Ok(Self { process, job })
87 }
88
89 pub async fn output(self) -> Result<std::process::Output> {
91 Ok(self.process.output().await?)
92 }
93
94 #[cfg(not(windows))]
95 pub fn kill(&mut self) -> Result<()> {
96 let pid = self.process.id();
97 unsafe {
98 libc::killpg(pid as i32, libc::SIGKILL);
99 }
100 Ok(())
101 }
102
103 #[cfg(windows)]
104 pub fn kill(&mut self) -> Result<()> {
105 if let Some(job) = &self.job {
106 job.terminate()
107 } else {
108 self.process.kill()?;
109 Ok(())
110 }
111 }
112}
113
114#[cfg(windows)]
115mod windows_job {
116 use crate::ResultExt as _;
117 use anyhow::{Context as _, Result};
118 use windows::Win32::{
119 Foundation::{CloseHandle, HANDLE},
120 System::{
121 JobObjects::{
122 AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
123 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
124 SetInformationJobObject, TerminateJobObject,
125 },
126 Threading::{OpenProcess, PROCESS_SET_QUOTA, PROCESS_TERMINATE},
127 },
128 };
129
130 pub(crate) struct JobObject(HANDLE);
131
132 unsafe impl Send for JobObject {}
133 unsafe impl Sync for JobObject {}
134
135 impl JobObject {
136 pub(crate) fn new() -> Result<Self> {
137 unsafe {
138 let job =
139 Self(CreateJobObjectW(None, None).context("failed to create job object")?);
140 let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
141 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
142 SetInformationJobObject(
143 job.0,
144 JobObjectExtendedLimitInformation,
145 &info as *const _ as *const _,
146 std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
147 )
148 .context("failed to set job object limits")?;
149 Ok(job)
150 }
151 }
152
153 pub(crate) fn assign_process(&self, pid: u32) -> Result<()> {
154 unsafe {
155 let process = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, false, pid)
156 .context("failed to open process")?;
157 let result = AssignProcessToJobObject(self.0, process)
158 .context("failed to assign process to job object");
159 CloseHandle(process).log_err();
160 result
161 }
162 }
163
164 pub(crate) fn terminate(&self) -> Result<()> {
165 unsafe { TerminateJobObject(self.0, 1).context("failed to terminate job object") }
166 }
167 }
168
169 impl Drop for JobObject {
170 fn drop(&mut self) {
171 unsafe {
172 CloseHandle(self.0).log_err();
173 }
174 }
175 }
176}
177
178#[cfg(all(test, windows))]
179mod windows_tests {
180 use super::*;
181 use std::time::{Duration, Instant};
182
183 fn spawn_process_tree(temp_dir: &std::path::Path) -> (Child, u32) {
184 let pid_file = temp_dir.join("grandchild_pid");
185 let mut command = std::process::Command::new("powershell.exe");
186 command.args(["-NoProfile", "-Command"]).arg(format!(
187 "$p = Start-Process -FilePath ping.exe -ArgumentList @('-n','60','127.0.0.1') -PassThru -WindowStyle Hidden; \
188 Set-Content -LiteralPath '{}' -Value $p.Id; \
189 Wait-Process -Id $p.Id",
190 pid_file.display()
191 ));
192 let child = Child::spawn(command, Stdio::null(), Stdio::null(), Stdio::null())
193 .expect("failed to spawn powershell");
194
195 let deadline = Instant::now() + Duration::from_secs(5);
196 let grandchild_pid = loop {
197 if let Ok(contents) = std::fs::read_to_string(&pid_file)
198 && let Ok(pid) = contents.trim().parse::<u32>()
199 {
200 break pid;
201 }
202 assert!(
203 Instant::now() < deadline,
204 "timed out waiting for grandchild pid file"
205 );
206 std::thread::sleep(Duration::from_millis(50));
207 };
208 assert!(
209 process_is_alive(grandchild_pid),
210 "grandchild should be alive after spawning"
211 );
212 (child, grandchild_pid)
213 }
214
215 fn process_is_alive(pid: u32) -> bool {
216 use windows::Win32::{
217 Foundation::{CloseHandle, STILL_ACTIVE},
218 System::Threading::{
219 GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
220 },
221 };
222
223 unsafe {
224 let Ok(handle) = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) else {
225 return false;
226 };
227 let mut exit_code = 0u32;
228 let alive = GetExitCodeProcess(handle, &mut exit_code).is_ok()
229 && exit_code == STILL_ACTIVE.0 as u32;
230 CloseHandle(handle).expect("failed to close process handle");
231 alive
232 }
233 }
234
235 fn assert_process_exits(pid: u32, message: &str) {
236 let deadline = Instant::now() + Duration::from_secs(2);
237 while process_is_alive(pid) {
238 assert!(Instant::now() < deadline, "{message} (pid {pid})");
239 std::thread::sleep(Duration::from_millis(100));
240 }
241 }
242
243 #[test]
244 fn test_kill_terminates_grandchildren() {
245 let temp_dir = tempfile::tempdir().unwrap();
246 let (mut child, grandchild_pid) = spawn_process_tree(temp_dir.path());
247
248 child.kill().expect("failed to kill child");
249
250 assert_process_exits(
251 grandchild_pid,
252 "grandchild should be terminated after killing the child",
253 );
254 }
255
256 #[test]
257 fn test_drop_terminates_grandchildren() {
258 let temp_dir = tempfile::tempdir().unwrap();
259 let (child, grandchild_pid) = spawn_process_tree(temp_dir.path());
260
261 drop(child);
262
263 assert_process_exits(
264 grandchild_pid,
265 "grandchild should be terminated after dropping the child",
266 );
267 }
268}