//! 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 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 } 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)); 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() }