diff options
| author | user <user@clank> | 2026-09-22 16:02:55 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-22 16:02:55 +0200 |
| commit | 43e43b6dcec29e2b008d20135f5d02523b62fbcf (patch) | |
| tree | d647acd07596e290c70a9884c1dc5d8c20a31067 | |
| parent | net: three TCP receive bugs - caller corruption, data loss, early EOF (diff) | |
| download | gbos-43e43b6dcec29e2b008d20135f5d02523b62fbcf.tar.gz gbos-43e43b6dcec29e2b008d20135f5d02523b62fbcf.tar.xz gbos-43e43b6dcec29e2b008d20135f5d02523b62fbcf.zip | |
usr: grep, more, cp, nc - and make pollcon pump the link
The shell has had pipes for a while with nothing to pipe *through*, and
an 18-row screen with no way to stop output scrolling off it.
- grep [-vin] PAT [file]: substring match (no regex), stdin or a file,
grep(1)'s exit convention. The other end of the pipe, at last.
- more [file]: pages 17 lines at a time, --More-- bar in white-on-blue,
any key pages, q quits. Counts *screen* rows, so wrapped lines pay
their real cost. Keys come from pollin/pollcon, not stdin, so
`cat big | more` still has a keyboard.
- cp SRC DST: the fs has rm/mkdir but no way to duplicate a file. mv is
this plus rm (there is no rename). Rejects SRC == DST, which would
otherwise truncate the source via O_WRITE before reading a byte.
- nc [-s] HOST PORT: raw TCP. Interactive (START sends the line with a
CRLF, /q quits) or -s to pump stdin and print the reply with its own
wall-clock idle timeout, because the kernel's blocking recv counts
pumps, not seconds. Generalizes what chat/irc hardcode:
`nc towel.blinkenlights.nl 23` works.
sys_pollcon now pumps the link before draining the console ring. The
ring is filled *by* net_pump, so a program that never touches the
network polled a ring nothing would ever fill: host-injected keys
(gbtype/gbdemo) hung `more` forever, and the Game Boy stopped answering
pings for as long as it ran. KGetc already pumps for this reason.
New tools take banks 35-38. Locals over statics in all four: a program's
_DATA starts at $A000, where the shell leaves the command line.
| -rw-r--r-- | Makefile | 3 | ||||
| -rw-r--r-- | README.md | 15 | ||||
| -rw-r--r-- | include/gbos.inc | 4 | ||||
| -rw-r--r-- | src/net.asm | 11 | ||||
| -rw-r--r-- | src/programs.asm | 28 | ||||
| -rw-r--r-- | usr/cp.c | 62 | ||||
| -rw-r--r-- | usr/grep.c | 99 | ||||
| -rw-r--r-- | usr/more.c | 83 | ||||
| -rw-r--r-- | usr/nc.c | 197 |
9 files changed, 493 insertions, 9 deletions
@@ -21,7 +21,8 @@ $(BUILD)/%.o: src/%.asm | $(BUILD) # $(BUILD)/usr and INCBIN'd by programs.asm PROGS := chello echo true false uname pid cat wc head args \ ls save rm sh ps kill spin blk mkdir count ptest necho \ - wget chat netd ping nslookup dhcp irc uptime ansi + wget chat netd ping nslookup dhcp irc uptime ansi \ + grep more cp nc CBLOBS := $(patsubst %,$(BUILD)/usr/%.bin,$(PROGS)) $(BUILD)/programs.o: $(CBLOBS) @@ -17,12 +17,15 @@ into the on-screen console. Regenerate it anytime with `tools/gbdemo` (below).* background jobs (`spin &`), `ps`, `kill` — round-robin cooperative scheduling across banked memory with an HRAM context switch. - **A shell** (written in C, running *on* the GB) with I/O redirection - (`>` `<`) and pipes (`|`), argument passing, and a cwd-aware prompt. + (`>` `<`) and pipes (`|`), argument passing, and a cwd-aware prompt — with + `grep` to put on the other end of the pipe, and `more` to page anything + longer than the 18-row screen. - **Networking, for real**: the kernel owns SLIP over the link port, IPv4, ICMP, UDP and TCP; userland gets BSD-ish sockets via one syscall. On top: `dhcp` (boot-time lease), `ping`, `nslookup` (real DNS), `wget` (HTTP), - `chat`, and a colorized **IRC client** (`irc HOST [NICK]`, `/join` and - friends, SELECT for the keyboard). + `nc` (netcat: `nc HOST PORT` interactively, or `nc -s HOST PORT < file` to + pipe a request in and print the reply), `chat`, and a colorized **IRC + client** (`irc HOST [NICK]`, `/join` and friends, SELECT for the keyboard). - **A network to plug into**: `tools/gbhub` is a virtual switch + DHCP server + NAT gateway — spawn a fleet of Game Boys that ping *each other* and reach the real internet; `tools/gbjoin` attaches an interactive GB to it, and @@ -31,9 +34,9 @@ into the on-screen console. Regenerate it anytime with `tools/gbdemo` (below).* colors) + an **on-screen keyboard** on the window layer (SELECT toggles it). - **Persistent filesystem** on battery-backed cart RAM: inodes, nested directories, per-process cwd — survives power-off. -- **A C toolchain**: userland is plain C compiled with SDCC (`sm83`); ~30 - tools ship in `usr/` (`cat`, `wc`, `head`, `ls`, `uptime`, …). Adding a - program is one `.c` file + a table entry. +- **A C toolchain**: userland is plain C compiled with SDCC (`sm83`); ~35 + tools ship in `usr/` (`cat`, `wc`, `head`, `grep`, `cp`, `more`, `ls`, + `uptime`, …). Adding a program is one `.c` file + a table entry. ## Quick start diff --git a/include/gbos.inc b/include/gbos.inc index cbd2670..9099bc9 100644 --- a/include/gbos.inc +++ b/include/gbos.inc @@ -292,5 +292,9 @@ DEF PROG_DHCP EQU 29 DEF PROG_IRC EQU 30 DEF PROG_UPTIME EQU 31 DEF PROG_ANSI EQU 32 +DEF PROG_GREP EQU 33 +DEF PROG_MORE EQU 34 +DEF PROG_CP EQU 35 +DEF PROG_NC EQU 36 ENDC diff --git a/src/net.asm b/src/net.asm index 8309242..a5f0210 100644 --- a/src/net.asm +++ b/src/net.asm @@ -56,9 +56,16 @@ sys_srecv_nb:: ; sys_pollcon() -> A = a queued console-ring byte (link-injected input, ; e.g. gbhub 'type'), or 0 if none. Non-blocking twin of KGetc's con_pop -; path: programs that own their main loop (irc) poll this alongside the OSK. -; The ring fills during net pumps, which such programs do constantly. +; path: programs that own their main loop (irc, nc, more) poll this alongside +; the OSK. +; +; Pump the link first. The ring is filled BY net_pump, so a caller that never +; touches the network (a pager, a game) would otherwise poll a ring nothing +; ever fills - host-injected keys (gbtype/gbdemo) would hang it forever, and +; the Game Boy would stop answering pings for as long as it ran. KGetc already +; pumps for exactly this reason; pollcon now matches it. sys_pollcon:: + call net_pump call con_pop ret nc ; got a byte xor a diff --git a/src/programs.asm b/src/programs.asm index c6228f3..a3bd680 100644 --- a/src/programs.asm +++ b/src/programs.asm @@ -139,6 +139,18 @@ ProgUptime: SECTION "prog_ansi", ROMX[$4000], BANK[34] ProgAnsi: INCBIN "build/usr/ansi.bin" +SECTION "prog_grep", ROMX[$4000], BANK[35] +ProgGrep: + INCBIN "build/usr/grep.bin" +SECTION "prog_more", ROMX[$4000], BANK[36] +ProgMore: + INCBIN "build/usr/more.bin" +SECTION "prog_cp", ROMX[$4000], BANK[37] +ProgCp: + INCBIN "build/usr/cp.bin" +SECTION "prog_nc", ROMX[$4000], BANK[38] +ProgNc: + INCBIN "build/usr/nc.bin" ; ----------------------------------------------------------------------------- ; PROG_SH (bank 3) - the shell, now written in C (usr/sh.c): parses >/</| and @@ -247,6 +259,14 @@ ProgramTable:: dw ProgUptime db LOW(BANK(ProgAnsi)), HIGH(BANK(ProgAnsi)) dw ProgAnsi + db LOW(BANK(ProgGrep)), HIGH(BANK(ProgGrep)) + dw ProgGrep + db LOW(BANK(ProgMore)), HIGH(BANK(ProgMore)) + dw ProgMore + db LOW(BANK(ProgCp)), HIGH(BANK(ProgCp)) + dw ProgCp + db LOW(BANK(ProgNc)), HIGH(BANK(ProgNc)) + dw ProgNc ProgramTableEnd:: ; ----------------------------------------------------------------------------- @@ -320,4 +340,12 @@ NameTable:: db PROG_UPTIME db "ansi", 0 db PROG_ANSI + db "grep", 0 + db PROG_GREP + db "more", 0 + db PROG_MORE + db "cp", 0 + db PROG_CP + db "nc", 0 + db PROG_NC db 0 diff --git a/usr/cp.c b/usr/cp.c new file mode 100644 index 0000000..61a1e0b --- /dev/null +++ b/usr/cp.c @@ -0,0 +1,62 @@ +#include "gbos.h" +/* cp SRC DST: copy a file. There is no rename syscall, so `mv` is this plus + * rm - and O_WRITE truncates (see fs.asm sys_open .trunc), which is exactly + * why the SRC == DST case below has to be rejected before opening DST. */ + +static unsigned char streq(const char *a, const char *b) { + while (*a && *a == *b) { + a++; + b++; + } + return (unsigned char)(*a == *b); +} + +void main(void) { + char *argv[4]; + unsigned char argc = argv_parse(argv, 4); + unsigned char in, out; + int ch; + + if (argc < 2) { + puts("usage: cp SRC DST"); + nl(); + sexit(2); + } + if (streq(argv[0], argv[1])) { + puts("cp: SRC and DST are the same file"); + nl(); + sexit(2); + } + + in = open(argv[0], O_READ); + if (in == EISDIR) { + puts("cp: SRC is a directory"); + nl(); + sexit(2); + } + if (in == NOFD) { + puts("cp: no such file: "); + puts(argv[0]); + nl(); + sexit(2); + } + + out = open(argv[1], O_WRITE); /* creates, or truncates an existing file */ + if (out == EISDIR) { + puts("cp: DST is a directory"); + nl(); + close(in); + sexit(2); + } + if (out == NOFD) { + puts("cp: cannot create "); + puts(argv[1]); + nl(); + close(in); + sexit(2); /* out of inodes/blocks, or a bad path */ + } + + while ((ch = fgetc(in)) >= 0) fputc(out, (char)ch); + close(in); + close(out); /* flushes the inode */ +} diff --git a/usr/grep.c b/usr/grep.c new file mode 100644 index 0000000..3d2b325 --- /dev/null +++ b/usr/grep.c @@ -0,0 +1,99 @@ +#include "gbos.h" +/* grep [-v][-i][-n] PAT [file]: print the lines of a file (or stdin) that + * contain the substring PAT. Plain substring matching, no regex - '.' and '*' + * are literal characters. -v inverts the match, -i folds case, -n prefixes + * each hit with its line number. + * + * NB: no statics here. A program's _DATA area starts at $A000, which is also + * where the shell leaves the command line (usr/sh.c ARGV0), so statics would + * eat argv. Locals live on the task stack below $BFFF and are always safe. */ + +#define MAXLINE 80 /* two terminal rows; longer lines are clipped */ + +static char fold(char c, unsigned char ic) { + if (ic && c >= 'A' && c <= 'Z') return (char)(c + 32); + return c; +} + +/* is pat[] present anywhere in line[0..len)? */ +static unsigned char contains(char *line, unsigned char len, char *pat, unsigned char ic) { + unsigned char i, j; + if (!pat[0]) return 1; /* empty pattern matches all */ + for (i = 0; i < len; i++) { + for (j = 0;; j++) { + if (!pat[j]) return 1; /* ran out of pattern: a hit */ + if (i + j >= len) break; /* ran out of line: no hit here */ + if (fold(line[i + j], ic) != fold(pat[j], ic)) break; + } + } + return 0; +} + +void main(void) { + char *argv[8]; + char line[MAXLINE]; + unsigned char argc = argv_parse(argv, 8); + unsigned char inv = hasflag(argv, argc, 'v') ? 1 : 0; + unsigned char ic = hasflag(argv, argc, 'i') ? 1 : 0; + unsigned char num = hasflag(argv, argc, 'n') ? 1 : 0; + unsigned char i, len = 0, hits = 0, fd = NOFD; + unsigned int lineno = 0; + char *pat = 0, *fname = 0; + int ch; + + /* first non-flag word is the pattern, second (if any) is the file */ + for (i = 0; i < argc; i++) { + if (argv[i][0] == '-') continue; + if (!pat) pat = argv[i]; + else { + fname = argv[i]; + break; + } + } + if (!pat) { + puts("usage: grep [-vin] PAT [file]"); + nl(); + sexit(2); + } + + if (fname) { + fd = open(fname, O_READ); + if (fd == EISDIR) { + puts("grep: is a directory"); + nl(); + sexit(2); + } + if (fd == NOFD) { + puts("grep: no such file"); + nl(); + sexit(2); + } + } + + for (;;) { + if (fd == NOFD) { + char c = readc(); + ch = (c == EOF) ? -1 : c; + } else ch = fgetc(fd); + if (ch == '\r') continue; /* CRLF: the LF ends the line */ + if (ch >= 0 && ch != '\n') { /* ordinary character */ + if (len < MAXLINE) line[len++] = (char)ch; + continue; + } + if (ch < 0 && len == 0) break; /* EOF, nothing pending */ + lineno++; + if (contains(line, len, pat, ic) != inv) { + if (num) { + putu(lineno); + putc(':'); + } + writes(line, len); /* one batched render */ + nl(); + hits = 1; + } + len = 0; + if (ch < 0) break; /* EOF after a final partial line */ + } + if (fd != NOFD) close(fd); + sexit(hits ? 0 : 1); /* grep(1) exit convention */ +} diff --git a/usr/more.c b/usr/more.c new file mode 100644 index 0000000..6f6afa7 --- /dev/null +++ b/usr/more.c @@ -0,0 +1,83 @@ +#include "gbos.h" +/* more [file]: page a file (or stdin) one screenful at a time. + * + * The terminal is 40x18, so anything longer than a screen scrolls off before + * you can read it (try `ls` on a full disk, or `cat` on a wget dump). This + * prints 17 lines, then paints a --More-- bar and waits for a keypress: + * SELECT opens the on-screen keyboard, any key pages forward, 'q' quits. + * + * Keys come from pollin()/pollcon(), NOT stdin - so `cat file | more` still + * has a keyboard even though its stdin is the pipe. (pollcon also means the + * host can drive it over the link with tools/gbtype.) */ + +#define PAGE 17 /* TROWS - 1: leave a row for the bar */ +#define TCOLS 40 + +static void sgr(unsigned char n) { /* emit ESC [ n m */ + putc(27); + putc('['); + putu(n); + putc('m'); +} + +/* show the bar, block until a key arrives; 0 = the user asked to quit */ +static unsigned char waitkey(void) { + unsigned char c; + sgr(44); + sgr(37); /* white on blue */ + puts("--More--"); + sgr(0); + for (;;) { + c = pollin(); /* on-screen keyboard */ + if (!c) c = pollcon(); /* link-injected console bytes */ + if (c) break; + yield(); /* cooperative: let other procs run */ + } + puts("\r \r"); /* wipe the bar, stay on the same row */ + return (c == 'q' || c == 'Q') ? 0 : 1; +} + +void main(void) { + char *argv[4]; + unsigned char argc = argv_parse(argv, 4); + unsigned char fd = NOFD, rows = 0, col = 0; + int ch; + + if (argc > 0 && argv[0][0] != '-') { + fd = open(argv[0], O_READ); + if (fd == EISDIR) { + puts("more: is a directory"); + nl(); + sexit(2); + } + if (fd == NOFD) { + puts("more: no such file"); + nl(); + sexit(2); + } + } + + for (;;) { + if (fd == NOFD) { + char c = readc(); + ch = (c == EOF) ? -1 : c; + } else ch = fgetc(fd); + if (ch < 0) break; + putc((char)ch); + + /* count screen rows, not input lines: a 100-char line wraps to three */ + if (ch == '\n') { + rows++; + col = 0; + } else if (ch != '\r' && ++col == TCOLS) { + rows++; + col = 0; + } + + if (rows >= PAGE) { + if (!waitkey()) break; + rows = 0; + } + } + if (fd != NOFD) close(fd); +} diff --git a/usr/nc.c b/usr/nc.c new file mode 100644 index 0000000..023bedb --- /dev/null +++ b/usr/nc.c @@ -0,0 +1,197 @@ +#include "resolve.h" +/* nc [-s] HOST PORT - raw TCP, the way netcat does it. The kernel owns + * SLIP/IP/TCP; this is just a pump between the socket and the terminal. + * + * nc towel.blinkenlights.nl 23 telnet-ish: ASCIImation on a Game Boy + * nc 10.0.0.1 6667 talk to anything line-oriented + * echo GET / HTTP/1.0 > r (blank line needed by most servers) + * nc -s example.com 80 < r -s: send stdin, print the reply, exit + * + * Interactive mode: SELECT opens the on-screen keyboard, typing echoes + * locally, START sends the line with a CRLF. The line "/q" quits without + * being sent (there is no ^C here); a remote EOF quits too. + * + * Everything lives on the stack on purpose: resolve.h's DNS buffer is a + * static, so it sits at $A000 - right on top of the command line the shell + * left for us. Copy argv before the first DNS call, exactly like ping.c. */ + +#define BUFSZ 208 /* one full segment: the most the kernel buffers */ +#define LINESZ 64 + +static unsigned char split(char *s, char **tok, unsigned char max) { + unsigned char n = 0; + for (;;) { + while (*s == ' ') *s++ = 0; + if (!*s) break; + if (n < max) tok[n++] = s; + while (*s && *s != ' ') s++; + } + return n; +} + +/* atou() is 8-bit; ports are not */ +static unsigned int atou16(const char *s) { + unsigned int n = 0; + while (*s >= '0' && *s <= '9') { + n = n * 10 + (unsigned char)(*s - '0'); + s++; + } + return n; +} + +void main(void) { + char args[LINESZ]; /* stack copy of the command line */ + char line[LINESZ]; + unsigned char buf[BUFSZ]; + unsigned char dst[4]; + char *tok[4]; + char *host = 0, *portstr = 0; + unsigned int port; + unsigned char ntok, i, n, c, sock, sflag = 0, llen = 0, idle = 0; + + { /* copy args off $A000 before resolve()'s statics land on them */ + char *a = getargs(); + for (i = 0; a[i] && i < LINESZ - 1; i++) args[i] = a[i]; + args[i] = 0; + } + + ntok = split(args, tok, 4); + for (i = 0; i < ntok; i++) { + if (tok[i][0] == '-') { + if (tok[i][1] == 's') sflag = 1; + continue; + } + if (!host) host = tok[i]; + else if (!portstr) portstr = tok[i]; + } + if (!host || !portstr) { + puts("usage: nc [-s] HOST PORT"); + nl(); + puts(" -s: send stdin, print reply, exit"); + nl(); + sexit(2); + } + port = atou16(portstr); + if (port == 0) { + puts("nc: bad port"); + nl(); + sexit(2); + } + + if (!resolve(host, dst)) { + puts("nc: cannot resolve "); + puts(host); + nl(); + sexit(1); + } + + sock = net_socket(SOCK_TCP); + if (sock == 0xFF) { + puts("nc: no socket"); + nl(); + sexit(1); + } + /* vary the local port per run (DIV noise + pid): a fixed one can collide + with a half-dead connection still open at the server (see irc.c) */ + net_bind(sock, 42000 + ((unsigned int)*(volatile unsigned char *)0xFF04 << 2) + getpid()); + + puts("-!- connecting "); + for (i = 0; i < 4; i++) { + putu(dst[i]); + if (i < 3) putc('.'); + } + putc(':'); + putu(port); + nl(); + if (net_connect(sock, dst, port) == 0xFF) { + puts("nc: connect failed"); + nl(); + net_close(sock); + sexit(1); + } + + if (sflag) { /* batch mode: stdin -> socket -> term */ + n = 0; + for (;;) { + char ch = readc(); /* blocks forever on a console stdin! */ + if (ch == EOF) break; + buf[n++] = (unsigned char)ch; + if (n == BUFSZ) { + net_send(sock, buf, n); + n = 0; + } + } + if (n) net_send(sock, buf, n); + /* Own the timeout out here (like ping): the kernel's blocking recv + counts pumps, not seconds, so a peer that just goes quiet would + wedge us for minutes instead of ending the transfer. */ + idle = 0; + for (;;) { + n = net_recv_nb(sock, buf, BUFSZ); + if (n == 0) { + nl(); + puts("[eof]"); + nl(); + break; + } + if (n == 0xFE || n == 0xFF) { + if (++idle >= 80) { + nl(); + puts("[timeout]"); + nl(); + break; + } + msleep(50); /* ~4s of silence ends it */ + continue; + } + writes((char *)buf, n); + idle = 0; + } + net_close(sock); + sexit(0); + } + + puts("-!- connected (SELECT = keyboard, /q quits)"); + nl(); + for (;;) { + n = net_recv_nb(sock, buf, BUFSZ); + if (n == 0) { + puts("-!- closed by peer"); + nl(); + break; + } + if (n != 0xFE && n != 0xFF) writes((char *)buf, n); /* batched render */ + + /* poll input every pass, even mid-flood, or a chatty peer starves the + keyboard and dropped keystrokes look like a broken OSK (irc.c) */ + c = pollin(); + if (!c) c = pollcon(); + if (c == '\n') { + nl(); + if (llen == 2 && line[0] == '/' && line[1] == 'q') break; + line[llen] = '\r'; + line[llen + 1] = '\n'; + net_send(sock, line, (unsigned char)(llen + 2)); + llen = 0; + } else if (c == 8) { + if (llen) { + llen--; + putc(8); + } + } else if (c) { + if (llen < LINESZ - 2) { + line[llen++] = (char)c; + putc((char)c); + } + } + + if (n == 0xFE || n == 0xFF) { /* nothing arrived: back off a little */ + if (!c && ++idle >= 4) { + msleep(20); + idle = 4; + } + } else idle = 0; + } + net_close(sock); + sexit(0); +} |
