From 39528a6ce60bfb560b2b17a827dd78b9f9359b7f Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 12:21:25 +0200 Subject: [PATCH] feat: add jobctlRunBackground and jobctlApplyRedirects to jobctl --- src/jobctl.c | 156 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/jobctl.c b/src/jobctl.c index a330be2..5a2e761 100644 --- a/src/jobctl.c +++ b/src/jobctl.c @@ -159,3 +159,159 @@ int64 jobctlWaitPipelineStages(const uint32* pids, int count) already does for a single job. */ keyboardSetForegroundPgid(pids[0]); + /* threadJoin blocks on a SPECIFIC pid regardless of which stage + actually exits first -- joining every one, in this fixed order, + reaps all of them correctly no matter the real completion order, the + same way a real shell's own wait() for each of a pipeline's known + children would. */ + int64 lastCode = 0; + for (int i = 0; i < count; i++) + lastCode = threadJoin(pids[i]); + + keyboardSetForegroundPgid(callerPgid); + + return lastCode; +} + +bool jobctlRunBackground(const char* path, const char* const* argv, int argc, + uint32* outPid) +{ + uint32 pid = jobctlSpawnPiped(path, argv, argc, -1, -1, threadInvalidId); + if (pid == threadInvalidId) + return false; + + /* Deliberately no keyboardSetForegroundPgid call at all -- a + backgrounded job never owns the terminal, so a Ctrl-C typed while it + runs reaches whatever WAS already in the foreground instead (the + shell itself, if nothing else is running in the foreground), never + this job. */ + *outPid = pid; + return true; +} + +bool jobctlApplyRedirects(uint32 pid, const char* inPath, const char* outPath, bool outAppend, + const char* errPath, bool errAppend) +{ + FileDescriptor* fds = threadAt(pid)->fds; + + if (inPath) { + /* Same "release the default console reader before overwriting fd 0" + step jobctlSpawnPiped already does for a piped stdin above -- + keyboardCloseReader is a safe no-op on an already-closed/invalid + handle (readerAt's own bounds+used check, keyboard.c), so this + is correct even if fd 0 already holds something else entirely + (e.g. a pipe end from an earlier jobctlSpawnPiped call on this + same pid, in runPipeline's left-side case below). */ + keyboardCloseReader(fds[0].pipeHandle); + if (vfsOpenInto(fds, 0, inPath, oReadOnly) < 0) + return false; + } + + if (outPath) { + uint32 flags = oCreate | oWriteOnly | (outAppend ? oAppend : oTrunc); + if (vfsOpenInto(fds, 1, outPath, flags) < 0) + return false; + } + + if (errPath) { + uint32 flags = oCreate | oWriteOnly | (errAppend ? oAppend : oTrunc); + if (vfsOpenInto(fds, 2, errPath, flags) < 0) + return false; + } + + return true; +} + +uint32 jobctlSpawnForegroundRedirected(const char* path, const char* const* argv, int argc, + const char* inPath, const char* outPath, bool outAppend, + const char* errPath, bool errAppend) +{ + uint32 pid = jobctlSpawnPiped(path, argv, argc, -1, -1, threadInvalidId); + if (pid == threadInvalidId) + return threadInvalidId; + + if (!jobctlApplyRedirects(pid, inPath, outPath, outAppend, errPath, errAppend)) { + threadKill(pid); + threadJoin(pid); + return threadInvalidId; + } + + keyboardSetForegroundPgid(pid); + return pid; +} + +bool jobctlRunBackgroundRedirected(const char* path, const char* const* argv, int argc, + const char* inPath, const char* outPath, bool outAppend, + const char* errPath, bool errAppend, + uint32* outPid) +{ + uint32 pid = jobctlSpawnPiped(path, argv, argc, -1, -1, threadInvalidId); + if (pid == threadInvalidId) + return false; + + if (!jobctlApplyRedirects(pid, inPath, outPath, outAppend, errPath, errAppend)) { + threadKill(pid); + threadJoin(pid); + return false; + } + + *outPid = pid; + return true; +} + +/* Generalizes jobctlSpawnPiped's own fd-baking-before-first-instruction + machinery to also here-string-feed stdin: spawns `path` with fd 0 wired + to a brand-new pipe AND fd 1/pgid wired exactly as `stdoutHandle`/ + `joinPgid` ask (passed straight through to jobctlSpawnPiped, unlike the + fixed -1/threadInvalidId every earlier here-string caller in this file + used), then feeds `text` plus a trailing newline into the new pipe's + write end and closes it -- all BEFORE returning, so the child can never + observe the write end still open. Ordering matters: the write end is + only added/used/closed AFTER jobctlSpawnPiped has already attached the + read end (pipeAddReader, via vfsPipeAttachInto in vfs.c) -- pipe.c's + pipeReleaseIfUnused only frees a pipe's slot once BOTH readers and + writers reach 0, so closing the write end while the read end is still + attached (readers==1) leaves the pipe, and the data already sitting in + its ring buffer, alive for the child to actually read; closing it + BEFORE the read end attached (readers==0) would instead free the whole + slot out from under the still-buffered payload the instant this + function's own pipeCloseWrite ran, handing the child a dangling handle. + `text`'s own length plus one newline byte is always far under + pipeBufferSize (256, pipe.h) for any line this shell's own lineCapacity + (128, shell.c) could ever have typed it from, so the non-blocking + pipeWrite below can never see "buffer full" and needs no retry loop the + way a general-purpose writer would. Returns threadInvalidId, having + spawned and written nothing, on the same load/spawn failures as + jobctlSpawnPiped, or if the pipe table itself is exhausted + (vfsPipeCreateHandle). */ +uint32 jobctlSpawnPipedHereString(const char* path, const char* const* argv, int argc, + const char* text, int stdoutHandle, uint32 joinPgid) +{ + int handle = vfsPipeCreateHandle(); + if (handle < 0) + return threadInvalidId; + + uint32 pid = jobctlSpawnPiped(path, argv, argc, handle, stdoutHandle, joinPgid); + if (pid == threadInvalidId) { + pipeDestroyIfUnattached(handle); + return threadInvalidId; + } + + pipeAddWriter(handle); + uint64 length = strlen(text); + if (length > 0) + pipeWrite(handle, text, length); + pipeWrite(handle, "\n", 1); + pipeCloseWrite(handle); + + return pid; +} + +uint32 jobctlSpawnForegroundHereString(const char* path, const char* const* argv, int argc, + const char* text, const char* outPath, bool outAppend, + const char* errPath, bool errAppend) +{ + uint32 pid = jobctlSpawnPipedHereString(path, argv, argc, text, -1, threadInvalidId); + if (pid == threadInvalidId) + return threadInvalidId; +