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