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