You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
306 lines
8.2 KiB
306 lines
8.2 KiB
# Debug - a debug narrative logger. |
|
# -- Colin McCormack / originally Wub server utilities |
|
# |
|
# Debugging areas of interest are represented by 'tokens' which have |
|
# independantly settable levels of interest (an integer, higher is more detailed) |
|
# |
|
# Debug narrative is provided as a tcl script whose value is [subst]ed in the |
|
# caller's scope if and only if the current level of interest matches or exceeds |
|
# the Debug call's level of detail. This is useful, as one can place arbitrarily |
|
# complex narrative in code without unnecessarily evaluating it. |
|
# |
|
# TODO: potentially different streams for different areas of interest. |
|
# (currently only stderr is used. there is some complexity in efficient |
|
# cross-threaded streams.) |
|
|
|
# # ## ### ##### ######## ############# ##################### |
|
## Requisites |
|
|
|
package require Tcl 8.5 |
|
|
|
namespace eval ::debug { |
|
namespace export -clear \ |
|
define on off prefix suffix header trailer \ |
|
names 2array level setting parray pdict \ |
|
nl tab hexl |
|
namespace ensemble create -subcommands {} |
|
} |
|
|
|
# # ## ### ##### ######## ############# ##################### |
|
## API & Implementation |
|
|
|
proc ::debug::noop {args} {} |
|
|
|
proc ::debug::debug {tag message {level 1}} { |
|
variable detail |
|
if {$detail($tag) < $level} { |
|
#puts stderr "$tag @@@ $detail($tag) >= $level" |
|
return |
|
} |
|
|
|
variable prefix |
|
variable suffix |
|
variable header |
|
variable trailer |
|
variable fds |
|
|
|
if {[info exists fds($tag)]} { |
|
set fd $fds($tag) |
|
} else { |
|
set fd stderr |
|
} |
|
|
|
# Assemble the shown text from the user message and the various |
|
# prefixes and suffices (global + per-tag). |
|
|
|
set themessage "" |
|
if {[info exists prefix(::)]} { append themessage $prefix(::) } |
|
if {[info exists prefix($tag)]} { append themessage $prefix($tag) } |
|
append themessage $message |
|
if {[info exists suffix($tag)]} { append themessage $suffix($tag) } |
|
if {[info exists suffix(::)]} { append themessage $suffix(::) } |
|
|
|
# Resolve variables references and command invokations embedded |
|
# into the message with plain text. |
|
set code [catch { |
|
set smessage [uplevel 1 [list ::subst -nobackslashes $themessage]] |
|
set sheader [uplevel 1 [list ::subst -nobackslashes $header]] |
|
set strailer [uplevel 1 [list ::subst -nobackslashes $trailer]] |
|
} __ eo] |
|
|
|
# And dump an internal error if that resolution failed. |
|
if {$code} { |
|
if {[catch { |
|
set caller [info level -1] |
|
}]} { set caller GLOBAL } |
|
if {[string length $caller] >= 1000} { |
|
set caller "[string range $caller 0 200]...[string range $caller end-200 end]" |
|
} |
|
foreach line [split $caller \n] { |
|
puts -nonewline $fd "@@(DebugError from $tag ($eo): $line)" |
|
} |
|
return |
|
} |
|
|
|
# From here we have a good message to show. We only shorten it a |
|
# bit if its a bit excessive in size. |
|
|
|
if {[string length $smessage] > 4096} { |
|
set head [string range $smessage 0 2048] |
|
set tail [string range $smessage end-2048 end] |
|
set smessage "${head}...(truncated)...$tail" |
|
} |
|
|
|
foreach line [split $smessage \n] { |
|
puts $fd "$sheader$tag | $line$strailer" |
|
} |
|
return |
|
} |
|
|
|
# names - return names of debug tags |
|
proc ::debug::names {} { |
|
variable detail |
|
return [lsort [array names detail]] |
|
} |
|
|
|
proc ::debug::2array {} { |
|
variable detail |
|
set result {} |
|
foreach n [lsort [array names detail]] { |
|
if {[interp alias {} debug.$n] ne "::debug::noop"} { |
|
lappend result $n $detail($n) |
|
} else { |
|
lappend result $n -$detail($n) |
|
} |
|
} |
|
return $result |
|
} |
|
|
|
# level - set level and fd for tag |
|
proc ::debug::level {tag {level ""} {fd {}}} { |
|
variable detail |
|
# TODO: Force level >=0. |
|
if {$level ne ""} { |
|
set detail($tag) $level |
|
} |
|
|
|
if {![info exists detail($tag)]} { |
|
set detail($tag) 1 |
|
} |
|
|
|
variable fds |
|
if {$fd ne {}} { |
|
set fds($tag) $fd |
|
} |
|
|
|
return $detail($tag) |
|
} |
|
|
|
proc ::debug::header {text} { variable header $text } |
|
proc ::debug::trailer {text} { variable trailer $text } |
|
|
|
proc ::debug::define {tag} { |
|
if {[interp alias {} debug.$tag] ne {}} return |
|
off $tag |
|
return |
|
} |
|
|
|
# Set a prefix/suffix to use for tag. |
|
# The global (tag-independent) prefix/suffix is adressed through tag '::'. |
|
# This works because colon (:) is an illegal character for user-specified tags. |
|
|
|
proc ::debug::prefix {tag {theprefix {}}} { |
|
variable prefix |
|
set prefix($tag) $theprefix |
|
|
|
if {[interp alias {} debug.$tag] ne {}} return |
|
off $tag |
|
return |
|
} |
|
|
|
proc ::debug::suffix {tag {theprefix {}}} { |
|
variable suffix |
|
set suffix($tag) $theprefix |
|
|
|
if {[interp alias {} debug.$tag] ne {}} return |
|
off $tag |
|
return |
|
} |
|
|
|
# turn on debugging for tag |
|
proc ::debug::on {tag {level ""} {fd {}}} { |
|
variable active |
|
set active($tag) 1 |
|
level $tag $level $fd |
|
interp alias {} debug.$tag {} ::debug::debug $tag |
|
return |
|
} |
|
|
|
# turn off debugging for tag |
|
proc ::debug::off {tag {level ""} {fd {}}} { |
|
variable active |
|
set active($tag) 1 |
|
level $tag $level $fd |
|
interp alias {} debug.$tag {} ::debug::noop |
|
return |
|
} |
|
|
|
proc ::debug::setting {args} { |
|
if {[llength $args] == 1} { |
|
set args [lindex $args 0] |
|
} |
|
set fd stderr |
|
if {[llength $args] % 2} { |
|
set fd [lindex $args end] |
|
set args [lrange $args 0 end-1] |
|
} |
|
foreach {tag level} $args { |
|
if {$level > 0} { |
|
level $tag $level $fd |
|
interp alias {} debug.$tag {} ::debug::debug $tag |
|
} else { |
|
level $tag [expr {-$level}] $fd |
|
interp alias {} debug.$tag {} ::debug::noop |
|
} |
|
} |
|
return |
|
} |
|
|
|
# # ## ### ##### ######## ############# ##################### |
|
## Convenience commands. |
|
# Format arrays and dicts as multi-line message. |
|
# Insert newlines and tabs. |
|
|
|
proc ::debug::nl {} { return \n } |
|
proc ::debug::tab {} { return \t } |
|
|
|
proc ::debug::parray {a {pattern *}} { |
|
upvar 1 $a array |
|
if {![array exists array]} { |
|
error "\"$a\" isn't an array" |
|
} |
|
pdict [array get array] $pattern |
|
} |
|
|
|
proc ::debug::pdict {dict {pattern *}} { |
|
set maxl 0 |
|
set names [lsort -dict [dict keys $dict $pattern]] |
|
foreach name $names { |
|
if {[string length $name] > $maxl} { |
|
set maxl [string length $name] |
|
} |
|
} |
|
set maxl [expr {$maxl + 2}] |
|
set lines {} |
|
foreach name $names { |
|
set nameString [format (%s) $name] |
|
lappend lines [format "%-*s = %s" \ |
|
$maxl $nameString \ |
|
[dict get $dict $name]] |
|
} |
|
return [join $lines \n] |
|
} |
|
|
|
proc ::debug::hexl {data {prefix {}}} { |
|
set r {} |
|
|
|
# Convert the data to hex and to characters. |
|
binary scan $data H*@0a* hexa asciia |
|
|
|
# Replace non-printing characters in the data with dots. |
|
regsub -all -- {[^[:graph:] ]} $asciia {.} asciia |
|
|
|
# Pad with spaces to a full multiple of 32/16. |
|
set n [expr {[string length $hexa] % 32}] |
|
if {$n < 32} { append hexa [string repeat { } [expr {32-$n}]] } |
|
#puts "pad H [expr {32-$n}]" |
|
|
|
set n [expr {[string length $asciia] % 32}] |
|
if {$n < 16} { append asciia [string repeat { } [expr {16-$n}]] } |
|
#puts "pad A [expr {32-$n}]" |
|
|
|
# Reassemble formatted, in groups of 16 bytes/characters. |
|
# The hex part is handled in groups of 32 nibbles. |
|
set addr 0 |
|
while {[string length $hexa]} { |
|
# Get front group of 16 bytes each. |
|
set hex [string range $hexa 0 31] |
|
set ascii [string range $asciia 0 15] |
|
# Prep for next iteration |
|
set hexa [string range $hexa 32 end] |
|
set asciia [string range $asciia 16 end] |
|
|
|
# Convert the hex to pairs of hex digits |
|
regsub -all -- {..} $hex {& } hex |
|
|
|
# Add the hex and latin-1 data to the result buffer |
|
append r $prefix [format %04x $addr] { | } $hex { |} $ascii |\n |
|
incr addr 16 |
|
} |
|
|
|
# And done |
|
return $r |
|
} |
|
|
|
# # ## ### ##### ######## ############# ##################### |
|
|
|
namespace eval debug { |
|
variable detail ; # map: TAG -> level of interest |
|
variable prefix ; # map: TAG -> message prefix to use |
|
variable suffix ; # map: TAG -> message suffix to use |
|
variable fds ; # map: TAG -> handle of open channel to log to. |
|
variable header {} ; # per-line heading, subst'ed |
|
variable trailer {} ; # per-line ending, subst'ed |
|
|
|
# Notes: |
|
# - The tag '::' is reserved. "prefix" and "suffix" use it to store |
|
# the global message prefix / suffix. |
|
# - prefix and suffix are applied per message. |
|
# - header and trailer are per line. And should not generate multiple lines! |
|
} |
|
|
|
# # ## ### ##### ######## ############# ##################### |
|
## Ready |
|
|
|
package provide debug 1.0.6 |
|
return
|
|
|