feat(userspace): programs in /bin w/o .elf; bin manifests (needs = display) so shell runs snake; Ctrl+C kills foreground via terminal interrupt notification; ctrl keys; sleep

This commit is contained in:
2026-10-27 21:16:00 +01:00
parent 1fc5a54f06
commit 83809bbe94
33 changed files with 608 additions and 121 deletions

8
Cargo.lock generated
View File

@@ -319,6 +319,14 @@ dependencies = [
"types",
]
[[package]]
name = "sleep"
version = "0.1.0"
dependencies = [
"libkos",
"types",
]
[[package]]
name = "smptest"
version = "0.1.0"

View File

@@ -6,6 +6,7 @@ members = [
"crates/apps/cat",
"crates/apps/ls",
"crates/apps/echo",
"crates/apps/sleep",
"crates/apps/smptest",
"crates/apps/spawntest",
"crates/apps/snake",

View File

@@ -52,7 +52,7 @@ kani: kani-types kani-domain kani-kernel
# adapter's build.rs).
build:
cargo build --target $(RISCV_TARGET) --release \
-p init -p echo -p snake \
-p init -p echo -p sleep -p snake \
-p display-qemu -p input-qemu -p storage-qemu \
-p log-server -p permission-manager -p session-server -p smptest -p spawntest -p hello \
-p terminal-server -p shell -p cat -p ls
@@ -61,7 +61,7 @@ build:
image: build
scripts/make-disk-image.sh
# Boots a test image (normal image + smptest.elf) on $(SMP) harts; init runs
# Boots a test image (normal image + bin/smptest) on $(SMP) harts; init runs
# the SMP self-test and halts. Passes only on "SMPTEST PASS".
test-qemu: build
IMG=test.img SMPTEST=1 scripts/make-disk-image.sh > /dev/null

View File

@@ -1,7 +1,8 @@
//! The shell: a few built-ins, and anything else is a program on disk.
//! `name args...` runs `name.elf` through init, handing it the terminal as
//! its output and read-only storage rooted at the current directory, and
//! nothing else; then waits for it and takes the storage back.
//! `name args...` runs `/bin/name` through init, handing it the terminal as
//! its output, read-only storage rooted at the current directory, and what
//! its manifest (`/bin/name.manifest`) asks for that the shell can give;
//! then waits for it (Ctrl+C kills it) and takes the storage back.
#![no_std]
#![no_main]
@@ -10,6 +11,7 @@ extern crate libkos;
use libkos::fs::Storage;
use libkos::spawn::{SpawnError, spawn};
use types::manifest::Needs;
use libkos::{print, println, syscall};
use types::bounded::CapSlot;
use types::budget::Limits;
@@ -19,7 +21,15 @@ use types::start_arg::StartArg;
/// Where init's manifest put them: `uses`, then the spawn endpoint.
const TERMINAL: CapSlot = CapSlot::const_new(0);
const STORAGE: CapSlot = CapSlot::const_new(1);
const INIT: CapSlot = CapSlot::const_new(2);
const DISPLAY: CapSlot = CapSlot::const_new(2);
const INIT: CapSlot = CapSlot::const_new(3);
/// Bits on the shell's notification: the program died (its death watch is
/// unbadged), or Ctrl+C was typed (the terminal's copy).
const DIED: usize = 1;
const INTERRUPTED: usize = 1 << 1;
/// Longest manifest read.
const MAX_MANIFEST: usize = 512;
/// What each program gets, out of the shell's allowance.
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
@@ -91,19 +101,46 @@ struct Shell {
done: CapSlot,
}
/// Runs `args[0].elf` with `args` in the current directory, and waits
/// for it to finish.
/// `/bin/<name><suffix>` into `out`, if it fits.
fn bin_path<'a>(out: &'a mut [u8], name: &str, suffix: &[u8]) -> Option<&'a [u8]> {
let path = out.get_mut(..4 + name.len() + suffix.len())?;
path[..4].copy_from_slice(b"bin/");
path[4..4 + name.len()].copy_from_slice(name.as_bytes());
path[4 + name.len()..].copy_from_slice(suffix);
Some(path)
}
/// What `name`'s manifest asks for; none without one. `None` if it asks
/// for something the shell does not know how to give.
fn needs(sh: &mut Shell, name: &str) -> Option<Needs> {
let mut path = [0u8; types::storage::MAX_PATH];
let path = bin_path(&mut path, name, b".manifest")?;
let Ok(file) = sh.fs.open(path) else {
return Some(Needs::default());
};
let mut text = [0u8; MAX_MANIFEST];
let read = sh.fs.read(file, &mut text);
let _ = sh.fs.close(file);
match types::manifest::parse(&text[..read.ok()?]) {
Ok(needs) => Some(needs),
Err(types::manifest::Unknown(what)) => {
println!("shell: {}: its manifest asks for {}", name, core::str::from_utf8(what).unwrap_or("?"));
None
}
}
}
/// Runs `/bin/args[0]` with `args` in the current directory, and waits
/// for it to finish or Ctrl+C.
fn run_program(sh: &mut Shell, args: &[&str]) {
let mut file = [0u8; types::spawn::MAX_NAME];
let name = args[0].as_bytes();
let Some(file) = file.get_mut(..name.len() + 4).map(|f| {
f[..name.len()].copy_from_slice(name);
f[name.len()..].copy_from_slice(b".elf");
&*f
}) else {
let Some(file) = bin_path(&mut file, args[0], b"") else {
println!("shell: {}: name too long", args[0]);
return;
};
let Some(needs) = needs(sh, args[0]) else {
return;
};
let child = match spawn(INIT, file, PROGRAM) {
Ok(c) => c,
Err(SpawnError::NotFound) => return println!("shell: {}: not found", args[0]),
@@ -116,11 +153,20 @@ fn run_program(sh: &mut Shell, args: &[&str]) {
log: child.give(TERMINAL, call),
storage: grant.and_then(|g| child.give(g, call)),
args: child.set_args(args),
// It subscribes to input by sending the display a notification.
display: needs.display.then(|| child.give_sending(DISPLAY, call)).flatten(),
console: None,
};
// A Ctrl+C typed since the last program is not for this one.
let _ = syscall::poll(sh.done);
let watched = syscall::watch_death(child.process, sh.done).is_ok();
if child.start_with(arg) && watched {
let _ = syscall::wait(sh.done);
while syscall::wait(sh.done).is_ok_and(|bits| {
if bits & INTERRUPTED != 0 {
syscall::kill(child.process);
}
bits & DIED == 0
}) {}
} else {
let _ = syscall::kill(child.process);
println!("shell: {}: cannot start", args[0]);
@@ -129,6 +175,10 @@ fn run_program(sh: &mut Shell, args: &[&str]) {
if let Some(g) = grant {
let _ = libkos::fs::release(g);
}
// It drew over the terminal and took the keyboard.
if needs.display {
libkos::terminal::redraw(TERMINAL);
}
}
fn cd(sh: &mut Shell, to: &str) {
@@ -152,6 +202,7 @@ fn run(sh: &mut Shell, line: &str) {
println!("built-ins: help, echo <text>, clear, cd <dir>, pwd");
println!("anything else runs a program from the disk: try ls, cat readme.txt");
println!("programs see the current directory, read-only, and nothing above it");
println!("Ctrl+C stops a running program; snake quits with q");
}
Some("cd") => cd(sh, args.get(1).copied().unwrap_or("/")),
Some("pwd") => println!("{}", sh.cwd),
@@ -188,6 +239,9 @@ extern "C" fn main() {
println!("shell: no storage");
return;
};
if !libkos::terminal::on_interrupt(TERMINAL, done, types::badge::Badge::new(INTERRUPTED)) {
println!("shell: Ctrl+C will not work");
}
let mut sh = Shell {
fs,
cwd: Cwd {

View File

@@ -0,0 +1,14 @@
[package]
name = "sleep"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "sleep"
test = false
bench = false
[dependencies]
types = { workspace = true }
libkos = { workspace = true }

View File

@@ -0,0 +1,28 @@
//! Waits: `sleep <seconds>`. Mostly something to stop with Ctrl+C.
#![no_std]
#![no_main]
extern crate libkos;
use libkos::{println, syscall};
use types::time::Ticks;
/// QEMU virt's timebase.
const TICKS_PER_SECOND: u64 = 10_000_000;
#[unsafe(no_mangle)]
extern "C" fn main() {
let Some(seconds) = libkos::start::args().nth(1).and_then(|s| s.parse::<u64>().ok()) else {
println!("usage: sleep <seconds>");
return;
};
let Ok(alarm) = syscall::create_notification() else {
println!("sleep: no notification");
return;
};
let at = syscall::get_time().raw().saturating_add(seconds.saturating_mul(TICKS_PER_SECOND));
if syscall::set_timer(alarm, Ticks::from_raw(at)).is_ok() {
let _ = syscall::wait(alarm);
}
}

View File

@@ -12,13 +12,17 @@ use libkos::syscall;
use server_framework::Encode;
use display_server::protocol::DisplayRequest;
/// Input comes through the display server too, while this app has focus.
const DISPLAY_EP: CapSlot = CapSlot::const_new(0);
const TICK_DELAY: u64 = 5_000_000;
const MAX_RENDER_CMDS: usize = 140;
/// The display server, which the shell hands over because snake's manifest
/// asks for it. Input comes through it too, while snake has focus.
fn display_ep() -> CapSlot {
libkos::start::display().unwrap_or(CapSlot::const_new(0))
}
fn display_send(req: &DisplayRequest) {
let _ = syscall::request(DISPLAY_EP, req.encode());
let _ = syscall::request(display_ep(), req.encode());
}
fn draw_rect(x: u16, y: u16, w: u16, h: u16, color: u32) {
@@ -37,8 +41,12 @@ fn flush() {
#[unsafe(no_mangle)]
extern "C" fn main() {
if libkos::start::display().is_none() {
libkos::println!("snake: needs the display (see /bin/snake.manifest)");
return;
}
// Takes focus; the game polls every tick, so it never waits on the signal.
let _ = libkos::display::subscribe_input(DISPLAY_EP);
let _ = libkos::display::subscribe_input(display_ep());
let mut snake = Snake::new();
let mut commands = [RenderCommand { x: 0, y: 0, w: 0, h: 0, color: 0 }; MAX_RENDER_CMDS];
@@ -55,32 +63,60 @@ extern "C" fn main() {
let gh = renderer::grid_pixel_height();
draw_rect(gw / 2 - 60, gh / 2 - 12, 120, 24, 0xFFCC0000);
flush();
loop { syscall::yield_now(); }
// Any key leaves.
while next_input() == Input::None {
wait_tick();
}
return;
}
wait_tick();
handle_input(&mut snake);
if handle_input(&mut snake) == Input::Quit {
return;
}
snake.tick();
}
}
fn handle_input(snake: &mut Snake) {
// Drain all pending events, apply the latest direction
loop {
let Ok([tag, kind, ch, _]) = syscall::request(DISPLAY_EP, [types::display::input::POLL, 0, 0, 0]) else {
break;
#[derive(PartialEq)]
enum Input {
None,
Quit,
Turn(Direction),
Other,
}
/// The next key, if one is waiting: q or Escape quits.
fn next_input() -> Input {
let Ok([tag, kind, ch, _]) = syscall::request(display_ep(), [types::display::input::POLL, 0, 0, 0]) else {
return Input::None;
};
if tag == 0 { break; } // NoEvent — queue drained
if tag == 1 {
if tag == 0 {
return Input::None; // NoEvent: queue drained
}
if tag != 1 {
return Input::Other; // a key let go
}
// Arrows, or WASD (kind 9 is a character).
match (kind, ch as u8) {
(0, _) | (9, b'w') => snake.set_direction(Direction::Up),
(1, _) | (9, b's') => snake.set_direction(Direction::Down),
(2, _) | (9, b'a') => snake.set_direction(Direction::Left),
(3, _) | (9, b'd') => snake.set_direction(Direction::Right),
_ => {}
(5, _) | (9, b'q') => Input::Quit,
(0, _) | (9, b'w') => Input::Turn(Direction::Up),
(1, _) | (9, b's') => Input::Turn(Direction::Down),
(2, _) | (9, b'a') => Input::Turn(Direction::Left),
(3, _) | (9, b'd') => Input::Turn(Direction::Right),
_ => Input::Other,
}
}
/// Drains the pending keys, applying each turn; reports a quit.
fn handle_input(snake: &mut Snake) -> Input {
loop {
match next_input() {
Input::None => return Input::None,
Input::Quit => return Input::Quit,
Input::Turn(d) => snake.set_direction(d),
Input::Other => {}
}
}
}

View File

@@ -31,7 +31,7 @@ const HELLO: Limits = Limits { pages: 80, endpoints: 0, notifications: 0, region
/// Spawns hello, runs it, and waits for it to die; hands it `grant`
/// first, if there is one.
fn run_hello(died: CapSlot, grant: Option<CapSlot>) -> Result<(), &'static str> {
let child = spawn(INIT, b"hello.elf", HELLO).map_err(|_| "init would not spawn hello")?;
let child = spawn(INIT, b"bin/hello", HELLO).map_err(|_| "init would not spawn hello")?;
let call = types::permission::PermissionSet::empty().with(types::permission::Permission::Execute);
if grant.is_some_and(|g| child.give(g, call).is_none()) {
return Err("a storage grant could not be passed on");
@@ -98,11 +98,11 @@ fn check() -> Result<(), &'static str> {
libkos::println!("spawntest: spawned program runs and its death is heard: ok");
let greedy = Limits { pages: 200, ..HELLO };
if spawn(INIT, b"hello.elf", greedy).err() != Some(SpawnError::OverAllowance) {
if spawn(INIT, b"bin/hello", greedy).err() != Some(SpawnError::OverAllowance) {
return Err("init spawned past the allowance");
}
let tiny = Limits { pages: 1, ..HELLO };
if spawn(INIT, b"nothere.elf", tiny).err() != Some(SpawnError::NotFound) {
if spawn(INIT, b"bin/nothere", tiny).err() != Some(SpawnError::NotFound) {
return Err("a missing program was not reported");
}
libkos::println!("spawntest: allowance and missing files refused: ok");

View File

@@ -4,6 +4,17 @@ use port_input_device::Key;
pub const LEFT_SHIFT: u16 = 42;
pub const RIGHT_SHIFT: u16 = 54;
pub const LEFT_CTRL: u16 = 29;
pub const RIGHT_CTRL: u16 = 97;
/// A letter typed with Ctrl held is its control character, as on a
/// terminal: Ctrl+C is 3. Anything else is itself.
pub fn with_ctrl(key: Key) -> Key {
match key {
Key::Char(c) if c.is_ascii_alphabetic() => Key::Char(c.to_ascii_lowercase() & 0x1f),
k => k,
}
}
/// Rows of the main block: the first key's code, then each key unshifted
/// and shifted.
@@ -41,6 +52,14 @@ pub fn key(code: u16, shift: bool) -> Option<Key> {
mod tests {
use super::*;
#[test]
fn ctrl_letters_are_control_characters() {
assert_eq!(with_ctrl(Key::Char(b'c')), Key::Char(3));
assert_eq!(with_ctrl(Key::Char(b'C')), Key::Char(3));
assert_eq!(with_ctrl(Key::Char(b'1')), Key::Char(b'1'));
assert_eq!(with_ctrl(Key::Enter), Key::Enter);
}
#[test]
fn letters_digits_and_shift() {
assert_eq!(key(35, false), Some(Key::Char(b'h')));

View File

@@ -31,6 +31,7 @@ pub struct VirtioInputDevice {
next_avail: u16,
evbuf_pa: usize,
shift: bool,
ctrl: bool,
}
impl VirtioInputDevice {
@@ -48,7 +49,7 @@ impl VirtioInputDevice {
let evbuf_pa = libkos::device::dma(1, EVBUF_VA).ok()?;
hw_virtio::mmio::init_legacy_device(&regs, vq_pa, QUEUE_SIZE as u32);
let mut dev = Self { last_used: 0, next_avail: 0, evbuf_pa, shift: false };
let mut dev = Self { last_used: 0, next_avail: 0, evbuf_pa, shift: false, ctrl: false };
dev.fill_queue();
Some(dev)
}
@@ -139,8 +140,13 @@ impl InputDevice for VirtioInputDevice {
self.shift = ev_value != 0;
continue;
}
if ev_type == EV_KEY && matches!(ev_code, keymap::LEFT_CTRL | keymap::RIGHT_CTRL) {
self.ctrl = ev_value != 0;
continue;
}
if ev_type == EV_KEY {
if let Some(key) = keymap::key(ev_code, self.shift) {
let key = keymap::key(ev_code, self.shift).map(|k| if self.ctrl { keymap::with_ctrl(k) } else { k });
if let Some(key) = key {
if ev_value == KEY_PRESS {
return Some(InputEvent::KeyDown(key));
} else if ev_value == 0 {

View File

@@ -58,7 +58,7 @@ pub fn run() -> ! {
// instead of the desktop, and the last one halts the machine.
let (storage, scratch) = launcher.storage();
if storage
.open(b"smptest.elf")
.open(b"bin/smptest")
.is_ok_and(|h| storage.close(h).is_ok())
{
libkos::println!("init: running the SMP self-test");

View File

@@ -51,19 +51,19 @@ const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Prog
/// Started on every boot. The log server comes first so the rest can print.
pub const SERVERS: &[Program] = &[
Program { console: true, ..server("log", b"log.elf", SMALL) },
Program { device: Some(Device::Input), ..server("input", b"input.elf", Limits { regions: 4, ..SMALL }) },
Program { console: true, ..server("log", b"bin/log", SMALL) },
Program { device: Some(Device::Input), ..server("input", b"bin/input", Limits { regions: 4, ..SMALL }) },
// The input server's only client: it passes input on to the app in focus.
Program {
device: Some(Device::Gpu),
uses: &["input"],
// The framebuffer alone is ~1000 pages at 1280x800.
..server("display", b"display.elf", Limits { pages: 1280, regions: 4, ..SMALL })
..server("display", b"bin/display", Limits { pages: 1280, regions: 4, ..SMALL })
},
// Text on the display, lines from the keyboard (it takes input focus).
Program { uses: &["display"], ..server("terminal", b"terminal.elf", SMALL) },
server("permis", b"permis.elf", SMALL),
server("session", b"session.elf", SMALL),
Program { uses: &["display"], ..server("terminal", b"bin/terminal", SMALL) },
server("permission", b"bin/permission", SMALL),
server("session", b"bin/session", SMALL),
];
/// Run on the test image after the SMP self-test, instead of the desktop:
@@ -73,14 +73,15 @@ pub const TESTS: &[Program] = &[Program {
uses: &["storage"],
spawns: Some(Limits { pages: 128, endpoints: 2, notifications: 2, regions: 2 }),
system_control: true,
..server("spawntest", b"spawntst.elf", SMALL)
..server("spawntest", b"bin/spawntest", SMALL)
}];
/// The desktop; the test image runs the SMP self-test instead.
pub const APPS: &[Program] = &[Program {
serves: false,
uses: &["terminal", "storage"],
// The display only to hand on, to programs whose manifest asks for it.
uses: &["terminal", "storage", "display"],
// Room for one program at a time, the way the shell runs them.
spawns: Some(Limits { pages: 256, endpoints: 2, notifications: 2, regions: 2 }),
..server("shell", b"shell.elf", SMALL)
..server("shell", b"bin/shell", SMALL)
}];

View File

@@ -1,4 +1,4 @@
//! SMP self-test, run when the disk carries `smptest.elf`: after revoking a
//! SMP self-test, run when the disk carries `bin/smptest`: after revoking a
//! spinner's page or killing it, no spinner counter may move on any hart.
use core::sync::atomic::{AtomicUsize, Ordering};
@@ -40,7 +40,7 @@ fn start_spinner(storage: &mut Storage, scratch: &mut [u8], page: CapSlot) -> Re
for i in 0..=THREADS {
word(i).store(0, Ordering::SeqCst);
}
let spinner = crate::load::load_binary(storage, scratch, b"smptest.elf", crate::manifest::SMALL).ok_or("cannot load smptest.elf")?;
let spinner = crate::load::load_binary(storage, scratch, b"bin/smptest", crate::manifest::SMALL).ok_or("cannot load bin/smptest")?;
let rw = PermissionSet::empty()
.with(Permission::Read)
.with(Permission::Write)

View File

@@ -55,18 +55,21 @@ pub const CELL_HEIGHT: usize = 16;
/// arrive; read them with `poll_input`.
pub fn subscribe_input(ep: CapSlot) -> Option<CapSlot> {
use types::badge::Badge;
use types::permission::{Permission, PermissionSet};
let notify = syscall::create_notification().ok()?;
subscribe(ep, notify, Badge::NONE).then_some(notify)
}
/// Takes input focus, to be told through `notify` with `badge`'s bits.
pub fn subscribe(ep: CapSlot, notify: CapSlot, badge: types::badge::Badge) -> bool {
use types::permission::{Permission, PermissionSet};
// Signal only (Delegate just to send it; the transfer strips it).
let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate);
let copy = syscall::mint(notify, Badge::NONE, signal).ok()?;
let sent = syscall::call(
ep,
[types::display::input::SUBSCRIBE, 0, 0, 0],
Some(copy.into()),
);
let Ok(copy) = syscall::mint(notify, badge, signal) else {
return false;
};
let sent = syscall::call(ep, [types::display::input::SUBSCRIBE, 0, 0, 0], Some(copy.into()));
syscall::drop_cap(copy);
(sent.ok()?.words()[0] == 0).then_some(notify)
sent.is_ok_and(|r| r.words()[0] == 0)
}
/// The next input event, if this process has focus and one is waiting.

View File

@@ -57,6 +57,12 @@ impl Spawned {
syscall::delegate(cap, self.process, perms.without(Permission::Delegate))
}
/// Like `give`, but an endpoint the program may send capabilities
/// through (which takes Delegate, so it could pass the endpoint on too).
pub fn give_sending(&self, endpoint: CapSlot, perms: PermissionSet) -> Option<CapSlot> {
syscall::delegate(endpoint, self.process, perms.with(Permission::Delegate))
}
/// Puts `args` (its name first) where the program finds them: a page
/// this process fills and then moves into the program, which owns it.
pub fn set_args(&self, args: &[&str]) -> bool {

View File

@@ -38,6 +38,11 @@ pub fn storage() -> Option<CapSlot> {
StartArg::decode(arg()).storage
}
/// The display server endpoint this process was given, if any.
pub fn display() -> Option<CapSlot> {
StartArg::decode(arg()).display
}
/// This program's arguments, its name first; none without an args page.
pub fn args() -> impl Iterator<Item = &'static str> {
let page: &'static [u8] = if StartArg::decode(arg()).args {

View File

@@ -27,3 +27,22 @@ pub fn read_line(terminal: CapSlot, buf: &mut [u8]) -> usize {
pub fn clear(terminal: CapSlot) {
let _ = syscall::request(terminal, [types::terminal::CLEAR, 0, 0, 0]);
}
/// Repaints the terminal and gives it input focus again, after a program
/// that drew on the screen.
pub fn redraw(terminal: CapSlot) {
let _ = syscall::request(terminal, [types::terminal::REDRAW, 0, 0, 0]);
}
/// Has the terminal signal `notify` with `badge`'s bits when Ctrl+C is
/// typed while no line is being read. Only the first caller is heard.
pub fn on_interrupt(terminal: CapSlot, notify: CapSlot, badge: types::badge::Badge) -> bool {
use types::permission::{Permission, PermissionSet};
let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate);
let Ok(copy) = syscall::mint(notify, badge, signal) else {
return false;
};
let sent = syscall::call(terminal, [types::terminal::ON_INTERRUPT, 0, 0, 0], Some(copy.into()));
syscall::drop_cap(copy);
sent.is_ok_and(|r| r.words()[0] == 0)
}

View File

@@ -11,7 +11,7 @@ pub fn run() -> ! {
// Slot 0 holds the endpoint init delegated. Init has no process
// creation authority to hand on yet, so Launch reports an error.
let mut srv = server::SessionServer::new(None);
let _ = srv.register_app("snake", "/apps/snake.elf");
let _ = srv.register_app("snake", "bin/snake");
let ep = types::bounded::CapSlot::new(0).unwrap();
server_framework::serve(&mut srv, ep)
}

View File

@@ -5,7 +5,7 @@ use server_framework::Server;
fn test_server() -> SessionServer {
let cap = types::bounded::CapSlot::new(0).unwrap();
let mut s = SessionServer::new(Some(cap));
s.register_app("snake", "/apps/snake.elf").unwrap();
s.register_app("snake", "bin/snake").unwrap();
s.register_app("clock", "/apps/clock.elf").unwrap();
s
}
@@ -108,7 +108,7 @@ fn the_app_table_fills_up_without_panicking() {
#[test]
fn launching_needs_process_creation_authority() {
let mut server = SessionServer::new(None);
server.register_app("snake", "/apps/snake.elf").unwrap();
server.register_app("snake", "bin/snake").unwrap();
let resp = server.handle(SessionRequest::Launch { app: AppId::new(0) });
assert_eq!(resp, SessionResponse::Error);
}

View File

@@ -80,6 +80,11 @@ impl Grid {
}
}
/// Marks every row to be drawn again.
pub fn touch_all(&mut self) {
self.dirty.fill(true);
}
pub fn clear(&mut self) {
self.cells.fill(b' ');
self.col = 0;

View File

@@ -12,8 +12,10 @@ mod tests;
use libkos::display::{self, CELL_HEIGHT, CELL_WIDTH};
use libkos::input::{InputEvent, Key};
use libkos::syscall;
use types::badge::Badge;
use types::bounded::CapSlot;
use types::display::{Color, ScreenCoord};
use types::terminal::{KEYS_BIT, TIMER_BIT};
/// Where init put them (its manifest: own endpoint, then `uses`).
const EP: CapSlot = CapSlot::const_new(0);
@@ -26,11 +28,23 @@ const BG: Color = Color::rgb(16, 16, 24);
const FLUSH_DELAY: u64 = 20 * 10_000;
struct Screen {
keys: CapSlot,
/// Bound to the serving thread; the flush timer signals it.
/// Bound to the serving thread: the display signals it with
/// `KEYS_BIT`, the flush timer with `TIMER_BIT`.
notify: CapSlot,
timer: CapSlot,
}
/// A key as the line editor wants it, if it is one.
fn byte(event: InputEvent) -> Option<u8> {
match event {
InputEvent::KeyDown(Key::Char(c)) => Some(c),
InputEvent::KeyDown(Key::Space | Key::Tab) => Some(b' '),
InputEvent::KeyDown(Key::Enter) => Some(b'\n'),
InputEvent::KeyDown(Key::Backspace) => Some(server::BACKSPACE),
_ => None,
}
}
fn at(col: usize, row: usize) -> (ScreenCoord, ScreenCoord) {
(ScreenCoord::new((col * CELL_WIDTH) as u16), ScreenCoord::new((row * CELL_HEIGHT) as u16))
}
@@ -57,34 +71,50 @@ impl server::Console for Screen {
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) {
let InputEvent::KeyDown(key) = event else { continue };
match key {
Key::Char(c) => return c,
Key::Space | Key::Tab => return b' ',
Key::Enter => return b'\n',
Key::Backspace => return server::BACKSPACE,
_ => {}
if let Some(key) = byte(event) {
return Some(key);
}
}
let _ = syscall::wait(self.keys);
None
}
fn take_focus(&mut self) {
display::subscribe(DISPLAY, self.notify, Badge::new(KEYS_BIT));
display::clear(DISPLAY, BG);
}
fn interrupt(&mut self, notification: CapSlot) {
let _ = syscall::signal(notification);
}
}
pub fn run() -> ! {
let Some(keys) = display::subscribe_input(DISPLAY) else {
let Some(notify) = syscall::create_notification().ok().filter(|n| syscall::bind_notification(*n).is_ok()) else {
libkos::println!("terminal: no notification, exiting");
syscall::exit();
};
let timer_perms = types::permission::PermissionSet::empty().with(types::permission::Permission::Write);
let Ok(timer) = syscall::mint(notify, Badge::new(TIMER_BIT), timer_perms) else {
libkos::println!("terminal: no timer, exiting");
syscall::exit();
};
if !display::subscribe(DISPLAY, notify, Badge::new(KEYS_BIT)) {
libkos::println!("terminal: no input focus, exiting");
syscall::exit();
};
let Some(timer) = syscall::create_notification().ok().filter(|t| syscall::bind_notification(*t).is_ok()) else {
libkos::println!("terminal: no timer notification, exiting");
syscall::exit();
};
}
let (w, h) = display::get_dimensions(DISPLAY);
display::clear(DISPLAY, BG);
let cols = w.raw() as usize / CELL_WIDTH;
let rows = h.raw() as usize / CELL_HEIGHT;
let mut srv = server::TerminalServer::new(Screen { keys, timer }, cols, rows);
let mut srv = server::TerminalServer::new(Screen { notify, timer }, cols, rows);
server_framework::serve(&mut srv, EP)
}

View File

@@ -17,10 +17,17 @@ pub trait Console {
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.
fn poll_key(&mut self) -> Option<u8>;
/// Takes input focus back from whoever took it, and clears the screen.
fn take_focus(&mut self);
fn interrupt(&mut self, notification: CapSlot);
}
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;
pub struct TerminalServer<C: Console> {
console: C,
@@ -30,6 +37,10 @@ pub struct TerminalServer<C: Console> {
line_len: usize,
line_read: usize,
flush_pending: bool,
typeahead: [u8; TYPEAHEAD],
typeahead_len: usize,
/// Told about Ctrl+C typed while no line is being read.
on_interrupt: Option<CapSlot>,
}
/// An empty reply, whose tag is not text.
@@ -50,6 +61,9 @@ impl<C: Console> TerminalServer<C> {
line_len: 0,
line_read: 0,
flush_pending: false,
typeahead: [0; TYPEAHEAD],
typeahead_len: 0,
on_interrupt: None,
};
s.redraw();
s.console.flush();
@@ -60,6 +74,11 @@ impl<C: Console> TerminalServer<C> {
&self.console
}
#[cfg(test)]
pub fn console_mut_for_test(&mut self) -> &mut C {
&mut self.console
}
pub fn grid(&self) -> &Grid {
&self.grid
}
@@ -88,13 +107,52 @@ impl<C: Console> TerminalServer<C> {
}
}
/// Edits a line from the keyboard, echoing it, until Enter.
/// A key typed ahead, or the next one.
fn next_key(&mut self) -> u8 {
if self.typeahead_len == 0 {
return self.console.next_key();
}
let key = self.typeahead[0];
self.typeahead.copy_within(1..self.typeahead_len, 0);
self.typeahead_len -= 1;
key
}
/// Keys typed while nobody reads a line: Ctrl+C interrupts, the rest
/// wait for the next line.
fn keys_arrived(&mut self) {
while let Some(key) = self.console.poll_key() {
if key == types::terminal::CTRL_C {
self.typeahead_len = 0;
self.write(b"^C\n");
if let Some(n) = self.on_interrupt {
self.console.interrupt(n);
}
} else if self.typeahead_len < TYPEAHEAD {
self.typeahead[self.typeahead_len] = key;
self.typeahead_len += 1;
}
}
}
/// Edits a line from the keyboard, echoing it, until Enter. Ctrl+C
/// drops it and reads as an empty line.
fn read_line(&mut self) {
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.console.next_key() {
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;
@@ -148,9 +206,23 @@ impl<C: Console> Server for TerminalServer<C> {
Done
}
fn handle_call(&mut self, _badge: usize, words: [usize; 4], _cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
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(),
types::terminal::REDRAW => {
self.console.take_focus();
self.grid.touch_all();
self.redraw();
self.console.flush();
[0; 4]
}
types::terminal::ON_INTERRUPT => match (cap, self.on_interrupt) {
(Some(n), None) => {
self.on_interrupt = Some(n);
[0; 4]
}
_ => [MALFORMED, 0, 0, 0],
},
types::terminal::CLEAR => {
self.handle(Request::Clear);
[0; 4]
@@ -166,12 +238,17 @@ impl<C: Console> Server for TerminalServer<C> {
(reply, None)
}
/// The flush timer: show a burst of output in one frame.
fn signal(&mut self, _bits: usize) {
/// Keys, or the flush timer: show a burst of output in one frame.
fn signal(&mut self, bits: usize) {
if bits & types::terminal::KEYS_BIT != 0 {
self.keys_arrived();
}
if bits & types::terminal::TIMER_BIT != 0 {
self.flush_pending = false;
self.console.flush();
}
}
}
pub enum Request {
Clear,

View File

@@ -2,14 +2,21 @@ extern crate std;
use crate::server::{BACKSPACE, Console, TerminalServer};
use server_framework::Server;
use std::vec;
use std::vec::Vec;
use types::bounded::CapSlot;
use types::log::{text_of, text_words};
use types::terminal::{CTRL_C, KEYS_BIT, TIMER_BIT};
#[derive(Default)]
struct Fake {
keys: Vec<u8>,
/// Typed while nobody reads a line.
typed: Vec<u8>,
flushes: usize,
timers: usize,
focus_taken: usize,
interrupted: Vec<CapSlot>,
}
impl Console for Fake {
@@ -24,6 +31,15 @@ impl Console for Fake {
fn next_key(&mut self) -> u8 {
self.keys.remove(0)
}
fn poll_key(&mut self) -> Option<u8> {
(!self.typed.is_empty()).then(|| self.typed.remove(0))
}
fn take_focus(&mut self) {
self.focus_taken += 1;
}
fn interrupt(&mut self, notification: CapSlot) {
self.interrupted.push(notification);
}
}
fn term(keys: &[u8]) -> TerminalServer<Fake> {
@@ -45,7 +61,7 @@ fn writes_land_on_the_grid_and_flush_once_per_burst() {
}
assert_eq!(&t.grid().row(0)[..11], b"hello world");
assert_eq!((t.console().timers, t.console().flushes), (1, before));
t.signal(1);
t.signal(TIMER_BIT);
assert_eq!(t.console().flushes, before + 1);
}
@@ -68,3 +84,62 @@ fn backspace_on_an_empty_line_does_nothing() {
let mut t = term(&[BACKSPACE, b'x', b'\n']);
assert_eq!(read(&mut t), b"x\n");
}
fn line_of(t: &mut TerminalServer<Fake>) -> Vec<u8> {
let mut line = read(t);
while line.last() != Some(&b'\n') {
line.extend(read(t));
}
line
}
fn on_interrupt(t: &mut TerminalServer<Fake>, slot: u8) -> usize {
t.handle_call(1, [types::terminal::ON_INTERRUPT, 0, 0, 0], CapSlot::new(slot)).0[0]
}
#[test]
fn ctrl_c_while_a_program_runs_interrupts_it() {
let mut t = term(&[]);
assert_eq!(on_interrupt(&mut t, 7), 0);
t.console_mut_for_test().typed = vec![b'x', CTRL_C];
t.signal(KEYS_BIT);
assert_eq!(t.console().interrupted, [CapSlot::new(7).unwrap()]);
assert_eq!(&t.grid().row(0)[..2], b"^C");
}
#[test]
fn only_the_first_interrupt_notification_is_taken() {
let mut t = term(&[]);
assert_eq!(on_interrupt(&mut t, 7), 0);
assert_ne!(on_interrupt(&mut t, 8), 0);
t.console_mut_for_test().typed = vec![CTRL_C];
t.signal(KEYS_BIT);
assert_eq!(t.console().interrupted, [CapSlot::new(7).unwrap()]);
}
#[test]
fn keys_typed_ahead_start_the_next_line() {
let mut t = term(b"s\n");
t.console_mut_for_test().typed = b"l".to_vec();
t.signal(KEYS_BIT);
assert!(t.console().interrupted.is_empty());
assert_eq!(line_of(&mut t), b"ls\n");
}
#[test]
fn ctrl_c_drops_the_line_being_typed() {
let mut t = term(&[b'r', b'm', CTRL_C, b'l', b's', b'\n']);
assert_eq!(on_interrupt(&mut t, 7), 0);
assert_eq!(line_of(&mut t), b"\n");
assert!(t.console().interrupted.is_empty(), "nothing runs to interrupt");
assert_eq!(line_of(&mut t), b"ls\n");
}
#[test]
fn redraw_takes_focus_back_and_paints_every_row() {
let mut t = term(&[]);
t.handle_call(1, text_words(b"hi"), None);
t.handle_call(1, [types::terminal::REDRAW, 0, 0, 0], None);
assert_eq!(t.console().focus_taken, 1);
assert_eq!(&t.grid().row(0)[..2], b"hi");
}

View File

@@ -11,6 +11,7 @@ pub mod id;
pub mod input;
pub mod log;
pub mod macros;
pub mod manifest;
pub mod mapping;
pub mod permission;
#[cfg(kani)]

View File

@@ -0,0 +1,64 @@
//! A program's manifest, `/bin/<name>.manifest`: what it needs beyond the
//! terminal and the storage every program gets. Lines of `needs = a, b`;
//! `#` starts a comment. No manifest means nothing more.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Needs {
/// Draws on the screen itself and takes input focus.
pub display: bool,
}
/// A line or need the parser does not know: the program asks for
/// something nobody can say it may have, so it should not run.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Unknown<'a>(pub &'a [u8]);
pub fn parse(text: &[u8]) -> Result<Needs, Unknown<'_>> {
let mut needs = Needs::default();
for line in text.split(|b| *b == b'\n') {
let line = line
.split(|b| *b == b'#')
.next()
.unwrap_or(&[])
.trim_ascii();
if line.is_empty() {
continue;
}
let mut sides = line.splitn(2, |b| *b == b'=');
let (key, value) = (sides.next().unwrap_or(&[]).trim_ascii(), sides.next());
let (b"needs", Some(value)) = (key, value) else {
return Err(Unknown(line));
};
for need in value
.split(|b| *b == b',')
.map(<[u8]>::trim_ascii)
.filter(|n| !n.is_empty())
{
match need {
b"display" => needs.display = true,
other => return Err(Unknown(other)),
}
}
}
Ok(needs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn needs_are_read_and_comments_skipped() {
let text = b"# snake\nneeds = display # the screen\n\n";
assert_eq!(parse(text), Ok(Needs { display: true }));
assert_eq!(parse(b""), Ok(Needs::default()));
assert_eq!(parse(b"needs = "), Ok(Needs::default()));
}
#[test]
fn anything_unknown_is_refused() {
assert_eq!(parse(b"needs = display, network"), Err(Unknown(b"network")));
assert_eq!(parse(b"wants = display"), Err(Unknown(b"wants = display")));
assert_eq!(parse(b"display"), Err(Unknown(b"display")));
}
}

View File

@@ -84,8 +84,8 @@ mod tests {
notifications: 3,
regions: 255,
};
let (name, len, back) = parse(request(b"hello.elf", l).unwrap()).unwrap();
assert_eq!(&name[..len], b"hello.elf");
let (name, len, back) = parse(request(b"bin/hello", l).unwrap()).unwrap();
assert_eq!(&name[..len], b"bin/hello");
assert_eq!(back, l);
}

View File

@@ -8,6 +8,7 @@ const CONSOLE_SHIFT: u32 = 0;
const LOG_SHIFT: u32 = 8;
const STORAGE_SHIFT: u32 = 16;
const ARGS_BIT: usize = 1 << 24;
const DISPLAY_SHIFT: u32 = 32;
/// Where the arguments page sits: the program's arguments, NUL separated,
/// the first its name. Below the stack, which ends at the image.
@@ -21,6 +22,8 @@ pub struct StartArg {
pub log: Option<CapSlot>,
pub storage: Option<CapSlot>,
pub args: bool,
/// The display server, for a program that draws on the screen.
pub display: Option<CapSlot>,
}
fn field(arg: usize, shift: u32) -> Option<CapSlot> {
@@ -38,6 +41,7 @@ impl StartArg {
| put(self.log, LOG_SHIFT)
| put(self.storage, STORAGE_SHIFT)
| if self.args { ARGS_BIT } else { 0 }
| put(self.display, DISPLAY_SHIFT)
}
pub fn decode(arg: usize) -> Self {
@@ -46,6 +50,7 @@ impl StartArg {
log: field(arg, LOG_SHIFT),
storage: field(arg, STORAGE_SHIFT),
args: arg & ARGS_BIT != 0,
display: field(arg, DISPLAY_SHIFT),
}
}
}
@@ -61,6 +66,7 @@ mod tests {
log: CapSlot::new(63),
storage: CapSlot::new(5),
args: true,
display: CapSlot::new(7),
};
assert_eq!(StartArg::decode(a.encode()), a);
assert_eq!(StartArg::decode(0), StartArg::default());

View File

@@ -5,3 +5,15 @@
/// the line's last chunk ends with `\n`. Blocks until a line is entered.
pub const READ: usize = 10;
pub const CLEAR: usize = 11;
/// Repaints the whole screen and takes input focus back, after a program
/// that drew on the screen itself.
pub const REDRAW: usize = 12;
/// Carries a notification the terminal signals on Ctrl+C while no line is
/// being read. Accepted once.
pub const ON_INTERRUPT: usize = 13;
/// What Ctrl+C types.
pub const CTRL_C: u8 = 3;
/// The terminal's notification bits: keys from the display, its flush timer.
pub const KEYS_BIT: usize = 1 << 1;
pub const TIMER_BIT: usize = 1 << 2;

2
disk/bin/snake.manifest Normal file
View File

@@ -0,0 +1,2 @@
# What snake needs beyond the terminal and storage.
needs = display

View File

@@ -1,8 +1,11 @@
The shell runs a program by asking init to load it (init alone may
The shell runs /bin/<name> by asking init to load it (init alone may
create processes), then gives the program the terminal as its output
and a read-only storage grant to the current directory, writes its
arguments onto a page it moves into the program, starts it and waits
for it to die. Then it takes the grant back.
for it to die. Then it takes the grant back. Ctrl+C kills it.
Built-ins: help, echo, clear, cd, pwd. Anything else is a program on
disk: `name args...` runs name.elf.
A program that needs more says so in /bin/<name>.manifest, e.g.
`needs = display` for snake, which then gets the screen and keyboard
until it quits. A need the shell does not know, it refuses to run.
Built-ins: help, echo, clear, cd, pwd.

View File

@@ -5,3 +5,4 @@ this file reached your screen because the shell handed `cat` the
terminal and read-only access to this directory, and nothing else.
Try: ls cd docs cat shell.txt cat ../readme.txt
ls bin snake (q quits) sleep 30, then Ctrl+C

View File

@@ -116,12 +116,21 @@ font. Its output side is the log protocol (`types::log`), so a program whose
log endpoint is the terminal prints there with `println!`: standard output
is whatever text endpoint a program was given. `READ` (`types::terminal`)
blocks until a line is typed and returns it in chunks, the last ending in
`\n`. The desktop is the shell, given the terminal, storage and a spawn
allowance. A command that is not a built-in runs `name.elf`: the shell
spawns it, gives it a call-only copy of the terminal (as its log, so its
output) and a read-only storage grant to the shell's current directory
(`cd`, `pwd`), writes its arguments page, starts it, waits for its death
and releases the grant.
`\n`. Ctrl+C there drops the line. Keys typed while no line is read are
kept for the next one, except Ctrl+C, which signals the notification its
first `ON_INTERRUPT` caller gave. `REDRAW` repaints the screen and takes
input focus back.
The desktop is the shell, given the terminal, storage, the display and a
spawn allowance. Programs live in `/bin`, without extensions. A command
that is not a built-in runs `/bin/name`: the shell spawns it, gives it a
call-only copy of the terminal (as its log, so its output) and a read-only
storage grant to the shell's current directory (`cd`, `pwd`), writes its
arguments page, starts it, waits for its death (Ctrl+C kills it) and
releases the grant. `/bin/name.manifest` (`types::manifest`) asks for more:
`needs = display` gets the display (`StartArg::display`), with which the
program takes focus; the shell has the terminal redraw once it is gone. A
need the shell does not know, it refuses to run.
Only init holds `ProcessCreate`. Everyone else runs programs through init's
spawn service (`types::spawn`, client `libkos::spawn`), which init serves

View File

@@ -1,6 +1,6 @@
#!/bin/bash
# Builds the k-os disk: one ext4 filesystem holding every program init may
# start. Init and storage themselves are packed into the kernel image.
# start, in /bin. Init and storage themselves are packed into the kernel image.
set -euo pipefail
IMG="${IMG:-boot.img}"
@@ -17,31 +17,33 @@ trap 'rm -rf "${ROOT}"' EXIT
copy() {
local src="$1" dst="$2"
if [ -f "${BUILD_DIR}/${src}" ]; then
cp "${BUILD_DIR}/${src}" "${ROOT}/${dst}"
cp "${BUILD_DIR}/${src}" "${ROOT}/bin/${dst}"
fi
}
copy echo "echo.elf"
copy snake "snake.elf"
copy display-qemu "display.elf"
copy input-qemu "input.elf"
copy log-server "log.elf"
copy permission-manager "permis.elf"
copy session-server "session.elf"
copy terminal-server "terminal.elf"
copy shell "shell.elf"
copy cat "cat.elf"
copy ls "ls.elf"
# Files that ship on the disk.
mkdir -p "${ROOT}/bin"
copy echo echo
copy snake snake
copy display-qemu display
copy input-qemu input
copy log-server log
copy permission-manager permission
copy session-server session
copy terminal-server terminal
copy shell shell
copy cat cat
copy ls ls
copy sleep sleep
# Files that ship on the disk, programs' manifests among them.
cp -r disk/. "${ROOT}/"
# Only the test image: its presence makes init run the self-test.
if [ -n "${SMPTEST:-}" ]; then
copy smptest "smptest.elf"
copy spawntest "spawntst.elf"
copy hello "hello.elf"
copy smptest smptest
copy spawntest spawntest
copy hello hello
fi
rm -f "${IMG}"
mkfs.ext4 -q -F -b 4096 -L k-os -O "${FEATURES}" -d "${ROOT}" "${IMG}" "${SIZE}"
echo "=== ${IMG}: ext4, ${SIZE} ==="
ls -1 "${ROOT}"
ls -1 "${ROOT}" "${ROOT}/bin"