From 1f733c120c281edc02ee8336920ce4fc6be54945 Mon Sep 17 00:00:00 2001 From: Aiken Harris Date: Mon, 20 Apr 2026 00:15:32 +0200 Subject: [PATCH] Implement extensible shell interface in the bootloader --- boot/xtldr/data.cc | 6 + boot/xtldr/includes/xtldr.hh | 23 ++ boot/xtldr/protocol.cc | 1 + boot/xtldr/shell.cc | 526 ++++++++++++++++++++++++++++++++++- sdk/xtdk/bltypes.h | 18 ++ sdk/xtdk/xtstruct.h | 1 + 6 files changed, 568 insertions(+), 7 deletions(-) diff --git a/boot/xtldr/data.cc b/boot/xtldr/data.cc index 6ba2b8a..9cc315d 100644 --- a/boot/xtldr/data.cc +++ b/boot/xtldr/data.cc @@ -56,6 +56,12 @@ XTBL_LOADER_PROTOCOL Protocol::LoaderProtocol; /* XT Boot Loader loaded modules list */ LIST_ENTRY Protocol::LoadedModules; +/* XT Boot Loader shell exit flag */ +BOOLEAN Shell::ExitRequest; + +/* XT Boot Loader shell commands list */ +LIST_ENTRY Shell::ShellCommands; + /* List of available block devices */ LIST_ENTRY Volume::EfiBlockDevices; diff --git a/boot/xtldr/includes/xtldr.hh b/boot/xtldr/includes/xtldr.hh index db7e8d7..edd67e9 100644 --- a/boot/xtldr/includes/xtldr.hh +++ b/boot/xtldr/includes/xtldr.hh @@ -253,11 +253,34 @@ class Protocol class Shell { + private: + STATIC BOOLEAN ExitRequest; + STATIC LIST_ENTRY ShellCommands; + public: + STATIC XTCDECL EFI_STATUS RegisterCommand(IN PCWSTR Command, + IN PCWSTR Description, + IN PBL_SHELL_COMMAND Handler); STATIC XTCDECL VOID StartLoaderShell(); private: + STATIC XTCDECL VOID CommandExit(IN ULONG Argc, + IN PWCHAR *Argv); + STATIC XTCDECL VOID CommandHelp(IN ULONG Argc, + IN PWCHAR *Argv); + STATIC XTCDECL VOID CommandReboot(IN ULONG Argc, + IN PWCHAR *Argv); + STATIC XTCDECL VOID CommandVersion(IN ULONG Argc, + IN PWCHAR *Argv); + STATIC XTCDECL VOID ExecuteCommand(IN ULONG Argc, + IN PWCHAR *Argv); + STATIC XTCDECL EFI_STATUS ParseCommand(IN PWCHAR CommandLine, + OUT PULONG Argc, + OUT PWCHAR **Argv); STATIC XTCDECL VOID PrintPrompt(); + STATIC XTCDECL VOID ReadCommand(OUT PWCHAR Buffer, + IN ULONG BufferSize); + STATIC XTCDECL VOID RegisterBuiltinCommands(); }; class TextUi diff --git a/boot/xtldr/protocol.cc b/boot/xtldr/protocol.cc index 2926a5d..922ae1f 100644 --- a/boot/xtldr/protocol.cc +++ b/boot/xtldr/protocol.cc @@ -1077,6 +1077,7 @@ Protocol::InstallXtLoaderProtocol() LoaderProtocol.Protocol.LocateHandles = LocateProtocolHandles; LoaderProtocol.Protocol.Open = OpenProtocol; LoaderProtocol.Protocol.OpenHandle = OpenProtocolHandle; + LoaderProtocol.Shell.RegisterCommand = Shell::RegisterCommand; LoaderProtocol.String.Compare = RTL::String::CompareString; LoaderProtocol.String.Length = RTL::String::StringLength; LoaderProtocol.String.ToWideString = RTL::String::StringToWideString; diff --git a/boot/xtldr/shell.cc b/boot/xtldr/shell.cc index 1e9c6cd..1e8c185 100644 --- a/boot/xtldr/shell.cc +++ b/boot/xtldr/shell.cc @@ -4,13 +4,21 @@ * FILE: xtldr/shell.cc * DESCRIPTION: XT Boot Loader shell * DEVELOPERS: Rafal Kupiec + * Aiken Harris */ #include /** - * Starts XTLDR shell. + * Implements the built-in `exit` command. Sets the exit flag to signal the main + * shell loop to terminate and return control to the boot menu. + * + * @param Argc + * Supplies the number of arguments. + * + * @param Argv + * Supplies a list of arguments provided by the user. * * @return This routine does not return any value. * @@ -18,14 +26,283 @@ */ XTCDECL VOID -Shell::StartLoaderShell() +Shell::CommandExit(IN ULONG Argc, + IN PWCHAR *Argv) { - /* Initialize console */ - Console::InitializeConsole(); + /* Signal the main shell loop to stop and return to the boot menu */ + ExitRequest = TRUE; +} - /* Print prompt */ - PrintPrompt(); - for(;;); +/** + * Implements the built-in `help` command. Prints a list of available commands alongside their descriptions. + * + * @param Argc + * Supplies the number of arguments. + * + * @param Argv + * Supplies a list of arguments provided by the user. + * + * @return This routine does not return any value. + * + * @since XT 1.0 + */ +XTCDECL +VOID +Shell::CommandHelp(IN ULONG Argc, + IN PWCHAR *Argv) +{ + PXTBL_SHELL_COMMAND CommandEntry; + PLIST_ENTRY ListEntry; + + /* Print a header line */ + Console::Print(L"Available commands:\n\n"); + + /* Walk the registered commands list */ + ListEntry = ShellCommands.Flink; + while(ListEntry != &ShellCommands) + { + /* Retrieve the current command entry */ + CommandEntry = CONTAIN_RECORD(ListEntry, XTBL_SHELL_COMMAND, Flink); + + /* Print the command name in a highlighted color */ + Console::SetAttributes(EFI_TEXT_BGCOLOR_BLACK | EFI_TEXT_FGCOLOR_WHITE); + Console::Print(L" %-12S", CommandEntry->Command); + + /* Print the description in the default color */ + Console::SetAttributes(EFI_TEXT_BGCOLOR_BLACK | EFI_TEXT_FGCOLOR_LIGHTGRAY); + Console::Print(L" %S\n", CommandEntry->Description); + + /* Advance to the next entry */ + ListEntry = ListEntry->Flink; + } +} + +/** + * Implements the built-in `reboot` command. Performs a normal system restart via the EFI runtime services. + * When the '/EFI' parameter is supplied, the routine instead schedules a reboot into the UEFI firmware setup interface. + * + * @param Argc + * Supplies the number of arguments. + * + * @param Argv + * Supplies a list of arguments provided by the user. + * + * @return This routine does not return any value. + * + * @since XT 1.0 + */ +XTCDECL +VOID +Shell::CommandReboot(IN ULONG Argc, + IN PWCHAR *Argv) +{ + /* Check if the /EFI flag was specified */ + if(Argc > 1 && RTL::WideString::CompareWideStringInsensitive(Argv[1], L"/EFI", 0) == 0) + { + /* Attempt to reboot into firmware setup */ + Console::Print(L"Rebooting into UEFI firmware setup...\n"); + EfiUtils::EnterFirmwareSetup(); + + /* The firmware does not support this feature, print error message */ + Console::Print(L"ERROR: Reboot into firmware setup interface not supported.\n"); + } + else + { + /* Perform a standard system reboot */ + Console::Print(L"Rebooting...\n"); + EfiUtils::RebootSystem(); + + /* The reboot call failed, print error message */ + Console::Print(L"ERROR: Failed to reboot the machine\n"); + } +} + +/** + * Implements the built-in `ver` command. Prints the bootloader identification string. + * + * @param Argc + * Supplies the number of arguments. + * + * @param Argv + * Supplies a list of arguments provided by the user. + * + * @return This routine does not return any value. + * + * @since XT 1.0 + */ +XTCDECL +VOID +Shell::CommandVersion(IN ULONG Argc, + IN PWCHAR *Argv) +{ + /* Check if debugging enabled */ + if(DEBUG) + { + /* Print debug version of XTLDR version string */ + Console::Print(L"XTLDR Boot Loader v%d.%d (%s-%s)\n", + XTLDR_VERSION_MAJOR, XTLDR_VERSION_MINOR, XTOS_VERSION_DATE, XTOS_VERSION_HASH); + } + else + { + /* Print standard XTLDR version string */ + Console::Print(L"XTLDR Boot Loader v%d.%d\n", XTLDR_VERSION_MAJOR, XTLDR_VERSION_MINOR); + } +} + +/** + * Looks up the given command name in the registered shell commands list and invokes the corresponding handler. + * + * @param Argc + * Supplies the number of arguments in the argument vector, including the command name itself. + * + * @param Argv + * Supplies a pointer to the argument vector. First argument is the command name. + * + * @return This routine does not return any value. + * + * @since XT 1.0 + */ +XTCDECL +VOID +Shell::ExecuteCommand(IN ULONG Argc, + IN PWCHAR *Argv) +{ + PXTBL_SHELL_COMMAND CommandEntry; + PLIST_ENTRY ListEntry; + + /* Walk through the list of registered shell commands */ + ListEntry = ShellCommands.Flink; + while(ListEntry != &ShellCommands) + { + /* Retrieve the shell command entry from the list node */ + CommandEntry = CONTAIN_RECORD(ListEntry, XTBL_SHELL_COMMAND, Flink); + + /* Perform a case-insensitive comparison against the command name */ + if(RTL::WideString::CompareWideStringInsensitive(CommandEntry->Command, Argv[0], 0) == 0) + { + /* Command matches, invoke its handler */ + CommandEntry->Handler(Argc, Argv); + + /* Print a trailing blank line for visual separation and return */ + Console::Print(L"\n"); + return; + } + + /* Advance to the next registered command */ + ListEntry = ListEntry->Flink; + } + + /* No matching command was found, print error message */ + Console::Print(L"ERROR: '%S' is not recognized as a valid command.\n\n", Argv[0]); +} + +/** + * Splits the supplied raw command line string into an argument count and an argument vector suitable + * for command dispatch. The input string is tokenized by whitespace. + * + * @param CommandLine + * Supplies a mutable wide-character string containing the raw command line. + * + * @param Argc + * Receives the number of arguments found in the command line. + * + * @param Argv + * Receives a pointer to an allocated array of wide-character string pointers, one for each argument. + * + * @return This routine returns a status code. + * + * @since XT 1.0 + */ +XTCDECL +EFI_STATUS +Shell::ParseCommand(IN PWCHAR CommandLine, + OUT PULONG Argc, + OUT PWCHAR **Argv) +{ + PWCHAR Token, SavePtr, TempLine; + PWCHAR *ArgumentVector; + ULONG ArgumentCount; + EFI_STATUS Status; + + /* Initialize argument count */ + ArgumentCount = 0; + + /* Count the tokens to determine the size of the argument vector */ + TempLine = CommandLine; + Token = RTL::WideString::TokenizeWideString(TempLine, L" ", &SavePtr); + while(Token != NULLPTR) + { + /* One more argument found */ + ArgumentCount++; + + /* Continue tokenizing */ + Token = RTL::WideString::TokenizeWideString(NULLPTR, L" ", &SavePtr); + } + + /* Check if the command line was empty */ + if(ArgumentCount == 0) + { + /* Set argument count and vector to zero and NULL */ + *Argc = 0; + *Argv = NULLPTR; + + /* Return success */ + return STATUS_EFI_SUCCESS; + } + + /* Allocate memory for the argument vector */ + Status = Memory::AllocatePool(ArgumentCount * sizeof(PWCHAR), (PVOID *)&ArgumentVector); + if(Status != STATUS_EFI_SUCCESS) + { + /* Memory allocation failure, return status code */ + return Status; + } + + /* Reset argument count and temp line */ + ArgumentCount = 0; + TempLine = CommandLine; + + /* Walk through the command line */ + while(*TempLine != L'\0') + { + /* Skip leading whitespace */ + while(*TempLine == L' ') + { + /* Move to the next character */ + TempLine++; + } + + /* Check if the end of the string was reached */ + if(*TempLine == L'\0') + { + /* End of string reached, break the loop */ + break; + } + + /* Record token */ + ArgumentVector[ArgumentCount] = TempLine; + ArgumentCount++; + + /* Advance past the token characters */ + while(*TempLine != L'\0' && *TempLine != L' ') + { + /* Move to the next character */ + TempLine++; + } + + /* Check if token was NULL-terminated */ + if(*TempLine != L'\0') + { + /* NULL-terminate the token and move to the next character */ + *TempLine = L'\0'; + TempLine++; + } + } + + /* Return results to the caller */ + *Argc = ArgumentCount; + *Argv = ArgumentVector; + return STATUS_EFI_SUCCESS; } /** @@ -48,3 +325,238 @@ Shell::PrintPrompt() /* Reset standard shell colors */ Console::SetAttributes(EFI_TEXT_BGCOLOR_BLACK | EFI_TEXT_FGCOLOR_LIGHTGRAY); } + +/** + * Reads a complete line of input from the shell console into the supplied buffer. + * + * @param Buffer + * Supplies a pointer to a wide-character buffer that receives the entered command line. + * + * @param BufferSize + * Supplies the capacity of the buffer, in wide characters. + * + * @return This routine does not return any value. + * + * @since XT 1.0 + */ +XTCDECL +VOID +Shell::ReadCommand(OUT PWCHAR Buffer, + IN ULONG BufferSize) +{ + ULONG CursorPosition; + UINT_PTR EventIndex; + EFI_INPUT_KEY Key; + + /* Start with an empty buffer */ + CursorPosition = 0; + Buffer[0] = L'\0'; + + /* Read characters until the user submits the command line */ + while(TRUE) + { + /* Wait until a key event is available */ + EfiUtils::WaitForEfiEvent(1, &(XtLoader::GetEfiSystemTable()->ConIn->WaitForKey), &EventIndex); + + /* Read the keystroke from the input device */ + Console::ReadKeyStroke(&Key); + + /* Check the keystroke */ + if(Key.UnicodeChar == 0x0D) + { + /* ENTER key pressed - terminate the buffer and move to a new line */ + Buffer[CursorPosition] = L'\0'; + Console::Print(L"\n"); + + /* Return the command line to the caller */ + return; + } + else if(Key.ScanCode == 0x17) + { + /* ESC key pressed - discard the current input, move to a new line and reprint the prompt */ + Buffer[0] = L'\0'; + Console::Print(L"\n"); + PrintPrompt(); + CursorPosition = 0; + + /* Continue reading the command line */ + continue; + } + else if(Key.UnicodeChar == 0x08) + { + /* Backspace key pressed - erase the last character from the buffer */ + if(CursorPosition > 0) + { + /* Erase the last character from the buffer */ + CursorPosition--; + Buffer[CursorPosition] = L'\0'; + + /* Move the cursor back, overwrite the character with a space, then move back again */ + Console::Print(L"\b \b"); + } + + /* Continue reading the command line */ + continue; + } + else if(Key.UnicodeChar == 0) + { + /* Ignore non-printable characters */ + continue; + } + + /* Make sure there is room in the buffer (reserve one slot for NULL terminator) */ + if(CursorPosition < BufferSize - 1) + { + /* Append the character to the buffer and NULL-terminate it */ + Buffer[CursorPosition] = Key.UnicodeChar; + CursorPosition++; + Buffer[CursorPosition] = L'\0'; + + /* Echo the character to the console */ + Console::PutChar(Key.UnicodeChar); + } + } +} + +/** + * Registers a new command in the XTLDR shell. + * + * @param Command + * Supplies the command keyword that the user types at the shell prompt. + * + * @param Description + * Supplies a short help string displayed by the 'help' command. + * + * @param Handler + * Supplies a pointer to the function that implements the command. + * + * @return This routine returns a status code. + * + * @since XT 1.0 + */ +XTCDECL +EFI_STATUS +Shell::RegisterCommand(IN PCWSTR Command, + IN PCWSTR Description, + IN PBL_SHELL_COMMAND Handler) +{ + PXTBL_SHELL_COMMAND CommandEntry; + PLIST_ENTRY ListEntry; + EFI_STATUS Status; + + /* Verify that a command with this name has not already been registered */ + ListEntry = ShellCommands.Flink; + while(ListEntry != &ShellCommands) + { + /* Retrieve the existing shell command entry */ + CommandEntry = CONTAIN_RECORD(ListEntry, XTBL_SHELL_COMMAND, Flink); + + /* Compare command names case-insensitively */ + if(RTL::WideString::CompareWideStringInsensitive(CommandEntry->Command, Command, 0) == 0) + { + /* Duplicate command name, return error */ + return STATUS_EFI_INVALID_PARAMETER; + } + + /* Advance to the next entry */ + ListEntry = ListEntry->Flink; + } + + /* Allocate memory for the new command entry */ + Status = Memory::AllocatePool(sizeof(XTBL_SHELL_COMMAND), (PVOID *)&CommandEntry); + if(Status != STATUS_EFI_SUCCESS) + { + /* Memory allocation failure, return error */ + return STATUS_EFI_OUT_OF_RESOURCES; + } + + /* Populate the new command entry */ + CommandEntry->Command = (PWCHAR)Command; + CommandEntry->Description = (PWCHAR)Description; + CommandEntry->Handler = Handler; + + /* Append the command to the global shell commands list */ + RTL::LinkedList::InsertTailList(&ShellCommands, &CommandEntry->Flink); + + /* Return success */ + return STATUS_EFI_SUCCESS; +} + +/** + * Registers all built-in shell commands that are provided by the XTLDR. + * + * @return This routine does not return any value. + * + * @since XT 1.0 + */ +XTCDECL +VOID +Shell::RegisterBuiltinCommands() +{ + /* Register all built-in shell commands */ + RegisterCommand(L"exit", L"Exits the shell and returns to the boot menu", CommandExit); + RegisterCommand(L"help", L"Displays a list of all available shell commands", CommandHelp); + RegisterCommand(L"reboot", L"Reboots the machine (/EFI to enter firmware setup)", CommandReboot); + RegisterCommand(L"ver", L"Displays the boot loader version information", CommandVersion); +} + +/** + * Initializes the command list, registers the built-in commands and enters an interactive XTLDR shell loop. + * + * @return This routine does not return any value. + * + * @since XT 1.0 + */ +XTCDECL +VOID +Shell::StartLoaderShell() +{ + WCHAR CommandLine[XTBL_SHELL_MAX_LINE_LENGTH]; + EFI_STATUS Status; + PWCHAR *ArgumentVector; + ULONG ArgumentCount; + + /* Initialize console */ + Console::InitializeConsole(); + + /* Initialize the shell commands list */ + RTL::LinkedList::InitializeListHead(&ShellCommands); + + /* Register all built-in commands */ + RegisterBuiltinCommands(); + + /* Clear the shell exit request flag */ + ExitRequest = FALSE; + + /* Main XTLDR shell loop */ + while(!ExitRequest) + { + /* Display the shell prompt */ + PrintPrompt(); + + /* Read a command line */ + ReadCommand(CommandLine, XTBL_SHELL_MAX_LINE_LENGTH); + + /* Parse the command line into a list of arguments */ + Status = ParseCommand(CommandLine, &ArgumentCount, &ArgumentVector); + if(Status != STATUS_EFI_SUCCESS) + { + /* Parsing failed, print error and continue */ + Console::Print(L"ERROR: Failed to parse command line (Status: 0x%llx).\n", Status); + continue; + } + + /* Check if command line is empty */ + if(ArgumentCount == 0) + { + /* Skip empty command line */ + continue; + } + + /* Dispatch the command */ + ExecuteCommand(ArgumentCount, ArgumentVector); + + /* Free the argument vector */ + Memory::FreePool(ArgumentVector); + } +} diff --git a/sdk/xtdk/bltypes.h b/sdk/xtdk/bltypes.h index 4b3f52a..80c61bc 100644 --- a/sdk/xtdk/bltypes.h +++ b/sdk/xtdk/bltypes.h @@ -41,6 +41,9 @@ /* TUI dialog box maximum width */ #define XTBL_TUI_MAX_DIALOG_WIDTH 100 +/* Maximum length of a single shell command line, in wide characters */ +#define XTBL_SHELL_MAX_LINE_LENGTH 256 + /* C/C++ specific code */ #ifndef D__XTOS_ASSEMBLER__ @@ -143,6 +146,8 @@ typedef XTSTATUS (XTAPI *PBL_WIDESTRING_FORMAT)(IN PRTL_PRINT_CONTEXT Context, I typedef SIZE_T (XTAPI *PBL_WIDESTRING_LENGTH)(IN PCWSTR String, IN SIZE_T MaxLength); typedef PWCHAR (XTAPI *PBL_WIDESTRING_TOKENIZE)(IN PWCHAR String, IN PCWSTR Delimiter, IN OUT PWCHAR *SavePtr); typedef EFI_STATUS (XTCDECL *PBL_WAIT_FOR_EFI_EVENT)(IN UINT_PTR NumberOfEvents, IN PEFI_EVENT Event, OUT PUINT_PTR Index); +typedef VOID (XTCDECL *PBL_SHELL_COMMAND)(IN ULONG Argc, IN PWCHAR *Argv); +typedef EFI_STATUS (XTCDECL *PBL_REGISTER_SHELL_COMMAND)(IN PCWSTR Command, IN PCWSTR Description, IN PBL_SHELL_COMMAND Handler); typedef VOID (XTCDECL *PBL_XT_BOOT_MENU)(); typedef VOID (XTAPI *PBL_ZERO_MEMORY)(OUT PVOID Destination, IN SIZE_T Length); @@ -233,6 +238,15 @@ typedef struct _XTBL_KNOWN_BOOT_PROTOCOL EFI_GUID Guid; } XTBL_KNOWN_BOOT_PROTOCOL, *PXTBL_KNOWN_BOOT_PROTOCOL; +/* XTLDR Shell command entry */ +typedef struct _XTBL_SHELL_COMMAND +{ + LIST_ENTRY Flink; + PWCHAR Command; + PWCHAR Description; + PBL_SHELL_COMMAND Handler; +} XTBL_SHELL_COMMAND, *PXTBL_SHELL_COMMAND; + /* Boot Loader memory mapping information */ typedef struct _XTBL_MEMORY_MAPPING { @@ -482,6 +496,10 @@ typedef struct _XTBL_LOADER_PROTOCOL PBL_OPEN_PROTOCOL_HANDLE OpenHandle; } Protocol; struct + { + PBL_REGISTER_SHELL_COMMAND RegisterCommand; + } Shell; + struct { PBL_STRING_COMPARE Compare; PBL_STRING_LENGTH Length; diff --git a/sdk/xtdk/xtstruct.h b/sdk/xtdk/xtstruct.h index cdd4455..c9c0e75 100644 --- a/sdk/xtdk/xtstruct.h +++ b/sdk/xtdk/xtstruct.h @@ -356,6 +356,7 @@ typedef struct _XTBL_MODULE_AUTHORS XTBL_MODULE_AUTHORS, *PXTBL_MODULE_AUTHORS; typedef struct _XTBL_MODULE_DEPS XTBL_MODULE_DEPS, *PXTBL_MODULE_DEPS; typedef struct _XTBL_MODULE_INFO XTBL_MODULE_INFO, *PXTBL_MODULE_INFO; typedef struct _XTBL_PAGE_MAPPING XTBL_PAGE_MAPPING, *PXTBL_PAGE_MAPPING; +typedef struct _XTBL_SHELL_COMMAND XTBL_SHELL_COMMAND, *PXTBL_SHELL_COMMAND; typedef struct _XTBL_STATUS XTBL_STATUS, *PXTBL_STATUS; /* Unions forward references */