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