Copperline Control Protocol: live JSON-RPC control server (--control / --control-gui) and copperline-ctl#201
Merged
Merged
Conversation
A versioned JSON-RPC 2.0 interface over loopback TCP for driving the emulator from scripts, CI, editors, and AI agents: inspect state, set breakpoints, resume, rewind, inject input, swap media, and capture the display within one live session, which the snapshot-once COPPERLINE_DBG_* environment debugger cannot do and the GDB remote protocol cannot carry. Two server modes share one typed command surface (src/control/exec.rs), a thin dispatch over the same ui_*/debug_*/tt_* machinery the debugger window, console, and GDB stub already share: - --control ADDR: headless server that owns the Emulator (the gdbstub ownership pattern); paused at reset until a client resumes; polls the socket once per frame/chunk while running. - --control-gui ADDR: attaches to the normal windowed session; socket threads enqueue over mpsc, the frame loop drains at the top of about_to_wait, and an EventLoopProxy wake covers ControlFlow::Wait. Resume verbs defer their response until the machine stops and report a consistent position (pc/frame/vpos/hpos/cck/seconds/retired); an optional collect list is evaluated atomically at the stop. Auth is a generated token announced on stderr and via --control-info (0600 JSON). Injected input and media changes journal through tt_note_input and the input recorder (--record-input now works headless), so a control-driven session replays deterministically as a .clscript. capture.digest and capture.screenshot render through the side-effect-free display path and are identical in both modes; last_writer exposes the reverse-debug attribution scan. A remote stop pauses the windowed machine without commandeering the local debugger window, a user pause completes a pending resume as "user_pause", GUI stops notify as event.stopped, and the status bar shows a CCP tag while a client is attached. src/bin/copperline-ctl.rs is a std+serde_json client with one-shot and --repl modes, built by default (features: control, ctl-bin). Covered by proto/exec/session unit tests, headless end-to-end tests over real loopback sockets, and windowed drain tests through the test_app() fixture; documented in docs/debugger/control.md.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds the Copperline Control Protocol (CCP): a versioned JSON-RPC 2.0 control surface over loopback TCP for driving the emulator headlessly (--control) or attached to the GUI (--control-gui), plus a bundled CLI client (copperline-ctl). This integrates with existing debugger/time-travel/input journaling machinery so remote sessions can deterministically inspect and steer an emulator instance.
Changes:
- Introduces the new
controlmodule (wire protocol, typed request parsing/execution, session state, headless server, and windowed socket plumbing). - Integrates GUI mode control draining into the winit event loop and adds a status bar indicator when a client is connected.
- Adds extensive tests and documentation for the protocol and client, and wires new CLI flags into
copperline.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/video/window/tests.rs | Updates status bar test fixtures and adds windowed control drain tests. |
| src/video/window/statusbar.rs | Adds CCP connection indicator and extends StatusBarView. |
| src/video/window/control.rs | Implements GUI-thread drain/dispatch for windowed control server messages. |
| src/video/window.rs | Integrates control draining into about_to_wait, handles stop completion/notifications, and wires status bar state. |
| src/main.rs | Adds --control/--control-gui CLI args, validation, and headless/server dispatch. |
| src/lib.rs | Exposes control module behind the control feature. |
| src/gdbstub.rs | Exposes shared listen-address normalization for debug/control servers. |
| src/control/windowed.rs | Adds windowed mode socket accept/read/write plumbing and cross-thread message channeling. |
| src/control/session.rs | Adds per-connection session state for breaks and scheduled/journaled input. |
| src/control/proto.rs | Adds JSON-RPC framing/auth gating and payload codecs. |
| src/control/headless.rs | Adds headless control server that owns/drives the emulator session-by-session. |
| src/control/exec.rs | Adds method parsing into typed ops plus core-op execution against the emulator. |
| src/control.rs | Adds top-level control module, config/token plumbing, and shared announce/test emulator helpers. |
| src/bin/copperline-ctl.rs | Adds copperline-ctl CLI client (one-shot + REPL) for the CCP wire protocol. |
| README.md | Documents the new control protocol feature at a high level. |
| docs/myst.yml | Adds the control protocol doc page to the documentation nav. |
| docs/index.md | Links to the new control protocol documentation. |
| docs/guide/headless.md | Documents using CCP as an alternative to fixed scripted flags for headless runs. |
| docs/debugger/control.md | Adds full control protocol documentation and method reference. |
| Cargo.toml | Adds control/ctl-bin features, serde_json optional dep, and wires binaries/features. |
| Cargo.lock | Adds serde_json to the resolved dependency graph. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ature gating - Normalize break specs at install time to the exact form the machine stores key them by (PC/watch through the address-bus mask, watches word-aligned, copper breaks masked to an even 24-bit address), so break.list attaches ids reliably and removing a copper break added with an unnormalized address cannot leak it installed. - Reject out-of-range beam coordinates and catch vectors instead of silently wrapping u32 -> u16, and reject negative or non-finite run_until seconds (previously saturated to a cck target of 0 and completed immediately); non-finite input.key at_seconds likewise. - Point the StopEvent reason doc comment at docs/debugger/control.md instead of enumerating a value list that had already drifted, and make the documented list exhaustive. - Keep the control feature genuinely optional for the copperline binary: CliArgs carries raw --control/--control-gui strings, the server config is assembled at the cfg-gated dispatch sites, and a build without the feature rejects the flags with a clear error, so --no-default-features --features frontend compiles again.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
A versioned JSON-RPC 2.0 interface over loopback TCP for driving the emulator from scripts, CI, editors, and AI agents -- inspect state, set breakpoints, resume, rewind, inject input, swap media, and capture the display, all within one live session. It completes the original goal of an emulator drivable from the command line: the
COPPERLINE_DBG_*headless debugger is snapshot-once at startup, and the GDB remote protocol cannot carry chipset state, media, or input.Full protocol documentation:
docs/debugger/control.md.Design
Two server modes, one typed command surface. Requests parse into
CoreOp(executed identically by both modes throughexec_core) orHostOp(resume verbs, input, media -- applied at each driver's own boundary). Everything is a thin dispatch over the existingui_*/debug_*/tt_*machinery the debugger window, console, and GDB stub already share -- no second debugger implementation.--control ADDR(headless): the server owns theEmulator, thegdbstub::run/serveownership pattern -- one client at a time, machine paused between sessions, socket polled once per frame/chunk while running (every deterministic stop is still detected per instruction by the core).--control-gui ADDR(windowed): socket threads enqueue parsed, authenticated requests over mpsc; the frame loop drains them at the top ofabout_to_wait(the same boundary as scheduled-input scripting); anEventLoopProxywake coversControlFlow::Waitwhile paused.Key semantics:
pc/frame/vpos/hpos/cck/seconds/retired_instructions); at most one outstanding resume; an optionalcollectlist (regs, memory ranges, digest...) is evaluated atomically at the stop, so one round trip resumes, waits, and gathers.tt_note_inputand the input recorder (--record-inputnow works headless too), every mutation reports the emulated time it landed, so an interactive control session converts into a replayable.clscript.mem.writeis the documented exception (flaggedreplay_unsafewhile time travel is armed).capture.digest/capture.screenshotrender through the side-effect-free display path and are identical in both modes -- the cheap change-detection primitive for regression scripts.last_writerexposes the reverse-debug attribution scan (machine parked at the writing instruction, position in the reply).user_pause; GUI-driven stops notify the client asevent.stopped; the status bar shows aCCPtag while a client is attached; session-owned breakpoints are removed on disconnect, GUI-set points left alone.--control-info FILE(0600 JSON) -- the preferred handoff since command lines are visible inps. Loopback-only by default, same trust model as--gdb.copperline-ctl(std + serde_json only) wraps the wire protocol for shells: one-shot mode and--repl, built by default (control/ctl-binfeatures; browser wasm builds compile it out via--no-default-features).Testing
test_app()fixture (stop completion without opening the debugger, run_until frame targets in the burst, user-pause completion, recorder journaling, connect/shutdown).cargo clippy --all-targetsandcargo fmt --checkclean;--no-default-features(+/-frontend,control) combinations build.copperline-ctl: exact run_until landings, digest stability,last_writerfinding the exception-vector install 0.8s into boot, cck-exact save/load state restore, and a windowed session (pause a running machine, step, run_until frame 400, screenshot, cleanshutdown).--record-inputjournaled a control-injected tap askey-after 3.000 0x45 66.No timing-model changes: STATE_VERSION untouched, no golden-probe re-bless needed.
Known v1 limits (documented)
Single client at a time (GDB-stub convention);
mem.writeis not aReplayAction; windowedreverse_continueblocks the GUI during replay; configuration is CLI-only like--gdb(no[control]TOML section); MCP/editor adapters are deliberately out of scope -- they sit on top of this neutral protocol.