From 0bdc393f39d225849fcaa9f285481acc337e9ce5 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 19:11:57 +0200 Subject: [PATCH] feat: add jobctl --- include/jobctl.h | 148 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 include/jobctl.h diff --git a/include/jobctl.h b/include/jobctl.h new file mode 100644 index 0000000..025100b --- /dev/null +++ b/include/jobctl.h @@ -0,0 +1,148 @@ +#ifndef FROSTBYTE_JOBCTL_H +#define FROSTBYTE_JOBCTL_H + +#include "types.h" + +/* + * Generic "run a binary by path, with real job control" helpers, callable + * only from plain kernel-thread context (shell.c's shellRun, or a boot-time + * demo thread in kmain.c) -- never from inside a syscall's interrupt-gate + * handler, exactly like keyboardReaderReadBlocking/pipeReadBlocking's own + * restriction, since jobctlWaitForeground below parks with threadJoin's + * real halt() loop. + * + * Unlike syscall.c's doSpawn (sysSpawn's underlying implementation, used by + * one ring-3 process to spawn another), a job started here always becomes + * its OWN new process group leader (threadSetPgid(pid, pid)) regardless of + * the caller's own pgid -- matching a real shell's fork()+setpgid(child, + * child) convention for every new job/pipeline, not doSpawn's default of + * inheriting the parent's group (right for a process spawning a plain + * worker of its own, wrong for an interactive shell launching a new + * foreground/background command). argv is already kernel memory here (the + * caller IS the kernel, not a ring-3 process going through a syscall), so + * neither function copies or range-checks it the way copyUserArgv/ + * userRangeOk (syscall.c) must for a user-supplied pointer. + */ + +/* Loads `path` and spawns it as a new process group leader, hands it "the + terminal" (keyboardSetForegroundPgid, keyboard.h) immediately, and + returns its pid without waiting -- split from the actual wait + (jobctlWaitForeground below) so a caller can learn the pid the instant + the job exists, not only after it has already exited (shell.c's own + background job table needs the same immediacy from + jobctlRunBackground below, so this mirrors it instead of hiding the pid + inside a single combined call). Returns threadInvalidId, touching the + terminal not at all, if `path` cannot be loaded (missing from the VFS, + malformed ELF, or the thread table is full). */ +uint32 jobctlSpawnForeground(const char* path, const char* const* argv, int argc); + +/* Blocks (threadJoin) until the pid jobctlSpawnForeground just returned + exits, then reclaims the terminal for the CALLING thread's own group -- + deliberately not "whatever pgid owned the terminal right before + jobctlSpawnForeground ran" (which could itself be some earlier, now-dead + job's stale id): a real shell always gets its own prompt's terminal back + when a foreground job quits, so a later Ctrl-C typed at the prompt + reaches the shell's NEXT job, not a group nothing will ever answer for + again. Returns the job's real exit code, same contract as threadJoin + itself. */ +int64 jobctlWaitForeground(uint32 pid); + +/* Loads `path` and spawns it as a new process group leader exactly like + jobctlSpawnForeground, but never touches the foreground pgid at all and + returns the new pid immediately without waiting -- a real shell's `&` + suffix. The caller is responsible for eventually reaping it (e.g. + shell.c's own background job table, reaped opportunistically via + threadJoinAnyChild). Returns false (touching *outPid not at all) on the + same load/spawn failures as jobctlSpawnForeground. */ +bool jobctlRunBackground(const char* path, const char* const* argv, int argc, + uint32* outPid); + +/* Generalizes jobctlSpawnForeground/jobctlRunBackground's shared "load and + spawn" step for shell.c's new `cmd1 | cmd2` pipeline syntax: `stdinHandle`/ + `stdoutHandle` are raw pipe.h handles from vfsPipeCreateHandle (vfs.h), + baked into the new process's own fd 0/fd 1 (vfsPipeAttachInto) BEFORE it + ever runs a single instruction -- -1 for either one leaves that fd at its + normal default (the real console), exactly like an unpiped command's own + stdin/stdout. `joinPgid` is threadInvalidId for "become a new group + leader" (jobctlSpawn's existing default, what every other caller here + still wants) or an already-live pgid to join instead -- a pipeline's + second-and-later stage joins its first stage's own new group so a single + Ctrl-C reaches every stage at once, exactly like a real shell's pipeline + job. Already fully general for a MIDDLE stage of a longer pipeline too: + `stdinHandle`/`stdoutHandle` are independent, so a stage can have both a + piped stdin AND a piped stdout at once (shell.c's runPipelineStages does + exactly this for every interior stage of a 3+-stage pipeline) -- the only + things that ever needed generalizing for that were shell.c's own + parsing/orchestration and jobctlWaitPipelineStages below, never this + function. Never touches the terminal itself: shell.c's own pipeline + runner grants and reclaims it once, for the pipeline as a whole, via + jobctlWaitPipelineStages below -- not per stage. Returns threadInvalidId + on the same load/spawn failures as jobctlSpawnForeground. */ +uint32 jobctlSpawnPiped(const char* path, const char* const* argv, int argc, + int stdinHandle, int stdoutHandle, uint32 joinPgid); + +/* Blocks until every one of `pids[0..count-1]` (jobctlSpawnPiped's own + return values for a pipeline's stages, `pids[1..count-1]` already joined + to `pids[0]`'s group) has exited, then reclaims the terminal for the + CALLING thread's own group -- the same two-step contract + jobctlWaitForeground has for a single job, just for a whole N-stage + pipeline at once (2 <= count <= shellMaxPipelineStages, shell.h). Grants + the terminal to `pids[0]`'s group (which every other pid is already a + member of) before waiting, exactly like jobctlSpawnForeground already + does for a single job, so a Ctrl-C typed while the pipeline runs reaches + every stage. threadJoin's own real blocking semantics mean joining every + pid in this fixed order is correct regardless of which stage actually + exits first. Returns the LAST stage's exit code, matching a real + pipeline's own `$?` convention (the last command's status). */ +int64 jobctlWaitPipelineStages(const uint32* pids, int count); + +/* Installs `inPath`/`outPath`/`errPath` file redirections (vfsOpenInto, + vfs.h) into an ALREADY-SPAWNED child's fd 0/fd 1/fd 2 -- shell.c's new + `cmd [< IN] [> OUT | >> OUT] [2> ERR | 2>> ERR]` syntax. Any path may be + nullptr to leave that fd untouched (still whatever jobctlSpawn/ + jobctlSpawnPiped already gave it -- the real console by default, or a + pipe end for a pipeline stage). `outAppend`/`errAppend` select `>>`/ + `2>>` (oAppend, never truncates) over `>`/`2>` (oTrunc) when the + matching path is non-null; ignored otherwise. fd 0's default console + reader is released first, exactly like jobctlSpawnPiped's own stdin + override above, so redirecting it never leaks that reader slot; fd 1 + and fd 2 have no such resource (their own default, fdConsole, owns + nothing -- neither is ever handed a reader by vfsFdInit). Exposed as + its own function (rather than folded into jobctlSpawnPiped's already- + long parameter list) so shell.c's `runPipeline` can layer a file + redirect onto exactly one end of a pipeline stage -- the other end + already wired to the shared pipe by jobctlSpawnPiped -- without a + combinatorial explosion of pipe-handle vs. path parameters on one call. + Returns false, leaving the child alive and running with whatever fds it + already had, on a bad path (missing without oCreate's implicit + create-on-write, unwritable parent, a fifo opened with the wrong mode, + exhausted pipe/inode resources) -- the caller decides whether that + warrants killing it (every caller in this kernel currently does, via + threadKill+threadJoin, exactly like runPipeline already does for a + failed second-stage spawn). */ +bool jobctlApplyRedirects(uint32 pid, const char* inPath, const char* outPath, bool outAppend, + const char* errPath, bool errAppend); + +/* Same as jobctlSpawnForeground, but installs `inPath`/`outPath`/`errPath` + file redirections (jobctlApplyRedirects above) into the child's fd 0/ + fd 1/fd 2 BEFORE the terminal is ever granted to it -- shell.c's + `NAME [ARGS] [< IN] [> OUT | >> OUT] [2> ERR | 2>> ERR]`. On a redirect + failure the half-spawned child is killed and reaped before returning + threadInvalidId, exactly as if the load/spawn itself had failed: the + terminal must never be granted to a child whose own stdio could not be + wired up as asked. */ +uint32 jobctlSpawnForegroundRedirected(const char* path, const char* const* argv, int argc, + const char* inPath, const char* outPath, bool outAppend, + const char* errPath, bool errAppend); + +/* Same as jobctlRunBackground, but with the same file-redirection support + as jobctlSpawnForegroundRedirected above -- shell.c's + `NAME [ARGS] [< IN] [> OUT | >> OUT] [2> ERR | 2>> ERR] &`. Returns + false (touching *outPid not at all) on the same load/spawn failures as + jobctlRunBackground, or on a redirect failure (the half-spawned child is + killed and reaped first, same as the foreground case). */ +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); +