chiark / gitweb /
mon/tripemon.in: Highlight entry background when contents are invalid.
[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(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-active')]
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('#ff6666')
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     me.connect("state-changed", me._check)
717     if callable(valid):
718       me.validate = valid
719     else:
720       me.validate = RX.compile(valid).match
721     me.ensure_style()
722     if size != -1: me.set_width_chars(size)
723     me.set_activates_default(True)
724     me.set_text(text)
725     me._check()
726
727   def _check(me, *hunoz):
728     """Check the current text and update validp and the text colour."""
729     if me.validate(G.Entry.get_text(me)):
730       me.validp = True
731       me.modify_base(G.STATE_NORMAL, None)
732     else:
733       me.validp = False
734       me.modify_base(G.STATE_NORMAL, me.is_sensitive() and c_red or None)
735
736   def get_text(me):
737     """
738     Return the text in the Entry if it's valid.  If it isn't, raise
739     ValidationError.
740     """
741     if not me.validp:
742       raise ValidationError
743     return G.Entry.get_text(me)
744
745 def numericvalidate(min = None, max = None):
746   """
747   Return a validation function for numbers.
748
749   Entry must consist of an optional sign followed by digits, and the
750   resulting integer must be within the given bounds.
751   """
752   return lambda x: (rx_num.match(x) and
753                     (min is None or long(x) >= min) and
754                     (max is None or long(x) <= max))
755
756 ###--------------------------------------------------------------------------
757 ### Various minor dialog boxen.
758
759 GPL = """This program is free software; you can redistribute it and/or modify
760 it under the terms of the GNU General Public License as published by
761 the Free Software Foundation; either version 2 of the License, or
762 (at your option) any later version.
763
764 This program is distributed in the hope that it will be useful,
765 but WITHOUT ANY WARRANTY; without even the implied warranty of
766 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
767 GNU General Public License for more details.
768
769 You should have received a copy of the GNU General Public License
770 along with this program; if not, write to the Free Software Foundation,
771 Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
772
773 class AboutBox (G.AboutDialog, MyWindowMixin):
774   """The program `About' box."""
775   def __init__(me):
776     G.AboutDialog.__init__(me)
777     me.mywininit()
778     me.set_name('TrIPEmon')
779     me.set_version(T.VERSION)
780     me.set_license(GPL)
781     me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
782     me.set_comments('A graphical monitor for the TrIPE VPN server')
783     me.set_copyright('Copyright Â© 2006-2008 Straylight/Edgeware')
784     me.connect('response', me.respond)
785     me.show()
786   def respond(me, hunoz, rid, *hukairz):
787     if rid == G.RESPONSE_CANCEL:
788       me.close()
789 aboutbox = WindowSlot(AboutBox)
790
791 def moanbox(msg):
792   """Report an error message in a window."""
793   d = G.Dialog('Error from %s' % M.quis,
794                flags = G.DIALOG_MODAL,
795                buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
796   label = G.Label(msg)
797   label.set_padding(20, 20)
798   d.vbox.pack_start(label)
799   label.show()
800   d.run()
801   d.destroy()
802
803 def unimplemented(*hunoz):
804   """Indicator of laziness."""
805   moanbox("I've not written that bit yet.")
806
807 ###--------------------------------------------------------------------------
808 ### Logging windows.
809
810 class LogModel (G.ListStore):
811   """
812   A simple list of log messages, usable as the model for a TreeView.
813
814   The column headings are stored in the `cols' attribute.
815   """
816
817   def __init__(me, columns):
818     """
819     COLUMNS must be a list of column name strings.  We add a time column to
820     the left.
821     """
822     me.cols = ('Time',) + columns
823     G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
824
825   def add(me, *entries):
826     """
827     Adds a new log message, with a timestamp.
828
829     The ENTRIES are the contents for the list columns.
830     """
831     now = TIME.strftime('%Y-%m-%d %H:%M:%S')
832     me.append((now, ) + entries)
833
834 class TraceLogModel (LogModel):
835   """Log model for trace messages."""
836   def __init__(me):
837     LogModel.__init__(me, ('Message',))
838   def notify(me, line):
839     """Call with a new trace message."""
840     me.add(line)
841
842 class WarningLogModel (LogModel):
843   """
844   Log model for warnings.
845
846   We split the category out into a separate column.
847   """
848   def __init__(me):
849     LogModel.__init__(me, ('Category', 'Message'))
850   def notify(me, tag, *rest):
851     """Call with a new warning message."""
852     me.add(tag, ' '.join([T.quotify(w) for w in rest]))
853
854 class LogViewer (MyWindow):
855   """
856   A log viewer window.
857
858   Its contents are a TreeView showing the log.
859
860   Attributes:
861
862     * model: an appropriate LogModel
863     * list: a TreeView widget to display the log
864   """
865
866   def __init__(me, model):
867     """
868     Create a log viewer showing the LogModel MODEL.
869     """
870     MyWindow.__init__(me)
871     me.model = model
872     scr = MyScrolledWindow()
873     me.list = MyTreeView(me.model)
874     i = 0
875     for c in me.model.cols:
876       crt = G.CellRendererText()
877       me.list.append_column(G.TreeViewColumn(c, crt, text = i))
878       i += 1
879     crt.set_property('family', 'monospace')
880     me.set_default_size(440, 256)
881     scr.add(me.list)
882     me.add(scr)
883     me.show_all()
884
885 ###--------------------------------------------------------------------------
886 ### Pinging peers.
887
888 class pingstate (struct):
889   """
890   Information kept for each peer by the Pinger.
891
892   Important attributes:
893
894     * peer = the peer name
895     * command = PING or EPING
896     * n = how many pings we've sent so far
897     * ngood = how many returned
898     * nmiss = how many didn't return
899     * nmissrun = how many pings since the last good one
900     * tlast = round-trip time for the last (good) ping
901     * ttot = total roung trip time
902   """
903   pass
904
905 class Pinger (T.Coroutine, HookClient):
906   """
907   Coroutine which pings known peers and collects statistics.
908
909   Interesting attributes:
910
911     * _map: dict mapping peer names to Peer objects
912     * _q: event queue for notifying pinger coroutine
913     * _timer: gobject timer for waking the coroutine
914   """
915
916   def __init__(me):
917     """
918     Initialize the pinger.
919
920     We watch the monitor's PeerList to track which peers we should ping.  We
921     maintain an event queue and put all the events on that.
922
923     The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
924     where CMD is 'PING' or 'EPING'.
925     """
926     T.Coroutine.__init__(me)
927     HookClient.__init__(me)
928     me._map = {}
929     me._q = T.Queue()
930     me._timer = None
931     me.hook(conn.connecthook, me._connected)
932     me.hook(conn.disconnecthook, me._disconnected)
933     me.hook(monitor.peers.addhook,
934             lambda p: T.defer(me._q.put, (p, 'ADD', None)))
935     me.hook(monitor.peers.delhook,
936             lambda p: T.defer(me._q.put, (p, 'KILL', None)))
937     if conn.connectedp(): me.connected()
938
939   def _connected(me):
940     """Respond to connection: start pinging thngs."""
941     me._timer = GO.timeout_add(1000, me._timerfunc)
942
943   def _timerfunc(me):
944     """Timer function: put a timer event on the queue."""
945     me._q.put((None, 'TIMER', None))
946     return True
947
948   def _disconnected(me, reason):
949     """Respond to disconnection: stop pinging."""
950     GO.source_remove(me._timer)
951
952   def run(me):
953     """
954     Coroutine function: read events from the queue and process them.
955
956     Interesting events:
957
958       * (PEER, 'KILL', None): remove PEER from the interesting peers list
959       * (PEER, 'ADD', None): add PEER to the list
960       * (PEER, 'INFO', TOKENS): result from a PING command
961       * (None, 'TIMER', None): interval timer went off: send more pings
962     """
963     while True:
964       tag, code, stuff = me._q.get()
965       if code == 'KILL':
966         name = tag.name
967         if name in me._map:
968           del me._map[name]
969       elif not conn.connectedp():
970         pass
971       elif code == 'ADD':
972         p = tag
973         p.ping = {}
974         for cmd in 'PING', 'EPING':
975           ps = pingstate(command = cmd, peer = p,
976                          n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
977                          tlast = 0, ttot = 0)
978           p.ping[cmd] = ps
979         me._map[p.name] = p
980       elif code == 'INFO':
981         ps = tag
982         if stuff[0] == 'ping-ok':
983           t = float(stuff[1])
984           ps.ngood += 1
985           ps.nmissrun = 0
986           ps.tlast = t
987           ps.ttot += t
988         else:
989           ps.nmiss += 1
990           ps.nmissrun += 1
991         ps.n += 1
992         ps.peer.pinghook.run(ps.peer, ps.command, ps)
993       elif code == 'TIMER':
994         for name, p in me._map.iteritems():
995           for cmd, ps in p.ping.iteritems():
996             conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
997               cmd, '-background', conn.bgtag(), '--', name]))
998
999 ###--------------------------------------------------------------------------
1000 ### Random dialogue boxes.
1001
1002 class AddPeerDialog (MyDialog):
1003   """
1004   Let the user create a new peer the low-level way.
1005
1006   Interesting attributes:
1007
1008     * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
1009   """
1010
1011   def __init__(me):
1012     """Initialize the dialogue."""
1013     MyDialog.__init__(me, 'Add peer',
1014                       buttons = [(G.STOCK_CANCEL, me.destroy),
1015                                  (G.STOCK_OK, me.ok)])
1016     me._setup()
1017
1018   @incr
1019   def _setup(me):
1020     """Coroutine function: background setup for AddPeerDialog."""
1021     table = GridPacker()
1022     me.vbox.pack_start(table)
1023     me.e_name = table.labelled('Name',
1024                                ValidatingEntry(r'^[^\s.:]+$', '', 16),
1025                                width = 3)
1026     me.e_addr = table.labelled('Address',
1027                                ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
1028                                newlinep = True)
1029     me.e_port = table.labelled('Port',
1030                                ValidatingEntry(numericvalidate(0, 65535),
1031                                                '4070',
1032                                                5))
1033     me.c_keepalive = G.CheckButton('Keepalives')
1034     me.l_tunnel = table.labelled('Tunnel',
1035                                  G.combo_box_new_text(),
1036                                  newlinep = True, width = 3)
1037     me.tuns = conn.tunnels()
1038     for t in me.tuns:
1039       me.l_tunnel.append_text(t)
1040     me.l_tunnel.set_active(0)
1041     table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
1042     me.c_keepalive.connect('toggled',
1043                            lambda t: me.e_keepalive.set_sensitive\
1044                                       (t.get_active()))
1045     me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
1046     me.e_keepalive.set_sensitive(False)
1047     table.pack(me.e_keepalive, width = 3)
1048     me.show_all()
1049
1050   def ok(me):
1051     """Handle an OK press: create the peer."""
1052     try:
1053       if me.c_keepalive.get_active():
1054         ka = me.e_keepalive.get_text()
1055       else:
1056         ka = None
1057       t = me.l_tunnel.get_active()
1058       if t == 0:
1059         tun = None
1060       else:
1061         tun = me.tuns[t]
1062         me._addpeer(me.e_name.get_text(),
1063                     me.e_addr.get_text(),
1064                     me.e_port.get_text(),
1065                     ka,
1066                     tun)
1067     except ValidationError:
1068       GDK.beep()
1069       return
1070
1071   @incr
1072   def _addpeer(me, name, addr, port, keepalive, tunnel):
1073     """Coroutine function: actually do the ADD command."""
1074     try:
1075       conn.add(name, addr, port, keepalive = keepalive, tunnel = tunnel)
1076       me.destroy()
1077     except T.TripeError, exc:
1078       T.defer(moanbox, ' '.join(exc))
1079
1080 class ServInfo (MyWindow):
1081   """
1082   Show information about the server and available services.
1083
1084   Interesting attributes:
1085
1086     * e: maps SERVINFO keys to entry widgets
1087     * svcs: Gtk ListStore describing services (columns are name and version)
1088   """
1089
1090   def __init__(me):
1091     MyWindow.__init__(me)
1092     me.set_title('TrIPE server info')
1093     table = GridPacker()
1094     me.add(table)
1095     me.e = {}
1096     def add(label, tag, text = None, **kw):
1097       me.e[tag] = table.info(label, text, **kw)
1098     add('Implementation', 'implementation')
1099     add('Version', 'version', newlinep = True)
1100     me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1101     me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1102     scr = MyScrolledWindow()
1103     lb = MyTreeView(me.svcs)
1104     i = 0
1105     for title in 'Service', 'Version':
1106       lb.append_column(G.TreeViewColumn(
1107         title, G.CellRendererText(), text = i))
1108       i += 1
1109     for svc in monitor.services:
1110       me.svcs.append([svc.name, svc.version])
1111     scr.add(lb)
1112     table.pack(scr, width = 2, newlinep = True,
1113                yopt = G.EXPAND | G.FILL | G.SHRINK)
1114     me.update()
1115     me.hook(conn.connecthook, me.update)
1116     me.hook(monitor.services.addhook, me.addsvc)
1117     me.hook(monitor.services.delhook, me.delsvc)
1118     me.show_all()
1119
1120   def addsvc(me, svc):
1121     me.svcs.append([svc.name, svc.version])
1122
1123   def delsvc(me, svc):
1124     for i in xrange(len(me.svcs)):
1125       if me.svcs[i][0] == svc.name:
1126         me.svcs.remove(me.svcs.get_iter(i))
1127         break
1128   @incr
1129   def update(me):
1130     info = conn.servinfo()
1131     for i in me.e:
1132       me.e[i].set_text(info[i])
1133
1134 class TraceOptions (MyDialog):
1135   """Tracing options window."""
1136   def __init__(me):
1137     MyDialog.__init__(me, title = 'Tracing options',
1138                       buttons = [(G.STOCK_CLOSE, me.destroy),
1139                                  (G.STOCK_OK, cr(me.ok))])
1140     me._setup()
1141
1142   @incr
1143   def _setup(me):
1144     me.opts = []
1145     for ch, st, desc in conn.trace():
1146       if ch.isupper(): continue
1147       text = desc[0].upper() + desc[1:]
1148       ticky = G.CheckButton(text)
1149       ticky.set_active(st == '+')
1150       me.vbox.pack_start(ticky)
1151       me.opts.append((ch, ticky))
1152     me.show_all()
1153   def ok(me):
1154     on = []
1155     off = []
1156     for ch, ticky in me.opts:
1157       if ticky.get_active():
1158         on.append(ch)
1159       else:
1160         off.append(ch)
1161     setting = ''.join(on) + '-' + ''.join(off)
1162     conn.trace(setting)
1163     me.destroy()
1164
1165 ###--------------------------------------------------------------------------
1166 ### Peer window.
1167
1168 def xlate_time(t):
1169   """Translate a TrIPE-format time to something human-readable."""
1170   if t == 'NEVER': return '(never)'
1171   YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1172   ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1173   ago = MATH.floor(ago); unit = 's'
1174   for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1175     if ago < 2*n: break
1176     ago /= n
1177     unit = u
1178   return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1179          (YY, MM, DD, hh, mm, ss, ago, unit)
1180 def xlate_bytes(b):
1181   """Translate a number of bytes into something a human might want to read."""
1182   suff = 'B'
1183   b = int(b)
1184   for s in 'KMG':
1185     if b < 4096: break
1186     b /= 1024
1187     suff = s
1188   return '%d %s' % (b, suff)
1189
1190 ## How to translate peer stats.  Maps the stat name to a translation
1191 ## function.
1192 statsxlate = \
1193   [('start-time', xlate_time),
1194    ('last-packet-time', xlate_time),
1195    ('last-keyexch-time', xlate_time),
1196    ('bytes-in', xlate_bytes),
1197    ('bytes-out', xlate_bytes),
1198    ('keyexch-bytes-in', xlate_bytes),
1199    ('keyexch-bytes-out', xlate_bytes),
1200    ('ip-bytes-in', xlate_bytes),
1201    ('ip-bytes-out', xlate_bytes)]
1202
1203 ## How to lay out the stats dialog.  Format is (LABEL, FORMAT): LABEL is
1204 ## the label to give the entry box; FORMAT is the format string to write into
1205 ## the entry.
1206 statslayout = \
1207   [('Start time', '%(start-time)s'),
1208    ('Last key-exchange', '%(last-keyexch-time)s'),
1209    ('Last packet', '%(last-packet-time)s'),
1210    ('Packets in/out',
1211     '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1212    ('Key-exchange in/out',
1213     '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1214    ('IP in/out',
1215     '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1216    ('Rejected packets', '%(rejected-packets)s')]
1217
1218 class PeerWindow (MyWindow):
1219   """
1220   Show information about a peer.
1221
1222   This gives a graphical view of the server's peer statistics.
1223
1224   Interesting attributes:
1225
1226     * e: dict mapping keys (mostly matching label widget texts, though pings
1227       use command names) to entry widgets so that we can update them easily
1228     * peer: the peer this window shows information about
1229     * cr: the info-fetching coroutine, or None if crrrently disconnected
1230     * doupate: whether the info-fetching corouting should continue running
1231   """
1232
1233   def __init__(me, peer):
1234     """Construct a PeerWindow, showing information about PEER."""
1235
1236     MyWindow.__init__(me)
1237     me.set_title('TrIPE statistics: %s' % peer.name)
1238     me.peer = peer
1239
1240     table = GridPacker()
1241     me.add(table)
1242
1243     ## Utility for adding fields.
1244     me.e = {}
1245     def add(label, text = None, key = None):
1246       if key is None: key = label
1247       me.e[key] = table.info(label, text, len = 42, newlinep = True)
1248
1249     ## Build the dialogue box.
1250     add('Peer name', peer.name)
1251     add('Tunnel', peer.tunnel)
1252     add('Interface', peer.ifname)
1253     add('Keepalives',
1254         (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1255     add('Address', peer.addr)
1256     add('Transport pings', key = 'PING')
1257     add('Encrypted pings', key = 'EPING')
1258
1259     for label, format in statslayout:
1260       add(label)
1261
1262     ## Hook onto various interesting events.
1263     me.hook(conn.connecthook, me.tryupdate)
1264     me.hook(conn.disconnecthook, me.stopupdate)
1265     me.hook(me.closehook, me.stopupdate)
1266     me.hook(me.peer.deadhook, me.dead)
1267     me.hook(me.peer.changehook, me.change)
1268     me.hook(me.peer.pinghook, me.ping)
1269     me.cr = None
1270     me.doupdate = True
1271     me.tryupdate()
1272
1273     ## Format the ping statistics.
1274     for cmd, ps in me.peer.ping.iteritems():
1275       me.ping(me.peer, cmd, ps)
1276
1277     ## And show the window.
1278     me.show_all()
1279
1280   def change(me):
1281     """Update the display in response to a notification."""
1282     me.e['Interface'].set_text(me.peer.ifname)
1283
1284   def _update(me):
1285     """
1286     Main display-updating coroutine.
1287
1288     This does an update, sleeps for a while, and starts again.  If the
1289     me.doupdate flag goes low, we stop the loop.
1290     """
1291     while me.peer.alivep and conn.connectedp() and me.doupdate:
1292       stat = conn.stats(me.peer.name)
1293       for s, trans in statsxlate:
1294         stat[s] = trans(stat[s])
1295       for label, format in statslayout:
1296         me.e[label].set_text(format % stat)
1297       GO.timeout_add(1000, lambda: me.cr.switch() and False)
1298       me.cr.parent.switch()
1299     me.cr = None
1300
1301   def tryupdate(me):
1302     """Start the updater coroutine, if it's not going already."""
1303     if me.cr is None:
1304       me.cr = T.Coroutine(me._update,
1305                           name = 'update-peer-window %s' % me.peer.name)
1306       me.cr.switch()
1307
1308   def stopupdate(me, *hunoz, **hukairz):
1309     """Stop the update coroutine, by setting me.doupdate."""
1310     me.doupdate = False
1311
1312   def dead(me):
1313     """Called when the peer is killed."""
1314     me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1315     me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1316     me.stopupdate()
1317
1318   def ping(me, peer, cmd, ps):
1319     """Called when a ping result for the peer is reported."""
1320     s = '%d/%d' % (ps.ngood, ps.n)
1321     if ps.n:
1322       s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1323     if ps.ngood:
1324       s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1325     me.e[ps.command].set_text(s)
1326
1327 ###--------------------------------------------------------------------------
1328 ### Cryptographic status.
1329
1330 class CryptoInfo (MyWindow):
1331   """Simple display of cryptographic algorithms in use."""
1332   def __init__(me):
1333     MyWindow.__init__(me)
1334     me.set_title('Cryptographic algorithms')
1335     T.aside(me.populate)
1336   def populate(me):
1337     table = GridPacker()
1338     me.add(table)
1339
1340     crypto = conn.algs()
1341     table.info('Diffie-Hellman group',
1342                '%s (%d-bit order, %d-bit elements)' %
1343                (crypto['kx-group'],
1344                 int(crypto['kx-group-order-bits']),
1345                 int(crypto['kx-group-elt-bits'])),
1346                len = 32)
1347     table.info('Data encryption',
1348                '%s (%d-bit key; %s)' %
1349                (crypto['cipher'],
1350                 int(crypto['cipher-keysz']) * 8,
1351                 crypto['cipher-blksz'] == '0'
1352                   and 'stream cipher'
1353                   or '%d-bit block' % (int(crypto['cipher-blksz']) * 8)),
1354                newlinep = True)
1355     table.info('Message authentication',
1356                '%s (%d-bit key; %d-bit tag)' %
1357                (crypto['mac'],
1358                 int(crypto['mac-keysz']) * 8,
1359                 int(crypto['mac-tagsz']) * 8),
1360                newlinep = True)
1361     table.info('Hash function',
1362                '%s (%d-bit output)' %
1363                (crypto['hash'],
1364                 int(crypto['hash-sz']) * 8),
1365                newlinep = True)
1366
1367     me.show_all()
1368
1369 ###--------------------------------------------------------------------------
1370 ### Main monitor window.
1371
1372 class MonitorWindow (MyWindow):
1373
1374   """
1375   The main monitor window.
1376
1377   This class creates, populates and maintains the main monitor window.
1378
1379   Lots of attributes:
1380
1381     * warnings, trace: log models for server output
1382     * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1383       WindowSlot objects for ancillary windows
1384     * ui: Gtk UIManager object for the menu system
1385     * apmenu: pair of identical autoconnecting peer menus
1386     * listmodel: Gtk ListStore for connected peers; contains peer name,
1387       address, and ping times (transport and encrypted, value and colour)
1388     * status: Gtk Statusbar at the bottom of the window
1389     * _kidding: an unpleasant backchannel between the apchange method (which
1390       builds the apmenus) and the menu handler, forced on us by a Gtk
1391       misfeature
1392
1393   Also installs attributes on Peer objects:
1394
1395     * i: index of peer's entry in listmodel
1396     * win: WindowSlot object for the peer's PeerWindow
1397   """
1398
1399   def __init__(me):
1400     """Construct the window."""
1401
1402     ## Basic stuff.
1403     MyWindow.__init__(me)
1404     me.set_title('TrIPE monitor')
1405
1406     ## Hook onto diagnostic outputs.
1407     me.warnings = WarningLogModel()
1408     me.hook(conn.warnhook, me.warnings.notify)
1409     me.trace = TraceLogModel()
1410     me.hook(conn.tracehook, me.trace.notify)
1411
1412     ## Make slots to store the various ancillary singleton windows.
1413     me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1414     me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1415     me.traceopts = WindowSlot(lambda: TraceOptions())
1416     me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1417     me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1418     me.servinfo = WindowSlot(lambda: ServInfo())
1419
1420     ## Main window structure.
1421     vbox = G.VBox()
1422     me.add(vbox)
1423
1424     ## UI manager  makes our menus.  (We're too cheap to have a toolbar.)
1425     me.ui = G.UIManager()
1426     actgroup = makeactiongroup('monitor',
1427       [('file-menu', '_File', None, None),
1428        ('connect', '_Connect', '<Control>C', conn.connect),
1429        ('disconnect', '_Disconnect', '<Control>D',
1430         lambda: conn.disconnect(None)),
1431        ('quit', '_Quit', '<Control>Q', me.close),
1432        ('server-menu', '_Server', None, None),
1433        ('daemon', 'Run in _background', None, cr(conn.daemon)),
1434        ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1435        ('crypto-algs', 'Cryptographic algorithms',
1436         '<Control>Y', me.cryptoinfo.open),
1437        ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1438        ('server-quit', 'Terminate server', None, cr(conn.quit)),
1439        ('conn-peer', 'Connect peer', None, None),
1440        ('logs-menu', '_Logs', None, None),
1441        ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1442        ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1443        ('trace-options', 'Trace _options...', None, me.traceopts.open),
1444        ('help-menu', '_Help', None, None),
1445        ('about', '_About tripemon...', None, aboutbox.open),
1446        ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1447        ('kill-peer', '_Kill peer', None, me.killpeer),
1448        ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1449
1450     ## Menu structures.
1451     uidef = '''
1452       <ui>
1453         <menubar>
1454           <menu action="file-menu">
1455             <menuitem action="quit"/>
1456           </menu>
1457           <menu action="server-menu">
1458             <menuitem action="connect"/>
1459             <menuitem action="disconnect"/>
1460             <separator/>
1461             <menuitem action="server-version"/>
1462             <menuitem action="crypto-algs"/>
1463             <menuitem action="add-peer"/>
1464             <menuitem action="conn-peer"/>
1465             <menuitem action="daemon"/>
1466             <menuitem action="reload-keys"/>
1467             <separator/>
1468             <menuitem action="server-quit"/>
1469           </menu>
1470           <menu action="logs-menu">
1471             <menuitem action="show-warnings"/>
1472             <menuitem action="show-trace"/>
1473             <menuitem action="trace-options"/>
1474           </menu>
1475           <menu action="help-menu">
1476             <menuitem action="about"/>
1477           </menu>
1478         </menubar>
1479         <popup name="peer-popup">
1480           <menuitem action="add-peer"/>
1481           <menuitem action="conn-peer"/>
1482           <menuitem action="kill-peer"/>
1483           <menuitem action="force-kx"/>
1484         </popup>
1485       </ui>
1486       '''
1487
1488     ## Populate the UI manager.
1489     me.ui.insert_action_group(actgroup, 0)
1490     me.ui.add_ui_from_string(uidef)
1491
1492     ## Construct the menu bar.
1493     vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
1494     me.add_accel_group(me.ui.get_accel_group())
1495
1496     ## Construct and attach the auto-peers menu.  (This is a horrible bodge
1497     ## because we can't attach the same submenu in two different places.)
1498     me.apmenu = G.Menu(), G.Menu()
1499     me.ui.get_widget('/menubar/server-menu/conn-peer') \
1500       .set_submenu(me.apmenu[0])
1501     me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1502
1503     ## Construct the main list model, and listen on hooks which report
1504     ## changes to the available peers.
1505     me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1506     me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1507     me.hook(monitor.peers.addhook, me.addpeer)
1508     me.hook(monitor.peers.delhook, me.delpeer)
1509     me.hook(monitor.autopeershook, me.apchange)
1510
1511     ## Construct the list viewer and put it in a scrolling window.
1512     scr = MyScrolledWindow()
1513     me.list = MyTreeView(me.listmodel)
1514     me.list.append_column(G.TreeViewColumn('Peer name',
1515                                            G.CellRendererText(),
1516                                            text = 0))
1517     me.list.append_column(G.TreeViewColumn('Address',
1518                                            G.CellRendererText(),
1519                                            text = 1))
1520     me.list.append_column(G.TreeViewColumn('T-ping',
1521                                            G.CellRendererText(),
1522                                            text = 2,
1523                                            foreground = 3))
1524     me.list.append_column(G.TreeViewColumn('E-ping',
1525                                            G.CellRendererText(),
1526                                            text = 4,
1527                                            foreground = 5))
1528     me.list.get_column(1).set_expand(True)
1529     me.list.connect('row-activated', me.activate)
1530     me.list.connect('button-press-event', me.buttonpress)
1531     me.list.set_reorderable(True)
1532     me.list.get_selection().set_mode(G.SELECTION_NONE)
1533     scr.add(me.list)
1534     vbox.pack_start(scr)
1535
1536     ## Construct the status bar, and listen on hooks which report changes to
1537     ## connection status.
1538     me.status = G.Statusbar()
1539     vbox.pack_start(me.status, expand = False)
1540     me.hook(conn.connecthook, cr(me.connected))
1541     me.hook(conn.disconnecthook, me.disconnected)
1542     me.hook(conn.notehook, me.notify)
1543
1544     ## Set a plausible default window size.
1545     me.set_default_size(512, 180)
1546
1547   def addpeer(me, peer):
1548     """Hook: announces that PEER has been added."""
1549     peer.i = me.listmodel.append([peer.name, peer.addr,
1550                                   '???', 'green', '???', 'green'])
1551     peer.win = WindowSlot(lambda: PeerWindow(peer))
1552     me.hook(peer.pinghook, me._ping)
1553     me.apchange()
1554
1555   def delpeer(me, peer):
1556     """Hook: announces that PEER has been removed."""
1557     me.listmodel.remove(peer.i)
1558     me.unhook(peer.pinghook)
1559     me.apchange()
1560
1561   def path_peer(me, path):
1562     """Return the peer corresponding to a given list-model PATH."""
1563     return monitor.peers[me.listmodel[path][0]]
1564
1565   def apchange(me):
1566     """
1567     Hook: announces that a change has been made to the peers available for
1568     automated connection.
1569
1570     This populates both auto-peer menus and keeps them in sync.  (As
1571     mentioned above, we can't attach the same submenu to two separate parent
1572     menu items.  So we end up with two identical menus instead.  Yes, this
1573     does suck.)
1574     """
1575
1576     ## The set_active method of a CheckMenuItem works by maybe activating the
1577     ## menu item.  This signals our handler.  But we don't actually want to
1578     ## signal the handler unless the user actually frobbed the item.  So the
1579     ## _kidding flag is used as an underhanded way of telling the handler
1580     ## that we don't actually want it to do anything.  Of course, this sucks
1581     ## mightily.
1582     me._kidding = True
1583
1584     ## Iterate over the two menus.
1585     for m in 0, 1:
1586       menu = me.apmenu[m]
1587       existing = menu.get_children()
1588       if monitor.autopeers is None:
1589
1590         ## No peers, so empty out the menu.
1591         for item in existing:
1592           menu.remove(item)
1593
1594       else:
1595
1596         ## Insert the new items into the menu.  (XXX this seems buggy XXX)
1597         ## Tick the peers which are actually connected.
1598         i = j = 0
1599         for peer in monitor.autopeers:
1600           if j < len(existing) and \
1601              existing[j].get_child().get_text() == peer:
1602             item = existing[j]
1603             j += 1
1604           else:
1605             item = G.CheckMenuItem(peer, use_underline = False)
1606             item.connect('activate', invoker(me._addautopeer, peer))
1607             menu.insert(item, i)
1608           item.set_active(peer in monitor.peers.table)
1609           i += 1
1610
1611       ## Make all the menu items visible.
1612       menu.show_all()
1613
1614     ## Set the parent menu items sensitive if and only if there are any peers
1615     ## to connect.
1616     for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1617       me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1618
1619     ## And now allow the handler to do its business normally.
1620     me._kidding = False
1621
1622   def _addautopeer(me, peer):
1623     """
1624     Automatically connect an auto-peer.
1625
1626     This method is invoked from the main coroutine.  Since the actual
1627     connection needs to issue administration commands, we must spawn a new
1628     child coroutine for it.
1629     """
1630     if me._kidding:
1631       return
1632     T.Coroutine(me._addautopeer_hack,
1633                 name = '_addautopeerhack %s' % peer).switch(peer)
1634
1635   def _addautopeer_hack(me, peer):
1636     """Make an automated connection to PEER in response to a user click."""
1637     if me._kidding:
1638       return
1639     try:
1640       T._simple(conn.svcsubmit('connect', 'active', peer))
1641     except T.TripeError, exc:
1642       T.defer(moanbox, ' '.join(exc.args))
1643     me.apchange()
1644
1645   def activate(me, l, path, col):
1646     """
1647     Handle a double-click on a peer in the main list: open a PeerInfo window.
1648     """
1649     peer = me.path_peer(path)
1650     peer.win.open()
1651
1652   def buttonpress(me, l, ev):
1653     """
1654     Handle a mouse click on the main list.
1655
1656     Currently we're only interested in button-3, which pops up the peer menu.
1657     For future reference, we stash the peer that was clicked in me.menupeer.
1658     """
1659     if ev.button == 3:
1660       x, y = int(ev.x), int(ev.y)
1661       r = me.list.get_path_at_pos(x, y)
1662       for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1663         me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1664                                           r is not None)
1665       me.ui.get_widget('/peer-popup/conn-peer'). \
1666         set_sensitive(bool(monitor.autopeers))
1667       if r:
1668         me.menupeer = me.path_peer(r[0])
1669       else:
1670         me.menupeer = None
1671       me.ui.get_widget('/peer-popup').popup(
1672         None, None, None, ev.button, ev.time)
1673
1674   def killpeer(me):
1675     """Kill a peer from the popup menu."""
1676     cr(conn.kill, me.menupeer.name)()
1677
1678   def forcekx(me):
1679     """Kickstart a key-exchange from the popup menu."""
1680     cr(conn.forcekx, me.menupeer.name)()
1681
1682   _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1683   def _ping(me, p, cmd, ps):
1684     """Hook: responds to ping reports."""
1685     textcol, colourcol = me._columnmap[cmd]
1686     if ps.nmissrun:
1687       me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1688       me.listmodel[p.i][colourcol] = 'red'
1689     else:
1690       me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1691       me.listmodel[p.i][colourcol] = 'black'
1692
1693   def setstatus(me, status):
1694     """Update the message in the status bar."""
1695     me.status.pop(0)
1696     me.status.push(0, status)
1697
1698   def notify(me, note, *rest):
1699     """Hook: invoked when interesting notifications occur."""
1700     if note == 'DAEMON':
1701       me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1702
1703   def connected(me):
1704     """
1705     Hook: invoked when a connection is made to the server.
1706
1707     Make options which require a server connection sensitive.
1708     """
1709     me.setstatus('Connected (port %s)' % conn.port())
1710     me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1711     for i in ('/menubar/server-menu/disconnect',
1712               '/menubar/server-menu/server-version',
1713               '/menubar/server-menu/add-peer',
1714               '/menubar/server-menu/server-quit',
1715               '/menubar/logs-menu/trace-options'):
1716       me.ui.get_widget(i).set_sensitive(True)
1717     me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1718       set_sensitive(bool(monitor.autopeers))
1719     me.ui.get_widget('/menubar/server-menu/daemon'). \
1720       set_sensitive(conn.servinfo()['daemon'] == 'nil')
1721
1722   def disconnected(me, reason):
1723     """
1724     Hook: invoked when the connection to the server is lost.
1725
1726     Make most options insensitive.
1727     """
1728     me.setstatus('Disconnected')
1729     me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1730     for i in ('/menubar/server-menu/disconnect',
1731               '/menubar/server-menu/server-version',
1732               '/menubar/server-menu/add-peer',
1733               '/menubar/server-menu/conn-peer',
1734               '/menubar/server-menu/daemon',
1735               '/menubar/server-menu/server-quit',
1736               '/menubar/logs-menu/trace-options'):
1737       me.ui.get_widget(i).set_sensitive(False)
1738     if reason: moanbox(reason)
1739
1740 ###--------------------------------------------------------------------------
1741 ### Main program.
1742
1743 def parse_options():
1744   """
1745   Parse command-line options.
1746
1747   Process the boring ones.  Return all of them, for later.
1748   """
1749   op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1750                     version = '%prog (tripe version 1.0.0)')
1751   op.add_option('-a', '--admin-socket',
1752                 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1753                 help = 'Select socket to connect to [default %default]')
1754   op.add_option('-d', '--directory',
1755                 metavar = 'DIR', dest = 'dir', default = T.configdir,
1756                 help = 'Select current diretory [default %default]')
1757   opts, args = op.parse_args()
1758   if args: op.error('no arguments permitted')
1759   OS.chdir(opts.dir)
1760   return opts
1761
1762 def init(opts):
1763   """Initialization."""
1764
1765   global conn, monitor, pinger
1766
1767   ## Try to establish a connection.
1768   conn = Connection(opts.tripesock)
1769
1770   ## Make the main interesting coroutines and objects.
1771   monitor = Monitor()
1772   pinger = Pinger()
1773   pinger.switch()
1774
1775 def main():
1776
1777   ## Main window.
1778   root = MonitorWindow()
1779   conn.connect()
1780   root.show_all()
1781
1782   ## Main loop.
1783   HookClient().hook(root.closehook, exit)
1784   conn.mainloop()
1785
1786 if __name__ == '__main__':
1787   opts = parse_options()
1788   init(opts)
1789   main()
1790
1791 ###----- That's all, folks --------------------------------------------------