Browse Source

punk::console 0.7.1: fix tcl 8.6 windows console misdetection - one root cause, three sites

Tcl 8.6 windows console channels expose no -inputmode configure key, so
punk::console guards testing -inputmode/-mode classified a real 8.6 console
as a pipe. A twapi console handle for stdin is now treated as definitive
console detection at all three affected sites (stdin only - pipe probes
still never touch the process console; mintty-as-pipes has no console
handle and falls through to the env heuristics as before):

1. get_ansi_response_payload's raw-cycling gate (introduced 0.1.5) skipped
   the raw cycle for 8.6 line-mode queries.
2. is_input_console_or_tty returned a false negative, making
   settle_can_respond's layer-2 heuristic settle a real 8.6 console as
   unable to respond.
3. input_at_eof (introduced 0.1.2) took the pipe branch and performed its
   probe read on the drained console channel. This was the line-mode query
   killer: verified empirically against clean tclkits 8.6.13 and 8.6.17,
   a read on a drained 8.6 console channel makes the channel driver park a
   blocking cooked-mode ReadConsole; driver reads sample the console mode
   when issued, not when data arrives, so the query's subsequent raw flip
   cannot rescue it and the terminal response is swallowed until Enter.
   Since get_ansi_response_payload calls input_at_eof immediately before
   its raw cycle, every 8.6 line-mode query timed out (~500ms) with the
   response later leaking to the line reader as phantom input - also the
   source of increased ANSI artifacts (e.g. 'help env') once 0.7.0
   brokering made code-interp queries actually reach the terminal.

Interactively verified on punksys (tcl 8.6.13, src launch): line-mode
get_cursor_pos and dec_get_mode succeed mid-command, brokered query loops
run clean, and typed-ahead lines are recovered without phantom input.
Known residue (documented in the G-007 detail notes): queries fired while
the repl reader is armed and idle at a line-mode prompt (including some
repl-init detection queries) still hit the parked-read limitation on 8.6 -
repl init ordering is a follow-up; raw mode is unaffected.

Tests: probes.test gains constraint-gated wiring tests for the twapi
console branch of is_input_console_or_tty and input_at_eof (skip in piped
runs, engage on a real console). Full suite passes at baseline (exec-14.3
only) under Tcl 9.0.3; probes suite also verified under Tcl 8.6.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
528d1eca72
  1. 19
      goals/G-007-console-location-transparency.md
  2. 28
      src/modules/punk/console-999999.0a1.0.tm
  3. 3
      src/modules/punk/console-buildversion.txt
  4. 36
      src/tests/modules/punk/console/testsuites/console/probes.test

19
goals/G-007-console-location-transparency.md

@ -128,13 +128,18 @@ already lives in tsv because it must be visible to all threads.
in-flight cooked-mode read starves the raw-window response read (per-query timeouts, in-flight cooked-mode read starves the raw-window response read (per-query timeouts,
~560ms), and the responses later echo outside any read window and are submitted to the ~560ms), and the responses later echo outside any read window and are submitted to the
line parser as phantom input (e.g. `invalid command name 60`). line parser as phantom input (e.g. `invalid command name 60`).
- On tcl 8.6 (punksys) line mode is worse: *every* query - owner-side included, no routing - On tcl 8.6 (punksys), *every* line-mode query initially failed the same way - owner-side
involved (`repl eval {punk::console::get_cursor_pos}` reproduces it) - times out (~500ms) included, no routing involved. Root-caused (2026-07-05, empirically against clean tclkits
with the response leaking to the reader and being parsed as input. Raw mode works cleanly 8.6.13/8.6.17): 8.6 console channels expose no -inputmode configure key, so three
on both runtimes, brokered and owner-side. The user recalls 8.6 line-mode queries working punk::console guards misclassified a real 8.6 console as a pipe; fatally, input_at_eof's
previously - candidate owner-side regression somewhere in the punk::console 0.2.0-0.7.0 pipe-branch probe read ran on the drained console channel immediately before each query's
migration or repl reader changes; to be investigated as separate work (also suspected raw cycle, making the 8.6 channel driver park a blocking cooked-mode ReadConsole (driver
behind increased ANSI artifacts in `help env` output on 8.6). reads sample the console mode at issue time - a later raw flip cannot rescue them) that
swallowed the query response until Enter. Fixed in punk::console 0.7.1 by treating a twapi
console handle for stdin as definitive console detection at all three sites (see
console-buildversion.txt). Queries issued while the repl reader is armed and idle at a
line-mode prompt (e.g. from background after-scripts) remain subject to the parked-read
limitation on 8.6 - raw mode, or the raw-default roadmap, covers that residue.
- Raw mode (the repl's own editor) cooperates cleanly in both directions - queries succeed - Raw mode (the repl's own editor) cooperates cleanly in both directions - queries succeed
and typed-ahead is preserved intact. Line mode is today's default (raw is the intended and typed-ahead is preserved intact. Line mode is today's default (raw is the intended
future default); fixing line-mode query cooperation would be its own goal. future default); fixing line-mode query cooperation would be its own goal.

28
src/modules/punk/console-999999.0a1.0.tm

@ -705,6 +705,16 @@ namespace eval punk::console {
#For mintty-without-winpty (terminal presenting pipes) the twapi raw cycle was already a #For mintty-without-winpty (terminal presenting pipes) the twapi raw cycle was already a
#no-op via its get_console_handle early-return, so nothing is lost by skipping. #no-op via its get_console_handle early-return, so nothing is lost by skipping.
set input_is_console_or_tty [expr {[dict exists $previous_input_state -inputmode] || [dict exists $previous_input_state -mode]}] set input_is_console_or_tty [expr {[dict exists $previous_input_state -inputmode] || [dict exists $previous_input_state -mode]}]
if {!$input_is_console_or_tty && $input eq "stdin" && $::punk::console::has_twapi} {
#Tcl 8.6 windows console channels have no -inputmode configure key, so the
#dict test alone misclassifies a real 8.6 console as a pipe: the raw cycle
#is then skipped and the query response sits in the cooked line buffer until
#Enter - timeout here plus the response leaking to the line reader as phantom
#input. A twapi console handle for stdin is definitive. Channels other than
#stdin stay excluded - the guard's purpose (pipe probes must not flip the
#process console) is unchanged.
set input_is_console_or_tty [expr {![catch {twapi::get_console_handle stdin}]}]
}
if {![tsv::get punk_console is_raw]} { if {![tsv::get punk_console is_raw]} {
set was_raw 0 set was_raw 0
if {$input_is_console_or_tty} { if {$input_is_console_or_tty} {
@ -2009,6 +2019,16 @@ namespace eval punk::console {
#pending keystroke with a probe read here. #pending keystroke with a probe read here.
return 0 return 0
} }
variable has_twapi
if {$input eq "stdin" && $has_twapi && ![catch {twapi::get_console_handle stdin}]} {
#Tcl 8.6 windows console channels expose no -inputmode key, so the dict test above
#misses them. Beyond the pending-keystroke concern, the probe read is fatal here:
#a read on a drained 8.6 console channel makes the channel driver park a blocking
#cooked-mode ReadConsole that cannot be cancelled by a later raw-mode flip - it
#swallows a terminal query response emitted afterwards (get_ansi_response_payload
#calls this guard immediately before its raw cycle) and only completes on Enter.
return 0
}
#pipe-like channel - probe without blocking to force eof detection #pipe-like channel - probe without blocking to force eof detection
if {[catch { if {[catch {
set prior_blocking [dict get $conf -blocking] set prior_blocking [dict get $conf -blocking]
@ -2057,6 +2077,14 @@ namespace eval punk::console {
#tty on unix-like platforms #tty on unix-like platforms
return 1 return 1
} }
variable has_twapi
if {$input eq "stdin" && $has_twapi && ![catch {twapi::get_console_handle stdin}]} {
#Tcl 8.6 windows console channels expose no -inputmode key - a twapi console
#handle for stdin is definitive: process stdin is the real console. Terminals
#presenting channels as pipes (mintty without winpty) have no console handle
#and fall through to the env heuristics below.
return 1
}
if {[input_at_eof $input]} { if {[input_at_eof $input]} {
return 0 return 0
} }

3
src/modules/punk/console-buildversion.txt

@ -1,6 +1,7 @@
0.7.0 0.7.1
#First line must be a semantic version number #First line must be a semantic version number
#all other lines are ignored. #all other lines are ignored.
#0.7.1 - fix Tcl 8.6 windows console misdetection (one root cause, three sites): 8.6 console channels expose no -inputmode configure key, so guards that test for -inputmode/-mode classified a real 8.6 console as a pipe. A twapi console handle for stdin is now treated as definitive at all three sites (stdin only - pipe probes still never flip the process console; mintty-as-pipes has no console handle and falls through to the env heuristics). Site 1: get_ansi_response_payload's raw-cycling gate (0.1.5) skipped the raw cycle for 8.6 line-mode queries. Site 2: is_input_console_or_tty false negative made settle_can_respond's layer-2 heuristic settle a real 8.6 console as unable to respond. Site 3 (the line-mode query killer, introduced 0.1.2): input_at_eof took the pipe branch and performed its probe read on the drained console channel - a read on a drained 8.6 console makes the channel driver park a blocking cooked-mode ReadConsole that a later raw flip cannot cancel (verified empirically against clean tclkits 8.6.13/8.6.17: driver reads sample the console mode when issued, not when data arrives), and since get_ansi_response_payload calls input_at_eof immediately before its raw cycle, every 8.6 line-mode query's response was swallowed by that parked read until Enter (~500ms timeout + response leaking to the line reader as phantom input; also the cause of increased ANSI artifacts in outputs like 'help env' once 0.7.0 brokering made code-interp queries actually reach the terminal).
#0.7.0 - G-007 slice 2 (choke-point brokering): a terminal query (internal::get_ansi_response_payload and every query proc layered above it) on the process-default console {stdin stdout} issued by a thread that is not the registered owner is forwarded whole to the owning thread via synchronous thread::send, so queueing, raw-mode cycling, cooperative reader handling (input_chunks_waiting) and can_respond gating all execute in the owner's context; owner-side errors propagate to the caller with routing context #0.7.0 - G-007 slice 2 (choke-point brokering): a terminal query (internal::get_ansi_response_payload and every query proc layered above it) on the process-default console {stdin stdout} issued by a thread that is not the registered owner is forwarded whole to the owning thread via synchronous thread::send, so queueing, raw-mode cycling, cooperative reader handling (input_chunks_waiting) and can_respond gating all execute in the owner's context; owner-side errors propagate to the caller with routing context
#0.7.0 - new internal::console_route_owner decides the routing: default console pair only (non-std channel names are thread-local, so an {in out} pair spec always names the calling thread's own console and operates locally - a console anchored by code-interp/worker code round-trips nowhere); unregistered, owner==caller, and dead-owner (liveness-validated) cases operate locally, preserving single-interp behaviour exactly #0.7.0 - new internal::console_route_owner decides the routing: default console pair only (non-std channel names are thread-local, so an {in out} pair spec always names the calling thread's own console and operates locally - a console anchored by code-interp/worker code round-trips nowhere); unregistered, owner==caller, and dead-owner (liveness-validated) cases operate locally, preserving single-interp behaviour exactly
#0.6.0 - G-007 slice 1 (facts + ownership): per-console facts store is tsv-backed (shared array punk_console_facts) so all threads of a punk session read the same values; the default console {stdin stdout} keeps the legacy namespace variables as its authoritative local storage with write traces mirroring every write (including direct variable writes) into tsv; non-default consoles store facts only in tsv, keyed with an owner qualifier because non-std channel names are thread-local #0.6.0 - G-007 slice 1 (facts + ownership): per-console facts store is tsv-backed (shared array punk_console_facts) so all threads of a punk session read the same values; the default console {stdin stdout} keeps the legacy namespace variables as its authoritative local storage with write traces mirroring every write (including direct variable writes) into tsv; non-default consoles store facts only in tsv, keyed with an owner qualifier because non-std channel names are thread-local

36
src/tests/modules/punk/console/testsuites/console/probes.test

@ -137,6 +137,42 @@ namespace eval ::testspace {
1\ 1\
] ]
#A real windows console is detectable via twapi even where the channel exposes no
#-inputmode configure key (tcl 8.6). The constraint shares the implementation's primitive,
#so this is a wiring regression test: it fails if the twapi branch is removed or ordered
#after the eof/env fallthroughs (which return 0 for a console stdin with hints cleared).
tcltest::testConstraint win_console_twapi [expr {
"windows" eq $::tcl_platform(platform)
&& $::punk::console::has_twapi
&& ![catch {twapi::get_console_handle stdin}]
}]
test is_input_console_or_tty_stdin_windows_console {stdin backed by a real windows console is a console/tty regardless of -inputmode key availability (tcl 8.6 vs 9)}\
-constraints win_console_twapi\
-setup [string cat $common $hints_save]\
-body {
lappend result [punk::console::is_input_console_or_tty stdin]
}\
-cleanup $hints_restore\
-result [list\
1\
]
test input_at_eof_stdin_windows_console {input_at_eof on a real windows console stdin returns 0 via the console guard - the pipe-branch probe read must not run (on tcl 8.6 a drained-console read parks a cooked ReadConsole that swallows subsequent query responses)}\
-constraints win_console_twapi\
-setup $common\
-body {
lappend result [punk::console::input_at_eof stdin]
#the console guard path must not have parked probe data
lappend result [expr {![info exists ::punk::console::input_chunks_waiting(stdin)]
|| ![llength $::punk::console::input_chunks_waiting(stdin)]}]
}\
-cleanup {}\
-result [list\
0\
1\
]
test is_input_console_or_tty_pipe_no_hints {open pipe with terminal env hints cleared is not considered a console/tty}\ test is_input_console_or_tty_pipe_no_hints {open pipe with terminal env hints cleared is not considered a console/tty}\
-setup [string cat $common $hints_save]\ -setup [string cat $common $hints_save]\
-body { -body {

Loading…
Cancel
Save