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", "types",
] ]
[[package]]
name = "sleep"
version = "0.1.0"
dependencies = [
"libkos",
"types",
]
[[package]] [[package]]
name = "smptest" name = "smptest"
version = "0.1.0" version = "0.1.0"

View File

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

View File

@@ -52,7 +52,7 @@ kani: kani-types kani-domain kani-kernel
# adapter's build.rs). # adapter's build.rs).
build: build:
cargo build --target $(RISCV_TARGET) --release \ 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 display-qemu -p input-qemu -p storage-qemu \
-p log-server -p permission-manager -p session-server -p smptest -p spawntest -p hello \ -p log-server -p permission-manager -p session-server -p smptest -p spawntest -p hello \
-p terminal-server -p shell -p cat -p ls -p terminal-server -p shell -p cat -p ls
@@ -61,7 +61,7 @@ build:
image: build image: build
scripts/make-disk-image.sh 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". # the SMP self-test and halts. Passes only on "SMPTEST PASS".
test-qemu: build test-qemu: build
IMG=test.img SMPTEST=1 scripts/make-disk-image.sh > /dev/null 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. //! 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 //! `name args...` runs `/bin/name` through init, handing it the terminal as
//! its output and read-only storage rooted at the current directory, and //! its output, read-only storage rooted at the current directory, and what
//! nothing else; then waits for it and takes the storage back. //! 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_std]
#![no_main] #![no_main]
@@ -10,6 +11,7 @@ extern crate libkos;
use libkos::fs::Storage; use libkos::fs::Storage;
use libkos::spawn::{SpawnError, spawn}; use libkos::spawn::{SpawnError, spawn};
use types::manifest::Needs;
use libkos::{print, println, syscall}; use libkos::{print, println, syscall};
use types::bounded::CapSlot; use types::bounded::CapSlot;
use types::budget::Limits; use types::budget::Limits;
@@ -19,7 +21,15 @@ use types::start_arg::StartArg;
/// Where init's manifest put them: `uses`, then the spawn endpoint. /// Where init's manifest put them: `uses`, then the spawn endpoint.
const TERMINAL: CapSlot = CapSlot::const_new(0); const TERMINAL: CapSlot = CapSlot::const_new(0);
const STORAGE: CapSlot = CapSlot::const_new(1); 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. /// What each program gets, out of the shell's allowance.
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 }; const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
@@ -91,19 +101,46 @@ struct Shell {
done: CapSlot, done: CapSlot,
} }
/// Runs `args[0].elf` with `args` in the current directory, and waits /// `/bin/<name><suffix>` into `out`, if it fits.
/// for it to finish. 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]) { fn run_program(sh: &mut Shell, args: &[&str]) {
let mut file = [0u8; types::spawn::MAX_NAME]; let mut file = [0u8; types::spawn::MAX_NAME];
let name = args[0].as_bytes(); let Some(file) = bin_path(&mut file, args[0], b"") else {
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 {
println!("shell: {}: name too long", args[0]); println!("shell: {}: name too long", args[0]);
return; return;
}; };
let Some(needs) = needs(sh, args[0]) else {
return;
};
let child = match spawn(INIT, file, PROGRAM) { let child = match spawn(INIT, file, PROGRAM) {
Ok(c) => c, Ok(c) => c,
Err(SpawnError::NotFound) => return println!("shell: {}: not found", args[0]), 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), log: child.give(TERMINAL, call),
storage: grant.and_then(|g| child.give(g, call)), storage: grant.and_then(|g| child.give(g, call)),
args: child.set_args(args), 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, 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(); let watched = syscall::watch_death(child.process, sh.done).is_ok();
if child.start_with(arg) && watched { 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 { } else {
let _ = syscall::kill(child.process); let _ = syscall::kill(child.process);
println!("shell: {}: cannot start", args[0]); println!("shell: {}: cannot start", args[0]);
@@ -129,6 +175,10 @@ fn run_program(sh: &mut Shell, args: &[&str]) {
if let Some(g) = grant { if let Some(g) = grant {
let _ = libkos::fs::release(g); 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) { 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!("built-ins: help, echo <text>, clear, cd <dir>, pwd");
println!("anything else runs a program from the disk: try ls, cat readme.txt"); 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!("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("cd") => cd(sh, args.get(1).copied().unwrap_or("/")),
Some("pwd") => println!("{}", sh.cwd), Some("pwd") => println!("{}", sh.cwd),
@@ -188,6 +239,9 @@ extern "C" fn main() {
println!("shell: no storage"); println!("shell: no storage");
return; return;
}; };
if !libkos::terminal::on_interrupt(TERMINAL, done, types::badge::Badge::new(INTERRUPTED)) {
println!("shell: Ctrl+C will not work");
}
let mut sh = Shell { let mut sh = Shell {
fs, fs,
cwd: Cwd { 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 server_framework::Encode;
use display_server::protocol::DisplayRequest; 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 TICK_DELAY: u64 = 5_000_000;
const MAX_RENDER_CMDS: usize = 140; 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) { 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) { fn draw_rect(x: u16, y: u16, w: u16, h: u16, color: u32) {
@@ -37,8 +41,12 @@ fn flush() {
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
extern "C" fn main() { 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. // 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 snake = Snake::new();
let mut commands = [RenderCommand { x: 0, y: 0, w: 0, h: 0, color: 0 }; MAX_RENDER_CMDS]; let mut commands = [RenderCommand { x: 0, y: 0, w: 0, h: 0, color: 0 }; MAX_RENDER_CMDS];
@@ -55,31 +63,59 @@ extern "C" fn main() {
let gh = renderer::grid_pixel_height(); let gh = renderer::grid_pixel_height();
draw_rect(gw / 2 - 60, gh / 2 - 12, 120, 24, 0xFFCC0000); draw_rect(gw / 2 - 60, gh / 2 - 12, 120, 24, 0xFFCC0000);
flush(); flush();
loop { syscall::yield_now(); } // Any key leaves.
while next_input() == Input::None {
wait_tick();
}
return;
} }
wait_tick(); wait_tick();
handle_input(&mut snake); if handle_input(&mut snake) == Input::Quit {
return;
}
snake.tick(); snake.tick();
} }
} }
fn handle_input(snake: &mut Snake) { #[derive(PartialEq)]
// Drain all pending events, apply the latest direction enum Input {
loop { None,
let Ok([tag, kind, ch, _]) = syscall::request(DISPLAY_EP, [types::display::input::POLL, 0, 0, 0]) else { Quit,
break; 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 == 0 {
if tag == 1 { return Input::None; // NoEvent: queue drained
}
if tag != 1 {
return Input::Other; // a key let go
}
// Arrows, or WASD (kind 9 is a character). // Arrows, or WASD (kind 9 is a character).
match (kind, ch as u8) { match (kind, ch as u8) {
(0, _) | (9, b'w') => snake.set_direction(Direction::Up), (5, _) | (9, b'q') => Input::Quit,
(1, _) | (9, b's') => snake.set_direction(Direction::Down), (0, _) | (9, b'w') => Input::Turn(Direction::Up),
(2, _) | (9, b'a') => snake.set_direction(Direction::Left), (1, _) | (9, b's') => Input::Turn(Direction::Down),
(3, _) | (9, b'd') => snake.set_direction(Direction::Right), (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` /// Spawns hello, runs it, and waits for it to die; hands it `grant`
/// first, if there is one. /// first, if there is one.
fn run_hello(died: CapSlot, grant: Option<CapSlot>) -> Result<(), &'static str> { 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); let call = types::permission::PermissionSet::empty().with(types::permission::Permission::Execute);
if grant.is_some_and(|g| child.give(g, call).is_none()) { if grant.is_some_and(|g| child.give(g, call).is_none()) {
return Err("a storage grant could not be passed on"); 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"); libkos::println!("spawntest: spawned program runs and its death is heard: ok");
let greedy = Limits { pages: 200, ..HELLO }; 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"); return Err("init spawned past the allowance");
} }
let tiny = Limits { pages: 1, ..HELLO }; 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"); return Err("a missing program was not reported");
} }
libkos::println!("spawntest: allowance and missing files refused: ok"); 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 LEFT_SHIFT: u16 = 42;
pub const RIGHT_SHIFT: u16 = 54; 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 /// Rows of the main block: the first key's code, then each key unshifted
/// and shifted. /// and shifted.
@@ -41,6 +52,14 @@ pub fn key(code: u16, shift: bool) -> Option<Key> {
mod tests { mod tests {
use super::*; 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] #[test]
fn letters_digits_and_shift() { fn letters_digits_and_shift() {
assert_eq!(key(35, false), Some(Key::Char(b'h'))); assert_eq!(key(35, false), Some(Key::Char(b'h')));

View File

@@ -31,6 +31,7 @@ pub struct VirtioInputDevice {
next_avail: u16, next_avail: u16,
evbuf_pa: usize, evbuf_pa: usize,
shift: bool, shift: bool,
ctrl: bool,
} }
impl VirtioInputDevice { impl VirtioInputDevice {
@@ -48,7 +49,7 @@ impl VirtioInputDevice {
let evbuf_pa = libkos::device::dma(1, EVBUF_VA).ok()?; let evbuf_pa = libkos::device::dma(1, EVBUF_VA).ok()?;
hw_virtio::mmio::init_legacy_device(&regs, vq_pa, QUEUE_SIZE as u32); 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(); dev.fill_queue();
Some(dev) Some(dev)
} }
@@ -139,8 +140,13 @@ impl InputDevice for VirtioInputDevice {
self.shift = ev_value != 0; self.shift = ev_value != 0;
continue; 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 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 { if ev_value == KEY_PRESS {
return Some(InputEvent::KeyDown(key)); return Some(InputEvent::KeyDown(key));
} else if ev_value == 0 { } 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. // instead of the desktop, and the last one halts the machine.
let (storage, scratch) = launcher.storage(); let (storage, scratch) = launcher.storage();
if storage if storage
.open(b"smptest.elf") .open(b"bin/smptest")
.is_ok_and(|h| storage.close(h).is_ok()) .is_ok_and(|h| storage.close(h).is_ok())
{ {
libkos::println!("init: running the SMP self-test"); 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. /// Started on every boot. The log server comes first so the rest can print.
pub const SERVERS: &[Program] = &[ pub const SERVERS: &[Program] = &[
Program { console: true, ..server("log", b"log.elf", SMALL) }, Program { console: true, ..server("log", b"bin/log", SMALL) },
Program { device: Some(Device::Input), ..server("input", b"input.elf", Limits { regions: 4, ..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. // The input server's only client: it passes input on to the app in focus.
Program { Program {
device: Some(Device::Gpu), device: Some(Device::Gpu),
uses: &["input"], uses: &["input"],
// The framebuffer alone is ~1000 pages at 1280x800. // 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). // Text on the display, lines from the keyboard (it takes input focus).
Program { uses: &["display"], ..server("terminal", b"terminal.elf", SMALL) }, Program { uses: &["display"], ..server("terminal", b"bin/terminal", SMALL) },
server("permis", b"permis.elf", SMALL), server("permission", b"bin/permission", SMALL),
server("session", b"session.elf", SMALL), server("session", b"bin/session", SMALL),
]; ];
/// Run on the test image after the SMP self-test, instead of the desktop: /// 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"], uses: &["storage"],
spawns: Some(Limits { pages: 128, endpoints: 2, notifications: 2, regions: 2 }), spawns: Some(Limits { pages: 128, endpoints: 2, notifications: 2, regions: 2 }),
system_control: true, 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. /// The desktop; the test image runs the SMP self-test instead.
pub const APPS: &[Program] = &[Program { pub const APPS: &[Program] = &[Program {
serves: false, 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. // Room for one program at a time, the way the shell runs them.
spawns: Some(Limits { pages: 256, endpoints: 2, notifications: 2, regions: 2 }), 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. //! spinner's page or killing it, no spinner counter may move on any hart.
use core::sync::atomic::{AtomicUsize, Ordering}; 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 { for i in 0..=THREADS {
word(i).store(0, Ordering::SeqCst); 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() let rw = PermissionSet::empty()
.with(Permission::Read) .with(Permission::Read)
.with(Permission::Write) .with(Permission::Write)

View File

@@ -55,18 +55,21 @@ pub const CELL_HEIGHT: usize = 16;
/// arrive; read them with `poll_input`. /// arrive; read them with `poll_input`.
pub fn subscribe_input(ep: CapSlot) -> Option<CapSlot> { pub fn subscribe_input(ep: CapSlot) -> Option<CapSlot> {
use types::badge::Badge; use types::badge::Badge;
use types::permission::{Permission, PermissionSet};
let notify = syscall::create_notification().ok()?; 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). // Signal only (Delegate just to send it; the transfer strips it).
let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate); let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate);
let copy = syscall::mint(notify, Badge::NONE, signal).ok()?; let Ok(copy) = syscall::mint(notify, badge, signal) else {
let sent = syscall::call( return false;
ep, };
[types::display::input::SUBSCRIBE, 0, 0, 0], let sent = syscall::call(ep, [types::display::input::SUBSCRIBE, 0, 0, 0], Some(copy.into()));
Some(copy.into()),
);
syscall::drop_cap(copy); 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. /// 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)) 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 /// Puts `args` (its name first) where the program finds them: a page
/// this process fills and then moves into the program, which owns it. /// this process fills and then moves into the program, which owns it.
pub fn set_args(&self, args: &[&str]) -> bool { pub fn set_args(&self, args: &[&str]) -> bool {

View File

@@ -38,6 +38,11 @@ pub fn storage() -> Option<CapSlot> {
StartArg::decode(arg()).storage 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. /// This program's arguments, its name first; none without an args page.
pub fn args() -> impl Iterator<Item = &'static str> { pub fn args() -> impl Iterator<Item = &'static str> {
let page: &'static [u8] = if StartArg::decode(arg()).args { 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) { pub fn clear(terminal: CapSlot) {
let _ = syscall::request(terminal, [types::terminal::CLEAR, 0, 0, 0]); 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 // Slot 0 holds the endpoint init delegated. Init has no process
// creation authority to hand on yet, so Launch reports an error. // creation authority to hand on yet, so Launch reports an error.
let mut srv = server::SessionServer::new(None); 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(); let ep = types::bounded::CapSlot::new(0).unwrap();
server_framework::serve(&mut srv, ep) server_framework::serve(&mut srv, ep)
} }

View File

@@ -5,7 +5,7 @@ use server_framework::Server;
fn test_server() -> SessionServer { fn test_server() -> SessionServer {
let cap = types::bounded::CapSlot::new(0).unwrap(); let cap = types::bounded::CapSlot::new(0).unwrap();
let mut s = SessionServer::new(Some(cap)); 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.register_app("clock", "/apps/clock.elf").unwrap();
s s
} }
@@ -108,7 +108,7 @@ fn the_app_table_fills_up_without_panicking() {
#[test] #[test]
fn launching_needs_process_creation_authority() { fn launching_needs_process_creation_authority() {
let mut server = SessionServer::new(None); 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) }); let resp = server.handle(SessionRequest::Launch { app: AppId::new(0) });
assert_eq!(resp, SessionResponse::Error); 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) { pub fn clear(&mut self) {
self.cells.fill(b' '); self.cells.fill(b' ');
self.col = 0; self.col = 0;

View File

@@ -12,8 +12,10 @@ mod tests;
use libkos::display::{self, CELL_HEIGHT, CELL_WIDTH}; use libkos::display::{self, CELL_HEIGHT, CELL_WIDTH};
use libkos::input::{InputEvent, Key}; use libkos::input::{InputEvent, Key};
use libkos::syscall; use libkos::syscall;
use types::badge::Badge;
use types::bounded::CapSlot; use types::bounded::CapSlot;
use types::display::{Color, ScreenCoord}; use types::display::{Color, ScreenCoord};
use types::terminal::{KEYS_BIT, TIMER_BIT};
/// Where init put them (its manifest: own endpoint, then `uses`). /// Where init put them (its manifest: own endpoint, then `uses`).
const EP: CapSlot = CapSlot::const_new(0); 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; const FLUSH_DELAY: u64 = 20 * 10_000;
struct Screen { struct Screen {
keys: CapSlot, /// Bound to the serving thread: the display signals it with
/// Bound to the serving thread; the flush timer signals it. /// `KEYS_BIT`, the flush timer with `TIMER_BIT`.
notify: CapSlot,
timer: 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) { fn at(col: usize, row: usize) -> (ScreenCoord, ScreenCoord) {
(ScreenCoord::new((col * CELL_WIDTH) as u16), ScreenCoord::new((row * CELL_HEIGHT) as u16)) (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 { fn next_key(&mut self) -> u8 {
loop { 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) { while let Some(event) = display::poll_input(DISPLAY) {
let InputEvent::KeyDown(key) = event else { continue }; if let Some(key) = byte(event) {
match key { return Some(key);
Key::Char(c) => return c,
Key::Space | Key::Tab => return b' ',
Key::Enter => return b'\n',
Key::Backspace => return server::BACKSPACE,
_ => {}
} }
} }
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() -> ! { 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"); libkos::println!("terminal: no input focus, exiting");
syscall::exit(); 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); let (w, h) = display::get_dimensions(DISPLAY);
display::clear(DISPLAY, BG); display::clear(DISPLAY, BG);
let cols = w.raw() as usize / CELL_WIDTH; let cols = w.raw() as usize / CELL_WIDTH;
let rows = h.raw() as usize / CELL_HEIGHT; 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) server_framework::serve(&mut srv, EP)
} }

View File

@@ -17,10 +17,17 @@ pub trait Console {
fn flush_later(&mut self); fn flush_later(&mut self);
/// Blocks until a key is typed: a byte, `\n` for Enter, 8 for Backspace. /// Blocks until a key is typed: a byte, `\n` for Enter, 8 for Backspace.
fn next_key(&mut self) -> u8; 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; pub const BACKSPACE: u8 = 8;
const MAX_LINE: usize = 256; 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> { pub struct TerminalServer<C: Console> {
console: C, console: C,
@@ -30,6 +37,10 @@ pub struct TerminalServer<C: Console> {
line_len: usize, line_len: usize,
line_read: usize, line_read: usize,
flush_pending: bool, 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. /// An empty reply, whose tag is not text.
@@ -50,6 +61,9 @@ impl<C: Console> TerminalServer<C> {
line_len: 0, line_len: 0,
line_read: 0, line_read: 0,
flush_pending: false, flush_pending: false,
typeahead: [0; TYPEAHEAD],
typeahead_len: 0,
on_interrupt: None,
}; };
s.redraw(); s.redraw();
s.console.flush(); s.console.flush();
@@ -60,6 +74,11 @@ impl<C: Console> TerminalServer<C> {
&self.console &self.console
} }
#[cfg(test)]
pub fn console_mut_for_test(&mut self) -> &mut C {
&mut self.console
}
pub fn grid(&self) -> &Grid { pub fn grid(&self) -> &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) { fn read_line(&mut self) {
self.console.flush(); self.console.flush();
// Flushed now; a flush timer still running is harmless.
self.flush_pending = false;
self.line_len = 0; self.line_len = 0;
self.line_read = 0; self.line_read = 0;
loop { 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' => { b'\n' => {
self.line[self.line_len] = b'\n'; self.line[self.line_len] = b'\n';
self.line_len += 1; self.line_len += 1;
@@ -148,9 +206,23 @@ impl<C: Console> Server for TerminalServer<C> {
Done 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] { let reply = match words[0] {
types::terminal::READ => self.read(), 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 => { types::terminal::CLEAR => {
self.handle(Request::Clear); self.handle(Request::Clear);
[0; 4] [0; 4]
@@ -166,11 +238,16 @@ impl<C: Console> Server for TerminalServer<C> {
(reply, None) (reply, None)
} }
/// The flush timer: show a burst of output in one frame. /// Keys, or the flush timer: show a burst of output in one frame.
fn signal(&mut self, _bits: usize) { 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.flush_pending = false;
self.console.flush(); self.console.flush();
} }
}
} }
pub enum Request { pub enum Request {

View File

@@ -2,14 +2,21 @@ extern crate std;
use crate::server::{BACKSPACE, Console, TerminalServer}; use crate::server::{BACKSPACE, Console, TerminalServer};
use server_framework::Server; use server_framework::Server;
use std::vec;
use std::vec::Vec; use std::vec::Vec;
use types::bounded::CapSlot;
use types::log::{text_of, text_words}; use types::log::{text_of, text_words};
use types::terminal::{CTRL_C, KEYS_BIT, TIMER_BIT};
#[derive(Default)] #[derive(Default)]
struct Fake { struct Fake {
keys: Vec<u8>, keys: Vec<u8>,
/// Typed while nobody reads a line.
typed: Vec<u8>,
flushes: usize, flushes: usize,
timers: usize, timers: usize,
focus_taken: usize,
interrupted: Vec<CapSlot>,
} }
impl Console for Fake { impl Console for Fake {
@@ -24,6 +31,15 @@ impl Console for Fake {
fn next_key(&mut self) -> u8 { fn next_key(&mut self) -> u8 {
self.keys.remove(0) 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> { 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.grid().row(0)[..11], b"hello world");
assert_eq!((t.console().timers, t.console().flushes), (1, before)); assert_eq!((t.console().timers, t.console().flushes), (1, before));
t.signal(1); t.signal(TIMER_BIT);
assert_eq!(t.console().flushes, before + 1); 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']); let mut t = term(&[BACKSPACE, b'x', b'\n']);
assert_eq!(read(&mut t), b"x\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 input;
pub mod log; pub mod log;
pub mod macros; pub mod macros;
pub mod manifest;
pub mod mapping; pub mod mapping;
pub mod permission; pub mod permission;
#[cfg(kani)] #[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, notifications: 3,
regions: 255, regions: 255,
}; };
let (name, len, back) = parse(request(b"hello.elf", l).unwrap()).unwrap(); let (name, len, back) = parse(request(b"bin/hello", l).unwrap()).unwrap();
assert_eq!(&name[..len], b"hello.elf"); assert_eq!(&name[..len], b"bin/hello");
assert_eq!(back, l); assert_eq!(back, l);
} }

View File

@@ -8,6 +8,7 @@ const CONSOLE_SHIFT: u32 = 0;
const LOG_SHIFT: u32 = 8; const LOG_SHIFT: u32 = 8;
const STORAGE_SHIFT: u32 = 16; const STORAGE_SHIFT: u32 = 16;
const ARGS_BIT: usize = 1 << 24; const ARGS_BIT: usize = 1 << 24;
const DISPLAY_SHIFT: u32 = 32;
/// Where the arguments page sits: the program's arguments, NUL separated, /// Where the arguments page sits: the program's arguments, NUL separated,
/// the first its name. Below the stack, which ends at the image. /// the first its name. Below the stack, which ends at the image.
@@ -21,6 +22,8 @@ pub struct StartArg {
pub log: Option<CapSlot>, pub log: Option<CapSlot>,
pub storage: Option<CapSlot>, pub storage: Option<CapSlot>,
pub args: bool, 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> { fn field(arg: usize, shift: u32) -> Option<CapSlot> {
@@ -38,6 +41,7 @@ impl StartArg {
| put(self.log, LOG_SHIFT) | put(self.log, LOG_SHIFT)
| put(self.storage, STORAGE_SHIFT) | put(self.storage, STORAGE_SHIFT)
| if self.args { ARGS_BIT } else { 0 } | if self.args { ARGS_BIT } else { 0 }
| put(self.display, DISPLAY_SHIFT)
} }
pub fn decode(arg: usize) -> Self { pub fn decode(arg: usize) -> Self {
@@ -46,6 +50,7 @@ impl StartArg {
log: field(arg, LOG_SHIFT), log: field(arg, LOG_SHIFT),
storage: field(arg, STORAGE_SHIFT), storage: field(arg, STORAGE_SHIFT),
args: arg & ARGS_BIT != 0, args: arg & ARGS_BIT != 0,
display: field(arg, DISPLAY_SHIFT),
} }
} }
} }
@@ -61,6 +66,7 @@ mod tests {
log: CapSlot::new(63), log: CapSlot::new(63),
storage: CapSlot::new(5), storage: CapSlot::new(5),
args: true, args: true,
display: CapSlot::new(7),
}; };
assert_eq!(StartArg::decode(a.encode()), a); assert_eq!(StartArg::decode(a.encode()), a);
assert_eq!(StartArg::decode(0), StartArg::default()); 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. /// the line's last chunk ends with `\n`. Blocks until a line is entered.
pub const READ: usize = 10; pub const READ: usize = 10;
pub const CLEAR: usize = 11; 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 create processes), then gives the program the terminal as its output
and a read-only storage grant to the current directory, writes its 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 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 A program that needs more says so in /bin/<name>.manifest, e.g.
disk: `name args...` runs name.elf. `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. terminal and read-only access to this directory, and nothing else.
Try: ls cd docs cat shell.txt cat ../readme.txt 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 log endpoint is the terminal prints there with `println!`: standard output
is whatever text endpoint a program was given. `READ` (`types::terminal`) 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 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 `\n`. Ctrl+C there drops the line. Keys typed while no line is read are
allowance. A command that is not a built-in runs `name.elf`: the shell kept for the next one, except Ctrl+C, which signals the notification its
spawns it, gives it a call-only copy of the terminal (as its log, so its first `ON_INTERRUPT` caller gave. `REDRAW` repaints the screen and takes
output) and a read-only storage grant to the shell's current directory input focus back.
(`cd`, `pwd`), writes its arguments page, starts it, waits for its death
and releases the grant. 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 Only init holds `ProcessCreate`. Everyone else runs programs through init's
spawn service (`types::spawn`, client `libkos::spawn`), which init serves spawn service (`types::spawn`, client `libkos::spawn`), which init serves

View File

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