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