feat: extend shell with BackgroundJob and shellLastSpawnedPid

This commit is contained in:
dualfroz committed 2026-09-07 18:12:59 +02:00
1 parent e03c0d5f7e
commit 96148ff0e7
1 file changed
+179 -1
+179 -1
View File
@@ -1,6 +1,7 @@
#include "shell.h"
#include "console.h"
#include "keyboard.h"
#include "ports.h"
#include "vga.h"
#include "pmm.h"
#include "heap.h"
@@ -9,11 +10,188 @@
#include "string.h"
#include "vfs.h"
#include "fs.h"
#include "jobctl.h"
#include "klib.h"
#include "pipe.h"
#include "rtc.h"
#define lineCapacity 128
#define pathCapacity 256
#define binPathCapacity 272
static char cwd[pathCapacity] = "/";
/* Real shell "command could not even be started" exit status (bad
redirection, wildcard overflow, unknown binary, ...) -- the same 127
real shells report for "command not found", reused here for every
pre-spawn failure in runProgram/runPipelineStages so shell.c's new
`&&`/`||` chaining (execute()) always sees a nonzero, "this failed"
status for them, exactly like a real spawned process's own nonzero
exit code would. */
#define shellExecNotFound 127
/* The shell used to own the only "current directory" concept in this whole
kernel, a single static local shared by nothing else -- sched.h's new
Thread.cwd (per-thread, real chdir(2)/getcwd(2) semantics, vfs.h's
vfsChdir/vfsGetcwd/vfsResolvePath) replaces it: the shell is a plain
kernel thread like any other (see shellRun's own comment below), so it
simply becomes one more caller of the same real subsystem instead of
keeping a second, disconnected notion of "cwd" in parallel. This also
means a job this shell spawns now genuinely inherits whatever directory
the shell itself is currently in (initialCwd, sched.c) -- true before
this fala only by accident of both defaulting to "/", never actually
wired together. */
/*
* Real job control for the interactive shell (jobctl.h does the actual
* spawning/terminal-ownership work): any command line that is not one of
* the built-ins above is tried as /bin/<word> instead of just being
* rejected outright, with the rest of the line tokenized into a real argv
* exactly like a normal shell does. A trailing lone "&" token backgrounds
* it (jobctlRunBackground, no terminal, returns immediately) instead of
* running it in the foreground (jobctlSpawnForeground + jobctlWaitForeground,
* which blocks until it exits with the terminal genuinely handed to it --
* see jobctl.c).
*/
#define maxBackgroundJobs 8
typedef struct BackgroundJob {
uint32 pid;
char name[32];
bool used;
} BackgroundJob;
static BackgroundJob backgroundJobs[maxBackgroundJobs];
static uint32 lastSpawnedPid = threadInvalidId;
/* Real shell `$?` -- the exit status of the most recently completed
command-or-pipeline-or-builtin, persisting across SEPARATELY typed
lines (unlike the `&&`/`||` chaining `status` local in execute() below,
which only ever lives for the duration of one typed line's own
segments). Initialized to 0, matching a real shell's own `$?` before
anything has ever run. Updated in exactly one place (execute(), right
after every executeSingle() call, chained or not) and read in exactly
one place (shellExpandToken, via shellIsStatusVarToken) -- the same
"shell is a plain kernel thread, needs no syscall to reach its own
state" shape doExport/doUnset/changeDir already use for their own
thread-local state, just kept in a file-scope static here since $? is
a shell-language special parameter, not a real environment variable
(real shells never let you `export $?` either). */
static int64 lastStatus = 0;
uint32 shellLastSpawnedPid(void)
{
return lastSpawnedPid;
}
/* Opportunistically reaps any of THIS shell's own children that have
already exited (threadJoinAnyChild, sched.h -- the same non-blocking
waitpid(-1)-shaped scan user/waitanytest.c already proves) and prints
what finished. Only ever finds background jobs here: a foreground job is
already fully joined-and-reaped by jobctlWaitForeground itself before
that call ever returns, so by the time control comes back to this
function there is nothing left over from it to double-reap. */
static void reapFinishedBackgroundJobs(void)
{
uint32 deadId;
int64 code;
while (threadJoinAnyChild(currentThreadId(), &deadId, &code) == waitScanFoundDead) {
for (int i = 0; i < maxBackgroundJobs; i++) {
if (backgroundJobs[i].used && backgroundJobs[i].pid == deadId) {
kprintf("[%u] %s done, exit code %ld\n", deadId, backgroundJobs[i].name, code);
backgroundJobs[i].used = false;
break;
}
}
}
}
static void listBackgroundJobs(void)
{
bool any = false;
for (int i = 0; i < maxBackgroundJobs; i++) {
if (backgroundJobs[i].used) {
kprintf("[%u] running %s\n", backgroundJobs[i].pid, backgroundJobs[i].name);
any = true;
}
}
if (!any)
kprintf("no background jobs\n");
}
int shellTokenize(char* text, char* tokens[], int maxTokens)
{
int count = 0;
char* cursor = text;
while (*cursor && count < maxTokens) {
while (*cursor == ' ')
cursor++;
if (*cursor == 0)
break;
tokens[count++] = cursor;
while (*cursor && *cursor != ' ')
cursor++;
if (*cursor == ' ') {
*cursor = 0;
cursor++;
}
}
return count;
}
int shellSplitPipelineStages(char* line, char* stages[], int maxStages)
{
/* First pass: count top-level '|' occurrences without mutating
anything, so a too-many-stages line can be rejected with `line`
completely untouched -- the same "validate before mutating" shape
shellExtractRedirects already uses for its own dangling-operator
check. */
int pipeCount = 0;
for (const char* cursor = line; *cursor; cursor++)
if (*cursor == '|')
pipeCount++;
if (pipeCount == 0)
return 0;
if (pipeCount + 1 > maxStages)
return -1;
int count = 0;
char* stageStart = line;
char* cursor = line;
while (true) {
bool atPipe = (*cursor == '|');
bool atEnd = (*cursor == 0);
if (atPipe || atEnd) {
char* stageEnd = cursor;
while (stageEnd > stageStart && stageEnd[-1] == ' ')
stageEnd--;
*stageEnd = 0;
stages[count++] = stageStart;
if (atEnd)
break;
*cursor = 0;
stageStart = cursor + 1;
while (*stageStart == ' ')
stageStart++;
}
cursor++;
}
return count;
}
int shellSplitAndOr(char* line, char* segments[], ShellAndOrOp andOrOps[], int maxSegments)
{
/* Same "count first, mutate nothing, reject up front" shape
shellSplitPipelineStages uses for its own analogous limit --
matching two-character operators one at a time (never overlapping:
stepping by 2 on a match means "&&&" is one "&&" followed by a lone
'&' left inside a segment, not two overlapping matches). */
int opCount = 0;
for (const char* cursor = line; *cursor; ) {
if ((cursor[0] == '&' && cursor[1] == '&') || (cursor[0] == '|' && cursor[1] == '|')) {
static void printHelp(void)
{