From 164265bed1eb0d0ebf453196c39c79809d4f1b7a Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Tue, 9 Dec 2025 09:23:51 -0800 Subject: [PATCH] Vendor ConPtySystem (#7656) The repo we were depending on is very large and we need very small part of it. --------- Co-authored-by: Pavel --- .codespellignore | 1 + codex-rs/Cargo.lock | 13 +- codex-rs/Cargo.toml | 1 - codex-rs/utils/pty/Cargo.toml | 16 + codex-rs/utils/pty/src/lib.rs | 16 +- codex-rs/utils/pty/src/win/conpty.rs | 144 +++++++++ codex-rs/utils/pty/src/win/mod.rs | 169 ++++++++++ codex-rs/utils/pty/src/win/procthreadattr.rs | 91 ++++++ codex-rs/utils/pty/src/win/psuedocon.rs | 322 +++++++++++++++++++ third_party/wezterm/LICENSE | 21 ++ 10 files changed, 789 insertions(+), 5 deletions(-) create mode 100644 codex-rs/utils/pty/src/win/conpty.rs create mode 100644 codex-rs/utils/pty/src/win/mod.rs create mode 100644 codex-rs/utils/pty/src/win/procthreadattr.rs create mode 100644 codex-rs/utils/pty/src/win/psuedocon.rs create mode 100644 third_party/wezterm/LICENSE diff --git a/.codespellignore b/.codespellignore index 546a192701..d74f5ed86c 100644 --- a/.codespellignore +++ b/.codespellignore @@ -1 +1,2 @@ iTerm +psuedo \ No newline at end of file diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index cfb4669747..2dde6c07cd 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1673,8 +1673,13 @@ name = "codex-utils-pty" version = "0.0.0" dependencies = [ "anyhow", + "filedescriptor", + "lazy_static", + "log", "portable-pty", + "shared_library", "tokio", + "winapi", ] [[package]] @@ -2578,7 +2583,8 @@ dependencies = [ [[package]] name = "filedescriptor" version = "0.8.3" -source = "git+https://github.com/pakrym/wezterm?branch=PSUEDOCONSOLE_INHERIT_CURSOR#fe38df8409545a696909aa9a09e63438630f217d" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" dependencies = [ "libc", "thiserror 1.0.69", @@ -4656,7 +4662,8 @@ dependencies = [ [[package]] name = "portable-pty" version = "0.9.0" -source = "git+https://github.com/pakrym/wezterm?branch=PSUEDOCONSOLE_INHERIT_CURSOR#fe38df8409545a696909aa9a09e63438630f217d" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" dependencies = [ "anyhow", "bitflags 1.3.2", @@ -4665,7 +4672,7 @@ dependencies = [ "lazy_static", "libc", "log", - "nix 0.29.0", + "nix 0.28.0", "serial2", "shared_library", "shell-words", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 2086fbe897..9f55f67ce3 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -289,7 +289,6 @@ opt-level = 0 # Uncomment to debug local changes. # ratatui = { path = "../../ratatui" } crossterm = { git = "https://github.com/nornagon/crossterm", branch = "nornagon/color-query" } -portable-pty = { git = "https://github.com/pakrym/wezterm", branch = "PSUEDOCONSOLE_INHERIT_CURSOR" } ratatui = { git = "https://github.com/nornagon/ratatui", branch = "nornagon-v0.29.0-patch" } # Uncomment to debug local changes. diff --git a/codex-rs/utils/pty/Cargo.toml b/codex-rs/utils/pty/Cargo.toml index 2b3de5aa15..1a460ea3de 100644 --- a/codex-rs/utils/pty/Cargo.toml +++ b/codex-rs/utils/pty/Cargo.toml @@ -11,3 +11,19 @@ workspace = true anyhow = { workspace = true } portable-pty = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "time"] } + +[target.'cfg(windows)'.dependencies] +filedescriptor = "0.8.3" +lazy_static = { workspace = true } +log = { workspace = true } +shared_library = "0.1.9" +winapi = { version = "0.3.9", features = [ + "handleapi", + "minwinbase", + "processthreadsapi", + "synchapi", + "winbase", + "wincon", + "winerror", + "winnt", +] } diff --git a/codex-rs/utils/pty/src/lib.rs b/codex-rs/utils/pty/src/lib.rs index 23d69b6f6a..dbaf4b81f7 100644 --- a/codex-rs/utils/pty/src/lib.rs +++ b/codex-rs/utils/pty/src/lib.rs @@ -7,7 +7,11 @@ use std::sync::Arc; use std::sync::Mutex as StdMutex; use std::time::Duration; +#[cfg(windows)] +mod win; + use anyhow::Result; +#[cfg(not(windows))] use portable_pty::native_pty_system; use portable_pty::CommandBuilder; use portable_pty::MasterPty; @@ -125,6 +129,16 @@ pub struct SpawnedPty { pub exit_rx: oneshot::Receiver, } +#[cfg(windows)] +fn platform_native_pty_system() -> Box { + Box::new(win::ConPtySystem::default()) +} + +#[cfg(not(windows))] +fn platform_native_pty_system() -> Box { + native_pty_system() +} + pub async fn spawn_pty_process( program: &str, args: &[String], @@ -136,7 +150,7 @@ pub async fn spawn_pty_process( anyhow::bail!("missing program for PTY spawn"); } - let pty_system = native_pty_system(); + let pty_system = platform_native_pty_system(); let pair = pty_system.openpty(PtySize { rows: 24, cols: 80, diff --git a/codex-rs/utils/pty/src/win/conpty.rs b/codex-rs/utils/pty/src/win/conpty.rs new file mode 100644 index 0000000000..03caaa36ad --- /dev/null +++ b/codex-rs/utils/pty/src/win/conpty.rs @@ -0,0 +1,144 @@ +#![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. + +use crate::win::psuedocon::PsuedoCon; +use anyhow::Error; +use filedescriptor::FileDescriptor; +use filedescriptor::Pipe; +use portable_pty::cmdbuilder::CommandBuilder; +use portable_pty::Child; +use portable_pty::MasterPty; +use portable_pty::PtyPair; +use portable_pty::PtySize; +use portable_pty::PtySystem; +use portable_pty::SlavePty; +use std::sync::Arc; +use std::sync::Mutex; +use winapi::um::wincon::COORD; + +#[derive(Default)] +pub struct ConPtySystem {} + +impl PtySystem for ConPtySystem { + fn openpty(&self, size: PtySize) -> anyhow::Result { + let stdin = Pipe::new()?; + let stdout = Pipe::new()?; + + let con = PsuedoCon::new( + COORD { + X: size.cols as i16, + Y: size.rows as i16, + }, + stdin.read, + stdout.write, + )?; + + let master = ConPtyMasterPty { + inner: Arc::new(Mutex::new(Inner { + con, + readable: stdout.read, + writable: Some(stdin.write), + size, + })), + }; + + let slave = ConPtySlavePty { + inner: master.inner.clone(), + }; + + Ok(PtyPair { + master: Box::new(master), + slave: Box::new(slave), + }) + } +} + +struct Inner { + con: PsuedoCon, + readable: FileDescriptor, + writable: Option, + size: PtySize, +} + +impl Inner { + pub fn resize( + &mut self, + num_rows: u16, + num_cols: u16, + pixel_width: u16, + pixel_height: u16, + ) -> Result<(), Error> { + self.con.resize(COORD { + X: num_cols as i16, + Y: num_rows as i16, + })?; + self.size = PtySize { + rows: num_rows, + cols: num_cols, + pixel_width, + pixel_height, + }; + Ok(()) + } +} + +#[derive(Clone)] +pub struct ConPtyMasterPty { + inner: Arc>, +} + +pub struct ConPtySlavePty { + inner: Arc>, +} + +impl MasterPty for ConPtyMasterPty { + fn resize(&self, size: PtySize) -> anyhow::Result<()> { + let mut inner = self.inner.lock().unwrap(); + inner.resize(size.rows, size.cols, size.pixel_width, size.pixel_height) + } + + fn get_size(&self) -> Result { + let inner = self.inner.lock().unwrap(); + Ok(inner.size) + } + + fn try_clone_reader(&self) -> anyhow::Result> { + Ok(Box::new(self.inner.lock().unwrap().readable.try_clone()?)) + } + + fn take_writer(&self) -> anyhow::Result> { + Ok(Box::new( + self.inner + .lock() + .unwrap() + .writable + .take() + .ok_or_else(|| anyhow::anyhow!("writer already taken"))?, + )) + } +} + +impl SlavePty for ConPtySlavePty { + fn spawn_command(&self, cmd: CommandBuilder) -> anyhow::Result> { + let inner = self.inner.lock().unwrap(); + let child = inner.con.spawn_command(cmd)?; + Ok(Box::new(child)) + } +} diff --git a/codex-rs/utils/pty/src/win/mod.rs b/codex-rs/utils/pty/src/win/mod.rs new file mode 100644 index 0000000000..8206c9b890 --- /dev/null +++ b/codex-rs/utils/pty/src/win/mod.rs @@ -0,0 +1,169 @@ +#![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. + +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 mod conpty; +mod procthreadattr; +mod psuedocon; + +pub use conpty::ConPtySystem; + +#[derive(Debug)] +pub struct WinChild { + proc: Mutex, +} + +impl WinChild { + fn is_complete(&mut self) -> IoResult> { + 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) }; + let err = IoError::last_os_error(); + if res != 0 { + Err(err) + } else { + Ok(()) + } + } +} + +impl ChildKiller for WinChild { + fn kill(&mut self) -> IoResult<()> { + self.do_kill().ok(); + Ok(()) + } + + fn clone_killer(&self) -> Box { + 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) }; + let err = IoError::last_os_error(); + if res != 0 { + Err(err) + } else { + Ok(()) + } + } + + fn clone_killer(&self) -> Box { + let proc = self.proc.try_clone().unwrap(); + Box::new(WinChildKiller { proc }) + } +} + +impl Child for WinChild { + fn try_wait(&mut self) -> IoResult> { + self.is_complete() + } + + fn wait(&mut self) -> IoResult { + 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 { + 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 { + let proc = self.proc.lock().unwrap(); + Some(proc.as_raw_handle()) + } +} + +impl std::future::Future for WinChild { + type Output = anyhow::Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + 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 + } + } + } +} diff --git a/codex-rs/utils/pty/src/win/procthreadattr.rs b/codex-rs/utils/pty/src/win/procthreadattr.rs new file mode 100644 index 0000000000..6d464e99de --- /dev/null +++ b/codex-rs/utils/pty/src/win/procthreadattr.rs @@ -0,0 +1,91 @@ +#![allow(clippy::uninit_vec)] + +// 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. + +use super::psuedocon::HPCON; +use anyhow::ensure; +use anyhow::Error; +use std::io::Error as IoError; +use std::mem; +use std::ptr; +use winapi::shared::minwindef::DWORD; +use winapi::um::processthreadsapi::*; + +const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE: usize = 0x00020016; + +pub struct ProcThreadAttributeList { + data: Vec, +} + +impl ProcThreadAttributeList { + pub fn with_capacity(num_attributes: DWORD) -> Result { + let mut bytes_required: usize = 0; + unsafe { + InitializeProcThreadAttributeList( + ptr::null_mut(), + num_attributes, + 0, + &mut bytes_required, + ) + }; + let mut data = Vec::with_capacity(bytes_required); + unsafe { data.set_len(bytes_required) }; + + let attr_ptr = data.as_mut_slice().as_mut_ptr() as *mut _; + let res = unsafe { + InitializeProcThreadAttributeList(attr_ptr, num_attributes, 0, &mut bytes_required) + }; + ensure!( + res != 0, + "InitializeProcThreadAttributeList failed: {}", + IoError::last_os_error() + ); + Ok(Self { data }) + } + + pub fn as_mut_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST { + self.data.as_mut_slice().as_mut_ptr() as *mut _ + } + + pub fn set_pty(&mut self, con: HPCON) -> Result<(), Error> { + let res = unsafe { + UpdateProcThreadAttribute( + self.as_mut_ptr(), + 0, + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, + con, + mem::size_of::(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + ensure!( + res != 0, + "UpdateProcThreadAttribute failed: {}", + IoError::last_os_error() + ); + Ok(()) + } +} + +impl Drop for ProcThreadAttributeList { + fn drop(&mut self) { + unsafe { DeleteProcThreadAttributeList(self.as_mut_ptr()) }; + } +} diff --git a/codex-rs/utils/pty/src/win/psuedocon.rs b/codex-rs/utils/pty/src/win/psuedocon.rs new file mode 100644 index 0000000000..a8db98eefe --- /dev/null +++ b/codex-rs/utils/pty/src/win/psuedocon.rs @@ -0,0 +1,322 @@ +#![allow(clippy::expect_used)] +#![allow(clippy::upper_case_acronyms)] + +// 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. + +use super::WinChild; +use crate::win::procthreadattr::ProcThreadAttributeList; +use anyhow::bail; +use anyhow::ensure; +use anyhow::Error; +use filedescriptor::FileDescriptor; +use filedescriptor::OwnedHandle; +use lazy_static::lazy_static; +use portable_pty::cmdbuilder::CommandBuilder; +use shared_library::shared_library; +use std::env; +use std::ffi::OsStr; +use std::ffi::OsString; +use std::io::Error as IoError; +use std::mem; +use std::os::windows::ffi::OsStrExt; +use std::os::windows::ffi::OsStringExt; +use std::os::windows::io::AsRawHandle; +use std::os::windows::io::FromRawHandle; +use std::path::Path; +use std::ptr; +use std::sync::Mutex; +use winapi::shared::minwindef::DWORD; +use winapi::shared::winerror::HRESULT; +use winapi::shared::winerror::S_OK; +use winapi::um::handleapi::*; +use winapi::um::processthreadsapi::*; +use winapi::um::winbase::CREATE_UNICODE_ENVIRONMENT; +use winapi::um::winbase::EXTENDED_STARTUPINFO_PRESENT; +use winapi::um::winbase::STARTF_USESTDHANDLES; +use winapi::um::winbase::STARTUPINFOEXW; +use winapi::um::wincon::COORD; +use winapi::um::winnt::HANDLE; + +pub type HPCON = HANDLE; + +pub const PSEUDOCONSOLE_RESIZE_QUIRK: DWORD = 0x2; +#[allow(dead_code)] +pub const PSEUDOCONSOLE_PASSTHROUGH_MODE: DWORD = 0x8; + +shared_library!(ConPtyFuncs, + pub fn CreatePseudoConsole( + size: COORD, + hInput: HANDLE, + hOutput: HANDLE, + flags: DWORD, + hpc: *mut HPCON + ) -> HRESULT, + pub fn ResizePseudoConsole(hpc: HPCON, size: COORD) -> HRESULT, + pub fn ClosePseudoConsole(hpc: HPCON), +); + +fn load_conpty() -> ConPtyFuncs { + let kernel = ConPtyFuncs::open(Path::new("kernel32.dll")).expect( + "this system does not support conpty. Windows 10 October 2018 or newer is required", + ); + + if let Ok(sideloaded) = ConPtyFuncs::open(Path::new("conpty.dll")) { + sideloaded + } else { + kernel + } +} + +lazy_static! { + static ref CONPTY: ConPtyFuncs = load_conpty(); +} + +pub struct PsuedoCon { + con: HPCON, +} + +unsafe impl Send for PsuedoCon {} +unsafe impl Sync for PsuedoCon {} + +impl Drop for PsuedoCon { + fn drop(&mut self) { + unsafe { (CONPTY.ClosePseudoConsole)(self.con) }; + } +} + +impl PsuedoCon { + pub fn new(size: COORD, input: FileDescriptor, output: FileDescriptor) -> Result { + let mut con: HPCON = INVALID_HANDLE_VALUE; + let result = unsafe { + (CONPTY.CreatePseudoConsole)( + size, + input.as_raw_handle() as _, + output.as_raw_handle() as _, + PSEUDOCONSOLE_RESIZE_QUIRK, + &mut con, + ) + }; + ensure!( + result == S_OK, + "failed to create psuedo console: HRESULT {result}" + ); + Ok(Self { con }) + } + + pub fn resize(&self, size: COORD) -> Result<(), Error> { + let result = unsafe { (CONPTY.ResizePseudoConsole)(self.con, size) }; + ensure!( + result == S_OK, + "failed to resize console to {}x{}: HRESULT: {}", + size.X, + size.Y, + result + ); + Ok(()) + } + + pub fn spawn_command(&self, cmd: CommandBuilder) -> anyhow::Result { + let mut si: STARTUPINFOEXW = unsafe { mem::zeroed() }; + si.StartupInfo.cb = mem::size_of::() 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 mut attrs = ProcThreadAttributeList::with_capacity(1)?; + attrs.set_pty(self.con)?; + si.lpAttributeList = attrs.as_mut_ptr(); + + let mut pi: PROCESS_INFORMATION = unsafe { mem::zeroed() }; + + let (mut exe, mut cmdline) = build_cmdline(&cmd)?; + let cmd_os = OsString::from_wide(&cmdline); + + let cwd = resolve_current_directory(&cmd); + let mut env_block = build_environment_block(&cmd); + + let res = unsafe { + CreateProcessW( + exe.as_mut_ptr(), + cmdline.as_mut_ptr(), + ptr::null_mut(), + ptr::null_mut(), + 0, + EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, + env_block.as_mut_ptr() as *mut _, + cwd.as_ref().map_or(ptr::null(), std::vec::Vec::as_ptr), + &mut si.StartupInfo, + &mut pi, + ) + }; + if res == 0 { + let err = IoError::last_os_error(); + let msg = format!( + "CreateProcessW `{:?}` in cwd `{:?}` failed: {}", + cmd_os, + cwd.as_ref().map(|c| OsString::from_wide(c)), + err + ); + log::error!("{msg}"); + bail!("{msg}"); + } + + let _main_thread = unsafe { OwnedHandle::from_raw_handle(pi.hThread as _) }; + let proc = unsafe { OwnedHandle::from_raw_handle(pi.hProcess as _) }; + + Ok(WinChild { + proc: Mutex::new(proc), + }) + } +} + +fn resolve_current_directory(cmd: &CommandBuilder) -> Option> { + let home = cmd + .get_env("USERPROFILE") + .and_then(|path| Path::new(path).is_dir().then(|| path.to_owned())); + let cwd = cmd + .get_cwd() + .and_then(|path| Path::new(path).is_dir().then(|| path.to_owned())); + let dir = cwd.or(home)?; + + let mut wide = Vec::new(); + if Path::new(&dir).is_relative() { + if let Ok(current_dir) = env::current_dir() { + wide.extend(current_dir.join(&dir).as_os_str().encode_wide()); + } else { + wide.extend(dir.encode_wide()); + } + } else { + wide.extend(dir.encode_wide()); + } + wide.push(0); + Some(wide) +} + +fn build_environment_block(cmd: &CommandBuilder) -> Vec { + let mut block = Vec::new(); + for (key, value) in cmd.iter_full_env_as_str() { + block.extend(OsStr::new(key).encode_wide()); + block.push(b'=' as u16); + block.extend(OsStr::new(value).encode_wide()); + block.push(0); + } + block.push(0); + block +} + +fn build_cmdline(cmd: &CommandBuilder) -> anyhow::Result<(Vec, Vec)> { + let exe_os: OsString = if cmd.is_default_prog() { + cmd.get_env("ComSpec") + .unwrap_or(OsStr::new("cmd.exe")) + .to_os_string() + } else { + let argv = cmd.get_argv(); + let Some(first) = argv.first() else { + anyhow::bail!("missing program name"); + }; + search_path(cmd, first) + }; + + let mut cmdline = Vec::new(); + append_quoted(&exe_os, &mut cmdline); + for arg in cmd.get_argv().iter().skip(1) { + cmdline.push(' ' as u16); + ensure!( + !arg.encode_wide().any(|c| c == 0), + "invalid encoding for command line argument {arg:?}" + ); + append_quoted(arg, &mut cmdline); + } + cmdline.push(0); + + let mut exe: Vec = exe_os.encode_wide().collect(); + exe.push(0); + + Ok((exe, cmdline)) +} + +fn search_path(cmd: &CommandBuilder, exe: &OsStr) -> OsString { + if let Some(path) = cmd.get_env("PATH") { + let extensions = cmd.get_env("PATHEXT").unwrap_or(OsStr::new(".EXE")); + for path in env::split_paths(path) { + let candidate = path.join(exe); + if candidate.exists() { + return candidate.into_os_string(); + } + + for ext in env::split_paths(extensions) { + let ext = ext.to_str().unwrap_or(""); + let path = path + .join(exe) + .with_extension(ext.strip_prefix('.').unwrap_or(ext)); + if path.exists() { + return path.into_os_string(); + } + } + } + } + + exe.to_os_string() +} + +fn append_quoted(arg: &OsStr, cmdline: &mut Vec) { + if !arg.is_empty() + && !arg.encode_wide().any(|c| { + c == ' ' as u16 + || c == '\t' as u16 + || c == '\n' as u16 + || c == '\x0b' as u16 + || c == '\"' as u16 + }) + { + cmdline.extend(arg.encode_wide()); + return; + } + cmdline.push('"' as u16); + + let arg: Vec<_> = arg.encode_wide().collect(); + let mut i = 0; + while i < arg.len() { + let mut num_backslashes = 0; + while i < arg.len() && arg[i] == '\\' as u16 { + i += 1; + num_backslashes += 1; + } + + if i == arg.len() { + for _ in 0..num_backslashes * 2 { + cmdline.push('\\' as u16); + } + break; + } else if arg[i] == b'"' as u16 { + for _ in 0..num_backslashes * 2 + 1 { + cmdline.push('\\' as u16); + } + cmdline.push(arg[i]); + } else { + for _ in 0..num_backslashes { + cmdline.push('\\' as u16); + } + cmdline.push(arg[i]); + } + i += 1; + } + cmdline.push('"' as u16); +} diff --git a/third_party/wezterm/LICENSE b/third_party/wezterm/LICENSE new file mode 100644 index 0000000000..d6c7256999 --- /dev/null +++ b/third_party/wezterm/LICENSE @@ -0,0 +1,21 @@ +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.