From 83809bbe949ba4e759107ea120a833b14a0462f7 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 27 Oct 2026 21:16:00 +0100 Subject: [PATCH] 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 --- Cargo.lock | 8 ++ Cargo.toml | 1 + Makefile | 4 +- crates/apps/shell/src/main.rs | 80 +++++++++++++--- crates/apps/sleep/Cargo.toml | 14 +++ crates/apps/sleep/src/main.rs | 28 ++++++ crates/apps/snake/src/main.rs | 78 +++++++++++----- crates/apps/spawntest/src/main.rs | 6 +- crates/drivers/input/virtio/src/keymap.rs | 19 ++++ crates/drivers/input/virtio/src/lib.rs | 10 +- crates/init/src/lib.rs | 2 +- crates/init/src/manifest.rs | 19 ++-- crates/init/src/selftest.rs | 4 +- crates/libkos/src/display.rs | 19 ++-- crates/libkos/src/spawn.rs | 6 ++ crates/libkos/src/start.rs | 5 + crates/libkos/src/terminal.rs | 19 ++++ crates/servers/session-server/src/lib.rs | 2 +- .../session-server/src/tests/server.rs | 4 +- crates/servers/terminal-server/src/grid.rs | 5 + crates/servers/terminal-server/src/lib.rs | 68 ++++++++++---- crates/servers/terminal-server/src/server.rs | 91 +++++++++++++++++-- .../terminal-server/src/tests/server.rs | 77 +++++++++++++++- crates/types/src/lib.rs | 1 + crates/types/src/manifest.rs | 64 +++++++++++++ crates/types/src/spawn.rs | 4 +- crates/types/src/start_arg.rs | 6 ++ crates/types/src/terminal.rs | 12 +++ disk/bin/snake.manifest | 2 + disk/docs/shell.txt | 11 ++- disk/readme.txt | 1 + docs/ipc.md | 21 +++-- scripts/make-disk-image.sh | 38 ++++---- 33 files changed, 608 insertions(+), 121 deletions(-) create mode 100644 crates/apps/sleep/Cargo.toml create mode 100644 crates/apps/sleep/src/main.rs create mode 100644 crates/types/src/manifest.rs create mode 100644 disk/bin/snake.manifest diff --git a/Cargo.lock b/Cargo.lock index 20906c4..bb878a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,6 +319,14 @@ dependencies = [ "types", ] +[[package]] +name = "sleep" +version = "0.1.0" +dependencies = [ + "libkos", + "types", +] + [[package]] name = "smptest" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 519510b..42be4f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/Makefile b/Makefile index f8c4f5e..0718374 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/crates/apps/shell/src/main.rs b/crates/apps/shell/src/main.rs index b3d6a86..a92c7d4 100644 --- a/crates/apps/shell/src/main.rs +++ b/crates/apps/shell/src/main.rs @@ -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/` 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 { + 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 , clear, cd , 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 { diff --git a/crates/apps/sleep/Cargo.toml b/crates/apps/sleep/Cargo.toml new file mode 100644 index 0000000..c732735 --- /dev/null +++ b/crates/apps/sleep/Cargo.toml @@ -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 } diff --git a/crates/apps/sleep/src/main.rs b/crates/apps/sleep/src/main.rs new file mode 100644 index 0000000..c7a0ad0 --- /dev/null +++ b/crates/apps/sleep/src/main.rs @@ -0,0 +1,28 @@ +//! Waits: `sleep `. 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::().ok()) else { + println!("usage: sleep "); + 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); + } +} diff --git a/crates/apps/snake/src/main.rs b/crates/apps/snake/src/main.rs index 112c0c9..ae59713 100644 --- a/crates/apps/snake/src/main.rs +++ b/crates/apps/snake/src/main.rs @@ -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,31 +63,59 @@ 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 +#[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 { + 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) { + (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 { - let Ok([tag, kind, ch, _]) = syscall::request(DISPLAY_EP, [types::display::input::POLL, 0, 0, 0]) else { - break; - }; - if tag == 0 { break; } // NoEvent — queue drained - if tag == 1 { - // 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), - _ => {} - } + match next_input() { + Input::None => return Input::None, + Input::Quit => return Input::Quit, + Input::Turn(d) => snake.set_direction(d), + Input::Other => {} } } } diff --git a/crates/apps/spawntest/src/main.rs b/crates/apps/spawntest/src/main.rs index 000e638..acb7698 100644 --- a/crates/apps/spawntest/src/main.rs +++ b/crates/apps/spawntest/src/main.rs @@ -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) -> 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"); diff --git a/crates/drivers/input/virtio/src/keymap.rs b/crates/drivers/input/virtio/src/keymap.rs index 5f6583c..eb8520b 100644 --- a/crates/drivers/input/virtio/src/keymap.rs +++ b/crates/drivers/input/virtio/src/keymap.rs @@ -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 { 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'))); diff --git a/crates/drivers/input/virtio/src/lib.rs b/crates/drivers/input/virtio/src/lib.rs index a896245..b78c84c 100644 --- a/crates/drivers/input/virtio/src/lib.rs +++ b/crates/drivers/input/virtio/src/lib.rs @@ -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(®s, 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 { diff --git a/crates/init/src/lib.rs b/crates/init/src/lib.rs index 92af958..65a2453 100644 --- a/crates/init/src/lib.rs +++ b/crates/init/src/lib.rs @@ -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"); diff --git a/crates/init/src/manifest.rs b/crates/init/src/manifest.rs index 06b0040..4b6fa17 100644 --- a/crates/init/src/manifest.rs +++ b/crates/init/src/manifest.rs @@ -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) }]; diff --git a/crates/init/src/selftest.rs b/crates/init/src/selftest.rs index f60eb6e..22420ae 100644 --- a/crates/init/src/selftest.rs +++ b/crates/init/src/selftest.rs @@ -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) diff --git a/crates/libkos/src/display.rs b/crates/libkos/src/display.rs index 8df300b..6904161 100644 --- a/crates/libkos/src/display.rs +++ b/crates/libkos/src/display.rs @@ -55,18 +55,21 @@ pub const CELL_HEIGHT: usize = 16; /// arrive; read them with `poll_input`. pub fn subscribe_input(ep: CapSlot) -> Option { 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. diff --git a/crates/libkos/src/spawn.rs b/crates/libkos/src/spawn.rs index 65d17bb..ec0c5f9 100644 --- a/crates/libkos/src/spawn.rs +++ b/crates/libkos/src/spawn.rs @@ -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 { + 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 { diff --git a/crates/libkos/src/start.rs b/crates/libkos/src/start.rs index ca53651..7b44229 100644 --- a/crates/libkos/src/start.rs +++ b/crates/libkos/src/start.rs @@ -38,6 +38,11 @@ pub fn storage() -> Option { StartArg::decode(arg()).storage } +/// The display server endpoint this process was given, if any. +pub fn display() -> Option { + StartArg::decode(arg()).display +} + /// This program's arguments, its name first; none without an args page. pub fn args() -> impl Iterator { let page: &'static [u8] = if StartArg::decode(arg()).args { diff --git a/crates/libkos/src/terminal.rs b/crates/libkos/src/terminal.rs index dd6852a..ff4cf62 100644 --- a/crates/libkos/src/terminal.rs +++ b/crates/libkos/src/terminal.rs @@ -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) +} diff --git a/crates/servers/session-server/src/lib.rs b/crates/servers/session-server/src/lib.rs index 62a9b95..cf25d7e 100644 --- a/crates/servers/session-server/src/lib.rs +++ b/crates/servers/session-server/src/lib.rs @@ -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) } diff --git a/crates/servers/session-server/src/tests/server.rs b/crates/servers/session-server/src/tests/server.rs index 516dd5b..7df8cb8 100644 --- a/crates/servers/session-server/src/tests/server.rs +++ b/crates/servers/session-server/src/tests/server.rs @@ -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); } diff --git a/crates/servers/terminal-server/src/grid.rs b/crates/servers/terminal-server/src/grid.rs index 7f49544..d3374d9 100644 --- a/crates/servers/terminal-server/src/grid.rs +++ b/crates/servers/terminal-server/src/grid.rs @@ -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; diff --git a/crates/servers/terminal-server/src/lib.rs b/crates/servers/terminal-server/src/lib.rs index 581a76f..3abca18 100644 --- a/crates/servers/terminal-server/src/lib.rs +++ b/crates/servers/terminal-server/src/lib.rs @@ -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 { + 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 { - 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) = self.poll_key() { + return key; } - let _ = syscall::wait(self.keys); + let _ = syscall::wait(self.notify); } } + + fn poll_key(&mut self) -> Option { + while let Some(event) = display::poll_input(DISPLAY) { + if let Some(key) = byte(event) { + return Some(key); + } + } + 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) } diff --git a/crates/servers/terminal-server/src/server.rs b/crates/servers/terminal-server/src/server.rs index d4c5092..141bac4 100644 --- a/crates/servers/terminal-server/src/server.rs +++ b/crates/servers/terminal-server/src/server.rs @@ -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; + /// 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 { console: C, @@ -30,6 +37,10 @@ pub struct TerminalServer { 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, } /// An empty reply, whose tag is not text. @@ -50,6 +61,9 @@ impl TerminalServer { 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 TerminalServer { &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 TerminalServer { } } - /// 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 Server for TerminalServer { Done } - fn handle_call(&mut self, _badge: usize, words: [usize; 4], _cap: Option) -> ([usize; 4], Option) { + fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option) -> ([usize; 4], Option) { let reply = match words[0] { types::terminal::READ => self.read(), + 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,10 +238,15 @@ impl Server for TerminalServer { (reply, None) } - /// The flush timer: show a burst of output in one frame. - fn signal(&mut self, _bits: usize) { - self.flush_pending = false; - self.console.flush(); + /// 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(); + } } } diff --git a/crates/servers/terminal-server/src/tests/server.rs b/crates/servers/terminal-server/src/tests/server.rs index aae87cf..edea85d 100644 --- a/crates/servers/terminal-server/src/tests/server.rs +++ b/crates/servers/terminal-server/src/tests/server.rs @@ -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, + /// Typed while nobody reads a line. + typed: Vec, flushes: usize, timers: usize, + focus_taken: usize, + interrupted: Vec, } 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 { + (!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 { @@ -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) -> Vec { + let mut line = read(t); + while line.last() != Some(&b'\n') { + line.extend(read(t)); + } + line +} + +fn on_interrupt(t: &mut TerminalServer, 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"); +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 21447ca..43df8ac 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -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)] diff --git a/crates/types/src/manifest.rs b/crates/types/src/manifest.rs new file mode 100644 index 0000000..43d77a7 --- /dev/null +++ b/crates/types/src/manifest.rs @@ -0,0 +1,64 @@ +//! A program's manifest, `/bin/.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> { + 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"))); + } +} diff --git a/crates/types/src/spawn.rs b/crates/types/src/spawn.rs index d886366..f136dee 100644 --- a/crates/types/src/spawn.rs +++ b/crates/types/src/spawn.rs @@ -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); } diff --git a/crates/types/src/start_arg.rs b/crates/types/src/start_arg.rs index eb07889..5eded42 100644 --- a/crates/types/src/start_arg.rs +++ b/crates/types/src/start_arg.rs @@ -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, pub storage: Option, pub args: bool, + /// The display server, for a program that draws on the screen. + pub display: Option, } fn field(arg: usize, shift: u32) -> Option { @@ -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()); diff --git a/crates/types/src/terminal.rs b/crates/types/src/terminal.rs index cfd1bb7..31c5faa 100644 --- a/crates/types/src/terminal.rs +++ b/crates/types/src/terminal.rs @@ -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; diff --git a/disk/bin/snake.manifest b/disk/bin/snake.manifest new file mode 100644 index 0000000..76f0f83 --- /dev/null +++ b/disk/bin/snake.manifest @@ -0,0 +1,2 @@ +# What snake needs beyond the terminal and storage. +needs = display diff --git a/disk/docs/shell.txt b/disk/docs/shell.txt index bcee5d6..26aa4ff 100644 --- a/disk/docs/shell.txt +++ b/disk/docs/shell.txt @@ -1,8 +1,11 @@ -The shell runs a program by asking init to load it (init alone may +The shell runs /bin/ 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/.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. diff --git a/disk/readme.txt b/disk/readme.txt index 39cc748..472d84a 100644 --- a/disk/readme.txt +++ b/disk/readme.txt @@ -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 diff --git a/docs/ipc.md b/docs/ipc.md index a55cb32..dd5f646 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -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 diff --git a/scripts/make-disk-image.sh b/scripts/make-disk-image.sh index 83312ce..ccaed56 100755 --- a/scripts/make-disk-image.sh +++ b/scripts/make-disk-image.sh @@ -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"