diff options
author | Theodore Dubois <tblodt@icloud.com> | 2019-04-28 06:24:58 -0700 |
---|---|---|
committer | Christian Duerr <chrisduerr@users.noreply.github.com> | 2019-04-28 13:24:58 +0000 |
commit | dbd8538762ef8968a493e1bf996e8693479ca783 (patch) | |
tree | 32ac2a6a5e01238a272d4ba534551d2e42903c7a /alacritty_terminal/src/tty | |
parent | 9c6d12ea2c863ba76015bdedc00db13b7307725a (diff) | |
download | alacritty-dbd8538762ef8968a493e1bf996e8693479ca783.tar.gz alacritty-dbd8538762ef8968a493e1bf996e8693479ca783.zip |
Split alacritty into a separate crates
The crate containing the entry point is called alacritty, and the crate
containing everything else is called alacritty_terminal.
Diffstat (limited to 'alacritty_terminal/src/tty')
-rw-r--r-- | alacritty_terminal/src/tty/mod.rs | 96 | ||||
-rw-r--r-- | alacritty_terminal/src/tty/unix.rs | 405 | ||||
-rw-r--r-- | alacritty_terminal/src/tty/windows/conpty.rs | 289 | ||||
-rw-r--r-- | alacritty_terminal/src/tty/windows/mod.rs | 303 | ||||
-rw-r--r-- | alacritty_terminal/src/tty/windows/winpty.rs | 169 |
5 files changed, 1262 insertions, 0 deletions
diff --git a/alacritty_terminal/src/tty/mod.rs b/alacritty_terminal/src/tty/mod.rs new file mode 100644 index 00000000..ec175ee6 --- /dev/null +++ b/alacritty_terminal/src/tty/mod.rs @@ -0,0 +1,96 @@ +// 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. +// +//! tty related functionality +use mio; +use std::{env, io}; + +use terminfo::Database; + +use crate::config::Config; + +#[cfg(not(windows))] +mod unix; +#[cfg(not(windows))] +pub use self::unix::*; + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub use self::windows::*; + +/// This trait defines the behaviour needed to read and/or write to a stream. +/// It defines an abstraction over mio's interface in order to allow either one +/// read/write object or a seperate read and write object. +pub trait EventedReadWrite { + type Reader: io::Read; + type Writer: io::Write; + + fn register( + &mut self, + _: &mio::Poll, + _: &mut dyn Iterator<Item = mio::Token>, + _: mio::Ready, + _: mio::PollOpt, + ) -> io::Result<()>; + fn reregister(&mut self, _: &mio::Poll, _: mio::Ready, _: mio::PollOpt) -> io::Result<()>; + fn deregister(&mut self, _: &mio::Poll) -> io::Result<()>; + + fn reader(&mut self) -> &mut Self::Reader; + fn read_token(&self) -> mio::Token; + fn writer(&mut self) -> &mut Self::Writer; + fn write_token(&self) -> mio::Token; +} + +/// Events concerning TTY child processes +#[derive(PartialEq)] +pub enum ChildEvent { + /// Indicates the child has exited + Exited, +} + +/// A pseudoterminal (or PTY) +/// +/// This is a refinement of EventedReadWrite that also provides a channel through which we can be +/// notified if the PTY child process does something we care about (other than writing to the TTY). +/// In particular, this allows for race-free child exit notification on UNIX (cf. `SIGCHLD`). +pub trait EventedPty: EventedReadWrite { + #[cfg(unix)] + fn child_event_token(&self) -> mio::Token; + + /// Tries to retrieve an event + /// + /// Returns `Some(event)` on success, or `None` if there are no events to retrieve. + #[cfg(unix)] + fn next_child_event(&mut self) -> Option<ChildEvent>; +} + +// Setup environment variables +pub fn setup_env(config: &Config) { + // Default to 'alacritty' terminfo if it is available, otherwise + // default to 'xterm-256color'. May be overridden by user's config + // below. + env::set_var( + "TERM", + if Database::from_name("alacritty").is_ok() { "alacritty" } else { "xterm-256color" }, + ); + + // Advertise 24-bit color support + env::set_var("COLORTERM", "truecolor"); + + // Set env vars from config + for (key, value) in config.env().iter() { + env::set_var(key, value); + } +} diff --git a/alacritty_terminal/src/tty/unix.rs b/alacritty_terminal/src/tty/unix.rs new file mode 100644 index 00000000..0e3dc2fd --- /dev/null +++ b/alacritty_terminal/src/tty/unix.rs @@ -0,0 +1,405 @@ +// 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. +// +//! tty related functionality +//! + +use crate::cli::Options; +use crate::config::{Config, Shell}; +use crate::display::OnResize; +use crate::term::SizeInfo; +use crate::tty::{ChildEvent, EventedPty, EventedReadWrite}; +use mio; + +use libc::{self, c_int, pid_t, winsize, TIOCSCTTY}; +use nix::pty::openpty; +use signal_hook::{self as sighook, iterator::Signals}; + +use mio::unix::EventedFd; +use std::ffi::CStr; +use std::fs::File; +use std::io; +use std::os::unix::{ + io::{AsRawFd, FromRawFd, RawFd}, + process::CommandExt, +}; +use std::process::{Child, Command, Stdio}; +use std::ptr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// Process ID of child process +/// +/// Necessary to put this in static storage for `sigchld` to have access +static PID: AtomicUsize = AtomicUsize::new(0); + +pub fn child_pid() -> pid_t { + PID.load(Ordering::Relaxed) as pid_t +} + +/// Get the current value of errno +fn errno() -> c_int { + ::errno::errno().0 +} + +/// Get raw fds for master/slave ends of a new pty +fn make_pty(size: winsize) -> (RawFd, RawFd) { + let mut win_size = size; + win_size.ws_xpixel = 0; + win_size.ws_ypixel = 0; + + let ends = openpty(Some(&win_size), None).expect("openpty failed"); + + (ends.master, ends.slave) +} + +/// Really only needed on BSD, but should be fine elsewhere +fn set_controlling_terminal(fd: c_int) { + let res = unsafe { + // TIOSCTTY changes based on platform and the `ioctl` call is different + // based on architecture (32/64). So a generic cast is used to make sure + // there are no issues. To allow such a generic cast the clippy warning + // is disabled. + #[allow(clippy::cast_lossless)] + libc::ioctl(fd, TIOCSCTTY as _, 0) + }; + + if res < 0 { + die!("ioctl TIOCSCTTY failed: {}", errno()); + } +} + +#[derive(Debug)] +struct Passwd<'a> { + name: &'a str, + passwd: &'a str, + uid: libc::uid_t, + gid: libc::gid_t, + gecos: &'a str, + dir: &'a str, + shell: &'a str, +} + +/// Return a Passwd struct with pointers into the provided buf +/// +/// # Unsafety +/// +/// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen. +fn get_pw_entry(buf: &mut [i8; 1024]) -> Passwd<'_> { + // Create zeroed passwd struct + let mut entry: libc::passwd = unsafe { ::std::mem::uninitialized() }; + + let mut res: *mut libc::passwd = ptr::null_mut(); + + // Try and read the pw file. + let uid = unsafe { libc::getuid() }; + let status = unsafe { + libc::getpwuid_r(uid, &mut entry, buf.as_mut_ptr() as *mut _, buf.len(), &mut res) + }; + + if status < 0 { + die!("getpwuid_r failed"); + } + + if res.is_null() { + die!("pw not found"); + } + + // sanity check + assert_eq!(entry.pw_uid, uid); + + // Build a borrowed Passwd struct + Passwd { + name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() }, + passwd: unsafe { CStr::from_ptr(entry.pw_passwd).to_str().unwrap() }, + uid: entry.pw_uid, + gid: entry.pw_gid, + gecos: unsafe { CStr::from_ptr(entry.pw_gecos).to_str().unwrap() }, + dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() }, + shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() }, + } +} + +pub struct Pty { + child: Child, + pub fd: File, + token: mio::Token, + signals: Signals, + signals_token: mio::Token, +} + +impl Pty { + /// Resize the pty + /// + /// Tells the kernel that the window size changed with the new pixel + /// dimensions and line/column counts. + pub fn resize<T: ToWinsize>(&self, size: &T) { + let win = size.to_winsize(); + + let res = unsafe { libc::ioctl(self.fd.as_raw_fd(), libc::TIOCSWINSZ, &win as *const _) }; + + if res < 0 { + die!("ioctl TIOCSWINSZ failed: {}", errno()); + } + } +} + +/// Create a new tty and return a handle to interact with it. +pub fn new<T: ToWinsize>( + config: &Config, + options: &Options, + size: &T, + window_id: Option<usize>, +) -> Pty { + let win_size = size.to_winsize(); + let mut buf = [0; 1024]; + let pw = get_pw_entry(&mut buf); + + let (master, slave) = make_pty(win_size); + + let default_shell = if cfg!(target_os = "macos") { + let shell_name = pw.shell.rsplit('/').next().unwrap(); + let argv = vec![String::from("-c"), format!("exec -a -{} {}", shell_name, pw.shell)]; + + Shell::new_with_args("/bin/bash", argv) + } else { + Shell::new(pw.shell) + }; + let shell = config.shell().unwrap_or(&default_shell); + + let initial_command = options.command().unwrap_or(shell); + + let mut builder = Command::new(initial_command.program()); + for arg in initial_command.args() { + builder.arg(arg); + } + + // Setup child stdin/stdout/stderr as slave fd of pty + // Ownership of fd is transferred to the Stdio structs and will be closed by them at the end of + // this scope. (It is not an issue that the fd is closed three times since File::drop ignores + // error on libc::close.) + builder.stdin(unsafe { Stdio::from_raw_fd(slave) }); + builder.stderr(unsafe { Stdio::from_raw_fd(slave) }); + builder.stdout(unsafe { Stdio::from_raw_fd(slave) }); + + // Setup shell environment + builder.env("LOGNAME", pw.name); + builder.env("USER", pw.name); + builder.env("SHELL", pw.shell); + builder.env("HOME", pw.dir); + + if let Some(window_id) = window_id { + builder.env("WINDOWID", format!("{}", window_id)); + } + + builder.before_exec(move || { + // Create a new process group + unsafe { + let err = libc::setsid(); + if err == -1 { + die!("Failed to set session id: {}", errno()); + } + } + + set_controlling_terminal(slave); + + // No longer need slave/master fds + unsafe { + libc::close(slave); + libc::close(master); + } + + unsafe { + libc::signal(libc::SIGCHLD, libc::SIG_DFL); + libc::signal(libc::SIGHUP, libc::SIG_DFL); + libc::signal(libc::SIGINT, libc::SIG_DFL); + libc::signal(libc::SIGQUIT, libc::SIG_DFL); + libc::signal(libc::SIGTERM, libc::SIG_DFL); + libc::signal(libc::SIGALRM, libc::SIG_DFL); + } + Ok(()) + }); + + // Handle set working directory option + if let Some(ref dir) = options.working_dir { + builder.current_dir(dir.as_path()); + } + + // Prepare signal handling before spawning child + let signals = Signals::new(&[sighook::SIGCHLD]).expect("error preparing signal handling"); + + match builder.spawn() { + Ok(child) => { + // Remember child PID so other modules can use it + PID.store(child.id() as usize, Ordering::Relaxed); + + unsafe { + // Maybe this should be done outside of this function so nonblocking + // isn't forced upon consumers. Although maybe it should be? + set_nonblocking(master); + } + + let pty = Pty { + child, + fd: unsafe { File::from_raw_fd(master) }, + token: mio::Token::from(0), + signals, + signals_token: mio::Token::from(0), + }; + pty.resize(size); + pty + }, + Err(err) => { + die!("Failed to spawn command: {}", err); + }, + } +} + +impl EventedReadWrite for Pty { + type Reader = File; + type Writer = File; + + #[inline] + fn register( + &mut self, + poll: &mio::Poll, + token: &mut dyn Iterator<Item = mio::Token>, + interest: mio::Ready, + poll_opts: mio::PollOpt, + ) -> io::Result<()> { + self.token = token.next().unwrap(); + poll.register(&EventedFd(&self.fd.as_raw_fd()), self.token, interest, poll_opts)?; + + self.signals_token = token.next().unwrap(); + poll.register( + &self.signals, + self.signals_token, + mio::Ready::readable(), + mio::PollOpt::level(), + ) + } + + #[inline] + fn reregister( + &mut self, + poll: &mio::Poll, + interest: mio::Ready, + poll_opts: mio::PollOpt, + ) -> io::Result<()> { + poll.reregister(&EventedFd(&self.fd.as_raw_fd()), self.token, interest, poll_opts)?; + + poll.reregister( + &self.signals, + self.signals_token, + mio::Ready::readable(), + mio::PollOpt::level(), + ) + } + + #[inline] + fn deregister(&mut self, poll: &mio::Poll) -> io::Result<()> { + poll.deregister(&EventedFd(&self.fd.as_raw_fd()))?; + poll.deregister(&self.signals) + } + + #[inline] + fn reader(&mut self) -> &mut File { + &mut self.fd + } + + #[inline] + fn read_token(&self) -> mio::Token { + self.token + } + + #[inline] + fn writer(&mut self) -> &mut File { + &mut self.fd + } + + #[inline] + fn write_token(&self) -> mio::Token { + self.token + } +} + +impl EventedPty for Pty { + #[inline] + fn next_child_event(&mut self) -> Option<ChildEvent> { + self.signals.pending().next().and_then(|signal| { + if signal != sighook::SIGCHLD { + return None; + } + + match self.child.try_wait() { + Err(e) => { + error!("Error checking child process termination: {}", e); + None + }, + Ok(None) => None, + Ok(_) => Some(ChildEvent::Exited), + } + }) + } + + #[inline] + fn child_event_token(&self) -> mio::Token { + self.signals_token + } +} + +pub fn process_should_exit() -> bool { + false +} + +/// Types that can produce a `libc::winsize` +pub trait ToWinsize { + /// Get a `libc::winsize` + fn to_winsize(&self) -> winsize; +} + +impl<'a> ToWinsize for &'a SizeInfo { + fn to_winsize(&self) -> winsize { + winsize { + ws_row: self.lines().0 as libc::c_ushort, + ws_col: self.cols().0 as libc::c_ushort, + ws_xpixel: self.width as libc::c_ushort, + ws_ypixel: self.height as libc::c_ushort, + } + } +} + +impl OnResize for i32 { + fn on_resize(&mut self, size: &SizeInfo) { + let win = size.to_winsize(); + + let res = unsafe { libc::ioctl(*self, libc::TIOCSWINSZ, &win as *const _) }; + + if res < 0 { + die!("ioctl TIOCSWINSZ failed: {}", errno()); + } + } +} + +unsafe fn set_nonblocking(fd: c_int) { + use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK}; + + let res = fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); + assert_eq!(res, 0); +} + +#[test] +fn test_get_pw_entry() { + let mut buf: [i8; 1024] = [0; 1024]; + let _pw = get_pw_entry(&mut buf); +} diff --git a/alacritty_terminal/src/tty/windows/conpty.rs b/alacritty_terminal/src/tty/windows/conpty.rs new file mode 100644 index 00000000..f23d78a7 --- /dev/null +++ b/alacritty_terminal/src/tty/windows/conpty.rs @@ -0,0 +1,289 @@ +// 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 + } +} diff --git a/alacritty_terminal/src/tty/windows/mod.rs b/alacritty_terminal/src/tty/windows/mod.rs new file mode 100644 index 00000000..c87c5257 --- /dev/null +++ b/alacritty_terminal/src/tty/windows/mod.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 std::io::{self, Read, Write}; +use std::os::raw::c_void; +use std::sync::atomic::{AtomicBool, Ordering}; + +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::{EventedPty, 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; +static IS_CONPTY: AtomicBool = AtomicBool::new(false); + +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 + }, + } + } +} + +pub fn is_conpty() -> bool { + IS_CONPTY.load(Ordering::Relaxed) +} + +#[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"); + IS_CONPTY.store(true, Ordering::Relaxed); + 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.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 = mio::Token>, + interest: mio::Ready, + poll_opts: mio::PollOpt, + ) -> io::Result<()> { + self.read_token = token.next().unwrap(); + self.write_token = token.next().unwrap(); + + 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 + } +} + +impl<'a> EventedPty for Pty<'a> {} diff --git a/alacritty_terminal/src/tty/windows/winpty.rs b/alacritty_terminal/src/tty/windows/winpty.rs new file mode 100644 index 00000000..10bd9d01 --- /dev/null +++ b/alacritty_terminal/src/tty/windows/winpty.rs @@ -0,0 +1,169 @@ +// 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::fs::OpenOptions; +use std::io; +use std::os::windows::fs::OpenOptionsExt; +use std::os::windows::io::{FromRawHandle, IntoRawHandle}; +use std::sync::Arc; +use std::u16; + +use dunce::canonicalize; +use mio_named_pipes::NamedPipe; +use winapi::um::winbase::FILE_FLAG_OVERLAPPED; +use winpty::Config as WinptyConfig; +use winpty::{ConfigFlags, MouseMode, SpawnConfig, SpawnFlags, Winpty}; + +use crate::cli::Options; +use crate::config::{Config, Shell}; +use crate::display::OnResize; +use crate::term::SizeInfo; + +// 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>, +} + +/// Handle can be cloned freely and moved between threads. +pub type WinptyHandle<'a> = Arc<Agent<'a>>; + +// 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)) } + } + + /// Get immutable access to Winpty. + pub fn winpty(&self) -> &Winpty<'a> { + unsafe { &*self.winpty } + } + + pub fn resize(&self, size: &SizeInfo) { + // This is safe since Winpty uses a mutex internally. + unsafe { + (&mut *self.winpty).on_resize(size); + } + } +} + +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> { + // Create config + let mut wconfig = WinptyConfig::new(ConfigFlags::empty()).unwrap(); + + wconfig.set_initial_size(size.cols().0 as i32, size.lines().0 as i32); + wconfig.set_mouse_mode(&MouseMode::Auto); + wconfig.set_agent_timeout(AGENT_TIMEOUT); + + // Start agent + let mut winpty = Winpty::open(&wconfig).unwrap(); + let (conin, conout) = (winpty.conin_name(), winpty.conout_name()); + + // 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()); + + // Spawn process + let spawnconfig = SpawnConfig::new( + SpawnFlags::AUTO_SHUTDOWN | SpawnFlags::EXIT_AFTER_SHUTDOWN, + None, // appname + Some(&cmdline.join(" ")), + cwd, + None, // Env + ) + .unwrap(); + + let default_opts = &mut OpenOptions::new(); + default_opts.share_mode(0).custom_flags(FILE_FLAG_OVERLAPPED); + + let (conout_pipe, conin_pipe); + unsafe { + conout_pipe = NamedPipe::from_raw_handle( + default_opts.clone().read(true).open(conout).unwrap().into_raw_handle(), + ); + conin_pipe = NamedPipe::from_raw_handle( + default_opts.clone().write(true).open(conin).unwrap().into_raw_handle(), + ); + }; + + if let Some(err) = conout_pipe.connect().err() { + if err.kind() != io::ErrorKind::WouldBlock { + panic!(err); + } + } + assert!(conout_pipe.take_error().unwrap().is_none()); + + if let Some(err) = conin_pipe.connect().err() { + if err.kind() != io::ErrorKind::WouldBlock { + panic!(err); + } + } + assert!(conin_pipe.take_error().unwrap().is_none()); + + winpty.spawn(&spawnconfig).unwrap(); + + unsafe { + HANDLE = winpty.raw_handle(); + } + + let agent = Agent::new(winpty); + + Pty { + 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> OnResize for Winpty<'a> { + fn on_resize(&mut self, sizeinfo: &SizeInfo) { + let (cols, lines) = (sizeinfo.cols().0, sizeinfo.lines().0); + if cols > 0 && cols <= u16::MAX as usize && lines > 0 && lines <= u16::MAX as usize { + self.set_size(cols as u16, lines as u16) + .unwrap_or_else(|_| info!("Unable to set winpty size, did it die?")); + } + } +} |