You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

5.4 KiB

G-090 shellfilter/shellthread stack and log-worker lifecycle audit

Status: proposed Scope: src/modules/shellfilter-999999.0a1.0.tm (stack placement, insert_transform, junctions), src/modules/shellthread-999999.0a1.0.tm (worker park/adoption/termination), src/tests/modules/ (new characterization suites), README.md ('very unripe parts' note) Goal: the shellfilter transform-stack placement logic (float/sink/junction interactions, insert/remove re-instantiation) and the shellthread log-worker lifecycle (park/adoption/settings application, termination responsibility) behave by documented, test-pinned rules with no silent data loss or target misrouting, and the tcludp Tcl 9 receive-event limitation has a recorded resolution or workaround. Acceptance: characterization suites under src/tests/modules/ pin the probed hazard cases (a tee transform combined with a float-locked capture junction observes the stream in a supported configuration regardless of add order; stack add/remove churn does not silently drop a transform's log output or rebind its worker to stale targets; a new tag adopting a parked worker either receives its requested targets or errors - the 2026-07-18 misroute scenario writes to the correct file); the parked-worker termination contract (who runs shutdown_free_threads, when) and the tcludp receive caveat are documented in the owning module docs/AGENTS chain; goals_lint and the full test suite stay green.

Context

The shellfilter stack system is production plumbing (e.g ansible-on-freebsd runs producing an ANSI-faithful log, an ansistripped log, and streaming console output; punkshell app logging to local UDP syslog targets), but README.md's 'very unripe parts' note ("api is clumsy") is largely about the float-placement logic: never audited for sanity/gotchas, not fully tested, yet used in working applications (user statement 2026-07-18).

Probe evidence recorded 2026-07-18 during the runx -teelog work (session probes, scratch only - none of this is currently pinned by committed tests):

  1. Float placement: a float added while floats already exist is inserted BELOW the existing floats. Combined with runx's float-locked capture junction (a junction blocks flow to everything below it), a tee_to_log float is starved whichever order it is added in: added second it lands under the junction directly; added first, the junction's insertion churn re-instantiates it (finding 2) and it ends up non-functional.
  2. insert_transform re-instantiation churn: inserting/removing stack members pops and re-pushes the transforms above the insertion point as NEW transform objects. For tee_to_log this finalizes the old instance (closing/parking its log worker) and constructs a new one mid-flight, which re-opens the tag and hits the adoption paths of finding 3.
  3. shellthread parked-worker adoption never re-applies settings: both the placeholder-share branch and the free_threads reuse branch of shellthread::manager::new_worker return the existing worker thread without sending it the new settings, and the park-time reset (unsubscribe) resets only the worker's settings dict - NOT the operative sysloghost_port/logfile variables its log proc actually reads. Probe result: after closing tag 'regrow' (targeting file A), opening tag 'tagB' (targeting file B) adopted the parked worker and tagB's messages were written to file A - silent cross-target misrouting. The code's own comment asks "review - where/when do we override the default settings?".
  4. Unwatched singleproc test runs leak two teehandle worker threads (punkshout/punksherr) per test file: each per-file testinterp's shellrun load spawns them and interp deletion orphans them (manager state dies with the interp; the threads persist). Benign in practice (process exits at run end; an unconditional per-file terminate was measured at +59s per full suite run and deliberately not adopted) - runtests terminates them only in watch mode. See src/tests/AGENTS.md and commit a65f9e5b.
  5. tcludp receive events under Tcl 9.0.3/Windows: a udp_open listener's readable fileevent delivered only the FIRST datagram (canonical one-read-per-event pattern, blocking and non-blocking both tried); the send side is unaffected (a .NET UdpClient listener received all datagrams from the same tcludp sender). Bites any tcludp-based viewer (e.g the PUNK_TEST_UDPTEE tk watch app) - workaround until resolved: timer-driven non-blocking reads or a non-tcludp receiver.

Notes

  • The shipped workaround that motivated the probes: shellrun 0.2.0 runx -teelog implements its live tee as write traces on the capture variables specifically to avoid findings 1-3 (commit a65f9e5b); if this audit makes stacked tees safe alongside junctions, -teelog could optionally move back to a transform-based tee.
  • The exit-wedge history (tcludp 1.0.12 UDP_ExitProc, Tcl 9 console+udp worker wedge on exit/quit, async unsubscribe hardening; G-036, achieved - see goals/archive/G-036-tcl9-udp-console-worker-wedge.md) is adjacent prior work in the same lifecycle territory - see shellthread's unsubscribe/shutdown_free_threads comments.
  • shellthread::manager::close_worker is noted in-code as no longer called since the unsubscribe mechanism landed, with a free-thread cap TODO - part of the same lifecycle contract question.
  • Related: PUNK_TEST_UDPTEE watch mode contracts in src/tests/AGENTS.md; user registered the env var in punk::config's known-env-vars table 2026-07-18.