diff --git a/src/modules/punk/console-999999.0a1.0.tm b/src/modules/punk/console-999999.0a1.0.tm index 1e53b8dd..9a833056 100644 --- a/src/modules/punk/console-999999.0a1.0.tm +++ b/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 { diff --git a/src/modules/punk/console-buildversion.txt b/src/modules/punk/console-buildversion.txt index 00e1ea05..d4696937 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 - 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 diff --git a/src/tests/modules/punk/console/testsuites/console/objectintegration.test b/src/tests/modules/punk/console/testsuites/console/objectintegration.test new file mode 100644 index 00000000..738abb53 --- /dev/null +++ b/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.