diff options
author | William Joye <wjoye@cfa.harvard.edu> | 2016-10-27 19:39:39 (GMT) |
---|---|---|
committer | William Joye <wjoye@cfa.harvard.edu> | 2016-10-27 19:39:39 (GMT) |
commit | ea28451286d3ea4a772fa174483f9a7a66bb1ab3 (patch) | |
tree | 6ee9d8a7848333a7ceeee3b13d492e40225f8b86 /tcllib/modules/nns | |
parent | b5ca09bae0d6a1edce939eea03594dd56383f2c8 (diff) | |
parent | 7c621da28f07e449ad90c387344f07a453927569 (diff) | |
download | blt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.zip blt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.tar.gz blt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.tar.bz2 |
Merge commit '7c621da28f07e449ad90c387344f07a453927569' as 'tcllib'
Diffstat (limited to 'tcllib/modules/nns')
-rw-r--r-- | tcllib/modules/nns/ChangeLog | 192 | ||||
-rw-r--r-- | tcllib/modules/nns/common.tcl | 38 | ||||
-rw-r--r-- | tcllib/modules/nns/common.test | 34 | ||||
-rw-r--r-- | tcllib/modules/nns/nns.tcl | 432 | ||||
-rw-r--r-- | tcllib/modules/nns/nns_auto.man | 119 | ||||
-rw-r--r-- | tcllib/modules/nns/nns_auto.tcl | 443 | ||||
-rw-r--r-- | tcllib/modules/nns/nns_client.man | 338 | ||||
-rw-r--r-- | tcllib/modules/nns/nns_cluster.tcl | 499 | ||||
-rw-r--r-- | tcllib/modules/nns/nns_cluster.test | 195 | ||||
-rw-r--r-- | tcllib/modules/nns/nns_common.man | 47 | ||||
-rw-r--r-- | tcllib/modules/nns/nns_intro.man | 128 | ||||
-rw-r--r-- | tcllib/modules/nns/nns_protocol.man | 182 | ||||
-rw-r--r-- | tcllib/modules/nns/nns_server.man | 145 | ||||
-rw-r--r-- | tcllib/modules/nns/pkgIndex.tcl | 10 | ||||
-rw-r--r-- | tcllib/modules/nns/server.tcl | 385 |
15 files changed, 3187 insertions, 0 deletions
diff --git a/tcllib/modules/nns/ChangeLog b/tcllib/modules/nns/ChangeLog new file mode 100644 index 0000000..75de1b2 --- /dev/null +++ b/tcllib/modules/nns/ChangeLog @@ -0,0 +1,192 @@ +2013-02-01 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.15 ======================== + * + +2011-12-13 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.14 ======================== + * + +2011-01-24 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.13 ======================== + * + +2009-12-07 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.12 ======================== + * + +2008-12-12 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.11.1 ======================== + * + +2008-10-21 Andreas Kupries <andreask@activestate.com> + + * nns.tcl: Fixed [Bug 2182378], reported by Joe Brandt + * pkgIndex.tcl: <vonbane@users.sourceforge.net>. Added + * nns_client.man: the missing assignment for 'oneshot'. + Version bumped to 0.4.2. + +2008-10-16 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.11 ======================== + * + +2008-08-13 Michael Schlenker <mic42@users.sourceforge.net> + + * nns.tcl: Fixed missing variable. Bumped version to 0.4.1. + * nns_auto.tcl: + * pkgIndex.tcl: + +2008-05-05 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * ../../apps/nns: Switched to use of nameserv::auto to handle the + * ../../apps/nnslog: loss and restoration of the server + automatically. Got rid of the colorization frills. + + * server.tcl (::nameserv::server::bind): Small extension of log + * pkgIndex.tcl: output for failure case of bind. Added log output + * nns_server.man: to trace searches. Bumped package version to 0.3.2. + + * nns_auto.tcl: Refactored the bind and restore code, put the + * nns_auto.man: commonalities into shared commands. Extended the + * pkgIndex.tcl: API to become a full drop-in replacement for + 'nameserv', just with the persistence feature. Extended the + persistence to continuous and unfulfilled async searches. Now + exporting the API commands. Bumped package version to 0.3. + + * nns.tcl: Factored the argument processing for searches into a + * pkgIndex.tcl: separate command. Pseudo-public. Undocumented, but + * nns_client.man: can be used by other nameserver packages. Fixed + leak when encountering a missing name server during creation of + a continuous or async search. Fixed async destruction of a + continous search from receiver object. Now exporting the API + commands. Bumped package version to 0.4. + +2008-05-03 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * nns_auto.tcl (::nameserv::auto::configure): Fixed incorrect + * server.tcl (::nameserv::server::configure): checking for + * nns.tcl (::nameserv::configure): wrong#args in the code + * pkgIndex.tcl: handling the various options. Bumped client + * nns_client.man: to version 0.3.2, server to 0.3.1, and auto + * nns_server.man: to 0.2.1. Also general documentation work. + * nns_auto.man: + * nns_intro.man: + +2008-04-30 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * ../../apps/nns (::nns::ProcessCommandLine): Fixed incorrect + * ../../apps/nns.man: checking for wrong#args in the code handling + * ../../apps/nnsd.man: the options -host and -port. Reworked + the descriptiond of the applications a bit. + + * ../../apps/nnslog: New application and its documentation, a + * ../../apps/nnslog.man: stripped down form of 'nns search + -continuous *' with different output (log of events). + +2008-04-30 Andreas Kupries <andreask@activestate.com> + + * nns.tcl (::nameserv::configure): Fixed [Bug 1954771], bringing + * pkgIndex.tcl: missing variable into scope. Thanks to Joe Brandt + * nns_client:man: <vonbane@users.sourceforge.net> for both report + and fix. Bumped version to 0.3.1. + +2008-04-30 Andreas Kupries <andreask@activestate.com> + + * nns_intro.man: New file, giving an overview of the packages and + * nns_auto.man: applications in the module. All other documents now + * nns_client.man: refer back to the introduction. Also clarified + * nns_common.man: the relationship to DNS, which is none, plus + * nns_protocol.man: reference to the Tcllib packages which do + * nns_server.man: handle DNS. Pointed applications out as examples + * ../../apps/nnsd.man: of use for the packages. + * ../../apps/nns.man: + +2008-04-03 Andreas Kupries <andreask@activestate.com> + + * nns_protocol.man: Renamed nns_procotol.man, fixed the typo in + the filename. Thanks to Reinhard Max for seeing this. + +2008-03-14 Andreas Kupries <andreask@activestate.com> + + * nns_client.man: Cleaned up a bit, replaced deprecated [lst_item] + usage with [def]. + +2008-02-29 Andreas Kupries <andreask@activestate.com> + + * nns_auto.tcl (::nameserv::auto::Rebind, ::nameserv::auto::bind): + * nns_auto.man: Fixed string match with bad pattern and missing + * pkgIndex.tcl: string to match against. Version bumped to 0.2. + +2007-09-12 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.10 ======================== + * + +2007-07-18 Andreas Kupries <andreask@activestate.com> + + * ../../apps/nnsd: Fixed option bug. Bumped version to 1.0.1. + + * ../../apps/nns: Extended example client application with + * ../../apps/nns.man: continuous search. Further extended to + detect and handle loss of connection with service, by exiting. + Bumped version to 1.1 + + * server.tcl: Implemented asynchronous and continuous searches. + * nns.tcl: Ditto in client. Documented this feature, and the + * nns_client.man: extensions to the protocol it needs. + * nns_server.man: Bumped both server and client to version 0.3. + * nns_protocol.man: + * pkgIndex.tcl: + +2007-07-17 Andreas Kupries <andreask@activestate.com> + + * nns_auto.tcl: Name service client on top of the basic facility. + * nns_auto.man: Provides automatic restoration of registered names + * pkgIndex.tcl: after a loss of connection. + + * nns.tcl: Extended to use the services of 'uevent' to generate + events for important changes, like the loss of the connection to + the server. The package version is bumped to 0.2. + +2007-05-08 Andreas Kupries <andreask@activestate.com> + + * nns_client.man: New name for nns.man, to avoid clashing with the + * nns.tcl: nns.man of the command line client, and removed unwanted + log output from the client package. + + * ../../apps/nnsl: Merged nnsl and nnst into one command line client + * ../../apps/nnst: application, nns. Added documentation for that + * ../../apps/nns: application. + * ../../apps/nns.man + + * ../../apps/nnsd.man: Added documentation for the command line + server application. + + * nns_server.man: Changed configuration -local to -localonly + * server.tcl: for better understanding. Bumped to version 0.2 + * pkgIndex.tcl: Removed unwanted log output. + +2007-05-07 Andreas Kupries <andreask@activestate.com> + + * nns.man: Added documentation for client and server packages. + * nns_server.man: + +2007-05-04 Andreas Kupries <andreask@activestate.com> + + * NNS - Nano Name Service. + Initial commit. TODO: Documentation for client and server, ditto + testsuites, are needed. Only the trivial code shared by both is + documented and tested. Manual testing has been done however, + using the nns* applications, see apps/ diff --git a/tcllib/modules/nns/common.tcl b/tcllib/modules/nns/common.tcl new file mode 100644 index 0000000..5c3e1ca --- /dev/null +++ b/tcllib/modules/nns/common.tcl @@ -0,0 +1,38 @@ +# -*- tcl -*- +# ### ### ### ######### ######### ######### +## Name service - Common/shared information. + +# ### ### ### ######### ######### ######### +## Requirements + +namespace eval ::nameserv::common {} + +# ### ### ### ######### ######### ######### +## API + +proc ::nameserv::common::port {} { + variable port + return $port +} + +namespace eval ::nameserv::common { + # Derivation of the standard port number for this service. + + # nameserv::server + # -> nameservserver / remove ':' + # -> 62637378737837 / phonecode + # -> 38573 / mod 65536 + + variable port 38573 + + # The modulo operation is required because IP port numbers are + # restricted to unsigned short (16 bit), i.e. 1 ... 65535. +} + +# ### ### ### ######### ######### ######### +## Ready + +package provide nameserv::common 0.1 + +## +# ### ### ### ######### ######### ######### diff --git a/tcllib/modules/nns/common.test b/tcllib/modules/nns/common.test new file mode 100644 index 0000000..c23873c --- /dev/null +++ b/tcllib/modules/nns/common.test @@ -0,0 +1,34 @@ +# -*- tcl -*- +# common.test: Tests for the common code of the name service +# +# Sourcing this file into Tcl runs the tests and generates output for +# errors. No output means no errors were found. + +# ------------------------------------------------------------------------- + +source [file join \ + [file dirname [file dirname [file join [pwd] [info script]]]] \ + devtools testutilities.tcl] + +testsNeedTcl 8 +testsNeedTcltest 1.0 + +testing { + useLocal common.tcl nameserv::common +} + +# ------------------------------------------------------------------------- + +test names-common-1.0 {get IP port number, wrong#args, too many} { + catch {nameserv::common::port a} msg + set msg +} [tcltest::tooManyArgs nameserv::common::port {}] + +test names-common-2.0 {get IP port number} { + nameserv::common::port +} 38573 + +# ------------------------------------------------------------------------- + +testsuiteCleanup +return diff --git a/tcllib/modules/nns/nns.tcl b/tcllib/modules/nns/nns.tcl new file mode 100644 index 0000000..e256206 --- /dev/null +++ b/tcllib/modules/nns/nns.tcl @@ -0,0 +1,432 @@ +# -*- tcl -*- +# ### ### ### ######### ######### ######### +## Name Service - Client side access + +# ### ### ### ######### ######### ######### +## Requirements + +package require Tcl 8.4 +package require comm ; # Generic message transport +package require interp ; # Interpreter helpers. +package require logger ; # Tracing internal activity +package require nameserv::common ; # Common/shared utilities +package require snit ; # OO support, for streaming search class +package require uevent ; # Generate events for connection-loss + +namespace eval ::nameserv {} + +# ### ### ### ######### ######### ######### +## API: Write, Read, Search + +proc ::nameserv::bind {name data} { + # Registers this application at the configured name service under + # the specified name, and provides a value. + # + # Note: The application is allowed register multiple names. + # + # Note: A registered name is automatically removed by the server + # when the connection to it collapses. + + DO Bind $name $data + return +} + +proc ::nameserv::release {} { + # Releases all names the application has registered at the + # configured name service. + + DO Release + return +} + +proc ::nameserv::search {args} { + # Searches the configured name service for applications whose name + # matches the given pattern. Returns a dictionary mapping from the + # names to the data they provided at 'bind' time. + + # In continuous and async modes it returns an object whose + # contents reflect the current set of matching entries. + + array set a [search-parseargs $args] + upvar 0 a(oneshot) oneshot + upvar 0 a(continuous) continuous + upvar 0 a(pattern) pattern + + if {$continuous} { + variable search + # This client uses the receiver object as tag for the search + # in the service. This is easily unique, and makes dispatch of + # incoming results later easy too. + + set receiver [receiver %AUTO% $oneshot] + if {[catch { + ASYNC Search/Continuous/Start $receiver $pattern + } err]} { + # Release the allocated object to prevent a leak, then + # rethrow the error. + $receiver destroy + return -code error $err + } + + set search($receiver) . + return $receiver + } else { + return [DO Search $pattern] + } +} + +proc ::nameserv::protocol {} { + return 1 +} + +proc ::nameserv::server_protocol {} { + return [DO ProtocolVersion] +} + +proc ::nameserv::server_features {} { + return [DO ProtocolFeatures] +} + +# ### ### ### ######### ######### ######### +## semi-INT: search argument processing. + +proc ::nameserv::search-parseargs {arguments} { + # This command is semi-public. It is not documented for public + # use, however the package nameserv::auto uses as helper in its + # implementation of the search command. + + switch -exact [llength $arguments] { + 0 { + set oneshot 0 + set continuous 0 + set pattern * + } + 1 { + set opt [lindex $arguments 0] + if {$opt eq "-continuous"} { + set oneshot 0 + set continuous 1 + set pattern * + } elseif {$opt eq "-async"} { + set oneshot 1 + set continuous 1 + set pattern * + } else { + set oneshot 0 + set continuous 0 + set pattern $opt + } + } + 2 { + set opt [lindex $arguments 0] + if {$opt eq "-continuous"} { + set oneshot 0 + set continuous 1 + set pattern [lindex $arguments 1] + } elseif {$opt eq "-async"} { + set oneshot 1 + set continuous 1 + set pattern [lindex $arguments 1] + } else { + return -code error "wrong\#args: Expected ?-continuous|-async? ?pattern?" + } + } + default { + return -code error "wrong\#args: Expected ?-continuous|-async? ?pattern?" + } + } + + return [list oneshot $oneshot continuous $continuous pattern $pattern] +} + +# ### ### ### ######### ######### ######### +## INT: Communication setup / teardown / use + +proc ::nameserv::DO {args} { + variable sid + log::debug [linsert $args end @ $sid] + + if {[catch { + [SERV] send $sid $args + #eval [linsert $args 0 [SERV] send $sid] ;# $args + } msg]} { + if {[string match "*refused*" $msg]} { + return -code error "No name server present @ $sid" + } else { + return -code error $msg + } + } + # Result of the call + return $msg +} + +proc ::nameserv::ASYNC {args} { + variable sid + log::debug [linsert $args end @ $sid] + + if {[catch { + [SERV] send -async $sid $args + #eval [linsert $args 0 [SERV] send $sid] ;# $args + } msg]} { + if {[string match "*refused*" $msg]} { + return -code error "No name server present @ $sid" + } else { + return -code error $msg + } + } + # No result to return + return +} + +proc ::nameserv::SERV {} { + variable comm + variable sid + variable host + variable port + if {$comm ne ""} {return $comm} + + # NOTE + # -local 1 means that clients can only talk to a local + # name service. Might make sense to auto-force + # -local 0 for host ne "localhost". + + set interp [interp::createEmpty] + foreach msg { + Search/Continuous/Change + } { + interp alias $interp $msg {} ::nameserv::$msg + } + + set sid [list $port $host] + set comm [comm::comm new ::nameserv::CSERV \ + -interp $interp \ + -local 1 \ + -listen 1] + + $comm hook lost ::nameserv::LOST + + log::debug [list SERV @ $sid : $comm] + return $comm +} + +proc ::nameserv::LOST {args} { + upvar 1 id id chan chan reason reason + variable comm + variable sid + variable search + + log::debug [list LOST @ $sid - $reason] + + $comm destroy + + set comm {} + set sid {} + + # Notify async/cont search of the loss. + foreach r [array names search] { + $r DATA stop + unset search($r) + } + + uevent::generate nameserv lost-connection [list reason $reason] + return +} + +# ### ### ### ######### ######### ######### +## Initialization - System state + +namespace eval ::nameserv { + # Object command of the communication channel to the server. + # If present re-configuration is not possible. Also the comm + # id of the server. + + variable comm {} + variable sid {} + + # Table of active async/cont searches + + variable search ; array set search {} +} + +# ### ### ### ######### ######### ######### +## API: Configuration management (host, port) + +proc ::nameserv::cget {option} { + return [configure $option] +} + +proc ::nameserv::configure {args} { + variable host + variable port + variable comm + + if {![llength $args]} { + return [list -host $host -port $port] + } + if {[llength $args] == 1} { + # cget + set opt [lindex $args 0] + switch -exact -- $opt { + -host { return $host } + -port { return $port } + default { + return -code error "bad option \"$opt\", expected -host, or -port" + } + } + } + + if {$comm ne ""} { + return -code error "Unable to configure an active connection" + } + + # Note: Should -port/-host be made configurable after + # communication has started it will be necessary to provide code + # which retracts everything from the old server and re-initializes + # the new one. + + while {[llength $args]} { + set opt [lindex $args 0] + switch -exact -- $opt { + -host { + if {[llength $args] < 2} { + return -code error "value for \"$opt\" is missing" + } + set host [lindex $args 1] + set args [lrange $args 2 end] + } + -port { + if {[llength $args] < 2} { + return -code error "value for \"$opt\" is missing" + } + set port [lindex $args 1] + # Todo: Check non-zero unsigned short integer + set args [lrange $args 2 end] + } + default { + return -code error "bad option \"$opt\", expected -host, or -port" + } + } + } + return +} + +# ### ### ### ######### ######### ######### +## Receiver for continuous and async searches + +proc ::nameserv::Search/Continuous/Change {tag type response} { + + # Ignore messages for searches which were canceled already. + # + # Due to the async nature of the messages for cont/async search + # the client may have canceled the receiver object already, sent + # the stop message already, but still has to process search + # results which were already in flight. We ignore them. + + if {![llength [info commands $tag]]} return + + # This client uses the receiver object as tag, dispatch the + # received notification to it. + + $tag DATA $type $response + return +} + +snit::type ::nameserv::receiver { + option -command -default {} + + constructor {{once 0}} { + set singleshot $once + return + } + + destructor { + if {$singleshot} return + ::nameserv::ASYNC Search/Continuous/Stop $self + Callback stop {} + return + } + + method get {k} { + if {![info exists current($k)]} {return -code error "Unknown key \"$k\""} + return $current($k) + } + + method names {} { + return [array names current] + } + + method size {} { + return [array size current] + } + + method getall {{pattern *}} { + return [array get current $pattern] + } + + method filled {} { + return $filled + } + + method {DATA stop} {} { + if {$filled && $singleshot} return + set singleshot 1 ; # Prevent 'stop' again during destruction. + Callback stop {} + return + } + + method {DATA add} {response} { + set filled 1 + if {$singleshot} { + ASYNC Search/Continuous/Stop $self + } + array set current $response + Callback add $response + if {$singleshot} { + Callback stop {} + } + return + } + + method {DATA remove} {response} { + set filled 1 + foreach {k v} $response { + unset -nocomplain current($k) + } + Callback remove $response + return + } + + proc Callback {type response} { + upvar 1 options options + if {$options(-command) eq ""} return + # Defer execution to event loop + after 0 [linsert $options(-command) end $type $response] + return + } + + variable singleshot 0 + variable current -array {} + variable filled 0 +} + +# ### ### ### ######### ######### ######### +## Initialization - Tracing, Configuration + +logger::initNamespace ::nameserv +namespace eval ::nameserv { + # Host and port to connect to, to get access to the nameservice. + + variable host localhost + variable port [nameserv::common::port] + + namespace export bind release search protocol \ + server_protocol server_features configure cget +} + +# ### ### ### ######### ######### ######### +## Ready + +package provide nameserv 0.4.2 + +## +# ### ### ### ######### ######### ######### diff --git a/tcllib/modules/nns/nns_auto.man b/tcllib/modules/nns/nns_auto.man new file mode 100644 index 0000000..2547073 --- /dev/null +++ b/tcllib/modules/nns/nns_auto.man @@ -0,0 +1,119 @@ +[manpage_begin nameserv::auto n 0.3] +[see_also nameserv(n)] +[keywords automatic] +[keywords client] +[keywords {name service}] +[keywords reconnect] +[keywords restore] +[copyright {2007-2008 Andreas Kupries <andreas_kupries@users.sourceforge.net>}] +[moddesc {Name service facility}] +[titledesc {Name service facility, Client Extension}] +[category Networking] +[require Tcl 8.4] +[require nameserv::auto [opt 0.3]] +[require nameserv] +[description] + +Please read the document [term {Name service facility, introduction}] +first. + +[para] + +This package provides the [emph exact] same API as is provided by +package [package nameserv], i.e. the regular name service client. It +differs from the former by taking measures to ensure that longer-lived +data, i.e. bound names, continuous and unfullfilled async searches, +survive the loss of the connection to the name server as much as is +possible. + +[para] + +This means that the bound names and continuous and unfullfilled async +searches are remembered client-side and automatically re-entered into +the server when the connection comes back after its loss. For bound +names there is one important limitation to such restoration: It is +possible that a name of this client was bound by a different client +while the connection was gone. Such names are fully lost, and the best +the package can and will do is to inform the user of this. + +[section API] + +The user-visible API is mainly identical to the API of [package nameserv] +and is therefore not described here. Please read the documentation of +[package nameserv]. + +[para] + +The differences are explained below, in the sections [sectref OPTIONS] and +[sectref EVENTS]. + +[section OPTIONS] + +This package supports all the options of package [package nameserv], +plus one more. The additional option allows the user to specify the +time interval between attempts to restore a lost connection. + +[list_begin options] +[opt_def -delay [arg milliseconds]] + +The value of this option is an integer value > 0 which specifies the +interval to wait between attempts to restore a lost connection, in +milliseconds. The default value is [const 1000], i.e. one second. + +[list_end] + +[section EVENTS] + +This package generates all of the events of package [package nameserv], +plus two more. Both events are generated for the tag [term nameserv]. + +[list_begin definitions] +[def [term lost-name]] + +This event is generated when a bound name is truly lost, i.e. could +not be restored after the temporary loss of the connection to the name +server. It indicates that a different client took ownership of the +name while this client was out of contact. + +[para] + +The detail information of the event will be a Tcl dictionary +containing two keys, [const name], and [const data]. Their values hold +all the information about the lost name. + +[def [term re-connection]] + +This event is generated when the connection to the server is +restored. The remembered data has been restored when the event is +posted. + +[para] + +The event has no detail information. + +[list_end] + +[section DESIGN] + +The package is implemented on top of the regular nameservice client, +i.e. package [package nameserv]. It detects the loss of the +connection by listening for [term lost-connection] events, on the tag +[term nameserv]. + +[para] + +It reacts to such events by starting a periodic timer and trying to +reconnect to the server whenver this timer triggers. On success the +timer is canceled, a [term re-connection] event generated, and the +package proceeds to re-enter the remembered bound names and continuous +searches. + +[para] + +Another loss of the connection, be it during or after re-entering the +remembered information simply restarts the timer and subsequent +reconnection attempts. + +[vset CATEGORY nameserv] +[include ../doctools2base/include/feedback.inc] +[manpage_end] diff --git a/tcllib/modules/nns/nns_auto.tcl b/tcllib/modules/nns/nns_auto.tcl new file mode 100644 index 0000000..b081741 --- /dev/null +++ b/tcllib/modules/nns/nns_auto.tcl @@ -0,0 +1,443 @@ +# -*- tcl -*- +# ### ### ### ######### ######### ######### +## Name Service - Client side connection monitor + +# ### ### ### ######### ######### ######### +## Requirements + +package require nameserv 0.4.1 ; # Name service client-side core +package require uevent ; # Watch for connection-loss + +namespace eval ::nameserv::auto {} + +# ### ### ### ######### ######### ######### +## API: Write, Read, Search + +proc ::nameserv::auto::bind {name data} { + # See nameserv::bind. Remembers the information, for re-binding + # when the connection was lost, and later restored. + + # Note: Enter has a return value we do not want, bind has no + # return value. Otherwise 'Enter' would not be necessary and + # simply be 'bind'. + + Enter $name $data normal + return +} + +proc ::nameserv::auto::release {} { + # Releases all names the application has registered at the + # configured name service. + variable bindings + variable timer + + array unset bindings * + if {$timer ne ""} { + # Actually release the data only if the connection is + # currently not lost. Otherwise they are gone already, and + # just forgetting them here (see above) was enough. + nameserv::release + } + return +} + +proc ::nameserv::auto::search {args} { + variable searches + + # Note: Here we are using a semi-public command of 'nameserv' to + # parse the search arguments on our own to determine if we need + # the persistence or not. + + array set a [nameserv::search-parseargs $args] + upvar 0 a(oneshot) oneshot + upvar 0 a(continuous) continuous + upvar 0 a(pattern) pattern + + if {!$continuous} { + # Result is direct result of the search, pass through to + # caller, nothing to persist. + + return [eval [linsert $args 0 ::nameserv::search]] + # 8.5: return [nameserv::search {*}$args] + } + + # Continuous or async search. The result we got is a receiver + # object. Wrap our own persistent receiver around it so that it + # can handle a loss of connection while we are waiting for the + # search result. + + return [receiver %AUTO% $oneshot $args] +} + +proc ::nameserv::auto::protocol {} { + return [nameserv::protocol] +} + +proc ::nameserv::auto::server_protocol {} { + return [nameserv::server_protocol] +} + +proc ::nameserv::auto::server_features {} { + return [nameserv::server_features] +} + +# ### ### ### ######### ######### ######### +## Internal helper commands. + +proc ::nameserv::auto::Reconnect {args} { + # args = <>|<tags event details> + # <tag,event> = <'nameserv','lost'> + # details = dict ('reason' -> string) + + StopReconnect + + if {![catch { + ::nameserv::server_features + }]} { + # Note: Reloss of connection during Rebind will also + # StartReconnect + Rebind + return + } + + StartReconnect + return +} + +proc ::nameserv::auto::Rebind {} { + variable bindings + variable searches + + foreach {name data} [array get bindings] { + if {![Enter $name $data restore]} return + } + + foreach receiver [array names searches] { + if {![$receiver restore]} return + } + + # Fully restored, time to notify interested parties + uevent::generate nameserv re-connection {} + return +} + +proc ::nameserv::auto::Enter {name data how} { + variable bindings + + # Remember locally for possible loss of connection ... + set bindings($name) $data + + # ... then forward to name server + if {[catch { + nameserv::bind $name $data + } msg]} { + # Problem with server while (re)binding a name. + + if {[string match {*No name server*} $msg]} { + # Lost the server (again), while (re)binding a name. Abort + # and restart the watcher waiting for the server to come + # back. + StartReconnect + return 0 + } + + # Other error => (name already bound). This means that someone + # else took the name while we were not connected to the + # service, or the name was bound before the call anyway. The + # reaction depends on our entry point. For regular bind we + # return the error as is to keep API compatibility. During + # restoration OTOH the best effort we can do is to deliver a + # note about the total loss of this binding to all interested + # observers via event. Additionally remove the lost item from + # the set of names to remember. Note that there is no need to + # restart the watcher, the server was _not_ lost. + + unset bindings($name) + if {$how eq "normal"} { + return -code $msg + } else { + uevent::generate nameserv lost-name [list name $name data $data] + return 1 + } + } + + # Success, nothing further to do. + return 1 +} + +# ### ### ### ######### ######### ######### +## Management of the reconnect timer. + +proc ::nameserv::auto::StartReconnect {} { + variable timer + variable delay + if {$timer ne ""} return + set timer [after $delay ::nameserv::auto::Reconnect] + return +} + +proc ::nameserv::auto::StopReconnect {} { + variable timer "" + return +} + +# ### ### ### ######### ######### ######### +## Persistent receiver for continuous and async searches. + +snit::type ::nameserv::auto::receiver { + + option -command -default {} + + constructor {once search} { + set mysingleshot $once + set mysearch $search + $self restore ; # Create internal volatile receiver. + return + } + + destructor { + if {$myreceiver ne ""} { $myreceiver destroy } + if {$mysingleshot} return + Callback stop {} + return + } + + method restore {} { + set nameserv::auto::searches($self) . + + if {[catch { + set result [eval [linsert $mysearch 0 ::nameserv::search]] + # 8.5: set result [nameserv::search {*}$mysearch] + } msg]} { + # Problem with server while restoring a search. + + if {[string match {*No name server*} $msg]} { + # Lost the server (again), while restoring the search. + # Abort and restart the watcher waiting for the server + # to come back. + ::nameserv::auto::StartReconnect + return 0 + } + + # Rethrow other problems. + return -code error $msg + } + + # Restored, prepare ourselves + set myreceiver $result + set myclear 1 ; # Have to clear previous data when + # the new set comes in. + $myreceiver configure -command [mymethod DATA] + return 1 + } + + method get {k} { + if {![info exists mycurrent($k)]} {return -code error "Unknown key \"$k\""} + return $current($k) + } + + method names {} { + return [array names mycurrent] + } + + method size {} { + return [array size mycurrent] + } + + method getall {{pattern *}} { + return [array get mycurrent $pattern] + } + + method filled {} { + return $myfilled + } + + # Handler for events coming from the breakable search. + + method {DATA stop} {args} { + # Ignore the response dict, it is empty anyway. + # Get rid of the volatile receiver. + if {$myreceiver ne ""} { $myreceiver destroy } + # Oneshot handling happened already. + return + } + + method {DATA add} {response} { + # New entries to handle + set myfilled 1 + if {$mysingleshot} { + # The search was async and is now done, therefore we can + # get rid of the volatile receiver and do not have to care + # about the loss of the connection any longer. + $myreceiver destroy + set myreceiver "" + unset ::nameserv::auto::searches($self) + } + if {$myclear} { + # Handle a refill after a connection loss, the new data + # overwrites everything known before. + array unset mycurrent * + set myclear 0 + } + array set mycurrent $response + Callback add $response + if {$mysingleshot} { + Callback stop {} + } + return + } + + method {DATA remove} {response} { + set myfilled 1 + foreach {k v} $response { + unset -nocomplain mycurrent($k) + } + Callback remove $response + return + } + + # Run our own callback. + + proc Callback {type response} { + upvar 1 options options + if {$options(-command) eq ""} return + # Defer execution to event loop + after 0 [linsert $options(-command) end $type $response] + return + } + + # Search state + + variable mysingleshot 0 ; # Bool flag, set if search is + # async, not continous. + variable mycurrent -array {} ; # Current state of search results + variable myfilled 0 ; # Bool flag, set when result has arrived. + + variable mysearch "" ; # Copy of search definition, for + # its restoration after our + # connection to the service was + # restored. + variable myclear 0 ; # Bool flag, set when state has to + # be cleared before adding new + # data, for refill after a + # connection has been restored. + variable myreceiver "" ; # Volatile breakable regular search + # receiver. +} + +# ### ### ### ######### ######### ######### +## Initialization - System state + +namespace eval ::nameserv::auto { + # In-memory database of bindings to restore after connection was + # lost and restored. + + variable bindings ; array set bindings {} + + # In-memory database of continuous and unfulfilled async searches + # to restore after the connection was lost and restored. + + variable searches ; array set searches {} + + # Handle of the timer used to periodically try to reconnect with + # the server in the case it was lost. + + variable timer "" +} + +# ### ### ### ######### ######### ######### +## API: Configuration management (host, port) + +proc ::nameserv::auto::cget {option} { + return [configure $option] +} + +proc ::nameserv::auto::configure {args} { + variable delay + + if {![llength $args]} { + # Merge the underlying configuration with the local settings + # before returning. + return [linsert [nameserv::configure] 0 -delay $delay] + } + if {[llength $args] == 1} { + # cget + set opt [lindex $args 0] + switch -exact -- $opt { + -delay { return $delay } + default { + # Not a local option, check with underlying package + # before throwing an error. + if {![catch { + nameserv::cget $opt + } v]} { + return $v + } + return -code error "[string map {{expected } {expected -delay, }} $v]" + } + } + } + + while {[llength $args]} { + set opt [lindex $args 0] + switch -exact -- $opt { + -delay { + if {[llength $args] < 2} { + return -code error "value for \"$opt\" is missing" + } + set delay [lindex $args 1] + set args [lrange $args 2 end] + + # Using the 'incr' hack instead of 'string is integer' + # allows delays larger than 32bit in Tcl 8.5. + if {[catch {incr delay 0}]} { + return -code error "bad value for \"$opt\", expected integer, got \"$delay\"" + } elseif {$delay <= 0} { + return -code error "bad value for \"$opt\", is not greater than zero" + } + } + default { + # Not a local option, check with underlying package + # before throwing an error. + if {[catch { + nameserv::configure $opt [lindex $args 1] + } v]} { + if {[string match {bad option*} $v]} { + # Fix list of options in error before rethrowing. + return -code error "[string map {{expected } {expected -delay, }} $v]" + } else { + # Rethrow error unchanged + return -code error $v + } + } + # No error, option is processed, continue after it. + set args [lrange $args 2 end] + } + } + } + return +} + +# ### ### ### ######### ######### ######### +## Initialization - Tracing, Configuration + +logger::initNamespace ::nameserv::auto +namespace eval ::nameserv::auto { + # Interval between reconnection attempts when connection was lost. + + variable delay 1000 ; # One second + + namespace export bind release search protocol \ + server_protocol server_features configure cget +} + +# Watch the base client for the loss of the connection. +uevent::bind nameserv lost-connection ::nameserv::auto::Reconnect + +# ### ### ### ######### ######### ######### +## Ready + +package provide nameserv::auto 0.3 + +## +# ### ### ### ######### ######### ######### diff --git a/tcllib/modules/nns/nns_client.man b/tcllib/modules/nns/nns_client.man new file mode 100644 index 0000000..60068c5 --- /dev/null +++ b/tcllib/modules/nns/nns_client.man @@ -0,0 +1,338 @@ +[manpage_begin nameserv n 0.4.2] +[see_also nameserv::common(n)] +[see_also nameserv::server(n)] +[keywords client] +[keywords {name service}] +[copyright {2007-2008 Andreas Kupries <andreas_kupries@users.sourceforge.net>}] +[moddesc {Name service facility}] +[titledesc {Name service facility, Client}] +[category Networking] +[require Tcl 8.4] +[require nameserv [opt 0.4.2]] +[require comm] +[require logger] +[description] + +Please read [term {Name service facility, introduction}] first. + +[para] + +This package provides a client for the name service facility +implemented by the package [package nameserv::server]. + +[para] + +This service is built in top of and for the package [package comm]. +It has nothing to do with the Internet's Domain Name System. If the +reader is looking for a package dealing with that please see Tcllib's +packages [package dns] and [package resolv]. + +[section API] + +The package exports eight commands, as specified below: + +[list_begin definitions] + +[call [cmd ::nameserv::bind] [arg name] [arg data]] + +The caller of this command registers the given [arg name] as its name +in the configured name service, and additionally associates a piece of +[arg data] with it. The service does nothing with this information +beyond storing it and delivering it as part of search results. The +meaning is entirely up to the applications using the name service. + +[para] + +A generally useful choice would for example be an identifier for a +communication endpoint managed by the package [package comm]. Anybody +retrieving the name becomes immediately able to talk to this endpoint, +i.e. the registering application. + +[para] + +Of further importance is that a caller can register itself under more +than one name, and each name can have its own piece of [arg data]. + +[para] + +Note that the name service, and thwerefore this command, will throw an +error if the chosen name is already registered. + +[call [cmd ::nameserv::release]] + +Invoking this command releases all names (and their data) registered +by all previous calls to [cmd ::nameserv::bind] of this client. Note +that the name service will run this command implicitly when it loses +the connection to this client. + +[call [cmd ::nameserv::search] [opt [option -async]|[option -continuous]] [opt [arg pattern]]] + +This command searches the name service for all registered names +matching the specified glob-[arg pattern]. If not specified the +pattern defaults to [const *], matching everything. The result of the +command is a dictionary mapping the matching names to the data +associated with them at [term bind]-time. + +[para] + +If either option [option -async] or [option -continuous] were +specified the result of this command changes and becomes the Tcl +command of an object holding the actual result. + +These two options are supported if and only if the service the client +is connected to supports the protocol feature +[term Search/Continuous]. + +[para] + +For [option -async] the result object is asynchronously filled with +the entries matching the pattern at the time of the search and then +not modified any more. + +The option [option -continuous] extends this behaviour by additionally +continuously monitoring the service for the addition and removal of +entries which match the pattern, and updating the object's contents +appropriately. + +[para] + +[emph Note] that the caller is responsible for configuring the object +with a callback for proper notification when the current result (or +further changes) arrive. + +[para] + +For more information about this object see section +[sectref {ASYNCHRONOUS AND CONTINUOUS SEARCHES}]. + +[call [cmd ::nameserv::protocol]] + +This command returns the highest version of the name service protocol +supported by the package. + +[call [cmd ::nameserv::server_protocol]] + +This command returns the highest version of the name service protocol +supported by the name service the client is currently connected to. + +[call [cmd ::nameserv::server_features]] + +This command returns a list containing the names of the features of +the name service protocol which are supported by the name service the +client is currently connected to. + +[call [cmd ::nameserv::cget] [option -option]] + +This command returns the currently configured value for the specified +[option -option]. The list of supported options and their meaning can +be found in section [sectref OPTIONS]. + +[call [cmd ::nameserv::configure]] + +In this form the command returns a dictionary of all supported +options, and their current values. The list of supported options and +their meaning can be found in section [sectref OPTIONS]. + +[call [cmd ::nameserv::configure] [option -option]] + +In this form the command is an alias for +"[cmd ::nameserv::cget] [option -option]]". + +The list of supported options and their meaning can be found in +section [sectref OPTIONS]. + +[call [cmd ::nameserv::configure] "[option -option] [arg value]..."] + +In this form the command is used to configure one or more of the +supported options. At least one option has to be specified, and each +option is followed by its new value. + +The list of supported options and their meaning can be found in +section [sectref OPTIONS]. + +[para] + +This form can be used only as long as the client has not contacted the +name service yet. After contact has been made reconfiguration is not +possible anymore. This means that this form of the command is for the +initalization of the client before it use. + +The command forcing a contact with the name service are + +[list_begin commands] +[cmd_def bind] +[cmd_def release] +[cmd_def search] +[cmd_def server_protocol] +[cmd_def server_features] +[list_end] +[list_end] + +[section {CONNECTION HANDLING}] + +The client automatically connects to the service when one of the +commands below is run for the first time, or whenever one of the +commands is run after the connection was lost, when it was lost. + +[para] +[list_begin commands] +[cmd_def bind] +[cmd_def release] +[cmd_def search] +[cmd_def server_protocol] +[cmd_def server_features] +[list_end] +[para] + +Since version 0.2 of the client it will generate an event when the +connection is lost, allowing higher layers to perform additional +actions. This is done via the support package [package uevent]. This +and all other name service related packages hereby reserve the +uevent-tag [term nameserv]. All their events will be posted to that +tag. + +[section EVENTS] + +This package generates only one event, [term lost-connection]. The +detail information provided to that event is a Tcl dictionary. The +only key contained in the dictionnary is [const reason], and its value +will be a string describing why the connection was lost. + +This string is supplied by the underlying communication package, +i.e. [package comm]. + +[section OPTIONS] + +The options supported by the client are for the specification of which +name service to contact, i.e. of the location of the name service. + +They are: + +[list_begin options] +[opt_def -host [arg name]|[arg ipaddress]] + +This option specifies the host name service to contact is running on, +either by [arg name], or by [arg ipaddress]. The initial default is +[const localhost], i.e. it is expected to contact a name service +running on the same host as the application using this package. + +[opt_def -port [arg number]] + +This option specifies the port the name service to contact is +listening on. It has to be a positive integer number (> 0) not greater +than 65536 (unsigned short). The initial default is the number +returned by the command [cmd ::nameserv::common::port], as provided by +the package [package ::nameserv::common]. + +[list_end] + +[section {ASYNCHRONOUS AND CONTINUOUS SEARCHES}] + +Asynchronous and continuous searches are invoked by using either +option [option -async] or [option -continuous] as argument to the +command [cmd ::nameserv::search]. + +[para] + +[emph Note] that these two options are supported if and only if the +service the client is connected to supports the protocol feature +[term Search/Continuous]. The service provided by the package +[package ::nameserv::server] does this since version 0.3. + +[para] + +For such searches the result of the search command is the Tcl command +of an object holding the actual result. The API provided by these +objects is: + +[list_begin definitions] + +[def Options:] +[list_begin options] +[opt_def -command [arg command_prefix]] + +This option has to be set if a user of the result object wishes to get +asynchronous notifications when the search result or changes to it +arrive. + +[para] + +[emph Note] that while it is possible to poll for the arrival of the +initial search result via the method [method filled], and for +subsequent changes by comparing the output of method [method getall] +against a saved copy, this is not the recommended behaviour. Setting +the [option -command] callback and processing the notifications as +they arrive is much more efficient. + +[para] + +The [arg command_prefix] is called with two arguments, the type of +change, and the data of the change. The type is either [const add] or +[const remove], indicating new data, or deleted data, respectively. +The data of the change is always a dictionary listing the +added/removed names and their associated data. + +[para] + +The first change reported for a search is always the set of matching +entries at the time of the search. + +[list_end] + +[def Methods:] +[list_begin definitions] + +[call [cmd \$result] [method destroy]] + +Destroys the object and cancels any continuous monitoring of the +service the object may have had active. + +[call [cmd \$result] [method filled]] + +The result is a boolean value indicating whether the search result has +already arrived ([const True]), or not ([const False]). + +[call [cmd \$result] [method get] [arg name]] + +Returns the data associated with the given [arg name] at +[term bind]-time. + +[call [cmd \$result] [method names]] + +Returns a list containing all names known to the object at the time of +the invokation. + +[call [cmd \$result] [method size]] + +Returns an integer value specifying the size of the result at the time +of the invokation. + +[call [cmd \$result] [method getall] [opt [arg pattern]]] + +Returns a dictionary containing the search result at the time of the +invokation, mapping the matching names to the data associated with +them at [term bind]-time. + +[list_end] +[list_end] + +[section HISTORY] +[list_begin definitions] +[def 0.3.1] +Fixed SF Bug 1954771. + +[def 0.3] +Extended the client with the ability to perform asynchronous and +continuous searches. + +[def 0.2] +Extended the client with the ability to generate events when it loses +its connection to the name service. Based on package [package uevent]. + +[def 0.1] +Initial implementation of the client. +[list_end] + +[vset CATEGORY nameserv] +[include ../doctools2base/include/feedback.inc] +[manpage_end] diff --git a/tcllib/modules/nns/nns_cluster.tcl b/tcllib/modules/nns/nns_cluster.tcl new file mode 100644 index 0000000..6d49c1a --- /dev/null +++ b/tcllib/modules/nns/nns_cluster.tcl @@ -0,0 +1,499 @@ +# -*- tcl -*- +# ### ### ### ######### ######### ######### +## Name Service - Cluster + +# ### ### ### ######### ######### ######### +## Requirements + +package require Tcl 8.5 +package require comm ; # Generic message transport +package require interp ; # Interpreter helpers. +package require logger ; # Tracing internal activity +package require uuid +package require cron +package require nettool 0.4 +package require udp + +namespace eval ::comm {} +::namespace eval ::cluster {} + +### +# This package implements an ad/hoc zero configuration +# like network of comm (and other) network connections +### + +### +# topic: 5cffdc91e554c923ebe43df13fac77d5 +### +proc ::cluster::broadcast {args} { + if {$::cluster::config(debug)} { + puts [list $::cluster::local_pid SEND $args] + } + while {[catch { + set sock [listen] + puts -nonewline $sock [list [pid] {*}$args] + flush $sock + } error]} { + set ::cluster::broadcast_sock {} + if {$::cluster::config(debug)} { + puts "Broadcast ERR: $error - Reopening Socket" + ::cluster::sleep 2000 + } else { + # Double the delay + ::cluster::sleep 250 + } + } +} + +### +# topic: 963e24601d0dc61580c9727a74cdba67 +### +proc ::cluster::cname rawname { + # Convert rawname to a canonical name + if {[string first @ $rawname] < 0 } { + return $rawname + } + lassign [split $rawname @] service host + if {$host eq {}} { + set host * + } + if {$host in {local localhost}} { + set host [::cluster::self] + } + return $service@$host +} + +### +# topic: 3f5f9e197cc9666dd7953d97fef34019 +### +proc ::cluster::ipaddr macid { + # Convert rawname to a canonical name + if {$macid eq [::cluster::self]} { + return 127.0.0.1 + } + foreach {servname dat} [search [cname *@$macid]] { + if {[dict exists $dat ipaddr]} { + return [dict get $dat ipaddr] + } + } + ### + # Do a lookup + ### + error "Could not locate $macid" +} + +### +# topic: e57db306f0e931d7febb5ad1f9cb2247 +### +proc ::cluster::listen {} { + variable broadcast_sock + if {$broadcast_sock != {}} { + return $broadcast_sock + } + variable discovery_port + variable discovery_group + set broadcast_sock [udp_open $discovery_port reuse] + fconfigure $broadcast_sock -buffering none -blocking 0 \ + -mcastadd $discovery_group \ + -remote [list $discovery_group $discovery_port] + fileevent $broadcast_sock readable [list [namespace current]::UDPPacket $broadcast_sock] + ::cron::every cluster_heartbeat 30 ::cluster::heartbeat + + return $broadcast_sock +} + +### +# topic: 2a33c825920162b0791e2cdae62e6164 +### +proc ::cluster::UDPPacket sock { + variable ptpdata + set pid [pid] + set packet [string trim [read $sock]] + set peer [fconfigure $sock -peer] + + if {![string is ascii $packet]} return + if {![::info complete $packet]} return + + set sender [lindex $packet 0] + if {$::cluster::config(debug)} { + puts [list $::cluster::local_pid RECV $peer $packet] + } + if { $sender eq [pid] } { + # Ignore messages from myself + return + } + + set messagetype [lindex $packet 1] + set messageinfo [lrange $packet 2 end] + switch -- [string toupper $messagetype] { + -SERVICE { + set serviceurl [lindex $messageinfo 0] + set serviceinfo [lindex $messageinfo 1] + dict set serviceinfo ipaddr [lindex $peer 0] + dict set serviceinfo closed 1 + Service_Remove $serviceurl $serviceinfo + } + ~SERVICE { + set ::cluster::recv_message 1 + set serviceurl [lindex $messageinfo 0] + set serviceinfo [lindex $messageinfo 1] + dict set serviceinfo ipaddr [lindex $peer 0] + Service_Modified $serviceurl $serviceinfo + set ::cluster::ping_recv($serviceurl) [clock seconds] + } + +SERVICE { + set ::cluster::recv_message 1 + set serviceurl [lindex $messageinfo 0] + set serviceinfo [lindex $messageinfo 1] + dict set serviceinfo ipaddr [lindex $peer 0] + Service_Add $serviceurl $serviceinfo + set ::cluster::ping_recv($serviceurl) [clock seconds] + } + DISCOVERY { + variable config + ::cluster::heartbeat + if {$config(local_registry)==1} { + variable ptpdata + # A local registry barfs back all data that is sees + set now [clock seconds] + foreach {url info} [array get ptpdata] { + broadcast ~SERVICE $url $info + } + } + } + LOG { + set serviceurl [lindex $messageinfo 0] + set serviceinfo [lindex $messageinfo 1] + Service_Log $serviceurl $serviceinfo + } + ?WHOIS { + set wmacid [lindex $messageinfo 0] + if { $wmacid eq [::cluster::self] } { + broadcast +WHOIS [::cluster::self] + } + } + PONG { + set serviceurl [lindex $messageinfo 0] + set serviceinfo [lindex $messageinfo 1] + Service_Modified $serviceurl $serviceinfo + set ::cluster::ping_recv($serviceurl) [clock seconds] + } + PING { + set serviceurl [lindex $messageinfo 0] + foreach {url info} [search_local $serviceurl] { + broadcast PONG $url $info + } + } + } +} + +proc ::cluster::ping {rawname} { + set rcpt [cname $rawname] + set ::cluster::ping_recv($rcpt) 0 + set starttime [clock seconds] + set sleeptime 1 + while 1 { + broadcast PING $rcpt + update + if {$::cluster::ping_recv($rcpt)} break + if {([clock seconds] - $starttime) > 120} { + error "Could not locate a local dispatch service" + } + sleep [incr sleeptime $sleeptime] + } +} + +proc ::cluster::publish {url infodict} { + variable local_data + dict set infodict macid [self] + dict set infodict pid [pid] + set local_data($url) $infodict + broadcast +SERVICE $url $infodict +} + +proc ::cluster::heartbeat {} { + variable ptpdata + variable config + + set now [clock seconds] + foreach {item info} [array get ptpdata] { + set remove 0 + if {[dict exists $info closed] && [dict get $info closed]} { + set remove 1 + } + if {[dict exists $info updated] && ($now - [dict get $info updated])>$config(discovery_ttl)} { + set remove 1 + } + if {$remove} { + Service_Remove $item $info + } + } + ### + # Broadcast the status of our local services + ### + variable local_data + foreach {url info} [array get local_data] { + broadcast ~SERVICE $url $info + } + ### + # Trigger any cluster events that haven't fired off + ### + foreach {eventid info} [array get ::cluster::events] { + if {$info eq "-1"} { + unset ::cluster::events($eventid) + } else { + lassign $info seconds ms + if {$seconds < $now} { + set ::cluster::events($eventid) -1 + } + } + } +} + +proc ::cluster::info url { + variable local_data + return [array get local_data $url] +} + +proc ::cluster::unpublish {url infodict} { + variable local_data + foreach {field value} $infodict { + dict set local_data($url) $field $value + } + set info [lindex [array get local_data $url] 1] + broadcast -SERVICE $url $info + unset -nocomplain local_data($url) +} + +proc ::cluster::configure {url infodict {send 1}} { + variable local_data + if {![::info exists local_data($url)]} return + foreach {field value} $infodict { + dict set local_data($url) $field $value + } + if {$send} { + broadcast ~SERVICE $url $local_data($url) + update + } +} + +proc ::cluster::get_free_port {{startport 50000}} { + ::cluster::listen + ::cluster::broadcast DISCOVERY + after 10000 {set ::cluster::recv_message 0} + # Wait for a pingback or timeout + vwait ::cluster::recv_message + cluster::sleep 2000 + + set macid [::cluster::macid] + set port $startport + set conflict 1 + while {$conflict} { + set conflict 0 + set port [::nettool::find_port $port] + foreach {url info} [search *@[macid]] { + if {[dict exists $info port] && [dict get $info port] eq $port} { + incr port + set conflict 1 + break + } + } + update + } + return $port +} + +proc ::cluster::log args { + broadcast LOG {*}$args +} + +proc ::cluster::LookUp {rawname} { + set self [self] + foreach {servname dat} [search [cname $rawname]] { + # Ignore services in the process of closing + if {[dict exists $dat macid] && [dict get $dat macid] eq $self} { + set ipaddr 127.0.0.1 + } elseif {![dict exists $dat ipaddr]} { + set ipaddr [ipaddr [lindex [split $servname @] 1]] + } else { + set ipaddr [dict get $dat ipaddr] + } + if {![dict exists $dat port]} continue + if {[llength $ipaddr] > 1} { + ## Sort out which ipaddr is proper later + # for now take the last one + set ipaddr [lindex [dict get $dat ipaddr] end] + } + set port [dict get $dat port] + return [list $port $ipaddr] + } + return {} +} + +### +# topic: 2c04e58c7f93798f9a5ed31a7f5779ab +### +proc ::cluster::resolve {rawname} { + set result [LookUp $rawname] + if { $result ne {} } { + return $result + } + broadcast DISCOVERY + sleep 250 + set result [LookUp $rawname] + if { $result ne {} } { + return $result + } + error "Could not locate $rawname" +} + +### +# topic: 6c7a0a3a8cb2a7ae98ff0dba960c37a7 +### +proc ::cluster::pid {} { + variable local_pid + return $local_pid +} + +proc ::cluster::macid {} { + variable local_macid + return $local_macid +} + +proc ::cluster::self {} { + variable local_macid + return $local_macid +} + +### +# topic: f1b71ff12a8ac10373c67ac5d973cd81 +### +proc ::cluster::send {service command args} { + set commid [resolve $service] + return [::comm::comm send $commid $command {*}$args] +} + +proc ::cluster::throw {service command args} { + set commid [LookUp $service] + if { $commid eq {} } { + return + } + if [catch {::comm::comm send -async $commid $command {*}$args} reply] { + puts $stderr "ERR: SEND $service $reply" + } +} + +proc ::cluster::sleep ms { + set eventid [incr ::cluster::eventcount] + set ::cluster::event($eventid) [list [clock seconds] [expr {[clock milliseconds]+$ms}]] + after $ms set ::cluster::event($eventid) -1 + vwait ::cluster::event($eventid) +} + +### +# topic: c8475e832c912e962f238c61580b669e +### +proc ::cluster::search pattern { + set result {} + variable ptpdata + foreach {service dat} [array get ptpdata $pattern] { + foreach {field value} $dat { + dict set result $service $field $value + } + } + variable local_data + foreach {service dat} [array get local_data $pattern] { + foreach {field value} $dat { + dict set result $service $field $value + dict set result $service ipaddr 127.0.0.1 + } + } + return $result +} + +proc ::cluster::is_local pattern { + variable local_data + if {[array exists local_data $pattern]} { + return 1 + } + if {[array exists local_data [cname $pattern]]} { + return 1 + } + return 0 +} + +proc ::cluster::search_local pattern { + set result {} + variable local_data + foreach {service dat} [array get local_data $pattern] { + foreach {field value} $dat { + dict set result $service $field $value + } + } + return $result +} + +proc ::cluster::Service_Add {serviceurl serviceinfo} { + # Code to register the presence of a service + if {[dict exists $serviceinfo pid] && [dict get $serviceinfo pid] eq [pid] } { + # Ignore attempts to overwrite locally managed services from the network + return + } + variable ptpdata + set ptpdata($serviceurl) $serviceinfo + dict set ptpdata($serviceurl) updated [clock seconds] +} + +proc ::cluster::Service_Remove {serviceurl serviceinfo} { + # Code to register the loss of a service + if {[dict exists $serviceinfo pid] && [dict get $serviceinfo pid] eq [pid] } { + # Ignore attempts to overwrite locally managed services from the network + return + } + variable ptpdata + unset -nocomplain ptpdata($serviceurl) +} + +proc ::cluster::Service_Modified {serviceurl serviceinfo} { + # Code to register an update to a service + if {[dict exists $serviceinfo pid] && [dict get $serviceinfo pid] eq [pid] } { + # Ignore attempts to overwrite locally managed services from the network + return + } + variable ptpdata + foreach {field value} $serviceinfo { + dict set ptpdata($serviceurl) $field $value + } + dict set ptpdata($serviceurl) updated [clock seconds] +} + +proc ::cluster::Service_Log {service data} { + # Code to register an event +} + +### +# topic: d3e48e31cc4baf81395179f4097fee1b +### +namespace eval ::cluster { + # Number of seconds to "remember" data + variable config + array set config { + debug 0 + discovery_ttl 300 + local_registry 0 + } + variable eventcount 0 + variable cache {} + variable broadcast_sock {} + variable cache_maxage 500 + variable discovery_port 38573 + # Currently an unassigned group in the + # Local Network Control Block (224.0.0/24) + # See: RFC3692 and http://www.iana.org + variable discovery_group 224.0.0.200 + variable local_port {} + variable local_macid [lindex [::nettool::mac_list] 0] + variable local_pid [::uuid::uuid generate] +} + +package provide nameserv::cluster 0.2.3 diff --git a/tcllib/modules/nns/nns_cluster.test b/tcllib/modules/nns/nns_cluster.test new file mode 100644 index 0000000..cdd4fac --- /dev/null +++ b/tcllib/modules/nns/nns_cluster.test @@ -0,0 +1,195 @@ +# -*- tcl -*- +# common.test: Tests for the common code of the name service +# +# Sourcing this file into Tcl runs the tests and generates output for +# errors. No output means no errors were found. + +# ------------------------------------------------------------------------- + +set testutilsscript [file join \ + [file dirname [file dirname [file join [pwd] [info script]]]] \ + devtools testutilities.tcl] +source $testutilsscript + +package require tcltest +testsNeedTcl 8.5 +testsNeedTcltest 1.0 + +set ::WHOAMI Main + +support { + use snit/snit2.tcl snit + use comm/comm.tcl comm + use dns/ip.tcl ip + use nettool/nettool.tcl nettool + use cron/cron.tcl cron + use uuid/uuid.tcl uuid + use interp/interp.tcl interp + use log/logger.tcl logger + use md5/md5x.tcl md5 +} +testing { + useLocal nns_cluster.tcl nameserv::cluster +} + +### +# Create a server in a seperate interp +### +interp create server +interp eval server [list set testutilsscript $testutilsscript] +interp eval server { + source $testutilsscript + set ::WHOAMI Server + + package require tcltest + testsNeedTcl 8.5 + testsNeedTcltest 1.0 + + support { + use snit/snit2.tcl snit + use comm/comm.tcl comm + use dns/ip.tcl ip + use nettool/nettool.tcl nettool + use cron/cron.tcl cron + use uuid/uuid.tcl uuid + use interp/interp.tcl interp + use log/logger.tcl logger + use md5/md5x.tcl md5 + } + testing { + use nns/nns_cluster.tcl nameserv::cluster + } + set ::cluster::local_pid SERVER + ::cluster::publish nns@[::cluster::macid] {} + update +} +set ::cluster::local_pid MAIN +set macid [::cluster::macid] +set myport [::nettool::allocate_port 10000] + +::cluster::ping nns@$macid +set data [::cluster::search *] +test cluster-comm-1.0 {Publish service - NNS} { + dict exists $data nns@[::cluster::macid] +} {1} + +test cluster-comm-1.1 {Check that non-existant service does not exist} { + dict exists $data foo@bar +} {0} + +### +# Create a phony service +### +set now [clock seconds] +::cluster::publish foo@bar [list clocktime $now] +# The windows event loop needs a breather +::cluster::ping nns@$macid + +set data [::cluster::search *] +test cluster-comm-2.0 {Publish service - NNS} { + dict exists $data nns@[::cluster::macid] +} {1} +test cluster-comm-2.1 {Check that new service does exists} { + dict exists $data foo@bar +} {1} + +### +# Modify a service +### +::cluster::configure foo@bar {color pink} +::cluster::ping nns@$macid + +set data [::cluster::search foo@bar] +test cluster-comm-2.3 {Modify a service} { + dict get $data foo@bar color +} {pink} + +::cluster::configure foo@bar {color blue} +::cluster::ping nns@$macid + +set data [::cluster::search foo@bar] +test cluster-comm-2.4 {Modify a service} { + dict get $data foo@bar color +} {blue} + + +### +# Create another client in a seperate interp +### +interp create otherclient +interp eval otherclient [list set testutilsscript $testutilsscript] +interp eval otherclient { + source $testutilsscript + set ::WHOAMI Other + + package require tcltest + testsNeedTcl 8 + testsNeedTcltest 1.0 + + support { + use snit/snit2.tcl snit + use comm/comm.tcl comm + use dns/ip.tcl ip + use nettool/nettool.tcl nettool + use cron/cron.tcl cron + use uuid/uuid.tcl uuid + use interp/interp.tcl interp + use log/logger.tcl logger + use md5/md5x.tcl md5 + } + testing { + use nns/nns_cluster.tcl nameserv::cluster + } + + ### + # Cheat and let this server know the server is local + ### + set macid [::cluster::macid] + set myport [::nettool::allocate_port 10000] + + set url other@$macid + ::comm::comm new $url -port $myport -local 0 -listen 1 + ::cluster::publish $url [list port $myport protocol comm class comm] +} +::cluster::ping nns@$macid + +set data [::cluster::search *] +test cluster-comm-3.0 {Publish service - NNS} { + dict exists $data nns@[::cluster::macid] +} {1} +test cluster-comm-3.1 {Check that new service does exists} { + dict exists $data foo@bar +} {1} +test cluster-comm-3.3 {Check that other service does exists} { + dict exists $data other@[::cluster::macid] +} {1} + +test cluster-comm-3.3 {Check that other service does exists} { + set chan [::cluster::resolve other@[::cluster::macid]] + ::comm::comm send $chan {set foo b} +} {b} + +### +# Remove the phony service +### +::cluster::unpublish foo@bar {} +::cluster::ping nns@$macid + +set data [::cluster::search *] +test cluster-comm-4.0 {Publish service - NNS} { + dict exists $data nns@[::cluster::macid] +} {1} +test cluster-comm-4.1 {Check that service is closed} { + dict exists $data foo@bar +} {0} + +### +# Have a non-existant service fail +### +test cluster-comm-5.0 {Service lookup failture} { + catch {cluster::resolve foo@bar} pat +} {1} +#puts $pat + +testsuiteCleanup +return diff --git a/tcllib/modules/nns/nns_common.man b/tcllib/modules/nns/nns_common.man new file mode 100644 index 0000000..8de8ca0 --- /dev/null +++ b/tcllib/modules/nns/nns_common.man @@ -0,0 +1,47 @@ +[manpage_begin nameserv::common n 0.1] +[see_also nameserv::client(n)] +[see_also nameserv::server(n)] +[keywords client] +[keywords {name service}] +[keywords server] +[copyright {2007-2008 Andreas Kupries <andreas_kupries@users.sourceforge.net>}] +[moddesc {Name service facility}] +[titledesc {Name service facility, shared definitions}] +[category Networking] +[require Tcl 8] +[require nameserv::common [opt 0.1]] +[description] + +Please read [term {Name service facility, introduction}] first. + +[para] + +This package is internal and of no interest to users. It provides the +commands of the name service facility which are shared by the client +and server implemented by the packages [package nameserv::server] and +[package nameserv] (the client). + +[para] + +This service is built in top of and for the package [package comm]. +It has nothing to do with the Internet's Domain Name System. If the +reader is looking for a package dealing with that please see Tcllib's +packages [package dns] and [package resolv]. + +[section API] + +The package exports a single command, as specified below: + +[list_begin definitions] + +[call [cmd ::nameserv::common::port]] + +The result returned by the command is the id of the default TCP/IP +port a nameservice server will listen on, and a name service client +will try to connect to. + +[list_end] + +[vset CATEGORY nameserv] +[include ../doctools2base/include/feedback.inc] +[manpage_end] diff --git a/tcllib/modules/nns/nns_intro.man b/tcllib/modules/nns/nns_intro.man new file mode 100644 index 0000000..4ed3551 --- /dev/null +++ b/tcllib/modules/nns/nns_intro.man @@ -0,0 +1,128 @@ +[comment {-*- tcl -*- doctools manpage}] +[manpage_begin nns_intro n 1.0] +[see_also nameserv(n)] +[see_also nameserv::auto(n)] +[see_also nameserv::common(n)] +[see_also nameserv::protocol(n)] +[see_also nameserv::server(n)] +[see_also nnsd(n)] +[see_also nss(n)] +[keywords client] +[keywords {name service}] +[keywords server] +[copyright {2008 Andreas Kupries <andreas_kupries@users.sourceforge.net>}] +[moddesc {Name service facility}] +[titledesc {Name service facility, introduction}] +[category Networking] +[description] +[para] + +[term nns] (short for [emph {nano nameservice}]) is a facility built +for the package [package comm], adding a simple name service to it. +It is also built on top of [package comm], using it for the exchange +of messages between the client and server parts. + +[para] + +This name service facility has nothing to do with the Internet's +[term {Domain Name System}], otherwise known as [term DNS]. If the +reader is looking for a package dealing with that please see either of +the packages [package dns] and [package resolv], both found in Tcllib +too. + +[para] + +Tcllib provides 2 applications and 4 packages which are working +together and provide access to the facility at different levels. + +[section Applications] + +The application [syscmd nnsd] provides a simple name server which can +be run by anybody anywhere on their system, as they see fit. + +It is also an example on the use of the server-side package +[package nameserv::server]. + +[para] + +Complementing this server is the [syscmd nns] client application. + +A possible, but no very sensible use would be to enter name/port +bindings into a server from a shell script. Not sensible, as shell +scripts normally do not provide a [package comm]-based service. + +[para] + +The only case for this to make some sense would be in a shell script +wrapped around a Tcl script FOO which is using comm, to register the +listening port used by FOO. + +However even there it would much more sensible to extend FOO to use +the nameservice directly. And in regard on how to that [syscmd nns] +can be used as both example and template. + +Beyond that it may also be useful to perform nameservice queries from +shell scripts. + +[para] + +The third application, [syscmd nnslog] is a stripped down form of the +[syscmd nns] client application. It is reduced to perform a continuous +search for all changes and logs all received events to stdout. + +[para] + +Both clients use the [package nameserv::auto] package to automatically +hande the loss and restoration of the connection to the server. + +[section Packages] + +The two main packages implementing the service are [package nameserv] +and [package nameserv::server], i.e. client and server. The latter has +not much of an API, just enough to start, stop, and configure it. See +the application [syscmd nnsd] on how to use it. + +[para] + +The basic client, in package [package nameserv], provides the main API +to manipulate and query the service. An example of its use is the +application [syscmd nns]. + +[para] + +The second client package, [package nameserv::auto] is API compatible +to the basic client, but provides the additional functionality that it +will automatically restore data like bound names when the connection +to the name service was lost and then reestablished. I.e. it +automatically detects the loss of the server and re-enters the data +when the server comes back. + +[para] + +The package [package nameserv::common] is of no interest to users. It +is an internal package containing code and definitions common to the +packages [package nameserv] and [package nameserv::server]. + +[para] + +All packages use the [package uevent] package for the reporting of +special circumstances via events, and reserve the uevent-tag +[term nameserv] for their exclusive use. All their events will be +posted to that tag. + +[section Internals] + +The document [term {Name service facility, client/server protocol}] +specifies the protocol used by the packages [package nameserv] and +[package nameserv::server] to talk to each other. It is of no interest +to users of either the packages or applications. + +[para] + +Developers wishing to modify and/or extend or to just understand the +internals of the nameservice facility however are strongly advised to +read it. + +[vset CATEGORY nameserv] +[include ../doctools2base/include/feedback.inc] +[manpage_end] diff --git a/tcllib/modules/nns/nns_protocol.man b/tcllib/modules/nns/nns_protocol.man new file mode 100644 index 0000000..968de1f --- /dev/null +++ b/tcllib/modules/nns/nns_protocol.man @@ -0,0 +1,182 @@ +[manpage_begin nameserv::protocol n 0.1] +[see_also comm_wire(n)] +[see_also nameserv(n)] +[see_also nameserv::server(n)] +[keywords comm] +[keywords {name service}] +[keywords protocol] +[copyright {2007-2008 Andreas Kupries <andreas_kupries@users.sourceforge.net>}] +[moddesc {Name service facility}] +[titledesc {Name service facility, client/server protocol}] +[category Networking] +[description] + +The packages [package nameserv::server], [package nameserv], and +[package nameserv::common] provide a simple unprotected name service +facility for use in small trusted environments. + +[para] + +Please read [term {Name service facility, introduction}] first. + +[para] + +This document contains the specification of the network protocol which +is used by client and server to talk to each other, enabling +implementations of the same protocol in other languages. + +[section {Nano Name Service Protocol Version 1}] + +This protocol defines the basic set of messages to be supported by a +name service, also called the [term Core] feature. + +[subsection {Basic Layer}] + +The basic communication between client and server is done using the +remote-execution protocol specified by the Tcl package [package comm]. +The relevant document specifying its on-the-wire protocol can be found +in [term comm_wire]. + +[para] + +All the scripts exchanged via this protocol are single commands in +list form and thus can be interpreted as plain messages instead of as +Tcl commands. The commands/messages specified in the next section are +the only commands understood by the server-side. Command and variable +substitutions are not allowed within the messages, i.e. arguments have +to be literal values. + +[para] + +The protocol is synchronous. I.e. for each message sent a response is +expected, and has to be generated. All messages are sent by the client. +The server does not sent messages, only responses to messages. + +[subsection {Message Layer}] + +[list_begin definitions] + +[call [method Bind] [arg name] [arg data]] + +The client sends this message when it registers itself at the service +with a [arg name] and some associated [arg data]. The server has to +send an error response if the [arg name] is already in use. Otherwise +the response has to be an empty string. + +[para] + +The server has to accept multiple names for the same client. + +[call [method Release]] + +The client sends this message to unregister all names it is known +under at the service. The response has to be an empty string, always. + +[call [method Search] [arg pattern]] + +The client sends this message to search the service for names matching +the glob-[arg pattern]. The response has to be a dictionary containing +the matching names as keys, and mapping them to the data associated +with it at [method Bind]-time. + +[call [method ProtocolVersion]] + +The client sends this message to query the service for the highest +version of the name service protocol it supports. The response has to +be a positive integer number. + +[para] + +Servers supporting only [term {Nano Name Service Protocol Version 1}] +have to return [const 1]. + +[call [method ProtocolFeatures]] + +The client sends this message to query the service for the features of +the name service protocol it supports. The response has to be a +list containing feature names. + +[para] + +Servers supporting only [term {Nano Name Service Protocol Version 1}] +have to return [const {{Core}}]. + +[list_end] + +[section {Nano Name Service Protocol Extension: Continuous Search}] + +This protocol defines an extended set of messages to be supported by a +name service, also called the [term Search/Continuous] feature. This +feature defines additional messages between client and server, and is +otherwise identical to version 1 of the protocol. See the last section +for the details of our foundation. + +[para] + +A service supporting this feature has to put the feature name +[const Search/Continuous] into the list of features returned by the +message [term ProtocolFeatures]. + +[para] + +For this extension the protocol is asynchronous. No direct response is +expected for any of the messages in the extension. Furthermore the +server will start sending messages on its own, instead of only +responses to messages, and the client has to be able to handle these +notifications. + +[list_begin definitions] + +[call [method Search/Continuous/Start] [arg tag] [arg pattern]] + +The client sends this message to start searching the service for names +matching the glob-[arg pattern]. + +In contrast to the regular [term Search] request this one asks the +server to continuously monitor the database for the addition and +removal of matching entries and to notify the client of all such +changes. The particular search is identified by the [arg tag]. + +[para] + +No direct response is expected, rather the clients expect to be +notified of changes via explicit [term Search/Continuous/Result] +messages generated by the service. + +[para] + +It is further expected that the [arg tag] information is passed +unchanged to the [term Search/Continuous/Result] messages. This +tagging of the results enables clients to start multiple searches and +distinguish between the different results. + +[call [method Search/Continuous/Stop] [arg tag]] + +The client sends this message to stop the continuous search identified +by the [arg tag]. + +[call [method Search/Continuous/Change] [arg tag] [method add]|[method remove] [arg response]] + +This message is sent by the service to clients with active continuous +searches to transfer found changes. The first such message for a new +continuous search has to contains the current set of matching entries. + +[para] + +To ensure this a service has to generate an [method add]-message with +an empty [arg response] if there were no matching entries at the time. + +[para] + +The [arg response] has to be a dictionary containing the matching +names as keys, and mapping them to the data associated with it at +[method Bind]-time. + +The argument coming before the response tells the client whether the +names in the response were added or removed from the service. + +[list_end] + +[vset CATEGORY nameserv] +[include ../doctools2base/include/feedback.inc] +[manpage_end] diff --git a/tcllib/modules/nns/nns_server.man b/tcllib/modules/nns/nns_server.man new file mode 100644 index 0000000..b62ec61 --- /dev/null +++ b/tcllib/modules/nns/nns_server.man @@ -0,0 +1,145 @@ +[manpage_begin nameserv::server n 0.3.2] +[see_also nameserv::client(n)] +[see_also nameserv::common(n)] +[keywords {name service}] +[keywords server] +[copyright {2007-2008 Andreas Kupries <andreas_kupries@users.sourceforge.net>}] +[moddesc {Name service facility}] +[titledesc {Name service facility, Server}] +[category Networking] +[require Tcl 8.4] +[require nameserv::server [opt 0.3.2]] +[require comm] +[require interp] +[require logger] +[description] + +Please read [term {Name service facility, introduction}] first. + +[para] + +This package provides an implementation of the serviver side of the +name service facility queried by the client provided by the package +[package nameserv]. All information required by the server will be +held in memory. There is no persistent state. + +[para] + +This service is built in top of and for the package [package comm]. +It has nothing to do with the Internet's Domain Name System. If the +reader is looking for a package dealing with that please see Tcllib's +packages [package dns] and [package resolv]. + +[para] + +This server supports the [term Core] protocol feature, and since +version 0.3 the [term Search/Continuous] feature as well. + +[section API] + +The package exports five commands, as specified below: + +[list_begin definitions] + +[call [cmd ::nameserv::server::start]] + +This command starts the server and causes it to listen on the +configured port. From now on clients are able to connect and make +requests. The result of the command is the empty string. + +[para] + +Note that any incoming requests will only be handled if the +application the server is part of does enter an event loop after this +command has been run. + +[call [cmd ::nameserv::server::stop]] + +Invoking this command stops the server and releases all information it +had. Existing connections are shut down, and no new connections will +be accepted any longer. The result of the command is the empty string. + +[call [cmd ::nameserv::server::active?]] + +This command returns a boolean value indicating the state of the +server. The result will be [const true] if the server is active, +i.e. has been started, and [const false] otherwise. + +[call [cmd ::nameserv::server::cget] [option -option]] + +This command returns the currently configured value for the specified +[option -option]. The list of supported options and their meaning can +be found in section [sectref OPTIONS]. + +[call [cmd ::nameserv::server::configure]] + +In this form the command returns a dictionary of all supported +options, and their current values. The list of supported options and +their meaning can be found in section [sectref OPTIONS]. + +[call [cmd ::nameserv::server::configure] [option -option]] + +In this form the command is an alias for +"[cmd ::nameserv::server::cget] [option -option]]". + +The list of supported options and their meaning can be found in +section [sectref OPTIONS]. + +[call [cmd ::nameserv::server::configure] "[option -option] [arg value]..."] + +In this form the command is used to configure one or more of the +supported options. At least one option has to be specified, and each +option is followed by its new value. + +The list of supported options and their meaning can be found in +section [sectref OPTIONS]. + +[para] + +This form can be used only if the server is not active, i.e. has not +been started yet, or has been stopped. While the server is active it +cannot be reconfigured. + +[list_end] + +[section OPTIONS] + +The options supported by the server are for the specification of the +TCP port to listen on, and whether to accept non-local connections or +not. + +They are: + +[list_begin options] +[opt_def -localonly [arg bool]] + +This option specifies whether to accept only local connections +(-localonly 1) or remote connections as well (-localonly 0). The +default is to accept only local connections. + +[opt_def -port [arg number]] + +This option specifies the port the name service will listen on after +it has been started. It has to be a positive integer number (> 0) not +greater than 65536 (unsigned short). The initial default is the number +returned by the command [cmd ::nameserv::server::common::port], as +provided by the package [package ::nameserv::server::common]. + +[list_end] + +[section HISTORY] +[list_begin definitions] +[def 0.3] +Extended the server with the ability to perform asynchronous and +continuous searches. + +[def 0.2] +Changed name of -local switch to -localonly. + +[def 0.1] +Initial implementation of the server. +[list_end] + +[vset CATEGORY nameserv] +[include ../doctools2base/include/feedback.inc] +[manpage_end] diff --git a/tcllib/modules/nns/pkgIndex.tcl b/tcllib/modules/nns/pkgIndex.tcl new file mode 100644 index 0000000..e51fba5 --- /dev/null +++ b/tcllib/modules/nns/pkgIndex.tcl @@ -0,0 +1,10 @@ +if {![package vsatisfies [package provide Tcl] 8]} {return} +package ifneeded nameserv::common 0.1 [list source [file join $dir common.tcl]] + +if {![package vsatisfies [package provide Tcl] 8.4]} {return} +package ifneeded nameserv 0.4.2 [list source [file join $dir nns.tcl]] +package ifneeded nameserv::server 0.3.2 [list source [file join $dir server.tcl]] +package ifneeded nameserv::auto 0.3 [list source [file join $dir nns_auto.tcl]] + +if {![package vsatisfies [package provide Tcl] 8.5]} {return} +package ifneeded nameserv::cluster 0.2.3 [list source [file join $dir nns_cluster.tcl]] diff --git a/tcllib/modules/nns/server.tcl b/tcllib/modules/nns/server.tcl new file mode 100644 index 0000000..54ee688 --- /dev/null +++ b/tcllib/modules/nns/server.tcl @@ -0,0 +1,385 @@ +# -*- tcl -*- +# ### ### ### ######### ######### ######### +## Name Service - Server (Singleton) + +# ### ### ### ######### ######### ######### +## Requirements + +package require Tcl 8.4 +package require comm ; # Generic message transport +package require interp ; # Interpreter helpers. +package require logger ; # Tracing internal activity +package require nameserv::common ; # Common/shared utilities + +namespace eval ::nameserv::server {} + +# ### ### ### ######### ######### ######### +## API: Start, Stop + +proc ::nameserv::server::start {} { + variable comm + variable port + variable localonly + + log::debug "start" + if {$comm ne ""} return + + log::debug "start /granted" + + set interp [interp::createEmpty] + foreach msg { + Bind + Release + Search + Search/Continuous/Start + Search/Continuous/Stop + ProtocolVersion + ProtocolFeatures + } { + interp alias $interp $msg {} ::nameserv::server::$msg + } + + set comm [comm::comm new ::nameserv::server::COMM \ + -interp $interp \ + -port $port \ + -listen 1 \ + -local $localonly] + + $comm hook lost ::nameserv::server::LOST + + log::debug "UP @$port local-only $localonly" + return +} + +proc ::nameserv::server::stop {} { + variable comm + variable names + variable data + + log::debug "stop" + if {$comm eq ""} return + + log::debug "stop /granted" + + # This kills all existing connection and destroys the configured + # -interp as well. + + $comm destroy + set comm "" + + array unset names * + array unset data * + + log::debug "DOWN" + return +} + +proc ::nameserv::server::active? {} { + variable comm + return [expr {$comm ne ""}] +} + +# ### ### ### ######### ######### ######### +## INT: Protocol operations + +proc ::nameserv::server::ProtocolVersion {} {return 1} +proc ::nameserv::server::ProtocolFeatures {} {return {Core Search/Continuous}} + +proc ::nameserv::server::Bind {name cdata} { + variable comm + variable names + variable data + + set id [$comm remoteid] + + log::debug "bind ([list $name -> $cdata]), for $id" + + if {[info exists data($name)]} { + log::debug "bind failed, \"$name\" is already bound" + return -code error "Name \"$name\" is already bound" + } + + lappend names($id) $name + set data($name) $cdata + + Search/Continuous/NotifyAdd $name $cdata + return +} + +proc ::nameserv::server::Release {} { + variable comm + ReleaseId [$comm remoteid] + return +} + +proc ::nameserv::server::Search {pattern} { + variable data + return [array get data $pattern] +} + +proc ::nameserv::server::ReleaseId {id} { + variable names + variable data + variable searchi + + log::debug "release id $id" + + # Two steps. Release all searches the client may have open, then + # all names it may have bound. That last step may trigger + # notifications for searches by other clients. It must not trigger + # searches from the client just going away, hence their release + # first. + + foreach k [array names searchi [list $id *]] { + Search/Release $k + } + + if {[info exists names($id)]} { + set gone {} + foreach n $names($id) { + lappend gone $n $data($n) + catch {unset data($n)} + + log::debug "release name <$n>" + } + unset names($id) + + Search/Continuous/NotifyRelease $gone + } + return +} + +# ### ### ### ######### ######### ######### +## Support for continuous and async searches + +proc ::nameserv::server::Search/Continuous/Start {tag pattern} { + variable data + variable searchi + variable searchp + variable comm + + set id [$comm remoteid] + + # Register the search, then generate the initial response. + # Non-unique tags are silently discarded. Clients will wait + # forever. + + set k [list $id $tag] + + log::debug "search <$k>" + + if {[info exists searchi($k)]} { + log::debug "search already known" + return + } + + log::debug "search added" + + set searchi($k) $pattern + lappend searchp($pattern) $k + + $comm send -async $id [list Search/Continuous/Change \ + $tag add [array get data $pattern]] + return +} + +proc ::nameserv::server::Search/Continuous/Stop {tag} { + Search/Release [list [$comm remoteid] $tag] + return +} + +proc ::nameserv::server::Search/Release {k} { + variable searchi + variable searchp + + # Remove search information from the data store + + if {![info exists searchi($k)]} return + + log::debug "release search <$k>" + + set pattern $searchi($k) + unset searchi($k) + + set pos [lsearch -exact $searchp($pattern) $k] + if {$pos < 0} return + set new [lreplace $searchp($pattern) $pos $pos] + if {[llength $new]} { + # Shorten the callback list. + set searchp($pattern) $new + } else { + # Nothing monitors that pattern anymore, remove it completely. + unset searchp($pattern) + } + return +} + +proc ::nameserv::server::Search/Continuous/NotifyAdd {name val} { + variable searchp + + # Abort quickly if there are no searches waiting. + if {![array size searchp]} return + + foreach p [array names searchp] { + if {![string match $p $name]} continue + Notify $p add [list $name $val] + } + return +} + +proc ::nameserv::server::Search/Continuous/NotifyRelease {gone} { + variable searchp + + # Abort quickly if there are no searches waiting. + if {![array size searchp]} return + + array set m $gone + foreach p [array names searchp] { + set response [array get m $p] + if {![llength $response]} continue + Notify $p remove $response + } + return +} + +proc ::nameserv::server::Notify {p type response} { + variable searchp + variable comm + + foreach item $searchp($p) { + foreach {id tag} $item break + $comm send -async $id \ + [list Search/Continuous/Change $tag $type $response] + } + return +} + +# ### ### ### ######### ######### ######### +## Initialization - In-memory database + +namespace eval ::nameserv::server { + # Database + # search = list (id tag) : Searches are identified by client and a tag. + # + # array (id -> list (name)) : Names under which a connection is known. + # array (name -> data) : Data associated with a name. + # + # array (pattern -> list (search)) : Per pattern the list of searches using it. + # array (search -> pattern) : Pattern per active search. + # + # searchp <~~> names + # searchi <~~> data + + variable names ; array set names {} + variable data ; array set data {} + variable searchp ; array set searchp {} + variable searchi ; array set searchi {} +} + +# ### ### ### ######### ######### ######### +## INT: Connection management + +proc ::nameserv::server::LOST {args} { + # Currently just to see when a client goes away. + + upvar 1 id id chan chan reason reason + ReleaseId $id + return +} + +# ### ### ### ######### ######### ######### +## Initialization - System state + +namespace eval ::nameserv::server { + # Object command of the communication channel of the server. + # If present re-configuration is not possible. + + variable comm {} +} + +# ### ### ### ######### ######### ######### +## API: Configuration management (host, port) + +proc ::nameserv::server::cget {option} { + return [configure $option] +} + +proc ::nameserv::server::configure {args} { + variable localonly + variable port + variable comm + + if {![llength $args]} { + return [list -localonly $localonly -port $port] + } + if {[llength $args] == 1} { + # cget + set opt [lindex $args 0] + switch -exact -- $opt { + -localonly { return $localonly } + -port { return $port } + default { + return -code error "bad option \"$opt\", expected -localonly, or -port" + } + } + } + + # Note: Should -port be made configurable after communication has + # started it might be necessary to provide code to re-initialize + # the connections to all known clients using the new + # configuration. + + while {[llength $args]} { + set opt [lindex $args 0] + switch -exact -- $opt { + -localonly { + if {[llength $args] < 2} { + return -code error "value for \"$opt\" is missing" + } + # Todo: Check boolean + set new [lindex $args 1] + set args [lrange $args 2 end] + + if {$new == $localonly} continue + set localonly $new + if {$comm eq ""} continue + $comm configure -local $localonly + } + -port { + if {$comm ne ""} { + return -code error "Unable to configure an active server" + } + if {[llength $args] < 2} { + return -code error "value for \"$opt\" is missing" + } + # Todo: Check non-zero unsigned short integer + set port [lindex $args 1] + set args [lrange $args 2 end] + } + default { + return -code error "bad option \"$opt\", expected -localonly, or -port" + } + } + } + return +} + +# ### ### ### ######### ######### ######### +## Initialization - Tracing, Configuration + +logger::initNamespace ::nameserv::server +namespace eval ::nameserv::server { + # Port the server will listen on, and boolean flag determining + # acceptance of non-local connections. + + variable port [nameserv::common::port] + variable localonly 1 +} + +# ### ### ### ######### ######### ######### +## Ready + +package provide nameserv::server 0.3.2 + +## +# ### ### ### ######### ######### ######### |