From d04dcfffa101bb604e16ba980c7031cdc26c3c6c Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Wed, 28 Oct 2026 02:09:34 +0100 Subject: [PATCH] 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 --- crates/domain/src/syscall.rs | 10 +- crates/init/src/manifest.rs | 4 +- crates/kernel/src/abi.rs | 7 +- crates/kernel/src/syscall/ipc/endpoint.rs | 11 +- crates/kernel/src/syscall/ipc/mod.rs | 7 + crates/kernel/src/syscall/ipc/saved.rs | 52 +++++ crates/kernel/src/syscall/mod.rs | 8 + crates/kernel/src/teardown.rs | 6 +- crates/kernel/src/tests/abi.rs | 7 + crates/kernel/src/tests/ipc.rs | 3 + crates/kernel/src/tests/ipc_saved.rs | 192 ++++++++++++++++++ crates/libkos/src/syscall/ipc.rs | 13 ++ crates/servers/terminal-server/src/lib.rs | 18 +- crates/servers/terminal-server/src/line.rs | 94 +++++++++ crates/servers/terminal-server/src/server.rs | 151 ++++++++------ .../terminal-server/src/tests/server.rs | 71 ++++++- crates/types/src/bounded.rs | 2 + crates/types/src/syscall_abi.rs | 2 + docs/ipc.md | 24 ++- 19 files changed, 588 insertions(+), 94 deletions(-) create mode 100644 crates/kernel/src/syscall/ipc/saved.rs create mode 100644 crates/kernel/src/tests/ipc_saved.rs create mode 100644 crates/servers/terminal-server/src/line.rs diff --git a/crates/domain/src/syscall.rs b/crates/domain/src/syscall.rs index 3cfcf4d..814b245 100644 --- a/crates/domain/src/syscall.rs +++ b/crates/domain/src/syscall.rs @@ -1,7 +1,7 @@ use types::{ address::{DataRef, EntryPoint, StackPointer, VirtualAddress}, badge::Badge, - bounded::{CapSlot, PageCount}, + bounded::{CapSlot, PageCount, SavedReply}, mapping::PagePerms, permission::PermissionSet, time::Ticks, @@ -104,6 +104,14 @@ pub enum Syscall { notification: CapSlot, deadline: Ticks, }, + /// Keeps the caller this thread owes a reply to, to answer later + /// (`ReplySaved`) while it receives others; returns which it is. + SaveReply, + /// Answers a caller kept with `SaveReply`, once. + ReplySaved { + reply: SavedReply, + message: Message, + }, /// Removes a capability from the caller's own table. DropCap { slot: CapSlot, diff --git a/crates/init/src/manifest.rs b/crates/init/src/manifest.rs index fb70cf3..35bdfd6 100644 --- a/crates/init/src/manifest.rs +++ b/crates/init/src/manifest.rs @@ -107,8 +107,8 @@ pub const SERVERS: &[Program] = &[ ..server("display", b"bin/display", Limits { pages: 1280, regions: 4, ..SMALL }) }, // Text on the display, lines from the keyboard (it takes input focus). - // Its clients wait on lines and Ctrl+C; `READ` waits for the user. - Program { uses: &["display"], restarts_clients: true, pinged: false, ..server("terminal", b"bin/terminal", SMALL) }, + // Its clients wait on lines and Ctrl+C. + Program { uses: &["display"], restarts_clients: true, ..server("terminal", b"bin/terminal", SMALL) }, server("session", b"bin/session", SMALL), ]; diff --git a/crates/kernel/src/abi.rs b/crates/kernel/src/abi.rs index edc4232..26b0e11 100644 --- a/crates/kernel/src/abi.rs +++ b/crates/kernel/src/abi.rs @@ -6,7 +6,7 @@ use domain::{message::Message, syscall::Syscall}; use types::{ address::{DataRef, EntryPoint, StackPointer, VirtualAddress}, badge::Badge, - bounded::{CapSlot, PageCount}, + bounded::{CapSlot, PageCount, SavedReply}, permission::PermissionSet, syscall_abi::*, }; @@ -151,6 +151,11 @@ pub fn decode(nr: usize, a: &[usize; 6]) -> Option { notification: slot(a[0])?, deadline: types::time::Ticks::from_raw(a[1] as u64), }, + SYS_SAVE_REPLY => Syscall::SaveReply, + SYS_REPLY_SAVED => Syscall::ReplySaved { + reply: SavedReply::new(u8::try_from(a[0]).ok()?)?, + message: message(a)?, + }, SYS_DEBUG_PUTCHAR => Syscall::DebugPutchar { console: slot(a[0])?, byte: a[1] as u8, diff --git a/crates/kernel/src/syscall/ipc/endpoint.rs b/crates/kernel/src/syscall/ipc/endpoint.rs index 575da67..7bee6ba 100644 --- a/crates/kernel/src/syscall/ipc/endpoint.rs +++ b/crates/kernel/src/syscall/ipc/endpoint.rs @@ -13,7 +13,7 @@ use types::{ }; use super::super::{Delivery, DeliveryKind, SyscallOutcome, SyscallResult}; -use super::Outgoing; +use super::{Outgoing, ThreadRef}; use crate::Kernel; impl Kernel

{ @@ -226,11 +226,17 @@ impl Kernel

{ .ipc_mut(tid) .and_then(|s| s.reply_to.take()) .ok_or(KernelError::NoReply)?; + self.answer(pid, caller, message); + Ok(()) + } + + /// Wakes `caller` with `message` from `pid`, if it is still waiting. + pub(super) fn answer(&mut self, pid: ProcessId, caller: ThreadRef, message: Message) { let waiting = self.is_live(caller) && self.is_blocked_as(caller.tid, |r| *r == BlockReason::AwaitingReply); if !waiting { // The caller died; the reply has nowhere to go. - return Ok(()); + return; } let to = self.thread_process(caller.tid); // A reply may always carry a capability the server can delegate. @@ -242,7 +248,6 @@ impl Kernel

{ cap, }; self.wake(caller.tid, SyscallResult::Received(delivery)); - Ok(()) } pub(crate) fn reply_recv( diff --git a/crates/kernel/src/syscall/ipc/mod.rs b/crates/kernel/src/syscall/ipc/mod.rs index 77144b0..e77bb5c 100644 --- a/crates/kernel/src/syscall/ipc/mod.rs +++ b/crates/kernel/src/syscall/ipc/mod.rs @@ -4,6 +4,7 @@ mod endpoint; mod events; mod irq; mod notification; +mod saved; use domain::{ error::KernelError, @@ -44,13 +45,19 @@ pub struct IpcState { pub(crate) outgoing: Option, /// The caller this thread owes a reply to. pub(crate) reply_to: Option, + /// Callers it kept to answer later (`SaveReply`). + pub(crate) saved: [Option; SAVED], pub(crate) bound: Option, } +/// Callers one thread can keep waiting. +pub(crate) const SAVED: usize = types::bounded::SavedReply::MAX as usize + 1; + impl IpcState { pub const EMPTY: Self = Self { outgoing: None, reply_to: None, + saved: [None; SAVED], bound: None, }; } diff --git a/crates/kernel/src/syscall/ipc/saved.rs b/crates/kernel/src/syscall/ipc/saved.rs new file mode 100644 index 0000000..07d4e28 --- /dev/null +++ b/crates/kernel/src/syscall/ipc/saved.rs @@ -0,0 +1,52 @@ +//! Replies kept for later (docs/ipc.md): a server that cannot answer a call +//! yet (it waits on the user, or on another server) keeps the caller and +//! receives others meanwhile. The kept caller is named only within the +//! thread that kept it, so it can be neither passed on nor forged, and it +//! is released with an error when that thread dies. + +use domain::{error::KernelError, message::Message}; +use platform::Platform; +use types::{ + bounded::SavedReply, + id::{ProcessId, ThreadId}, +}; + +use crate::Kernel; + +impl Kernel

{ + /// Moves the caller `tid` owes a reply to into its first free slot. + pub(crate) fn save_reply(&mut self, tid: ThreadId) -> Result { + let s = self.ipc_mut(tid).ok_or(KernelError::NoReply)?; + let caller = s.reply_to.ok_or(KernelError::NoReply)?; + let (index, free) = s + .saved + .iter_mut() + .enumerate() + .find(|(_, c)| c.is_none()) + .ok_or(KernelError::NoFreeSlot)?; + let reply = u8::try_from(index) + .ok() + .and_then(SavedReply::new) + .ok_or(KernelError::NoFreeSlot)?; + *free = Some(caller); + s.reply_to = None; + Ok(reply) + } + + /// Answers the caller kept as `reply`; it is forgotten either way. + pub(crate) fn reply_saved( + &mut self, + tid: ThreadId, + pid: ProcessId, + reply: SavedReply, + message: Message, + ) -> Result<(), KernelError> { + let caller = self + .ipc_mut(tid) + .and_then(|s| s.saved.get_mut(usize::from(reply.value()))) + .and_then(Option::take) + .ok_or(KernelError::NoReply)?; + self.answer(pid, caller, message); + Ok(()) + } +} diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index 4543d50..e826380 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -138,6 +138,14 @@ impl Kernel

{ Syscall::ReplyRecv { endpoint, message } => { self.reply_recv(tid, pid, endpoint, message) } + Syscall::SaveReply => complete( + self.save_reply(tid) + .map(|r| SyscallResult::Value(usize::from(r.value()))), + ), + Syscall::ReplySaved { reply, message } => complete( + self.reply_saved(tid, pid, reply, message) + .map(|()| SyscallResult::Ok), + ), Syscall::CreateNotification => { complete(self.create_notification_cap(pid).map(SyscallResult::Slot)) } diff --git a/crates/kernel/src/teardown.rs b/crates/kernel/src/teardown.rs index c01a559..a655521 100644 --- a/crates/kernel/src/teardown.rs +++ b/crates/kernel/src/teardown.rs @@ -46,7 +46,11 @@ impl Kernel

{ n.remove_thread(*tid); } let ipc = self.ipc_mut(*tid).map(core::mem::take).unwrap_or_default(); - if let Some(caller) = ipc.reply_to { + for caller in ipc + .reply_to + .into_iter() + .chain(ipc.saved.into_iter().flatten()) + { self.release_caller(caller); } self.evict(*tid); diff --git a/crates/kernel/src/tests/abi.rs b/crates/kernel/src/tests/abi.rs index b1fee40..148f537 100644 --- a/crates/kernel/src/tests/abi.rs +++ b/crates/kernel/src/tests/abi.rs @@ -44,6 +44,8 @@ fn every_defined_syscall_decodes() { SYS_REGION_ADDRESS, SYS_WATCH_DEATH, SYS_SET_TIMER, + SYS_SAVE_REPLY, + SYS_REPLY_SAVED, ]; // slot 1, slot 2, small values, no cap let args = [1, 2, 1, 0, 0, NONE]; @@ -61,6 +63,11 @@ fn retired_and_unknown_numbers_do_not_decode() { #[test] fn out_of_range_arguments_do_not_decode() { + assert!( + decode(SYS_REPLY_SAVED, &[8, 0, 0, 0, 0, NONE]).is_none(), + "a thread keeps 8 replies" + ); + assert!(decode(SYS_REPLY_SAVED, &[7, 0, 0, 0, 0, NONE]).is_some()); assert!(decode(SYS_RECEIVE, &[64, 0, 0, 0, 0, 0]).is_none()); assert!( decode(SYS_SEND, &[1, 0, 0, 0, 0, 64]).is_none(), diff --git a/crates/kernel/src/tests/ipc.rs b/crates/kernel/src/tests/ipc.rs index 8454c92..d568fb5 100644 --- a/crates/kernel/src/tests/ipc.rs +++ b/crates/kernel/src/tests/ipc.rs @@ -110,6 +110,9 @@ fn error(outcome: &SyscallOutcome) -> Option<&KernelError> { } } +#[path = "ipc_saved.rs"] +mod saved; + // ---- rendezvous ---- #[test] diff --git a/crates/kernel/src/tests/ipc_saved.rs b/crates/kernel/src/tests/ipc_saved.rs new file mode 100644 index 0000000..9e4f5b0 --- /dev/null +++ b/crates/kernel/src/tests/ipc_saved.rs @@ -0,0 +1,192 @@ +//! Replies kept for later: `SaveReply` and `ReplySaved`. + +use super::*; +use types::bounded::SavedReply; + +/// A call from `client` that `server` receives and keeps; which one. +fn call_and_save(t: &mut Ipc, client: ThreadId, word: usize) -> SavedReply { + let (tb, a_send, b_recv) = (t.tb, t.a_send, t.b_recv); + t.run( + client, + Syscall::Call { + endpoint: a_send, + message: msg([word, 0, 0, 0]), + }, + ); + t.run(tb, Syscall::Receive { endpoint: b_recv }); + match t.run(tb, Syscall::SaveReply) { + SyscallOutcome::Complete(SyscallResult::Value(v)) => SavedReply::new(v as u8).unwrap(), + _ => panic!("not saved"), + } +} + +fn reply_saved(t: &mut Ipc, reply: SavedReply, word: usize) -> SyscallOutcome { + let tb = t.tb; + t.run( + tb, + Syscall::ReplySaved { + reply, + message: msg([word, 0, 0, 0]), + }, + ) +} + +fn client(t: &mut Ipc) -> ThreadId { + t.k.spawn_thread_for_test(t.a, Priority::DEFAULT, TimeSlice::DEFAULT) +} + +#[test] +fn a_saved_caller_is_answered_after_others_are_served() { + let mut t = Ipc::new(); + let (ta, tb, b_recv) = (t.ta, t.tb, t.b_recv); + let first = call_and_save(&mut t, ta, 1); + // The server goes on: the next caller is received and answered first. + let other = client(&mut t); + let a_send = t.a_send; + t.run( + other, + Syscall::Call { + endpoint: a_send, + message: msg([2, 0, 0, 0]), + }, + ); + let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap(); + assert_eq!(d.words[0], 2); + assert!( + matches!(t.state(ta), Some(ThreadState::Blocked(_))), + "the saved caller still waits" + ); + t.run( + tb, + Syscall::Reply { + message: msg([20, 0, 0, 0]), + }, + ); + assert_eq!(pending_delivery(t.woken_with(other)).unwrap().words[0], 20); + assert!(matches!( + reply_saved(&mut t, first, 10), + SyscallOutcome::Complete(SyscallResult::Ok) + )); + assert!(matches!(t.state(ta), Some(ThreadState::Ready))); + assert_eq!(pending_delivery(t.woken_with(ta)).unwrap().words[0], 10); +} + +#[test] +fn nothing_to_save_without_a_pending_call() { + let mut t = Ipc::new(); + let tb = t.tb; + assert!(matches!( + error(&t.run(tb, Syscall::SaveReply)), + Some(KernelError::NoReply) + )); + // Saving takes it: there is no reply pending any more. + let ta = t.ta; + call_and_save(&mut t, ta, 0); + assert!(matches!( + error(&t.run( + tb, + Syscall::Reply { + message: msg([0; 4]) + } + )), + Some(KernelError::NoReply) + )); +} + +#[test] +fn a_saved_reply_is_answered_once() { + let mut t = Ipc::new(); + let ta = t.ta; + let r = call_and_save(&mut t, ta, 0); + assert!(error(&reply_saved(&mut t, r, 1)).is_none()); + assert!(matches!( + error(&reply_saved(&mut t, r, 2)), + Some(KernelError::NoReply) + )); + assert!( + matches!( + error(&reply_saved(&mut t, SavedReply::new(5).unwrap(), 2)), + Some(KernelError::NoReply) + ), + "never saved" + ); +} + +#[test] +fn a_receive_does_not_release_saved_callers() { + let mut t = Ipc::new(); + let (ta, tb, b_recv) = (t.ta, t.tb, t.b_recv); + call_and_save(&mut t, ta, 0); + // Nothing queued: the server just waits, the saved caller with it. + assert!(blocked(&t.run(tb, Syscall::Receive { endpoint: b_recv }))); + assert!(matches!(t.state(ta), Some(ThreadState::Blocked(_)))); + assert!(t.woken_with(ta).is_none()); +} + +#[test] +fn a_thread_keeps_eight_callers_at_most() { + let mut t = Ipc::new(); + let (tb, a_send, b_recv) = (t.tb, t.a_send, t.b_recv); + let ta = t.ta; + call_and_save(&mut t, ta, 0); + for i in 1..=usize::from(SavedReply::MAX) { + let c = client(&mut t); + assert_eq!(usize::from(call_and_save(&mut t, c, i).value()), i); + } + let late = client(&mut t); + t.run( + late, + Syscall::Call { + endpoint: a_send, + message: msg([9, 0, 0, 0]), + }, + ); + t.run(tb, Syscall::Receive { endpoint: b_recv }); + assert!(matches!( + error(&t.run(tb, Syscall::SaveReply)), + Some(KernelError::NoFreeSlot) + )); + // Still the pending reply: it can be answered the usual way. + t.run( + tb, + Syscall::Reply { + message: msg([90, 0, 0, 0]), + }, + ); + assert_eq!(pending_delivery(t.woken_with(late)).unwrap().words[0], 90); +} + +#[test] +fn server_death_releases_its_saved_callers() { + let mut t = Ipc::new(); + let (ta, b) = (t.ta, t.b); + call_and_save(&mut t, ta, 0); + let other = client(&mut t); + call_and_save(&mut t, other, 1); + t.k.destroy_process(b); + for c in [ta, other] { + assert!(matches!(t.state(c), Some(ThreadState::Ready))); + assert!(matches!( + t.woken_with(c), + Some(SyscallResult::Error(KernelError::NoReply)) + )); + } +} + +#[test] +fn a_saved_reply_to_a_caller_that_died_goes_nowhere() { + let mut t = Ipc::new(); + let other = client(&mut t); + let r = call_and_save(&mut t, other, 0); + // Killed while waiting for the reply (its process holds others). + t.k.cleanup_threads(&[Some(other)]); + let reborn = client(&mut t); + assert!( + error(&reply_saved(&mut t, r, 7)).is_none(), + "answering a dead caller is not the server's error" + ); + assert!( + t.woken_with(reborn).is_none(), + "and a thread that reused its id hears nothing" + ); +} diff --git a/crates/libkos/src/syscall/ipc.rs b/crates/libkos/src/syscall/ipc.rs index 8c72f55..0ecba91 100644 --- a/crates/libkos/src/syscall/ipc.rs +++ b/crates/libkos/src/syscall/ipc.rs @@ -86,6 +86,19 @@ pub fn reply(words: [usize; 4], cap: Option) -> Result<()> { raw(SYS_REPLY, message_regs(0, words, cap)).map(|_| ()) } +/// Keeps the caller this thread owes a reply to, to answer later with +/// `reply_saved` while it receives others; the reply is not pending any +/// more, so nothing answers it in the meantime. +pub fn save_reply() -> Result { + let r = raw(SYS_SAVE_REPLY, [0; 6])?; + u8::try_from(r[1]).ok().and_then(types::bounded::SavedReply::new).ok_or(Error::InvalidArgument) +} + +/// Answers a caller kept with `save_reply`. +pub fn reply_saved(reply: types::bounded::SavedReply, words: [usize; 4], cap: Option) -> Result<()> { + raw(SYS_REPLY_SAVED, message_regs(usize::from(reply.value()), words, cap)).map(|_| ()) +} + /// Replies to the pending `Call` (if any), then receives the next message. pub fn reply_recv(endpoint: CapSlot, words: [usize; 4], cap: Option) -> Result { raw( diff --git a/crates/servers/terminal-server/src/lib.rs b/crates/servers/terminal-server/src/lib.rs index dcf7eb3..777ee72 100644 --- a/crates/servers/terminal-server/src/lib.rs +++ b/crates/servers/terminal-server/src/lib.rs @@ -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 { 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 { + 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); diff --git a/crates/servers/terminal-server/src/line.rs b/crates/servers/terminal-server/src/line.rs new file mode 100644 index 0000000..f1ff12f --- /dev/null +++ b/crates/servers/terminal-server/src/line.rs @@ -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, + 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() + } +} diff --git a/crates/servers/terminal-server/src/server.rs b/crates/servers/terminal-server/src/server.rs index caa6768..14886f5 100644 --- a/crates/servers/terminal-server/src/server.rs +++ b/crates/servers/terminal-server/src/server.rs @@ -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; + /// Keeps the caller of the request being handled, to answer later. + fn save_reply(&mut self) -> Option; + 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 { 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 TerminalServer { 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 TerminalServer { } } - /// 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 { 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 TerminalServer { } } - /// 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 Server for TerminalServer { fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option) -> ([usize; 4], Option) { 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(); diff --git a/crates/servers/terminal-server/src/tests/server.rs b/crates/servers/terminal-server/src/tests/server.rs index dc69b68..c2991c0 100644 --- a/crates/servers/terminal-server/src/tests/server.rs +++ b/crates/servers/terminal-server/src/tests/server.rs @@ -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, forgotten: Vec, + 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 { - (!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 { + 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 { + 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)> = 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()); +} diff --git a/crates/types/src/bounded.rs b/crates/types/src/bounded.rs index dfde17a..a68200e 100644 --- a/crates/types/src/bounded.rs +++ b/crates/types/src/bounded.rs @@ -3,6 +3,8 @@ use crate::macros::bounded; // Kani runs the real kernel with tiny tables; see kernel/src/proofs. bounded!(CapSlot, u8, 0, if cfg!(kani) { 3 } else { 63 }); bounded!(Priority, u8, 0, 31); +// A caller a server thread saved to answer later; a thread keeps up to 8. +bounded!(SavedReply, u8, 0, 7); // Up to a process's whole address window (32 MiB): one request may be big (a // KDF's memory); budgets bound how much anyone gets. bounded!(PageCount, u16, 1, 8192); diff --git a/crates/types/src/syscall_abi.rs b/crates/types/src/syscall_abi.rs index 4d58fb9..f7e7d75 100644 --- a/crates/types/src/syscall_abi.rs +++ b/crates/types/src/syscall_abi.rs @@ -43,6 +43,8 @@ pub const SYS_MAP_REGION: usize = 34; pub const SYS_REGION_ADDRESS: usize = 35; pub const SYS_WATCH_DEATH: usize = 36; pub const SYS_SET_TIMER: usize = 37; +pub const SYS_SAVE_REPLY: usize = 39; +pub const SYS_REPLY_SAVED: usize = 40; /// `PagePerms` in a register. pub const PERMS_READ_ONLY: usize = 0; diff --git a/docs/ipc.md b/docs/ipc.md index 38749e7..34691eb 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -141,9 +141,11 @@ init's calls each server with `server_framework::PING` every 2 s, which the framework's loop answers without the server's code. A ping unanswered for 5 s means the server is stuck in a handler; init kills it, and the death is handled as a crash, so the restarted server answers the -still-queued ping. The terminal and the permission manager are not -pinged: their handlers wait for the user (`READ`, a consent prompt), -which a ping cannot tell from a hang. +still-queued ping. The permission manager is not pinged: while it asks +the user for consent it is itself waiting on the terminal inside a +handler, which a ping cannot tell from a hang. (The terminal does not +wait inside one: a `READ` before the line is typed is saved, `SaveReply`, +and answered when Enter is.) A program's `uses` endpoints arrive as badged copies with `Execute` and `Delegate`: the badge (unique per copy) tells the server which client is @@ -262,6 +264,13 @@ never reveals the sender's process or thread id. - `Reply(msg)`: deliver `msg` to the thread in the caller's reply slot and clear the slot. Error if the slot is empty. Never blocks. - `ReplyRecv(ep, msg)`: `Reply` if the slot is full, then `Receive(ep)`. +- `SaveReply`: move the caller in the reply slot into the thread's table of + saved replies (8 places) and return which place, a `SavedReply`; the + slot is empty again, so the thread can receive others. `NoReply` if the + slot is empty, `NoFreeSlot` if the table is full (the reply stays + pending). +- `ReplySaved(reply, msg)`: deliver `msg` to the caller saved there, as + `Reply` would, and free the place. `NoReply` if nothing is saved there. - `Signal(ntfn)`: let `b` be the capability's badge, or `1` if unbadged. If a waiter is blocked, wake the first with `b`. Else if the bound thread is blocked in `Receive`, wake it with `b`. Else `bits |= b`. @@ -285,9 +294,12 @@ set it stays masked. `NoReply` instead of blocking forever. - A reply slot names the caller by thread id **and generation**, so a reply can never reach a different thread that reused the id. -- If the server thread dies, its pending caller wakes with `NoReply`. -- If the caller dies, the reply slot is simply stale and `Reply` is a - successful no-op. +- If the server thread dies, its pending caller and every caller it saved + wake with `NoReply`. +- If the caller dies, the reply slot (or saved place) is simply stale and + `Reply` (`ReplySaved`) is a successful no-op. +- A saved reply is named only within the thread that saved it: it cannot + be passed on, and another thread's number means nothing. ## Budgets