From 552a311d6e72de798531abff3d4cce3591db4419 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Sun, 5 Jul 2026 21:55:17 +1000 Subject: [PATCH] punk::repl 0.2.2: waiting-chunks path avoids drained-channel read on tcl 8.6 consoles Companion to punk::console 0.7.1 (three-site 8.6 console misdetection fix). The line-mode waiting-chunks path in repl_handler performed an unsized read that, entered via 'after idle' with the channel drained, made the tcl 8.6 windows console driver park a blocking cooked-mode ReadConsole (driver reads sample the console mode at issue time - a later raw flip cannot cancel one). After typed-ahead was stashed during a terminal-query raw window, that parked read swallowed every subsequent query response in the session until Enter, with the responses later leaking to the reader. On hostage-prone consoles (windows, no -inputmode configure key, real twapi console handle) the path now consumes only data already in the Tcl channel buffer (chan pending input + sized read - no driver probe). With nothing buffered it processes the stashed complete lines directly - preserving the recovered-typeahead execution order - and arms the readable handler for any remaining partial line, replacing an after-idle reinvoke that, without the read, could never have progressed; the parked read that arming creates is the reader legitimately waiting for the user to finish typing. Behaviour on tcl 9/8.7 (-inputmode consoles) and in raw mode is unchanged. Interactively verified on punksys (tcl 8.6.13, src launch): the previously failing case - typing a full command during a line-mode brokered query loop - now runs with all queries answering, the typed line executing after the loop, and no timeouts, response leakage or chan-blocked diagnostics; the session remains healthy afterwards. Full suite passes at baseline (exec-14.3 only) under Tcl 9.0.3. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- src/modules/punk/repl-999999.0a1.0.tm | 49 ++++++++++++++++++++++++-- src/modules/punk/repl-buildversion.txt | 3 +- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/modules/punk/repl-999999.0a1.0.tm b/src/modules/punk/repl-999999.0a1.0.tm index 5233262f..9b8e4ceb 100644 --- a/src/modules/punk/repl-999999.0a1.0.tm +++ b/src/modules/punk/repl-999999.0a1.0.tm @@ -1635,6 +1635,7 @@ proc repl::repl_handler {inputchan readmore prompt_config} { #note -inputmode not available in Tcl 8.6 for chan configure! #According to DKF - -buffering option doesn't affect input channels set rawmode 0 + set waiting_needs_reader 0 ;#set when the 8.6-console waiting-chunks path skips its read on a drained channel - the readable handler must be armed (not the after-idle reinvoke) or the pending partial line could never complete set original_input_conf [chan configure $inputchan] ;#whether repl is in line or raw mode - we restore the inputchan (stdin) state if {[dict exists $original_input_conf -inputmode]} { @@ -1693,9 +1694,46 @@ proc repl::repl_handler {inputchan readmore prompt_config} { uplevel #0 [list repl::repl_process_data $inputchan line "" $stdinlines $prompt_config] set input_chunks_waiting($inputchan) [list $waitingchunk] } else { - set chunk [read $inputchan] + set hostage_noread 0 + if {"windows" eq $::tcl_platform(platform) + && ![dict exists $original_input_conf -inputmode] + && $::punk::console::has_twapi + && ![catch {twapi::get_console_handle stdin}]} { + #tcl 8.6 windows console channel (no -inputmode configure key, real console + #handle). On the 8.6 console driver a read on a *drained* channel parks a + #blocking cooked-mode ReadConsole that a later raw-mode flip cannot cancel: + #it swallows terminal query responses until Enter (see punk::console 0.7.1 + #buildversion notes). This waiting-chunks path is normally entered via + #'after idle' with the channel drained - only consume what is already in the + #Tcl channel buffer (sized read - no driver probe); when nothing is buffered, + #process the stashed complete lines without reading and arm the readable + #handler for the rest. + set avail [chan pending input $inputchan] + if {$avail > 0} { + set chunk [read $inputchan $avail] + } else { + set chunk "" + set hostage_noread 1 + set waiting_needs_reader 1 + } + } else { + set chunk [read $inputchan] + } set chunksize [string length $chunk] - if {$chunksize > 0} { + if {$hostage_noread} { + #nothing in the Tcl buffer and we must not probe the drained driver. + #Process the complete stashed lines now; a remaining partial line can only + #progress via more user input, which the armed readable handler delivers. + if {[llength $stdinlines]} { + punk::repl::repl_handler_restorechannel_if_not_eof $inputchan $original_input_conf + uplevel #0 [list repl::repl_process_data $inputchan line "" $stdinlines $prompt_config] + } + if {$waitingchunk ne ""} { + set input_chunks_waiting($inputchan) [list $waitingchunk] + } else { + set input_chunks_waiting($inputchan) [list] + } + } elseif {$chunksize > 0} { if {[string index $chunk end] eq "\n"} { lappend stdinlines $waitingchunk[string range $chunk 0 end-1] #punk::console::cursorsave_move_emitblock_return 30 30 "repl_handler num_stdinlines [llength $stdinlines] chunk:$yellow[ansistring VIEW -lf 1 $chunk][a] fblocked:[fblocked $inputchan] pending:[chan pending input stdin]" @@ -1864,7 +1902,12 @@ proc repl::repl_handler {inputchan readmore prompt_config} { ################################################################################## #Re-enable channel read handler only if no waiting chunks - must process in order ################################################################################## - if {![llength $input_chunks_waiting($inputchan)]} { + if {![llength $input_chunks_waiting($inputchan)] || $waiting_needs_reader} { + #waiting_needs_reader: the 8.6-console waiting-chunks path found nothing in the + #Tcl channel buffer and must not probe the drained driver - the waiting data is + #an incomplete line that can only progress when the user types more, so arm the + #reader (the driver listening for input is its job here) instead of an after-idle + #reinvoke that could never make progress. chan event $inputchan readable [list ::repl::repl_handler $inputchan $readmore $prompt_config] } else { #review diff --git a/src/modules/punk/repl-buildversion.txt b/src/modules/punk/repl-buildversion.txt index 99bf6678..700f4255 100644 --- a/src/modules/punk/repl-buildversion.txt +++ b/src/modules/punk/repl-buildversion.txt @@ -1,6 +1,7 @@ -0.2.1 +0.2.2 #First line must be a semantic version number #all other lines are ignored. +#0.2.2 - repl_handler line-mode waiting-chunks path no longer performs its opportunistic unsized read on tcl 8.6 windows console channels (no -inputmode key, real twapi console handle): the path is entered via 'after idle' with the channel usually drained, and on the 8.6 console driver a drained read parks a blocking cooked-mode ReadConsole that a later raw flip cannot cancel - after typed-ahead was stashed during a terminal-query raw window, that parked read swallowed every subsequent query response in the session until Enter (companion to the punk::console 0.7.1 three-site console-misdetection fix). On such consoles the path now consumes only data already in the Tcl channel buffer (chan pending input + sized read - no driver probe); with nothing buffered it processes the stashed complete lines directly and arms the readable handler for any remaining partial line (replacing an after-idle reinvoke that could never progress). Behaviour on tcl 9/8.7 (-inputmode consoles) and in raw mode is unchanged. #0.2.1 - call-site update for punk::console 0.6.0 tsv array rename: console -> punk_console (is_raw); no behaviour change #0.2.0 - codethread init_script now propagates package prefer via %packageprefer% scriptmap entry #0.2.0 - code interp (punk/0 repltype) now uses punk::lib::interp_sync_package_paths -libunknown 1 instead of manual epoch-only copy, propagating tm list, auto_path, package prefer, and libunknown to the code interp