feat: add jobctlRunBackground and jobctlApplyRedirects to jobctl

This commit is contained in:
dualfroz committed 2026-09-04 12:21:25 +02:00
1 parent 83421e7dba
commit 39528a6ce6
1 file changed
+156
+156
View File
@@ -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;