| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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 `>>`.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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 -> 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 -> 0.4/1.0/0.1s.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
/# httpd &
host$ curl http://10.0.0.2/ -> a listing of the cart RAM
host$ curl http://10.0.0.2/readme -> the file, from battery-backed SRAM
GET only. "/" serves /index.html if present, else a generated listing; any
directory lists with every entry linked, so you can click around a 1997
handheld's disk from a browser. Content type from the extension. No
Content-Length: HTTP/1.0 + Connection: close delimits the body by closing,
which suits a stack with one in-flight segment.
All output goes through one 200-byte buffer, so each net_send is a full
segment instead of one tiny segment per string.
It drains the whole request, not just the request line. That is not
politeness: leaving the rest unread means those segments are never
acknowledged, so the client retransmits headers for a minute, never sends
its FIN, and its dead connection keeps arriving at port 80 long after we
re-listen. One curl with 1.4 KB of headers stalled every following request
for 30-120s.
It deliberately does NOT read the keyboard either. Backgrounded - the useful
way to run it - every key it polled would be a key stolen from the shell's
prompt. It is a daemon: kill(1) stops it, and the kernel reclaims its socket.
Verified with curl: index, byte-exact file bodies, nested paths, directory
listings, 404, 501, a 1.4 KB request spanning segments, browser user-agents,
three parallel clients (0.1/1.1/2.1s), the shell staying usable alongside it,
and six kill/restart cycles - more than there are sockets.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Found by writing nc(1): a 96-byte receive buffer against a host that
answers in 200-byte segments hung the process every time.
- recv/recv_nb copied RXLEN bytes into the caller's buffer and never
looked at the caller's maxlen, so any short buffer got overrun and its
stack smashed. Deliver min(RXLEN, maxlen) and keep the tail in RXBUF
for the next call. wget only survived because its buffer (220) happens
to exceed our MSS.
- tcp_buffer_data overwrote RXBUF from offset 0 on every segment, so a
segment arriving before the app drained the previous one destroyed
unread bytes - silent, undetectable data loss. Append at RXBUF+RXLEN
instead, and drop (without advancing rcv_nxt) when it doesn't fit, so
the peer retransmits once there's room. RXLEN is one byte, so 255 is
the capacity that matters, not RXBUF's 288.
- A draining recv now clears RXLEN as well as HASRX: RXLEN is the append
offset, and a stale one made every later segment look too big to fit,
stalling the connection until the peer gave up.
- A FIN past a gap is not end-of-stream. tcp_in accepted any FIN whose
segment carried no data, without a sequence check, marking the socket
DONE and making recv report EOF at the hole - truncating a transfer
right before its last segments. Decide in-order-ness once, before
rcv_nxt moves, and re-ACK an out-of-order FIN instead.
Verified against a 6300-byte reply received while sending 831 bytes:
byte-exact (6339 = payload + banner, 303 lines, first/mid/last intact),
plus no regression in ping / nslookup / wget / irc.
|
| | |
|
| |
|
|
|
|
|
|
|
| |
README is now pitch -> proof (real capture gif) -> quick start -> docs
pointers, with the network stack and gbhub/gbjoin/gbtype getting
explicit billing. The full technical breakdown (memory model, syscall
ABI, fork/exec/context switch, FS layout, toolchain war stories) moves
to docs/internals.md, plus previously-undocumented sections on the
network stack and the terminal/OSK.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Rewrite the notes from 'open / not root-caused' to the resolution: the
socket never mysteriously dies; every producible drop signature is the
emulator process dying silently (SIGPIPE on console echo after a hub
restart, terminal death/HUP/stray quit keys in windowed mode - which
exactly explains the 'only when idle' correlation - or a guest $ED
poweroff), plus observation artifacts in the original session's
tooling. Records the fixes shipped on both sides (emulator link
auto-reconnect + SIGPIPE ignore + $ED logging; hub eof-vs-error
logging, kept stderr, EBUSY guard), the honest spawn-mode caveat, and
what's still worth doing.
|
| |
|