#![cfg(windows)]
use std::ffi::{OsStr, OsString};
use std::io::{self, Read, Write};
use std::os::windows::ffi::OsStrExt;
use std::os::windows::io::{AsRawHandle, FromRawHandle, IntoRawHandle, OwnedHandle, RawHandle};
use std::path::Path;
use std::sync::{Arc, Mutex};
use windows_sys::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE};
use windows_sys::Win32::System::Console::COORD;
use windows_sys::Win32::System::Threading::{
CreateProcessW, CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT, PROCESS_INFORMATION,
STARTF_USESTDHANDLES, STARTUPINFOEXW,
};
pub(super) const PSEUDOCONSOLE_PASSTHROUGH_MODE: u32 = 0x8;
pub(in crate::pty) mod child;
#[cfg(feature = "client")]
pub(super) mod conpty_acquire;
pub(super) mod conpty_api;
#[cfg(feature = "client")]
pub(super) mod conpty_sidecar_hashes;
pub(super) mod pipes;
pub(super) mod proc_thread_attr;
pub(super) mod pseudoconsole;
pub(super) mod win_version;
use child::ConPtyChild;
use pipes::{create_pipe, PipeDirection};
use proc_thread_attr::ProcThreadAttributeList;
use pseudoconsole::PseudoConsole;
#[derive(Debug, Clone, Copy)]
pub struct PtySize {
pub rows: u16,
pub cols: u16,
pub pixel_width: u16,
pub pixel_height: u16,
}
impl From<PtySize> for COORD {
fn from(size: PtySize) -> Self {
COORD {
X: size.cols as i16,
Y: size.rows as i16,
}
}
}
pub(super) struct ConPtyPair {
pub master: ConPtyMaster,
pub slave: ConPtySlave,
}
pub(crate) struct ConPtyMaster {
pseudo_console: Arc<Mutex<PseudoConsole>>,
reader: Option<OwnedHandle>,
writer: Option<OwnedHandle>,
current_size: Mutex<PtySize>,
}
impl ConPtyMaster {
pub(super) fn try_clone_reader(&mut self) -> io::Result<Box<dyn Read + Send>> {
let handle = self
.reader
.take()
.ok_or_else(|| io::Error::other("ConPtyMaster reader already taken"))?;
Ok(Box::new(HandleReader::new(handle)))
}
pub(super) fn take_writer(&mut self) -> io::Result<Box<dyn Write + Send>> {
let handle = self
.writer
.take()
.ok_or_else(|| io::Error::other("ConPtyMaster writer already taken"))?;
Ok(Box::new(HandleWriter::new(handle)))
}
pub(super) fn resize(&self, size: PtySize) -> io::Result<()> {
let pc = self
.pseudo_console
.lock()
.expect("conpty pseudo-console mutex poisoned");
pc.resize(size.into())?;
*self
.current_size
.lock()
.expect("conpty size mutex poisoned") = size;
Ok(())
}
pub(super) fn get_size(&self) -> PtySize {
*self
.current_size
.lock()
.expect("conpty size mutex poisoned")
}
}
pub(crate) struct ConPtySlave {
pseudo_console: Arc<Mutex<PseudoConsole>>,
}
impl ConPtySlave {
pub(super) fn spawn(
self,
argv: &[OsString],
cwd: Option<&Path>,
env: Option<&[(OsString, OsString)]>,
) -> io::Result<ConPtyChild> {
if argv.is_empty() {
return Err(io::Error::other("conpty spawn requires non-empty argv"));
}
let cmdline = build_command_line(argv)?;
let mut cmdline_w: Vec<u16> = OsStr::new(&cmdline).encode_wide().collect();
cmdline_w.push(0);
let cwd_w: Option<Vec<u16>> = cwd.map(|p| {
let mut v: Vec<u16> = p.as_os_str().encode_wide().collect();
v.push(0);
v
});
let cwd_ptr = cwd_w
.as_ref()
.map(|v| v.as_ptr())
.unwrap_or(std::ptr::null());
let env_block: Option<Vec<u16>> = env.map(build_env_block);
let env_ptr = env_block
.as_ref()
.map(|v| v.as_ptr() as *mut std::ffi::c_void)
.unwrap_or(std::ptr::null_mut());
let hpc_handle = self
.pseudo_console
.lock()
.expect("conpty pseudo-console mutex poisoned")
.as_handle();
let mut attr_list = ProcThreadAttributeList::with_pseudoconsole(hpc_handle)?;
let mut si: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
si.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() 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;
si.lpAttributeList = attr_list.as_mut_ptr();
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT;
let ok = unsafe {
CreateProcessW(
std::ptr::null(),
cmdline_w.as_mut_ptr(),
std::ptr::null(),
std::ptr::null(),
0, flags,
env_ptr,
cwd_ptr,
&si.StartupInfo,
&mut pi,
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
let process = unsafe { OwnedHandle::from_raw_handle(pi.hProcess as RawHandle) };
let main_thread = unsafe { OwnedHandle::from_raw_handle(pi.hThread as RawHandle) };
Ok(ConPtyChild::new(process, main_thread))
}
}
pub(super) fn openpty(size: PtySize) -> io::Result<ConPtyPair> {
let stdin_pipe = create_pipe(PipeDirection::HostWriteChildRead)?;
let stdout_pipe = create_pipe(PipeDirection::HostReadChildWrite)?;
let pseudo_console = PseudoConsole::new(
size.into(),
owned_to_handle(&stdin_pipe.child),
owned_to_handle(&stdout_pipe.child),
)?;
drop(stdin_pipe.child);
drop(stdout_pipe.child);
let pseudo_console = Arc::new(Mutex::new(pseudo_console));
let master = ConPtyMaster {
pseudo_console: Arc::clone(&pseudo_console),
reader: Some(stdout_pipe.host),
writer: Some(stdin_pipe.host),
current_size: Mutex::new(size),
};
let slave = ConPtySlave { pseudo_console };
Ok(ConPtyPair { master, slave })
}
fn owned_to_handle(handle: &OwnedHandle) -> HANDLE {
handle.as_raw_handle() as HANDLE
}
fn build_command_line(argv: &[OsString]) -> io::Result<OsString> {
let mut out = OsString::new();
for (i, arg) in argv.iter().enumerate() {
if i > 0 {
out.push(" ");
}
out.push(quote_argument(arg)?);
}
Ok(out)
}
fn quote_argument(arg: &OsStr) -> io::Result<OsString> {
let wide: Vec<u16> = arg.encode_wide().collect();
if wide.contains(&0) {
return Err(io::Error::other(
"argv element contains a NUL byte; cannot pass to CreateProcessW",
));
}
if wide.is_empty() {
return Ok(OsString::from("\"\""));
}
let needs_quoting = wide.iter().any(|&c| {
c == b' ' as u16 || c == b'\t' as u16 || c == b'\n' as u16 || c == b'"' as u16 || c == 0x0B
});
if !needs_quoting {
let s: OsString = std::os::windows::ffi::OsStringExt::from_wide(&wide);
return Ok(s);
}
let mut out_w: Vec<u16> = Vec::with_capacity(wide.len() + 2);
out_w.push(b'"' as u16);
let mut backslashes = 0usize;
for &c in &wide {
if c == b'\\' as u16 {
backslashes += 1;
out_w.push(c);
} else if c == b'"' as u16 {
for _ in 0..backslashes {
out_w.push(b'\\' as u16);
}
backslashes = 0;
out_w.push(b'\\' as u16);
out_w.push(b'"' as u16);
} else {
backslashes = 0;
out_w.push(c);
}
}
for _ in 0..backslashes {
out_w.push(b'\\' as u16);
}
out_w.push(b'"' as u16);
Ok(std::os::windows::ffi::OsStringExt::from_wide(&out_w))
}
fn build_env_block(env: &[(OsString, OsString)]) -> Vec<u16> {
let mut out: Vec<u16> = Vec::new();
for (k, v) in env {
for c in k.encode_wide() {
out.push(c);
}
out.push(b'=' as u16);
for c in v.encode_wide() {
out.push(c);
}
out.push(0);
}
out.push(0);
out
}
struct HandleReader {
handle: OwnedHandle,
}
impl HandleReader {
fn new(handle: OwnedHandle) -> Self {
Self { handle }
}
}
impl Read for HandleReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
use windows_sys::Win32::Storage::FileSystem::ReadFile;
let mut read: u32 = 0;
let ok = unsafe {
ReadFile(
self.handle.as_raw_handle() as HANDLE,
buf.as_mut_ptr(),
buf.len() as u32,
&mut read,
std::ptr::null_mut(),
)
};
if ok == 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(109) {
return Ok(0);
}
return Err(err);
}
Ok(read as usize)
}
}
struct HandleWriter {
handle: OwnedHandle,
}
impl HandleWriter {
fn new(handle: OwnedHandle) -> Self {
Self { handle }
}
}
impl Write for HandleWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
use windows_sys::Win32::Storage::FileSystem::WriteFile;
let mut written: u32 = 0;
let ok = unsafe {
WriteFile(
self.handle.as_raw_handle() as HANDLE,
buf.as_ptr(),
buf.len() as u32,
&mut written,
std::ptr::null_mut(),
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(written as usize)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[allow(dead_code)]
fn _silence_unused_into_raw_handle(h: OwnedHandle) -> RawHandle {
h.into_raw_handle()
}