From c885f33d76356dbd6677b0c33b957f213d8bb62b Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 8 Sep 2026 13:23:34 +0200 Subject: [PATCH] feat: extend shell with execute --- src/shell.c | 154 +++++++++++++++++++++++++++++++++++++++----- src/sigTrampoline.S | 41 ++++++++++++ 2 files changed, 178 insertions(+), 17 deletions(-) create mode 100644 src/sigTrampoline.S diff --git a/src/shell.c b/src/shell.c index 09a2059..e778c17 100644 --- a/src/shell.c +++ b/src/shell.c @@ -1545,7 +1545,29 @@ static bool shellExpandCommandSubst(const char* srcLine, char* outLine, int outC static int64 executeSingle(char* line) { if (line[0] == 0) - return; + return 0; + + char* pipeStages[shellMaxPipelineStages]; + int pipeStageCount = shellSplitPipelineStages(line, pipeStages, shellMaxPipelineStages); + if (pipeStageCount == -1) { + kprintf("pipeline: too many '|' stages (max %d)\n", shellMaxPipelineStages); + return shellExecNotFound; + } + if (pipeStageCount > 0) + return runPipelineStages(pipeStages, pipeStageCount); + + /* `$(cmd)` command substitution (shellExpandCommandSubst above) only + ever runs for a single, non-piped command -- an honestly-scoped + limitation matching this file's own history of landing a feature for + the plain case first and generalizing to pipeline stages in a later + fala (`<<<` here-strings took the same path: jobctlSpawnForegroundHereString + landed before jobctlSpawnPipedHereString ever fed a pipeline's first + stage). `|` has already been ruled out above (pipeStageCount == 0), + so nothing here can still be misread as a pipe. */ + char expandedLine[lineCapacity]; + if (!shellExpandCommandSubst(line, expandedLine, sizeof(expandedLine))) + return shellExecNotFound; + line = expandedLine; char* argument = line; while (*argument && *argument != ' ') @@ -1557,30 +1579,111 @@ static int64 executeSingle(char* line) while (*argument == ' ') argument++; - if (strcmp(line, "help") == 0) + if (strcmp(line, "help") == 0) { printHelp(); - else if (strcmp(line, "echo") == 0) + return 0; + } else if (strcmp(line, "echo") == 0) { doEcho(argument); - else if (strcmp(line, "ls") == 0) - listDir(argument); + return 0; + } else if (strcmp(line, "ls") == 0) + return listDir(argument); else if (strcmp(line, "cat") == 0) - catFile(argument); + return catFile(argument); else if (strcmp(line, "mkdir") == 0) - makeDir(argument); + return makeDir(argument); + else if (strcmp(line, "rm") == 0) + return removeFile(argument); + else if (strcmp(line, "rmdir") == 0) + return removeDir(argument); + else if (strcmp(line, "chmod") == 0) + return doChmod(argument); + else if (strcmp(line, "chown") == 0) + return doChown(argument); + else if (strcmp(line, "chgrp") == 0) + return doChgrp(argument); else if (strcmp(line, "cd") == 0) - changeDir(argument); - else if (strcmp(line, "pwd") == 0) - kprintf("%s\n", cwd); - else if (strcmp(line, "mem") == 0) + return changeDir(argument); + else if (strcmp(line, "pwd") == 0) { + char path[pathCapacity]; + vfsGetcwd(path, sizeof(path)); + kprintf("%s\n", path); + return 0; + } + else if (strcmp(line, "whoami") == 0) { + kprintf("%u\n", currentThreadUid()); + return 0; + } + else if (strcmp(line, "mem") == 0) { printMem(); - else if (strcmp(line, "uptime") == 0) + return 0; + } else if (strcmp(line, "uptime") == 0) { printUptime(); - else if (strcmp(line, "ps") == 0) + return 0; + } else if (strcmp(line, "ps") == 0) { printThreads(); - else if (strcmp(line, "clear") == 0) + return 0; + } else if (strcmp(line, "jobs") == 0) { + listBackgroundJobs(); + return 0; + } else if (strcmp(line, "export") == 0) + return doExport(argument); + else if (strcmp(line, "env") == 0) { + printEnv(); + return 0; + } else if (strcmp(line, "unset") == 0) + return doUnset(argument); + else if (strcmp(line, "clear") == 0) { vgaClear(); - else - kprintf("unknown command: %s (try 'help')\n", line); + return 0; + } else + return runProgram(line, argument); +} + +/* Splits `line` at every top-level `&&`/`||` (shellSplitAndOr, shell.h) + and runs each segment left to right through executeSingle above, + short-circuiting exactly like a real shell: a segment preceded by `&&` + only runs if the PREVIOUS segment that actually ran (not necessarily the + lexically previous one -- a skipped segment leaves `status` completely + unchanged) exited 0, one preceded by `||` only runs if that same status + was nonzero. This is real conditional chaining WITHIN a single typed + line (`cmd1 && cmd2 || cmd3`, the classic shell if-then-else idiom). + `lastStatus` (this file's own file-scope static above) is updated right + after every executeSingle() call below, chained or not -- a real shell's + own `$?` updates after EVERY simple command, including mid-chain (so + `false || echo $?` sees `false`'s own status while evaluating `echo`'s + argv, not some later value), and survives PAST this function returning, + so a later, completely separate typed line can still read it via + shellExpandToken's own `$?` handling -- closing the "no persistent `$?` + across separately typed lines" gap this file used to document here as a + deliberate limitation. A SKIPPED segment leaves `lastStatus` untouched + exactly like it already leaves the local chaining `status` untouched. */ +static void execute(char* line) +{ + if (line[0] == 0) + return; + + char* andOrSegments[shellMaxAndOrSegments]; + ShellAndOrOp andOrOps[shellMaxAndOrSegments - 1]; + int andOrCount = shellSplitAndOr(line, andOrSegments, andOrOps, shellMaxAndOrSegments); + if (andOrCount == -1) { + kprintf("shell: too many '&&'/'||' segments (max %d)\n", shellMaxAndOrSegments); + return; + } + if (andOrCount == 0) { + lastStatus = executeSingle(line); + return; + } + + int64 status = 0; + for (int i = 0; i < andOrCount; i++) { + if (i > 0) { + bool shouldRun = (andOrOps[i - 1] == shellAndOrAnd) ? (status == 0) : (status != 0); + if (!shouldRun) + continue; + } + status = executeSingle(andOrSegments[i]); + lastStatus = status; + } } void shellRun(void) @@ -1588,17 +1691,34 @@ void shellRun(void) char line[lineCapacity]; uint32 length = 0; + /* The shell is its own independent console reader (keyboard.h), same as + every ring-3 process's fd 0 -- a keystroke keyboardHandleScancode + decodes is broadcast to every open reader, this one included, so the + shell keeps working unchanged even while other processes are also + blocked reading fd 0 (see the consoleReaderA/consoleReaderB live boot + demo in kmain.c). Plain kernel-thread context (not a syscall + handler), so the blocking helper's real halt() loop is safe here, + exactly like pipeReadBlocking's callers. */ + int reader = keyboardOpenReader(); + if (reader == consoleReaderInvalid) { + kprintf("[shell] console reader table full, shell has no input\n"); + for (;;) + halt(); + } + kprintf("\nfrostbyte shell ready. type 'help'.\n"); kprintf("frostbyte> "); for (;;) { - char c = keyboardReadChar(); + char c; + keyboardReaderReadBlocking(reader, &c, 1); if (c == '\n') { kputchar('\n'); line[length] = 0; execute(line); length = 0; + reapFinishedBackgroundJobs(); kprintf("frostbyte> "); } else if (c == '\b') { if (length > 0) { diff --git a/src/sigTrampoline.S b/src/sigTrampoline.S new file mode 100644 index 0000000..c46b899 --- /dev/null +++ b/src/sigTrampoline.S @@ -0,0 +1,41 @@ +#include "syscall.h" + +/* + * Shared signal-delivery trampoline (see signal.h/signal.c). This is NOT + * entered like a normal function call from C -- signal.c's + * sigConsumePending rewrites a thread's own saved trapframe so that + * iretq lands ring 3 execution directly at sigTrampolineStart, with %rax + * already holding the handler's address and %rdi already holding the + * signal number (the handler's sole SysV argument), exactly as if the + * kernel itself had performed `call *%rax` with that argument set up. + * + * Position independent by construction: every instruction here is either + * PC-relative (the final backward jmp) or references no address at all + * (call through a register, an immediate syscall number, int $0x80) -- so + * sigInstallTrampoline can copy these bytes to any physical frame and map + * that frame at any virtual address (sigTrampolineVirtualAddr) and it + * still runs correctly, unlike this same code linked and executed in + * place inside the kernel's own (very different) virtual address range. + */ +.section .text +.code64 + +.global sigTrampolineStart +.global sigTrampolineEnd + +sigTrampolineStart: + call *%rax + movl $sysSigReturn, %eax + int $0x80 + +/* Only reached if sysSigReturn somehow returned instead of restoring the + pre-signal context and resuming it (a kernel bug, not a state a correct + sysOnSigReturn can produce) -- park here rather than run off into + whatever garbage happens to follow this page. */ +sigTrampolineHang: + hlt + jmp sigTrampolineHang + +sigTrampolineEnd: + +.section .note.GNU-stack, "", @progbits