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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user