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