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