RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / src / tty.rs
8541 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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
//! The console "tty": keyboard input, and the `File` behind file
//! descriptors 0, 1 and 2.
//!
//! Two input modes, like a Linux terminal:
//!
//! * **cooked** (canonical, the default): typed characters are echoed and
//!   collected in a line buffer (Backspace edits it); only when Enter is
//!   pressed does the whole line become readable from fd 0. So `read(0)`
//!   gets a complete, already-edited line. Programs like `cat` use this.
//! * **raw**: every key goes to the reader immediately and nothing is echoed;
//!   arrow keys arrive as ANSI escape sequences (`ESC [ A` = up). The shell
//!   switches to raw while it edits its command line, so it can do its own
//!   echo, Backspace and up-arrow history. (Linux: `tcsetattr` / `ioctl`.)
//!
//! In cooked mode Ctrl+C and Ctrl+Z don't become input: they send SIGINT /
//! SIGTSTP to the *foreground process group* (set by the shell with
//! ioctl TIOCSPGRP) -- that's how Ctrl+C stops the running program but not
//! the shell. (The virtio console, hvc0, has no job control.)
//!
//! Output (`Console::write`) understands two control characters besides
//! '\n': '\x08' erases the character before the cursor, and '\x0c' (form
//! feed) clears the screen.

use crate::errno::Errno;
use crate::fs::file::{File, KIND_CONSOLE, Stat};
use crate::signal;
use crate::thread::WaitQueue;
use crate::{print, serial_print, vga_buffer};
use alloc::string::String;
use conquer_once::spin::OnceCell;
use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use crossbeam_queue::ArrayQueue;
use pc_keyboard::KeyCode;
use spin::Mutex;

/// The line being typed in cooked mode (not yet readable).
static LINE: Mutex<String> = Mutex::new(String::new());
/// Bytes waiting for `read(0, ...)`: finished lines (cooked) or keys (raw).
static INPUT: OnceCell<ArrayQueue<u8>> = OnceCell::uninit();
/// Raw mode on?
static RAW: AtomicBool = AtomicBool::new(false);
/// pid of the process that switched raw mode on (0 = none). If it exits
/// without switching back, the console goes back to cooked mode.
static RAW_OWNER: AtomicU64 = AtomicU64::new(0);

/// The foreground process group: Ctrl+C / Ctrl+Z signal it (0 = none).
/// The shell sets it to each job it runs, and back to itself after.
static FOREGROUND: AtomicU64 = AtomicU64::new(0);

pub fn foreground() -> u64 {
    FOREGROUND.load(Ordering::SeqCst)
}

pub fn set_foreground(pgid: u64) {
    FOREGROUND.store(pgid, Ordering::SeqCst);
}

/// Threads blocked in `Console::read`, waiting for input.
static INPUT_WAIT: WaitQueue = WaitQueue::new();

fn input() -> &'static ArrayQueue<u8> {
    INPUT.get_or_init(|| ArrayQueue::new(4096))
}

/// Switch between raw (`true`) and cooked mode for process `pid`. Returns
/// the old mode.
pub fn set_raw(raw: bool, pid: u64) -> bool {
    LINE.lock().clear(); // a half-typed cooked line doesn't survive a switch
    RAW_OWNER.store(if raw { pid } else { 0 }, Ordering::SeqCst);
    RAW.swap(raw, Ordering::SeqCst)
}

/// A process ended: if it left the console in raw mode, undo that (so the
/// next program, or a respawned shell, gets normal line input).
pub fn process_exited(pid: u64) {
    if RAW_OWNER.compare_exchange(pid, 0, Ordering::SeqCst, Ordering::SeqCst).is_ok() {
        RAW.store(false, Ordering::SeqCst);
    }
}

pub fn is_raw() -> bool {
    RAW.load(Ordering::SeqCst)
}

/// Make `bytes` readable and wake readers. (Queue full: drop -- nobody reads.)
fn deliver(bytes: &[u8]) {
    for &b in bytes {
        let _ = input().push(b);
    }
    INPUT_WAIT.wake_all();
}

/// Called by the keyboard task for every typed character.
pub fn input_char(c: char) {
    if is_raw() {
        let mut utf8 = [0; 4];
        deliver(c.encode_utf8(&mut utf8).as_bytes());
        return;
    }
    let mut line = LINE.lock();
    match c {
        '\u{8}' => {
            // Backspace: only erase what's in the current line.
            if line.pop().is_some() {
                vga_buffer::backspace();
                serial_print!("\x08"); // uart_16550 turns this into "\x08 \x08"
            }
        }
        '\n' => {
            print!("\n");
            serial_print!("\n");
            line.push('\n');
            deliver(line.as_bytes());
            line.clear();
        }
        // Ctrl+C / Ctrl+Z: the "line discipline" turns them into signals
        // for the foreground job (Linux: the ISIG flag of termios). Raw mode
        // passes them through as bytes 3 / 26 instead (above).
        '\u{3}' | '\u{1a}' => {
            let (echo, sig) = if c == '\u{3}' { ("^C\n", signal::SIGINT) } else { ("^Z\n", signal::SIGTSTP) };
            print!("{}", echo);
            serial_print!("{}", echo);
            line.clear(); // the half-typed line is thrown away
            drop(line);
            let fg = foreground();
            if fg != 0 {
                crate::process::signal_group(fg, sig);
            }
        }
        c if (c as u32) < 0x20 && c != '\t' => {} // other control keys: ignore
        c => {
            print!("{}", c);
            serial_print!("{}", c);
            line.push(c);
        }
    }
}

/// Called by the keyboard task for keys that aren't characters (arrows...).
pub fn input_key(key: KeyCode) {
    if is_raw() {
        // The ANSI/VT100 sequences a Linux terminal would send.
        let seq: &[u8] = match key {
            KeyCode::ArrowUp => b"\x1b[A",
            KeyCode::ArrowDown => b"\x1b[B",
            KeyCode::ArrowRight => b"\x1b[C",
            KeyCode::ArrowLeft => b"\x1b[D",
            KeyCode::Home => b"\x1b[H",
            KeyCode::End => b"\x1b[F",
            KeyCode::Delete => b"\x1b[3~",
            _ => return,
        };
        deliver(seq);
    } else {
        print!("<{:?}>", key);
        serial_print!("<{:?}>", key);
    }
}

/// The console as a file: reading gets keyboard input, writing prints to the
/// screen and serial port.
pub struct Console;

/// Held for the whole of one console `write` (see `Console::write`).
static CONSOLE_WRITE: spin::Mutex<()> = spin::Mutex::new(());

impl File for Console {
    /// Blocks until input is available. Cooked: returns up to `buf.len()`
    /// bytes of a line, stopping after its '\n'. Raw: whatever keys are there.
    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {
        if buf.is_empty() {
            return Ok(0);
        }
        // Block until input arrives: the thread is parked on INPUT_WAIT
        // (using no CPU) and `deliver` wakes it. The closure runs with
        // interrupts off; the ArrayQueue is lock-free, so that's fine.
        INPUT_WAIT.wait_until_intr(|| {
            let mut n = 0;
            while n < buf.len() {
                let Some(b) = input().pop() else { break };
                buf[n] = b;
                n += 1;
                if b == b'\n' {
                    break;
                }
            }
            if n > 0 { Some(n) } else { None }
        })
    }

    fn write(&self, buf: &[u8]) -> Result<usize, Errno> {
        // One write = one unbroken piece of output, even when programs on
        // other CPUs write at the same time. Taken in thread context only
        // (never in an interrupt handler), so while waiting we just yield.
        let _turn = loop {
            match CONSOLE_WRITE.try_lock() {
                Some(guard) => break guard,
                None => crate::thread::yield_now(),
            }
        };
        // Print valid UTF-8 as is, anything else as '?'. Control characters
        // are handled one by one.
        for chunk in buf.utf8_chunks() {
            for piece in chunk.valid().split_inclusive(['\x08', '\x0c']) {
                let (text, control) = match piece.strip_suffix(['\x08', '\x0c']) {
                    Some(text) => (text, piece.chars().last()),
                    None => (piece, None),
                };
                print!("{}", text);
                serial_print!("{}", text);
                match control {
                    Some('\x08') => {
                        vga_buffer::backspace();
                        serial_print!("\x08");
                    }
                    Some(_) => vga_buffer::clear_screen(), // serial: nothing to clear
                    None => {}
                }
            }
            if !chunk.invalid().is_empty() {
                print!("?");
                serial_print!("?");
            }
        }
        Ok(buf.len())
    }

    fn stat(&self) -> Result<Stat, Errno> {
        Ok(Stat { kind: KIND_CONSOLE, size: 0, mode: 0 })
    }
}