chiark / gitweb /
svc/conntrack.in: New service to track connection status.
[tripe] / mon / tripemon.in
1 #! @PYTHON@
2 ### -*- mode: python; coding: utf-8 -*-
3 ###
4 ### Graphical monitor for tripe server
5 ###
6 ### (c) 2007 Straylight/Edgeware
7 ###
8
9 ###----- Licensing notice ---------------------------------------------------
10 ###
11 ### This file is part of Trivial IP Encryption (TrIPE).
12 ###
13 ### TrIPE is free software; you can redistribute it and/or modify
14 ### it under the terms of the GNU General Public License as published by
15 ### the Free Software Foundation; either version 2 of the License, or
16 ### (at your option) any later version.
17 ###
18 ### TrIPE is distributed in the hope that it will be useful,
19 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 ### GNU General Public License for more details.
22 ###
23 ### You should have received a copy of the GNU General Public License
24 ### along with TrIPE; if not, write to the Free Software Foundation,
25 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26
27 ###--------------------------------------------------------------------------
28 ### Dependencies.
29
30 import socket as S
31 import tripe as T
32 import mLib as M
33 from sys import argv, exit, stdin, stdout, stderr, exc_info, excepthook
34 import os as OS
35 from os import environ
36 import math as MATH
37 import sets as SET
38 from optparse import OptionParser
39 import time as TIME
40 import re as RX
41 from cStringIO import StringIO
42
43 import pygtk
44 pygtk.require('2.0')
45 import gtk as G
46 import gobject as GO
47 import gtk.gdk as GDK
48
49 if OS.getenv('TRIPE_DEBUG_MONITOR') is not None:
50   T._debug = 1
51
52 ###--------------------------------------------------------------------------
53 ### Doing things later.
54
55 def uncaught():
56   """Report an uncaught exception."""
57   excepthook(*exc_info())
58
59 def xwrap(func):
60   """
61   Return a function which behaves like FUNC, but reports exceptions via
62   uncaught.
63   """
64   def _(*args, **kw):
65     try:
66       return func(*args, **kw)
67     except SystemExit:
68       raise
69     except:
70       uncaught()
71       raise
72   return _
73
74 def invoker(func, *args, **kw):
75   """
76   Return a function which throws away its arguments and calls
77   FUNC(*ARGS, **KW).
78
79   If for loops worked by binding rather than assignment then we wouldn't need
80   this kludge.
81   """
82   return lambda *hunoz, **hukairz: xwrap(func)(*args, **kw)
83
84 def cr(func, *args, **kw):
85   """Return a function which invokes FUNC(*ARGS, **KW) in a coroutine."""
86   def _(*hunoz, **hukairz):
87     T.Coroutine(xwrap(func)).switch(*args, **kw)
88   return _
89
90 def incr(func):
91   """Decorator: runs its function in a coroutine of its own."""
92   return lambda *args, **kw: T.Coroutine(func).switch(*args, **kw)
93
94 ###--------------------------------------------------------------------------
95 ### Random bits of infrastructure.
96
97 ## Program name, shorn of extraneous stuff.
98 M.ego(argv[0])
99 moan = M.moan
100 die = M.die
101
102 class HookList (object):
103   """
104   Notification hook list.
105
106   Other objects can add functions onto the hook list.  When the hook list is
107   run, the functions are called in the order in which they were registered.
108   """
109
110   def __init__(me):
111     """Basic initialization: create the hook list."""
112     me.list = []
113
114   def add(me, func, obj):
115     """Add FUNC to the list of hook functions."""
116     me.list.append((obj, func))
117
118   def prune(me, obj):
119     """Remove hook functions registered with the given OBJ."""
120     new = []
121     for o, f in me.list:
122       if o is not obj:
123         new.append((o, f))
124     me.list = new
125
126   def run(me, *args, **kw):
127     """Invoke the hook functions with arguments *ARGS and **KW."""
128     for o, hook in me.list:
129       rc = hook(*args, **kw)
130       if rc is not None: return rc
131     return None
132
133 class HookClient (object):
134   """
135   Mixin for classes which are clients of hooks.
136
137   It keeps track of the hooks it's a client of, and has the ability to
138   extricate itself from all of them.  This is useful because weak objects
139   don't seem to work well.
140   """
141   def __init__(me):
142     """Basic initialization."""
143     me.hooks = SET.Set()
144
145   def hook(me, hk, func):
146     """Add FUNC to the hook list HK."""
147     hk.add(func, me)
148     me.hooks.add(hk)
149
150   def unhook(me, hk):
151     """Remove myself from the hook list HK."""
152     hk.prune(me)
153     me.hooks.discard(hk)
154
155   def unhookall(me):
156     """Remove myself from all hook lists."""
157     for hk in me.hooks:
158       hk.prune(me)
159     me.hooks.clear()
160
161 class struct (object):
162   """A very simple dumb data container object."""
163   def __init__(me, **kw):
164     me.__dict__.update(kw)
165
166 ## Matches ISO date format yyyy-mm-ddThh:mm:ss.
167 rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
168
169 ###--------------------------------------------------------------------------
170 ### Connections.
171
172 class GIOWatcher (object):
173   """
174   Monitor I/O events using glib.
175   """
176   def __init__(me, conn, mc = GO.main_context_default()):
177     me._conn = conn
178     me._watch = None
179     me._mc = mc
180   def connected(me, sock):
181     me._watch = GO.io_add_watch(sock, GO.IO_IN,
182                                 lambda *hunoz: me._conn.receive())
183   def disconnected(me):
184     GO.source_remove(me._watch)
185     me._watch = None
186   def iterate(me):
187     me._mc.iteration(True)
188
189 class Connection (T.TripeCommandDispatcher):
190   """
191   The main connection to the server.
192
193   The improvement over the TripeCommandDispatcher is that the Connection
194   provides hooklists for NOTE, WARN and TRACE messages, and for connect and
195   disconnect events.
196
197   This class knows about the Glib I/O dispatcher system, and plugs into it.
198
199   Hooks:
200
201     * connecthook(): a connection to the server has been established
202     * disconnecthook(): the connection has been dropped
203     * notehook(TOKEN, ...): server issued a notification
204     * warnhook(TOKEN, ...): server issued a warning
205     * tracehook(TOKEN, ...): server issued a trace message
206   """
207
208   def __init__(me, socket):
209     """Create a new Connection."""
210     T.TripeCommandDispatcher.__init__(me, socket)
211     me.connecthook = HookList()
212     me.disconnecthook = HookList()
213     me.notehook = HookList()
214     me.warnhook = HookList()
215     me.tracehook = HookList()
216     me.handler['NOTE'] = lambda _, *rest: me.notehook.run(*rest)
217     me.handler['WARN'] = lambda _, *rest: me.warnhook.run(*rest)
218     me.handler['TRACE'] = lambda _, *rest: me.tracehook.run(*rest)
219     me.iowatch = GIOWatcher(me)
220
221   def connected(me):
222     """Handles reconnection to the server, and signals the hook."""
223     T.TripeCommandDispatcher.connected(me)
224     me.connecthook.run()
225
226   def disconnected(me, reason):
227     """Handles disconnection from the server, and signals the hook."""
228     me.disconnecthook.run(reason)
229     T.TripeCommandDispatcher.disconnected(me, reason)
230
231 ###--------------------------------------------------------------------------
232 ### Watching the peers go by.
233
234 class MonitorObject (object):
235   """
236   An object with hooks it uses to notify others of changes in its state.
237   These are the objects tracked by the MonitorList class.
238
239   The object has a name, an `aliveness' state indicated by the `alivep' flag,
240   and hooks.
241
242   Hooks:
243
244     * changehook(): the object has changed its state
245     * deadhook(): the object has been destroyed
246
247   Subclass responsibilities:
248
249     * update(INFO): update internal state based on the provided INFO, and run
250       the changehook.
251   """
252
253   def __init__(me, name):
254     """Initialize the object with the given NAME."""
255     me.name = name
256     me.deadhook = HookList()
257     me.changehook = HookList()
258     me.alivep = True
259
260   def dead(me):
261     """Mark the object as dead; invoke the deadhook."""
262     me.alivep = False
263     me.deadhook.run()
264
265 class Peer (MonitorObject):
266   """
267   An object representing a connected peer.
268
269   As well as the standard hooks, a peer has a pinghook, which isn't used
270   directly by this class.
271
272   Hooks:
273
274     * pinghook(): invoked by the Pinger (q.v.) when ping statistics change
275
276   Attributes provided are:
277
278     * addr = a vaguely human-readable representation of the peer's address
279     * ifname = the peer's interface name
280     * tunnel = the kind of tunnel the peer is using
281     * keepalive = the peer's keepalive interval in seconds
282     * ping['EPING'] and ping['PING'] = pingstate statistics (maintained by
283       the Pinger)
284   """
285
286   def __init__(me, name):
287     """Initialize the object with the given name."""
288     MonitorObject.__init__(me, name)
289     me.pinghook = HookList()
290     me.update()
291
292   def update(me, hunoz = None):
293     """Update the peer, fetching information about it from the server."""
294     addr = conn.addr(me.name)
295     if addr[0] == 'INET':
296       ipaddr, port = addr[1:]
297       try:
298         name = S.gethostbyaddr(ipaddr)[0]
299         me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
300       except S.herror:
301         me.addr = 'INET %s:%s' % (ipaddr, port)
302     else:
303       me.addr = ' '.join(addr)
304     me.ifname = conn.ifname(me.name)
305     me.__dict__.update(conn.peerinfo(me.name))
306     me.changehook.run()
307
308   def setifname(me, newname):
309     """Informs the object of a change to its interface name to NEWNAME."""
310     me.ifname = newname
311     me.changehook.run()
312
313 class Service (MonitorObject):
314   """
315   Represents a service.
316
317   Additional attributes are:
318
319     * version = the service version
320   """
321   def __init__(me, name, version):
322     MonitorObject.__init__(me, name)
323     me.version = version
324
325   def update(me, version):
326     """Tell the Service that its version has changed to VERSION."""
327     me.version = version
328     me.changehook.run()
329
330 class MonitorList (object):
331   """
332   Maintains a collection of MonitorObjects.
333
334   The MonitorList can be indexed by name to retrieve the individual objects;
335   iteration generates the individual objects.  More complicated operations
336   can be done on the `table' dictionary directly.
337
338   Hooks addhook(OBJ) and delhook(OBJ) are invoked when objects are added or
339   deleted.
340
341   Subclass responsibilities:
342
343     * list(): return a list of (NAME, INFO) pairs.
344
345     * make(NAME, INFO): returns a new MonitorObject for the given NAME; INFO
346       is from the output of list().
347   """
348
349   def __init__(me):
350     """Initialize a new MonitorList."""
351     me.table = {}
352     me.addhook = HookList()
353     me.delhook = HookList()
354
355   def update(me):
356     """
357     Refresh the list of objects:
358
359     We add new object which have appeared, delete ones which have vanished,
360     and update any which persist.
361     """
362     new = {}
363     for name, stuff in me.list():
364       new[name] = True
365       me.add(name, stuff)
366     for name in me.table.copy():
367       if name not in new:
368         me.remove(name)
369
370   def add(me, name, stuff):
371     """
372     Add a new object created by make(NAME, STUFF) if it doesn't already
373     exist.  If it does, update it.
374     """
375     if name not in me.table:
376       obj = me.make(name, stuff)
377       me.table[name] = obj
378       me.addhook.run(obj)
379     else:
380       me.table[name].update(stuff)
381
382   def remove(me, name):
383     """
384     Remove the object called NAME from the list.
385
386     The object becomes dead.
387     """
388     if name in me.table:
389       obj = me.table[name]
390       del me.table[name]
391       me.delhook.run(obj)
392       obj.dead()
393
394   def __getitem__(me, name):
395     """Retrieve the object called NAME."""
396     return me.table[name]
397
398   def __iter__(me):
399     """Iterate over the objects."""
400     return me.table.itervalues()
401
402 class PeerList (MonitorList):
403   """The list of the known peers."""
404   def list(me):
405     return [(name, None) for name in conn.list()]
406   def make(me, name, stuff):
407     return Peer(name)
408
409 class ServiceList (MonitorList):
410   """The list of the registered services."""
411   def list(me):
412     return conn.svclist()
413   def make(me, name, stuff):
414     return Service(name, stuff)
415
416 class Monitor (HookClient):
417   """
418   The main monitor: keeps track of the changes happening to the server.
419
420   Exports the peers, services MonitorLists, and a (plain Python) list
421   autopeers of peers which the connect service knows how to start by name.
422
423   Hooks provided:
424
425     * autopeershook(): invoked when the auto-peers list changes.
426   """
427   def __init__(me):
428     """Initialize the Monitor."""
429     HookClient.__init__(me)
430     me.peers = PeerList()
431     me.services = ServiceList()
432     me.hook(conn.connecthook, me._connected)
433     me.hook(conn.notehook, me._notify)
434     me.autopeershook = HookList()
435     me.autopeers = None
436
437   def _connected(me):
438     """Handle a successful connection by starting the setup coroutine."""
439     me._setup()
440
441   @incr
442   def _setup(me):
443     """Coroutine function: initialize for a new connection."""
444     conn.watch('-A+wnt')
445     me.peers.update()
446     me.services.update()
447     me._updateautopeers()
448
449   def _updateautopeers(me):
450     """Update the auto-peers list from the connect service."""
451     if 'connect' in me.services.table:
452       me.autopeers = [' '.join(line)
453                       for line in conn.svcsubmit('connect', 'list')]
454       me.autopeers.sort()
455     else:
456       me.autopeers = None
457     me.autopeershook.run()
458
459   def _notify(me, code, *rest):
460     """
461     Handle notifications from the server.
462
463     ADD, KILL and NEWIFNAME notifications get passed up to the PeerList;
464     SVCCLAIM and SVCRELEASE get passed up to the ServiceList.  Finally,
465     peerdb-update notifications from the watch service cause us to refresh
466     the auto-peers list.
467     """
468     if code == 'ADD':
469       T.aside(me.peers.add, rest[0], None)
470     elif code == 'KILL':
471       T.aside(me.peers.remove, rest[0])
472     elif code == 'NEWIFNAME':
473       try:
474         me.peers[rest[0]].setifname(rest[2])
475       except KeyError:
476         pass
477     elif code == 'SVCCLAIM':
478       T.aside(me.services.add, rest[0], rest[1])
479       if rest[0] == 'connect':
480         T.aside(me._updateautopeers)
481     elif code == 'SVCRELEASE':
482       T.aside(me.services.remove, rest[0])
483       if rest[0] == 'connect':
484         T.aside(me._updateautopeers)
485     elif code == 'USER':
486       if not rest: return
487       if rest[0] == 'watch' and \
488          rest[1] == 'peerdb-update':
489         T.aside(me._updateautopeers)
490
491 ###--------------------------------------------------------------------------
492 ### Window management cruft.
493
494 class MyWindowMixin (G.Window, HookClient):
495   """
496   Mixin for windows which call a closehook when they're destroyed.  It's also
497   a hookclient, and will release its hooks when it's destroyed.
498
499   Hooks:
500
501     * closehook(): called when the window is closed.
502   """
503
504   def mywininit(me):
505     """Initialization function.  Note that it's not called __init__!"""
506     me.closehook = HookList()
507     HookClient.__init__(me)
508     me.connect('destroy', invoker(me.close))
509
510   def close(me):
511     """Close the window, invoking the closehook and releasing all hooks."""
512     me.closehook.run()
513     me.destroy()
514     me.unhookall()
515
516 class MyWindow (MyWindowMixin):
517   """A version of MyWindowMixin suitable as a single parent class."""
518   def __init__(me, kind = G.WINDOW_TOPLEVEL):
519     G.Window.__init__(me, kind)
520     me.mywininit()
521
522 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
523   """A dialogue box with a closehook and sensible button binding."""
524
525   def __init__(me, title = None, flags = 0, buttons = []):
526     """
527     The BUTTONS are a list of (STOCKID, THUNK) pairs: call the appropriate
528     THUNK when the button is pressed.  The other arguments are just like
529     GTK's Dialog class.
530     """
531     i = 0
532     br = []
533     me.rmap = []
534     for b, f in buttons:
535       br.append(b)
536       br.append(i)
537       me.rmap.append(f)
538       i += 1
539     G.Dialog.__init__(me, title, None, flags, tuple(br))
540     me.mywininit()
541     me.set_default_response(i - 1)
542     me.connect('response', me.respond)
543
544   def respond(me, hunoz, rid, *hukairz):
545     """Dispatch responses to the appropriate thunks."""
546     if rid >= 0: me.rmap[rid]()
547
548 def makeactiongroup(name, acts):
549   """
550   Creates an ActionGroup called NAME.
551
552   ACTS is a list of tuples containing:
553
554     * ACT: an action name
555     * LABEL: the label string for the action
556     * ACCEL: accelerator string, or None
557     * FUNC: thunk to call when the action is invoked
558   """
559   actgroup = G.ActionGroup(name)
560   for act, label, accel, func in acts:
561     a = G.Action(act, label, None, None)
562     if func: a.connect('activate', invoker(func))
563     actgroup.add_action_with_accel(a, accel)
564   return actgroup
565
566 class GridPacker (G.Table):
567   """
568   Like a Table, but with more state: makes filling in the widgets easier.
569   """
570
571   def __init__(me):
572     """Initialize a new GridPacker."""
573     G.Table.__init__(me)
574     me.row = 0
575     me.col = 0
576     me.rows = 1
577     me.cols = 1
578     me.set_border_width(4)
579     me.set_col_spacings(4)
580     me.set_row_spacings(4)
581
582   def pack(me, w, width = 1, newlinep = False,
583            xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
584            xpad = 0, ypad = 0):
585     """
586     Packs a new widget.
587
588     W is the widget to add.  XOPY, YOPT, XPAD and YPAD are as for Table.
589     WIDTH is how many cells to take up horizontally.  NEWLINEP is whether to
590     start a new line for this widget.  Returns W.
591     """
592     if newlinep:
593       me.row += 1
594       me.col = 0
595     bot = me.row + 1
596     right = me.col + width
597     if bot > me.rows or right > me.cols:
598       if bot > me.rows: me.rows = bot
599       if right > me.cols: me.cols = right
600       me.resize(me.rows, me.cols)
601     me.attach(w, me.col, me.col + width, me.row, me.row + 1,
602               xopt, yopt, xpad, ypad)
603     me.col += width
604     return w
605
606   def labelled(me, lab, w, newlinep = False, **kw):
607     """
608     Packs a labelled widget.
609
610     Other arguments are as for pack.  Returns W.
611     """
612     label = G.Label(lab + ' ')
613     label.set_alignment(1.0, 0)
614     me.pack(label, newlinep = newlinep, xopt = G.FILL)
615     me.pack(w, **kw)
616     return w
617
618   def info(me, label, text = None, len = 18, **kw):
619     """
620     Packs an information widget with a label.
621
622     LABEL is the label; TEXT is the initial text; LEN is the estimated length
623     in characters.  Returns the entry widget.
624     """
625     e = G.Label()
626     if text is not None: e.set_text(text)
627     e.set_width_chars(len)
628     e.set_selectable(True)
629     e.set_alignment(0.0, 0.5)
630     me.labelled(label, e, **kw)
631     return e
632
633 class WindowSlot (HookClient):
634   """
635   A place to store a window -- specificially a MyWindowMixin.
636
637   If the window is destroyed, remember this; when we come to open the window,
638   raise it if it already exists; otherwise make a new one.
639   """
640   def __init__(me, createfunc):
641     """
642     Constructor: CREATEFUNC must return a new Window which supports the
643     closehook protocol.
644     """
645     HookClient.__init__(me)
646     me.createfunc = createfunc
647     me.window = None
648
649   def open(me):
650     """Opens the window, creating it if necessary."""
651     if me.window:
652       me.window.window.raise_()
653     else:
654       me.window = me.createfunc()
655       me.hook(me.window.closehook, me.closed)
656
657   def closed(me):
658     """Handles the window being closed."""
659     me.unhook(me.window.closehook)
660     me.window = None
661
662 class MyTreeView (G.TreeView):
663   def __init__(me, model):
664     G.TreeView.__init__(me, model)
665     me.set_rules_hint(True)
666
667 class MyScrolledWindow (G.ScrolledWindow):
668   def __init__(me):
669     G.ScrolledWindow.__init__(me)
670     me.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
671     me.set_shadow_type(G.SHADOW_IN)
672
673 ## Matches a signed integer.
674 rx_num = RX.compile(r'^[-+]?\d+$')
675
676 ## The colour red.
677 c_red = GDK.color_parse('red')
678
679 class ValidationError (Exception):
680   """Raised by ValidatingEntry.get_text() if the text isn't valid."""
681   pass
682
683 class ValidatingEntry (G.Entry):
684   """
685   Like an Entry, but makes the text go red if the contents are invalid.
686
687   If get_text is called, and the text is invalid, ValidationError is raised.
688   The attribute validp reflects whether the contents are currently valid.
689   """
690
691   def __init__(me, valid, text = '', size = -1, *arg, **kw):
692     """
693     Make a validating Entry.
694
695     VALID is a regular expression or a predicate on strings.  TEXT is the
696     default text to insert.  SIZE is the size of the box to set, in
697     characters (ish).  Other arguments are passed to Entry.
698     """
699     G.Entry.__init__(me, *arg, **kw)
700     me.connect("changed", me.check)
701     if callable(valid):
702       me.validate = valid
703     else:
704       me.validate = RX.compile(valid).match
705     me.ensure_style()
706     me.c_ok = me.get_style().text[G.STATE_NORMAL]
707     me.c_bad = c_red
708     if size != -1: me.set_width_chars(size)
709     me.set_activates_default(True)
710     me.set_text(text)
711     me.check()
712
713   def check(me, *hunoz):
714     """Check the current text and update validp and the text colour."""
715     if me.validate(G.Entry.get_text(me)):
716       me.validp = True
717       me.modify_text(G.STATE_NORMAL, me.c_ok)
718     else:
719       me.validp = False
720       me.modify_text(G.STATE_NORMAL, me.c_bad)
721
722   def get_text(me):
723     """
724     Return the text in the Entry if it's valid.  If it isn't, raise
725     ValidationError.
726     """
727     if not me.validp:
728       raise ValidationError
729     return G.Entry.get_text(me)
730
731 def numericvalidate(min = None, max = None):
732   """
733   Return a validation function for numbers.
734
735   Entry must consist of an optional sign followed by digits, and the
736   resulting integer must be within the given bounds.
737   """
738   return lambda x: (rx_num.match(x) and
739                     (min is None or long(x) >= min) and
740                     (max is None or long(x) <= max))
741
742 ###--------------------------------------------------------------------------
743 ### Various minor dialog boxen.
744
745 GPL = """This program is free software; you can redistribute it and/or modify
746 it under the terms of the GNU General Public License as published by
747 the Free Software Foundation; either version 2 of the License, or
748 (at your option) any later version.
749
750 This program is distributed in the hope that it will be useful,
751 but WITHOUT ANY WARRANTY; without even the implied warranty of
752 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
753 GNU General Public License for more details.
754
755 You should have received a copy of the GNU General Public License
756 along with this program; if not, write to the Free Software Foundation,
757 Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
758
759 class AboutBox (G.AboutDialog, MyWindowMixin):
760   """The program `About' box."""
761   def __init__(me):
762     G.AboutDialog.__init__(me)
763     me.mywininit()
764     me.set_name('TrIPEmon')
765     me.set_version(T.VERSION)
766     me.set_license(GPL)
767     me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
768     me.set_comments('A graphical monitor for the TrIPE VPN server')
769     me.set_copyright('Copyright Â© 2006-2008 Straylight/Edgeware')
770     me.connect('response', me.respond)
771     me.show()
772   def respond(me, hunoz, rid, *hukairz):
773     if rid == G.RESPONSE_CANCEL:
774       me.close()
775 aboutbox = WindowSlot(AboutBox)
776
777 def moanbox(msg):
778   """Report an error message in a window."""
779   d = G.Dialog('Error from %s' % M.quis,
780                flags = G.DIALOG_MODAL,
781                buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
782   label = G.Label(msg)
783   label.set_padding(20, 20)
784   d.vbox.pack_start(label)
785   label.show()
786   d.run()
787   d.destroy()
788
789 def unimplemented(*hunoz):
790   """Indicator of laziness."""
791   moanbox("I've not written that bit yet.")
792
793 ###--------------------------------------------------------------------------
794 ### Logging windows.
795
796 class LogModel (G.ListStore):
797   """
798   A simple list of log messages, usable as the model for a TreeView.
799
800   The column headings are stored in the `cols' attribute.
801   """
802
803   def __init__(me, columns):
804     """
805     COLUMNS must be a list of column name strings.  We add a time column to
806     the left.
807     """
808     me.cols = ('Time',) + columns
809     G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
810
811   def add(me, *entries):
812     """
813     Adds a new log message, with a timestamp.
814
815     The ENTRIES are the contents for the list columns.
816     """
817     now = TIME.strftime('%Y-%m-%d %H:%M:%S')
818     me.append((now, ) + entries)
819
820 class TraceLogModel (LogModel):
821   """Log model for trace messages."""
822   def __init__(me):
823     LogModel.__init__(me, ('Message',))
824   def notify(me, line):
825     """Call with a new trace message."""
826     me.add(line)
827
828 class WarningLogModel (LogModel):
829   """
830   Log model for warnings.
831
832   We split the category out into a separate column.
833   """
834   def __init__(me):
835     LogModel.__init__(me, ('Category', 'Message'))
836   def notify(me, tag, *rest):
837     """Call with a new warning message."""
838     me.add(tag, ' '.join([T.quotify(w) for w in rest]))
839
840 class LogViewer (MyWindow):
841   """
842   A log viewer window.
843
844   Its contents are a TreeView showing the log.
845
846   Attributes:
847
848     * model: an appropriate LogModel
849     * list: a TreeView widget to display the log
850   """
851
852   def __init__(me, model):
853     """
854     Create a log viewer showing the LogModel MODEL.
855     """
856     MyWindow.__init__(me)
857     me.model = model
858     scr = MyScrolledWindow()
859     me.list = MyTreeView(me.model)
860     i = 0
861     for c in me.model.cols:
862       crt = G.CellRendererText()
863       me.list.append_column(G.TreeViewColumn(c, crt, text = i))
864       i += 1
865     crt.set_property('family', 'monospace')
866     me.set_default_size(440, 256)
867     scr.add(me.list)
868     me.add(scr)
869     me.show_all()
870
871 ###--------------------------------------------------------------------------
872 ### Pinging peers.
873
874 class pingstate (struct):
875   """
876   Information kept for each peer by the Pinger.
877
878   Important attributes:
879
880     * peer = the peer name
881     * command = PING or EPING
882     * n = how many pings we've sent so far
883     * ngood = how many returned
884     * nmiss = how many didn't return
885     * nmissrun = how many pings since the last good one
886     * tlast = round-trip time for the last (good) ping
887     * ttot = total roung trip time
888   """
889   pass
890
891 class Pinger (T.Coroutine, HookClient):
892   """
893   Coroutine which pings known peers and collects statistics.
894
895   Interesting attributes:
896
897     * _map: dict mapping peer names to Peer objects
898     * _q: event queue for notifying pinger coroutine
899     * _timer: gobject timer for waking the coroutine
900   """
901
902   def __init__(me):
903     """
904     Initialize the pinger.
905
906     We watch the monitor's PeerList to track which peers we should ping.  We
907     maintain an event queue and put all the events on that.
908
909     The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
910     where CMD is 'PING' or 'EPING'.
911     """
912     T.Coroutine.__init__(me)
913     HookClient.__init__(me)
914     me._map = {}
915     me._q = T.Queue()
916     me._timer = None
917     me.hook(conn.connecthook, me._connected)
918     me.hook(conn.disconnecthook, me._disconnected)
919     me.hook(monitor.peers.addhook,
920             lambda p: T.defer(me._q.put, (p, 'ADD', None)))
921     me.hook(monitor.peers.delhook,
922             lambda p: T.defer(me._q.put, (p, 'KILL', None)))
923     if conn.connectedp(): me.connected()
924
925   def _connected(me):
926     """Respond to connection: start pinging thngs."""
927     me._timer = GO.timeout_add(1000, me._timerfunc)
928
929   def _timerfunc(me):
930     """Timer function: put a timer event on the queue."""
931     me._q.put((None, 'TIMER', None))
932     return True
933
934   def _disconnected(me, reason):
935     """Respond to disconnection: stop pinging."""
936     GO.source_remove(me._timer)
937
938   def run(me):
939     """
940     Coroutine function: read events from the queue and process them.
941
942     Interesting events:
943
944       * (PEER, 'KILL', None): remove PEER from the interesting peers list
945       * (PEER, 'ADD', None): add PEER to the list
946       * (PEER, 'INFO', TOKENS): result from a PING command
947       * (None, 'TIMER', None): interval timer went off: send more pings
948     """
949     while True:
950       tag, code, stuff = me._q.get()
951       if code == 'KILL':
952         name = tag.name
953         if name in me._map:
954           del me._map[name]
955       elif not conn.connectedp():
956         pass
957       elif code == 'ADD':
958         p = tag
959         p.ping = {}
960         for cmd in 'PING', 'EPING':
961           ps = pingstate(command = cmd, peer = p,
962                          n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
963                          tlast = 0, ttot = 0)
964           p.ping[cmd] = ps
965         me._map[p.name] = p
966       elif code == 'INFO':
967         ps = tag
968         if stuff[0] == 'ping-ok':
969           t = float(stuff[1])
970           ps.ngood += 1
971           ps.nmissrun = 0
972           ps.tlast = t
973           ps.ttot += t
974         else:
975           ps.nmiss += 1
976           ps.nmissrun += 1
977         ps.n += 1
978         ps.peer.pinghook.run(ps.peer, ps.command, ps)
979       elif code == 'TIMER':
980         for name, p in me._map.iteritems():
981           for cmd, ps in p.ping.iteritems():
982             conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
983               cmd, '-background', conn.bgtag(), '--', name]))
984
985 ###--------------------------------------------------------------------------
986 ### Random dialogue boxes.
987
988 class AddPeerDialog (MyDialog):
989   """
990   Let the user create a new peer the low-level way.
991
992   Interesting attributes:
993
994     * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
995   """
996
997   def __init__(me):
998     """Initialize the dialogue."""
999     MyDialog.__init__(me, 'Add peer',
1000                       buttons = [(G.STOCK_CANCEL, me.destroy),
1001                                  (G.STOCK_OK, me.ok)])
1002     me._setup()
1003
1004   @incr
1005   def _setup(me):
1006     """Coroutine function: background setup for AddPeerDialog."""
1007     table = GridPacker()
1008     me.vbox.pack_start(table)
1009     me.e_name = table.labelled('Name',
1010                                ValidatingEntry(r'^[^\s.:]+$', '', 16),
1011                                width = 3)
1012     me.e_addr = table.labelled('Address',
1013                                ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
1014                                newlinep = True)
1015     me.e_port = table.labelled('Port',
1016                                ValidatingEntry(numericvalidate(0, 65535),
1017                                                '4070',
1018                                                5))
1019     me.c_keepalive = G.CheckButton('Keepalives')
1020     me.l_tunnel = table.labelled('Tunnel',
1021                                  G.combo_box_new_text(),
1022                                  newlinep = True, width = 3)
1023     me.tuns = conn.tunnels()
1024     for t in me.tuns:
1025       me.l_tunnel.append_text(t)
1026     me.l_tunnel.set_active(0)
1027     table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
1028     me.c_keepalive.connect('toggled',
1029                            lambda t: me.e_keepalive.set_sensitive\
1030                                       (t.get_active()))
1031     me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
1032     me.e_keepalive.set_sensitive(False)
1033     table.pack(me.e_keepalive, width = 3)
1034     me.show_all()
1035
1036   def ok(me):
1037     """Handle an OK press: create the peer."""
1038     try:
1039       if me.c_keepalive.get_active():
1040         ka = me.e_keepalive.get_text()
1041       else:
1042         ka = None
1043       t = me.l_tunnel.get_active()
1044       if t == 0:
1045         tun = None
1046       else:
1047         tun = me.tuns[t]
1048         me._addpeer(me.e_name.get_text(),
1049                     me.e_addr.get_text(),
1050                     me.e_port.get_text(),
1051                     ka,
1052                     tun)
1053     except ValidationError:
1054       GDK.beep()
1055       return
1056
1057   @incr
1058   def _addpeer(me, name, addr, port, keepalive, tunnel):
1059     """Coroutine function: actually do the ADD command."""
1060     try:
1061       conn.add(name, addr, port, keepalive = keepalive, tunnel = tunnel)
1062       me.destroy()
1063     except T.TripeError, exc:
1064       T.defer(moanbox, ' '.join(exc))
1065
1066 class ServInfo (MyWindow):
1067   """
1068   Show information about the server and available services.
1069
1070   Interesting attributes:
1071
1072     * e: maps SERVINFO keys to entry widgets
1073     * svcs: Gtk ListStore describing services (columns are name and version)
1074   """
1075
1076   def __init__(me):
1077     MyWindow.__init__(me)
1078     me.set_title('TrIPE server info')
1079     table = GridPacker()
1080     me.add(table)
1081     me.e = {}
1082     def add(label, tag, text = None, **kw):
1083       me.e[tag] = table.info(label, text, **kw)
1084     add('Implementation', 'implementation')
1085     add('Version', 'version', newlinep = True)
1086     me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1087     me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1088     scr = MyScrolledWindow()
1089     lb = MyTreeView(me.svcs)
1090     i = 0
1091     for title in 'Service', 'Version':
1092       lb.append_column(G.TreeViewColumn(
1093         title, G.CellRendererText(), text = i))
1094       i += 1
1095     for svc in monitor.services:
1096       me.svcs.append([svc.name, svc.version])
1097     scr.add(lb)
1098     table.pack(scr, width = 2, newlinep = True,
1099                yopt = G.EXPAND | G.FILL | G.SHRINK)
1100     me.update()
1101     me.hook(conn.connecthook, me.update)
1102     me.hook(monitor.services.addhook, me.addsvc)
1103     me.hook(monitor.services.delhook, me.delsvc)
1104     me.show_all()
1105
1106   def addsvc(me, svc):
1107     me.svcs.append([svc.name, svc.version])
1108
1109   def delsvc(me, svc):
1110     for i in xrange(len(me.svcs)):
1111       if me.svcs[i][0] == svc.name:
1112         me.svcs.remove(me.svcs.get_iter(i))
1113         break
1114   @incr
1115   def update(me):
1116     info = conn.servinfo()
1117     for i in me.e:
1118       me.e[i].set_text(info[i])
1119
1120 class TraceOptions (MyDialog):
1121   """Tracing options window."""
1122   def __init__(me):
1123     MyDialog.__init__(me, title = 'Tracing options',
1124                       buttons = [(G.STOCK_CLOSE, me.destroy),
1125                                  (G.STOCK_OK, cr(me.ok))])
1126     me._setup()
1127
1128   @incr
1129   def _setup(me):
1130     me.opts = []
1131     for ch, st, desc in conn.trace():
1132       if ch.isupper(): continue
1133       text = desc[0].upper() + desc[1:]
1134       ticky = G.CheckButton(text)
1135       ticky.set_active(st == '+')
1136       me.vbox.pack_start(ticky)
1137       me.opts.append((ch, ticky))
1138     me.show_all()
1139   def ok(me):
1140     on = []
1141     off = []
1142     for ch, ticky in me.opts:
1143       if ticky.get_active():
1144         on.append(ch)
1145       else:
1146         off.append(ch)
1147     setting = ''.join(on) + '-' + ''.join(off)
1148     conn.trace(setting)
1149     me.destroy()
1150
1151 ###--------------------------------------------------------------------------
1152 ### Peer window.
1153
1154 def xlate_time(t):
1155   """Translate a TrIPE-format time to something human-readable."""
1156   if t == 'NEVER': return '(never)'
1157   YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1158   ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1159   ago = MATH.floor(ago); unit = 's'
1160   for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1161     if ago < 2*n: break
1162     ago /= n
1163     unit = u
1164   return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1165          (YY, MM, DD, hh, mm, ss, ago, unit)
1166 def xlate_bytes(b):
1167   """Translate a number of bytes into something a human might want to read."""
1168   suff = 'B'
1169   b = int(b)
1170   for s in 'KMG':
1171     if b < 4096: break
1172     b /= 1024
1173     suff = s
1174   return '%d %s' % (b, suff)
1175
1176 ## How to translate peer stats.  Maps the stat name to a translation
1177 ## function.
1178 statsxlate = \
1179   [('start-time', xlate_time),
1180    ('last-packet-time', xlate_time),
1181    ('last-keyexch-time', xlate_time),
1182    ('bytes-in', xlate_bytes),
1183    ('bytes-out', xlate_bytes),
1184    ('keyexch-bytes-in', xlate_bytes),
1185    ('keyexch-bytes-out', xlate_bytes),
1186    ('ip-bytes-in', xlate_bytes),
1187    ('ip-bytes-out', xlate_bytes)]
1188
1189 ## How to lay out the stats dialog.  Format is (LABEL, FORMAT): LABEL is
1190 ## the label to give the entry box; FORMAT is the format string to write into
1191 ## the entry.
1192 statslayout = \
1193   [('Start time', '%(start-time)s'),
1194    ('Last key-exchange', '%(last-keyexch-time)s'),
1195    ('Last packet', '%(last-packet-time)s'),
1196    ('Packets in/out',
1197     '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1198    ('Key-exchange in/out',
1199     '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1200    ('IP in/out',
1201     '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1202    ('Rejected packets', '%(rejected-packets)s')]
1203
1204 class PeerWindow (MyWindow):
1205   """
1206   Show information about a peer.
1207
1208   This gives a graphical view of the server's peer statistics.
1209
1210   Interesting attributes:
1211
1212     * e: dict mapping keys (mostly matching label widget texts, though pings
1213       use command names) to entry widgets so that we can update them easily
1214     * peer: the peer this window shows information about
1215     * cr: the info-fetching coroutine, or None if crrrently disconnected
1216     * doupate: whether the info-fetching corouting should continue running
1217   """
1218
1219   def __init__(me, peer):
1220     """Construct a PeerWindow, showing information about PEER."""
1221
1222     MyWindow.__init__(me)
1223     me.set_title('TrIPE statistics: %s' % peer.name)
1224     me.peer = peer
1225
1226     table = GridPacker()
1227     me.add(table)
1228
1229     ## Utility for adding fields.
1230     me.e = {}
1231     def add(label, text = None, key = None):
1232       if key is None: key = label
1233       me.e[key] = table.info(label, text, len = 42, newlinep = True)
1234
1235     ## Build the dialogue box.
1236     add('Peer name', peer.name)
1237     add('Tunnel', peer.tunnel)
1238     add('Interface', peer.ifname)
1239     add('Keepalives',
1240         (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1241     add('Address', peer.addr)
1242     add('Transport pings', key = 'PING')
1243     add('Encrypted pings', key = 'EPING')
1244
1245     for label, format in statslayout:
1246       add(label)
1247
1248     ## Hook onto various interesting events.
1249     me.hook(conn.connecthook, me.tryupdate)
1250     me.hook(conn.disconnecthook, me.stopupdate)
1251     me.hook(me.closehook, me.stopupdate)
1252     me.hook(me.peer.deadhook, me.dead)
1253     me.hook(me.peer.changehook, me.change)
1254     me.hook(me.peer.pinghook, me.ping)
1255     me.cr = None
1256     me.doupdate = True
1257     me.tryupdate()
1258
1259     ## Format the ping statistics.
1260     for cmd, ps in me.peer.ping.iteritems():
1261       me.ping(me.peer, cmd, ps)
1262
1263     ## And show the window.
1264     me.show_all()
1265
1266   def change(me):
1267     """Update the display in response to a notification."""
1268     me.e['Interface'].set_text(me.peer.ifname)
1269
1270   def _update(me):
1271     """
1272     Main display-updating coroutine.
1273
1274     This does an update, sleeps for a while, and starts again.  If the
1275     me.doupdate flag goes low, we stop the loop.
1276     """
1277     while me.peer.alivep and conn.connectedp() and me.doupdate:
1278       stat = conn.stats(me.peer.name)
1279       for s, trans in statsxlate:
1280         stat[s] = trans(stat[s])
1281       for label, format in statslayout:
1282         me.e[label].set_text(format % stat)
1283       GO.timeout_add(1000, lambda: me.cr.switch() and False)
1284       me.cr.parent.switch()
1285     me.cr = None
1286
1287   def tryupdate(me):
1288     """Start the updater coroutine, if it's not going already."""
1289     if me.cr is None:
1290       me.cr = T.Coroutine(me._update)
1291       me.cr.switch()
1292
1293   def stopupdate(me, *hunoz, **hukairz):
1294     """Stop the update coroutine, by setting me.doupdate."""
1295     me.doupdate = False
1296
1297   def dead(me):
1298     """Called when the peer is killed."""
1299     me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1300     me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1301     me.stopupdate()
1302
1303   def ping(me, peer, cmd, ps):
1304     """Called when a ping result for the peer is reported."""
1305     s = '%d/%d' % (ps.ngood, ps.n)
1306     if ps.n:
1307       s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1308     if ps.ngood:
1309       s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1310     me.e[ps.command].set_text(s)
1311
1312 ###--------------------------------------------------------------------------
1313 ### Cryptographic status.
1314
1315 class CryptoInfo (MyWindow):
1316   """Simple display of cryptographic algorithms in use."""
1317   def __init__(me):
1318     MyWindow.__init__(me)
1319     me.set_title('Cryptographic algorithms')
1320     T.aside(me.populate)
1321   def populate(me):
1322     table = GridPacker()
1323     me.add(table)
1324
1325     crypto = conn.algs()
1326     table.info('Diffie-Hellman group',
1327                '%s (%d-bit order, %d-bit elements)' %
1328                (crypto['kx-group'],
1329                 int(crypto['kx-group-order-bits']),
1330                 int(crypto['kx-group-elt-bits'])),
1331                len = 32)
1332     table.info('Data encryption',
1333                '%s (%d-bit key; %s)' %
1334                (crypto['cipher'],
1335                 int(crypto['cipher-keysz']) * 8,
1336                 crypto['cipher-blksz'] == '0'
1337                   and 'stream cipher'
1338                   or '%d-bit block' % (int(crypto['cipher-blksz']) * 8)),
1339                newlinep = True)
1340     table.info('Message authentication',
1341                '%s (%d-bit key; %d-bit tag)' %
1342                (crypto['mac'],
1343                 int(crypto['mac-keysz']) * 8,
1344                 int(crypto['mac-tagsz']) * 8),
1345                newlinep = True)
1346     table.info('Hash function',
1347                '%s (%d-bit output)' %
1348                (crypto['hash'],
1349                 int(crypto['hash-sz']) * 8),
1350                newlinep = True)
1351
1352     me.show_all()
1353
1354 ###--------------------------------------------------------------------------
1355 ### Main monitor window.
1356
1357 class MonitorWindow (MyWindow):
1358
1359   """
1360   The main monitor window.
1361
1362   This class creates, populates and maintains the main monitor window.
1363
1364   Lots of attributes:
1365
1366     * warnings, trace: log models for server output
1367     * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1368       WindowSlot objects for ancillary windows
1369     * ui: Gtk UIManager object for the menu system
1370     * apmenu: pair of identical autoconnecting peer menus
1371     * listmodel: Gtk ListStore for connected peers; contains peer name,
1372       address, and ping times (transport and encrypted, value and colour)
1373     * status: Gtk Statusbar at the bottom of the window
1374     * _kidding: an unpleasant backchannel between the apchange method (which
1375       builds the apmenus) and the menu handler, forced on us by a Gtk
1376       misfeature
1377
1378   Also installs attributes on Peer objects:
1379
1380     * i: index of peer's entry in listmodel
1381     * win: WindowSlot object for the peer's PeerWindow
1382   """
1383
1384   def __init__(me):
1385     """Construct the window."""
1386
1387     ## Basic stuff.
1388     MyWindow.__init__(me)
1389     me.set_title('TrIPE monitor')
1390
1391     ## Hook onto diagnostic outputs.
1392     me.warnings = WarningLogModel()
1393     me.hook(conn.warnhook, me.warnings.notify)
1394     me.trace = TraceLogModel()
1395     me.hook(conn.tracehook, me.trace.notify)
1396
1397     ## Make slots to store the various ancillary singleton windows.
1398     me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1399     me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1400     me.traceopts = WindowSlot(lambda: TraceOptions())
1401     me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1402     me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1403     me.servinfo = WindowSlot(lambda: ServInfo())
1404
1405     ## Main window structure.
1406     vbox = G.VBox()
1407     me.add(vbox)
1408
1409     ## UI manager  makes our menus.  (We're too cheap to have a toolbar.)
1410     me.ui = G.UIManager()
1411     actgroup = makeactiongroup('monitor',
1412       [('file-menu', '_File', None, None),
1413        ('connect', '_Connect', '<Control>C', conn.connect),
1414        ('disconnect', '_Disconnect', '<Control>D',
1415         lambda: conn.disconnect(None)),
1416        ('quit', '_Quit', '<Control>Q', me.close),
1417        ('server-menu', '_Server', None, None),
1418        ('daemon', 'Run in _background', None, cr(conn.daemon)),
1419        ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1420        ('crypto-algs', 'Cryptographic algorithms',
1421         '<Control>Y', me.cryptoinfo.open),
1422        ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1423        ('server-quit', 'Terminate server', None, cr(conn.quit)),
1424        ('conn-peer', 'Connect peer', None, None),
1425        ('logs-menu', '_Logs', None, None),
1426        ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1427        ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1428        ('trace-options', 'Trace _options...', None, me.traceopts.open),
1429        ('help-menu', '_Help', None, None),
1430        ('about', '_About tripemon...', None, aboutbox.open),
1431        ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1432        ('kill-peer', '_Kill peer', None, me.killpeer),
1433        ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1434
1435     ## Menu structures.
1436     uidef = '''
1437       <ui>
1438         <menubar>
1439           <menu action="file-menu">
1440             <menuitem action="quit"/>
1441           </menu>
1442           <menu action="server-menu">
1443             <menuitem action="connect"/>
1444             <menuitem action="disconnect"/>
1445             <separator/>
1446             <menuitem action="server-version"/>
1447             <menuitem action="crypto-algs"/>
1448             <menuitem action="add-peer"/>
1449             <menuitem action="conn-peer"/>
1450             <menuitem action="daemon"/>
1451             <menuitem action="reload-keys"/>
1452             <separator/>
1453             <menuitem action="server-quit"/>
1454           </menu>
1455           <menu action="logs-menu">
1456             <menuitem action="show-warnings"/>
1457             <menuitem action="show-trace"/>
1458             <menuitem action="trace-options"/>
1459           </menu>
1460           <menu action="help-menu">
1461             <menuitem action="about"/>
1462           </menu>
1463         </menubar>
1464         <popup name="peer-popup">
1465           <menuitem action="add-peer"/>
1466           <menuitem action="conn-peer"/>
1467           <menuitem action="kill-peer"/>
1468           <menuitem action="force-kx"/>
1469         </popup>
1470       </ui>
1471       '''
1472
1473     ## Populate the UI manager.
1474     me.ui.insert_action_group(actgroup, 0)
1475     me.ui.add_ui_from_string(uidef)
1476
1477     ## Construct the menu bar.
1478     vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
1479     me.add_accel_group(me.ui.get_accel_group())
1480
1481     ## Construct and attach the auto-peers menu.  (This is a horrible bodge
1482     ## because we can't attach the same submenu in two different places.)
1483     me.apmenu = G.Menu(), G.Menu()
1484     me.ui.get_widget('/menubar/server-menu/conn-peer') \
1485       .set_submenu(me.apmenu[0])
1486     me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1487
1488     ## Construct the main list model, and listen on hooks which report
1489     ## changes to the available peers.
1490     me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1491     me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1492     me.hook(monitor.peers.addhook, me.addpeer)
1493     me.hook(monitor.peers.delhook, me.delpeer)
1494     me.hook(monitor.autopeershook, me.apchange)
1495
1496     ## Construct the list viewer and put it in a scrolling window.
1497     scr = MyScrolledWindow()
1498     me.list = MyTreeView(me.listmodel)
1499     me.list.append_column(G.TreeViewColumn('Peer name',
1500                                            G.CellRendererText(),
1501                                            text = 0))
1502     me.list.append_column(G.TreeViewColumn('Address',
1503                                            G.CellRendererText(),
1504                                            text = 1))
1505     me.list.append_column(G.TreeViewColumn('T-ping',
1506                                            G.CellRendererText(),
1507                                            text = 2,
1508                                            foreground = 3))
1509     me.list.append_column(G.TreeViewColumn('E-ping',
1510                                            G.CellRendererText(),
1511                                            text = 4,
1512                                            foreground = 5))
1513     me.list.get_column(1).set_expand(True)
1514     me.list.connect('row-activated', me.activate)
1515     me.list.connect('button-press-event', me.buttonpress)
1516     me.list.set_reorderable(True)
1517     me.list.get_selection().set_mode(G.SELECTION_NONE)
1518     scr.add(me.list)
1519     vbox.pack_start(scr)
1520
1521     ## Construct the status bar, and listen on hooks which report changes to
1522     ## connection status.
1523     me.status = G.Statusbar()
1524     vbox.pack_start(me.status, expand = False)
1525     me.hook(conn.connecthook, cr(me.connected))
1526     me.hook(conn.disconnecthook, me.disconnected)
1527     me.hook(conn.notehook, me.notify)
1528
1529     ## Set a plausible default window size.
1530     me.set_default_size(512, 180)
1531
1532   def addpeer(me, peer):
1533     """Hook: announces that PEER has been added."""
1534     peer.i = me.listmodel.append([peer.name, peer.addr,
1535                                   '???', 'green', '???', 'green'])
1536     peer.win = WindowSlot(lambda: PeerWindow(peer))
1537     me.hook(peer.pinghook, me._ping)
1538     me.apchange()
1539
1540   def delpeer(me, peer):
1541     """Hook: announces that PEER has been removed."""
1542     me.listmodel.remove(peer.i)
1543     me.unhook(peer.pinghook)
1544     me.apchange()
1545
1546   def path_peer(me, path):
1547     """Return the peer corresponding to a given list-model PATH."""
1548     return monitor.peers[me.listmodel[path][0]]
1549
1550   def apchange(me):
1551     """
1552     Hook: announces that a change has been made to the peers available for
1553     automated connection.
1554
1555     This populates both auto-peer menus and keeps them in sync.  (As
1556     mentioned above, we can't attach the same submenu to two separate parent
1557     menu items.  So we end up with two identical menus instead.  Yes, this
1558     does suck.)
1559     """
1560
1561     ## The set_active method of a CheckMenuItem works by maybe activating the
1562     ## menu item.  This signals our handler.  But we don't actually want to
1563     ## signal the handler unless the user actually frobbed the item.  So the
1564     ## _kidding flag is used as an underhanded way of telling the handler
1565     ## that we don't actually want it to do anything.  Of course, this sucks
1566     ## mightily.
1567     me._kidding = True
1568
1569     ## Iterate over the two menus.
1570     for m in 0, 1:
1571       menu = me.apmenu[m]
1572       existing = menu.get_children()
1573       if monitor.autopeers is None:
1574
1575         ## No peers, so empty out the menu.
1576         for item in existing:
1577           menu.remove(item)
1578
1579       else:
1580
1581         ## Insert the new items into the menu.  (XXX this seems buggy XXX)
1582         ## Tick the peers which are actually connected.
1583         i = j = 0
1584         for peer in monitor.autopeers:
1585           if j < len(existing) and \
1586              existing[j].get_child().get_text() == peer:
1587             item = existing[j]
1588             j += 1
1589           else:
1590             item = G.CheckMenuItem(peer, use_underline = False)
1591             item.connect('activate', invoker(me._addautopeer, peer))
1592             menu.insert(item, i)
1593           item.set_active(peer in monitor.peers.table)
1594           i += 1
1595
1596       ## Make all the menu items visible.
1597       menu.show_all()
1598
1599     ## Set the parent menu items sensitive if and only if there are any peers
1600     ## to connect.
1601     for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1602       me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1603
1604     ## And now allow the handler to do its business normally.
1605     me._kidding = False
1606
1607   def _addautopeer(me, peer):
1608     """
1609     Automatically connect an auto-peer.
1610
1611     This method is invoked from the main coroutine.  Since the actual
1612     connection needs to issue administration commands, we must spawn a new
1613     child coroutine for it.
1614     """
1615     if me._kidding:
1616       return
1617     T.Coroutine(me._addautopeer_hack).switch(peer)
1618
1619   def _addautopeer_hack(me, peer):
1620     """Make an automated connection to PEER in response to a user click."""
1621     if me._kidding:
1622       return
1623     try:
1624       T._simple(conn.svcsubmit('connect', 'active', peer))
1625     except T.TripeError, exc:
1626       T.defer(moanbox, ' '.join(exc.args))
1627     me.apchange()
1628
1629   def activate(me, l, path, col):
1630     """
1631     Handle a double-click on a peer in the main list: open a PeerInfo window.
1632     """
1633     peer = me.path_peer(path)
1634     peer.win.open()
1635
1636   def buttonpress(me, l, ev):
1637     """
1638     Handle a mouse click on the main list.
1639
1640     Currently we're only interested in button-3, which pops up the peer menu.
1641     For future reference, we stash the peer that was clicked in me.menupeer.
1642     """
1643     if ev.button == 3:
1644       x, y = int(ev.x), int(ev.y)
1645       r = me.list.get_path_at_pos(x, y)
1646       for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1647         me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1648                                           r is not None)
1649       me.ui.get_widget('/peer-popup/conn-peer'). \
1650         set_sensitive(bool(monitor.autopeers))
1651       if r:
1652         me.menupeer = me.path_peer(r[0])
1653       else:
1654         me.menupeer = None
1655       me.ui.get_widget('/peer-popup').popup(
1656         None, None, None, ev.button, ev.time)
1657
1658   def killpeer(me):
1659     """Kill a peer from the popup menu."""
1660     cr(conn.kill, me.menupeer.name)()
1661
1662   def forcekx(me):
1663     """Kickstart a key-exchange from the popup menu."""
1664     cr(conn.forcekx, me.menupeer.name)()
1665
1666   _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1667   def _ping(me, p, cmd, ps):
1668     """Hook: responds to ping reports."""
1669     textcol, colourcol = me._columnmap[cmd]
1670     if ps.nmissrun:
1671       me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1672       me.listmodel[p.i][colourcol] = 'red'
1673     else:
1674       me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1675       me.listmodel[p.i][colourcol] = 'black'
1676
1677   def setstatus(me, status):
1678     """Update the message in the status bar."""
1679     me.status.pop(0)
1680     me.status.push(0, status)
1681
1682   def notify(me, note, *rest):
1683     """Hook: invoked when interesting notifications occur."""
1684     if note == 'DAEMON':
1685       me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1686
1687   def connected(me):
1688     """
1689     Hook: invoked when a connection is made to the server.
1690
1691     Make options which require a server connection sensitive.
1692     """
1693     me.setstatus('Connected (port %s)' % conn.port())
1694     me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1695     for i in ('/menubar/server-menu/disconnect',
1696               '/menubar/server-menu/server-version',
1697               '/menubar/server-menu/add-peer',
1698               '/menubar/server-menu/server-quit',
1699               '/menubar/logs-menu/trace-options'):
1700       me.ui.get_widget(i).set_sensitive(True)
1701     me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1702       set_sensitive(bool(monitor.autopeers))
1703     me.ui.get_widget('/menubar/server-menu/daemon'). \
1704       set_sensitive(conn.servinfo()['daemon'] == 'nil')
1705
1706   def disconnected(me, reason):
1707     """
1708     Hook: invoked when the connection to the server is lost.
1709
1710     Make most options insensitive.
1711     """
1712     me.setstatus('Disconnected')
1713     me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1714     for i in ('/menubar/server-menu/disconnect',
1715               '/menubar/server-menu/server-version',
1716               '/menubar/server-menu/add-peer',
1717               '/menubar/server-menu/conn-peer',
1718               '/menubar/server-menu/daemon',
1719               '/menubar/server-menu/server-quit',
1720               '/menubar/logs-menu/trace-options'):
1721       me.ui.get_widget(i).set_sensitive(False)
1722     if reason: moanbox(reason)
1723
1724 ###--------------------------------------------------------------------------
1725 ### Main program.
1726
1727 def parse_options():
1728   """
1729   Parse command-line options.
1730
1731   Process the boring ones.  Return all of them, for later.
1732   """
1733   op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1734                     version = '%prog (tripe version 1.0.0)')
1735   op.add_option('-a', '--admin-socket',
1736                 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1737                 help = 'Select socket to connect to [default %default]')
1738   op.add_option('-d', '--directory',
1739                 metavar = 'DIR', dest = 'dir', default = T.configdir,
1740                 help = 'Select current diretory [default %default]')
1741   opts, args = op.parse_args()
1742   if args: op.error('no arguments permitted')
1743   OS.chdir(opts.dir)
1744   return opts
1745
1746 def init(opts):
1747   """Initialization."""
1748
1749   global conn, monitor, pinger
1750
1751   ## Try to establish a connection.
1752   conn = Connection(opts.tripesock)
1753
1754   ## Make the main interesting coroutines and objects.
1755   monitor = Monitor()
1756   pinger = Pinger()
1757   pinger.switch()
1758
1759 def main():
1760
1761   ## Main window.
1762   root = MonitorWindow()
1763   conn.connect()
1764   root.show_all()
1765
1766   ## Main loop.
1767   HookClient().hook(root.closehook, exit)
1768   conn.mainloop()
1769
1770 if __name__ == '__main__':
1771   opts = parse_options()
1772   init(opts)
1773   main()
1774
1775 ###----- That's all, folks --------------------------------------------------