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