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