# copyright (C) 1997-2006 Jean-Luc Fontaine (mailto:jfontain@free.fr)
# this program is free software: please read the COPYRIGHT file enclosed in this package or use the Help Copyright menu

# $Id: modules.tcl,v 2.122 2006/10/25 18:39:33 jfontain Exp $


class modules {

    class instance {

        proc instance {this module index} {
            set ($this,module) $module
            set ($this,loaded) [new module $module $index]
        }

        proc ~instance {this} {
            catch {unset (loaded,$($this,namespace))}
            delete $($this,loaded)
        }

        proc load {this} {                               ;# load module since its switches need to be known for command line parsing
            set loaded $($this,loaded)
            module::load $loaded
            set namespace $module::($loaded,namespace)
            set (loaded,$namespace) $loaded
            set ($this,namespace) $namespace
            if {[info exists ::${namespace}::data(switches)]} {                                        ;# module may take no options
                array set switch [set ::${namespace}::data(switches)]
                if {[info exists switch(--daemon)] && ($switch(--daemon) != 0)} {
                    error {--daemon option must not take any argument}
                }
                set ($this,switches) [set ::${namespace}::data(switches)]
            }
            set ($this,initialize) $module::($loaded,initialize)
            set ($this,version) $module::($loaded,version)
            initialize $this
        }

        proc initialize {this} {
            set namespace $($this,namespace)
            set ($this,identifier) [set ${namespace}::data(identifier)]                  ;# (always exists, set when loading module)
            if {![modules::validName $($this,identifier)]} {
                foreach {name index} [modules::decoded $namespace] {}
                puts stderr "\"$name\" module identifier: \"$($this,identifier)\" contains invalid characters"
                exit 1
            }
            catch {set ($this,times) [set ${namespace}::data(pollTimes)]}                          ;# may not be available initially
            catch {set ($this,views) [set ${namespace}::data(views)]}                                     ;# there could be no views
        }

        proc synchronize {this} {
            module::synchronize $($this,loaded)
            initialize $this   ;# reinitialize in case a few variables have been set or reset in the module initialization procedure
        }

        proc empty {this} {
            module::clear $($this,loaded)
        }

        proc moduleFromNamespace {string} {
            if {[catch {set loaded $(loaded,$string)}]} {
                return 0
            } else {
                return $loaded
            }
        }

    }


    set (instances) {}

    proc modules {this} error                                                                                   ;# object-less class

    proc source {interpreter package file} {
        switch [file extension $file] {
            .py {
                # the module is written in Python
                # if we got here, act as if the package was provided
                if {[catch {package require tclpython 4}] && [catch {package require tclpython 3}]} {
                    return                                                                             ;# cannot load Python modules
                }
                set python [python::interp new]                                             ;# create a temporary Python interpreter
                set code [catch {                                                                         ;# stop at the first error
                    $python exec "import sys\nsys.path.insert(0, '.')"      ;# so that module can be imported from current directory
                    # import the Python module, create a Tcl namespace and eventually copy a few data members:
                    $python exec {import re}                             ;# regular expressions are required by internal Python code
                    $python exec "import $package"
                    $python exec $module::python::utilityFunctions
                    array set data [$python eval formstring($package.form)]                  ;# retrieve the module static data part
                    foreach name {helpText switches updates} {
                        catch {$interpreter eval [list namespace eval $package [list set data($name) $data($name)]]}
                    }
                    $interpreter eval "package provide $package [$python eval $package.__version__]"                   ;# must exist
                } message]
                python::interp delete $python                                                       ;# delete the Python interpreter
                if {$code} {                                                                     ;# there was an error, so report it
                    error $message $::errorInfo $code
                }
            }
            .pm {
                # the module is written in Perl
                # if we got here, act as if the package was provided
                if {[catch {package require tclperl 3}] && [catch {package require tclperl 2}]} return   ;# cannot load Perl modules
                set perl [perl::interp new]                                                   ;# create a temporary Perl interpreter
                set code [catch {                                                                         ;# stop at the first error
                    $perl eval "use $package"  ;# use the Perl module, create a Tcl namespace and eventually copy a few data members
                    $perl eval $module::perl::utilities
                    array set data [$perl eval hash_string(%${package}::data)]               ;# retrieve the module static data part
                    foreach name {helpText switches updates} {
                        catch {$interpreter eval [list namespace eval $package [list set data($name) $data($name)]]}
                    }
                    $interpreter eval "package provide $package [$perl eval \$${package}::VERSION]"                    ;# must exist
                } message]
                perl::interp delete $perl                                                             ;# delete the Perl interpreter
                if {$code} {                                                                     ;# there was an error, so report it
                    error $message $::errorInfo $code
                }
            }
            default {
                # normal sourcing
                $interpreter eval _source [list $file]
            }
        }
    }

    # using Tcl built-in package management facilities, seek available moodss modules
    # in commands string, %M is replaced by module name and %S by switches list from module code
    proc available {{command {}} {scanCommand {}}} {
        set directory [pwd]
        set packages {}
        foreach package [package names] {
            if {[string match *::* $package]} continue                                                    ;# filter out sub-packages
            if {![info exists ::package(directory,$package)]} continue                                       ;# for Tcl, for example
            switch $package {instance - formulas - peer continue}                                           ;# skip internal modules
            if {!$global::debug && ![string match *moodss* $::package(directory,$package)]} {
                continue                      ;# when not debugging (developing), avoid potentially core crashing non moodss modules
            }
            if {$scanCommand ne ""} {
                regsub -all %M $scanCommand $package string
                uplevel #0 $string                                                          ;# always invoke command at global level
            }
            cd $::package(directory,$package)                                ;# switch to module directory only during loading phase
            set interpreter [interp create]              ;# use a separate interpreter in order not to interfere with loaded modules
            $interpreter eval "set auto_path [list $::auto_path]"                    ;# set packages paths list in child interpreter
            catch {$interpreter eval {package require {}}}  ;# preload all packages locations (many pkgIndex.tcl files sourced here)
            # then intercept source command to be able to detect non Tcl modules:
            $interpreter eval {rename source _source}; $interpreter alias source ::modules::source $interpreter $package
            if {[info exists ::package(exact,$package)]} {                                         ;# a specific version is required
                set error [catch {$interpreter eval "package require -exact $package $::package(version,$package)"}]
            } else {
                set error [catch {$interpreter eval "package require $package"}]
            }
            if {!$error && [$interpreter eval info exists ::${package}::data(updates)]} {                 ;# ignore invalid packages
                lappend packages $package
                set switches {}                                                                          ;# there may be no switches
                # module package name and module namespace are identical
                catch {set switches [$interpreter eval "set ::${package}::data(switches)"]}
                set switches [list $switches]                                                                ;# make it a valid list
                if {$command ne ""} {
                    regsub -all %M $command $package string
                    regsub -all %S $string $switches string
                    uplevel #0 $string                                                      ;# always invoke command at global level
                }
            }
            interp delete $interpreter
        }
        cd $directory                                                                                   ;# restore current directory
        return [lsort $packages]
    }

    proc printAvailable {} {            ;# using Tcl built-in package management facilities, seek and print available moodss modules
        puts {searching for module packages, please wait...}
        foreach package [available] {
            puts -nonewline "$package: possibly in"
            set count 0
            foreach directory $::auto_path {
                if {[file readable [file join $directory $package pkgIndex.tcl]]} {
                    if {$count > 0} {
                        puts -nonewline ,
                    }
                    puts -nonewline " [file join $directory $package]"
                    incr count
                }
            }
            puts {}
        }
    }

    # recursive procedure: possibly initialize next module and its options if any in command line arguments
    proc parse {relax arguments} {            ;# arguments list format is: module [-option [value] -option ...] module [-option ...]
        if {[llength $arguments] == 0} return                                                                                ;# done
        set name [lindex $arguments 0]
        set arguments [lrange $arguments 1 end]                                         ;# point to start of switches or next module
        foreach {name index} [decoded $name] {}    ;# possibly split module into its name and its index (if coming from a save file)
        # note: if coming from a dashboard file from moodss before 19.0, index is empty for the first instance of a module
        if {![info exists ::package(directory,$name)]} {                              ;# not a valid module (usually a wrong switch)
            error "error: \"$name\" is not a valid moodss module name"
        }
        if {![validName $name]} {
            error "\"$name\" module name contains invalid characters"
        }
        if {!$relax} {
            switch $name formulas - instance - peer - thresholds {error "loading \"$name\" module is not allowed here"}
        }
if {$global::withGUI} { ;# >8
        lifoLabel::push $global::messenger [format [mc {loading %s...}] $name]
        ::update idletasks
} else { ;# >8
        if {$global::debug} {
            writeLog "loading $name..."
        }
} ;# >8
        set instance [new instance $name $index]
        if {[catch {instance::load $instance} message]} {   ;# load module since its switches need be known for command line parsing
            if {$global::debug} {set information $::errorInfo}
if {$global::withGUI} { ;# >8
            lifoLabel::pop $global::messenger
} ;# >8
            delete $instance                                                                                             ;# clean up
            if {$global::debug} {
                error $information
            } else {
                error "module \"$name\" load error:\n$message"
            }
        }
if {$global::withGUI} { ;# >8
        lifoLabel::pop $global::messenger
} ;# >8
        set help [expr {[lsearch -exact $arguments --help] >= 0}]                                       ;# help requested for module
        if {[info exists instance::($instance,switches)]} {                                                  ;# module takes options
            if {[llength $instance::($instance,switches)] == 0} {
                error "module \"$name\" switches are empty"             ;# design error: no need to recover when dynamically loading
            }
            if {$help} {
                displayHelpMessage $name $instance::($instance,switches)
                exit
            }
            if {[catch {set next [parseCommandLineArguments $instance::($instance,switches) $arguments options]} message]} {
                delete $instance                                                                                         ;# clean up
                error "module \"$name\" options error: $message"
            }
            if {!$instance::($instance,initialize)} {
                error "module \"$name\" has no initialize procedure"    ;# design error: no need to recover when dynamically loading
            }
            set instance::($instance,options) [array get options]
            # save module arguments for saving in file if necessary
            set instance::($instance,arguments) [lrange $arguments 0 [expr {[llength $arguments] - [llength $next] - 1}]]
            set arguments $next
        } else {                                                                                          ;# module takes no options
            if {$help} {
                displayHelpMessage $name
                exit
            }
            set instance::($instance,arguments) {}                          ;# save module arguments for saving in file if necessary
        }
        lappend (instances) $instance                                              ;# add module to successfully loaded modules list
        parse $relax $arguments                                                                                  ;# process the rest
if {$global::withGUI} { ;# >8
        update idletasks                                       ;# make sure latest loading message is not left showing meaninglessly
} ;# >8
    }

    proc helpHTMLData {name} {                                                                          ;# module name with no index
        set noHelpText [mc {no help available}]                                          ;# in case module code does not handle help
        foreach instance $(instances) {
            set namespace $instance::($instance,namespace)
            foreach {module index} [decoded $namespace] {}
            if {$module ne $name} continue
            if {![info exists text]} {                                             ;# retrieve help text from first module namespace
                set text $noHelpText
                catch {set text [set ${namespace}::data(helpText)]}
                set version $instance::($instance,version)
                break
            }
        }
        if {![info exists text]} {                                    ;# no loaded module of that name: retrieve data in another way
            foreach {version text} [versionAndHelpText $name] {}
            if {$text eq ""} {
                set text $noHelpText
            }
        }
        set header [format [mc {<b>%s</b> module version <i>%s</i>}] $name $version]
        append header <br><br>
        if {[regsub -nocase <body> $text <body>$header text] > 0} {                          ;# insert header if HTML formatted help
            # possibly remove title which appears on viewer, since we already generated a title
            regsub -nocase {<title>.*</title>} $text {} text
            return $text
        } else {
            regsub -all \n $text <br> text                                                 ;# regular help, keep original formatting
            return ${header}$text
        }
    }

    proc versionAndHelpText {name} {                                               ;# returns module version and help text in a list
        set directory [pwd]
        cd $::package(directory,$name)                                       ;# switch to module directory only during loading phase
        set interpreter [interp create]                  ;# use a separate interpreter in order not to interfere with loaded modules
        $interpreter eval "set auto_path [list $::auto_path]"         ;# duplicate in case module interpreter requires some packages
        catch {$interpreter eval {package require {}}}                                                            ;# see available{}
        $interpreter eval {rename source _source}; $interpreter alias source ::modules::source $interpreter $name
        $interpreter eval "package require $name"
        set version [$interpreter eval "package provide $name"]
        set text {}                                                                           ;# there may be no help for the module
        catch {set text [$interpreter eval "namespace eval $name {set data(helpText)}"]}
        interp delete $interpreter
        cd $directory                                                                                   ;# restore current directory
        return [list $version $text]
    }

    # Invoke modules initialization procedure, if any, once. modules must be loaded first (see parse{}).
    # In case of an error in a module initialization phase, the module is unloaded and proper cleanup occurs, and if an error
    # command is specified, it is invoked and initialization continues for other modules, otherwise an error is thrown.
    proc initialize {{daemon 0} {errorCommand {}}} {
        foreach instance $(instances) {
            set namespace $instance::($instance,namespace)
            set error 0
            if {$instance::($instance,initialize)} {
                if {[info exists instance::($instance,identifier)]} {
                    set string $instance::($instance,identifier)                                    ;# favor identifier if it exists
                } else {                                                                                   ;# simply use module name
                    set string $namespace
                }
                regsub {<0>$} $string {} string                    ;# remove trailing namespace index for first instance of a module
if {$global::withGUI} { ;# >8
                lifoLabel::push $global::messenger [format [mc {initializing %s...}] $string]
                ::update idletasks
} else { ;# >8
                if {$global::debug} {
                    writeLog "initializing $string module..."
                }
} ;# >8
                catch {unset options}
                catch {array set options $instance::($instance,options)}                           ;# module may have no options set
                if {$daemon && [info exists instance::($instance,switches)]} {               ;# daemon mode and module takes options
                    array set switch $instance::($instance,switches)
                    if {![info exists option(--daemon)] && [info exists switch(--daemon)]} {
                        # module supports the daemon mode and corresponding option is not already set
                        set options(--daemon) {}                                     ;# force daemon option, which takes no argument
                    }
                    unset switch
                }
                if {[info exists options]} {
                    if {[catch {::${namespace}::initialize [array get options]} message]} {
                        if {$global::debug} {set information $::errorInfo}
                        set error 1
                    }
                } else {                                                                                  ;# module takes no options
                    if {[catch ::${namespace}::initialize message]} {
                        if {$global::debug} {set information $::errorInfo}
                        set error 1
                    }
                }
if {$global::withGUI} { ;# >8
                lifoLabel::pop $global::messenger
} ;# >8
            }
            if {!$error} {
                instance::synchronize $instance                                                          ;# in case data was updated
            } else {
                unload $instance                                                                                         ;# clean up
                regsub {<0>$} $namespace {} namespace              ;# remove trailing namespace index for first instance of a module
                set message "module \"$namespace\" initialize error:\n$message"
                if {$global::debug} {
                    error $information
                } elseif {$errorCommand ne ""} {
                    uplevel #0 $errorCommand $namespace [list $message]
                } else {
                    error $message
                }
            }
            set instance::($instance,initialize) 0                                ;# a module instance must be initialized once only
        }
if {$global::withGUI} { ;# >8
        update idletasks                                ;# make sure latest initialization message is not left showing meaninglessly
} ;# >8
    }

    proc setPollTimes {{override {}}} {
        if {[llength $(instances)] == 0} {
            set global::pollTimes {}
            set global::pollTime 0
            return
        }
        set default 0
        set minimum 0
        foreach instance $(instances) {
            set times $instance::($instance,times)
            if {[llength $times] == 0} {
                error "module $instance::($instance,namespace) poll times list is empty"
            }
            # for an asynchronous module, the sole time value would be negative and is used as graph interval, for example
            set time [lindex $times 0]
            if {$time < 0} {                                        ;# asynchronous module, poll time is a viewer interval (negated)
                set intervals($time) {}
                continue
            }
            if {$time > $default} {                                                            ;# default value is the first in list
                set default $time                                                    ;# keep the greater default time of all modules
            }
            set times [lsort -integer $times]                                                                     ;# sort poll times
            set time [lindex $times 0]
            if {$time > $minimum} {
                set minimum $time                                                    ;# keep the greater minimum time of all modules
                set minimumModule $instance::($instance,namespace)
            }
            foreach time $times {                                    ;# poll times list is the combination of all modules poll times
                set data($time) {}
            }
        }
        # sort and restrict poll times above maximum module minimum poll time
        set global::pollTimes [lsort -integer [array names data]]
        set global::pollTimes [lrange $global::pollTimes [lsearch -exact $global::pollTimes $minimum] end]
        if {$global::pollTime < $default} {                 ;# do not override existing poll time that may have been set by the user
            set global::pollTime $default
        }
        if {$override ne ""} {                                                              ;# validate command line override if any
            if {$override < $minimum} {
                puts stderr "$::argv0: minimum poll time is $minimum seconds for module $minimumModule"
                exit 1
            }
            set global::pollTime $override
        }
        if {($global::pollTime == 0) && [info exists intervals]} {
            # all modules are asynchronous, so use an average time as a viewer interval for viewers that need it, such as graphs.
            # the poll times list is empty at this point so the user cannot change the poll time.
            # note that the viewer interval can still be forced by the command line poll time option.
            set sum 0
            set number 0
            foreach interval [array names intervals] {
                incr sum $interval
                incr number
            }
            set global::pollTime [expr {round(double($sum) / -$number)}]
        }                                                        ;# else if poll time is 0, it must all be database instance modules
    }

    proc identifier {array} {                            ;# from an array name, return the module identifier (used in viewer labels)
        set namespace [namespaceFromArray $array]
        foreach instance $(instances) {
            if {$namespace eq $instance::($instance,namespace)} {                                          ;# this is a module array
                return $instance::($instance,identifier)                                                         ;# favor identifier
            }
        }
        return {}                                                                   ;# not a module array or identification unneeded
    }

    proc asynchronous {array} {                              ;# from an array name, tell whether the module instance is asynchronous
        set namespace [namespaceFromArray $array]
        foreach instance $(instances) {
            if {$namespace eq $instance::($instance,namespace)} {
                return [expr {[lindex $instance::($instance,times) 0] < 0}]              ;# sole time value negative if asynchronous
            }
        }
        error "could not find module instance for array $array"          ;# important: user code may rely on error being thrown here
    }

    proc 64Bit {array} {                                              ;# from an array name, tell whether the module is a 64 bit one
        set namespace [namespaceFromArray $array]
        foreach instance $(instances) {
            if {$namespace eq $instance::($instance,namespace)} {
                set 64Bits 0; catch {set 64Bits [set ${namespace}::data(64Bits)]}                                        ;# optional
                return $64Bits
            }
        }
        error "could not find module instance for array $array"
    }

    proc instanceData {array} {                                              ;# return instance data needed by database storage code
        variable instanceData                                                                                ;# instances data cache

        set namespace [namespaceFromArray $array]
        foreach identifier $(instances) {
            if {$namespace eq $instance::($identifier,namespace)} {                                        ;# this is a module array
                set instance $identifier
                break                                                                                              ;# found instance
            }
        }
        if {![info exists instance]} {                   ;# in case of derived data, in its own namespace, such as in summary tables
            return {}
        }
        if {[info exists instanceData($instance)]} {                                                             ;# already in cache
            return $instanceData($instance)
        }
        foreach {data(module) dummy} [modules::decoded $namespace] {}
        set data(identifier) $instance::($instance,identifier)
        set data(version) $instance::($instance,version)
        catch {set data(options) $instance::($instance,options)}                                     ;# module switches are optional
        upvar 1 ::${namespace}::data module                                                          ;# actual module namespace data
        set columns {}
        foreach name [array names module *,label] {                                                         ;# gather column numbers
            if {[scan $name %u column] > 0} {lappend columns $column}
        }
        set list {}
        foreach column [lsort -integer $columns] {                           ;# in indexed order since a list is used as data holder
            lappend list $module($column,label) $module($column,type) $module($column,message)
            if {[catch {lappend list $module($column,anchor)}]} {lappend list {}}                              ;# anchor is optional
        }
        set data(data) $list
        set data(indexColumns) 0; catch {set data(indexColumns) $module(indexColumns)}             ;# optional (default is column 0)
        return [set instanceData($instance) [array get data]]                                     ;# return array in serialized form
    }

    proc decoded {name} {     ;# return module and index list (index may be empty if module is not indexed: name instead of name<N>)
        set index {}
        scan $name {%[^<]<%u>} name index                                         ;# split module into its name and its index if any
        return [list $name $index]
    }

    proc validName {string} {                                                                               ;# includes iso-8859 set
        return [regexp {^[\w ,<>@%&*()=+:.-]+$} $string]
    }

    proc displayHelpMessage {name {switches {}}} {
        puts -nonewline "$name module usage:"
        if {[llength $switches] == 0} {
            puts -nonewline { <no arguments allowed>}
        } else {
            foreach {switch argument} $switches {
                puts -nonewline " \[$switch"
                if {$argument} {                                                                        ;# option takes one argument
                    puts -nonewline { argument}
                }
                puts -nonewline \]
            }
        }
        puts {}
    }

    # return a list of namespaces with identifier and options, options being of list of switch, argument required and argument
    proc loaded {} {
        if {[llength $(instances)] == 0} {
            return {}
        }
        foreach instance $(instances) {
            lappend list [list $instance $instance::($instance,namespace)]
        }
        set return {}
        foreach list [lsort -dictionary -index 1 $list] {                                    ;# sort in namespace alphabetical order
            foreach {instance namespace} $list {}
            lappend return $namespace $instance::($instance,identifier)
            set switches {}                                                                       ;# in case module takes no options
            catch {set switches $instance::($instance,switches)}
            if {[llength $switches] == 0} {
                lappend return {}
            } else {
                set arguments $instance::($instance,arguments)
                set list {}
                foreach {switch required} $switches {
                    lappend list $switch $required
                    set index [lsearch -exact $arguments $switch]                             ;# look for switch in module arguments
                    if {$required} {
                        if {$index < 0} {                                                                     ;# option was not used
                            lappend list {}
                        } else {
                            lappend list [lindex $arguments [incr index]]                             ;# option value follows switch
                        }
                    } else {
                        lappend list [expr {$index >= 0}]                    ;# value is true for boolean options if switch was used
                    }
                }
                lappend return $list
            }
        }
        return $return
    }

    proc instancesWithout {{modules {}}} {
        foreach module $modules {set skip($module) {}}
        set instances {}
        foreach instance $(instances) {                                      ;# note: in modules list, modules are in creation order
            if {[info exists skip($instance::($instance,module))]} continue
            lappend instances $instance
        }
        return $instances
    }

    proc namesWithout {modules} {   ;# list of all different loaded module names (a name is unique even if several instances loaded)
        set list {}
        foreach instance [instancesWithout $modules] {
            set module $instance::($instance,module)
            if {[lsearch -exact $list $module] < 0} {
                lappend list $module
            }
        }
        return $list
    }

    proc terminate {} {       ;# terminate all modules by trying to invoke the related module procedure, which may fail or not exist
        foreach instance $(instances) {
            catch {module::terminate $instance::($instance,loaded)}
        }
    }

    proc unload {instance} {
        ldelete (instances) $instance
        delete $instance
if {$global::withGUI} { ;# >8
        pages::monitorActiveCells         ;# refresh pages monitored cells since tables cells with thresholds could have disappeared
        thresholdLabel::monitorActiveCells                                               ;# refresh global thresholds viewer as well
} ;# >8
    }

    proc loadedNamespace {string} {                       ;# whether the module corresponding with the namespace is currently loaded
        foreach instance $(instances) {
            if {$string eq $instance::($instance,namespace)} {
                return 1
            }
        }
        return 0
    }

    proc namespaceFromArray {name} {
        return [string trimleft [namespace qualifiers [namespace which -variable $name]] :]
    }

    # return the type of a data cell, as provided by its module
    proc type {array row column} {             ;# note: catch errors in this procedure in order to detect a no longer existing array
        set namespace [namespaceFromArray $array]
        if {[lindex [modules::decoded $namespace] 0] eq "peer"} {                              ;# peer module proxies original cells
            return [${namespace}::type $row]                                                            ;# so return their real type
        } else {
            return [set ${array}($column,type)]                                                         ;# directly from module data
        }
    }

    proc loadResidentTraceModule {} {                                       ;# load resident trace module: must be invoked once only
        if {[info exists (trace)]} {error {trying to load several resident trace modules}}
        set (trace) [new instance trace {}]
        instance::load $(trace)
        set namespace $instance::($(trace),namespace)
        ::${namespace}::initialize [list --rows $global::traceNumberOfRows]
    }

    proc trace {module identifier message} {             ;# destined to resident trace and possibly other instantiated trace modules
        regsub {<0>$} $identifier {} identifier                                ;# remove trailing namespace index for first instance
        set namespace $instance::($(trace),namespace)                                                                    ;# resident
        ::${namespace}::update $module $identifier $message
        foreach instance $(instances) {                                                                                    ;# others
            if {$instance::($instance,module) eq "trace"} {
                set namespace $instance::($instance,namespace)
                ::${namespace}::update $module $identifier $message
            }
        }
    }

    proc loadFormulasModule {index object category} {                                                ;# based on parse{}, simplified
        set instance [new instance formulas $index]
        instance::load $instance
        set namespace $instance::($instance,namespace)
        set options {}
        if {$object ne ""} {lappend options --object $object}
        if {$category ne ""} {lappend options --category $category}
        set instance::($instance,options) $options
        ::${namespace}::initialize $options
        set instance::($instance,initialize) 0                 ;# a module instance must be initialized once only (see initialize{})
        set instance::($instance,arguments) {}                                       ;# needed by instance code but unimportant here
        instance::synchronize $instance
        lappend (instances) $instance                                              ;# add module to successfully loaded modules list
        return $instance
    }

    proc loadPeerModule {index port source messageName} {                                            ;# based on parse{}, simplified
        upvar 1 $messageName message

        set instance [new instance peer $index]
        instance::load $instance                                                                       ;# no error should occur here
        set namespace $instance::($instance,namespace)
        set options [list --port $port]
        if {$source ne ""} {lappend options --source $source}
        set instance::($instance,options) $options
        if {![catch {::${namespace}::initialize $instance::($instance,options)} message]} {
            set message {}                                                             ;# always report initialization errors if any
        }
        set instance::($instance,initialize) 0                 ;# a module instance must be initialized once only (see initialize{})
        set instance::($instance,arguments) {}                                       ;# needed by instance code but unimportant here
        instance::synchronize $instance
        lappend (instances) $instance                                              ;# add module to successfully loaded modules list
        return $instance
    }

    proc flashMessage {module namespace message {seconds 1}} {
        # use identifier set by the module code (defaults to namespace otherwise) so that user knows better which module
        regsub {<0>$} [set ::${namespace}::data(identifier)] {} identifier     ;# remove trailing namespace index for first instance
if {$global::withGUI} { ;# >8
        ::lifoLabel::flash $::global::messenger "$identifier: $message" $seconds
        switched::configure [moduleFromNamespace $namespace] -state error        ;### consider these type of messages errors for now
        if {$::global::automaticallyShowTrace} {residentTraceModule 1}
} else { ;# >8
        writeLog "$identifier: $message"
} ;# >8
        trace $module $identifier $message                                                         ;# also possibly display in trace
    }

    proc pushMessage {module namespace message} {
        # use identifier set by the module code (defaults to namespace otherwise) so that user knows better which module
        regsub {<0>$} [set ::${namespace}::data(identifier)] {} identifier     ;# remove trailing namespace index for first instance
if {$global::withGUI} { ;# >8
        ::lifoLabel::push $::global::messenger "$identifier: $message"
        ::update idletasks
        if {$::global::automaticallyShowTrace} {residentTraceModule 1}
} else { ;# >8
        writeLog "$identifier: $message"
} ;# >8
        trace $module $identifier $message                                                         ;# also possibly display in trace
    }

    proc popMessage {} {
if {$global::withGUI} { ;# >8
        ::lifoLabel::pop $::global::messenger
} ;# >8
    }

    # override previous procedure with much faster one (useful when there are many thresholds):
    interp alias {} ::modules::moduleFromNamespace {} ::modules::instance::moduleFromNamespace

}
