425 lines
12 KiB
Rust
425 lines
12 KiB
Rust
//! TTY related functionality.
|
|
|
|
use std::ffi::CStr;
|
|
use std::fs::File;
|
|
use std::io::{Error, ErrorKind, Read, Result};
|
|
use std::mem::MaybeUninit;
|
|
use std::os::unix::io::{AsRawFd, FromRawFd};
|
|
use std::os::unix::net::UnixStream;
|
|
use std::os::unix::process::CommandExt;
|
|
use std::process::{Child, Command, Stdio};
|
|
use std::sync::Arc;
|
|
use std::{env, ptr};
|
|
|
|
use libc::{c_int, TIOCSCTTY};
|
|
use log::error;
|
|
use polling::{Event, PollMode, Poller};
|
|
use rustix_openpty::openpty;
|
|
use rustix_openpty::rustix::termios::Winsize;
|
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
|
use rustix_openpty::rustix::termios::{self, InputModes, OptionalActions};
|
|
use signal_hook::consts as sigconsts;
|
|
use signal_hook::low_level::pipe as signal_pipe;
|
|
|
|
use crate::event::{OnResize, WindowSize};
|
|
use crate::tty::{ChildEvent, EventedPty, EventedReadWrite, Options};
|
|
|
|
// Interest in PTY read/writes.
|
|
pub(crate) const PTY_READ_WRITE_TOKEN: usize = 0;
|
|
|
|
// Interest in new child events.
|
|
pub(crate) const PTY_CHILD_EVENT_TOKEN: usize = 1;
|
|
|
|
macro_rules! die {
|
|
($($arg:tt)*) => {{
|
|
error!($($arg)*);
|
|
std::process::exit(1);
|
|
}}
|
|
}
|
|
|
|
/// 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: {}", Error::last_os_error());
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Passwd<'a> {
|
|
name: &'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]) -> Result<Passwd<'_>> {
|
|
// Create zeroed passwd struct.
|
|
let mut entry: MaybeUninit<libc::passwd> = MaybeUninit::uninit();
|
|
|
|
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, entry.as_mut_ptr(), buf.as_mut_ptr() as *mut _, buf.len(), &mut res)
|
|
};
|
|
let entry = unsafe { entry.assume_init() };
|
|
|
|
if status < 0 {
|
|
return Err(Error::new(ErrorKind::Other, "getpwuid_r failed"));
|
|
}
|
|
|
|
if res.is_null() {
|
|
return Err(Error::new(ErrorKind::Other, "pw not found"));
|
|
}
|
|
|
|
// Sanity check.
|
|
assert_eq!(entry.pw_uid, uid);
|
|
|
|
// Build a borrowed Passwd struct.
|
|
Ok(Passwd {
|
|
name: unsafe { CStr::from_ptr(entry.pw_name).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,
|
|
file: File,
|
|
signals: UnixStream,
|
|
}
|
|
|
|
impl Pty {
|
|
pub fn child(&self) -> &Child {
|
|
&self.child
|
|
}
|
|
|
|
pub fn file(&self) -> &File {
|
|
&self.file
|
|
}
|
|
}
|
|
|
|
/// User information that is required for a new shell session.
|
|
struct ShellUser {
|
|
user: String,
|
|
home: String,
|
|
shell: String,
|
|
}
|
|
|
|
impl ShellUser {
|
|
/// look for shell, username, longname, and home dir in the respective environment variables
|
|
/// before falling back on looking in to `passwd`.
|
|
fn from_env() -> Result<Self> {
|
|
let mut buf = [0; 1024];
|
|
let pw = get_pw_entry(&mut buf);
|
|
|
|
let user = match env::var("USER") {
|
|
Ok(user) => user,
|
|
Err(_) => match pw {
|
|
Ok(ref pw) => pw.name.to_owned(),
|
|
Err(err) => return Err(err),
|
|
},
|
|
};
|
|
|
|
let home = match env::var("HOME") {
|
|
Ok(home) => home,
|
|
Err(_) => match pw {
|
|
Ok(ref pw) => pw.dir.to_owned(),
|
|
Err(err) => return Err(err),
|
|
},
|
|
};
|
|
|
|
let shell = match env::var("SHELL") {
|
|
Ok(shell) => shell,
|
|
Err(_) => match pw {
|
|
Ok(ref pw) => pw.shell.to_owned(),
|
|
Err(err) => return Err(err),
|
|
},
|
|
};
|
|
|
|
Ok(Self { user, home, shell })
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
fn default_shell_command(shell: &str, _user: &str) -> Command {
|
|
Command::new(shell)
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn default_shell_command(shell: &str, user: &str) -> Command {
|
|
let shell_name = shell.rsplit('/').next().unwrap();
|
|
|
|
// On macOS, use the `login` command so the shell will appear as a tty session.
|
|
let mut login_command = Command::new("/usr/bin/login");
|
|
|
|
// Exec the shell with argv[0] prepended by '-' so it becomes a login shell.
|
|
// `login` normally does this itself, but `-l` disables this.
|
|
let exec = format!("exec -a -{} {}", shell_name, shell);
|
|
|
|
// -f: Bypasses authentication for the already-logged-in user.
|
|
// -l: Skips changing directory to $HOME and prepending '-' to argv[0].
|
|
// -p: Preserves the environment.
|
|
//
|
|
// XXX: we use zsh here over sh due to `exec -a`.
|
|
login_command.args(["-flp", user, "/bin/zsh", "-c", &exec]);
|
|
login_command
|
|
}
|
|
|
|
/// Create a new TTY and return a handle to interact with it.
|
|
pub fn new(config: &Options, window_size: WindowSize, window_id: u64) -> Result<Pty> {
|
|
let pty = openpty(None, Some(&window_size.to_winsize()))?;
|
|
let (master, slave) = (pty.controller, pty.user);
|
|
let master_fd = master.as_raw_fd();
|
|
let slave_fd = slave.as_raw_fd();
|
|
|
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
|
if let Ok(mut termios) = termios::tcgetattr(&master) {
|
|
// Set character encoding to UTF-8.
|
|
termios.input_modes.set(InputModes::IUTF8, true);
|
|
let _ = termios::tcsetattr(&master, OptionalActions::Now, &termios);
|
|
}
|
|
|
|
let user = ShellUser::from_env()?;
|
|
|
|
let mut builder = if let Some(shell) = config.shell.as_ref() {
|
|
let mut cmd = Command::new(&shell.program);
|
|
cmd.args(shell.args.as_slice());
|
|
cmd
|
|
} else {
|
|
default_shell_command(&user.shell, &user.user)
|
|
};
|
|
|
|
// 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_fd) });
|
|
builder.stderr(unsafe { Stdio::from_raw_fd(slave_fd) });
|
|
builder.stdout(unsafe { Stdio::from_raw_fd(slave_fd) });
|
|
|
|
// Setup shell environment.
|
|
let window_id = window_id.to_string();
|
|
builder.env("ALACRITTY_WINDOW_ID", &window_id);
|
|
builder.env("USER", user.user);
|
|
builder.env("HOME", user.home);
|
|
// Set Window ID for clients relying on X11 hacks.
|
|
builder.env("WINDOWID", window_id);
|
|
for (key, value) in &config.env {
|
|
builder.env(key, value);
|
|
}
|
|
|
|
unsafe {
|
|
builder.pre_exec(move || {
|
|
// Create a new process group.
|
|
let err = libc::setsid();
|
|
if err == -1 {
|
|
return Err(Error::new(ErrorKind::Other, "Failed to set session id"));
|
|
}
|
|
|
|
set_controlling_terminal(slave_fd);
|
|
|
|
// No longer need slave/master fds.
|
|
libc::close(slave_fd);
|
|
libc::close(master_fd);
|
|
|
|
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(dir) = &config.working_directory {
|
|
builder.current_dir(dir);
|
|
}
|
|
|
|
// Prepare signal handling before spawning child.
|
|
let signals = {
|
|
let (sender, recv) = UnixStream::pair()?;
|
|
|
|
// Register the recv end of the pipe for SIGCHLD.
|
|
signal_pipe::register(sigconsts::SIGCHLD, sender)?;
|
|
recv.set_nonblocking(true)?;
|
|
recv
|
|
};
|
|
|
|
match builder.spawn() {
|
|
Ok(child) => {
|
|
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_fd);
|
|
}
|
|
|
|
Ok(Pty { child, file: File::from(master), signals })
|
|
},
|
|
Err(err) => Err(Error::new(
|
|
err.kind(),
|
|
format!(
|
|
"Failed to spawn command '{}': {}",
|
|
builder.get_program().to_string_lossy(),
|
|
err
|
|
),
|
|
)),
|
|
}
|
|
}
|
|
|
|
impl Drop for Pty {
|
|
fn drop(&mut self) {
|
|
// Make sure the PTY is terminated properly.
|
|
unsafe {
|
|
libc::kill(self.child.id() as i32, libc::SIGHUP);
|
|
}
|
|
let _ = self.child.wait();
|
|
}
|
|
}
|
|
|
|
impl EventedReadWrite for Pty {
|
|
type Reader = File;
|
|
type Writer = File;
|
|
|
|
#[inline]
|
|
unsafe fn register(
|
|
&mut self,
|
|
poll: &Arc<Poller>,
|
|
mut interest: Event,
|
|
poll_opts: PollMode,
|
|
) -> Result<()> {
|
|
interest.key = PTY_READ_WRITE_TOKEN;
|
|
unsafe {
|
|
poll.add_with_mode(&self.file, interest, poll_opts)?;
|
|
}
|
|
|
|
unsafe {
|
|
poll.add_with_mode(
|
|
&self.signals,
|
|
Event::readable(PTY_CHILD_EVENT_TOKEN),
|
|
PollMode::Level,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn reregister(
|
|
&mut self,
|
|
poll: &Arc<Poller>,
|
|
mut interest: Event,
|
|
poll_opts: PollMode,
|
|
) -> Result<()> {
|
|
interest.key = PTY_READ_WRITE_TOKEN;
|
|
poll.modify_with_mode(&self.file, interest, poll_opts)?;
|
|
|
|
poll.modify_with_mode(
|
|
&self.signals,
|
|
Event::readable(PTY_CHILD_EVENT_TOKEN),
|
|
PollMode::Level,
|
|
)
|
|
}
|
|
|
|
#[inline]
|
|
fn deregister(&mut self, poll: &Arc<Poller>) -> Result<()> {
|
|
poll.delete(&self.file)?;
|
|
poll.delete(&self.signals)
|
|
}
|
|
|
|
#[inline]
|
|
fn reader(&mut self) -> &mut File {
|
|
&mut self.file
|
|
}
|
|
|
|
#[inline]
|
|
fn writer(&mut self) -> &mut File {
|
|
&mut self.file
|
|
}
|
|
}
|
|
|
|
impl EventedPty for Pty {
|
|
#[inline]
|
|
fn next_child_event(&mut self) -> Option<ChildEvent> {
|
|
// See if there has been a SIGCHLD.
|
|
let mut buf = [0u8; 1];
|
|
if let Err(err) = self.signals.read(&mut buf) {
|
|
if err.kind() != ErrorKind::WouldBlock {
|
|
error!("Error reading from signal pipe: {}", err);
|
|
}
|
|
return None;
|
|
}
|
|
|
|
// Match on the child process.
|
|
match self.child.try_wait() {
|
|
Err(err) => {
|
|
error!("Error checking child process termination: {}", err);
|
|
None
|
|
},
|
|
Ok(None) => None,
|
|
Ok(exit_status) => Some(ChildEvent::Exited(exit_status.and_then(|s| s.code()))),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl OnResize for Pty {
|
|
/// Resize the PTY.
|
|
///
|
|
/// Tells the kernel that the window size changed with the new pixel
|
|
/// dimensions and line/column counts.
|
|
fn on_resize(&mut self, window_size: WindowSize) {
|
|
let win = window_size.to_winsize();
|
|
|
|
let res = unsafe { libc::ioctl(self.file.as_raw_fd(), libc::TIOCSWINSZ, &win as *const _) };
|
|
|
|
if res < 0 {
|
|
die!("ioctl TIOCSWINSZ failed: {}", Error::last_os_error());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Types that can produce a `Winsize`.
|
|
pub trait ToWinsize {
|
|
/// Get a `Winsize`.
|
|
fn to_winsize(self) -> Winsize;
|
|
}
|
|
|
|
impl ToWinsize for WindowSize {
|
|
fn to_winsize(self) -> Winsize {
|
|
let ws_row = self.num_lines as libc::c_ushort;
|
|
let ws_col = self.num_cols as libc::c_ushort;
|
|
|
|
let ws_xpixel = ws_col * self.cell_width as libc::c_ushort;
|
|
let ws_ypixel = ws_row * self.cell_height as libc::c_ushort;
|
|
Winsize { ws_row, ws_col, ws_xpixel, ws_ypixel }
|
|
}
|
|
}
|
|
|
|
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).unwrap();
|
|
}
|