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