feat(ipc): saved replies: SaveReply/ReplySaved (8 per thread, released on thread death, unforgeable); terminal READ saved until Enter (line.rs editor, reader queue), now pinged
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
extern crate alloc;
|
||||
|
||||
pub mod grid;
|
||||
pub mod line;
|
||||
pub mod server;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -77,15 +78,6 @@ impl server::Console for Screen {
|
||||
let _ = syscall::set_timer(self.timer, types::time::Ticks::from_raw(at));
|
||||
}
|
||||
|
||||
fn next_key(&mut self) -> u8 {
|
||||
loop {
|
||||
if let Some(key) = self.poll_key() {
|
||||
return key;
|
||||
}
|
||||
let _ = syscall::wait(self.notify);
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_key(&mut self) -> Option<u8> {
|
||||
while let Some(event) = display::poll_input(DISPLAY) {
|
||||
if let Some(key) = byte(event) {
|
||||
@@ -95,6 +87,14 @@ impl server::Console for Screen {
|
||||
None
|
||||
}
|
||||
|
||||
fn save_reply(&mut self) -> Option<types::bounded::SavedReply> {
|
||||
syscall::save_reply().ok()
|
||||
}
|
||||
|
||||
fn answer(&mut self, reply: types::bounded::SavedReply, words: [usize; 4]) {
|
||||
let _ = syscall::reply_saved(reply, words, None);
|
||||
}
|
||||
|
||||
fn take_focus(&mut self) {
|
||||
display::subscribe(DISPLAY, self.notify, Badge::new(KEYS_BIT));
|
||||
display::clear(DISPLAY, BG);
|
||||
|
||||
94
crates/servers/terminal-server/src/line.rs
Normal file
94
crates/servers/terminal-server/src/line.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
//! The line being typed: edited key by key and echoed into the grid (as
|
||||
//! `*` if it is secret) until Enter; Ctrl+C drops it, and it reads as an
|
||||
//! empty line. Once done it is read out in text-message chunks; a secret
|
||||
//! line is forgotten as soon as it is.
|
||||
|
||||
use types::log::{TEXT_BYTES, text_words};
|
||||
|
||||
use crate::grid::Grid;
|
||||
use crate::server::BACKSPACE;
|
||||
|
||||
const MAX_LINE: usize = 256;
|
||||
|
||||
pub struct Line {
|
||||
bytes: [u8; MAX_LINE],
|
||||
len: usize,
|
||||
/// How much of a finished line has been read.
|
||||
read: usize,
|
||||
/// Being typed, and whether it is secret.
|
||||
typing: Option<bool>,
|
||||
secret: bool,
|
||||
}
|
||||
|
||||
impl Line {
|
||||
pub const fn new() -> Self {
|
||||
Self { bytes: [0; MAX_LINE], len: 0, read: 0, typing: None, secret: false }
|
||||
}
|
||||
|
||||
pub fn start(&mut self, secret: bool) {
|
||||
self.len = 0;
|
||||
self.read = 0;
|
||||
self.typing = Some(secret);
|
||||
self.secret = secret;
|
||||
}
|
||||
|
||||
pub fn typing(&self) -> bool {
|
||||
self.typing.is_some()
|
||||
}
|
||||
|
||||
/// Finished, and not all read yet.
|
||||
pub fn unread(&self) -> bool {
|
||||
self.typing.is_none() && self.read < self.len
|
||||
}
|
||||
|
||||
/// One key of the line; true once it is done.
|
||||
pub fn key(&mut self, key: u8, grid: &mut Grid) -> bool {
|
||||
let Some(secret) = self.typing else { return false };
|
||||
match key {
|
||||
types::terminal::CTRL_C => {
|
||||
self.bytes[0] = b'\n';
|
||||
self.len = 1;
|
||||
for &b in b"^C\n" {
|
||||
grid.put(b);
|
||||
}
|
||||
}
|
||||
b'\n' => {
|
||||
self.bytes[self.len] = b'\n';
|
||||
self.len += 1;
|
||||
grid.put(b'\n');
|
||||
}
|
||||
BACKSPACE if self.len > 0 => {
|
||||
self.len -= 1;
|
||||
grid.back();
|
||||
return false;
|
||||
}
|
||||
// Room is kept for the closing newline.
|
||||
b if self.len + 1 < MAX_LINE && (b' '..=b'~').contains(&b) => {
|
||||
self.bytes[self.len] = b;
|
||||
self.len += 1;
|
||||
grid.put(if secret { b'*' } else { b });
|
||||
return false;
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
self.typing = None;
|
||||
true
|
||||
}
|
||||
|
||||
/// The next chunk of a finished line.
|
||||
pub fn chunk(&mut self) -> [usize; 4] {
|
||||
let end = (self.read + TEXT_BYTES).min(self.len);
|
||||
let words = text_words(&self.bytes[self.read..end]);
|
||||
self.read = end;
|
||||
if self.secret && self.read == self.len {
|
||||
self.bytes.fill(0);
|
||||
}
|
||||
words
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Line {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,11 @@
|
||||
//! behind `Console`.
|
||||
|
||||
use server_framework::{Encode, MALFORMED, Server};
|
||||
use types::bounded::CapSlot;
|
||||
use types::log::{TEXT_BYTES, text_of, text_words};
|
||||
use types::bounded::{CapSlot, SavedReply};
|
||||
use types::log::{text_of, text_words};
|
||||
|
||||
use crate::grid::Grid;
|
||||
use crate::line::Line;
|
||||
|
||||
/// What the terminal draws on and reads from.
|
||||
pub trait Console {
|
||||
@@ -17,10 +18,12 @@ pub trait Console {
|
||||
fn scroll(&mut self, rows: usize);
|
||||
/// Soon calls `TerminalServer::signal`, to flush a burst of output once.
|
||||
fn flush_later(&mut self);
|
||||
/// Blocks until a key is typed: a byte, `\n` for Enter, 8 for Backspace.
|
||||
fn next_key(&mut self) -> u8;
|
||||
/// A key already typed, without waiting.
|
||||
/// A key already typed, without waiting: a byte, `\n` for Enter, 8
|
||||
/// for Backspace.
|
||||
fn poll_key(&mut self) -> Option<u8>;
|
||||
/// Keeps the caller of the request being handled, to answer later.
|
||||
fn save_reply(&mut self) -> Option<SavedReply>;
|
||||
fn answer(&mut self, reply: SavedReply, words: [usize; 4]);
|
||||
/// Takes input focus back from whoever took it, and clears the screen.
|
||||
fn take_focus(&mut self);
|
||||
fn interrupt(&mut self, notification: CapSlot);
|
||||
@@ -29,17 +32,19 @@ pub trait Console {
|
||||
}
|
||||
|
||||
pub const BACKSPACE: u8 = 8;
|
||||
const MAX_LINE: usize = 256;
|
||||
/// Keys typed while no line is being read, kept for the next one.
|
||||
const TYPEAHEAD: usize = 64;
|
||||
/// Callers waiting for a line at once, at most.
|
||||
const READERS: usize = 4;
|
||||
|
||||
pub struct TerminalServer<C: Console> {
|
||||
console: C,
|
||||
grid: Grid,
|
||||
/// A typed line not yet handed out in full, and how much of it was.
|
||||
line: [u8; MAX_LINE],
|
||||
line_len: usize,
|
||||
line_read: usize,
|
||||
line: Line,
|
||||
/// Callers waiting for a line, oldest first, and whether theirs is
|
||||
/// secret: the oldest gets the line being typed.
|
||||
waiting: [Option<(SavedReply, bool)>; READERS],
|
||||
flush_pending: bool,
|
||||
/// How many cells of each row are drawn on the screen.
|
||||
drawn: [usize; crate::grid::MAX_ROWS],
|
||||
@@ -63,9 +68,8 @@ impl<C: Console> TerminalServer<C> {
|
||||
let mut s = Self {
|
||||
console,
|
||||
grid: Grid::new(cols, rows),
|
||||
line: [0; MAX_LINE],
|
||||
line_len: 0,
|
||||
line_read: 0,
|
||||
line: Line::new(),
|
||||
waiting: [None; READERS],
|
||||
flush_pending: false,
|
||||
drawn: [0; crate::grid::MAX_ROWS],
|
||||
typeahead: [0; TYPEAHEAD],
|
||||
@@ -131,20 +135,26 @@ impl<C: Console> TerminalServer<C> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A key typed ahead, or the next one.
|
||||
fn next_key(&mut self) -> u8 {
|
||||
/// A key typed ahead, or one just typed.
|
||||
fn take_key(&mut self) -> Option<u8> {
|
||||
if self.typeahead_len == 0 {
|
||||
return self.console.next_key();
|
||||
return self.console.poll_key();
|
||||
}
|
||||
let key = self.typeahead[0];
|
||||
self.typeahead.copy_within(1..self.typeahead_len, 0);
|
||||
self.typeahead_len -= 1;
|
||||
key
|
||||
Some(key)
|
||||
}
|
||||
|
||||
/// Keys typed while nobody reads a line: Ctrl+C interrupts, the rest
|
||||
/// wait for the next line.
|
||||
fn keys_arrived(&mut self) {
|
||||
if self.line.typing() {
|
||||
if self.type_keys() {
|
||||
self.serve_waiting();
|
||||
}
|
||||
return;
|
||||
}
|
||||
while let Some(key) = self.console.poll_key() {
|
||||
if key == types::terminal::CTRL_C {
|
||||
self.typeahead_len = 0;
|
||||
@@ -159,63 +169,73 @@ impl<C: Console> TerminalServer<C> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Edits a line from the keyboard, echoing it (as `*` if `secret`),
|
||||
/// until Enter. Ctrl+C drops it and reads as an empty line.
|
||||
fn read_line(&mut self, secret: bool) {
|
||||
fn begin_line(&mut self, secret: bool) {
|
||||
self.console.flush();
|
||||
// Flushed now; a flush timer still running is harmless.
|
||||
self.flush_pending = false;
|
||||
self.line_len = 0;
|
||||
self.line_read = 0;
|
||||
loop {
|
||||
match self.next_key() {
|
||||
types::terminal::CTRL_C => {
|
||||
self.line[0] = b'\n';
|
||||
self.line_len = 1;
|
||||
for &b in b"^C\n" {
|
||||
self.grid.put(b);
|
||||
}
|
||||
break;
|
||||
}
|
||||
b'\n' => {
|
||||
self.line[self.line_len] = b'\n';
|
||||
self.line_len += 1;
|
||||
self.grid.put(b'\n');
|
||||
break;
|
||||
}
|
||||
BACKSPACE if self.line_len > 0 => {
|
||||
self.line_len -= 1;
|
||||
self.grid.back();
|
||||
}
|
||||
BACKSPACE => continue,
|
||||
// Room is kept for the closing newline.
|
||||
b if self.line_len + 1 < MAX_LINE && (b' '..=b'~').contains(&b) => {
|
||||
self.line[self.line_len] = b;
|
||||
self.line_len += 1;
|
||||
self.grid.put(if secret { b'*' } else { b });
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
self.line.start(secret);
|
||||
}
|
||||
|
||||
/// Feeds the keys there are into the line; whether it is done.
|
||||
fn type_keys(&mut self) -> bool {
|
||||
let mut done = false;
|
||||
while !done && let Some(key) = self.take_key() {
|
||||
done = self.line.key(key, &mut self.grid);
|
||||
self.redraw();
|
||||
self.console.flush();
|
||||
}
|
||||
self.redraw();
|
||||
self.console.flush();
|
||||
done
|
||||
}
|
||||
|
||||
/// The next chunk of the typed line, reading one first if none is left.
|
||||
/// A secret line never reaches the screen, and is forgotten once read.
|
||||
fn read(&mut self, secret: bool) -> [usize; 4] {
|
||||
if self.line_read == self.line_len {
|
||||
self.read_line(secret);
|
||||
/// The oldest waiting caller gets the finished line; once it is all
|
||||
/// read, the next waiting caller's line starts.
|
||||
fn serve_waiting(&mut self) {
|
||||
loop {
|
||||
if self.line.unread() {
|
||||
let Some((reply, _)) = self.waiting[0] else { return };
|
||||
self.waiting.rotate_left(1);
|
||||
self.waiting[READERS - 1] = None;
|
||||
let chunk = self.line.chunk();
|
||||
self.console.answer(reply, chunk);
|
||||
// More of it comes when it asks again.
|
||||
if self.line.unread() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let Some((_, secret)) = self.waiting[0] else { return };
|
||||
self.begin_line(secret);
|
||||
if !self.type_keys() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let end = (self.line_read + TEXT_BYTES).min(self.line_len);
|
||||
let words = text_words(&self.line[self.line_read..end]);
|
||||
self.line_read = end;
|
||||
if secret && self.line_read == self.line_len {
|
||||
self.line.fill(0);
|
||||
}
|
||||
|
||||
/// The next chunk of the typed line now, if there is one; otherwise
|
||||
/// the caller is kept and answered once the line is typed.
|
||||
fn read(&mut self, secret: bool) -> Option<[usize; 4]> {
|
||||
if self.line.unread() {
|
||||
let chunk = self.line.chunk();
|
||||
if !self.line.unread() {
|
||||
self.serve_waiting();
|
||||
}
|
||||
return Some(chunk);
|
||||
}
|
||||
let nobody_waits = self.waiting[0].is_none();
|
||||
if nobody_waits && !self.line.typing() {
|
||||
self.begin_line(secret);
|
||||
}
|
||||
if nobody_waits && self.type_keys() {
|
||||
return Some(self.line.chunk());
|
||||
}
|
||||
let free = self.waiting.iter().position(Option::is_none);
|
||||
match (free, free.and_then(|_| self.console.save_reply())) {
|
||||
(Some(i), Some(reply)) => {
|
||||
self.waiting[i] = Some((reply, secret));
|
||||
None
|
||||
}
|
||||
// Too many waiting: an empty line.
|
||||
_ => Some(text_words(b"\n")),
|
||||
}
|
||||
words
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,8 +256,9 @@ impl<C: Console> Server for TerminalServer<C> {
|
||||
|
||||
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
||||
let reply = match words[0] {
|
||||
types::terminal::READ => self.read(false),
|
||||
types::terminal::READ_SECRET => self.read(true),
|
||||
// Kept for later: what goes back now is not sent.
|
||||
types::terminal::READ => self.read(false).unwrap_or([0; 4]),
|
||||
types::terminal::READ_SECRET => self.read(true).unwrap_or([0; 4]),
|
||||
types::terminal::REDRAW => {
|
||||
// Taking focus back clears the screen.
|
||||
self.console.take_focus();
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::server::{BACKSPACE, Console, TerminalServer};
|
||||
use server_framework::Server;
|
||||
use std::vec;
|
||||
use std::vec::Vec;
|
||||
use types::bounded::CapSlot;
|
||||
use types::bounded::{CapSlot, SavedReply};
|
||||
use types::log::{text_of, text_words};
|
||||
use types::terminal::{CTRL_C, KEYS_BIT, TIMER_BIT};
|
||||
|
||||
@@ -21,6 +21,9 @@ struct Fake {
|
||||
rows_drawn: Vec<(usize, usize)>,
|
||||
interrupted: Vec<CapSlot>,
|
||||
forgotten: Vec<CapSlot>,
|
||||
saved: u8,
|
||||
/// Callers answered later, with what.
|
||||
answered: Vec<(SavedReply, [usize; 4])>,
|
||||
}
|
||||
|
||||
impl Console for Fake {
|
||||
@@ -37,11 +40,17 @@ impl Console for Fake {
|
||||
fn flush_later(&mut self) {
|
||||
self.timers += 1;
|
||||
}
|
||||
fn next_key(&mut self) -> u8 {
|
||||
self.keys.remove(0)
|
||||
}
|
||||
/// The keys a test types for a read, then those typed meanwhile.
|
||||
fn poll_key(&mut self) -> Option<u8> {
|
||||
(!self.typed.is_empty()).then(|| self.typed.remove(0))
|
||||
let from = if self.keys.is_empty() { &mut self.typed } else { &mut self.keys };
|
||||
(!from.is_empty()).then(|| from.remove(0))
|
||||
}
|
||||
fn save_reply(&mut self) -> Option<SavedReply> {
|
||||
self.saved += 1;
|
||||
SavedReply::new(self.saved - 1)
|
||||
}
|
||||
fn answer(&mut self, reply: SavedReply, words: [usize; 4]) {
|
||||
self.answered.push((reply, words));
|
||||
}
|
||||
fn take_focus(&mut self) {
|
||||
self.focus_taken += 1;
|
||||
@@ -122,10 +131,12 @@ fn ctrl_c_while_a_program_runs_interrupts_it() {
|
||||
|
||||
#[test]
|
||||
fn keys_typed_ahead_start_the_next_line() {
|
||||
let mut t = term(b"s\n");
|
||||
let mut t = term(&[]);
|
||||
// Typed while nobody reads, then the rest during the read.
|
||||
t.console_mut_for_test().typed = b"l".to_vec();
|
||||
t.signal(KEYS_BIT);
|
||||
assert!(t.console().interrupted.is_empty());
|
||||
t.console_mut_for_test().typed = b"s\n".to_vec();
|
||||
assert_eq!(line_of(&mut t), b"ls\n");
|
||||
}
|
||||
|
||||
@@ -196,3 +207,51 @@ fn only_the_control_copy_takes_ctrl_c_and_the_latest_counts() {
|
||||
t.signal(KEYS_BIT);
|
||||
assert_eq!(t.console().interrupted, [CapSlot::new(9).unwrap()]);
|
||||
}
|
||||
|
||||
fn text(words: [usize; 4]) -> Vec<u8> {
|
||||
let (bytes, len) = text_of(words).unwrap();
|
||||
bytes[..len].to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_read_before_anything_is_typed_is_answered_once_the_line_is() {
|
||||
let mut t = term(&[]);
|
||||
t.handle_call(1, [types::terminal::READ, 0, 0, 0], None);
|
||||
assert_eq!(t.console().saved, 1, "the caller is kept");
|
||||
assert!(t.console().answered.is_empty());
|
||||
// Meanwhile the terminal serves others.
|
||||
t.handle_call(2, text_words(b"out"), None);
|
||||
t.console_mut_for_test().typed = b"hi".to_vec();
|
||||
t.signal(KEYS_BIT);
|
||||
assert!(t.console().answered.is_empty(), "not before Enter");
|
||||
t.console_mut_for_test().typed = vec![b'\n'];
|
||||
t.signal(KEYS_BIT);
|
||||
let (reply, words) = t.console().answered[0];
|
||||
assert_eq!((reply, text(words)), (SavedReply::new(0).unwrap(), b"hi\n".to_vec()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readers_wait_their_turn_each_for_a_line_of_their_own() {
|
||||
let mut t = term(&[]);
|
||||
t.handle_call(1, [types::terminal::READ, 0, 0, 0], None);
|
||||
t.handle_call(2, [types::terminal::READ_SECRET, 0, 0, 0], None);
|
||||
t.console_mut_for_test().typed = b"one\n".to_vec();
|
||||
t.signal(KEYS_BIT);
|
||||
assert_eq!(t.console().answered.len(), 1);
|
||||
t.console_mut_for_test().typed = b"two\n".to_vec();
|
||||
t.signal(KEYS_BIT);
|
||||
let answered: Vec<(u8, Vec<u8>)> = t.console().answered.iter().map(|(r, w)| (r.value(), text(*w))).collect();
|
||||
assert_eq!(answered, [(0, b"one\n".to_vec()), (1, b"two\n".to_vec())]);
|
||||
assert!(t.grid().row(1).starts_with(b"***"), "the second, secret line was echoed as stars");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_c_while_a_reader_waits_drops_the_line_and_interrupts_nobody() {
|
||||
let mut t = term(&[]);
|
||||
assert_eq!(on_interrupt(&mut t, 7), 0);
|
||||
t.handle_call(1, [types::terminal::READ, 0, 0, 0], None);
|
||||
t.console_mut_for_test().typed = vec![b'x', CTRL_C];
|
||||
t.signal(KEYS_BIT);
|
||||
assert_eq!(text(t.console().answered[0].1), b"\n");
|
||||
assert!(t.console().interrupted.is_empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user