From b4f8d5215f30e3df8383edc697db1302ab434cc3 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 19:14:06 +0200 Subject: [PATCH] feat: extend kmain with execReaperThread and consoleInjectorThread --- src/kmain.c | 194 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 192 insertions(+), 2 deletions(-) diff --git a/src/kmain.c b/src/kmain.c index cdea35e..b209fa8 100644 --- a/src/kmain.c +++ b/src/kmain.c @@ -307,13 +307,203 @@ static uint32 spawnUser(const char* path, const char* name, uint64 stackTop, uin return threadInvalidId; } - pagingMapUserRange(stackTop - userStackSize, userStackSize); - uint32 pid = threadCreateUser(name, entry, stackTop); + 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 } void kmain(uint32 magic, uint64 multibootInfo)