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