aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-17 20:31:00 +0200
committeruser <user@clank>2026-07-17 20:31:00 +0200
commit138e0ad2eb25ee078464c7e2ff7d6a919a188634 (patch)
tree67090285cbd0ec47a4d2f87a93717f63aebf7d84 /tools
parentirc: a bitchx/irssi-style IRC client (diff)
downloadgbos-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 '')
-rwxr-xr-xtools/gbhub77
-rwxr-xr-xtools/gbtype79
2 files changed, 153 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)
diff --git a/tools/gbtype b/tools/gbtype
new file mode 100755
index 0000000..f58a1f8
--- /dev/null
+++ b/tools/gbtype
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+"""gbtype - inject shell input into a running gbhub Game Boy, no OSK needed.
+
+The Game Boy's link port *is* its console when nothing SLIP-frames the bytes,
+so text sent over the hub's control socket lands straight on the shell's stdin
+- exactly as if typed on the on-screen keyboard, but driven from your host.
+
+Start the hub first (in another terminal): sudo tools/gbhub --daemon
+
+ tools/gbtype 'ls -l' run 'ls -l' on GB0 (a trailing Enter is added)
+ tools/gbtype -g 1 ps target GB1 (the Nth joined Game Boy)
+ tools/gbtype -n abc type 'abc' but no Enter (leave it on the line)
+ tools/gbtype -r just press Enter
+ tools/gbtype -l list the connected Game Boys
+ printf 'ls\\ncat readme\\n' | tools/gbtype pipe a script via stdin
+
+Options must come before the command; the first non-option word starts the
+literal text (so 'gbtype ls -l' needs no quoting, and '-l' there is not --list).
+No root needed - the hub opens the control socket world-connectable."""
+import os, sys, socket
+
+CTL = "/tmp/gbhub.ctl"
+
+def usage(code=0):
+ (sys.stdout if code == 0 else sys.stderr).write(__doc__ + "\n")
+ sys.exit(code)
+
+def send(header, payload=b""):
+ if not os.path.exists(CTL):
+ sys.exit("gbtype: no hub control socket at %s\n"
+ " start it first: sudo tools/gbhub --daemon" % CTL)
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ try: s.connect(CTL)
+ except OSError as e: sys.exit("gbtype: cannot connect %s (%s)" % (CTL, e))
+ s.sendall(header + b"\n" + payload)
+ s.shutdown(socket.SHUT_WR) # end-of-payload marker for the hub
+ out = b""
+ while True:
+ try: d = s.recv(4096)
+ except Exception: break
+ if not d: break
+ out += d
+ s.close()
+ sys.stdout.write(out.decode(errors="replace"))
+
+def main():
+ args = sys.argv[1:]
+ idx = None
+ newline = True
+ i = 0
+ while i < len(args):
+ a = args[i]
+ if a in ("-h", "--help"): usage(0)
+ elif a in ("-l", "--list"): send(b"list"); return
+ elif a in ("-g", "--gb"):
+ i += 1
+ if i >= len(args): usage(1)
+ idx = args[i]
+ elif a in ("-n", "--no-enter"): newline = False
+ elif a in ("-r", "--enter"):
+ send(b"type" + (b" " + idx.encode() if idx else b""), b"\n"); return
+ elif a == "--": i += 1; break
+ elif a.startswith("-") and len(a) > 1:
+ sys.stderr.write("gbtype: unknown option %s\n" % a); usage(1)
+ else: break # first positional: rest is literal text
+ i += 1
+
+ words = args[i:]
+ if words:
+ payload = " ".join(words).encode() + (b"\n" if newline else b"")
+ else:
+ payload = sys.stdin.buffer.read() # piped script, sent verbatim
+ if newline and payload and not payload.endswith(b"\n"):
+ payload += b"\n"
+
+ send(b"type" + (b" " + idx.encode() if idx is not None else b""), payload)
+
+if __name__ == "__main__":
+ main()