Files
codex/codex-rs/utils/pty/src/win/mod.rs
T
iceweasel-oai 5d5500650b Fix Windows PTY teardown by preserving ConPTY ownership (#20685)
## Why

On Windows, background terminals could stay visible after their shell
process had already exited. The elevated runner waits for the PTY output
reader to reach EOF before it sends the final exit message, but the
ConPTY helper was reducing ownership down to raw handles too early. That
left the pseudoconsole's borrowed pipe handles alive past teardown, so
EOF never propagated and the session stayed `running`.

## What changed

- change `utils/pty/src/win/conpty.rs` to hand off owned ConPTY
resources instead of leaking only raw handles
- make `windows-sandbox-rs/src/conpty/mod.rs` keep the pseudoconsole
owner and the backing pipe handles together until teardown
- update the elevated runner and the legacy unified-exec backend to keep
that `ConptyInstance` alive, take only the specific pipe handles they
need, and drop the owner at teardown instead of trying to close a
detached pseudoconsole handle later

## Testing

- desktop app in `Auto-review`: 11 x `cmd /c "ping -n 3 google.com"` all
exited cleanly and did not accumulate in the UI
- desktop app in `Auto-review`: 5 x `cmd /c "ping -n 30 google.com"`
appeared in the UI and drained back out on their own
2026-05-04 18:40:00 +00:00

176 lines
5.9 KiB
Rust

#![allow(clippy::unwrap_used)]
// This file is copied from https://github.com/wezterm/wezterm (MIT license).
// Copyright (c) 2018-Present Wez Furlong
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// Local modifications:
// - Fix Codex bug #13945 in the Windows PTY kill path. The vendored code treated
// `TerminateProcess`'s nonzero success return as failure and `0` as success,
// which inverts kill outcomes for both `WinChild::do_kill` and
// `WinChildKiller::kill`.
// - This bug still exists in the original WezTerm source as of 2026-03-08, so
// this is an intentional divergence from upstream.
use anyhow::Context as _;
use filedescriptor::OwnedHandle;
use portable_pty::Child;
use portable_pty::ChildKiller;
use portable_pty::ExitStatus;
use std::io::Error as IoError;
use std::io::Result as IoResult;
use std::os::windows::io::AsRawHandle;
use std::pin::Pin;
use std::sync::Mutex;
use std::task::Context;
use std::task::Poll;
use winapi::shared::minwindef::DWORD;
use winapi::um::minwinbase::STILL_ACTIVE;
use winapi::um::processthreadsapi::*;
use winapi::um::synchapi::WaitForSingleObject;
use winapi::um::winbase::INFINITE;
pub(crate) mod conpty;
mod procthreadattr;
mod psuedocon;
pub use conpty::ConPtySystem;
pub use psuedocon::PsuedoCon;
pub use psuedocon::conpty_supported;
#[derive(Debug)]
pub struct WinChild {
proc: Mutex<OwnedHandle>,
}
impl WinChild {
fn is_complete(&mut self) -> IoResult<Option<ExitStatus>> {
let mut status: DWORD = 0;
let proc = self.proc.lock().unwrap().try_clone().unwrap();
let res = unsafe { GetExitCodeProcess(proc.as_raw_handle() as _, &mut status) };
if res != 0 {
if status == STILL_ACTIVE {
Ok(None)
} else {
Ok(Some(ExitStatus::with_exit_code(status)))
}
} else {
Ok(None)
}
}
fn do_kill(&mut self) -> IoResult<()> {
let proc = self.proc.lock().unwrap().try_clone().unwrap();
let res = unsafe { TerminateProcess(proc.as_raw_handle() as _, 1) };
// Codex bug #13945: Win32 returns nonzero on success, so only `0` is an error.
if res == 0 {
Err(IoError::last_os_error())
} else {
Ok(())
}
}
}
impl ChildKiller for WinChild {
fn kill(&mut self) -> IoResult<()> {
self.do_kill().ok();
Ok(())
}
fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> {
let proc = self.proc.lock().unwrap().try_clone().unwrap();
Box::new(WinChildKiller { proc })
}
}
#[derive(Debug)]
pub struct WinChildKiller {
proc: OwnedHandle,
}
impl ChildKiller for WinChildKiller {
fn kill(&mut self) -> IoResult<()> {
let res = unsafe { TerminateProcess(self.proc.as_raw_handle() as _, 1) };
// Codex bug #13945: Win32 returns nonzero on success, so only `0` is an error.
if res == 0 {
Err(IoError::last_os_error())
} else {
Ok(())
}
}
fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync> {
let proc = self.proc.try_clone().unwrap();
Box::new(WinChildKiller { proc })
}
}
impl Child for WinChild {
fn try_wait(&mut self) -> IoResult<Option<ExitStatus>> {
self.is_complete()
}
fn wait(&mut self) -> IoResult<ExitStatus> {
if let Ok(Some(status)) = self.try_wait() {
return Ok(status);
}
let proc = self.proc.lock().unwrap().try_clone().unwrap();
unsafe {
WaitForSingleObject(proc.as_raw_handle() as _, INFINITE);
}
let mut status: DWORD = 0;
let res = unsafe { GetExitCodeProcess(proc.as_raw_handle() as _, &mut status) };
if res != 0 {
Ok(ExitStatus::with_exit_code(status))
} else {
Err(IoError::last_os_error())
}
}
fn process_id(&self) -> Option<u32> {
let res = unsafe { GetProcessId(self.proc.lock().unwrap().as_raw_handle() as _) };
if res == 0 { None } else { Some(res) }
}
fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle> {
let proc = self.proc.lock().unwrap();
Some(proc.as_raw_handle())
}
}
impl std::future::Future for WinChild {
type Output = anyhow::Result<ExitStatus>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<anyhow::Result<ExitStatus>> {
match self.is_complete() {
Ok(Some(status)) => Poll::Ready(Ok(status)),
Err(err) => Poll::Ready(Err(err).context("Failed to retrieve process exit status")),
Ok(None) => {
let proc = self.proc.lock().unwrap().try_clone()?;
let waker = cx.waker().clone();
std::thread::spawn(move || {
unsafe {
WaitForSingleObject(proc.as_raw_handle() as _, INFINITE);
}
waker.wake();
});
Poll::Pending
}
}
}
}