430 lines
18 KiB
C
430 lines
18 KiB
C
#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]);
|
|
|
|
/* 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;
|
|
|
|
/* inPath is always nullptr here: fd 0 is already the here-string pipe
|
|
jobctlSpawnPipedHereString just wired up, exactly like a piped
|
|
stdin's slot in runPipeline (shell.c) is never ALSO given a file
|
|
redirect on the same fd. */
|
|
if (!jobctlApplyRedirects(pid, nullptr, outPath, outAppend, errPath, errAppend)) {
|
|
threadKill(pid);
|
|
threadJoin(pid);
|
|
return threadInvalidId;
|
|
}
|
|
|
|
keyboardSetForegroundPgid(pid);
|
|
return pid;
|
|
}
|
|
|
|
bool jobctlRunBackgroundHereString(const char* path, const char* const* argv, int argc,
|
|
const char* text, const char* outPath, bool outAppend,
|
|
const char* errPath, bool errAppend,
|
|
uint32* outPid)
|
|
{
|
|
uint32 pid = jobctlSpawnPipedHereString(path, argv, argc, text, -1, threadInvalidId);
|
|
if (pid == threadInvalidId)
|
|
return false;
|
|
|
|
if (!jobctlApplyRedirects(pid, nullptr, outPath, outAppend, errPath, errAppend)) {
|
|
threadKill(pid);
|
|
threadJoin(pid);
|
|
return false;
|
|
}
|
|
|
|
*outPid = pid;
|
|
return true;
|
|
}
|
|
|
|
/* The mirror image of jobctlSpawnPipedHereString above: that one feeds a
|
|
fixed string INTO a freshly spawned child's stdin (the shell is the
|
|
writer), this one reads a freshly spawned child's stdout back OUT as a
|
|
string (the shell is the reader) -- shell.c's new `$(cmd)` command
|
|
substitution. Attaches this thread as the pipe's reader (pipeAddReader)
|
|
BEFORE jobctlSpawnPiped ever attaches the child as its writer, the exact
|
|
opposite order of jobctlSpawnPipedHereString's own writer-after-reader
|
|
ordering but for the same reason: whichever end never gets attached at
|
|
all must not let pipeCloseRead/pipeCloseWrite release the slot out from
|
|
under the other end while it is still in use. stdin is left at its plain
|
|
default (whatever fd 0 already is for this kernel thread, exactly like
|
|
jobctlRunBackground's own unpiped stdin) -- an honestly-scoped limitation
|
|
matching runPipelineStages' "no stage may be a builtin": a substituted
|
|
command that genuinely needs to read stdin itself has nothing useful
|
|
wired to it here, since command substitution has no argv-supplied input
|
|
of its own to give it.
|
|
Joins the child (threadJoin) BEFORE ever reading a single byte, then
|
|
drains whatever it wrote with plain non-blocking pipeRead calls -- the
|
|
opposite order of "read until EOF, then join" would deadlock in this
|
|
kernel every time: reclaimOpenFds (sched.c), which closes a dead
|
|
thread's own fd 1 and so is what actually makes pipeRead ever report
|
|
real end-of-file (writers reaching 0), runs ONLY from inside threadReap
|
|
(threadJoin's own last step), never from threadExit/threadKill
|
|
themselves -- a thread that has already died but not yet been reaped
|
|
still counts, from the pipe's own point of view, as holding fd 1 open.
|
|
Reading first would therefore block forever waiting for an EOF that can
|
|
only ever arrive AFTER the very threadJoin call that would come next.
|
|
Joining first instead means the child's own write(s) must already have
|
|
completed (and fit in the pipe's ring buffer without blocking on free
|
|
space, exactly like jobctlSpawnPipedHereString's own here-string write
|
|
already assumes for the identical pipeBufferSize=256 constant) by the
|
|
time it actually exits -- an honestly-scoped limitation this function
|
|
shares with that one: a substituted command whose own combined stdout
|
|
exceeds pipeBufferSize bytes would block on a full buffer waiting for a
|
|
reader that will not start draining until this function's own
|
|
threadJoin below returns, which itself requires the write to unblock
|
|
first -- the same ordering deadlock in the opposite direction, avoided
|
|
here simply by staying within the same generous margin every other
|
|
pipeBufferSize-sized fixed buffer in this kernel already relies on.
|
|
Copies at most outCap-1 bytes into outBuf (NUL-terminated); any further
|
|
buffered bytes past that are still drained (into a small scratch
|
|
buffer, discarded) rather than left sitting in the pipe. *outStatus
|
|
receives the child's real exit code. Returns false, touching neither
|
|
outBuf nor *outStatus, on the same load/spawn failures as
|
|
jobctlSpawnPiped, or if the pipe table itself is exhausted
|
|
(vfsPipeCreateHandle). */
|
|
bool jobctlCaptureOutput(const char* path, const char* const* argv, int argc,
|
|
char* outBuf, int outCap, int64* outStatus)
|
|
{
|
|
int handle = vfsPipeCreateHandle();
|
|
if (handle < 0)
|
|
return false;
|
|
|
|
pipeAddReader(handle);
|
|
|
|
uint32 pid = jobctlSpawnPiped(path, argv, argc, -1, handle, threadInvalidId);
|
|
if (pid == threadInvalidId) {
|
|
pipeCloseRead(handle);
|
|
return false;
|
|
}
|
|
|
|
*outStatus = threadJoin(pid);
|
|
|
|
int used = 0;
|
|
char scratch[64];
|
|
for (;;) {
|
|
int64 n = (used < outCap - 1)
|
|
? pipeRead(handle, outBuf + used, (uint64)(outCap - 1 - used))
|
|
: pipeRead(handle, scratch, sizeof(scratch));
|
|
if (n <= 0)
|
|
break;
|
|
if (used < outCap - 1)
|
|
used += (int)n;
|
|
}
|
|
outBuf[used] = 0;
|
|
|
|
pipeCloseRead(handle);
|
|
return true;
|
|
}
|