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