chiark / gitweb /
tripemon: Major reworking to use new module and coroutines.
[tripe] / mon / tripemon.in
index c71644aa685152f0bb8d100714da7eac221967f1..e666ab9f3c06c5bfee0d6c5f44dcccd2b55d47ea 100644 (file)
@@ -1,16 +1,42 @@
 #! @PYTHON@
-# -*-python-*-
-
-#----- Dependencies ---------------------------------------------------------
+### -*- mode: python; coding: utf-8 -*-
+###
+### Graphical monitor for tripe server
+###
+### (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.
+
+###--------------------------------------------------------------------------
+### Dependencies.
 
 import socket as S
-from sys import argv, exit, stdin, stdout, stderr
+import tripe as T
+import mLib as M
+from sys import argv, exit, stdin, stdout, stderr, exc_info, excepthook
 import os as OS
 from os import environ
-import math as M
+import math as MATH
 import sets as SET
-import getopt as O
-import time as T
+from optparse import OptionParser
+import time as TIME
 import re as RX
 from cStringIO import StringIO
 
@@ -20,465 +46,284 @@ import gtk as G
 import gobject as GO
 import gtk.gdk as GDK
 
-#----- Configuration --------------------------------------------------------
+if OS.getenv('TRIPE_DEBUG_MONITOR') is not None:
+  T._debug = 1
 
-tripedir = "@configdir@"
-socketdir = "@socketdir@"
-PACKAGE = "@PACKAGE@"
-VERSION = "@VERSION@"
+###--------------------------------------------------------------------------
+### Doing things later.
 
-debug = False
+def uncaught():
+  """Report an uncaught exception."""
+  excepthook(*exc_info())
 
-#----- Utility functions ----------------------------------------------------
+_idles = []
+def _runidles():
+  """Invoke the functions on the idles queue."""
+  global _idles
+  while _idles:
+    old = _idles
+    _idles = []
+    for func, args, kw in old:
+      try:
+        func(*args, **kw)
+      except:
+        uncaught()
+  return False
+
+def idly(func, *args, **kw):
+  """Invoke FUNC(*ARGS, **KW) at some later point in time."""
+  if not _idles:
+    GO.idle_add(_runidles)
+  _idles.append((func, args, kw))
+
+_asides = T.Queue()
+def _runasides():
+  """
+  Coroutine function: reads (FUNC, ARGS, KW) triples from a queue and invokes
+  FUNC(*ARGS, **KW)
+  """
+  while True:
+    func, args, kw = _asides.get()
+    try:
+      func(*args, **kw)
+    except:
+      uncaught()
+
+def aside(func, *args, **kw):
+  """
+  Arrange for FUNC(*ARGS, **KW) to be performed at some point in the future,
+  and not from the main coroutine.
+  """
+  idly(_asides.put, (func, args, kw))
+
+def xwrap(func):
+  """
+  Return a function which behaves like FUNC, but reports exceptions via
+  uncaught.
+  """
+  def _(*args, **kw):
+    try:
+      return func(*args, **kw)
+    except SystemExit:
+      raise
+    except:
+      uncaught()
+      raise
+  return _
 
-## Program name, shorn of extraneous stuff.
-quis = OS.path.basename(argv[0])
+def invoker(func, *args, **kw):
+  """
+  Return a function which throws away its arguments and calls
+  FUNC(*ARGS, **KW).
 
-def moan(msg):
-  """Report a message to standard error."""
-  stderr.write('%s: %s\n' % (quis, msg))
+  If for loops worked by binding rather than assignment then we wouldn't need
+  this kludge.
+  """
+  return lambda *hunoz, **hukairz: xwrap(func)(*args, **kw)
 
-def die(msg, rc = 1):
-  """Report a message to standard error and exit."""
-  moan(msg)
-  exit(rc)
+def cr(func, *args, **kw):
+  """Return a function which invokes FUNC(*ARGS, **KW) in a coroutine."""
+  def _(*hunoz, **hukairz):
+    T.Coroutine(xwrap(func)).switch(*args, **kw)
+  return _
 
-rx_space = RX.compile(r'\s+')
-rx_ordinary = RX.compile(r'[^\\\'\"\s]+')
-rx_weird = RX.compile(r'([\\\'])')
-rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
-rx_num = RX.compile(r'^[-+]?\d+$')
+def incr(func):
+  """Decorator: runs its function in a coroutine of its own."""
+  return lambda *args, **kw: T.Coroutine(func).switch(*args, **kw)
 
-c_red = GDK.color_parse('red')
+###--------------------------------------------------------------------------
+### Random bits of infrastructure.
 
-def getword(s):
-  """Pull a word from the front of S, handling quoting according to the
-  tripe-admin(5) rules.  Returns the word and the rest of S, or (None, None)
-  if there are no more words left."""
-  i = 0
-  m = rx_space.match(s, i)
-  if m: i = m.end()
-  r = ''
-  q = None
-  if i >= len(s):
-    return None, None
-  while i < len(s) and (q or not s[i].isspace()):
-    m = rx_ordinary.match(s, i)
-    if m:
-      r += m.group()
-      i = m.end()
-    elif s[i] == '\\':
-      r += s[i + 1]
-      i += 2
-    elif s[i] == q:
-      q = None
-      i += 1
-    elif not q and s[i] == '`' or s[i] == "'":
-      q = "'"
-      i += 1
-    elif not q and s[i] == '"':
-      q = '"'
-      i += 1
-    else:
-      r += s[i]
-      i += 1
-  if q:
-    raise SyntaxError, 'missing close quote'
-  m = rx_space.match(s, i)
-  if m: i = m.end()
-  return r, s[i:]
-
-def quotify(s):
-  """Quote S according to the tripe-admin(5) rules."""
-  m = rx_ordinary.match(s)
-  if m and m.end() == len(s):
-    return s
-  else:
-    return "'" + rx_weird.sub(r'\\\1', s) + "'"
-
-#----- Random bits of infrastructure ----------------------------------------
-
-class struct (object):
-  """Simple object which stores attributes and has a sensible construction
-  syntax."""
-  def __init__(me, **kw):
-    me.__dict__.update(kw)
+## Program name, shorn of extraneous stuff.
+M.ego(argv[0])
+moan = M.moan
+die = M.die
 
-class peerinfo (struct): pass
-class pingstate (struct): pass
+class HookList (object):
+  """
+  Notification hook list.
 
-def invoker(func):
-  """Return a function which throws away its arguments and calls FUNC.  (If
-  for loops worked by binding rather than assignment then we wouldn't need
-  this kludge."""
-  return lambda *hunoz, **hukairz: func()
+  Other objects can add functions onto the hook list.  When the hook list is
+  run, the functions are called in the order in which they were registered.
+  """
 
-class HookList (object):
-  """I maintain a list of functions, and provide the ability to call them
-  when something interesting happens.  The functions are called in the order
-  they were added to the list, with all the arguments.  If a function returns
-  a non-None result, no further functions are called."""
   def __init__(me):
+    """Basic initialization: create the hook list."""
     me.list = []
+
   def add(me, func, obj):
+    """Add FUNC to the list of hook functions."""
     me.list.append((obj, func))
+
   def prune(me, obj):
+    """Remove hook functions registered with the given OBJ."""
     new = []
     for o, f in me.list:
       if o is not obj:
         new.append((o, f))
     me.list = new
+
   def run(me, *args, **kw):
+    """Invoke the hook functions with arguments *ARGS and **KW."""
     for o, hook in me.list:
       rc = hook(*args, **kw)
       if rc is not None: return rc
     return None
 
+  def runidly(me, *args, **kw):
+    """
+    Invoke the hook functions as for run, but at some point in the future.
+    """
+    idly(me.run, *args, **kw)
+
 class HookClient (object):
+  """
+  Mixin for classes which are clients of hooks.
+
+  It keeps track of the hooks it's a client of, and has the ability to
+  extricate itself from all of them.  This is useful because weak objects
+  don't seem to work well.
+  """
   def __init__(me):
+    """Basic initialization."""
     me.hooks = SET.Set()
+
   def hook(me, hk, func):
+    """Add FUNC to the hook list HK."""
     hk.add(func, me)
     me.hooks.add(hk)
+
   def unhook(me, hk):
+    """Remove myself from the hook list HK."""
     hk.prune(me)
     me.hooks.discard(hk)
+
   def unhookall(me):
+    """Remove myself from all hook lists."""
     for hk in me.hooks:
       hk.prune(me)
     me.hooks.clear()
-  ##def __del__(me):
-  ##  print '%s dying' % me
 
-#----- Connections and commands ---------------------------------------------
+class struct (object):
+  """A very simple dumb data container object."""
+  def __init__(me, **kw):
+    me.__dict__.update(kw)
 
-class ConnException (Exception):
-  """Some sort of problem occurred while communicating with the tripe
-  server."""
-  pass
+## Matches ISO date format yyyy-mm-ddThh:mm:ss.
+rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
 
-class Error (ConnException):
-  """A command caused the server to issue a FAIL message."""
-  pass
+###--------------------------------------------------------------------------
+### Connections.
 
-class ConnectionFailed (ConnException):
-  """The connection failed while communicating with the server."""
+class Connection (T.TripeCommandDispatcher):
+  """
+  The main connection to the server.
 
-jobid_seq = 0
-def jobid():
-  """Return a job tag.  Used for background commands."""
-  global jobid_seq
-  jobid_seq += 1
-  return 'bg-%d' % jobid_seq
+  The improvement over the TripeCommandDispatcher is that the Connection
+  provides hooklists for NOTE, WARN and TRACE messages, and for connect and
+  disconnect events.
 
-class BackgroundCommand (HookClient):
-  def __init__(me, conn, cmd):
-    HookClient.__init__(me)
-    me.conn = conn
-    me.tag = None
-    me.cmd = cmd
-    me.donehook = HookList()
-    me.losthook = HookList()
-    me.info = []
-    me.submit()
-    me.hook(me.conn.disconnecthook, me.lost)
-  def submit(me):
-    me.conn.bgcommand(me.cmd, me)
-  def lost(me):
-    me.losthook.run()
-    me.unhookall()
-  def fail(me, msg):
-    me.conn.error("Unexpected error from server command `%s': %s" %
-                  (me.cmd % msg))
-    me.unhookall()
-  def ok(me):
-    me.donehook.run(me.info)
-    me.unhookall()
+  This class knows about the Glib I/O dispatcher system, and plugs into it.
 
-class SimpleBackgroundCommand (BackgroundCommand):
-  def submit(me):
-    try:
-      BackgroundCommand.submit(me)
-    except ConnectionFailed, err:
-      me.conn.error('Unexpected error communicating with server: %s' % msg)
-      raise
-
-class Connection (HookClient):
-
-  """I represent a connection to the TrIPE server.  I provide facilities for
-  sending commands and receiving replies.  The connection is notional: the
-  underlying socket connection can come and go under our feet.
+  Hooks:
 
-  Useful attributes:
-  connectedp: whether the connection is active
-  connecthook: called when we have connected
-  disconnecthook: called if we have disconnected
-  notehook: called with asynchronous notifications
-  errorhook: called if there was a command error"""
+    * connecthook(): a connection to the server has been established
+    * disconnecthook(): the connection has been dropped
+    * notehook(TOKEN, ...): server issued a notification
+    * warnhook(TOKEN, ...): server issued a warning
+    * tracehook(TOKEN, ...): server issued a trace message
+  """
 
-  def __init__(me, sockname):
-    """Make a new connection to the server listening to SOCKNAME.  In fact,
-    we're initially disconnected, to allow the caller to get his life in
-    order before opening the floodgates."""
-    HookClient.__init__(me)
-    me.sockname = sockname
-    me.sock = None
-    me.connectedp = False
+  def __init__(me, socket):
+    """Create a new Connection."""
+    T.TripeCommandDispatcher.__init__(me, socket)
     me.connecthook = HookList()
     me.disconnecthook = HookList()
-    me.errorhook = HookList()
-    me.inbuf = ''
-    me.info = []
-    me.waitingp = False
-    me.bgcmd = None
-    me.bgmap = {}
-  def connect(me):
-    "Connect to the server.  Runs connecthook if it works."""
-    if me.sock: return
-    sock = S.socket(S.AF_UNIX, S.SOCK_STREAM)
-    try:
-      sock.connect(me.sockname)
-    except S.error, err:
-      me.error('error opening connection: %s' % err[1])
-      me.disconnecthook.run()
-      return
-    sock.setblocking(0)
-    me.socketwatch = GO.io_add_watch(sock, GO.IO_IN, me.ready)
-    me.sock = sock
-    me.connectedp = True
-    me.connecthook.run()
-  def disconnect(me):
-    "Disconnects from the server.  Runs disconnecthook."
-    if not me.sock: return
-    GO.source_remove(me.socketwatch)
-    me.sock.close()
-    me.sock = None
-    me.connectedp = False
-    me.disconnecthook.run()
-  def error(me, msg):
-    """Reports an error on the connection."""
-    me.errorhook.run(msg)
-
-  def bgcommand(me, cmd, bg):
-    """Sends a background command and feeds it properly."""
-    try:
-      me.bgcmd = bg
-      err = me.docommand(cmd)
-      if err:
-        bg.fail(err)
-    finally:
-      me.bgcmd = None
-  def command(me, cmd):
-    """Sends a command to the server.  Returns a list of INFO responses.  Do
-    not use this for backgrounded commands: create a BackgroundCommand
-    instead.  Raises apprpopriate exceptions on error, but doesn't send
-    report them to the errorhook."""
-    err = me.docommand(cmd)
-    if err:
-      raise Error, err
-    return me.info
-  def docommand(me, cmd):
-    if not me.sock:
-      raise ConnException, 'not connected'
-    if debug: print ">>> %s" % cmd
-    me.sock.sendall(cmd + '\n')
-    me.waitingp = True
-    me.info = []
-    try:
-      me.sock.setblocking(1)
-      while True:
-        rc, err = me.collect()
-        if rc: break
-    finally:
-      me.waitingp = False
-      me.sock.setblocking(0)
-      if len(me.inbuf) > 0:
-        GO.idle_add(lambda: me.flushbuf() and False)
-    return err
-  def simplecmd(me, cmd):
-    """Like command(), but reports errors via the errorhook as well as
-    raising exceptions."""
-    try:
-      i = me.command(cmd)
-    except Error, msg:
-      me.error("Unexpected error from server command `%s': %s" % (cmd, msg))
-      raise
-    except ConnectionFailed, msg:
-      me.error("Unexpected error communicating with server: %s" % msg);
-      raise
-    return i
-  def ready(me, sock, condition):
-    try:
-      me.collect()
-    except ConnException, msg:
-      me.error("Error watching server connection: %s" % msg)
-      if me.sock:
-        me.disconnect()
-        me.connect()
-    return True
-  def collect(me):
-    data = me.sock.recv(16384)
-    if data == '':
-      me.disconnect()
-      raise ConnectionFailed, 'server disconnected'
-    me.inbuf += data
-    return me.flushbuf()
-  def flushbuf(me):
-    while True:
-      nl = me.inbuf.find('\n')
-      if nl < 0: break
-      line = me.inbuf[:nl]
-      if debug: print "<<< %s" % line
-      me.inbuf = me.inbuf[nl + 1:]
-      tag, line = getword(line)
-      rc, err = me.parseline(tag, line)
-      if rc: return rc, err
-    return False, None
-  def parseline(me, code, line):
-    if code == 'BGDETACH':
-      if not me.bgcmd:
-        raise ConnectionFailed, 'unexpected detach'
-      me.bgcmd.tag = line
-      me.bgmap[line] = me.bgcmd
-      me.waitingp = False
-      me.bgcmd = None
-      return True, None
-    elif code == 'BGINFO':
-      tag, line = getword(line)
-      me.bgmap[tag].info.append(line)
-      return False, None
-    elif code == 'BGFAIL':
-      tag, line = getword(line)
-      me.bgmap[tag].fail(line)
-      del me.bgmap[tag]
-      return False, None
-    elif code == 'BGOK':
-      tag, line = getword(line)
-      me.bgmap[tag].ok()
-      del me.bgmap[tag]
-      return False, None
-    elif code == 'INFO':
-      if not me.waitingp or me.bgcmd:
-        raise ConnectionFailed, 'unexpected INFO response'
-      me.info.append(line)
-      return False, None
-    elif code == 'OK':
-      if not me.waitingp or me.bgcmd:
-        raise ConnectionFailed, 'unexpected OK response'
-      return True, None
-    elif code == 'FAIL':
-      if not me.waitingp:
-        raise ConnectionFailed, 'unexpected FAIL response'
-      return True, line
-    else:
-      raise ConnectionFailed, 'unknown response code `%s' % code
-
-class Monitor (Connection):
-  """I monitor a TrIPE server, noticing when it changes state and keeping
-  track of its peers.  I also provide facilities for sending the server
-  commands and collecting the answers.
-
-  Useful attributes:
-  addpeerhook: called with a new Peer when the server adds one
-  delpeerhook: called with a Peer when the server kills one
-  tracehook: called with a trace message
-  warnhook: called with a warning message
-  peers: mapping from names to Peer objects"""
-  def __init__(me, sockname):
-    """Initializes the monitor."""
-    Connection.__init__(me, sockname)
-    me.addpeerhook = HookList()
-    me.delpeerhook = HookList()
-    me.tracehook = HookList()
-    me.warnhook = HookList()
     me.notehook = HookList()
-    me.hook(me.connecthook, me.connected)
-    me.delay = []
-    me.peers = {}
-  def addpeer(me, peer):
-    if peer not in me.peers:
-      p = Peer(me, peer)
-      me.peers[peer] = p
-      me.addpeerhook.run(p)
-  def delpeer(me, peer):
-    if peer in me.peers:
-      p = me.peers[peer]
-      me.delpeerhook.run(p)
-      p.dead()
-      del me.peers[peer]
-  def updatelist(me, peers):
-    newmap = {}
-    for p in peers:
-      newmap[p] = True
-      if p not in me.peers:
-        me.addpeer(p)
-    oldpeers = me.peers.copy()
-    for p in oldpeers:
-      if p not in newmap:
-        me.delpeer(p)
+    me.warnhook = HookList()
+    me.tracehook = HookList()
+    me.handler['NOTE'] = lambda _, *rest: me.notehook.run(*rest)
+    me.handler['WARN'] = lambda _, *rest: me.warnhook.run(*rest)
+    me.handler['TRACE'] = lambda _, *rest: me.tracehook.run(*rest)
+    me._watch = None
+
   def connected(me):
-    try:
-      me.simplecmd('WATCH -A+wnt')
-      me.updatelist([s.strip() for s in me.simplecmd('LIST')])
-    except ConnException:
-      me.disconnect()
-      return
-  def parseline(me, code, line):
-    ## Delay async messages until the current command is done.  Otherwise the
-    ## handler for the async message might send another command before this
-    ## one's complete, and the whole edifice turns to jelly.
-    ##
-    ## No, this isn't the server's fault.  If we rely on the server to delay
-    ## notifications then there's a race between when we send a command and
-    ## when the server gets it.
-    if me.waitingp and code in ('TRACE', 'WARN', 'NOTE'):
-      if len(me.delay) == 0: GO.idle_add(me.flushdelay)
-      me.delay.append((code, line))
-    elif code == 'TRACE':
-      me.tracehook.run(line)
-    elif code == 'WARN':
-      me.warnhook.run(line)
-    elif code == 'NOTE':
-      note, line = getword(line)
-      me.notehook.run(note, line)
-      if note == 'ADD':
-        me.addpeer(getword(line)[0])
-      elif note == 'KILL':
-        me.delpeer(line)
-      else:
-        ## Well, I asked for it.
-        pass
-    else:
-      return Connection.parseline(me, code, line)
-    return False, None
-  def flushdelay(me):
-    delay = me.delay
-    me.delay = []
-    for tag, line in delay:
-      me.parseline(tag, line)
-    return False
-
-def parseinfo(info):
-  """Parse key=value output into a dictionary."""
-  d = {}
-  for i in info:
-    for w in i.split(' '):
-      q = w.index('=')
-      d[w[:q]] = w[q + 1:]
-  return d
-
-class Peer (object):
-  """I represent a TrIPE peer.  Useful attributes are:
-
-  name: peer's name
-  addr: human-friendly representation of the peer's address
-  ifname: interface associated with the peer
-  alivep: true if the peer hasn't been killed
-  deadhook: called with no arguments when the peer is killed"""
-  def __init__(me, monitor, name):
-    me.mon = monitor
+    """Handles reconnection to the server, and signals the hook."""
+    T.TripeCommandDispatcher.connected(me)
+    me._watch = GO.io_add_watch(me.sock, GO.IO_IN, invoker(me.receive))
+    me.connecthook.run()
+
+  def disconnected(me, reason):
+    """Handles disconnection from the server, and signals the hook."""
+    GO.source_remove(me._watch)
+    me._watch = None
+    me.disconnecthook.run(reason)
+    T.TripeCommandDispatcher.disconnected(me, reason)
+
+###--------------------------------------------------------------------------
+### Watching the peers go by.
+
+class MonitorObject (object):
+  """
+  An object with hooks it uses to notify others of changes in its state.
+  These are the objects tracked by the MonitorList class.
+
+  The object has a name, an `aliveness' state indicated by the `alivep' flag,
+  and hooks.
+
+  Hooks:
+
+    * changehook(): the object has changed its state
+    * deadhook(): the object has been destroyed
+
+  Subclass responsibilities:
+
+    * update(INFO): update internal state based on the provided INFO, and run
+      the changehook.
+  """
+
+  def __init__(me, name):
+    """Initialize the object with the given NAME."""
     me.name = name
-    addr = me.mon.simplecmd('ADDR %s' % name)[0].split(' ')
+    me.deadhook = HookList()
+    me.changehook = HookList()
+    me.alivep = True
+
+  def dead(me):
+    """Mark the object as dead; invoke the deadhook."""
+    me.alivep = False
+    me.deadhook.run()
+
+class Peer (MonitorObject):
+  """
+  An object representing a connected peer.
+
+  As well as the standard hooks, a peer has a pinghook, which isn't used
+  directly by this class.
+
+  Hooks:
+
+    * pinghook(): invoked by the Pinger (q.v.) when ping statistics change
+
+  Attributes provided are:
+
+    * addr = a vaguely human-readable representation of the peer's address
+    * ifname = the peer's interface name
+    * tunnel = the kind of tunnel the peer is using
+    * keepalive = the peer's keepalive interval in seconds
+    * ping['EPING'] and ping['PING'] = pingstate statistics (maintained by
+      the Pinger)
+  """
+
+  def __init__(me, name):
+    """Initialize the object with the given name."""
+    MonitorObject.__init__(me, name)
+    me.pinghook = HookList()
+    me.update()
+
+  def update(me, hunoz = None):
+    """Update the peer, fetching information about it from the server."""
+    addr = conn.addr(me.name)
     if addr[0] == 'INET':
       ipaddr, port = addr[1:]
       try:
@@ -488,37 +333,233 @@ class Peer (object):
         me.addr = 'INET %s:%s' % (ipaddr, port)
     else:
       me.addr = ' '.join(addr)
-    me.ifname = me.mon.simplecmd('IFNAME %s' % me.name)[0]
-    me.__dict__.update(parseinfo(me.mon.simplecmd('PEERINFO %s' % me.name)))
-    me.deadhook = HookList()
-    me.alivep = True
-  def dead(me):
-    me.alivep = False
-    me.deadhook.run()
+    me.ifname = conn.ifname(me.name)
+    me.__dict__.update(conn.peerinfo(me.name))
+    me.changehook.run()
+
+  def setifname(me, newname):
+    """Informs the object of a change to its interface name to NEWNAME."""
+    me.ifname = newname
+    me.changehook.run()
+
+class Service (MonitorObject):
+  """
+  Represents a service.
+
+  Additional attributes are:
+
+    * version = the service version
+  """
+  def __init__(me, name, version):
+    MonitorObject.__init__(me, name)
+    me.version = version
+
+  def update(me, version):
+    """Tell the Service that its version has changed to VERSION."""
+    me.version = version
+    me.changehook.run()
+
+class MonitorList (object):
+  """
+  Maintains a collection of MonitorObjects.
+
+  The MonitorList can be indexed by name to retrieve the individual objects;
+  iteration generates the individual objects.  More complicated operations
+  can be done on the `table' dictionary directly.
+
+  Hooks addhook(OBJ) and delhook(OBJ) are invoked when objects are added or
+  deleted.
 
-#----- Window management cruft ----------------------------------------------
+  Subclass responsibilities:
+
+    * list(): return a list of (NAME, INFO) pairs.
+
+    * make(NAME, INFO): returns a new MonitorObject for the given NAME; INFO
+      is from the output of list().
+  """
+
+  def __init__(me):
+    """Initialize a new MonitorList."""
+    me.table = {}
+    me.addhook = HookList()
+    me.delhook = HookList()
+
+  def update(me):
+    """
+    Refresh the list of objects:
+
+    We add new object which have appeared, delete ones which have vanished,
+    and update any which persist.
+    """
+    new = {}
+    for name, stuff in me.list():
+      new[name] = True
+      me.add(name, stuff)
+    for name in me.table.copy():
+      if name not in new:
+        me.remove(name)
+
+  def add(me, name, stuff):
+    """
+    Add a new object created by make(NAME, STUFF) if it doesn't already
+    exist.  If it does, update it.
+    """
+    if name not in me.table:
+      obj = me.make(name, stuff)
+      me.table[name] = obj
+      me.addhook.run(obj)
+    else:
+      me.table[name].update(stuff)
+
+  def remove(me, name):
+    """
+    Remove the object called NAME from the list.
+
+    The object becomes dead.
+    """
+    if name in me.table:
+      obj = me.table[name]
+      del me.table[name]
+      me.delhook.run(obj)
+      obj.dead()
+
+  def __getitem__(me, name):
+    """Retrieve the object called NAME."""
+    return me.table[name]
+
+  def __iter__(me):
+    """Iterate over the objects."""
+    return me.table.itervalues()
+
+class PeerList (MonitorList):
+  """The list of the known peers."""
+  def list(me):
+    return [(name, None) for name in conn.list()]
+  def make(me, name, stuff):
+    return Peer(name)
+
+class ServiceList (MonitorList):
+  """The list of the registered services."""
+  def list(me):
+    return conn.svclist()
+  def make(me, name, stuff):
+    return Service(name, stuff)
+
+class Monitor (HookClient):
+  """
+  The main monitor: keeps track of the changes happening to the server.
+
+  Exports the peers, services MonitorLists, and a (plain Python) list
+  autopeers of peers which the connect service knows how to start by name.
+
+  Hooks provided:
+
+    * autopeershook(): invoked when the auto-peers list changes.
+  """
+  def __init__(me):
+    """Initialize the Monitor."""
+    HookClient.__init__(me)
+    me.peers = PeerList()
+    me.services = ServiceList()
+    me.hook(conn.connecthook, me._connected)
+    me.hook(conn.notehook, me._notify)
+    me.autopeershook = HookList()
+    me.autopeers = None
+
+  def _connected(me):
+    """Handle a successful connection by starting the setup coroutine."""
+    me._setup()
+
+  @incr
+  def _setup(me):
+    """Coroutine function: initialize for a new connection."""
+    conn.watch('-A+wnt')
+    me.peers.update()
+    me.services.update()
+    me._updateautopeers()
+
+  def _updateautopeers(me):
+    """Update the auto-peers list from the connect service."""
+    if 'connect' in me.services.table:
+      me.autopeers = [' '.join(line)
+                      for line in conn.svcsubmit('connect', 'list')]
+      me.autopeers.sort()
+    else:
+      me.autopeers = None
+    me.autopeershook.run()
+
+  def _notify(me, code, *rest):
+    """
+    Handle notifications from the server.
+
+    ADD, KILL and NEWIFNAME notifications get passed up to the PeerList;
+    SVCCLAIM and SVCRELEASE get passed up to the ServiceList.  Finally,
+    peerdb-update notifications from the watch service cause us to refresh
+    the auto-peers list.
+    """
+    if code == 'ADD':
+      aside(me.peers.add, rest[0], None)
+    elif code == 'KILL':
+      aside(me.peers.remove, rest[0])
+    elif code == 'NEWIFNAME':
+      try:
+        me.peers[rest[0]].setifname(rest[2])
+      except KeyError:
+        pass
+    elif code == 'SVCCLAIM':
+      aside(me.services.add, rest[0], rest[1])
+      if rest[0] == 'connect':
+        aside(me._updateautopeers)
+    elif code == 'SVCRELEASE':
+      aside(me.services.remove, rest[0])
+      if rest[0] == 'connect':
+        aside(me._updateautopeers)
+    elif code == 'USER':
+      if not rest: return
+      if rest[0] == 'watch' and \
+         rest[1] == 'peerdb-update':
+        aside(me._updateautopeers)
+
+###--------------------------------------------------------------------------
+### Window management cruft.
 
 class MyWindowMixin (G.Window, HookClient):
-  """Mixin for windows which call a closehook when they're destroyed."""
+  """
+  Mixin for windows which call a closehook when they're destroyed.  It's also
+  a hookclient, and will release its hooks when it's destroyed.
+
+  Hooks:
+
+    * closehook(): called when the window is closed.
+  """
+
   def mywininit(me):
+    """Initialization function.  Note that it's not called __init__!"""
     me.closehook = HookList()
     HookClient.__init__(me)
     me.connect('destroy', invoker(me.close))
+
   def close(me):
+    """Close the window, invoking the closehook and releasing all hooks."""
     me.closehook.run()
     me.destroy()
     me.unhookall()
+
 class MyWindow (MyWindowMixin):
-  """A window which calls a closehook when it's destroyed."""
+  """A version of MyWindowMixin suitable as a single parent class."""
   def __init__(me, kind = G.WINDOW_TOPLEVEL):
     G.Window.__init__(me, kind)
     me.mywininit()
+
 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
   """A dialogue box with a closehook and sensible button binding."""
+
   def __init__(me, title = None, flags = 0, buttons = []):
-    """The buttons are a list of (STOCKID, THUNK) pairs: call the appropriate
-    THUNK when the button is pressed.  The others are just like GTK's Dialog
-    class."""
+    """
+    The BUTTONS are a list of (STOCKID, THUNK) pairs: call the appropriate
+    THUNK when the button is pressed.  The other arguments are just like
+    GTK's Dialog class.
+    """
     i = 0
     br = []
     me.rmap = []
@@ -528,20 +569,25 @@ class MyDialog (G.Dialog, MyWindowMixin, HookClient):
       me.rmap.append(f)
       i += 1
     G.Dialog.__init__(me, title, None, flags, tuple(br))
-    HookClient.__init__(me)
     me.mywininit()
     me.set_default_response(i - 1)
     me.connect('response', me.respond)
+
   def respond(me, hunoz, rid, *hukairz):
+    """Dispatch responses to the appropriate thunks."""
     if rid >= 0: me.rmap[rid]()
 
 def makeactiongroup(name, acts):
-  """Creates an ActionGroup called NAME.  ACTS is a list of tuples
-  containing:
-  ACT: an action name
-  LABEL: the label string for the action
-  ACCEL: accelerator string, or None
-  FUNC: thunk to call when the action is invoked"""
+  """
+  Creates an ActionGroup called NAME.
+
+  ACTS is a list of tuples containing:
+
+    * ACT: an action name
+    * LABEL: the label string for the action
+    * ACCEL: accelerator string, or None
+    * FUNC: thunk to call when the action is invoked
+  """
   actgroup = G.ActionGroup(name)
   for act, label, accel, func in acts:
     a = G.Action(act, label, None, None)
@@ -550,9 +596,12 @@ def makeactiongroup(name, acts):
   return actgroup
 
 class GridPacker (G.Table):
-  """Like a Table, but with more state: makes filling in the widgets
-  easier."""
+  """
+  Like a Table, but with more state: makes filling in the widgets easier.
+  """
+
   def __init__(me):
+    """Initialize a new GridPacker."""
     G.Table.__init__(me)
     me.row = 0
     me.col = 0
@@ -561,12 +610,17 @@ class GridPacker (G.Table):
     me.set_border_width(4)
     me.set_col_spacings(4)
     me.set_row_spacings(4)
+
   def pack(me, w, width = 1, newlinep = False,
            xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
            xpad = 0, ypad = 0):
-    """Packs a new widget.  W is the widget to add.  XOPY, YOPT, XPAD and
-    YPAD are as for Table.  WIDTH is how many cells to take up horizontally.
-    NEWLINEP is whether to start a new line for this widget.  Returns W."""
+    """
+    Packs a new widget.
+
+    W is the widget to add.  XOPY, YOPT, XPAD and YPAD are as for Table.
+    WIDTH is how many cells to take up horizontally.  NEWLINEP is whether to
+    start a new line for this widget.  Returns W.
+    """
     if newlinep:
       me.row += 1
       me.col = 0
@@ -580,35 +634,50 @@ class GridPacker (G.Table):
               xopt, yopt, xpad, ypad)
     me.col += width
     return w
+
   def labelled(me, lab, w, newlinep = False, **kw):
-    """Packs a labelled widget.  Other arguments are as for pack.  Returns
-    W."""
-    label = G.Label(lab)
+    """
+    Packs a labelled widget.
+
+    Other arguments are as for pack.  Returns W.
+    """
+    label = G.Label(lab + ' ')
     label.set_alignment(1.0, 0)
     me.pack(label, newlinep = newlinep, xopt = G.FILL)
     me.pack(w, **kw)
     return w
+
   def info(me, label, text = None, len = 18, **kw):
-    """Packs an information widget with a label.  LABEL is the label; TEXT is
-    the initial text; LEN is the estimated length in characters.  Returns the
-    entry widget."""
-    e = G.Entry()
+    """
+    Packs an information widget with a label.
+
+    LABEL is the label; TEXT is the initial text; LEN is the estimated length
+    in characters.  Returns the entry widget.
+    """
+    e = G.Label()
     if text is not None: e.set_text(text)
     e.set_width_chars(len)
-    e.set_editable(False)
+    e.set_selectable(True)
+    e.set_alignment(0.0, 0.5)
     me.labelled(label, e, **kw)
     return e
 
 class WindowSlot (HookClient):
-  """A place to store a window.  If the window is destroyed, remember this;
-  when we come to open the window, raise it if it already exists; otherwise
-  make a new one."""
+  """
+  A place to store a window -- specificially a MyWindowMixin.
+
+  If the window is destroyed, remember this; when we come to open the window,
+  raise it if it already exists; otherwise make a new one.
+  """
   def __init__(me, createfunc):
-    """Constructor: CREATEFUNC must return a new Window which supports the
-    closehook protocol."""
+    """
+    Constructor: CREATEFUNC must return a new Window which supports the
+    closehook protocol.
+    """
     HookClient.__init__(me)
     me.createfunc = createfunc
     me.window = None
+
   def open(me):
     """Opens the window, creating it if necessary."""
     if me.window:
@@ -616,21 +685,49 @@ class WindowSlot (HookClient):
     else:
       me.window = me.createfunc()
       me.hook(me.window.closehook, me.closed)
+
   def closed(me):
+    """Handles the window being closed."""
     me.unhook(me.window.closehook)
     me.window = None
 
+class MyTreeView (G.TreeView):
+  def __init__(me, model):
+    G.TreeView.__init__(me, model)
+    me.set_rules_hint(True)
+
+class MyScrolledWindow (G.ScrolledWindow):
+  def __init__(me):
+    G.ScrolledWindow.__init__(me)
+    me.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
+    me.set_shadow_type(G.SHADOW_IN)
+
+## Matches a signed integer.
+rx_num = RX.compile(r'^[-+]?\d+$')
+
+## The colour red.
+c_red = GDK.color_parse('red')
+
 class ValidationError (Exception):
   """Raised by ValidatingEntry.get_text() if the text isn't valid."""
   pass
+
 class ValidatingEntry (G.Entry):
-  """Like an Entry, but makes the text go red if the contents are invalid.
-  If get_text is called, and the text is invalid, ValidationError is
-  raised."""
+  """
+  Like an Entry, but makes the text go red if the contents are invalid.
+
+  If get_text is called, and the text is invalid, ValidationError is raised.
+  The attribute validp reflects whether the contents are currently valid.
+  """
+
   def __init__(me, valid, text = '', size = -1, *arg, **kw):
-    """Make an Entry.  VALID is a regular expression or a predicate on
-    strings.  TEXT is the default text to insert.  SIZE is the size of the
-    box to set, in characters (ish).  Other arguments are passed to Entry."""
+    """
+    Make a validating Entry.
+
+    VALID is a regular expression or a predicate on strings.  TEXT is the
+    default text to insert.  SIZE is the size of the box to set, in
+    characters (ish).  Other arguments are passed to Entry.
+    """
     G.Entry.__init__(me, *arg, **kw)
     me.connect("changed", me.check)
     if callable(valid):
@@ -644,27 +741,38 @@ class ValidatingEntry (G.Entry):
     me.set_activates_default(True)
     me.set_text(text)
     me.check()
+
   def check(me, *hunoz):
+    """Check the current text and update validp and the text colour."""
     if me.validate(G.Entry.get_text(me)):
       me.validp = True
       me.modify_text(G.STATE_NORMAL, me.c_ok)
     else:
       me.validp = False
       me.modify_text(G.STATE_NORMAL, me.c_bad)
+
   def get_text(me):
+    """
+    Return the text in the Entry if it's valid.  If it isn't, raise
+    ValidationError.
+    """
     if not me.validp:
       raise ValidationError
     return G.Entry.get_text(me)
 
 def numericvalidate(min = None, max = None):
-  """Validation function for numbers.  Entry must consist of an optional sign
-  followed by digits, and the resulting integer must be within the given
-  bounds."""
+  """
+  Return a validation function for numbers.
+
+  Entry must consist of an optional sign followed by digits, and the
+  resulting integer must be within the given bounds.
+  """
   return lambda x: (rx_num.match(x) and
                     (min is None or long(x) >= min) and
                     (max is None or long(x) <= max))
 
-#----- Various minor dialog boxen -------------------------------------------
+###--------------------------------------------------------------------------
+### Various minor dialog boxen.
 
 GPL = """This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
@@ -686,16 +794,21 @@ class AboutBox (G.AboutDialog, MyWindowMixin):
     G.AboutDialog.__init__(me)
     me.mywininit()
     me.set_name('TrIPEmon')
-    me.set_version(VERSION)
+    me.set_version(T.VERSION)
     me.set_license(GPL)
-    me.set_authors(['Mark Wooding'])
-    me.connect('unmap', invoker(me.close))
+    me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
+    me.set_comments('A graphical monitor for the TrIPE VPN server')
+    me.set_copyright('Copyright Â© 2006-2008 Straylight/Edgeware')
+    me.connect('response', me.respond)
     me.show()
+  def respond(me, hunoz, rid, *hukairz):
+    if rid == G.RESPONSE_CANCEL:
+      me.close()
 aboutbox = WindowSlot(AboutBox)
 
 def moanbox(msg):
   """Report an error message in a window."""
-  d = G.Dialog('Error from %s' % quis,
+  d = G.Dialog('Error from %s' % M.quis,
                flags = G.DIALOG_MODAL,
                buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
   label = G.Label(msg)
@@ -709,69 +822,32 @@ def unimplemented(*hunoz):
   """Indicator of laziness."""
   moanbox("I've not written that bit yet.")
 
-class ServInfo (MyWindow):
-  def __init__(me, monitor):
-    MyWindow.__init__(me)
-    me.set_title('TrIPE server info')
-    me.mon = monitor
-    me.table = GridPacker()
-    me.add(me.table)
-    me.e = {}
-    def add(label, tag, text = None, **kw):
-      me.e[tag] = me.table.info(label, text, **kw)
-    add('Implementation', 'implementation')
-    add('Version', 'version', newlinep = True)
-    me.update()
-    me.hook(me.mon.connecthook, me.update)
-    me.show_all()
-  def update(me):
-    info = parseinfo(me.mon.simplecmd('SERVINFO'))
-    for i in me.e:
-      me.e[i].set_text(info[i])
+###--------------------------------------------------------------------------
+### Logging windows.
 
-class TraceOptions (MyDialog):
-  """Tracing options window."""
-  def __init__(me, monitor):
-    MyDialog.__init__(me, title = 'Tracing options',
-                      buttons = [(G.STOCK_CLOSE, me.destroy),
-                                 (G.STOCK_OK, me.ok)])
-    me.mon = monitor
-    me.opts = []
-    for o in me.mon.simplecmd('TRACE'):
-      char = o[0]
-      onp = o[1]
-      text = o[3].upper() + o[4:]
-      if char.isupper(): continue
-      ticky = G.CheckButton(text)
-      ticky.set_active(onp != ' ')
-      me.vbox.pack_start(ticky)
-      me.opts.append((char, ticky))
-    me.show_all()
-  def ok(me):
-    on = []
-    off = []
-    for char, ticky in me.opts:
-      if ticky.get_active():
-        on.append(char)
-      else:
-        off.append(char)
-    setting = ''.join(on) + '-' + ''.join(off)
-    me.mon.simplecmd('TRACE %s' % setting)
-    me.destroy()
+class LogModel (G.ListStore):
+  """
+  A simple list of log messages, usable as the model for a TreeView.
 
-#----- Logging windows ------------------------------------------------------
+  The column headings are stored in the `cols' attribute.
+  """
 
-class LogModel (G.ListStore):
-  """A simple list of log messages."""
   def __init__(me, columns):
-    """Call with a list of column names.  All must be strings.  We add a time
-    column to the left."""
+    """
+    COLUMNS must be a list of column name strings.  We add a time column to
+    the left.
+    """
     me.cols = ('Time',) + columns
     G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
+
   def add(me, *entries):
-    """Adds a new log message, with a timestamp."""
-    now = T.strftime('%Y-%m-%d %H:%M:%S')
-    me.append((now,) + entries)
+    """
+    Adds a new log message, with a timestamp.
+
+    The ENTRIES are the contents for the list columns.
+    """
+    now = TIME.strftime('%Y-%m-%d %H:%M:%S')
+    me.append((now, ) + entries)
 
 class TraceLogModel (LogModel):
   """Log model for trace messages."""
@@ -782,43 +858,337 @@ class TraceLogModel (LogModel):
     me.add(line)
 
 class WarningLogModel (LogModel):
-  """Log model for warnings.  We split the category out into a separate
-  column."""
+  """
+  Log model for warnings.
+
+  We split the category out into a separate column.
+  """
   def __init__(me):
     LogModel.__init__(me, ('Category', 'Message'))
-  def notify(me, line):
+  def notify(me, tag, *rest):
     """Call with a new warning message."""
-    me.add(*getword(line))
+    me.add(tag, ' '.join([T.quotify(w) for w in rest]))
 
 class LogViewer (MyWindow):
-  """Log viewer window.  Nothing very exciting."""
+  """
+  A log viewer window.
+
+  Its contents are a TreeView showing the log.
+
+  Attributes:
+
+    * model: an appropriate LogModel
+    * list: a TreeView widget to display the log
+  """
+
   def __init__(me, model):
+    """
+    Create a log viewer showing the LogModel MODEL.
+    """
     MyWindow.__init__(me)
     me.model = model
-    scr = G.ScrolledWindow()
-    scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
-    me.list = G.TreeView(me.model)
-    me.closehook = HookList()
+    scr = MyScrolledWindow()
+    me.list = MyTreeView(me.model)
     i = 0
     for c in me.model.cols:
-      me.list.append_column(G.TreeViewColumn(c,
-                                             G.CellRendererText(),
-                                             text = i))
+      crt = G.CellRendererText()
+      me.list.append_column(G.TreeViewColumn(c, crt, text = i))
       i += 1
+    crt.set_property('family', 'monospace')
     me.set_default_size(440, 256)
     scr.add(me.list)
     me.add(scr)
     me.show_all()
 
-#----- Peer window ----------------------------------------------------------
+###--------------------------------------------------------------------------
+### Pinging peers.
+
+class pingstate (struct):
+  """
+  Information kept for each peer by the Pinger.
+
+  Important attributes:
+
+    * peer = the peer name
+    * command = PING or EPING
+    * n = how many pings we've sent so far
+    * ngood = how many returned
+    * nmiss = how many didn't return
+    * nmissrun = how many pings since the last good one
+    * tlast = round-trip time for the last (good) ping
+    * ttot = total roung trip time
+  """
+  pass
+
+class Pinger (T.Coroutine, HookClient):
+  """
+  Coroutine which pings known peers and collects statistics.
+
+  Interesting attributes:
+
+    * _map: dict mapping peer names to Peer objects
+    * _q: event queue for notifying pinger coroutine
+    * _timer: gobject timer for waking the coroutine
+  """
+
+  def __init__(me):
+    """
+    Initialize the pinger.
+
+    We watch the monitor's PeerList to track which peers we should ping.  We
+    maintain an event queue and put all the events on that.
+
+    The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
+    where CMD is 'PING' or 'EPING'.
+    """
+    T.Coroutine.__init__(me)
+    HookClient.__init__(me)
+    me._map = {}
+    me._q = T.Queue()
+    me._timer = None
+    me.hook(conn.connecthook, me._connected)
+    me.hook(conn.disconnecthook, me._disconnected)
+    me.hook(monitor.peers.addhook,
+            lambda p: idly(me._q.put, (p, 'ADD', None)))
+    me.hook(monitor.peers.delhook,
+            lambda p: idly(me._q.put, (p, 'KILL', None)))
+    if conn.connectedp(): me.connected()
+
+  def _connected(me):
+    """Respond to connection: start pinging thngs."""
+    me._timer = GO.timeout_add(1000, me._timerfunc)
+
+  def _timerfunc(me):
+    """Timer function: put a timer event on the queue."""
+    me._q.put((None, 'TIMER', None))
+    return True
+
+  def _disconnected(me, reason):
+    """Respond to disconnection: stop pinging."""
+    GO.source_remove(me._timer)
+
+  def run(me):
+    """
+    Coroutine function: read events from the queue and process them.
+
+    Interesting events:
+
+      * (PEER, 'KILL', None): remove PEER from the interesting peers list
+      * (PEER, 'ADD', None): add PEER to the list
+      * (PEER, 'INFO', TOKENS): result from a PING command
+      * (None, 'TIMER', None): interval timer went off: send more pings
+    """
+    while True:
+      tag, code, stuff = me._q.get()
+      if code == 'KILL':
+        name = tag.name
+        if name in me._map:
+          del me._map[name]
+      elif not conn.connectedp():
+        pass
+      elif code == 'ADD':
+        p = tag
+        p.ping = {}
+        for cmd in 'PING', 'EPING':
+          ps = pingstate(command = cmd, peer = p,
+                         n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
+                         tlast = 0, ttot = 0)
+          p.ping[cmd] = ps
+        me._map[p.name] = p
+      elif code == 'INFO':
+        ps = tag
+        if stuff[0] == 'ping-ok':
+          t = float(stuff[1])
+          ps.ngood += 1
+          ps.nmissrun = 0
+          ps.tlast = t
+          ps.ttot += t
+        else:
+          ps.nmiss += 1
+          ps.nmissrun += 1
+        ps.n += 1
+        ps.peer.pinghook.run(ps.peer, ps.command, ps)
+      elif code == 'TIMER':
+        for name, p in me._map.iteritems():
+          for cmd, ps in p.ping.iteritems():
+            conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
+              cmd, '-background', conn.bgtag(), '--', name]))
+
+###--------------------------------------------------------------------------
+### Random dialogue boxes.
+
+class AddPeerDialog (MyDialog):
+  """
+  Let the user create a new peer the low-level way.
+
+  Interesting attributes:
+
+    * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
+  """
+
+  def __init__(me):
+    """Initialize the dialogue."""
+    MyDialog.__init__(me, 'Add peer',
+                      buttons = [(G.STOCK_CANCEL, me.destroy),
+                                 (G.STOCK_OK, me.ok)])
+    me._setup()
+
+  @incr
+  def _setup(me):
+    """Coroutine function: background setup for AddPeerDialog."""
+    table = GridPacker()
+    me.vbox.pack_start(table)
+    me.e_name = table.labelled('Name',
+                               ValidatingEntry(r'^[^\s.:]+$', '', 16),
+                               width = 3)
+    me.e_addr = table.labelled('Address',
+                               ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
+                               newlinep = True)
+    me.e_port = table.labelled('Port',
+                               ValidatingEntry(numericvalidate(0, 65535),
+                                               '4070',
+                                               5))
+    me.c_keepalive = G.CheckButton('Keepalives')
+    me.l_tunnel = table.labelled('Tunnel',
+                                 G.combo_box_new_text(),
+                                 newlinep = True, width = 3)
+    me.tuns = conn.tunnels()
+    for t in me.tuns:
+      me.l_tunnel.append_text(t)
+    me.l_tunnel.set_active(0)
+    table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
+    me.c_keepalive.connect('toggled',
+                           lambda t: me.e_keepalive.set_sensitive\
+                                      (t.get_active()))
+    me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
+    me.e_keepalive.set_sensitive(False)
+    table.pack(me.e_keepalive, width = 3)
+    me.show_all()
+
+  def ok(me):
+    """Handle an OK press: create the peer."""
+    try:
+      if me.c_keepalive.get_active():
+        ka = me.e_keepalive.get_text()
+      else:
+        ka = None
+      t = me.l_tunnel.get_active()
+      if t == 0:
+        tun = None
+      else:
+        tun = me.tuns[t]
+        me._addpeer(me.e_name.get_text(),
+                    me.e_addr.get_text(),
+                    me.e_port.get_text(),
+                    ka,
+                    tun)
+    except ValidationError:
+      GDK.beep()
+      return
+
+  @incr
+  def _addpeer(me, name, addr, port, keepalive, tunnel):
+    """Coroutine function: actually do the ADD command."""
+    try:
+      conn.add(name, addr, port, keepalive = keepalive, tunnel = tunnel)
+      me.destroy()
+    except T.TripeError, exc:
+      idly(moanbox, ' '.join(exc))
+
+class ServInfo (MyWindow):
+  """
+  Show information about the server and available services.
+
+  Interesting attributes:
+
+    * e: maps SERVINFO keys to entry widgets
+    * svcs: Gtk ListStore describing services (columns are name and version)
+  """
+
+  def __init__(me):
+    MyWindow.__init__(me)
+    me.set_title('TrIPE server info')
+    table = GridPacker()
+    me.add(table)
+    me.e = {}
+    def add(label, tag, text = None, **kw):
+      me.e[tag] = table.info(label, text, **kw)
+    add('Implementation', 'implementation')
+    add('Version', 'version', newlinep = True)
+    me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
+    me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
+    scr = MyScrolledWindow()
+    lb = MyTreeView(me.svcs)
+    i = 0
+    for title in 'Service', 'Version':
+      lb.append_column(G.TreeViewColumn(
+        title, G.CellRendererText(), text = i))
+      i += 1
+    for svc in monitor.services:
+      me.svcs.append([svc.name, svc.version])
+    scr.add(lb)
+    table.pack(scr, width = 2, newlinep = True,
+               yopt = G.EXPAND | G.FILL | G.SHRINK)
+    me.update()
+    me.hook(conn.connecthook, me.update)
+    me.hook(monitor.services.addhook, me.addsvc)
+    me.hook(monitor.services.delhook, me.delsvc)
+    me.show_all()
+
+  def addsvc(me, svc):
+    me.svcs.append([svc.name, svc.version])
+
+  def delsvc(me, svc):
+    for i in xrange(len(me.svcs)):
+      if me.svcs[i][0] == svc.name:
+        me.svcs.remove(me.svcs.get_iter(i))
+        break
+  @incr
+  def update(me):
+    info = conn.servinfo()
+    for i in me.e:
+      me.e[i].set_text(info[i])
+
+class TraceOptions (MyDialog):
+  """Tracing options window."""
+  def __init__(me):
+    MyDialog.__init__(me, title = 'Tracing options',
+                      buttons = [(G.STOCK_CLOSE, me.destroy),
+                                 (G.STOCK_OK, cr(me.ok))])
+    me._setup()
+
+  @incr
+  def _setup(me):
+    me.opts = []
+    for ch, st, desc in conn.trace():
+      if ch.isupper(): continue
+      text = desc[0].upper() + desc[1:]
+      ticky = G.CheckButton(text)
+      ticky.set_active(st == '+')
+      me.vbox.pack_start(ticky)
+      me.opts.append((ch, ticky))
+    me.show_all()
+  def ok(me):
+    on = []
+    off = []
+    for ch, ticky in me.opts:
+      if ticky.get_active():
+        on.append(ch)
+      else:
+        off.append(ch)
+    setting = ''.join(on) + '-' + ''.join(off)
+    conn.trace(setting)
+    me.destroy()
+
+###--------------------------------------------------------------------------
+### Peer window.
 
 def xlate_time(t):
-  """Translate a time in tripe's stats format to something a human might
-  actually want to read."""
+  """Translate a TrIPE-format time to something human-readable."""
   if t == 'NEVER': return '(never)'
   YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
-  ago = T.time() - T.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
-  ago = M.floor(ago); unit = 's'
+  ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
+  ago = MATH.floor(ago); unit = 's'
   for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
     if ago < 2*n: break
     ago /= n
@@ -860,211 +1230,241 @@ statslayout = \
    ('Key-exchange in/out',
     '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
    ('IP in/out',
-    '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-in)s (%(ip-bytes-in)s)'),
+    '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
    ('Rejected packets', '%(rejected-packets)s')]
 
 class PeerWindow (MyWindow):
-  """Show information about a peer."""
-  def __init__(me, monitor, peer):
+  """
+  Show information about a peer.
+
+  This gives a graphical view of the server's peer statistics.
+
+  Interesting attributes:
+
+    * e: dict mapping keys (mostly matching label widget texts, though pings
+      use command names) to entry widgets so that we can update them easily
+    * peer: the peer this window shows information about
+    * cr: the info-fetching coroutine, or None if crrrently disconnected
+    * doupate: whether the info-fetching corouting should continue running
+  """
+
+  def __init__(me, peer):
+    """Construct a PeerWindow, showing information about PEER."""
+
     MyWindow.__init__(me)
     me.set_title('TrIPE statistics: %s' % peer.name)
-    me.mon = monitor
     me.peer = peer
+
     table = GridPacker()
     me.add(table)
+
+    ## Utility for adding fields.
     me.e = {}
-    def add(label, text = None):
-      me.e[label] = table.info(label, text, len = 42, newlinep = True)
+    def add(label, text = None, key = None):
+      if key is None: key = label
+      me.e[key] = table.info(label, text, len = 42, newlinep = True)
+
+    ## Build the dialogue box.
     add('Peer name', peer.name)
     add('Tunnel', peer.tunnel)
     add('Interface', peer.ifname)
     add('Keepalives',
         (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
     add('Address', peer.addr)
-    add('Transport pings')
-    add('Encrypted pings')
-    for label, format in statslayout: add(label)
-    me.timeout = None
-    me.hook(me.mon.connecthook, me.tryupdate)
-    me.hook(me.mon.disconnecthook, me.stopupdate)
+    add('Transport pings', key = 'PING')
+    add('Encrypted pings', key = 'EPING')
+
+    for label, format in statslayout:
+      add(label)
+
+    ## Hook onto various interesting events.
+    me.hook(conn.connecthook, me.tryupdate)
+    me.hook(conn.disconnecthook, me.stopupdate)
     me.hook(me.closehook, me.stopupdate)
     me.hook(me.peer.deadhook, me.dead)
+    me.hook(me.peer.changehook, me.change)
     me.hook(me.peer.pinghook, me.ping)
+    me.cr = None
+    me.doupdate = True
     me.tryupdate()
-    me.ping()
+
+    ## Format the ping statistics.
+    for cmd, ps in me.peer.ping.iteritems():
+      me.ping(me.peer, cmd, ps)
+
+    ## And show the window.
     me.show_all()
-  def update(me):
-    if not me.peer.alivep or not me.mon.connectedp: return False
-    stat = parseinfo(me.mon.simplecmd('STATS %s' % me.peer.name))
-    for s, trans in statsxlate:
-      stat[s] = trans(stat[s])
-    for label, format in statslayout:
-      me.e[label].set_text(format % stat)
-    return True
+
+  def change(me):
+    """Update the display in response to a notification."""
+    me.e['Interface'].set_text(me.peer.ifname)
+
+  def _update(me):
+    """
+    Main display-updating coroutine.
+
+    This does an update, sleeps for a while, and starts again.  If the
+    me.doupdate flag goes low, we stop the loop.
+    """
+    while me.peer.alivep and conn.connectedp() and me.doupdate:
+      stat = conn.stats(me.peer.name)
+      for s, trans in statsxlate:
+        stat[s] = trans(stat[s])
+      for label, format in statslayout:
+        me.e[label].set_text(format % stat)
+      GO.timeout_add(1000, lambda: me.cr.switch() and False)
+      me.cr.parent.switch()
+    me.cr = None
+
   def tryupdate(me):
-    if me.timeout is None and me.update():
-      me.timeout = GO.timeout_add(1000, me.update)
-  def stopupdate(me):
-    if me.timeout is not None:
-      GO.source_remove(me.timeout)
-      me.timeout = None
+    """Start the updater coroutine, if it's not going already."""
+    if me.cr is None:
+      me.cr = T.Coroutine(me._update)
+      me.cr.switch()
+
+  def stopupdate(me, *hunoz, **hukairz):
+    """Stop the update coroutine, by setting me.doupdate."""
+    me.doupdate = False
+
   def dead(me):
+    """Called when the peer is killed."""
     me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
     me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
     me.stopupdate()
-  def ping(me):
-    for ping in me.peer.ping, me.peer.eping:
-      s = '%d/%d' % (ping.ngood, ping.n)
-      if ping.n:
-        s += ' (%.1f%%)' % (ping.ngood * 100.0/ping.n)
-      if ping.ngood:
-        s += '; %.2f ms (last %.1f ms)' % (ping.ttot/ping.ngood, ping.tlast);
-      me.e[ping.cmd].set_text(s)
-
-#----- Add peer -------------------------------------------------------------
-
-class AddPeerCommand (SimpleBackgroundCommand):
-  def __init__(me, conn, dlg, name, addr, port,
-               keepalive = None, tunnel = None):
-    me.name = name
-    me.addr = addr
-    me.port = port
-    me.keepalive = keepalive
-    me.tunnel = tunnel
-    cmd = StringIO()
-    cmd.write('ADD %s' % name)
-    cmd.write(' -background %s' % jobid())
-    if keepalive is not None: cmd.write(' -keepalive %s' % keepalive)
-    if tunnel is not None: cmd.write(' -tunnel %s' % tunnel)
-    cmd.write(' INET %s %s' % (addr, port))
-    SimpleBackgroundCommand.__init__(me, conn, cmd.getvalue())
-    me.hook(me.donehook, invoker(dlg.destroy))
-  def fail(me, err):
-    token, msg = getword(str(err))
-    if token in ('resolve-error', 'resolver-timeout'):
-      moanbox("Unable to resolve hostname `%s'" % me.addr)
-    elif token == 'peer-create-fail':
-      moanbox("Couldn't create new peer `%s'" % me.name)
-    elif token == 'peer-exists':
-      moanbox("Peer `%s' already exists" % me.name)
-    else:
-      moanbox("Unexpected error from server command `ADD': %s" % err)
 
-class AddPeerDialog (MyDialog):
-  def __init__(me, monitor):
-    MyDialog.__init__(me, 'Add peer',
-                      buttons = [(G.STOCK_CANCEL, me.destroy),
-                                 (G.STOCK_OK, me.ok)])
-    me.mon = monitor
+  def ping(me, peer, cmd, ps):
+    """Called when a ping result for the peer is reported."""
+    s = '%d/%d' % (ps.ngood, ps.n)
+    if ps.n:
+      s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
+    if ps.ngood:
+      s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
+    me.e[ps.command].set_text(s)
+
+###--------------------------------------------------------------------------
+### Cryptographic status.
+
+class CryptoInfo (MyWindow):
+  """Simple display of cryptographic algorithms in use."""
+  def __init__(me):
+    MyWindow.__init__(me)
+    me.set_title('Cryptographic algorithms')
+    aside(me.populate)
+  def populate(me):
     table = GridPacker()
-    me.vbox.pack_start(table)
-    me.e_name = table.labelled('Name',
-                               ValidatingEntry(r'^[^\s.:]+$', '', 16),
-                               width = 3)
-    me.e_addr = table.labelled('Address',
-                               ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
-                               newlinep = True)
-    me.e_port = table.labelled('Port',
-                               ValidatingEntry(numericvalidate(0, 65535),
-                                               '4070',
-                                               5))
-    me.c_keepalive = G.CheckButton('Keepalives')
-    me.l_tunnel = table.labelled('Tunnel',
-                                 G.combo_box_new_text(),
-                                 newlinep = True, width = 3)
-    me.tuns = me.mon.simplecmd('TUNNELS')
-    for t in me.tuns:
-      me.l_tunnel.append_text(t)
-    me.l_tunnel.set_active(0)
-    table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
-    me.c_keepalive.connect('toggled',
-                           lambda t: me.e_keepalive.set_sensitive\
-                                      (t.get_active()))
-    me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
-    me.e_keepalive.set_sensitive(False)
-    table.pack(me.e_keepalive, width = 3)
-    me.show_all()
-  def ok(me):
-    try:
-      if me.c_keepalive.get_active():
-        ka = me.e_keepalive.get_text()
-      else:
-        ka = None
-      t = me.l_tunnel.get_active()
-      if t == 0:
-        tun = None
-      else:
-        tun = me.tuns[t]
-      AddPeerCommand(me.mon, me,
-                     me.e_name.get_text(),
-                     me.e_addr.get_text(),
-                     me.e_port.get_text(),
-                     keepalive = ka,
-                     tunnel = tun)
-    except ValidationError:
-      GDK.beep()
-      return
+    me.add(table)
 
-#----- The server monitor ---------------------------------------------------
+    crypto = conn.algs()
+    table.info('Diffie-Hellman group',
+               '%s (%d-bit order, %d-bit elements)' %
+               (crypto['kx-group'],
+                int(crypto['kx-group-order-bits']),
+                int(crypto['kx-group-elt-bits'])),
+               len = 32)
+    table.info('Data encryption',
+               '%s (%d-bit key; %s)' %
+               (crypto['cipher'],
+                int(crypto['cipher-keysz']) * 8,
+                crypto['cipher-blksz'] == '0'
+                  and 'stream cipher'
+                  or '%d-bit block' % (int(crypto['cipher-blksz']) * 8)),
+               newlinep = True)
+    table.info('Message authentication',
+               '%s (%d-bit key; %d-bit tag)' %
+               (crypto['mac'],
+                int(crypto['mac-keysz']) * 8,
+                int(crypto['mac-tagsz']) * 8),
+               newlinep = True)
+    table.info('Hash function',
+               '%s (%d-bit output)' %
+               (crypto['hash'],
+                int(crypto['hash-sz']) * 8),
+               newlinep = True)
 
-class PingCommand (SimpleBackgroundCommand):
-  def __init__(me, conn, cmd, peer, func):
-    me.peer = peer
-    me.func = func
-    SimpleBackgroundCommand.__init__ \
-      (me, conn, '%s -background %s %s' % (cmd, jobid(), peer.name))
-  def ok(me):
-    tok, rest = getword(me.info[0])
-    if tok == 'ping-ok':
-      me.func(me.peer, float(rest))
-    else:
-      me.func(me.peer, None)
-    me.unhookall()
-  def fail(me, err): me.unhookall()
-  def lost(me): me.unhookall()
+    me.show_all()
+
+###--------------------------------------------------------------------------
+### Main monitor window.
 
 class MonitorWindow (MyWindow):
 
-  def __init__(me, monitor):
+  """
+  The main monitor window.
+
+  This class creates, populates and maintains the main monitor window.
+
+  Lots of attributes:
+
+    * warnings, trace: log models for server output
+    * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
+      WindowSlot objects for ancillary windows
+    * ui: Gtk UIManager object for the menu system
+    * apmenu: pair of identical autoconnecting peer menus
+    * listmodel: Gtk ListStore for connected peers; contains peer name,
+      address, and ping times (transport and encrypted, value and colour)
+    * status: Gtk Statusbar at the bottom of the window
+    * _kidding: an unpleasant backchannel between the apchange method (which
+      builds the apmenus) and the menu handler, forced on us by a Gtk
+      misfeature
+
+  Also installs attributes on Peer objects:
+
+    * i: index of peer's entry in listmodel
+    * win: WindowSlot object for the peer's PeerWindow
+  """
+
+  def __init__(me):
+    """Construct the window."""
+
+    ## Basic stuff.
     MyWindow.__init__(me)
     me.set_title('TrIPE monitor')
-    me.mon = monitor
-    me.hook(me.mon.errorhook, me.report)
+
+    ## Hook onto diagnostic outputs.
     me.warnings = WarningLogModel()
-    me.hook(me.mon.warnhook, me.warnings.notify)
+    me.hook(conn.warnhook, me.warnings.notify)
     me.trace = TraceLogModel()
-    me.hook(me.mon.tracehook, me.trace.notify)
+    me.hook(conn.tracehook, me.trace.notify)
 
+    ## Make slots to store the various ancillary singleton windows.
     me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
     me.traceview = WindowSlot(lambda: LogViewer(me.trace))
-    me.traceopts = WindowSlot(lambda: TraceOptions(me.mon))
-    me.addpeerwin = WindowSlot(lambda: AddPeerDialog(me.mon))
-    me.servinfo = WindowSlot(lambda: ServInfo(me.mon))
+    me.traceopts = WindowSlot(lambda: TraceOptions())
+    me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
+    me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
+    me.servinfo = WindowSlot(lambda: ServInfo())
 
+    ## Main window structure.
     vbox = G.VBox()
     me.add(vbox)
 
+    ## UI manager  makes our menus.  (We're too cheap to have a toolbar.)
     me.ui = G.UIManager()
-    def cmd(c): return lambda: me.mon.simplecmd(c)
     actgroup = makeactiongroup('monitor',
       [('file-menu', '_File', None, None),
-       ('connect', '_Connect', '<Alt>C', me.mon.connect),
-       ('disconnect', '_Disconnect', '<Alt>D', me.mon.disconnect),
-       ('quit', '_Quit', '<Alt>Q', me.close),
+       ('connect', '_Connect', '<Control>C', conn.connect),
+       ('disconnect', '_Disconnect', '<Control>D',
+        lambda: conn.disconnect(None)),
+       ('quit', '_Quit', '<Control>Q', me.close),
        ('server-menu', '_Server', None, None),
-       ('daemon', 'Run in _background', None, cmd('DAEMON')),
-       ('server-version', 'Server version', '<Alt>V', me.servinfo.open),
-       ('reload-keys', 'Reload keys', '<Alt>R', cmd('RELOAD')),
-       ('server-quit', 'Terminate server', None, cmd('QUIT')),
+       ('daemon', 'Run in _background', None, cr(conn.daemon)),
+       ('server-version', 'Server version', '<Control>V', me.servinfo.open),
+       ('crypto-algs', 'Cryptographic algorithms',
+        '<Control>Y', me.cryptoinfo.open),
+       ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
+       ('server-quit', 'Terminate server', None, cr(conn.quit)),
+       ('conn-peer', 'Connect peer', None, None),
        ('logs-menu', '_Logs', None, None),
-       ('show-warnings', 'Show _warnings', '<Alt>W', me.warnview.open),
-       ('show-trace', 'Show _trace', '<Alt>T', me.traceview.open),
+       ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
+       ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
        ('trace-options', 'Trace _options...', None, me.traceopts.open),
        ('help-menu', '_Help', None, None),
        ('about', '_About tripemon...', None, aboutbox.open),
-       ('add-peer', '_Add peer...', '<Alt>A', me.addpeerwin.open),
+       ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
        ('kill-peer', '_Kill peer', None, me.killpeer),
        ('force-kx', 'Force key e_xchange', None, me.forcekx)])
+
+    ## Menu structures.
     uidef = '''
       <ui>
         <menubar>
@@ -1072,11 +1472,13 @@ class MonitorWindow (MyWindow):
             <menuitem action="quit"/>
           </menu>
           <menu action="server-menu">
-             <menuitem action="connect"/>
+            <menuitem action="connect"/>
             <menuitem action="disconnect"/>
             <separator/>
             <menuitem action="server-version"/>
+            <menuitem action="crypto-algs"/>
             <menuitem action="add-peer"/>
+            <menuitem action="conn-peer"/>
             <menuitem action="daemon"/>
             <menuitem action="reload-keys"/>
             <separator/>
@@ -1093,25 +1495,39 @@ class MonitorWindow (MyWindow):
         </menubar>
         <popup name="peer-popup">
           <menuitem action="add-peer"/>
+          <menuitem action="conn-peer"/>
           <menuitem action="kill-peer"/>
           <menuitem action="force-kx"/>
         </popup>
       </ui>
       '''
+
+    ## Populate the UI manager.
     me.ui.insert_action_group(actgroup, 0)
     me.ui.add_ui_from_string(uidef)
+
+    ## Construct the menu bar.
     vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
     me.add_accel_group(me.ui.get_accel_group())
-    me.status = G.Statusbar()
 
+    ## Construct and attach the auto-peers menu.  (This is a horrible bodge
+    ## because we can't attach the same submenu in two different places.)
+    me.apmenu = G.Menu(), G.Menu()
+    me.ui.get_widget('/menubar/server-menu/conn-peer') \
+      .set_submenu(me.apmenu[0])
+    me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
+
+    ## Construct the main list model, and listen on hooks which report
+    ## changes to the available peers.
     me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
     me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
-    me.hook(me.mon.addpeerhook, me.addpeer)
-    me.hook(me.mon.delpeerhook, me.delpeer)
+    me.hook(monitor.peers.addhook, me.addpeer)
+    me.hook(monitor.peers.delhook, me.delpeer)
+    me.hook(monitor.autopeershook, me.apchange)
 
-    scr = G.ScrolledWindow()
-    scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
-    me.list = G.TreeView(me.listmodel)
+    ## Construct the list viewer and put it in a scrolling window.
+    scr = MyScrolledWindow()
+    me.list = MyTreeView(me.listmodel)
     me.list.append_column(G.TreeViewColumn('Peer name',
                                            G.CellRendererText(),
                                            text = 0))
@@ -1134,90 +1550,179 @@ class MonitorWindow (MyWindow):
     scr.add(me.list)
     vbox.pack_start(scr)
 
+    ## Construct the status bar, and listen on hooks which report changes to
+    ## connection status.
+    me.status = G.Statusbar()
     vbox.pack_start(me.status, expand = False)
-    me.hook(me.mon.connecthook, me.connected)
-    me.hook(me.mon.disconnecthook, me.disconnected)
-    me.hook(me.mon.notehook, me.notify)
-    me.pinger = None
-    me.set_default_size(420, 180)
-    me.mon.connect()
-    me.show_all()
+    me.hook(conn.connecthook, cr(me.connected))
+    me.hook(conn.disconnecthook, me.disconnected)
+    me.hook(conn.notehook, me.notify)
+
+    ## Set a plausible default window size.
+    me.set_default_size(512, 180)
 
   def addpeer(me, peer):
+    """Hook: announces that PEER has been added."""
     peer.i = me.listmodel.append([peer.name, peer.addr,
                                   '???', 'green', '???', 'green'])
-    peer.win = WindowSlot(lambda: PeerWindow(me.mon, peer))
-    peer.pinghook = HookList()
-    peer.ping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
-                          tlast = 0, ttot = 0,
-                          tcol = 2, ccol = 3, cmd = 'Transport pings')
-    peer.eping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
-                           tlast = 0, ttot = 0,
-                           tcol = 4, ccol = 5, cmd = 'Encrypted pings')
+    peer.win = WindowSlot(lambda: PeerWindow(peer))
+    me.hook(peer.pinghook, me._ping)
+    me.apchange()
+
   def delpeer(me, peer):
+    """Hook: announces that PEER has been removed."""
     me.listmodel.remove(peer.i)
+    me.unhook(peer.pinghook)
+    me.apchange()
+
   def path_peer(me, path):
-    return me.mon.peers[me.listmodel[path][0]]
+    """Return the peer corresponding to a given list-model PATH."""
+    return monitor.peers[me.listmodel[path][0]]
+
+  def apchange(me):
+    """
+    Hook: announces that a change has been made to the peers available for
+    automated connection.
+
+    This populates both auto-peer menus and keeps them in sync.  (As
+    mentioned above, we can't attach the same submenu to two separate parent
+    menu items.  So we end up with two identical menus instead.  Yes, this
+    does suck.)
+    """
+
+    ## The set_active method of a CheckMenuItem works by maybe activating the
+    ## menu item.  This signals our handler.  But we don't actually want to
+    ## signal the handler unless the user actually frobbed the item.  So the
+    ## _kidding flag is used as an underhanded way of telling the handler
+    ## that we don't actually want it to do anything.  Of course, this sucks
+    ## mightily.
+    me._kidding = True
+
+    ## Iterate over the two menus.
+    for m in 0, 1:
+      menu = me.apmenu[m]
+      existing = menu.get_children()
+      if monitor.autopeers is None:
+
+        ## No peers, so empty out the menu.
+        for item in existing:
+          menu.remove(item)
+
+      else:
+
+        ## Insert the new items into the menu.  (XXX this seems buggy XXX)
+       ## Tick the peers which are actually connected.
+        i = j = 0
+        for peer in monitor.autopeers:
+          if j < len(existing) and \
+             existing[j].get_child().get_text() == peer:
+            item = existing[j]
+            j += 1
+          else:
+            item = G.CheckMenuItem(peer, use_underline = False)
+            item.connect('activate', invoker(me._addautopeer, peer))
+            menu.insert(item, i)
+          item.set_active(peer in monitor.peers.table)
+          i += 1
+
+      ## Make all the menu items visible.
+      menu.show_all()
+
+    ## Set the parent menu items sensitive if and only if there are any peers
+    ## to connect.
+    for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
+      me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
+
+    ## And now allow the handler to do its business normally.
+    me._kidding = False
+
+  def _addautopeer(me, peer):
+    """
+    Automatically connect an auto-peer.
+
+    This method is invoked from the main coroutine.  Since the actual
+    connection needs to issue administration commands, we must spawn a new
+    child coroutine for it.
+    """
+    if me._kidding:
+      return
+    T.Coroutine(me._addautopeer_hack).switch(peer)
+
+  def _addautopeer_hack(me, peer):
+    """Make an automated connection to PEER in response to a user click."""
+    if me._kidding:
+      return
+    try:
+      T._simple(conn.svcsubmit('connect', 'active', peer))
+    except T.TripeError, exc:
+      idly(moanbox, ' '.join(exc.args))
+    me.apchange()
 
   def activate(me, l, path, col):
+    """
+    Handle a double-click on a peer in the main list: open a PeerInfo window.
+    """
     peer = me.path_peer(path)
     peer.win.open()
+
   def buttonpress(me, l, ev):
+    """
+    Handle a mouse click on the main list.
+
+    Currently we're only interested in button-3, which pops up the peer menu.
+    For future reference, we stash the peer that was clicked in me.menupeer.
+    """
     if ev.button == 3:
-      r = me.list.get_path_at_pos(ev.x, ev.y)
+      x, y = int(ev.x), int(ev.y)
+      r = me.list.get_path_at_pos(x, y)
       for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
-        me.ui.get_widget(i).set_sensitive(me.mon.connectedp and
+        me.ui.get_widget(i).set_sensitive(conn.connectedp() and
                                           r is not None)
+      me.ui.get_widget('/peer-popup/conn-peer'). \
+        set_sensitive(bool(monitor.autopeers))
       if r:
         me.menupeer = me.path_peer(r[0])
       else:
         me.menupeer = None
-      me.ui.get_widget('/peer-popup').popup(None, None, None,
-                                            ev.button, ev.time)
+      me.ui.get_widget('/peer-popup').popup(
+        None, None, None, ev.button, ev.time)
 
   def killpeer(me):
-    me.mon.simplecmd('KILL %s' % me.menupeer.name)
+    """Kill a peer from the popup menu."""
+    cr(conn.kill, me.menupeer.name)()
+
   def forcekx(me):
-    me.mon.simplecmd('FORCEKX %s' % me.menupeer.name)
-
-  def reping(me):
-    if me.pinger is not None:
-      GO.source_remove(me.pinger)
-    me.pinger = GO.timeout_add(10000, me.ping)
-    me.ping()
-  def unping(me):
-    if me.pinger is not None:
-      GO.source_remove(me.pinger)
-      me.pinger = None
-  def ping(me):
-    for name in me.mon.peers:
-      p = me.mon.peers[name]
-      PingCommand(me.mon, 'PING', p, lambda p, t: me.pong(p, p.ping, t))
-      PingCommand(me.mon, 'EPING', p, lambda p, t: me.pong(p, p.eping, t))
-    return True
-  def pong(me, p, ping, t):
-    ping.n += 1
-    if t is None:
-      ping.nmiss += 1
-      ping.nmissrun += 1
-      me.listmodel[p.i][ping.tcol] = '(miss %d)' % ping.nmissrun
-      me.listmodel[p.i][ping.ccol] = 'red'
+    """Kickstart a key-exchange from the popup menu."""
+    cr(conn.forcekx, me.menupeer.name)()
+
+  _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
+  def _ping(me, p, cmd, ps):
+    """Hook: responds to ping reports."""
+    textcol, colourcol = me._columnmap[cmd]
+    if ps.nmissrun:
+      me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
+      me.listmodel[p.i][colourcol] = 'red'
     else:
-      ping.ngood += 1
-      ping.nmissrun = 0
-      ping.tlast = t
-      ping.ttot += t
-      me.listmodel[p.i][ping.tcol] = '%.1f ms' % t
-      me.listmodel[p.i][ping.ccol] = 'black'
-    p.pinghook.run()
+      me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
+      me.listmodel[p.i][colourcol] = 'black'
+
   def setstatus(me, status):
+    """Update the message in the status bar."""
     me.status.pop(0)
     me.status.push(0, status)
-  def notify(me, note, rest):
+
+  def notify(me, note, *rest):
+    """Hook: invoked when interesting notifications occur."""
     if note == 'DAEMON':
       me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
+
   def connected(me):
-    me.setstatus('Connected (port %s)' % me.mon.simplecmd('PORT')[0])
+    """
+    Hook: invoked when a connection is made to the server.
+
+    Make options which require a server connection sensitive.
+    """
+    me.setstatus('Connected (port %s)' % conn.port())
     me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
     for i in ('/menubar/server-menu/disconnect',
               '/menubar/server-menu/server-version',
@@ -1225,93 +1730,81 @@ class MonitorWindow (MyWindow):
               '/menubar/server-menu/server-quit',
               '/menubar/logs-menu/trace-options'):
       me.ui.get_widget(i).set_sensitive(True)
+    me.ui.get_widget('/menubar/server-menu/conn-peer'). \
+      set_sensitive(bool(monitor.autopeers))
     me.ui.get_widget('/menubar/server-menu/daemon'). \
-      set_sensitive(parseinfo(me.mon.simplecmd('SERVINFO'))['daemon'] ==
-                    'nil')
-    me.reping()
-  def disconnected(me):
+      set_sensitive(conn.servinfo()['daemon'] == 'nil')
+
+  def disconnected(me, reason):
+    """
+    Hook: invoked when the connection to the server is lost.
+
+    Make most options insensitive.
+    """
     me.setstatus('Disconnected')
     me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
     for i in ('/menubar/server-menu/disconnect',
               '/menubar/server-menu/server-version',
               '/menubar/server-menu/add-peer',
+              '/menubar/server-menu/conn-peer',
               '/menubar/server-menu/daemon',
               '/menubar/server-menu/server-quit',
               '/menubar/logs-menu/trace-options'):
       me.ui.get_widget(i).set_sensitive(False)
-    me.unping()
-  def destroy(me):
-    if me.pinger is not None:
-      GO.source_remove(me.pinger)
-  def report(me, msg):
-    moanbox(msg)
-    return True
-
-#----- Parse options --------------------------------------------------------
+    if reason: moanbox(reason)
+
+###--------------------------------------------------------------------------
+### Main program.
+
+def parse_options():
+  """
+  Parse command-line options.
+
+  Process the boring ones.  Return all of them, for later.
+  """
+  op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
+                    version = '%prog (tripe version 1.0.0)')
+  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]')
+  opts, args = op.parse_args()
+  if args: op.error('no arguments permitted')
+  OS.chdir(opts.dir)
+  return opts
+
+def init(opts):
+  """Initialization."""
+
+  global conn, monitor, pinger
+
+  ## Run jobs put off for later.
+  T.Coroutine(_runasides).switch()
+
+  ## Try to establish a connection.
+  conn = Connection(opts.tripesock)
+
+  ## Make the main interesting coroutines and objects.
+  monitor = Monitor()
+  pinger = Pinger()
+  pinger.switch()
 
-def version(fp = stdout):
-  """Print the program's version number."""
-  fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION))
+def main():
 
-def usage(fp):
-  """Print a brief usage message for the program."""
-  fp.write('Usage: %s [-d DIR] [-a SOCK]\n' % quis)
+  ## Main window.
+  root = MonitorWindow()
+  conn.connect()
+  root.show_all()
 
-def main():
-  global tripedir
-  if 'TRIPEDIR' in environ:
-    tripedir = environ['TRIPEDIR']
-  tripesock = environ.get('TRIPESOCK', '%s/%s' % (socketdir, 'tripesock'))
-
-  try:
-    opts, args = O.getopt(argv[1:],
-                          'hvud:a:',
-                          ['help', 'version', 'usage',
-                           'directory=', 'admin-socket='])
-  except O.GetoptError, exc:
-    moan(exc)
-    usage(stderr)
-    exit(1)
-  for o, v in opts:
-    if o in ('-h', '--help'):
-      version(stdout)
-      print
-      usage(stdout)
-      print """
-Graphical monitor for TrIPE VPN.
-
-Options supported:
-
--h, --help              Show this help message.
--v, --version           Show the version number.
--u, --usage             Show pointlessly short usage string.
-
--d, --directory=DIR     Use TrIPE directory DIR.
--a, --admin-socket=FILE Select socket to connect to."""
-      exit(0)
-    elif o in ('-v', '--version'):
-      version(stdout)
-      exit(0)
-    elif o in ('-u', '--usage'):
-      usage(stdout)
-      exit(0)
-    elif o in ('-d', '--directory'):
-      tripedir = v
-    elif o in ('-a', '--admin-socket'):
-      tripesock = v
-    else:
-      raise "can't happen!"
-  if len(args) > 0:
-    usage(stderr)
-    exit(1)
-
-  OS.chdir(tripedir)
-  mon = Monitor(tripesock)
-  root = MonitorWindow(mon)
+  ## Main loop.
   HookClient().hook(root.closehook, exit)
   G.main()
 
 if __name__ == '__main__':
+  opts = parse_options()
+  init(opts)
   main()
 
-#----- That's all, folks ----------------------------------------------------
+###----- That's all, folks --------------------------------------------------