//! 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::fd::OwnedFd; use std::os::unix::io::AsRawFd; use std::os::unix::net::UnixStream; use std::os::unix::process::CommandExt; use std::process::{Child, Command}; 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::low_level::{pipe as signal_pipe, unregister as unregister_signal}; use signal_hook::{consts as sigconsts, SigId}; 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> { // Create zeroed passwd struct. let mut entry: MaybeUninit = 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, sig_id: SigId, } 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 { 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", "-fc", &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 { let pty = openpty(None, Some(&window_size.to_winsize()))?; let (master, slave) = (pty.controller, pty.user); from_fd(config, window_id, master, slave) } /// Create a new TTY from a PTY's file descriptors. pub fn from_fd(config: &Options, window_id: u64, master: OwnedFd, slave: OwnedFd) -> Result { 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. builder.stdin(slave.try_clone()?); builder.stderr(slave.try_clone()?); builder.stdout(slave); // 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, sig_id) = { let (sender, recv) = UnixStream::pair()?; // Register the recv end of the pipe for SIGCHLD. let sig_id = signal_pipe::register(sigconsts::SIGCHLD, sender)?; recv.set_nonblocking(true)?; (recv, sig_id) }; 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, sig_id }) }, 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); } // Clear signal-hook handler. unregister_signal(self.sig_id); let _ = self.child.wait(); } } impl EventedReadWrite for Pty { type Reader = File; type Writer = File; #[inline] unsafe fn register( &mut self, poll: &Arc, 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, 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) -> 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 { // 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(); }