use std::os::windows::io::AsRawHandle;
use std::os::windows::process::CommandExt;
use std::process::{Child, Command};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::Diagnostics::ToolHelp::{CreateToolhelp32Snapshot, TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next};
use windows::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JobObjectExtendedLimitInformation, SetInformationJobObject,
};
use windows::Win32::System::Threading::{OpenThread, ResumeThread, THREAD_SUSPEND_RESUME};
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
const CREATE_SUSPENDED: u32 = 0x0000_0004;
pub struct Job(HANDLE);
unsafe impl Send for Job {}
impl Drop for Job {
fn drop(&mut self) {
let _ = unsafe { CloseHandle(self.0) };
}
}
impl Job {
fn new() -> windows::core::Result<Job> {
unsafe {
let job = Job(CreateJobObjectW(None, windows::core::PCWSTR::null())?);
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
SetInformationJobObject(
job.0,
JobObjectExtendedLimitInformation,
&info as *const _ as *const _,
size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)?;
Ok(job)
}
}
}
pub fn spawn(cmd: &mut Command) -> std::io::Result<(Child, Option<Job>)> {
let Ok(job) = Job::new() else {
cmd.creation_flags(CREATE_NO_WINDOW);
return cmd.spawn().map(|c| (c, None));
};
cmd.creation_flags(CREATE_NO_WINDOW | CREATE_SUSPENDED);
let mut child = cmd.spawn()?;
let assigned = unsafe { AssignProcessToJobObject(job.0, HANDLE(child.as_raw_handle())) }.is_ok();
if !resume(child.id()) {
let _ = child.kill();
let _ = child.wait();
return Err(std::io::Error::other("could not resume the suspended ffmpeg"));
}
Ok((child, assigned.then_some(job)))
}
fn resume(pid: u32) -> bool {
unsafe {
let Ok(snap) = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) else { return false };
let mut entry = THREADENTRY32 { dwSize: size_of::<THREADENTRY32>() as u32, ..Default::default() };
let mut resumed = false;
let mut more = Thread32First(snap, &mut entry).is_ok();
while more {
if entry.th32OwnerProcessID == pid
&& let Ok(t) = OpenThread(THREAD_SUSPEND_RESUME, false, entry.th32ThreadID)
{
resumed |= ResumeThread(t) != u32::MAX;
let _ = CloseHandle(t);
}
more = Thread32Next(snap, &mut entry).is_ok();
}
let _ = CloseHandle(snap);
resumed
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, Instant};
fn running(pid: u32) -> bool {
let out = Command::new("tasklist").args(["/FI", &format!("PID eq {pid}"), "/NH"]).output().unwrap();
String::from_utf8_lossy(&out.stdout).contains(&pid.to_string())
}
#[test]
fn dropping_the_job_ends_grandchildren_too() {
let mut cmd = Command::new("cmd");
cmd.args(["/C", "ping -n 60 127.0.0.1 > NUL"]);
let (mut child, job) = spawn(&mut cmd).unwrap();
assert!(job.is_some(), "job created");
std::thread::sleep(Duration::from_millis(500));
let query = format!("(Get-CimInstance Win32_Process -Filter 'ParentProcessId={}').ProcessId", child.id());
let out = Command::new("powershell").args(["-NoProfile", "-Command", &query]).output();
let grandchild: Option<u32> =
out.ok().and_then(|o| String::from_utf8_lossy(&o.stdout).lines().find_map(|l| l.trim().parse().ok()));
let _ = child.kill();
drop(job);
let _ = child.wait();
let Some(pid) = grandchild else {
eprintln!("skipped: could not list the child process");
return;
};
let start = Instant::now();
while running(pid) {
assert!(start.elapsed() < Duration::from_secs(5), "grandchild {pid} survived the job");
std::thread::sleep(Duration::from_millis(100));
}
}
}