+++ /dev/null
-#! @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 ----------------------------------------------------