use std::sync::{Arc, Mutex};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::IO::CancelIoEx;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct RawHandle(isize);
impl RawHandle {
pub(crate) fn from_handle(handle: HANDLE) -> Self {
Self(handle.0 as isize)
}
pub(crate) fn as_handle(self) -> HANDLE {
HANDLE(self.0 as *mut core::ffi::c_void)
}
}
pub(crate) struct PipeHandle {
handle: RawHandle,
cancelled: Mutex<bool>,
}
impl PipeHandle {
pub(crate) fn new(handle: HANDLE) -> Arc<Self> {
Arc::new(Self {
handle: RawHandle::from_handle(handle),
cancelled: Mutex::new(false),
})
}
pub(crate) fn as_handle(&self) -> HANDLE {
self.handle.as_handle()
}
pub(crate) fn issue<T>(&self, start: impl FnOnce(HANDLE) -> T) -> Option<T> {
let cancelled = self
.cancelled
.lock()
.expect("the cancellation flag is only read and set, never held across a panic");
if *cancelled {
return None;
}
Some(start(self.as_handle()))
}
pub(crate) fn cancel(&self) {
let mut cancelled = self
.cancelled
.lock()
.expect("the cancellation flag is only read and set, never held across a panic");
*cancelled = true;
_ = unsafe { CancelIoEx(self.as_handle(), None) };
}
}
impl Drop for PipeHandle {
fn drop(&mut self) {
let handle = self.as_handle();
if handle.is_invalid() {
return;
}
_ = unsafe { CloseHandle(handle) };
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::sync::Barrier;
use std::thread;
use super::*;
#[test]
fn round_trips_a_handle_value() {
let handle = HANDLE(0x1234 as *mut core::ffi::c_void);
assert_eq!(RawHandle::from_handle(handle).as_handle(), handle);
}
fn detached_pipe() -> Arc<PipeHandle> {
PipeHandle::new(HANDLE::default())
}
#[test]
fn issues_an_operation_while_live() {
assert_eq!(detached_pipe().issue(|_handle| 7), Some(7));
}
#[test]
#[cfg_attr(miri, ignore)] fn refuses_an_operation_started_after_cancellation() {
let pipe = detached_pipe();
pipe.cancel();
assert_eq!(pipe.issue(|_handle| 7), None);
}
#[test]
#[cfg_attr(miri, ignore)] fn every_racing_operation_is_either_started_or_refused() {
const WORKERS: usize = 8;
const ATTEMPTS: usize = 200;
testing::with_watchdog(|| {
let pipe = detached_pipe();
let start = Arc::new(Barrier::new(WORKERS.saturating_add(1)));
let mut workers = Vec::with_capacity(WORKERS);
for _ in 0..WORKERS {
let pipe = Arc::clone(&pipe);
let start = Arc::clone(&start);
workers.push(thread::spawn(move || {
start.wait();
let mut refused_then_started = false;
let mut refused = false;
for _ in 0..ATTEMPTS {
match pipe.issue(|_handle| ()) {
Some(()) => refused_then_started |= refused,
None => refused = true,
}
}
refused_then_started
}));
}
start.wait();
pipe.cancel();
for worker in workers {
assert!(
!worker.join().unwrap(),
"an operation started after this thread had seen the handle cancelled"
);
}
assert_eq!(pipe.issue(|_handle| 7), None);
});
}
}