alacritty_terminal/tty/
unix.rs

1//! TTY related functionality.
2
3use std::ffi::CStr;
4use std::fs::File;
5use std::io::{Error, ErrorKind, Read, Result};
6use std::mem::MaybeUninit;
7use std::os::fd::OwnedFd;
8use std::os::unix::io::AsRawFd;
9use std::os::unix::net::UnixStream;
10use std::os::unix::process::CommandExt;
11#[cfg(target_os = "macos")]
12use std::path::Path;
13use std::process::{Child, Command};
14use std::sync::Arc;
15use std::{env, ptr};
16
17use libc::{F_GETFL, F_SETFL, O_NONBLOCK, TIOCSCTTY, c_int, fcntl};
18use log::error;
19use polling::{Event, PollMode, Poller};
20use rustix_openpty::openpty;
21use rustix_openpty::rustix::termios::Winsize;
22#[cfg(any(target_os = "linux", target_os = "macos"))]
23use rustix_openpty::rustix::termios::{self, InputModes, OptionalActions};
24use signal_hook::low_level::{pipe as signal_pipe, unregister as unregister_signal};
25use signal_hook::{SigId, consts as sigconsts};
26
27use crate::event::{OnResize, WindowSize};
28use crate::tty::{ChildEvent, EventedPty, EventedReadWrite, Options};
29
30// Interest in PTY read/writes.
31pub(crate) const PTY_READ_WRITE_TOKEN: usize = 0;
32
33// Interest in new child events.
34pub(crate) const PTY_CHILD_EVENT_TOKEN: usize = 1;
35
36macro_rules! die {
37    ($($arg:tt)*) => {{
38        error!($($arg)*);
39        std::process::exit(1);
40    }};
41}
42
43/// Really only needed on BSD, but should be fine elsewhere.
44fn set_controlling_terminal(fd: c_int) {
45    let res = unsafe {
46        // TIOSCTTY changes based on platform and the `ioctl` call is different
47        // based on architecture (32/64). So a generic cast is used to make sure
48        // there are no issues. To allow such a generic cast the clippy warning
49        // is disabled.
50        #[allow(clippy::cast_lossless)]
51        libc::ioctl(fd, TIOCSCTTY as _, 0)
52    };
53
54    if res < 0 {
55        die!("ioctl TIOCSCTTY failed: {}", Error::last_os_error());
56    }
57}
58
59#[derive(Debug)]
60struct Passwd<'a> {
61    name: &'a str,
62    dir: &'a str,
63    shell: &'a str,
64}
65
66/// Return a Passwd struct with pointers into the provided buf.
67///
68/// # Unsafety
69///
70/// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen.
71fn get_pw_entry(buf: &mut [i8; 1024]) -> Result<Passwd<'_>> {
72    // Create zeroed passwd struct.
73    let mut entry: MaybeUninit<libc::passwd> = MaybeUninit::uninit();
74
75    let mut res: *mut libc::passwd = ptr::null_mut();
76
77    // Try and read the pw file.
78    let uid = unsafe { libc::getuid() };
79    let status = unsafe {
80        libc::getpwuid_r(uid, entry.as_mut_ptr(), buf.as_mut_ptr() as *mut _, buf.len(), &mut res)
81    };
82    let entry = unsafe { entry.assume_init() };
83
84    if status < 0 {
85        return Err(Error::other("getpwuid_r failed"));
86    }
87
88    if res.is_null() {
89        return Err(Error::other("pw not found"));
90    }
91
92    // Sanity check.
93    assert_eq!(entry.pw_uid, uid);
94
95    // Build a borrowed Passwd struct.
96    Ok(Passwd {
97        name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() },
98        dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() },
99        shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() },
100    })
101}
102
103pub struct Pty {
104    child: Child,
105    file: File,
106    signals: UnixStream,
107    sig_id: SigId,
108}
109
110impl Pty {
111    pub fn child(&self) -> &Child {
112        &self.child
113    }
114
115    pub fn file(&self) -> &File {
116        &self.file
117    }
118}
119
120/// User information that is required for a new shell session.
121struct ShellUser {
122    user: String,
123    home: String,
124    shell: String,
125}
126
127impl ShellUser {
128    /// look for shell, username, longname, and home dir in the respective environment variables
129    /// before falling back on looking into `passwd`.
130    fn from_env() -> Result<Self> {
131        let mut buf = [0; 1024];
132        let pw = get_pw_entry(&mut buf);
133
134        let user = match env::var("USER") {
135            Ok(user) => user,
136            Err(_) => match pw {
137                Ok(ref pw) => pw.name.to_owned(),
138                Err(err) => return Err(err),
139            },
140        };
141
142        let home = match env::var("HOME") {
143            Ok(home) => home,
144            Err(_) => match pw {
145                Ok(ref pw) => pw.dir.to_owned(),
146                Err(err) => return Err(err),
147            },
148        };
149
150        let shell = match env::var("SHELL") {
151            Ok(shell) => shell,
152            Err(_) => match pw {
153                Ok(ref pw) => pw.shell.to_owned(),
154                Err(err) => return Err(err),
155            },
156        };
157
158        Ok(Self { user, home, shell })
159    }
160}
161
162#[cfg(not(target_os = "macos"))]
163fn default_shell_command(shell: &str, _user: &str, _home: &str) -> Command {
164    Command::new(shell)
165}
166
167#[cfg(target_os = "macos")]
168fn default_shell_command(shell: &str, user: &str, home: &str) -> Command {
169    let shell_name = shell.rsplit('/').next().unwrap();
170
171    // On macOS, use the `login` command so the shell will appear as a tty session.
172    let mut login_command = Command::new("/usr/bin/login");
173
174    // Exec the shell with argv[0] prepended by '-' so it becomes a login shell.
175    // `login` normally does this itself, but `-l` disables this.
176    let exec = format!("exec -a -{} {}", shell_name, shell);
177
178    // Since we use -l, `login` will not change directory to the user's home. However,
179    // `login` only checks the current working directory for a .hushlogin file, causing
180    // it to miss any in the user's home directory. We can fix this by doing the check
181    // ourselves and passing `-q`
182    let has_home_hushlogin = Path::new(home).join(".hushlogin").exists();
183
184    // -f: Bypasses authentication for the already-logged-in user.
185    // -l: Skips changing directory to $HOME and prepending '-' to argv[0].
186    // -p: Preserves the environment.
187    // -q: Act as if `.hushlogin` exists.
188    //
189    // XXX: we use zsh here over sh due to `exec -a`.
190    let flags = if has_home_hushlogin { "-qflp" } else { "-flp" };
191    login_command.args([flags, user, "/bin/zsh", "-fc", &exec]);
192    login_command
193}
194
195/// Create a new TTY and return a handle to interact with it.
196pub fn new(config: &Options, window_size: WindowSize, window_id: u64) -> Result<Pty> {
197    let pty = openpty(None, Some(&window_size.to_winsize()))?;
198    let (master, slave) = (pty.controller, pty.user);
199    from_fd(config, window_id, master, slave)
200}
201
202/// Create a new TTY from a PTY's file descriptors.
203pub fn from_fd(config: &Options, window_id: u64, master: OwnedFd, slave: OwnedFd) -> Result<Pty> {
204    let master_fd = master.as_raw_fd();
205    let slave_fd = slave.as_raw_fd();
206
207    #[cfg(any(target_os = "linux", target_os = "macos"))]
208    if let Ok(mut termios) = termios::tcgetattr(&master) {
209        // Set character encoding to UTF-8.
210        termios.input_modes.set(InputModes::IUTF8, true);
211        let _ = termios::tcsetattr(&master, OptionalActions::Now, &termios);
212    }
213
214    let user = ShellUser::from_env()?;
215
216    let mut builder = if let Some(shell) = config.shell.as_ref() {
217        let mut cmd = Command::new(&shell.program);
218        cmd.args(shell.args.as_slice());
219        cmd
220    } else {
221        default_shell_command(&user.shell, &user.user, &user.home)
222    };
223
224    // Setup child stdin/stdout/stderr as slave fd of PTY.
225    builder.stdin(slave.try_clone()?);
226    builder.stderr(slave.try_clone()?);
227    builder.stdout(slave);
228
229    // Setup shell environment.
230    let window_id = window_id.to_string();
231    builder.env("ALACRITTY_WINDOW_ID", &window_id);
232    builder.env("USER", user.user);
233    builder.env("HOME", user.home);
234    // Set Window ID for clients relying on X11 hacks.
235    builder.env("WINDOWID", window_id);
236    for (key, value) in &config.env {
237        builder.env(key, value);
238    }
239
240    // Prevent child processes from inheriting linux-specific startup notification env.
241    builder.env_remove("XDG_ACTIVATION_TOKEN");
242    builder.env_remove("DESKTOP_STARTUP_ID");
243
244    let working_directory = config.working_directory.clone();
245    unsafe {
246        builder.pre_exec(move || {
247            // Create a new process group.
248            let err = libc::setsid();
249            if err == -1 {
250                return Err(Error::other("Failed to set session id"));
251            }
252
253            // Set working directory, ignoring invalid paths.
254            if let Some(working_directory) = working_directory.as_ref() {
255                let _ = env::set_current_dir(working_directory);
256            }
257
258            set_controlling_terminal(slave_fd);
259
260            // No longer need slave/master fds.
261            libc::close(slave_fd);
262            libc::close(master_fd);
263
264            libc::signal(libc::SIGCHLD, libc::SIG_DFL);
265            libc::signal(libc::SIGHUP, libc::SIG_DFL);
266            libc::signal(libc::SIGINT, libc::SIG_DFL);
267            libc::signal(libc::SIGQUIT, libc::SIG_DFL);
268            libc::signal(libc::SIGTERM, libc::SIG_DFL);
269            libc::signal(libc::SIGALRM, libc::SIG_DFL);
270
271            Ok(())
272        });
273    }
274
275    // Prepare signal handling before spawning child.
276    let (signals, sig_id) = {
277        let (sender, recv) = UnixStream::pair()?;
278
279        // Register the recv end of the pipe for SIGCHLD.
280        let sig_id = signal_pipe::register(sigconsts::SIGCHLD, sender)?;
281        recv.set_nonblocking(true)?;
282        (recv, sig_id)
283    };
284
285    match builder.spawn() {
286        Ok(child) => {
287            unsafe {
288                // Maybe this should be done outside of this function so nonblocking
289                // isn't forced upon consumers. Although maybe it should be?
290                set_nonblocking(master_fd);
291            }
292
293            Ok(Pty { child, file: File::from(master), signals, sig_id })
294        },
295        Err(err) => Err(Error::new(
296            err.kind(),
297            format!(
298                "Failed to spawn command '{}': {}",
299                builder.get_program().to_string_lossy(),
300                err
301            ),
302        )),
303    }
304}
305
306impl Drop for Pty {
307    fn drop(&mut self) {
308        // Make sure the PTY is terminated properly.
309        unsafe {
310            libc::kill(self.child.id() as i32, libc::SIGHUP);
311        }
312
313        // Clear signal-hook handler.
314        unregister_signal(self.sig_id);
315
316        let _ = self.child.wait();
317    }
318}
319
320impl EventedReadWrite for Pty {
321    type Reader = File;
322    type Writer = File;
323
324    #[inline]
325    unsafe fn register(
326        &mut self,
327        poll: &Arc<Poller>,
328        mut interest: Event,
329        poll_opts: PollMode,
330    ) -> Result<()> {
331        interest.key = PTY_READ_WRITE_TOKEN;
332        unsafe {
333            poll.add_with_mode(&self.file, interest, poll_opts)?;
334        }
335
336        unsafe {
337            poll.add_with_mode(
338                &self.signals,
339                Event::readable(PTY_CHILD_EVENT_TOKEN),
340                PollMode::Level,
341            )
342        }
343    }
344
345    #[inline]
346    fn reregister(
347        &mut self,
348        poll: &Arc<Poller>,
349        mut interest: Event,
350        poll_opts: PollMode,
351    ) -> Result<()> {
352        interest.key = PTY_READ_WRITE_TOKEN;
353        poll.modify_with_mode(&self.file, interest, poll_opts)?;
354
355        poll.modify_with_mode(
356            &self.signals,
357            Event::readable(PTY_CHILD_EVENT_TOKEN),
358            PollMode::Level,
359        )
360    }
361
362    #[inline]
363    fn deregister(&mut self, poll: &Arc<Poller>) -> Result<()> {
364        poll.delete(&self.file)?;
365        poll.delete(&self.signals)
366    }
367
368    #[inline]
369    fn reader(&mut self) -> &mut File {
370        &mut self.file
371    }
372
373    #[inline]
374    fn writer(&mut self) -> &mut File {
375        &mut self.file
376    }
377}
378
379impl EventedPty for Pty {
380    #[inline]
381    fn next_child_event(&mut self) -> Option<ChildEvent> {
382        // See if there has been a SIGCHLD.
383        let mut buf = [0u8; 1];
384        if let Err(err) = self.signals.read(&mut buf) {
385            if err.kind() != ErrorKind::WouldBlock {
386                error!("Error reading from signal pipe: {err}");
387            }
388            return None;
389        }
390
391        // Match on the child process.
392        match self.child.try_wait() {
393            Err(err) => {
394                error!("Error checking child process termination: {err}");
395                None
396            },
397            Ok(None) => None,
398            Ok(exit_status) => Some(ChildEvent::Exited(exit_status.and_then(|s| s.code()))),
399        }
400    }
401}
402
403impl OnResize for Pty {
404    /// Resize the PTY.
405    ///
406    /// Tells the kernel that the window size changed with the new pixel
407    /// dimensions and line/column counts.
408    fn on_resize(&mut self, window_size: WindowSize) {
409        let win = window_size.to_winsize();
410
411        let res = unsafe { libc::ioctl(self.file.as_raw_fd(), libc::TIOCSWINSZ, &win as *const _) };
412
413        if res < 0 {
414            die!("ioctl TIOCSWINSZ failed: {}", Error::last_os_error());
415        }
416    }
417}
418
419/// Types that can produce a `Winsize`.
420pub trait ToWinsize {
421    /// Get a `Winsize`.
422    fn to_winsize(self) -> Winsize;
423}
424
425impl ToWinsize for WindowSize {
426    fn to_winsize(self) -> Winsize {
427        let ws_row = self.num_lines as libc::c_ushort;
428        let ws_col = self.num_cols as libc::c_ushort;
429
430        let ws_xpixel = ws_col * self.cell_width as libc::c_ushort;
431        let ws_ypixel = ws_row * self.cell_height as libc::c_ushort;
432        Winsize { ws_row, ws_col, ws_xpixel, ws_ypixel }
433    }
434}
435
436unsafe fn set_nonblocking(fd: c_int) {
437    let res = unsafe { fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK) };
438    assert_eq!(res, 0);
439}
440
441#[test]
442fn test_get_pw_entry() {
443    let mut buf: [i8; 1024] = [0; 1024];
444    let _pw = get_pw_entry(&mut buf).unwrap();
445}