Refine command execution and enhance documentation. Improved the CommandTaskContext structure for better task management and updated README.md with clearer instructions for 'memtest' and 'tasktest' commands.
This commit is contained in:
491
docs/commands-and-terminal.md
Normal file
491
docs/commands-and-terminal.md
Normal file
@@ -0,0 +1,491 @@
|
||||
## 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, 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;
|
||||
|
||||
if (ctx == NULL || ctx->Boot == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
Boot = ctx->Boot;
|
||||
depth = ctx->depth;
|
||||
|
||||
SAFE_PRINT(Boot, L"\n\r[Starling Terminal depth %d] ready.\n\r\n\r", depth);
|
||||
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);
|
||||
|
||||
/* 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;
|
||||
starling_terminal_task(&inline_ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx->Boot = Boot;
|
||||
ctx->depth = 0;
|
||||
|
||||
terminal_task = task_create(L"starling-term", starling_terminal_task, ctx);
|
||||
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:144:/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.",
|
||||
cmd_shutdown
|
||||
},
|
||||
{
|
||||
L"help",
|
||||
L"Display available commands",
|
||||
L"Usage: help\n\r Lists all available commands with brief descriptions.",
|
||||
cmd_help
|
||||
},
|
||||
{
|
||||
L"man",
|
||||
L"Display manual page for a command",
|
||||
L"Usage: man <command>\n\r Shows detailed help for the specified command.",
|
||||
cmd_man
|
||||
},
|
||||
{
|
||||
L"clear",
|
||||
L"Clear the screen",
|
||||
L"Usage: clear\n\r Clears the console screen.",
|
||||
cmd_clear
|
||||
},
|
||||
{
|
||||
L"about",
|
||||
L"Display system information",
|
||||
L"Usage: about\n\r Shows information about this operating system.",
|
||||
cmd_about
|
||||
},
|
||||
{
|
||||
L"mem",
|
||||
L"Display memory statistics",
|
||||
L"Usage: mem\n\r Shows physical memory, heap, and paging information.",
|
||||
cmd_mem
|
||||
},
|
||||
{
|
||||
L"ps",
|
||||
L"List running tasks",
|
||||
L"Usage: ps\n\r Displays all active tasks with PID, state, and name.",
|
||||
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.",
|
||||
cmd_spawn
|
||||
},
|
||||
{
|
||||
L"memtest",
|
||||
L"Test memory allocation and deallocation",
|
||||
...
|
||||
cmd_memtest
|
||||
},
|
||||
{
|
||||
L"tasktest",
|
||||
L"Test task scheduler with multiple tasks",
|
||||
...
|
||||
cmd_tasktest
|
||||
},
|
||||
{NULL, NULL, NULL, 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`.
|
||||
- `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)
|
||||
{
|
||||
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(commands[i].name, command_task_entry, ctx);
|
||||
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:
|
||||
- Task name = command name.
|
||||
- Entry = `command_task_entry`.
|
||||
- Argument = pointer to the context.
|
||||
- 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` → `cmd_shutdown` calls `Boot->shutdown` via `request_shutdown` to power off the machine.
|
||||
- `clear` → `cmd_clear` uses `Boot->clear_screen` to wipe the console.
|
||||
- **Information and help**:
|
||||
- `help` → `cmd_help` calls `show_help` to print a formatted table of available commands.
|
||||
- `man` → `cmd_man` prints the `usage` field for a specific command.
|
||||
- `about` → `cmd_about` prints OS information and feature list.
|
||||
- **Diagnostics**:
|
||||
- `mem` → `cmd_mem` calls `memory_print_stats` to show PMM and heap state.
|
||||
- `ps` → `cmd_ps` calls `task_print_list` to show current tasks.
|
||||
- `memtest` → `cmd_memtest` exercises heap and PMM allocations.
|
||||
- `tasktest` → `cmd_tasktest` spawns multiple worker tasks to demonstrate cooperative scheduling.
|
||||
- **Tasking demo**:
|
||||
- `spawn` → `cmd_spawn` creates a demonstration task using `demo_task_fn`, which yields in a loop and reports progress.
|
||||
|
||||
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:41:/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);
|
||||
/* 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:143:/home/lochlan/Documents/Coding/c/os/commands.c
|
||||
{
|
||||
L"tasktest",
|
||||
...
|
||||
cmd_tasktest
|
||||
},
|
||||
{NULL, NULL, NULL, NULL} /* sentinel */
|
||||
```
|
||||
|
||||
Insert a new block above the sentinel:
|
||||
|
||||
```c
|
||||
{
|
||||
L"foo",
|
||||
L"One-line description",
|
||||
L"Usage: foo [args]\n\r Detailed explanation...",
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user