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