diff options
| author | user <user@clank> | 2026-07-17 20:31:00 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-07-17 20:31:00 +0200 |
| commit | 138e0ad2eb25ee078464c7e2ff7d6a919a188634 (patch) | |
| tree | 67090285cbd0ec47a4d2f87a93717f63aebf7d84 /tools/gbhub | |
| parent | irc: a bitchx/irssi-style IRC client (diff) | |
| download | gbos-138e0ad2eb25ee078464c7e2ff7d6a919a188634.tar.gz gbos-138e0ad2eb25ee078464c7e2ff7d6a919a188634.tar.xz gbos-138e0ad2eb25ee078464c7e2ff7d6a919a188634.zip | |
gbhub: control socket + gbtype to inject shell input without the OSK
Typing into a Game Boy meant driving the on-screen keyboard by hand. But
the link port already *is* the shell's console: bytes the hub sends
un-SLIP-framed land straight on stdin (that's what GB.type() has always
done for spawn-mode command scripting). This just exposes it.
gbhub now serves a control socket (/tmp/gbhub.ctl, world-connectable like
the join socket) accepting two commands: 'list' and 'type [idx]' followed
by a payload (delimited by the client's half-close). Injection is chunked
and paced to respect the 64-byte kernel console ring, so arbitrarily long
input - even multi-line scripts - reaches the shell without overflow. The
server runs in both daemon and spawn modes.
tools/gbtype is the client:
tools/gbtype 'ls -l' run a command on GB0 (Enter appended)
tools/gbtype -g 1 ps target the Nth joined Game Boy
tools/gbtype -n abc no trailing Enter
tools/gbtype -r just press Enter
tools/gbtype -l list connected Game Boys
printf 'ls\nuname\n' | tools/gbtype pipe a script via stdin
Options precede the command; the first non-option word starts literal
text, so 'gbtype ls -l' needs no quoting. No root required.
Verified end to end: injected list/uname/echo through the real CTL
protocol and saw the shell execute each and echo output back.
Diffstat (limited to 'tools/gbhub')
| -rwxr-xr-x | tools/gbhub | 77 |
1 files changed, 74 insertions, 3 deletions
diff --git a/tools/gbhub b/tools/gbhub index 85a4843..21ee19b 100755 --- a/tools/gbhub +++ b/tools/gbhub @@ -22,6 +22,7 @@ HOME = os.path.expanduser("~" + (os.environ.get("SUDO_USER") or "")) EMU = os.path.join(HOME, "dev/gbc/build/sl0pboy") ROM = os.path.join(HOME, "dev/gbos/gbos.gb") SOCK = "/tmp/gbhub.sock" +CTL = "/tmp/gbhub.ctl" # control socket: external shell-input injection (gbtype) IFACE, GW, MTU = "gbtun0", b"\x0a\x00\x00\x01", 320 END, ESC, BCAST = 0xC0, 0xDB, b"\xff\xff\xff\xff" @@ -142,8 +143,9 @@ def net_setup(): def cleanup(): sh("iptables -t nat -D POSTROUTING -s 10.0.0.0/24 -o %s -j MASQUERADE" % wan) sh("iptables -D FORWARD -i %s -j ACCEPT" % IFACE); sh("iptables -D FORWARD -o %s -j ACCEPT" % IFACE) - try: os.unlink(SOCK) - except Exception: pass + for p in (SOCK, CTL): + try: os.unlink(p) + except Exception: pass atexit.register(cleanup) return wan @@ -157,6 +159,73 @@ def register(conn): % (time.strftime("%H:%M:%S"), gb.idx, octet)) return gb +# ----------------------------------------------------------------------------- +# Control socket: inject shell input into a GB from the host (see tools/gbtype). +# The link port is the GB's console when nothing SLIP-frames the bytes, so text +# written with GB.type() lands on the shell's stdin - the OSK, remotely. +# ----------------------------------------------------------------------------- +def _find_gb(idx): + gbs = sorted(conns.values(), key=lambda g: g.idx) + if not gbs: return None + if idx is None: return gbs[0] # default: lowest-index GB + for g in gbs: + if g.idx == idx: return g + return None + +def _type_paced(gb, data): + # The kernel console ring is 64 bytes and one net_pump drains a whole burst + # into it at once, so feed in small chunks with a brief gap - the shell + # keeps up easily, and arbitrarily long scripts inject without overflow. + for i in range(0, len(data), 32): + gb.send(data[i:i+32]); time.sleep(0.04) + +def _control_client(conn): + try: + conn.settimeout(5) + data = b"" + while True: # read header + payload + try: d = conn.recv(4096) # until the client + except Exception: break # half-closes (SHUT_WR) + if not d: break + data += d + head, _, payload = data.partition(b"\n") + parts = head.decode(errors="replace").split() + cmd = parts[0].lower() if parts else "" + if cmd == "list": + gbs = sorted(conns.values(), key=lambda g: g.idx) + msg = "".join("GB%d 10.0.0.%d\n" % (g.idx, g.octet) for g in gbs) + conn.sendall(msg.encode() or b"(no Game Boys connected)\n") + elif cmd == "type": + idx = None + if len(parts) >= 2: + try: idx = int(parts[1]) + except ValueError: idx = None + gb = _find_gb(idx) + if gb is None: + conn.sendall(b"err: no such Game Boy (try 'list')\n") + else: + _type_paced(gb, payload) + conn.sendall(("ok: %d bytes -> GB%d\n" % (len(payload), gb.idx)).encode()) + else: + conn.sendall(b"err: commands are 'type [idx]' (payload follows newline) or 'list'\n") + except Exception: + pass + finally: + try: conn.close() + except Exception: pass + +def control_server(): + try: os.unlink(CTL) + except Exception: pass + c = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + c.bind(CTL); c.listen(8) + try: os.chmod(CTL, 0o777) # let non-root gbtype connect + except Exception: pass + while True: + try: conn, _ = c.accept() + except Exception: return + threading.Thread(target=_control_client, args=(conn,), daemon=True).start() + def main(): if os.geteuid() != 0: sys.exit("gbhub: run as root") args = sys.argv[1:] @@ -166,11 +235,13 @@ def main(): except Exception: pass srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); srv.bind(SOCK); srv.listen(8) threading.Thread(target=tun_reader, daemon=True).start() + threading.Thread(target=control_server, daemon=True).start() # gbtype input if daemon: os.chmod(SOCK, 0o777) # let non-root gbjoin connect sys.stderr.write("[gbhub] up on 10.0.0.0/24, NAT via %s. " - "join Game Boys in other terminals: tools/gbjoin\n" % wan) + "join Game Boys in other terminals: tools/gbjoin\n" + "[gbhub] inject shell input without the OSK: tools/gbtype 'ls'\n" % wan) try: while True: conn, _ = srv.accept(); register(conn) |
