Browse Source

punk::console 0.7.1: ensure_object_integration hardening - graceful no-op + catch-up ownership registration

Two wiring-order holes surfaced during the G-007 interactive verification:

1. Calling ensure_object_integration before opunk::console is loaded errored
   into the nonexistent namespace (can't set "::opunk::console::
   waiting_chunks_arrayvar": parent namespace doesn't exist). It now returns
   0 as a graceful no-op without latching object_integration_done, so a
   later call after opunk::console loads still performs the wiring; the
   wired/latched path returns 1.

2. 'package require opunk::console; opunk::console::create ...' with no
   intervening punk::console object operation left the created console
   without a registered owner: the lifecycle callback that records
   ownership is only installed by ensure_object_integration, which is wired
   lazily from punk::console's object-spec paths. Behaviour was unaffected
   for the owning thread (share qualifiers fall back to the calling thread)
   but other threads could not address that console's facts. On wiring the
   callback, ensure_object_integration now retro-registers ownership for
   anchors already present in the interp - anchors are per-interp/per-thread
   so the anchoring context is the calling thread. Only empty registry
   entries are filled; existing live registrations are preserved (and the
   default console keeps first-registration-wins semantics).

Tests: new objectintegration.test covers the no-op return (unlatched),
retro-registration of a pre-wiring anchor with lifecycle forget clearing
the entry, idempotency of the latched path, and normal post-wiring
registration - passing under Tcl 9.0.3 and 8.6. Full suite at baseline
(exec-14.3 only).

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 2 weeks ago
parent
commit
d68d4d4f26
  1. 28
      src/modules/punk/console-999999.0a1.0.tm
  2. 1
      src/modules/punk/console-buildversion.txt
  3. 97
      src/tests/modules/punk/console/testsuites/console/objectintegration.test

28
src/modules/punk/console-999999.0a1.0.tm

@ -2889,7 +2889,14 @@ namespace eval punk::console {
proc ensure_object_integration {} {
variable object_integration_done
if {$object_integration_done} {
return
return 1
}
if {![namespace exists ::opunk::console]} {
#opunk::console not loaded - nothing to integrate with yet. In-module callers only
#reach here after a successful 'package require opunk::console'; external/manual
#callers may call speculatively. Deliberately does not set object_integration_done,
#so a later call after opunk::console loads performs the wiring.
return 0
}
set ::opunk::console::waiting_chunks_arrayvar ::punk::console::input_chunks_waiting
if {$::opunk::console::size_query_provider eq ""} {
@ -2900,8 +2907,27 @@ namespace eval punk::console {
#consult-time liveness validation still cover the default console there)
if {[info exists ::opunk::console::lifecycle_callback] && $::opunk::console::lifecycle_callback eq ""} {
set ::opunk::console::lifecycle_callback [list ::punk::console::internal::object_console_lifecycle]
#catch-up registration: anchors created in this interp before the callback was
#wired never fired a 'created' event, so their consoles have no recorded owner
#(seen when user code does 'package require opunk::console; opunk::console::create ...'
#before any punk::console object operation triggers this wiring). Anchors are
#per-interp/per-thread, so the anchoring context is this thread - register it.
#Only fills empty entries: an existing live registration is newer information than
#these pre-wiring anchors (and for the default console first-registration wins).
foreach v [info vars ::opunk::console::instances::*] {
if {![info exists $v]} {
continue
}
if {[catch {::opunk::Console::channels [set $v]} channels]} {
continue
}
if {[console_owner_get $channels] eq ""} {
console_owner_register $channels
}
}
}
set object_integration_done 1
return 1
}
lappend PUNKARGS [list {

1
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 - 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).
#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

97
src/tests/modules/punk/console/testsuites/console/objectintegration.test

@ -0,0 +1,97 @@
package require tcltest
tcltest::configure {*}$::argv
#min-version bound documents that these tests target the dev module's API and protects
#against stable bootsupport copies shadowing the alpha dev version if this file is sourced outside
#runtests.tcl (whose testinterp runs 'package prefer latest' making the bound redundant there).
package require punk::console 999999.0a1.0-
#Tests for punk::console::ensure_object_integration wiring-order behaviour: the graceful no-op
#when opunk::console is not loaded, and catch-up ownership registration for anchors created
#before the lifecycle callback was wired.
#ORDER MATTERS within this file: the first test relies on opunk::console not yet being loaded
#in this interp (runtests gives each test file a fresh testinterp), and the retro-registration
#test relies on creating its anchor before the first successful ensure_object_integration.
namespace eval ::testspace {
namespace import ::tcltest::*
variable common {
set result ""
}
tcltest::testConstraint opunk_not_loaded [expr {[catch {package present opunk::console}]}]
test ensure_object_integration_no_opunk {graceful no-op (0, no error, no latch) when opunk::console is not loaded}\
-constraints opunk_not_loaded\
-setup $common -body {
lappend result [punk::console::ensure_object_integration]
#must not have latched - a later call after opunk::console loads must still wire
lappend result [set ::punk::console::object_integration_done]
}\
-cleanup {}\
-result [list\
0\
0\
]
test ensure_object_integration_retro_registration {anchor created before wiring gains a registered owner when ensure_object_integration wires the callback}\
-setup $common -body {
package require opunk::console
lassign [chan pipe] rd wr
#create BEFORE integration wiring - lifecycle callback is empty so no 'created'
#event fires and no owner is registered (the wiring-order hole)
opunk::console::create preintegration $rd $wr
lappend result [punk::console::console_owner_get [list $rd $wr]]
#wiring performs the catch-up registration for the pre-existing anchor
lappend result [punk::console::ensure_object_integration]
lappend result [expr {[punk::console::console_owner_get [list $rd $wr]] eq [thread::id]}]
#lifecycle callback is now wired - forget clears the entry as usual
opunk::console::forget preintegration
lappend result [punk::console::console_owner_get [list $rd $wr]]
}\
-cleanup {
catch {opunk::console::forget preintegration}
catch {punk::console::console_owner_forget [list $rd $wr]}
catch {chan close $rd}
catch {chan close $wr}
}\
-result [list\
{}\
1\
1\
{}\
]
test ensure_object_integration_idempotent {second call is a latched no-op returning 1}\
-setup $common -body {
lappend result [punk::console::ensure_object_integration]
lappend result [set ::punk::console::object_integration_done]
}\
-cleanup {}\
-result [list\
1\
1\
]
test ensure_object_integration_create_after_wiring {anchor created after wiring registers via the lifecycle callback (no retro path involved)}\
-setup $common -body {
lassign [chan pipe] rd wr
opunk::console::create postintegration $rd $wr
lappend result [expr {[punk::console::console_owner_get [list $rd $wr]] eq [thread::id]}]
opunk::console::forget postintegration
lappend result [punk::console::console_owner_get [list $rd $wr]]
}\
-cleanup {
catch {opunk::console::forget postintegration}
catch {punk::console::console_owner_forget [list $rd $wr]}
catch {chan close $rd}
catch {chan close $wr}
}\
-result [list\
1\
{}\
]
}
tcltest::cleanupTests ;#needed to produce test summary.
Loading…
Cancel
Save