Skip to main content

open_gpui_util/
process.rs

1use anyhow::{Context as _, Result};
2use std::process::Stdio;
3
4/// A wrapper around `smol::process::Child` that ensures all subprocesses
5/// are killed when the process is terminated: on Unix by using process
6/// groups, and on Windows by using job objects.
7///
8/// On Windows, dropping this struct closes the job object handle, which
9/// terminates all processes in the job. This also applies when the Open GPUI
10/// process exits for any reason, since the OS closes its handles, so spawned
11/// process trees can never outlive their owner.
12pub 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    /// Consumes the child, drains stdout/stderr, and waits for exit.
90    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}