Browse Source
New goal (user-approved wording): punkshell exit paths on unix ttys restore blocking on the shared tty file description and drain pending stdout/stderr (bounded best-effort) before teardown - closing the tclIO behaviour where stdin's non-blocking read windows make stdout non-blocking underneath, 'flush' silently no-ops once a background flush is scheduled, and TclInExit discards EAGAIN'd buffers (measured 2026-08-03: 161/5000 lines survive exit on a throttled WSL pty; ~498KB pending after a 'successful' flush). Guard homes in punk::console, invoked after the G-038 eof-restart decision point; acceptance includes complete delivery under a slow reader, bounded exit under a dead reader, unchanged windows/piped behaviour, ARCHITECTURE.md exit-section documentation, and a unix-pty-gated pin. Related: G-038, G-013, G-011, G-090 (+ archived G-036 lineage). goals_lint clean; overlap survey recorded in Notes. Claude-Session: https://claude.ai/code/session_01QgaxV27VZkmEec7oNbEVFc Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.commaster
2 changed files with 73 additions and 0 deletions
@ -0,0 +1,69 @@ |
|||||||
|
# G-163 unix tty exit-path output drain: no tail loss from shared-description O_NONBLOCK |
||||||
|
|
||||||
|
Status: proposed |
||||||
|
Scope: src/modules/punk/repl-999999.0a1.0.tm (exit/eof done paths, reader-window blocking-restore discipline); src/modules/punk/console-999999.0a1.0.tm (drain helper, -blocking 0 window sites); src/lib/app-punkshell/punkshell.tcl (final-exit seam); ARCHITECTURE.md (Process exit and shutdown section); src/tests/shell/testsuites/punkexe/ (unix-pty-gated drain pin) |
||||||
|
Goal: punkshell sessions on unix ttys do not silently lose queued stdout/stderr output at exit: the exit paths restore blocking on the shared tty file description (undoing the O_NONBLOCK that stdin's non-blocking read windows set underneath Tcl's per-channel view) and drain pending channel output with a bounded best-effort wait before teardown - closing the tclIO behaviour where a large emission near exit is silently truncated (background-flush-scheduled makes 'flush' a no-op and TclInExit discards EAGAIN'd buffers). |
||||||
|
Acceptance: on a linux/WSL pty with a rate-limited reader, a punkshell kit session that emits a numbered-line block larger than the pty buffer and immediately exits delivers the block complete (all markers present in the captured stream), where the same harness with the guard absent demonstrates tail loss (pre-fix baseline recorded in this file); in that scenario the exit path reaches 'chan pending output' 0 on stdout and stderr before teardown; with a fully-stopped reader the session still exits within the guard's declared bounded timeout (best-effort drain, never a hang); existing windows exit/quit behaviour and the piped-eof handling path are unchanged (relevant punkexe pins stay green); the mechanism and guard are documented in ARCHITECTURE.md's Process exit and shutdown section; a unix-pty-gated pin covers the complete-delivery and bounded-timeout cases (constraint-skipped where no unix pty is available). |
||||||
|
|
||||||
|
## Context |
||||||
|
|
||||||
|
On a unix tty, fd 0/1/2 typically share one open file description, so the repl's and |
||||||
|
punk::console's read-window 'chan configure stdin -blocking 0' sets O_NONBLOCK on |
||||||
|
stdout/stderr underneath Tcl's per-channel view (stdout still reports -blocking 1). |
||||||
|
tclIO.c then behaves as follows (verified 2026-08-03 against the TEMP_REFERENCE/tcl9 |
||||||
|
checkout; the code comments acknowledge the dup'd-tty case): a write hitting EAGAIN |
||||||
|
queues with BG_FLUSH_SCHEDULED; from then on synchronous 'flush' returns success |
||||||
|
without writing (drain needs event-loop writable events); and during exit the EAGAIN |
||||||
|
path does not reschedule - queued output is discarded at finalization. |
||||||
|
|
||||||
|
Measured (stock tclsh 8.6.14, WSL pty, throttled reader, 2026-08-03): blocking control |
||||||
|
delivered 5000/5000 lines; with stdin -blocking 0, /proc/self/fdinfo/1 flags went |
||||||
|
02 -> 04002, 'flush' claimed success with ~498KB pending ('chan pending output'), and |
||||||
|
exit delivered 161/5000; a 6s event-loop grace still lost the final ~22KB. The window |
||||||
|
flickers with every repl read cycle, so the exposure is a large emission while a |
||||||
|
window is open followed by prompt exit (scripted sessions, error exits, quit right |
||||||
|
after a big table). WSL's slow console pipeline is the amplifier; windows console |
||||||
|
channels do not share flags this way and are unaffected. Each thread's std channel |
||||||
|
instance has its own queue, so worker threads that stop servicing their event loop |
||||||
|
strand queued output the same way (the thread dimension - G-090 territory). |
||||||
|
|
||||||
|
Full investigation record: session memory 2026-08-03 (punkshell-unix-repl-output-loss); |
||||||
|
distinct from the deterministic tee_to_pipe clear defect fixed as shellfilter 0.2.5. |
||||||
|
|
||||||
|
## Approach |
||||||
|
|
||||||
|
- A drain guard on the true-exit path: restore stdin -blocking 1 first (Tcl's fcntl |
||||||
|
clears O_NONBLOCK on the shared description, so queued data drains with blocking |
||||||
|
writes on the next writable event), then service events until 'chan pending output' |
||||||
|
is 0 on stdout/stderr or the bounded timeout expires. |
||||||
|
- Home for the helper: punk::console (owns raw/mode arbitration and the window sites); |
||||||
|
invoked from the repl/app final-exit seam - after the G-038 eof-restart decision |
||||||
|
point, so a restarting session never pays or interferes with the drain. |
||||||
|
- The pin drives a kit on a pty with a rate-limited consumer (the 2026-08-03 harness |
||||||
|
pattern: script(1) + throttled reader) and a stopped consumer for the timeout case. |
||||||
|
|
||||||
|
## Alternatives considered |
||||||
|
|
||||||
|
- Dedicated /dev/tty output channel (own file description, immune by construction) - |
||||||
|
deferred: larger surface change to output routing; intersects G-011's err-channel |
||||||
|
design and would be better decided there. |
||||||
|
- Do nothing until raw-default lands - rejected: G-013 widens the non-blocking window |
||||||
|
substantially, increasing exposure; the guard should exist first. |
||||||
|
|
||||||
|
## Notes |
||||||
|
|
||||||
|
- Related: G-038 (piped-to-interactive restart) - shares the exit/eof seam; the drain |
||||||
|
applies only to true exit and must leave the eof-restart handover untouched. |
||||||
|
- Related: G-013 (raw mode as default input mode) - raw mode keeps stdin non-blocking |
||||||
|
for much longer, widening this hazard's window; this guard is a prerequisite-grade |
||||||
|
hardening for that goal. |
||||||
|
- Related: G-011 (per-console err channel) - stderr shares the tty description today; |
||||||
|
the deferred dedicated-channel alternative belongs to that design space. |
||||||
|
- Related: G-090 (shellfilter/shellthread lifecycle audit) - worker threads stranding |
||||||
|
their per-thread channel queues at finalize is the thread dimension of this hazard; |
||||||
|
G-036 (achieved - see goals/archive/G-036-tcl9-udp-console-worker-wedge.md) is the |
||||||
|
adjacent exit-wedge lineage. |
||||||
|
- Overlap survey 2026-08-03: goals_xref paths repl + console modules + ARCHITECTURE.md; |
||||||
|
mechanism-relevant intersections named above; the remaining repl/console-scoped |
||||||
|
proposals (G-002/G-003/G-008/G-009/G-010/G-014/G-042/G-043/G-044) overlap only by |
||||||
|
module and are judged non-overlapping for this goal's mechanism. |
||||||
Loading…
Reference in new issue