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