diff --git a/src/jobctl.c b/src/jobctl.c new file mode 100644 index 0000000..a330be2 --- /dev/null +++ b/src/jobctl.c @@ -0,0 +1,161 @@ +#include "jobctl.h" +#include "sched.h" +#include "vfs.h" +#include "fs.h" +#include "elf.h" +#include "paging.h" +#include "keyboard.h" +#include "pipe.h" +#include "string.h" + +/* Matches kmain.c's userStackSize and syscall.c's execStackSize -- all + three name the exact same "one slot's worth of stack" constant (elf.h's + elfSlotStackOffset leaves 0x8000000 bytes below the stack top before the + next thing in the slot, brk, starts; nothing in this kernel has ever + needed anywhere near that much real stack). Kept as jobctl.c's own copy + rather than sharing a header-level define with the other two call sites, + the same "each caller of this pattern owns its own named copy" choice + kmain.c and syscall.c already made independently of each other. */ +#define jobctlStackSize 0x4000 + +/* Shared by both functions below: resolves `path` through the VFS, loads + it at its own fixed ELF-linked slot (elfLoad picks the address, same as + syscall.c's loadImageByPath), and spawns it as a brand new process group + leader -- the one behavioral difference from doSpawn (syscall.c), which + leaves a freshly spawned child in its parent's existing group instead. + Returns threadInvalidId, having created nothing, on any failure (missing + binary, no execute permission, bad ELF, stack mapping failure, or a full + thread table). */ +static uint32 jobctlSpawn(const char* path, const char* const* argv, int argc) +{ + VfsNode* node = vfsLookup(path); + if (!node) + return threadInvalidId; + /* Real EACCES-shaped refusal, same check and same reasoning as + syscall.c's loadImageByPath (sysExec/sysSpawn's own parallel loading + path) -- a file the CALLING thread's identity is not permitted to + exec (vfsPermitted, owner-vs-other-vs-root) can never be spawned this + way either. Collapsed into the same undifferentiated threadInvalidId + failure as every other reason jobctlSpawn already refuses (missing + binary, bad ELF, ...), matching this function's own existing doc + comment. */ + if (!vfsPermitted(vfsMode(node), vfsUid(node), vfsGid(node), currentThreadUid(), currentThreadGid(), fsModeOwnerExec)) + return threadInvalidId; + + const void* image = vfsFileData(node); + uint64 size = vfsSize(node); + uint64 entry = elfLoad(image, size); + if (entry == 0) + return threadInvalidId; + + uint64 stackTop = elfSlotStackTop(entry); + if (!pagingMapUserRangeFlags(stackTop - jobctlStackSize, jobctlStackSize, pageNoExecute)) + return threadInvalidId; + + uint64 initialRsp; + if (!elfBuildInitialStack(stackTop, jobctlStackSize, argv, argc, &initialRsp)) + return threadInvalidId; + + uint32 pid = threadCreateUser(path, entry, initialRsp); + if (pid == threadInvalidId) + return threadInvalidId; + + threadSetBrk(pid, elfSlotBrkStart(entry)); + + /* A real shell's fork()+setpgid(child, child): every job started + through jobctl gets its own fresh group regardless of the caller's + own pgid, so a later Ctrl-C aimed at THIS job (via + keyboardSetForegroundPgid below) can never also reach some earlier, + unrelated job that happened to share the caller's group. */ + threadSetPgid(pid, pid); + return pid; +} + +uint32 jobctlSpawnPiped(const char* path, const char* const* argv, int argc, + int stdinHandle, int stdoutHandle, uint32 joinPgid) +{ + uint32 pid = jobctlSpawn(path, argv, argc); + if (pid == threadInvalidId) + return threadInvalidId; + + /* jobctlSpawn above already made this its own new group leader -- + overridden here, after the fact, only for a pipeline's second-and- + later stage, which needs to join the FIRST stage's group instead so + the two are scoped together for signals (threadKillGroup/ + sigRaiseGroup, sched.h/signal.h) exactly like a real shell's + fork()+setpgid(child, leaderPid) for every stage after the first. */ + if (joinPgid != threadInvalidId) + threadSetPgid(pid, joinPgid); + + /* Bakes the requested pipe ends into fd 0/fd 1 of a thread that has not + run a single instruction yet (threadCreateUser, inside jobctlSpawn + above, already set its state Runnable -- see that function's own + field-by-field ordering -- but nothing yields between here and + jobctlSpawn's return, the same timing this file's own + threadSetBrk/threadSetPgid calls already rely on). fd 0's default + console reader (vfsFdInit, vfs.c) is released first so redirecting it + never leaks that reader slot -- exactly the resource reclaimOpenFds + (sched.c) already frees for a thread that dies still holding it, + just done here up front instead of at exit. fd 1 has no such + resource to release: its own default (fdConsole, no reader) owns + nothing. */ + FileDescriptor* fds = threadAt(pid)->fds; + if (stdinHandle >= 0) { + keyboardCloseReader(fds[0].pipeHandle); + vfsPipeAttachInto(fds, 0, stdinHandle, false); + } + if (stdoutHandle >= 0) + vfsPipeAttachInto(fds, 1, stdoutHandle, true); + + return pid; +} + +uint32 jobctlSpawnForeground(const char* path, const char* const* argv, int argc) +{ + uint32 pid = jobctlSpawnPiped(path, argv, argc, -1, -1, threadInvalidId); + if (pid == threadInvalidId) + return threadInvalidId; + + keyboardSetForegroundPgid(pid); + return pid; +} + +int64 jobctlWaitForeground(uint32 pid) +{ + /* Read BEFORE threadJoin, not after: this is the WAITER's own group + (the shell, still very much alive), not the job's -- reading it + after threadJoin would still be correct today since nothing else + changes the calling thread's own pgid in between, but reading it + first keeps this function correct even if that ever changes, and + costs nothing. */ + uint32 callerPgid = threadPgid(currentThreadId()); + + int64 code = threadJoin(pid); + + /* Reclaim the terminal for the CALLER's own group the instant the job + exits -- deliberately not "whatever pgid happened to own the + terminal right before jobctlSpawnForeground ran" (which could itself + be some earlier, now-dead job's stale id if this is the very first + foreground job the caller has ever run): 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. */ + keyboardSetForegroundPgid(callerPgid); + + return code; +} + +int64 jobctlWaitPipelineStages(const uint32* pids, int count) +{ + /* Same "read before threadJoin" reasoning as jobctlWaitForeground + above. */ + uint32 callerPgid = threadPgid(currentThreadId()); + + /* Grants the terminal to the pipeline's shared group (pids[0] IS that + group's id -- jobctlSpawnPiped's own joinPgid contract already made + every other pid a member of it) once, for the whole chain together, + rather than per-spawn -- a Ctrl-C typed while the pipeline runs + reaches every stage at once, exactly like jobctlSpawnForeground + already does for a single job. */ + keyboardSetForegroundPgid(pids[0]); +