1#!/bin/sh
2# Copyright (c) 1999-2000 Jean-Claude Wippler <jcw@equi4.com>
3#
4# Tequilas  -  the "Tequila Server" implements shared persistent arrays
5#\
6exec tclkit "$0" ${1+"$@"}
7
8# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
9# Imlementation notes:
10#
11# Commands starting with "Tqs" can be called from the remote client
12# The rest uses lowercase "tqs" to prevent this (and for uniqueness)
13#
14# There is one global array which is used for all information which
15# this server needs to carry around and track, called "tqs_info":
16#
17#   tqs_info(pending)   - id of pending "after" request, unset if none
18#   tqs_info(timeout)   - milliSecs before timed commit, unset if never
19#   tqs_info(verbose)   - log level: 0=off, 1=req's, 2=notify, 3=reply
20#
21# External views (type "X") are stored as files in directory, one item
22# per text file.  This can be used to store large amounts of text in
23# regular files, outside Metakit (though commit doesn't apply to them):
24#
25#   tqs_external(view)  - directory name, set for each external view
26#
27# Valid while processing an incoming request:
28#   tqs_info(port)      - socket name of current client request
29#
30# The following will be defined for individual views:
31#   tqs_notify($view)   - socket name of client to notify on changes
32# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
33
34    # conditonal logging output
35proc tqsPuts {level msg} {
36    global tqs_info
37    if {$level <= $tqs_info(verbose)} {
38        puts $msg
39    }
40}
41
42    # return a displayable string of limited length
43proc tqsDisplay {str len} {
44    if {[string length $str] > $len} {
45        set str "[string range $str 0 $len]..."
46    }
47    regsub -all {[^ -~]} $str {?} str
48    return $str
49}
50
51    # remote execution of any Metakit command, added 20-02-2000
52proc TqsRemote {cmd args} {
53	eval mk::$cmd $args
54}
55
56    # return the names of all views currently on file
57proc TqsInfo {} {
58    mk::file views tqs
59}
60
61    # get or set log level (see above for meaning of values 0..3)
62proc TqsVerbose {{level ""}} {
63    global tqs_info
64    if {$level != ""} {
65        set tqs_info(verbose) $level
66    }
67    return $tqs_info(verbose)
68}
69
70    # define a view (Metakit's equivalent concept for a Tcl array)
71    # if the second argument is true, all existing data is removed
72    # the third arg is used to specify a binary (B) of memo format (M)
73    # if the third arg is "X", use a directory with files for storage
74proc TqsDefine {view {clear 0} {type S}} {
75    if {$type == "X"} {
76        global tqs_external
77        set tqs_external($view) ""
78        if {$clear} {
79            catch {file delete -force $view.data}
80            tqsTrace $view "" u
81        }
82        file mkdir $view.data
83        #catch {mk::view delete tqs.$view}
84    } else {
85        mk::view layout tqs.$view "name text:$type date:I"
86        if {$clear && [mk::view size tqs.$view] > 0} {
87            mk::view size tqs.$view 0
88            tqsTrace $view "" u
89        }
90        #file delete -force $view.data
91    }
92    return
93}
94
95    # get rid of a view
96proc TqsUndefine {view} {
97    global tqs_external
98    if {[info exists tqs_external($view)]} {
99        file delete -force $view.data
100        unset tqs_external($view)
101    } else {
102        mk::view delete tqs.$view
103    }
104    tqsTrace $view "" u
105    return
106}
107
108    # return the list of all keys, like "array names view"
109proc TqsNames {view} {
110    set result {}
111    global tqs_external
112    if {[info exists tqs_external($view)]} {
113        foreach x [glob -nocomplain $view.data/*] {
114            regsub {.*/} $x {} x
115            lappend result $x
116        }
117    } else {
118        mk::loop c tqs.$view {
119            lappend result [mk::get $c name]
120        }
121    }
122    return $result
123}
124
125    # return the number of keys, like "array size view"
126proc TqsSize {view} {
127    set result {}
128    global tqs_external
129    if {[info exists tqs_external($view)]} {
130        set result [llength [glob -nocomplain $view.data/*]]
131    } else {
132        set result [mk::view size tqs.$view]
133    }
134    return $result
135}
136
137    # return an existing value, lookup by key, like "set view(key)"
138proc TqsGet {view key} {
139    global tqs_external
140    if {[info exists tqs_external($view)]} {
141        set fd [open $view.data/$key]
142        fconfigure $fd -translation binary
143        set v [read $fd]
144        close $fd
145        return $v
146    } else {
147        set n [mk::select tqs.$view name $key]
148        mk::get tqs.$view!$n text ;# throws error if absent
149    }
150}
151
152    # store a value, create if necessary, like "set view(key) data"
153    # the optional last arg can be used to force a specific timestamp
154proc TqsSet {view key data {timestamp ""}} {
155    global tqs_external
156    if {[info exists tqs_external($view)]} {
157        set fd [open $view.data/$key w]
158        fconfigure $fd -translation binary
159        puts -nonewline $fd $data
160        close $fd
161        # timestamp is ignored
162    } else {
163        set n [mk::select tqs.$view name $key]
164        if {[llength $n] == 0} {
165            set n [mk::view size tqs.$view]
166        } elseif {[mk::get tqs.$view!$n text] == $data} {
167            return ;# no change, ignore
168        }
169        if {$timestamp == ""} {
170            set timestamp [clock seconds]
171        }
172        mk::set tqs.$view!$n name $key text $data date $timestamp
173    }
174    tqsTrace $view $key w
175    return
176}
177
178    # Append a value, create if entry did not exist
179proc TqsAppend {view key data} {
180    global tqs_external
181    if {[info exists tqs_external($view)]} {
182        set fd [open $view.data/$key a]
183        fconfigure $fd -translation binary
184        puts -nonewline $fd $data
185        close $fd
186    } else {
187        set n [mk::select tqs.$view name $key]
188        if {[llength $n] > 0} {
189            if {[string length $data] == 0} then return ;# no change
190            set data "[mk::get tqs.$view!$n text]$data"
191        }
192        mk::set tqs.$view!$n name $key text $data date [clock seconds]
193    }
194    tqsTrace $view $key w
195    return
196}
197
198    # delete an existing entry by key, similar to "unset view(key)"
199proc TqsUnset {view key} {
200    global tqs_external
201    if {[info exists tqs_external($view)]} {
202        file delete $view.data/$key
203    } else {
204        set n [mk::select tqs.$view name $key]
205        if {[llength $n] == 0} {
206            return ;# no change, ignore
207        }
208        mk::row delete tqs.$view!$n
209    }
210    tqsTrace $view $key u
211    return
212}
213
214    # return all key/value pairs, like "array get view"
215    # if set, the optional arg sets up change notification
216proc TqsGetAll {view {tracking 0}} {
217    set result {}
218    global tqs_external
219    if {[info exists tqs_external($view)]} {
220        foreach x [TqsNames $view] {
221            lappend result $x [TqsGet $view $x]
222        }
223    } else {
224        mk::loop c tqs.$view {
225            eval lappend result [mk::get $c name text]
226        }
227    }
228    if {$tracking} { tqsSubscribe $view }
229    return $result
230}
231
232    # like TqsGetAll, returns modification dates instead of contents
233    # this can be used by the client to synchronize and track dates
234    # if set, the optional arg sets up change notification
235proc TqsListing {view {tracking 0}} {
236    set result {}
237    global tqs_external
238    if {[info exists tqs_external($view)]} {
239        foreach x [TqsNames $view] {
240            lappend result $x [file mtime $view.data/$x]
241        }
242    } else {
243        mk::loop c tqs.$view {
244            eval lappend result [mk::get $c name date]
245        }
246    }
247    if {$tracking} { tqsSubscribe $view }
248    return $result
249}
250
251    # called to set up notification for a client
252proc tqsSubscribe {view} {
253    global tqs_info tqs_notify
254
255        # remember the client IP and listening number for this view
256    tqsPuts 1 "Notification set up for '$view': $tqs_info(port)"
257    lappend tqs_notify($view) $tqs_info(port)
258}
259
260    # called to unset all notifications for a client
261proc tqsUnsubscribe {port} {
262    global tqs_notify
263
264    foreach {k v} [array get tqs_notify] {
265        set n [lsearch -exact $v $port]
266        if {$n >= 0} {
267            tqsPuts 1 "  Forget notify for $k"
268
269            if {[llength $v] > 1} {
270                set tqs_notify($k) [lreplace $v $n $n]
271            } else {
272                unset tqs_notify($k)
273                tqsPuts 1 "   No more notifications for $k"
274            }
275        }
276    }
277}
278
279    # set a number of key/value pairs, like "array set view pairs"
280proc TqsSetAll {view pairs} {
281    foreach {key value} $pairs {
282        TqsSet $view $key $value
283    }
284}
285
286    # save changes to file now
287proc TqsCommit {} {
288    global tqs_info
289
290    set n [clock clicks]
291    mk::file commit tqs
292    tqsPuts 1 "Commit done ([expr {[clock clicks] - $n}])"
293
294    after cancel TqsCommit
295    catch {unset tqs_info(pending)}
296    return
297}
298
299    # change commit timer, default is to commit with explicit calls
300proc TqsTimer {{timer ""}} {
301    global tqs_info
302
303    after cancel TqsCommit
304
305    if {$timer == ""} {
306        catch {unset tqs_info(timeout)}
307    } else {
308        if {[info exists tqs_info(pending)]} {
309            set tqs_info(pending) [after $timer TqsCommit]
310        }
311        set tqs_info(timeout) $timer
312    }
313}
314
315    # handles tracing of all view changes (there's no read tracing)
316    # this is also the place where delayed commits are scheduled
317proc tqsTrace {view key operation} {
318    global tqs_info tqs_notify
319
320    if [info exists tqs_notify($view)] {
321        switch $operation {
322            w   { set req [list Set $view $key [TqsGet $view $key]] }
323            u   { set req [list Unset $view $key] }
324        }
325
326            # this is the data that gets sent out
327        set msg "[string length $req]\n$req"
328
329        foreach p $tqs_notify($view) {
330            if {$p == $tqs_info(port)} continue ;# skip originator
331
332            if [catch {
333                tqsPuts 2 [tqsDisplay "Notify $p - $req" 65]
334                puts -nonewline $p $msg
335                #flush $p
336            } error] {
337                tqsPuts 1 "Notify to $p failed for $view $key"
338                tqsPuts 1 "  Reason: $error"
339                catch {close $p}
340                tqsUnsubscribe $p
341            }
342        }
343    }
344
345    if {![info exists tqs_info(pending)] &&
346            [info exists tqs_info(timeout)]} {
347        set tqs_info(pending) [after $tqs_info(timeout) TqsCommit]
348    }
349}
350
351    # called whenever a request comes in
352proc tqsRequest {sock} {
353    global tqs_info
354
355    if {[gets $sock bytes] > 0} {
356        set request [read $sock [lindex $bytes 0]]
357        if ![eof $sock] {
358                # debugging: incoming request
359            tqsPuts 1 [tqsDisplay " $request" 65]
360
361            set tqs_info(port) $sock
362
363            set err [catch {uplevel #0 Tqs$request} reply]
364            set msg [list Reply $err $reply]
365            puts -nonewline $sock "[string length $msg]\n$msg"
366
367                # debugging: returned results
368            if {[string length $reply] > 0} {
369                tqsPuts 3 "   result: [tqsDisplay $reply 54]"
370            }
371
372            #flush $sock
373            return
374        }
375    }
376
377    tqsPuts 1 "Closing $sock"
378    close $sock
379    tqsUnsubscribe $sock
380}
381
382    # called whenever a connection is opened
383proc tqsAccept {sock addr port} {
384    global tqs_info
385    fconfigure $sock -translation binary -buffering none
386    fileevent $sock readable [list tqsRequest $sock]
387}
388
389    # this can be called to start a background server
390proc tqsStart {port} {
391    global tqs_notify tqs_external
392
393    array set tqs_notify {}
394
395    foreach x [glob -nocomplain *.data] {
396        regsub {\.data$} $x {} x
397        set tqs_external($x) ""
398    }
399
400    socket -server tqsAccept $port
401}
402
403    # this wraps the server into a standalone, it runs until shutdown
404proc tqsRun {port} {
405    global tqs_info
406
407    set tqs_info(shutdown) [clock seconds]
408
409        # these status messages are not disabled if verbose is off
410    puts "Tequila server on port $port started."
411    tqsStart $port
412    vwait tqs_info(shutdown)
413    puts "Tequila server on port $port has been shut down."
414}
415
416    # client-callable: terminate a server started with "tqsRun"
417proc TqsShutdown {} {
418    global tqs_info
419
420        # returns number of seconds since the server was started
421        # main effect is setting tqs_info(shutdown), which ends vwait
422    set tqs_info(shutdown) [expr {[clock seconds]-$tqs_info(shutdown)}]
423}
424
425# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
426# This script can be used standalone, in which case the code below will
427# be run, or as part of a scripted document, which expects a "package
428# ifneeded tequilas ..." to have been set up.  In that case, the code
429# below will not be executed, allowing the caller so set up different
430# parameter values before calling tqsRun or tqsStart (background use).
431# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
432
433if {[lsearch -exact [package names] tequilas] < 0} {
434    package require Mk4tcl
435    mk::file open tqs tequilas.dat -nocommit
436
437    set tqs_info(verbose) 0     ;# default logging is off
438    TqsTimer 30000              ;# default commit timer is 30 seconds
439    tqsRun 20458                ;# default port is 20458
440}
441