diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-23 21:43:23 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-23 21:43:23 +0300 |
| commit | a66f4c64e9d1591f042a94d6f3bef3b4298a4d1c (patch) | |
| tree | 747de08b6ada4300afb4124da9feff0cc66b96a7 /vendor/portable-pty/src/win | |
| parent | 5981ba426e8ed8f6785195a03cc0d8a190173e61 (diff) | |
fix: force system conpty on windows
refs #761
Diffstat (limited to 'vendor/portable-pty/src/win')
| -rw-r--r-- | vendor/portable-pty/src/win/conpty.rs | 117 | ||||
| -rw-r--r-- | vendor/portable-pty/src/win/mod.rs | 149 | ||||
| -rw-r--r-- | vendor/portable-pty/src/win/procthreadattr.rs | 72 | ||||
| -rw-r--r-- | vendor/portable-pty/src/win/psuedocon.rs | 168 |
4 files changed, 506 insertions, 0 deletions
diff --git a/vendor/portable-pty/src/win/conpty.rs b/vendor/portable-pty/src/win/conpty.rs new file mode 100644 index 00000000..0e9a18c6 --- /dev/null +++ b/vendor/portable-pty/src/win/conpty.rs @@ -0,0 +1,117 @@ +use crate::cmdbuilder::CommandBuilder; +use crate::win::psuedocon::PsuedoCon; +use crate::{Child, MasterPty, PtyPair, PtySize, PtySystem, SlavePty}; +use anyhow::Error; +use filedescriptor::{FileDescriptor, Pipe}; +use std::sync::{Arc, Mutex}; +use winapi::um::wincon::COORD; + +#[derive(Default)] +pub struct ConPtySystem {} + +impl PtySystem for ConPtySystem { + fn openpty(&self, size: PtySize) -> anyhow::Result<PtyPair> { + 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<FileDescriptor>, + 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<Mutex<Inner>>, +} + +pub struct ConPtySlavePty { + inner: Arc<Mutex<Inner>>, +} + +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<PtySize, Error> { + let inner = self.inner.lock().unwrap(); + Ok(inner.size.clone()) + } + + fn try_clone_reader(&self) -> anyhow::Result<Box<dyn std::io::Read + Send>> { + Ok(Box::new(self.inner.lock().unwrap().readable.try_clone()?)) + } + + fn take_writer(&self) -> anyhow::Result<Box<dyn std::io::Write + Send>> { + 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<Box<dyn Child + Send + Sync>> { + let inner = self.inner.lock().unwrap(); + let child = inner.con.spawn_command(cmd)?; + Ok(Box::new(child)) + } +} diff --git a/vendor/portable-pty/src/win/mod.rs b/vendor/portable-pty/src/win/mod.rs new file mode 100644 index 00000000..24f8f536 --- /dev/null +++ b/vendor/portable-pty/src/win/mod.rs @@ -0,0 +1,149 @@ +use crate::{Child, ChildKiller, ExitStatus}; +use anyhow::Context as _; +use std::io::{Error as IoError, Result as IoResult}; +use std::os::windows::io::{AsRawHandle, RawHandle}; +use std::pin::Pin; +use std::sync::Mutex; +use std::task::{Context, 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; + +use filedescriptor::OwnedHandle; + +#[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) }; + 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<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) }; + let err = IoError::last_os_error(); + if res != 0 { + Err(err) + } 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) => { + struct PassRawHandleToWaiterThread(pub RawHandle); + unsafe impl Send for PassRawHandleToWaiterThread {} + + let proc = self.proc.lock().unwrap().try_clone()?; + let handle = PassRawHandleToWaiterThread(proc.as_raw_handle()); + + let waker = cx.waker().clone(); + std::thread::spawn(move || { + unsafe { + WaitForSingleObject(handle.0 as _, INFINITE); + } + waker.wake(); + }); + Poll::Pending + } + } + } +} diff --git a/vendor/portable-pty/src/win/procthreadattr.rs b/vendor/portable-pty/src/win/procthreadattr.rs new file mode 100644 index 00000000..ad97d582 --- /dev/null +++ b/vendor/portable-pty/src/win/procthreadattr.rs @@ -0,0 +1,72 @@ +use crate::win::psuedocon::HPCON; +use anyhow::{ensure, Error}; +use std::io::Error as IoError; +use std::{mem, ptr}; +use winapi::shared::minwindef::DWORD; +use winapi::um::processthreadsapi::*; + +const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE: usize = 0x00020016; + +pub struct ProcThreadAttributeList { + data: Vec<u8>, +} + +impl ProcThreadAttributeList { + pub fn with_capacity(num_attributes: DWORD) -> Result<Self, Error> { + 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); + // We have the right capacity, so force the vec to consider itself + // that length. The contents of those bytes will be maintained + // by the win32 apis used in this impl. + 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::<HPCON>(), + 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/vendor/portable-pty/src/win/psuedocon.rs b/vendor/portable-pty/src/win/psuedocon.rs new file mode 100644 index 00000000..133eab52 --- /dev/null +++ b/vendor/portable-pty/src/win/psuedocon.rs @@ -0,0 +1,168 @@ +use super::WinChild; +use crate::cmdbuilder::CommandBuilder; +use crate::win::procthreadattr::ProcThreadAttributeList; +use anyhow::{bail, ensure, Error}; +use filedescriptor::{FileDescriptor, OwnedHandle}; +use lazy_static::lazy_static; +use shared_library::shared_library; +use std::ffi::OsString; +use std::io::Error as IoError; +use std::os::windows::ffi::OsStringExt; +use std::os::windows::io::{AsRawHandle, FromRawHandle}; +use std::path::Path; +use std::sync::Mutex; +use std::{mem, ptr}; +use winapi::shared::minwindef::DWORD; +use winapi::shared::winerror::{HRESULT, S_OK}; +use winapi::um::handleapi::*; +use winapi::um::processthreadsapi::*; +use winapi::um::winbase::{ + CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT, STARTF_USESTDHANDLES, STARTUPINFOEXW, +}; +use winapi::um::wincon::COORD; +use winapi::um::winnt::HANDLE; + +pub type HPCON = HANDLE; + +pub const PSUEDOCONSOLE_INHERIT_CURSOR: DWORD = 0x1; +pub const PSEUDOCONSOLE_RESIZE_QUIRK: DWORD = 0x2; +pub const PSEUDOCONSOLE_WIN32_INPUT_MODE: DWORD = 0x4; +#[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 { + // If the kernel doesn't export these functions then their system is + // too old and we cannot run. + let kernel = ConPtyFuncs::open(Path::new("kernel32.dll")).expect( + "this system does not support conpty. Windows 10 October 2018 or newer is required", + ); + + 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<Self, Error> { + let mut con: HPCON = INVALID_HANDLE_VALUE; + let result = unsafe { + (CONPTY.CreatePseudoConsole)( + size, + input.as_raw_handle() as _, + output.as_raw_handle() as _, + PSUEDOCONSOLE_INHERIT_CURSOR + | PSEUDOCONSOLE_RESIZE_QUIRK + | PSEUDOCONSOLE_WIN32_INPUT_MODE, + &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<WinChild> { + let mut si: STARTUPINFOEXW = unsafe { mem::zeroed() }; + si.StartupInfo.cb = mem::size_of::<STARTUPINFOEXW>() as u32; + // Explicitly set the stdio handles as invalid handles otherwise + // we can end up with a weird state where the spawned process can + // inherit the explicitly redirected output handles from its parent. + // For example, when daemonizing wezterm-mux-server, the stdio handles + // are redirected to a log file and the spawned process would end up + // writing its output there instead of to the pty we just created. + 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) = cmd.cmdline()?; + let cmd_os = OsString::from_wide(&cmdline); + + let cwd = cmd.current_directory(); + + let res = unsafe { + CreateProcessW( + exe.as_mut_slice().as_mut_ptr(), + cmdline.as_mut_slice().as_mut_ptr(), + ptr::null_mut(), + ptr::null_mut(), + 0, + EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, + cmd.environment_block().as_mut_slice().as_mut_ptr() as *mut _, + cwd.as_ref() + .map(|c| c.as_slice().as_ptr()) + .unwrap_or(ptr::null()), + &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); + } + + // Make sure we close out the thread handle so we don't leak it; + // we do this simply by making it owned + 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), + }) + } +} |
