use std::os::windows::io::AsRawHandle;
use std::process::Child;
use std::thread;
use std::time::{Duration, Instant};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JobObjectBasicAccountingInformation,
JobObjectExtendedLimitInformation, QueryInformationJobObject, SetInformationJobObject,
TerminateJobObject, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_ACTIVE_PROCESS,
JOB_OBJECT_LIMIT_JOB_MEMORY, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOB_OBJECT_LIMIT_PROCESS_MEMORY,
};
use windows::Win32::System::Threading::{
GetCurrentProcess, OpenProcess, PROCESS_SET_QUOTA, PROCESS_TERMINATE,
};
use crate::process_windows::os_error;
pub const DEFAULT_ACTIVE_PROCESS_LIMIT: u32 = 64;
pub struct WindowsJob {
handle: HANDLE,
job_memory_limit: Option<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExitReason {
Exited,
JobMemoryLimit,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct WindowsJobBuilder {
active_process_limit: Option<u32>,
job_memory_limit: Option<usize>,
process_memory_limit: Option<usize>,
}
impl WindowsJobBuilder {
pub fn active_process_limit(mut self, cap: u32) -> Self {
self.active_process_limit = Some(cap);
self
}
pub fn job_memory_limit(mut self, bytes: usize) -> Self {
self.job_memory_limit = Some(bytes);
self
}
pub fn process_memory_limit(mut self, bytes: usize) -> Self {
self.process_memory_limit = Some(bytes);
self
}
pub fn build(self) -> std::io::Result<WindowsJob> {
if self.active_process_limit == Some(0) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"active process cap must be at least 1",
));
}
if self.job_memory_limit == Some(0) || self.process_memory_limit == Some(0) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"memory limits must be at least 1 byte",
));
}
let handle = unsafe { CreateJobObjectW(None, None) }.map_err(os_error)?;
let job = WindowsJob {
handle,
job_memory_limit: self.job_memory_limit,
};
let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if let Some(cap) = self.active_process_limit {
limits.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_ACTIVE_PROCESS;
limits.BasicLimitInformation.ActiveProcessLimit = cap;
}
if let Some(bytes) = self.job_memory_limit {
limits.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_JOB_MEMORY;
limits.JobMemoryLimit = bytes;
}
if let Some(bytes) = self.process_memory_limit {
limits.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_PROCESS_MEMORY;
limits.ProcessMemoryLimit = bytes;
}
unsafe {
SetInformationJobObject(
job.handle,
JobObjectExtendedLimitInformation,
&limits as *const _ as *const std::ffi::c_void,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
}
.map_err(os_error)?;
Ok(job)
}
}
unsafe impl Send for WindowsJob {}
unsafe impl Sync for WindowsJob {}
impl WindowsJob {
pub fn new(active_process_limit: Option<u32>) -> std::io::Result<Self> {
WindowsJobBuilder {
active_process_limit,
..WindowsJobBuilder::default()
}
.build()
}
pub fn builder() -> WindowsJobBuilder {
WindowsJobBuilder::default()
}
pub fn contain_current_process() -> std::io::Result<()> {
let job = WindowsJob::new(None)?;
let process = unsafe { GetCurrentProcess() };
unsafe { AssignProcessToJobObject(job.handle, process) }.map_err(os_error)?;
std::mem::forget(job);
Ok(())
}
pub fn peak_job_memory(&self) -> std::io::Result<usize> {
let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
unsafe {
QueryInformationJobObject(
Some(self.handle),
JobObjectExtendedLimitInformation,
(&mut limits as *mut JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
None,
)
}
.map_err(os_error)?;
Ok(limits.PeakJobMemoryUsed)
}
pub fn exit_reason(&self) -> std::io::Result<ExitReason> {
match self.job_memory_limit {
Some(limit) if self.peak_job_memory()? >= limit => Ok(ExitReason::JobMemoryLimit),
_ => Ok(ExitReason::Exited),
}
}
pub fn assign_child(&self, child: &Child) -> std::io::Result<()> {
let process = HANDLE(child.as_raw_handle());
unsafe { AssignProcessToJobObject(self.handle, process) }.map_err(os_error)
}
pub fn assign_process_id(&self, pid: u32) -> std::io::Result<()> {
let process = unsafe { OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, false, pid) }
.map_err(os_error)?;
let assigned = unsafe { AssignProcessToJobObject(self.handle, process) };
unsafe {
let _ = CloseHandle(process);
}
assigned.map_err(os_error)
}
pub fn terminate(&self) -> std::io::Result<()> {
unsafe { TerminateJobObject(self.handle, 1) }.map_err(os_error)
}
pub fn active_process_count(&self) -> std::io::Result<u32> {
let mut accounting = JOBOBJECT_BASIC_ACCOUNTING_INFORMATION::default();
unsafe {
QueryInformationJobObject(
Some(self.handle),
JobObjectBasicAccountingInformation,
(&mut accounting as *mut JOBOBJECT_BASIC_ACCOUNTING_INFORMATION).cast(),
std::mem::size_of::<JOBOBJECT_BASIC_ACCOUNTING_INFORMATION>() as u32,
None,
)
}
.map_err(os_error)?;
Ok(accounting.ActiveProcesses)
}
pub fn is_empty(&self) -> std::io::Result<bool> {
Ok(self.active_process_count()? == 0)
}
pub fn wait_empty(&self, timeout: Duration) -> std::io::Result<bool> {
const POLL_INTERVAL: Duration = Duration::from_millis(10);
let started = Instant::now();
loop {
if self.is_empty()? {
return Ok(true);
}
let remaining = timeout.saturating_sub(started.elapsed());
if remaining.is_zero() {
return Ok(false);
}
thread::sleep(POLL_INTERVAL.min(remaining));
}
}
}
impl Drop for WindowsJob {
fn drop(&mut self) {
unsafe {
let _ = CloseHandle(self.handle);
}
}
}