chiark / gitweb /
Add new `knock' protocol.
[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###
11ad66c2
MW
13### TrIPE is free software: you can redistribute it and/or modify it under
14### the terms of the GNU General Public License as published by the Free
15### Software Foundation; either version 3 of the License, or (at your
16### option) any later version.
984b6d33 17###
11ad66c2
MW
18### TrIPE is distributed in the hope that it will be useful, but WITHOUT
19### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
20### FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
21### for more details.
984b6d33
MW
22###
23### You should have received a copy of the GNU General Public License
11ad66c2 24### along with TrIPE. If not, see <https://www.gnu.org/licenses/>.
984b6d33
MW
25
26###--------------------------------------------------------------------------
27### Dependencies.
060ca767 28
29import socket as S
984b6d33
MW
30import tripe as T
31import mLib as M
32from sys import argv, exit, stdin, stdout, stderr, exc_info, excepthook
060ca767 33import os as OS
34from os import environ
984b6d33 35import math as MATH
060ca767 36import sets as SET
984b6d33
MW
37from optparse import OptionParser
38import time as TIME
c55f0b7c 39import re as RX
060ca767 40from cStringIO import StringIO
41
a69f4417 42try:
171206b5 43 if OS.getenv('TRIPEMON_FORCE_GI'): raise ImportError()
a69f4417
MW
44 import pygtk
45 pygtk.require('2.0')
46 import gtk as G
47 import gobject as GO
48 import gtk.gdk as GDK
49 GL = GO
dcd075c8 50 GDK.KEY_Escape = G.keysyms.Escape
a69f4417
MW
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()
1ff5474e 314 me.__dict__.update(conn.algs(name))
984b6d33
MW
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:
171206b5 776 raise ValidationError()
060ca767 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
11ad66c2
MW
793GPL = """\
794TrIPE is free software: you can redistribute it and/or modify it under
795the terms of the GNU General Public License as published by the Free
796Software Foundation; either version 3 of the License, or (at your
797option) any later version.
060ca767 798
11ad66c2
MW
799TrIPE is distributed in the hope that it will be useful, but WITHOUT
800ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
801FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
802for more details.
060ca767 803
804You should have received a copy of the GNU General Public License
11ad66c2 805along with TrIPE. If not, see <https://www.gnu.org/licenses/>."""
060ca767 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 1057 me.e_name = table.labelled('Name',
8362ac1c 1058 ValidatingEntry(r'^[^\s:]+$', '', 16),
984b6d33
MW
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))
a69f4417 1067 me.l_tunnel = table.labelled('Tunnel', combo_box_text(),
984b6d33 1068 newlinep = True, width = 3)
0adda4f8 1069 me.tuns = ['(Default)'] + conn.tunnels()
984b6d33
MW
1070 for t in me.tuns:
1071 me.l_tunnel.append_text(t)
1072 me.l_tunnel.set_active(0)
15e8239f
MW
1073
1074 def tickybox_sensitivity(tickybox, target):
1075 tickybox.connect('toggled',
1076 lambda t: target.set_sensitive (t.get_active()))
1077
bf4a8e07
MW
1078 def optional_entry(label, rx_valid, width):
1079 c = G.CheckButton(label)
1080 table.pack(c, newlinep = True, xopt = G.FILL)
1081 e = ValidatingEntry(rx_valid, '', width)
1082 e.set_sensitive(False)
1083 tickybox_sensitivity(c, e)
1084 table.pack(e, width = 3)
1085 return c, e
1086
1087 me.c_keepalive, me.e_keepalive = \
1088 optional_entry('Keepalives', r'^\d+[hms]?$', 5)
15e8239f 1089
f6208364
MW
1090 me.c_cork = G.CheckButton('Cork')
1091 table.pack(me.c_cork, newlinep = True, width = 4, xopt = G.FILL)
1092
15e8239f
MW
1093 me.c_mobile = G.CheckButton('Mobile')
1094 table.pack(me.c_mobile, newlinep = True, width = 4, xopt = G.FILL)
1095
bf4a8e07
MW
1096 me.c_peerkey, me.e_peerkey = \
1097 optional_entry('Peer key tag', r'^[^.:\s]+$', 16)
1098 me.c_privkey, me.e_privkey = \
1099 optional_entry('Private key tag', r'^[^.:\s]+$', 16)
15e8239f 1100
8362ac1c
MW
1101 me.c_knock, me.e_knock = \
1102 optional_entry('Knock string', r'^[^:\s]+$', 16)
1103
984b6d33
MW
1104 me.show_all()
1105
1106 def ok(me):
1107 """Handle an OK press: create the peer."""
1108 try:
984b6d33 1109 t = me.l_tunnel.get_active()
15e8239f
MW
1110 me._addpeer(me.e_name.get_text(),
1111 me.e_addr.get_text(),
1112 me.e_port.get_text(),
1113 keepalive = (me.c_keepalive.get_active() and
1114 me.e_keepalive.get_text() or None),
1115 tunnel = t and me.tuns[t] or None,
f6208364 1116 cork = me.c_cork.get_active() or None,
f40cd7f1 1117 mobile = me.c_mobile.get_active() or None,
15e8239f
MW
1118 key = (me.c_peerkey.get_active() and
1119 me.e_peerkey.get_text() or None),
1120 priv = (me.c_privkey.get_active() and
8362ac1c
MW
1121 me.e_privkey.get_text() or None),
1122 knock = (me.c_knock.get_active() and
1123 me.e_knock.get_text() or None))
984b6d33
MW
1124 except ValidationError:
1125 GDK.beep()
1126 return
1127
1128 @incr
15e8239f 1129 def _addpeer(me, *args, **kw):
984b6d33
MW
1130 """Coroutine function: actually do the ADD command."""
1131 try:
15e8239f 1132 conn.add(*args, **kw)
984b6d33
MW
1133 me.destroy()
1134 except T.TripeError, exc:
690a6ec1 1135 T.defer(moanbox, ' '.join(exc))
984b6d33 1136
dcd075c8 1137class ServInfo (TrivialWindow):
984b6d33
MW
1138 """
1139 Show information about the server and available services.
1140
1141 Interesting attributes:
1142
1143 * e: maps SERVINFO keys to entry widgets
1144 * svcs: Gtk ListStore describing services (columns are name and version)
1145 """
1146
1147 def __init__(me):
dcd075c8 1148 TrivialWindow.__init__(me)
984b6d33
MW
1149 me.set_title('TrIPE server info')
1150 table = GridPacker()
1151 me.add(table)
1152 me.e = {}
1153 def add(label, tag, text = None, **kw):
1154 me.e[tag] = table.info(label, text, **kw)
1155 add('Implementation', 'implementation')
1156 add('Version', 'version', newlinep = True)
1157 me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1158 me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1159 scr = MyScrolledWindow()
1160 lb = MyTreeView(me.svcs)
1161 i = 0
1162 for title in 'Service', 'Version':
1163 lb.append_column(G.TreeViewColumn(
1164 title, G.CellRendererText(), text = i))
1165 i += 1
1166 for svc in monitor.services:
1167 me.svcs.append([svc.name, svc.version])
1168 scr.add(lb)
1169 table.pack(scr, width = 2, newlinep = True,
1170 yopt = G.EXPAND | G.FILL | G.SHRINK)
1171 me.update()
1172 me.hook(conn.connecthook, me.update)
1173 me.hook(monitor.services.addhook, me.addsvc)
1174 me.hook(monitor.services.delhook, me.delsvc)
1175 me.show_all()
1176
1177 def addsvc(me, svc):
1178 me.svcs.append([svc.name, svc.version])
1179
1180 def delsvc(me, svc):
1181 for i in xrange(len(me.svcs)):
1182 if me.svcs[i][0] == svc.name:
1183 me.svcs.remove(me.svcs.get_iter(i))
1184 break
1185 @incr
1186 def update(me):
1187 info = conn.servinfo()
1188 for i in me.e:
1189 me.e[i].set_text(info[i])
1190
1191class TraceOptions (MyDialog):
1192 """Tracing options window."""
1193 def __init__(me):
1194 MyDialog.__init__(me, title = 'Tracing options',
1195 buttons = [(G.STOCK_CLOSE, me.destroy),
1196 (G.STOCK_OK, cr(me.ok))])
1197 me._setup()
1198
1199 @incr
1200 def _setup(me):
1201 me.opts = []
1202 for ch, st, desc in conn.trace():
1203 if ch.isupper(): continue
1204 text = desc[0].upper() + desc[1:]
1205 ticky = G.CheckButton(text)
1206 ticky.set_active(st == '+')
a69f4417 1207 me.vbox.pack_start(ticky, True, True, 0)
984b6d33
MW
1208 me.opts.append((ch, ticky))
1209 me.show_all()
1210 def ok(me):
1211 on = []
1212 off = []
1213 for ch, ticky in me.opts:
1214 if ticky.get_active():
1215 on.append(ch)
1216 else:
1217 off.append(ch)
1218 setting = ''.join(on) + '-' + ''.join(off)
1219 conn.trace(setting)
1220 me.destroy()
1221
1222###--------------------------------------------------------------------------
1223### Peer window.
060ca767 1224
1225def xlate_time(t):
984b6d33 1226 """Translate a TrIPE-format time to something human-readable."""
060ca767 1227 if t == 'NEVER': return '(never)'
37941236 1228 YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
984b6d33
MW
1229 ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1230 ago = MATH.floor(ago); unit = 's'
37941236 1231 for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1232 if ago < 2*n: break
1233 ago /= n
1234 unit = u
1235 return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1236 (YY, MM, DD, hh, mm, ss, ago, unit)
060ca767 1237def xlate_bytes(b):
9a06b923 1238 """Translate a raw byte count into something a human might want to read."""
060ca767 1239 suff = 'B'
1240 b = int(b)
1241 for s in 'KMG':
1242 if b < 4096: break
1243 b /= 1024
1244 suff = s
1245 return '%d %s' % (b, suff)
1246
1247## How to translate peer stats. Maps the stat name to a translation
1248## function.
1249statsxlate = \
1250 [('start-time', xlate_time),
1251 ('last-packet-time', xlate_time),
1252 ('last-keyexch-time', xlate_time),
1253 ('bytes-in', xlate_bytes),
1254 ('bytes-out', xlate_bytes),
1255 ('keyexch-bytes-in', xlate_bytes),
1256 ('keyexch-bytes-out', xlate_bytes),
1257 ('ip-bytes-in', xlate_bytes),
1258 ('ip-bytes-out', xlate_bytes)]
1259
3f230a0f
MW
1260def format_stat(format, dict):
1261 if callable(format): return format(dict)
1262 else: return format % dict
1263
060ca767 1264## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
1265## the label to give the entry box; FORMAT is the format string to write into
1266## the entry.
cc1bff4e
MW
1267cryptolayout = \
1268 [('Diffie-Hellman group',
1ff5474e
MW
1269 '%(kx-group)s '
1270 '(%(kx-group-order-bits)s-bit order, '
1271 '%(kx-group-elt-bits)s-bit elements)'),
fd0e15ee
MW
1272 ('Bulk crypto transform',
1273 '%(bulk-transform)s (%(bulk-overhead)s byte overhead)'),
1274 ('Data encryption', lambda d: '%s (%s; %s)' % (
1275 d['cipher'],
1276 '%d-bit key' % (8*int(d['cipher-keysz'])),
1277 d.get('cipher-blksz', '0') == '0'
1278 and 'stream cipher'
1279 or '%d-bit block' % (8*int(d['cipher-blksz'])))),
1280 ('Message authentication', lambda d: '%s (%s; %s)' % (
1281 d['mac'],
1282 d.get('mac-keysz') is None
1283 and 'one-time MAC'
1284 or '%d-bit key' % (8*int(d['mac-keysz'])),
1285 '%d-bit tag' % (8*int(d['mac-tagsz'])))),
1286 ('Hash', lambda d: '%s (%d-bit output)' %
1287 (d['hash'], 8*int(d['hash-sz'])))]
cc1bff4e
MW
1288
1289statslayout = \
1290 [('Start time', '%(start-time)s'),
1291 ('Private key', '%(current-key)s')] + \
1292 cryptolayout + \
1293 [('Last key-exchange', '%(last-keyexch-time)s'),
060ca767 1294 ('Last packet', '%(last-packet-time)s'),
1295 ('Packets in/out',
1296 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1297 ('Key-exchange in/out',
1298 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1299 ('IP in/out',
984b6d33 1300 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
060ca767 1301 ('Rejected packets', '%(rejected-packets)s')]
1302
dcd075c8 1303class PeerWindow (TrivialWindow):
984b6d33
MW
1304 """
1305 Show information about a peer.
1306
1307 This gives a graphical view of the server's peer statistics.
1308
1309 Interesting attributes:
1310
1311 * e: dict mapping keys (mostly matching label widget texts, though pings
1312 use command names) to entry widgets so that we can update them easily
1313 * peer: the peer this window shows information about
1314 * cr: the info-fetching coroutine, or None if crrrently disconnected
1315 * doupate: whether the info-fetching corouting should continue running
1316 """
1317
1318 def __init__(me, peer):
1319 """Construct a PeerWindow, showing information about PEER."""
1320
dcd075c8 1321 TrivialWindow.__init__(me)
060ca767 1322 me.set_title('TrIPE statistics: %s' % peer.name)
060ca767 1323 me.peer = peer
984b6d33 1324
060ca767 1325 table = GridPacker()
1326 me.add(table)
984b6d33
MW
1327
1328 ## Utility for adding fields.
060ca767 1329 me.e = {}
984b6d33
MW
1330 def add(label, text = None, key = None):
1331 if key is None: key = label
1332 me.e[key] = table.info(label, text, len = 42, newlinep = True)
1333
1334 ## Build the dialogue box.
060ca767 1335 add('Peer name', peer.name)
1336 add('Tunnel', peer.tunnel)
1337 add('Interface', peer.ifname)
1338 add('Keepalives',
1339 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1340 add('Address', peer.addr)
984b6d33
MW
1341 add('Transport pings', key = 'PING')
1342 add('Encrypted pings', key = 'EPING')
1343
1344 for label, format in statslayout:
1345 add(label)
1346
1347 ## Hook onto various interesting events.
1348 me.hook(conn.connecthook, me.tryupdate)
1349 me.hook(conn.disconnecthook, me.stopupdate)
060ca767 1350 me.hook(me.closehook, me.stopupdate)
1351 me.hook(me.peer.deadhook, me.dead)
984b6d33 1352 me.hook(me.peer.changehook, me.change)
060ca767 1353 me.hook(me.peer.pinghook, me.ping)
984b6d33
MW
1354 me.cr = None
1355 me.doupdate = True
060ca767 1356 me.tryupdate()
984b6d33
MW
1357
1358 ## Format the ping statistics.
1359 for cmd, ps in me.peer.ping.iteritems():
1360 me.ping(me.peer, cmd, ps)
1361
1362 ## And show the window.
060ca767 1363 me.show_all()
984b6d33
MW
1364
1365 def change(me):
1366 """Update the display in response to a notification."""
1367 me.e['Interface'].set_text(me.peer.ifname)
7949d109 1368 me.e['Address'].set_text(me.peer.addr)
984b6d33
MW
1369
1370 def _update(me):
1371 """
1372 Main display-updating coroutine.
1373
1374 This does an update, sleeps for a while, and starts again. If the
1375 me.doupdate flag goes low, we stop the loop.
1376 """
1377 while me.peer.alivep and conn.connectedp() and me.doupdate:
1378 stat = conn.stats(me.peer.name)
1379 for s, trans in statsxlate:
1380 stat[s] = trans(stat[s])
1ff5474e 1381 stat.update(me.peer.__dict__)
984b6d33 1382 for label, format in statslayout:
3f230a0f 1383 me.e[label].set_text(format_stat(format, stat))
a69f4417 1384 GL.timeout_add(1000, lambda: me.cr.switch() and False)
984b6d33
MW
1385 me.cr.parent.switch()
1386 me.cr = None
1387
060ca767 1388 def tryupdate(me):
984b6d33
MW
1389 """Start the updater coroutine, if it's not going already."""
1390 if me.cr is None:
22b47552
MW
1391 me.cr = T.Coroutine(me._update,
1392 name = 'update-peer-window %s' % me.peer.name)
984b6d33
MW
1393 me.cr.switch()
1394
1395 def stopupdate(me, *hunoz, **hukairz):
1396 """Stop the update coroutine, by setting me.doupdate."""
1397 me.doupdate = False
1398
060ca767 1399 def dead(me):
984b6d33 1400 """Called when the peer is killed."""
060ca767 1401 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1402 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1403 me.stopupdate()
060ca767 1404
984b6d33
MW
1405 def ping(me, peer, cmd, ps):
1406 """Called when a ping result for the peer is reported."""
1407 s = '%d/%d' % (ps.ngood, ps.n)
1408 if ps.n:
1409 s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1410 if ps.ngood:
1411 s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1412 me.e[ps.command].set_text(s)
1413
1414###--------------------------------------------------------------------------
1415### Cryptographic status.
1416
dcd075c8 1417class CryptoInfo (TrivialWindow):
984b6d33
MW
1418 """Simple display of cryptographic algorithms in use."""
1419 def __init__(me):
dcd075c8 1420 TrivialWindow.__init__(me)
984b6d33 1421 me.set_title('Cryptographic algorithms')
690a6ec1 1422 T.aside(me.populate)
984b6d33 1423 def populate(me):
060ca767 1424 table = GridPacker()
984b6d33 1425 me.add(table)
060ca767 1426
984b6d33 1427 crypto = conn.algs()
98444c8a
MW
1428 firstp = True
1429 for label, format in cryptolayout:
1430 table.info(label, format_stat(format, crypto),
1431 len = 42, newlinep = not firstp)
1432 firstp = False
060ca767 1433
984b6d33
MW
1434 me.show_all()
1435
1436###--------------------------------------------------------------------------
1437### Main monitor window.
060ca767 1438
1439class MonitorWindow (MyWindow):
1440
984b6d33
MW
1441 """
1442 The main monitor window.
1443
1444 This class creates, populates and maintains the main monitor window.
1445
1446 Lots of attributes:
1447
1448 * warnings, trace: log models for server output
1449 * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1450 WindowSlot objects for ancillary windows
1451 * ui: Gtk UIManager object for the menu system
1452 * apmenu: pair of identical autoconnecting peer menus
1453 * listmodel: Gtk ListStore for connected peers; contains peer name,
1454 address, and ping times (transport and encrypted, value and colour)
1455 * status: Gtk Statusbar at the bottom of the window
1456 * _kidding: an unpleasant backchannel between the apchange method (which
1457 builds the apmenus) and the menu handler, forced on us by a Gtk
1458 misfeature
1459
1460 Also installs attributes on Peer objects:
1461
1462 * i: index of peer's entry in listmodel
1463 * win: WindowSlot object for the peer's PeerWindow
1464 """
1465
1466 def __init__(me):
1467 """Construct the window."""
1468
1469 ## Basic stuff.
060ca767 1470 MyWindow.__init__(me)
1471 me.set_title('TrIPE monitor')
984b6d33
MW
1472
1473 ## Hook onto diagnostic outputs.
060ca767 1474 me.warnings = WarningLogModel()
984b6d33 1475 me.hook(conn.warnhook, me.warnings.notify)
060ca767 1476 me.trace = TraceLogModel()
984b6d33 1477 me.hook(conn.tracehook, me.trace.notify)
060ca767 1478
984b6d33 1479 ## Make slots to store the various ancillary singleton windows.
060ca767 1480 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1481 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
984b6d33
MW
1482 me.traceopts = WindowSlot(lambda: TraceOptions())
1483 me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1484 me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1485 me.servinfo = WindowSlot(lambda: ServInfo())
060ca767 1486
984b6d33 1487 ## Main window structure.
060ca767 1488 vbox = G.VBox()
1489 me.add(vbox)
1490
984b6d33 1491 ## UI manager makes our menus. (We're too cheap to have a toolbar.)
060ca767 1492 me.ui = G.UIManager()
1493 actgroup = makeactiongroup('monitor',
1494 [('file-menu', '_File', None, None),
984b6d33
MW
1495 ('connect', '_Connect', '<Control>C', conn.connect),
1496 ('disconnect', '_Disconnect', '<Control>D',
1497 lambda: conn.disconnect(None)),
1498 ('quit', '_Quit', '<Control>Q', me.close),
060ca767 1499 ('server-menu', '_Server', None, None),
984b6d33
MW
1500 ('daemon', 'Run in _background', None, cr(conn.daemon)),
1501 ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1502 ('crypto-algs', 'Cryptographic algorithms',
1503 '<Control>Y', me.cryptoinfo.open),
1504 ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1505 ('server-quit', 'Terminate server', None, cr(conn.quit)),
1506 ('conn-peer', 'Connect peer', None, None),
060ca767 1507 ('logs-menu', '_Logs', None, None),
984b6d33
MW
1508 ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1509 ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
060ca767 1510 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1511 ('help-menu', '_Help', None, None),
1512 ('about', '_About tripemon...', None, aboutbox.open),
984b6d33 1513 ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
060ca767 1514 ('kill-peer', '_Kill peer', None, me.killpeer),
1515 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
984b6d33
MW
1516
1517 ## Menu structures.
060ca767 1518 uidef = '''
1519 <ui>
1520 <menubar>
1521 <menu action="file-menu">
1522 <menuitem action="quit"/>
1523 </menu>
1524 <menu action="server-menu">
984b6d33 1525 <menuitem action="connect"/>
060ca767 1526 <menuitem action="disconnect"/>
1527 <separator/>
ca6eb20c 1528 <menuitem action="server-version"/>
984b6d33 1529 <menuitem action="crypto-algs"/>
060ca767 1530 <menuitem action="add-peer"/>
984b6d33 1531 <menuitem action="conn-peer"/>
060ca767 1532 <menuitem action="daemon"/>
ca6eb20c 1533 <menuitem action="reload-keys"/>
060ca767 1534 <separator/>
1535 <menuitem action="server-quit"/>
1536 </menu>
1537 <menu action="logs-menu">
1538 <menuitem action="show-warnings"/>
1539 <menuitem action="show-trace"/>
1540 <menuitem action="trace-options"/>
1541 </menu>
1542 <menu action="help-menu">
1543 <menuitem action="about"/>
1544 </menu>
1545 </menubar>
1546 <popup name="peer-popup">
1547 <menuitem action="add-peer"/>
984b6d33 1548 <menuitem action="conn-peer"/>
060ca767 1549 <menuitem action="kill-peer"/>
1550 <menuitem action="force-kx"/>
1551 </popup>
1552 </ui>
1553 '''
984b6d33
MW
1554
1555 ## Populate the UI manager.
060ca767 1556 me.ui.insert_action_group(actgroup, 0)
1557 me.ui.add_ui_from_string(uidef)
984b6d33
MW
1558
1559 ## Construct the menu bar.
a69f4417 1560 vbox.pack_start(me.ui.get_widget('/menubar'), False, True, 0)
060ca767 1561 me.add_accel_group(me.ui.get_accel_group())
060ca767 1562
984b6d33
MW
1563 ## Construct and attach the auto-peers menu. (This is a horrible bodge
1564 ## because we can't attach the same submenu in two different places.)
1565 me.apmenu = G.Menu(), G.Menu()
1566 me.ui.get_widget('/menubar/server-menu/conn-peer') \
1567 .set_submenu(me.apmenu[0])
1568 me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1569
1570 ## Construct the main list model, and listen on hooks which report
1571 ## changes to the available peers.
060ca767 1572 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1573 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
984b6d33
MW
1574 me.hook(monitor.peers.addhook, me.addpeer)
1575 me.hook(monitor.peers.delhook, me.delpeer)
1576 me.hook(monitor.autopeershook, me.apchange)
060ca767 1577
984b6d33
MW
1578 ## Construct the list viewer and put it in a scrolling window.
1579 scr = MyScrolledWindow()
1580 me.list = MyTreeView(me.listmodel)
060ca767 1581 me.list.append_column(G.TreeViewColumn('Peer name',
1582 G.CellRendererText(),
1583 text = 0))
1584 me.list.append_column(G.TreeViewColumn('Address',
1585 G.CellRendererText(),
1586 text = 1))
1587 me.list.append_column(G.TreeViewColumn('T-ping',
1588 G.CellRendererText(),
1589 text = 2,
1590 foreground = 3))
1591 me.list.append_column(G.TreeViewColumn('E-ping',
1592 G.CellRendererText(),
1593 text = 4,
1594 foreground = 5))
1595 me.list.get_column(1).set_expand(True)
1596 me.list.connect('row-activated', me.activate)
1597 me.list.connect('button-press-event', me.buttonpress)
1598 me.list.set_reorderable(True)
1599 me.list.get_selection().set_mode(G.SELECTION_NONE)
1600 scr.add(me.list)
a69f4417 1601 vbox.pack_start(scr, True, True, 0)
060ca767 1602
984b6d33
MW
1603 ## Construct the status bar, and listen on hooks which report changes to
1604 ## connection status.
1605 me.status = G.Statusbar()
a69f4417 1606 vbox.pack_start(me.status, False, True, 0)
984b6d33
MW
1607 me.hook(conn.connecthook, cr(me.connected))
1608 me.hook(conn.disconnecthook, me.disconnected)
1609 me.hook(conn.notehook, me.notify)
1610
1611 ## Set a plausible default window size.
1612 me.set_default_size(512, 180)
060ca767 1613
1614 def addpeer(me, peer):
984b6d33 1615 """Hook: announces that PEER has been added."""
060ca767 1616 peer.i = me.listmodel.append([peer.name, peer.addr,
1617 '???', 'green', '???', 'green'])
984b6d33
MW
1618 peer.win = WindowSlot(lambda: PeerWindow(peer))
1619 me.hook(peer.pinghook, me._ping)
7949d109 1620 me.hook(peer.changehook, lambda: me._change(peer))
984b6d33
MW
1621 me.apchange()
1622
060ca767 1623 def delpeer(me, peer):
984b6d33 1624 """Hook: announces that PEER has been removed."""
060ca767 1625 me.listmodel.remove(peer.i)
984b6d33
MW
1626 me.unhook(peer.pinghook)
1627 me.apchange()
1628
060ca767 1629 def path_peer(me, path):
984b6d33
MW
1630 """Return the peer corresponding to a given list-model PATH."""
1631 return monitor.peers[me.listmodel[path][0]]
1632
1633 def apchange(me):
1634 """
1635 Hook: announces that a change has been made to the peers available for
1636 automated connection.
1637
1638 This populates both auto-peer menus and keeps them in sync. (As
1639 mentioned above, we can't attach the same submenu to two separate parent
1640 menu items. So we end up with two identical menus instead. Yes, this
1641 does suck.)
1642 """
1643
1644 ## The set_active method of a CheckMenuItem works by maybe activating the
1645 ## menu item. This signals our handler. But we don't actually want to
1646 ## signal the handler unless the user actually frobbed the item. So the
1647 ## _kidding flag is used as an underhanded way of telling the handler
1648 ## that we don't actually want it to do anything. Of course, this sucks
1649 ## mightily.
1650 me._kidding = True
1651
1652 ## Iterate over the two menus.
1653 for m in 0, 1:
1654 menu = me.apmenu[m]
1655 existing = menu.get_children()
1656 if monitor.autopeers is None:
1657
1658 ## No peers, so empty out the menu.
1659 for item in existing:
1660 menu.remove(item)
1661
1662 else:
1663
1664 ## Insert the new items into the menu. (XXX this seems buggy XXX)
37bc5549 1665 ## Tick the peers which are actually connected.
984b6d33
MW
1666 i = j = 0
1667 for peer in monitor.autopeers:
1668 if j < len(existing) and \
1669 existing[j].get_child().get_text() == peer:
1670 item = existing[j]
1671 j += 1
1672 else:
1673 item = G.CheckMenuItem(peer, use_underline = False)
1674 item.connect('activate', invoker(me._addautopeer, peer))
1675 menu.insert(item, i)
1676 item.set_active(peer in monitor.peers.table)
1677 i += 1
1678
1679 ## Make all the menu items visible.
1680 menu.show_all()
1681
1682 ## Set the parent menu items sensitive if and only if there are any peers
1683 ## to connect.
1684 for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1685 me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1686
1687 ## And now allow the handler to do its business normally.
1688 me._kidding = False
1689
1690 def _addautopeer(me, peer):
1691 """
1692 Automatically connect an auto-peer.
1693
1694 This method is invoked from the main coroutine. Since the actual
1695 connection needs to issue administration commands, we must spawn a new
1696 child coroutine for it.
1697 """
1698 if me._kidding:
1699 return
22b47552
MW
1700 T.Coroutine(me._addautopeer_hack,
1701 name = '_addautopeerhack %s' % peer).switch(peer)
984b6d33
MW
1702
1703 def _addautopeer_hack(me, peer):
1704 """Make an automated connection to PEER in response to a user click."""
1705 if me._kidding:
1706 return
1707 try:
1708 T._simple(conn.svcsubmit('connect', 'active', peer))
1709 except T.TripeError, exc:
690a6ec1 1710 T.defer(moanbox, ' '.join(exc.args))
984b6d33 1711 me.apchange()
060ca767 1712
1713 def activate(me, l, path, col):
984b6d33
MW
1714 """
1715 Handle a double-click on a peer in the main list: open a PeerInfo window.
1716 """
060ca767 1717 peer = me.path_peer(path)
1718 peer.win.open()
984b6d33 1719
060ca767 1720 def buttonpress(me, l, ev):
984b6d33
MW
1721 """
1722 Handle a mouse click on the main list.
1723
1724 Currently we're only interested in button-3, which pops up the peer menu.
1725 For future reference, we stash the peer that was clicked in me.menupeer.
1726 """
060ca767 1727 if ev.button == 3:
984b6d33
MW
1728 x, y = int(ev.x), int(ev.y)
1729 r = me.list.get_path_at_pos(x, y)
060ca767 1730 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
984b6d33 1731 me.ui.get_widget(i).set_sensitive(conn.connectedp() and
060ca767 1732 r is not None)
984b6d33
MW
1733 me.ui.get_widget('/peer-popup/conn-peer'). \
1734 set_sensitive(bool(monitor.autopeers))
060ca767 1735 if r:
1736 me.menupeer = me.path_peer(r[0])
1737 else:
1738 me.menupeer = None
984b6d33
MW
1739 me.ui.get_widget('/peer-popup').popup(
1740 None, None, None, ev.button, ev.time)
060ca767 1741
1742 def killpeer(me):
984b6d33
MW
1743 """Kill a peer from the popup menu."""
1744 cr(conn.kill, me.menupeer.name)()
1745
060ca767 1746 def forcekx(me):
984b6d33
MW
1747 """Kickstart a key-exchange from the popup menu."""
1748 cr(conn.forcekx, me.menupeer.name)()
1749
1750 _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1751 def _ping(me, p, cmd, ps):
1752 """Hook: responds to ping reports."""
1753 textcol, colourcol = me._columnmap[cmd]
1754 if ps.nmissrun:
1755 me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1756 me.listmodel[p.i][colourcol] = 'red'
060ca767 1757 else:
984b6d33
MW
1758 me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1759 me.listmodel[p.i][colourcol] = 'black'
1760
7949d109
MW
1761 def _change(me, p):
1762 """Hook: notified when the peer changes state."""
1763 me.listmodel[p.i][1] = p.addr
1764
060ca767 1765 def setstatus(me, status):
984b6d33 1766 """Update the message in the status bar."""
060ca767 1767 me.status.pop(0)
1768 me.status.push(0, status)
984b6d33
MW
1769
1770 def notify(me, note, *rest):
1771 """Hook: invoked when interesting notifications occur."""
060ca767 1772 if note == 'DAEMON':
1773 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
984b6d33 1774
060ca767 1775 def connected(me):
984b6d33
MW
1776 """
1777 Hook: invoked when a connection is made to the server.
1778
1779 Make options which require a server connection sensitive.
1780 """
1781 me.setstatus('Connected (port %s)' % conn.port())
060ca767 1782 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1783 for i in ('/menubar/server-menu/disconnect',
1784 '/menubar/server-menu/server-version',
1785 '/menubar/server-menu/add-peer',
1786 '/menubar/server-menu/server-quit',
1787 '/menubar/logs-menu/trace-options'):
1788 me.ui.get_widget(i).set_sensitive(True)
984b6d33
MW
1789 me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1790 set_sensitive(bool(monitor.autopeers))
060ca767 1791 me.ui.get_widget('/menubar/server-menu/daemon'). \
984b6d33
MW
1792 set_sensitive(conn.servinfo()['daemon'] == 'nil')
1793
1794 def disconnected(me, reason):
1795 """
1796 Hook: invoked when the connection to the server is lost.
1797
1798 Make most options insensitive.
1799 """
060ca767 1800 me.setstatus('Disconnected')
1801 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1802 for i in ('/menubar/server-menu/disconnect',
1803 '/menubar/server-menu/server-version',
1804 '/menubar/server-menu/add-peer',
984b6d33 1805 '/menubar/server-menu/conn-peer',
060ca767 1806 '/menubar/server-menu/daemon',
1807 '/menubar/server-menu/server-quit',
1808 '/menubar/logs-menu/trace-options'):
1809 me.ui.get_widget(i).set_sensitive(False)
984b6d33
MW
1810 if reason: moanbox(reason)
1811
1812###--------------------------------------------------------------------------
1813### Main program.
1814
1815def parse_options():
1816 """
1817 Parse command-line options.
1818
1819 Process the boring ones. Return all of them, for later.
1820 """
1821 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1822 version = '%prog (tripe version 1.0.0)')
1823 op.add_option('-a', '--admin-socket',
1824 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1825 help = 'Select socket to connect to [default %default]')
1826 op.add_option('-d', '--directory',
1827 metavar = 'DIR', dest = 'dir', default = T.configdir,
1828 help = 'Select current diretory [default %default]')
1829 opts, args = op.parse_args()
1830 if args: op.error('no arguments permitted')
1831 OS.chdir(opts.dir)
1832 return opts
1833
1834def init(opts):
1835 """Initialization."""
1836
1837 global conn, monitor, pinger
1838
984b6d33
MW
1839 ## Try to establish a connection.
1840 conn = Connection(opts.tripesock)
1841
1842 ## Make the main interesting coroutines and objects.
1843 monitor = Monitor()
1844 pinger = Pinger()
1845 pinger.switch()
060ca767 1846
984b6d33 1847def main():
060ca767 1848
984b6d33
MW
1849 ## Main window.
1850 root = MonitorWindow()
1851 conn.connect()
1852 root.show_all()
060ca767 1853
984b6d33 1854 ## Main loop.
060ca767 1855 HookClient().hook(root.closehook, exit)
690a6ec1 1856 conn.mainloop()
060ca767 1857
1858if __name__ == '__main__':
984b6d33
MW
1859 opts = parse_options()
1860 init(opts)
060ca767 1861 main()
1862
984b6d33 1863###----- That's all, folks --------------------------------------------------