X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/tripe/blobdiff_plain/e6b06b6b61b4b877937d4a56ba704d3f18154dc2..786989941b7b4504f0234c4a318f929802e981ad:/tripemon.in diff --git a/tripemon.in b/tripemon.in deleted file mode 100644 index 0db07752..00000000 --- a/tripemon.in +++ /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', 'C', me.mon.connect), - ('disconnect', '_Disconnect', 'D', me.mon.disconnect), - ('quit', '_Quit', 'Q', me.close), - ('server-menu', '_Server', None, None), - ('daemon', 'Run in _background', None, cmd('DAEMON')), - ('server-version', 'Server version', 'V', me.servinfo.open), - ('reload-keys', 'Reload keys', 'R', cmd('RELOAD')), - ('server-quit', 'Terminate server', None, cmd('QUIT')), - ('logs-menu', '_Logs', None, None), - ('show-warnings', 'Show _warnings', 'W', me.warnview.open), - ('show-trace', 'Show _trace', '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...', 'A', me.addpeerwin.open), - ('kill-peer', '_Kill peer', None, me.killpeer), - ('force-kx', 'Force key e_xchange', None, me.forcekx)]) - uidef = ''' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ''' - 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 ----------------------------------------------------