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:
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -319,6 +319,14 @@ dependencies = [
|
||||
"types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sleep"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libkos",
|
||||
"types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smptest"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -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",
|
||||
|
||||
4
Makefile
4
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
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! The shell: a few built-ins, and anything else is a program on disk.
|
||||
//! `name args...` runs `name.elf` through init, handing it the terminal as
|
||||
//! its output and read-only storage rooted at the current directory, and
|
||||
//! nothing else; then waits for it and takes the storage back.
|
||||
//! `name args...` runs `/bin/name` through init, handing it the terminal as
|
||||
//! its output, read-only storage rooted at the current directory, and what
|
||||
//! its manifest (`/bin/name.manifest`) asks for that the shell can give;
|
||||
//! then waits for it (Ctrl+C kills it) and takes the storage back.
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
@@ -10,6 +11,7 @@ extern crate libkos;
|
||||
|
||||
use libkos::fs::Storage;
|
||||
use libkos::spawn::{SpawnError, spawn};
|
||||
use types::manifest::Needs;
|
||||
use libkos::{print, println, syscall};
|
||||
use types::bounded::CapSlot;
|
||||
use types::budget::Limits;
|
||||
@@ -19,7 +21,15 @@ use types::start_arg::StartArg;
|
||||
/// Where init's manifest put them: `uses`, then the spawn endpoint.
|
||||
const TERMINAL: CapSlot = CapSlot::const_new(0);
|
||||
const STORAGE: CapSlot = CapSlot::const_new(1);
|
||||
const INIT: CapSlot = CapSlot::const_new(2);
|
||||
const DISPLAY: CapSlot = CapSlot::const_new(2);
|
||||
const INIT: CapSlot = CapSlot::const_new(3);
|
||||
|
||||
/// Bits on the shell's notification: the program died (its death watch is
|
||||
/// unbadged), or Ctrl+C was typed (the terminal's copy).
|
||||
const DIED: usize = 1;
|
||||
const INTERRUPTED: usize = 1 << 1;
|
||||
/// Longest manifest read.
|
||||
const MAX_MANIFEST: usize = 512;
|
||||
|
||||
/// What each program gets, out of the shell's allowance.
|
||||
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
|
||||
@@ -91,19 +101,46 @@ struct Shell {
|
||||
done: CapSlot,
|
||||
}
|
||||
|
||||
/// Runs `args[0].elf` with `args` in the current directory, and waits
|
||||
/// for it to finish.
|
||||
/// `/bin/<name><suffix>` into `out`, if it fits.
|
||||
fn bin_path<'a>(out: &'a mut [u8], name: &str, suffix: &[u8]) -> Option<&'a [u8]> {
|
||||
let path = out.get_mut(..4 + name.len() + suffix.len())?;
|
||||
path[..4].copy_from_slice(b"bin/");
|
||||
path[4..4 + name.len()].copy_from_slice(name.as_bytes());
|
||||
path[4 + name.len()..].copy_from_slice(suffix);
|
||||
Some(path)
|
||||
}
|
||||
|
||||
/// What `name`'s manifest asks for; none without one. `None` if it asks
|
||||
/// for something the shell does not know how to give.
|
||||
fn needs(sh: &mut Shell, name: &str) -> Option<Needs> {
|
||||
let mut path = [0u8; types::storage::MAX_PATH];
|
||||
let path = bin_path(&mut path, name, b".manifest")?;
|
||||
let Ok(file) = sh.fs.open(path) else {
|
||||
return Some(Needs::default());
|
||||
};
|
||||
let mut text = [0u8; MAX_MANIFEST];
|
||||
let read = sh.fs.read(file, &mut text);
|
||||
let _ = sh.fs.close(file);
|
||||
match types::manifest::parse(&text[..read.ok()?]) {
|
||||
Ok(needs) => Some(needs),
|
||||
Err(types::manifest::Unknown(what)) => {
|
||||
println!("shell: {}: its manifest asks for {}", name, core::str::from_utf8(what).unwrap_or("?"));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `/bin/args[0]` with `args` in the current directory, and waits
|
||||
/// for it to finish or Ctrl+C.
|
||||
fn run_program(sh: &mut Shell, args: &[&str]) {
|
||||
let mut file = [0u8; types::spawn::MAX_NAME];
|
||||
let name = args[0].as_bytes();
|
||||
let Some(file) = file.get_mut(..name.len() + 4).map(|f| {
|
||||
f[..name.len()].copy_from_slice(name);
|
||||
f[name.len()..].copy_from_slice(b".elf");
|
||||
&*f
|
||||
}) else {
|
||||
let Some(file) = bin_path(&mut file, args[0], b"") else {
|
||||
println!("shell: {}: name too long", args[0]);
|
||||
return;
|
||||
};
|
||||
let Some(needs) = needs(sh, args[0]) else {
|
||||
return;
|
||||
};
|
||||
let child = match spawn(INIT, file, PROGRAM) {
|
||||
Ok(c) => c,
|
||||
Err(SpawnError::NotFound) => return println!("shell: {}: not found", args[0]),
|
||||
@@ -116,11 +153,20 @@ fn run_program(sh: &mut Shell, args: &[&str]) {
|
||||
log: child.give(TERMINAL, call),
|
||||
storage: grant.and_then(|g| child.give(g, call)),
|
||||
args: child.set_args(args),
|
||||
// It subscribes to input by sending the display a notification.
|
||||
display: needs.display.then(|| child.give_sending(DISPLAY, call)).flatten(),
|
||||
console: None,
|
||||
};
|
||||
// A Ctrl+C typed since the last program is not for this one.
|
||||
let _ = syscall::poll(sh.done);
|
||||
let watched = syscall::watch_death(child.process, sh.done).is_ok();
|
||||
if child.start_with(arg) && watched {
|
||||
let _ = syscall::wait(sh.done);
|
||||
while syscall::wait(sh.done).is_ok_and(|bits| {
|
||||
if bits & INTERRUPTED != 0 {
|
||||
syscall::kill(child.process);
|
||||
}
|
||||
bits & DIED == 0
|
||||
}) {}
|
||||
} else {
|
||||
let _ = syscall::kill(child.process);
|
||||
println!("shell: {}: cannot start", args[0]);
|
||||
@@ -129,6 +175,10 @@ fn run_program(sh: &mut Shell, args: &[&str]) {
|
||||
if let Some(g) = grant {
|
||||
let _ = libkos::fs::release(g);
|
||||
}
|
||||
// It drew over the terminal and took the keyboard.
|
||||
if needs.display {
|
||||
libkos::terminal::redraw(TERMINAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn cd(sh: &mut Shell, to: &str) {
|
||||
@@ -152,6 +202,7 @@ fn run(sh: &mut Shell, line: &str) {
|
||||
println!("built-ins: help, echo <text>, clear, cd <dir>, pwd");
|
||||
println!("anything else runs a program from the disk: try ls, cat readme.txt");
|
||||
println!("programs see the current directory, read-only, and nothing above it");
|
||||
println!("Ctrl+C stops a running program; snake quits with q");
|
||||
}
|
||||
Some("cd") => cd(sh, args.get(1).copied().unwrap_or("/")),
|
||||
Some("pwd") => println!("{}", sh.cwd),
|
||||
@@ -188,6 +239,9 @@ extern "C" fn main() {
|
||||
println!("shell: no storage");
|
||||
return;
|
||||
};
|
||||
if !libkos::terminal::on_interrupt(TERMINAL, done, types::badge::Badge::new(INTERRUPTED)) {
|
||||
println!("shell: Ctrl+C will not work");
|
||||
}
|
||||
let mut sh = Shell {
|
||||
fs,
|
||||
cwd: Cwd {
|
||||
|
||||
14
crates/apps/sleep/Cargo.toml
Normal file
14
crates/apps/sleep/Cargo.toml
Normal 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 }
|
||||
28
crates/apps/sleep/src/main.rs
Normal file
28
crates/apps/sleep/src/main.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const HELLO: Limits = Limits { pages: 80, endpoints: 0, notifications: 0, region
|
||||
/// Spawns hello, runs it, and waits for it to die; hands it `grant`
|
||||
/// first, if there is one.
|
||||
fn run_hello(died: CapSlot, grant: Option<CapSlot>) -> Result<(), &'static str> {
|
||||
let child = spawn(INIT, b"hello.elf", HELLO).map_err(|_| "init would not spawn hello")?;
|
||||
let child = spawn(INIT, b"bin/hello", HELLO).map_err(|_| "init would not spawn hello")?;
|
||||
let call = types::permission::PermissionSet::empty().with(types::permission::Permission::Execute);
|
||||
if grant.is_some_and(|g| child.give(g, call).is_none()) {
|
||||
return Err("a storage grant could not be passed on");
|
||||
@@ -98,11 +98,11 @@ fn check() -> Result<(), &'static str> {
|
||||
libkos::println!("spawntest: spawned program runs and its death is heard: ok");
|
||||
|
||||
let greedy = Limits { pages: 200, ..HELLO };
|
||||
if spawn(INIT, b"hello.elf", greedy).err() != Some(SpawnError::OverAllowance) {
|
||||
if spawn(INIT, b"bin/hello", greedy).err() != Some(SpawnError::OverAllowance) {
|
||||
return Err("init spawned past the allowance");
|
||||
}
|
||||
let tiny = Limits { pages: 1, ..HELLO };
|
||||
if spawn(INIT, b"nothere.elf", tiny).err() != Some(SpawnError::NotFound) {
|
||||
if spawn(INIT, b"bin/nothere", tiny).err() != Some(SpawnError::NotFound) {
|
||||
return Err("a missing program was not reported");
|
||||
}
|
||||
libkos::println!("spawntest: allowance and missing files refused: ok");
|
||||
|
||||
@@ -4,6 +4,17 @@ use port_input_device::Key;
|
||||
|
||||
pub const LEFT_SHIFT: u16 = 42;
|
||||
pub const RIGHT_SHIFT: u16 = 54;
|
||||
pub const LEFT_CTRL: u16 = 29;
|
||||
pub const RIGHT_CTRL: u16 = 97;
|
||||
|
||||
/// A letter typed with Ctrl held is its control character, as on a
|
||||
/// terminal: Ctrl+C is 3. Anything else is itself.
|
||||
pub fn with_ctrl(key: Key) -> Key {
|
||||
match key {
|
||||
Key::Char(c) if c.is_ascii_alphabetic() => Key::Char(c.to_ascii_lowercase() & 0x1f),
|
||||
k => k,
|
||||
}
|
||||
}
|
||||
|
||||
/// Rows of the main block: the first key's code, then each key unshifted
|
||||
/// and shifted.
|
||||
@@ -41,6 +52,14 @@ pub fn key(code: u16, shift: bool) -> Option<Key> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ctrl_letters_are_control_characters() {
|
||||
assert_eq!(with_ctrl(Key::Char(b'c')), Key::Char(3));
|
||||
assert_eq!(with_ctrl(Key::Char(b'C')), Key::Char(3));
|
||||
assert_eq!(with_ctrl(Key::Char(b'1')), Key::Char(b'1'));
|
||||
assert_eq!(with_ctrl(Key::Enter), Key::Enter);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn letters_digits_and_shift() {
|
||||
assert_eq!(key(35, false), Some(Key::Char(b'h')));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
}];
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -55,18 +55,21 @@ pub const CELL_HEIGHT: usize = 16;
|
||||
/// arrive; read them with `poll_input`.
|
||||
pub fn subscribe_input(ep: CapSlot) -> Option<CapSlot> {
|
||||
use types::badge::Badge;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
let notify = syscall::create_notification().ok()?;
|
||||
subscribe(ep, notify, Badge::NONE).then_some(notify)
|
||||
}
|
||||
|
||||
/// Takes input focus, to be told through `notify` with `badge`'s bits.
|
||||
pub fn subscribe(ep: CapSlot, notify: CapSlot, badge: types::badge::Badge) -> bool {
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
// Signal only (Delegate just to send it; the transfer strips it).
|
||||
let signal = PermissionSet::empty().with(Permission::Write).with(Permission::Delegate);
|
||||
let copy = syscall::mint(notify, Badge::NONE, signal).ok()?;
|
||||
let sent = syscall::call(
|
||||
ep,
|
||||
[types::display::input::SUBSCRIBE, 0, 0, 0],
|
||||
Some(copy.into()),
|
||||
);
|
||||
let Ok(copy) = syscall::mint(notify, badge, signal) else {
|
||||
return false;
|
||||
};
|
||||
let sent = syscall::call(ep, [types::display::input::SUBSCRIBE, 0, 0, 0], Some(copy.into()));
|
||||
syscall::drop_cap(copy);
|
||||
(sent.ok()?.words()[0] == 0).then_some(notify)
|
||||
sent.is_ok_and(|r| r.words()[0] == 0)
|
||||
}
|
||||
|
||||
/// The next input event, if this process has focus and one is waiting.
|
||||
|
||||
@@ -57,6 +57,12 @@ impl Spawned {
|
||||
syscall::delegate(cap, self.process, perms.without(Permission::Delegate))
|
||||
}
|
||||
|
||||
/// Like `give`, but an endpoint the program may send capabilities
|
||||
/// through (which takes Delegate, so it could pass the endpoint on too).
|
||||
pub fn give_sending(&self, endpoint: CapSlot, perms: PermissionSet) -> Option<CapSlot> {
|
||||
syscall::delegate(endpoint, self.process, perms.with(Permission::Delegate))
|
||||
}
|
||||
|
||||
/// Puts `args` (its name first) where the program finds them: a page
|
||||
/// this process fills and then moves into the program, which owns it.
|
||||
pub fn set_args(&self, args: &[&str]) -> bool {
|
||||
|
||||
@@ -38,6 +38,11 @@ pub fn storage() -> Option<CapSlot> {
|
||||
StartArg::decode(arg()).storage
|
||||
}
|
||||
|
||||
/// The display server endpoint this process was given, if any.
|
||||
pub fn display() -> Option<CapSlot> {
|
||||
StartArg::decode(arg()).display
|
||||
}
|
||||
|
||||
/// This program's arguments, its name first; none without an args page.
|
||||
pub fn args() -> impl Iterator<Item = &'static str> {
|
||||
let page: &'static [u8] = if StartArg::decode(arg()).args {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,8 +12,10 @@ mod tests;
|
||||
use libkos::display::{self, CELL_HEIGHT, CELL_WIDTH};
|
||||
use libkos::input::{InputEvent, Key};
|
||||
use libkos::syscall;
|
||||
use types::badge::Badge;
|
||||
use types::bounded::CapSlot;
|
||||
use types::display::{Color, ScreenCoord};
|
||||
use types::terminal::{KEYS_BIT, TIMER_BIT};
|
||||
|
||||
/// Where init put them (its manifest: own endpoint, then `uses`).
|
||||
const EP: CapSlot = CapSlot::const_new(0);
|
||||
@@ -26,11 +28,23 @@ const BG: Color = Color::rgb(16, 16, 24);
|
||||
const FLUSH_DELAY: u64 = 20 * 10_000;
|
||||
|
||||
struct Screen {
|
||||
keys: CapSlot,
|
||||
/// Bound to the serving thread; the flush timer signals it.
|
||||
/// Bound to the serving thread: the display signals it with
|
||||
/// `KEYS_BIT`, the flush timer with `TIMER_BIT`.
|
||||
notify: CapSlot,
|
||||
timer: CapSlot,
|
||||
}
|
||||
|
||||
/// A key as the line editor wants it, if it is one.
|
||||
fn byte(event: InputEvent) -> Option<u8> {
|
||||
match event {
|
||||
InputEvent::KeyDown(Key::Char(c)) => Some(c),
|
||||
InputEvent::KeyDown(Key::Space | Key::Tab) => Some(b' '),
|
||||
InputEvent::KeyDown(Key::Enter) => Some(b'\n'),
|
||||
InputEvent::KeyDown(Key::Backspace) => Some(server::BACKSPACE),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn at(col: usize, row: usize) -> (ScreenCoord, ScreenCoord) {
|
||||
(ScreenCoord::new((col * CELL_WIDTH) as u16), ScreenCoord::new((row * CELL_HEIGHT) as u16))
|
||||
}
|
||||
@@ -57,34 +71,50 @@ impl server::Console for Screen {
|
||||
|
||||
fn next_key(&mut self) -> u8 {
|
||||
loop {
|
||||
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<u8> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -17,10 +17,17 @@ pub trait Console {
|
||||
fn flush_later(&mut self);
|
||||
/// Blocks until a key is typed: a byte, `\n` for Enter, 8 for Backspace.
|
||||
fn next_key(&mut self) -> u8;
|
||||
/// A key already typed, without waiting.
|
||||
fn poll_key(&mut self) -> Option<u8>;
|
||||
/// Takes input focus back from whoever took it, and clears the screen.
|
||||
fn take_focus(&mut self);
|
||||
fn interrupt(&mut self, notification: CapSlot);
|
||||
}
|
||||
|
||||
pub const BACKSPACE: u8 = 8;
|
||||
const MAX_LINE: usize = 256;
|
||||
/// Keys typed while no line is being read, kept for the next one.
|
||||
const TYPEAHEAD: usize = 64;
|
||||
|
||||
pub struct TerminalServer<C: Console> {
|
||||
console: C,
|
||||
@@ -30,6 +37,10 @@ pub struct TerminalServer<C: Console> {
|
||||
line_len: usize,
|
||||
line_read: usize,
|
||||
flush_pending: bool,
|
||||
typeahead: [u8; TYPEAHEAD],
|
||||
typeahead_len: usize,
|
||||
/// Told about Ctrl+C typed while no line is being read.
|
||||
on_interrupt: Option<CapSlot>,
|
||||
}
|
||||
|
||||
/// An empty reply, whose tag is not text.
|
||||
@@ -50,6 +61,9 @@ impl<C: Console> TerminalServer<C> {
|
||||
line_len: 0,
|
||||
line_read: 0,
|
||||
flush_pending: false,
|
||||
typeahead: [0; TYPEAHEAD],
|
||||
typeahead_len: 0,
|
||||
on_interrupt: None,
|
||||
};
|
||||
s.redraw();
|
||||
s.console.flush();
|
||||
@@ -60,6 +74,11 @@ impl<C: Console> TerminalServer<C> {
|
||||
&self.console
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn console_mut_for_test(&mut self) -> &mut C {
|
||||
&mut self.console
|
||||
}
|
||||
|
||||
pub fn grid(&self) -> &Grid {
|
||||
&self.grid
|
||||
}
|
||||
@@ -88,13 +107,52 @@ impl<C: Console> TerminalServer<C> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Edits a line from the keyboard, echoing it, until Enter.
|
||||
/// A key typed ahead, or the next one.
|
||||
fn next_key(&mut self) -> u8 {
|
||||
if self.typeahead_len == 0 {
|
||||
return self.console.next_key();
|
||||
}
|
||||
let key = self.typeahead[0];
|
||||
self.typeahead.copy_within(1..self.typeahead_len, 0);
|
||||
self.typeahead_len -= 1;
|
||||
key
|
||||
}
|
||||
|
||||
/// Keys typed while nobody reads a line: Ctrl+C interrupts, the rest
|
||||
/// wait for the next line.
|
||||
fn keys_arrived(&mut self) {
|
||||
while let Some(key) = self.console.poll_key() {
|
||||
if key == types::terminal::CTRL_C {
|
||||
self.typeahead_len = 0;
|
||||
self.write(b"^C\n");
|
||||
if let Some(n) = self.on_interrupt {
|
||||
self.console.interrupt(n);
|
||||
}
|
||||
} else if self.typeahead_len < TYPEAHEAD {
|
||||
self.typeahead[self.typeahead_len] = key;
|
||||
self.typeahead_len += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Edits a line from the keyboard, echoing it, until Enter. Ctrl+C
|
||||
/// drops it and reads as an empty line.
|
||||
fn read_line(&mut self) {
|
||||
self.console.flush();
|
||||
// Flushed now; a flush timer still running is harmless.
|
||||
self.flush_pending = false;
|
||||
self.line_len = 0;
|
||||
self.line_read = 0;
|
||||
loop {
|
||||
match self.console.next_key() {
|
||||
match self.next_key() {
|
||||
types::terminal::CTRL_C => {
|
||||
self.line[0] = b'\n';
|
||||
self.line_len = 1;
|
||||
for &b in b"^C\n" {
|
||||
self.grid.put(b);
|
||||
}
|
||||
break;
|
||||
}
|
||||
b'\n' => {
|
||||
self.line[self.line_len] = b'\n';
|
||||
self.line_len += 1;
|
||||
@@ -148,9 +206,23 @@ impl<C: Console> Server for TerminalServer<C> {
|
||||
Done
|
||||
}
|
||||
|
||||
fn handle_call(&mut self, _badge: usize, words: [usize; 4], _cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
||||
fn handle_call(&mut self, _badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<libkos::syscall::Give>) {
|
||||
let reply = match words[0] {
|
||||
types::terminal::READ => self.read(),
|
||||
types::terminal::REDRAW => {
|
||||
self.console.take_focus();
|
||||
self.grid.touch_all();
|
||||
self.redraw();
|
||||
self.console.flush();
|
||||
[0; 4]
|
||||
}
|
||||
types::terminal::ON_INTERRUPT => match (cap, self.on_interrupt) {
|
||||
(Some(n), None) => {
|
||||
self.on_interrupt = Some(n);
|
||||
[0; 4]
|
||||
}
|
||||
_ => [MALFORMED, 0, 0, 0],
|
||||
},
|
||||
types::terminal::CLEAR => {
|
||||
self.handle(Request::Clear);
|
||||
[0; 4]
|
||||
@@ -166,10 +238,15 @@ impl<C: Console> Server for TerminalServer<C> {
|
||||
(reply, None)
|
||||
}
|
||||
|
||||
/// The flush timer: show a burst of output in one frame.
|
||||
fn signal(&mut self, _bits: usize) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,21 @@ extern crate std;
|
||||
|
||||
use crate::server::{BACKSPACE, Console, TerminalServer};
|
||||
use server_framework::Server;
|
||||
use std::vec;
|
||||
use std::vec::Vec;
|
||||
use types::bounded::CapSlot;
|
||||
use types::log::{text_of, text_words};
|
||||
use types::terminal::{CTRL_C, KEYS_BIT, TIMER_BIT};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Fake {
|
||||
keys: Vec<u8>,
|
||||
/// Typed while nobody reads a line.
|
||||
typed: Vec<u8>,
|
||||
flushes: usize,
|
||||
timers: usize,
|
||||
focus_taken: usize,
|
||||
interrupted: Vec<CapSlot>,
|
||||
}
|
||||
|
||||
impl Console for Fake {
|
||||
@@ -24,6 +31,15 @@ impl Console for Fake {
|
||||
fn next_key(&mut self) -> u8 {
|
||||
self.keys.remove(0)
|
||||
}
|
||||
fn poll_key(&mut self) -> Option<u8> {
|
||||
(!self.typed.is_empty()).then(|| self.typed.remove(0))
|
||||
}
|
||||
fn take_focus(&mut self) {
|
||||
self.focus_taken += 1;
|
||||
}
|
||||
fn interrupt(&mut self, notification: CapSlot) {
|
||||
self.interrupted.push(notification);
|
||||
}
|
||||
}
|
||||
|
||||
fn term(keys: &[u8]) -> TerminalServer<Fake> {
|
||||
@@ -45,7 +61,7 @@ fn writes_land_on_the_grid_and_flush_once_per_burst() {
|
||||
}
|
||||
assert_eq!(&t.grid().row(0)[..11], b"hello world");
|
||||
assert_eq!((t.console().timers, t.console().flushes), (1, before));
|
||||
t.signal(1);
|
||||
t.signal(TIMER_BIT);
|
||||
assert_eq!(t.console().flushes, before + 1);
|
||||
}
|
||||
|
||||
@@ -68,3 +84,62 @@ fn backspace_on_an_empty_line_does_nothing() {
|
||||
let mut t = term(&[BACKSPACE, b'x', b'\n']);
|
||||
assert_eq!(read(&mut t), b"x\n");
|
||||
}
|
||||
|
||||
fn line_of(t: &mut TerminalServer<Fake>) -> Vec<u8> {
|
||||
let mut line = read(t);
|
||||
while line.last() != Some(&b'\n') {
|
||||
line.extend(read(t));
|
||||
}
|
||||
line
|
||||
}
|
||||
|
||||
fn on_interrupt(t: &mut TerminalServer<Fake>, slot: u8) -> usize {
|
||||
t.handle_call(1, [types::terminal::ON_INTERRUPT, 0, 0, 0], CapSlot::new(slot)).0[0]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_c_while_a_program_runs_interrupts_it() {
|
||||
let mut t = term(&[]);
|
||||
assert_eq!(on_interrupt(&mut t, 7), 0);
|
||||
t.console_mut_for_test().typed = vec![b'x', CTRL_C];
|
||||
t.signal(KEYS_BIT);
|
||||
assert_eq!(t.console().interrupted, [CapSlot::new(7).unwrap()]);
|
||||
assert_eq!(&t.grid().row(0)[..2], b"^C");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_first_interrupt_notification_is_taken() {
|
||||
let mut t = term(&[]);
|
||||
assert_eq!(on_interrupt(&mut t, 7), 0);
|
||||
assert_ne!(on_interrupt(&mut t, 8), 0);
|
||||
t.console_mut_for_test().typed = vec![CTRL_C];
|
||||
t.signal(KEYS_BIT);
|
||||
assert_eq!(t.console().interrupted, [CapSlot::new(7).unwrap()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keys_typed_ahead_start_the_next_line() {
|
||||
let mut t = term(b"s\n");
|
||||
t.console_mut_for_test().typed = b"l".to_vec();
|
||||
t.signal(KEYS_BIT);
|
||||
assert!(t.console().interrupted.is_empty());
|
||||
assert_eq!(line_of(&mut t), b"ls\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_c_drops_the_line_being_typed() {
|
||||
let mut t = term(&[b'r', b'm', CTRL_C, b'l', b's', b'\n']);
|
||||
assert_eq!(on_interrupt(&mut t, 7), 0);
|
||||
assert_eq!(line_of(&mut t), b"\n");
|
||||
assert!(t.console().interrupted.is_empty(), "nothing runs to interrupt");
|
||||
assert_eq!(line_of(&mut t), b"ls\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redraw_takes_focus_back_and_paints_every_row() {
|
||||
let mut t = term(&[]);
|
||||
t.handle_call(1, text_words(b"hi"), None);
|
||||
t.handle_call(1, [types::terminal::REDRAW, 0, 0, 0], None);
|
||||
assert_eq!(t.console().focus_taken, 1);
|
||||
assert_eq!(&t.grid().row(0)[..2], b"hi");
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
64
crates/types/src/manifest.rs
Normal file
64
crates/types/src/manifest.rs
Normal 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")));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const CONSOLE_SHIFT: u32 = 0;
|
||||
const LOG_SHIFT: u32 = 8;
|
||||
const STORAGE_SHIFT: u32 = 16;
|
||||
const ARGS_BIT: usize = 1 << 24;
|
||||
const DISPLAY_SHIFT: u32 = 32;
|
||||
|
||||
/// Where the arguments page sits: the program's arguments, NUL separated,
|
||||
/// the first its name. Below the stack, which ends at the image.
|
||||
@@ -21,6 +22,8 @@ pub struct StartArg {
|
||||
pub log: Option<CapSlot>,
|
||||
pub storage: Option<CapSlot>,
|
||||
pub args: bool,
|
||||
/// The display server, for a program that draws on the screen.
|
||||
pub display: Option<CapSlot>,
|
||||
}
|
||||
|
||||
fn field(arg: usize, shift: u32) -> Option<CapSlot> {
|
||||
@@ -38,6 +41,7 @@ impl StartArg {
|
||||
| put(self.log, LOG_SHIFT)
|
||||
| put(self.storage, STORAGE_SHIFT)
|
||||
| if self.args { ARGS_BIT } else { 0 }
|
||||
| put(self.display, DISPLAY_SHIFT)
|
||||
}
|
||||
|
||||
pub fn decode(arg: usize) -> Self {
|
||||
@@ -46,6 +50,7 @@ impl StartArg {
|
||||
log: field(arg, LOG_SHIFT),
|
||||
storage: field(arg, STORAGE_SHIFT),
|
||||
args: arg & ARGS_BIT != 0,
|
||||
display: field(arg, DISPLAY_SHIFT),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +66,7 @@ mod tests {
|
||||
log: CapSlot::new(63),
|
||||
storage: CapSlot::new(5),
|
||||
args: true,
|
||||
display: CapSlot::new(7),
|
||||
};
|
||||
assert_eq!(StartArg::decode(a.encode()), a);
|
||||
assert_eq!(StartArg::decode(0), StartArg::default());
|
||||
|
||||
@@ -5,3 +5,15 @@
|
||||
/// the line's last chunk ends with `\n`. Blocks until a line is entered.
|
||||
pub const READ: usize = 10;
|
||||
pub const CLEAR: usize = 11;
|
||||
/// Repaints the whole screen and takes input focus back, after a program
|
||||
/// that drew on the screen itself.
|
||||
pub const REDRAW: usize = 12;
|
||||
/// Carries a notification the terminal signals on Ctrl+C while no line is
|
||||
/// being read. Accepted once.
|
||||
pub const ON_INTERRUPT: usize = 13;
|
||||
/// What Ctrl+C types.
|
||||
pub const CTRL_C: u8 = 3;
|
||||
|
||||
/// The terminal's notification bits: keys from the display, its flush timer.
|
||||
pub const KEYS_BIT: usize = 1 << 1;
|
||||
pub const TIMER_BIT: usize = 1 << 2;
|
||||
|
||||
2
disk/bin/snake.manifest
Normal file
2
disk/bin/snake.manifest
Normal file
@@ -0,0 +1,2 @@
|
||||
# What snake needs beyond the terminal and storage.
|
||||
needs = display
|
||||
@@ -1,8 +1,11 @@
|
||||
The shell runs a program by asking init to load it (init alone may
|
||||
The shell runs /bin/<name> by asking init to load it (init alone may
|
||||
create processes), then gives the program the terminal as its output
|
||||
and a read-only storage grant to the current directory, writes its
|
||||
arguments onto a page it moves into the program, starts it and waits
|
||||
for it to die. Then it takes the grant back.
|
||||
for it to die. Then it takes the grant back. Ctrl+C kills it.
|
||||
|
||||
Built-ins: help, echo, clear, cd, pwd. Anything else is a program on
|
||||
disk: `name args...` runs name.elf.
|
||||
A program that needs more says so in /bin/<name>.manifest, e.g.
|
||||
`needs = display` for snake, which then gets the screen and keyboard
|
||||
until it quits. A need the shell does not know, it refuses to run.
|
||||
|
||||
Built-ins: help, echo, clear, cd, pwd.
|
||||
|
||||
@@ -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
|
||||
|
||||
21
docs/ipc.md
21
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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user