Remote-control a real Amiga over TCP/IP. Run commands, capture the screen, inject mouse and keyboard input, and reboot it — all from another machine, with nobody sitting at the Amiga.
$ nhctl.py --host 192.168.1.32 EXEC version
rc=0
Kickstart 47.115, Workbench 47.5
$ nhctl.py --host 192.168.1.32 SCREENSHOT desktop.png
OK desktop.png 1024x768x24
It exists because testing Amiga software normally means physically sitting at the machine: clicking through a GUI, squinting at a screen, and power-cycling by hand. NetHarness turns that into something a script can drive.
| Command | |
|---|---|
EXEC <command> |
Run any AmigaDOS command; returns its exit code and its output |
SCREENSHOT [file] |
Capture the front screen as a PNG |
CLICK x y, MOVETO, MOVE, BUTTON |
Mouse, injected at hardware level |
TYPE <text>, PRESSKEY, KEY, CLEARFIELD |
Keyboard, with shift handled |
RESETINPUT |
Release any stuck buttons or qualifiers |
REBOOT |
Cold-reboot the machine (flushes disks first) |
PING |
Liveness check |
Input is injected through input.device (IND_WRITEEVENT), the same path real
hardware uses, so it exercises window activation, GadTools, menus — everything
a genuine click would.
| Command | |
|---|---|
UITREE |
The front screen's windows and gadgets: id, kind, bounds, label, string contents |
UICLICK <text> / UICLICKID <id> |
Click a gadget by identity, not coordinates |
MENUS / MENUSEL <menu> <item> |
Enumerate the menu strip; pick an item by name |
POINTER |
Where the pointer actually is (it's a hardware sprite — invisible in screenshots) |
SHOTREGION x y w h |
Capture just a region — far cheaper than a full frame |
REGIONSUM x y w h |
4-byte checksum: "has this redrawn yet?" |
WAITCHANGE x y w h |
Block until a region actually changes, instead of guessing a delay |
GETFILE / PUTFILE |
Binary-safe file transfer — deploy a build through the harness itself |
SCREENS |
List open screens, front to back |
netharness [port] |
Listen port is an argument, so a new build can be tested beside the running one |
New in 1.4:
| Command | |
|---|---|
RELOAD |
Apply a staged C:netharness.new and restart the harness in place - no machine reboot |
New in 1.5:
- Plays fair with the TCP/IP stack's own shutdown: the harness blocks in
WaitSelecton socket and break signal, so a stack shutdown (Roadshow'sNetShutdown) sees it releasebsdsocket.libraryimmediately instead of timing out against a blockedaccept()/recv()and deferring the teardown.
New in 1.6:
- Send-stall guard: a controller that vanishes mid-transfer no longer freezes
the harness in a blocking
send()- every send waits for writability first (20 s cap), then the dead client is dropped. - Idle-client reaper: a silent client is disconnected after 600 s, so a controller that died without closing can't hold the single client slot.
New in 1.7:
- Bring-up retries forever instead of giving up after 60 s: 2 s apart for the first minute, then every 10 s (CTRL-C aborts the wait). The harness now self-connects whenever its network path appears late - a companion Pi still booting, WiFi rejoining, or the stack restarting - with no manual run needed at the machine.
UITREE + UICLICK are the headline: drive the GUI by what things are.
$ nhctl.py UITREE
W 0 0 11 332 199 "Time Preferences"
G 16 STRING 58 29 44 8 "" "2026"
G 15 GADGET 128 146 192 10 "Hours" ""
Honest limitation: some gadgets expose no text to Intuition — OS 3.2 Prefs'
Save/Use/Cancel buttons report empty labels because those apps draw the text
themselves. Slider labels and string-gadget contents do come through. Use
UICLICKID <id> for such buttons, UICLICK <text> everywhere else.
Every input command is acknowledged after injection, so OK means the
Amiga really did it — not merely that a packet was sent.
Driving a GUI blind is miserable: you guess coordinates, click, screenshot,
and hope. EXEC avoids most of that — anything expressible as a Shell command
is one call, with output returned to you. Combined with ARexx it reaches inside
applications too:
nhctl.py EXEC 'rx "address IBROWSE; ''GOTOURL https://aminet.net''"'
On the Amiga (needs a TCP/IP stack — Roadshow, AmiTCP, Miami, a314bsd…):
Execute Install
Run >NIL: C:netharness
It listens on TCP port 7800. To start it at every boot, add that Run line
to the end of S:User-Startup, after your TCP/IP stack comes up.
One addressing nuance: with a proxy-style stack such as a314bsd (where the sockets actually live on a Raspberry Pi), connect to the proxy host's IP — the Amiga itself has no address of its own.
Use the installer rather than copying by hand, especially if you unpacked the
.zip: ZIP archives cannot carry AmigaDOS protection bits, so netharness
arrives without its e (executable) flag and the Shell refuses to run it. The
installer sets the flag for you; to do it manually:
Copy netharness C:
Protect C:netharness +e
The .lha archive preserves the flag, so it needs no such fixup.
On the controlling machine (Python 3, plus Pillow for screenshots):
python3 nhctl.py --host <amiga-ip> PING
python3 nhctl.py --host <amiga-ip> EXEC list SYS:
python3 nhctl.py --host <amiga-ip> --batch < commands.txt
--batch sends many commands over one connection, which is much quicker than
one invocation each.
Screen capture handles the awkward cases real Amigas actually present:
- Interleaved bitmaps, which AGA Workbench screens normally use
- True-colour RTG screens (Picasso96/CyberGraphX) via
ReadPixelArray - Planar screens of any depth, with the screen's real palette
The mouse pointer is a hardware sprite and never appears in a capture — verify mouse actions by their effect (a window activating, a gadget highlighting), not by looking for the cursor.
- Every input command is acknowledged by the Amiga after injection, so
OKmeans delivered and injected, not merely sent. Without that, a command that lands on nothing looks exactly like one that worked. - Prefer
RELOADoverREBOOTfor updates.REBOOTcallsColdReboot(), and not every machine survives a warm CPU reset - one of the test machines here (an A2000) lands on a grey screen and needs a power cycle.RELOADapplies a stagedC:netharness.newand restarts the harness in place instead, which is what rebooting was being used for anyway. It applies the update while still serving, so a failed copy leaves the working build running rather than bricking your remote access. REBOOTflushes filesystems first.ColdReboot()resets instantly, and without an explicit flush any file written moments earlier is quietly lost — which looks uncannily like the file "reverting" after a reboot.- DOS requesters are suppressed (
pr_WindowPtr = -1). A command touching a missing volume would otherwise raise "Please insert volume…" and block the single-threaded harness with no way to dismiss it remotely. EXECis synchronous. A command that never returns holds the connection, so start long-lived programs withEXEC run >NIL: <program>.- There is no authentication. It executes commands as sent — use it on a network you trust, not a public one.
The binary carries a standard AmigaDOS version cookie:
$ nhctl.py --host <ip> EXEC "version C:netharness full"
netharness 1.4 (08/12/26)
cd amiga && make # needs the bebbo amiga-gcc cross-compiler
The Amiga side is a single C file using only standard bsdsocket.library
calls, built -m68020 — change that to -m68000 in amiga/Makefile if you
need it to run on a stock 68000 machine.
MIT — see LICENSE.