Files
Operator-system/docs/commands-and-terminal.md
2026-02-27 21:04:56 +00:00

17 KiB
Raw Blame History

Starling Terminal and command pipeline

User interaction with the kernel is mediated by the Starling Terminal and a command registry implemented in kernel.c and commands.c.

  • The Starling Terminal is a task that runs a readevalprint loop, reading keystrokes via BootInfo services and dispatching commands.
  • The command subsystem maintains a table of commands, each with a name, description, usage string, minimum privilege level, and handler function.
  • Each command typically runs in its own task so that long-running work does not block the terminal.

Starling Terminal task

The Starling Terminal is implemented as a task entry function in kernel.c:

static void starling_terminal_task(void *arg)
{
    StarlingContext *ctx = (StarlingContext *)arg;
    BootInfo *Boot = NULL;
    KeyEvent Key;
    KSTATUS Status;
    UINTN read_errors = 0;
    CHAR16 line[128];
    UINTN len = 0;
    UINTN depth = 0;
    TaskPrivilege shell_priv;

    if (ctx == NULL || ctx->Boot == NULL) {
        return;
    }

    Boot       = ctx->Boot;
    depth      = ctx->depth;
    shell_priv = ctx->shell_priv;

    SAFE_PRINT(Boot, L"\n\r[Starling Terminal depth %d, priv %d] ready.\n\r\n\r",
               depth, (INT32)shell_priv);
    SAFE_PRINT(Boot, L"starling> ");

    while (TRUE) {
        /* Try non-blocking read first; yield to other tasks while idle */
        if (Boot->try_read_key != NULL) {
            Status = Boot->try_read_key(&Key);
            if (Status != 0) {
                task_yield();
                continue;
            }
        } else if (Boot->read_key != NULL) {
            Status = Boot->read_key(&Key);
        } else {
            SAFE_PRINT(Boot, L"Console input unavailable.\n\r");
            break;
        }

        if (Status != 0) {
            read_errors++;
            if (read_errors == 1 || (read_errors % 64) == 0) {
                SAFE_PRINT(Boot, L"read_key failed (status=%ld)\n\r",
                           (UINT64)Status);
            }
            continue;
        }
        read_errors = 0;

        if (Key.unicode_char == L'\r' || Key.unicode_char == L'\n') {
            /* Enter pressed: execute the buffered command */
            line[len] = L'\0';
            SAFE_PRINT(Boot, L"\n\r");
            trim_spaces_inplace(line);
            ...
            } else {
            Task *cmd_task = execute_command(Boot, line, shell_priv);

                /* If a command task was spawned, wait for it to finish. */
                if (cmd_task != NULL) {
                    task_wait(cmd_task);
                }

                /* Reset for next command */
                len = 0;
                SAFE_PRINT(Boot, L"starling> ");
            }
        } else if (Key.scan_code == 0x08 || Key.unicode_char == L'\b' || Key.unicode_char == 0x7F) {
            /* Backspace */
            if (len > 0) {
                len--;
                SAFE_PRINT(Boot, L"\b \b");
            }
        } else if (Key.unicode_char >= 32 && Key.unicode_char < 127) {
            /* Printable ASCII */
            if (len < (sizeof(line) / sizeof(line[0]) - 1)) {
                line[len++] = Key.unicode_char;
                SAFE_PRINT(Boot, L"%c", Key.unicode_char);
            }
        }
    }

    /* Free our context on exit (allocated by the spawner). */
    kfree(ctx);
}

Notable design choices:

  • Non-blocking polling: When Boot->try_read_key is available, the terminal uses it and calls task_yield if no key is present. This avoids monopolising the CPU while idle.
  • Line editing: A fixed-size buffer line[128] accumulates ASCII characters. Backspace decrements len and erases the last character on screen.
  • Command execution: When the user presses Enter, the line is trimmed and either:
    • Handled as a built-in shell control command (exit, starling).
    • Sent to execute_command for lookup and execution.
  • Nested shells: The starling command spawns a nested Starling Terminal task with increased depth, allowing recursive shells.

Spawning the terminal

kmain spawns the initial Starling Terminal as its own task and then turns the core thread into an idle loop:

ctx = (StarlingContext *)kmalloc(sizeof(StarlingContext));
if (ctx == NULL) {
    SAFE_PRINT(Boot, L"Failed to allocate Starling Terminal context; starting inline.\n\r");
    StarlingContext inline_ctx;
    inline_ctx.Boot       = Boot;
    inline_ctx.depth      = 0;
    inline_ctx.shell_priv = TASK_PRIV_USER;
    starling_terminal_task(&inline_ctx);
    return;
}

ctx->Boot       = Boot;
ctx->depth      = 0;
ctx->shell_priv = TASK_PRIV_USER;

terminal_task = task_create_with_priv(L"starling-term",
                                      starling_terminal_task,
                                      ctx,
                                      TASK_PRIV_USER);
if (terminal_task == NULL) {
    SAFE_PRINT(Boot, L"Failed to start Starling Terminal task; falling back to kernel loop.\n\r");
    ...
    starling_terminal_task(Boot);
    return;
}

SAFE_PRINT(Boot, L"[core] Started Starling Terminal (PID %d).\n\r", terminal_task->pid);

/* Core thread becomes an idle loop, yielding to the terminal and others. */
while (TRUE) {
    task_yield();
}

This ensures that:

  • The terminal runs as a regular task managed by the cooperative scheduler.
  • The core thread remains available to run other tasks or future subsystems, rather than being permanently blocked in terminal I/O.

Command registry (commands[])

The command registry is defined in commands.c as a static array:

static Command commands[] = {
    {
        L"shutdown",
        L"Shutdown the system",
        L"Usage: shutdown\n\r  Initiates a system shutdown using UEFI runtime services.",
        TASK_PRIV_KERNEL,
        cmd_shutdown
    },
    {
        L"help",
        L"Display available commands",
        L"Usage: help\n\r  Lists all available commands with brief descriptions.",
        TASK_PRIV_USER,
        cmd_help
    },
    {
        L"man",
        L"Display manual page for a command",
        L"Usage: man <command>\n\r  Shows detailed help for the specified command.",
        TASK_PRIV_USER,
        cmd_man
    },
    {
        L"clear",
        L"Clear the screen",
        L"Usage: clear\n\r  Clears the console screen.",
        TASK_PRIV_USER,
        cmd_clear
    },
    {
        L"about",
        L"Display system information",
        L"Usage: about\n\r  Shows information about this operating system.",
        TASK_PRIV_USER,
        cmd_about
    },
    {
        L"mem",
        L"Display memory statistics",
        L"Usage: mem\n\r  Shows physical memory, heap, and paging information.",
        TASK_PRIV_KERNEL,
        cmd_mem
    },
    {
        L"ps",
        L"List running tasks",
        L"Usage: ps\n\r  Displays all active tasks with PID, state, and name.",
        TASK_PRIV_DRIVER,
        cmd_ps
    },
    {
        L"spawn",
        L"Spawn a demo background task",
        L"Usage: spawn [name]\n\r  Creates a cooperative demo task.\n\r  Optional argument sets the task name.",
        TASK_PRIV_DRIVER,
        cmd_spawn
    },
    {
        L"memtest",
        L"Test memory allocation and deallocation",
        ...
        TASK_PRIV_KERNEL,
        cmd_memtest
    },
    {
        L"tasktest",
        L"Test task scheduler with multiple tasks",
        ...
        TASK_PRIV_DRIVER,
        cmd_tasktest
    },
    {
        L"kusr",
        L"Run a command with kernel privilege",
        L"Usage: kusr <command> [args...]\n\r"
        L"  Temporarily elevates the current task to kernel privilege,\n\r"
        L"  executes the given command, then restores the original level.",
        TASK_PRIV_USER,
        cmd_kusr
    },
    {NULL, NULL, NULL, 0, NULL}  /* sentinel */
};

Each Command entry includes:

  • name the token typed at the prompt.
  • description a short summary used by help.
  • usage a longer description and usage details for man.
  • min_priv the minimum TaskPrivilege required to run the command (see task.h).
  • handler a function of type:
typedef void (*CommandHandlerFn)(BootInfo *Boot, CHAR16 *Args);

To add a new command, follow the guide in the file header:

 * To add a new command:
 *   1. Write a static handler  cmd_foo(BootInfo *Boot, CHAR16 *Args)
 *   2. Add a forward declaration above the table
 *   3. Append an entry to commands[] (before the sentinel)

Command execution pipeline

The central function that processes a line of user input is execute_command:

Task *execute_command(BootInfo *Boot, CHAR16 *Input, TaskPrivilege caller_priv)
{
    CHAR16 *cmd_start = NULL;
    CHAR16 *args_start = NULL;
    UINTN i = 0;
    CommandTaskContext *ctx;
    Task *t;
    
    if (Boot == NULL || Input == NULL) {
        return NULL;
    }

    trim_spaces_inplace(Input);
    
    if (Input[0] == L'\0') {
        return NULL;
    }

    /* Split input into command and argument strings */
    cmd_start = Input;
    args_start = Input;

    /* Advance past the command keyword */
    while (*args_start != L'\0' && !is_space16(*args_start)) {
        args_start++;
    }
    
    /* NUL-terminate the command and skip leading whitespace in args */
    if (*args_start != L'\0') {
        *args_start = L'\0';
        args_start++;
        
        /* skip leading whitespace in args */
        while (*args_start != L'\0' && is_space16(*args_start)) {
            args_start++;
        }
    }
    
    /* Look up and dispatch the command */
    for (i = 0; commands[i].name != NULL; i++) {
        if (ascii_streq_ci(cmd_start, commands[i].name)) {
            /* Allocate a context block for the command task. */
            ctx = (CommandTaskContext *)kmalloc(sizeof(CommandTaskContext));
            if (ctx == NULL) {
                SAFE_PRINT(Boot, L"Failed to allocate command context; running in core thread.\n\r");
                commands[i].handler(Boot, args_start);
                return NULL;
            }

            ctx->Boot    = Boot;
            ctx->handler = commands[i].handler;
            wstrcpy16_local(ctx->args, args_start, sizeof(ctx->args) / sizeof(ctx->args[0]));

            t = task_create_with_priv(commands[i].name,
                                      command_task_entry,
                                      ctx,
                                      caller_priv);
            if (t == NULL) {
                SAFE_PRINT(Boot, L"Failed to create task for command '%s'; running in core thread.\n\r",
                           commands[i].name);
                kfree(ctx);
                commands[i].handler(Boot, args_start);
                return NULL;
            }

            SAFE_PRINT(Boot, L"[starling] spawned '%s' as PID %d\n\r", t->name, t->pid);
            return t;
        }
    }

    /* Command not found */
    SAFE_PRINT(Boot, L"Unknown command: %s\n\r", cmd_start);
    SAFE_PRINT(Boot, L"Type 'help' for a list of available commands.\n\r");
    return NULL;
}

Pipeline stages:

  1. Normalisation:
    • trim_spaces_inplace removes leading/trailing spaces.
    • Empty lines are ignored.
  2. Tokenisation:
    • cmd_start points to the command token.
    • args_start is advanced past the command; the first whitespace is replaced with L'\0', splitting the string in-place.
    • Leading whitespace in args_start is skipped.
  3. Lookup:
    • commands[] is scanned for a name that matches cmd_start using case-insensitive ascii_streq_ci.
  4. Dispatch:
    • On match, a CommandTaskContext is allocated via kmalloc and filled with:
      • Boot pointer.
      • Handler function.
      • A bounded copy of the argument string.
    • A new task is created via task_create_with_priv with:
      • Task name = command name.
      • Entry = command_task_entry.
      • Argument = pointer to the context.
      • Privilege = caller_priv (inherited from the calling shell).
    • If task creation fails, the command handler is run synchronously in the current thread as a fallback.

The terminal then optionally task_waits on the returned Task *, serialising command execution from the user's perspective while still letting the scheduler run other tasks (e.g., background demos).


Command task entry and context

Command handlers are executed in a dedicated task, whose entry function is command_task_entry:

typedef struct {
    BootInfo         *Boot;
    CommandHandlerFn  handler;
    CHAR16            args[128];
} CommandTaskContext;

static void command_task_entry(void *arg);

Implementation:

static void command_task_entry(void *arg)
{
    CommandTaskContext *ctx = (CommandTaskContext *)arg;
    BootInfo *Boot = NULL;

    if (ctx == NULL) {
        return;
    }

    Boot = ctx->Boot;

    if (ctx->handler != NULL) {
        ctx->handler(Boot, ctx->args);
    }

    /* Context was heap-allocated in execute_command. */
    kfree(ctx);
}

This design gives each command:

  • Its own stack, independent of the terminal.
  • Its own argument buffer, isolated from the terminal's input buffer.
  • Automatic cleanup of the context when the command finishes.

Built-in commands

Some notable built-in handlers:

  • System control:
    • shutdown (KERNEL) → cmd_shutdown calls Boot->shutdown via request_shutdown to power off the machine. request_shutdown enforces kernel privilege.
    • clear (USER) → cmd_clear uses Boot->clear_screen to wipe the console.
  • Information and help:
    • help (USER) → cmd_help calls show_help to print a formatted table of available commands.
    • man (USER) → cmd_man prints the usage field for a specific command.
    • about (USER) → cmd_about prints OS information and feature list.
  • Diagnostics:
    • mem (KERNEL) → cmd_mem calls memory_print_stats to show PMM and heap state. The callee enforces kernel privilege.
    • ps (DRIVER) → cmd_ps calls task_print_list to show current tasks. The callee enforces driver privilege.
    • memtest (KERNEL) → cmd_memtest exercises heap and PMM allocations. The handler enforces kernel privilege.
    • tasktest (DRIVER) → cmd_tasktest spawns multiple worker tasks to demonstrate cooperative scheduling.
  • Tasking demo:
    • spawn (DRIVER) → cmd_spawn creates a demonstration task using demo_task_fn, which yields in a loop and reports progress.
  • Privilege escalation:
    • kusr (USER) → cmd_kusr temporarily elevates the calling task to TASK_PRIV_KERNEL, dispatches the given sub-command, then restores the original privilege level.

Examples:

static void cmd_mem(BootInfo *Boot, CHAR16 *Args)
{
    (void)Args;
    memory_print_stats(Boot);
}
static void cmd_ps(BootInfo *Boot, CHAR16 *Args)
{
    (void)Args;
    task_print_list(Boot);
}

From a user's perspective:

  • Commands are discoverable via help and man.
  • Many commands provide deep insight into internal subsystems (memory, tasks) without requiring external tooling.

Adding new commands

To add a new command foo:

  1. Declare the handler near the top of commands.c:
static void cmd_shutdown(BootInfo *Boot, CHAR16 *Args);
static void cmd_help(BootInfo *Boot, CHAR16 *Args);
...
static void cmd_tasktest(BootInfo *Boot, CHAR16 *Args);
static void cmd_kusr(BootInfo *Boot, CHAR16 *Args);
/* Add: */
static void cmd_foo(BootInfo *Boot, CHAR16 *Args);
  1. Implement the handler:
static void cmd_shutdown(BootInfo *Boot, CHAR16 *Args)
{
    (void)Args;
    SAFE_PRINT(Boot, L"Shutting down...\n\r");
    request_shutdown(Boot);
}

Use this as a template for cmd_foo, replacing the body with your logic and using SAFE_PRINT for output.

  1. Register the command in commands[] before the sentinel:
    {
        L"kusr",
        ...
        TASK_PRIV_USER,
        cmd_kusr
    },
    {NULL, NULL, NULL, 0, NULL}  /* sentinel */

Insert a new block above the sentinel:

    {
        L"foo",
        L"One-line description",
        L"Usage: foo [args]\n\r  Detailed explanation...",
        TASK_PRIV_USER,     /* minimum privilege required */
        cmd_foo
    },
  1. Rebuild and run. Typing foo at the starling> prompt will now execute your handler in its own task.

This extensible design makes it straightforward to grow the OS with additional diagnostics, demos, or experimental subsystems, all accessible from the interactive shell.