Skip to content

os/v3/puppy: the console-TUI promise 'a paint failure redraws' was never implemented - one EINTR ended the console, and the cycle-26 heal doubled the exposure #1101

Closed

Description

@bong-water-water-bong

os/v3/puppy/console-tui/src/main.rs opens with three promises, and the first one covers paint failures:

  • the console never goes dead. q does not exit; Ctrl-C does not exit; a paint failure redraws; if this process does
    die, 1bit-console falls back to the dialog menu and /etc/inittab respawns it.

The loop does not implement the bolded half: the first paint error ends the process.

if term.draw(|f| ui::draw(f, &app, &mut links)).is_err() {
    break 1;
}

Two of the three error classes there are handled correctly by exiting - a terminal that cannot be written should fall back to
the dialog menu - but the third one is not an error at all:

  • ErrorKind::Interrupted: a signal arrived before the write reached the terminal. Nothing was drawn, the screen is exactly
    as it was, and the right answer is to paint again on the next pass. Ending the console over it turns a harmless resize into a
    screen change, which is the one thing this binary exists to prevent.
  • and it is reachable: crossterm's event source installs a SIGWINCH handler (that is how resize events arrive), so an ssh
    user dragging a window edge is the realistic trigger. On tty1 there is no resizing, so the appliance itself is unlikely to see
    it - ssh -t root@<box> /usr/local/bin/1bit-console-tui is the documented way in, and that is where windows get resized.

#1093 made it twice as likely per second. The heal added term.clear() before each full repaint, guarded the same way, so the
loop now has two "one error ends the console" points and the clear runs once a second instead of never.

Fix

paint_failed(&io::Error) -> bool = "is this error fatal for the console?", true for everything except Interrupted; clear()
and draw() both consult it - an interrupted clear is ignored (the heal retries next pass), an interrupted draw skips the
hyperlink pass and repaints on the next loop iteration, and any other error still exits so 1bit-console shows the dialog menu.

Measured

  • Unit test an_interrupted_paint_is_repainted_but_a_dead_terminal_ends_the_console: Interrupted -> retry; BrokenPipe,
    PermissionDenied, NotFound, Other -> fatal. 53 tests (52 + this).
  • 12 window resizes in 2 s while the TUI runs (tmux resize-window, i.e. a SIGWINCH storm): the session survives, nine actions
    drawn, q still stays on the console.
  • The heal itself is unchanged on the happy path: a stray write is gone at +1.0 s (80x25, pty), both committed snapshots
    byte-identical, the eight gate strings still on the 80x25 frame, artifact 906705b04783a87d (1,581,568 B, static).

Not measured: the EINTR itself. Inducing a signal inside crossterm's write is not something a pty can be made to do on
demand, so the classification is unit-tested and the code path is reasoned - what is demonstrated is the resize storm (survives)
and that a fatal error still exits (the fallback path was measured in cycle 18: TUI exits 0 / exits 7 / absent all end in the
dialog menu).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions