alacritty/alacritty_terminal/src/tty/windows/conpty.rs

322 lines
11 KiB
Rust

use log::{info, warn};
use std::collections::{HashMap, HashSet};
use std::ffi::OsStr;
use std::io::Error;
use std::os::windows::ffi::OsStrExt;
use std::os::windows::io::IntoRawHandle;
use std::{mem, ptr};
use windows_sys::core::{HRESULT, PWSTR};
use windows_sys::Win32::Foundation::{HANDLE, S_OK};
use windows_sys::Win32::System::Console::{
ClosePseudoConsole, CreatePseudoConsole, ResizePseudoConsole, COORD, HPCON,
};
use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW};
use windows_sys::{s, w};
use windows_sys::Win32::System::Threading::{
CreateProcessW, InitializeProcThreadAttributeList, UpdateProcThreadAttribute,
CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT, PROCESS_INFORMATION,
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, STARTF_USESTDHANDLES, STARTUPINFOEXW, STARTUPINFOW,
};
use crate::event::{OnResize, WindowSize};
use crate::tty::windows::blocking::{UnblockedReader, UnblockedWriter};
use crate::tty::windows::child::ChildExitWatcher;
use crate::tty::windows::{cmdline, win32_string, Pty};
use crate::tty::Options;
const PIPE_CAPACITY: usize = crate::event_loop::READ_BUFFER_SIZE;
/// Load the pseudoconsole API from conpty.dll if possible, otherwise use the
/// standard Windows API.
///
/// The conpty.dll from the Windows Terminal project
/// supports loading OpenConsole.exe, which offers many improvements and
/// bugfixes compared to the standard conpty that ships with Windows.
///
/// The conpty.dll and OpenConsole.exe files will be searched in PATH and in
/// the directory where Alacritty's executable is located.
type CreatePseudoConsoleFn =
unsafe extern "system" fn(COORD, HANDLE, HANDLE, u32, *mut HPCON) -> HRESULT;
type ResizePseudoConsoleFn = unsafe extern "system" fn(HPCON, COORD) -> HRESULT;
type ClosePseudoConsoleFn = unsafe extern "system" fn(HPCON);
struct ConptyApi {
create: CreatePseudoConsoleFn,
resize: ResizePseudoConsoleFn,
close: ClosePseudoConsoleFn,
}
impl ConptyApi {
fn new() -> Self {
match Self::load_conpty() {
Some(conpty) => {
info!("Using conpty.dll for pseudoconsole");
conpty
},
None => {
// Cannot load conpty.dll - use the standard Windows API.
info!("Using Windows API for pseudoconsole");
Self {
create: CreatePseudoConsole,
resize: ResizePseudoConsole,
close: ClosePseudoConsole,
}
},
}
}
/// Try loading ConptyApi from conpty.dll library.
fn load_conpty() -> Option<Self> {
type LoadedFn = unsafe extern "system" fn() -> isize;
unsafe {
let hmodule = LoadLibraryW(w!("conpty.dll"));
if hmodule == 0 {
return None;
}
let create_fn = GetProcAddress(hmodule, s!("CreatePseudoConsole"))?;
let resize_fn = GetProcAddress(hmodule, s!("ResizePseudoConsole"))?;
let close_fn = GetProcAddress(hmodule, s!("ClosePseudoConsole"))?;
Some(Self {
create: mem::transmute::<LoadedFn, CreatePseudoConsoleFn>(create_fn),
resize: mem::transmute::<LoadedFn, ResizePseudoConsoleFn>(resize_fn),
close: mem::transmute::<LoadedFn, ClosePseudoConsoleFn>(close_fn),
})
}
}
}
/// RAII Pseudoconsole.
pub struct Conpty {
pub handle: HPCON,
api: ConptyApi,
}
impl Drop for Conpty {
fn drop(&mut self) {
// XXX: This will block until the conout pipe is drained. Will cause a deadlock if the
// conout pipe has already been dropped by this point.
//
// See PR #3084 and https://docs.microsoft.com/en-us/windows/console/closepseudoconsole.
unsafe { (self.api.close)(self.handle) }
}
}
// The ConPTY handle can be sent between threads.
unsafe impl Send for Conpty {}
pub fn new(config: &Options, window_size: WindowSize) -> Option<Pty> {
let api = ConptyApi::new();
let mut pty_handle: HPCON = 0;
// Passing 0 as the size parameter allows the "system default" buffer
// size to be used. There may be small performance and memory advantages
// to be gained by tuning this in the future, but it's likely a reasonable
// start point.
let (conout, conout_pty_handle) = miow::pipe::anonymous(0).unwrap();
let (conin_pty_handle, conin) = miow::pipe::anonymous(0).unwrap();
// Create the Pseudo Console, using the pipes.
let result = unsafe {
(api.create)(
window_size.into(),
conin_pty_handle.into_raw_handle() as HANDLE,
conout_pty_handle.into_raw_handle() as HANDLE,
0,
&mut pty_handle as *mut _,
)
};
assert_eq!(result, S_OK);
let mut success;
// Prepare child process startup info.
let mut size: usize = 0;
let mut startup_info_ex: STARTUPINFOEXW = unsafe { mem::zeroed() };
startup_info_ex.StartupInfo.lpTitle = std::ptr::null_mut() as PWSTR;
startup_info_ex.StartupInfo.cb = mem::size_of::<STARTUPINFOEXW>() as u32;
// Setting this flag but leaving all the handles as default (null) ensures the
// PTY process does not inherit any handles from this Alacritty process.
startup_info_ex.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
// Create the appropriately sized thread attribute list.
unsafe {
let failure =
InitializeProcThreadAttributeList(ptr::null_mut(), 1, 0, &mut size as *mut usize) > 0;
// This call was expected to return false.
if failure {
panic_shell_spawn();
}
}
let mut attr_list: Box<[u8]> = vec![0; size].into_boxed_slice();
// Set startup info's attribute list & initialize it
//
// Lint failure is spurious; it's because winapi's definition of PROC_THREAD_ATTRIBUTE_LIST
// implies it is one pointer in size (32 or 64 bits) but really this is just a dummy value.
// Casting a *mut u8 (pointer to 8 bit type) might therefore not be aligned correctly in
// the compiler's eyes.
#[allow(clippy::cast_ptr_alignment)]
{
startup_info_ex.lpAttributeList = attr_list.as_mut_ptr() as _;
}
unsafe {
success = InitializeProcThreadAttributeList(
startup_info_ex.lpAttributeList,
1,
0,
&mut size as *mut usize,
) > 0;
if !success {
panic_shell_spawn();
}
}
// Set thread attribute list's Pseudo Console to the specified ConPTY.
unsafe {
success = UpdateProcThreadAttribute(
startup_info_ex.lpAttributeList,
0,
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE as usize,
pty_handle as *mut std::ffi::c_void,
mem::size_of::<HPCON>(),
ptr::null_mut(),
ptr::null_mut(),
) > 0;
if !success {
panic_shell_spawn();
}
}
// Prepare child process creation arguments.
let cmdline = win32_string(&cmdline(config));
let cwd = config.working_directory.as_ref().map(win32_string);
let mut creation_flags = EXTENDED_STARTUPINFO_PRESENT;
let custom_env_block = convert_custom_env(&config.env);
let custom_env_block_pointer = match &custom_env_block {
Some(custom_env_block) => {
creation_flags |= CREATE_UNICODE_ENVIRONMENT;
custom_env_block.as_ptr() as *mut std::ffi::c_void
},
None => ptr::null_mut(),
};
let mut proc_info: PROCESS_INFORMATION = unsafe { mem::zeroed() };
unsafe {
success = CreateProcessW(
ptr::null(),
cmdline.as_ptr() as PWSTR,
ptr::null_mut(),
ptr::null_mut(),
false as i32,
creation_flags,
custom_env_block_pointer,
cwd.as_ref().map_or_else(ptr::null, |s| s.as_ptr()),
&mut startup_info_ex.StartupInfo as *mut STARTUPINFOW,
&mut proc_info as *mut PROCESS_INFORMATION,
) > 0;
if !success {
panic_shell_spawn();
}
}
let conin = UnblockedWriter::new(conin, PIPE_CAPACITY);
let conout = UnblockedReader::new(conout, PIPE_CAPACITY);
let child_watcher = ChildExitWatcher::new(proc_info.hProcess).unwrap();
let conpty = Conpty { handle: pty_handle as HPCON, api };
Some(Pty::new(conpty, conout, conin, child_watcher))
}
// Windows environment variables are case-insensitive, and the caller is responsible for
// deduplicating environment variables, so do that here while converting.
//
// https://learn.microsoft.com/en-us/previous-versions/troubleshoot/windows/win32/createprocess-cannot-eliminate-duplicate-variables#environment-variables
fn convert_custom_env(custom_env: &HashMap<String, String>) -> Option<Vec<u16>> {
// Windows inherits parent's env when no `lpEnvironment` parameter is specified.
if custom_env.is_empty() {
return None;
}
let mut converted_block = Vec::new();
let mut all_env_keys = HashSet::new();
for (custom_key, custom_value) in custom_env {
let custom_key_os = OsStr::new(custom_key);
if all_env_keys.insert(custom_key_os.to_ascii_uppercase()) {
add_windows_env_key_value_to_block(
&mut converted_block,
custom_key_os,
OsStr::new(&custom_value),
);
} else {
warn!(
"Omitting environment variable pair with duplicate key: \
'{custom_key}={custom_value}'"
);
}
}
// Pull the current process environment after, to avoid overwriting the user provided one.
for (inherited_key, inherited_value) in std::env::vars_os() {
if all_env_keys.insert(inherited_key.to_ascii_uppercase()) {
add_windows_env_key_value_to_block(
&mut converted_block,
&inherited_key,
&inherited_value,
);
}
}
converted_block.push(0);
Some(converted_block)
}
// According to the `lpEnvironment` parameter description:
// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa#parameters
//
// > An environment block consists of a null-terminated block of null-terminated strings. Each
// string is in the following form:
// >
// > name=value\0
fn add_windows_env_key_value_to_block(block: &mut Vec<u16>, key: &OsStr, value: &OsStr) {
block.extend(key.encode_wide());
block.push('=' as u16);
block.extend(value.encode_wide());
block.push(0);
}
// Panic with the last os error as message.
fn panic_shell_spawn() {
panic!("Unable to spawn shell: {}", Error::last_os_error());
}
impl OnResize for Conpty {
fn on_resize(&mut self, window_size: WindowSize) {
let result = unsafe { (self.api.resize)(self.handle, window_size.into()) };
assert_eq!(result, S_OK);
}
}
impl From<WindowSize> for COORD {
fn from(window_size: WindowSize) -> Self {
let lines = window_size.num_lines;
let columns = window_size.num_cols;
COORD { X: columns as i16, Y: lines as i16 }
}
}