brewery
notashelf /
a3c41c6ccb204abc6cea3025ebf51ba02b0780ec

beer

public

A terminal worth pouring time into

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/beer.git
src/pty.rsRust112 lines3.5 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=beer`.
24
    pub fn spawn(cols: u16, rows: u16) -> 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", "beer")
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
    /// Reap the child if it has exited.
77
    pub fn wait(&mut self) -> io::Result<ExitStatus> {
78
        self.child.wait()
79
    }
80
}
81
82
fn set_winsize(master: &OwnedFd, cols: u16, rows: u16) -> anyhow::Result<()> {
83
    let ws = Winsize {
84
        ws_row: rows,
85
        ws_col: cols,
86
        ws_xpixel: 0,
87
        ws_ypixel: 0,
88
    };
89
    tcsetwinsize(master.as_fd(), ws).context("set pty winsize")
90
}
91
92
/// Login-shell argv[0] is the shell's basename with a leading '-'.
93
fn login_argv0(shell: &OsStr) -> std::ffi::OsString {
94
    let name = Path::new(shell)
95
        .file_name()
96
        .unwrap_or_else(|| OsStr::new("sh"));
97
    let mut argv0 = std::ffi::OsString::from("-");
98
    argv0.push(name);
99
    argv0
100
}
101
102
#[cfg(test)]
103
mod tests {
104
    use super::*;
105
106
    #[test]
107
    fn argv0_is_dash_prefixed_basename() {
108
        assert_eq!(login_argv0(OsStr::new("/usr/bin/bash")), "-bash");
109
        assert_eq!(login_argv0(OsStr::new("zsh")), "-zsh");
110
        assert_eq!(login_argv0(OsStr::new("")), "-sh");
111
    }
112
}