#! @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
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:
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 = []
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)
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
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
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:
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):
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
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 <mdw@distorted.org.uk>'])
+ 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)
"""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."""
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
('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', '<Alt>C', me.mon.connect),
- ('disconnect', '_Disconnect', '<Alt>D', me.mon.disconnect),
- ('quit', '_Quit', '<Alt>Q', me.close),
+ ('connect', '_Connect', '<Control>C', conn.connect),
+ ('disconnect', '_Disconnect', '<Control>D',
+ lambda: conn.disconnect(None)),
+ ('quit', '_Quit', '<Control>Q', me.close),
('server-menu', '_Server', None, None),
- ('daemon', 'Run in _background', None, cmd('DAEMON')),
- ('server-version', 'Server version', '<Alt>V', me.servinfo.open),
- ('reload-keys', 'Reload keys', '<Alt>R', cmd('RELOAD')),
- ('server-quit', 'Terminate server', None, cmd('QUIT')),
+ ('daemon', 'Run in _background', None, cr(conn.daemon)),
+ ('server-version', 'Server version', '<Control>V', me.servinfo.open),
+ ('crypto-algs', 'Cryptographic algorithms',
+ '<Control>Y', me.cryptoinfo.open),
+ ('reload-keys', 'Reload keys', '<Control>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', '<Alt>W', me.warnview.open),
- ('show-trace', 'Show _trace', '<Alt>T', me.traceview.open),
+ ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
+ ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
('trace-options', 'Trace _options...', None, me.traceopts.open),
('help-menu', '_Help', None, None),
('about', '_About tripemon...', None, aboutbox.open),
- ('add-peer', '_Add peer...', '<Alt>A', me.addpeerwin.open),
+ ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
('kill-peer', '_Kill peer', None, me.killpeer),
('force-kx', 'Force key e_xchange', None, me.forcekx)])
+
+ ## Menu structures.
uidef = '''
<ui>
<menubar>
<menuitem action="quit"/>
</menu>
<menu action="server-menu">
- <menuitem action="connect"/>
+ <menuitem action="connect"/>
<menuitem action="disconnect"/>
<separator/>
<menuitem action="server-version"/>
+ <menuitem action="crypto-algs"/>
<menuitem action="add-peer"/>
+ <menuitem action="conn-peer"/>
<menuitem action="daemon"/>
<menuitem action="reload-keys"/>
<separator/>
</menubar>
<popup name="peer-popup">
<menuitem action="add-peer"/>
+ <menuitem action="conn-peer"/>
<menuitem action="kill-peer"/>
<menuitem action="force-kx"/>
</popup>
</ui>
'''
+
+ ## 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))
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',
'/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 --------------------------------------------------