Phase 4: keyboard driver, shell

Bash · Jul 17, 19:16:40 · toolu_01BmihPJbWhzNWbsVsxXzPch
Commit Phase 3 disk driver and filesystem
$ git commit -m "$(cat <<'EOF'
Phase 3: ATA disk driver + tinyfs filesystem

Adds a polled ATA PIO (LBA28) driver and tinyfs: a minimal flat-namespace
filesystem (superblock + free-block bitmap + fixed inode table + direct
data blocks, 512-byte blocks, 4KB max file size). The core format logic
in kernel/fs/tinyfs.c is compiled twice - freestanding for the kernel
(via ata.c) and natively for tools/mkfs_tinyfs, a host build-time tool
that formats the disk image and seeds it with an initial file - so
there is exactly one implementation of the on-disk format, not two.

Also fixes a latent bug: the raw disk-loaded kernel image was never
guaranteed to have zero-initialized .bss (objcopy -O binary drops NOBITS
sections), so entry.S now zeroes .bss explicitly before calling kmain.

kmain mounts tinyfs at boot and the self-test now covers list/read of
the seeded file plus a create/write/read/delete roundtrip, verified
green in headless QEMU via tools/run_tests.sh.
EOF
)"
result
[main a3b80d1] Phase 3: ATA disk driver + tinyfs filesystem
 13 files changed, 620 insertions(+), 8 deletions(-)
 create mode 100644 docs/tinyfs.md
 create mode 100644 kernel/ata.c
 create mode 100644 kernel/ata.h
 create mode 100644 kernel/fs/tinyfs.c
 create mode 100644 kernel/fs/tinyfs.h
 create mode 100644 kernel/fs/tinyfs_disk.c
 create mode 100644 kernel/fs/tinyfs_disk.h
 create mode 100644 tools/mkfs_tinyfs.c
TaskUpdate · Jul 17, 19:16:42 · toolu_01DdQzwkK4yuTewafM5n3Qq4
{
 "taskId": "4",
 "status": "completed"
}
result
Updated task #4 status
TaskUpdate · Jul 17, 19:16:43 · toolu_01WTkMcyeWuvfmu6UB9sw8r8
{
 "taskId": "5",
 "status": "in_progress"
}
result
Updated task #5 status
agent · Jul 17, 19:17:49
Now the keyboard driver and shell. First checking what's available for scripting keystrokes into headless QEMU for the test harness.
Bash · Jul 17, 19:17:50 · toolu_01PRRbuBwy7oMJpc2P3ekaxH
Check availability of socat/nc/python3 for scripting QEMU monitor input
$ which socat nc ncat python3 2>/dev/null; echo "---"; qemu-system-i386 -monitor help 2>&1 | head -5; echo "---"; echo 'test' | timeout 1 bash -c 'exec 3<>/dev/tcp/127.0.0.1/1 2>&1' ; echo "bash /dev/tcp support: $?"
result
/bin/socat
/bin/nc
/bin/python3
---
qemu-system-i386: -monitor help: 'help' is not a valid char driver
qemu-system-i386: -monitor help: parse error: help
---
bash: connect: Connection refused
bash: line 1: /dev/tcp/127.0.0.1/1: Connection refused
bash /dev/tcp support: 1
agent · Jul 17, 19:18:31
Let me validate the QEMU monitor + `sendkey` mechanism works before building the full harness around it.
Bash · Jul 17, 19:18:34 · toolu_01DX1bHiEQVWQBgUsbHu4wJM
Smoke test QEMU monitor unix socket + sendkey command
$ cd /home/martin/workspace/prototypes/boot-to-claude
rm -f build/mon.sock build/serial.log
timeout 6 qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/mon.sock,server,nowait -no-reboot &
QPID=$!
for i in $(seq 1 25); do [ -S build/mon.sock ] && break; sleep 0.2; done
sleep 0.5
python3 - <<'EOF'
import socket, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect("build/mon.sock")
time.sleep(0.2)
print("banner:", s.recv(4096))
s.sendall(b"sendkey a\n")
time.sleep(0.2)
print("resp:", s.recv(4096))
s.close()
EOF
wait $QPID 2>/dev/null
cat build/serial.log
result
banner: b"QEMU 10.1.0 monitor - type 'help' for more information\r\n(qemu) "
resp: b's\x1b[K\x1b[Dse\x1b[K\x1b[D\x1b[Dsen\x1b[K\x1b[D\x1b[D\x1b[Dsend\x1b[K\x1b[D\x1b[D\x1b[D\x1b[Dsendk\x1b[K\x1b[D\x1b[D\x1b[D\x1b[D\x1b[Dsendke\x1b[K\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[Dsendkey\x1b[K\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[Dsendkey \x1b[K\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[Dsendkey a\x1b[K\r\n(qemu) '
qemu-system-i386: terminating on signal 15 from pid 1877182 (timeout)
tiny-os: kernel entered
selftest: running
SELFTEST PASS
agent · Jul 17, 19:18:47
Mechanism confirmed. Now implementing the keyboard driver and shell.
Bash · Jul 17, 19:18:47 · toolu_017Sb79ZBuw5TZRqapHrbdqN
Create shell directory
$ mkdir -p /home/martin/workspace/prototypes/boot-to-claude/kernel/shell
result
(Bash completed with no output)
Write · Jul 17, 19:18:52 · toolu_01LxQHtgWgH4kAgTWatbkSgd
/home/martin/workspace/prototypes/boot-to-claude/kernel/keyboard.h
#pragma once

/* Blocks until a key is pressed and returns its ASCII value. Polled PS/2,
 * scancode set 1, US layout subset - no interrupts, matching the rest of
 * this single-tasking kernel. */
char keyboard_getchar(void);
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/keyboard.h (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:19:01 · toolu_01PPpvcZkmusPFxvEykP7nAa
/home/martin/workspace/prototypes/boot-to-claude/kernel/keyboard.c
66 lines
#include "keyboard.h"

#include <stddef.h>
#include <stdint.h>

#define KBD_DATA   0x60
#define KBD_STATUS 0x64

#define SC_LSHIFT 0x2A
#define SC_RSHIFT 0x36

static inline uint8_t inb(uint16_t port) {
    uint8_t ret;
    __asm__ volatile ("inb %1, %0" : "=a"(ret) : "Nd"(port));
    return ret;
}

/* Scancode set 1, indexed by make code. 0 = unmapped/non-printable. */
static const char lower_table[] = {
    0,    27,  '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b',
    '\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n',
    0 /*ctrl*/, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '`',
    0 /*lshift*/, '\\', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/',
    0 /*rshift*/, '*', 0 /*alt*/, ' ',
};

static const char upper_table[] = {
    0,    27,  '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', '\b',
    '\t', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '\n',
    0, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', '~',
    0, '|', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?',
    0, '*', 0, ' ',
};

#define TABLE_LEN (sizeof(lower_table) / sizeof(lower_table[0]))

static int shift_down = 0;

static int scancode_ready(void) {
    return inb(KBD_STATUS) & 1;
}

char keyboard_getchar(void) {
    for (;;) {
        while (!scancode_ready()) {}
        uint8_t sc = inb(KBD_DATA);

        int release = sc & 0x80;
        uint8_t code = sc & 0x7F;

        if (code == SC_LSHIFT || code == SC_RSHIFT) {
            shift_down = !release;
            continue;
        }
        if (release || code >= TABLE_LEN) {
            continue;
        }

        char c = shift_down ? upper_table[code] : lower_table[code];
        if (c == 0) {
            continue;
        }
        return c;
    }
}
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/keyboard.c (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:19:03
Now update `console.c` to handle backspace properly:
Edit · Jul 17, 19:19:05 · toolu_011tRzbwYxa3UFdYc1NNj5PA
/home/martin/workspace/prototypes/boot-to-claude/kernel/console.c
- void console_putc(char c) {
-     if (c == '\n') {
-         cursor_col = 0;
-         cursor_row++;
-     } else {
+ void console_putc(char c) {
+     if (c == '\n') {
+         cursor_col = 0;
+         cursor_row++;
+     } else if (c == '\b') {
+         if (cursor_col > 0) {
+             cursor_col--;
+         } else if (cursor_row > 0) {
+             cursor_row--;
+             cursor_col = VGA_COLS - 1;
+         }
+     } else {
result
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/console.c has been updated successfully. (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:19:08
Now the shell:
Write · Jul 17, 19:19:09 · toolu_01XUwatxZjRsjL6MZ7LkpCzv
/home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.h
#pragma once

/* Interactive command loop. Never returns. */
void shell_run(void);
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.h (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:19:29 · toolu_017BooVsJYji3obZXogEDDq6
/home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c
194 lines
#include "shell.h"

#include <stdint.h>

#include "console.h"
#include "fs/tinyfs.h"
#include "keyboard.h"
#include "serial.h"
#include "string.h"

#define LINE_MAX 128

static inline void outb(uint16_t port, uint8_t val) {
    __asm__ volatile ("outb %0, %1" : : "a"(val), "Nd"(port));
}

static inline uint8_t inb(uint16_t port) {
    uint8_t ret;
    __asm__ volatile ("inb %1, %0" : "=a"(ret) : "Nd"(port));
    return ret;
}

static void echo_char(char c) {
    console_putc(c);
    serial_putc(c);
}

static void print_line(const char *s) {
    console_write(s);
    console_write("\n");
    serial_write(s);
    serial_write("\n");
}

static int read_line(char *buf, int max_len) {
    int len = 0;
    for (;;) {
        char c = keyboard_getchar();
        if (c == '\n') {
            echo_char('\n');
            buf[len] = '\0';
            return len;
        } else if (c == '\b') {
            if (len > 0) {
                len--;
                echo_char('\b');
                echo_char(' ');
                echo_char('\b');
            }
        } else if (c >= 32 && c < 127 && len < max_len - 1) {
            buf[len++] = c;
            echo_char(c);
        }
    }
}

static void skip_spaces(const char **p) {
    while (**p == ' ') {
        (*p)++;
    }
}

static int next_word(const char **p, char *out, int max_len) {
    skip_spaces(p);
    int n = 0;
    while (**p && **p != ' ' && n < max_len - 1) {
        out[n++] = *(*p)++;
    }
    out[n] = '\0';
    return n;
}

static void cmd_help(void) {
    print_line("commands: ls, cat <file>, write <file> <text>, rm <file>, help, clear, reboot");
}

static void cmd_ls(void) {
    tinyfs_dirent_t entries[TINYFS_NUM_INODES];
    int n = tinyfs_list(entries, TINYFS_NUM_INODES);
    char numbuf[16];
    for (int i = 0; i < n; i++) {
        console_write(entries[i].name);
        console_write("  ");
        console_write(itoa((int)entries[i].size, numbuf, 10));
        console_write("\n");
        serial_write(entries[i].name);
        serial_write("  ");
        serial_write(itoa((int)entries[i].size, numbuf, 10));
        serial_write("\n");
    }
}

static void cmd_cat(const char *name) {
    static char buf[TINYFS_MAX_FILE_SIZE + 1];
    uint32_t len = 0;
    if (tinyfs_read(name, buf, sizeof(buf) - 1, &len) != 0) {
        print_line("cat: no such file");
        return;
    }
    buf[len] = '\0';
    console_write(buf);
    serial_write(buf);
    if (len == 0 || buf[len - 1] != '\n') {
        console_write("\n");
        serial_write("\n");
    }
}

static void cmd_write(const char *name, const char *contents) {
    tinyfs_create(name); /* ignored if it already exists - write() below overwrites it */
    uint32_t len = (uint32_t)strlen(contents);
    if (len > TINYFS_MAX_FILE_SIZE) {
        print_line("write: content too long");
        return;
    }
    if (tinyfs_write(name, contents, len) != 0) {
        print_line("write: failed (no space or bad name)");
        return;
    }
    print_line("write: ok");
}

static void cmd_rm(const char *name) {
    if (tinyfs_delete(name) != 0) {
        print_line("rm: no such file");
    } else {
        print_line("rm: ok");
    }
}

static void cmd_reboot(void) {
    print_line("rebooting...");
    uint8_t status;
    do {
        status = inb(0x64);
    } while (status & 0x02);
    outb(0x64, 0xFE); /* pulse the keyboard controller's reset line */
    for (;;) {
        __asm__ volatile ("hlt");
    }
}

void shell_run(void) {
    static char line[LINE_MAX];

    print_line("tiny-os shell. type 'help' for commands.");

    for (;;) {
        console_write("> ");
        serial_write("> ");
        read_line(line, sizeof(line));

        const char *p = line;
        char cmd[32];
        if (next_word(&p, cmd, sizeof(cmd)) == 0) {
            continue;
        }

        if (strcmp(cmd, "help") == 0) {
            cmd_help();
        } else if (strcmp(cmd, "ls") == 0) {
            cmd_ls();
        } else if (strcmp(cmd, "clear") == 0) {
            console_clear();
        } else if (strcmp(cmd, "reboot") == 0) {
            cmd_reboot();
        } else if (strcmp(cmd, "cat") == 0) {
            char name[TINYFS_NAME_MAX];
            if (next_word(&p, name, sizeof(name)) == 0) {
                print_line("usage: cat <file>");
            } else {
                cmd_cat(name);
            }
        } else if (strcmp(cmd, "rm") == 0) {
            char name[TINYFS_NAME_MAX];
            if (next_word(&p, name, sizeof(name)) == 0) {
                print_line("usage: rm <file>");
            } else {
                cmd_rm(name);
            }
        } else if (strcmp(cmd, "write") == 0) {
            char name[TINYFS_NAME_MAX];
            if (next_word(&p, name, sizeof(name)) == 0) {
                print_line("usage: write <file> <text>");
            } else {
                skip_spaces(&p);
                cmd_write(name, p);
            }
        } else {
            print_line("unknown command (type 'help')");
        }
    }
}
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:19:31
Now wire the shell into `kmain.c`:
Edit · Jul 17, 19:19:33 · toolu_014w1Rtb5twz2hythTsaDTAW
/home/martin/workspace/prototypes/boot-to-claude/kernel/kmain.c
- #include "console.h"
- #include "fs/tinyfs_disk.h"
- #include "gdt.h"
- #include "idt.h"
- #include "memory.h"
- #include "selftest.h"
- #include "serial.h"
+ #include "console.h"
+ #include "fs/tinyfs_disk.h"
+ #include "gdt.h"
+ #include "idt.h"
+ #include "memory.h"
+ #include "selftest.h"
+ #include "serial.h"
+ #include "shell/shell.h"
result
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/kmain.c has been updated successfully. (file state is current in your context — no need to Read it back)
Edit · Jul 17, 19:19:37 · toolu_01LTwTHuXTcrzZa3AsLfYi8q
/home/martin/workspace/prototypes/boot-to-claude/kernel/kmain.c
-     selftest_run();
- 
-     for (;;) {
-         __asm__ volatile ("hlt");
-     }
- }
+     selftest_run();
+ 
+     shell_run();
+ }
result
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/kmain.c has been updated successfully. (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:19:40
Now update the Makefile to include the new sources:
Edit · Jul 17, 19:19:42 · toolu_01Wdiocc7FAZZTiqZiVxioe6
/home/martin/workspace/prototypes/boot-to-claude/Makefile
- KERNEL_C_SRCS := kernel/kmain.c kernel/console.c kernel/serial.c kernel/gdt.c \
-                  kernel/idt.c kernel/string.c kernel/memory.c kernel/selftest.c \
-                  kernel/ata.c kernel/fs/tinyfs.c kernel/fs/tinyfs_disk.c
+ KERNEL_C_SRCS := kernel/kmain.c kernel/console.c kernel/serial.c kernel/gdt.c \
+                  kernel/idt.c kernel/string.c kernel/memory.c kernel/selftest.c \
+                  kernel/ata.c kernel/fs/tinyfs.c kernel/fs/tinyfs_disk.c \
+                  kernel/keyboard.c kernel/shell/shell.c
result
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
Edit · Jul 17, 19:19:49 · toolu_01J5LGLcdnhZ2n9VBbPKJk4D
/home/martin/workspace/prototypes/boot-to-claude/Makefile
- $(BUILD) $(BUILD)/fs:
- 	mkdir -p $@
- 
- $(BOOT_BIN): boot/boot.S | $(BUILD)
- 	$(AS) -f bin -o $@ $<
- 
- $(STAGE2_BIN): boot/stage2.S | $(BUILD)
- 	$(AS) -f bin -o $@ $<
- 
- $(BUILD)/%.o: kernel/%.S | $(BUILD) $(BUILD)/fs
- 	$(AS) -f elf32 -o $@ $<
- 
- $(BUILD)/%.o: kernel/%.c | $(BUILD) $(BUILD)/fs
- 	$(CC) $(CFLAGS) -c -o $@ $<
+ $(BUILD) $(BUILD)/fs $(BUILD)/shell:
+ 	mkdir -p $@
+ 
+ $(BOOT_BIN): boot/boot.S | $(BUILD)
+ 	$(AS) -f bin -o $@ $<
+ 
+ $(STAGE2_BIN): boot/stage2.S | $(BUILD)
+ 	$(AS) -f bin -o $@ $<
+ 
+ $(BUILD)/%.o: kernel/%.S | $(BUILD) $(BUILD)/fs $(BUILD)/shell
+ 	$(AS) -f elf32 -o $@ $<
+ 
+ $(BUILD)/%.o: kernel/%.c | $(BUILD) $(BUILD)/fs $(BUILD)/shell
+ 	$(CC) $(CFLAGS) -c -o $@ $<
result
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
agent · Jul 17, 19:19:56
Now build and check size budget (kernel is capped at 64 sectors / 32KB):
Bash · Jul 17, 19:19:57 · toolu_01AaXoyLHsCW6iPVG6fjVzbj
Rebuild with Phase 4 keyboard/shell additions
$ make clean >/dev/null && make 2>&1
result
32 lines
mkdir -p build
nasm -f bin -o build/boot.bin boot/boot.S
nasm -f bin -o build/stage2.bin boot/stage2.S
mkdir -p build/fs
mkdir -p build/shell
nasm -f elf32 -o build/entry.o kernel/entry.S
nasm -f elf32 -o build/idt_stubs.o kernel/idt_stubs.S
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/kmain.o kernel/kmain.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/console.o kernel/console.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/serial.o kernel/serial.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/gdt.o kernel/gdt.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/idt.o kernel/idt.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/string.o kernel/string.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/memory.o kernel/memory.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/selftest.o kernel/selftest.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/ata.o kernel/ata.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/fs/tinyfs.o kernel/fs/tinyfs.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/fs/tinyfs_disk.o kernel/fs/tinyfs_disk.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/keyboard.o kernel/keyboard.c
gcc -m32 -ffreestanding -fno-pic -fno-stack-protector -fno-builtin -nostdlib -Wall -Wextra -O2 -std=c11 -Ikernel -c -o build/shell/shell.o kernel/shell/shell.c
ld -m elf_i386 -T kernel/link.ld -nostdlib -o build/kernel.elf build/entry.o build/idt_stubs.o build/kmain.o build/console.o build/serial.o build/gdt.o build/idt.o build/string.o build/memory.o build/selftest.o build/ata.o build/fs/tinyfs.o build/fs/tinyfs_disk.o build/keyboard.o build/shell/shell.o
ld: warning: build/kernel.elf has a LOAD segment with RWX permissions
objcopy -O binary build/kernel.elf build/kernel.bin
gcc -Wall -Wextra -O2 -c -o build/host_tinyfs.o kernel/fs/tinyfs.c
gcc -Wall -Wextra -O2 -c -o build/host_mkfs_tinyfs.o tools/mkfs_tinyfs.c
gcc -o build/mkfs_tinyfs build/host_tinyfs.o build/host_mkfs_tinyfs.o
dd if=/dev/zero of=build/disk.img bs=512 count=4096 status=none
dd if=build/boot.bin of=build/disk.img conv=notrunc status=none
dd if=build/stage2.bin of=build/disk.img bs=512 seek=1 conv=notrunc status=none
dd if=build/kernel.bin of=build/disk.img bs=512 seek=5 conv=notrunc status=none
build/mkfs_tinyfs build/disk.img
mkfs_tinyfs: formatted build/disk.img (3958 data blocks available)
agent · Jul 17, 19:20:00
Clean build. Checking kernel size budget, then a quick manual sanity boot.
Bash · Jul 17, 19:20:01 · toolu_01JNT1FGqYL3HeBGRAQSohu5
Check kernel size and boot to see selftest + shell banner
$ stat -c%s build/kernel.bin
timeout 4 qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial stdio -no-reboot; echo "exit=$?"
result
13504
tiny-os: kernel entered
selftest: running
SELFTEST PASS
tiny-os shell. type 'help' for commands.
> qemu-system-i386: terminating on signal 15 from pid 1878330 (timeout)
exit=124
agent · Jul 17, 19:20:09
Now building the scripted-keystroke test harness.
Write · Jul 17, 19:20:14 · toolu_01PnHGqR3X4XZb4kXhgh4dvS
/home/martin/workspace/prototypes/boot-to-claude/tools/qemu_type.py
55 lines
#!/usr/bin/env python3
"""Types text into a running QEMU guest via its HMP monitor's `sendkey`
command, over a unix socket (-monitor unix:PATH,server,nowait). Used by
tools/run_tests.sh to drive the kernel shell headlessly - there is no
other way to feed the polled PS/2 keyboard driver without real hardware
or a display.
"""
import socket
import sys
import time

KEYMAP = {
    " ": "spc",
    ".": "dot",
    "\n": "ret",
    "-": "minus",
    "_": "shift-minus",
    ",": "comma",
    "/": "slash",
}


def key_name(c):
    if c in KEYMAP:
        return KEYMAP[c]
    if c.isalpha():
        return c.lower()
    if c.isdigit():
        return c
    raise ValueError(f"qemu_type: no key mapping for {c!r}")


def main():
    if len(sys.argv) != 3:
        print(f"usage: {sys.argv[0]} <monitor-socket> <text>", file=sys.stderr)
        sys.exit(1)

    sock_path, text = sys.argv[1], sys.argv[2]

    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(sock_path)
    time.sleep(0.2)
    s.recv(4096)  # monitor banner

    for c in text:
        s.sendall(f"sendkey {key_name(c)}\n".encode())
        time.sleep(0.05)
        s.recv(4096)

    s.close()


if __name__ == "__main__":
    main()
result
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tools/qemu_type.py (file state is current in your context — no need to Read it back)
Write · Jul 17, 19:20:23 · toolu_01HbgXokZ8pqHgeiRLv2m2c4
/home/martin/workspace/prototypes/boot-to-claude/tools/run_tests.sh
85 lines
#!/usr/bin/env bash
# Boots the kernel headlessly in QEMU, checks the kernel self-test result,
# then drives the interactive shell via QEMU's monitor `sendkey` (see
# tools/qemu_type.py - there's no other way to feed the polled PS/2
# keyboard driver without real hardware) and asserts on serial output.
set -euo pipefail

cd "$(dirname "$0")/.."
make >/dev/null

BUILD=build
LOG="$BUILD/serial.log"
MON="$BUILD/qemu-monitor.sock"
rm -f "$LOG" "$MON"

qemu-system-i386 \
    -drive file="$BUILD/disk.img",format=raw \
    -display none \
    -serial file:"$LOG" \
    -monitor unix:"$MON",server,nowait \
    -no-reboot &
QEMU_PID=$!

cleanup() {
    kill "$QEMU_PID" 2>/dev/null || true
    wait "$QEMU_PID" 2>/dev/null || true
}
trap cleanup EXIT

fail() {
    echo "run_tests: FAIL - $1"
    echo "--- serial log ---"
    cat "$LOG" 2>/dev/null || true
    exit 1
}

wait_for() {
    local pattern="$1"
    local max_tries="${2:-50}" # 50 * 0.2s = 10s
    local i=0
    while ! grep -qF "$pattern" "$LOG" 2>/dev/null; do
        i=$((i + 1))
        if [ "$i" -ge "$max_tries" ]; then
            fail "timed out waiting for '$pattern'"
        fi
        sleep 0.2
    done
}

type_line() {
    python3 tools/qemu_type.py "$MON" "$1"$'\n'
}

for i in $(seq 1 25); do
    [ -S "$MON" ] && break
    sleep 0.2
done
[ -S "$MON" ] || fail "qemu monitor socket never appeared"

wait_for "SELFTEST PASS"
wait_for "type 'help'"

type_line "ls"
wait_for "hello.txt"

type_line "cat hello.txt"
wait_for "Hello, tinyfs!"

type_line "write greet.txt hi there"
wait_for "write: ok"

type_line "cat greet.txt"
wait_for "hi there"

type_line "rm greet.txt"
wait_for "rm: ok"

type_line "cat greet.txt"
wait_for "cat: no such file"

type_line "help"
wait_for "commands:"

echo "run_tests: PASS"
result
The file /home/martin/workspace/prototypes/boot-to-claude/tools/run_tests.sh has been updated successfully. (file state is current in your context — no need to Read it back)
Bash · Jul 17, 19:20:26 · toolu_01Gwo6QmR2FXeDfJtR1bx26c
Run the full scripted-shell test harness
$ chmod +x /home/martin/workspace/prototypes/boot-to-claude/tools/run_tests.sh /home/martin/workspace/prototypes/boot-to-claude/tools/qemu_type.py
./tools/run_tests.sh
result
run_tests: PASS
qemu-system-i386: terminating on signal 15 from pid 1881176 (bash)
agent · Jul 17, 19:20:35
All shell commands verified end-to-end via scripted keystrokes. Committing Phase 4.
Bash · Jul 17, 19:20:36 · toolu_01Tj6gPuP98Xaa1yUrFjqhuW
Stage Phase 4 files
$ git add -A && git status --short
result
M  Makefile
M  kernel/console.c
A  kernel/keyboard.c
A  kernel/keyboard.h
M  kernel/kmain.c
A  kernel/shell/shell.c
A  kernel/shell/shell.h
A  tools/qemu_type.py
M  tools/run_tests.sh
Bash · Jul 17, 19:20:40 · toolu_01EpgQeGoDqcut6YJCuSHrgZ
Commit Phase 4 keyboard and shell
$ git commit -m "$(cat <<'EOF'
Phase 4: polled keyboard driver + interactive shell

Adds a polled PS/2 keyboard driver (scancode set 1, US layout, shift
support) and a shell REPL: ls, cat, write, rm, help, clear, reboot,
backed by tinyfs. console_putc now handles backspace so the shell's
line editor can erase characters on both VGA and serial.

tools/run_tests.sh now drives the shell headlessly through QEMU's HMP
monitor `sendkey` command (tools/qemu_type.py) over a unix socket -
the only way to feed the polled keyboard driver without real hardware
or a display - and asserts on serial output for each command.
EOF
)"
result
[main 594867b] Phase 4: polled keyboard driver + interactive shell
 9 files changed, 411 insertions(+), 21 deletions(-)
 create mode 100644 kernel/keyboard.c
 create mode 100644 kernel/keyboard.h
 create mode 100644 kernel/shell/shell.c
 create mode 100644 kernel/shell/shell.h
 create mode 100755 tools/qemu_type.py