diff --git a/CHANGELOG.md b/CHANGELOG.md index a6ba68a4..4f0072eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` "Project Versioning" section for the bump policy. +## [0.2.2] - 2026-07-06 + +- punk::console: terminal queries fired while an 8.6-based shell waits at an idle line-mode prompt now fail fast with a discriminable errorcode (`PUNK CONSOLE QUERY HOSTAGE_COOKED_READ`) and emit nothing, instead of timing out (~500ms) and later corrupting input with the swallowed response (idle-reader hostage guard; raw mode and mid-command queries unaffected). Owner-routed queries preserve the owner-side errorcode. + ## [0.2.1] - 2026-07-06 - `make.tcl projectversion`: new advisory subcommand verifying `CHANGELOG.md` matches `punkproject.toml` and warning if `src/` has commits since the last project-version bump. diff --git a/punkproject.toml b/punkproject.toml index 1991c22d..8200647f 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,3 +1,3 @@ [project] name = "punkshell" -version = "0.2.1" +version = "0.2.2" diff --git a/src/modules/punk/console-999999.0a1.0.tm b/src/modules/punk/console-999999.0a1.0.tm index 86572af6..ce8240ba 100644 --- a/src/modules/punk/console-999999.0a1.0.tm +++ b/src/modules/punk/console-999999.0a1.0.tm @@ -581,7 +581,10 @@ namespace eval punk::console { try { return [thread::send $route_owner [list ::punk::console::internal::get_ansi_response_payload -console $inoutchannels -expected_ms $expected -ignoreok $ignoreok -return $returntype -passthrough $passthrough $query $capturingendregex]] } on error {errM erropts} { - return -code error "get_ansi_response_payload query routed to console-owning thread $route_owner failed: $errM" + #preserve the owner-side errorcode (where the transport propagates it) so + #callers can discriminate, e.g. {PUNK CONSOLE QUERY HOSTAGE_COOKED_READ} + #from the idle-reader guard + return -code error -errorcode [dict get $erropts -errorcode] "get_ansi_response_payload query routed to console-owning thread $route_owner failed: $errM" } } @@ -611,6 +614,28 @@ namespace eval punk::console { error "punk::console::get_ansi_response_payload input channel '$input' is closed or at eof - cannot receive terminal response" } + #tcl 8.6 windows console idle-reader hostage guard (fail fast, no emission). + #With the console in cooked (line) mode and a readable handler armed on the input + #channel, the 8.6 channel driver has a blocking cooked-mode ReadConsole parked + #(arming is what posts it), and driver reads sample the console mode at issue time - + #the raw cycle below cannot rescue it. Any response would be swallowed by that read + #until the user presses Enter, then leak to the line reader as phantom input + #(~500ms timeout now, input corruption later). This is the idle-at-a-line-mode-prompt + #condition: the repl disarms its reader during command dispatch, so mid-command + #queries are unaffected, and raw mode reads cooperatively and is exempt. Typical + #trigger: a query from an after-script or worker thread firing while the shell sits + #at a line-mode prompt. Best-effort by design: a parked read can outlive a removed + #handler, so this catches the systematic case, not every conceivable one. + if {![tsv::get punk_console is_raw] && $input eq "stdin" && $::punk::console::has_twapi} { + set guard_conf [chan configure $input] + if {![dict exists $guard_conf -inputmode] && ![dict exists $guard_conf -mode] + && [chan event $input readable] ne "" + && ![catch {twapi::get_console_handle stdin}]} { + return -code error -errorcode [list PUNK CONSOLE QUERY HOSTAGE_COOKED_READ]\ + "punk::console::get_ansi_response_payload refusing to emit query on '$input': tcl 8.6 console is in cooked (line) mode with an armed readable handler - the driver's parked cooked read would swallow the response until Enter (query issued from an idle line-mode prompt?). Use raw mode, or issue queries while the reader is disarmed (e.g. during repl command evaluation)." + } + } + #chunks from input that need to be handled by readers upvar ::punk::console::input_chunks_waiting input_chunks_waiting diff --git a/src/modules/punk/console-buildversion.txt b/src/modules/punk/console-buildversion.txt index 62e2eb28..454dab0e 100644 --- a/src/modules/punk/console-buildversion.txt +++ b/src/modules/punk/console-buildversion.txt @@ -1,6 +1,7 @@ 0.7.1 #First line must be a semantic version number #all other lines are ignored. +#0.7.1 - new idle-reader hostage guard in get_ansi_response_payload: on a tcl 8.6 windows console in cooked (line) mode with a readable handler armed on stdin (the idle-at-a-line-mode-prompt condition - e.g. a query fired from an after-script or worker thread while the shell waits for input), the query now fails fast with errorcode {PUNK CONSOLE QUERY HOSTAGE_COOKED_READ} and emits nothing, instead of timing out (~500ms) and having the response swallowed by the driver's parked cooked ReadConsole until Enter then leaking to the line reader as phantom input. Mid-command queries (repl reader disarmed) and raw mode are unaffected; best-effort - a parked read can outlive a removed handler, so the guard catches the systematic case only #0.7.1 - compound emit-then-query operations flush their emissions before querying: get_size_using_cursormove/get_size_using_cursorrestore (the far-corner move), test_char_width (positioning and the measured test emission) and test_string_cursor (alt-screen/move/erase). Under G-007 routing the position query may execute in the console-owning thread, whose flush acts on its own channel instance for the same OS handle - the caller's unflushed emissions then reach the terminal after the query measures, e.g. get_size returning 'columns 1' on tcl 8.6 where the -winsize shortcut is unavailable and the ANSI mechanism actually runs (pre-routing this was masked because emit and query shared one channel instance, so the query's flush pushed the emissions too). Error paths flush their cursor-restore emissions likewise. #0.7.1 - ensure_object_integration hardening: returns 0 as a graceful no-op (without latching object_integration_done) when opunk::console is not loaded, instead of erroring into the nonexistent namespace; and on wiring the lifecycle callback it retro-registers ownership for anchors created in this interp before the wiring (previously 'package require opunk::console; opunk::console::create ...' with no intervening punk::console object operation left the console without a registered owner - fact-store share qualifiers then fell back to the calling thread and other threads could not address that console's facts). Retro-registration fills empty registry entries only; existing live registrations are preserved. #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). diff --git a/src/tests/modules/punk/console/testsuites/console/probes.test b/src/tests/modules/punk/console/testsuites/console/probes.test index 87a48f04..a4e22a40 100644 --- a/src/tests/modules/punk/console/testsuites/console/probes.test +++ b/src/tests/modules/punk/console/testsuites/console/probes.test @@ -158,6 +158,36 @@ namespace eval ::testspace { 1\ ] + #The idle-reader hostage guard only applies where chan configure exposes no -inputmode + #key (tcl 8.6 windows console) - on 8.7/9 consoles this constraint self-skips. + tcltest::testConstraint win_console_86_cooked [expr { + "windows" eq $::tcl_platform(platform) + && $::punk::console::has_twapi + && ![catch {twapi::get_console_handle stdin}] + && ![dict exists [chan configure stdin] -inputmode] + && ![tsv::get punk_console is_raw] + }] + + test query_hostage_guard_armed_reader {a terminal query on a cooked 8.6 console with an armed stdin readable handler fails fast without emitting (idle-prompt hostage condition)}\ + -constraints win_console_86_cooked\ + -setup [string cat $common { + set prior_handler [chan event stdin readable] + chan event stdin readable {set ::probes_dummy_readable 1} + }]\ + -body { + set status [catch {punk::console::get_cursor_pos} msg opts] + lappend result $status + lappend result [dict get $opts -errorcode] + }\ + -cleanup { + chan event stdin readable $prior_handler + unset -nocomplain ::probes_dummy_readable + }\ + -result [list\ + 1\ + {PUNK CONSOLE QUERY HOSTAGE_COOKED_READ}\ + ] + 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\