diff options
-rw-r--r-- | CHANGELOG.md | 3 | ||||
-rw-r--r-- | Cargo.lock | 53 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | alacritty_windows.yml | 11 | ||||
-rw-r--r-- | src/config.rs | 13 | ||||
-rw-r--r-- | src/lib.rs | 13 | ||||
-rw-r--r-- | src/main.rs | 6 | ||||
-rw-r--r-- | src/tty/windows/conpty.rs | 303 | ||||
-rw-r--r-- | src/tty/windows/mod.rs | 334 | ||||
-rw-r--r-- | src/tty/windows/winpty.rs (renamed from src/tty/windows.rs) | 200 |
10 files changed, 759 insertions, 182 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d46d65..df645133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New configuration field `visual_bell.color` allows changing the visual bell color - Crashes on Windows are now also reported with a popup in addition to stderr +- Windows: New configuration field `enable_experimental_conpty_backend` which enables support + for the Pseudoconsole API (ConPTY) added in Windows 10 October 2018 (1809) update ### Changed @@ -22,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix panic after quitting Alacritty on macOS - Tabs are no longer replaced by spaces when copying them to the clipboard - Alt modifier is no longer sent separately from the modified key +- Various Windows issues, like color support and performance, through the new ConPTY ## Version 0.2.4 @@ -51,14 +51,16 @@ dependencies = [ "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", + "mio-anonymous-pipes 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "mio-extras 2.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "mio-named-pipes 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "miow 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "notify 4.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "objc 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.82 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.82 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.83 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.83 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", "serde_yaml 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)", "static_assertions 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -68,6 +70,7 @@ dependencies = [ "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "vte 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "winpty 0.1.0", "x11-dl 2.18.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1269,6 +1272,17 @@ dependencies = [ ] [[package]] +name = "mio-anonymous-pipes" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", + "miow 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "spsc-buffer 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "mio-extras" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1913,7 +1927,7 @@ dependencies = [ "mime 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 2.0.0-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)", "native-tls 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.82 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.83 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", "serde_urlencoded 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2029,12 +2043,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde_derive" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2049,7 +2063,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.82 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.83 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2059,7 +2073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.82 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.83 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2070,7 +2084,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", "linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.82 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.83 (registry+https://github.com/rust-lang/crates.io-index)", "yaml-rust 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2129,7 +2143,7 @@ dependencies = [ [[package]] name = "smithay-client-toolkit" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "andrew 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2156,6 +2170,11 @@ dependencies = [ ] [[package]] +name = "spsc-buffer" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "stable_deref_trait" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2646,6 +2665,11 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "widestring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "winapi" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2707,7 +2731,7 @@ dependencies = [ "objc 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "smithay-client-toolkit 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "smithay-client-toolkit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-client 0.21.7 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "x11-dl 2.18.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2947,6 +2971,7 @@ dependencies = [ "checksum miniz_oxide 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5ad30a47319c16cde58d0314f5d98202a80c9083b5f61178457403dfb14e509c" "checksum miniz_oxide_c_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "28edaef377517fd9fe3e085c37d892ce7acd1fbeab9239c5a36eec352d8a8b7e" "checksum mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)" = "71646331f2619b1026cc302f87a2b8b648d5c6dd6937846a16cc8ce0f347f432" +"checksum mio-anonymous-pipes 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8c274c3c52dcd1d78c5d7ed841eca1e9ea2db8353f3b8ec25789cc62c471aaf" "checksum mio-extras 2.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "46e73a04c2fa6250b8d802134d56d554a9ec2922bf977777c805ea5def61ce40" "checksum mio-named-pipes 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f5e374eff525ce1c5b7687c4cef63943e7686524a387933ad27ca7ec43779cb3" "checksum mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125" @@ -3031,8 +3056,8 @@ dependencies = [ "checksum security-framework-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "40d95f3d7da09612affe897f320d78264f0d2320f3e8eea27d12bd1bd94445e2" "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -"checksum serde 1.0.82 (registry+https://github.com/rust-lang/crates.io-index)" = "6fa52f19aee12441d5ad11c9a00459122bd8f98707cadf9778c540674f1935b6" -"checksum serde_derive 1.0.82 (registry+https://github.com/rust-lang/crates.io-index)" = "96a7f9496ac65a2db5929afa087b54f8fc5008dcfbe48a8874ed20049b0d6154" +"checksum serde 1.0.83 (registry+https://github.com/rust-lang/crates.io-index)" = "157e12af46859e968da75dea9845530e13d03bcab2009a41b9b7bb3cf4eb3ec2" +"checksum serde_derive 1.0.83 (registry+https://github.com/rust-lang/crates.io-index)" = "9469829702497daf2daf3c190e130c3fa72f719920f73c86160d43e8f8d76951" "checksum serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)" = "c37ccd6be3ed1fdf419ee848f7c758eb31b054d7cd3ae3600e3bae0adf569811" "checksum serde_urlencoded 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d48f9f99cd749a2de71d29da5f948de7f2764cc5a9d7f3c97e3514d4ee6eabf2" "checksum serde_yaml 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)" = "0887a8e097a69559b56aa2526bf7aff7c3048cf627dff781f0b56a6001534593" @@ -3042,8 +3067,9 @@ dependencies = [ "checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" "checksum slab 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5f9776d6b986f77b35c6cf846c11ad986ff128fe0b2b63a3628e3755e8d3102d" "checksum smallvec 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b73ea3738b47563803ef814925e69be00799a8c07420be8b996f8e98fb2336db" -"checksum smithay-client-toolkit 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4bfd1c912756e610ab598d60fb16adeb3b6745ac0b0a4a2cc1a6b9fa88111409" +"checksum smithay-client-toolkit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d858330eeed4efaf71c560555e2a6a0597d01b7d52685c3cc964ab1cc360f8c6" "checksum socket2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "c4d11a52082057d87cb5caa31ad812f4504b97ab44732cd8359df2e9ff9f48e7" +"checksum spsc-buffer 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "be6c3f39c37a4283ee4b43d1311c828f2e1fb0541e76ea0cb1a2abd9ef2f5b3b" "checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" "checksum static_assertions 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "389ce475f424f267dbed6479cbd8f126c5e1afb053b0acdaa019c74305fc65d1" "checksum stb_truetype 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "71a7d260b43b6129a22dc341be18a231044ca67a48b7e32625f380cc5ec9ad70" @@ -3099,6 +3125,7 @@ dependencies = [ "checksum wayland-sys 0.21.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a0931c24c91e4e56c1119e4137e237df2ccc3696df94f64b1e2f61982d89cc32" "checksum which 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e84a603e7e0b1ce1aa1ee2b109c7be00155ce52df5081590d1ffb93f4f515cb2" "checksum widestring 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7157704c2e12e3d2189c507b7482c52820a16dfa4465ba91add92f266667cadb" +"checksum widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" @@ -58,9 +58,12 @@ x11-dl = "2" [target.'cfg(windows)'.dependencies] winpty = { path = "./winpty" } mio-named-pipes = "0.1" -winapi = { version = "0.3.5", features = ["winuser", "synchapi", "roerrorapi", "winerror"]} +miow = "0.3" +winapi = { version = "0.3.5", features = ["impl-default", "winuser", "synchapi", "roerrorapi", "winerror", "wincon"]} dunce = "0.1" dirs = "1.0" +widestring = "0.4" +mio-anonymous-pipes = "0.1" [target.'cfg(target_os = "macos")'.dependencies] objc = "0.2.2" diff --git a/alacritty_windows.yml b/alacritty_windows.yml index 59de4099..ba764dfc 100644 --- a/alacritty_windows.yml +++ b/alacritty_windows.yml @@ -283,6 +283,17 @@ shell: #args: # - --login +# Windows 10 ConPTY backend +# +# This will enable better color support and may resolve other issues, +# however this API and its implementation is still young and so is +# disabled by default, as stability may not be as good as the winpty +# backend. +# +# Alacritty will fall back to the WinPTY automatically if the ConPTY +# backend cannot be initialized. +enable_experimental_conpty_backend: false + # Key bindings # # Key bindings are specified as a list of objects. Each binding will specify diff --git a/src/config.rs b/src/config.rs index 76d9d7e3..547bce1f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -541,6 +541,12 @@ pub struct Config { // TODO: DEPRECATED #[serde(default, deserialize_with = "failure_default")] unfocused_hollow_cursor: Option<bool>, + + /// Enable experimental conpty backend instead of using winpty. + /// Will only take effect on Windows 10 Oct 2018 and later. + #[cfg(windows)] + #[serde(default, deserialize_with="failure_default")] + enable_experimental_conpty_backend: bool } fn failure_default_vec<'a, D, T>(deserializer: D) -> ::std::result::Result<Vec<T>, D::Error> @@ -1709,6 +1715,13 @@ impl Config { self.colors.cursor.cursor.map(|_| Color::Named(NamedColor::Cursor)) } + /// Enable experimental conpty backend (Windows only) + #[cfg(windows)] + #[inline] + pub fn enable_experimental_conpty_backend(&self) -> bool { + self.enable_experimental_conpty_backend + } + // Update the history size, used in ref tests pub fn set_history(&mut self, history: u32) { self.scrolling.history = history; @@ -20,19 +20,6 @@ #[macro_use] extern crate log; #[macro_use] extern crate serde_derive; -#[cfg(windows)] -extern crate mio_named_pipes; -#[cfg(windows)] -extern crate winapi; -#[cfg(windows)] -extern crate winpty; -#[cfg(windows)] -extern crate dunce; -#[cfg(windows)] -extern crate image; -#[cfg(windows)] -extern crate dirs; - #[cfg(target_os = "macos")] #[macro_use] extern crate objc; diff --git a/src/main.rs b/src/main.rs index 2ce43fe1..ca8da5af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -160,9 +160,9 @@ fn run( // and we need to be able to resize the PTY from the main thread while the IO // thread owns the EventedRW object. #[cfg(windows)] - let resize_handle = unsafe { &mut *pty.winpty.get() }; + let mut resize_handle = pty.resize_handle(); #[cfg(not(windows))] - let resize_handle = &mut pty.fd.as_raw_fd(); + let mut resize_handle = pty.fd.as_raw_fd(); // Create the pseudoterminal I/O loop // @@ -239,7 +239,7 @@ fn run( // // The second argument is a list of types that want to be notified // of display size changes. - display.handle_resize(&mut terminal_lock, &config, &mut [resize_handle, &mut processor]); + display.handle_resize(&mut terminal_lock, &config, &mut [&mut resize_handle, &mut processor]); drop(terminal_lock); diff --git a/src/tty/windows/conpty.rs b/src/tty/windows/conpty.rs new file mode 100644 index 00000000..b16a61c4 --- /dev/null +++ b/src/tty/windows/conpty.rs @@ -0,0 +1,303 @@ +// 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::{process_should_exit, Pty, HANDLE}; + +use std::env; +use std::i16; +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::ctypes::c_void; +use winapi::shared::basetsd::{PSIZE_T, SIZE_T}; +use winapi::shared::minwindef::{BYTE, DWORD}; +use winapi::shared::ntdef::{HANDLE, HRESULT, LPCWSTR, 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, STARTUPINFOEXW}; +use winapi::um::wincon::COORD; + +use crate::cli::Options; +use crate::config::{Config, Shell}; +use crate::display::OnResize; +use crate::term::SizeInfo; + +// This will be merged into winapi as PR #699 +// TODO: Use the winapi definition directly after that. +pub type HPCON = *mut c_void; + +/// 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) -> HRESULT, +} + +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) { + // The pseusdoconsole might already have been closed by the console process exiting. + // ClosePseudoConsole will fail with error code 1 in that case. + // + // This check should be sufficient to avoid that. + if !process_should_exit() { + let result = unsafe { (self.api.ClosePseudoConsole)(self.handle) }; + + // As noted above, if the pseudoconsole is already closed then + // ClosePseudoConsole will fail with the error code 1. + // (This was not in the MSDN docs as of Nov 2018.) + // + // If ClosePseudoConsole is successful then result is S_OK. + assert!(result == S_OK); + } + } +} + +// 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(); + startup_info_ex.StartupInfo.cb = mem::size_of::<STARTUPINFOEXW>() as u32; + + // Create the appropriately sized thread attribute list. + unsafe { + success = + InitializeProcThreadAttributeList(ptr::null_mut(), 1, 0, &mut size as PSIZE_T) > 0; + + // This call was expected to return false. + assert!(!success); + } + + 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; + + assert!(success); + } + + // 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; + + assert!(success); + } + + // Get process commandline + let default_shell = &Shell::new(env::var("COMSPEC").unwrap_or_else(|_| "cmd".into())); + 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().into_raw(); + let cwd = cwd.map(|s| U16CString::from_str(&s).unwrap()); + let cwd_ptr = match &cwd { + Some(b) => b.as_ptr() as LPCWSTR, + None => ptr::null(), + }; + + let mut proc_info: PROCESS_INFORMATION = Default::default(); + unsafe { + success = CreateProcessW( + ptr::null(), + cmdline as LPWSTR, + ptr::null_mut(), + ptr::null_mut(), + true as i32, + EXTENDED_STARTUPINFO_PRESENT, + ptr::null_mut(), + cwd_ptr, + &mut startup_info_ex.StartupInfo as *mut STARTUPINFOW, + &mut proc_info as *mut PROCESS_INFORMATION, + ) > 0; + + assert!(success); + } + + // Recover raw memory to cmdline so it can be freed + unsafe { + U16CString::from_raw(cmdline); + } + + // 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(), + }) +} + +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 + } +} diff --git a/src/tty/windows/mod.rs b/src/tty/windows/mod.rs new file mode 100644 index 00000000..1eb442ad --- /dev/null +++ b/src/tty/windows/mod.rs @@ -0,0 +1,334 @@ +// 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 std::io::{self, Read, Write}; +use std::os::raw::c_void; + +use mio::{self, Evented, Poll, PollOpt, Ready, Token}; +use mio_anonymous_pipes::{EventedAnonRead, EventedAnonWrite}; +use mio_named_pipes::NamedPipe; + +use winapi::shared::winerror::WAIT_TIMEOUT; +use winapi::um::synchapi::WaitForSingleObject; +use winapi::um::winbase::WAIT_OBJECT_0; + +use crate::cli::Options; +use crate::config::Config; +use crate::display::OnResize; +use crate::term::SizeInfo; +use crate::tty::EventedReadWrite; + +mod conpty; +mod winpty; + +/// Handle to the winpty agent or conpty process. Required so we know when it closes. +static mut HANDLE: *mut c_void = 0usize as *mut c_void; + +pub fn process_should_exit() -> bool { + unsafe { + match WaitForSingleObject(HANDLE, 0) { + // Process has exited + WAIT_OBJECT_0 => { + info!("wait_object_0"); + true + } + // Reached timeout of 0, process has not exited + WAIT_TIMEOUT => false, + // Error checking process, winpty gave us a bad agent handle? + _ => { + info!("Bad exit: {}", ::std::io::Error::last_os_error()); + true + } + } + } +} + +#[derive(Clone)] +pub enum PtyHandle<'a> { + Winpty(winpty::WinptyHandle<'a>), + Conpty(conpty::ConptyHandle), +} + +pub struct Pty<'a> { + handle: PtyHandle<'a>, + // TODO: It's on the roadmap for the Conpty API to support Overlapped I/O. + // See https://github.com/Microsoft/console/issues/262 + // When support for that lands then it should be possible to use + // NamedPipe for the conout and conin handles + conout: EventedReadablePipe, + conin: EventedWritablePipe, + read_token: mio::Token, + write_token: mio::Token, +} + +impl<'a> Pty<'a> { + pub fn resize_handle(&self) -> impl OnResize + 'a { + self.handle.clone() + } +} + +pub fn new<'a>( + config: &Config, + options: &Options, + size: &SizeInfo, + window_id: Option<usize>, +) -> Pty<'a> { + if let Some(pty) = conpty::new(config, options, size, window_id) { + info!("Using Conpty agent."); + pty + } else { + info!("Using Winpty agent."); + winpty::new(config, options, size, window_id) + } +} + +// TODO: The ConPTY API curently must use synchronous pipes as the input +// and output handles. This has led to the need to support two different +// types of pipe. +// +// When https://github.com/Microsoft/console/issues/262 lands then the +// Anonymous variant of this enum can be removed from the codebase and +// everything can just use NamedPipe. +pub enum EventedReadablePipe { + Anonymous(EventedAnonRead), + Named(NamedPipe), +} + +pub enum EventedWritablePipe { + Anonymous(EventedAnonWrite), + Named(NamedPipe), +} + +impl Evented for EventedReadablePipe { + fn register( + &self, + poll: &Poll, + token: Token, + interest: Ready, + opts: PollOpt, + ) -> io::Result<()> { + match self { + EventedReadablePipe::Anonymous(p) => p.register(poll, token, interest, opts), + EventedReadablePipe::Named(p) => p.register(poll, token, interest, opts), + } + } + + fn reregister( + &self, + poll: &Poll, + token: Token, + interest: Ready, + opts: PollOpt, + ) -> io::Result<()> { + match self { + EventedReadablePipe::Anonymous(p) => p.reregister(poll, token, interest, opts), + EventedReadablePipe::Named(p) => p.reregister(poll, token, interest, opts), + } + } + + fn deregister(&self, poll: &Poll) -> io::Result<()> { + match self { + EventedReadablePipe::Anonymous(p) => p.deregister(poll), + EventedReadablePipe::Named(p) => p.deregister(poll), + } + } +} + +impl Read for EventedReadablePipe { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + match self { + EventedReadablePipe::Anonymous(p) => p.read(buf), + EventedReadablePipe::Named(p) => p.read(buf), + } + } +} + +impl Evented for EventedWritablePipe { + fn register( + &self, + poll: &Poll, + token: Token, + interest: Ready, + opts: PollOpt, + ) -> io::Result<()> { + match self { + EventedWritablePipe::Anonymous(p) => p.register(poll, token, interest, opts), + EventedWritablePipe::Named(p) => p.register(poll, token, interest, opts), + } + } + + fn reregister( + &self, + poll: &Poll, + token: Token, + interest: Ready, + opts: PollOpt, + ) -> io::Result<()> { + match self { + EventedWritablePipe::Anonymous(p) => p.reregister(poll, token, interest, opts), + EventedWritablePipe::Named(p) => p.reregister(poll, token, interest, opts), + } + } + + fn deregister(&self, poll: &Poll) -> io::Result<()> { + match self { + EventedWritablePipe::Anonymous(p) => p.deregister(poll), + EventedWritablePipe::Named(p) => p.deregister(poll), + } + } +} + +impl Write for EventedWritablePipe { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + match self { + EventedWritablePipe::Anonymous(p) => p.write(buf), + EventedWritablePipe::Named(p) => p.write(buf), + } + } + + fn flush(&mut self) -> io::Result<()> { + match self { + EventedWritablePipe::Anonymous(p) => p.flush(), + EventedWritablePipe::Named(p) => p.flush(), + } + } +} + +impl<'a> OnResize for PtyHandle<'a> { + fn on_resize(&mut self, sizeinfo: &SizeInfo) { + match self { + PtyHandle::Winpty(w) => w.winpty_mut().on_resize(sizeinfo), + PtyHandle::Conpty(c) => { + let mut handle = c.clone(); + handle.on_resize(sizeinfo) + } + } + } +} + +impl<'a> EventedReadWrite for Pty<'a> { + type Reader = EventedReadablePipe; + type Writer = EventedWritablePipe; + + #[inline] + fn register( + &mut self, + poll: &mio::Poll, + token: &mut dyn Iterator<Item = &usize>, + interest: mio::Ready, + poll_opts: mio::PollOpt, + ) -> io::Result<()> { + self.read_token = (*token.next().unwrap()).into(); + self.write_token = (*token.next().unwrap()).into(); + + if interest.is_readable() { + poll.register( + &self.conout, + self.read_token, + mio::Ready::readable(), + poll_opts, + )? + } else { + poll.register( + &self.conout, + self.read_token, + mio::Ready::empty(), + poll_opts, + )? + } + if interest.is_writable() { + poll.register( + &self.conin, + self.write_token, + mio::Ready::writable(), + poll_opts, + )? + } else { + poll.register( + &self.conin, + self.write_token, + mio::Ready::empty(), + poll_opts, + )? + } + Ok(()) + } + + #[inline] + fn reregister( + &mut self, + poll: &mio::Poll, + interest: mio::Ready, + poll_opts: mio::PollOpt, + ) -> io::Result<()> { + if interest.is_readable() { + poll.reregister( + &self.conout, + self.read_token, + mio::Ready::readable(), + poll_opts, + )?; + } else { + poll.reregister( + &self.conout, + self.read_token, + mio::Ready::empty(), + poll_opts, + )?; + } + if interest.is_writable() { + poll.reregister( + &self.conin, + self.write_token, + mio::Ready::writable(), + poll_opts, + )?; + } else { + poll.reregister( + &self.conin, + self.write_token, + mio::Ready::empty(), + poll_opts, + )?; + } + Ok(()) + } + + #[inline] + fn deregister(&mut self, poll: &mio::Poll) -> io::Result<()> { + poll.deregister(&self.conout)?; + poll.deregister(&self.conin)?; + Ok(()) + } + + #[inline] + fn reader(&mut self) -> &mut Self::Reader { + &mut self.conout + } + + #[inline] + fn read_token(&self) -> mio::Token { + self.read_token + } + + #[inline] + fn writer(&mut self) -> &mut Self::Writer { + &mut self.conin + } + + #[inline] + fn write_token(&self) -> mio::Token { + self.write_token + } +} diff --git a/src/tty/windows.rs b/src/tty/windows/winpty.rs index 4895b37d..a3a45e39 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows/winpty.rs @@ -12,73 +12,78 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::{Pty, HANDLE}; + use std::io; use std::fs::OpenOptions; -use std::os::raw::c_void; use std::os::windows::io::{FromRawHandle, IntoRawHandle}; use std::os::windows::fs::OpenOptionsExt; use std::env; -use std::cell::UnsafeCell; +use std::sync::Arc; use std::u16; use dunce::canonicalize; -use mio; -use mio::Evented; use mio_named_pipes::NamedPipe; -use winapi::um::synchapi::WaitForSingleObject; -use winapi::um::winbase::{WAIT_OBJECT_0, FILE_FLAG_OVERLAPPED}; -use winapi::shared::winerror::WAIT_TIMEOUT; +use winapi::um::winbase::FILE_FLAG_OVERLAPPED; use winpty::{ConfigFlags, MouseMode, SpawnConfig, SpawnFlags, Winpty}; use winpty::Config as WinptyConfig; use crate::config::{Config, Shell}; use crate::display::OnResize; use crate::cli::Options; -use crate::tty::EventedReadWrite; use crate::term::SizeInfo; -/// Handle to the winpty agent process. Required so we know when it closes. -static mut HANDLE: *mut c_void = 0usize as *mut c_void; +// We store a raw pointer because we need mutable access to call +// on_resize from a separate thread. Winpty internally uses a mutex +// so this is safe, despite outwards appearance. +pub struct Agent<'a> { + winpty: *mut Winpty<'a> +} -/// How long the winpty agent should wait for any RPC request -/// This is a placeholder value until we see how often long responses happen -const AGENT_TIMEOUT: u32 = 10000; +/// Handle can be cloned freely and moved between threads. +pub type WinptyHandle<'a> = Arc<Agent<'a>>; -pub fn process_should_exit() -> bool { - unsafe { - match WaitForSingleObject(HANDLE, 0) { - // Process has exited - WAIT_OBJECT_0 => { - info!("wait_object_0"); - true - } - // Reached timeout of 0, process has not exited - WAIT_TIMEOUT => false, - // Error checking process, winpty gave us a bad agent handle? - _ => { - info!("Bad exit: {}", ::std::io::Error::last_os_error()); - true - } +// Because Winpty has a mutex, we can do this. +unsafe impl<'a> Send for Agent<'a> {} +unsafe impl<'a> Sync for Agent<'a> {} + +impl<'a> Agent<'a> { + pub fn new(winpty: Winpty<'a>) -> Self { + Self { + winpty: Box::into_raw(Box::new(winpty)) } } -} -pub struct Pty<'a, R: io::Read + Evented + Send, W: io::Write + Evented + Send> { - // TODO: Provide methods for accessing this safely - pub winpty: UnsafeCell<Winpty<'a>>, + /// Get immutable access to Winpty. + pub fn winpty(&self) -> &Winpty<'a> { + unsafe {&*self.winpty} + } + + /// Get mutable access to Winpty. + /// Can offer internal mutability like this because Winpty uses + /// a mutex internally. + pub fn winpty_mut(&self) -> &mut Winpty<'a> { + unsafe {&mut *self.winpty} + } +} - conout: R, - conin: W, - read_token: mio::Token, - write_token: mio::Token, +impl<'a> Drop for Agent<'a> { + fn drop(&mut self) { + unsafe { Box::from_raw(self.winpty); } + } } + +/// How long the winpty agent should wait for any RPC request +/// This is a placeholder value until we see how often long responses happen +const AGENT_TIMEOUT: u32 = 10000; + pub fn new<'a>( config: &Config, options: &Options, size: &SizeInfo, _window_id: Option<usize>, -) -> Pty<'a, NamedPipe, NamedPipe> { +) -> Pty<'a> { // Create config let mut wconfig = WinptyConfig::new(ConfigFlags::empty()).unwrap(); @@ -155,123 +160,14 @@ pub fn new<'a>( HANDLE = winpty.raw_handle(); } + let agent = Agent::new(winpty); + Pty { - winpty: UnsafeCell::new(winpty), - conout: conout_pipe, - conin: conin_pipe, - // Placeholder tokens that are overwritten + handle: super::PtyHandle::Winpty(WinptyHandle::new(agent)), + conout: super::EventedReadablePipe::Named(conout_pipe), + conin: super::EventedWritablePipe::Named(conin_pipe), read_token: 0.into(), - write_token: 0.into(), - } -} - -impl<'a> EventedReadWrite for Pty<'a, NamedPipe, NamedPipe> { - type Reader = NamedPipe; - type Writer = NamedPipe; - - #[inline] - fn register( - &mut self, - poll: &mio::Poll, - token: &mut Iterator<Item = &usize>, - interest: mio::Ready, - poll_opts: mio::PollOpt, - ) -> io::Result<()> { - self.read_token = (*token.next().unwrap()).into(); - self.write_token = (*token.next().unwrap()).into(); - if interest.is_readable() { - poll.register( - &self.conout, - self.read_token, - mio::Ready::readable(), - poll_opts, - )? - } else { - poll.register( - &self.conout, - self.read_token, - mio::Ready::empty(), - poll_opts, - )? - } - if interest.is_writable() { - poll.register( - &self.conin, - self.write_token, - mio::Ready::writable(), - poll_opts, - )? - } else { - poll.register( - &self.conin, - self.write_token, - mio::Ready::empty(), - poll_opts, - )? - } - Ok(()) - } - - #[inline] - fn reregister(&mut self, poll: &mio::Poll, interest: mio::Ready, poll_opts: mio::PollOpt) -> io::Result<()> { - if interest.is_readable() { - poll.reregister( - &self.conout, - self.read_token, - mio::Ready::readable(), - poll_opts, - )?; - } else { - poll.reregister( - &self.conout, - self.read_token, - mio::Ready::empty(), - poll_opts, - )?; - } - if interest.is_writable() { - poll.reregister( - &self.conin, - self.write_token, - mio::Ready::writable(), - poll_opts, - )?; - } else { - poll.reregister( - &self.conin, - self.write_token, - mio::Ready::empty(), - poll_opts, - )?; - } - Ok(()) - } - - #[inline] - fn deregister(&mut self, poll: &mio::Poll) -> io::Result<()> { - poll.deregister(&self.conout)?; - poll.deregister(&self.conin)?; - Ok(()) - } - - #[inline] - fn reader(&mut self) -> &mut NamedPipe { - &mut self.conout - } - - #[inline] - fn read_token(&self) -> mio::Token { - self.read_token - } - - #[inline] - fn writer(&mut self) -> &mut NamedPipe { - &mut self.conin - } - - #[inline] - fn write_token(&self) -> mio::Token { - self.write_token + write_token: 0.into() } } |