15 KiB
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
BootInfoservices 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:
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_keyis available, the terminal uses it and callstask_yieldif no key is present. This avoids monopolising the CPU while idle. - Line editing: A fixed-size buffer
line[128]accumulates ASCII characters. Backspace decrementslenand 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_commandfor lookup and execution.
- Handled as a built-in shell control command (
- Nested shells: The
starlingcommand spawns a nested Starling Terminal task with increaseddepth, 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;
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:
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 byhelp.usage– a longer description and usage details forman.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)
{
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:
- Normalisation:
trim_spaces_inplaceremoves leading/trailing spaces.- Empty lines are ignored.
- Tokenisation:
cmd_startpoints to the command token.args_startis advanced past the command; the first whitespace is replaced withL'\0', splitting the string in-place.- Leading whitespace in
args_startis skipped.
- Lookup:
commands[]is scanned for a name that matchescmd_startusing case-insensitiveascii_streq_ci.
- Dispatch:
- On match, a
CommandTaskContextis allocated viakmallocand filled with:Bootpointer.- Handler function.
- A bounded copy of the argument string.
- A new task is created via
task_createwith:- 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.
- On match, a
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→cmd_shutdowncallsBoot->shutdownviarequest_shutdownto power off the machine.clear→cmd_clearusesBoot->clear_screento wipe the console.
- Information and help:
help→cmd_helpcallsshow_helpto print a formatted table of available commands.man→cmd_manprints theusagefield for a specific command.about→cmd_aboutprints OS information and feature list.
- Diagnostics:
mem→cmd_memcallsmemory_print_statsto show PMM and heap state.ps→cmd_pscallstask_print_listto show current tasks.memtest→cmd_memtestexercises heap and PMM allocations.tasktest→cmd_tasktestspawns multiple worker tasks to demonstrate cooperative scheduling.
- Tasking demo:
spawn→cmd_spawncreates a demonstration task usingdemo_task_fn, which yields in a loop and reports progress.
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
helpandman. - Many commands provide deep insight into internal subsystems (memory, tasks) without requiring external tooling.
Adding new commands
To add a new command foo:
- 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);
/* Add: */
static void cmd_foo(BootInfo *Boot, CHAR16 *Args);
- 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.
- Register the command in
commands[]before the sentinel:
{
L"tasktest",
...
cmd_tasktest
},
{NULL, NULL, NULL, NULL} /* sentinel */
Insert a new block above the sentinel:
{
L"foo",
L"One-line description",
L"Usage: foo [args]\n\r Detailed explanation...",
cmd_foo
},
- Rebuild and run. Typing
fooat thestarling>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.