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