brewery
notashelf /
a5249b2315f93df1b265b11ea4de0aac0eca5450

beer

public

A terminal worth pouring time into

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/beer.git
src/pty.rsRust117 lines3.7 KB
1
//! Pseudo-terminal: open a master/slave pair and run the user's shell on it.
2
3
use std::ffi::OsStr;
4
use std::io;
5
use std::os::fd::{AsFd, OwnedFd};
6
use std::os::unix::process::CommandExt;
7
use std::path::Path;
8
use std::process::{Child, Command, ExitStatus, Stdio};
9
10
use anyhow::Context;
11
use rustix::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
12
use rustix::termios::{Winsize, tcsetwinsize};
13
14
/// A running child process attached to a PTY master.
15
#[derive(Debug)]
16
pub struct Pty {
17
    master: OwnedFd,
18
    child: Child,
19
}
20
21
impl Pty {
22
    /// Open a PTY, size it to `cols`x`rows`, and exec the user's login shell on
23
    /// the slave end with `TERM=term`.
24
    pub fn spawn(cols: u16, rows: u16, term: &str) -> anyhow::Result<Self> {
25
        let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)
26
            .context("open pty master")?;
27
        grantpt(&master).context("grantpt")?;
28
        unlockpt(&master).context("unlockpt")?;
29
        let slave = ioctl_tiocgptpeer(&master, OpenptFlags::RDWR | OpenptFlags::NOCTTY)
30
            .context("open pty slave")?;
31
32
        set_winsize(&master, cols, rows)?;
33
34
        let shell = std::env::var_os("SHELL").unwrap_or_else(|| "/bin/sh".into());
35
        let argv0 = login_argv0(&shell);
36
37
        // Hand the slave to the child's stdio. try_clone gives O_CLOEXEC dups, so
38
        // the parent's copies and the controlling-tty handle vanish at exec.
39
        let ctty = slave.try_clone().context("dup slave")?;
40
        let (stdin, stdout, stderr) = (
41
            slave.try_clone().context("dup slave")?,
42
            slave.try_clone().context("dup slave")?,
43
            slave,
44
        );
45
46
        let mut cmd = Command::new(&shell);
47
        cmd.arg0(&argv0)
48
            .env("TERM", term)
49
            .env_remove("COLUMNS")
50
            .env_remove("LINES")
51
            .env_remove("TERMCAP")
52
            .stdin(Stdio::from(stdin))
53
            .stdout(Stdio::from(stdout))
54
            .stderr(Stdio::from(stderr));
55
56
        // SAFETY: setsid and the TIOCSCTTY ioctl are async-signal-safe raw
57
        // syscalls; ctty is a valid fd captured by move. We touch no parent
58
        // heap state, satisfying pre_exec's contract.
59
        unsafe {
60
            cmd.pre_exec(move || {
61
                rustix::process::setsid()?;
62
                rustix::process::ioctl_tiocsctty(&ctty)?;
63
                Ok(())
64
            });
65
        }
66
67
        let child = cmd.spawn().context("spawn shell")?;
68
        Ok(Self { master, child })
69
    }
70
71
    /// The PTY master, for reading child output and writing input.
72
    pub fn master(&self) -> &OwnedFd {
73
        &self.master
74
    }
75
76
    /// Inform the kernel (and thus the child) of a new terminal size.
77
    pub fn resize(&self, cols: u16, rows: u16) -> anyhow::Result<()> {
78
        set_winsize(&self.master, cols, rows)
79
    }
80
81
    /// Reap the child if it has exited.
82
    pub fn wait(&mut self) -> io::Result<ExitStatus> {
83
        self.child.wait()
84
    }
85
}
86
87
fn set_winsize(master: &OwnedFd, cols: u16, rows: u16) -> anyhow::Result<()> {
88
    let ws = Winsize {
89
        ws_row: rows,
90
        ws_col: cols,
91
        ws_xpixel: 0,
92
        ws_ypixel: 0,
93
    };
94
    tcsetwinsize(master.as_fd(), ws).context("set pty winsize")
95
}
96
97
/// Login-shell argv[0] is the shell's basename with a leading '-'.
98
fn login_argv0(shell: &OsStr) -> std::ffi::OsString {
99
    let name = Path::new(shell)
100
        .file_name()
101
        .unwrap_or_else(|| OsStr::new("sh"));
102
    let mut argv0 = std::ffi::OsString::from("-");
103
    argv0.push(name);
104
    argv0
105
}
106
107
#[cfg(test)]
108
mod tests {
109
    use super::*;
110
111
    #[test]
112
    fn argv0_is_dash_prefixed_basename() {
113
        assert_eq!(login_argv0(OsStr::new("/usr/bin/bash")), "-bash");
114
        assert_eq!(login_argv0(OsStr::new("zsh")), "-zsh");
115
        assert_eq!(login_argv0(OsStr::new("")), "-sh");
116
    }
117
}