mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add Windows sandbox unified exec runtime support (#15578)
## Summary This is the runtime/foundation half of the Windows sandbox unified-exec work. - add Windows sandbox `unified_exec` session support in `windows-sandbox-rs` for both: - the legacy restricted-token backend - the elevated runner backend - extend the PTY/process runtime so driver-backed sessions can support: - stdin streaming - stdout/stderr separation - exit propagation - PTY resize hooks - add Windows sandbox runtime coverage in `codex-windows-sandbox` / `codex-utils-pty` This PR does **not** enable Windows sandbox `UnifiedExec` for product callers yet because hooking this up to app-server comes in the next PR. Windows sandbox advertising is intentionally kept aligned with `main`, so sandboxed Windows callers still fall back to `ShellCommand`. This PR isolates the runtime/session layer so it can be reviewed independently from product-surface enablement. --------- Co-authored-by: jif-oai <jif@openai.com> Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
co-authored by
jif-oai
Codex
parent
38ba876ea9
commit
8612714aa6
@@ -0,0 +1,118 @@
|
||||
use super::windows_common::finish_driver_spawn;
|
||||
use super::windows_common::make_runner_resizer;
|
||||
use super::windows_common::start_runner_pipe_writer;
|
||||
use super::windows_common::start_runner_stdin_writer;
|
||||
use super::windows_common::start_runner_stdout_reader;
|
||||
use crate::ipc_framed::EmptyPayload;
|
||||
use crate::ipc_framed::FramedMessage;
|
||||
use crate::ipc_framed::Message;
|
||||
use crate::ipc_framed::SpawnRequest;
|
||||
use crate::runner_client::spawn_runner_transport;
|
||||
use crate::spawn_prep::prepare_elevated_spawn_context;
|
||||
use anyhow::Result;
|
||||
use codex_utils_pty::ProcessDriver;
|
||||
use codex_utils_pty::SpawnedProcess;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn spawn_windows_sandbox_session_elevated(
|
||||
policy_json_or_preset: &str,
|
||||
sandbox_policy_cwd: &Path,
|
||||
codex_home: &Path,
|
||||
command: Vec<String>,
|
||||
cwd: &Path,
|
||||
mut env_map: HashMap<String, String>,
|
||||
timeout_ms: Option<u64>,
|
||||
tty: bool,
|
||||
stdin_open: bool,
|
||||
use_private_desktop: bool,
|
||||
) -> Result<SpawnedProcess> {
|
||||
let elevated = prepare_elevated_spawn_context(
|
||||
policy_json_or_preset,
|
||||
sandbox_policy_cwd,
|
||||
codex_home,
|
||||
cwd,
|
||||
&mut env_map,
|
||||
&command,
|
||||
)?;
|
||||
|
||||
let spawn_request = SpawnRequest {
|
||||
command: command.clone(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: env_map.clone(),
|
||||
policy_json_or_preset: policy_json_or_preset.to_string(),
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
|
||||
codex_home: elevated.common.sandbox_base.clone(),
|
||||
real_codex_home: codex_home.to_path_buf(),
|
||||
cap_sids: elevated.cap_sids.clone(),
|
||||
timeout_ms,
|
||||
tty,
|
||||
stdin_open,
|
||||
use_private_desktop,
|
||||
};
|
||||
let codex_home = codex_home.to_path_buf();
|
||||
let cwd = cwd.to_path_buf();
|
||||
let sandbox_creds = elevated.sandbox_creds.clone();
|
||||
let logs_base_dir = elevated.common.logs_base_dir.clone();
|
||||
let transport = tokio::task::spawn_blocking(move || -> Result<_> {
|
||||
let mut transport =
|
||||
spawn_runner_transport(&codex_home, &cwd, &sandbox_creds, logs_base_dir.as_deref())?;
|
||||
transport.send_spawn_request(spawn_request)?;
|
||||
transport.read_spawn_ready()?;
|
||||
Ok(transport)
|
||||
})
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("runner handshake task failed: {err}"))??;
|
||||
let (pipe_write, pipe_read) = transport.into_files();
|
||||
|
||||
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
|
||||
let (stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(256);
|
||||
let stderr_rx = if tty {
|
||||
None
|
||||
} else {
|
||||
Some(broadcast::channel::<Vec<u8>>(256))
|
||||
};
|
||||
let (exit_tx, exit_rx) = oneshot::channel::<i32>();
|
||||
|
||||
let outbound_tx = start_runner_pipe_writer(pipe_write);
|
||||
let writer_handle = start_runner_stdin_writer(writer_rx, outbound_tx.clone(), tty, stdin_open);
|
||||
let terminator = {
|
||||
let outbound_tx = outbound_tx.clone();
|
||||
Some(Box::new(move || {
|
||||
let _ = outbound_tx.send(FramedMessage {
|
||||
version: 1,
|
||||
message: Message::Terminate {
|
||||
payload: EmptyPayload::default(),
|
||||
},
|
||||
});
|
||||
}) as Box<dyn FnMut() + Send + Sync>)
|
||||
};
|
||||
|
||||
start_runner_stdout_reader(
|
||||
pipe_read,
|
||||
stdout_tx,
|
||||
stderr_rx.as_ref().map(|(tx, _rx)| tx.clone()),
|
||||
exit_tx,
|
||||
);
|
||||
|
||||
Ok(finish_driver_spawn(
|
||||
ProcessDriver {
|
||||
writer_tx,
|
||||
stdout_rx,
|
||||
stderr_rx: stderr_rx.map(|(_tx, rx)| rx),
|
||||
exit_rx,
|
||||
terminator,
|
||||
writer_handle: Some(writer_handle),
|
||||
resizer: if tty {
|
||||
Some(make_runner_resizer(outbound_tx))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
stdin_open,
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
use super::windows_common::finish_driver_spawn;
|
||||
use super::windows_common::normalize_windows_tty_input;
|
||||
use crate::acl::revoke_ace;
|
||||
use crate::conpty::spawn_conpty_process_as_user;
|
||||
use crate::desktop::LaunchDesktop;
|
||||
use crate::logging::log_failure;
|
||||
use crate::logging::log_success;
|
||||
use crate::process::StderrMode;
|
||||
use crate::process::StdinMode;
|
||||
use crate::process::read_handle_loop;
|
||||
use crate::process::spawn_process_with_pipes;
|
||||
use crate::spawn_prep::LocalSid;
|
||||
use crate::spawn_prep::allow_null_device_for_workspace_write;
|
||||
use crate::spawn_prep::apply_legacy_session_acl_rules;
|
||||
use crate::spawn_prep::prepare_legacy_session_security;
|
||||
use crate::spawn_prep::prepare_legacy_spawn_context;
|
||||
use anyhow::Result;
|
||||
use codex_utils_pty::ProcessDriver;
|
||||
use codex_utils_pty::SpawnedProcess;
|
||||
use codex_utils_pty::TerminalSize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
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::Storage::FileSystem::WriteFile;
|
||||
use windows_sys::Win32::System::Console::COORD;
|
||||
use windows_sys::Win32::System::Console::ClosePseudoConsole;
|
||||
use windows_sys::Win32::System::Console::ResizePseudoConsole;
|
||||
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
|
||||
use windows_sys::Win32::System::Threading::INFINITE;
|
||||
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
||||
use windows_sys::Win32::System::Threading::TerminateProcess;
|
||||
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
||||
|
||||
const WAIT_TIMEOUT: u32 = 0x0000_0102;
|
||||
|
||||
struct LegacyProcessHandles {
|
||||
process: PROCESS_INFORMATION,
|
||||
output_join: std::thread::JoinHandle<()>,
|
||||
writer_handle: tokio::task::JoinHandle<()>,
|
||||
hpc: Option<HANDLE>,
|
||||
token_handle: HANDLE,
|
||||
desktop: Option<LaunchDesktop>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn spawn_legacy_process(
|
||||
h_token: HANDLE,
|
||||
command: &[String],
|
||||
cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
use_private_desktop: bool,
|
||||
tty: bool,
|
||||
stdin_open: bool,
|
||||
stdout_tx: broadcast::Sender<Vec<u8>>,
|
||||
stderr_tx: Option<broadcast::Sender<Vec<u8>>>,
|
||||
writer_rx: mpsc::Receiver<Vec<u8>>,
|
||||
logs_base_dir: Option<&Path>,
|
||||
) -> Result<LegacyProcessHandles> {
|
||||
let (pi, output_join, writer_handle, hpc, desktop) = if tty {
|
||||
let (pi, conpty) = spawn_conpty_process_as_user(
|
||||
h_token,
|
||||
command,
|
||||
cwd,
|
||||
env_map,
|
||||
use_private_desktop,
|
||||
logs_base_dir,
|
||||
)?;
|
||||
let (hpc, input_write, output_read, desktop) = conpty.into_raw();
|
||||
let output_join = spawn_output_reader(output_read, stdout_tx);
|
||||
let writer_handle = spawn_input_writer(
|
||||
Some(input_write),
|
||||
writer_rx,
|
||||
/*normalize_newlines*/ true,
|
||||
);
|
||||
(pi, output_join, writer_handle, Some(hpc), desktop)
|
||||
} else {
|
||||
let pipe_handles = spawn_process_with_pipes(
|
||||
h_token,
|
||||
command,
|
||||
cwd,
|
||||
env_map,
|
||||
if stdin_open {
|
||||
StdinMode::Open
|
||||
} else {
|
||||
StdinMode::Closed
|
||||
},
|
||||
StderrMode::Separate,
|
||||
use_private_desktop,
|
||||
logs_base_dir,
|
||||
)?;
|
||||
let stdout_join = spawn_output_reader(pipe_handles.stdout_read, stdout_tx);
|
||||
let Some(stderr_read) = pipe_handles.stderr_read else {
|
||||
anyhow::bail!("separate stderr handle should be present");
|
||||
};
|
||||
let Some(stderr_tx) = stderr_tx else {
|
||||
anyhow::bail!("separate stderr channel should be present");
|
||||
};
|
||||
let stderr_join = spawn_output_reader(stderr_read, stderr_tx);
|
||||
let output_join = std::thread::spawn(move || {
|
||||
let _ = stdout_join.join();
|
||||
let _ = stderr_join.join();
|
||||
});
|
||||
let writer_handle = spawn_input_writer(
|
||||
pipe_handles.stdin_write,
|
||||
writer_rx,
|
||||
/*normalize_newlines*/ false,
|
||||
);
|
||||
(
|
||||
pipe_handles.process,
|
||||
output_join,
|
||||
writer_handle,
|
||||
None,
|
||||
Some(pipe_handles.desktop),
|
||||
)
|
||||
};
|
||||
Ok(LegacyProcessHandles {
|
||||
process: pi,
|
||||
output_join,
|
||||
writer_handle,
|
||||
hpc,
|
||||
token_handle: h_token,
|
||||
desktop,
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_output_reader(
|
||||
output_read: HANDLE,
|
||||
output_tx: broadcast::Sender<Vec<u8>>,
|
||||
) -> std::thread::JoinHandle<()> {
|
||||
read_handle_loop(output_read, move |chunk| {
|
||||
let _ = output_tx.send(chunk.to_vec());
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_input_writer(
|
||||
input_write: Option<HANDLE>,
|
||||
mut writer_rx: mpsc::Receiver<Vec<u8>>,
|
||||
normalize_newlines: bool,
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut previous_was_cr = false;
|
||||
while let Some(bytes) = writer_rx.blocking_recv() {
|
||||
let Some(handle) = input_write else {
|
||||
continue;
|
||||
};
|
||||
let bytes = if normalize_newlines {
|
||||
normalize_windows_tty_input(&bytes, &mut previous_was_cr)
|
||||
} else {
|
||||
bytes
|
||||
};
|
||||
if write_all_handle(handle, &bytes).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(handle) = input_write {
|
||||
unsafe {
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn write_all_handle(handle: HANDLE, mut bytes: &[u8]) -> Result<()> {
|
||||
while !bytes.is_empty() {
|
||||
let mut written = 0u32;
|
||||
let ok = unsafe {
|
||||
WriteFile(
|
||||
handle,
|
||||
bytes.as_ptr() as *const _,
|
||||
bytes.len() as u32,
|
||||
&mut written,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
let err = unsafe { GetLastError() } as i32;
|
||||
return Err(anyhow::anyhow!("WriteFile failed: {err}"));
|
||||
}
|
||||
if written == 0 {
|
||||
anyhow::bail!("WriteFile returned success but wrote 0 bytes");
|
||||
}
|
||||
bytes = &bytes[written as usize..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn finalize_exit(
|
||||
exit_tx: oneshot::Sender<i32>,
|
||||
process_handle: Arc<StdMutex<Option<HANDLE>>>,
|
||||
thread_handle: HANDLE,
|
||||
output_join: std::thread::JoinHandle<()>,
|
||||
guards: Vec<PathBuf>,
|
||||
cap_sid: Option<String>,
|
||||
logs_base_dir: Option<&Path>,
|
||||
command: Vec<String>,
|
||||
) {
|
||||
let exit_code = {
|
||||
let mut raw_exit = 1u32;
|
||||
if let Ok(guard) = process_handle.lock()
|
||||
&& let Some(handle) = guard.as_ref()
|
||||
{
|
||||
unsafe {
|
||||
WaitForSingleObject(*handle, INFINITE);
|
||||
GetExitCodeProcess(*handle, &mut raw_exit);
|
||||
}
|
||||
}
|
||||
raw_exit as i32
|
||||
};
|
||||
|
||||
let _ = output_join.join();
|
||||
let _ = exit_tx.send(exit_code);
|
||||
|
||||
unsafe {
|
||||
if thread_handle != 0 && thread_handle != INVALID_HANDLE_VALUE {
|
||||
CloseHandle(thread_handle);
|
||||
}
|
||||
if let Ok(mut guard) = process_handle.lock()
|
||||
&& let Some(handle) = guard.take()
|
||||
{
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
|
||||
if exit_code == 0 {
|
||||
log_success(&command, logs_base_dir);
|
||||
} else {
|
||||
log_failure(&command, &format!("exit code {exit_code}"), logs_base_dir);
|
||||
}
|
||||
|
||||
if let Some(cap_sid) = cap_sid
|
||||
&& let Ok(sid) = LocalSid::from_string(&cap_sid)
|
||||
{
|
||||
unsafe {
|
||||
for path in guards {
|
||||
revoke_ace(&path, sid.as_ptr());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_conpty_handle(hpc: &Arc<StdMutex<Option<HANDLE>>>, size: TerminalSize) -> Result<()> {
|
||||
let guard = hpc
|
||||
.lock()
|
||||
.map_err(|_| anyhow::anyhow!("failed to lock ConPTY handle"))?;
|
||||
let hpc = guard
|
||||
.as_ref()
|
||||
.copied()
|
||||
.ok_or_else(|| anyhow::anyhow!("process is not attached to a PTY"))?;
|
||||
let result = unsafe {
|
||||
ResizePseudoConsole(
|
||||
hpc,
|
||||
COORD {
|
||||
X: size.cols as i16,
|
||||
Y: size.rows as i16,
|
||||
},
|
||||
)
|
||||
};
|
||||
if result == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"failed to resize console: HRESULT {result}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn spawn_windows_sandbox_session_legacy(
|
||||
policy_json_or_preset: &str,
|
||||
sandbox_policy_cwd: &Path,
|
||||
codex_home: &Path,
|
||||
command: Vec<String>,
|
||||
cwd: &Path,
|
||||
mut env_map: HashMap<String, String>,
|
||||
timeout_ms: Option<u64>,
|
||||
tty: bool,
|
||||
stdin_open: bool,
|
||||
use_private_desktop: bool,
|
||||
) -> Result<SpawnedProcess> {
|
||||
let common = prepare_legacy_spawn_context(
|
||||
policy_json_or_preset,
|
||||
codex_home,
|
||||
cwd,
|
||||
&mut env_map,
|
||||
&command,
|
||||
/*inherit_path*/ false,
|
||||
/*add_git_safe_directory*/ false,
|
||||
)?;
|
||||
if !common.policy.has_full_disk_read_access() {
|
||||
anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");
|
||||
}
|
||||
let security = prepare_legacy_session_security(&common.policy, codex_home, cwd)?;
|
||||
allow_null_device_for_workspace_write(common.is_workspace_write);
|
||||
|
||||
let persist_aces = common.is_workspace_write;
|
||||
let guards = apply_legacy_session_acl_rules(
|
||||
&common.policy,
|
||||
sandbox_policy_cwd,
|
||||
&common.current_dir,
|
||||
&env_map,
|
||||
&security.psid_generic,
|
||||
security.psid_workspace.as_ref(),
|
||||
persist_aces,
|
||||
);
|
||||
|
||||
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
|
||||
let (stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(256);
|
||||
let stderr_rx = if tty {
|
||||
None
|
||||
} else {
|
||||
Some(broadcast::channel::<Vec<u8>>(256))
|
||||
};
|
||||
let (exit_tx, exit_rx) = oneshot::channel::<i32>();
|
||||
|
||||
let LegacyProcessHandles {
|
||||
process: pi,
|
||||
output_join,
|
||||
writer_handle,
|
||||
hpc,
|
||||
token_handle,
|
||||
desktop,
|
||||
} = match spawn_legacy_process(
|
||||
security.h_token,
|
||||
&command,
|
||||
cwd,
|
||||
&env_map,
|
||||
use_private_desktop,
|
||||
tty,
|
||||
stdin_open,
|
||||
stdout_tx,
|
||||
stderr_rx.as_ref().map(|(tx, _rx)| tx.clone()),
|
||||
writer_rx,
|
||||
common.logs_base_dir.as_deref(),
|
||||
) {
|
||||
Ok(handles) => handles,
|
||||
Err(err) => {
|
||||
unsafe {
|
||||
if !persist_aces
|
||||
&& !guards.is_empty()
|
||||
&& let Ok(sid) = LocalSid::from_string(&security.cap_sid_str)
|
||||
{
|
||||
for path in &guards {
|
||||
revoke_ace(path, sid.as_ptr());
|
||||
}
|
||||
}
|
||||
CloseHandle(security.h_token);
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
let hpc_handle = hpc.map(|hpc| Arc::new(StdMutex::new(Some(hpc))));
|
||||
|
||||
let process_handle = Arc::new(StdMutex::new(Some(pi.hProcess)));
|
||||
let wait_handle = Arc::clone(&process_handle);
|
||||
let command_for_wait = command.clone();
|
||||
let guards_for_wait = if persist_aces { Vec::new() } else { guards };
|
||||
let cap_sid_for_wait = if guards_for_wait.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(security.cap_sid_str)
|
||||
};
|
||||
let hpc_for_wait = hpc_handle.clone();
|
||||
std::thread::spawn(move || {
|
||||
let _desktop = desktop;
|
||||
let timeout = timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE);
|
||||
let wait_res = unsafe { WaitForSingleObject(pi.hProcess, timeout) };
|
||||
if wait_res == WAIT_TIMEOUT {
|
||||
unsafe {
|
||||
if let Ok(guard) = wait_handle.lock()
|
||||
&& let Some(handle) = guard.as_ref()
|
||||
{
|
||||
let _ = TerminateProcess(*handle, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(hpc) = hpc_for_wait
|
||||
&& let Ok(mut guard) = hpc.lock()
|
||||
&& let Some(hpc) = guard.take()
|
||||
{
|
||||
unsafe {
|
||||
ClosePseudoConsole(hpc);
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
if token_handle != 0 && token_handle != INVALID_HANDLE_VALUE {
|
||||
CloseHandle(token_handle);
|
||||
}
|
||||
}
|
||||
finalize_exit(
|
||||
exit_tx,
|
||||
wait_handle,
|
||||
pi.hThread,
|
||||
output_join,
|
||||
guards_for_wait,
|
||||
cap_sid_for_wait,
|
||||
common.logs_base_dir.as_deref(),
|
||||
command_for_wait,
|
||||
);
|
||||
});
|
||||
|
||||
let terminator = {
|
||||
let process_handle = Arc::clone(&process_handle);
|
||||
Some(Box::new(move || {
|
||||
if let Ok(guard) = process_handle.lock()
|
||||
&& let Some(handle) = guard.as_ref()
|
||||
{
|
||||
unsafe {
|
||||
let _ = TerminateProcess(*handle, 1);
|
||||
}
|
||||
}
|
||||
}) as Box<dyn FnMut() + Send + Sync>)
|
||||
};
|
||||
|
||||
let driver = ProcessDriver {
|
||||
writer_tx,
|
||||
stdout_rx,
|
||||
stderr_rx: stderr_rx.map(|(_tx, rx)| rx),
|
||||
exit_rx,
|
||||
terminator,
|
||||
writer_handle: Some(writer_handle),
|
||||
resizer: hpc_handle.map(|hpc| {
|
||||
Box::new(move |size| resize_conpty_handle(&hpc, size))
|
||||
as Box<dyn FnMut(TerminalSize) -> Result<()> + Send>
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(finish_driver_spawn(driver, stdin_open))
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub(crate) mod elevated;
|
||||
pub(crate) mod legacy;
|
||||
pub(crate) mod windows_common;
|
||||
@@ -0,0 +1,191 @@
|
||||
use crate::ipc_framed::EmptyPayload;
|
||||
use crate::ipc_framed::FramedMessage;
|
||||
use crate::ipc_framed::Message;
|
||||
use crate::ipc_framed::OutputStream;
|
||||
use crate::ipc_framed::ResizePayload;
|
||||
use crate::ipc_framed::StdinPayload;
|
||||
use crate::ipc_framed::decode_bytes;
|
||||
use crate::ipc_framed::encode_bytes;
|
||||
use anyhow::Result;
|
||||
use codex_utils_pty::ProcessDriver;
|
||||
use codex_utils_pty::SpawnedProcess;
|
||||
use codex_utils_pty::TerminalSize;
|
||||
use codex_utils_pty::spawn_from_driver;
|
||||
use std::fs::File;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
pub(crate) fn finish_driver_spawn(driver: ProcessDriver, stdin_open: bool) -> SpawnedProcess {
|
||||
let spawned = spawn_from_driver(driver);
|
||||
if !stdin_open {
|
||||
spawned.session.close_stdin();
|
||||
}
|
||||
spawned
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_windows_tty_input(bytes: &[u8], previous_was_cr: &mut bool) -> Vec<u8> {
|
||||
let mut normalized = Vec::with_capacity(bytes.len());
|
||||
for &byte in bytes {
|
||||
if byte == b'\n' {
|
||||
if !*previous_was_cr {
|
||||
normalized.push(b'\r');
|
||||
}
|
||||
normalized.push(b'\n');
|
||||
*previous_was_cr = false;
|
||||
} else {
|
||||
normalized.push(byte);
|
||||
*previous_was_cr = byte == b'\r';
|
||||
}
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
pub(crate) fn start_runner_pipe_writer(
|
||||
mut pipe_write: File,
|
||||
) -> std::sync::mpsc::Sender<FramedMessage> {
|
||||
let (outbound_tx, outbound_rx) = std::sync::mpsc::channel::<FramedMessage>();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
while let Ok(msg) = outbound_rx.recv() {
|
||||
if crate::ipc_framed::write_frame(&mut pipe_write, &msg).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
outbound_tx
|
||||
}
|
||||
|
||||
pub(crate) fn start_runner_stdin_writer(
|
||||
mut writer_rx: mpsc::Receiver<Vec<u8>>,
|
||||
outbound_tx: std::sync::mpsc::Sender<FramedMessage>,
|
||||
normalize_newlines: bool,
|
||||
stdin_open: bool,
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut previous_was_cr = false;
|
||||
while let Some(bytes) = writer_rx.blocking_recv() {
|
||||
let bytes = if normalize_newlines {
|
||||
normalize_windows_tty_input(&bytes, &mut previous_was_cr)
|
||||
} else {
|
||||
bytes
|
||||
};
|
||||
let msg = FramedMessage {
|
||||
version: 1,
|
||||
message: Message::Stdin {
|
||||
payload: StdinPayload {
|
||||
data_b64: encode_bytes(&bytes),
|
||||
},
|
||||
},
|
||||
};
|
||||
if outbound_tx.send(msg).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if stdin_open {
|
||||
let _ = outbound_tx.send(FramedMessage {
|
||||
version: 1,
|
||||
message: Message::CloseStdin {
|
||||
payload: EmptyPayload::default(),
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn start_runner_stdout_reader(
|
||||
mut pipe_read: File,
|
||||
stdout_tx: broadcast::Sender<Vec<u8>>,
|
||||
stderr_tx: Option<broadcast::Sender<Vec<u8>>>,
|
||||
exit_tx: oneshot::Sender<i32>,
|
||||
) {
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
let msg = match crate::ipc_framed::read_frame(&mut pipe_read) {
|
||||
Ok(Some(v)) => v,
|
||||
Ok(None) => {
|
||||
send_runner_error(
|
||||
"runner pipe closed before exit",
|
||||
&stdout_tx,
|
||||
stderr_tx.as_ref(),
|
||||
);
|
||||
let _ = exit_tx.send(-1);
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
send_runner_error(
|
||||
&format!("runner read failed: {err}"),
|
||||
&stdout_tx,
|
||||
stderr_tx.as_ref(),
|
||||
);
|
||||
let _ = exit_tx.send(-1);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
match msg.message {
|
||||
Message::Output { payload } => {
|
||||
if let Ok(data) = decode_bytes(&payload.data_b64) {
|
||||
match payload.stream {
|
||||
OutputStream::Stdout => {
|
||||
let _ = stdout_tx.send(data);
|
||||
}
|
||||
OutputStream::Stderr => {
|
||||
if let Some(stderr_tx) = stderr_tx.as_ref() {
|
||||
let _ = stderr_tx.send(data);
|
||||
} else {
|
||||
let _ = stdout_tx.send(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Exit { payload } => {
|
||||
let _ = exit_tx.send(payload.exit_code);
|
||||
break;
|
||||
}
|
||||
Message::Error { payload } => {
|
||||
send_runner_error(&payload.message, &stdout_tx, stderr_tx.as_ref());
|
||||
let _ = exit_tx.send(-1);
|
||||
break;
|
||||
}
|
||||
Message::SpawnReady { .. }
|
||||
| Message::Stdin { .. }
|
||||
| Message::CloseStdin { .. }
|
||||
| Message::Resize { .. }
|
||||
| Message::SpawnRequest { .. }
|
||||
| Message::Terminate { .. } => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn make_runner_resizer(
|
||||
outbound_tx: std::sync::mpsc::Sender<FramedMessage>,
|
||||
) -> Box<dyn FnMut(TerminalSize) -> Result<()> + Send> {
|
||||
Box::new(move |size: TerminalSize| {
|
||||
outbound_tx
|
||||
.send(FramedMessage {
|
||||
version: 1,
|
||||
message: Message::Resize {
|
||||
payload: ResizePayload {
|
||||
rows: size.rows,
|
||||
cols: size.cols,
|
||||
},
|
||||
},
|
||||
})
|
||||
.map_err(|_| anyhow::anyhow!("runner resize pipe closed"))
|
||||
})
|
||||
}
|
||||
|
||||
fn send_runner_error(
|
||||
message: &str,
|
||||
stdout_tx: &broadcast::Sender<Vec<u8>>,
|
||||
stderr_tx: Option<&broadcast::Sender<Vec<u8>>>,
|
||||
) {
|
||||
let formatted = format!("runner error: {message}\n").into_bytes();
|
||||
if let Some(stderr_tx) = stderr_tx {
|
||||
let _ = stderr_tx.send(formatted);
|
||||
} else {
|
||||
let _ = stdout_tx.send(formatted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//! Unified exec session spawner for Windows sandboxing.
|
||||
//!
|
||||
//! This module is the thin orchestration layer for Windows unified-exec sessions.
|
||||
//! Backend-specific mechanics live in sibling modules:
|
||||
//! - `backends::legacy` adapts the direct restricted-token spawn path into a live session.
|
||||
//! - `backends::elevated` adapts the elevated command-runner IPC path into the same session API.
|
||||
//! - `backends::windows_common` holds the small shared Windows backend helpers
|
||||
//! used by both.
|
||||
|
||||
mod backends;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_utils_pty::SpawnedProcess;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn spawn_windows_sandbox_session_legacy(
|
||||
policy_json_or_preset: &str,
|
||||
sandbox_policy_cwd: &Path,
|
||||
codex_home: &Path,
|
||||
command: Vec<String>,
|
||||
cwd: &Path,
|
||||
env_map: HashMap<String, String>,
|
||||
timeout_ms: Option<u64>,
|
||||
tty: bool,
|
||||
stdin_open: bool,
|
||||
use_private_desktop: bool,
|
||||
) -> Result<SpawnedProcess> {
|
||||
backends::legacy::spawn_windows_sandbox_session_legacy(
|
||||
policy_json_or_preset,
|
||||
sandbox_policy_cwd,
|
||||
codex_home,
|
||||
command,
|
||||
cwd,
|
||||
env_map,
|
||||
timeout_ms,
|
||||
tty,
|
||||
stdin_open,
|
||||
use_private_desktop,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn spawn_windows_sandbox_session_elevated(
|
||||
policy_json_or_preset: &str,
|
||||
sandbox_policy_cwd: &Path,
|
||||
codex_home: &Path,
|
||||
command: Vec<String>,
|
||||
cwd: &Path,
|
||||
env_map: HashMap<String, String>,
|
||||
timeout_ms: Option<u64>,
|
||||
tty: bool,
|
||||
stdin_open: bool,
|
||||
use_private_desktop: bool,
|
||||
) -> Result<SpawnedProcess> {
|
||||
backends::elevated::spawn_windows_sandbox_session_elevated(
|
||||
policy_json_or_preset,
|
||||
sandbox_policy_cwd,
|
||||
codex_home,
|
||||
command,
|
||||
cwd,
|
||||
env_map,
|
||||
timeout_ms,
|
||||
tty,
|
||||
stdin_open,
|
||||
use_private_desktop,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use backends::windows_common::finish_driver_spawn;
|
||||
#[cfg(test)]
|
||||
pub(crate) use backends::windows_common::make_runner_resizer;
|
||||
#[cfg(test)]
|
||||
pub(crate) use backends::windows_common::start_runner_pipe_writer;
|
||||
#[cfg(test)]
|
||||
pub(crate) use backends::windows_common::start_runner_stdin_writer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,533 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use super::spawn_windows_sandbox_session_legacy;
|
||||
use crate::ipc_framed::Message;
|
||||
use crate::ipc_framed::decode_bytes;
|
||||
use crate::ipc_framed::read_frame;
|
||||
use crate::run_windows_sandbox_capture;
|
||||
use codex_utils_pty::ProcessDriver;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Seek;
|
||||
use std::io::SeekFrom;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tempfile::TempDir;
|
||||
use tokio::runtime::Builder;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::timeout;
|
||||
|
||||
static TEST_HOME_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn current_thread_runtime() -> tokio::runtime::Runtime {
|
||||
Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("build tokio runtime")
|
||||
}
|
||||
|
||||
fn pwsh_path() -> Option<PathBuf> {
|
||||
let program_files = std::env::var_os("ProgramFiles")?;
|
||||
let path = PathBuf::from(program_files).join("PowerShell\\7\\pwsh.exe");
|
||||
path.is_file().then_some(path)
|
||||
}
|
||||
|
||||
fn sandbox_cwd() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.expect("repo root")
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
fn sandbox_home(name: &str) -> TempDir {
|
||||
let id = TEST_HOME_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!("codex-windows-sandbox-{name}-{id}"));
|
||||
let _ = fs::remove_dir_all(&path);
|
||||
fs::create_dir_all(&path).expect("create sandbox home");
|
||||
tempfile::TempDir::new_in(&path).expect("create sandbox home tempdir")
|
||||
}
|
||||
|
||||
fn sandbox_log(codex_home: &Path) -> String {
|
||||
let log_path = codex_home.join(".sandbox").join("sandbox.log");
|
||||
fs::read_to_string(&log_path)
|
||||
.unwrap_or_else(|err| format!("failed to read {}: {err}", log_path.display()))
|
||||
}
|
||||
|
||||
fn wait_for_frame_count(frames_path: &Path, expected_frames: usize) -> Vec<Message> {
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
loop {
|
||||
let mut reader = OpenOptions::new()
|
||||
.read(true)
|
||||
.open(frames_path)
|
||||
.expect("open frame file for read");
|
||||
reader
|
||||
.seek(SeekFrom::Start(0))
|
||||
.expect("seek to start of frame file");
|
||||
|
||||
let mut frames = Vec::new();
|
||||
loop {
|
||||
match read_frame(&mut reader) {
|
||||
Ok(Some(frame)) => frames.push(frame.message),
|
||||
Ok(None) => break,
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
if frames.len() >= expected_frames {
|
||||
return frames;
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"timed out waiting for {expected_frames} frames, saw {}",
|
||||
frames.len()
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_stdout_and_exit(
|
||||
spawned: codex_utils_pty::SpawnedProcess,
|
||||
codex_home: &Path,
|
||||
timeout_duration: Duration,
|
||||
) -> (Vec<u8>, i32) {
|
||||
let codex_utils_pty::SpawnedProcess {
|
||||
session: _session,
|
||||
mut stdout_rx,
|
||||
stderr_rx: _stderr_rx,
|
||||
exit_rx,
|
||||
} = spawned;
|
||||
let stdout_task = tokio::spawn(async move {
|
||||
let mut stdout = Vec::new();
|
||||
while let Some(chunk) = stdout_rx.recv().await {
|
||||
stdout.extend(chunk);
|
||||
}
|
||||
stdout
|
||||
});
|
||||
let exit_code = timeout(timeout_duration, exit_rx)
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("timed out waiting for exit\n{}", sandbox_log(codex_home)))
|
||||
.unwrap_or(-1);
|
||||
let stdout = timeout(timeout_duration, stdout_task)
|
||||
.await
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"timed out waiting for stdout task\n{}",
|
||||
sandbox_log(codex_home)
|
||||
)
|
||||
})
|
||||
.expect("stdout task join");
|
||||
(stdout, exit_code)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_non_tty_cmd_emits_output() {
|
||||
let runtime = current_thread_runtime();
|
||||
runtime.block_on(async move {
|
||||
let cwd = sandbox_cwd();
|
||||
let codex_home = sandbox_home("legacy-non-tty-cmd");
|
||||
println!("cmd codex_home={}", codex_home.path().display());
|
||||
let spawned = spawn_windows_sandbox_session_legacy(
|
||||
"workspace-write",
|
||||
cwd.as_path(),
|
||||
codex_home.path(),
|
||||
vec![
|
||||
"C:\\Windows\\System32\\cmd.exe".to_string(),
|
||||
"/c".to_string(),
|
||||
"echo LEGACY-NONTTY-CMD".to_string(),
|
||||
],
|
||||
cwd.as_path(),
|
||||
HashMap::new(),
|
||||
Some(5_000),
|
||||
/*tty*/ false,
|
||||
/*stdin_open*/ false,
|
||||
/*use_private_desktop*/ true,
|
||||
)
|
||||
.await
|
||||
.expect("spawn legacy non-tty cmd session");
|
||||
println!("cmd spawn returned");
|
||||
let (stdout, exit_code) =
|
||||
collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(10)).await;
|
||||
println!("cmd collect returned exit_code={exit_code}");
|
||||
let stdout = String::from_utf8_lossy(&stdout);
|
||||
assert_eq!(exit_code, 0, "stdout={stdout:?}");
|
||||
assert!(stdout.contains("LEGACY-NONTTY-CMD"), "stdout={stdout:?}");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_non_tty_powershell_emits_output() {
|
||||
let Some(pwsh) = pwsh_path() else {
|
||||
return;
|
||||
};
|
||||
let runtime = current_thread_runtime();
|
||||
runtime.block_on(async move {
|
||||
let cwd = sandbox_cwd();
|
||||
let codex_home = sandbox_home("legacy-non-tty-pwsh");
|
||||
println!("pwsh codex_home={}", codex_home.path().display());
|
||||
let spawned = spawn_windows_sandbox_session_legacy(
|
||||
"workspace-write",
|
||||
cwd.as_path(),
|
||||
codex_home.path(),
|
||||
vec![
|
||||
pwsh.display().to_string(),
|
||||
"-NoProfile".to_string(),
|
||||
"-Command".to_string(),
|
||||
"Write-Output LEGACY-NONTTY-DIRECT".to_string(),
|
||||
],
|
||||
cwd.as_path(),
|
||||
HashMap::new(),
|
||||
Some(5_000),
|
||||
/*tty*/ false,
|
||||
/*stdin_open*/ false,
|
||||
/*use_private_desktop*/ true,
|
||||
)
|
||||
.await
|
||||
.expect("spawn legacy non-tty powershell session");
|
||||
println!("pwsh spawn returned");
|
||||
let (stdout, exit_code) =
|
||||
collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(10)).await;
|
||||
println!("pwsh collect returned exit_code={exit_code}");
|
||||
let stdout = String::from_utf8_lossy(&stdout);
|
||||
assert_eq!(exit_code, 0, "stdout={stdout:?}");
|
||||
assert!(stdout.contains("LEGACY-NONTTY-DIRECT"), "stdout={stdout:?}");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finish_driver_spawn_keeps_stdin_open_when_requested() {
|
||||
let runtime = current_thread_runtime();
|
||||
runtime.block_on(async move {
|
||||
let (writer_tx, mut writer_rx) = mpsc::channel::<Vec<u8>>(1);
|
||||
let (_stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(1);
|
||||
let (exit_tx, exit_rx) = oneshot::channel::<i32>();
|
||||
drop(exit_tx);
|
||||
|
||||
let spawned = super::finish_driver_spawn(
|
||||
ProcessDriver {
|
||||
writer_tx,
|
||||
stdout_rx,
|
||||
stderr_rx: None,
|
||||
exit_rx,
|
||||
terminator: None,
|
||||
writer_handle: None,
|
||||
resizer: None,
|
||||
},
|
||||
/*stdin_open*/ true,
|
||||
);
|
||||
|
||||
spawned
|
||||
.session
|
||||
.writer_sender()
|
||||
.send(b"open".to_vec())
|
||||
.await
|
||||
.expect("stdin should stay open");
|
||||
assert_eq!(writer_rx.recv().await, Some(b"open".to_vec()));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finish_driver_spawn_closes_stdin_when_not_requested() {
|
||||
let runtime = current_thread_runtime();
|
||||
runtime.block_on(async move {
|
||||
let (writer_tx, _writer_rx) = mpsc::channel::<Vec<u8>>(1);
|
||||
let (_stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(1);
|
||||
let (exit_tx, exit_rx) = oneshot::channel::<i32>();
|
||||
drop(exit_tx);
|
||||
|
||||
let spawned = super::finish_driver_spawn(
|
||||
ProcessDriver {
|
||||
writer_tx,
|
||||
stdout_rx,
|
||||
stderr_rx: None,
|
||||
exit_rx,
|
||||
terminator: None,
|
||||
writer_handle: None,
|
||||
resizer: None,
|
||||
},
|
||||
/*stdin_open*/ false,
|
||||
);
|
||||
|
||||
assert!(
|
||||
spawned
|
||||
.session
|
||||
.writer_sender()
|
||||
.send(b"closed".to_vec())
|
||||
.await
|
||||
.is_err(),
|
||||
"stdin should be closed when streaming input is disabled"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runner_stdin_writer_sends_close_stdin_after_input_eof() {
|
||||
let runtime = current_thread_runtime();
|
||||
runtime.block_on(async move {
|
||||
let tempdir = TempDir::new().expect("create tempdir");
|
||||
let frames_path = tempdir.path().join("runner-stdin-frames.bin");
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&frames_path)
|
||||
.expect("create frame file");
|
||||
let outbound_tx = super::start_runner_pipe_writer(file);
|
||||
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(1);
|
||||
let writer_handle = super::start_runner_stdin_writer(
|
||||
writer_rx,
|
||||
outbound_tx,
|
||||
/*normalize_newlines*/ false,
|
||||
/*stdin_open*/ true,
|
||||
);
|
||||
|
||||
writer_tx
|
||||
.send(b"hello".to_vec())
|
||||
.await
|
||||
.expect("send stdin bytes");
|
||||
drop(writer_tx);
|
||||
writer_handle.await.expect("join stdin writer");
|
||||
|
||||
let frames = wait_for_frame_count(&frames_path, 2);
|
||||
|
||||
match &frames[0] {
|
||||
Message::Stdin { payload } => {
|
||||
let bytes = decode_bytes(&payload.data_b64).expect("decode stdin payload");
|
||||
assert_eq!(bytes, b"hello".to_vec());
|
||||
}
|
||||
other => panic!("expected stdin frame, got {other:?}"),
|
||||
}
|
||||
|
||||
match &frames[1] {
|
||||
Message::CloseStdin { .. } => {}
|
||||
other => panic!("expected close-stdin frame, got {other:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runner_resizer_sends_resize_frame() {
|
||||
let runtime = current_thread_runtime();
|
||||
runtime.block_on(async move {
|
||||
let tempdir = TempDir::new().expect("create tempdir");
|
||||
let frames_path = tempdir.path().join("runner-resize-frames.bin");
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&frames_path)
|
||||
.expect("create frame file");
|
||||
let outbound_tx = super::start_runner_pipe_writer(file);
|
||||
let mut resizer = super::make_runner_resizer(outbound_tx);
|
||||
|
||||
resizer(codex_utils_pty::TerminalSize {
|
||||
rows: 45,
|
||||
cols: 132,
|
||||
})
|
||||
.expect("send resize frame");
|
||||
|
||||
let frames = wait_for_frame_count(&frames_path, 1);
|
||||
match &frames[0] {
|
||||
Message::Resize { payload } => {
|
||||
assert_eq!(payload.rows, 45);
|
||||
assert_eq!(payload.cols, 132);
|
||||
}
|
||||
other => panic!("expected resize frame, got {other:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_capture_powershell_emits_output() {
|
||||
let Some(pwsh) = pwsh_path() else {
|
||||
return;
|
||||
};
|
||||
let cwd = sandbox_cwd();
|
||||
let codex_home = sandbox_home("legacy-capture-pwsh");
|
||||
println!("capture pwsh codex_home={}", codex_home.path().display());
|
||||
let result = run_windows_sandbox_capture(
|
||||
"workspace-write",
|
||||
cwd.as_path(),
|
||||
codex_home.path(),
|
||||
vec![
|
||||
pwsh.display().to_string(),
|
||||
"-NoProfile".to_string(),
|
||||
"-Command".to_string(),
|
||||
"Write-Output LEGACY-CAPTURE-DIRECT".to_string(),
|
||||
],
|
||||
cwd.as_path(),
|
||||
HashMap::new(),
|
||||
Some(10_000),
|
||||
/*use_private_desktop*/ true,
|
||||
)
|
||||
.expect("run legacy capture powershell");
|
||||
println!("capture pwsh exit_code={}", result.exit_code);
|
||||
println!("capture pwsh timed_out={}", result.timed_out);
|
||||
let stdout = String::from_utf8_lossy(&result.stdout);
|
||||
let stderr = String::from_utf8_lossy(&result.stderr);
|
||||
println!("capture pwsh stderr={stderr:?}");
|
||||
assert_eq!(result.exit_code, 0, "stdout={stdout:?} stderr={stderr:?}");
|
||||
assert!(
|
||||
stdout.contains("LEGACY-CAPTURE-DIRECT"),
|
||||
"stdout={stdout:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_tty_powershell_emits_output_and_accepts_input() {
|
||||
let Some(pwsh) = pwsh_path() else {
|
||||
return;
|
||||
};
|
||||
let runtime = current_thread_runtime();
|
||||
runtime.block_on(async move {
|
||||
let cwd = sandbox_cwd();
|
||||
let codex_home = sandbox_home("legacy-tty-pwsh");
|
||||
println!("tty pwsh codex_home={}", codex_home.path().display());
|
||||
let spawned = spawn_windows_sandbox_session_legacy(
|
||||
"workspace-write",
|
||||
cwd.as_path(),
|
||||
codex_home.path(),
|
||||
vec![
|
||||
pwsh.display().to_string(),
|
||||
"-NoLogo".to_string(),
|
||||
"-NoProfile".to_string(),
|
||||
"-NoExit".to_string(),
|
||||
"-Command".to_string(),
|
||||
"$PID; Write-Output ready".to_string(),
|
||||
],
|
||||
cwd.as_path(),
|
||||
HashMap::new(),
|
||||
Some(10_000),
|
||||
/*tty*/ true,
|
||||
/*stdin_open*/ true,
|
||||
/*use_private_desktop*/ true,
|
||||
)
|
||||
.await
|
||||
.expect("spawn legacy tty powershell session");
|
||||
println!("tty pwsh spawn returned");
|
||||
|
||||
let writer = spawned.session.writer_sender();
|
||||
writer
|
||||
.send(b"Write-Output second\n".to_vec())
|
||||
.await
|
||||
.expect("send second command");
|
||||
writer
|
||||
.send(b"exit\n".to_vec())
|
||||
.await
|
||||
.expect("send exit command");
|
||||
spawned.session.close_stdin();
|
||||
|
||||
let (stdout, exit_code) =
|
||||
collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(15)).await;
|
||||
let stdout = String::from_utf8_lossy(&stdout);
|
||||
assert_eq!(exit_code, 0, "stdout={stdout:?}");
|
||||
assert!(stdout.contains("ready"), "stdout={stdout:?}");
|
||||
assert!(stdout.contains("second"), "stdout={stdout:?}");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_tty_cmd_emits_output_and_accepts_input() {
|
||||
let runtime = current_thread_runtime();
|
||||
runtime.block_on(async move {
|
||||
let cwd = sandbox_cwd();
|
||||
let codex_home = sandbox_home("legacy-tty-cmd");
|
||||
println!("tty cmd codex_home={}", codex_home.path().display());
|
||||
let spawned = spawn_windows_sandbox_session_legacy(
|
||||
"workspace-write",
|
||||
cwd.as_path(),
|
||||
codex_home.path(),
|
||||
vec![
|
||||
"C:\\Windows\\System32\\cmd.exe".to_string(),
|
||||
"/K".to_string(),
|
||||
"echo ready".to_string(),
|
||||
],
|
||||
cwd.as_path(),
|
||||
HashMap::new(),
|
||||
Some(10_000),
|
||||
/*tty*/ true,
|
||||
/*stdin_open*/ true,
|
||||
/*use_private_desktop*/ true,
|
||||
)
|
||||
.await
|
||||
.expect("spawn legacy tty cmd session");
|
||||
println!("tty cmd spawn returned");
|
||||
|
||||
let writer = spawned.session.writer_sender();
|
||||
writer
|
||||
.send(b"echo second\n".to_vec())
|
||||
.await
|
||||
.expect("send second command");
|
||||
writer
|
||||
.send(b"exit\n".to_vec())
|
||||
.await
|
||||
.expect("send exit command");
|
||||
spawned.session.close_stdin();
|
||||
|
||||
let (stdout, exit_code) =
|
||||
collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(15)).await;
|
||||
let stdout = String::from_utf8_lossy(&stdout);
|
||||
assert_eq!(exit_code, 0, "stdout={stdout:?}");
|
||||
assert!(stdout.contains("ready"), "stdout={stdout:?}");
|
||||
assert!(stdout.contains("second"), "stdout={stdout:?}");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_tty_cmd_default_desktop_emits_output_and_accepts_input() {
|
||||
let runtime = current_thread_runtime();
|
||||
runtime.block_on(async move {
|
||||
let cwd = sandbox_cwd();
|
||||
let codex_home = sandbox_home("legacy-tty-cmd-default-desktop");
|
||||
println!(
|
||||
"tty cmd default desktop codex_home={}",
|
||||
codex_home.path().display()
|
||||
);
|
||||
let spawned = spawn_windows_sandbox_session_legacy(
|
||||
"workspace-write",
|
||||
cwd.as_path(),
|
||||
codex_home.path(),
|
||||
vec![
|
||||
"C:\\Windows\\System32\\cmd.exe".to_string(),
|
||||
"/K".to_string(),
|
||||
"echo ready".to_string(),
|
||||
],
|
||||
cwd.as_path(),
|
||||
HashMap::new(),
|
||||
Some(10_000),
|
||||
/*tty*/ true,
|
||||
/*stdin_open*/ true,
|
||||
/*use_private_desktop*/ false,
|
||||
)
|
||||
.await
|
||||
.expect("spawn legacy tty cmd session");
|
||||
println!("tty cmd default desktop spawn returned");
|
||||
|
||||
let writer = spawned.session.writer_sender();
|
||||
writer
|
||||
.send(b"echo second\n".to_vec())
|
||||
.await
|
||||
.expect("send second command");
|
||||
writer
|
||||
.send(b"exit\n".to_vec())
|
||||
.await
|
||||
.expect("send exit command");
|
||||
spawned.session.close_stdin();
|
||||
|
||||
let (stdout, exit_code) =
|
||||
collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(15)).await;
|
||||
let stdout = String::from_utf8_lossy(&stdout);
|
||||
assert_eq!(exit_code, 0, "stdout={stdout:?}");
|
||||
assert!(stdout.contains("ready"), "stdout={stdout:?}");
|
||||
assert!(stdout.contains("second"), "stdout={stdout:?}");
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user