feat(userspace): shell runs programs (ls, cat) via spawn w/ terminal+storage+args page; ext4 holes read as zeros; storage re-attach same buffer

This commit is contained in:
2026-10-27 17:22:08 +01:00
parent 0fd568cdad
commit c603a578c0
22 changed files with 396 additions and 66 deletions

View File

@@ -0,0 +1,13 @@
[package]
name = "cat"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "cat"
test = false
bench = false
[dependencies]
types = { workspace = true }
libkos = { workspace = true }

View File

@@ -0,0 +1,45 @@
//! Prints files: `cat <file>...`.
#![no_std]
#![no_main]
extern crate libkos;
use libkos::fs::{FsError, Storage};
use libkos::println;
/// Where storage's buffer is mapped.
const STORAGE_BUF_VA: usize = 0x0180_0000;
fn cat(fs: &mut Storage, path: &str) -> Result<(), FsError> {
let h = fs.open(path.as_bytes())?;
let mut buf = [0u8; 4096];
loop {
let n = fs.read(h, &mut buf)?;
libkos::io::write_bytes(&buf[..n]);
if n < buf.len() {
break;
}
}
fs.close(h)
}
#[unsafe(no_mangle)]
extern "C" fn main() {
let Some(mut fs) = libkos::start::storage().and_then(|ep| Storage::attach(ep, STORAGE_BUF_VA).ok()) else {
println!("cat: no storage");
return;
};
let mut any = false;
for path in libkos::start::args().skip(1) {
any = true;
match cat(&mut fs, path) {
Ok(()) => {}
Err(FsError::NotFound) => println!("cat: {}: no such file", path),
Err(_) => println!("cat: {}: cannot read", path),
}
}
if !any {
println!("usage: cat <file>...");
}
}

13
crates/apps/ls/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "ls"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "ls"
test = false
bench = false
[dependencies]
types = { workspace = true }
libkos = { workspace = true }

View File

@@ -0,0 +1,35 @@
//! Lists a directory: `ls [dir]`, the root by default.
#![no_std]
#![no_main]
extern crate libkos;
use libkos::fs::Storage;
use libkos::println;
/// Where storage's buffer is mapped.
const STORAGE_BUF_VA: usize = 0x0180_0000;
#[unsafe(no_mangle)]
extern "C" fn main() {
let Some(mut fs) = libkos::start::storage().and_then(|ep| Storage::attach(ep, STORAGE_BUF_VA).ok()) else {
println!("ls: no storage");
return;
};
let dir = libkos::start::args().nth(1).unwrap_or("/");
let listed = fs.list_dir(dir.as_bytes(), |e| {
let name = core::str::from_utf8(e.name()).unwrap_or("?");
if name == "." || name == ".." {
return;
}
if e.is_dir {
println!("{}/", name);
} else {
println!("{:<24} {:>8}", name, e.size);
}
});
if listed.is_err() {
println!("ls: {}: no such directory", dir);
}
}

View File

@@ -1,33 +1,95 @@
//! The shell, for now built-ins only; running programs comes next.
//! 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 the storage server, and nothing else; then waits for it.
#![no_std]
#![no_main]
extern crate libkos;
use libkos::{print, println};
use libkos::spawn::{SpawnError, spawn};
use libkos::{print, println, syscall};
use types::bounded::CapSlot;
use types::budget::Limits;
use types::permission::{Permission, PermissionSet};
use types::start_arg::StartArg;
/// Init's manifest gives it the terminal first.
/// 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);
fn run(line: &str) {
let mut words = line.split_ascii_whitespace();
match words.next() {
/// What each program gets, out of the shell's allowance.
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
const MAX_ARGS: usize = 16;
/// Runs `args[0].elf` with `args` and waits for it to finish.
fn run_program(args: &[&str], done: CapSlot) {
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 {
println!("shell: {}: name too long", args[0]);
return;
};
let child = match spawn(INIT, file, PROGRAM) {
Ok(c) => c,
Err(SpawnError::NotFound) => return println!("shell: {}: not found", args[0]),
Err(SpawnError::OverAllowance) => return println!("shell: no room to run {}", args[0]),
Err(SpawnError::Failed) => return println!("shell: {}: cannot run", args[0]),
};
let call = PermissionSet::empty().with(Permission::Execute);
let arg = StartArg {
log: child.give(TERMINAL, call),
storage: child.give(STORAGE, call),
args: child.set_args(args),
console: None,
};
let watched = syscall::watch_death(child.process, done).is_ok();
if child.start_with(arg) && watched {
let _ = syscall::wait(done);
} else {
let _ = syscall::kill(child.process);
println!("shell: {}: cannot start", args[0]);
}
syscall::drop_cap(child.process);
}
fn run(line: &str, done: CapSlot) {
let mut args = [""; MAX_ARGS];
let mut n = 0;
for w in line.split_ascii_whitespace().take(MAX_ARGS) {
args[n] = w;
n += 1;
}
let args = &args[..n];
match args.first().copied() {
None => {}
Some("help") => {
println!("built-ins: help, echo <text>, clear");
println!("anything else runs a program from the disk: try ls, cat readme.txt");
}
Some("echo") => {
let mut first = true;
for w in words {
print!("{}{}", if first { "" } else { " " }, w);
first = false;
}
println!();
}
Some("echo") => println!("{}", Joined(&args[1..])),
Some("clear") => libkos::terminal::clear(TERMINAL),
Some(other) => println!("shell: {}: not found", other),
Some(_) => run_program(args, done),
}
}
/// Words with single spaces between them.
struct Joined<'a>(&'a [&'a str]);
impl core::fmt::Display for Joined<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
for (i, w) in self.0.iter().enumerate() {
if i > 0 {
f.write_str(" ")?;
}
f.write_str(w)?;
}
Ok(())
}
}
@@ -35,13 +97,17 @@ fn run(line: &str) {
extern "C" fn main() {
// Everything this shell prints goes to the terminal.
libkos::start::set_log(Some(TERMINAL));
let Ok(done) = syscall::create_notification() else {
println!("shell: no notification to wait on");
return;
};
println!("k-os shell. Type 'help'.");
let mut buf = [0u8; 256];
loop {
print!("$ ");
let n = libkos::terminal::read_line(TERMINAL, &mut buf);
match core::str::from_utf8(&buf[..n]) {
Ok(line) => run(line.trim_end_matches('\n')),
Ok(line) => run(line.trim_end_matches('\n'), done),
Err(_) => println!("shell: not text"),
}
}