529 lines
17 KiB
Markdown
529 lines
17 KiB
Markdown
## 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 read–eval–print 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.
|
||
|