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/unix.rs | |
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/unix.rs')
-rw-r--r-- | alacritty_terminal/src/tty/unix.rs | 405 |
1 files changed, 405 insertions, 0 deletions
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); +} |