diff --git a/goals/G-007-console-location-transparency.md b/goals/G-007-console-location-transparency.md index 03f99d16..495692be 100644 --- a/goals/G-007-console-location-transparency.md +++ b/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, ~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`). - - On tcl 8.6 (punksys) line mode is worse: *every* query - owner-side included, no routing - involved (`repl eval {punk::console::get_cursor_pos}` reproduces it) - times out (~500ms) - with the response leaking to the reader and being parsed as input. Raw mode works cleanly - on both runtimes, brokered and owner-side. The user recalls 8.6 line-mode queries working - previously - candidate owner-side regression somewhere in the punk::console 0.2.0-0.7.0 - migration or repl reader changes; to be investigated as separate work (also suspected - behind increased ANSI artifacts in `help env` output on 8.6). + - On tcl 8.6 (punksys), *every* line-mode query initially failed the same way - owner-side + included, no routing involved. Root-caused (2026-07-05, empirically against clean tclkits + 8.6.13/8.6.17): 8.6 console channels expose no -inputmode configure key, so three + punk::console guards misclassified a real 8.6 console as a pipe; fatally, input_at_eof's + pipe-branch probe read ran on the drained console channel immediately before each query's + raw cycle, making the 8.6 channel driver park a blocking cooked-mode ReadConsole (driver + 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 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. diff --git a/src/modules/punk/console-999999.0a1.0.tm b/src/modules/punk/console-999999.0a1.0.tm index 242a65bc..1e53b8dd 100644 --- a/src/modules/punk/console-999999.0a1.0.tm +++ b/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 #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]}] + 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]} { set was_raw 0 if {$input_is_console_or_tty} { @@ -2009,6 +2019,16 @@ namespace eval punk::console { #pending keystroke with a probe read here. 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 if {[catch { set prior_blocking [dict get $conf -blocking] @@ -2057,6 +2077,14 @@ namespace eval punk::console { #tty on unix-like platforms 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]} { return 0 } diff --git a/src/modules/punk/console-buildversion.txt b/src/modules/punk/console-buildversion.txt index ef450191..00e1ea05 100644 --- a/src/modules/punk/console-buildversion.txt +++ b/src/modules/punk/console-buildversion.txt @@ -1,6 +1,7 @@ -0.7.0 +0.7.1 #First line must be a semantic version number #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 - 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 diff --git a/src/tests/modules/punk/console/testsuites/console/probes.test b/src/tests/modules/punk/console/testsuites/console/probes.test index 858628b3..87a48f04 100644 --- a/src/tests/modules/punk/console/testsuites/console/probes.test +++ b/src/tests/modules/punk/console/testsuites/console/probes.test @@ -137,6 +137,42 @@ namespace eval ::testspace { 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}\ -setup [string cat $common $hints_save]\ -body {