//! ptytest -- checks pseudo-terminals (ptys) and the line discipline. //! //! A pty is a pair: the *master* side is held by a terminal emulator (or //! ssh, tmux, a compositor's terminal window), the *slave* side //! (/dev/pts/N) looks exactly like a real terminal to the program running //! on it. Between them sits the line discipline: in *canonical* mode it //! collects a whole line (with backspace editing) before the program can //! read it, echoes what's typed, turns ^C into SIGINT; in *raw* mode bytes //! pass straight through (what editors and games use). #![no_std] #![no_main] #[macro_use] #[path = "ulib.rs"] mod ulib; extern crate alloc; use alloc::format; use alloc::string::String; use ulib::*; /// Read whatever the other side has (waits up to 1 s for the first byte). fn read_some(fd: i64, buf: &mut [u8]) -> usize { let mut p = [PollFd { fd: fd as i32, events: POLLIN, revents: 0 }]; if poll(&mut p, 1000) <= 0 { return 0; } read(fd, buf).max(0) as usize } /// musl's ttyname(fd), step by step (stage 19): the name is what /// /proc/self/fd/N points to, but only if stat() of that name is the same /// file (st_dev and st_ino) as fstat(fd). Otherwise ENODEV. fn ttyname(fd: i64) -> Result { let mut buf = [0u8; 64]; let n = readlink(&format!("/proc/self/fd/{}", fd), &mut buf); if n < 0 { return Err(n); } let name = String::from(core::str::from_utf8(&buf[..n as usize]).map_err(|_| -EINVAL)?); let (mut a, mut b) = (Stat::default(), Stat::default()); if stat(&name, &mut a) != 0 || fstat(fd, &mut b) != 0 { return Err(-ENOENT); } if a.dev != b.dev || a.ino != b.ino { return Err(-ENODEV); } Ok(name) } /// Linux's device numbers (`makedev`). fn makedev(major: u64, minor: u64) -> u64 { ((major & 0xffff_f000) << 32) | ((major & 0xfff) << 8) | ((minor & 0xffff_ff00) << 12) | (minor & 0xff) } /// Every /dev node has one identity: stat twice, or stat and fstat of an /// open fd, agree; st_rdev and the mode are Linux's. fn device_identities(c: &mut Checks, master: i64, slave: i64, n: u32) { for (path, major, minor, perm) in [ ("/dev/null", 1, 3, 0o666), ("/dev/zero", 1, 5, 0o666), ("/dev/random", 1, 8, 0o666), ("/dev/urandom", 1, 9, 0o666), ("/dev/tty", 5, 0, 0o666), ("/dev/console", 5, 1, 0o600), ("/dev/ptmx", 5, 2, 0o666), ] { let (mut a, mut b, mut f) = (Stat::default(), Stat::default(), Stat::default()); stat(path, &mut a); stat(path, &mut b); let fd = if path == "/dev/tty" { -1 } else { open(path, O_RDWR | O_NOCTTY) }; let fd_same = fd < 0 || (fstat(fd, &mut f) == 0 && f.dev == a.dev && f.ino == a.ino && f.rdev == a.rdev && f.mode == a.mode); if fd >= 0 { close(fd); // (for /dev/ptmx: a new pty, gone again) } c.check( a.ino == b.ino && a.dev == b.dev && a.rdev == makedev(major, minor) && a.mode == S_IFCHR | perm && fd_same, &format!("{}: char device {}:{}, mode {:o}, one inode for stat and fstat", path, major, minor, perm), ); } let (mut a, mut m) = (Stat::default(), Stat::default()); stat(&format!("/dev/pts/{}", n), &mut a); c.check(a.rdev == makedev(136, n as u64) && a.mode == S_IFCHR | 0o620, "/dev/pts/N: char device 136:N, mode 620"); stat("/dev/ptmx", &mut a); fstat(master, &mut m); c.check(m.ino == a.ino && m.rdev == makedev(5, 2), "a pty master fstats as /dev/ptmx"); c.check(ttyname(slave) == Ok(format!("/dev/pts/{}", n)), "ttyname(pty slave) = /dev/pts/N"); let console = open("/dev/console", O_RDWR | O_NOCTTY); c.check(ttyname(console).as_deref() == Ok("/dev/console"), "ttyname(console fd) = /dev/console"); close(console); // The console fds the kernel gave us (if stdin is the console). if isatty(0) { c.check(ttyname(0).is_ok(), "ttyname(0) works for the console the kernel handed us"); } let mut buf = [0u8; 64]; let r = readlink("/proc/self/cwd", &mut buf); let mut cwd = [0u8; 64]; let k = getcwd(&mut cwd); c.check(r > 0 && r == k && buf[..r as usize] == cwd[..k as usize], "/proc/self/cwd links to the current directory"); } fn main() -> i64 { let mut c = Checks::new("ptytest"); let (master, n) = match openpty() { Ok(x) => x, Err(e) => { println!("ptytest: openpty: {}", strerror(e)); return 1; } }; let path = format!("/dev/pts/{}", n); let slave = open(&path, O_RDWR | O_NOCTTY); c.check(slave >= 0, &format!("open {} (the slave side)", path)); device_identities(&mut c, master, slave, n); let mut buf = [0u8; 64]; // Canonical mode: a line is readable only once it's complete. write(master, b"hel"); set_nonblocking(slave, true); c.check(read(slave, &mut buf) == -EAGAIN, "canonical: half a line can't be read yet (EAGAIN)"); write(master, b"lo\x7f\x7fp me\r"); // two backspaces, then Enter ('\r') let got = read_some(slave, &mut buf); c.check(&buf[..got] == b"help me\n", "canonical: backspace edits the line, Enter ('\\r') ends it as '\\n'"); let got = read_some(master, &mut buf); c.check(got > 0 && buf[..got].ends_with(b"\r\n"), "echo: what's typed comes back to the master"); // Output processing: the program's '\n' reaches the terminal as "\r\n". write(slave, b"out\n"); let got = read_some(master, &mut buf); c.check(&buf[..got] == b"out\r\n", "output: '\\n' becomes \"\\r\\n\" (ONLCR)"); // Raw mode: every byte at once, no echo, no conversions. let saved = tcgetattr(slave).unwrap_or_default(); let mut raw = saved; cfmakeraw(&mut raw); c.check(tcsetattr(slave, &raw) == 0, "TCSETS: switch to raw mode"); write(master, b"x\r\x03"); let got = read_some(slave, &mut buf); c.check(&buf[..got] == b"x\r\x03", "raw: bytes arrive at once, unchanged (even ^C)"); set_nonblocking(master, true); c.check(read(master, &mut buf) == -EAGAIN, "raw: no echo"); set_nonblocking(master, false); tcsetattr(slave, &saved); c.check(tcgetattr(slave).map(|t| t.lflag & ICANON != 0).unwrap_or(false), "TCGETS: canonical mode back on"); // (We keep `slave` open: with no slave open at all, the master reports // a hang-up (POLLHUP) instead of waiting for the child to open it.) // ^C: a child makes the pty its controlling terminal (setsid + open) // and waits; typing ^C on the master must kill it with SIGINT. let pid = fork(); if pid == 0 { setsid(); let fd = open(&path, O_RDWR); // session leader: becomes our ctty if fd < 0 { exit(3); } // /dev/tty is "my controlling terminal", whichever that is. let me = open("/dev/tty", O_RDWR); if me < 0 { exit(4); } write(me, b"ready\n"); loop { pause(); } } let got = read_some(master, &mut buf); // "ready\r\n" c.check(&buf[..got] == b"ready\r\n", "child opened the pty as its controlling terminal"); write(master, b"\x03"); let status = match wait4(pid, 0) { Ok((_, s)) => s, Err(_) => 0, }; c.check(wifsignaled(status) && wtermsig(status) == SIGINT, "^C on the master: SIGINT for the foreground job"); let got = read_some(master, &mut buf); c.check(got >= 2 && &buf[..2] == b"^C", "^C is echoed as \"^C\""); // ^\ is SIGQUIT (it would dump core on Unix). let pid = fork(); if pid == 0 { setsid(); if open(&path, O_RDWR) < 0 { exit(3); } loop { pause(); } } sleep_ms(100); // let the child open the pty first write(master, b"\x1c"); let status = match wait4(pid, 0) { Ok((_, s)) => s, Err(_) => 0, }; c.check(wifsignaled(status) && wtermsig(status) == SIGQUIT, "^\\ on the master: SIGQUIT"); set_nonblocking(master, true); while read(master, &mut buf) > 0 {} // drop the "^\\" echo set_nonblocking(master, false); // Without a controlling terminal, /dev/tty doesn't exist for us. let pid = fork(); if pid == 0 { setsid(); exit(if open("/dev/tty", O_RDWR) < 0 { 0 } else { 1 }); } c.check(matches!(wait4(pid, 0), Ok((_, 0))), "no controlling terminal: /dev/tty fails"); // Hang-up: closing the master gives the slave end-of-file. close(master); c.check(read(slave, &mut buf) == 0, "master closed: the slave reads end-of-file"); close(slave); c.done() }