Browse Source

New punk::basictelnet module - still has fast-type misordering bug when mode raw

master
Julian Noble 9 months ago
parent
commit
37f410960f
  1. 62
      src/bootsupport/modules/punk/console-0.1.1.tm
  2. 9
      src/bootsupport/modules/punk/repo-0.1.1.tm
  3. 933
      src/modules/punk/basictelnet-999999.0a1.0.tm
  4. 3
      src/modules/punk/basictelnet-buildversion.txt
  5. 62
      src/modules/punk/console-999999.0a1.0.tm
  6. 9
      src/modules/punk/repo-999999.0a1.0.tm

62
src/bootsupport/modules/punk/console-0.1.1.tm

@ -42,6 +42,10 @@ namespace eval punk::console {
if {![info exists input_chunks_waiting(stdin)]} {
set input_chunks_waiting(stdin) [list]
}
variable ansi_response_chunk ;#array keyed on callid
variable ansi_response_wait ;#array keyed on callid
variable ansi_response_queue ;#list of callids
variable ansi_response_queuedata ;#dict keyed on callid - with function params
# --
variable ansi_available -1 ;#default -1 for unknown. Leave it this way so test for ansi support is run.
@ -520,14 +524,34 @@ namespace eval punk::console {
upvar ::punk::console::ansi_response_chunk accumulator
upvar ::punk::console::ansi_response_wait waitvar
upvar ::punk::console::ansi_response_queue queue
upvar ::punk::console::ansi_response_queuedata queuedata
set accumulator($callid) ""
set waitvar($callid) ""
lappend queue $callid
#todo - use a linked array and an accumulatorid and waitvar id? When can there be more than one terminal query in-flight?
set existing_handler [fileevent $input readable] ;#review!
set this_handler ::punk::console::internal::ansi_response_handler_regex
if {[lindex $existing_handler 0] eq $this_handler} {
puts stderr "[punk::ansi::a+ red]Warning for callid $callid get_ansi_response_payload called while existing ansi response handler in place[a]: $this_handler"
puts stderr "queue state: $queue"
if {[lindex $queue 0] ne $callid} {
while { $waitvar($callid) ne "go_ahead"} {
after 10
set waitvar([lindex $queue 0]) trigger
puts -nonewline stderr "\n[info level 1]\n"
puts -nonewline stderr "<callid:$callid waitvar [array get waitvar]>"
vwait ::punk::console::ansi_response_wait ;#wait on array - not specific element
}
#dict set queuedata $callid [list $query $capturingendregex $inputchannels]
}
}
fileevent $input readable {}
set input_state [fconfigure $input]
@ -542,10 +566,6 @@ namespace eval punk::console {
}
fconfigure $input -blocking 0
#
set this_handler ::punk::console::internal::ansi_response_handler_regex
if {[lindex $existing_handler 0] eq $this_handler} {
puts stderr "[punk::ansi::a+ red]Warning get_ansi_response_payload called while existing ansi response handler in place[a]: $this_handler"
}
#in handler - its used for a boolean match (capturing aspect not used)
fileevent $input readable [list $this_handler $input $callid $capturingendregex]
@ -564,7 +584,8 @@ namespace eval punk::console {
#todo - make timeout configurable?
set waitvarname "::punk::console::ansi_response_wait($callid)"
set cancel_timeout_id [after 500 [list set $waitvarname timedout]]
#500ms is generally plenty for a terminal to respond.. but not in some cases. e.g event loop busy with stdin keypress?? review
set cancel_timeout_id [after 1000 [list set $waitvarname timedout]]
if {[set waitvar($callid)] eq ""} {
vwait ::punk::console::ansi_response_wait($callid)
@ -611,17 +632,33 @@ namespace eval punk::console {
#is there a way to know if existing_handler is input_chunks_waiting aware?
if {[string length $existing_handler] && [lindex $existing_handler 0] ne $this_handler} {
#puts "get_ansi_response_paylaod reinstalling ------>$existing_handler<------"
#puts "get_ansi_response_payload reinstalling ------>$existing_handler<------"
fileevent $input readable $existing_handler
#we may have consumed all pending input on $input - so there may be no trigger for the readable fileevent
if {[llength $input_chunks_waiting($input)]} {
#This is experimental If a handler is aware of input_chunks_waiting - there should be no need to schedule a trigger
#If it isn't, but the handler can accept an existing chunk of data as an argument - we could trigger and pass it the waiting chunks - but there's no way to know its API.
#If it isn't, but the handler can accept an existing chunk of data as a 'waiting' argument - we could trigger and pass it the waiting chunks - but there's no way to know its API.
#we could look at info args - but that's not likely to tell us much in a robust way.
#we could create a reflected channel for stdin? That is potentially an overreach..?
#triggering it manually... as it was already listening - this should generally do no harm as it was the active reader anyway, but won't help with the missing data if it's input_chunks_waiting-unaware.
puts stderr "[punk::ansi::a+ yellow bold]-->punk::console::get_ansi_response_payload triggering existing handler while over-read data is in punk::console::input_chunks_waiting($input) instead of channel [ansistring VIEW $input_chunks_waiting($input)][punk::ansi::a]"
after idle [list after 0 $existing_handler]
set handler_args [info args [lindex $existing_handler 0]]
if {[lindex $handler_args end] eq "waiting"} {
#Looks like the existing handler is setup for punk repl cooperation.
puts stderr "[punk::ansi::a+ yellow bold]-->punk::console::get_ansi_response_payload triggering existing handler $existing_handler while over-read data is in punk::console::input_chunks_waiting($input) instead of channel[punk::ansi::a]"
puts stderr "[punk::ansi::a+ yellow bold]-->waiting: [ansistring VIEW $input_chunks_waiting($input)][punk::ansi::a]"
flush stderr
#FIX - this doesn't work. Fast typing gets out of order!!!!
#concat and supply to existing handler in single text block - review
set waitingdata [join $input_chunks_waiting($input) ""]
set input_chunks_waiting($input) [list]
after idle [list after 0 [list {*}$existing_handler $waitingdata]]
unset waitingdata
} else {
#! todo? for now, emit a clue as to what's happening.
puts stderr "[punk::ansi::a+ yellow bold]-->punk::console::get_ansi_response_payload cannot trigger existing handler $existing_handler while over-read data is in punk::console::input_chunks_waiting($input) instead of channel [ansistring VIEW $input_chunks_waiting($input)][punk::ansi::a]"
}
}
#Note - we still may be in_repl_handler here (which disables its own reader while executing commandlines)
#The input_chunks_waiting may really belong to the existing_handler we found - but if it doesn't consume them they will end up being read by the repl_handler when it eventually re-enables.
@ -638,7 +675,14 @@ namespace eval punk::console {
catch {
unset accumulator($callid)
unset waitvar($callid)
dict unset queuedata $callid
}
if {[llength $queue] > 1} {
set next_callid [lindex $queue 1]
set waitvar($callid) go_ahead
}
lpop queue 0
#set punk::console::chunk ""
return $payload

9
src/bootsupport/modules/punk/repo-0.1.1.tm

@ -242,7 +242,8 @@ namespace eval punk::repo {
if {$path eq {}} { set path [pwd] }
expr {[is_fossil_root $path] || [is_git_root $path]}
}
#require a minimum of /src and /modules|lib|scriptapps|*.vfs - and that it's otherwise sensible
#require a minimum of src and src/modules|src/scriptapps|src/*/*.vfs - and that it's otherwise sensible
#we still run a high chance of picking up unintended candidates - but hopefully it's a reasonable balance.
proc is_candidate_root {{path {}}} {
if {$path eq {}} { set path [pwd] }
if {[file pathtype $path] eq "relative"} {
@ -250,7 +251,8 @@ namespace eval punk::repo {
} else {
set normpath $path
}
set unwise_paths [list "/" "/usr/local" "/usr/local/bin" "/usr/local/lib" "c:/windows"]
#we're not pickup all possible unwise paths - mainly ones that are likely to be above us, or some that are probably just really bad ideas.
set unwise_paths [list "/" "/dev" "/bin" "/root" "/etc" "/opt" "/usr" "/usr/local" "/usr/local/bin" "/usr/local/lib" "c:/windows"]
if {[string tolower $normpath] in $unwise_paths} {
return 0
}
@ -268,7 +270,8 @@ namespace eval punk::repo {
}
set src_subs [glob -nocomplain -dir $path/src -types d -tail *]
if {"modules" in $src_subs || "lib" in $src_subs || "scriptapps" in $src_subs} {
#test for $path/src/lib is too common to be a useful indicator
if {"modules" in $src_subs || "scriptapps" in $src_subs} {
return 1
}
foreach sub $src_subs {

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

@ -0,0 +1,933 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from <pkg>-buildversion.txt
# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2024
#
# @@ Meta Begin
# Application punk::basictelnet 999999.0a1.0
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin shellspy_module_punk::basictelnet 0 999999.0a1.0]
#[copyright "2024"]
#[titledesc {basic telnet client - DKF/Wiki}] [comment {-- Name section and table of contents description --}]
#[moddesc {basic telnet client}] [comment {-- Description at end of page heading --}]
#[require punk::basictelnet]
#[keywords module]
#[description]
#[para] see https://wiki.tcl-lang.org/page/Tcl+Telnet
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Overview]
#[para] overview of punk::basictelnet
#[subsection Concepts]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection dependencies]
#[para] packages used by punk::basictelnet
#[list_begin itemized]
package require Tcl 8.6
#*** !doctools
#[item] [package {Tcl 8.6}]
# #package require frobz
# #*** !doctools
# #[item] [package {frobz}]
#*** !doctools
#[list_end]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section API]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# oo::class namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punk::basictelnet::class {
#*** !doctools
#[subsection {Namespace punk::basictelnet::class}]
#[para] class definitions
if {[info commands [namespace current]::interface_sample1] eq ""} {
#*** !doctools
#[list_begin enumerated]
# oo::class create interface_sample1 {
# #*** !doctools
# #[enum] CLASS [class interface_sample1]
# #[list_begin definitions]
# method test {arg1} {
# #*** !doctools
# #[call class::interface_sample1 [method test] [arg arg1]]
# #[para] test method
# puts "test: $arg1"
# }
# #*** !doctools
# #[list_end] [comment {-- end definitions interface_sample1}]
# }
#*** !doctools
#[list_end] [comment {--- end class enumeration ---}]
}
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Base namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punk::basictelnet {
namespace export *
#todo - use these as defaults - provide a way to configure/listen to local events and notify server
set window_cols 80
set window_rows 25
variable debug
set debug 0
proc debug {{on_off ""}} {
variable debug
if {$on_off eq ""} {
return $debug
}
if {![string is boolean -strict $on_off]} {
error "punk::basictelnet::debug on_off must be empty string to query, or a boolean value"
}
set debug [expr {$on_off}]
}
variable can_debug
set can_debug 1
if {[catch {
package require textblock
package require punk::lib
package require punk::ansi
package require punk::char
package require punk::console
package require overtype
#and whatever these depend on!
} errMsg]} {
set can_debug 0
}
#variable terminal_type "ANSI"
variable terminal_type "VT100"
#try: DUMB,ANSI,VT100,XTERM
#see also the Mud Terminal Type Standard as an extensiont to RFC1091 Telenet Terminal-Type
variable remote_terminal_type
set remote_terminal_type "" ;#emtpy until/unless reported via an option 24 send from the remote
#*** !doctools
#[subsection {Namespace punk::basictelnet}]
#[para] Core API functions for punk::basictelnet
#[list_begin definitions]
variable optioncodes
dict set optioncodes 0 [list name "Binary Transmission"]
dict set optioncodes 1 [list name "Echo"]
dict set optioncodes 2 [list name "Reconnection"]
dict set optioncodes 3 [list name "Suppress Go Ahead"]
dict set optioncodes 4 [list name "Approx Message Size Negotiation"]
dict set optioncodes 5 [list name "Status"] ;#RFC 859
dict set optioncodes 6 [list name "Timing Mark"]
dict set optioncodes 7 [list name "Remote Controlled Trans and Echo"]
dict set optioncodes 8 [list name "Output Line Width"]
dict set optioncodes 9 [list name "Output Page Size"]
dict set optioncodes 10 [list name "Output Carriage-Return Disposition"]
dict set optioncodes 11 [list name "Output Horizontal Tab Stops"]
dict set optioncodes 12 [list name "Output Horizontal Tab Disposition"]
dict set optioncodes 13 [list name "Output Formfeed Disposition"]
dict set optioncodes 14 [list name "Output Vertical Tabstops"]
dict set optioncodes 15 [list name "Output Vertical Tab Disposition"]
dict set optioncodes 16 [list name "Output Linefeed Disposition"]
dict set optioncodes 17 [list name "Extended Ascii"]
dict set optioncodes 18 [list name "Logout"]
dict set optioncodes 19 [list name "Byte Macro"]
dict set optioncodes 20 [list name "Data Entry Terminal"]
dict set optioncodes 21 [list name "SUPDUP"]
dict set optioncodes 22 [list name "SUPDUP Output"]
dict set optioncodes 23 [list name "Send Location"]
dict set optioncodes 24 [list name "Terminal Type"]
dict set optioncodes 25 [list name "End of Record"]
dict set optioncodes 26 [list name "TACACS User Identification"]
dict set optioncodes 27 [list name "Output Marking"]
dict set optioncodes 28 [list name "Terminal Location Number"]
dict set optioncodes 29 [list name "Telnet 3270 Regime"]
dict set optioncodes 30 [list name "X.3 PAD"]
dict set optioncodes 31 [list name "Negotiate About Window Size"] ;#RFC 1073 NAWS
dict set optioncodes 32 [list name "Terminal Speed"] ;#rfc 1079
dict set optioncodes 33 [list name "Remote Flow Control"]
dict set optioncodes 34 [list name "Line Mode"]
dict set optioncodes 35 [list name "X Display Location"]
dict set optioncodes 36 [list name "Environment Option"]
dict set optioncodes 37 [list name "Authentication Option"]
dict set optioncodes 38 [list name "Encryption Option"]
dict set optioncodes 39 [list name "New Environment Option"]
dict set optioncodes 40 [list name "TN3270E"]
dict set optioncodes 41 [list name "XAUTH"]
dict set optioncodes 42 [list name "CHARSET"]
dict set optioncodes 43 [list name "Telnet Remote Serial Port (RSP)"]
dict set optioncodes 44 [list name "Com Port Control Option"]
dict set optioncodes 45 [list name "Telnet Supress Local Echo"]
dict set optioncodes 46 [list name "Telnet Start TLS"]
dict set optioncodes 47 [list name "KERMIT"]
dict set optioncodes 48 [list name "SEND-URL"]
dict set optioncodes 49 [list name "FORWARD_X"]
#50-137 Unassigned
#dict set optioncodes 70 [list name "?"] ;#trekmush
dict set optioncodes 138 [list name "TELOPT PRAGMA LOGON"]
dict set optioncodes 139 [list name "TELOPT SSPI LOGON"]
dict set optioncodes 140 [list name "TELOPT PRAGMA HEARTBEAT"]
#141-254 Unassigned
#dict set optioncodes 201 [list name "?"] ;#trekmush
dict set optioncodes 255 [list name "Extended-Options-List"]
#we are assuming we initiated the connection, and are in some sense the 'client'
variable server_option_state
variable client_option_state
#not all these will make sense as a boolean? review.
#we use this also to support the Status option
#this structure doesn't retain which side initiated - but it appears from the nature of the protocol that isn't always determinable or of importance.
dict for {k _v} $optioncodes {
dict set server_option_state $k 0 ;#DO from our perspective
dict set client_option_state $k 0 ;#WILL from our perspective
}
# -----------------------------------
# A rudimentary hardcoded configuration for options/negotiation
# The way in which features are enabled/disabled and what goes together needs refinement & better understanding
# todo - review
#Note: further logic required, for example even something as supposedly simple as echo shouldn't be active on both ends at once or we get a loop.
# Can't necessarily rely on other end not to allow us to do something insane.
# Probably also.. some options should be under direct user ability to initiate/control - not just a configuration
# For that to work fully we may need a separate punk::telnet package that has a pseudoterminal in front of the real console (scrolling sub-area), allowing a custom repl, custom status display etc.
# We will keep punk::basictelnet as a simple interface using the real terminal - as a fallback for diagnosing oddities etc.
# -----------------------------------
#Passively enabled server features - ie those we don't initiate but will accept
#default response to WILL is WON'T
#define our positive responses here for those that we will do
variable respond_will_do
set respond_will_do [list]
lappend respond_will_do 0 ;#binary
lappend respond_will_do 1 ;#echo
lappend respond_will_do 3 ;#suppress go-ahead
lappend respond_will_do 5 ;#status - by agreeing to this we should be able to read unsolicited "IAC SB STATUS IS ... IAC SE" reports and compare to our perception of state. (and do something if mismatches?)
lappend respond_will_do 24 ;#remote is letting us know they are willing to send terminal-type - but we would still have to request it
#passively enabled client features - requests for our own behaviours we will respond positively
variable respond_do_will
set respond_do_will [list]
lappend respond_do_will 0 ;#binary
lappend respond_do_will 3 ;#Suppress go-ahead
lappend respond_do_will 5 ;#status - by agreeing to this - we need to handle the subnegotiation "IAC SB STATUS SEND IAC SE" and respond with "IAC SB STATUS IS ... IAC SE"
lappend respond_do_will 24 ;#terminal-type
lappend respond_do_will 31 ;#window size - for now we will just immediately respond to a server's DO 31 with our window size
#ACTIVE server features - those we attempt to initiate regarding the server's behaviour
variable initiate_do
set initiate_do [list]
variable initiate_will
set initiate_will [list]
# -----------------------------------
variable in_sb ;#whether we are in subnegotiation parameters and waiting for SE e.g for STATUS we may get multiple or evan all other codes as a report of the other side's perception of the option states.
set in_sb 0
variable sb_state [dict create]
#proc sample1 {p1 n args} {
# #*** !doctools
# #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]]
# #[para]Description of sample1
# #[para] Arguments:
# # [list_begin arguments]
# # [arg_def tring p1] A description of string argument p1.
# # [arg_def integer n] A description of integer argument n.
# # [list_end]
# return "ok"
#}
# ---------------------
#window height/width
proc int_to_2telnetbytes {int} {
set binstr [format %16.16b $int]
set a [string range $binstr 0 7]
set b [string range $binstr 8 end]
return [binary format B8B8 $a $b]
}
#double up \xff as per Telnet protocol. ie double the IAC byte to send as literal
proc int_to_telnetbytes {int} {
set out ""
set 2bytes [int_to_2telnetbytes $int]
foreach b [split $2bytes ""] {
if {$b eq "\xFF"} {
append out \xff\xff
} else {
append out $b
}
}
return $out
}
proc sb_send_window_size {sock cols rows} {
#IAC SB NAWS c1 c2 r1 r2 IAC SE - where c1 c2 are 2 bytes for cols, r1 r2 are 2 bytes for rows - (unless any of the bytes is \xFF in which case it's doubled-up as per telnet protocol)
set c_bytes [int_to_telnetbytes $cols] ;#int_to_telnetbytes does the \xFF double-up if needed
set r_bytes [int_to_telnetbytes $rows]
puts -nonewline $sock \xff\xfa[binary format c 31]${c_bytes}${r_bytes}\xFF\xf0
}
# ---------------------
variable debug_buffer
set debug_buffer ""
variable writing_debug_frame 0 ;#re-entrancy protection
#experiment
proc debug_frame {info} {
variable writing_debug_frame
if {$writing_debug_frame > 1} {
if {$writing_debug_frame >= 3} {
puts stderr "Warning - writing_debug_frame=$writing_debug_frame"
}
return
} elseif {$writing_debug_frame == 1} {
incr writing_debug_frame
after 1 {punk::basictelnet::add_debug ""}
return
}
incr writing_debug_frame
variable debug
variable can_debug ;#we'll only support debug if we can use the punk ansi frame mechanism
#The frame mechanism isn't as good as a proper split-screen as it redraws on rhs and looks bad in scrollback - but it's better than putting debug output on lhs in with data
if {!$can_debug || !$debug} {return}
set infoframe [textblock::frame -width 80 -ansiborder [a+ green bold] -title "[a cyan]Telnet Debug[a]" $info]
set w [textblock::width $infoframe]
set spacepatch [textblock::block $w 4 " "]
puts -nonewline [punk::ansi::cursor_off]
#use non cursorsave version - slower - but less likely to interfere with cursor operations in data
#punk::console::move_emitblock_return 6 90 $spacepatch\n$infoframe
punk::console::move_emitblock_return 6 90 $spacepatch
punk::console::move_emitblock_return 10 90 $infoframe
puts -nonewline [punk::ansi::cursor_on]
#todo - try? finally?
set writing_debug_frame 0
return
}
proc add_debug {newlines} {
variable debug
variable can_debug
variable debug_buffer
if {!$can_debug} {return}
append debug_buffer $newlines
set lines [split $debug_buffer \n]
set lines [lrange $lines end-40 end]
set debug_buffer [join $lines \n]
if {[string length $debug_buffer] && $debug} {
debug_frame $debug_buffer
}
}
proc telnet {{server localhost} {port telnet}} {
set sock [socket $server $port]
#fconfigure $sock -buffering none -blocking 0 -encoding binary -translation crlf -eofchar {}
fconfigure $sock -buffering none -blocking 0 -encoding binary -translation binary -eofchar {}
fconfigure stdout -buffering none
#fileevent $sock readable [list initEvents $sock]
fileevent $sock readable [list [namespace current]::fromServer $sock]
chan configure stdin -blocking 0
fileevent stdin readable [list [namespace current]::toServer $sock]
global closed
vwait closed($sock)
unset closed($sock)
chan conf stdin -blocking 1
}
proc initEvents {sock} {
puts -nonewline [read $sock 4096]
fileevent $sock readable [list [namespace current]::fromServer $sock]
fileevent stdin readable [list [namespace current]::toServer $sock]
}
#specifically named 'waiting' argument as last argument for cooperative input reading with other punk channel handlers (repl in particular)
#waiting data will be supplied to this handler if the other handler over-read (e.g repl handling ANSI ESC \[6n response on stdin finding other data before the ANSI response.)
proc toServer {sock {waiting ""}} {
set line $waiting
if {[string length [append line [read stdin]]] >= 0} {
# - this mechanism is a heuristic rather than a proper determination of the situation. review
if {[string first \r $line] >=0} {
#assuming terminal newline is <CR> (usual case in raw mode)
if {[string first \r\n $line] < 0} {
#only map it if we don't already see \r\n present
set line [string map [list \r \r\n ] $line]
}
} else {
#presuming cooked mode
set line [string map [list \n \r\n] $line]
}
# - review
after 1 [::punk::basictelnet::add_debug "[a+ Yellow black]from stdin sending: [ansistring VIEW -lf 1 -vt 1 $line][a]\n"]
puts -nonewline $sock $line
flush $sock
update idletasks
} else {
disconnect $sock
}
}
proc toServer1 {sock} {
if {[gets stdin line] >= 0} {
puts -nonewline $sock $line\r\n
} else {
disconnect $sock
}
}
proc fromServer {sock} {
variable in_sb
set data x
while {[string length $data]} {
if {[catch {
set data [read $sock 4096]
} errM]} {
catch {disconnect $sock}
add_debug "[a+ red]socket read fail: $errM[a]\n"
return
}
if {[eof $sock]} {
disconnect $sock
return
}
#puts "1----------------------------------"
#puts [ansistring VIEW -lf 1 -vt 1 $data]
#mini debug buffer for each fromServer call - render using add_debug each loop
set debug_info ""
append debug_info "------raw data----------------------------" \n
append debug_info [ansistring VIEW -lf 1 -vt 1 [encoding convertfrom utf-8 $data]] \n
append debug_info "------------------------------------------" \n
if {[string length $data]} {
while 1 {
if {!$in_sb} {
#\xff 255 is the IAC Data Byte (Interpret As Command)
set idx [string first \xff $data]
if {$idx < 0} {
append debug_info "[a+ green]<response has no telnet commands string length data:[string length $data]>[a]" \n
if {[string length $data] == 1} {
append debug_info "SINGLE CHAR: [scan $data %c]" \n
}
after 1 [punk::basictelnet::add_debug $debug_info]
set debug_info ""
break
}
#write [string range $data 0 $idx-1]
puts -nonewline stdout [encoding convertfrom utf-8 [string range $data 0 $idx-1]]
set byte [string index $data [expr {$idx+1}]]
incr idx 2
if {$byte < "\xef"} {
#??
write \xf0$byte
set data [string range $data $idx end]
} elseif {$byte == "\xff"} {
#??
write \xf0
set data [string range $data $idx end]
} else {
set ophex ""
#telnet commands are at least 2 bytes
binary scan $byte H2 cmdhex
switch -- $cmdhex {
fb - fc - fd - fe {
#WILL, WON'T, DO, DON'T
#3bytes - last is option
set opbyte [string index $data [expr {$idx}]]
#don't incr idx - protocol will do so
#incr idx
binary scan $opbyte H2 ophex
}
fa {
#SB
#3 bytes +
#better handled in protocol - set flag to indicate next data expected is optiondata + IAC SE ?
#SB - ended by IAC SE (\xff \xf0)
set sb_posn [string first \xff\xf0 $data]
#no guarantee our read-chunk didn't split before corresponding SE!
#but then.. no guarantee our loop doesn't split after IAC either - need fromserver loop redesign to allow requeuing data?
if {$sb_posn < 0} {
puts stderr "SB missing terminating SE - loop programming incomplete - TODO"
} else {
}
set opbyte [string index $data [expr {$idx}]]
binary scan $opbyte H2 ophex
}
default {
}
}
protocol $sock $cmdhex $ophex
set data [string range $data $idx end]
}
} else {
#in_sb
#can we get carriage-returns mixed in? seems possible..
set byte [string index $data 0]
binary scan $byte H2 bytehex
#byte may be IAC or cmd such as DO,WILL etc (e.g for status cmd it will list bytes as something like DO opt1 WILL opt2 before trailing IAC SE)
switch -- $bytehex {
ff {
#expecting SE next - but will pass to protocol as if it's the 'cmd' for handling/verification
set expectedSE [string index $data 1]
binary scan $expectedSE H2 expectedSEhex
protocol $sock $expectedSEhex ""
}
default {
set opbyte [string index $data 1]
binary scan $opbyte H2 ophex
protocol $sock $bytehex $ophex
}
}
}
}
#puts -nonewline stdout $data
puts -nonewline stdout "[encoding convertfrom utf-8 $data]"
}
}
}
proc disconnect {sock} {
global closed
close $sock
set closed($sock) 1
}
proc write string {
puts -nonewline stdout "write:'[ansistring VIEW [encoding convertfrom iso8859-1 $string]]'"
#puts -nonewline stdout [encoding convertfrom utf-8 $string]
}
proc cmd_info {cmd} {
#ef - extension to rfc-854
set cmdmap [dict create\
ef [list name EOR code 239 meaning "End-of-Record"]\
f0 [list name SE code 240 meaning "End of subnegotiation parameters"]\
f1 [list name NOP code 241 meaning "no-op"]\
f2 [list name "Data Mark" code 242 meaning "The data stream portion of a Synch"]\
f3 [list name "Break" code 243 meaning "NVT character BRK"]\
f4 [list name "Interrupt Process" code 244 meaning "The function IP"]\
f5 [list name "Abort Output" code 245 meaning "The function AO"]\
f6 [list name "Are You There" code 246 meaning "The function AYT"]\
f7 [list name "Erase Character" code 247 meaning "The function EC"]\
f8 [list name "Erase Line" code 248 meaning "The function EL"]\
f9 [list name "Go Ahead" code 249 meaning "The GA signal"]\
fa [list name "SB" code 250 meaning "Indicates that what follows is subnegotiation of the indicated option"]\
fb [list name "WILL" code 251 meaning "Indicates the desire to begin performing, or confimation that you are now performing, the indicated option"]\
fc [list name "WON'T" code 252 meaning "Indicates the refusal to peform or continue performing, the indicated option"]\
fd [list name "DO" code 253 meaning "Indicates the request that the other party perform, or confirmation that you are expecting the other party to perform, the indicated option"]\
fe [list name "DON'T" code 254 meaning "Indicates the demand that the other party stop performaing, or confirmation that you are no longer expecting the other party to perform, the indicated option"]\
]
if {[dict exists $cmdmap $cmd]} {
return [dict get $cmdmap $cmd]
} else {
return "unknown cmd :$cmd"
}
}
proc protocol {sock cmdhex ophex} {
variable in_sb
variable sb_state
variable optioncodes
variable respond_will_do
variable respond_do_will
variable client_option_state ;#WILLs
variable server_option_state ;#DOs
upvar 1 debug_info debug_info
upvar 1 data data idx idx
set opdec ""
if {$ophex ne ""} {
set opdec [scan $ophex %x]
}
flush stderr
if {!$in_sb} {
append debug_info "cmdhex:$cmdhex [cmd_info $cmdhex]" \n
if {[dict exists $optioncodes $opdec]} {
append debug_info "option:$opdec [dict get $optioncodes $opdec]" \n
} else {
append debug_info "unrecognised option: $opdec" \n
}
flush stderr
switch $cmdhex {
f0 {# SE - End of subnegoatiation parameters 240
#error to get when not in sb?
puts stderr "Unexpected SE. We don't appear to be in SB!"
flush stderr
}
f1 {# NOP 241
return
}
f2 {# DATA MARK 242
}
f3 {# BRK 243
}
f4 {# IP - Interrupt Process 244
}
f5 {# AO - Abort Output 245
}
f6 {# AYT - Are you there 246
#return something screen visible
puts $sock {[YES]}
flush $sock
}
f7 {# EC - Erase Character 247
write \u007f
}
f8 {# EL - Erase Line 248
write \u0019
}
f9 {# GA - Go Ahead 249
append debug_info ">>> Received Go Ahead <<<" \n
}
fa {# SB - Subnegotiation 250
# Should search forward for IAC SE (\xff\xf0) - or use in_sb to keep reading
set in_sb 1
if {[dict get $client_option_state $opdec] || [dict get $server_option_state $opdec]} {
incr idx
#action for many subnegotiations is SEND=1 or IS=0
set actionbyte [string index $data $idx]
set actiondec [scan $actionbyte %c]
incr idx ;#for action
switch -- $opdec {
5 {
#status
switch -- $actiondec {
0 {
#IS
#we should only get these reports if status is in our DO list
#keep in_sb as 1 and initialise sb_state
dict set sb_state opdec $opdec
dict set sb_state actiondec 0
dict set sb_state data [dict create]
}
1 {
#SEND
#we should only get a request to send status if it is in our WILL list
#expect the IAC SE to immediately follow
if {[string range $data $idx $idx+1] ne "\xff\xf0"} {
error "malformed send status request"
}
incr idx 2
if {![dict get $client_option_state $opdec]} {
#ignore
puts stderr "Warning received status request - but it is not in our WILL list"
} else {
#build a list of WILLs and DOs and reply with status report
#e.g (no linebreaks/spaces implied)
#IAC SB STATUS IS
#WILL ECHO
#DO SUPPRESS-GO-AHEAD
#WILL STATUS
#DO STATUS
#IAC SE
set report \xff\xfa\x05\x00 ;#IAC SB STATUS IS
dict for {optdec state} $client_option_state {
if {$state} {
append report \xfb[format %c $optdec] ;#WILL
}
}
dict for {optdec state} $server_option_state {
if {$state} {
append report \xfd[format %c $optdec] ;#DO
}
}
append report \xff\xf0 ;#IAC SE
append debug_info "Sent status report" \n
#puts -nonewline $sock $report\r\n ;#newline or not?
puts -nonewline $sock $report
flush $sock
}
#The received subnegotiation is over
set in_sb 0
}
default {
#unknown/unsupported
}
}
}
24 {
#terminal-type
switch -- $actiondec {
0 {
#IS
#we should only get these reports if status is in our DO list
#as maximum
set nextSE [string first \xff\xf0 $data]
if {$nextSE > 0} {
set remote_terminal_type [string range $data $idx $nextSE-1]
set idx [expr {$nextSE+2}]
} else {
#could presumably happen.. todo
error "didn't receive terminal-type in single chunk - review code"
}
##keep in_sb as 1 and initialise sb_state
#dict set sb_state opdec $opdec
#dict set sb_state actiondec 0
#dict set sb_state data [dict create]
}
1 {
#SEND
#we should only get a request to send status if it is in our WILL list
#expect the IAC SE to immediately follow
if {[string range $data $idx $idx+1] ne "\xff\xf0"} {
error "malformed send status request"
}
incr idx 2
if {![dict get $client_option_state $opdec]} {
#ignore
puts stderr "Warning received terminal-type SB request - but it is not in our WILL list"
} else {
variable terminal_type
set report \xff\xfa\x18\x00 ;#IAC SB TERMINAL-TYPE IS
append report $terminal_type
append report \xff\xf0 ;#IAC SE
#debug
append debug_info "Sent terminal-type [ansistring VIEW $report\r\n]" \n
#puts -nonewline $sock $report\r\n ;#newline or not?
puts -nonewline $sock $report
flush $sock
}
#The received subnegotiation is over
set in_sb 0
}
}
}
default {
#if we've responded positively to supporting the option - it should have a switch-arm here
error "No switch handler for option '$opdec' [dict get $optioncodes $opdec]"
}
}
} else {
#an attempt to subnegotiate an item we haven't agreed upon?
puts stderr "Bad SB subnegotiation for operation [dict get $optioncodes $opdec] - not in our WILL list!"
#todo - ignore?
#we shouldn't get here if we are properly in sync with a well-behaved partner
#if we do however.. we need to either abort immediately.. or ignore the subnegotiation by skipping ahead to SE as it may not even be an SB structure we understand.
#let's try the ignore option first..
set next_SE [string first \xff\xf0 $data]
if {$next_SE >=0} {
set idx [expr {$next_SE +2}]
set in_sb 0
} else {
#unrecoverable?
error "Unable to find ending SE for bad SB!"
}
}
}
fb {# WILL - 251
variable respond_will_do
set byte [string index $data $idx]
if {$opdec in $respond_will_do} {
if {[dict get $server_option_state $opdec]} {
#already known DO
} else {
puts -nonewline $sock \xff\xfd$byte ;#respond DO
dict set server_option_state $opdec 1
}
} else {
# Attempt to negotiate; refuse!
puts -nonewline $sock \xff\xfe$byte ;#respond DON'T
}
flush $sock
incr idx
}
fc {# WON'T - 252
dict set server_option_state $opdec 0
incr idx
}
fd {# DO - 253
variable respond_do_will
set byte [string index $data $idx]
if {$opdec in $respond_do_will} {
if {[dict get $client_option_state $opdec]} {
#already stored WILL
} else {
append debug_info ">>>responding to DO. WILL $opdec [dict get $optioncodes $opdec]" \n
puts -nonewline $sock \xff\xfb$byte ;#respond WILL
dict set client_option_state $opdec 1
#immediate followup for NAWS (negotiate about window size)
flush $sock
if {$opdec == 31} {
#IAC SB NAWS c1 c2 r1 r2 IAC SE - where c1 c2 are 2 bytes for cols, r1 r2 are 2 bytes for rows - (unless any of the bytes is \xFF in which case it's doubled-up as per telnet protocol)
append debug_info ">>> sending window_size $::punk::basictelnet::window_cols $::punk::basictelnet::window_rows"
sb_send_window_size $sock $::punk::basictelnet::window_cols $::punk::basictelnet::window_rows
}
}
} else {
# Attempt to negotiate; refuse!
puts -nonewline $sock \xff\xfc$byte
}
flush $sock
incr idx
}
fe {# DON'T - 254
incr idx
}
}
} else {
#in_sb
set state_opdec [dict get $sb_state opdec]
if {$cmdhex eq "\xf0"} {
#this indicates an IAC SE sequence was received
#finished SB - do something with the gathered data
switch -- $state_opdec {
5 {
set reported_state [dict get $sb_state data]
#todo - compare our stored state with the report
#we assume report has all the will and do entries - so we enumerate the server_option_state and client_option_state entries to make sure we notice if there are extras
#these are DO from our perspective - but reported as WILL from perspective of the other end
set mismatches [list]
dict for {opt state} $server_option_state {
if {$state} {
if {![dict exists $reported_state will $opt]} {
lappend mismatches [list server $opt reported DON'T stored DO]
}
} else {
if {[dict exists $reported_state will $opt]} {
lappend mismatches [list server $opt reported DO stored DON'T]
}
}
}
#these are WILL from our perspective - but reported as DO from perspective of the other end
dict for {opt state} $client_option_state {
if {$state} {
if {![dict exists $reported_state do $opt]} {
lappend mismatches [list client $opt reported WON'T stored WILL]
}
} else {
if {[dict exists $reported_state do $opt]} {
lappend mismatches [list client $opt reported WILL stored WON'T]
}
}
}
if {[llength $mismatches]} {
puts stderr "Apparent mismatch in stored DO/WILL vs status report from server"
puts stderr "$mismatches"
#todo - what?
} else {
append debug_info "Server status report matches stored values" \n
}
}
}
set in_sb 0
set sb_state [dict create]
} else {
#in progress - some subelement of the SB
switch -- $state_opdec {
5 {
#only supported in_sb action is 0 - read the status report from the other side
append debug_info "Got status report element [cmd_info $cmdhex]" \n
set existing_data [dict get $sb_state data] ;#data is specific to each type of subnegotiation
set opdec [scan $ophex %x]
switch -- $cmdhex {
fb {
dict set existing_data will $opdec 1
}
fd {
dict set existing_data do $opdec 1
}
}
dict set sb_state data $existing_data ;#updated
}
}
}
}
}
interp alias "" tnet1 "" punk::basictelnet::telnet
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::basictelnet ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Secondary API namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punk::basictelnet::lib {
namespace export *
namespace path [namespace parent]
#*** !doctools
#[subsection {Namespace punk::basictelnet::lib}]
#[para] Secondary functions that are part of the API
#[list_begin definitions]
#proc utility1 {p1 args} {
# #*** !doctools
# #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]]
# #[para]Description of utility1
# return 1
#}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::basictelnet::lib ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Internal]
namespace eval punk::basictelnet::system {
#*** !doctools
#[subsection {Namespace punk::basictelnet::system}]
#[para] Internal functions that are not part of the API
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::basictelnet [namespace eval punk::basictelnet {
variable pkg punk::basictelnet
variable version
set version 999999.0a1.0
}]
return
#*** !doctools
#[manpage_end]

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

@ -0,0 +1,3 @@
0.1.0
#First line must be a semantic version number
#all other lines are ignored.

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

@ -42,6 +42,10 @@ namespace eval punk::console {
if {![info exists input_chunks_waiting(stdin)]} {
set input_chunks_waiting(stdin) [list]
}
variable ansi_response_chunk ;#array keyed on callid
variable ansi_response_wait ;#array keyed on callid
variable ansi_response_queue ;#list of callids
variable ansi_response_queuedata ;#dict keyed on callid - with function params
# --
variable ansi_available -1 ;#default -1 for unknown. Leave it this way so test for ansi support is run.
@ -520,14 +524,34 @@ namespace eval punk::console {
upvar ::punk::console::ansi_response_chunk accumulator
upvar ::punk::console::ansi_response_wait waitvar
upvar ::punk::console::ansi_response_queue queue
upvar ::punk::console::ansi_response_queuedata queuedata
set accumulator($callid) ""
set waitvar($callid) ""
lappend queue $callid
#todo - use a linked array and an accumulatorid and waitvar id? When can there be more than one terminal query in-flight?
set existing_handler [fileevent $input readable] ;#review!
set this_handler ::punk::console::internal::ansi_response_handler_regex
if {[lindex $existing_handler 0] eq $this_handler} {
puts stderr "[punk::ansi::a+ red]Warning for callid $callid get_ansi_response_payload called while existing ansi response handler in place[a]: $this_handler"
puts stderr "queue state: $queue"
if {[lindex $queue 0] ne $callid} {
while { $waitvar($callid) ne "go_ahead"} {
after 10
set waitvar([lindex $queue 0]) trigger
puts -nonewline stderr "\n[info level 1]\n"
puts -nonewline stderr "<callid:$callid waitvar [array get waitvar]>"
vwait ::punk::console::ansi_response_wait ;#wait on array - not specific element
}
#dict set queuedata $callid [list $query $capturingendregex $inputchannels]
}
}
fileevent $input readable {}
set input_state [fconfigure $input]
@ -542,10 +566,6 @@ namespace eval punk::console {
}
fconfigure $input -blocking 0
#
set this_handler ::punk::console::internal::ansi_response_handler_regex
if {[lindex $existing_handler 0] eq $this_handler} {
puts stderr "[punk::ansi::a+ red]Warning get_ansi_response_payload called while existing ansi response handler in place[a]: $this_handler"
}
#in handler - its used for a boolean match (capturing aspect not used)
fileevent $input readable [list $this_handler $input $callid $capturingendregex]
@ -564,7 +584,8 @@ namespace eval punk::console {
#todo - make timeout configurable?
set waitvarname "::punk::console::ansi_response_wait($callid)"
set cancel_timeout_id [after 500 [list set $waitvarname timedout]]
#500ms is generally plenty for a terminal to respond.. but not in some cases. e.g event loop busy with stdin keypress?? review
set cancel_timeout_id [after 1000 [list set $waitvarname timedout]]
if {[set waitvar($callid)] eq ""} {
vwait ::punk::console::ansi_response_wait($callid)
@ -611,17 +632,33 @@ namespace eval punk::console {
#is there a way to know if existing_handler is input_chunks_waiting aware?
if {[string length $existing_handler] && [lindex $existing_handler 0] ne $this_handler} {
#puts "get_ansi_response_paylaod reinstalling ------>$existing_handler<------"
#puts "get_ansi_response_payload reinstalling ------>$existing_handler<------"
fileevent $input readable $existing_handler
#we may have consumed all pending input on $input - so there may be no trigger for the readable fileevent
if {[llength $input_chunks_waiting($input)]} {
#This is experimental If a handler is aware of input_chunks_waiting - there should be no need to schedule a trigger
#If it isn't, but the handler can accept an existing chunk of data as an argument - we could trigger and pass it the waiting chunks - but there's no way to know its API.
#If it isn't, but the handler can accept an existing chunk of data as a 'waiting' argument - we could trigger and pass it the waiting chunks - but there's no way to know its API.
#we could look at info args - but that's not likely to tell us much in a robust way.
#we could create a reflected channel for stdin? That is potentially an overreach..?
#triggering it manually... as it was already listening - this should generally do no harm as it was the active reader anyway, but won't help with the missing data if it's input_chunks_waiting-unaware.
puts stderr "[punk::ansi::a+ yellow bold]-->punk::console::get_ansi_response_payload triggering existing handler while over-read data is in punk::console::input_chunks_waiting($input) instead of channel [ansistring VIEW $input_chunks_waiting($input)][punk::ansi::a]"
after idle [list after 0 $existing_handler]
set handler_args [info args [lindex $existing_handler 0]]
if {[lindex $handler_args end] eq "waiting"} {
#Looks like the existing handler is setup for punk repl cooperation.
puts stderr "[punk::ansi::a+ yellow bold]-->punk::console::get_ansi_response_payload triggering existing handler $existing_handler while over-read data is in punk::console::input_chunks_waiting($input) instead of channel[punk::ansi::a]"
puts stderr "[punk::ansi::a+ yellow bold]-->waiting: [ansistring VIEW $input_chunks_waiting($input)][punk::ansi::a]"
flush stderr
#FIX - this doesn't work. Fast typing gets out of order!!!!
#concat and supply to existing handler in single text block - review
set waitingdata [join $input_chunks_waiting($input) ""]
set input_chunks_waiting($input) [list]
after idle [list after 0 [list {*}$existing_handler $waitingdata]]
unset waitingdata
} else {
#! todo? for now, emit a clue as to what's happening.
puts stderr "[punk::ansi::a+ yellow bold]-->punk::console::get_ansi_response_payload cannot trigger existing handler $existing_handler while over-read data is in punk::console::input_chunks_waiting($input) instead of channel [ansistring VIEW $input_chunks_waiting($input)][punk::ansi::a]"
}
}
#Note - we still may be in_repl_handler here (which disables its own reader while executing commandlines)
#The input_chunks_waiting may really belong to the existing_handler we found - but if it doesn't consume them they will end up being read by the repl_handler when it eventually re-enables.
@ -638,7 +675,14 @@ namespace eval punk::console {
catch {
unset accumulator($callid)
unset waitvar($callid)
dict unset queuedata $callid
}
if {[llength $queue] > 1} {
set next_callid [lindex $queue 1]
set waitvar($callid) go_ahead
}
lpop queue 0
#set punk::console::chunk ""
return $payload

9
src/modules/punk/repo-999999.0a1.0.tm

@ -242,7 +242,8 @@ namespace eval punk::repo {
if {$path eq {}} { set path [pwd] }
expr {[is_fossil_root $path] || [is_git_root $path]}
}
#require a minimum of /src and /modules|lib|scriptapps|*.vfs - and that it's otherwise sensible
#require a minimum of src and src/modules|src/scriptapps|src/*/*.vfs - and that it's otherwise sensible
#we still run a high chance of picking up unintended candidates - but hopefully it's a reasonable balance.
proc is_candidate_root {{path {}}} {
if {$path eq {}} { set path [pwd] }
if {[file pathtype $path] eq "relative"} {
@ -250,7 +251,8 @@ namespace eval punk::repo {
} else {
set normpath $path
}
set unwise_paths [list "/" "/usr/local" "/usr/local/bin" "/usr/local/lib" "c:/windows"]
#we're not pickup all possible unwise paths - mainly ones that are likely to be above us, or some that are probably just really bad ideas.
set unwise_paths [list "/" "/dev" "/bin" "/root" "/etc" "/opt" "/usr" "/usr/local" "/usr/local/bin" "/usr/local/lib" "c:/windows"]
if {[string tolower $normpath] in $unwise_paths} {
return 0
}
@ -268,7 +270,8 @@ namespace eval punk::repo {
}
set src_subs [glob -nocomplain -dir $path/src -types d -tail *]
if {"modules" in $src_subs || "lib" in $src_subs || "scriptapps" in $src_subs} {
#test for $path/src/lib is too common to be a useful indicator
if {"modules" in $src_subs || "scriptapps" in $src_subs} {
return 1
}
foreach sub $src_subs {

Loading…
Cancel
Save