Overview | Index by: file name |
procedure name |
procedure call |
annotation
module.tcl
(annotations | original source)
#
# Copyright (C) 2010 Alexandros Stergiakis <alsterg@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#//#
# MikroConf modules are regular Tcl packages, that are loaded using the newer "Tcl modules"
# mechanism. We create a wrapper of this mechanism here, because we need to keep track some
# extra information regarding MikroConf modules, like the exact dependency tree.
#
# @author Alexandros Stergiakis <alsterg@gmail.com>
#//#
namespace eval ::module {
namespace export provide require reset version loaded available loadall init resetall reloadall
variable Modules {} ;# In the order they are loaded.
variable Module2Ver
# Declare that a module is provided. A mere wrapper to "package provide".
#
# @param args Same as in "package provide".
proc provide {name {ver {}}} {
variable Modules
variable Module2Ver
package provide $name {*}$ver
lappend Modules $name
set Module2Ver($name) $ver
}
# Load a required module, and its dependencies.
#
# Sytanx:
# module require name ?version?
# <version> strings adhere to the syntax of the "package require" Tcl command.
#
# @depends On the syntax for 'package require' sub-command.
# @assume Any module is loaded on MikroConf start, so that it is possible to print messages to stdout.
#
# @param args Arguments according to the syntax specification above
# @return returns the version of the package (just loaded or already loaded)
# @error
proc require {name args} {
variable Modules
if {$name ni $Modules} {
if {[catch {
set ver [package require $name {*}$args]
::module::${name}::constructor
} errMsg errStack]} {
log::Alert -error -stack $errStack Emergency "Failed to load module \"$name\": " $errMsg
}
log::Debug "Module $name version $ver was loaded successfully"
}
}
# Initialize a module's configuration state.
# All running configuration pertaining to this module will be lost.
#
# @param name Name of package
# @error
proc reset {name} {
variable Modules
if {$name ni $Modules} {
log::Error -error "Cannot reset module ${name}; module is not loaded"
}
if {[catch {
::module::${name}::reset
} errMsg errStack]} {
log::Emergency -error -stack $errStack "Failed to reset module \"$name\": " $errMsg
}
log::Debug "Module $name resetted successfully"
}
# Initialize the configuration state of the whole system (all loaded modules).
#
# @error
proc resetall {} {
variable Modules
log::Info "Resetting all MikroConf modules: $Modules"
# Need to reset starting from the leefs of the dependency tree.
foreach name [lreverse $Modules] {
reset $name
}
}
# Reload all module code.
#
# @error
proc reloadall {} {
variable Modules
log::Info "Reloading all MikroConf modules: $Modules"
# Need to reset starting from the root of the dependency tree.
foreach name $Modules {
::module::${name}::destructor
::module::${name}::constructor
}
}
# Return the version of a loaded module.
#
# @assume Requested module is loaded.
# @param mod Name of package
# @return Version string.
proc version {name} {
variable Module2Ver
return $Module2Ver($name)
}
# Return all loaded modules in order of first to last loaded.
#
# @return A list of loaded modules, or empty list if no module is loaded.
proc loaded {} {
variable Modules
return $Modules
}
# List all the modules that can be loaded and their versions.
#
# @assume All module files are in the same directory (no subdirectories): MODULES_DIR.
#
# @return A list of the form: filename1 modname1 modversion1 filename2 modname2 modversion2 ... \
The list is sorted according to filename.
proc available {} {
Global MODULES_DIR
set result [list]
set RE {([[:alpha:]][:[:alnum:]]*)-([[:digit:]].*)\.tm}
foreach mod [lsort [glob -nocomplain -directory $MODULES_DIR -types f *.tm]] {
set mod [file tail $mod]
if {[regexp $RE $mod filename modname modver]} {
lappend result $filename $modname $modver
}
}
return $result
}
# Load as many modules as possible from the available ones.
# Ignore already loaded, those loaded with different version, and load failures.
# In effect, try to load as many modules as possible.
proc loadall {} {
Global SKIP_MODULES
foreach {filename name version} [available] {
if {$name ni $SKIP_MODULES && $name ni [loaded]} {
catch { require $name } ;# errors are reported within proc require.
}
}
}
# Set-up Module loading.
proc init {} {
Global MODULES_DIR
# We use the newer Tcl Modules architecture to find MikroConf modules.
# Tcl Extensions are still discoverable via the older mechanism.
# Remove all current path entries from the search path
foreach path [::tcl::tm::path list] {
::tcl::tm::path remove $path
}
# Add our module path entry
::tcl::tm::path add $MODULES_DIR
# Note: "package require" does not respect the namespace hierarchy. The namespace of
# the module will always be loaded relatively to ::, even if we use "namespace inscope".
# Therefore we expect that modules declare their namespace within ::module
namespace eval ::module {}
}
namespace ensemble create
} ;# End of Namespace
namespace eval ::module::api {
namespace export ferror getall getpos getval puts exec bgexec ptyexec seval command OnError Catch
# Print an error message with an indication of the erroneous word
# and generate an error in the context of the caller.
#
# @assume cmdline exists on the context of the caller and it is a valid list.
#
# @param explanation Explanatory text of the error.
# @param index List index of the erroneous word. Can point after the list.
# @error
proc ferror {explanation index} {
upvar cmdline cmdline
upvar sid sid
set prefix [lrange $cmdline 0 $index-1]
set suffix [lrange $cmdline $index+1 end]
if {$index >= [llength $cmdline]} {
set word [ansi $sid -underline -- { }] ;# because if out of bounds, it doesn't underline anything
} else {
set word [ansi $sid -underline -- [lindex $cmdline $index]]
}
# If $prefix or $suffix does not exist, then string trim will remove the unecessary space on the left/right of $word.
return -code error [ansi $sid -color red -bold -- [string trim "Syntax error: $prefix $word $suffix"]]\n$explanation
}
# Find the position on the command line where the value for the argument name
# $argname appears. $index specifies (zero-based) which among possibly many
# arguments with same name be returned. Defaults to 0 (the first found).
# The caller should have a dict "arguments" and a variable "argstart" defined properly.
#
# @param argname The argument name whose possition we are looking for.
# @param index If multiple arguments with $argname name, return the $index one. (zero-based).
# @return The position in the command line the value for "argname" appears.
proc getpos {argname {index 0}} {
upvar arguments arguments argstart argstart
for {set i 0} {$i < [llength $arguments]} {incr i 2} {
if {[lindex $arguments $i] eq $argname} {
if {$index == 0} {
return [expr {$argstart + 1 + $i / 2}]
} else {
incr index -1
}
}
}
}
# Find in a dictionary named "arguments" on the context of the caller, the
# value for the key $argname within the dictionary. If the key is defined, its
# value is returned, otherwise, $default is returned.
#
# @param argname The argument name whose possition we are looking for.
# @param args If a single extra argument provided, then it is the default value for the key, \
which is used if the key is not present in the dict.
# @return The key value is key "argname" is present, otherwise $default.
# @error
proc getval {argname args} {
upvar arguments arguments
if {[dict exists $arguments $argname]} {
return [dict get $arguments $argname]
} else {
if {[lempty $args]} {
error "default value not provided"
} else {
return [lindex $args 0]
}
}
}
# From a list named "arguments" in the context of the caller,
# find and return in the order they appear, all the values of all the
# instances of key $key. Remove duplicates; keep only the first instance.
#
# @param key The name of the key, which values will be looked for.
# @return The values for all the instances of $key, returned in the order found in $lst.
proc getall {argname} {
upvar arguments arguments
set result [list]
foreach {var val} $arguments {
if {$var eq $argname && $val ni $result} {
lappend result $val
}
}
return $result
}
proc puts {args} {
upvar out out
switch -exact -- [llength $args] {
0 { ::puts $out "" }
1 { ::puts $out [lindex $args 0] }
2 {
if {[lindex $args 0] eq "-nonewline"} { ;# nonewline txt
::puts -nonewline $out [lindex $args 1]
} else { ;# chan txt
::puts {*}$args
}
}
3 { ;# nonewline chan txt
::puts {*}$args
}
default { error "syntax error in puts" }
}
}
# Wrapper for exec.
proc exec {args} {
log::Debug "Running external program (exec) : $args"
::exec {*}$args
}
# Wrapper for bgexec.
# Since bgexec is executed in Master thread, which executes one command handler till
# termination before serving another one, this results to only one bgexec session
# be run at any time from Master.
proc bgexec {args} {
global bgexec_status
log::Debug "Running external program (bgexec) : $args"
::bgexec ::bgexec_status {*}$args ;# Returns command output
}
# Wrapper for ptyexec.
# One ptyexec session can run for each Session at any time.
proc ptyexec {args} {
upvar sid sid
log::Debug "Running external program (ptyexec) : $args"
Thread::bgsend $sid [list ::pty::ptyexec {*}$args]
}
# Evaluate a script like "eval" but on a Session interpreter. By doing so the script has
# access to constructs pressent only in a specific Session interpreter. The session Id
# is found by looking for a variable named 'sid' on the context of the caller.
#
# The command returns when the script completes evaluation.
# Meanwhile the Master's loop is not blocked.
#
# @param args Same arguments as "eval".
# @returns Same as "eval".
proc seval {script} {
upvar sid sid
Thread::bgsend $sid $script
}
# Convenience procedure that catch and logs errors.
#
# @param level Either "Ignore" or a syslog severity level: Debug Info Notice Warning Error Critical Alert Emergency. If "Ignore" then errors will not be logged at all.
# @param name Optional. The name of the proc to apply OnError on. This name is resolved according to namespace rules.
proc OnError {level {name {}}} {
set level [::textutil::string::cap $level]
if {$level ni [list Ignore Debug Info Notice Warning Error Critical Alert Emergency]} {
error "Unrecognized level: $level"
}
if {[lempty $name]} {
set name [lindex [info level -1] 0]
# If no namespace specified, we will use the caller's one
if {[namespace tail $name] eq $name} {
set ns [uplevel 1 namespace current]
set name "${ns}::${name}"
}
}
trace remove execution $name leave [lindex [trace info execution $name] 0 1]
# We use uplevel because $name can be relative, and need to be resolved in caller's namespace.
if {$level ne "Ignore"} {
trace add execution $name leave [list ::module::api::OnError_leave_trace $level]
}
}
proc OnError_leave_trace {args} {
lassign $args level command code result
set command [lindex $command 0]
if {$code ne "0"} {
global errorInfo
log::$level -stack $errorInfo "handler \"$command\" : $result"
#XXX Also include: "module \"$modname\", sid \"$sid\", $code, $::errorCode
}
}
# 'proc' wrapper for command handlers, to catch and properly log erros.
# Unclassified errors are reported as "Error" severity.
proc command {name arguments body} {
# If no namespace specified, we will use the caller's one
if {[namespace tail $name] eq $name} {
set ns [uplevel 1 namespace current]
set name "${ns}::${name}"
}
proc $name $arguments $body
# By default errors will not be logged.
uplevel OnError Error $name
}
# Convenience procedure that catch and logs errors
#
# @param level Syslog level: Debug Info Notice Warning Error Critical Alert Emergency.
# @param body The body of code to evaluate on calling scope.
# @param msg Additional error message to display before that generated by 'error'.
proc Catch {level body {msg {}}} {
if {$level ni [list Debug Info Notice Warning Error Critical Alert Emergency]} {
error "Unrecognized level: $level"
}
if {[catch {
uplevel 1 $body
} errMsg errStack]} {
if {[lempty $msg]} {
log::$level -error -stack $errStack $errMsg
} else {
log::$level -error -stack $errStack "$msg: $errMsg"
}
}
}
} ;# End of Namespace
Overview | Index by: file name |
procedure name |
procedure call |
annotation
File generated 2010-03-13 at 22:28.