mycelium_kernel/
shell.rs

1//! A rudimentary kernel-mode command shell, primarily for debugging and testing
2//! purposes.
3//!
4use crate::rt;
5use core::str::FromStr;
6use mycelium_util::fmt::{self, Write};
7
8/// Defines a shell command, including its name, help text, and how the command
9/// is executed.
10#[derive(Debug)]
11pub struct Command<'cmd> {
12    name: &'cmd str,
13    help: &'cmd str,
14    run: Option<RunKind<'cmd>>,
15    usage: &'cmd str,
16    subcommands: Option<&'cmd [Command<'cmd>]>,
17}
18
19#[derive(Debug)]
20pub struct Error<'a> {
21    line: &'a str,
22    kind: ErrorKind<'a>,
23}
24
25pub type CmdResult<'a> = core::result::Result<(), Error<'a>>;
26
27pub trait Run: Send + Sync {
28    fn run<'ctx>(&'ctx self, ctx: Context<'ctx>) -> CmdResult<'ctx>;
29}
30
31#[derive(Debug)]
32enum ErrorKind<'a> {
33    UnknownCommand(&'a [Command<'a>]),
34
35    SubcommandRequired(&'a [Command<'a>]),
36    InvalidArguments {
37        help: &'a str,
38        arg: &'a str,
39        flag: Option<&'a str>,
40    },
41    FlagRequired {
42        flags: &'a [&'a str],
43    },
44    Other(&'static str),
45}
46
47enum RunKind<'a> {
48    Fn(fn(Context<'_>) -> CmdResult<'_>),
49    Runnable(&'a dyn Run),
50}
51
52pub fn eval(line: &str) {
53    static COMMANDS: &[Command] = &[
54        DUMP,
55        SLEEP,
56        PANIC,
57        FAULT,
58        VERSION,
59        crate::drivers::pci::LSPCI_CMD,
60    ];
61
62    let _span = tracing::info_span!(target: "shell", "$", message = %line).entered();
63    tracing::info!(target: "shell", "");
64
65    if line == "help" {
66        tracing::info!(target: "shell", "available commands:");
67        print_help("", COMMANDS);
68        tracing::info!(target: "shell", "");
69        return;
70    }
71
72    match handle_command(Context::new(line), COMMANDS) {
73        Ok(_) => {}
74        Err(error) => tracing::error!(target: "shell", "error: {error}"),
75    }
76
77    tracing::info!(target: "shell", "");
78}
79
80#[derive(Copy, Clone)]
81pub struct Context<'cmd> {
82    line: &'cmd str,
83    current: &'cmd str,
84}
85
86pub fn handle_command<'cmd>(ctx: Context<'cmd>, commands: &'cmd [Command]) -> CmdResult<'cmd> {
87    let chunk = ctx.current.trim();
88    for cmd in commands {
89        if let Some(current) = chunk.strip_prefix(cmd.name) {
90            let current = current.trim();
91            return cmd.run(Context { current, ..ctx });
92        }
93    }
94
95    Err(ctx.unknown_command(commands))
96}
97
98// === commands ===
99
100const DUMP: Command = Command::new("dump")
101    .with_help("print formatted representations of a kernel structure")
102    .with_subcommands(&[
103        Command::new("bootinfo")
104            .with_help("print the boot information structure")
105            .with_fn(|ctx| Err(ctx.other_error("not yet implemented"))),
106        Command::new("archinfo")
107            .with_help("print the architecture information structure")
108            .with_fn(|ctx| Err(ctx.other_error("not yet implemented"))),
109        // Command::new("timer")
110        //     .with_help("print the timer wheel")
111        //     .with_fn(|_| {
112        //         tracing::info!(target: "shell", timer = ?rt::TIMER);
113        //         Ok(())
114        //     }),
115        rt::DUMP_RT,
116        crate::arch::shell::DUMP_ARCH,
117        Command::new("heap")
118            .with_help("print kernel heap statistics")
119            .with_fn(|_| {
120                tracing::info!(target: "shell", heap = ?crate::ALLOC.state());
121                Ok(())
122            }),
123    ]);
124
125const SLEEP: Command = Command::new("sleep")
126    .with_help("spawns a task to sleep for SECONDS")
127    .with_usage("<SECONDS>")
128    .with_fn(|ctx| {
129        use maitake::time;
130
131        let line = ctx.command().trim();
132        if line.is_empty() {
133            return Err(ctx.invalid_argument("expected a number of seconds to sleep for"));
134        }
135
136        let secs: u64 = line
137            .parse()
138            .map_err(|_| ctx.invalid_argument("number of seconds must be an integer"))?;
139        let duration = time::Duration::from_secs(secs);
140
141        tracing::info!(target: "shell", ?duration, "spawning a sleep");
142        rt::spawn(async move {
143            time::sleep(duration).await;
144            tracing::info!(target: "shell", ?duration, "slept");
145        });
146
147        Ok(())
148    });
149
150const PANIC: Command = Command::new("panic")
151    .with_usage("<MESSAGE>")
152    .with_help("cause a kernel panic with the given message. use with caution.")
153    .with_fn(|line| {
154        panic!("{}", line.current);
155    });
156
157const FAULT: Command = Command::new("fault")
158    .with_help("cause a CPU fault (divide-by-zero). use with caution.")
159    .with_fn(|_| {
160        unsafe {
161            core::arch::asm!(
162                "div {0:e}",
163                in(reg) 0,
164            )
165        }
166        Ok(())
167    });
168
169const VERSION: Command = Command::new("version")
170    .with_help("print verbose build and version info.")
171    .with_fn(|_| {
172        tracing::info!("Mycelium v{}", env!("CARGO_PKG_VERSION"));
173        tracing::info!(build.version = %crate::MYCELIUM_VERSION);
174        tracing::info!(build.timestamp = %env!("VERGEN_BUILD_TIMESTAMP"));
175        tracing::info!(build.features = %env!("VERGEN_CARGO_FEATURES"));
176        tracing::info!(build.profile = %env!("VERGEN_CARGO_PROFILE"));
177        tracing::info!(build.target = %env!("VERGEN_CARGO_TARGET_TRIPLE"));
178        tracing::info!(commit.sha = %env!("VERGEN_GIT_SHA"));
179        tracing::info!(commit.branch = %env!("VERGEN_GIT_BRANCH"));
180        tracing::info!(commit.date = %env!("VERGEN_GIT_COMMIT_TIMESTAMP"));
181        tracing::info!(rustc.version = %env!("VERGEN_RUSTC_SEMVER"));
182        tracing::info!(rustc.channel = %env!("VERGEN_RUSTC_CHANNEL"));
183
184        Ok(())
185    });
186
187// === impl Command ===
188
189impl<'cmd> Command<'cmd> {
190    /// Constructs a new `Command` with the given `name`.
191    ///
192    /// By default, this command will have no help text, no subcommands, no
193    /// usage hints, and do nothing. Use the [`Command::with_help`] and
194    /// [`Command::with_usage`] to add help text to the command. Use
195    /// [`Command::with_subcommands`] to add subcommands, and/or
196    /// [`Command::with_fn`] or [`Command::with_runnable`] to add a function
197    /// that defines how to execute the command.
198    #[must_use]
199    pub const fn new(name: &'cmd str) -> Self {
200        Self {
201            name,
202            help: "",
203            usage: "",
204            run: None,
205            subcommands: None,
206        }
207    }
208
209    /// Add help text to the command.
210    ///
211    /// This should define what the command does, and is printed when running
212    /// `help` commands.
213    ///
214    /// # Examples
215    ///
216    /// ```rust
217    /// use mycelium_kernel::shell::Command;
218    ///
219    /// const DUMP: Command = Command::new("dump")
220    ///     .with_help("print formatted representations of a kernel structure");
221    ///
222    /// // The shell will format this command's help text as:
223    /// let help_text = "dump --- print formatted representations of a kernel structure";
224    /// assert_eq!(format!("{DUMP}"), help_text);
225    /// ```
226    #[must_use]
227    pub const fn with_help(self, help: &'cmd str) -> Self {
228        Self { help, ..self }
229    }
230
231    /// Add usage text to the command.
232    ///
233    /// This should define what, if any, arguments the command takes. If the
234    /// command does not take any arguments, it is not necessary to call this
235    /// method.
236    ///
237    /// # Examples
238    ///
239    /// ```rust
240    /// use mycelium_kernel::shell::Command;
241    ///
242    /// const ECHO: Command = Command::new("echo")
243    ///     .with_help("print the provided text")
244    ///     .with_usage("<TEXT>");
245    ///
246    /// // The shell will format this command's help text as:
247    /// let help_text = "echo <TEXT> --- print the provided text";
248    /// assert_eq!(format!("{ECHO}"), help_text);
249    /// ```
250    #[must_use]
251    pub const fn with_usage(self, usage: &'cmd str) -> Self {
252        Self { usage, ..self }
253    }
254
255    /// Add a list of subcommands to this command.
256    ///
257    /// If subcommands are added, they will be handled automatically by checking
258    /// if the next argument matches the name of a subcommand, before calling
259    /// the command's [function] or [runnable], if it has one.
260    ///
261    /// If the next argument matches the name of a subcommand, that subcommand
262    /// will be automatically executed. If it does *not* match a subcommand, and
263    /// the command has a function or runnable, that function or runnable will
264    /// be called with the remainder of the input. If the command does not have
265    /// a function or runnable, a "subcommand expected" error is returned.
266    ///
267    /// # Examples
268    ///
269    /// A command with only subcommands, and no root function/runnable:
270    ///
271    /// ```rust
272    /// use mycelium_kernel::shell::Command;
273    ///
274    /// // let's pretend we're implementing git (inside the mycelium kernel? for
275    /// // some reason????)...
276    /// const GIT: Command = Command::new("git")
277    ///     .with_subcommands(&[
278    ///         SUBCMD_ADD,
279    ///         SUBCMD_COMMIT,
280    ///         SUBCMD_PUSH,
281    ///         // more git commands ...
282    ///     ]);
283    ///
284    /// const SUBCMD_ADD: Command = Command::new("add")
285    ///     .with_help("add file contents to the index")
286    ///     .with_fn(|ctx| {
287    ///         // ...
288    ///         # drop(ctx); Ok(())
289    ///     });
290    /// const SUBCMD_COMMIT: Command = Command::new("commit")
291    ///     .with_help("record changes to the repository")
292    ///     .with_fn(|ctx| {
293    ///         // ...
294    ///         # drop(ctx); Ok(())
295    ///     });
296    /// const SUBCMD_PUSH: Command = Command::new("push")
297    ///     .with_help("update remote refs along with associated objects")
298    ///     .with_fn(|ctx| {
299    ///         // ...
300    ///         # drop(ctx); Ok(())
301    ///     });
302    /// // more git commands ...
303    /// # drop(GIT);
304    /// ```
305    ///
306    /// [function]: Command::with_fn
307    /// [runnable]: Command::with_runnable
308    #[must_use]
309    pub const fn with_subcommands(self, subcommands: &'cmd [Self]) -> Self {
310        Self {
311            subcommands: Some(subcommands),
312            ..self
313        }
314    }
315
316    /// Add a function that's run to execute this command.
317    ///
318    /// If [`Command::with_fn`] or [`Command::with_runnable`] was previously
319    /// called, this overwrites the previously set value.
320    #[must_use]
321    pub const fn with_fn(self, func: fn(Context<'_>) -> CmdResult<'_>) -> Self {
322        Self {
323            run: Some(RunKind::Fn(func)),
324            ..self
325        }
326    }
327
328    /// Add a [runnable item] that's run to execute this command.
329    ///
330    /// If [`Command::with_fn`] or [`Command::with_runnable`] was previously
331    /// called, this overwrites the previously set value.
332    ///
333    /// [runnable item]: Run
334    #[must_use]
335    pub const fn with_runnable(self, run: &'cmd dyn Run) -> Self {
336        Self {
337            run: Some(RunKind::Runnable(run)),
338            ..self
339        }
340    }
341
342    /// Run this command in the provided [`Context`].
343    pub fn run<'ctx>(&'cmd self, ctx: Context<'ctx>) -> CmdResult<'ctx>
344    where
345        'cmd: 'ctx,
346    {
347        let current = ctx.current.trim();
348
349        if current == "help" {
350            let name = ctx.line.strip_suffix(" help").unwrap_or("<???BUG???>");
351            if let Some(subcommands) = self.subcommands {
352                tracing::info!(target: "shell", "{name} <COMMAND>: {help}", help = self.help);
353                tracing::info!(target: "shell", "commands:");
354                print_help(name, subcommands);
355            } else {
356                tracing::info!(target: "shell", "{name}");
357            }
358            return Ok(());
359        }
360
361        if let Some(subcommands) = self.subcommands {
362            return match handle_command(ctx, subcommands) {
363                Err(e) if e.is_unknown_command() => {
364                    if let Some(ref run) = self.run {
365                        run.run(ctx)
366                    } else if current.is_empty() {
367                        Err(ctx.subcommand_required(subcommands))
368                    } else {
369                        Err(e)
370                    }
371                }
372                res => res,
373            };
374        }
375
376        self.run
377            .as_ref()
378            .ok_or_else(|| ctx.subcommand_required(self.subcommands.unwrap_or(&[])))
379            .and_then(|run| run.run(ctx))
380    }
381}
382
383impl fmt::Display for Command<'_> {
384    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385        let Self {
386            run: _func,
387            name,
388            help,
389            usage,
390            subcommands: _subcommands,
391        } = self;
392
393        let usage = if self.subcommands.is_some() && usage.is_empty() {
394            "<COMMAND>"
395        } else {
396            usage
397        };
398
399        write!(
400            f,
401            "{name}{usage_pad}{usage} --- {help}",
402            usage_pad = if !usage.is_empty() { " " } else { "" },
403        )
404    }
405}
406
407// === impl Error ===
408
409impl Error<'_> {
410    fn is_unknown_command(&self) -> bool {
411        matches!(self.kind, ErrorKind::UnknownCommand(_))
412    }
413}
414
415impl fmt::Display for Error<'_> {
416    fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result {
417        fn command_names<'cmd>(
418            cmds: &'cmd [Command<'cmd>],
419        ) -> impl Iterator<Item = &'cmd str> + 'cmd {
420            cmds.iter()
421                .map(|Command { name, .. }| *name)
422                .chain(core::iter::once("help"))
423        }
424
425        fn fmt_flag_names(f: &mut fmt::Formatter<'_>, flags: &[&str]) -> fmt::Result {
426            let mut names = flags.iter();
427            if let Some(name) = names.next() {
428                f.write_str(name)?;
429                for name in names {
430                    write!(f, "|{name}")?;
431                }
432            }
433            Ok(())
434        }
435
436        let Self { line, kind } = self;
437        match kind {
438            ErrorKind::UnknownCommand(commands) => {
439                write!(f, "unknown command {line:?}, expected one of: [")?;
440                fmt::comma_delimited(&mut f, command_names(commands))?;
441                f.write_char(']')?;
442            }
443            ErrorKind::InvalidArguments { arg, help, flag } => {
444                f.write_str("invalid argument")?;
445                if let Some(flag) = flag {
446                    write!(f, " {flag}")?;
447                }
448                write!(f, " {arg:?}: {help}")?;
449            }
450            ErrorKind::SubcommandRequired(subcommands) => {
451                writeln!(
452                    f,
453                    "the '{line}' command requires one of the following subcommands: ["
454                )?;
455                fmt::comma_delimited(&mut f, command_names(subcommands))?;
456                f.write_char(']')?;
457            }
458            ErrorKind::FlagRequired { flags } => {
459                write!(f, "the '{line}' command requires the ")?;
460                fmt_flag_names(f, flags)?;
461                write!(f, " flag")?;
462            }
463            ErrorKind::Other(msg) => write!(f, "could not execute {line:?}: {msg}")?,
464        }
465
466        Ok(())
467    }
468}
469
470// === impl Context ===
471
472impl<'cmd> Context<'cmd> {
473    pub const fn new(line: &'cmd str) -> Self {
474        Self {
475            line,
476            current: line,
477        }
478    }
479
480    pub fn command(&self) -> &'cmd str {
481        self.current.trim()
482    }
483
484    fn unknown_command(&self, commands: &'cmd [Command]) -> Error<'cmd> {
485        Error {
486            line: self.line,
487            kind: ErrorKind::UnknownCommand(commands),
488        }
489    }
490
491    fn subcommand_required(&self, subcommands: &'cmd [Command]) -> Error<'cmd> {
492        Error {
493            line: self.line,
494            kind: ErrorKind::SubcommandRequired(subcommands),
495        }
496    }
497
498    pub fn invalid_argument(&self, help: &'static str) -> Error<'cmd> {
499        Error {
500            line: self.line,
501            kind: ErrorKind::InvalidArguments {
502                arg: self.current,
503                flag: None,
504                help,
505            },
506        }
507    }
508
509    pub fn invalid_argument_named(&self, name: &'static str, help: &'static str) -> Error<'cmd> {
510        Error {
511            line: self.line,
512            kind: ErrorKind::InvalidArguments {
513                arg: self.current,
514                flag: Some(name),
515                help,
516            },
517        }
518    }
519
520    pub fn other_error(&self, msg: &'static str) -> Error<'cmd> {
521        Error {
522            line: self.line,
523            kind: ErrorKind::Other(msg),
524        }
525    }
526
527    pub fn parse_bool_flag(&mut self, flag: &str) -> bool {
528        if let Some(rest) = self.command().trim().strip_prefix(flag) {
529            self.current = rest.trim();
530            true
531        } else {
532            false
533        }
534    }
535
536    pub fn parse_optional_u32_hex_or_dec(
537        &mut self,
538        name: &'static str,
539    ) -> Result<Option<u32>, Error<'cmd>> {
540        let (chunk, rest) = match self.command().split_once(" ") {
541            Some((chunk, rest)) => (chunk.trim(), rest),
542            None => (self.command(), ""),
543        };
544
545        if chunk.is_empty() {
546            return Ok(None);
547        }
548
549        let val = if let Some(hex_num) = chunk.strip_prefix("0x") {
550            u32::from_str_radix(hex_num.trim(), 16).map_err(|_| Error {
551                line: self.line,
552                kind: ErrorKind::InvalidArguments {
553                    arg: chunk,
554                    flag: Some(name),
555                    help: "expected a 32-bit hex number",
556                },
557            })?
558        } else {
559            u32::from_str(chunk).map_err(|_| Error {
560                line: self.line,
561                kind: ErrorKind::InvalidArguments {
562                    arg: chunk,
563                    flag: Some(name),
564                    help: "expected a 32-bit decimal number",
565                },
566            })?
567        };
568
569        self.current = rest;
570        Ok(Some(val))
571    }
572
573    pub fn parse_u32_hex_or_dec(&mut self, name: &'static str) -> Result<u32, Error<'cmd>> {
574        self.parse_optional_u32_hex_or_dec(name).and_then(|val| {
575            val.ok_or_else(|| self.invalid_argument_named(name, "expected a number"))
576        })
577    }
578
579    pub fn parse_optional_flag<T>(
580        &mut self,
581        names: &'static [&'static str],
582    ) -> Result<Option<T>, Error<'cmd>>
583    where
584        T: FromStr,
585        T::Err: core::fmt::Display,
586    {
587        for name in names {
588            if let Some(rest) = self.command().strip_prefix(name) {
589                let (chunk, rest) = match rest.trim().split_once(" ") {
590                    Some((chunk, rest)) => (chunk.trim(), rest),
591                    None => (rest, ""),
592                };
593
594                if chunk.is_empty() {
595                    return Err(Error {
596                        line: self.line,
597                        kind: ErrorKind::InvalidArguments {
598                            arg: chunk,
599                            flag: Some(name),
600                            help: "expected a value",
601                        },
602                    });
603                }
604
605                match chunk.parse() {
606                    Ok(val) => {
607                        self.current = rest;
608                        return Ok(Some(val));
609                    }
610                    Err(e) => {
611                        tracing::warn!(target: "shell", "invalid value {chunk:?} for flag {name}: {e}");
612                        return Err(Error {
613                            line: self.line,
614                            kind: ErrorKind::InvalidArguments {
615                                arg: chunk,
616                                flag: Some(name),
617                                help: "invalid value",
618                            },
619                        });
620                    }
621                }
622            }
623        }
624
625        Ok(None)
626    }
627
628    pub fn parse_required_flag<T>(
629        &mut self,
630        names: &'static [&'static str],
631    ) -> Result<T, Error<'cmd>>
632    where
633        T: FromStr,
634        T::Err: core::fmt::Display,
635    {
636        self.parse_optional_flag(names).and_then(|val| {
637            val.ok_or(Error {
638                line: self.line,
639                kind: ErrorKind::FlagRequired { flags: names },
640            })
641        })
642    }
643}
644
645// === impl RunKind ===
646
647impl RunKind<'_> {
648    #[inline]
649    fn run<'ctx>(&'ctx self, ctx: Context<'ctx>) -> CmdResult<'ctx> {
650        match self {
651            Self::Fn(func) => func(ctx),
652            Self::Runnable(runnable) => runnable.run(ctx),
653        }
654    }
655}
656
657impl fmt::Debug for RunKind<'_> {
658    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659        match self {
660            Self::Fn(func) => f.debug_tuple("Run::Func").field(&fmt::ptr(func)).finish(),
661            Self::Runnable(runnable) => f
662                .debug_tuple("Run::Runnable")
663                .field(&fmt::ptr(runnable))
664                .finish(),
665        }
666    }
667}
668
669// === impl Run ===
670
671impl<F> Run for F
672where
673    F: Fn(Context<'_>) -> CmdResult<'_> + Send + Sync,
674{
675    fn run<'ctx>(&'ctx self, ctx: Context<'ctx>) -> CmdResult<'ctx> {
676        self(ctx)
677    }
678}
679
680fn print_help(parent_cmd: &str, commands: &[Command]) {
681    let parent_cmd_pad = if parent_cmd.is_empty() { "" } else { " " };
682    for command in commands {
683        tracing::info!(target: "shell", "  {parent_cmd}{parent_cmd_pad}{command}");
684    }
685    tracing::info!(target: "shell", "  {parent_cmd}{parent_cmd_pad}help --- prints this help message");
686}
687
688#[derive(Copy, Clone, Debug, Eq, PartialEq)]
689pub enum NumberFormat {
690    Binary,
691    Hex,
692    Decimal,
693}
694
695impl FromStr for NumberFormat {
696    type Err = &'static str;
697    fn from_str(s: &str) -> Result<Self, Self::Err> {
698        match s.trim() {
699            "b" | "bin" | "binary" => Ok(Self::Binary),
700            "h" | "hex" => Ok(Self::Hex),
701            "d" | "dec" | "decimal" => Ok(Self::Decimal),
702            _ => Err("expected one of: [b, bin, binary, h, hex, d, decimal]"),
703        }
704    }
705}