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