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