<feed xmlns='http://www.w3.org/2005/Atom'>
<title>gbos.git/include, branch master</title>
<subtitle>gbos - a tiny Unix-flavored OS for the Game Boy Color (kernel, shell, coreutils in C, RAM filesystem)</subtitle>
<id>https://git.sl0p.foo/gbos.git/atom/include?h=master</id>
<link rel='self' href='https://git.sl0p.foo/gbos.git/atom/include?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/'/>
<updated>2026-09-24T21:08:35Z</updated>
<entry>
<title>fs: hard links - ln(1), and rm that removes a NAME</title>
<updated>2026-09-24T21:08:35Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-24T21:08:35Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=1430515ad419f2fddeb0265337a0da043901411b'/>
<id>urn:sha1:1430515ad419f2fddeb0265337a0da043901411b</id>
<content type='text'>
gbfs has carried I_NLINK since the beginning and nothing ever incremented it,
so a file could only have one name. SYS_LINK adds a second directory entry
pointing at the same inode: the data is stored once, and both names read it.

That makes rm's job different. It now decrements the link count and only frees
the inode and its blocks when the LAST name goes - removing one of two links
used to free blocks the other still pointed at.

Refused, with the reasons that matter here: linking a directory (it would make
a cycle the tree walkers cannot survive), an existing name, and anything under
/bin, which is ROM.

The syscall takes a request block because the trap needs HL for its dispatch
table - and ln copies both paths to the stack first, since that block is a
static living at $A000, on top of the command line it is reading.
</content>
</entry>
<entry>
<title>usr: touch, tr, rev, cut, uniq</title>
<updated>2026-09-24T20:45:04Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-24T20:45:04Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=4cd933009dd6ce87fe710acfca6b1f786d51040c'/>
<id>urn:sha1:4cd933009dd6ce87fe710acfca6b1f786d51040c</id>
<content type='text'>
    /# ls -l /bin &gt; b
    /# cut -d \s -f 1 b | uniq -c
    52 -

- touch FILE...: gbfs has no timestamps (an MBC5 cart has no RTC), so touch
  does the half that means something here - make an empty file. O_APPEND is
  exactly right: it creates, and it does not truncate what is already there.
- tr [-d] SET1 [SET2]: ranges (a-z), -d to delete, and a short SET2 padded
  with its last character, like tr(1). ASCII only - a 128-entry map.
- rev, uniq [-c]: one line each, from a file or stdin like the other filters.

Two things the tests caught. A bare "-" is a SET, not a flag, so `tr . -`
has to skip flag-matching only when a letter follows the dash. And uniq
compared an empty "line" against the last run at EOF and flushed a phantom
blank one - a stray "1" in uniq -c output.

tr and cut also take \s \t \n \r escapes, because the shell has no quoting:
`-d ' '` arrives as three tokens, so a space or tab delimiter was simply
untypeable. Shell quoting would be the better fix; this makes the tools
usable today without touching the parser.

Banks 49-53; 10 program slots left in the 1 MB ROM.
</content>
</entry>
<entry>
<title>fs: mount the builtins at /bin</title>
<updated>2026-09-24T12:15:46Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-24T12:15:46Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=50a56b48f22cd31868403f1fde10cf8367344e2e'/>
<id>urn:sha1:50a56b48f22cd31868403f1fde10cf8367344e2e</id>
<content type='text'>
The commands only existed in the kernel's program table, so the only way to
find out what the machine could do was to already know (or read the source).
They are now a directory in the tree:

    /# ls /                      /# xxd -l 16 /bin/uname
    bin                          0000: cd 6b 43 06 00 0e 00 f7 .kC.....
    readme                       0008: 18 fe 47 0e 03 f7 c9 21 ..G....!
    /# ls -l /bin | head -2
    -  1 16384 worker

/bin is synthetic, not disk: dir_find resolves "bin" in the root to BIN_INO
and names inside it through the NameTable, so `ls /bin` lists exactly what the
shell can run - one table, one truth, no second list to keep in sync. The root
listing carries "bin" as a synthetic first entry so it is discoverable by
walking the tree.

A program image is inode BIN_FILE|progid: it stats as a 16 KiB file (a program
IS its ROM bank) and reads through rom_getb, which maps that bank, takes the
byte, and maps the CALLER'S text bank back before returning - the caller is
itself executing from $4000, so leaving the wrong bank mapped would return
into another program's code. Verified byte-for-byte against the host's view of
the blob. The tree is read-only: create/remove/write inside /bin all fail.

xxd grows -l (stop after N bytes), because a program image is 2048 dump lines
- more than a pipe's temp file can hold, let alone the screen.

Two traps worth remembering, both found the hard way here: `and BIN_FILE`
destroys A, so the inode must be reloaded before inode_ptr (missing it once
made ls -l stat inode 0 and call every file an empty non-directory); and ls
now copies its directory argument to the stack first, because stat()'s request
block is a static living at $A000 - on top of the command line it was reading.
</content>
</entry>
<entry>
<title>usr: xxd - hex dump a file or stdin</title>
<updated>2026-09-24T11:56:42Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-24T11:56:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=0fa1b946ec5fd3f5cba49215ea9ab62c4004aa53'/>
<id>urn:sha1:0fa1b946ec5fd3f5cba49215ea9ab62c4004aa53</id>
<content type='text'>
Eight bytes per line rather than the usual sixteen: offset + 8 hex pairs +
the ASCII column is 38 of the terminal's 40 columns, where 16 would wrap
every line and make the dump unreadable. -c overrides it.

No -s (skip): there is no seek syscall, so it streams from the start - the
same reason tail(1) has to keep a ring.

    /# xxd h
    0000: 48 65 6c 6c 6f 20 67 62 Hello gb
    0008: 6f 73 0d 0a             os..

blk(1) could already peek at raw disk blocks; nothing could look inside a
file. Verified against known bytes, from stdin, with -c, through a pipe, and
on a missing file.
</content>
</entry>
<entry>
<title>kernel: stat/fsstat/isatty syscalls, and O_APPEND</title>
<updated>2026-09-24T10:48:37Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-24T10:48:37Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=dc9dffb9069e32a1a0cf14883176544b234fc3e5'/>
<id>urn:sha1:dc9dffb9069e32a1a0cf14883176544b234fc3e5</id>
<content type='text'>
Four things userland had no way to ask for:

- SYS_STAT: type, link count, size and inode for a path. The trap needs HL
  for its dispatch table, so a call taking both a path and an output buffer
  passes a request block - the shape SYS_NET already established. usr/stat.h
  wraps it, header-only like sock.h, because the block must live in the
  caller's RAM.
- SYS_FSSTAT: blocks/inodes, total and free. The bitmaps are already cached
  write-through in fs_bmbuf, so this is the popcount boot_fs already prints.
- SYS_ISATTY: is this process's stdin the console? The shell needs it to tell
  an interactive session from a script.
- O_APPEND: open a file positioned at EOF. There is no seek syscall, so this
  is the only way to add to a file - without it nothing running ON the Game
  Boy can build a multi-line file, shell scripts included.

Together these back ls -l, df, and `&gt;&gt;`.
</content>
</entry>
<entry>
<title>usr: ircd - an IRC server running on the Game Boy</title>
<updated>2026-09-22T19:54:13Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T19:54:13Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=0a7ff09a910eaf5bcb0dc76d08dc5a6c9c239495'/>
<id>urn:sha1:0a7ff09a910eaf5bcb0dc76d08dc5a6c9c239495</id>
<content type='text'>
    /# ircd &amp;
    host$ irssi -c 10.0.0.2       /join #gbos
    other GB$ irc 10.0.0.2 gb2    /join #gbos

Implements the subset a real client needs to get in and talk: NICK, USER,
PING/PONG, JOIN, PART, PRIVMSG/NOTICE (to a channel or to a nick), NAMES and
QUIT, with the 001/004/375/372/376 numerics clients wait for on registration,
353/366 on join, and 421 for anything else. One channel per client keeps the
bookkeeping in fixed-size tables; this is a handheld with 8 KiB of task RAM.

MAXCL is 3, bounded by the kernel socket table (listener + client each take a
slot, the rest left for other programs). A fourth client is refused and the
daemon keeps serving - which is how the ~30s close() stall turned up.

Every event is logged to the LCD, so the Game Boy shows its own server
traffic: "+ alice", "alice joined", "&lt;alice&gt; hello".

Verified with real clients over the hub: two and three simultaneous hosts
registering, joining, channel relay (sender not echoed to itself), private
messages by nick, PING/PONG, unknown-command numerics, QUIT propagation, the
overflow refusal, and a second Game Boy running gbos's own irc(1) in the same
channel as a laptop client - messages crossing both ways between them.
</content>
</entry>
<entry>
<title>net: one socket per client - four-tuple demux, spawning accept, bounded close</title>
<updated>2026-09-22T19:54:03Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T19:54:03Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=dcd1f1320c102f9cc4cff7eee387ec538304f7d1'/>
<id>urn:sha1:dcd1f1320c102f9cc4cff7eee387ec538304f7d1</id>
<content type='text'>
A server could only ever hold ONE connection, which makes an ircd pointless:
listen() turned the listener into the connection, and net_find_tcp demuxed on
local port alone, so two clients on 6667 were indistinguishable.

- net_find_tcp now matches an established socket on the full four-tuple
  (local port + peer IP + peer port) and only falls back to a LISTENing
  socket when none matches.
- A SYN at a listener no longer consumes it: tcp_spawn_conn clones a new
  socket (local port + owning pid, so exit/kill still reclaims it) and the
  listener keeps listening. accept() returns that socket; closing it leaves
  the listener alone. A full table drops the SYN, and the client's retransmit
  is taken once a slot frees.
- tcp_peer_is_current preserves BC: net_find_tcp scans with the socket index
  there, and the compare needs BC for the rx buffer base. Without this the
  scan died at the first mismatching socket, so a second client's handshake
  ACK never reached its socket and it hung in SYNRCVD - tcpdump showed our
  SYN+ACK going out and the client's NICK retransmitted five times into
  silence.
- listen() only conflicts with another LISTENER on the port; connections
  share it by design now.
- MAX_SOCKS 4 -&gt; 6 (listener + 3 clients + 2 spare; 537 bytes of WRAM0 still
  free), and SK_ACC marks a spawned connection as not yet accepted.

tcp_close's settle is also time-boxed to ~250 ms of real time. It counted
PUMPS (8 x 8000), which is ~30s of wall clock when the peer never answers -
invisible with well-behaved peers, but a daemon refusing a client that stayed
connected froze itself, and every other client with it, for half a minute.

httpd keeps its listener open across requests now, so the next client's SYN
is accepted immediately instead of being dropped between close and re-listen:
three parallel clients go 0.1/1.1/2.1s -&gt; 0.4/1.0/0.1s.
</content>
</entry>
<entry>
<title>fs: bounds-check direct blocks - a file over 2 KiB ate the next inode</title>
<updated>2026-09-22T15:12:25Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T15:12:25Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=a7a9cfdfa6b25101ae13a87abaddce0e736c7fd1'/>
<id>urn:sha1:a7a9cfdfa6b25101ae13a87abaddce0e736c7fd1</id>
<content type='text'>
sys_getb/sys_putb indexed inode.blocks[pos/256] with no limit at all, so a
file that grew past its pointers just kept walking: first through the 4
unused bytes at the tail of the 16-byte inode, then straight into the NEXT
inode, reading its type/nlink/size as block numbers.

It hides well. Reads and writes alias identically, so a big file can be
written and read back byte-for-byte and look fine - until something else
touches the neighbouring inode, after which the tail of the file is garbage
from whatever block those bytes now name. Found by serving a 7 KB file over
httpd: corruption began at exactly offset 3072, and only sometimes.

- NDIRECT 8 -&gt; 12: bytes 4..15 are all block pointers now, which is what the
  runaway indexing was already doing by accident. Files go to 3 KiB, no
  on-disk layout change, no format bump.
- getb past the last direct block reports EOF; putb drops the byte like a
  full disk. A capped file beats a corrupted neighbour.

count 250 &gt; big (7142 bytes) now stops at 3072 and its neighbours survive.
</content>
</entry>
<entry>
<title>usr: grep, more, cp, nc - and make pollcon pump the link</title>
<updated>2026-09-22T14:02:55Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T14:02:55Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=43e43b6dcec29e2b008d20135f5d02523b62fbcf'/>
<id>urn:sha1:43e43b6dcec29e2b008d20135f5d02523b62fbcf</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>term: latch SCY in VBlank - fix ring-scroll shear on live displays</title>
<updated>2026-07-18T19:56:21Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-18T19:56:21Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=bea52bc8f99ee6dbcc8154a366560fc18e7b24c8'/>
<id>urn:sha1:bea52bc8f99ee6dbcc8154a366560fc18e7b24c8</id>
<content type='text'>
The ring-scroll wrote SCY mid-frame; SCY is sampled per scanline (on
hardware and in sl0pboy's PPU), so a scroll landing mid-frame rendered
the top of the frame at the old offset and the bottom at the new one -
a one-frame shear, invisible in frame-sampled GIF captures but ugly on
a live sixel view.

term_view_update now writes hSCY (HRAM) and a transparent VBlank ISR
(push af / apply / pop af / reti, same profile as TimerISR) copies it
to rSCY, so the view only ever moves at frame boundaries. The scroll's
tile+map writes stay immediate: the new map row is invisible at the
old SCY by construction (it's the ring row one past the visible 18).
</content>
</entry>
</feed>
