aboutsummaryrefslogtreecommitdiff
path: root/vendor/portable-pty/src/win
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-23 21:43:23 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-23 21:43:23 +0300
commita66f4c64e9d1591f042a94d6f3bef3b4298a4d1c (patch)
tree747de08b6ada4300afb4124da9feff0cc66b96a7 /vendor/portable-pty/src/win
parent5981ba426e8ed8f6785195a03cc0d8a190173e61 (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.rs117
-rw-r--r--vendor/portable-pty/src/win/mod.rs149
-rw-r--r--vendor/portable-pty/src/win/procthreadattr.rs72
-rw-r--r--vendor/portable-pty/src/win/psuedocon.rs168
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),
+ })
+ }
+}