RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / user / ptytest.rs
4187 bytes ยท raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//! 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);
        }
        write(fd, 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\"");

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