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

529 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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`:
```47:163:/home/lochlan/Documents/Coding/c/os/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:
```221:253:/home/lochlan/Documents/Coding/c/os/kernel.c
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:
```73:164:/home/lochlan/Documents/Coding/c/os/commands.c
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:
```14:19:/home/lochlan/Documents/Coding/c/os/commands.h
typedef void (*CommandHandlerFn)(BootInfo *Boot, CHAR16 *Args);
```
To add a new command, follow the guide in the file header:
```8:12:/home/lochlan/Documents/Coding/c/os/commands.c
* 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`:
```493:557:/home/lochlan/Documents/Coding/c/os/commands.c
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_wait`s 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`:
```44:50:/home/lochlan/Documents/Coding/c/os/commands.c
typedef struct {
BootInfo *Boot;
CommandHandlerFn handler;
CHAR16 args[128];
} CommandTaskContext;
static void command_task_entry(void *arg);
```
Implementation:
```570:587:/home/lochlan/Documents/Coding/c/os/commands.c
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:
```248:252:/home/lochlan/Documents/Coding/c/os/commands.c
static void cmd_mem(BootInfo *Boot, CHAR16 *Args)
{
(void)Args;
memory_print_stats(Boot);
}
```
```275:279:/home/lochlan/Documents/Coding/c/os/commands.c
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`:
```32:42:/home/lochlan/Documents/Coding/c/os/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);
```
2. **Implement the handler**:
```200:207:/home/lochlan/Documents/Coding/c/os/commands.c
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.
3. **Register the command** in `commands[]` before the sentinel:
```140:164:/home/lochlan/Documents/Coding/c/os/commands.c
{
L"kusr",
...
TASK_PRIV_USER,
cmd_kusr
},
{NULL, NULL, NULL, 0, NULL} /* sentinel */
```
Insert a new block above the sentinel:
```c
{
L"foo",
L"One-line description",
L"Usage: foo [args]\n\r Detailed explanation...",
TASK_PRIV_USER, /* minimum privilege required */
cmd_foo
},
```
4. 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.