mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## 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
159 lines
5.7 KiB
Rust
159 lines
5.7 KiB
Rust
//! ConPTY helpers for spawning sandboxed processes with a PTY on Windows.
|
||
//!
|
||
//! This module encapsulates ConPTY creation and process spawn with the required
|
||
//! `PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE` plumbing. It is shared by both the legacy
|
||
//! restricted‑token path and the elevated runner path when unified_exec runs with
|
||
//! `tty=true`. The helpers are not tied to the IPC layer and can be reused by other
|
||
//! Windows sandbox flows that need a PTY.
|
||
|
||
use crate::desktop::LaunchDesktop;
|
||
use crate::proc_thread_attr::ProcThreadAttributeList;
|
||
use crate::winutil::format_last_error;
|
||
use crate::winutil::quote_windows_arg;
|
||
use crate::winutil::to_wide;
|
||
use anyhow::Result;
|
||
use codex_utils_pty::PsuedoCon;
|
||
use codex_utils_pty::RawConPty;
|
||
use std::collections::HashMap;
|
||
use std::ffi::c_void;
|
||
use std::os::windows::io::IntoRawHandle;
|
||
use std::path::Path;
|
||
use windows_sys::Win32::Foundation::CloseHandle;
|
||
use windows_sys::Win32::Foundation::GetLastError;
|
||
use windows_sys::Win32::Foundation::HANDLE;
|
||
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||
use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT;
|
||
use windows_sys::Win32::System::Threading::CreateProcessAsUserW;
|
||
use windows_sys::Win32::System::Threading::EXTENDED_STARTUPINFO_PRESENT;
|
||
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
||
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
|
||
use windows_sys::Win32::System::Threading::STARTUPINFOEXW;
|
||
|
||
use crate::process::make_env_block;
|
||
|
||
/// Owns a ConPTY handle and its backing pipe handles.
|
||
pub struct ConptyInstance {
|
||
pseudoconsole: Option<PsuedoCon>,
|
||
input_write: HANDLE,
|
||
output_read: HANDLE,
|
||
_desktop: Option<LaunchDesktop>,
|
||
}
|
||
|
||
impl Drop for ConptyInstance {
|
||
fn drop(&mut self) {
|
||
unsafe {
|
||
if self.input_write != 0 && self.input_write != INVALID_HANDLE_VALUE {
|
||
CloseHandle(self.input_write);
|
||
}
|
||
if self.output_read != 0 && self.output_read != INVALID_HANDLE_VALUE {
|
||
CloseHandle(self.output_read);
|
||
}
|
||
}
|
||
let _ = self.pseudoconsole.take();
|
||
}
|
||
}
|
||
|
||
impl ConptyInstance {
|
||
pub fn raw_handle(&self) -> Option<HANDLE> {
|
||
self.pseudoconsole
|
||
.as_ref()
|
||
.map(|pseudoconsole| pseudoconsole.raw_handle() as HANDLE)
|
||
}
|
||
|
||
pub fn take_input_write(&mut self) -> HANDLE {
|
||
std::mem::replace(&mut self.input_write, 0)
|
||
}
|
||
|
||
pub fn take_output_read(&mut self) -> HANDLE {
|
||
std::mem::replace(&mut self.output_read, 0)
|
||
}
|
||
}
|
||
|
||
/// Create a ConPTY with backing pipes.
|
||
///
|
||
/// This is public so callers that need lower-level PTY setup can build on the same
|
||
/// primitive, although the common entry point is `spawn_conpty_process_as_user`.
|
||
#[allow(dead_code)]
|
||
pub fn create_conpty(cols: i16, rows: i16) -> Result<ConptyInstance> {
|
||
let raw = RawConPty::new(cols, rows)?;
|
||
let (pseudoconsole, input_write, output_read) = raw.into_handles();
|
||
|
||
Ok(ConptyInstance {
|
||
pseudoconsole: Some(pseudoconsole),
|
||
input_write: input_write.into_raw_handle() as HANDLE,
|
||
output_read: output_read.into_raw_handle() as HANDLE,
|
||
_desktop: None,
|
||
})
|
||
}
|
||
|
||
/// Spawn a process under `h_token` with ConPTY attached.
|
||
///
|
||
/// This is the main shared ConPTY entry point and is used by both the legacy/direct path
|
||
/// and the elevated runner path whenever a PTY-backed sandboxed process is needed.
|
||
pub fn spawn_conpty_process_as_user(
|
||
h_token: HANDLE,
|
||
argv: &[String],
|
||
cwd: &Path,
|
||
env_map: &HashMap<String, String>,
|
||
use_private_desktop: bool,
|
||
logs_base_dir: Option<&Path>,
|
||
) -> Result<(PROCESS_INFORMATION, ConptyInstance)> {
|
||
let cmdline_str = argv
|
||
.iter()
|
||
.map(|arg| quote_windows_arg(arg))
|
||
.collect::<Vec<_>>()
|
||
.join(" ");
|
||
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
|
||
let env_block = make_env_block(env_map);
|
||
let mut si: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
|
||
si.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
|
||
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||
si.StartupInfo.hStdInput = INVALID_HANDLE_VALUE;
|
||
si.StartupInfo.hStdOutput = INVALID_HANDLE_VALUE;
|
||
si.StartupInfo.hStdError = INVALID_HANDLE_VALUE;
|
||
let desktop = LaunchDesktop::prepare(use_private_desktop, logs_base_dir)?;
|
||
si.StartupInfo.lpDesktop = desktop.startup_info_desktop();
|
||
|
||
let raw = RawConPty::new(/*cols*/ 80, /*rows*/ 24)?;
|
||
let (pseudoconsole, input_write, output_read) = raw.into_handles();
|
||
let hpc = pseudoconsole.raw_handle() as HANDLE;
|
||
let conpty = ConptyInstance {
|
||
pseudoconsole: Some(pseudoconsole),
|
||
input_write: input_write.into_raw_handle() as HANDLE,
|
||
output_read: output_read.into_raw_handle() as HANDLE,
|
||
_desktop: Some(desktop),
|
||
};
|
||
let mut attrs = ProcThreadAttributeList::new(/*attr_count*/ 1)?;
|
||
attrs.set_pseudoconsole(hpc)?;
|
||
si.lpAttributeList = attrs.as_mut_ptr();
|
||
|
||
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
|
||
let ok = unsafe {
|
||
CreateProcessAsUserW(
|
||
h_token,
|
||
std::ptr::null(),
|
||
cmdline.as_mut_ptr(),
|
||
std::ptr::null_mut(),
|
||
std::ptr::null_mut(),
|
||
0,
|
||
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT,
|
||
env_block.as_ptr() as *mut c_void,
|
||
to_wide(cwd).as_ptr(),
|
||
&si.StartupInfo,
|
||
&mut pi,
|
||
)
|
||
};
|
||
if ok == 0 {
|
||
let err = unsafe { GetLastError() } as i32;
|
||
return Err(anyhow::anyhow!(
|
||
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={}",
|
||
err,
|
||
format_last_error(err),
|
||
cwd.display(),
|
||
cmdline_str,
|
||
env_block.len()
|
||
));
|
||
}
|
||
Ok((pi, conpty))
|
||
}
|