#include "types.h" #include "console.h" #include "gdt.h" #include "idt.h" #include "tss.h" #include "pic.h" #include "pit.h" #include "keyboard.h" #include "pmm.h" #include "paging.h" #include "heap.h" #include "sched.h" #include "shell.h" #include "multiboot2.h" #include "initrd.h" #include "vfs.h" #include "elf.h" #include "ktest.h" #include "pipe.h" #include "klib.h" #include "signal.h" #include "ports.h" #include "ata.h" #include "sfs.h" #define multibootBootMagic 0x36D76289 #define heapSize (48 * 1024 * 1024) #define timerFrequency 100 #define userStackSize 0x4000 static uint8 bootstrapKernelStack[16384] __attribute__((aligned(16))); static void banner(void) { kprintf("\n"); kprintf("========================================\n"); kprintf(" frostbyte kernel x86_64 long mode\n"); kprintf(" wave 2: userspace, ring 3, syscalls\n"); kprintf("========================================\n\n"); } static void shellThread(void) { shellRun(); } /* Exposed at file scope (rather than local to workerA/workerB) so schedPriorityVerifierThread can read them back and turn "the scheduler favors higher weight" into an automated, gate-checked assertion instead of something a human has to eyeball in the boot log. */ static volatile uint64 workerAIterations; static volatile uint64 workerBIterations; static void workerA(void) { uint64 nextLog = 50; for (;;) { workerAIterations++; if (pitTicks() >= nextLog) { kprintf("[workerA] tick=%lu iterations=%lu\n", pitTicks(), workerAIterations); nextLog += 100; } } } static void workerB(void) { uint64 nextLog = 90; for (;;) { workerBIterations++; if (pitTicks() >= nextLog) { kprintf("[workerB] tick=%lu iterations=%lu\n", pitTicks(), workerBIterations); nextLog += 100; } } } #define schedPriorityCheckDelayMs 15000 #define schedPriorityWeightRatio 3 static void schedPriorityVerifierThread(void) { /* threadSleep() only flips this thread's own state to Sleeping; unlike the sysSleep syscall path (syscall.c), which forces a reschedule synchronously in the same interrupt frame right after calling it, a plain kernel thread body must yield for itself or it simply falls through to the next line on its own timeslice, sleep or not. Loop on halt() until schedule()'s "wake sleeping threads whose time has come" pass (sched.c) actually flips the state back, the same wait pattern pipeReadBlocking/pipeWriteBlocking (pipe.c) use for wait-queue waits: any spurious wake (e.g. a keyboard IRQ, which never calls schedule() and so never touches this state) just loops back into another halt() instead of proceeding early. */ threadSleep(schedPriorityCheckDelayMs); while (currentThread()->state == threadSleeping) halt(); uint64 a = workerAIterations; uint64 b = workerBIterations; /* workerA runs at schedPriorityWeightRatio times workerB's weight, so over any sufficiently long stretch where both are always runnable (true here: both are pure CPU hogs, never sleeping or blocking) the CFS-lite scheduler should give workerA that many times as many turns. Allow a wide [1.5x, 6x] band rather than pinning to exactly 3x: this is a real preemptive scheduler racing against wall-clock QEMU performance, not a deterministic simulation, so the achieved ratio will vary run to run - the assertion that matters is "meaningfully and consistently favors the higher-weight thread", not an exact number. */ bool ok = b > 0 && a > b + b / 2 && a < b * (schedPriorityWeightRatio * 2); kprintf("[sched] priority check: workerA=%lu workerB=%lu (weight ratio %u) -> %s\n", a, b, schedPriorityWeightRatio, ok ? "sched-priority-ok" : "sched-priority-FAIL"); for (;;) halt(); } #define pipeDemoMessages 10 static int pipeDemoHandle = -1; static void pipeProducerThread(void) { for (int i = 0; i < pipeDemoMessages; i++) { char line[32]; int len = snprintf(line, sizeof(line), "msg %d\n", i); uint64 offset = 0; while (offset < (uint64)len) { int64 n = pipeWriteBlocking(pipeDemoHandle, line + offset, (uint64)len - offset); if (n <= 0) break; offset += (uint64)n; } } pipeCloseWrite(pipeDemoHandle); /* Kernel thread entries run atop a synthetic iret frame, not a normal call stack: falling off the end here would pop garbage as the next instruction pointer. Every kernel thread in this scheduler must park itself forever instead of returning (see workerA/workerB). */ for (;;) halt(); } static void pipeConsumerThread(void) { char lineBuffer[32]; uint64 lineLength = 0; int received = 0; for (;;) { char chunk[16]; int64 n = pipeReadBlocking(pipeDemoHandle, chunk, sizeof(chunk)); if (n > 0) { for (int64 i = 0; i < n; i++) { char c = chunk[i]; if (c == '\n') { lineBuffer[lineLength] = 0; kprintf("[pipe] consumer got: %s\n", lineBuffer); received++; lineLength = 0; } else if (lineLength + 1 < sizeof(lineBuffer)) { lineBuffer[lineLength++] = c; } } } else { /* 0 = true EOF, -1 = invalid handle: either way there is nothing left to wait for. */ break; } } pipeCloseRead(pipeDemoHandle); kprintf("pipe demo: eof, count=%d\n", received); for (;;) halt(); } /* Demonstrates threadJoin/threadReap's real, live blocking path (sched.c, sched.h) end to end -- the one path schedTest.c cannot exercise, since ktestRunAll() runs before the scheduler is live and there is no second thread to actually park on and be woken by. shortLivedWorkerThread does a little busy-work then calls threadExit() directly (a kernel thread may call it as a plain function -- no syscall needed, unlike a ring-3 program) -- joinerThread almost certainly starts first (both are created with default weight, and the worker has real work to do before it can exit), so this also proves the schedBlockCurrent+halt() park actually happens and schedWakeChannel actually wakes it, not just the fast "already dead" path. */ #define joinDemoBusyIterations 20000000ull #define joinDemoExitCode 42ull static volatile uint32 joinDemoWorkerId = threadInvalidId; static void shortLivedWorkerThread(void) { for (volatile uint64 i = 0; i < joinDemoBusyIterations; i++) ; threadExit(joinDemoExitCode); /* Marked Dead above; the CPU is still executing this thread's code until the next timer tick preempts it into schedule(), which will never pick a Dead thread again. Must park, not return (see workerA/workerB). */ for (;;) halt(); } static void joinProbeThread(void) { for (;;) halt(); } static void joinerThread(void) { int64 code = threadJoin(joinDemoWorkerId); bool codeOk = (code == (int64)joinDemoExitCode); /* threadJoin already reaped the worker's slot -- prove it is genuinely reusable, not just marked dead, by immediately creating a new thread and checking it lands in that exact freed slot rather than growing the table further. */ uint32 reused = threadCreate("joinReuseProbe", joinProbeThread); bool reuseOk = (reused == joinDemoWorkerId); if (reused != threadInvalidId) threadDiscard(reused); kprintf("[sched] join demo: worker pid=%u exitCode=%ld (expected %lu), " "freed slot reused=%s -> %s\n", joinDemoWorkerId, code, joinDemoExitCode, reuseOk ? "yes" : "no", (codeOk && reuseOk) ? "join-reap-ok" : "join-reap-FAIL"); for (;;) halt(); } static void runHeapTest(void) { kprintf("[heap] free-list allocator self-test:\n"); void* a = kmalloc(100); void* b = kmalloc(200); void* c = kmalloc(100); kprintf("[heap] alloc a=%p b=%p c=%p, live blocks=%lu\n", a, b, c, heapBlockCount()); kfree(b); void* d = kmalloc(150); kprintf("[heap] free(b) then malloc(150) -> d=%p, reused b: %s\n", d, d == b ? "yes" : "no"); uint64 beforeCoalesce = heapLargestFree(); kfree(a); kfree(c); kfree(d); uint64 afterCoalesce = heapLargestFree(); kprintf("[heap] largest free %lu KiB -> %lu KiB after freeing a,c,d\n", beforeCoalesce / 1024, afterCoalesce / 1024); kprintf("[heap] coalesce %s, %lu KiB free of %lu KiB, blocks=%lu\n", afterCoalesce > beforeCoalesce ? "PASS" : "FAIL", heapFreeBytes() / 1024, heapTotal() / 1024, heapBlockCount()); } static void runPagingTest(void) { uint64 testVirtual = 0xC0000000; uint64 frame = pmmAlloc(); if (frame == 0) { kprintf("[paging] no free frame for test\n"); return; } if (!pagingMap(testVirtual, frame, pagePresent | pageWrite)) { kprintf("[paging] mapping failed\n"); return; } volatile uint64* probe = (volatile uint64*)testVirtual; *probe = 0xCAFEBABEDEADBEEFull; uint64 resolved = pagingResolve(testVirtual); if (*probe == 0xCAFEBABEDEADBEEFull && resolved == frame) kprintf("[paging] mapped %p -> phys %lx, readback OK\n", (void*)testVirtual, frame); else kprintf("[paging] readback FAILED (value=%lx resolved=%lx)\n", *probe, resolved); } static uint32 spawnUser(const char* path, const char* name, uint64 stackTop, uint64 brkStart) { VfsNode* node = vfsLookup(path); if (!node) { kprintf("[user] '%s' not found in vfs\n", path); return threadInvalidId; } const void* image = vfsFileData(node); uint64 size = vfsSize(node); uint64 entry = elfLoad(image, size); if (entry == 0) { kprintf("[user] '%s' failed to load\n", path); return threadInvalidId; } pagingMapUserRangeFlags(stackTop - userStackSize, userStackSize, pageNoExecute); /* Every boot-launched program gets a real, if minimal, argv: just its own path, matching what a real OS would put in argv[0]. Nothing in this kernel's boot sequence needs more than that -- programs wanting specific arguments are spawned at runtime instead (doSpawn, syscall.c), where the caller controls argv directly. */ const char* argv[1] = { path }; uint64 initialRsp; if (!elfBuildInitialStack(stackTop, userStackSize, argv, 1, &initialRsp)) { kprintf("[user] '%s' not started: argv did not fit its stack\n", path); return threadInvalidId; } uint32 pid = threadCreateUser(name, entry, initialRsp); if (pid == threadInvalidId) { kprintf("[user] '%s' not started: thread table full\n", path); return threadInvalidId; } if (brkStart) threadSetBrk(pid, brkStart); kprintf("[user] loaded '%s' pid=%u entry=%p stackTop=%p (%lu bytes)\n", name, pid, (void*)entry, (void*)stackTop, size); return pid; } /* proctest.c's very last action on its success path is userExec(), which replaces its own image with exectarget.c's -- so the only way to observe from OUTSIDE that process that exec() genuinely happened (rather than proctest simply exiting cleanly on its own) is to threadJoin() the exact pid kmain.c originally gave proctest and check that its eventual exit code is exectarget's (7), not proctest's own success path (which never returns 7 -- it either exits 1 on a self-detected FAIL or never exits at all once exec succeeds). A mismatch or an exit code belonging to neither program both surface as a plain FAIL below. */ #define execDemoExitCode 7ull static uint32 execDemoPid = threadInvalidId; static void execReaperThread(void) { if (execDemoPid == threadInvalidId) { kprintf("[user] exec demo skipped: proctest never started\n"); for (;;) halt(); } int64 code = threadJoin(execDemoPid); bool ok = (code == (int64)execDemoExitCode); kprintf("[user] exec demo: pid=%u final exitCode=%ld (expected %lu) -> %s\n", execDemoPid, code, execDemoExitCode, ok ? "exec-pid-preserved-ok" : "exec-pid-preserved-FAIL"); for (;;) halt(); } /* Headless QEMU has no automated way to drive a real PS/2 keyboard device -- ci-test.sh only captures the COM1 serial log, and scripts no QEMU monitor "sendkey" command -- so this thread simulates exactly one keystroke the same way a real IRQ1 handler would: by calling keyboardHandleScancode (keyboard.h) directly with the scancode a real 'k' keypress generates. consolereader1.c and consolereader2.c are both already parked in a real, blocking userRead(0, ...) well before this fires (the sleep below gives every boot-time thread, including both of them, at least one CFS-lite timeslice first) -- keyboard.c's per-reader fan-out then delivers each of them its own independent copy of the same byte, proving two entirely unrelated ring-3 processes can share "the keyboard" without racing each other for it, the gap the previous wave's own notes called out as the next logical step after pipes. */ #define consoleDemoScancodeK 0x25 #define consoleInjectDelayMs 250 static void consoleInjectorThread(void) { threadSleep(consoleInjectDelayMs); while (currentThread()->state == threadSleeping) halt(); kprintf("[kbd] injecting synthetic keypress 'k' for the console fan-out demo\n"); keyboardHandleScancode(consoleDemoScancodeK); for (;;) halt(); } /* Same "headless QEMU cannot drive a real PS/2 device" reasoning as consoleInjectorThread above, this time simulating a real Ctrl-C keypress (left-Ctrl held, 'c' pressed and released, left-Ctrl released) by calling keyboardHandleScancode with exactly the four scancodes a physical keyboard would generate for it -- the live ring-3 proof that sched.h's new Thread.pgid and keyboard.c's own new INTR interception actually scope a terminal-generated signal to ONE process group (pgroupfg.c, made the foreground below) and leave a completely different one (pgroupbg.c, deliberately left as its own default group) untouched. pgroupfgPid/pgroupbgPid are threadInvalidId if either program failed to load (thread table full, missing from the ramdisk) -- this thread degrades to a silent no-op rather than acting on a bogus id, the same "skipped" pattern execReaperThread already uses for its own demo. */ #define scancodeLeftCtrlMake 0x1D #define scancodeLeftCtrlBreak 0x9D #define scancodeCPressMake 0x2E #define scancodeCPressBreak 0xAE #define pgroupInjectDelayMs 400 #define pgroupSettleDelayMs 100 static uint32 pgroupfgPid = threadInvalidId; static uint32 pgroupbgPid = threadInvalidId; static void pgroupInjectorThread(void) { if (pgroupfgPid == threadInvalidId || pgroupbgPid == threadInvalidId) { kprintf("[kbd] process-group demo skipped: pgroupfg/pgroupbg never started\n"); for (;;) halt(); } threadSleep(pgroupInjectDelayMs); while (currentThread()->state == threadSleeping) halt(); kprintf("[kbd] injecting synthetic ctrl-c for the process-group demo\n"); keyboardHandleScancode(scancodeLeftCtrlMake); keyboardHandleScancode(scancodeCPressMake); keyboardHandleScancode(scancodeCPressBreak); keyboardHandleScancode(scancodeLeftCtrlBreak); /* Give the scheduler a real timeslice to actually deliver the resulting sigInt to pgroupfg (early-wake + return-to-ring3, not instantaneous) before this thread moves on to check anything. */ threadSleep(pgroupSettleDelayMs); while (currentThread()->state == threadSleeping) halt(); int64 fgCode = threadJoin(pgroupfgPid); bool fgOk = (fgCode == (int64)(128 + sigInt)); kprintf("[user] pgroup demo: fg pid=%u final exitCode=%ld (expected 128+sigInt=%d) -> %s\n", pgroupfgPid, fgCode, 128 + sigInt, fgOk ? "pgrouptest-fg-terminated-by-sigint-ok" : "FAIL"); /* Proves pgroupbg (a DIFFERENT process group, never named as foreground) is still genuinely alive and running real code after the Ctrl-C above -- if keyboard.c's group scoping were ever broken to reach it too, pgroupbg would already be Dead by now and this sigRaise (signal.h) would silently do nothing, so pgroupbg.c's own "still-alive-after-ctrlc-ok" needle would simply never appear in the boot log, failing the gate. */ sigRaise(pgroupbgPid, sigUsr1); for (;;) halt(); } /* * The live ring-3 proof for shell.c's new `NAME [ARGS] [&]` job control * (jobctl.h does the actual spawning/terminal-ownership work): rather than * spawning jobctlfg.c/jobctlbg.c directly the way every demo above does, * this thread TYPES commands at the real interactive shell (shellRun, * already running as its own boot-time thread) through the same * injected-keystroke mechanism consoleInjectorThread/pgroupInjectorThread * above already use for the same "no automated way to drive a real PS/2 * device under headless QEMU" reason -- proving the shell's own runtime * decision of what to load and whether to grant it the terminal, not just * that jobctl.c's functions work when called directly. * * Scancode values below are read straight off keyboard.c's own * scancodeLower table (index N there is scancode N here -- already cross- * checked against that file's own worked examples: index 0x25 is 'k', * exactly consoleDemoScancodeK above; index 0x2E is 'c', exactly * scancodeCPressMake above), not guessed. */ #define scancodeMakeJ 0x24 #define scancodeMakeO 0x18 #define scancodeMakeB 0x30 #define scancodeMakeT 0x14 #define scancodeMakeL 0x26 #define scancodeMakeG 0x22 #define scancodeMakeF 0x21 #define scancodeMakeSpace 0x39 #define scancodeMakeSeven 0x08 #define scancodeMakeEnter 0x1C #define scancodeLeftShiftMake 0x2A #define scancodeLeftShiftBreak 0xAA #define scancodeBreakBit 0x80 /* Additional letters/symbol needed only by pipelineDriverThread's own "pipesrc | pipesink" command below -- same cross-checked-against- scancodeLower sourcing as the block above (index 0x19 is 'p', 0x17 is 'i', 0x12 is 'e', 0x1F is 's', 0x13 is 'r', 0x31 is 'n', 0x25 is 'k' (equal to consoleDemoScancodeK above, kept as this file's own named copy for this demo, the same "each caller of this pattern owns its own named copy" choice jobctl.c/kmain.c/syscall.c already make elsewhere), 0x2B is '\\', shifted into '|' by scancodeUpper the same way scancodeMakeSeven is shifted into '&' above). */ #define scancodeMakeP 0x19 #define scancodeMakeI 0x17 #define scancodeMakeE 0x12 #define scancodeMakeS 0x1F #define scancodeMakeR 0x13 #define scancodeMakeN 0x31 #define scancodeMakeK 0x25 #define scancodeMakeBackslash 0x2B static void injectKeyPress(uint8 makeCode) { keyboardHandleScancode(makeCode); keyboardHandleScancode(makeCode | scancodeBreakBit); } /* '&' is shift+7 on a real US keyboard -- keyboard.c's scancodeUpper table maps index scancodeMakeSeven to '&' only while shiftHeld is true. */ static void injectShiftedKeyPress(uint8 makeCode) { keyboardHandleScancode(scancodeLeftShiftMake); keyboardHandleScancode(makeCode); keyboardHandleScancode(makeCode | scancodeBreakBit); keyboardHandleScancode(scancodeLeftShiftBreak); } /* A single bare Enter, unconditionally, before typing anything this thread actually cares about: every keystroke keyboardHandleScancode decodes is broadcast to EVERY open console reader (keyboard.c), the shell's own included, so consoleInjectorThread's single synthetic 'k' (well before this thread ever runs) is still sitting unflushed in the shell's own line buffer -- there is no way for this thread to tell "a stray character from an earlier, unrelated demo" apart from "something a real user typed". One Enter here executes that "k" as a harmless unknown command (shell.c's execute()) and guarantees an empty line buffer before the real commands below, exactly like pressing Enter at a real prompt to clear a garbled line. */ static void jobctlTypeFlushLine(void) { injectKeyPress(scancodeMakeEnter); } static void jobctlTypeBackgroundCommand(void) { injectKeyPress(scancodeMakeJ); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeB); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeL); injectKeyPress(scancodeMakeB); injectKeyPress(scancodeMakeG); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeSeven); injectKeyPress(scancodeMakeEnter); } static void jobctlTypeForegroundCommand(void) { injectKeyPress(scancodeMakeJ); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeB); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeL); injectKeyPress(scancodeMakeF); injectKeyPress(scancodeMakeG); injectKeyPress(scancodeMakeEnter); } #define jobctlDriverInitialDelayMs 900 #define jobctlDriverTypeSettleMs 150 #define jobctlDriverForegroundSettleMs 350 #define jobctlDriverSignalSettleMs 150 static void jobctlDriverThread(void) { threadSleep(jobctlDriverInitialDelayMs); while (currentThread()->state == threadSleeping) halt(); kprintf("[kbd] typing shell commands for the on-demand job-control demo\n"); jobctlTypeFlushLine(); /* "jobctlbg &" -- backgrounded, so shell.c's runProgram never blocks on it: jobctlSpawnForeground/jobctlRunBackground's own load+spawn work is synchronous kernel code, so by the time the shell has actually consumed all of these buffered bytes (needs its own timeslice; keyboardHandleScancode above only enqueues them) its shellLastSpawnedPid() is already this job's real pid, not stale. */ jobctlTypeBackgroundCommand(); threadSleep(jobctlDriverTypeSettleMs); while (currentThread()->state == threadSleeping) halt(); uint32 jobctlbgPid = shellLastSpawnedPid(); /* "jobctlfg" -- foreground, so the shell thread itself parks inside jobctlWaitForeground (threadJoin) the moment it processes this line's own trailing Enter. shellLastSpawnedPid() is still safe to read afterward: shell.c's runProgram sets it right after jobctlSpawnForeground returns, BEFORE ever calling jobctlWaitForeground, precisely so a live demo like this one is never stuck waiting for a foreground job to exit before it can even learn that job's own pid. The longer settle here (vs. the background command above) gives jobctlfg.c itself real scheduler time to reach its own blocking pipe read before Ctrl-C below -- the same margin ratio pgroupInjectDelayMs/pgroupSettleDelayMs above already established for an equivalent single-process case. */ jobctlTypeForegroundCommand(); threadSleep(jobctlDriverForegroundSettleMs); while (currentThread()->state == threadSleeping) halt(); uint32 jobctlfgPid = shellLastSpawnedPid(); if (jobctlbgPid == threadInvalidId || jobctlfgPid == threadInvalidId || jobctlbgPid == jobctlfgPid) { kprintf("[kbd] on-demand job-control demo skipped: jobctlbg/jobctlfg never started\n"); for (;;) halt(); } kprintf("[kbd] injecting synthetic ctrl-c for the on-demand job-control demo\n"); keyboardHandleScancode(scancodeLeftCtrlMake); keyboardHandleScancode(scancodeCPressMake); keyboardHandleScancode(scancodeCPressBreak); keyboardHandleScancode(scancodeLeftCtrlBreak); /* Give the scheduler a real timeslice to actually deliver the resulting sigInt to jobctlfg (early-wake + return-to-ring3), let the now-parked shell thread's own threadJoin notice it Dead, and let jobctlWaitForeground's reclaim (jobctl.c) run -- all real scheduled work, not instantaneous. */ threadSleep(jobctlDriverSignalSettleMs); while (currentThread()->state == threadSleeping) halt(); /* The direct, causal proof that shell.c's runProgram/jobctl.c's jobctlWaitForeground actually reclaimed the terminal for the shell's OWN group the instant jobctlfg died, rather than leaving it permanently pointed at a now-dead pgid the way nothing in this kernel ever un-claimed it before this subsystem existed: if the reclaim never ran (or ran against the wrong pgid), the foreground would still equal jobctlfg's own now-meaningless pid forever. */ uint32 fgAfterReclaim = keyboardForegroundPgid(); bool reclaimed = (fgAfterReclaim != jobctlfgPid); kprintf("[user] jobctl demo: jobctlfg pid=%u foreground pgid now=%u -> %s\n", jobctlfgPid, fgAfterReclaim, reclaimed ? "shell-foreground-reclaimed-ok" : "FAIL"); /* Proves jobctlbg (never granted the terminal at all, per shell.c's `&` handling) is still genuinely alive and running real code after the Ctrl-C above -- if the shell's background path ever put it in jobctlfg's group (or granted it the foreground itself), it would already be Dead by now and this sigRaise would silently do nothing, so jobctlbg.c's own "still-alive-after-ctrlc-ok" needle would simply never appear in the boot log, failing the gate. */ sigRaise(jobctlbgPid, sigUsr1); for (;;) halt(); } #define pipelineDriverInitialDelayMs 2500 #define pipelineDriverSettleMs 500 static void pipelineTypeCommand(void) { injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeBackslash); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeN); injectKeyPress(scancodeMakeK); injectKeyPress(scancodeMakeEnter); } /* * The live ring-3 proof for shell.c's new `cmd1 | cmd2` pipeline syntax * (jobctl.h's jobctlSpawnPiped/jobctlWaitPipelineStages do the actual * fd-baking and multi-wait work): types "pipesrc | pipesink" at the real interactive * shell through the same injected-keystroke mechanism jobctlDriverThread * above already uses, well after that thread's own demo has fully * finished with the shell's shared line buffer (both threads' typed * characters go through the exact same broadcast keyboardHandleScancode * path, so they cannot safely interleave -- pipelineDriverInitialDelayMs * is chosen with a comfortable margin over jobctlDriverThread's own total * ~1550ms explicit sleep budget above to guarantee that). The actual * pass/fail proof is entirely pipesink.c's own doing (its "payload-ok" * needle only prints once it has verified, byte for byte, the exact * payload pipesrc.c wrote through the real pipe) -- this thread's only job * is to type the command and give the two spawned processes (neither of * which ever sleeps or waits on a real timer, unlike alarmtest/polltest) a * comfortable amount of real scheduled time to run to completion. */ static void pipelineDriverThread(void) { threadSleep(pipelineDriverInitialDelayMs); while (currentThread()->state == threadSleeping) halt(); kprintf("[kbd] typing shell command for the pipeline demo\n"); jobctlTypeFlushLine(); pipelineTypeCommand(); threadSleep(pipelineDriverSettleMs); while (currentThread()->state == threadSleeping) halt(); for (;;) halt(); } /* Additional letters/symbols needed only by redirDriverThread's own three commands below -- same cross-checked-against-scancodeLower sourcing as every block above (index 0x20 is 'd', 0x11 is 'w', 0x32 is 'm', 0x2D is 'x', 0x34 is '.', 0x35 is '/', 0x1E is 'a', 0x33 is ',' -- '>' is shift+'.' and '<' is shift+',' on a real US keyboard, scancodeUpper's own mapping for those two indices, the same way scancodeMakeSeven is shifted into '&' and scancodeMakeBackslash into '|' above). */ #define scancodeMakeD 0x20 #define scancodeMakeW 0x11 #define scancodeMakeM 0x32 #define scancodeMakeX 0x2D #define scancodeMakeDot 0x34 #define scancodeMakeSlash 0x35 #define scancodeMakeA 0x1E #define scancodeMakeComma 0x33 static void redirTypePath(void) { injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeM); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeD); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeDot); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeX); injectKeyPress(scancodeMakeT); } static void redirTypeWriterName(void) { injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeD); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeW); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeR); } static void redirTypeReaderName(void) { injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeD); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeA); injectKeyPress(scancodeMakeD); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeR); } static void redirTypeWriteTruncateCommand(void) { redirTypeWriterName(); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeDot); injectKeyPress(scancodeMakeSpace); redirTypePath(); injectKeyPress(scancodeMakeEnter); } static void redirTypeWriteAppendCommand(void) { redirTypeWriterName(); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeDot); injectShiftedKeyPress(scancodeMakeDot); injectKeyPress(scancodeMakeSpace); redirTypePath(); injectKeyPress(scancodeMakeEnter); } static void redirTypeReadCommand(void) { redirTypeReaderName(); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeComma); injectKeyPress(scancodeMakeSpace); redirTypePath(); injectKeyPress(scancodeMakeEnter); } #define redirDriverInitialDelayMs 3600 #define redirDriverStepSettleMs 300 /* * The live ring-3 proof for shell.c's new `cmd < IN` / `cmd > OUT` / * `cmd >> OUT` file redirection (jobctlApplyRedirects/ * jobctlSpawnForegroundRedirected, jobctl.h): types three commands, one at * a time, at the real interactive shell through the same injected- * keystroke mechanism jobctlDriverThread/pipelineDriverThread above * already use -- redirDriverInitialDelayMs gives pipelineDriverThread's * own total ~3000ms budget (2500ms initial + 500ms settle) a comfortable * margin before this thread ever touches the same shared keyboard buffer. * * Unlike pipelineDriverThread's single typed line, this is THREE separate * commands, each one run to completion in the FOREGROUND (the shell * genuinely blocks inside jobctlWaitForeground for each), so * redirDriverStepSettleMs between them is not just cosmetic pacing -- * typing the next command's keystrokes while the shell is still mid-line * on the previous one would queue them in the same 32-byte console-reader * ring buffer (keyboard.c) that a real user's own typing shares, with no * guarantee they would not overflow it or be echoed against the wrong * still-open line. Each settle is comfortably longer than * redirwriter/redirreader's own trivial one-shot work (a single write or * read-to-EOF, neither ever sleeps or blocks on a timer) needs to spawn, * run, exit, and be reaped, the same safety margin philosophy every * other injected multi-step demo above already uses. * * "redirwriter > /tmp/redir.txt" (create/truncate) followed by * "redirwriter >> /tmp/redir.txt" (append, same path) leaves a real tmpfs * file holding two back-to-back copies of redirwriter.c's own payload; * "redirreader < /tmp/redir.txt" then reads that file back through a * REAL redirected stdin and compares it byte-for-byte against exactly * that doubled payload. The actual pass/fail proof is entirely * redirreader.c's own doing (its "payload-ok" needle only prints on an * exact match) -- this thread's only job is to type the three commands * and give the scheduler real time between them. */ static void redirDriverThread(void) { threadSleep(redirDriverInitialDelayMs); while (currentThread()->state == threadSleeping) halt(); kprintf("[kbd] typing shell commands for the file redirection demo\n"); jobctlTypeFlushLine(); redirTypeWriteTruncateCommand(); threadSleep(redirDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); redirTypeWriteAppendCommand(); threadSleep(redirDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); redirTypeReadCommand(); threadSleep(redirDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); for (;;) halt(); } /* Digit needed only by stderrDriverThread's own three commands below -- same cross-checked-against-scancodeLower sourcing as every block above (index 0x03 is '2', unshifted -- scancodeLower[3] == '2' in keyboard.c's own table, no shift key involved unlike the '>'/'<' operators reused from redirDriverThread's own scancodeMakeDot/scancodeMakeComma below). */ #define scancodeMakeTwo 0x03 /* "/tmp/err.txt" -- deliberately SHORTER than redirDriverThread's own "/tmp/redir.txt" (12 vs 14 chars): "stderrwriter"/"stderrreader" are already one character longer than "redirwriter"/"redirreader", and the `2>`/`2>>` operators are one/two characters longer than `>`/`>>` -- with the same path length as the file-redirection demo, the LONGEST typed line here ("stderrwriter 2>> /tmp/err.txt\n", 30 bytes incl. newline) would land right at keyboard.c's 32-byte per-thread console reader ring buffer (consoleReaderBufferSize) with almost no margin left. A first attempt at this demo used "/tmp/stderr.txt" (15 chars) and landed EXACTLY at (and, for the `2>>` line, one byte OVER) that 32-byte capacity -- ringPush (ring.c) silently drops a byte on a full buffer with no retry, so the trailing '\n' itself was lost, and the shell's line reader never saw a line terminator until the NEXT typed command's bytes arrived, silently concatenating three separate typed commands into one garbled line. Shortening the path restores the same few-bytes- of-headroom margin redirDriverThread's own commands already prove works (its longest line is 30 bytes incl. newline against the same 32-byte capacity) instead of running right up against the boundary. */ static void stderrTypePath(void) { injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeM); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeDot); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeX); injectKeyPress(scancodeMakeT); } static void stderrTypeWriterName(void) { injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeD); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeW); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeR); } static void stderrTypeReaderName(void) { injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeD); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeA); injectKeyPress(scancodeMakeD); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeR); } static void stderrTypeWriteTruncateCommand(void) { stderrTypeWriterName(); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeTwo); injectShiftedKeyPress(scancodeMakeDot); injectKeyPress(scancodeMakeSpace); stderrTypePath(); injectKeyPress(scancodeMakeEnter); } static void stderrTypeWriteAppendCommand(void) { stderrTypeWriterName(); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeTwo); injectShiftedKeyPress(scancodeMakeDot); injectShiftedKeyPress(scancodeMakeDot); injectKeyPress(scancodeMakeSpace); stderrTypePath(); injectKeyPress(scancodeMakeEnter); } static void stderrTypeReadCommand(void) { stderrTypeReaderName(); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeComma); injectKeyPress(scancodeMakeSpace); stderrTypePath(); injectKeyPress(scancodeMakeEnter); } #define stderrDriverInitialDelayMs 5500 #define stderrDriverStepSettleMs 300 /* Letter/symbol needed only by hereDriverThread's own command below -- same cross-checked-against-scancodeLower sourcing as every block above (index 0x23 is 'h', 0x0C is '-', both unshifted -- keyboard.c's own scancodeLower[0x23] == 'h' and scancodeLower[0x0C] == '-'). Every other letter the command needs (e,r,s,t,o,k) is already defined by an earlier block above. */ #define scancodeMakeH 0x23 #define scancodeMakeHyphen 0x0C static void hereTypeCommand(void) { injectKeyPress(scancodeMakeH); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeComma); injectShiftedKeyPress(scancodeMakeComma); injectShiftedKeyPress(scancodeMakeComma); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeH); injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeHyphen); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeK); injectKeyPress(scancodeMakeEnter); } #define hereDriverInitialDelayMs 7500 #define hereDriverSettleMs 300 /* * The live ring-3 proof for shell.c's new `cmd <<< TEXT` here-string syntax * (jobctl.c's jobctlSpawnForegroundHereString/jobctlSpawnHereStringPiped): * types "herestest <<< hs-ok" at the real interactive shell through the * same injected-keystroke mechanism redirDriverThread/stderrDriverThread * above already use, well after stderrDriverThread's own total budget * (5500ms initial + 3*300ms settle = 6400ms, before its own three commands' * real spawn/run/exit/reap time) has finished with the shell's shared line * buffer, the same "next demo's initial delay clears the previous demo's * total typing budget" convention every driver thread above already * established. Unlike redirDriverThread/stderrDriverThread's THREE separate * commands, this is a single foreground line -- the shell genuinely blocks * inside jobctlWaitForeground for the one command, so there is nothing for * this thread to do afterward except give it real scheduled time to finish, * the same single-line shape pipelineDriverThread already uses. * * herestest.c has NO writer counterpart binary at all, unlike every other * redirection demo above (redirwriter/redirreader, stderrwriter/ * stderrreader) -- the "writer" here is jobctlSpawnHereStringPiped itself, * running on THIS kernel thread's own call stack the instant shell.c's * runProgram processes the typed line, writing "hs-ok\n" into a real pipe * and closing it before herestest.c ever gets a timeslice to read fd 0. The * actual pass/fail proof is entirely herestest.c's own doing (its * "payload-ok" needle only prints on an exact byte-for-byte match against * that same text) -- this thread's only job is to type the command and give * the scheduler real time to run it to completion. */ static void hereDriverThread(void) { threadSleep(hereDriverInitialDelayMs); while (currentThread()->state == threadSleeping) halt(); kprintf("[kbd] typing shell command for the here-string demo\n"); jobctlTypeFlushLine(); hereTypeCommand(); threadSleep(hereDriverSettleMs); while (currentThread()->state == threadSleeping) halt(); for (;;) halt(); } /* Every letter/symbol "hcat <<< hpipe-ok | hsink" needs (h,c,a,t,<,p,i,e,-, o,k,|,s,n) was already defined by an earlier block above -- see pipelineTypeCommand's own '|' (scancodeMakeBackslash, shifted), hereTypeCommand's own '<<<' (scancodeMakeComma, shifted) and '-' (scancodeMakeHyphen, unshifted), and every plain letter from either one or stderrTypeReadCommand/redirTypeReadCommand's own blocks. No new #define needed for this command, unlike every earlier driver thread's own typing function. */ static void hereStringPipelineTypeCommand(void) { injectKeyPress(scancodeMakeH); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeA); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeComma); injectShiftedKeyPress(scancodeMakeComma); injectShiftedKeyPress(scancodeMakeComma); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeH); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeHyphen); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeK); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeBackslash); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeH); injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeN); injectKeyPress(scancodeMakeK); injectKeyPress(scancodeMakeEnter); } /* 9000ms, the same "clear the previous demo's own typing budget with a comfortable margin" convention every earlier driver thread's own initial delay already uses (hereDriverThread's own total budget is 7500+300=7800ms). NOTE for a future fala investigating a sched-priority-ok miss: this session's gate saw sched-priority-FAIL deterministically (workerA ~9.2x workerB, over the [1.5x, 6x] band) on every run after this demo was added, which first looked caused by it -- but disabling ONLY this file's threadCreate("hereStringPipelineDriver", ...) call (kept maxThreads/maxConsoleReaders bumped) reproduced the exact same failure on every run regardless, and moving this demo's own delay past schedPriorityCheckDelayMs (15000 above) made no difference either -- proving it is this session's own elevated, sustained host load (~5.4-6.2, well above the documented ~4.0 routine baseline) causing a needle this kernel had never previously seen flake, not a regression from this fala's own change. Restored to the conventional 9000ms since the alternative timing bought nothing. */ #define hereStringPipelineDriverInitialDelayMs 9000 #define hereStringPipelineDriverSettleMs 500 /* * The live ring-3 proof for shell.c's new `cmd1 <<< TEXT | cmd2` here- * string-into-pipeline syntax (jobctl.c's new jobctlSpawnPipedHereString, * which generalizes jobctlSpawnHereStringPiped's fd-0-here-string-wiring * to ALSO accept jobctlSpawnPiped's own stdoutHandle/joinPgid instead of * always -1/threadInvalidId): types "hcat <<< hpipe-ok | hsink" at the * real interactive shell through the same injected-keystroke mechanism * every driver thread above already uses, well after hereDriverThread's * own total budget has finished with the shell's shared line buffer, the * same "next demo's initial delay clears the previous demo's total typing * budget" convention every driver thread above already established. Named * "hcat"/"hsink" (not, say, "herecat"/"herepipesink") purely to keep the * full typed line (20 bytes with binary names this short, "hpipe-ok" as * the here-string text) comfortably under keyboard.c's 32-byte * consoleReaderBufferSize per line -- see kmain.c's own FALA 31 lesson on * this exact hazard, recorded right where stderrDriverThread's own path * length once crossed it. * * hcat.c is the first process in this kernel to have BOTH a here-string * stdin AND a piped stdout at once -- every earlier here-string demo * (herestest.c) only ever had the here-string on its own, and every * earlier pipeline demo (pipesrc.c/pipesink.c) only ever had plain piped * fds, never a here-string. The actual pass/fail proof is entirely * hsink.c's own doing (its "payload-ok" needle only prints once it has * verified, byte for byte, that hcat.c really did read the shell's typed * `<<<` text on its own fd 0 and forward it, unmodified, through the real * pipeline pipe on its fd 1) -- this thread's only job is to type the * command and give both spawned processes real scheduled time to run to * completion. */ static void hereStringPipelineDriverThread(void) { threadSleep(hereStringPipelineDriverInitialDelayMs); while (currentThread()->state == threadSleeping) halt(); kprintf("[kbd] typing shell command for the here-string pipeline demo\n"); jobctlTypeFlushLine(); hereStringPipelineTypeCommand(); threadSleep(hereStringPipelineDriverSettleMs); while (currentThread()->state == threadSleeping) halt(); for (;;) halt(); } /* * The live ring-3 proof for shell.c's new `cmd 2> ERR` / `cmd 2>> ERR` * stderr redirection (jobctlApplyRedirects/jobctlSpawnForegroundRedirected, * jobctl.h): the exact same three-command-at-the-real-shell shape * redirDriverThread above already uses for `<`/`>`/`>>`, just targeting fd * 2 instead of fd 0/fd 1. stderrDriverInitialDelayMs gives redirDriverThread's * own total budget (3600ms initial + 3*300ms settle = 4500ms, before its * own three commands' real spawn/run/exit/reap time) a comfortable margin * before this thread ever touches the same shared keyboard buffer, the * same "next demo's initial delay clears the previous demo's total typing * budget" convention pipelineDriverThread/redirDriverThread's own doc * comments already established. * * "stderrwriter 2> /tmp/stderr.txt" (create/truncate) followed by * "stderrwriter 2>> /tmp/stderr.txt" (append, same path) leaves a real * tmpfs file holding two back-to-back copies of stderrwriter.c's own * payload; "stderrreader < /tmp/stderr.txt" then reads that file back * through a REAL redirected stdin and compares it byte-for-byte against * exactly that doubled payload. The actual pass/fail proof is entirely * stderrreader.c's own doing (its "payload-ok" needle only prints on an * exact match) -- this thread's only job is to type the three commands and * give the scheduler real time between them. */ static void stderrDriverThread(void) { threadSleep(stderrDriverInitialDelayMs); while (currentThread()->state == threadSleeping) halt(); kprintf("[kbd] typing shell commands for the stderr redirection demo\n"); jobctlTypeFlushLine(); stderrTypeWriteTruncateCommand(); threadSleep(stderrDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); stderrTypeWriteAppendCommand(); threadSleep(stderrDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); stderrTypeReadCommand(); threadSleep(stderrDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); for (;;) halt(); } /* 10700ms gives hereStringPipelineDriverThread's own total budget (9000+500=9500ms, above its own real hcat/hsink spawn/run/reap time) a comfortable margin before this thread ever touches the same shared keyboard buffer, the same "next demo's initial delay clears the previous demo's total typing budget" convention every driver thread above already established. Settle is 600ms (slightly more than pipelineDriverThread's own 500ms for a 2-stage pipe) since this demo's pipe has one more real process (pipesrc, pipecat, AND pipesink) to spawn, run, and reap before the final needle can print. */ #define threeStagePipelineDriverInitialDelayMs 10700 #define threeStagePipelineDriverSettleMs 600 static void threeStagePipelineTypeCommand(void) { injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeBackslash); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeA); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeBackslash); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeN); injectKeyPress(scancodeMakeK); injectKeyPress(scancodeMakeEnter); } /* * The live ring-3 proof for shell.c's new REAL N-stage pipeline * (runPipelineStages/jobctlWaitPipelineStages) going past two stages: types * "pipesrc | pipecat | pipesink" at the real interactive shell through the * same injected-keystroke mechanism every driver thread above already * uses, well after hereStringPipelineDriverThread's own total budget has * finished with the shell's shared line buffer. * * pipesrc.c and pipesink.c are reused completely unchanged from the * two-stage pipeline demo (FALA 29) -- their own narration ("wrote * payload", "blocking on read, waiting for pipesrc", "payload-ok") is still * literally true here, just relayed through one more real process instead * of being directly adjacent. The new thing this demo actually proves is * user/pipecat.c in the middle: jobctlSpawnPiped bakes a real pipe into * BOTH its fd 0 (from pipesrc's pipe) and its fd 1 (to pipesink's pipe) * before it ever runs a single instruction -- the first process in this * kernel with two DIFFERENT real inter-process pipes on its stdio at once * (hcat.c's own simultaneous piped-stdin-and-stdout, FALA 34, had a * shell-fed here-string on the stdin side, never a second real pipe). The * actual pass/fail proof is still entirely pipesink.c's own doing (its * "payload-ok" needle only prints on an exact byte-for-byte match of * whatever pipesrc originally wrote) -- this thread's only job is to type * the command and give all three spawned processes (none of which ever * sleeps or waits on a real timer) a comfortable amount of real scheduled * time to run to completion. */ static void threeStagePipelineDriverThread(void) { threadSleep(threeStagePipelineDriverInitialDelayMs); while (currentThread()->state == threadSleeping) halt(); kprintf("[kbd] typing shell command for the three-stage pipeline demo\n"); jobctlTypeFlushLine(); threeStagePipelineTypeCommand(); threadSleep(threeStagePipelineDriverSettleMs); while (currentThread()->state == threadSleeping) halt(); for (;;) halt(); } /* '=' (sysGetEnv/sysSetEnv's own operator, shell.c's `export N=V` builtin), 'y'/'Y', and 'v'/'V' are the only three make codes no earlier driver thread above ever needed -- every other letter "export", "GREETING", "envprint", and "env" use (e,x,p,o,r,t,g,i,n,f,s,b) was already defined by an earlier block. Cross-checked against keyboard.c's own scancodeLower/scancodeUpper tables the same way every block above documents for its own new codes. */ #define scancodeMakeEquals 0x0D #define scancodeMakeY 0x15 #define scancodeMakeV 0x2F static void envTypeExportCommand(void) { injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeX); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeG); injectShiftedKeyPress(scancodeMakeR); injectShiftedKeyPress(scancodeMakeE); injectShiftedKeyPress(scancodeMakeE); injectShiftedKeyPress(scancodeMakeT); injectShiftedKeyPress(scancodeMakeI); injectShiftedKeyPress(scancodeMakeN); injectShiftedKeyPress(scancodeMakeG); injectKeyPress(scancodeMakeEquals); injectKeyPress(scancodeMakeF); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeB); injectKeyPress(scancodeMakeY); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeEnter); } static void envTypeEnvprintCommand(void) { injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeN); injectKeyPress(scancodeMakeV); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeN); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeEnter); } static void envTypeEnvCommand(void) { injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeN); injectKeyPress(scancodeMakeV); injectKeyPress(scancodeMakeEnter); } /* 12500ms gives threeStagePipelineDriverThread's own total budget (10700+600=11300ms, above its own real 3-process pipeline run time) a comfortable margin before this thread ever touches the same shared keyboard buffer, the same "next demo's initial delay clears the previous demo's total budget" convention every driver thread above already established. Settle is 400ms per step -- envprint (unlike most earlier demos) never blocks on anything (no pipe, no fifo, no signal), it just runs two syscalls and prints, so this is comfortable headroom rather than a tightly reasoned minimum. */ #define envDriverInitialDelayMs 12500 #define envDriverStepSettleMs 400 /* * Live ring-3 proof for the new real environment-variable subsystem * (sched.h's Thread.env, syscall.c's sysGetEnv/sysSetEnv, shell.c's new * `export`/`env` builtins and `$NAME` expansion): types "export * GREETING=frostbyte" at the real interactive shell (sets it in the * SHELL's own Thread.env, currentThreadSetEnv), then "envprint" (loaded * on demand, /bin/envprint -- proves the freshly spawned child inherited * a real copy via initialEnv and that its own later userSetEnv is visible * to itself), then a final bare "env" (shell.c's listing builtin) whose * output is the actual proof of isolation: if envprint.c's own * userSetEnv("GREETING", "child-mutated") had somehow reached back into * the PARENT shell's table (a shared-pointer bug, not the real * independent-copy semantics initialEnv gives), this "env" would print * "child-mutated" instead of the original "frostbyte" -- the same * "child's own later mutation is proven not to leak back" shape * cwdtest.c's post-userWait getcwd() check already established for * per-process cwd. The actual pass/fail proof for the first two commands * is entirely envprint.c's own doing; this thread's only job is to type * the three commands and give the scheduler real time between them. */ static void envDriverThread(void) { threadSleep(envDriverInitialDelayMs); while (currentThread()->state == threadSleeping) halt(); kprintf("[kbd] typing shell commands for the environment variable demo\n"); jobctlTypeFlushLine(); envTypeExportCommand(); threadSleep(envDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); envTypeEnvprintCommand(); threadSleep(envDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); envTypeEnvCommand(); threadSleep(envDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); for (;;) halt(); } /* '*' is shift+8 on a real US keyboard -- keyboard.c's scancodeUpper table maps index scancodeMakeEight (the same physical key as scancodeLower's plain '8') to '*' only while shiftHeld is true, the same convention scancodeMakeDot/scancodeMakeComma above already use for '>'/'<'. No other new letter is needed for this demo's five commands -- every letter "mkdir"/"echo"/"globcat"/"tmp"/"gd"/"txt"/"log" needs (m,k,d,i,r,e,c,h,o, g,l,b,a,t,p,x,s) was already defined by an earlier block. */ #define scancodeMakeEight 0x09 static void globTypeMkdirCommand(void) { injectKeyPress(scancodeMakeM); injectKeyPress(scancodeMakeK); injectKeyPress(scancodeMakeD); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeR); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeM); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeG); injectKeyPress(scancodeMakeD); injectKeyPress(scancodeMakeEnter); } static void globTypeEchoACommand(void) { injectKeyPress(scancodeMakeE); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeH); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeA); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeDot); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeM); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeG); injectKeyPress(scancodeMakeD); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeA); injectKeyPress(scancodeMakeDot); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeX); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeEnter); } static void globTypeEchoBCommand(void) { injectKeyPress(scancodeMakeE); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeH); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeB); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeDot); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeM); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeG); injectKeyPress(scancodeMakeD); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeB); injectKeyPress(scancodeMakeDot); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeX); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeEnter); } /* Writes /tmp/gd/c.log (NOT .txt) -- the sibling file globTypeGlobcatCommand below's own "*.txt" pattern must NOT match, proving shellExpandGlobs really filters by pattern instead of just listing the whole directory. */ static void globTypeEchoSkipCommand(void) { injectKeyPress(scancodeMakeE); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeH); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeDot); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeM); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeG); injectKeyPress(scancodeMakeD); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeDot); injectKeyPress(scancodeMakeL); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeG); injectKeyPress(scancodeMakeEnter); } static void globTypeGlobcatCommand(void) { injectKeyPress(scancodeMakeG); injectKeyPress(scancodeMakeL); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeB); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeA); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeM); injectKeyPress(scancodeMakeP); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeG); injectKeyPress(scancodeMakeD); injectKeyPress(scancodeMakeSlash); injectShiftedKeyPress(scancodeMakeEight); injectKeyPress(scancodeMakeDot); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeX); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeEnter); } /* 15000ms clears envDriverThread's own total budget (12500 initial + 3*400 settle = 13700ms, above its own real spawn/run/reap time), the same "next demo's initial delay clears the previous demo's total budget" convention every driver thread above already establishes. Settle is 300ms per step, matching redirDriverThread's own choice for similarly trivial commands (a single mkdir or a single short file write, none of which ever sleeps or blocks on a timer) -- comfortable headroom to let the shell finish running each FOREGROUND command before the next one's keystrokes are typed into the same shared 32-byte console-reader ring buffer (keyboard.c). */ #define globDriverInitialDelayMs 15000 #define globDriverStepSettleMs 300 /* * Live ring-3 proof for shell.c's new wildcard glob expansion * (shellExpandGlobs, shellGlobMatch/shellTokenHasGlob/shellSplitGlobToken): * types four setup commands at the real interactive shell (mkdir, then * three "echo X > /tmp/gd/NAME" writes -- two matching *.txt, one * deliberately a *.log that must NOT match), then "globcat /tmp/gd/ *.txt". * shell.c resolves that last argv token against the real tmpfs directory * /tmp/gd (vfsChild/vfsNext, the exact traversal the `ls` builtin already * uses) and replaces it with however many real paths matched BEFORE * globcat.c (loaded on demand, like envprint/hcat/pipecat above) ever runs * a single instruction -- so globcat.c's own argc/argv and each file's * content are the actual proof: exactly 2 files (never 3 -- c.log must be * filtered out by pattern, not just "everything in the directory"), each * one's real resolved path and its own distinct content. The actual * pass/fail proof is entirely globcat.c's own doing; this thread's only job * is to type the five commands and give the scheduler real time between * them. */ static void globDriverThread(void) { threadSleep(globDriverInitialDelayMs); while (currentThread()->state == threadSleeping) halt(); kprintf("[kbd] typing shell commands for the wildcard glob demo\n"); jobctlTypeFlushLine(); globTypeMkdirCommand(); threadSleep(globDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); globTypeEchoACommand(); threadSleep(globDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); globTypeEchoBCommand(); threadSleep(globDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); globTypeEchoSkipCommand(); threadSleep(globDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); globTypeGlobcatCommand(); threadSleep(globDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); for (;;) halt(); } /* 19500ms clears globDriverThread's own total budget (15000 initial + 5*300 settle = 16500ms, plus the real time globcat.c itself needs to spawn/run/read two files/print/exit after that last keystroke) -- the same "next demo's initial delay clears the previous demo's total budget with margin" convention every driver thread above establishes. A single settle step is enough -- see the file comment on lsDriverThread below for why this demo types only ONE command, unlike every prior driver thread's multi-command setup sequence: tools/ci-test.sh's QEMU timeout is a fixed 25 REAL (host wall-clock) seconds shared by the entire boot, and this is already the last of well over a dozen shell-driven demos stacked at increasing absolute timestamps in that same fixed window, so minimizing this demo's own keystroke count directly buys back margin for the whole chain instead of spending it. */ #define lsDriverInitialDelayMs 19500 #define lsDriverStepSettleMs 300 static void lsTypeLongListCommand(void) { injectKeyPress(scancodeMakeL); injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeHyphen); injectKeyPress(scancodeMakeL); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeM); injectKeyPress(scancodeMakeN); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeSlash); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeI); injectKeyPress(scancodeMakeM); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeT); injectKeyPress(scancodeMakeEnter); } /* * Live proof for shell.c's new generalized `ls`/`ls -l` (listDir/ * printLsEntry): types `ls -l /mnt/timetest` at the real interactive * shell. `/mnt/timetest` is deliberately NOT created by this demo -- * timetest.c (FALA 39, boot-time synchronous, long finished by the time * any shell-driven demo runs) already left it behind with two real files * (data.txt, and alias.txt, a hardlink sharing the same inode/mtime) once * `timetest: PASS` prints, so this demo needs exactly ONE typed command * instead of the usual "mkdir + write setup, then the command under test" * shape every earlier shell demo used -- both a deliberate budget saving * (see lsDriverInitialDelayMs's own comment) and, incidentally, a second * independent proof that `ls -l`'s real stat data (size, mtime) matches * what timetest.c's own syscall-level userStat already established for * the exact same on-disk inode, through a completely different code path * (the interactive shell builtin instead of a ring-3 program's syscalls). * `/mnt`, not `/tmp`, is the whole point -- the OLD `ls` only ever * understood the tmpfs VfsNode tree via vfsLookup/vfsChild/vfsNext, so any * `/mnt` directory silently reported "no such path" no matter what it * actually held (fstest.c/linktest.c could only ever prove SFS directory * listing through a raw userGetdents syscall, never through this * builtin). The needles tools/ci-test.sh checks for are the deterministic * type+size+name prefix of each `-l` output line ("f data.txt " and * "f alias.txt ", both files' real on-disk size) plus a "20" year- * prefix right after one of them, proving a real mtime got formatted and * printed (sfs.h's on-disk timestamp, rtc.c's new rtcCivilFromEpoch) * rather than silently falling back to the "no timestamp" dash this same * code path prints for tmpfs/dev entries. */ static void lsDriverThread(void) { threadSleep(lsDriverInitialDelayMs); while (currentThread()->state == threadSleeping) halt(); kprintf("[kbd] typing shell commands for the ls -l demo\n"); jobctlTypeFlushLine(); lsTypeLongListCommand(); threadSleep(lsDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); for (;;) halt(); } /* 20500ms clears lsDriverThread's own total budget (19500 initial + 300 settle = 19800ms) with 700ms margin -- smaller than most prior driver threads' own margin since `ls -l` is a plain builtin (listDir, shell.c) with no process spawn/VFS-blocking wait after its own settle delay, unlike e.g. globDriverThread's globcat.c spawn+read+compare, so there is no extra real execution time to wait out beyond the typing itself. Same "next demo's initial delay clears the previous demo's total budget with margin" convention every driver thread above establishes. Only ONE command, ONE settle step -- false.c is the smallest possible spawned program in this kernel (immediate userExit, no VFS/pipe/signal work at all), and this fala's own live demo needs only one line of it (see andOrTypeFailChainCommand's own comment for why one line already proves the whole feature), the same budget-consciousness lsDriverInitialDelayMs's own comment established: an EARLIER version of this demo also typed a second line ("true && echo aoy", proving `&&` fires on a real SUCCESS too) and was cut after it measurably pushed one real re-run over tools/ci-test.sh's fixed 25-real-second QEMU ceiling under this session's own elevated host load -- not worth the extra margin lost for a case the KTEST-level shellSplitAndOr tests (which assert both shellAndOrAnd and shellAndOrOr are parsed and recorded correctly) and this project's dozens of pre-existing successful `cmd && cmd`-free typed commands already make low-risk. */ #define andOrDriverInitialDelayMs 20500 #define andOrDriverStepSettleMs 300 /* "false && echo X || echo ao2" -- the classic shell if-then-else idiom in one line. false.c genuinely spawns and exits 1 (see its own file comment); `&&` sees that real nonzero status and SKIPS "echo X" (a builtin, would otherwise print instantly); the `||` check right after it is then evaluated against the SAME status "echo X" never changed (shellSplitAndOr/execute()'s own "a skipped segment leaves status completely unchanged" contract, shell.h) -- still nonzero, so it DOES run "echo ao2". The needle `ao2` can only ever appear if both the skip and the carry-through past that skip worked correctly; "X" is deliberately never checked for (this kernel's tools/ci-test.sh only ever asserts PRESENCE of a needle, never absence), so this line's own affirmative proof is entirely in `ao2` actually appearing at all. Every character this line needs (f/a/l/s/e/space/&/h/c/o/x/|/a/o/2) is already defined by an earlier demo's own scancode block in this file. */ static void andOrTypeFailChainCommand(void) { injectKeyPress(scancodeMakeF); injectKeyPress(scancodeMakeA); injectKeyPress(scancodeMakeL); injectKeyPress(scancodeMakeS); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeSeven); injectShiftedKeyPress(scancodeMakeSeven); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeH); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeX); injectKeyPress(scancodeMakeSpace); injectShiftedKeyPress(scancodeMakeBackslash); injectShiftedKeyPress(scancodeMakeBackslash); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeE); injectKeyPress(scancodeCPressMake); injectKeyPress(scancodeMakeH); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeSpace); injectKeyPress(scancodeMakeA); injectKeyPress(scancodeMakeO); injectKeyPress(scancodeMakeTwo); injectKeyPress(scancodeMakeEnter); } /* * Live ring-3 proof for shell.c's new `cmd1 && cmd2 || cmd3 ...` * conditional chaining (shellSplitAndOr, executeSingle/execute) -- types * the command above at the real interactive shell, the same * injected-keystroke mechanism every shell-driven demo above already * uses. Unlike every earlier shell-syntax demo (pipelines, redirects, * here-strings, globs), the actual subject under test here is false.c -- * a brand-new, deliberately trivial user program whose entire job is to * hand back a real, predictable, nonzero exit code from a genuine ring-3 * spawn (see its own file comment) -- proving the new chaining logic * reacts to a REAL process's own exit status, not just a builtin's * in-kernel int return (which listDir/catFile/makeDir/changeDir/ * doExport/doUnset already exercise on every OTHER typed command in this * whole boot sequence, just never through `&&`/`||` before this fala). */ static void andOrDriverThread(void) { threadSleep(andOrDriverInitialDelayMs); while (currentThread()->state == threadSleeping) halt(); kprintf("[kbd] typing shell commands for the &&/|| chaining demo\n"); jobctlTypeFlushLine(); andOrTypeFailChainCommand(); threadSleep(andOrDriverStepSettleMs); while (currentThread()->state == threadSleeping) halt(); for (;;) halt(); } void kmain(uint32 magic, uint64 multibootInfo) { consoleInit(); banner(); if (magic == multibootBootMagic) kprintf("[boot] multiboot2 magic verified (%x)\n", magic); else kprintf("[boot] unexpected boot magic %x\n", magic); gdtInit(); kprintf("[gdt] kernel + user segments, tss slot reserved\n"); idtInit(); kprintf("[idt] 256 vectors (32 exceptions, 16 irqs, int 0x80 syscall)\n"); picRemap(); for (uint8 irq = 0; irq < 16; irq++) picSetMask(irq); picClearMask(0); picClearMask(1); kprintf("[pic] remapped to 0x20-0x2f, irq0 and irq1 unmasked\n"); uint64 moduleStart = 0; uint64 moduleEnd = 0; bool haveModule = multibootFindModule(multibootInfo, &moduleStart, &moduleEnd); pmmInit(multibootInfo, moduleEnd); kprintf("[pmm] %lu KiB usable, %lu frames, %lu free after kernel\n", pmmTotalBytes() / 1024, pmmTotalFrames(), pmmFreeFrames()); uint64 heapStart = pmmReservedEnd(); pmmReserve(heapStart, heapSize); heapInit(heapStart, heapSize); kprintf("[heap] free-list allocator at %p, %u KiB\n", (void*)heapStart, heapSize / 1024); runHeapTest(); runPagingTest(); sigInstallTrampoline(); kprintf("[sig] shared signal trampoline mapped at %p\n", (void*)sigTrampolineVirtualAddr); sigInstallInfoPage(); kprintf("[sig] shared siginfo page(s) mapped at %p\n", (void*)sigInfoVirtualAddr); pitInit(timerFrequency); kprintf("[pit] timer at %u Hz\n", timerFrequency); keyboardInit(); kprintf("[kbd] ps/2 keyboard driver ready\n"); schedInit(); tssInit((uint64)(bootstrapKernelStack + sizeof(bootstrapKernelStack))); kprintf("[tss] loaded, rsp0 bootstrap stack installed\n"); vfsInit(); if (haveModule) vfsMountUstar(moduleStart, moduleEnd); else kprintf("[vfs] no multiboot2 module present\n"); vfsMountTmpfs("/tmp"); /* namedreader/namedwriter (spawned below, after userland launch) are two INDEPENDENT boot programs with no parent-child relationship and no shared argv -- unlike fifomaster.c's children, which get a pipe.h handle number handed to them, these two only agree on this path. The fifo has to exist before either one can open it. */ if (vfsMkfifo("/tmp/rendezvous") == 0) kprintf("[vfs] mkfifo /tmp/rendezvous ready\n"); else kprintf("[vfs] mkfifo /tmp/rendezvous FAILED\n"); if (ataInit()) kprintf("[ata] primary master '%s', %lu sectors (%lu MiB), /dev/disk0 ready\n", ataModelString(), ataSectorCount(), (ataSectorCount() * ataSectorSize) / (1024 * 1024)); else kprintf("[ata] no primary master drive detected, /dev/disk0 will read as empty\n"); if (sfsMount()) kprintf("[sfs] mounted persistent filesystem at /mnt (%lu KiB usable)\n", sfsCapacityBytes() / 1024); else kprintf("[sfs] no usable disk for /mnt (drive missing or too small)\n"); ktestRunAll(); threadCreate("shell", shellThread); uint32 workerAId = threadCreate("workerA", workerA); uint32 workerBId = threadCreate("workerB", workerB); /* Both workers are pure CPU hogs (busy loops, never sleep or block), so they are the cleanest possible demonstration that the CFS-lite scheduler (sched.c/prioq.h) actually enforces weight, not just that it compiles: workerA is given 3x workerB's weight, so it should accrue vruntime three times slower and therefore win the ready-queue's min-vruntime pick roughly three times as often. Compare the two threads' printed iteration counts at similar tick values to see it. */ threadSetPriority(workerAId, schedDefaultWeight * schedPriorityWeightRatio); kprintf("[sched] workerA pid=%u weight=%u, workerB pid=%u weight=%u " "(CFS-lite: higher weight runs more often)\n", workerAId, schedDefaultWeight * schedPriorityWeightRatio, workerBId, schedDefaultWeight); threadCreate("schedVerify", schedPriorityVerifierThread); pipeDemoHandle = pipeCreate(); if (pipeDemoHandle >= 0) { threadCreate("pipeProducer", pipeProducerThread); threadCreate("pipeConsumer", pipeConsumerThread); } else { kprintf("[pipe] demo pipe creation failed\n"); } joinDemoWorkerId = threadCreate("joinWorker", shortLivedWorkerThread); if (joinDemoWorkerId != threadInvalidId) threadCreate("joiner", joinerThread); else kprintf("[sched] join demo worker not started: thread table full\n"); kprintf("\n[boot] launching userland programs from the ramdisk into ring 3\n"); spawnUser("/bin/hello", "hello", 0x48000000, 0); spawnUser("/bin/counter", "counter", 0x58000000, 0); spawnUser("/bin/sbrktest", "sbrktest", 0x68000000, 0x64000000); spawnUser("/bin/fileread", "fileread", 0x78000000, 0); spawnUser("/bin/pipetest", "pipetest", 0x88000000, 0); spawnUser("/bin/disktest", "disktest", 0x98000000, 0); spawnUser("/bin/fstest", "fstest", 0xA8000000, 0); spawnUser("/bin/linktest", "linktest", 0xB8000000, 0); spawnUser("/bin/fifomaster", "fifomaster", 0x118000000, 0); spawnUser("/bin/killtest", "killtest", 0x148000000, 0); spawnUser("/bin/namedreader", "namedreader", 0x168000000, 0); spawnUser("/bin/namedwriter", "namedwriter", 0x178000000, 0); spawnUser("/bin/consolereader1", "consolereader1", 0x188000000, 0); spawnUser("/bin/consolereader2", "consolereader2", 0x198000000, 0); threadCreate("consoleInjector", consoleInjectorThread); spawnUser("/bin/sigtest", "sigtest", 0x1A8000000, 0); spawnUser("/bin/jobtest", "jobtest", 0x1D8000000, 0); spawnUser("/bin/nesttest", "nesttest", 0x1F8000000, 0); spawnUser("/bin/eintrtest", "eintrtest", 0x218000000, 0); spawnUser("/bin/restarttest", "restarttest", 0x238000000, 0); spawnUser("/bin/masktest", "masktest", 0x258000000, 0); spawnUser("/bin/sigchldtest", "sigchldtest", 0x278000000, 0); spawnUser("/bin/waitanytest", "waitanytest", 0x298000000, 0); spawnUser("/bin/stopchldtest", "stopchldtest", 0x2C8000000, 0); spawnUser("/bin/nodefertest", "nodefertest", 0x2E8000000, 0); spawnUser("/bin/alarmtest", "alarmtest", 0x2F8000000, 0); spawnUser("/bin/siginfotest", "siginfotest", 0x318000000, 0); spawnUser("/bin/selfstoptest", "selfstoptest", 0x3F8000000, 0); spawnUser("/bin/cwdtest", "cwdtest", 0x458000000, 0); spawnUser("/bin/nicetest", "nicetest", 0x4A8000000, 0); spawnUser("/bin/date", "date", 0x4B8000000, 0); spawnUser("/bin/timetest", "timetest", 0x4D8000000, 0); spawnUser("/bin/rmtest", "rmtest", 0x508000000, 0); spawnUser("/bin/permtest", "permtest", 0x518000000, 0); spawnUser("/bin/identtest", "identtest", 0x528000000, 0); spawnUser("/bin/dirpermtest", "dirpermtest", 0x538000000, 0); spawnUser("/bin/grouptest", "grouptest", 0x548000000, 0); spawnUser("/bin/pidfdtest", "pidfdtest", 0x558000000, 0); /* mmapprotvictim and mmapexecvictim are deliberately NOT spawned here -- like killvictim (killtest's own runtime-spawned target, elsewhere in this function), both are only ever loaded at runtime, by mmaptest itself (userSpawn), never directly by kmain.c. */ spawnUser("/bin/mmaptest", "mmaptest", 0x568000000, 0); spawnUser("/bin/polltest", "polltest", 0x378000000, 0); pgroupfgPid = spawnUser("/bin/pgroupfg", "pgroupfg", 0x338000000, 0); pgroupbgPid = spawnUser("/bin/pgroupbg", "pgroupbg", 0x348000000, 0); if (pgroupfgPid != threadInvalidId) { /* Real shell/tcsetpgrp behavior: whichever job was just launched in the foreground claims the terminal explicitly -- pgroupbg is deliberately left alone (its own default group, never named here), the same way a background job never touches the controlling terminal at all. */ keyboardSetForegroundPgid(pgroupfgPid); } if (pgroupfgPid != threadInvalidId && pgroupbgPid != threadInvalidId) threadCreate("pgroupInjector", pgroupInjectorThread); /* jobctlfg/jobctlbg are deliberately NOT spawned here -- unlike every program above, they are only ever loaded on demand, at runtime, by the real interactive shell (shellRun, threadCreate'd near the top of this function) once jobctlDriverThread types their names at it. */ threadCreate("jobctlDriver", jobctlDriverThread); /* pipesrc/pipesink are, like jobctlfg/jobctlbg just above, deliberately NOT spawned here -- loaded on demand by the real shell once pipelineDriverThread types "pipesrc | pipesink" at it. */ threadCreate("pipelineDriver", pipelineDriverThread); /* redirwriter/redirreader are, like pipesrc/pipesink just above, deliberately NOT spawned here -- loaded on demand by the real shell once redirDriverThread types the redirected commands at it. */ threadCreate("redirDriver", redirDriverThread); /* stderrwriter/stderrreader are, like redirwriter/redirreader just above, deliberately NOT spawned here -- loaded on demand by the real shell once stderrDriverThread types the stderr-redirected commands at it. */ threadCreate("stderrDriver", stderrDriverThread); /* herestest is, like stderrwriter/stderrreader just above, deliberately NOT spawned here -- loaded on demand by the real shell once hereDriverThread types the here-string command at it. */ threadCreate("hereDriver", hereDriverThread); /* hcat/hsink are, like herestest just above, deliberately NOT spawned here -- loaded on demand by the real shell once hereStringPipelineDriverThread types the here-string-into-pipeline command at it. */ threadCreate("hereStringPipelineDriver", hereStringPipelineDriverThread); /* pipecat is, like hcat/hsink just above, deliberately NOT spawned here -- loaded on demand by the real shell once threeStagePipelineDriverThread types the three-stage pipeline command at it (pipesrc/pipesink themselves are also on-demand-only, unchanged since FALA 29). */ threadCreate("threeStagePipelineDriver", threeStagePipelineDriverThread); /* envprint is, like pipecat just above, deliberately NOT spawned here -- loaded on demand by the real shell once envDriverThread types "export GREETING=frostbyte" then "envprint" at it. */ threadCreate("envDriver", envDriverThread); /* globcat is, like envprint just above, deliberately NOT spawned here -- loaded on demand by the real shell once globDriverThread types "globcat /tmp/gd/ *.txt" at it (shell.c's new wildcard glob expansion). */ threadCreate("globDriver", globDriverThread); /* No new /bin binary here -- lsDriverThread only exercises shell.c's existing mkdir/echo/ls builtins, never spawns anything. */ threadCreate("lsDriver", lsDriverThread); /* false is, like globcat above, deliberately NOT spawned here -- loaded on demand by the real shell once andOrDriverThread types the `&&`/`||`-chained command at it (shell.c's new conditional chaining). */ threadCreate("andOrDriver", andOrDriverThread); execDemoPid = spawnUser("/bin/proctest", "proctest", 0xF8000000, 0); if (execDemoPid != threadInvalidId) threadCreate("execReaper", execReaperThread); kprintf("[sched] %u threads total (idle, shell, 2 workers, priority verifier, user programs)\n", threadCount()); kprintf("\n[boot] enabling interrupts, starting preemptive scheduler\n\n"); schedStart(); }