Commit Graph

1 Commits

Author SHA1 Message Date
f602ecdbbf fix: restore the terminal on SIGINT/SIGQUIT (closes #12)
The port installed handlers for SIGHUP and SIGTERM only, so SIGINT and
SIGQUIT killed the process with tcell still holding the tty and dropped
the user into a shell with no echo and a scrambled screen. All four
signals now go to one os/signal channel read by one goroutine, and every
path calls Terminal.Fini before os.Exit(0) -- C's leave(), "leave
quickly but curteously" (main.c).

The save decision, written into the savesOnSignal comment: SIGHUP and
SIGTERM keep autosaving; SIGINT and SIGQUIT restore and exit without
saving. No path in C saves on INT or QUIT (leave() is endwin-and-exit,
quit() confirms/scores/exits, endit() goes through fatal(), and
save.c auto_save is reserved for HUP/TERM), the semantics agree
(involuntary teardown is worth rescuing a game from; a deliberate "stop
now" must not become a one-keystroke checkpoint against an anti-save-scum
save discipline), and it is the safe choice, since AutoSave gob-encodes
live state the main goroutine is still mutating after removing the old
file.

One reader of one signal is also what closes the corruption window: a
second signal arriving while a SIGHUP's AutoSave is mid-write stays
unread in the buffer instead of exiting out from under the writer.

cmd/rogue/main_test.go covers the ordering per signal, the save/no-save
split, the mid-save second-signal interleaving, and real
SIGINT/SIGQUIT/SIGHUP/SIGTERM delivered to the test process through the
same notifySignals wiring the game uses.

Two premises behind the report were wrong and are recorded rather than
silently fixed: leave() is not installed on SIGINT/SIGQUIT during play
(the wiring is in mdport.c; the shipped build calls md_onsignal_default
and installs nothing, and leave() appears only in the endgame paths of
rip.c and main.c), and Ctrl-C never generated SIGINT here anyway, since
tcell's raw mode clears ISIG and the key arrives as byte 0x03 -- as it
did in C, whose setup() calls curses raw(). The real exposure is
kill -INT / kill -QUIT and the window before term.New().

ARCHITECTURE.md section 9 gains rows for SIGTSTP/tstp() (deliberately
dropped: raw mode means Ctrl-Z cannot reach the process, suspending the
screen from the signal goroutine would race the drawing goroutine, and C
armed tstp only after a successful restore(); the ! shell escape covers
the need), for SIGINT not routing to the interactive quit() prompt, and
for auto_save on the fault signals. Section 5.3's claim that tcell
handles SIGTSTP was false -- tcell registers only SIGWINCH -- and is
corrected.
2026-08-09 05:45:45 +00:00