Next Generation WASM Microkernel Operating System
1// Copyright 2025 Jonas Kruckenberg
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! Basic kernel shell for debugging purposes, taken from
9//! <https://github.com/hawkw/mycelium/blob/main/src/shell.rs> (MIT)
10
11const S: &str = r#"
12 __ ___ ____
13 / /__ |_ ||_ /
14 / '_// __/_/_ <
15/_/\_\/____/____/
16"#;
17
18use alloc::string::{String, ToString};
19use core::fmt;
20use core::fmt::Write;
21use core::ops::DerefMut;
22use core::range::Range;
23use core::str::FromStr;
24
25use fallible_iterator::FallibleIterator;
26use kasync::executor::Executor;
27use spin::{Barrier, OnceLock};
28
29use crate::device_tree::DeviceTree;
30use crate::mem::{Mmap, PhysicalAddress, with_kernel_aspace};
31use crate::state::global;
32use crate::{arch, irq};
33
34static COMMANDS: &[Command] = &[PANIC, FAULT, VERSION, SHUTDOWN];
35
36pub fn init(devtree: &'static DeviceTree, sched: &'static Executor, num_cpus: usize) {
37 // The `Barrier` below is here so that the maybe verbose startup logging is
38 // out of the way before dropping the user into the kernel shell. If we don't
39 // wait for the last CPU to have finished initializing it will mess up the shell output.
40 static SYNC: OnceLock<Barrier> = OnceLock::new();
41 let barrier = SYNC.get_or_init(|| Barrier::new(num_cpus));
42
43 if barrier.wait().is_leader() {
44 tracing::info!("{S}");
45 tracing::info!("type `help` to list available commands");
46
47 sched
48 .try_spawn(async move {
49 let (mut uart, _mmap, irq_num) = init_uart(devtree);
50
51 let mut line = String::new();
52 loop {
53 let res = irq::next_event(irq_num).await;
54 assert!(res.is_ok());
55 let mut newline = false;
56
57 let ch = uart.recv() as char;
58 uart.write_char(ch).unwrap();
59 match ch {
60 '\n' | '\r' => {
61 newline = true;
62 uart.write_str("\n\r").unwrap();
63 }
64 '\u{007F}' => {
65 line.pop();
66 }
67 ch => line.push(ch),
68 }
69
70 if newline {
71 eval(&line);
72 line.clear();
73 }
74 }
75 })
76 .unwrap();
77 }
78}
79
80fn init_uart(devtree: &DeviceTree) -> (uart_16550::SerialPort, Mmap, u32) {
81 let s = devtree.find_by_path("/soc/serial").unwrap();
82 assert!(s.is_compatible(["ns16550a"]));
83
84 let clock_freq = s.property("clock-frequency").unwrap().as_u32().unwrap();
85 let mut regs = s.regs().unwrap();
86 let reg = regs.next().unwrap().unwrap();
87 assert!(regs.next().unwrap().is_none());
88 let irq_num = s.property("interrupts").unwrap().as_u32().unwrap();
89
90 let mmap = with_kernel_aspace(|aspace| {
91 // FIXME: this is gross, we're using the PhysicalAddress as an alignment utility :/
92 let size = PhysicalAddress::new(reg.size.unwrap())
93 .checked_align_up(arch::PAGE_SIZE)
94 .unwrap()
95 .get();
96
97 let range_phys = {
98 let start = PhysicalAddress::new(reg.starting_address);
99 Range::from(start..start.checked_add(size).unwrap())
100 };
101
102 let mmap = Mmap::new_phys(
103 aspace.clone(),
104 range_phys,
105 size,
106 arch::PAGE_SIZE,
107 Some("UART-16550".to_string()),
108 )
109 .unwrap();
110
111 mmap.commit(aspace.lock().deref_mut(), Range::from(0..size), true)
112 .unwrap();
113
114 mmap
115 });
116
117 // Safety: info comes from device tree
118 let uart = unsafe { uart_16550::SerialPort::new(mmap.range().start.get(), clock_freq, 115200) };
119
120 (uart, mmap, irq_num)
121}
122
123pub fn eval(line: &str) {
124 if line == "help" {
125 tracing::info!(target: "shell", "available commands:");
126 print_help("", COMMANDS);
127 tracing::info!(target: "shell", "");
128 return;
129 }
130
131 match handle_command(Context::new(line), COMMANDS) {
132 Ok(_) => {}
133 Err(error) => tracing::error!(target: "shell", "error: {error}"),
134 }
135}
136
137const PANIC: Command = Command::new("panic")
138 .with_usage("<MESSAGE>")
139 .with_help("cause a kernel panic with the given message. use with caution.")
140 .with_fn(|line| {
141 panic!("{}", line.current);
142 });
143
144const FAULT: Command = Command::new("fault")
145 .with_help("cause a CPU fault (null pointer dereference). use with caution.")
146 .with_fn(|_| {
147 // Safety: This actually *is* unsafe and *is* causing problematic behaviour, but that is exactly what
148 // we want here!
149 unsafe {
150 #[expect(clippy::zero_ptr, reason = "we actually want to cause a fault here")]
151 (0x0 as *const u8).read_volatile();
152 }
153 Ok(())
154 });
155
156const VERSION: Command = Command::new("version")
157 .with_help("print verbose build and version info.")
158 .with_fn(|_| {
159 tracing::info!("k23 v{}", env!("CARGO_PKG_VERSION"));
160 tracing::info!(build.version = %concat!(
161 env!("CARGO_PKG_VERSION"),
162 "-",
163 env!("VERGEN_GIT_BRANCH"),
164 ".",
165 env!("VERGEN_GIT_SHA")
166 ));
167 tracing::info!(build.timestamp = %env!("VERGEN_BUILD_TIMESTAMP"));
168 tracing::info!(build.opt_level = %env!("VERGEN_CARGO_OPT_LEVEL"));
169 tracing::info!(build.target = %env!("VERGEN_CARGO_TARGET_TRIPLE"));
170 tracing::info!(commit.sha = %env!("VERGEN_GIT_SHA"));
171 tracing::info!(commit.branch = %env!("VERGEN_GIT_BRANCH"));
172 tracing::info!(commit.date = %env!("VERGEN_GIT_COMMIT_TIMESTAMP"));
173 tracing::info!(rustc.version = %env!("VERGEN_RUSTC_SEMVER"));
174 tracing::info!(rustc.channel = %env!("VERGEN_RUSTC_CHANNEL"));
175
176 Ok(())
177 });
178
179const SHUTDOWN: Command = Command::new("shutdown")
180 .with_help("exit the kernel and shutdown the machine.")
181 .with_fn(|_| {
182 tracing::info!("Bye, Bye!");
183
184 global().executor.close();
185
186 Ok(())
187 });
188
189#[derive(Debug)]
190pub struct Command<'cmd> {
191 name: &'cmd str,
192 help: &'cmd str,
193 usage: &'cmd str,
194 run: fn(Context<'_>) -> CmdResult<'_>,
195}
196
197pub type CmdResult<'a> = Result<(), Error<'a>>;
198
199#[derive(Debug)]
200pub struct Error<'a> {
201 line: &'a str,
202 kind: ErrorKind<'a>,
203}
204
205#[derive(Debug)]
206enum ErrorKind<'a> {
207 UnknownCommand(&'a [Command<'a>]),
208 InvalidArguments {
209 help: &'a str,
210 arg: &'a str,
211 flag: Option<&'a str>,
212 },
213 FlagRequired {
214 flags: &'a [&'a str],
215 },
216 Other(&'static str),
217}
218
219#[derive(Copy, Clone)]
220pub struct Context<'cmd> {
221 line: &'cmd str,
222 current: &'cmd str,
223}
224
225fn print_help(parent_cmd: &str, commands: &[Command]) {
226 let parent_cmd_pad = if parent_cmd.is_empty() { "" } else { " " };
227 for command in commands {
228 tracing::info!(target: "shell", " {parent_cmd}{parent_cmd_pad}{command}");
229 }
230 tracing::info!(target: "shell", " {parent_cmd}{parent_cmd_pad}help --- prints this help message");
231}
232
233fn handle_command<'cmd>(ctx: Context<'cmd>, commands: &'cmd [Command]) -> CmdResult<'cmd> {
234 let chunk = ctx.current.trim();
235 for cmd in commands {
236 if let Some(current) = chunk.strip_prefix(cmd.name) {
237 let current = current.trim();
238
239 return panic_unwind2::catch_unwind(|| cmd.run(Context { current, ..ctx })).unwrap_or(
240 {
241 Err(Error {
242 line: cmd.name,
243 kind: ErrorKind::Other("command failed"),
244 })
245 },
246 );
247 }
248 }
249
250 Err(ctx.unknown_command(commands))
251}
252
253// === impl Command ===
254
255impl<'cmd> Command<'cmd> {
256 #[must_use]
257 pub const fn new(name: &'cmd str) -> Self {
258 #[cold]
259 fn invalid_command(_ctx: Context<'_>) -> CmdResult<'_> {
260 panic!("command is missing run function, this is a bug");
261 }
262
263 Self {
264 name,
265 help: "",
266 usage: "",
267 run: invalid_command,
268 }
269 }
270
271 #[must_use]
272 pub const fn with_help(self, help: &'cmd str) -> Self {
273 Self { help, ..self }
274 }
275
276 #[must_use]
277 pub const fn with_usage(self, usage: &'cmd str) -> Self {
278 Self { usage, ..self }
279 }
280
281 #[must_use]
282 pub const fn with_fn(self, run: fn(Context<'_>) -> CmdResult<'_>) -> Self {
283 Self { run, ..self }
284 }
285
286 pub fn run<'ctx>(&'cmd self, ctx: Context<'ctx>) -> CmdResult<'ctx>
287 where
288 'cmd: 'ctx,
289 {
290 let current = ctx.current.trim();
291
292 if current == "help" {
293 let name = ctx.line.strip_suffix(" help").unwrap_or("<???BUG???>");
294 tracing::info!(target: "shell", "{name}");
295
296 return Ok(());
297 }
298
299 (self.run)(ctx)
300 }
301}
302
303impl fmt::Display for Command<'_> {
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 let Self {
306 run: _func,
307 name,
308 help,
309 usage,
310 } = self;
311
312 write!(
313 f,
314 "{name}{usage_pad}{usage} --- {help}",
315 usage_pad = if !usage.is_empty() { " " } else { "" },
316 )
317 }
318}
319
320// === impl Error ===
321
322impl fmt::Display for Error<'_> {
323 fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result {
324 fn command_names<'cmd>(
325 cmds: &'cmd [Command<'cmd>],
326 ) -> impl Iterator<Item = &'cmd str> + 'cmd {
327 cmds.iter()
328 .map(|Command { name, .. }| *name)
329 .chain(core::iter::once("help"))
330 }
331
332 fn fmt_flag_names(f: &mut fmt::Formatter<'_>, flags: &[&str]) -> fmt::Result {
333 let mut names = flags.iter();
334 if let Some(name) = names.next() {
335 f.write_str(name)?;
336 for name in names {
337 write!(f, "|{name}")?;
338 }
339 }
340 Ok(())
341 }
342
343 let Self { line, kind } = self;
344 match kind {
345 ErrorKind::UnknownCommand(commands) => {
346 write!(f, "unknown command {line:?}, expected one of: [")?;
347 comma_delimited(&mut f, command_names(commands))?;
348 f.write_char(']')?;
349 }
350 ErrorKind::InvalidArguments { help, arg, flag } => {
351 f.write_str("invalid argument")?;
352 if let Some(flag) = flag {
353 write!(f, " {flag}")?;
354 }
355 write!(f, " {arg:?}: {help}")?;
356 }
357 ErrorKind::FlagRequired { flags } => {
358 write!(f, "the '{line}' command requires the ")?;
359 fmt_flag_names(f, flags)?;
360 write!(f, " flag")?;
361 }
362 ErrorKind::Other(msg) => write!(f, "could not execute {line:?}: {msg}")?,
363 }
364
365 Ok(())
366 }
367}
368
369impl core::error::Error for Error<'_> {}
370
371fn comma_delimited<F: fmt::Display>(
372 mut writer: impl Write,
373 values: impl IntoIterator<Item = F>,
374) -> fmt::Result {
375 let mut values = values.into_iter();
376 if let Some(value) = values.next() {
377 write!(writer, "{value}")?;
378 for value in values {
379 write!(writer, ", {value}")?;
380 }
381 }
382
383 Ok(())
384}
385
386// === impl Context ===
387
388impl<'cmd> Context<'cmd> {
389 pub const fn new(line: &'cmd str) -> Self {
390 Self {
391 line,
392 current: line,
393 }
394 }
395
396 pub fn command(&self) -> &'cmd str {
397 self.current.trim()
398 }
399
400 fn unknown_command(&self, commands: &'cmd [Command]) -> Error<'cmd> {
401 Error {
402 line: self.line,
403 kind: ErrorKind::UnknownCommand(commands),
404 }
405 }
406
407 pub fn invalid_argument(&self, help: &'static str) -> Error<'cmd> {
408 Error {
409 line: self.line,
410 kind: ErrorKind::InvalidArguments {
411 arg: self.current,
412 flag: None,
413 help,
414 },
415 }
416 }
417
418 pub fn invalid_argument_named(&self, name: &'static str, help: &'static str) -> Error<'cmd> {
419 Error {
420 line: self.line,
421 kind: ErrorKind::InvalidArguments {
422 arg: self.current,
423 flag: Some(name),
424 help,
425 },
426 }
427 }
428
429 pub fn other_error(&self, msg: &'static str) -> Error<'cmd> {
430 Error {
431 line: self.line,
432 kind: ErrorKind::Other(msg),
433 }
434 }
435
436 pub fn parse_bool_flag(&mut self, flag: &str) -> bool {
437 if let Some(rest) = self.command().trim().strip_prefix(flag) {
438 self.current = rest.trim();
439 true
440 } else {
441 false
442 }
443 }
444
445 pub fn parse_optional_u32_hex_or_dec(
446 &mut self,
447 name: &'static str,
448 ) -> Result<Option<u32>, Error<'cmd>> {
449 let (chunk, rest) = match self.command().split_once(" ") {
450 Some((chunk, rest)) => (chunk.trim(), rest),
451 None => (self.command(), ""),
452 };
453
454 if chunk.is_empty() {
455 return Ok(None);
456 }
457
458 let val = if let Some(hex_num) = chunk.strip_prefix("0x") {
459 u32::from_str_radix(hex_num.trim(), 16).map_err(|_| Error {
460 line: self.line,
461 kind: ErrorKind::InvalidArguments {
462 arg: chunk,
463 flag: Some(name),
464 help: "expected a 32-bit hex number",
465 },
466 })?
467 } else {
468 u32::from_str(chunk).map_err(|_| Error {
469 line: self.line,
470 kind: ErrorKind::InvalidArguments {
471 arg: chunk,
472 flag: Some(name),
473 help: "expected a 32-bit decimal number",
474 },
475 })?
476 };
477
478 self.current = rest;
479 Ok(Some(val))
480 }
481
482 pub fn parse_u32_hex_or_dec(&mut self, name: &'static str) -> Result<u32, Error<'cmd>> {
483 self.parse_optional_u32_hex_or_dec(name).and_then(|val| {
484 val.ok_or_else(|| self.invalid_argument_named(name, "expected a number"))
485 })
486 }
487
488 pub fn parse_optional_flag<T>(
489 &mut self,
490 names: &'static [&'static str],
491 ) -> Result<Option<T>, Error<'cmd>>
492 where
493 T: FromStr,
494 T::Err: fmt::Display,
495 {
496 for name in names {
497 if let Some(rest) = self.command().strip_prefix(name) {
498 let (chunk, rest) = match rest.trim().split_once(" ") {
499 Some((chunk, rest)) => (chunk.trim(), rest),
500 None => (rest, ""),
501 };
502
503 if chunk.is_empty() {
504 return Err(Error {
505 line: self.line,
506 kind: ErrorKind::InvalidArguments {
507 arg: chunk,
508 flag: Some(name),
509 help: "expected a value",
510 },
511 });
512 }
513
514 match chunk.parse() {
515 Ok(val) => {
516 self.current = rest;
517 return Ok(Some(val));
518 }
519 Err(e) => {
520 tracing::warn!(target: "shell", "invalid value {chunk:?} for flag {name}: {e}");
521 return Err(Error {
522 line: self.line,
523 kind: ErrorKind::InvalidArguments {
524 arg: chunk,
525 flag: Some(name),
526 help: "invalid value",
527 },
528 });
529 }
530 }
531 }
532 }
533
534 Ok(None)
535 }
536
537 pub fn parse_required_flag<T>(
538 &mut self,
539 names: &'static [&'static str],
540 ) -> Result<T, Error<'cmd>>
541 where
542 T: FromStr,
543 T::Err: fmt::Display,
544 {
545 self.parse_optional_flag(names).and_then(|val| {
546 val.ok_or(Error {
547 line: self.line,
548 kind: ErrorKind::FlagRequired { flags: names },
549 })
550 })
551 }
552}