From ff36cc027599b1e2baee1adbe158824b306a17b5 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 8 Sep 2026 12:23:41 +0200 Subject: [PATCH] feat: extend shell with threadStateName --- src/shell.c | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 2 deletions(-) diff --git a/src/shell.c b/src/shell.c index eb7cadf..e0ea5e0 100644 --- a/src/shell.c +++ b/src/shell.c @@ -711,6 +711,163 @@ static void splitCommandWord(char* text, char** word, char** rest) never even got every stage spawned) -- same "feeds shell.c's new `&&`/`||` chaining" reasoning as runProgram's own return above. */ static int64 runPipelineStages(char* stages[], int count) +{ + char* word[shellMaxPipelineStages]; + char* argv[shellMaxPipelineStages][shellMaxArgs]; + int argc[shellMaxPipelineStages]; + ShellRedirect redirect[shellMaxPipelineStages]; + /* Per-stage expansion scratch for shellExpandArgv, same "argv[0] is + never expanded" reasoning as runProgram's own expandedArgv -- each + stage's actual binary comes from word[i], never from argv[i][0]. */ + char expanded[shellMaxPipelineStages][shellMaxArgs][envEntryMax]; + /* Per-stage wildcard expansion scratch (shellExpandGlobs) -- a + dedicated, smaller shellGlobPathCapacity buffer rather than reusing + pathCapacity, since this array is already multiplied by + shellMaxPipelineStages on top of shellMaxArgs and shares a single + 16KiB kernel-thread stack (sched.c's threadStackSize) with every + other local array in this function -- see shellExpandGlobs' own doc + comment in shell.c for the full sizing reasoning. */ + char globStorage[shellMaxPipelineStages][shellMaxArgs][shellGlobPathCapacity]; + /* Per-stage redirect-target expansion scratch (shellExpandRedirect) -- + same reasoning as runProgram's own redirectStorage, one dedicated + 4-slot array per stage since a stage's own inPath/outPath/errPath/ + hereString could all need expanding on the same line at once. */ + char redirectStorage[shellMaxPipelineStages][4][envEntryMax]; + + for (int i = 0; i < count; i++) { + char* rest; + splitCommandWord(stages[i], &word[i], &rest); + argc[i] = 0; + argv[i][argc[i]++] = word[i]; + argc[i] += shellTokenize(rest, &argv[i][argc[i]], shellMaxArgs - argc[i]); + shellExpandArgv(&argv[i][1], argc[i] - 1, &expanded[i][1]); + + if (!shellExtractRedirects(argv[i], &argc[i], &redirect[i])) { + kprintf("%s: redirection operator with no filename\n", word[i]); + return shellExecNotFound; + } + shellExpandRedirect(&redirect[i], redirectStorage[i]); + + /* Same "expand real arguments last, after redirects" ordering as + runProgram's own single-command path above. */ + int expandedArgc = shellExpandGlobs(&argv[i][1], argc[i] - 1, shellMaxArgs - 1, globStorage[i]); + if (expandedArgc < 0) { + kprintf("%s: too many arguments after wildcard expansion\n", word[i]); + return shellExecNotFound; + } + argc[i] = 1 + expandedArgc; + } + + /* Only the two OPEN ends of the pipeline may be file/here-string + redirected for stdin/stdout -- stage 0's stdin and the LAST stage's + stdout -- exactly like this function's own header comment already + scopes "no stage may be a builtin" for a similar reason. Every + interior fd is already wired to a real pipe on both sides, so those + combinations are rejected outright below rather than silently + guessing which of "the pipe" or "the file" should win. fd 2 + (stderr) is never part of any pipe, so `2>`/`2>>` are always allowed + on every stage independently, with no such restriction. */ + for (int i = 0; i < count; i++) { + if (i > 0 && redirect[i].inPath) { + kprintf("pipeline: '%s' already reads from the pipe, input redirection is only " + "supported on the first stage\n", word[i]); + return shellExecNotFound; + } + if (i > 0 && redirect[i].hereString) { + kprintf("pipeline: '%s' already reads from the pipe, here-string redirection is " + "only supported on the first stage\n", word[i]); + return shellExecNotFound; + } + if (i < count - 1 && redirect[i].outPath) { + kprintf("pipeline: '%s' already writes to the pipe, output redirection is only " + "supported on the last stage\n", word[i]); + return shellExecNotFound; + } + } + + uint32 pids[shellMaxPipelineStages]; + /* pipeHandle[i] connects stage i's stdout to stage i+1's stdin -- only + count-1 links exist for count stages. */ + int pipeHandle[shellMaxPipelineStages - 1]; + + for (int i = 0; i < count; i++) { + int stdinHandle = (i == 0) ? -1 : pipeHandle[i - 1]; + int stdoutHandle = -1; + if (i < count - 1) { + pipeHandle[i] = vfsPipeCreateHandle(); + if (pipeHandle[i] < 0) { + kprintf("pipeline: no pipe slots free\n"); + for (int k = 0; k < i; k++) { + threadKill(pids[k]); + threadJoin(pids[k]); + } + return shellExecNotFound; + } + stdoutHandle = pipeHandle[i]; + } + + /* joinPgid: every stage after the first joins stage 0's own group + (jobctlSpawnPiped's own doc comment) so a single Ctrl-C, or this + function's own eventual keyboardSetForegroundPgid via + jobctlWaitPipelineStages, reaches every stage at once. */ + uint32 joinPgid = (i == 0) ? threadInvalidId : pids[0]; + + char binPath[binPathCapacity]; + snprintf(binPath, sizeof(binPath), "/bin/%s", word[i]); + + /* A here-string is only ever possible on stage 0 (validated above) + -- jobctlSpawnPipedHereString wires fd 0 to a real pipe the + SHELL itself writes the text into, rather than leaving it at the + console, then passes stdoutHandle/joinPgid straight through + exactly like the plain jobctlSpawnPiped every other stage + uses. */ + uint32 pid = (i == 0 && redirect[i].hereString) + ? jobctlSpawnPipedHereString(binPath, (const char* const*)argv[i], argc[i], + redirect[i].hereString, stdoutHandle, joinPgid) + : jobctlSpawnPiped(binPath, (const char* const*)argv[i], argc[i], + stdinHandle, stdoutHandle, joinPgid); + + if (pid == threadInvalidId) { + kprintf("unknown command: %s (try 'help')\n", word[i]); + if (i < count - 1) + pipeDestroyIfUnattached(pipeHandle[i]); + for (int k = 0; k < i; k++) { + threadKill(pids[k]); + threadJoin(pids[k]); + } + return shellExecNotFound; + } + pids[i] = pid; + + const char* inPath = (i == 0) ? redirect[i].inPath : nullptr; + const char* outPath = (i == count - 1) ? redirect[i].outPath : nullptr; + bool appendOut = (i == count - 1) ? redirect[i].appendOut : false; + if ((inPath || outPath || redirect[i].errPath) && + !jobctlApplyRedirects(pid, inPath, outPath, appendOut, + redirect[i].errPath, redirect[i].appendErr)) { + kprintf("%s: cannot open %s\n", word[i], + inPath ? inPath : (outPath ? outPath : redirect[i].errPath)); + for (int k = 0; k <= i; k++) { + threadKill(pids[k]); + threadJoin(pids[k]); + } + return shellExecNotFound; + } + } + + lastSpawnedPid = pids[count - 1]; + int64 exitCode = jobctlWaitPipelineStages(pids, count); + + char summary[lineCapacity]; + int pos = 0; + for (int i = 0; i < count; i++) { + if (i > 0) + pos += snprintf(summary + pos, sizeof(summary) - pos, " | "); + pos += snprintf(summary + pos, sizeof(summary) - pos, "%s", word[i]); + } + kprintf("%s exited with code %ld\n", summary, exitCode); + return exitCode; +} static void printHelp(void) { @@ -718,15 +875,36 @@ static void printHelp(void) kprintf(" help show this list\n"); kprintf(" echo TEXT print the given text\n"); kprintf(" echo TEXT >F write the text to file F\n"); - kprintf(" ls [PATH] list a directory\n"); + kprintf(" ls [-l] [PATH] list a directory (tmpfs or /mnt), -l adds type/size/mtime\n"); kprintf(" cat FILE print a file\n"); kprintf(" mkdir PATH create a directory (writable mounts only)\n"); + kprintf(" rm PATH remove a file (refuses a directory)\n"); + kprintf(" rmdir PATH remove an empty directory\n"); + kprintf(" chmod MODE PATH set owner/group/other rwx bits (octal, e.g. 644 or 750)\n"); + kprintf(" chgrp GID PATH set group (decimal); owner may only set its own gid\n"); kprintf(" cd PATH change the working directory\n"); kprintf(" pwd print the working directory\n"); + kprintf(" whoami print this shell's own uid (0 = root)\n"); kprintf(" mem physical memory and heap usage\n"); kprintf(" uptime ticks and seconds since boot\n"); kprintf(" ps list kernel threads\n"); + kprintf(" jobs list running background jobs\n"); + kprintf(" export N=V set environment variable N to V (inherited by children)\n"); + kprintf(" unset N remove environment variable N\n"); + kprintf(" env list this shell's environment variables\n"); kprintf(" clear clear the screen\n"); + kprintf(" $NAME (in a spawned program's arguments) expands to that variable\n"); + kprintf(" $? (same place) expands to the last command's exit status\n"); + kprintf(" NAME dir/*.ext (glob) expands to every real matching filename in dir\n"); + kprintf(" NAME [ARGS] run /bin/NAME in the foreground, waits for it to exit\n"); + kprintf(" NAME [ARGS] & run /bin/NAME in the background, prints its pid\n"); + kprintf(" NAME < IN redirect NAME's stdin from file IN\n"); + kprintf(" NAME <<< TEXT feed TEXT (plus a newline) to NAME's stdin\n"); + kprintf(" NAME > OUT redirect NAME's stdout to file OUT (create/truncate)\n"); + kprintf(" NAME >> OUT redirect NAME's stdout to file OUT (create/append)\n"); + kprintf(" NAME 2> ERR redirect NAME's stderr to file ERR (create/truncate)\n"); + kprintf(" NAME 2>> ERR redirect NAME's stderr to file ERR (create/append)\n"); + kprintf(" A [ARGS] | B [ARGS] pipe A's stdout into B's stdin, waits for both\n"); } static void printMem(void) @@ -751,12 +929,26 @@ static void printUptime(void) ticks, ticks / frequency, frequency); } +static const char* threadStateName(ThreadState state) +{ + switch (state) { + case threadUnused: return "unused"; + case threadRunnable: return "runnable"; + case threadRunning: return "running"; + case threadSleeping: return "sleeping"; + case threadDead: return "dead"; + case threadBlocked: return "blocked"; + case threadStopped: return "stopped"; + default: return "?"; + } +} + static void printThreads(void) { kprintf("id state name\n"); for (uint32 i = 0; i < threadCount(); i++) { Thread* thread = threadAt(i); - const char* state = thread->state == threadRunning ? "running" : "runnable"; + const char* state = threadStateName(thread->state); char marker = thread->id == currentThreadId() ? '*' : ' '; kprintf("%c%u %s %s\n", marker, thread->id, state, thread->name); }