use std::io;
use std::os::windows::io::AsRawHandle;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt};
use windows_sys::Win32::Foundation::ERROR_OPERATION_ABORTED;
use windows_sys::Win32::System::IO::CancelIoEx;
pub(crate) async fn shutdown_output_reader<R>(mut reader: R, pending: bool) -> io::Result<()>
where
R: AsyncRead + Unpin + AsRawHandle,
{
if !pending {
drop(reader);
return Ok(());
}
let handle = reader.as_raw_handle() as usize;
let mut byte = [0];
let read = reader.read(&mut byte);
tokio::pin!(read);
let result = loop {
unsafe {
CancelIoEx(handle as _, std::ptr::null());
}
tokio::select! {
result = &mut read => break result,
_ = tokio::time::sleep(Duration::from_millis(1)) => {}
}
};
match result {
Ok(_) => Ok(()),
Err(error) if error.raw_os_error() == Some(ERROR_OPERATION_ABORTED as i32) => Ok(()),
Err(error) => Err(error),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{SpawnSpec, StreamMode};
struct ReleaseWorker(Option<std::sync::mpsc::Sender<()>>);
impl Drop for ReleaseWorker {
fn drop(&mut self) {
if let Some(release) = self.0.take() {
let _ = release.send(());
}
}
}
#[test]
fn shutdown_retries_when_pipe_read_is_queued_before_its_syscall() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.max_blocking_threads(1)
.build()
.expect("test runtime");
runtime.block_on(async {
let child = SpawnSpec::new(std::env::current_exe().expect("test executable"))
.arg("--exact")
.arg("output_shutdown_tests::silent_output_fixture")
.env("RUNNING_PROCESS_OUTPUT_SHUTDOWN_FIXTURE", "1")
.stdin(StreamMode::Null)
.stdout(StreamMode::Null)
.stderr(StreamMode::Piped)
.spawn()
.await
.expect("silent child");
let (mut lifecycle, _, _, _, stderr) = child.into_actor_parts();
let mut stderr = stderr.expect("silent stderr pipe");
let (entered_tx, entered_rx) = std::sync::mpsc::channel();
let (release_tx, release_rx) = std::sync::mpsc::channel();
let release = ReleaseWorker(Some(release_tx));
let blocker = tokio::task::spawn_blocking(move || {
let _ = entered_tx.send(());
let _ = release_rx.recv();
});
entered_rx
.recv_timeout(Duration::from_secs(2))
.expect("sole blocking worker occupied");
let mut byte = [0];
assert!(
tokio::time::timeout(Duration::from_millis(20), stderr.read_chunk(&mut byte))
.await
.is_err()
);
let result = {
let shutdown = stderr.shutdown();
tokio::pin!(shutdown);
use std::future::Future as _;
assert!(shutdown
.as_mut()
.poll(&mut std::task::Context::from_waker(std::task::Waker::noop()))
.is_pending());
drop(release);
tokio::time::timeout(Duration::from_secs(2), shutdown).await
};
lifecycle.start_kill().expect("kill fixture");
lifecycle.wait().await.expect("reap fixture");
blocker.await.expect("join occupied worker");
result
.expect("queued read cancellation must retry after worker starts")
.expect("read cleanup");
});
}
}