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.
This commit is contained in:
42
TODO.md
42
TODO.md
@@ -34,6 +34,48 @@ wizard commands).
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-08-09 Signal-time terminal restore (`sig-leave`, closes #12): the port
|
||||
handled only SIGHUP and SIGTERM, so SIGINT and SIGQUIT killed the process with
|
||||
tcell still holding the tty, leaving the user at a shell with no echo. All
|
||||
four signals now go to one `os/signal` channel read by one goroutine in
|
||||
`cmd/rogue/main.go`, and every path calls `Terminal.Fini` before `os.Exit(0)`
|
||||
— C's `leave()`, "leave quickly but curteously". **The decision** (written
|
||||
into the `savesOnSignal` comment): SIGHUP/SIGTERM keep autosaving,
|
||||
SIGINT/SIGQUIT restore and exit **without** saving. C never saves on INT or
|
||||
QUIT anywhere — `leave()` is endwin-and-exit, `quit()` confirms/scores/exits,
|
||||
`endit()` goes through `fatal()`, and `save.c auto_save` is reserved for
|
||||
HUP/TERM — and the semantics agree: HUP/TERM are involuntary teardown worth
|
||||
rescuing a game from, while INT/QUIT are a deliberate "stop now" that must not
|
||||
become a one-keystroke checkpoint against a save discipline built to be
|
||||
anti-save-scum. It is also the safe choice: `AutoSave` gob-encodes live state
|
||||
that the main goroutine is still mutating, after removing the old file, so on
|
||||
the signals with nothing to rescue the port takes the option with no
|
||||
corruption window. The single-reader design closes the window the issue warned
|
||||
about: a second signal arriving mid-save stays unread in the buffer instead of
|
||||
exiting out from under the writer (`TestLeaveOnSignalIgnoresLaterSignals`
|
||||
reproduces exactly that interleaving). New `cmd/rogue/main_test.go` covers the
|
||||
ordering for each signal, the save/no-save split against `savesOnSignal`, the
|
||||
mid-save-second-signal case, and real SIGINT/SIGQUIT/SIGHUP/SIGTERM delivered
|
||||
to the test process through the same `notifySignals` wiring the game uses; the
|
||||
tty leaving raw mode is the one step not checkable headlessly (it needs a
|
||||
controlling terminal), and `term.Tcell.Fini` is a direct pass-through to
|
||||
tcell's `Screen.Fini` that `myExit` already depends on. Two premises in the
|
||||
issue turned out to be wrong and are recorded in ARCHITECTURE.md: `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`/`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 §9 gained rows for SIGTSTP/`tstp()` (dropped:
|
||||
raw mode means Ctrl-Z cannot reach us, a suspend from the signal goroutine
|
||||
would race the drawing goroutine, and C armed `tstp` only after a `restore()`;
|
||||
the `!` shell escape covers the need), for SIGINT not routing to the
|
||||
interactive `quit()` prompt, and for `auto_save` on the fault signals; §5.3's
|
||||
claim that tcell handles SIGTSTP was false — tcell registers only SIGWINCH —
|
||||
and is corrected. `Next Step` deliberately not rotated: out-of-band issue
|
||||
work.
|
||||
|
||||
- 2026-08-09 Wizard-create bounds fix (`fix/wizard-which-bounds`, closes #10):
|
||||
`createObj` stored the raw `0-f` nibble as `Object.Which` with no bounds
|
||||
check, so wizard mode -> `C` -> `/` -> `f` produced a wand numbered 15 against
|
||||
|
||||
Reference in New Issue
Block a user