chiark / gitweb /
Great reorganization.
[tripe] / tripemon.in
diff --git a/tripemon.in b/tripemon.in
deleted file mode 100644 (file)
index 0db0775..0000000
+++ /dev/null
@@ -1,1317 +0,0 @@
-#! @PYTHON@
-# -*-python-*-
-
-#----- Dependencies ---------------------------------------------------------
-
-import socket as S
-from sys import argv, exit, stdin, stdout, stderr
-import os as OS
-from os import environ
-import math as M
-import sets as SET
-import getopt as O
-import time as T
-import sre as RX
-from cStringIO import StringIO
-
-import pygtk
-pygtk.require('2.0')
-import gtk as G
-import gobject as GO
-import gtk.gdk as GDK
-
-#----- Configuration --------------------------------------------------------
-
-tripedir = "@configdir@"
-socketdir = "@socketdir@"
-PACKAGE = "@PACKAGE@"
-VERSION = "@VERSION@"
-
-debug = False
-
-#----- Utility functions ----------------------------------------------------
-
-## Program name, shorn of extraneous stuff.
-quis = OS.path.basename(argv[0])
-
-def moan(msg):
-  """Report a message to standard error."""
-  stderr.write('%s: %s\n' % (quis, msg))
-
-def die(msg, rc = 1):
-  """Report a message to standard error and exit."""
-  moan(msg)
-  exit(rc)
-
-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+$')
-
-c_red = GDK.color_parse('red')
-
-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)
-
-class peerinfo (struct): pass
-class pingstate (struct): pass
-
-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()
-
-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):
-    me.list = []
-  def add(me, func, obj):
-    me.list.append((obj, func))
-  def prune(me, 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):
-    for o, hook in me.list:
-      rc = hook(*args, **kw)
-      if rc is not None: return rc
-    return None
-
-class HookClient (object):
-  def __init__(me):
-    me.hooks = SET.Set()
-  def hook(me, hk, func):
-    hk.add(func, me)
-    me.hooks.add(hk)
-  def unhook(me, hk):
-    hk.prune(me)
-    me.hooks.discard(hk)
-  def unhookall(me):
-    for hk in me.hooks:
-      hk.prune(me)
-    me.hooks.clear()
-  ##def __del__(me):
-  ##  print '%s dying' % me
-
-#----- Connections and commands ---------------------------------------------
-
-class ConnException (Exception):
-  """Some sort of problem occurred while communicating with the tripe
-  server."""
-  pass
-
-class Error (ConnException):
-  """A command caused the server to issue a FAIL message."""
-  pass
-
-class ConnectionFailed (ConnException):
-  """The connection failed while communicating with 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
-
-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()
-
-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.
-
-  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"""
-
-  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
-    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)
-  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
-    me.name = name
-    addr = me.mon.simplecmd('ADDR %s' % name)[0].split(' ')
-    if addr[0] == 'INET':
-      ipaddr, port = addr[1:]
-      try:
-        name = S.gethostbyaddr(ipaddr)[0]
-        me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
-      except S.herror:
-        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()
-
-#----- Window management cruft ----------------------------------------------
-
-class MyWindowMixin (G.Window, HookClient):
-  """Mixin for windows which call a closehook when they're destroyed."""
-  def mywininit(me):
-    me.closehook = HookList()
-    HookClient.__init__(me)
-    me.connect('destroy', invoker(me.close))
-  def close(me):
-    me.closehook.run()
-    me.destroy()
-    me.unhookall()
-class MyWindow (MyWindowMixin):
-  """A window which calls a closehook when it's destroyed."""
-  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."""
-    i = 0
-    br = []
-    me.rmap = []
-    for b, f in buttons:
-      br.append(b)
-      br.append(i)
-      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):
-    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"""
-  actgroup = G.ActionGroup(name)
-  for act, label, accel, func in acts:
-    a = G.Action(act, label, None, None)
-    if func: a.connect('activate', invoker(func))
-    actgroup.add_action_with_accel(a, accel)
-  return actgroup
-
-class GridPacker (G.Table):
-  """Like a Table, but with more state: makes filling in the widgets
-  easier."""
-  def __init__(me):
-    G.Table.__init__(me)
-    me.row = 0
-    me.col = 0
-    me.rows = 1
-    me.cols = 1
-    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."""
-    if newlinep:
-      me.row += 1
-      me.col = 0
-    bot = me.row + 1
-    right = me.col + width
-    if bot > me.rows or right > me.cols:
-      if bot > me.rows: me.rows = bot
-      if right > me.cols: me.cols = right
-      me.resize(me.rows, me.cols)
-    me.attach(w, me.col, me.col + width, me.row, me.row + 1,
-              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)
-    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()
-    if text is not None: e.set_text(text)
-    e.set_width_chars(len)
-    e.set_editable(False)
-    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."""
-  def __init__(me, createfunc):
-    """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:
-      me.window.window.raise_()
-    else:
-      me.window = me.createfunc()
-      me.hook(me.window.closehook, me.closed)
-  def closed(me):
-    me.unhook(me.window.closehook)
-    me.window = None
-
-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."""
-  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."""
-    G.Entry.__init__(me, *arg, **kw)
-    me.connect("changed", me.check)
-    if callable(valid):
-      me.validate = valid
-    else:
-      me.validate = RX.compile(valid).match
-    me.ensure_style()
-    me.c_ok = me.get_style().text[G.STATE_NORMAL]
-    me.c_bad = c_red
-    if size != -1: me.set_width_chars(size)
-    me.set_activates_default(True)
-    me.set_text(text)
-    me.check()
-  def check(me, *hunoz):
-    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):
-    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 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 -------------------------------------------
-
-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
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-
-This program 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 this program; if not, write to the Free Software Foundation,
-Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
-
-class AboutBox (G.AboutDialog, MyWindowMixin):
-  """The program `About' box."""
-  def __init__(me):
-    G.AboutDialog.__init__(me)
-    me.mywininit()
-    me.set_name('TrIPEmon')
-    me.set_version(VERSION)
-    me.set_license(GPL)
-    me.set_authors(['Mark Wooding'])
-    me.connect('unmap', invoker(me.close))
-    me.show()
-aboutbox = WindowSlot(AboutBox)
-
-def moanbox(msg):
-  """Report an error message in a window."""
-  d = G.Dialog('Error from %s' % quis,
-               flags = G.DIALOG_MODAL,
-               buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
-  label = G.Label(msg)
-  label.set_padding(20, 20)
-  d.vbox.pack_start(label)
-  label.show()
-  d.run()
-  d.destroy()
-
-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])
-
-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()
-
-#----- Logging windows ------------------------------------------------------
-
-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."""
-    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)
-
-class TraceLogModel (LogModel):
-  """Log model for trace messages."""
-  def __init__(me):
-    LogModel.__init__(me, ('Message',))
-  def notify(me, line):
-    """Call with a new trace message."""
-    me.add(line)
-
-class WarningLogModel (LogModel):
-  """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):
-    """Call with a new warning message."""
-    me.add(*getword(line))
-
-class LogViewer (MyWindow):
-  """Log viewer window.  Nothing very exciting."""
-  def __init__(me, 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()
-    i = 0
-    for c in me.model.cols:
-      me.list.append_column(G.TreeViewColumn(c,
-                                             G.CellRendererText(),
-                                             text = i))
-      i += 1
-    me.set_default_size(440, 256)
-    scr.add(me.list)
-    me.add(scr)
-    me.show_all()
-
-#----- Peer window ----------------------------------------------------------
-
-def xlate_time(t):
-  """Translate a time in tripe's stats format to something a human might
-  actually want to read."""
-  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'
-  for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
-    if ago < 2*n: break
-    ago /= n
-    unit = u
-  return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
-         (YY, MM, DD, hh, mm, ss, ago, unit)
-def xlate_bytes(b):
-  """Translate a number of bytes into something a human might want to read."""
-  suff = 'B'
-  b = int(b)
-  for s in 'KMG':
-    if b < 4096: break
-    b /= 1024
-    suff = s
-  return '%d %s' % (b, suff)
-
-## How to translate peer stats.  Maps the stat name to a translation
-## function.
-statsxlate = \
-  [('start-time', xlate_time),
-   ('last-packet-time', xlate_time),
-   ('last-keyexch-time', xlate_time),
-   ('bytes-in', xlate_bytes),
-   ('bytes-out', xlate_bytes),
-   ('keyexch-bytes-in', xlate_bytes),
-   ('keyexch-bytes-out', xlate_bytes),
-   ('ip-bytes-in', xlate_bytes),
-   ('ip-bytes-out', xlate_bytes)]
-
-## How to lay out the stats dialog.  Format is (LABEL, FORMAT): LABEL is
-## the label to give the entry box; FORMAT is the format string to write into
-## the entry.
-statslayout = \
-  [('Start time', '%(start-time)s'),
-   ('Last key-exchange', '%(last-keyexch-time)s'),
-   ('Last packet', '%(last-packet-time)s'),
-   ('Packets in/out',
-    '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
-   ('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)'),
-   ('Rejected packets', '%(rejected-packets)s')]
-
-class PeerWindow (MyWindow):
-  """Show information about a peer."""
-  def __init__(me, monitor, peer):
-    MyWindow.__init__(me)
-    me.set_title('TrIPE statistics: %s' % peer.name)
-    me.mon = monitor
-    me.peer = peer
-    table = GridPacker()
-    me.add(table)
-    me.e = {}
-    def add(label, text = None):
-      me.e[label] = table.info(label, text, len = 42, newlinep = True)
-    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)
-    me.hook(me.closehook, me.stopupdate)
-    me.hook(me.peer.deadhook, me.dead)
-    me.hook(me.peer.pinghook, me.ping)
-    me.tryupdate()
-    me.ping()
-    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 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
-  def dead(me):
-    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
-    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),
-                                               '22003',
-                                               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
-
-#----- The server monitor ---------------------------------------------------
-
-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()
-
-class MonitorWindow (MyWindow):
-
-  def __init__(me, monitor):
-    MyWindow.__init__(me)
-    me.set_title('TrIPE monitor')
-    me.mon = monitor
-    me.hook(me.mon.errorhook, me.report)
-    me.warnings = WarningLogModel()
-    me.hook(me.mon.warnhook, me.warnings.notify)
-    me.trace = TraceLogModel()
-    me.hook(me.mon.tracehook, me.trace.notify)
-
-    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))
-
-    vbox = G.VBox()
-    me.add(vbox)
-
-    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),
-       ('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')),
-       ('logs-menu', '_Logs', None, None),
-       ('show-warnings', 'Show _warnings', '<Alt>W', me.warnview.open),
-       ('show-trace', 'Show _trace', '<Alt>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),
-       ('kill-peer', '_Kill peer', None, me.killpeer),
-       ('force-kx', 'Force key e_xchange', None, me.forcekx)])
-    uidef = '''
-      <ui>
-        <menubar>
-          <menu action="file-menu">
-            <menuitem action="quit"/>
-          </menu>
-          <menu action="server-menu">
-             <menuitem action="connect"/>
-            <menuitem action="disconnect"/>
-            <separator/>
-            <menuitem action="server-version"/>
-            <menuitem action="add-peer"/>
-            <menuitem action="daemon"/>
-            <menuitem action="reload-keys"/>
-            <separator/>
-            <menuitem action="server-quit"/>
-          </menu>
-          <menu action="logs-menu">
-            <menuitem action="show-warnings"/>
-            <menuitem action="show-trace"/>
-            <menuitem action="trace-options"/>
-          </menu>
-          <menu action="help-menu">
-            <menuitem action="about"/>
-          </menu>
-        </menubar>
-        <popup name="peer-popup">
-          <menuitem action="add-peer"/>
-          <menuitem action="kill-peer"/>
-          <menuitem action="force-kx"/>
-        </popup>
-      </ui>
-      '''
-    me.ui.insert_action_group(actgroup, 0)
-    me.ui.add_ui_from_string(uidef)
-    vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
-    me.add_accel_group(me.ui.get_accel_group())
-    me.status = G.Statusbar()
-
-    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)
-
-    scr = G.ScrolledWindow()
-    scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
-    me.list = G.TreeView(me.listmodel)
-    me.list.append_column(G.TreeViewColumn('Peer name',
-                                           G.CellRendererText(),
-                                           text = 0))
-    me.list.append_column(G.TreeViewColumn('Address',
-                                           G.CellRendererText(),
-                                           text = 1))
-    me.list.append_column(G.TreeViewColumn('T-ping',
-                                           G.CellRendererText(),
-                                           text = 2,
-                                           foreground = 3))
-    me.list.append_column(G.TreeViewColumn('E-ping',
-                                           G.CellRendererText(),
-                                           text = 4,
-                                           foreground = 5))
-    me.list.get_column(1).set_expand(True)
-    me.list.connect('row-activated', me.activate)
-    me.list.connect('button-press-event', me.buttonpress)
-    me.list.set_reorderable(True)
-    me.list.get_selection().set_mode(G.SELECTION_NONE)
-    scr.add(me.list)
-    vbox.pack_start(scr)
-
-    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()
-
-  def addpeer(me, peer):
-    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')
-  def delpeer(me, peer):
-    me.listmodel.remove(peer.i)
-  def path_peer(me, path):
-    return me.mon.peers[me.listmodel[path][0]]
-
-  def activate(me, l, path, col):
-    peer = me.path_peer(path)
-    peer.win.open()
-  def buttonpress(me, l, ev):
-    if ev.button == 3:
-      r = me.list.get_path_at_pos(ev.x, ev.y)
-      for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
-        me.ui.get_widget(i).set_sensitive(me.mon.connectedp and
-                                          r is not None)
-      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)
-
-  def killpeer(me):
-    me.mon.simplecmd('KILL %s' % 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'
-    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()
-  def setstatus(me, status):
-    me.status.pop(0)
-    me.status.push(0, status)
-  def notify(me, note, rest):
-    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])
-    me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
-    for i in ('/menubar/server-menu/disconnect',
-              '/menubar/server-menu/server-version',
-              '/menubar/server-menu/add-peer',
-              '/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/daemon'). \
-      set_sensitive(parseinfo(me.mon.simplecmd('SERVINFO'))['daemon'] ==
-                    'nil')
-    me.reping()
-  def disconnected(me):
-    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/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 --------------------------------------------------------
-
-def version(fp = stdout):
-  """Print the program's version number."""
-  fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION))
-
-def usage(fp):
-  """Print a brief usage message for the program."""
-  fp.write('Usage: %s [-d DIR] [-a SOCK]\n' % quis)
-
-def main():
-  global tripedir
-  if 'TRIPEDIR' in environ:
-    tripedir = environ['TRIPEDIR']
-  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)
-  HookClient().hook(root.closehook, exit)
-  G.main()
-
-if __name__ == '__main__':
-  main()
-
-#----- That's all, folks ----------------------------------------------------