From c5aaad2954c20456079bc162dfd9ec07618caa19 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Wed, 8 Jul 2026 01:27:14 +1000 Subject: [PATCH] shellthread/shellfilter: piped-stdin exit/quit hang fix - teardown must not block on wedged workers - 0.4.2 Piping a script into shell mode then exiting the restarted interactive repl intermittently froze the shell: shellfilter::run's teardown made a bare synchronous thread::send (settings-reset in shellthread::manager::unsubscribe) to the shellfilter-run syslog log worker, whose event loop had silently stopped servicing events (G-036: Tcl 9-only console+udp worker wedge - root-cause tracked separately). - shellthread 1.6.3: unsubscribe settings-reset send is now -async (sends to a thread are FIFO, so a later reuse still sees the reset applied first); shutdown_free_threads keeps its timeout timer armed across all vwait iterations (was cancelled on the first response, leaving later waits unbounded); get_tag_config's bare sync send flagged as same hazard class. - shellfilter 0.2.4: shellfilter::run honors its -syslog option (default empty -> noop runtag log worker, no thread created) instead of the hardcoded 127.0.0.1:514 debug leftover. Syslog remains supported when explicitly configured. Verified: automated repro (hidden console, piped script scheduling a delayed ::punk::repl::exit in the restarted repl) hung before, exits cleanly after - including with syslog force-enabled against a genuinely wedged worker. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- punkproject.toml | 2 +- src/modules/shellfilter-999999.0a1.0.tm | 7 +++++-- src/modules/shellfilter-buildversion.txt | 5 +++-- src/modules/shellthread-999999.0a1.0.tm | 18 ++++++++++++++---- src/modules/shellthread-buildversion.txt | 5 +++-- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/punkproject.toml b/punkproject.toml index 890d81c5..349f36e7 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,3 +1,3 @@ [project] name = "punkshell" -version = "0.4.1" +version = "0.4.2" diff --git a/src/modules/shellfilter-999999.0a1.0.tm b/src/modules/shellfilter-999999.0a1.0.tm index 572d6b5b..125fac32 100644 --- a/src/modules/shellfilter-999999.0a1.0.tm +++ b/src/modules/shellfilter-999999.0a1.0.tm @@ -2813,8 +2813,11 @@ namespace eval shellfilter { set syslog [dict get $opts -syslog] dict unset opts -syslog set runtag "shellfilter-run" - set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] - #set tid [::shellfilter::log::open $runtag [list -syslog $syslog]] + #honor the -syslog option (default empty) - the previous hardcoded 127.0.0.1:514 was a + #debug leftover. With no -syslog and no -file the runtag log worker is a noop (no thread + #is created). Syslog remains supported when explicitly configured; see G-036 for the + #Tcl 9 console+udp worker wedge that the always-on syslog worker exposed on exit/quit. + set tid [::shellfilter::log::open $runtag [list -syslog $syslog]] log::info {::shellfilter::log::write $runtag " opts: $opts"} log::info {::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]"} diff --git a/src/modules/shellfilter-buildversion.txt b/src/modules/shellfilter-buildversion.txt index 0cc93910..7f0143d3 100644 --- a/src/modules/shellfilter-buildversion.txt +++ b/src/modules/shellfilter-buildversion.txt @@ -1,4 +1,5 @@ -0.2.3 +0.2.4 #First line must be a semantic version number #all other lines are ignored. -#0.2.3 - shellfilter::run: try/finally guarantees tee-stack removal (stdout/stderr restoration) even when the body errors; decorative stack-status table rendering in log::critical blocks is catch-guarded so a rendering failure (e.g mismatched module snapshots - stale textblock vs new punk::ansi sgr_merge_singles options) degrades to a log note instead of aborting the run and leaving the process's stdout/stderr diverted (silent-output-loss failure mode found via tomlish runtests 2026-07-06) \ No newline at end of file +#0.2.4 - shellfilter::run honors its -syslog option (default empty -> the shellfilter-run log worker is a noop, no thread created) instead of the hardcoded 127.0.0.1:514 debug leftover; syslog remains supported when explicitly configured. Part of the piped-stdin exit/quit hang fix - the always-on syslog worker exposed the G-036 Tcl 9 console+udp worker wedge on the exit teardown path (see shellthread 1.6.3) +#0.2.3 - shellfilter::run: try/finally guarantees tee-stack removal (stdout/stderr restoration) even when the body errors; decorative stack-status table rendering in log::critical blocks is catch-guarded so a rendering failure (e.g mismatched module snapshots - stale textblock vs new punk::ansi sgr_merge_singles options) degrades to a log note instead of aborting the run and leaving the process's stdout/stderr diverted (silent-output-loss failure mode found via tomlish runtests 2026-07-06) diff --git a/src/modules/shellthread-999999.0a1.0.tm b/src/modules/shellthread-999999.0a1.0.tm index bd1ff041..42767dc3 100644 --- a/src/modules/shellthread-999999.0a1.0.tm +++ b/src/modules/shellthread-999999.0a1.0.tm @@ -788,8 +788,12 @@ namespace eval shellthread::manager { foreach workertid $subscriberless_workers { if {$workertid ni $shuttingdown_workers} { if {$workertid ni $free_threads && $workertid ne "noop"} { - #JMN - thread::send $workertid {set ::shellthread::worker::settings $::shellthread::worker::settings_defaults} + #Must be async: unsubscribe is on the exit/quit teardown path and a worker whose + #event loop has stopped servicing events (G-036: Tcl 9 console+udp worker wedge) + #would block a synchronous send forever, freezing the shell. Async is safe - + #sends to a thread are processed FIFO, so a later reuse via new_worker sees the + #settings reset applied before anything it sends. + thread::send -async $workertid {set ::shellthread::worker::settings $::shellthread::worker::settings_defaults} #todo - log freeing up of thread lappend free_threads $workertid } @@ -812,7 +816,10 @@ namespace eval shellthread::manager { } proc get_tag_config {tag} { - #review + #review - bare synchronous thread::send: blocks forever if the worker's event loop is + #wedged (G-036 hazard class). Reached from new_worker when a tag already has a live + #worker. Needs a result so can't simply go async - convert to async + vwait-with-timeout + #if this path shows up in a hang. variable workers if {![dict exists $workers $tag]} { error "shellthread::manager::get_tag_config error no existing tag $tag" @@ -858,10 +865,13 @@ namespace eval shellthread::manager { set timedout 1 break } else { - after cancel $timeout_timer + #Do not cancel the timeout timer until all awaited workers have responded - + #cancelling on the first response left later vwait iterations unbounded, so a + #wedged worker (G-036) after a healthy one hung shutdown forever. lappend ended $::shellthread::waitfor } } + after cancel $timeout_timer } set free_threads [list] return [dict create existed $waiting_for ended $ended timedout $timedout allthreads [thread::names]] diff --git a/src/modules/shellthread-buildversion.txt b/src/modules/shellthread-buildversion.txt index 492c916c..a34e44ca 100644 --- a/src/modules/shellthread-buildversion.txt +++ b/src/modules/shellthread-buildversion.txt @@ -1,3 +1,4 @@ -1.6.2 +1.6.3 #First line must be a semantic version number -#all other lines are ignored. \ No newline at end of file +#all other lines are ignored. +#1.6.3 - manager teardown must not block on a wedged worker (G-036: Tcl 9 console+udp worker event-loop wedge): unsubscribe's settings-reset thread::send is now -async (was bare synchronous - froze exit/quit when the shellfilter-run syslog worker had wedged); shutdown_free_threads keeps its timeout timer armed across all vwait iterations (was cancelled on first response, leaving later waits unbounded); get_tag_config's bare synchronous send flagged as the same hazard class (needs a result - convert to async+vwait-with-timeout if it shows in a hang)