290 lines
9.3 KiB
Rust
290 lines
9.3 KiB
Rust
// Copyright 2016 Joe Wilm, The Alacritty Project Contributors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use super::{Pty, HANDLE};
|
|
|
|
use std::i16;
|
|
use std::io::Error;
|
|
use std::mem;
|
|
use std::os::windows::io::IntoRawHandle;
|
|
use std::ptr;
|
|
use std::sync::Arc;
|
|
|
|
use dunce::canonicalize;
|
|
use mio_anonymous_pipes::{EventedAnonRead, EventedAnonWrite};
|
|
use miow;
|
|
use widestring::U16CString;
|
|
use winapi::shared::basetsd::{PSIZE_T, SIZE_T};
|
|
use winapi::shared::minwindef::{BYTE, DWORD};
|
|
use winapi::shared::ntdef::{HANDLE, HRESULT, LPWSTR};
|
|
use winapi::shared::winerror::S_OK;
|
|
use winapi::um::libloaderapi::{GetModuleHandleA, GetProcAddress};
|
|
use winapi::um::processthreadsapi::{
|
|
CreateProcessW, InitializeProcThreadAttributeList, UpdateProcThreadAttribute,
|
|
PROCESS_INFORMATION, STARTUPINFOW,
|
|
};
|
|
use winapi::um::winbase::{EXTENDED_STARTUPINFO_PRESENT, STARTF_USESTDHANDLES, STARTUPINFOEXW};
|
|
use winapi::um::wincontypes::{COORD, HPCON};
|
|
|
|
use crate::cli::Options;
|
|
use crate::config::{Config, Shell};
|
|
use crate::display::OnResize;
|
|
use crate::term::SizeInfo;
|
|
|
|
/// Dynamically-loaded Pseudoconsole API from kernel32.dll
|
|
///
|
|
/// The field names are deliberately PascalCase as this matches
|
|
/// the defined symbols in kernel32 and also is the convention
|
|
/// that the `winapi` crate follows.
|
|
#[allow(non_snake_case)]
|
|
struct ConptyApi {
|
|
CreatePseudoConsole:
|
|
unsafe extern "system" fn(COORD, HANDLE, HANDLE, DWORD, *mut HPCON) -> HRESULT,
|
|
ResizePseudoConsole: unsafe extern "system" fn(HPCON, COORD) -> HRESULT,
|
|
ClosePseudoConsole: unsafe extern "system" fn(HPCON),
|
|
}
|
|
|
|
impl ConptyApi {
|
|
/// Load the API or None if it cannot be found.
|
|
pub fn new() -> Option<Self> {
|
|
// Unsafe because windows API calls
|
|
unsafe {
|
|
let hmodule = GetModuleHandleA("kernel32\0".as_ptr() as _);
|
|
assert!(!hmodule.is_null());
|
|
|
|
let cpc = GetProcAddress(hmodule, "CreatePseudoConsole\0".as_ptr() as _);
|
|
let rpc = GetProcAddress(hmodule, "ResizePseudoConsole\0".as_ptr() as _);
|
|
let clpc = GetProcAddress(hmodule, "ClosePseudoConsole\0".as_ptr() as _);
|
|
|
|
if cpc.is_null() || rpc.is_null() || clpc.is_null() {
|
|
None
|
|
} else {
|
|
Some(Self {
|
|
CreatePseudoConsole: mem::transmute(cpc),
|
|
ResizePseudoConsole: mem::transmute(rpc),
|
|
ClosePseudoConsole: mem::transmute(clpc),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// RAII Pseudoconsole
|
|
pub struct Conpty {
|
|
pub handle: HPCON,
|
|
api: ConptyApi,
|
|
}
|
|
|
|
/// Handle can be cloned freely and moved between threads.
|
|
pub type ConptyHandle = Arc<Conpty>;
|
|
|
|
impl Drop for Conpty {
|
|
fn drop(&mut self) {
|
|
unsafe { (self.api.ClosePseudoConsole)(self.handle) }
|
|
}
|
|
}
|
|
|
|
// The Conpty API can be accessed from multiple threads.
|
|
unsafe impl Send for Conpty {}
|
|
unsafe impl Sync for Conpty {}
|
|
|
|
pub fn new<'a>(
|
|
config: &Config,
|
|
options: &Options,
|
|
size: &SizeInfo,
|
|
_window_id: Option<usize>,
|
|
) -> Option<Pty<'a>> {
|
|
if !config.enable_experimental_conpty_backend() {
|
|
return None;
|
|
}
|
|
|
|
let api = ConptyApi::new()?;
|
|
|
|
let mut pty_handle = 0 as HPCON;
|
|
|
|
// 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();
|
|
|
|
let coord =
|
|
coord_from_sizeinfo(size).expect("Overflow when creating initial size on pseudoconsole");
|
|
|
|
// Create the Pseudo Console, using the pipes
|
|
let result = unsafe {
|
|
(api.CreatePseudoConsole)(
|
|
coord,
|
|
conin_pty_handle.into_raw_handle(),
|
|
conout_pty_handle.into_raw_handle(),
|
|
0,
|
|
&mut pty_handle as *mut HPCON,
|
|
)
|
|
};
|
|
|
|
assert!(result == S_OK);
|
|
|
|
let mut success;
|
|
|
|
// Prepare child process startup info
|
|
|
|
let mut size: SIZE_T = 0;
|
|
|
|
let mut startup_info_ex: STARTUPINFOEXW = Default::default();
|
|
|
|
let title = options.title.as_ref().map(String::as_str).unwrap_or("Alacritty");
|
|
let title = U16CString::from_str(title).unwrap();
|
|
startup_info_ex.StartupInfo.lpTitle = title.as_ptr() as LPWSTR;
|
|
|
|
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 PSIZE_T) > 0;
|
|
|
|
// This call was expected to return false.
|
|
if failure {
|
|
panic_shell_spawn();
|
|
}
|
|
}
|
|
|
|
let mut attr_list: Box<[BYTE]> = 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 PSIZE_T,
|
|
) > 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,
|
|
22 | 0x0002_0000, // PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
|
|
pty_handle,
|
|
mem::size_of::<HPCON>(),
|
|
ptr::null_mut(),
|
|
ptr::null_mut(),
|
|
) > 0;
|
|
|
|
if !success {
|
|
panic_shell_spawn();
|
|
}
|
|
}
|
|
|
|
// Get process commandline
|
|
let default_shell = &Shell::new("powershell");
|
|
let shell = config.shell().unwrap_or(default_shell);
|
|
let initial_command = options.command().unwrap_or(shell);
|
|
let mut cmdline = initial_command.args().to_vec();
|
|
cmdline.insert(0, initial_command.program().into());
|
|
|
|
// Warning, here be borrow hell
|
|
let cwd = options.working_dir.as_ref().map(|dir| canonicalize(dir).unwrap());
|
|
let cwd = cwd.as_ref().map(|dir| dir.to_str().unwrap());
|
|
|
|
// Create the client application, using startup info containing ConPTY info
|
|
let cmdline = U16CString::from_str(&cmdline.join(" ")).unwrap();
|
|
let cwd = cwd.map(|s| U16CString::from_str(&s).unwrap());
|
|
|
|
let mut proc_info: PROCESS_INFORMATION = Default::default();
|
|
unsafe {
|
|
success = CreateProcessW(
|
|
ptr::null(),
|
|
cmdline.as_ptr() as LPWSTR,
|
|
ptr::null_mut(),
|
|
ptr::null_mut(),
|
|
false as i32,
|
|
EXTENDED_STARTUPINFO_PRESENT,
|
|
ptr::null_mut(),
|
|
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();
|
|
}
|
|
}
|
|
|
|
// Store handle to console
|
|
unsafe {
|
|
HANDLE = proc_info.hProcess;
|
|
}
|
|
|
|
let conin = EventedAnonWrite::new(conin);
|
|
let conout = EventedAnonRead::new(conout);
|
|
|
|
let agent = Conpty { handle: pty_handle, api };
|
|
|
|
Some(Pty {
|
|
handle: super::PtyHandle::Conpty(ConptyHandle::new(agent)),
|
|
conout: super::EventedReadablePipe::Anonymous(conout),
|
|
conin: super::EventedWritablePipe::Anonymous(conin),
|
|
read_token: 0.into(),
|
|
write_token: 0.into(),
|
|
})
|
|
}
|
|
|
|
// Panic with the last os error as message
|
|
fn panic_shell_spawn() {
|
|
panic!("Unable to spawn shell: {}", Error::last_os_error());
|
|
}
|
|
|
|
impl OnResize for ConptyHandle {
|
|
fn on_resize(&mut self, sizeinfo: &SizeInfo) {
|
|
if let Some(coord) = coord_from_sizeinfo(sizeinfo) {
|
|
let result = unsafe { (self.api.ResizePseudoConsole)(self.handle, coord) };
|
|
assert!(result == S_OK);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper to build a COORD from a SizeInfo, returing None in overflow cases.
|
|
fn coord_from_sizeinfo(sizeinfo: &SizeInfo) -> Option<COORD> {
|
|
let cols = sizeinfo.cols().0;
|
|
let lines = sizeinfo.lines().0;
|
|
|
|
if cols <= i16::MAX as usize && lines <= i16::MAX as usize {
|
|
Some(COORD { X: sizeinfo.cols().0 as i16, Y: sizeinfo.lines().0 as i16 })
|
|
} else {
|
|
None
|
|
}
|
|
}
|