File: modules.tcl

package info (click to toggle)
moodss 9.0-2
  • links: PTS
  • area: main
  • in suites: potato
  • size: 1,540 kB
  • ctags: 609
  • sloc: sh: 8,869; tcl: 6,909; ansic: 113; makefile: 44
file content (276 lines) | stat: -rw-r--r-- 15,928 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# copyright (C) 1997-1999 Jean-Luc Fontaine (mailto:jfontain@multimania.com)
# this program is free software: please read the COPYRIGHT file enclosed in this package or use the Help Copyright menu

set rcsId {$Id: modules.tcl,v 2.4 1999/09/22 11:09:17 jfontain Exp $}

class modules {

    set ::modules::(names) {}
    set ::modules::(namespaces) {}

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

    proc printAvailable {} {            ;# using Tcl built-in package management facilities, seek and print available moodss modules
        catch {package require {}}                                              ;# make sure Tcl package auto loading search is done
        foreach package [package names] {
            if {[catch {package require $package}]||![info exists ::${package}::data(updates)]} continue  ;# ignore invalid packages
            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: eventually initialize next module and its eventual options in command line arguments
    proc parse {arguments} {         ;# arguments list format is: module [-option [value] -option ...] module [-option ...]
        if {[llength $arguments]==0} return                                                                         ;# nothing to do
        set module [lindex $arguments 0]

        set arguments [lrange $arguments 1 end]                                         ;# point to start of switches or next module

        # eventually split module into its name and its index (if coming from a save file)
        foreach {module index} [decoded $module] {}

        if {![info exists ::packageDirectory($module)]} {                             ;# not a valid module (usually a wrong switch)
            puts stderr "error: \"$module\" is not a valid moodss module name"
            exit 1
        }
        if {![validName $module]} {
            puts stderr "\"$module\" module name contains invalid characters"
            exit
        }

        lifoLabel::push $global::messenger "loading $module..."
        update idletasks

        set namespace [load $module $index]

        lifoLabel::pop $global::messenger
        lappend modules::(namespaces) $namespace                 ;# we never get here if there is an error when the module is loaded
        if {[lsearch -exact $(names) $module]<0} {                                              ;# keep track of loaded module names
            lappend modules::(names) $module
        }
        if {[catch {set ::${namespace}::data(switches)} switches]} {                                      ;# module takes no options
            set modules::($namespace,arguments) {}                              ;# save module arguments for eventual saving in file
        } else {                                                                                             ;# module takes options
            if {[llength $switches]==0} {
                puts stderr "module \"$module\" switches are empty"
                exit 1
            }
            if {[catch {set next [parseCommandLineArguments $switches $arguments options]} message]}  {
                puts stderr "module \"$module\" options error: $message"
                exit 1
            }
            if {!$($namespace,initialize)} {
                puts stderr "module \"$module\" has no initialize procedure"
                exit 1
            }
            set modules::($namespace,options) [array get options]
            # save module arguments for eventual saving in file
            set modules::($namespace,arguments) [lrange $arguments 0 [expr {[llength $arguments]-[llength $next]-1}]]
            set arguments $next
        }
        parse $arguments                                                                                         ;# process the rest
        update idletasks                                       ;# make sure latest loading message is not left showing meaninglessly
    }

    proc helpHTMLData {namespace} {       ;# return HTML formatted help no matter whether provided plain or preformatted from module
        if {[catch {set ${namespace}::data(helpText)} text]} {
            set text {no help available}
        }
        set header "<h6>module $namespace</h6><i>version $($namespace,version)"
        if {[string length $($namespace,arguments)]>0} {
            append header ", invoked with arguments: $($namespace,arguments)"
        }
        append header </i><br><br>
        if {[regsub -nocase <body> $text <body>$header text]==0} {                           ;# insert header if HTML formatted help
            regsub -all \n $text <br> text                                                 ;# regular help, keep original formatting
            return ${header}$text
        } else {                                                                                              ;# HTML formatted help
            return $text
        }
    }

    proc initialize {} {          ;# eventually invoke modules initialization procedures. modules must be loaded first (see parse{})
        foreach namespace $(namespaces) {
            if {!$($namespace,initialize)} continue
            lifoLabel::push $global::messenger "initializing $namespace..."
            update idletasks
            if {[info exists ($namespace,options)]} {
                ::${namespace}::initialize $($namespace,options)                                       ;# let module initialize self
            } else {                                                                                      ;# module takes no options
                ::${namespace}::initialize                                                             ;# let module initialize self
            }
            synchronize $namespace                                                                       ;# in case data was updated
            if {![catch {set ${namespace}::data(identifier)} identifier]} {            ;# store identifier if it exists and is valid
                if {![validName $identifier]} {
                    puts stderr "\"$namespace\" module identifier: \"$identifier\" contains invalid characters"
                    exit
                }
                set ($namespace,identifier) $identifier
            }
            lifoLabel::pop $global::messenger
        }
        update idletasks                                ;# make sure latest initialization message is not left showing meaninglessly
    }

    proc setPollTimes {{override {}}} {
        set default 0
        set minimum 0
        foreach namespace $(namespaces) {
            set times [set ${namespace}::data(pollTimes)]
            if {[llength $times]==0} {
                error "module $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 $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]
        set global::pollTime $default
        if {[string length $override]>0} {                                              ;# eventually validate command line override
            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} { 
            # 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)}]
        }
    }

    # load a module in its own interpreter, in order to allow multiple instances of the same module
    proc load {module index} {            ;# index may be forced (when coming from a save file), left empty for automatic generation
        variable nextIndex

        if {[string length $index]==0} {
            if {[catch {set index $nextIndex($module)}]} {                                             ;# first instance of a module
                set namespace $module                                                                    ;# use original module name
                set index 0
            } else {
                set namespace ${module}<$index>                                    ;# this is another instance of an existing module
            }
        } else {                                                                                    ;# index was passed as parameter
            set namespace ${module}<$index>
        }
        set nextIndex($module) [incr index]

        set ($namespace,module) $module

        set interpreter [interp create]                                                ;# use a separate interpreter for each module
        set ($namespace,interpreter) $interpreter
        set ::packageDirectory($namespace) $::packageDirectory($module)                              ;# set module package directory
        $interpreter eval {                    ;# since Tk is not loaded in module interpreter, provide a background error procedure
            proc bgerror {error} {
                puts stderr $::errorInfo
                exit 1
            }
        }
        $interpreter eval "
            set auto_path [list $::auto_path]                          ;# copy a few required global variables in module interpreter
            set ::packageDirectory($module) $::packageDirectory($module)
            package require $module                                                                         ;# module is loaded here
        "
        # we never get here if there is an error when the module is loaded
        # the new namespace, "interface' to the protected module namespace in its interpreter, is child of the global namespace
        namespace eval ::$namespace [subst -nocommands {
            proc update {} {$interpreter eval ::${module}::update}
        }]
        set ($namespace,initialize)\
            [$interpreter eval [subst -nocommands {expr {[string length [namespace eval ::$module {info proc initialize}]]>0}}]]
        if {$($namespace,initialize)} {                                                               ;# initialize procedure exists
            namespace eval ::$namespace [subst -nocommands {        ;# create an interface initialize procedure within new namespace
                proc initialize {arguments} {                     ;# arguments are a list of option / value (eventually empty) pairs
                    $interpreter eval "
                        array set _options [list \$arguments]
                        ::${module}::initialize _options
                        unset _options
                    "
                }
            }]
        }
        set ($namespace,version) [$interpreter eval "package provide $module"]
        synchronize $namespace                                           ;# initialize namespace data from module in its interpreter
        # keep on eye on special module data array member "update"
        $interpreter alias _updated ::modules::updated $namespace
        $interpreter eval "trace variable ::${module}::data(updates) w _updated"
        # setup interface to messenger:
        $interpreter alias pushMessage ::lifoLabel::push $::global::messenger
        $interpreter alias popMessage ::lifoLabel::pop $::global::messenger
        $interpreter alias flashMessage ::lifoLabel::flash $::global::messenger

        return $namespace                                                                                     ;# new name for module
    }

    proc updated {namespace args} {                            ;# module data was just updated. ignore already known trace arguments
        synchronize $namespace {[0-9]*,[0-9]*}                                       ;# just copy all dynamic data from module array
        # and copy updates counter
        set ::${namespace}::data(updates) [$($namespace,interpreter) eval "set ::$modules::($namespace,module)::data(updates)"]
    }

    proc synchronize {namespace {pattern *}} {                  ;# copy data from module in its interpreter to module namespace here
        array set ::${namespace}::data [$($namespace,interpreter) eval "array get ::$($namespace,module)::data {$pattern}"]
    }

    proc identifier {array} {  ;# from an array name, eventually deduce a unique module identifier if needed (used in viewer labels)
        variable nextIndex

        set namespace [string trimleft [namespace qualifiers [namespace which -variable $array]] :]
        if {[lsearch -exact $(namespaces) $namespace]>=0} {                                                ;# this is a module array
            if {[info exists ($namespace,identifier)]} {
                return $($namespace,identifier)                                                     ;# favor identifier if it exists
            }
            foreach {module index} [decoded $namespace] {}
            if {$nextIndex($module)>1} {               ;# there are more than 1 instance of this module, so identification is needed
                return $namespace
            }
        }
        return {}                                                                   ;# not a module array or identification unneeded
    }

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

    proc validName {string} {
        return [regexp {^[ 0-9a-zA-Z~!@%^&*()_=+|;:',.?-]+$} $string]
    }

}