7.3 Exit codes, and the capture that was never taken¶
jnext exits 0 on success and non-zero on error. One case deserves calling
out, because it is the one that silently ruins a CI pipeline everywhere else:
A screenshot that was requested and never taken is an error. If the automatic exit fires before the capture comes due, or before a frame has been rendered for it, JNEXT logs an error and exits non-zero. It never writes nothing and reports success, and it never writes a stale frame in place of the one you asked for.
You can see it directly:
$ jnext --headless --load myprog.nex \
--delayed-screenshot never.png --delayed-screenshot-frames 5000 \
--delayed-automatic-exit-frames 100
...
[platform] [error] --delayed-screenshot: NO screenshot was written to
'never.png' (layers: all); --delayed-automatic-exit fired 4899 frame(s)
before the capture was due. Exiting non-zero.
$ echo $?
1
A capture is always taken from a frame rendered after it comes due, so a
paused machine defers it. Under --headless nothing resumes a paused machine:
a program that stops itself with --magic-breakpoint before its capture point
renders no further frames, and the exit bound finds the capture still waiting:
$ jnext --headless --magic-breakpoint --load stops-itself.nex \
--delayed-screenshot paused.png --delayed-screenshot-frames 200 \
--delayed-automatic-exit-frames 250
...
[platform] [error] --delayed-screenshot: NO screenshot was written to
'paused.png' (layers: all); the emulator exited with the capture still
pending. Exiting non-zero.
$ echo $?
1
No paused.png is written, not even the picture that was on screen when the
machine stopped: that would be a frame from before the one you asked for.
Without that guarantee, a script whose program hangs early gets no PNG, a zero exit status, and — if the comparison step is written defensively — a green build. The contract means a broken run fails loudly at the emulator, before your test logic ever has to be clever about it.
Give the exit bound headroom over the capture point (the example above uses
frames + 50) so a slightly slower boot does not trip this.
Nothing else the command line deferred may be cut off either. The same
error and non-zero exit follow when the automatic exit fires before any other
deferred work has happened: a --load still waiting out its boot delay (a
.tzx or .wav waits 100 frames for BASIC), an --inject with
--inject-delay, an --rzx-record waiting for that load, a
--delayed-keypress or --delayed-nmi still to come, a --delayed-pok-frames
trainer file still to be applied, a --joy-uart-rx stream still in its start
delay, or an edge of a scheduled ESP outage. The log names each one:
$ jnext --headless --machine 48k --load game.tzx --delayed-automatic-exit-frames 5
...
[platform] [error] --load: 'game.tzx' never happened - --delayed-automatic-exit
fired first. Exiting non-zero.
$ echo $?
1
A program that fails to load is an error too. If the file given to
--load (or as a bare file name) cannot be loaded — truncated, too small, not
the format its extension says — JNEXT logs the error, keeps running the bare
machine until it exits, and then exits non-zero, whatever the format. The same
holds for an RZX recording given to --load or --rzx-play, under
--headless, in the window and in the SDL-only build alike. A script never mistakes "the program never loaded" for a clean
run.
A stopped run is not a successful one. A timeout or kill that stops
JNEXT (SIGTERM, or Ctrl-C's SIGINT) still gets its --wav-record, --record
and --rzx-record output finalized, as a window close would, under
--headless, in the window and in the SDL-only build alike. JNEXT then ends by
that signal, so the status is 128+N (143, or 130 for Ctrl-C) and a stopped run
never reads as success. A second signal (either of the two) ends it at once, and a signal that was
ignored when JNEXT started stays ignored. This does not apply on Windows.