Browse Source

punk::console 0.6.0: G-007 slice 1 - tsv-backed console facts + ownership registry

Per-console facts are now visible to every thread of a punk session: the
default console {stdin stdout} keeps its legacy namespace variables as
authoritative local storage, with write traces mirroring every write
(including direct variable writes) into tsv punk_console_facts; non-default
consoles store facts only in tsv with owner-qualified keys (non-std channel
names are thread-local and would otherwise alias). console_fact_set from a
non-owner thread forwards to the owning thread (vt52-alias transport) and
always writes the tsv mirror itself (covers unreachable or older-version
owners). New console_fact_clear for tests/maintenance.

New console ownership registry (console_owner_register/get/forget, tsv
punk_console_owners keyed by canonical {in out} pair): ownership is captured
when an opunk::console instance is anchored - via the new pluggable
::opunk::console::lifecycle_callback (opunk::console 0.4.0, base class
unchanged) wired by ensure_object_integration - and by default_console.
Consult-time liveness validation clears entries for exited threads. For
{stdin stdout} first registration wins and only the owner's forget releases
the entry, so a thread anchoring a local view cannot steal ownership.

dec_has_mode/ansi_has_mode caches moved to tsv punk_console_modecache
(single-key atomic entries shared process-wide).

Infrastructure tsv arrays are now punk_-prefixed to avoid collisions with
application tsv usage in subshells: console -> punk_console (is_raw), with
call-site patches in punk::repl 0.2.1, punk::lib 0.2.1, punk::basictelnet
0.1.1 (rule recorded in src/modules/AGENTS.md; legacy repl/codethread_*/
zzzload_pkg* arrays left for a coordinated follow-up).

punk::console is now loadable in secondary threads without ::argv0
(powershell consolemode fallback guarded).

Tests: consolefacts gains ownership-registry and cross-thread visibility
coverage (worker-thread punk::console load, trace-mirror, forwarded set with
deadline-polled sync - see comments re runtests child-interp topology);
cleanups use console_fact_clear / tsv instead of poking removed internal
stores. Full suite passes (baseline exec-14.3 only).

G-007 flipped proposed -> active (user-confirmed); remaining acceptance work
is the choke-point brokering slice.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 2 weeks ago
parent
commit
aca967e1a5
  1. 2
      GOALS.md
  2. 2
      goals/G-007-console-location-transparency.md
  3. 1
      src/modules/AGENTS.md
  4. 1
      src/modules/opunk/AGENTS.md
  5. 24
      src/modules/opunk/console-999999.0a1.0.tm
  6. 3
      src/modules/opunk/console-buildversion.txt
  7. 3
      src/modules/punk/AGENTS.md
  8. 2
      src/modules/punk/basictelnet-999999.0a1.0.tm
  9. 3
      src/modules/punk/basictelnet-buildversion.txt
  10. 474
      src/modules/punk/console-999999.0a1.0.tm
  11. 9
      src/modules/punk/console-buildversion.txt
  12. 2
      src/modules/punk/lib-999999.0a1.0.tm
  13. 3
      src/modules/punk/lib-buildversion.txt
  14. 16
      src/modules/punk/repl-999999.0a1.0.tm
  15. 3
      src/modules/punk/repl-buildversion.txt
  16. 167
      src/tests/modules/punk/console/testsuites/console/consolefacts.test
  17. 15
      src/tests/modules/punk/console/testsuites/console/consolespec.test
  18. 2
      src/tests/modules/punk/console/testsuites/console/emitconsole.test

2
GOALS.md

@ -86,7 +86,7 @@ Detail: goals/G-006-prebuilt-artifact-download.md
Goal: pre-built binary artifacts can be downloaded from a separate related binary-artifacts repository or user-configured sources, gated by explicit user consent/configuration by default. Goal: pre-built binary artifacts can be downloaded from a separate related binary-artifacts repository or user-configured sources, gated by explicit user consent/configuration by default.
Acceptance: a download mechanism fetches binary artifacts (the same set the zig build produces) from a configured source on demand; by default the download is gated behind explicit user consent (a config flag or interactive prompt) and does not occur silently; a user-configured source URL overrides the default binary-artifacts repo; downloaded artifacts satisfy the same build requirements as zig-built artifacts so `tclsh src/make.tcl project` succeeds with downloaded artifacts in place of built ones. Acceptance: a download mechanism fetches binary artifacts (the same set the zig build produces) from a configured source on demand; by default the download is gated behind explicit user consent (a config flag or interactive prompt) and does not occur silently; a user-configured source URL overrides the default binary-artifacts repo; downloaded artifacts satisfy the same build requirements as zig-built artifacts so `tclsh src/make.tcl project` succeeds with downloaded artifacts in place of built ones.
### G-007 [proposed] Location-transparent punk::console across repl and code interps ### G-007 [active] Location-transparent punk::console across repl and code interps
Scope: src/modules/punk/console-999999.0a1.0.tm, src/modules/punk/repl-999999.0a1.0.tm, src/modules/punk/repl/codethread-999999.0a1.0.tm Scope: src/modules/punk/console-999999.0a1.0.tm, src/modules/punk/repl-999999.0a1.0.tm, src/modules/punk/repl/codethread-999999.0a1.0.tm
Detail: goals/G-007-console-location-transparency.md Detail: goals/G-007-console-location-transparency.md
Goal: punk::console presents one API in every interp/thread of a punk session - code-interp callers see the same console facts and can perform the same queries/operations as the parent, routed to the console-owning context via a punk-side ownership registry, with no `repl eval` required. Goal: punk::console presents one API in every interp/thread of a punk session - code-interp callers see the same console facts and can perform the same queries/operations as the parent, routed to the console-owning context via a punk-side ownership registry, with no `repl eval` required.

2
goals/G-007-console-location-transparency.md

@ -1,6 +1,6 @@
# G-007 Location-transparent punk::console across repl and code interps # G-007 Location-transparent punk::console across repl and code interps
Status: proposed Status: active
Scope: src/modules/punk/console-999999.0a1.0.tm, src/modules/punk/repl-999999.0a1.0.tm, src/modules/punk/repl/codethread-999999.0a1.0.tm Scope: src/modules/punk/console-999999.0a1.0.tm, src/modules/punk/repl-999999.0a1.0.tm, src/modules/punk/repl/codethread-999999.0a1.0.tm
Acceptance: from a running punk session's code interp, without `repl eval`: `console_fact_get` returns the same values the parent sees and a fact set in the parent is immediately visible; a terminal query (e.g. `get_cursor_pos` or `dec_get_mode`) against the default console succeeds and cooperates with the repl reader (no lost or garbled input); a console constructed and owned by code-interp code is operated on locally (no round-trip to the parent); the existing console test suites pass and single-interp (non-repl) usage is unchanged. Acceptance: from a running punk session's code interp, without `repl eval`: `console_fact_get` returns the same values the parent sees and a fact set in the parent is immediately visible; a terminal query (e.g. `get_cursor_pos` or `dec_get_mode`) against the default console succeeds and cooperates with the repl reader (no lost or garbled input); a console constructed and owned by code-interp code is operated on locally (no round-trip to the parent); the existing console test suites pass and single-interp (non-repl) usage is unchanged.

1
src/modules/AGENTS.md

@ -24,6 +24,7 @@ Source of truth for all editable Punk project modules. This is where agents shou
- Use semantic versions that `package vcompare` can interpret; strip leading zeros according to tcl package version rules ie in each segment including either side of a or b. - Use semantic versions that `package vcompare` can interpret; strip leading zeros according to tcl package version rules ie in each segment including either side of a or b.
- For optional features, probe with `if {[catch {package require foo}]} { ... }` and degrade gracefully. - For optional features, probe with `if {[catch {package require foo}]} { ... }` and degrade gracefully.
- Vendormodules, bootsupport snapshots, VFS modules, and built output modules do not use the magic version scheme unless their own local docs explicitly say otherwise. - Vendormodules, bootsupport snapshots, VFS modules, and built output modules do not use the magic version scheme unless their own local docs explicitly say otherwise.
- tsv shared-array names are process-global and subshell application code may use tsv for its own purposes: infrastructure tsv arrays must be `punk_`-prefixed (e.g `punk_console`, `punk_console_facts`). Legacy unprefixed arrays still in use (`repl`, `codethread_*`, `zzzload_pkg*`) predate this rule; rename them only as a coordinated change across all referencing modules.
### Module Subsystems ### Module Subsystems

1
src/modules/opunk/AGENTS.md

@ -18,6 +18,7 @@ Modules under the `opunk::*` namespace explore value-based class implementations
- Entity-like objects (e.g. consoles) anchor canonical instances at well-known namespace variables (`::opunk::console::instances::<name>`) with lowercase wrapper procs owning mutation; the value semantics serve as snapshots/copies where cheap. - Entity-like objects (e.g. consoles) anchor canonical instances at well-known namespace variables (`::opunk::console::instances::<name>`) with lowercase wrapper procs owning mutation; the value semantics serve as snapshots/copies where cheap.
- Cooperative shared state is pluggable rather than hardwired: the probe-byte waiting store is the array named by `::opunk::console::waiting_chunks_arrayvar` (default `::opunk::console::waiting_chunks`); integrating layers redirect it to their established store (punk::console::default_console points it at `punk::console::input_chunks_waiting`) so probe-consumed bytes stay visible to active readers. - Cooperative shared state is pluggable rather than hardwired: the probe-byte waiting store is the array named by `::opunk::console::waiting_chunks_arrayvar` (default `::opunk::console::waiting_chunks`); integrating layers redirect it to their established store (punk::console::default_console points it at `punk::console::input_chunks_waiting`) so probe-consumed bytes stay visible to active readers.
- Query-based mechanisms enter the class the same way: `::opunk::console::size_query_provider` is a command prefix (invoked with the console object) the base `size` method consults after the -winsize fast path and capability gating; punk::console registers `console_size_provider` via `ensure_object_integration`. Non-channel subclasses override the relevant `-virtual` methods and never touch providers. - Query-based mechanisms enter the class the same way: `::opunk::console::size_query_provider` is a command prefix (invoked with the console object) the base `size` method consults after the -winsize fast path and capability gating; punk::console registers `console_size_provider` via `ensure_object_integration`. Non-channel subclasses override the relevant `-virtual` methods and never touch providers.
- Anchor lifecycle is observable the same way: `::opunk::console::lifecycle_callback` is a command prefix invoked as `{*}$cb created|forgotten <name> {in out}` after `create` anchors / `forget` releases an instance; punk::console registers its handler via `ensure_object_integration` to maintain the G-007 console ownership registry. Empty by default; callback errors propagate to the create/forget caller.
- Method-entry validity guards use `[llength $this] < N` (not `!= N`) so field-adding subclasses keep working. - Method-entry validity guards use `[llength $this] < N` (not `!= N`) so field-adding subclasses keep working.
- Module filenames use the `-999999.0a1.0.tm` magic suffix with `<modulename>-buildversion.txt` per parent conventions. - Module filenames use the `-999999.0a1.0.tm` magic suffix with `<modulename>-buildversion.txt` per parent conventions.
- `opunk::console` (with its dependency `voo`) is included in bootsupport via `src/bootsupport/modules/include_modules.config` for eventual boot/repl-startup console objects; after releasing a new version, rebuild with `make.tcl modules` then refresh the snapshot with `make.tcl bootsupport`. - `opunk::console` (with its dependency `voo`) is included in bootsupport via `src/bootsupport/modules/include_modules.config` for eventual boot/repl-startup console objects; after releasing a new version, rebuild with `make.tcl modules` then refresh the snapshot with `make.tcl bootsupport`.

24
src/modules/opunk/console-999999.0a1.0.tm

@ -75,6 +75,18 @@ tcl::namespace::eval ::opunk::console {
if {![info exists size_query_provider]} { if {![info exists size_query_provider]} {
set size_query_provider "" set size_query_provider ""
} }
#Pluggable anchor-lifecycle callback: a command prefix invoked as
# {*}$lifecycle_callback created <name> {in out} (after the instance is anchored)
# {*}$lifecycle_callback forgotten <name> {in out} (after the anchor is released)
#Empty by default - integrating layers register one to track anchored consoles (punk::console
#maintains its console ownership registry this way). The class/anchor layer itself stays free
#of any punk::console or Thread dependency; callback errors propagate to the create/forget
#caller (a broken integration should be visible, not swallowed).
variable lifecycle_callback
if {![info exists lifecycle_callback]} {
set lifecycle_callback ""
}
} }
voo::class ::opunk::Console -virtual { voo::class ::opunk::Console -virtual {
@ -256,6 +268,10 @@ tcl::namespace::eval ::opunk::console {
error "opunk::console::create name must be alphanumeric/underscore, got '$name'" error "opunk::console::create name must be alphanumeric/underscore, got '$name'"
} }
set [namespace current]::instances::$name [::opunk::Console::new $in $out] set [namespace current]::instances::$name [::opunk::Console::new $in $out]
variable lifecycle_callback
if {$lifecycle_callback ne ""} {
{*}$lifecycle_callback created $name [list $in $out]
}
return [namespace current]::instances::$name return [namespace current]::instances::$name
} }
proc instancevar {name} { proc instancevar {name} {
@ -276,7 +292,13 @@ tcl::namespace::eval ::opunk::console {
return $result return $result
} }
proc forget {name} { proc forget {name} {
unset [instancevar $name] set v [instancevar $name]
set channels [::opunk::Console::channels [set $v]]
unset $v
variable lifecycle_callback
if {$lifecycle_callback ne ""} {
{*}$lifecycle_callback forgotten $name $channels
}
return return
} }

3
src/modules/opunk/console-buildversion.txt

@ -1,6 +1,7 @@
0.3.0 0.4.0
#First line must be a semantic version number #First line must be a semantic version number
#all other lines are ignored. #all other lines are ignored.
#0.4.0 - new pluggable anchor-lifecycle callback (::opunk::console::lifecycle_callback) invoked as '{*}$cb created|forgotten <name> {in out}' after create anchors / forget releases an instance; empty by default, registered by integrating layers (punk::console maintains its G-007 console ownership registry through it); the class/anchor layer remains free of punk::console and Thread dependencies
#0.1.1 - can_respond method docs reference punk::console::settle_can_respond for active-probe settling (no behaviour change) #0.1.1 - can_respond method docs reference punk::console::settle_can_respond for active-probe settling (no behaviour change)
#0.2.0 - probe-byte waiting store changed from dict to array (::opunk::console::waiting_chunks) and made pluggable via waiting_chunks_arrayvar so integrating layers can redirect to an established cooperative store (e.g punk::console::input_chunks_waiting) #0.2.0 - probe-byte waiting store changed from dict to array (::opunk::console::waiting_chunks) and made pluggable via waiting_chunks_arrayvar so integrating layers can redirect to an established cooperative store (e.g punk::console::input_chunks_waiting)
#0.3.0 - size method gains capability gating (can_respond) and consults the new pluggable ::opunk::console::size_query_provider hook for query-based mechanisms; non-channel subclasses continue to simply override size #0.3.0 - size method gains capability gating (can_respond) and consults the new pluggable ::opunk::console::size_query_provider hook for query-based mechanisms; non-channel subclasses continue to simply override size

3
src/modules/punk/AGENTS.md

@ -30,7 +30,8 @@ Source of truth for all modules under the `punk::*` namespace. This is the prima
- New modules under `punk::*` should be created as `<subpath>/<modulename>-999999.0a1.0.tm` following the namespace-to-path convention. - New modules under `punk::*` should be created as `<subpath>/<modulename>-999999.0a1.0.tm` following the namespace-to-path convention.
- punk::console uses the documented `-console` convention throughout: a `-console` value may be a 2-element {in out} channel list, an anchored `opunk::console` instance name, or an `::opunk::Console` object value (resolved via `punk::console::console_spec_resolve`). Query functions use the hybrid pattern (legacy trailing positional spec also accepted, parsed by `punk::console::internal::hybrid_console_spec`) - new query procs must follow it, with tests (see `src/tests/modules/punk/console/testsuites/console/queryprocs.test`). PUNKARGS definitions include the `-console` option via the shared fragments `::punk::console::argdoc::console_opts` (query/set functions) or `::punk::console::argdoc::console_emit_opts` (emit functions) rather than duplicating the option text; never re-add a `-minsize 2` constraint to `-console` (it rejects instance-name specs). The internal `get_size_using_*` size mechanisms deliberately remain canonical-pair positional (always fed by `get_size`). - punk::console uses the documented `-console` convention throughout: a `-console` value may be a 2-element {in out} channel list, an anchored `opunk::console` instance name, or an `::opunk::Console` object value (resolved via `punk::console::console_spec_resolve`). Query functions use the hybrid pattern (legacy trailing positional spec also accepted, parsed by `punk::console::internal::hybrid_console_spec`) - new query procs must follow it, with tests (see `src/tests/modules/punk/console/testsuites/console/queryprocs.test`). PUNKARGS definitions include the `-console` option via the shared fragments `::punk::console::argdoc::console_opts` (query/set functions) or `::punk::console::argdoc::console_emit_opts` (emit functions) rather than duplicating the option text; never re-add a `-minsize 2` constraint to `-console` (it rejects instance-name specs). The internal `get_size_using_*` size mechanisms deliberately remain canonical-pair positional (always fed by `get_size`).
- punk::console emit-side functions (the `punk::console::ansi::*` emit wrappers, mouse/paste toggles, `vt52`, `set_tabstop_width`, `titleset`, top-level `move`, and the width-test probes) accept an optional trailing `-console <consolespec>` pair, parsed manually for performance by `punk::console::internal::opt_console_out`/`opt_console_channels` (`_var` variants for procs whose args-tail also carries row/col/data triples). Each carries a documentation-only PUNKARGS definition that includes the shared `::punk::console::argdoc::console_emit_opts` fragment via `punk::args::resolved_def`; keep manual parsing and PUNKARGS synchronized. New emit procs must follow this pattern. Tests live in `src/tests/modules/punk/console/testsuites/console/emitconsole.test`. - punk::console emit-side functions (the `punk::console::ansi::*` emit wrappers, mouse/paste toggles, `vt52`, `set_tabstop_width`, `titleset`, top-level `move`, and the width-test probes) accept an optional trailing `-console <consolespec>` pair, parsed manually for performance by `punk::console::internal::opt_console_out`/`opt_console_channels` (`_var` variants for procs whose args-tail also carries row/col/data triples). Each carries a documentation-only PUNKARGS definition that includes the shared `::punk::console::argdoc::console_emit_opts` fragment via `punk::args::resolved_def`; keep manual parsing and PUNKARGS synchronized. New emit procs must follow this pattern. Tests live in `src/tests/modules/punk/console/testsuites/console/emitconsole.test`.
- punk::console terminal-property facts (is_vt52, tabwidth, cell_size, last_da1_result, grapheme_cluster_support, check::has_bug_*) are per-console: read/write them via `punk::console::console_fact_get`/`console_fact_set`, keyed by canonical {in out} channel pair. The process-default console `{stdin stdout}` stores its facts in the legacy namespace variables (`::punk::console::is_vt52`, `tabwidth`, ...) so existing external readers keep working - do not bypass the helpers for non-default consoles. `ansi_wanted`/`colour_disabled` (string-generation gates), `ansi_available` and raw-mode state are deliberately process-global (rationale documented at the `console_facts` store in the module). Tests live in `src/tests/modules/punk/console/testsuites/console/consolefacts.test`. - punk::console terminal-property facts (is_vt52, tabwidth, cell_size, last_da1_result, grapheme_cluster_support, check::has_bug_*) are per-console: read/write them via `punk::console::console_fact_get`/`console_fact_set`, keyed by canonical {in out} channel pair. The store is tsv-backed (G-007) so all threads read the same values: the process-default console `{stdin stdout}` keeps the legacy namespace variables (`::punk::console::is_vt52`, `tabwidth`, ...) as its authoritative local storage with write traces mirroring into tsv `punk_console_facts` (so existing external readers and direct writers keep working); non-default consoles store facts only in tsv with an owner-qualified key. Do not bypass the helpers for non-default consoles; use `console_fact_clear` (not direct store manipulation) to reset facts in tests. `ansi_wanted`/`colour_disabled` (string-generation gates), `ansi_available` and raw-mode state are deliberately process-global (rationale documented at the fact store in the module). Tests live in `src/tests/modules/punk/console/testsuites/console/consolefacts.test`.
- punk::console has a console ownership registry (G-007): `console_owner_register`/`console_owner_get`/`console_owner_forget`, tsv `punk_console_owners` keyed by canonical {in out} pair. Ownership is captured when an opunk::console instance is anchored (via `::opunk::console::lifecycle_callback`, wired by `ensure_object_integration`) and by `default_console`; an unregistered console reads as "operate locally". For `{stdin stdout}` first registration wins and only the owner's forget releases the entry. Owner liveness is validated at consult time. The `dec_has_mode`/`ansi_has_mode` caches live in tsv `punk_console_modecache`.
- Use `punk::args::parse` with `@id` references in `argdoc` namespaces for public API procs. - Use `punk::args::parse` with `@id` references in `argdoc` namespaces for public API procs.
- Private helpers go in `namespace eval private { ... }` blocks. - Private helpers go in `namespace eval private { ... }` blocks.
- Keep `namespace export` lists alphabetized. - Keep `namespace export` lists alphabetized.

2
src/modules/punk/basictelnet-999999.0a1.0.tm

@ -640,7 +640,7 @@ namespace eval punk::basictelnet {
# - review # - review
#if we didn't make agreement that server would echo and we're in raw mode #if we didn't make agreement that server would echo and we're in raw mode
if {![dict get $server_option_state 1] && [tsv::get console is_raw]} { if {![dict get $server_option_state 1] && [tsv::get punk_console is_raw]} {
puts -nonewline stdout $chunk puts -nonewline stdout $chunk
} }
# -- --- --- --- # -- --- --- ---

3
src/modules/punk/basictelnet-buildversion.txt

@ -1,3 +1,4 @@
0.1.0 0.1.1
#First line must be a semantic version number #First line must be a semantic version number
#all other lines are ignored. #all other lines are ignored.
#0.1.1 - call-site update for punk::console 0.6.0 tsv array rename: console -> punk_console (is_raw); no behaviour change

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

@ -99,8 +99,8 @@ namespace eval punk::console {
variable previous_stty_state_stderr "" variable previous_stty_state_stderr ""
#variable is_raw 0 #variable is_raw 0
if {![tsv::exists console is_raw]} { if {![tsv::exists punk_console is_raw]} {
tsv::set console is_raw 0 tsv::set punk_console is_raw 0
} }
variable input_chunks_waiting variable input_chunks_waiting
@ -277,7 +277,7 @@ namespace eval punk::console {
variable ansi_available variable ansi_available
set raw_or_line [string tolower $raw_or_line] set raw_or_line [string tolower $raw_or_line]
if {$raw_or_line eq "query"} { if {$raw_or_line eq "query"} {
if {[tsv::get console is_raw]} { if {[tsv::get punk_console is_raw]} {
return "raw" return "raw"
} else { } else {
return "line" return "line"
@ -684,7 +684,7 @@ namespace eval punk::console {
#For mintty-without-winpty (terminal presenting pipes) the twapi raw cycle was already a #For mintty-without-winpty (terminal presenting pipes) the twapi raw cycle was already a
#no-op via its get_console_handle early-return, so nothing is lost by skipping. #no-op via its get_console_handle early-return, so nothing is lost by skipping.
set input_is_console_or_tty [expr {[dict exists $previous_input_state -inputmode] || [dict exists $previous_input_state -mode]}] set input_is_console_or_tty [expr {[dict exists $previous_input_state -inputmode] || [dict exists $previous_input_state -mode]}]
if {![tsv::get console is_raw]} { if {![tsv::get punk_console is_raw]} {
set was_raw 0 set was_raw 0
if {$input_is_console_or_tty} { if {$input_is_console_or_tty} {
punk::console::enableRaw punk::console::enableRaw
@ -2102,6 +2102,11 @@ namespace eval punk::console {
if {[info exists ::opunk::console::instances::default]} { if {[info exists ::opunk::console::instances::default]} {
#instance exists => opunk::console is loaded - ensure integration wiring applies #instance exists => opunk::console is loaded - ensure integration wiring applies
ensure_object_integration ensure_object_integration
#the instance may predate the lifecycle-hook wiring (or opunk::console may be an
#older version without it) - make sure the default console has a registered owner
if {[console_owner_get {stdin stdout}] eq ""} {
console_owner_register {stdin stdout}
}
return default return default
} }
if {[catch {package require opunk::console}]} { if {[catch {package require opunk::console}]} {
@ -2109,29 +2114,40 @@ namespace eval punk::console {
} }
ensure_object_integration ensure_object_integration
opunk::console::create default stdin stdout opunk::console::create default stdin stdout
if {[console_owner_get {stdin stdout}] eq ""} {
console_owner_register {stdin stdout}
}
return default return default
} }
# -- --- --- --- --- --- --- # -- --- --- --- --- --- ---
#Per-console facts (interim store for the multi-console goals G-001/G-002). #Per-console facts (G-007 location transparency; originated as interim store for G-001/G-002).
#Facts that are properties of a particular terminal (vt52 mode, tabwidth, cell size, quirk #Facts that are properties of a particular terminal (vt52 mode, tabwidth, cell size, quirk
#test results, DA1 response) are keyed by the console's canonical {in out} channel pair. #test results, DA1 response) are keyed by the console's canonical {in out} channel pair and
#stored in tsv (shared array 'punk_console_facts') so every thread of the process reads the
#same values with no RPC. Infrastructure tsv arrays are punk_-prefixed: tsv names are
#process-global and subshell application code may use tsv for its own purposes.
#The process-default console {stdin stdout} keeps the legacy namespace variables #The process-default console {stdin stdout} keeps the legacy namespace variables
#(::punk::console::is_vt52, tabwidth, cell_size, last_da1_result, grapheme_cluster_support, #(::punk::console::is_vt52, tabwidth, cell_size, last_da1_result, grapheme_cluster_support,
#::punk::console::check::has_bug_*) as its storage, so external readers of those variables #::punk::console::check::has_bug_*) as its authoritative local storage, so external readers
#(punk::repl, textblock, overtype, punk::ansi, punk::ns) continue to see current values. #AND writers of those variables (punk::repl, textblock, overtype, punk::ansi, punk::ns, test
#suites) continue to work. Write traces on those variables (installed at the end of this
#module) mirror every write - including direct variable writes that bypass console_fact_set -
#into tsv, where non-owner threads read them. Which context may treat the legacy variables as
#authoritative is decided by the console ownership registry below (unregistered = every
#context is local, preserving single-interp behaviour exactly).
#Non-default consoles store facts only in tsv, with the key qualified by the owner context
#(registry owner, else the calling thread) because non-std channel *names* are thread-local -
#two threads can each have a channel named e.g sock10 referring to different consoles, and an
#unqualified key would alias them.
#Deliberately NOT stored here (process-global): #Deliberately NOT stored here (process-global):
# - ansi_wanted/colour_disabled - they gate string *generation* (code_a+ etc) where the # - ansi_wanted/colour_disabled - they gate string *generation* (code_a+ etc) where the
# eventual output console is unknown # eventual output console is unknown
# - ansi_available - determined via the real process console (twapi); object consoles carry # - ansi_available - determined via the real process console (twapi); object consoles carry
# their own settled can_respond capability instead # their own settled can_respond capability instead
# - tsv console is_raw and the enableRaw/disableRaw family - raw mode is a property of the # - tsv punk_console is_raw and the enableRaw/disableRaw family - raw mode is a property of the
# real attached console; get_ansi_response_payload already guards against cycling it for # real attached console; get_ansi_response_payload already guards against cycling it for
# non-console channels # non-console channels
variable console_facts
if {![info exists console_facts]} {
set console_facts [dict create]
}
variable console_fact_info variable console_fact_info
set console_fact_info [dict create {*}{ set console_fact_info [dict create {*}{
is_vt52 {legacyvar ::punk::console::is_vt52 default 0} is_vt52 {legacyvar ::punk::console::is_vt52 default 0}
@ -2142,13 +2158,105 @@ namespace eval punk::console {
has_bug_legacysymbolwidth {legacyvar ::punk::console::check::has_bug_legacysymbolwidth default -1} has_bug_legacysymbolwidth {legacyvar ::punk::console::check::has_bug_legacysymbolwidth default -1}
has_bug_zwsp {legacyvar ::punk::console::check::has_bug_zwsp default -1} has_bug_zwsp {legacyvar ::punk::console::check::has_bug_zwsp default -1}
}] }]
# -- --- --- --- --- --- ---
#Console ownership registry (G-007).
#Exactly one context can own an input channel's reader and the process console's raw mode -
#operations on a console you do not own must be brokered to the owner. This registry records
#which thread that is, per console, keyed by canonical {in out} channel pair in tsv (shared
#array 'punk_console_owners') so any thread can consult it.
#Ownership is captured at registration time: the context anchoring an opunk::console instance
#is recorded via the ::opunk::console lifecycle callback (wired by ensure_object_integration),
#and default_console registers the default console {stdin stdout} - in a punk session that
#happens in the repl thread during repl::init. Re-registration (last anchor wins) is
#deliberate: ownership is process topology, not a terminal fact, and consoles can be handed
#between contexts. Consult-time liveness validation clears entries whose owning thread has
#exited, recovering lifecycle cohesion when forget was never called.
lappend PUNKARGS [list {
@id -id ::punk::console::console_owner_register
@cmd -name punk::console::console_owner_register -summary\
"Record the owning thread for a console in the process-wide ownership registry."\
-help\
"The owner is the context whose reader/raw-mode arbitration governs the console -
operations from other contexts consult the registry to route to it. Normally called
automatically when an opunk::console instance is anchored (and by default_console);
direct calls are for consoles managed without the object layer. Re-registration
overwrites (last anchor wins). Returns the recorded owner thread id."
@values -min 1 -max 2
consolespec -type list -help\
"Console specification: {in out} channel pair, anchored opunk::console instance
name, or ::opunk::Console object value."
ownerthread -type string -optional 1 -help\
"Thread id of the owning thread (as returned by thread::id in that thread).
Defaults to the calling thread."
}]
proc console_owner_register {consolespec {ownerthread ""}} {
#see PUNKARGS id ::punk::console::console_owner_register
set pair [internal::spec_to_channelpair $consolespec]
if {$ownerthread eq ""} {
set ownerthread [thread::id]
}
tsv::set punk_console_owners $pair $ownerthread
return $ownerthread
}
lappend PUNKARGS [list {
@id -id ::punk::console::console_owner_get
@cmd -name punk::console::console_owner_get -summary\
"Return the registered owning thread id for a console, or empty string."\
-help\
"Returns empty string when the console is unregistered - callers treat that as
'operate locally' so single-interp (non-repl) usage is unaffected by the registry.
Owner liveness is validated at consult time: a registry entry whose thread no longer
exists is cleared and reported as unregistered."
@values -min 1 -max 1
consolespec -type list -help\
"Console specification: {in out} channel pair, anchored opunk::console instance
name, or ::opunk::Console object value."
}]
proc console_owner_get {consolespec} {
#see PUNKARGS id ::punk::console::console_owner_get
set pair [internal::spec_to_channelpair $consolespec]
if {![tsv::get punk_console_owners $pair owner]} {
return ""
}
if {[catch {thread::exists $owner} alive] || !$alive} {
#stale entry - owning thread has exited (or recorded handle is invalid)
catch {tsv::unset punk_console_owners $pair}
return ""
}
return $owner
}
lappend PUNKARGS [list {
@id -id ::punk::console::console_owner_forget
@cmd -name punk::console::console_owner_forget -summary\
"Remove a console's entry from the ownership registry."\
-help\
"Normally called automatically when the anchored opunk::console instance is
forgotten. A forgotten console reads as unregistered (operate locally)."
@values -min 1 -max 1
consolespec -type list -help\
"Console specification: {in out} channel pair, anchored opunk::console instance
name, or ::opunk::Console object value."
}]
proc console_owner_forget {consolespec} {
#see PUNKARGS id ::punk::console::console_owner_forget
set pair [internal::spec_to_channelpair $consolespec]
if {[tsv::exists punk_console_owners $pair]} {
tsv::unset punk_console_owners $pair
}
return
}
# -- --- --- --- --- --- ---
lappend PUNKARGS [list { lappend PUNKARGS [list {
@id -id ::punk::console::console_fact_get @id -id ::punk::console::console_fact_get
@cmd -name punk::console::console_fact_get -summary\ @cmd -name punk::console::console_fact_get -summary\
"Read a per-console fact for the specified console."\ "Read a per-console fact for the specified console."\
-help\ -help\
"Facts for the process-default console {stdin stdout} are stored in the legacy "Facts for the process-default console {stdin stdout} are read from the legacy
namespace variables; other consoles use a store keyed by canonical channel pair. namespace variables in the console's owning context (or any context when
unregistered), and from their tsv mirror elsewhere; other consoles use the tsv
store keyed by owner-qualified canonical channel pair.
Returns the fact's default when nothing has been recorded for the console." Returns the fact's default when nothing has been recorded for the console."
@values -min 2 -max 2 @values -min 2 -max 2
consolespec -type list -help\ consolespec -type list -help\
@ -2159,16 +2267,21 @@ namespace eval punk::console {
proc console_fact_get {consolespec key} { proc console_fact_get {consolespec key} {
#see PUNKARGS id ::punk::console::console_fact_get #see PUNKARGS id ::punk::console::console_fact_get
variable console_fact_info variable console_fact_info
if {[llength $consolespec] != 2} { set consolespec [internal::spec_to_channelpair $consolespec]
set cinfo [console_spec_resolve $consolespec]
set consolespec [list [dict get $cinfo in] [dict get $cinfo out]]
}
if {[lindex $consolespec 0] eq "stdin" && [lindex $consolespec 1] eq "stdout"} { if {[lindex $consolespec 0] eq "stdin" && [lindex $consolespec 1] eq "stdout"} {
if {[internal::is_default_console_context]} {
return [set [dict get $console_fact_info $key legacyvar]]
}
#another context owns the default console - its legacy-variable writes (traced) are
#mirrored in tsv
if {[tsv::get punk_console_facts [list {} stdin stdout $key] mirrored]} {
return $mirrored
}
#nothing mirrored yet - this context's own legacy variable holds the module default
return [set [dict get $console_fact_info $key legacyvar]] return [set [dict get $console_fact_info $key legacyvar]]
} }
variable console_facts if {[tsv::get punk_console_facts [internal::console_fact_key $consolespec $key] val]} {
if {[dict exists $console_facts $consolespec $key]} { return $val
return [dict get $console_facts $consolespec $key]
} }
return [dict get $console_fact_info $key default] return [dict get $console_fact_info $key default]
} }
@ -2178,8 +2291,11 @@ namespace eval punk::console {
"Record a per-console fact for the specified console."\ "Record a per-console fact for the specified console."\
-help\ -help\
"Facts for the process-default console {stdin stdout} are stored in the legacy "Facts for the process-default console {stdin stdout} are stored in the legacy
namespace variables (visible to their external readers); other consoles use a namespace variables (visible to their external readers) with a traced tsv mirror;
store keyed by canonical channel pair. Returns the value." a write from a context that does not own the default console is forwarded to the
owning thread so the owner's legacy variables stay authoritative. Other consoles
use the tsv store keyed by owner-qualified canonical channel pair. Returns the
value."
@values -min 3 -max 3 @values -min 3 -max 3
consolespec -type list -help\ consolespec -type list -help\
"Console specification: {in out} channel pair, anchored opunk::console instance "Console specification: {in out} channel pair, anchored opunk::console instance
@ -2190,18 +2306,216 @@ namespace eval punk::console {
proc console_fact_set {consolespec key value} { proc console_fact_set {consolespec key value} {
#see PUNKARGS id ::punk::console::console_fact_set #see PUNKARGS id ::punk::console::console_fact_set
variable console_fact_info variable console_fact_info
if {[llength $consolespec] != 2} { set consolespec [internal::spec_to_channelpair $consolespec]
set cinfo [console_spec_resolve $consolespec]
set consolespec [list [dict get $cinfo in] [dict get $cinfo out]]
}
if {[lindex $consolespec 0] eq "stdin" && [lindex $consolespec 1] eq "stdout"} { if {[lindex $consolespec 0] eq "stdin" && [lindex $consolespec 1] eq "stdout"} {
set [dict get $console_fact_info $key legacyvar] $value set owner [console_owner_get {stdin stdout}]
if {$owner eq "" || $owner eq [thread::id]} {
#this context is authoritative - the write trace mirrors the value to tsv
set [dict get $console_fact_info $key legacyvar] $value
} else {
#keep this thread's own legacy variable current for its local readers
#(the write trace does not mirror from a non-owner context)
set [dict get $console_fact_info $key legacyvar] $value
#apply the write in the owning thread so the owner's legacy variables (its
#authoritative local store) stay in sync - safe while the owner services
#events, as the repl thread does while a codethread executes (the established
#vt52-alias transport). thread::send executes in the target thread's MAIN
#interp (where a punk session's console-owning punk::console lives); routing
#for nested/child interps is the interp-alias layer of G-007, not this path.
catch {thread::send $owner [list ::punk::console::console_fact_set [list stdin stdout] $key $value]}
#ensure the tsv mirror holds the value regardless of the send outcome: the
#owner's write trace mirrors it with a current punk::console, but the owner
#may be unreachable, have no punk::console in its main interp, or run an
#older punk::console that accepts the write without mirroring (idempotent
#when the trace already did it)
tsv::set punk_console_facts [list {} stdin stdout $key] $value
}
} else { } else {
variable console_facts tsv::set punk_console_facts [internal::console_fact_key $consolespec $key] $value
dict set console_facts $consolespec $key $value
} }
return $value return $value
} }
lappend PUNKARGS [list {
@id -id ::punk::console::console_fact_clear
@cmd -name punk::console::console_fact_clear -summary\
"Clear all recorded facts for the specified console."\
-help\
"For the process-default console {stdin stdout} every fact is re-recorded at its
module default (via console_fact_set, so ownership forwarding and the tsv mirror
apply); for other consoles the recorded tsv entries are removed so subsequent
reads fall back to the fact defaults. Primarily for tests and maintenance."
@values -min 1 -max 1
consolespec -type list -help\
"Console specification: {in out} channel pair, anchored opunk::console instance
name, or ::opunk::Console object value."
}]
proc console_fact_clear {consolespec} {
#see PUNKARGS id ::punk::console::console_fact_clear
variable console_fact_info
set consolespec [internal::spec_to_channelpair $consolespec]
if {[lindex $consolespec 0] eq "stdin" && [lindex $consolespec 1] eq "stdout"} {
dict for {key finfo} $console_fact_info {
console_fact_set $consolespec $key [dict get $finfo default]
}
} else {
dict for {key finfo} $console_fact_info {
set tsvkey [internal::console_fact_key $consolespec $key]
if {[tsv::exists punk_console_facts $tsvkey]} {
tsv::unset punk_console_facts $tsvkey
}
}
}
return
}
namespace eval internal {
#Helpers for the per-console fact store and ownership registry (G-007).
punk::args::define {
@id -id ::punk::console::internal::spec_to_channelpair
@cmd -name punk::console::internal::spec_to_channelpair -summary\
"Resolve any console spec form to its canonical {in out} channel pair."\
-help\
"2-element specs are already canonical and returned as-is (no object-layer
cost); instance-name and object-value specs resolve via console_spec_resolve."
@values -min 1 -max 1
spec -type list -help\
"Console specification: {in out} channel pair, anchored opunk::console instance
name, or ::opunk::Console object value."
}
proc spec_to_channelpair {spec} {
#see PUNKARGS id ::punk::console::internal::spec_to_channelpair
if {[llength $spec] != 2} {
set cinfo [punk::console::console_spec_resolve $spec]
return [list [dict get $cinfo in] [dict get $cinfo out]]
}
return $spec
}
punk::args::define {
@id -id ::punk::console::internal::is_default_console_context
@cmd -name punk::console::internal::is_default_console_context -summary\
"True when the calling thread may treat the default-console legacy variables as authoritative."\
-help\
"True when the calling thread is the registered owner of the default console
{stdin stdout}, or when no owner is registered (single-interp/non-repl usage
behaves exactly as before the ownership registry existed)."
@values -min 0 -max 0
}
proc is_default_console_context {} {
#see PUNKARGS id ::punk::console::internal::is_default_console_context
set owner [punk::console::console_owner_get {stdin stdout}]
return [expr {$owner eq "" || $owner eq [thread::id]}]
}
punk::args::define {
@id -id ::punk::console::internal::console_share_qualifier
@cmd -name punk::console::internal::console_share_qualifier -summary\
"Owner qualifier used in shared (tsv) store keys for a canonical channel pair."\
-help\
"Empty string for the process-default console {stdin stdout} (std channels are
process-wide names). For other consoles: the registered owner thread, else the
calling thread - non-std channel names are thread-local, so unqualified keys
would alias distinct consoles that happen to share a channel name across
threads."
@values -min 1 -max 1
inoutchannels -type list -help\
"Canonical {in out} channel pair."
}
proc console_share_qualifier {inoutchannels} {
#see PUNKARGS id ::punk::console::internal::console_share_qualifier
if {[lindex $inoutchannels 0] eq "stdin" && [lindex $inoutchannels 1] eq "stdout"} {
return ""
}
set owner [punk::console::console_owner_get $inoutchannels]
if {$owner ne ""} {
return $owner
}
return [thread::id]
}
punk::args::define {
@id -id ::punk::console::internal::console_fact_key
@cmd -name punk::console::internal::console_fact_key -summary\
"tsv key for a fact of a non-default console: {qualifier in out factkey}."
@values -min 2 -max 2
inoutchannels -type list -help\
"Canonical {in out} channel pair."
key -type string -help\
"Fact key (as accepted by console_fact_get/console_fact_set)."
}
proc console_fact_key {inoutchannels key} {
#see PUNKARGS id ::punk::console::internal::console_fact_key
return [list [console_share_qualifier $inoutchannels] [lindex $inoutchannels 0] [lindex $inoutchannels 1] $key]
}
punk::args::define {
@id -id ::punk::console::internal::default_fact_write_trace
@cmd -name punk::console::internal::default_fact_write_trace -summary\
"Variable write-trace callback mirroring default-console legacy fact variables to tsv."\
-help\
"Installed at module load on each legacy fact variable so every write - including
direct variable writes that bypass console_fact_set - is visible to other
threads. Only mirrors when the calling context is authoritative for the default
console (a non-owner thread's hollow copies of the variables must not clobber
the owner's values)."
@leaders
key -type string -help\
"Fact key the traced variable stores."
legacyvar -type string -help\
"Fully qualified name of the traced legacy variable."
@values -min 0 -max 3
traceargs -type any -optional 1 -multiple 1 -help\
"name1 name2 op appended by the trace mechanism (unused)."
}
proc default_fact_write_trace {key legacyvar args} {
#see PUNKARGS id ::punk::console::internal::default_fact_write_trace
if {[is_default_console_context]} {
tsv::set punk_console_facts [list {} stdin stdout $key] [set $legacyvar]
}
}
punk::args::define {
@id -id ::punk::console::internal::object_console_lifecycle
@cmd -name punk::console::internal::object_console_lifecycle -summary\
"opunk::console lifecycle callback maintaining the console ownership registry."\
-help\
"Registered as ::opunk::console::lifecycle_callback by ensure_object_integration.
The context anchoring a console instance is recorded as that console's owner;
forgetting the instance clears the entry. For the process-default console
{stdin stdout} the first registration wins: anchored instances are per-thread,
so a second thread anchoring the std channels is creating a local view of the
already-owned console, not taking over its reader. Non-std channel names are
thread-local, so for other pairs each anchor is a genuinely distinct console
and the latest registration wins."
@values -min 3 -max 3
event -type string -choices {created forgotten} -help\
"Lifecycle event."
name -type string -help\
"Anchored instance name (unused - ownership is keyed by channel pair)."
channels -type list -help\
"Canonical {in out} channel pair of the instance."
}
proc object_console_lifecycle {event name channels} {
#see PUNKARGS id ::punk::console::internal::object_console_lifecycle
switch -exact -- $event {
created {
if {[lindex $channels 0] eq "stdin" && [lindex $channels 1] eq "stdout"} {
if {[punk::console::console_owner_get $channels] ne ""} {
return
}
}
punk::console::console_owner_register $channels
}
forgotten {
if {[lindex $channels 0] eq "stdin" && [lindex $channels 1] eq "stdout"} {
#only the owner's forget releases the default console - a non-owner
#thread forgetting its local anchor view must not clear the entry
set owner [punk::console::console_owner_get $channels]
if {$owner ne "" && $owner ne [thread::id]} {
return
}
}
punk::console::console_owner_forget $channels
}
}
return
}
}
# -- --- --- --- --- --- --- # -- --- --- --- --- --- ---
#default timeout for the active can_respond settling probe (see settle_can_respond) #default timeout for the active can_respond settling probe (see settle_can_respond)
@ -2501,6 +2815,12 @@ namespace eval punk::console {
if {$::opunk::console::size_query_provider eq ""} { if {$::opunk::console::size_query_provider eq ""} {
set ::opunk::console::size_query_provider [list ::punk::console::console_size_provider] set ::opunk::console::size_query_provider [list ::punk::console::console_size_provider]
} }
#ownership registry maintenance rides the anchor lifecycle (older opunk::console
#versions have no lifecycle_callback - default_console's explicit registration and
#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]
}
set object_integration_done 1 set object_integration_done 1
} }
@ -2903,8 +3223,9 @@ namespace eval punk::console {
puts -nonewline $term_out "\x1b\[?${modes_string}l" puts -nonewline $term_out "\x1b\[?${modes_string}l"
} }
variable dec_has_mode_cache #dec_has_mode/ansi_has_mode results are cached in tsv (shared array 'punk_console_modecache',
set dec_has_mode_cache [dict create] #one entry per {dectype qualifier in out passthrough mode} key) so every thread of the
#process shares one cache per console and cache writes are single-key atomic (G-007).
namespace eval argdoc { namespace eval argdoc {
lappend PUNKARGS [list { lappend PUNKARGS [list {
@id -id ::punk::console::dec_has_mode @id -id ::punk::console::dec_has_mode
@ -2960,19 +3281,19 @@ namespace eval punk::console {
error "punk::console::dec_has_mode unrecognised mode '$num_or_name'. Known mode names: [dict keys $decmode_names]" error "punk::console::dec_has_mode unrecognised mode '$num_or_name'. Known mode names: [dict keys $decmode_names]"
} }
} }
variable dec_has_mode_cache
set cinfo [console_spec_resolve $console] set cinfo [console_spec_resolve $console]
set console [list [dict get $cinfo in] [dict get $cinfo out]] set console [list [dict get $cinfo in] [dict get $cinfo out]]
#cache on the canonical channel pair and passthrough - all -console spec forms #cache in tsv on the canonical channel pair (owner-qualified for non-default consoles)
#addressing the same console share one cache entry #and passthrough - all -console spec forms addressing the same console share one cache
set cachekey "$console $passthrough" #entry, visible to all threads of the process
set cachekey [list dec [internal::console_share_qualifier $console] [lindex $console 0] [lindex $console 1] $passthrough $m]
if {$do_refresh} { if {$do_refresh} {
if {[dict exists $dec_has_mode_cache $cachekey $m]} { if {[tsv::exists punk_console_modecache $cachekey]} {
dict unset dec_has_mode_cache $cachekey $m tsv::unset punk_console_modecache $cachekey
} }
} }
if {![dict exists $dec_has_mode_cache $cachekey $m]} { if {![tsv::get punk_console_modecache $cachekey has_mode]} {
set capturingregex [string map [list %MODE% $m] {(.*)(\x1b\[\?%MODE%;([0-9]+)\$y)$}] ;#must capture prefix,entire-response,response-payload set capturingregex [string map [list %MODE% $m] {(.*)(\x1b\[\?%MODE%;([0-9]+)\$y)$}] ;#must capture prefix,entire-response,response-payload
set request "\x1b\[?$m\$p" set request "\x1b\[?$m\$p"
set payload [punk::console::internal::get_ansi_response_payload -console $console -passthrough $passthrough $request $capturingregex] set payload [punk::console::internal::get_ansi_response_payload -console $console -passthrough $passthrough $request $capturingregex]
@ -2980,7 +3301,7 @@ namespace eval punk::console {
#we can use the payload result as the response as non-zero responses evaluate to true #we can use the payload result as the response as non-zero responses evaluate to true
set has_mode $payload set has_mode $payload
if {$has_mode ne ""} { if {$has_mode ne ""} {
dict set dec_has_mode_cache $cachekey $m $has_mode tsv::set punk_console_modecache $cachekey $has_mode
set source "query" set source "query"
} else { } else {
#don't cache an empty/failed response - review #don't cache an empty/failed response - review
@ -2988,7 +3309,6 @@ namespace eval punk::console {
set source "failedquery" set source "failedquery"
} }
} else { } else {
set has_mode [dict get $dec_has_mode_cache $cachekey $m]
set source "cache" set source "cache"
} }
if {$return eq "dict"} { if {$return eq "dict"} {
@ -3163,8 +3483,6 @@ namespace eval punk::console {
return $out return $out
} }
variable ansi_has_mode_cache
set ansi_has_mode_cache [dict create]
namespace eval argdoc { namespace eval argdoc {
lappend PUNKARGS [list { lappend PUNKARGS [list {
@id -id ::punk::console::ansi_has_mode @id -id ::punk::console::ansi_has_mode
@ -3214,26 +3532,26 @@ namespace eval punk::console {
error "punk::console::ansi_has_mode unrecognised mode '$num_or_name'. Known mode names: [dict keys $ansimode_names]" error "punk::console::ansi_has_mode unrecognised mode '$num_or_name'. Known mode names: [dict keys $ansimode_names]"
} }
} }
variable ansi_has_mode_cache
set cinfo [console_spec_resolve $console] set cinfo [console_spec_resolve $console]
set console [list [dict get $cinfo in] [dict get $cinfo out]] set console [list [dict get $cinfo in] [dict get $cinfo out]]
#cache on the canonical channel pair and passthrough - all -console spec forms #cache in tsv on the canonical channel pair (owner-qualified for non-default consoles)
#addressing the same console share one cache entry #and passthrough - all -console spec forms addressing the same console share one cache
set cachekey "$console $passthrough" #entry, visible to all threads of the process
set cachekey [list ansi [internal::console_share_qualifier $console] [lindex $console 0] [lindex $console 1] $passthrough $m]
if {$do_refresh} { if {$do_refresh} {
if {[dict exists $ansi_has_mode_cache $cachekey $m]} { if {[tsv::exists punk_console_modecache $cachekey]} {
dict unset ansi_has_mode_cache $cachekey $m tsv::unset punk_console_modecache $cachekey
} }
} }
if {![dict exists $ansi_has_mode_cache $cachekey $m]} { if {![tsv::get punk_console_modecache $cachekey has_mode]} {
set capturingregex [string map [list %MODE% $m] {(.*)(\x1b\[%MODE%;([0-9]+)\$y)$}] ;#must capture prefix,entire-response,response-payload set capturingregex [string map [list %MODE% $m] {(.*)(\x1b\[%MODE%;([0-9]+)\$y)$}] ;#must capture prefix,entire-response,response-payload
set request "\x1b\[$m\$p" set request "\x1b\[$m\$p"
set payload [punk::console::internal::get_ansi_response_payload -console $console -passthrough $passthrough $request $capturingregex] set payload [punk::console::internal::get_ansi_response_payload -console $console -passthrough $passthrough $request $capturingregex]
#set has_mode [expr {$payload != 0}] #set has_mode [expr {$payload != 0}]
set has_mode $payload set has_mode $payload
if {$has_mode ne ""} { if {$has_mode ne ""} {
dict set ansi_has_mode_cache $cachekey $m $has_mode tsv::set punk_console_modecache $cachekey $has_mode
set source "query" set source "query"
} else { } else {
#don't cache an empty/failed response - review #don't cache an empty/failed response - review
@ -3241,7 +3559,6 @@ namespace eval punk::console {
set source "failedquery" set source "failedquery"
} }
} else { } else {
set has_mode [dict get $ansi_has_mode_cache $cachekey $m]
set source "cache" set source "cache"
} }
if {$return eq "dict"} { if {$return eq "dict"} {
@ -4141,7 +4458,7 @@ namespace eval punk::console {
#todo - compare speed with get_cursor_pos - work out why the big difference #todo - compare speed with get_cursor_pos - work out why the big difference
proc test_cursor_pos {} { proc test_cursor_pos {} {
if {![tsv::get console is_raw]} { if {![tsv::get punk_console is_raw]} {
set was_raw 0 set was_raw 0
enableRaw enableRaw
} else { } else {
@ -5298,9 +5615,9 @@ namespace eval punk::console::system {
} }
} }
if {[catch { if {[catch {
tsv::set console is_raw 1 tsv::set punk_console is_raw 1
} errMsg]} { } errMsg]} {
puts stderr "enableRaw_stty failed to run tsv::set console is_raw 1: $errMsg" puts stderr "enableRaw_stty failed to run tsv::set punk_console is_raw 1: $errMsg"
} }
catch { catch {
chan configure stdin -inputmode raw chan configure stdin -inputmode raw
@ -5326,9 +5643,9 @@ namespace eval punk::console::system {
} }
} }
if {[catch { if {[catch {
tsv::set console is_raw 0 tsv::set punk_console is_raw 0
} errMsg]} { } errMsg]} {
puts stderr "disableRaw_stty failed to run tsv::set console is_raw 0: $errMsg" puts stderr "disableRaw_stty failed to run tsv::set punk_console is_raw 0: $errMsg"
} }
catch { catch {
chan configure stdin -inputmode normal chan configure stdin -inputmode normal
@ -5357,7 +5674,7 @@ namespace eval punk::console::system {
} }
} }
catch { catch {
tsv::set console is_raw 1 tsv::set punk_console is_raw 1
} }
catch { catch {
@ -5387,7 +5704,7 @@ namespace eval punk::console::system {
} }
catch { catch {
tsv::set console is_raw 0 tsv::set punk_console is_raw 0
} }
#mintty on windows seems to have -inputmode available when running with winpty, #mintty on windows seems to have -inputmode available when running with winpty,
#and is missing the -inputmode key when running without winpty. #and is missing the -inputmode key when running without winpty.
@ -5420,7 +5737,7 @@ namespace eval punk::console::system {
#set newmode [dict merge $oldmode [dict create -lineinput 0 -echoinput 0]] #set newmode [dict merge $oldmode [dict create -lineinput 0 -echoinput 0]]
set newmode [twapi::get_console_input_mode] set newmode [twapi::get_console_input_mode]
tsv::set console is_raw 1 tsv::set punk_console is_raw 1
#don't disable handler - it will detect is_raw #don't disable handler - it will detect is_raw
### twapi::set_console_control_handler {} ### twapi::set_console_control_handler {}
return [list stdin [list from $oldmode to $newmode]] return [list stdin [list from $oldmode to $newmode]]
@ -5431,7 +5748,7 @@ namespace eval punk::console::system {
set ch_state [chan conf $channel] set ch_state [chan conf $channel]
if {[dict exists $ch_state -inputmode]} { if {[dict exists $ch_state -inputmode]} {
chan conf $channel -inputmode normal chan conf $channel -inputmode normal
tsv::set console is_raw 0 tsv::set punk_console is_raw 0
return [list $channel [list from [dict get $ch_state -inputmode] to normal]] return [list $channel [list from [dict get $ch_state -inputmode] to normal]]
} else { } else {
if {[catch {twapi::get_console_handle stdin} console_handle]} { if {[catch {twapi::get_console_handle stdin} console_handle]} {
@ -5443,7 +5760,7 @@ namespace eval punk::console::system {
# Turn on the echo and line-editing bits # Turn on the echo and line-editing bits
twapi::modify_console_input_mode $console_handle -lineinput 1 -echoinput 1 twapi::modify_console_input_mode $console_handle -lineinput 1 -echoinput 1
set newmode [twapi::get_console_input_mode] set newmode [twapi::get_console_input_mode]
tsv::set console is_raw 0 tsv::set punk_console is_raw 0
return [list stdin [list from $oldmode to $newmode]] return [list stdin [list from $oldmode to $newmode]]
} }
} }
@ -5483,7 +5800,7 @@ namespace eval punk::console::system {
} }
} }
if {$wrote} { if {$wrote} {
tsv::set console is_raw 1 tsv::set punk_console is_raw 1
#after 100 #after 100
close $pipe close $pipe
} else { } else {
@ -5501,7 +5818,7 @@ namespace eval punk::console::system {
} }
exec {*}$sttycmd raw -echo <@$channel exec {*}$sttycmd raw -echo <@$channel
tsv::set console is_raw 1 tsv::set punk_console is_raw 1
#review - inconsistent return dict #review - inconsistent return dict
return [dict create stdin [list from [set previous_stty_state_$channel] to "" note "fixme - to state not shown"]] return [dict create stdin [list from [set previous_stty_state_$channel] to "" note "fixme - to state not shown"]]
} else { } else {
@ -5515,7 +5832,7 @@ namespace eval punk::console::system {
set ch_state [chan conf $channel] set ch_state [chan conf $channel]
if {[dict exists $ch_state -inputmode]} { if {[dict exists $ch_state -inputmode]} {
chan conf $channel -inputmode normal chan conf $channel -inputmode normal
tsv::set console is_raw 0 tsv::set punk_console is_raw 0
return [list $channel [list from [dict get $ch_state -inputmode] to normal]] return [list $channel [list from [dict get $ch_state -inputmode] to normal]]
} else { } else {
#tcl <= 8.6x doesn't support -inputmode #tcl <= 8.6x doesn't support -inputmode
@ -5529,7 +5846,7 @@ namespace eval punk::console::system {
return restored return restored
} }
exec {*}$sttycmd -raw echo <@$channel exec {*}$sttycmd -raw echo <@$channel
tsv::set console is_raw 0 tsv::set punk_console is_raw 0
#do we really want to exec stty yet again to show final 'to' state? #do we really want to exec stty yet again to show final 'to' state?
#probably not. We should work out how to read the stty result flags and set a result.. or just limit from,to to showing echo and lineedit states. #probably not. We should work out how to read the stty result flags and set a result.. or just limit from,to to showing echo and lineedit states.
return [list stdin [list from "[set previous_stty_state_$channel]" to "" note "fixme - to state not shown"]] return [list stdin [list from "[set previous_stty_state_$channel]" to "" note "fixme - to state not shown"]]
@ -5719,7 +6036,9 @@ namespace eval punk::console {
variable ps_pipename variable ps_pipename
if {![info exists ps_consolemode_contents]} { if {![info exists ps_consolemode_contents]} {
#start persistent powershell consolemode_server.ps1 named pipe server #start persistent powershell consolemode_server.ps1 named pipe server
if {$::argv0 ne ""} { #::argv0 is absent in secondary threads (thread::create workers, codethreads) -
#the module must remain loadable there
if {[info exists ::argv0] && $::argv0 ne ""} {
set pstooldir [file dirname [file dirname [file normalize $::argv0]]]/scriptlib/utils/pwsh set pstooldir [file dirname [file dirname [file normalize $::argv0]]]/scriptlib/utils/pwsh
} else { } else {
set pstooldir [pwd] set pstooldir [pwd]
@ -5853,6 +6172,25 @@ namespace eval punk::console {
} }
namespace eval punk::console {
#G-007: mirror every write of the default-console legacy fact variables into tsv so
#non-owner threads read current values (see the console_facts store comments). Installed
#here, at the end of the module, so all legacy fact variables and their namespaces exist.
#Re-source safe: an identical previously-installed trace is removed before adding.
dict for {factkey factinfo} $console_fact_info {
set _legacyvar [dict get $factinfo legacyvar]
set _tracecmd [list ::punk::console::internal::default_fact_write_trace $factkey $_legacyvar]
foreach _tinfo [trace info variable $_legacyvar] {
lassign $_tinfo _top _tcmd
if {$_top eq "write" && $_tcmd eq $_tracecmd} {
trace remove variable $_legacyvar write $_tracecmd
}
}
trace add variable $_legacyvar write $_tracecmd
}
unset -nocomplain factkey factinfo _legacyvar _tracecmd _tinfo _top _tcmd
}
namespace eval ::punk::args::register { namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace #use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punk::console ::punk::console::argdoc ::punk::console::internal ::punk::console::local ::punk::console::ansi ::punk::console::check lappend ::punk::args::register::NAMESPACES ::punk::console ::punk::console::argdoc ::punk::console::internal ::punk::console::local ::punk::console::ansi ::punk::console::check

9
src/modules/punk/console-buildversion.txt

@ -1,6 +1,13 @@
0.5.0 0.6.0
#First line must be a semantic version number #First line must be a semantic version number
#all other lines are ignored. #all other lines are ignored.
#0.6.0 - G-007 slice 1 (facts + ownership): per-console facts store is tsv-backed (shared array punk_console_facts) so all threads of a punk session read the same values; the default console {stdin stdout} keeps the legacy namespace variables as its authoritative local storage with write traces mirroring every write (including direct variable writes) into tsv; non-default consoles store facts only in tsv, keyed with an owner qualifier because non-std channel names are thread-local
#0.6.0 - new console ownership registry: console_owner_register/console_owner_get/console_owner_forget record which thread owns a console (tsv punk_console_owners, keyed by canonical {in out} pair); ownership is captured at anchor time via the new opunk::console lifecycle_callback (wired by ensure_object_integration) and by default_console; consult-time liveness validation clears entries for exited threads; for {stdin stdout} first registration wins and only the owner's forget releases the entry
#0.6.0 - console_fact_set from a thread that does not own the default console forwards the write to the owning thread via thread::send (vt52-alias transport precedent) with a direct tsv fallback; console_fact_get from a non-owner reads the tsv mirror
#0.6.0 - new console_fact_clear proc clears recorded facts for a console (default console facts re-record their module defaults)
#0.6.0 - dec_has_mode/ansi_has_mode caches moved from per-interp dicts to tsv (shared array punk_console_modecache, one entry per {dectype qualifier in out passthrough mode}) - all threads share one cache per console
#0.6.0 - infrastructure tsv arrays renamed with punk_ prefix to avoid collisions with application tsv usage in subshells: console -> punk_console (is_raw; coordinated call-site updates in punk::repl, punk::lib, punk::basictelnet), and the new stores are punk_console_facts/punk_console_owners/punk_console_modecache; mixed loading of pre-0.6.0 and 0.6.0 punk::console in one process would see divergent raw-mode state
#0.6.0 - module is now loadable in secondary threads without ::argv0 (powershell consolemode fallback guarded with info exists)
#0.5.0 - migration complete: the remaining trailing-positional query functions (get_cursor_pos, get_cursor_pos_list, get_checksum_rect, get_device_status, get_device_attributes, get_device_attributes_tertiary, get_terminal_id, get_tabstops, get_tabstop_apparent_width, get_dimensions, get_xterm_size, get_xterm_pixels, dec_get_mode_line_wrap, ansi_get_mode_LNM) converted to the get_size hybrid pattern via new internal::hybrid_console_spec - each accepts the legacy positional console spec or -console <consolespec> (any spec form), with PUNKARGS documentation #0.5.0 - migration complete: the remaining trailing-positional query functions (get_cursor_pos, get_cursor_pos_list, get_checksum_rect, get_device_status, get_device_attributes, get_device_attributes_tertiary, get_terminal_id, get_tabstops, get_tabstop_apparent_width, get_dimensions, get_xterm_size, get_xterm_pixels, dec_get_mode_line_wrap, ansi_get_mode_LNM) converted to the get_size hybrid pattern via new internal::hybrid_console_spec - each accepts the legacy positional console spec or -console <consolespec> (any spec form), with PUNKARGS documentation
#0.5.0 - get_size refactored onto internal::hybrid_console_spec (behaviour unchanged) #0.5.0 - get_size refactored onto internal::hybrid_console_spec (behaviour unchanged)
#0.5.0 - documentation-only PUNKARGS added for input_at_eof, is_input_console_or_tty, size_via_query_mechanisms, console_size_provider and the get_size_using_* mechanism helpers (signatures unchanged - internal size mechanisms remain canonical-pair only) #0.5.0 - documentation-only PUNKARGS added for input_at_eof, is_input_console_or_tty, size_via_query_mechanisms, console_size_provider and the get_size_using_* mechanism helpers (signatures unchanged - internal size mechanisms remain canonical-pair only)

2
src/modules/punk/lib-999999.0a1.0.tm

@ -6706,7 +6706,7 @@ namespace eval punk::lib {
set stdin_state [chan configure stdin] set stdin_state [chan configure stdin]
if {[catch { if {[catch {
package require punk::console package require punk::console
set console_raw [tsv::get console is_raw] set console_raw [tsv::get punk_console is_raw]
} err_console]} { } err_console]} {
#assume normal line mode #assume normal line mode
set console_raw 0 set console_raw 0

3
src/modules/punk/lib-buildversion.txt

@ -1,5 +1,6 @@
0.2.0 0.2.1
#First line must be a semantic version number #First line must be a semantic version number
#all other lines are ignored. #all other lines are ignored.
#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 - new: snapshot_package_paths proc returns a script string reproducing tm list, auto_path, and package prefer for use in thread::create init scripts #0.2.0 - new: snapshot_package_paths proc returns a script string reproducing tm list, auto_path, and package prefer for use in thread::create init scripts
#0.2.0 - extended: interp_sync_package_paths now propagates package prefer and supports optional -libunknown 1 flag for epoch copy + libunknown init #0.2.0 - extended: interp_sync_package_paths now propagates package prefer and supports optional -libunknown 1 flag for epoch copy + libunknown init

16
src/modules/punk/repl-999999.0a1.0.tm

@ -162,7 +162,7 @@ namespace eval repl {
#variable last_unknown "" #variable last_unknown ""
tsv::set repl last_unknown "" tsv::set repl last_unknown ""
tsv::set console is_raw 0 tsv::set punk_console is_raw 0
variable output "" variable output ""
#important not to initialize - as it can be preset by cooperating package before app-punk has been package required #important not to initialize - as it can be preset by cooperating package before app-punk has been package required
#(this is an example of a deaddrop) #(this is an example of a deaddrop)
@ -276,7 +276,7 @@ proc ::punk::repl::init_signal_handlers {} {
flush stderr flush stderr
incr signal_control_c incr signal_control_c
#rputs stderr "* console_control: $args" #rputs stderr "* console_control: $args"
if {[tsv::get console is_raw]} { if {[tsv::get punk_console is_raw]} {
if {[lindex $::errorCode 0] eq "CHILDKILLED"} { if {[lindex $::errorCode 0] eq "CHILDKILLED"} {
#rputs stderr "\n|repl> ctrl-c errorCode: $::errorCode" #rputs stderr "\n|repl> ctrl-c errorCode: $::errorCode"
#avoid spurious triggers after interrupting a command.. #avoid spurious triggers after interrupting a command..
@ -1643,10 +1643,10 @@ proc repl::repl_handler {inputchan readmore prompt_config} {
if {[dict get $original_input_conf -inputmode] eq "raw"} { if {[dict get $original_input_conf -inputmode] eq "raw"} {
#user or script has apparently put stdin into raw mode - update punk::console::is_raw to match #user or script has apparently put stdin into raw mode - update punk::console::is_raw to match
set rawmode 1 set rawmode 1
tsv::set console is_raw 1 tsv::set punk_console is_raw 1
} else { } else {
#set ::punk::console::is_raw 0 #set ::punk::console::is_raw 0
tsv::set console is_raw 0 tsv::set punk_console is_raw 0
} }
#what about enable/disable virtualTerminal ? #what about enable/disable virtualTerminal ?
#using stdin -inputmode to switch modes won't set virtualterminal input state appropriately #using stdin -inputmode to switch modes won't set virtualterminal input state appropriately
@ -1654,7 +1654,7 @@ proc repl::repl_handler {inputchan readmore prompt_config} {
#if it's been set to raw - assume it is deliberately done this way as the user could have alternatively called punk::mode raw or punk::console::enableVirtualTerminal #if it's been set to raw - assume it is deliberately done this way as the user could have alternatively called punk::mode raw or punk::console::enableVirtualTerminal
#by not doing this automatically - we assume the caller has a reason. #by not doing this automatically - we assume the caller has a reason.
} else { } else {
set rawmode [tsv::get console is_raw] set rawmode [tsv::get punk_console is_raw]
} }
if {!$rawmode} { if {!$rawmode} {
@ -2107,7 +2107,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
set infoprompt [dict get $prompt_config infoprompt] set infoprompt [dict get $prompt_config infoprompt]
set debugprompt [dict get $prompt_config debugprompt] set debugprompt [dict get $prompt_config debugprompt]
set rawmode [tsv::get console is_raw] set rawmode [tsv::get punk_console is_raw]
if {!$rawmode} { if {!$rawmode} {
#puts stderr "-->got [ansistring VIEW -lf 1 $stdinlines]<--" #puts stderr "-->got [ansistring VIEW -lf 1 $stdinlines]<--"
@ -2980,7 +2980,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
#after any external command - raw mode as the console sees it can be disabled #after any external command - raw mode as the console sees it can be disabled
#set it to match current state of the tsv #set it to match current state of the tsv
#---------------------------------------------------------------------------- #----------------------------------------------------------------------------
if {[tsv::get console is_raw]} { if {[tsv::get punk_console is_raw]} {
if {$::tcl_platform(platform) eq "windows"} { if {$::tcl_platform(platform) eq "windows"} {
#review #review
#we are in parent process - twapi might not be loaded here - even if it is in the code interp #we are in parent process - twapi might not be loaded here - even if it is in the code interp
@ -3437,7 +3437,7 @@ namespace eval repl {
return [thread::send %replthread% [list punk::console::vt52 {*}$args]] return [thread::send %replthread% [list punk::console::vt52 {*}$args]]
} }
proc mode args { proc mode args {
#with tsv::set console is_raw we don't need to call mode in both the replthread and the codethread #with tsv::set punk_console is_raw we don't need to call mode in both the replthread and the codethread
# REVIEW - call in local interp? how about if codethread is safe interp? # REVIEW - call in local interp? how about if codethread is safe interp?
#interp eval code [list ::punk::console::mode {*}$args] #interp eval code [list ::punk::console::mode {*}$args]
thread::send %replthread% [list punk::console::mode {*}$args] thread::send %replthread% [list punk::console::mode {*}$args]

3
src/modules/punk/repl-buildversion.txt

@ -1,6 +1,7 @@
0.2.0 0.2.1
#First line must be a semantic version number #First line must be a semantic version number
#all other lines are ignored. #all other lines are ignored.
#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 - 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 #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
#0.1.6 - repl::init routes default console construction through punk::console::default_console (also gains the probe-byte store unification) #0.1.6 - repl::init routes default console construction through punk::console::default_console (also gains the probe-byte store unification)

167
src/tests/modules/punk/console/testsuites/console/consolefacts.test

@ -11,7 +11,10 @@ package require opunk::console 999999.0a1.0-
#(is_vt52, tabwidth, cell_size, last_da1_result, grapheme_cluster_support, has_bug_*) are keyed #(is_vt52, tabwidth, cell_size, last_da1_result, grapheme_cluster_support, has_bug_*) are keyed
#by canonical {in out} channel pair; the process-default console {stdin stdout} stores its facts #by canonical {in out} channel pair; the process-default console {stdin stdout} stores its facts
#in the legacy namespace variables so external readers (punk::repl, textblock, overtype, ...) #in the legacy namespace variables so external readers (punk::repl, textblock, overtype, ...)
#keep working. #keep working. The store is tsv-backed (G-007): legacy-variable writes are trace-mirrored to tsv,
#non-default consoles store facts only in tsv (owner-qualified keys), and the console ownership
#registry (console_owner_register/get/forget) decides which thread's legacy variables are
#authoritative for the default console.
namespace eval ::testspace { namespace eval ::testspace {
namespace import ::tcltest::* namespace import ::tcltest::*
@ -79,7 +82,7 @@ namespace eval ::testspace {
}\ }\
-cleanup { -cleanup {
set ::punk::console::tabwidth $saved_tabwidth set ::punk::console::tabwidth $saved_tabwidth
set ::punk::console::console_facts [dict create] punk::console::console_fact_clear {chanx chany}
}\ }\
-result [list\ -result [list\
4\ 4\
@ -98,7 +101,7 @@ namespace eval ::testspace {
}\ }\
-cleanup [string cat { -cleanup [string cat {
catch {opunk::console::forget facttest1} catch {opunk::console::forget facttest1}
set ::punk::console::console_facts [dict create] punk::console::console_fact_clear [list $rd $wr]
} $pipe_cleanup]\ } $pipe_cleanup]\
-result [list\ -result [list\
5\ 5\
@ -132,7 +135,7 @@ namespace eval ::testspace {
-cleanup [string cat { -cleanup [string cat {
#vt52 on/off toggles process-global colour state - restore #vt52 on/off toggles process-global colour state - restore
set ::punk::console::colour_disabled $saved_colour_disabled set ::punk::console::colour_disabled $saved_colour_disabled
set ::punk::console::console_facts [dict create] punk::console::console_fact_clear [list $rd $wr]
} $pipe_cleanup]\ } $pipe_cleanup]\
-result [list\ -result [list\
0\ 0\
@ -169,7 +172,7 @@ namespace eval ::testspace {
set ::env($ev) $val set ::env($ev) $val
} }
set ::punk::console::tabwidth $saved_tabwidth set ::punk::console::tabwidth $saved_tabwidth
set ::punk::console::console_facts [dict create] punk::console::console_fact_clear [list $rd $wr]
} $pipe_cleanup]\ } $pipe_cleanup]\
-result [list\ -result [list\
1\ 1\
@ -185,7 +188,7 @@ namespace eval ::testspace {
lappend result [expr {$::punk::console::cell_size eq ""}] lappend result [expr {$::punk::console::cell_size eq ""}]
}\ }\
-cleanup [string cat { -cleanup [string cat {
set ::punk::console::console_facts [dict create] punk::console::console_fact_clear [list $rd $wr]
} $pipe_cleanup]\ } $pipe_cleanup]\
-result [list\ -result [list\
12x24\ 12x24\
@ -212,11 +215,11 @@ namespace eval ::testspace {
lappend result [expr {$::punk::console::last_da1_result eq $saved_da1}] lappend result [expr {$::punk::console::last_da1_result eq $saved_da1}]
}\ }\
-cleanup { -cleanup {
punk::console::console_fact_clear [list $rdi $wro]
foreach c [list $wri $rdi $wro $rdo] { foreach c [list $wri $rdi $wro $rdo] {
catch {chan close $c} catch {chan close $c}
} }
array unset ::punk::console::input_chunks_waiting $rdi array unset ::punk::console::input_chunks_waiting $rdi
set ::punk::console::console_facts [dict create]
}\ }\
-result [list\ -result [list\
{1;2}\ {1;2}\
@ -243,7 +246,7 @@ namespace eval ::testspace {
}\ }\
-cleanup { -cleanup {
set ::punk::console::check::has_bug_zwsp $saved_zwsp set ::punk::console::check::has_bug_zwsp $saved_zwsp
set ::punk::console::console_facts [dict create] punk::console::console_fact_clear {chanx chany}
}\ }\
-result [list\ -result [list\
1\ 1\
@ -261,12 +264,158 @@ namespace eval ::testspace {
lappend result [read $rd] lappend result [read $rd]
}\ }\
-cleanup [string cat { -cleanup [string cat {
set ::punk::console::console_facts [dict create] punk::console::console_fact_clear [list $rd $wr]
} $pipe_cleanup]\ } $pipe_cleanup]\
-result [list\ -result [list\
{available 1 mode set}\ {available 1 mode set}\
{}\ {}\
] ]
test owner_register_get_forget {ownership registry basics - register/get/forget and stale-owner clearing at consult time}\
-setup [string cat $common {
set workertid [thread::create {thread::wait}]
}]\
-body {
#unregistered console reads as empty (operate locally)
lappend result [punk::console::console_owner_get {chanx chany}]
#register defaults to the calling thread
punk::console::console_owner_register {chanx chany}
lappend result [expr {[punk::console::console_owner_get {chanx chany}] eq [thread::id]}]
punk::console::console_owner_forget {chanx chany}
lappend result [punk::console::console_owner_get {chanx chany}]
#explicit owner thread + liveness validation: entry for an exited thread is
#cleared at consult time
punk::console::console_owner_register {chanx chany} $workertid
lappend result [expr {[punk::console::console_owner_get {chanx chany}] eq $workertid}]
thread::release $workertid
#wait for the thread to actually exit
set deadline [expr {[clock milliseconds] + 2000}]
while {[thread::exists $workertid] && [clock milliseconds] < $deadline} {
after 10
}
lappend result [punk::console::console_owner_get {chanx chany}]
}\
-cleanup {
catch {punk::console::console_owner_forget {chanx chany}}
catch {thread::release $workertid}
}\
-result [list\
{}\
1\
{}\
1\
{}\
]
test owner_anchor_lifecycle {anchoring an opunk::console instance registers the anchoring thread as owner; forget clears the entry}\
-setup [string cat $pipe_setup {
::punk::console::ensure_object_integration
}]\
-body {
opunk::console::create ownertest1 $rd $wr
lappend result [expr {[punk::console::console_owner_get [list $rd $wr]] eq [thread::id]}]
#instance-name spec resolves to the same registry entry
lappend result [expr {[punk::console::console_owner_get ownertest1] eq [thread::id]}]
opunk::console::forget ownertest1
lappend result [punk::console::console_owner_get [list $rd $wr]]
}\
-cleanup [string cat {
catch {opunk::console::forget ownertest1}
catch {punk::console::console_owner_forget [list $rd $wr]}
} $pipe_cleanup]\
-result [list\
1\
1\
{}\
]
test owner_default_console_first_wins {a second context anchoring {stdin stdout} neither steals nor releases the default console's ownership}\
-setup [string cat $common {
::punk::console::ensure_object_integration
set workertid [thread::create {thread::wait}]
}]\
-body {
#another live thread owns the default console
punk::console::console_owner_register {stdin stdout} $workertid
#anchoring a local view of the std channels must not take over ownership
opunk::console::create defaultview stdin stdout
lappend result [expr {[punk::console::console_owner_get {stdin stdout}] eq $workertid}]
#and forgetting that local view must not release the owner's entry
opunk::console::forget defaultview
lappend result [expr {[punk::console::console_owner_get {stdin stdout}] eq $workertid}]
}\
-cleanup {
catch {opunk::console::forget defaultview}
catch {punk::console::console_owner_forget {stdin stdout}}
catch {thread::release $workertid}
}\
-result [list\
1\
1\
]
test facts_cross_thread_visibility {default-console facts set (or legacy variables written directly) in the owning thread are visible from another thread; a non-owner set is forwarded to the owner}\
-setup [string cat $common {
set saved_tabwidth $::punk::console::tabwidth
#this thread is the default console's owner (as the repl thread is in a punk session)
punk::console::console_owner_register {stdin stdout}
set workertid [thread::create {thread::wait}]
#the worker loads punk::console from the same module paths as the testinterp
thread::send $workertid [list set ::tmpaths [tcl::tm::list]]
thread::send $workertid [list set ::auto_path $::auto_path]
thread::send $workertid {
foreach p $::tmpaths {tcl::tm::path add $p}
package prefer latest
package require punk::console 999999.0a1.0-
}
}]\
-body {
#a fact set in the owner is immediately visible in the worker
punk::console::console_fact_set {stdin stdout} tabwidth 11
lappend result [thread::send $workertid {punk::console::console_fact_get {stdin stdout} tabwidth}]
#a direct legacy-variable write (bypassing console_fact_set) is trace-mirrored
set ::punk::console::tabwidth 13
lappend result [thread::send $workertid {punk::console::console_fact_get {stdin stdout} tabwidth}]
#the worker's own hollow legacy variable is untouched by the owner's writes
lappend result [thread::send $workertid {set ::punk::console::tabwidth}]
#a non-owner console_fact_set is forwarded to the owning thread (this thread
#services events in the update poll below, as the repl thread does while a
#codethread runs). thread::send executes in the target thread's MAIN interp: in a
#punk session that is where the owner's punk::console lives, so its legacy
#variables sync - but under runtests this testinterp is a child interp, so the
#forwarded write lands in console_fact_set's fallback tsv-mirror path instead
#(nested-interp routing is the interp-alias layer of G-007, a later slice).
#Assert the invariant that holds in both topologies: after a non-owner set the
#value is in the tsv mirror and every non-owner reader sees it.
tsv::set consolefactstest done 0
thread::send -async $workertid {
punk::console::console_fact_set {stdin stdout} tabwidth 17
tsv::set consolefactstest done 1
}
#poll with deadline so a worker-side error fails the test instead of hanging the run
set deadline [expr {[clock milliseconds] + 15000}]
while {![tsv::get consolefactstest done] && [clock milliseconds] < $deadline} {
update
after 10
}
lappend result [tsv::get consolefactstest done]
lappend result [tsv::get punk_console_facts [list {} stdin stdout tabwidth]]
lappend result [thread::send $workertid {punk::console::console_fact_get {stdin stdout} tabwidth}]
}\
-cleanup {
catch {thread::release $workertid}
set ::punk::console::tabwidth $saved_tabwidth
punk::console::console_owner_forget {stdin stdout}
catch {tsv::unset consolefactstest}
}\
-result [list\
11\
13\
8\
1\
17\
17\
]
} }
tcltest::cleanupTests ;#needed to produce test summary. tcltest::cleanupTests ;#needed to produce test summary.

15
src/tests/modules/punk/console/testsuites/console/consolespec.test

@ -481,8 +481,13 @@ namespace eval ::testspace {
chan configure $wri -buffering none -translation lf chan configure $wri -buffering none -translation lf
chan configure $wro -buffering none -translation lf chan configure $wro -buffering none -translation lf
chan configure $rdo -blocking 0 -translation lf chan configure $rdo -blocking 0 -translation lf
set saved_cache $::punk::console::dec_has_mode_cache #the mode cache lives in tsv keyed {dectype qualifier in out passthrough mode} -
set ::punk::console::dec_has_mode_cache [dict create] #clear any entries for this channel pair (names can be reused across [chan pipe] calls)
foreach k [tsv::array names punk_console_modecache] {
if {[lindex $k 2] eq $rdi && [lindex $k 3] eq $wro} {
catch {tsv::unset punk_console_modecache $k}
}
}
}\ }\
-body { -body {
opunk::console::create cachetest1 $rdi $wro opunk::console::create cachetest1 $rdi $wro
@ -500,7 +505,11 @@ namespace eval ::testspace {
lappend result [read $rdo] lappend result [read $rdo]
}\ }\
-cleanup { -cleanup {
set ::punk::console::dec_has_mode_cache $saved_cache foreach k [tsv::array names punk_console_modecache] {
if {[lindex $k 2] eq $rdi && [lindex $k 3] eq $wro} {
catch {tsv::unset punk_console_modecache $k}
}
}
catch {opunk::console::forget cachetest1} catch {opunk::console::forget cachetest1}
foreach c [list $wri $rdi $wro $rdo] { foreach c [list $wri $rdi $wro $rdo] {
catch {chan close $c} catch {chan close $c}

2
src/tests/modules/punk/console/testsuites/console/emitconsole.test

@ -142,7 +142,7 @@ namespace eval ::testspace {
lappend result [expr {[read_emitted $rd] eq [punk::ansi::cursor_off]}] lappend result [expr {[read_emitted $rd] eq [punk::ansi::cursor_off]}]
}\ }\
-cleanup [string cat { -cleanup [string cat {
set ::punk::console::console_facts [dict create] punk::console::console_fact_clear [list $rd $wr]
} $pipe_cleanup]\ } $pipe_cleanup]\
-result [list\ -result [list\
1\ 1\

Loading…
Cancel
Save