fix: preserve windows pty shutdown during handoff

refs #3651
This commit is contained in:
Jonathan Liebig
2026-09-15 22:08:50 +02:00
committed by GitHub
parent bdd9b408f8
commit c83fd476f8
2 changed files with 70 additions and 25 deletions
+68 -21
View File
@@ -7,7 +7,7 @@ pub(crate) use unix::*;
#[cfg(windows)]
mod windows {
use std::io::{Read, Write};
use std::os::windows::io::{AsRawHandle, BorrowedHandle, OwnedHandle};
use std::os::windows::io::{AsHandle, OwnedHandle};
use std::sync::{
atomic::{AtomicBool, Ordering},
mpsc as std_mpsc, Arc, Condvar, Mutex,
@@ -116,12 +116,18 @@ mod windows {
}
}
fn resume(&self, state: ActorState) {
*self
fn resume(&self, next: ActorState) -> std::io::Result<()> {
let mut state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = state;
.unwrap_or_else(|poisoned| poisoned.into_inner());
if matches!(*state, ActorState::Released | ActorState::Shutdown) {
return Err(pty_actor_closed());
}
*state = next;
drop(state);
self.resumed.notify_all();
Ok(())
}
}
@@ -437,8 +443,7 @@ mod windows {
&self,
target: &std::process::Child,
) -> std::io::Result<crate::pty::backend::WindowsPtyHandoff> {
let target_process = unsafe { BorrowedHandle::borrow_raw(target.as_raw_handle()) }
.try_clone_to_owned()?;
let target_process = target.as_handle().try_clone_to_owned()?;
let (reply, completion) = std_mpsc::channel();
self.control_tx
.send(PtyIoControlCommand::DuplicateForHandoff {
@@ -631,18 +636,14 @@ mod windows {
&reader_cancel,
&write_tx,
deadline,
);
)
.and_then(|()| state.resume(ActorState::Quiesced));
if result.is_ok() {
handoff_active = Some(active);
*state
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) =
ActorState::Quiesced;
} else {
active.store(false, Ordering::Release);
reader_pause.clear();
state.resume(ActorState::Running);
let _ = state.resume(ActorState::Running);
}
let _ = reply.send(result);
false
@@ -688,8 +689,7 @@ mod windows {
active.store(false, Ordering::Release);
}
reader_pause.clear();
state.resume(ActorState::Running);
Ok(())
state.resume(ActorState::Running)
};
let _ = reply.send(result);
false
@@ -704,8 +704,7 @@ mod windows {
active.store(false, Ordering::Release);
}
reader_pause.clear();
state.resume(ActorState::Running);
Ok(())
state.resume(ActorState::Running)
} else {
Err(std::io::Error::other(
"PTY actor must be quiesced before activation",
@@ -724,10 +723,11 @@ mod windows {
active.store(false, Ordering::Release);
}
reader_pause.clear();
state.resume(ActorState::Released);
write_tx
.send(PtyIoWriteCommand::Release)
.map_err(|_| pty_actor_closed())
state.resume(ActorState::Released).and_then(|()| {
write_tx
.send(PtyIoWriteCommand::Release)
.map_err(|_| pty_actor_closed())
})
} else {
Err(std::io::Error::other(
"PTY actor must be quiesced before release",
@@ -1567,6 +1567,53 @@ mod windows {
drop(harness.output);
}
#[test]
fn shutdown_stays_terminal_when_inflight_handoff_completes() {
for pause_succeeds in [true, false] {
let harness = actor_harness(
false,
true,
Box::new(|_| PtyReadResult {
terminal_responses: Vec::new(),
}),
None,
);
*harness.handle.state.state.lock().unwrap() = ActorState::Quiescing;
let (input_ack, input_paused) = std_mpsc::channel();
let (reader_ack, reader_paused) = std_mpsc::channel();
let (reply, completion) = std_mpsc::channel();
harness
.handle
.control_tx
.send(PtyIoControlCommand::BeginHandoff {
active: Arc::new(AtomicBool::new(true)),
input_paused,
reader_paused,
deadline: Instant::now() + Duration::from_secs(1),
reply,
})
.unwrap();
// Hold completion until shutdown has established the terminal state.
harness.handle.shutdown();
if pause_succeeds {
input_ack.send(()).unwrap();
reader_ack.send(()).unwrap();
}
drop(input_ack);
drop(reader_ack);
let result = completion.recv_timeout(Duration::from_secs(2)).unwrap();
let state = *harness.handle.state.state.lock().unwrap();
drop(harness.handle);
drop(harness.output);
assert_eq!(
state,
ActorState::Shutdown,
"pause_succeeds={pause_succeeds}"
);
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::BrokenPipe);
}
}
#[test]
fn handoff_timeout_does_not_satisfy_the_next_pause_with_a_stale_ack() {
let harness = actor_harness(
+2 -4
View File
@@ -202,7 +202,7 @@ mod tests {
use super::*;
use std::io::{BufRead, BufReader, Read, Write};
use std::os::windows::{
io::{AsRawHandle, BorrowedHandle, FromRawHandle, OwnedHandle},
io::{AsHandle, FromRawHandle, OwnedHandle},
process::CommandExt,
};
use std::process::{ChildStdin, ChildStdout, Command, Stdio};
@@ -244,9 +244,7 @@ mod tests {
.stderr(Stdio::inherit())
.creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW)
.spawn()?;
let target_process =
unsafe { BorrowedHandle::borrow_raw(AsRawHandle::as_raw_handle(&child)) }
.try_clone_to_owned()?;
let target_process = child.as_handle().try_clone_to_owned()?;
let handles = duplicate_windows_handoff(master, handoff_child, target_process)?
.into_raw_handles();
let mut input = child