Skip to main content

browser_automation_cli/
win_job.rs

1//! Windows Job Object helpers for residual-zero Chrome reap (PRD 5W / GAP-009).
2//!
3//! On Windows, launched Chrome is assigned to a Job Object with
4//! `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` so FINALIZE/Drop kills the full tree.
5//! On non-Windows targets this module is a validated stub (always returns 0).
6
7#![allow(dead_code)]
8
9/// True when this build can create real Job Objects (Windows only).
10pub fn platform_supports_job_objects() -> bool {
11    cfg!(windows)
12}
13
14/// Human-readable capability line for doctor / diagnostics.
15pub fn capability_summary() -> &'static str {
16    if platform_supports_job_objects() {
17        "windows_job_object:available (KILL_ON_JOB_CLOSE)"
18    } else {
19        "windows_job_object:stub (non-windows host)"
20    }
21}
22
23/// Create a Job Object and assign `pid` to it. Returns handle as `usize` (0 on failure/non-Windows).
24#[cfg(windows)]
25pub fn create_and_assign(pid: u32) -> usize {
26    use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
27    use windows_sys::Win32::System::Threading::{
28        AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation, OpenProcess,
29        SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
30        JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, PROCESS_SET_QUOTA, PROCESS_TERMINATE,
31    };
32
33    if pid == 0 {
34        return 0;
35    }
36
37    unsafe {
38        let job: HANDLE = CreateJobObjectW(std::ptr::null(), std::ptr::null());
39        if job.is_null() {
40            return 0;
41        }
42        let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
43        info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
44        let ok = SetInformationJobObject(
45            job,
46            JobObjectExtendedLimitInformation,
47            &info as *const _ as *const _,
48            std::mem::size_of_val(&info) as u32,
49        );
50        if ok == 0 {
51            let _ = CloseHandle(job);
52            return 0;
53        }
54        let proc: HANDLE = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, 0, pid);
55        if proc.is_null() {
56            let _ = CloseHandle(job);
57            return 0;
58        }
59        let assigned = AssignProcessToJobObject(job, proc);
60        let _ = CloseHandle(proc);
61        if assigned == 0 {
62            let _ = CloseHandle(job);
63            return 0;
64        }
65        job as usize
66    }
67}
68
69/// Terminate all processes in the job.
70#[cfg(windows)]
71pub fn terminate_job(handle: usize) {
72    use windows_sys::Win32::System::Threading::TerminateJobObject;
73    if handle == 0 {
74        return;
75    }
76    unsafe {
77        let _ = TerminateJobObject(handle as _, 1);
78    }
79}
80
81/// Close job handle.
82#[cfg(windows)]
83pub fn close_job(handle: usize) {
84    use windows_sys::Win32::Foundation::CloseHandle;
85    if handle == 0 {
86        return;
87    }
88    unsafe {
89        let _ = CloseHandle(handle as _);
90    }
91}
92
93/// Terminate a single process by pid (fallback when no job).
94#[cfg(windows)]
95pub fn terminate_pid(pid: u32) {
96    use windows_sys::Win32::Foundation::CloseHandle;
97    use windows_sys::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
98    if pid == 0 {
99        return;
100    }
101    unsafe {
102        let proc = OpenProcess(PROCESS_TERMINATE, 0, pid);
103        if !proc.is_null() {
104            let _ = TerminateProcess(proc, 1);
105            let _ = CloseHandle(proc);
106        }
107    }
108}
109
110/// Self-test: on Windows, create a job for the current process and tear it down
111/// without terminating the process (do not set kill-on-close for self-test path).
112///
113/// Returns `Ok(handle)` if assignment succeeded; caller must `close_job` only
114/// (must NOT `terminate_job` on the current process).
115#[cfg(windows)]
116pub fn validate_assign_current_process() -> Result<usize, String> {
117    use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
118    use windows_sys::Win32::System::Threading::{
119        AssignProcessToJobObject, CreateJobObjectW, GetCurrentProcess, GetCurrentProcessId,
120        OpenProcess, PROCESS_SET_QUOTA, PROCESS_TERMINATE,
121    };
122
123    let pid = unsafe { GetCurrentProcessId() };
124    unsafe {
125        let job: HANDLE = CreateJobObjectW(std::ptr::null(), std::ptr::null());
126        if job.is_null() {
127            return Err("CreateJobObjectW failed".into());
128        }
129        // Assign current process WITHOUT kill-on-close so the test process survives.
130        let proc: HANDLE = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, 0, pid);
131        if proc.is_null() {
132            // Fall back to current process pseudo-handle path.
133            let cur = GetCurrentProcess();
134            let assigned = AssignProcessToJobObject(job, cur);
135            if assigned == 0 {
136                let _ = CloseHandle(job);
137                return Err("AssignProcessToJobObject(current) failed".into());
138            }
139            return Ok(job as usize);
140        }
141        let assigned = AssignProcessToJobObject(job, proc);
142        let _ = CloseHandle(proc);
143        if assigned == 0 {
144            let _ = CloseHandle(job);
145            return Err("AssignProcessToJobObject failed".into());
146        }
147        Ok(job as usize)
148    }
149}
150
151#[cfg(not(windows))]
152/// Stub: Job Object only exists on Windows.
153pub fn create_and_assign(_pid: u32) -> usize {
154    0
155}
156
157#[cfg(not(windows))]
158/// Stub.
159pub fn terminate_job(_handle: usize) {}
160
161#[cfg(not(windows))]
162/// Stub.
163pub fn close_job(_handle: usize) {}
164
165#[cfg(not(windows))]
166/// Stub.
167pub fn terminate_pid(_pid: u32) {}
168
169#[cfg(not(windows))]
170/// Stub validation: reports that job objects are not available on this OS.
171pub fn validate_assign_current_process() -> Result<usize, String> {
172    Err("job objects unavailable on non-windows".into())
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn capability_summary_is_non_empty() {
181        assert!(!capability_summary().is_empty());
182    }
183
184    #[test]
185    fn stub_or_create_does_not_panic() {
186        let h = create_and_assign(0);
187        assert_eq!(h, 0, "pid 0 must never produce a job handle");
188        terminate_job(0);
189        close_job(0);
190        terminate_pid(0);
191    }
192
193    #[test]
194    fn platform_flag_matches_cfg() {
195        assert_eq!(platform_supports_job_objects(), cfg!(windows));
196    }
197
198    #[test]
199    fn validate_current_process_behavior() {
200        match validate_assign_current_process() {
201            Ok(h) => {
202                // On Linux this branch is unreachable; on Windows we must close only.
203                assert_ne!(h, 0);
204                close_job(h);
205            }
206            Err(e) => {
207                #[cfg(windows)]
208                assert!(!e.is_empty());
209                #[cfg(not(windows))]
210                assert!(e.contains("non-windows"));
211            }
212        }
213    }
214}