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