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