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/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
 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 "."
 }
 
   echo "."
 }
 
-restart_services conntrack connect watch
+restart_services conntrack connect
 
 #DEBHELPER#
 
 #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.
 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
 .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.
 .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.
 .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
 .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
 .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
 .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
 .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
 .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
 .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
 .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
 .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.
 .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
 .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
 .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
 .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
 .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.
 and
 .BR tripe-newpeers (8);
 described below.
+.
 .SS "Conversion"
 This section describes how the textual
 .B peers.in
 .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 tripe-newpeers (8),
 .BR peers.cdb (5),
 .BR connect (8),
-.BR watch (8),
 .BR tripe-ifup (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 $@
 
        $(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
 ## 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 --------------------------------------------------
                chmod +x $@.new && mv $@.new $@
 
 ###----- That's all, folks --------------------------------------------------
-
index 2cc3c5cea6b51255440f3cd45baec4ef71651d14..7021c6b6fd5a9976594eae14f4b08041d36aa158 100644 (file)
 .so ../defs.man.in \"@@@PRE@@@
 .
 .\"--------------------------------------------------------------------------
 .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"
 .
 .
 .\"--------------------------------------------------------------------------
 .SH "NAME"
 .
-connect \- tripe service to make connections to peers
+connect \- tripe service to handle addition and removal of peers
 .
 .\"--------------------------------------------------------------------------
 .SH "SYNOPSIS"
 .
 .\"--------------------------------------------------------------------------
 .SH "SYNOPSIS"
@@ -55,9 +55,24 @@ connect \- tripe service to make connections to peers
 .
 The
 .B connect
 .
 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)
 .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
 .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.
 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
 .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
 .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.
 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
 .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
 .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
 .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.
 .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
 .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"
 .
 .\"--------------------------------------------------------------------------
 .SH "SERVICE COMMAND REFERENCE"
@@ -174,6 +414,8 @@ The service will submit the command
 .IR time ]
 .RB [ \-key
 .IR tag ]
 .IR time ]
 .RB [ \-key
 .IR tag ]
+.RB [ \-priv
+.IR tag ]
 .RB [ \-mobile ]
 .RB [ \-tunnel
 .IR driver ]
 .RB [ \-mobile ]
 .RB [ \-tunnel
 .IR driver ]
@@ -212,6 +454,15 @@ to the
 key.
 .hP \*o
 The option
 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
 .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
 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 .
 .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
 .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
 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.
 .
 .\"--------------------------------------------------------------------------
 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
 .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
 .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
 .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
 .SP
-.BI "unknown-peer " peer
+.BI "USER connect " process\fR... " stdout " line
 The
 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"
 .
 .\"--------------------------------------------------------------------------
 .SH "WARNINGS"
@@ -372,6 +629,99 @@ automatically failed: the
 command reported
 .B FAIL
 .IR error ...
 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"
 .
 .\"--------------------------------------------------------------------------
 .SH "SUMMARY"
@@ -383,7 +733,6 @@ command reported
 .
 .BR tripe-service (7),
 .BR peers.in (5),
 .
 .BR tripe-service (7),
 .BR peers.in (5),
-.BR watch (8),
 .BR tripe (8).
 .
 .\"--------------------------------------------------------------------------
 .BR tripe (8).
 .
 .\"--------------------------------------------------------------------------
index 0031d36fcee85b72aa226a79f747742c11af2352..000ca50eba511232b1ae82dd3d7af613b1b33797 100644 (file)
@@ -1,9 +1,9 @@
 #! @PYTHON@
 ### -*-python-*-
 ###
 #! @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 ---------------------------------------------------
 ###
 
 ###----- Licensing notice ---------------------------------------------------
@@ -32,14 +32,243 @@ VERSION = '@VERSION@'
 from optparse import OptionParser
 import tripe as T
 import os as OS
 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 cdb as CDB
 import mLib as M
+import re as RX
 from time import time
 from time import time
+import subprocess as PROC
 
 S = T.svcmgr
 
 ###--------------------------------------------------------------------------
 
 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
 
 
 _magic = ['_magic']                     # An object distinct from all others
 
@@ -54,30 +283,382 @@ class Peer (object):
     one given on the command-line.
     """
     me.name = peer
     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))
 
     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
     """
     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)
     """
     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
 
     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 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.
 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)
 
   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.
   """
 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])
 
   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.
   """
   """
   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.
   """
   """
   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:
   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.
   """
   """
   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):
   """
 
 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)
     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))
   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')
     addr = cr.parent.switch()
     if addr is None:
       raise T.TripeJobError('connect-timeout')
-    addpeer(Peer(peer), addr)
+    addpeer(peer, addr)
   finally:
     del chalmap[chal]
 
   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.
 
 ###--------------------------------------------------------------------------
 ### Start up.
 
@@ -194,10 +803,14 @@ def setup():
   """
   Service 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')
   """
   S.handler['NOTE'] = notify
   S.watch('+n')
+
+  pinger.rescan(opts.startup)
+
   if opts.startup:
     cdb = CDB.init(opts.cdb)
     try:
   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)
 
       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.
 def parse_options():
   """
   Parse the command-line options.
@@ -247,18 +872,20 @@ def parse_options():
   return opts
 
 ## Service table, for running manually.
   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),
   '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,
   '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 --------------------------------------------------
                 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 --------------------------------------------------