#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]);