+#! @PYTHON@
+# -*-python-*-
+
+#----- Dependencies ---------------------------------------------------------
+
+import socket as S
+from sys import argv, exit, stdin, stdout, stderr
+import os as OS
+from os import environ
+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]()
+
+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()
+
+#----- 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()
+
+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 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()
+
+def unimplemented(*hunoz):
+ """Indicator of laziness."""
+ moanbox("I've not written that bit yet.")
+
+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):
+ 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
+
+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)'
+ Y, M, D, h, m, s = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
+ return '%04d:%02d:%02d %02d:%02d:%02d' % (Y, M, D, h, m, s)
+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.ngood:
+ s += '; %.2f ms (last %.1f ms)' % (ping.ttot/ping.ngood, ping.tlast);
+ me.e[ping.cmd].set_text(s)
+
+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
+
+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 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()
+ 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,
+ lambda: me.mon.simplecmd('DAEMON')),
+ ('server-version', 'Server version', None, me.servinfo.open),
+ ('server-quit', 'Terminate server', None,
+ lambda: me.mon.simplecmd('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="add-peer"/>
+ <menuitem action="daemon"/>
+ <menuitem action="server-version"/>
+ <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()
+