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

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

View File

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