feat(windows): port Ghostty's Windows code paths
Ports the Windows branches of the Zig code behind cfg(windows): ConPTY pty (CreatePseudoConsole, pipes, resize), CreateProcessW with the pseudo-console attribute list, env blocks and command-line quoting, a pipe read thread with CancelIoEx and a WaitForMultipleObjects IO loop, plus os helpers (home dir, env, paths, temp dirs, hostname, XDG via LOCALAPPDATA) and config/input/terminal/crash Windows branches. Compile-checked with `cargo check --target x86_64-pc-windows-gnu` for vt, unicode, datastruct, input, os, config, terminal and termio (setup in docs/rewrite/notes/windows-WIN.md); cannot run here. Platform-independent pieces (quoting with a CommandLineToArgvW round-trip, env block encoding) have unit tests. Test Plan: - cargo test -p ghostty-termio windows_args - cargo check --target x86_64-pc-windows-gnu -p ghostty-termio
This commit is contained in:
1 parent
42cb8796da
commit
3c8fae1089
8 files changed
+2402
No files matched your search
@@ -0,0 +1,348 @@
|
||||
//! Windows PTY creation and management (`WindowsPty` in `pty.zig`): a ConPTY
|
||||
//! pseudo console (`CreatePseudoConsole`) connected to a pair of pipes.
|
||||
//!
|
||||
//! The Zig struct has `in_pipe`/`out_pipe` (our ends) and
|
||||
//! `in_pipe_pty`/`out_pipe_pty` (the ends handed to the pseudo console). All
|
||||
//! handles are `OwnedHandle`s here and are closed on drop, in the same order
|
||||
//! as `WindowsPty.deinit` (pipes first, the pseudo console last).
|
||||
|
||||
// Win32 FFI takes out-parameters as `*mut`; implicit `&mut` -> pointer coercions
|
||||
// are the idiomatic spelling at these call sites.
|
||||
#![allow(clippy::borrow_as_ptr)]
|
||||
|
||||
use std::{
|
||||
io,
|
||||
os::windows::io::{AsHandle, BorrowedHandle, FromRawHandle, OwnedHandle, RawHandle},
|
||||
ptr,
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{
|
||||
FALSE,
|
||||
GENERIC_READ,
|
||||
HANDLE,
|
||||
HANDLE_FLAG_INHERIT,
|
||||
INVALID_HANDLE_VALUE,
|
||||
S_OK,
|
||||
SetHandleInformation,
|
||||
},
|
||||
Security::SECURITY_ATTRIBUTES,
|
||||
Storage::FileSystem::{
|
||||
CreateFileW,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
FILE_FLAG_FIRST_PIPE_INSTANCE,
|
||||
FILE_FLAG_OVERLAPPED,
|
||||
OPEN_EXISTING,
|
||||
PIPE_ACCESS_OUTBOUND,
|
||||
},
|
||||
System::{
|
||||
Console::{COORD, ClosePseudoConsole, CreatePseudoConsole, HPCON, ResizePseudoConsole},
|
||||
Pipes::{CreateNamedPipeW, CreatePipe, PIPE_TYPE_BYTE},
|
||||
Threading::GetCurrentProcessId,
|
||||
},
|
||||
};
|
||||
|
||||
/// Redeclare the winsize struct so we can just use a Rust struct. The
|
||||
/// defaults on this are some reasonable screen size but you should probably
|
||||
/// not use them.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Winsize {
|
||||
pub ws_row: u16,
|
||||
pub ws_col: u16,
|
||||
pub ws_xpixel: u16,
|
||||
pub ws_ypixel: u16,
|
||||
}
|
||||
|
||||
impl Default for Winsize {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ws_row: 100,
|
||||
ws_col: 80,
|
||||
ws_xpixel: 800,
|
||||
ws_ypixel: 600,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The modes of a pty. Not all of these modes are supported on
|
||||
/// all platforms but all platforms share the same mode struct.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Mode {
|
||||
/// ICANON on POSIX
|
||||
pub canonical: bool,
|
||||
|
||||
/// ECHO on POSIX
|
||||
pub echo: bool,
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
canonical: true,
|
||||
echo: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// A Win32 call failed (`windows.unexpectedError`); carries the
|
||||
/// `GetLastError` code, or the failing `HRESULT` for ConPTY calls.
|
||||
#[error("unexpected windows error {0:#x}")]
|
||||
Unexpected(u32),
|
||||
#[error("resize failed")]
|
||||
ResizeFailed,
|
||||
}
|
||||
|
||||
/// Process-wide counter for pipe names.
|
||||
static PIPE_NAME_COUNTER: AtomicU32 = AtomicU32::new(1);
|
||||
|
||||
/// The pseudo console handle; closed (after the pipes) on drop.
|
||||
#[derive(Debug)]
|
||||
struct PseudoConsole(HPCON);
|
||||
|
||||
impl Drop for PseudoConsole {
|
||||
#[allow(unsafe_code)]
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: the handle came from a successful CreatePseudoConsole and
|
||||
// is closed exactly once, here.
|
||||
unsafe { ClosePseudoConsole(self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
/// A ConPTY pseudo console with its pipes.
|
||||
///
|
||||
/// Field order is the drop order: pipes first, the pseudo console last.
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct Pty {
|
||||
// The pseudo console's ends of the pipes are only held to keep them open.
|
||||
#[allow(dead_code)]
|
||||
in_pipe_pty: OwnedHandle,
|
||||
/// Our write end (named pipe, overlapped) for input to the child.
|
||||
in_pipe: OwnedHandle,
|
||||
#[allow(dead_code)]
|
||||
out_pipe_pty: OwnedHandle,
|
||||
/// Our read end for output from the child.
|
||||
out_pipe: OwnedHandle,
|
||||
pseudo_console: PseudoConsole,
|
||||
size: Winsize,
|
||||
}
|
||||
|
||||
fn last_error() -> Error {
|
||||
Error::Unexpected(io::Error::last_os_error().raw_os_error().unwrap_or(0) as u32)
|
||||
}
|
||||
|
||||
/// The unique named pipe path for the input pipe, as UTF-16 with a NUL.
|
||||
fn pipe_path(pid: u32, counter: u32) -> Vec<u16> {
|
||||
format!("\\\\.\\pipe\\LOCAL\\ghostty-pty-{pid}-{counter}\0")
|
||||
.encode_utf16()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn coord(size: Winsize) -> COORD {
|
||||
COORD {
|
||||
X: size.ws_col.min(i16::MAX as u16) as i16,
|
||||
Y: size.ws_row.min(i16::MAX as u16) as i16,
|
||||
}
|
||||
}
|
||||
|
||||
impl Pty {
|
||||
/// Open a new PTY with the given initial size.
|
||||
#[allow(unsafe_code)]
|
||||
pub fn open(size: Winsize) -> Result<Pty, Error> {
|
||||
let pipe_path_w = pipe_path(
|
||||
// SAFETY: no preconditions.
|
||||
unsafe { GetCurrentProcessId() },
|
||||
PIPE_NAME_COUNTER.fetch_add(1, Ordering::Relaxed),
|
||||
);
|
||||
|
||||
let security_attributes = SECURITY_ATTRIBUTES {
|
||||
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
bInheritHandle: FALSE,
|
||||
lpSecurityDescriptor: ptr::null_mut(),
|
||||
};
|
||||
|
||||
// The in_pipe needs to be created as a named pipe, since anonymous
|
||||
// pipes created with CreatePipe do not support overlapped operations
|
||||
// (which Zig's libxev IOCP backend needs; we keep the same shape).
|
||||
//
|
||||
// SAFETY: the path is NUL terminated and the attributes outlive the
|
||||
// call.
|
||||
let in_pipe = unsafe {
|
||||
CreateNamedPipeW(
|
||||
pipe_path_w.as_ptr(),
|
||||
PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE | FILE_FLAG_OVERLAPPED,
|
||||
PIPE_TYPE_BYTE,
|
||||
1,
|
||||
4096,
|
||||
4096,
|
||||
0,
|
||||
&security_attributes,
|
||||
)
|
||||
};
|
||||
if in_pipe == INVALID_HANDLE_VALUE {
|
||||
return Err(last_error());
|
||||
}
|
||||
// SAFETY: valid handle we own.
|
||||
let in_pipe = unsafe { OwnedHandle::from_raw_handle(in_pipe as RawHandle) };
|
||||
|
||||
// SAFETY: as above.
|
||||
let in_pipe_pty = unsafe {
|
||||
CreateFileW(
|
||||
pipe_path_w.as_ptr(),
|
||||
GENERIC_READ,
|
||||
0,
|
||||
&security_attributes,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if in_pipe_pty == INVALID_HANDLE_VALUE {
|
||||
return Err(last_error());
|
||||
}
|
||||
// SAFETY: valid handle we own.
|
||||
let in_pipe_pty = unsafe { OwnedHandle::from_raw_handle(in_pipe_pty as RawHandle) };
|
||||
|
||||
let mut out_pipe: HANDLE = ptr::null_mut();
|
||||
let mut out_pipe_pty: HANDLE = ptr::null_mut();
|
||||
// SAFETY: both out pointers are valid for writes.
|
||||
if unsafe { CreatePipe(&mut out_pipe, &mut out_pipe_pty, ptr::null(), 0) } == 0 {
|
||||
return Err(last_error());
|
||||
}
|
||||
// SAFETY: CreatePipe succeeded: two valid handles we own.
|
||||
let (out_pipe, out_pipe_pty) = unsafe {
|
||||
(
|
||||
OwnedHandle::from_raw_handle(out_pipe as RawHandle),
|
||||
OwnedHandle::from_raw_handle(out_pipe_pty as RawHandle),
|
||||
)
|
||||
};
|
||||
|
||||
for h in [&in_pipe, &in_pipe_pty, &out_pipe, &out_pipe_pty] {
|
||||
set_not_inheritable(h.as_handle())?;
|
||||
}
|
||||
|
||||
let mut hpc: HPCON = 0;
|
||||
// SAFETY: the pipe handles are valid; `hpc` is a valid out pointer.
|
||||
let result = unsafe {
|
||||
CreatePseudoConsole(
|
||||
coord(size),
|
||||
raw(&in_pipe_pty),
|
||||
raw(&out_pipe_pty),
|
||||
0,
|
||||
&mut hpc,
|
||||
)
|
||||
};
|
||||
if result != S_OK {
|
||||
return Err(Error::Unexpected(result as u32));
|
||||
}
|
||||
|
||||
Ok(Pty {
|
||||
in_pipe_pty,
|
||||
in_pipe,
|
||||
out_pipe_pty,
|
||||
out_pipe,
|
||||
pseudo_console: PseudoConsole(hpc),
|
||||
size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the size of the pty.
|
||||
pub fn get_size(&self) -> Result<Winsize, Error> {
|
||||
Ok(self.size)
|
||||
}
|
||||
|
||||
/// Set the size of the pty.
|
||||
#[allow(unsafe_code)]
|
||||
pub fn set_size(&mut self, size: Winsize) -> Result<(), Error> {
|
||||
// SAFETY: the pseudo console handle is valid until drop.
|
||||
let result = unsafe { ResizePseudoConsole(self.pseudo_console.0, coord(size)) };
|
||||
if result != S_OK {
|
||||
return Err(Error::ResizeFailed);
|
||||
}
|
||||
self.size = size;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The pseudo console to attach a child process to (`Command.pseudo_console`).
|
||||
pub fn pseudo_console(&self) -> HPCON {
|
||||
self.pseudo_console.0
|
||||
}
|
||||
|
||||
/// Our read end: output of the child.
|
||||
pub fn out_pipe(&self) -> BorrowedHandle<'_> {
|
||||
self.out_pipe.as_handle()
|
||||
}
|
||||
|
||||
/// Our write end: input to the child (opened for overlapped I/O).
|
||||
pub fn in_pipe(&self) -> BorrowedHandle<'_> {
|
||||
self.in_pipe.as_handle()
|
||||
}
|
||||
}
|
||||
|
||||
fn raw(h: &OwnedHandle) -> HANDLE {
|
||||
std::os::windows::io::AsRawHandle::as_raw_handle(h) as HANDLE
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn set_not_inheritable(h: BorrowedHandle<'_>) -> Result<(), Error> {
|
||||
// SAFETY: the handle is valid for the duration of the call.
|
||||
if unsafe {
|
||||
SetHandleInformation(
|
||||
std::os::windows::io::AsRawHandle::as_raw_handle(&h) as HANDLE,
|
||||
HANDLE_FLAG_INHERIT,
|
||||
0,
|
||||
)
|
||||
} == 0
|
||||
{
|
||||
return Err(last_error());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pty_size() {
|
||||
let mut ws = Winsize {
|
||||
ws_row: 50,
|
||||
ws_col: 80,
|
||||
ws_xpixel: 1,
|
||||
ws_ypixel: 1,
|
||||
};
|
||||
|
||||
let mut pty = Pty::open(ws).unwrap();
|
||||
|
||||
// Initialize size should match what we gave it
|
||||
assert_eq!(ws, pty.get_size().unwrap());
|
||||
|
||||
// Can set and read new sizes
|
||||
ws.ws_row *= 2;
|
||||
pty.set_size(ws).unwrap();
|
||||
assert_eq!(ws, pty.get_size().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipe_names_are_unique_and_nul_terminated() {
|
||||
let a = pipe_path(7, 1);
|
||||
let b = pipe_path(7, 2);
|
||||
assert_ne!(a, b);
|
||||
assert_eq!(a.last(), Some(&0));
|
||||
assert!(String::from_utf16_lossy(&a).starts_with("\\\\.\\pipe\\LOCAL\\ghostty-pty-7-1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coord_clamps() {
|
||||
let c = coord(Winsize {
|
||||
ws_row: 50,
|
||||
ws_col: u16::MAX,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
});
|
||||
assert_eq!((c.X, c.Y), (i16::MAX, 50));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user