chiark / gitweb /
Merge the `connect' and `watch' services.
authorMark Wooding <mdw@distorted.org.uk>
Sat, 13 Jul 2013 15:34:40 +0000 (16:34 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Tue, 14 Jan 2014 20:31:43 +0000 (20:31 +0000)
The result is called `connect', because that's the one which provided
scripted external interface.  But it's best looked at as a merge of
pieces of `connect' into `watch', followed by a rename.

debian/tripe-peer-services.install
debian/tripe-peer-services.postinst
peerdb/peers.in.5.in
svc/Makefile.am
svc/connect.8.in
svc/connect.in
svc/watch.8.in [deleted file]
svc/watch.in [deleted file]

index 3227e15576f7226fa223b3751f1a6ff08ff01572..9bd42cbed500e85e7fb93584be386a0f80dd7f11 100644 (file)
@@ -2,8 +2,6 @@ debian/tmp/usr/lib/tripe/services/connect
 debian/tmp/usr/share/man/man8/connect.8
 debian/tmp/usr/lib/tripe/services/conntrack
 debian/tmp/usr/share/man/man8/conntrack.8
-debian/tmp/usr/lib/tripe/services/watch
-debian/tmp/usr/share/man/man8/watch.8
 debian/tmp/usr/sbin/tripe-newpeers
 debian/tmp/usr/share/man/man8/tripe-newpeers.8
 debian/tmp/usr/share/man/man5/peers.in.5
index 870ba1aec3f82dbb252730ebef09193560e938dd..db937debbe1dc6750833d599e06aa575ce3607d0 100644 (file)
@@ -95,6 +95,6 @@ restart_services () {
   echo "."
 }
 
-restart_services conntrack connect watch
+restart_services conntrack connect
 
 #DEBHELPER#
index e31d0e37973a28b884b8da4245ae7a2a385ad292..240296e996ebaf405d0ed22d10b49106f04d0306 100644 (file)
@@ -44,6 +44,7 @@ file is a plain text configuration file.  It is read by
 in order to produce the
 .BR tripe.cdb (8)
 database used by services and other tools.
+.
 .SS "General structure"
 The configuration file is line-oriented.  Blank lines are ignored; lines
 beginning with a hash
@@ -115,6 +116,7 @@ Apart from its effect on lookups, as just described, the
 .B @inherits
 key is entirely ignored.  In particular, it is never written to the
 database.
+.
 .SS "Standard keys and their meanings"
 The following keys have meanings to programs in the TrIPE suite.  Other
 keys may be used by separately distributed extensions or for local use.
@@ -132,7 +134,7 @@ described below.
 .TP
 .B connect
 Shell command for initiating connection to this peer.  Used by
-.BR watch (8).
+.BR connect (8).
 .TP
 .B cork
 Don't initiate immediate key exchange.  Used by
@@ -140,15 +142,15 @@ Don't initiate immediate key exchange.  Used by
 .TP
 .B disconnect
 Shell command for closing down connection to this peer.  Used by
-.BR watch (8).
+.BR connect (8).
 .TP
 .B every
 Interval for checking that the peer is still alive and well.  Used by
-.BR watch (8).
+.BR connect (8).
 .TP
 .B ifdown
 Script to bring down tunnel interface connected to the peer.  Used by
-.BR watch (8).
+.BR connect (8).
 .TP
 .B ifname
 Interface name to set for the tunnel interface to the peer.  Used by
@@ -156,7 +158,7 @@ Interface name to set for the tunnel interface to the peer.  Used by
 .TP
 .B ifup
 Script to bring up tunnel interface connected to the peer.  Used by
-.BR watch (8).
+.BR connect (8).
 .TP
 .B ifupextra
 Script containing additional interface setup.  Used by
@@ -194,6 +196,8 @@ Used by
 .TP
 .B priv
 Tag of the private key to use when communicating with the peer.
+Used by
+.BR connect (8).
 .TP
 .B raddr
 Remote address for the tunnel interface to the peer.  Used by
@@ -201,11 +205,11 @@ Remote address for the tunnel interface to the peer.  Used by
 .TP
 .B retries
 Number of failed ping attempts before attempting reconnection.  Used by
-.BR watch (8).
+.BR connect (8).
 .TP
 .B timeout
 Timeout for ping probes.  Used by
-.BR watch (8).
+.BR connect (8).
 .TP
 .B tunnel
 Tunnel driver to use when adding the peer.  Used by
@@ -219,6 +223,7 @@ Used by
 and
 .BR tripe-newpeers (8);
 described below.
+.
 .SS "Conversion"
 This section describes how the textual
 .B peers.in
@@ -286,7 +291,6 @@ is created whose contents is the section name.
 .BR tripe-newpeers (8),
 .BR peers.cdb (5),
 .BR connect (8),
-.BR watch (8),
 .BR tripe-ifup (8).
 .
 .\"--------------------------------------------------------------------------
index 121df6dfde97d10ce8e97d4d1b437247e0e4576f..a695e14a2c6fe77e104223900184c69dc07270a4 100644 (file)
@@ -51,19 +51,6 @@ connect: connect.in Makefile
        $(SUBST) $(srcdir)/connect.in >$@.new $(SUBSTITUTIONS) && \
                chmod +x $@.new && mv $@.new $@
 
-## Watch for peers arriving and disconnecting.
-services_SCRIPTS       += watch
-CLEANFILES             += watch
-EXTRA_DIST             += watch.in
-
-man_MANS               += watch.8
-CLEANFILES             += watch.8
-EXTRA_DIST             += watch.8.in
-
-watch: watch.in Makefile
-       $(SUBST) $(srcdir)/watch.in >$@.new $(SUBSTITUTIONS) && \
-               chmod +x $@.new && mv $@.new $@
-
 ## Watch D-Bus to keep track of external connectivity.
 services_SCRIPTS       += conntrack
 CLEANFILES             += conntrack
@@ -91,4 +78,3 @@ tripe-ifup: tripe-ifup.in Makefile
                chmod +x $@.new && mv $@.new $@
 
 ###----- That's all, folks --------------------------------------------------
-
index 2cc3c5cea6b51255440f3cd45baec4ef71651d14..7021c6b6fd5a9976594eae14f4b08041d36aa158 100644 (file)
 .so ../defs.man.in \"@@@PRE@@@
 .
 .\"--------------------------------------------------------------------------
-.TH connect 8 "8 January 2007" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption"
+.TH connect 8 "11 December 2007" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption"
 .
 .\"--------------------------------------------------------------------------
 .SH "NAME"
 .
-connect \- tripe service to make connections to peers
+connect \- tripe service to handle addition and removal of peers
 .
 .\"--------------------------------------------------------------------------
 .SH "SYNOPSIS"
@@ -55,9 +55,24 @@ connect \- tripe service to make connections to peers
 .
 The
 .B connect
-service registers new peers with the
+service tracks associations with peers and performs various actions at
+appropriate stages in the assocations' lifecycles.  It also registers
+new peers with the
 .BR tripe (8)
-server.
+server on demand.
+.PP
+For example:
+.hP \*o
+When a peer is added, it arranges to configure the corresponding network
+interface correctly, and (if necessary) to initiate a dynamic
+connection.
+.hP \*o
+When a peer is removed, it arranges to bring down the network interface.
+.hP \*o
+While the peer is known, it
+.BR PING s
+it at regular intervals.  If the peer fails to respond, it can be
+removed or reconnected.
 .PP
 A peer may participate
 .I actively
@@ -98,11 +113,189 @@ environment variable is used; if that's not set either, then the default
 default of
 .B peers.cdb
 in the current working directory is used instead.
-.SS "Dynamic connection protocol"
-Dynamic connections are used when the peer's address or port are
-unknown, e.g., when it is hidden behind a NAT firewall.
+.
+.\"--------------------------------------------------------------------------
+.SH "BEHAVIOUR"
+.
+.SS "Adoption"
+The
+.B connect
+service maintains a list of peers which it has adopted.  A peer is
+.I eligible for adoption
+if it has a record in the peer database
+.BR peers.cdb (5)
+in which the
+.B watch
+key is assigned the value
+.BR t ,
+.BR true ,
+.BR y ,
+.BR yes ,
+or
+.BR on .
+.PP
+The service pings adopted peers periodically in order to ensure that
+they are alive, and takes appropriate action if no replies are received.
 .PP
-The protocol for passive connection works as follows.
+A peer is said to be
+.I adopted
+when it is added to this list, and
+.I disowned
+when it removed.
+.
+.SS "Configuring interfaces"
+The
+.B connect
+service configures network interfaces by invoking an
+.B ifup
+script.  The script is invoked as
+.IP
+.I script
+.IR args ...
+.I peer
+.I ifname
+.IR addr ...
+.PP
+where the elements are as described below.
+.TP
+.IR script " and " args
+The peer's database record is retrieved; the value assigned to the
+.B ifup
+key is split into words (quoting is allowed; see
+.BR tripe-admin (5)
+for details).  The first word is the
+.IR script ;
+subsequent words are gathered to form the
+.IR args .
+.TP
+.I peer
+The name of the peer.
+.TP
+.I ifname
+The name of the network interface associated with the peer, as returned
+by the
+.B IFNAME
+administration command (see
+.BR tripe-admin (5)).
+.TP
+.I addr
+The network address of the peer's TrIPE server, in the form output by
+the
+.B ADDR
+administration command (see
+.BR tripe-admin (5)).
+The first word of
+.I addr
+is therefore a network address family, e.g.,
+.BR INET .
+.PP
+The
+.B connect
+service deconfigures interfaces by invoking an
+.B ifdown
+script, in a similar manner.  The script is invoked as
+.IP
+.I script
+.IR args ...
+.I peer
+.PP
+where the elements are as above, except that
+.I script
+and
+.I args
+are formed by splitting the value associated with the peer record's
+.B ifdown
+key.
+.PP
+In both of the above cases, if the relevant key (either
+.B ifup
+or
+.BR ifdown )
+is absent, no action is taken.
+.PP
+The key/value pairs in the peer's database record and the server's
+response to the
+.B ALGS
+administration command (see
+.BR tripe-admin (5))
+are passed to the
+.B ifup
+and
+.B ifdown
+scripts as environment variables.  The environment variable name
+corresponding to a key is determined as follows:
+.hP \*o
+Convert all letters to upper-case.
+.hP \*o Convert all sequences of one or more non-alphanumeric characters
+to an underscore
+.RB ` _ '.
+.hP \*o Prefix the resulting name by
+.RB ` P_ '
+or
+.RB ` A_ '
+depending on whether it came from the peer's database record or the
+.B ALGS
+output respectively.
+.PP
+For example,
+.B ifname
+becomes
+.BR P_IFNAME ;
+and
+.B cipher-blksz
+becomes
+.BR A_CIPHER_BLKSZ .
+.
+.SS "Dynamic connection"
+If a peer's database record assigns a value to the
+.B connect
+key, then the
+.B connect
+service will attempt to establish a connection dynamically with the
+peer.  The value of the
+.B connect
+key is invoked as a Bourne shell command, i.e.,
+.IP
+.B /bin/sh \-c
+.I connect
+.PP
+is executed.  The command is expected to contact the remote server and
+report, on standard output, a challenge string, typically by issuing
+a
+.B passive
+command to the instance of the
+.B connect
+service running on the peer.  The
+.B connect
+service reads this challenge, and submits the command
+.IP
+.B GREET
+.I peer
+.I challenge
+.PP
+Typically, the
+.B connect
+command will issue a command such as
+.IP
+.B SVCSUBMIT connect passive
+.I our-name
+.PP
+where
+.I our-name
+is the remote peer's name for this host.
+.PP
+Similarly, if the database record has a
+.B disconnect
+entry, then
+.B connect
+will use this to give the peer explicit notification that its services
+are no longer needed.  The value of the
+.B disconnect
+key is invoked as a Bourne shell command.  This ought to result in a
+.B KILL
+command being issued to the peer's server.
+.PP
+In detail, the protocol for passive connection works as follows.
 .hP 1.
 The active peer
 .BR ADD s
@@ -111,7 +304,7 @@ its partner, typically using the
 option to suppress the key-exchange message which the server usually
 sends immediately, since otherwise the passive peer will warn about it.
 .hP 2.
-The active peer somehow issues the command
+The active peer issues the command
 .RS
 .IP
 .B SVCSUBMIT connect passive
@@ -119,10 +312,10 @@ The active peer somehow issues the command
 .PP
 to the passive peer's server.  (Here,
 .I user
-is a name identifying the active peer; see below.)  This may be handled
-by the
-.BR watch (8)
-service.
+is a name identifying the active peer; see below.)  Doing this is the
+responsibility of the
+.B connect
+command.
 .RE
 .hP 3.
 The
@@ -147,14 +340,61 @@ from the initial connection request, and
 .BR ADD s
 the appropriate peer, with the address from the
 .BR GREET ing.
+.
+.SS "Operation"
+On startup,
+.B connect
+requests a list of current peers from the
+.BR tripe (8)
+server, and adopts any eligible peers.  If the
+.B \-\-startup
+flag was passed on the command line, the newly adopted peers have their
+interfaces configured and connection attempts are made.
 .PP
-The
-.BR watch (8)
-service is capable of performing the active-peer part of this protocol,
-sending the correct
-.B GREET
-command once the challenge has been obtained.  The remaining difficulty
-is in collecting the challenge from the passive peer.
+Adopted peers are pinged at regular intervals (using the
+.B PING
+administrative command; see
+.BR tripe-admin (5)).
+This process can be configured by assigning values to keys in the peer's
+database record.  Some of these parameters are time intervals,
+expressed as a nonnegative integer followed optionally by
+.BR d ,
+.BR h ,
+.BR m ,
+or
+.B s
+for days, hours, minutes, or seconds, respectively; if no suffix is
+given, seconds are assumed.
+.PP
+The parameters are as follows.
+.TP
+.B every
+A time interval: how often to ping the peer to ensure that it's still
+alive.  The default is 2 minutes.
+.TP
+.B timeout
+A time interval: how long to wait for a reply before retrying or giving
+up.  The default is 10 seconds.
+.TP
+.B retries
+An integer: how many failed attempts to make before deciding that the
+peer is unreachable and taking action.  The default is 5 attempts.
+.PP
+The algorithm is as follows.  Send up to
+.I retries
+pings; if a reply is received before the
+.I timeout
+then the peer is alive; wait
+.I every
+and check again.  If no reply is received within the
+.IR timeout ,
+then try again up to
+.I retries
+times.  If no attempt succeeds, the peer is declared unreachable.  If
+the peer has a
+.B connect
+command (i.e., it connects dynamically) then another connection attempt
+is made.  Otherwise the peer is killed.
 .
 .\"--------------------------------------------------------------------------
 .SH "SERVICE COMMAND REFERENCE"
@@ -174,6 +414,8 @@ The service will submit the command
 .IR time ]
 .RB [ \-key
 .IR tag ]
+.RB [ \-priv
+.IR tag ]
 .RB [ \-mobile ]
 .RB [ \-tunnel
 .IR driver ]
@@ -212,6 +454,15 @@ to the
 key.
 .hP \*o
 The option
+.B \-priv
+.I tag
+is provided if the database record assigns a value
+.I tag
+to the
+.B priv
+key.
+.hP \*o
+The option
 .B \-mobile
 is provided if the peer's database record assigns the
 .B mobile
@@ -239,6 +490,16 @@ is the value assigned to the
 key in the database record.
 .RE
 .SP
+.B adopted
+For each peer being tracked by the
+.B connect
+service, write a line
+.B INFO
+.IR name .
+(Compatibility note: it's possible that further information will be
+provided about each peer, in the form of subsequent tokens.  Clients
+should be prepared to ignore such tokens.)
+.SP
 .BI "info " peer
 Lists the database record for the named
 .IR peer .
@@ -250,8 +511,18 @@ For each key/value pair, a line
 .PP
 is output.  The key/value pairs are output in an arbitrary order.
 .RE
-.TP
-.B "list"
+.SP
+.BI "kick " peer
+If
+.I peer
+is currently added, and its record in the peer database contains a
+.B connect
+key (see
+.BR peers.in )
+then force a reconnection attempt.  See
+.BR "Dynamic connection" .
+.SP
+.B "list-active"
 Output a list of peers in the database.  For each peer name
 .IR peer ,
 a line
@@ -311,49 +582,35 @@ line identifying the peer corresponding to the
 name.
 .
 .\"--------------------------------------------------------------------------
-.SH "ERROR MESSAGES"
+.SH "NOTIFICATIONS"
 .
-.\"* 20 Error messages (FAIL codes)
-The following error codes may be reported.
-.SP
-.B "connect-timeout"
-(For
-.BR passive .)
-No
-.BR GREET ing
-was received within the timeout period (default 30 seconds).
+.\"* 30 Notification broadcasts (NOTE codes)
+All notifications issued by
+.B connect
+begin with the tokens
+.BR "USER connect" .
 .SP
-.BI "malformed-peer " peer " missing-key " key
-The database record for
-.I peer
-has no value for the
-.I key
-but one was expected.
+.B "USER connect peerdb-update"
+The peer database has changed.  Other interested clients should reopen
+the database.
 .SP
-.BI "passive-peer " peer
-(For
-.BR active .)
-An active connection to
+.BI "USER connect ping-failed " peer " " error\fR...
+An attempt to
+.B PING
+the named
 .I peer
-was requested, but the database record indicates that it is passive,
-i.e., its
-.B peer
-key has the value
-.BR PASSIVE .
+failed; the server replied
+.B FAIL
+.IR error ...
 .SP
-.BI "unknown-peer " peer
+.BI "USER connect " process\fR... " stdout " line
 The
-.I peer
-has no record in the database.
-.SP
-.BI "unknown-user " user
-(For
-.B passive
-and
-.BR userinfo .)
-There is no record of
-.I user
-in the database.
+.I process
+spawned by the
+.B connect
+service unexpectedly wrote
+.I line
+to its standard output.
 .
 .\"--------------------------------------------------------------------------
 .SH "WARNINGS"
@@ -372,6 +629,99 @@ automatically failed: the
 command reported
 .B FAIL
 .IR error ...
+.SP
+.BI "USER connect ping-ok " peer
+A reply was received to a
+.B PING
+sent to the
+.IR peer ,
+though earlier attempts had failed.
+.SP
+.BI "USER connect ping-timeout " peer " attempt " i " of " n
+No reply was received to a
+.B PING
+sent to the
+.IR peer .
+So far,
+.I i
+.BR PING s
+have been sent; if a total of
+.I n
+consecutive attempts time out, the
+.B connect
+service will take further action.
+.SP
+.B "USER connect reconnecting " peer
+The dynamically connected
+.I peer
+seems to be unresponsive.  The
+.B connect
+service will attempt to reconnect.
+.SP
+.BI "USER connect " process\fR... " stderr " line
+The
+.I process
+spawned by the
+.B connect
+service wrote
+.I line
+to its standard error.
+.SP
+.BI "USER connect " process\fR... " exit-nonzero " code
+The
+.I process
+spawned by the
+.B connect
+service exited with the nonzero status
+.IR code .
+.SP
+.BI "USER connect " process\fR... " exit-signal S" code
+The
+.I process
+spawned by the
+.B connect
+service was killed by signal
+.IR code .
+Here,
+.I code
+is the numeric value of the fatal signal.
+.SP
+.BI "USER connect " process\fR... " exit-unknown " status
+The
+.I process
+spawned by the
+.B connect
+service exited with an unknown
+.IR status .
+Here,
+.I status
+is the raw exit status, as returned by
+.BR waitpid (2),
+in hexadecimal.
+.
+.\"--------------------------------------------------------------------------
+.SH "CHILD PROCESS IDENTIFIERS"
+.
+.\"* 50 Child process identifiers
+Some of the warnings and notifications refer to processes spawned by
+.B connect
+under various circumstances.  The process identifiers are as follows.
+.SP
+.BI "connect " peer
+A child spawned in order to establish a dynamic connection with
+.IR peer .
+.SP
+.BI "disconnect " peer
+A child spawned in order to shut down a dynamic connection with
+.IR peer .
+.SP
+.BI "ifdown " peer
+A child spawned to deconfigure the network interface for
+.IR peer .
+.SP
+.BI "ifup " peer
+A child spawned to configure the network interface for
+.IR peer .
 .
 .\"--------------------------------------------------------------------------
 .SH "SUMMARY"
@@ -383,7 +733,6 @@ command reported
 .
 .BR tripe-service (7),
 .BR peers.in (5),
-.BR watch (8),
 .BR tripe (8).
 .
 .\"--------------------------------------------------------------------------
index 0031d36fcee85b72aa226a79f747742c11af2352..000ca50eba511232b1ae82dd3d7af613b1b33797 100644 (file)
@@ -1,9 +1,9 @@
 #! @PYTHON@
 ### -*-python-*-
 ###
-### Service for establishing dynamic connections
+### Connect to remote peers, and keep track of them
 ###
-### (c) 2006 Straylight/Edgeware
+### (c) 2007 Straylight/Edgeware
 ###
 
 ###----- Licensing notice ---------------------------------------------------
@@ -32,14 +32,243 @@ VERSION = '@VERSION@'
 from optparse import OptionParser
 import tripe as T
 import os as OS
+import signal as SIG
+import errno as E
 import cdb as CDB
 import mLib as M
+import re as RX
 from time import time
+import subprocess as PROC
 
 S = T.svcmgr
 
 ###--------------------------------------------------------------------------
-### Main service machinery.
+### Running auxiliary commands.
+
+class SelLineQueue (M.SelLineBuffer):
+  """Glues the select-line-buffer into the coroutine queue system."""
+
+  def __new__(cls, file, queue, tag, kind):
+    """See __init__ for documentation."""
+    return M.SelLineBuffer.__new__(cls, file.fileno())
+
+  def __init__(me, file, queue, tag, kind):
+    """
+    Initialize a new line-reading adaptor.
+
+    The adaptor reads lines from FILE.  Each line is inserted as a message of
+    the stated KIND, bearing the TAG, into the QUEUE.  End-of-file is
+    represented as None.
+    """
+    me._q = queue
+    me._file = file
+    me._tag = tag
+    me._kind = kind
+    me.enable()
+
+  @T._callback
+  def line(me, line):
+    me._q.put((me._tag, me._kind, line))
+
+  @T._callback
+  def eof(me):
+    me.disable()
+    me._q.put((me._tag, me._kind, None))
+
+class ErrorWatch (T.Coroutine):
+  """
+  An object which watches stderr streams for errors and converts them into
+  warnings of the form
+
+    WARN connect INFO stderr LINE
+
+  The INFO is a list of tokens associated with the file when it was
+  registered.
+
+  Usually there is a single ErrorWatch object, called errorwatch.
+  """
+
+  def __init__(me):
+    """Initialization: there are no arguments."""
+    T.Coroutine.__init__(me)
+    me._q = T.Queue()
+    me._map = {}
+    me._seq = 1
+
+  def watch(me, file, info):
+    """
+    Adds FILE to the collection of files to watch.
+
+    INFO will be written in the warning messages from this FILE.  Returns a
+    sequence number which can be used to unregister the file again.
+    """
+    seq = me._seq
+    me._seq += 1
+    me._map[seq] = info, SelLineQueue(file, me._q, seq, 'stderr')
+    return seq
+
+  def unwatch(me, seq):
+    """Stop watching the file with sequence number SEQ."""
+    del me._map[seq]
+    return me
+
+  def run(me):
+    """
+    Coroutine function: read items from the queue and report them.
+
+    Unregisters files automatically when they reach EOF.
+    """
+    while True:
+      seq, _, line = me._q.get()
+      if line is None:
+        me.unwatch(seq)
+      else:
+        S.warn(*['connect'] + me._map[seq][0] + ['stderr', line])
+
+def dbwatch():
+  """
+  Coroutine function: wake up every second and notice changes to the
+  database.  When a change happens, tell the Pinger (q.v.) to rescan its
+  peers.
+  """
+  cr = T.Coroutine.getcurrent()
+  main = cr.parent
+  fw = M.FWatch(opts.cdb)
+  while True:
+    timer = M.SelTimer(time() + 1, lambda: cr.switch())
+    main.switch()
+    if fw.update():
+      pinger.rescan(False)
+      S.notify('connect', 'peerdb-update')
+
+class ChildWatch (M.SelSignal):
+  """
+  An object which watches for specified processes exiting and reports
+  terminations by writing items of the form (TAG, 'exit', RESULT) to a queue.
+
+  There is usually only one ChildWatch object, called childwatch.
+  """
+
+  def __new__(cls):
+    """Initialize the child-watcher."""
+    return M.SelSignal.__new__(cls, SIG.SIGCHLD)
+
+  def __init__(me):
+    """Initialize the child-watcher."""
+    me._pid = {}
+    me.enable()
+
+  def watch(me, pid, queue, tag):
+    """
+    Register PID as a child to watch.  If it exits, write (TAG, 'exit', CODE)
+    to the QUEUE, where CODE is one of
+
+      * None (successful termination)
+      * ['exit-nonzero', CODE] (CODE is a string!)
+      * ['exit-signal', 'S' + CODE] (CODE is the signal number as a string)
+      * ['exit-unknown', STATUS] (STATUS is the entire exit status, in hex)
+    """
+    me._pid[pid] = queue, tag
+    return me
+
+  def unwatch(me, pid):
+    """Unregister PID as a child to watch."""
+    del me._pid[pid]
+    return me
+
+  @T._callback
+  def signalled(me):
+    """
+    Called when child processes exit: collect exit statuses and report
+    failures.
+    """
+    while True:
+      try:
+        pid, status = OS.waitpid(-1, OS.WNOHANG)
+      except OSError, exc:
+        if exc.errno == E.ECHILD:
+          break
+      if pid == 0:
+        break
+      if pid not in me._pid:
+        continue
+      queue, tag = me._pid[pid]
+      if OS.WIFEXITED(status):
+        exit = OS.WEXITSTATUS(status)
+        if exit == 0:
+          code = None
+        else:
+          code = ['exit-nonzero', str(exit)]
+      elif OS.WIFSIGNALED(status):
+        code = ['exit-signal', 'S' + str(OS.WTERMSIG(status))]
+      else:
+        code = ['exit-unknown', hex(status)]
+      queue.put((tag, 'exit', code))
+
+class Command (object):
+  """
+  Represents a running command.
+
+  This class is the main interface to the machery provided by the ChildWatch
+  and ErrorWatch objects.  See also potwatch.
+  """
+
+  def __init__(me, info, queue, tag, args, env):
+    """
+    Start a new child process.
+
+    The ARGS are a list of arguments to be given to the child process.  The
+    ENV is either None or a dictionary of environment variable assignments to
+    override the extant environment.  INFO is a list of tokens to be included
+    in warnings about the child's stderr output.  If the child writes a line
+    to standard output, put (TAG, 'stdout', LINE) to the QUEUE.  When the
+    child exits, write (TAG, 'exit', CODE) to the QUEUE.
+    """
+    me._info = info
+    me._q = queue
+    me._tag = tag
+    myenv = OS.environ.copy()
+    if env: myenv.update(env)
+    me._proc = PROC.Popen(args = args, env = myenv, bufsize = 1,
+                          stdout = PROC.PIPE, stderr = PROC.PIPE)
+    me._lq = SelLineQueue(me._proc.stdout, queue, tag, 'stdout')
+    errorwatch.watch(me._proc.stderr, info)
+    childwatch.watch(me._proc.pid, queue, tag)
+
+  def __del__(me):
+    """
+    If I've been forgotten then stop watching for termination.
+    """
+    childwatch.unwatch(me._proc.pid)
+
+def potwatch(what, name, q):
+  """
+  Watch the queue Q for activity as reported by a Command object.
+
+  Information from the process's stdout is reported as
+
+    NOTE WHAT NAME stdout LINE
+
+  abnormal termination is reported as
+
+    WARN WHAT NAME CODE
+
+  where CODE is what the ChildWatch wrote.
+  """
+  eofp = deadp = False
+  while not deadp or not eofp:
+    _, kind, more = q.get()
+    if kind == 'stdout':
+      if more is None:
+        eofp = True
+      else:
+        S.notify('connect', what, name, 'stdout', more)
+    elif kind == 'exit':
+      if more: S.warn('connect', what, name, *more)
+      deadp = True
+
+###--------------------------------------------------------------------------
+### Peer database utilities.
 
 _magic = ['_magic']                     # An object distinct from all others
 
@@ -54,30 +283,382 @@ class Peer (object):
     one given on the command-line.
     """
     me.name = peer
-    try:
-      record = (cdb or CDB.init(opts.cdb))['P' + peer]
-    except KeyError:
-      raise T.TripeJobError('unknown-peer', peer)
+    record = (cdb or CDB.init(opts.cdb))['P' + peer]
     me.__dict__.update(M.URLDecode(record, semip = True))
 
-  def get(me, key, default = _magic):
+  def get(me, key, default = _magic, filter = None):
     """
     Get the information stashed under KEY from the peer's database record.
 
     If DEFAULT is given, then use it if the database doesn't contain the
-    necessary information.  If no DEFAULT is given, then report an error.
+    necessary information.  If no DEFAULT is given, then report an error.  If
+    a FILTER function is given then apply it to the information from the
+    database before returning it.
     """
     attr = me.__dict__.get(key, default)
     if attr is _magic:
       raise T.TripeJobError('malformed-peer', me.name, 'missing-key', key)
+    elif filter is not None:
+      attr = filter(attr)
     return attr
 
+  def has(me, key):
+    """
+    Return whether the peer's database record has the KEY.
+    """
+    return key in me.__dict__
+
   def list(me):
     """
     Iterate over the available keys in the peer's database record.
     """
     return me.__dict__.iterkeys()
 
+def boolean(value):
+  """Parse VALUE as a boolean."""
+  return value in ['t', 'true', 'y', 'yes', 'on']
+
+###--------------------------------------------------------------------------
+### Waking up and watching peers.
+
+def run_connect(peer, cmd):
+  """
+  Start the job of connecting to the passive PEER.
+
+  The CMD string is a shell command which will connect to the peer (via some
+  back-channel, say ssh and userv), issue a command
+
+    SVCSUBMIT connect passive [OPTIONS] USER
+
+  and write the resulting challenge to standard error.
+  """
+  q = T.Queue()
+  cmd = Command(['connect', peer.name], q, 'connect',
+                ['/bin/sh', '-c', cmd], None)
+  _, kind, more = q.peek()
+  if kind == 'stdout':
+    if more is None:
+      S.warn('connect', 'connect', peer.name, 'unexpected-eof')
+    else:
+      chal = more
+      S.greet(peer.name, chal)
+      q.get()
+  potwatch('connect', peer.name, q)
+
+def run_disconnect(peer, cmd):
+  """
+  Start the job of disconnecting from a passive PEER.
+
+  The CMD string is a shell command which will disconnect from the peer.
+  """
+  q = T.Queue()
+  cmd = Command(['disconnect', peer.name], q, 'disconnect',
+                ['/bin/sh', '-c', cmd], None)
+  potwatch('disconnect', peer.name, q)
+
+_pingseq = 0
+class PingPeer (object):
+  """
+  Object representing a peer which we are pinging to ensure that it is still
+  present.
+
+  PingPeer objects are held by the Pinger (q.v.).  The Pinger maintains an
+  event queue -- which saves us from having an enormous swarm of coroutines
+  -- but most of the actual work is done here.
+
+  In order to avoid confusion between different PingPeer instances for the
+  same actual peer, each PingPeer has a sequence number (its `seq'
+  attribute).  Events for the PingPeer are identified by a (PEER, SEQ) pair.
+  (Using the PingPeer instance itself will prevent garbage collection of
+  otherwise defunct instances.)
+  """
+
+  def __init__(me, pinger, queue, peer, pingnow):
+    """
+    Create a new PingPeer.
+
+    The PINGER is the Pinger object we should send the results to.  This is
+    used when we remove ourselves, if the peer has been explicitly removed.
+
+    The QUEUE is the event queue on which timer and ping-command events
+    should be written.
+
+    The PEER is a `Peer' object describing the peer.
+
+    If PINGNOW is true, then immediately start pinging the peer.  Otherwise
+    wait until the usual retry interval.
+    """
+    global _pingseq
+    me._pinger = pinger
+    me._q = queue
+    me._peer = peer.name
+    me.update(peer)
+    me.seq = _pingseq
+    _pingseq += 1
+    me._failures = 0
+    if pingnow:
+      me._timer = None
+      me._ping()
+    else:
+      me._timer = M.SelTimer(time() + me._every, me._time)
+
+  def update(me, peer):
+    """
+    Refreshes the timer parameters for this peer.  We don't, however,
+    immediately reschedule anything: that will happen next time anything
+    interesting happens.
+    """
+    if peer is None: peer = Peer(me._peer)
+    assert peer.name == me._peer
+    me._every = peer.get('every', filter = T.timespec, default = 120)
+    me._timeout = peer.get('timeout', filter = T.timespec, default = 10)
+    me._retries = peer.get('retries', filter = int, default = 5)
+    me._connectp = peer.has('connect')
+    return me
+
+  def _ping(me):
+    """
+    Send a ping to the peer; the result is sent to the Pinger's event queue.
+    """
+    S.rawcommand(T.TripeAsynchronousCommand(
+      me._q, (me._peer, me.seq),
+      ['EPING',
+       '-background', S.bgtag(),
+       '-timeout', str(me._timeout),
+       '--',
+       me._peer]))
+
+  def _reconnect(me):
+    peer = Peer(me._peer)
+    if me._connectp:
+      S.warn('connect', 'reconnecting', me._peer)
+      S.forcekx(me._peer)
+      T.spawn(run_connect, peer, peer.get('connect'))
+      me._timer = M.SelTimer(time() + me._every, me._time)
+    else:
+      S.kill(me._peer)
+
+  def event(me, code, stuff):
+    """
+    Respond to an event which happened to this peer.
+
+    Timer events indicate that we should start a new ping.  (The server has
+    its own timeout which detects lost packets.)
+
+    We trap unknown-peer responses and detach from the Pinger.
+
+    If the ping fails and we run out of retries, we attempt to restart the
+    connection.
+    """
+    if code == 'TIMER':
+      me._failures = 0
+      me._ping()
+    elif code == 'FAIL':
+      S.notify('connect', 'ping-failed', me._peer, *stuff)
+      if not stuff:
+        pass
+      elif stuff[0] == 'unknown-peer':
+        me._pinger.kill(me._peer)
+      elif stuff[0] == 'ping-send-failed':
+        me._reconnect()
+    elif code == 'INFO':
+      if stuff[0] == 'ping-ok':
+        if me._failures > 0:
+          S.warn('connect', 'ping-ok', me._peer)
+        me._timer = M.SelTimer(time() + me._every, me._time)
+      elif stuff[0] == 'ping-timeout':
+        me._failures += 1
+        S.warn('connect', 'ping-timeout', me._peer,
+               'attempt', str(me._failures), 'of', str(me._retries))
+        if me._failures < me._retries:
+          me._ping()
+        else:
+          me._reconnect()
+      elif stuff[0] == 'ping-peer-died':
+        me._pinger.kill(me._peer)
+
+  @T._callback
+  def _time(me):
+    """
+    Handle timer callbacks by posting a timeout event on the queue.
+    """
+    me._timer = None
+    me._q.put(((me._peer, me.seq), 'TIMER', None))
+
+  def __str__(me):
+    return 'PingPeer(%s, %d, f = %d)' % (me._peer, me.seq, me._failures)
+  def __repr__(me):
+    return str(me)
+
+class Pinger (T.Coroutine):
+  """
+  The Pinger keeps track of the peers which we expect to be connected and
+  takes action if they seem to stop responding.
+
+  There is usually only one Pinger, called pinger.
+
+  The Pinger maintains a collection of PingPeer objects, and an event queue.
+  The PingPeers direct the results of their pings, and timer events, to the
+  event queue.  The Pinger's coroutine picks items off the queue and
+  dispatches them back to the PingPeers as appropriate.
+  """
+
+  def __init__(me):
+    """Initialize the Pinger."""
+    T.Coroutine.__init__(me)
+    me._peers = {}
+    me._q = T.Queue()
+
+  def run(me):
+    """
+    Coroutine function: reads the pinger queue and sends events to the
+    PingPeer objects they correspond to.
+    """
+    while True:
+      (peer, seq), code, stuff = me._q.get()
+      if peer in me._peers and seq == me._peers[peer].seq:
+        me._peers[peer].event(code, stuff)
+
+  def add(me, peer, pingnow):
+    """
+    Add PEER to the collection of peers under the Pinger's watchful eye.
+    The arguments are as for PingPeer: see above.
+    """
+    me._peers[peer.name] = PingPeer(me, me._q, peer, pingnow)
+    return me
+
+  def kill(me, peername):
+    """Remove PEER from the peers being watched by the Pinger."""
+    del me._peers[peername]
+    return me
+
+  def rescan(me, startup):
+    """
+    General resynchronization method.
+
+    We scan the list of peers (with connect scripts) known at the server.
+    Any which are known to the Pinger but aren't known to the server are
+    removed from our list; newly arrived peers are added.  (Note that a peer
+    can change state here either due to the server sneakily changing its list
+    without issuing notifications or, more likely, the database changing its
+    idea of whether a peer is interesting.)  Finally, PingPeers which are
+    still present are prodded to update their timing parameters.
+
+    This method is called once at startup to pick up the peers already
+    installed, and again by the dbwatcher coroutine when it detects a change
+    to the database.
+    """
+    if T._debug: print '# rescan peers'
+    correct = {}
+    start = {}
+    for name in S.list():
+      try: peer = Peer(name)
+      except KeyError: continue
+      if peer.get('watch', filter = boolean, default = False):
+        if T._debug: print '# interesting peer %s' % peer
+        correct[peer.name] = start[peer.name] = peer
+      elif startup:
+        if T._debug: print '# peer %s ready for adoption' % peer
+        start[peer.name] = peer
+    for name, obj in me._peers.items():
+      try:
+        peer = correct[name]
+      except KeyError:
+        if T._debug: print '# peer %s vanished' % name
+        del me._peers[name]
+      else:
+        obj.update(peer)
+    for name, peer in start.iteritems():
+      if name in me._peers: continue
+      if startup:
+        if T._debug: print '# setting up peer %s' % name
+        ifname = S.ifname(name)
+        addr = S.addr(name)
+        T.defer(adoptpeer, peer, ifname, *addr)
+      else:
+        if T._debug: print '# adopting new peer %s' % name
+        me.add(peer, True)
+    return me
+
+  def adopted(me):
+    """
+    Returns the list of peers being watched by the Pinger.
+    """
+    return me._peers.keys()
+
+###--------------------------------------------------------------------------
+### New connections.
+
+def encode_envvars(env, prefix, vars):
+  """
+  Encode the variables in VARS suitably for including in a program
+  environment.  Lowercase letters in variable names are forced to uppercase;
+  runs of non-alphanumeric characters are replaced by single underscores; and
+  the PREFIX is prepended.  The resulting variables are written to ENV.
+  """
+  for k, v in vars.iteritems():
+    env[prefix + r_bad.sub('_', k.upper())] = v
+
+r_bad = RX.compile(r'[\W_]+')
+def envvars(peer):
+  """
+  Translate the database information for a PEER into a dictionary of
+  environment variables with plausible upper-case names and a P_ prefix.
+  Also collect the crypto information into A_ variables.
+  """
+  env = {}
+  encode_envvars(env, 'P_', dict([(k, peer.get(k)) for k in peer.list()]))
+  encode_envvars(env, 'A_', S.algs(peer.name))
+  return env
+
+def run_ifupdown(what, peer, *args):
+  """
+  Run the interface up/down script for a peer.
+
+  WHAT is 'ifup' or 'ifdown'.  PEER names the peer in question.  ARGS is a
+  list of arguments to pass to the script, in addition to the peer name.
+
+  The command is run and watched in the background by potwatch.
+  """
+  q = T.Queue()
+  c = Command([what, peer.name], q, what,
+              M.split(peer.get(what), quotep = True)[0] +
+              [peer.name] + list(args),
+              envvars(peer))
+  potwatch(what, peer.name, q)
+
+def adoptpeer(peer, ifname, *addr):
+  """
+  Add a new peer to our collection.
+
+  PEER is the `Peer' object; IFNAME is the interface name for its tunnel; and
+  ADDR is the list of tokens representing its address.
+
+  We try to bring up the interface and provoke a connection to the peer if
+  it's passive.
+  """
+  if peer.has('ifup'):
+    T.Coroutine(run_ifupdown, name = 'ifup %s' % peer.name) \
+        .switch('ifup', peer, ifname, *addr)
+  cmd = peer.get('connect', default = None)
+  if cmd is not None:
+    T.Coroutine(run_connect, name = 'connect %s' % peer.name) \
+        .switch(peer, cmd)
+  if peer.get('watch', filter = boolean, default = False):
+    pinger.add(peer, False)
+
+def disownpeer(peer):
+  """Drop the PEER from the Pinger and put its interface to bed."""
+  try: pinger.kill(peer)
+  except KeyError: pass
+  cmd = peer.get('disconnect', default = None)
+  if cmd is not None:
+    T.Coroutine(run_disconnect, name = 'disconnect %s' % peer.name) \
+        .switch(peer, cmd)
+  if peer.has('ifdown'):
+    T.Coroutine(run_ifupdown, name = 'ifdown %s' % peer.name) \
+        .switch('ifdown', peer)
+
 def addpeer(peer, addr):
   """
   Process a connect request from a new peer PEER on address ADDR.
@@ -99,19 +680,65 @@ def addpeer(peer, addr):
   except T.TripeError, exc:
     raise T.TripeJobError(*exc.args)
 
+## Dictionary mapping challenges to waiting passive-connection coroutines.
+chalmap = {}
+
+def notify(_, code, *rest):
+  """
+  Watch for notifications.
+
+  We trap ADD and KILL notifications, and send them straight to adoptpeer and
+  disownpeer respectively; and dispatch GREET notifications to the
+  corresponding waiting coroutine.
+  """
+  if code == 'ADD':
+    try: p = Peer(rest[0])
+    except KeyError: return
+    adoptpeer(p, *rest[1:])
+  elif code == 'KILL':
+    try: p = Peer(rest[0])
+    except KeyError: return
+    disownpeer(p, *rest[1:])
+  elif code == 'GREET':
+    chal = rest[0]
+    try: cr = chalmap[chal]
+    except KeyError: pass
+    else: cr.switch(rest[1:])
+
+###--------------------------------------------------------------------------
+### Command implementation.
+
+def cmd_kick(name):
+  """
+  kick NAME: Force a new connection attempt for the NAMEd peer.
+  """
+  if name not in pinger.adopted():
+    raise T.TripeJobError('peer-not-adopted', name)
+  try: peer = Peer(name)
+  except KeyError: raise T.TripeJobError('unknown-peer', name)
+  T.spawn(connect, peer)
+
+def cmd_adopted():
+  """
+  adopted: Report a list of adopted peers.
+  """
+  for name in pinger.adopted():
+    T.svcinfo(name)
+
 def cmd_active(name):
   """
   active NAME: Handle an active connection request for the peer called NAME.
 
   The appropriate address is read from the database automatically.
   """
-  peer = Peer(name)
+  try: peer = Peer(name)
+  except KeyError: raise T.TripeJobError('unknown-peer', name)
   addr = peer.get('peer')
   if addr == 'PASSIVE':
     raise T.TripeJobError('passive-peer', name)
   addpeer(peer, M.split(addr, quotep = True)[0])
 
-def cmd_list():
+def cmd_listactive():
   """
   list: Report a list of the available active peers.
   """
@@ -124,7 +751,8 @@ def cmd_info(name):
   """
   info NAME: Report the database entries for the named peer.
   """
-  peer = Peer(name)
+  try: peer = Peer(name)
+  except KeyError: raise T.TripeJobError('unknown-peer', name)
   items = list(peer.list())
   items.sort()
   for i in items:
@@ -134,14 +762,9 @@ def cmd_userpeer(user):
   """
   userpeer USER: Report the peer name for the named user.
   """
-  try:
-    peer = CDB.init(opts.cdb)['U' + user]
-  except KeyError:
-    raise T.TripeJobError('unknown-user', user)
-  T.svcinfo(peer)
-
-## Dictionary mapping challenges to waiting passive-connection coroutines.
-chalmap = {}
+  try: name = CDB.init(opts.cdb)['U' + user]
+  except KeyError: raise T.TripeJobError('unknown-user', user)
+  T.svcinfo(name)
 
 def cmd_passive(*args):
   """
@@ -156,10 +779,10 @@ def cmd_passive(*args):
     if opt == '-timeout':
       timeout = T.timespec(op.arg())
   user, = op.rest(1, 1)
-  try:
-    peer = CDB.init(opts.cdb)['U' + user]
-  except KeyError:
-    raise T.TripeJobError('unknown-user', user)
+  try: name = CDB.init(opts.cdb)['U' + user]
+  except KeyError: raise T.TripeJobError('unknown-user', user)
+  try: peer = Peer(name)
+  except KeyError: raise T.TripeJobError('unknown-peer', name)
   chal = S.getchal()
   cr = T.Coroutine.getcurrent()
   timer = M.SelTimer(time() + timeout, lambda: cr.switch(None))
@@ -169,24 +792,10 @@ def cmd_passive(*args):
     addr = cr.parent.switch()
     if addr is None:
       raise T.TripeJobError('connect-timeout')
-    addpeer(Peer(peer), addr)
+    addpeer(peer, addr)
   finally:
     del chalmap[chal]
 
-def notify(_, code, *rest):
-  """
-  Watch for notifications.
-
-  In particular, if a GREETing appears quoting a challenge in the chalmap
-  then wake up the corresponding coroutine.
-  """
-  if code != 'GREET':
-    return
-  chal = rest[0]
-  addr = rest[1:]
-  if chal in chalmap:
-    chalmap[chal].switch(addr)
-
 ###--------------------------------------------------------------------------
 ### Start up.
 
@@ -194,10 +803,14 @@ def setup():
   """
   Service setup.
 
-  Register the notification-watcher, and add the automatic active peers.
+  Register the notification watcher, rescan the peers, and add automatic
+  active peers.
   """
   S.handler['NOTE'] = notify
   S.watch('+n')
+
+  pinger.rescan(opts.startup)
+
   if opts.startup:
     cdb = CDB.init(opts.cdb)
     try:
@@ -211,6 +824,18 @@ def setup():
       except T.TripeJobError, err:
         S.warn('connect', 'auto-add-failed', name, *err.args)
 
+def init():
+  """
+  Initialization to be done before service startup.
+  """
+  global errorwatch, childwatch, pinger
+  errorwatch = ErrorWatch()
+  childwatch = ChildWatch()
+  pinger = Pinger()
+  T.Coroutine(dbwatch, name = 'dbwatch').switch()
+  errorwatch.switch()
+  pinger.switch()
+
 def parse_options():
   """
   Parse the command-line options.
@@ -247,18 +872,20 @@ def parse_options():
   return opts
 
 ## Service table, for running manually.
-service_info = [('connect', VERSION, {
+service_info = [('connect', T.VERSION, {
+  'adopted': (0, 0, '', cmd_adopted),
+  'kick': (1, 1, 'PEER', cmd_kick),
   'passive': (1, None, '[OPTIONS] USER', cmd_passive),
   'active': (1, 1, 'PEER', cmd_active),
   'info': (1, 1, 'PEER', cmd_info),
-  'list': (0, 0, '', cmd_list),
+  'list-active': (0, 0, '', cmd_listactive),
   'userpeer': (1, 1, 'USER', cmd_userpeer)
 })]
 
 if __name__ == '__main__':
   opts = parse_options()
   T.runservices(opts.tripesock, service_info,
-                setup = setup,
+                init = init, setup = setup,
                 daemon = opts.daemon)
 
 ###----- That's all, folks --------------------------------------------------
diff --git a/svc/watch.8.in b/svc/watch.8.in
deleted file mode 100644 (file)
index 79ddbef..0000000
+++ /dev/null
@@ -1,495 +0,0 @@
-.\" -*-nroff-*-
-.\".
-.\" Manual for the watch service
-.\"
-.\" (c) 2008 Straylight/Edgeware
-.\"
-.
-.\"----- Licensing notice ---------------------------------------------------
-.\"
-.\" This file is part of Trivial IP Encryption (TrIPE).
-.\"
-.\" TrIPE is free software; you can redistribute it and/or modify
-.\" it under the terms of the GNU General Public License as published by
-.\" the Free Software Foundation; either version 2 of the License, or
-.\" (at your option) any later version.
-.\"
-.\" TrIPE is distributed in the hope that it will be useful,
-.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
-.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-.\" GNU General Public License for more details.
-.\"
-.\" You should have received a copy of the GNU General Public License
-.\" along with TrIPE; if not, write to the Free Software Foundation,
-.\" Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-.
-.\"--------------------------------------------------------------------------
-.so ../defs.man.in \"@@@PRE@@@
-.
-.\"--------------------------------------------------------------------------
-.TH watch 8 "11 December 2007" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption"
-.
-.\"--------------------------------------------------------------------------
-.SH "NAME"
-.
-watch \- tripe service handle addition and removal of peers
-.
-.\"--------------------------------------------------------------------------
-.SH "SYNOPSIS"
-.
-.B watch
-.RB [ \-a
-.IR socket ]
-.RB [ \-d
-.IR dir ]
-.RB [ \-p
-.IR file ]
-.br
-\&     \c
-.RB [ \-\-daemon ]
-.RB [ \-\-debug ]
-.RB [ \-\-startup ]
-.
-.\"--------------------------------------------------------------------------
-.SH "DESCRIPTION"
-.
-The
-.B watch
-service tracks associations with peers and performs various actions at
-appropriate stages in the assocations' lifecycles.
-.PP
-For example:
-.hP \*o
-When a peer is added, it arranges to configure the corresponding network
-interface correctly, and (if necessary) to initiate a dynamic
-connection.
-.hP \*o
-When a peer is removed, it arranges to bring down the network interface.
-.hP \*o
-While the peer is known, it
-.BR PING s
-it at regular intervals.  If the peer fails to respond, it can be
-removed or reconnected.
-.SS "Command line"
-In addition to the standard options described in
-.BR tripe-service (7),
-the following command-line options are recognized.
-.TP
-.BI "\-p, \-\-peerdb=" file
-Use
-.I file
-as the (CDB format) peer database.  In the absence of this option, the
-file named by the
-.B TRIPEPEERDB
-environment variable is used; if that's not set either, then the default
-default of
-.B peers.cdb
-in the current working directory is used instead.
-.
-.\"--------------------------------------------------------------------------
-.SH "BEHAVIOUR"
-.
-.SS "Adoption"
-The
-.B watch
-service maintains a list of peers which it has adopted.  A peer is
-.I eligible for adoption
-if it has a record in the peer database
-.BR peers.cdb (5)
-in which the
-.B watch
-key is assigned the value
-.BR t ,
-.BR true ,
-.BR y ,
-.BR yes ,
-or
-.BR on .
-.PP
-The service pings adopted peers periodically in order to ensure that
-they are alive, and takes appropriate action if no replies are received.
-.PP
-A peer is said to be
-.I adopted
-when it is added to this list, and
-.I disowned
-when it removed.
-.SS "Configuring interfaces"
-The
-.B watch
-service configures network interfaces by invoking an
-.B ifup
-script.  The script is invoked as
-.IP
-.I script
-.IR args ...
-.I peer
-.I ifname
-.IR addr ...
-.PP
-where the elements are as described below.
-.TP
-.IR script " and " args
-The peer's database record is retrieved; the value assigned to the
-.B ifup
-key is split into words (quoting is allowed; see
-.BR tripe-admin (5)
-for details).  The first word is the
-.IR script ;
-subsequent words are gathered to form the
-.IR args .
-.TP
-.I peer
-The name of the peer.
-.TP
-.I ifname
-The name of the network interface associated with the peer, as returned
-by the
-.B IFNAME
-administration command (see
-.BR tripe-admin (5)).
-.TP
-.I addr
-The network address of the peer's TrIPE server, in the form output by
-the
-.B ADDR
-administration command (see
-.BR tripe-admin (5)).
-The first word of
-.I addr
-is therefore a network address family, e.g.,
-.BR INET .
-.PP
-The
-.B watch
-service deconfigures interfaces by invoking an
-.B ifdown
-script, in a similar manner.  The script is invoked as
-.IP
-.I script
-.IR args ...
-.I peer
-.PP
-where the elements are as above, except that
-.I script
-and
-.I args
-are formed by splitting the value associated with the peer record's
-.B ifdown
-key.
-.PP
-In both of the above cases, if the relevant key (either
-.B ifup
-or
-.BR ifdown )
-is absent, no action is taken.
-.PP
-The key/value pairs in the peer's database record and the server's
-response to the
-.B ALGS
-administration command (see
-.BR tripe-admin (5))
-are passed to the
-.B ifup
-and
-.B ifdown
-scripts as environment variables.  The environment variable name
-corresponding to a key is determined as follows:
-.hP \*o
-Convert all letters to upper-case.
-.hP \*o Convert all sequences of one or more non-alphanumeric characters
-to an underscore
-.RB ` _ '.
-.hP \*o Prefix the resulting name by
-.RB ` P_ '
-or
-.RB ` A_ '
-depending on whether it came from the peer's database record or the
-.B ALGS
-output respectively.
-.PP
-For example,
-.B ifname
-becomes
-.BR P_IFNAME ;
-and
-.B cipher-blksz
-becomes
-.BR A_CIPHER_BLKSZ .
-.SS "Dynamic connection"
-If a peer's database record assigns a value to the
-.B connect
-key, then the
-.B watch
-service will attempt to establish a connection dynamically with the
-peer.  The value of the
-.B connect
-key is invoked as a Bourne shell command, i.e.,
-.IP
-.B /bin/sh \-c
-.I connect
-.PP
-is executed.  The command is expected to contact the remote server and
-report, on standard output, a challenge string.  The
-.B watch
-service reads this challenge, and submits the command
-.IP
-.B GREET
-.I peer
-.I challenge
-.PP
-Typically, the
-.B connect
-command will issue a command such as
-.IP
-.B SVCSUBMIT connect passive
-.I our-name
-.PP
-where
-.I our-name
-is the remote peer's name for this host.
-.PP
-Similarly, if the database record has a
-.B disconnect
-entry, then
-.B watch
-will use this to give the peer explicit notification that its services
-are no longer needed.  The value f the
-.B disconnect
-key is invoked as a Bourne shell command.  This ought to result in a
-.B KILL
-command being issued to the peer's server.
-.SS "Operation"
-On startup,
-.B watch
-requests a list of current peers from the
-.BR tripe (8)
-server, and adopts any eligible peers.  If the
-.B \-\-startup
-flag was passed on the command line, 
-the newly adopted peers have their interfaces configured and connection
-attempts are made.
-.PP
-Adopted peers are pinged at regular intervals (using the
-.B PING
-administrative command; see
-.BR tripe-admin (5)).
-This process can be configured by assigning values to keys in the peer's
-database record.  Some of these parameters are time intervals,
-expressed as a nonnegative integer followed optionally by
-.BR d ,
-.BR h ,
-.BR m ,
-or
-.B s
-for days, hours, minutes, or seconds, respectively; if no suffix is
-given, seconds are assumed.
-.PP
-The parameters are as follows.
-.TP
-.B every
-A time interval: how often to ping the peer to ensure that it's still
-alive.  The default is 2 minutes.
-.TP
-.B timeout
-A time interval: how long to wait for a reply before retrying or giving
-up.  The default is 10 seconds.
-.TP
-.B retries
-An integer: how many failed attempts to make before deciding that the
-peer is unreachable and taking action.  The default is 5 attempts.
-.PP
-The algorithm is as follows.  Send up to
-.I retries
-pings; if a reply is received before the
-.I timeout
-then the peer is alive; wait
-.I every
-and check again.  If no reply is received within the
-.IR timeout ,
-then try again up to
-.I retries
-times.  If no attempt succeeds, the peer is declared unreachable.  If
-the peer has a
-.B connect
-command (i.e., it connects dynamically) then another connection attempt
-is made.  Otherwise the peer is killed.
-.
-.\"--------------------------------------------------------------------------
-.SH "SERVICE COMMAND REFERENCE"
-.
-.\"* 10 Service commands
-The commands provided by the service are as follows.
-.SP
-.B adopted
-For each peer being tracked by the
-.B watch
-service, write a line
-.B INFO
-.IR name .
-(Compatibility note: it's possible that further information will be
-provided about each peer, in the form of subsequent tokens.  Clients
-should be prepared to ignore such tokens.)
-.SP
-.BI "kick " peer
-If
-.I peer
-is currently added, and its record in the peer database contains a
-.B connect
-key (see
-.BR peers.in )
-then force a reconnection attempt.  See
-.BR "Dynamic connection" .
-.
-.\"--------------------------------------------------------------------------
-.SH "NOTIFICATIONS"
-.
-.\"* 30 Notification broadcasts (NOTE codes)
-All notifications issued by
-.B watch
-begin with the tokens
-.BR "USER watch" .
-.SP
-.B "USER watch peerdb-update"
-The peer database has changed.  Other interested clients should reopen
-the database.
-.SP
-.BI "USER watch ping-failed " peer " " error\fR...
-An attempt to
-.B PING
-the named
-.I peer
-failed; the server replied
-.B FAIL
-.IR error ...
-.SP
-.BI "USER watch " process\fR... " stdout " line
-The
-.I process
-spawned by the
-.B watch
-service unexpectedly wrote
-.I line
-to its standard output.
-.
-.\"--------------------------------------------------------------------------
-.SH "WARNINGS"
-.
-.\"* 40 Warning broadcasts (WARN codes)
-All warnings issued by
-.B watch
-begin with the tokens
-.BR "USER watch" .
-.SP
-.BI "USER watch ping-ok " peer
-A reply was received to a
-.B PING
-sent to the
-.IR peer ,
-though earlier attempts had failed.
-.SP
-.BI "USER watch ping-timeout " peer " attempt " i " of " n
-No reply was received to a
-.B PING
-sent to the
-.IR peer .
-So far,
-.I i
-.BR PING s
-have been sent; if a total of
-.I n
-consecutive attempts time out, the
-.B watch
-service will take further action.
-.SP
-.B "USER watch reconnecting " peer
-The dynamically connected
-.I peer
-seems to be unresponsive.  The
-.B watch
-service will attempt to reconnect.
-.SP
-.BI "USER watch " process\fR... " stderr " line
-The
-.I process
-spawned by the
-.B watch
-service wrote
-.I line
-to its standard error.
-.SP
-.BI "USER watch " process\fR... " exit-nonzero " code
-The
-.I process
-spawned by the
-.B watch
-service exited with the nonzero status
-.IR code .
-.SP
-.BI "USER watch " process\fR... " exit-signal S" code
-The
-.I process
-spawned by the
-.B watch
-service was killed by signal
-.IR code .
-Here,
-.I code
-is the numeric value of the fatal signal.
-.SP
-.BI "USER watch " process\fR... " exit-unknown " status
-The
-.I process
-spawned by the
-.B watch
-service exited with an unknown
-.IR status .
-Here,
-.I status
-is the raw exit status, as returned by
-.BR waitpid (2),
-in hexadecimal.
-.
-.\"--------------------------------------------------------------------------
-.SH "CHILD PROCESS IDENTIFIERS"
-.
-.\"* 50 Child process identifiers
-Some of the warnings and notifications refer to processes spawned by
-.B watch
-under various circumstances.  The process identifiers are as follows.
-.SP
-.BI "connect " peer
-A child spawned in order to establish a dynamic connection with
-.IR peer .
-.SP
-.BI "disconnect " peer
-A child spawned in order to shut down a dynamic connection with
-.IR peer .
-.SP
-.BI "ifdown " peer
-A child spawned to deconfigure the network interface for
-.IR peer .
-.SP
-.BI "ifup " peer
-A child spawned to configure the network interface for
-.IR peer .
-.
-.\"--------------------------------------------------------------------------
-.SH "SUMMARY"
-.
-.\"= summary
-.
-.\"--------------------------------------------------------------------------
-.SH "SEE ALSO"
-.
-.BR tripe-service (7),
-.BR peers.in (5),
-.BR connect (8),
-.BR tripe (8).
-.
-.\"--------------------------------------------------------------------------
-.SH "AUTHOR"
-.
-Mark Wooding, <mdw@distorted.org.uk>
-.
-.\"----- That's all, folks --------------------------------------------------
diff --git a/svc/watch.in b/svc/watch.in
deleted file mode 100644 (file)
index 7bdda36..0000000
+++ /dev/null
@@ -1,773 +0,0 @@
-#! @PYTHON@
-### -*-python-*-
-###
-### Watch arrival and departure of peers
-###
-### (c) 2007 Straylight/Edgeware
-###
-
-###----- Licensing notice ---------------------------------------------------
-###
-### This file is part of Trivial IP Encryption (TrIPE).
-###
-### TrIPE is free software; you can redistribute it and/or modify
-### it under the terms of the GNU General Public License as published by
-### the Free Software Foundation; either version 2 of the License, or
-### (at your option) any later version.
-###
-### TrIPE is distributed in the hope that it will be useful,
-### but WITHOUT ANY WARRANTY; without even the implied warranty of
-### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-### GNU General Public License for more details.
-###
-### You should have received a copy of the GNU General Public License
-### along with TrIPE; if not, write to the Free Software Foundation,
-### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-
-VERSION = '@VERSION@'
-
-###--------------------------------------------------------------------------
-### External dependencies.
-
-from optparse import OptionParser
-import tripe as T
-import os as OS
-import signal as SIG
-import errno as E
-import cdb as CDB
-import mLib as M
-import re as RX
-from time import time
-import subprocess as PROC
-
-S = T.svcmgr
-
-###--------------------------------------------------------------------------
-### Running auxiliary commands.
-
-class SelLineQueue (M.SelLineBuffer):
-  """Glues the select-line-buffer into the coroutine queue system."""
-
-  def __new__(cls, file, queue, tag, kind):
-    """See __init__ for documentation."""
-    return M.SelLineBuffer.__new__(cls, file.fileno())
-
-  def __init__(me, file, queue, tag, kind):
-    """
-    Initialize a new line-reading adaptor.
-
-    The adaptor reads lines from FILE.  Each line is inserted as a message of
-    the stated KIND, bearing the TAG, into the QUEUE.  End-of-file is
-    represented as None.
-    """
-    me._q = queue
-    me._file = file
-    me._tag = tag
-    me._kind = kind
-    me.enable()
-
-  @T._callback
-  def line(me, line):
-    me._q.put((me._tag, me._kind, line))
-
-  @T._callback
-  def eof(me):
-    me.disable()
-    me._q.put((me._tag, me._kind, None))
-
-class ErrorWatch (T.Coroutine):
-  """
-  An object which watches stderr streams for errors and converts them into
-  warnings of the form
-
-    WARN watch INFO stderr LINE
-
-  The INFO is a list of tokens associated with the file when it was
-  registered.
-
-  Usually there is a single ErrorWatch object, called errorwatch.
-  """
-
-  def __init__(me):
-    """Initialization: there are no arguments."""
-    T.Coroutine.__init__(me)
-    me._q = T.Queue()
-    me._map = {}
-    me._seq = 1
-
-  def watch(me, file, info):
-    """
-    Adds FILE to the collection of files to watch.
-
-    INFO will be written in the warning messages from this FILE.  Returns a
-    sequence number which can be used to unregister the file again.
-    """
-    seq = me._seq
-    me._seq += 1
-    me._map[seq] = info, SelLineQueue(file, me._q, seq, 'stderr')
-    return seq
-
-  def unwatch(me, seq):
-    """Stop watching the file with sequence number SEQ."""
-    del me._map[seq]
-    return me
-
-  def run(me):
-    """
-    Coroutine function: read items from the queue and report them.
-
-    Unregisters files automatically when they reach EOF.
-    """
-    while True:
-      seq, _, line = me._q.get()
-      if line is None:
-        me.unwatch(seq)
-      else:
-        S.warn(*['watch'] + me._map[seq][0] + ['stderr', line])
-
-def dbwatch():
-  """
-  Coroutine function: wake up every second and notice changes to the
-  database.  When a change happens, tell the Pinger (q.v.) to rescan its
-  peers.
-  """
-  cr = T.Coroutine.getcurrent()
-  main = cr.parent
-  fw = M.FWatch(opts.cdb)
-  while True:
-    timer = M.SelTimer(time() + 1, lambda: cr.switch())
-    main.switch()
-    if fw.update():
-      pinger.rescan(False)
-      S.notify('watch', 'peerdb-update')
-
-class ChildWatch (M.SelSignal):
-  """
-  An object which watches for specified processes exiting and reports
-  terminations by writing items of the form (TAG, 'exit', RESULT) to a queue.
-
-  There is usually only one ChildWatch object, called childwatch.
-  """
-
-  def __new__(cls):
-    """Initialize the child-watcher."""
-    return M.SelSignal.__new__(cls, SIG.SIGCHLD)
-
-  def __init__(me):
-    """Initialize the child-watcher."""
-    me._pid = {}
-    me.enable()
-
-  def watch(me, pid, queue, tag):
-    """
-    Register PID as a child to watch.  If it exits, write (TAG, 'exit', CODE)
-    to the QUEUE, where CODE is one of
-
-      * None (successful termination)
-      * ['exit-nonzero', CODE] (CODE is a string!)
-      * ['exit-signal', 'S' + CODE] (CODE is the signal number as a string)
-      * ['exit-unknown', STATUS] (STATUS is the entire exit status, in hex)
-    """
-    me._pid[pid] = queue, tag
-    return me
-
-  def unwatch(me, pid):
-    """Unregister PID as a child to watch."""
-    del me._pid[pid]
-    return me
-
-  @T._callback
-  def signalled(me):
-    """
-    Called when child processes exit: collect exit statuses and report
-    failures.
-    """
-    while True:
-      try:
-        pid, status = OS.waitpid(-1, OS.WNOHANG)
-      except OSError, exc:
-        if exc.errno == E.ECHILD:
-          break
-      if pid == 0:
-        break
-      if pid not in me._pid:
-        continue
-      queue, tag = me._pid[pid]
-      if OS.WIFEXITED(status):
-        exit = OS.WEXITSTATUS(status)
-        if exit == 0:
-          code = None
-        else:
-          code = ['exit-nonzero', str(exit)]
-      elif OS.WIFSIGNALED(status):
-        code = ['exit-signal', 'S' + str(OS.WTERMSIG(status))]
-      else:
-        code = ['exit-unknown', hex(status)]
-      queue.put((tag, 'exit', code))
-
-class Command (object):
-  """
-  Represents a running command.
-
-  This class is the main interface to the machery provided by the ChildWatch
-  and ErrorWatch objects.  See also potwatch.
-  """
-
-  def __init__(me, info, queue, tag, args, env):
-    """
-    Start a new child process.
-
-    The ARGS are a list of arguments to be given to the child process.  The
-    ENV is either None or a dictionary of environment variable assignments to
-    override the extant environment.  INFO is a list of tokens to be included
-    in warnings about the child's stderr output.  If the child writes a line
-    to standard output, put (TAG, 'stdout', LINE) to the QUEUE.  When the
-    child exits, write (TAG, 'exit', CODE) to the QUEUE.
-    """
-    me._info = info
-    me._q = queue
-    me._tag = tag
-    myenv = OS.environ.copy()
-    if env: myenv.update(env)
-    me._proc = PROC.Popen(args = args, env = myenv, bufsize = 1,
-                          stdout = PROC.PIPE, stderr = PROC.PIPE)
-    me._lq = SelLineQueue(me._proc.stdout, queue, tag, 'stdout')
-    errorwatch.watch(me._proc.stderr, info)
-    childwatch.watch(me._proc.pid, queue, tag)
-
-  def __del__(me):
-    """
-    If I've been forgotten then stop watching for termination.
-    """
-    childwatch.unwatch(me._proc.pid)
-
-def potwatch(what, name, q):
-  """
-  Watch the queue Q for activity as reported by a Command object.
-
-  Information from the process's stdout is reported as
-
-    NOTE WHAT NAME stdout LINE
-
-  abnormal termination is reported as
-
-    WARN WHAT NAME CODE
-
-  where CODE is what the ChildWatch wrote.
-  """
-  eofp = deadp = False
-  while not deadp or not eofp:
-    _, kind, more = q.get()
-    if kind == 'stdout':
-      if more is None:
-        eofp = True
-      else:
-        S.notify('watch', what, name, 'stdout', more)
-    elif kind == 'exit':
-      if more: S.warn('watch', what, name, *more)
-      deadp = True
-
-###--------------------------------------------------------------------------
-### Peer database utilities.
-
-_magic = ['_magic']                     # An object distinct from all others
-
-class Peer (object):
-  """Representation of a peer in the database."""
-
-  def __init__(me, peer, cdb = None):
-    """
-    Create a new peer, named PEER.
-
-    Information about the peer is read from the database CDB, or the default
-    one given on the command-line.
-    """
-    me.name = peer
-    record = (cdb or CDB.init(opts.cdb))['P' + peer]
-    me.__dict__.update(M.URLDecode(record, semip = True))
-
-  def get(me, key, default = _magic, filter = None):
-    """
-    Get the information stashed under KEY from the peer's database record.
-
-    If DEFAULT is given, then use it if the database doesn't contain the
-    necessary information.  If no DEFAULT is given, then report an error.  If
-    a FILTER function is given then apply it to the information from the
-    database before returning it.
-    """
-    attr = me.__dict__.get(key, default)
-    if attr is _magic:
-      raise T.TripeJobError('malformed-peer', me.name, 'missing-key', key)
-    elif filter is not None:
-      attr = filter(attr)
-    return attr
-
-  def has(me, key):
-    """
-    Return whether the peer's database record has the KEY.
-    """
-    return key in me.__dict__
-
-  def list(me):
-    """
-    Iterate over the available keys in the peer's database record.
-    """
-    return me.__dict__.iterkeys()
-
-def boolean(value):
-  """Parse VALUE as a boolean."""
-  return value in ['t', 'true', 'y', 'yes', 'on']
-
-###--------------------------------------------------------------------------
-### Waking up and watching peers.
-
-def run_connect(peer, cmd):
-  """
-  Start the job of connecting to the passive PEER.
-
-  The CMD string is a shell command which will connect to the peer (via some
-  back-channel, say ssh and userv), issue a command
-
-    SVCSUBMIT connect passive [OPTIONS] USER
-
-  and write the resulting challenge to standard error.
-  """
-  q = T.Queue()
-  cmd = Command(['connect', peer.name], q, 'connect',
-                ['/bin/sh', '-c', cmd], None)
-  _, kind, more = q.peek()
-  if kind == 'stdout':
-    if more is None:
-      S.warn('watch', 'connect', peer.name, 'unexpected-eof')
-    else:
-      chal = more
-      S.greet(peer.name, chal)
-      q.get()
-  potwatch('connect', peer.name, q)
-
-def run_disconnect(peer, cmd):
-  """
-  Start the job of disconnecting from a passive PEER.
-
-  The CMD string is a shell command which will disconnect from the peer.
-  """
-  q = T.Queue()
-  cmd = Command(['disconnect', peer.name], q, 'disconnect',
-                ['/bin/sh', '-c', cmd], None)
-  potwatch('disconnect', peer.name, q)
-
-_pingseq = 0
-class PingPeer (object):
-  """
-  Object representing a peer which we are pinging to ensure that it is still
-  present.
-
-  PingPeer objects are held by the Pinger (q.v.).  The Pinger maintains an
-  event queue -- which saves us from having an enormous swarm of coroutines
-  -- but most of the actual work is done here.
-
-  In order to avoid confusion between different PingPeer instances for the
-  same actual peer, each PingPeer has a sequence number (its `seq'
-  attribute).  Events for the PingPeer are identified by a (PEER, SEQ) pair.
-  (Using the PingPeer instance itself will prevent garbage collection of
-  otherwise defunct instances.)
-  """
-
-  def __init__(me, pinger, queue, peer, pingnow):
-    """
-    Create a new PingPeer.
-
-    The PINGER is the Pinger object we should send the results to.  This is
-    used when we remove ourselves, if the peer has been explicitly removed.
-
-    The QUEUE is the event queue on which timer and ping-command events
-    should be written.
-
-    The PEER is a `Peer' object describing the peer.
-
-    If PINGNOW is true, then immediately start pinging the peer.  Otherwise
-    wait until the usual retry interval.
-    """
-    global _pingseq
-    me._pinger = pinger
-    me._q = queue
-    me._peer = peer.name
-    me.update(peer)
-    me.seq = _pingseq
-    _pingseq += 1
-    me._failures = 0
-    if pingnow:
-      me._timer = None
-      me._ping()
-    else:
-      me._timer = M.SelTimer(time() + me._every, me._time)
-
-  def update(me, peer):
-    """
-    Refreshes the timer parameters for this peer.  We don't, however,
-    immediately reschedule anything: that will happen next time anything
-    interesting happens.
-    """
-    if peer is None: peer = Peer(me._peer)
-    assert peer.name == me._peer
-    me._every = peer.get('every', filter = T.timespec, default = 120)
-    me._timeout = peer.get('timeout', filter = T.timespec, default = 10)
-    me._retries = peer.get('retries', filter = int, default = 5)
-    me._connectp = peer.has('connect')
-    return me
-
-  def _ping(me):
-    """
-    Send a ping to the peer; the result is sent to the Pinger's event queue.
-    """
-    S.rawcommand(T.TripeAsynchronousCommand(
-      me._q, (me._peer, me.seq),
-      ['EPING',
-       '-background', S.bgtag(),
-       '-timeout', str(me._timeout),
-       '--',
-       me._peer]))
-
-  def _reconnect(me):
-    peer = Peer(me._peer)
-    if peer.has('connect'):
-      S.warn('watch', 'reconnecting', me._peer)
-      S.forcekx(me._peer)
-      T.spawn(run_connect, peer, peer.get('connect'))
-      me._timer = M.SelTimer(time() + me._every, me._time)
-    else:
-      S.kill(me._peer)
-
-  def event(me, code, stuff):
-    """
-    Respond to an event which happened to this peer.
-
-    Timer events indicate that we should start a new ping.  (The server has
-    its own timeout which detects lost packets.)
-
-    We trap unknown-peer responses and detach from the Pinger.
-
-    If the ping fails and we run out of retries, we attempt to restart the
-    connection.
-    """
-    if code == 'TIMER':
-      me._failures = 0
-      me._ping()
-    elif code == 'FAIL':
-      S.notify('watch', 'ping-failed', me._peer, *stuff)
-      if not stuff:
-        pass
-      elif stuff[0] == 'unknown-peer':
-        me._pinger.kill(me._peer)
-      elif stuff[0] == 'ping-send-failed':
-        me._reconnect()
-    elif code == 'INFO':
-      if stuff[0] == 'ping-ok':
-        if me._failures > 0:
-          S.warn('watch', 'ping-ok', me._peer)
-        me._timer = M.SelTimer(time() + me._every, me._time)
-      elif stuff[0] == 'ping-timeout':
-        me._failures += 1
-        S.warn('watch', 'ping-timeout', me._peer,
-               'attempt', str(me._failures), 'of', str(me._retries))
-        if me._failures < me._retries:
-          me._ping()
-        else:
-          me._reconnect()
-      elif stuff[0] == 'ping-peer-died':
-        me._pinger.kill(me._peer)
-
-  @T._callback
-  def _time(me):
-    """
-    Handle timer callbacks by posting a timeout event on the queue.
-    """
-    me._timer = None
-    me._q.put(((me._peer, me.seq), 'TIMER', None))
-
-  def __str__(me):
-    return 'PingPeer(%s, %d, f = %d)' % (me._peer, me.seq, me._failures)
-  def __repr__(me):
-    return str(me)
-
-class Pinger (T.Coroutine):
-  """
-  The Pinger keeps track of the peers which we expect to be connected and
-  takes action if they seem to stop responding.
-
-  There is usually only one Pinger, called pinger.
-
-  The Pinger maintains a collection of PingPeer objects, and an event queue.
-  The PingPeers direct the results of their pings, and timer events, to the
-  event queue.  The Pinger's coroutine picks items off the queue and
-  dispatches them back to the PingPeers as appropriate.
-  """
-
-  def __init__(me):
-    """Initialize the Pinger."""
-    T.Coroutine.__init__(me)
-    me._peers = {}
-    me._q = T.Queue()
-
-  def run(me):
-    """
-    Coroutine function: reads the pinger queue and sends events to the
-    PingPeer objects they correspond to.
-    """
-    while True:
-      (peer, seq), code, stuff = me._q.get()
-      if peer in me._peers and seq == me._peers[peer].seq:
-        me._peers[peer].event(code, stuff)
-
-  def add(me, peer, pingnow):
-    """
-    Add PEER to the collection of peers under the Pinger's watchful eye.
-    The arguments are as for PingPeer: see above.
-    """
-    me._peers[peer.name] = PingPeer(me, me._q, peer, pingnow)
-    return me
-
-  def kill(me, peername):
-    """Remove PEER from the peers being watched by the Pinger."""
-    del me._peers[peername]
-    return me
-
-  def rescan(me, startup):
-    """
-    General resynchronization method.
-
-    We scan the list of peers (with connect scripts) known at the server.
-    Any which are known to the Pinger but aren't known to the server are
-    removed from our list; newly arrived peers are added.  (Note that a peer
-    can change state here either due to the server sneakily changing its list
-    without issuing notifications or, more likely, the database changing its
-    idea of whether a peer is interesting.)  Finally, PingPeers which are
-    still present are prodded to update their timing parameters.
-
-    This method is called once at startup to pick up the peers already
-    installed, and again by the dbwatcher coroutine when it detects a change
-    to the database.
-    """
-    if T._debug: print '# rescan peers'
-    correct = {}
-    start = {}
-    for name in S.list():
-      try: peer = Peer(name)
-      except KeyError: continue
-      if peer.get('watch', filter = boolean, default = False):
-        if T._debug: print '# interesting peer %s' % peer
-        correct[peer.name] = start[peer.name] = peer
-      elif startup:
-        if T._debug: print '# peer %s ready for adoption' % peer
-        start[peer.name] = peer
-    for name, obj in me._peers.items():
-      try:
-        peer = correct[name]
-      except KeyError:
-        if T._debug: print '# peer %s vanished' % name
-        del me._peers[name]
-      else:
-        obj.update(peer)
-    for name, peer in start.iteritems():
-      if name in me._peers: continue
-      if startup:
-        if T._debug: print '# setting up peer %s' % name
-        ifname = S.ifname(name)
-        addr = S.addr(name)
-        T.defer(adoptpeer, peer, ifname, *addr)
-      else:
-        if T._debug: print '# adopting new peer %s' % name
-        me.add(peer, True)
-    return me
-
-  def adopted(me):
-    """
-    Returns the list of peers being watched by the Pinger.
-    """
-    return me._peers.keys()
-
-###--------------------------------------------------------------------------
-### New connections.
-
-def encode_envvars(env, prefix, vars):
-  """
-  Encode the variables in VARS suitably for including in a program
-  environment.  Lowercase letters in variable names are forced to uppercase;
-  runs of non-alphanumeric characters are replaced by single underscores; and
-  the PREFIX is prepended.  The resulting variables are written to ENV.
-  """
-  for k, v in vars.iteritems():
-    env[prefix + r_bad.sub('_', k.upper())] = v
-
-r_bad = RX.compile(r'[\W_]+')
-def envvars(peer):
-  """
-  Translate the database information for a PEER into a dictionary of
-  environment variables with plausible upper-case names and a P_ prefix.
-  Also collect the crypto information into A_ variables.
-  """
-  env = {}
-  encode_envvars(env, 'P_', dict([(k, peer.get(k)) for k in peer.list()]))
-  encode_envvars(env, 'A_', S.algs(peer.name))
-  return env
-
-def run_ifupdown(what, peer, *args):
-  """
-  Run the interface up/down script for a peer.
-
-  WHAT is 'ifup' or 'ifdown'.  PEER names the peer in question.  ARGS is a
-  list of arguments to pass to the script, in addition to the peer name.
-
-  The command is run and watched in the background by potwatch.
-  """
-  q = T.Queue()
-  c = Command([what, peer.name], q, what,
-              M.split(peer.get(what), quotep = True)[0] +
-              [peer.name] + list(args),
-              envvars(peer))
-  potwatch(what, peer.name, q)
-
-def adoptpeer(peer, ifname, *addr):
-  """
-  Add a new peer to our collection.
-
-  PEER is the `Peer' object; IFNAME is the interface name for its tunnel; and
-  ADDR is the list of tokens representing its address.
-
-  We try to bring up the interface and provoke a connection to the peer if
-  it's passive.
-  """
-  if peer.has('ifup'):
-    T.Coroutine(run_ifupdown, name = 'ifup %s' % peer.name) \
-        .switch('ifup', peer, ifname, *addr)
-  cmd = peer.get('connect', default = None)
-  if cmd is not None:
-    T.Coroutine(run_connect, name = 'connect %s' % peer.name) \
-        .switch(peer, cmd)
-  if peer.get('watch', filter = boolean, default = False):
-    pinger.add(peer, False)
-
-def disownpeer(peer):
-  """Drop the PEER from the Pinger and put its interface to bed."""
-  try: pinger.kill(peer)
-  except KeyError: pass
-  cmd = peer.get('disconnect', default = None)
-  if cmd is not None:
-    T.Coroutine(run_disconnect, name = 'disconnect %s' % peer.name) \
-        .switch(peer, cmd)
-  if peer.has('ifdown'):
-    T.Coroutine(run_ifupdown, name = 'ifdown %s' % peer.name) \
-        .switch('ifdown', peer)
-
-def notify(_, code, *rest):
-  """
-  Watch for notifications.
-
-  We trap ADD and KILL notifications, and send them straight to addpeer and
-  delpeer respectively.
-  """
-  if code == 'ADD':
-    try: p = Peer(rest[0])
-    except KeyError: return
-    adoptpeer(p, *rest[1:])
-  elif code == 'KILL':
-    try: p = Peer(rest[0])
-    except KeyError: return
-    disownpeer(p, *rest[1:])
-
-###--------------------------------------------------------------------------
-### Command stubs.
-
-def cmd_stub(*args):
-  raise T.TripeJobError('not-implemented')
-
-def cmd_kick(name):
-  """
-  kick NAME: Force a new connection attempt for the NAMEd peer.
-  """
-  if name not in pinger.adopted():
-    raise T.TripeJobError('peer-not-adopted', name)
-  try: peer = Peer(name)
-  except KeyError: raise T.TripeJobError('unknown-peer', name)
-  T.spawn(connect, peer)
-
-def cmd_adopted():
-  """
-  adopted: Report a list of adopted peers.
-  """
-  for name in pinger.adopted():
-    T.svcinfo(name)
-
-###--------------------------------------------------------------------------
-### Start up.
-
-def setup():
-  """
-  Service setup.
-
-  Register the notification watcher, and rescan the peers.
-  """
-  S.handler['NOTE'] = notify
-  S.watch('+n')
-  pinger.rescan(opts.startup)
-
-def init():
-  """
-  Initialization to be done before service startup.
-  """
-  global errorwatch, childwatch, pinger
-  errorwatch = ErrorWatch()
-  childwatch = ChildWatch()
-  pinger = Pinger()
-  T.Coroutine(dbwatch, name = 'dbwatch').switch()
-  errorwatch.switch()
-  pinger.switch()
-
-def parse_options():
-  """
-  Parse the command-line options.
-
-  Automatically changes directory to the requested configdir, and turns on
-  debugging.  Returns the options object.
-  """
-  op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
-                    version = '%%prog %s' % VERSION)
-
-  op.add_option('-a', '--admin-socket',
-                metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
-                help = 'Select socket to connect to [default %default]')
-  op.add_option('-d', '--directory',
-                metavar = 'DIR', dest = 'dir', default = T.configdir,
-                help = 'Select current diretory [default %default]')
-  op.add_option('-p', '--peerdb',
-                metavar = 'FILE', dest = 'cdb', default = T.peerdb,
-                help = 'Select peers database [default %default]')
-  op.add_option('--daemon', dest = 'daemon',
-                default = False, action = 'store_true',
-                help = 'Become a daemon after successful initialization')
-  op.add_option('--debug', dest = 'debug',
-                default = False, action = 'store_true',
-                help = 'Emit debugging trace information')
-  op.add_option('--startup', dest = 'startup',
-                default = False, action = 'store_true',
-                help = 'Being called as part of the server startup')
-
-  opts, args = op.parse_args()
-  if args: op.error('no arguments permitted')
-  OS.chdir(opts.dir)
-  T._debug = opts.debug
-  return opts
-
-## Service table, for running manually.
-service_info = [('watch', T.VERSION, {
-  'adopted': (0, 0, '', cmd_adopted),
-  'kick': (1, 1, 'PEER', cmd_kick)
-})]
-
-if __name__ == '__main__':
-  opts = parse_options()
-  T.runservices(opts.tripesock, service_info,
-                init = init, setup = setup,
-                daemon = opts.daemon)
-
-###----- That's all, folks --------------------------------------------------