#![cfg(windows)]
use std::os::windows::io::AsRawHandle;
use std::process::{Command, Stdio};
use std::time::Duration;
use windows_threadpool_sys::io::{IoCompletion, ThreadpoolIo};
use windows_threadpool_sys::timer::{ThreadpoolPeriodicTimer, ThreadpoolTimer};
use windows_threadpool_sys::wait::{ThreadpoolWait, WaitableHandle};
use windows_threadpool_sys::work::ThreadpoolWork;
use windows_overlapped_io_sys::{Issued, Operation, Submitted, UnassociatedEndpoint};
use windows_sys::Win32::Foundation::ERROR_IO_PENDING;
use windows_sys::Win32::Storage::FileSystem::ReadFile;
use windows_sys::Win32::System::Threading::SetEvent;
const SCENARIO_VAR: &str = "WTPS_ABORT_SCENARIO";
const CHILD_TIMEOUT: Duration = Duration::from_secs(60);
const CHILD_LINGER: Duration = Duration::from_secs(5);
const SETUP_FAILURE_EXIT_CODE: i32 = 111;
fn assert_child_aborts(scenario: &str) {
let exe = std::env::current_exe().expect("locate the test binary");
let mut child = Command::new(exe)
.env(SCENARIO_VAR, scenario)
.env("RUST_TEST_THREADS", "1")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn the child");
let deadline = std::time::Instant::now() + CHILD_TIMEOUT;
let status = loop {
if let Some(status) = child.try_wait().expect("poll the child") {
break status;
}
if std::time::Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
panic!(
"the {scenario} child neither aborted nor exited within {CHILD_TIMEOUT:?}; \
the panic was probably contained, or the callback never ran"
);
}
std::thread::sleep(Duration::from_millis(20));
};
assert!(
!status.success(),
"the {scenario} child exited cleanly, so its callback panic was contained"
);
assert_ne!(
status.code(),
Some(SETUP_FAILURE_EXIT_CODE),
"the {scenario} child failed during setup, before its callback could ever \
run -- this proves nothing about whether a callback panic aborts"
);
}
fn child_work_panics() -> ! {
let work = ThreadpoolWork::new(|| panic!("work callback panics on purpose"), None)
.expect("create work");
work.submit();
work.wait();
std::process::exit(0);
}
fn child_timer_panics() -> ! {
let timer = ThreadpoolTimer::new(|_firing| panic!("timer callback panics on purpose"), None)
.expect("create timer");
timer.set_after(Duration::from_millis(1));
std::thread::sleep(CHILD_LINGER);
std::process::exit(0);
}
fn child_wait_panics() -> ! {
let handle = WaitableHandle::event(true, false).expect("create an event");
let wait = ThreadpoolWait::new(
handle,
|_activation| panic!("wait callback panics on purpose"),
None,
)
.expect("create wait");
wait.arm(None);
let ok = unsafe { SetEvent(wait.handle().as_raw_handle()) };
assert_ne!(ok, 0, "SetEvent failed");
std::thread::sleep(CHILD_LINGER);
std::process::exit(0);
}
fn child_io_panics() -> ! {
let path = std::env::temp_dir().join(format!(
"windows-threadpool-sys-abort-io-{}.tmp",
std::process::id()
));
std::fs::write(&path, b"overlapped").expect("write temp file");
let endpoint = UnassociatedEndpoint::open(&path, true, false, 0).expect("open endpoint");
let tp = ThreadpoolIo::new(
endpoint,
|_completion: &IoCompletion| panic!("io callback panics on purpose"),
None,
)
.expect("create TP_IO");
let mut buffer = [0_u8; 32];
let buf_ptr = buffer.as_mut_ptr();
let buf_len = buffer.len() as u32;
let mut operation = Operation::new(());
operation.set_offset(0);
let submitted = unsafe {
tp.submit(operation, |handle, overlapped| {
let ok = ReadFile(
handle.as_raw_handle(),
buf_ptr,
buf_len,
std::ptr::null_mut(),
overlapped,
);
if ok != 0 {
return Ok(Issued::Pending);
}
let error = std::io::Error::last_os_error();
if error.raw_os_error() == Some(ERROR_IO_PENDING as i32) {
return Ok(Issued::Pending);
}
Err(error)
})
};
assert!(matches!(submitted, Submitted::Pending(_)));
tp.run_down();
let _ = std::fs::remove_file(&path);
std::process::exit(0);
}
fn child_periodic_panics() -> ! {
let timer = ThreadpoolPeriodicTimer::new(
Duration::from_millis(1),
|_tick| panic!("periodic timer callback panics on purpose"),
None,
)
.expect("create periodic timer");
timer.start();
std::thread::sleep(CHILD_LINGER);
std::process::exit(0);
}
fn dispatch_if_child() {
let Ok(scenario) = std::env::var(SCENARIO_VAR) else {
return;
};
let caught = std::panic::catch_unwind(|| match scenario.as_str() {
"work" => child_work_panics(),
"timer" => child_timer_panics(),
"wait" => child_wait_panics(),
"io" => child_io_panics(),
"periodic" => child_periodic_panics(),
other => panic!("unknown child scenario {other}"),
});
if caught.is_err() {
std::process::exit(SETUP_FAILURE_EXIT_CODE);
}
}
#[test]
fn a_panicking_work_callback_aborts_the_process() {
dispatch_if_child();
assert_child_aborts("work");
}
#[test]
fn a_panicking_timer_callback_aborts_the_process() {
dispatch_if_child();
assert_child_aborts("timer");
}
#[test]
fn a_panicking_wait_callback_aborts_the_process() {
dispatch_if_child();
assert_child_aborts("wait");
}
#[test]
fn a_panicking_io_callback_aborts_the_process() {
dispatch_if_child();
assert_child_aborts("io");
}
#[test]
fn a_panicking_periodic_timer_callback_aborts_the_process() {
dispatch_if_child();
assert_child_aborts("periodic");
}