#! @PYTHON@ ### -*- 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 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 MATH import sets as SET from optparse import OptionParser import time as TIME import re as RX from cStringIO import StringIO try: if OS.getenv('TRIPEMON_FORCE_GI'): raise ImportError import pygtk pygtk.require('2.0') import gtk as G import gobject as GO import gtk.gdk as GDK GL = GO GDK.KEY_Escape = G.keysyms.Escape def raise_window(w): w.window.raise_() combo_box_text = G.combo_box_new_text def set_entry_bg(e, c): e.modify_base(G.STATE_NORMAL, c) except ImportError: from gi.repository import GObject as GO, GLib as GL, Gtk as G, Gdk as GDK G.WINDOW_TOPLEVEL = G.WindowType.TOPLEVEL G.EXPAND = G.AttachOptions.EXPAND G.SHRINK = G.AttachOptions.SHRINK G.FILL = G.AttachOptions.FILL G.SORT_ASCENDING = G.SortType.ASCENDING G.POLICY_AUTOMATIC = G.PolicyType.AUTOMATIC G.SHADOW_IN = G.ShadowType.IN G.SELECTION_NONE = G.SelectionMode.NONE G.DIALOG_MODAL = G.DialogFlags.MODAL G.RESPONSE_CANCEL = G.ResponseType.CANCEL G.RESPONSE_NONE = G.ResponseType.NONE def raise_window(w): getattr(w.get_window(), 'raise')() combo_box_text = G.ComboBoxText def set_entry_bg(e, c): e.modify_bg(G.StateType.NORMAL, c) if OS.getenv('TRIPE_DEBUG_MONITOR') is not None: T._debug = 1 ###-------------------------------------------------------------------------- ### Doing things later. def uncaught(): """Report an uncaught exception.""" excepthook(*exc_info()) 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 _ def invoker(func, *args, **kw): """ Return a function which throws away its arguments and calls FUNC(*ARGS, **KW). 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 cr(func, *args, **kw): """Return a function which invokes FUNC(*ARGS, **KW) in a coroutine.""" name = T.funargstr(func, args, kw) return lambda *hunoz, **hukairz: \ T.Coroutine(xwrap(func), name = name).switch(*args, **kw) def incr(func): """Decorator: runs its function in a coroutine of its own.""" return lambda *args, **kw: \ (T.Coroutine(func, name = T.funargstr(func, args, kw)) .switch(*args, **kw)) ###-------------------------------------------------------------------------- ### Random bits of infrastructure. ## Program name, shorn of extraneous stuff. M.ego(argv[0]) moan = M.moan die = M.die class HookList (object): """ Notification hook list. 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. """ 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 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() class struct (object): """A very simple dumb data container object.""" def __init__(me, **kw): me.__dict__.update(kw) ## Matches ISO date format yyyy-mm-ddThh:mm:ss. rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$') ###-------------------------------------------------------------------------- ### Connections. class GIOWatcher (object): """ Monitor I/O events using glib. """ def __init__(me, conn, mc = GL.main_context_default()): me._conn = conn me._watch = None me._mc = mc def connected(me, sock): me._watch = GL.io_add_watch(sock, GL.IO_IN, lambda *hunoz: me._conn.receive()) def disconnected(me): GL.source_remove(me._watch) me._watch = None def iterate(me): me._mc.iteration(True) class Connection (T.TripeCommandDispatcher): """ The main connection to the server. The improvement over the TripeCommandDispatcher is that the Connection provides hooklists for NOTE, WARN and TRACE messages, and for connect and disconnect events. This class knows about the Glib I/O dispatcher system, and plugs into it. Hooks: * 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, socket): """Create a new Connection.""" T.TripeCommandDispatcher.__init__(me, socket) me.connecthook = HookList() me.disconnecthook = HookList() me.notehook = HookList() 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.iowatch = GIOWatcher(me) def connected(me): """Handles reconnection to the server, and signals the hook.""" T.TripeCommandDispatcher.connected(me) me.connecthook.run() def disconnected(me, reason): """Handles disconnection from the server, and signals the hook.""" 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 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.__dict__.update(conn.algs(name)) me.update() def update(me, hunoz = None): """Update the peer, fetching information about it from the server.""" me._setaddr(conn.addr(me.name)) me.ifname = conn.ifname(me.name) me.__dict__.update(conn.peerinfo(me.name)) me.changehook.run() def _setaddr(me, addr): """Set the peer's address.""" 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) def setaddr(me, addr): """Informs the object of a change to its address to ADDR.""" me._setaddr(addr) 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. 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-active')] 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': T.aside(me.peers.add, rest[0], None) elif code == 'KILL': T.aside(me.peers.remove, rest[0]) elif code == 'NEWIFNAME': try: me.peers[rest[0]].setifname(rest[2]) except KeyError: pass elif code == 'NEWADDR': try: me.peers[rest[0]].setaddr(rest[1:]) except KeyError: pass elif code == 'SVCCLAIM': T.aside(me.services.add, rest[0], rest[1]) if rest[0] == 'connect': T.aside(me._updateautopeers) elif code == 'SVCRELEASE': T.aside(me.services.remove, rest[0]) if rest[0] == 'connect': T.aside(me._updateautopeers) elif code == 'USER': if not rest: return if rest[0] == 'watch' and \ rest[1] == 'peerdb-update': T.aside(me._updateautopeers) ###-------------------------------------------------------------------------- ### Window management cruft. class MyWindowMixin (G.Window, HookClient): """ 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 version of MyWindowMixin suitable as a single parent class.""" def __init__(me, kind = G.WINDOW_TOPLEVEL): G.Window.__init__(me, kind) me.mywininit() class TrivialWindowMixin (MyWindowMixin): """A simple window which you can close with Escape.""" def mywininit(me): super(TrivialWindowMixin, me).mywininit() me.connect('key-press-event', me._keypress) def _keypress(me, _, ev): if ev.keyval == GDK.KEY_Escape: me.destroy() class TrivialWindow (MyWindow, TrivialWindowMixin): pass 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 other arguments 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)) 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 """ 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): """Initialize a new GridPacker.""" 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.Label() if text is not None: e.set_text(text) e.set_width_chars(len) 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 -- 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. """ HookClient.__init__(me) me.createfunc = createfunc me.window = None def open(me): """Opens the window, creating it if necessary.""" if me.window: raise_window(me.window) 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('#ff6666') 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. The attribute validp reflects whether the contents are currently valid. """ def __init__(me, valid, text = '', size = -1, *arg, **kw): """ 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) me.connect("state-changed", me._check) if callable(valid): me.validate = valid else: me.validate = RX.compile(valid).match me.ensure_style() if size != -1: me.set_width_chars(size) 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 set_entry_bg(me, None) else: me.validp = False set_entry_bg(me, me.is_sensitive() and c_red or None) 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): """ 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. 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, TrivialWindowMixin): """The program `About' box.""" def __init__(me): G.AboutDialog.__init__(me) me.mywininit() me.set_name('TrIPEmon') me.set_version(T.VERSION) me.set_license(GPL) 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' % M.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, True, True, 0) label.show() d.run() d.destroy() def unimplemented(*hunoz): """Indicator of laziness.""" moanbox("I've not written that bit yet.") ###-------------------------------------------------------------------------- ### Logging windows. class LogModel (G.ListStore): """ A simple list of log messages, usable as the model for a TreeView. The column headings are stored in the `cols' attribute. """ def __init__(me, columns): """ 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. 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.""" 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, tag, *rest): """Call with a new warning message.""" me.add(tag, ' '.join([T.quotify(w) for w in rest])) class LogViewer (TrivialWindow): """ 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. """ TrivialWindow.__init__(me) me.model = model scr = MyScrolledWindow() me.list = MyTreeView(me.model) i = 0 for c in me.model.cols: 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() ###-------------------------------------------------------------------------- ### 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: T.defer(me._q.put, (p, 'ADD', None))) me.hook(monitor.peers.delhook, lambda p: T.defer(me._q.put, (p, 'KILL', None))) if conn.connectedp(): me.connected() def _connected(me): """Respond to connection: start pinging thngs.""" me._timer = GL.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.""" GL.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, True, True, 0) 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.l_tunnel = table.labelled('Tunnel', combo_box_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) def tickybox_sensitivity(tickybox, target): tickybox.connect('toggled', lambda t: target.set_sensitive (t.get_active())) me.c_keepalive = G.CheckButton('Keepalives') table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL) me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5) me.e_keepalive.set_sensitive(False) tickybox_sensitivity(me.c_keepalive, me.e_keepalive) table.pack(me.e_keepalive, width = 3) me.c_mobile = G.CheckButton('Mobile') table.pack(me.c_mobile, newlinep = True, width = 4, xopt = G.FILL) me.c_peerkey = G.CheckButton('Peer key tag') table.pack(me.c_peerkey, newlinep = True, xopt = G.FILL) me.e_peerkey = ValidatingEntry(r'^[^.:\s]+$', '', 16) me.e_peerkey.set_sensitive(False) tickybox_sensitivity(me.c_peerkey, me.e_peerkey) table.pack(me.e_peerkey, width = 3) me.c_privkey = G.CheckButton('Private key tag') table.pack(me.c_privkey, newlinep = True, xopt = G.FILL) me.e_privkey = ValidatingEntry(r'^[^.:\s]+$', '', 16) me.e_privkey.set_sensitive(False) tickybox_sensitivity(me.c_privkey, me.e_privkey) table.pack(me.e_privkey, width = 3) me.show_all() def ok(me): """Handle an OK press: create the peer.""" try: t = me.l_tunnel.get_active() me._addpeer(me.e_name.get_text(), me.e_addr.get_text(), me.e_port.get_text(), keepalive = (me.c_keepalive.get_active() and me.e_keepalive.get_text() or None), tunnel = t and me.tuns[t] or None, key = (me.c_peerkey.get_active() and me.e_peerkey.get_text() or None), priv = (me.c_privkey.get_active() and me.e_privkey.get_text() or None)) except ValidationError: GDK.beep() return @incr def _addpeer(me, *args, **kw): """Coroutine function: actually do the ADD command.""" try: conn.add(*args, **kw) me.destroy() except T.TripeError, exc: T.defer(moanbox, ' '.join(exc)) class ServInfo (TrivialWindow): """ 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): TrivialWindow.__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, True, True, 0) 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 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 = 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 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'), ('Private key', '%(current-key)s'), ('Diffie-Hellman group', '%(kx-group)s ' '(%(kx-group-order-bits)s-bit order, ' '%(kx-group-elt-bits)s-bit elements)'), ('Cipher', '%(cipher)s (%(cipher-keysz)s-bit key, %(cipher-blksz)s-bit block)'), ('Mac', '%(mac)s (%(mac-keysz)s-bit key, %(mac-tagsz)s-bit tag)'), ('Hash', '%(hash)s (%(hash-sz)s-bit output)'), ('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-out)s (%(ip-bytes-out)s)'), ('Rejected packets', '%(rejected-packets)s')] class PeerWindow (TrivialWindow): """ 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.""" TrivialWindow.__init__(me) me.set_title('TrIPE statistics: %s' % peer.name) me.peer = peer table = GridPacker() me.add(table) ## Utility for adding fields. me.e = {} 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', 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() ## 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 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]) stat.update(me.peer.__dict__) for label, format in statslayout: me.e[label].set_text(format % stat) GL.timeout_add(1000, lambda: me.cr.switch() and False) me.cr.parent.switch() me.cr = None def tryupdate(me): """Start the updater coroutine, if it's not going already.""" if me.cr is None: me.cr = T.Coroutine(me._update, name = 'update-peer-window %s' % me.peer.name) 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, 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 (TrivialWindow): """Simple display of cryptographic algorithms in use.""" def __init__(me): TrivialWindow.__init__(me) me.set_title('Cryptographic algorithms') T.aside(me.populate) def populate(me): table = GridPacker() me.add(table) 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) me.show_all() ###-------------------------------------------------------------------------- ### Main monitor window. class MonitorWindow (MyWindow): """ 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') ## Hook onto diagnostic outputs. me.warnings = WarningLogModel() me.hook(conn.warnhook, me.warnings.notify) me.trace = TraceLogModel() 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.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() actgroup = makeactiongroup('monitor', [('file-menu', '_File', None, None), ('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, 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), ('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)]) ## Menu structures. uidef = ''' ''' ## 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'), False, True, 0) me.add_accel_group(me.ui.get_accel_group()) ## 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(monitor.peers.addhook, me.addpeer) me.hook(monitor.peers.delhook, me.delpeer) me.hook(monitor.autopeershook, me.apchange) ## 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)) 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, True, True, 0) ## Construct the status bar, and listen on hooks which report changes to ## connection status. me.status = G.Statusbar() vbox.pack_start(me.status, False, True, 0) 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(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 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, name = '_addautopeerhack %s' % peer).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: T.defer(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: 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(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) def killpeer(me): """Kill a peer from the popup menu.""" cr(conn.kill, me.menupeer.name)() def forcekx(me): """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: 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): """Hook: invoked when interesting notifications occur.""" if note == 'DAEMON': me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False) def connected(me): """ 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', '/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/conn-peer'). \ set_sensitive(bool(monitor.autopeers)) me.ui.get_widget('/menubar/server-menu/daemon'). \ 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) 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 ## Try to establish a connection. conn = Connection(opts.tripesock) ## Make the main interesting coroutines and objects. monitor = Monitor() pinger = Pinger() pinger.switch() def main(): ## Main window. root = MonitorWindow() conn.connect() root.show_all() ## Main loop. HookClient().hook(root.closehook, exit) conn.mainloop() if __name__ == '__main__': opts = parse_options() init(opts) main() ###----- That's all, folks --------------------------------------------------