JNEXT
Skip to content

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.