server/: Use modern functions for address/text conversions.
[tripe] / mon / tripemon.in
1 #! @PYTHON@
2 ### -*- mode: python; coding: utf-8 -*-
3 ###
4 ### Graphical monitor for tripe server
5 ###
6 ### (c) 2007 Straylight/Edgeware
7 ###
8
9 ###----- Licensing notice ---------------------------------------------------
10 ###
11 ### This file is part of Trivial IP Encryption (TrIPE).
12 ###
13 ### TrIPE is free software: you can redistribute it and/or modify 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.
17 ###
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.
22 ###
23 ### You should have received a copy of the GNU General Public License
24 ### along with TrIPE. If not, see <https://www.gnu.org/licenses/>.
25
26 ###--------------------------------------------------------------------------
27 ### Dependencies.
28
29 import socket as S
30 import tripe as T
31 import mLib as M
32 from sys import argv, exit, stdin, stdout, stderr, exc_info, excepthook
33 import os as OS
34 from os import environ
35 import math as MATH
36 import sets as SET
37 from optparse import OptionParser
38 import time as TIME
39 import re as RX
40 from cStringIO import StringIO
41
42 try:
43 if OS.getenv('TRIPEMON_FORCE_GI'): raise ImportError()
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
50 GDK.KEY_Escape = G.keysyms.Escape
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)
54 except 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)
70
71 if OS.getenv('TRIPE_DEBUG_MONITOR') is not None:
72 T._debug = 1
73
74 ###--------------------------------------------------------------------------
75 ### Doing things later.
76
77 def uncaught():
78 """Report an uncaught exception."""
79 excepthook(*exc_info())
80
81 def 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 _
95
96 def invoker(func, *args, **kw):
97 """
98 Return a function which throws away its arguments and calls
99 FUNC(*ARGS, **KW).
100
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)
105
106 def cr(func, *args, **kw):
107 """Return a function which invokes FUNC(*ARGS, **KW) in a coroutine."""
108 name = T.funargstr(func, args, kw)
109 return lambda *hunoz, **hukairz: \
110 T.Coroutine(xwrap(func), name = name).switch(*args, **kw)
111
112 def incr(func):
113 """Decorator: runs its function in a coroutine of its own."""
114 return lambda *args, **kw: \
115 (T.Coroutine(func, name = T.funargstr(func, args, kw))
116 .switch(*args, **kw))
117
118 ###--------------------------------------------------------------------------
119 ### Random bits of infrastructure.
120
121 ## Program name, shorn of extraneous stuff.
122 M.ego(argv[0])
123 moan = M.moan
124 die = M.die
125
126 class HookList (object):
127 """
128 Notification hook list.
129
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 """
133
134 def __init__(me):
135 """Basic initialization: create the hook list."""
136 me.list = []
137
138 def add(me, func, obj):
139 """Add FUNC to the list of hook functions."""
140 me.list.append((obj, func))
141
142 def prune(me, obj):
143 """Remove hook functions registered with the given OBJ."""
144 new = []
145 for o, f in me.list:
146 if o is not obj:
147 new.append((o, f))
148 me.list = new
149
150 def run(me, *args, **kw):
151 """Invoke the hook functions with arguments *ARGS and **KW."""
152 for o, hook in me.list:
153 rc = hook(*args, **kw)
154 if rc is not None: return rc
155 return None
156
157 class HookClient (object):
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 """
165 def __init__(me):
166 """Basic initialization."""
167 me.hooks = SET.Set()
168
169 def hook(me, hk, func):
170 """Add FUNC to the hook list HK."""
171 hk.add(func, me)
172 me.hooks.add(hk)
173
174 def unhook(me, hk):
175 """Remove myself from the hook list HK."""
176 hk.prune(me)
177 me.hooks.discard(hk)
178
179 def unhookall(me):
180 """Remove myself from all hook lists."""
181 for hk in me.hooks:
182 hk.prune(me)
183 me.hooks.clear()
184
185 class struct (object):
186 """A very simple dumb data container object."""
187 def __init__(me, **kw):
188 me.__dict__.update(kw)
189
190 ## Matches ISO date format yyyy-mm-ddThh:mm:ss.
191 rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
192
193 ###--------------------------------------------------------------------------
194 ### Connections.
195
196 class GIOWatcher (object):
197 """
198 Monitor I/O events using glib.
199 """
200 def __init__(me, conn, mc = GL.main_context_default()):
201 me._conn = conn
202 me._watch = None
203 me._mc = mc
204 def connected(me, sock):
205 me._watch = GL.io_add_watch(sock, GL.IO_IN,
206 lambda *hunoz: me._conn.receive())
207 def disconnected(me):
208 GL.source_remove(me._watch)
209 me._watch = None
210 def iterate(me):
211 me._mc.iteration(True)
212
213 class Connection (T.TripeCommandDispatcher):
214 """
215 The main connection to the server.
216
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.
220
221 This class knows about the Glib I/O dispatcher system, and plugs into it.
222
223 Hooks:
224
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 """
231
232 def __init__(me, socket):
233 """Create a new Connection."""
234 T.TripeCommandDispatcher.__init__(me, socket)
235 me.connecthook = HookList()
236 me.disconnecthook = HookList()
237 me.notehook = HookList()
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)
243 me.iowatch = GIOWatcher(me)
244
245 def connected(me):
246 """Handles reconnection to the server, and signals the hook."""
247 T.TripeCommandDispatcher.connected(me)
248 me.connecthook.run()
249
250 def disconnected(me, reason):
251 """Handles disconnection from the server, and signals the hook."""
252 me.disconnecthook.run(reason)
253 T.TripeCommandDispatcher.disconnected(me, reason)
254
255 ###--------------------------------------------------------------------------
256 ### Watching the peers go by.
257
258 class 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."""
279 me.name = name
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
289 class Peer (MonitorObject):
290 """
291 An object representing a connected peer.
292
293 As well as the standard hooks, a peer has a pinghook, which isn't used
294 directly by this class.
295
296 Hooks:
297
298 * pinghook(): invoked by the Pinger (q.v.) when ping statistics change
299
300 Attributes provided are:
301
302 * addr = a vaguely human-readable representation of the peer's address
303 * ifname = the peer's interface name
304 * tunnel = the kind of tunnel the peer is using
305 * keepalive = the peer's keepalive interval in seconds
306 * ping['EPING'] and ping['PING'] = pingstate statistics (maintained by
307 the Pinger)
308 """
309
310 def __init__(me, name):
311 """Initialize the object with the given name."""
312 MonitorObject.__init__(me, name)
313 me.pinghook = HookList()
314 me.__dict__.update(conn.algs(name))
315 me.update()
316
317 def update(me, hunoz = None):
318 """Update the peer, fetching information about it from the server."""
319 me._setaddr(conn.addr(me.name))
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."""
326 if addr[0] == 'INET':
327 af, ipaddr, port = addr
328 try:
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)
335 else:
336 me.addr = ' '.join(addr)
337
338 def setaddr(me, addr):
339 """Informs the object of a change to its address to ADDR."""
340 me._setaddr(addr)
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
348 class 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
365 class 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.
375
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
437 class 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
444 class 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
451 class 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)
488 for line in conn.svcsubmit('connect', 'list-active')]
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':
504 T.aside(me.peers.add, rest[0], None)
505 elif code == 'KILL':
506 T.aside(me.peers.remove, rest[0])
507 elif code == 'NEWIFNAME':
508 try:
509 me.peers[rest[0]].setifname(rest[2])
510 except KeyError:
511 pass
512 elif code == 'NEWADDR':
513 try:
514 me.peers[rest[0]].setaddr(rest[1:])
515 except KeyError:
516 pass
517 elif code == 'SVCCLAIM':
518 T.aside(me.services.add, rest[0], rest[1])
519 if rest[0] == 'connect':
520 T.aside(me._updateautopeers)
521 elif code == 'SVCRELEASE':
522 T.aside(me.services.remove, rest[0])
523 if rest[0] == 'connect':
524 T.aside(me._updateautopeers)
525 elif code == 'USER':
526 if not rest: return
527 if rest[0] == 'watch' and \
528 rest[1] == 'peerdb-update':
529 T.aside(me._updateautopeers)
530
531 ###--------------------------------------------------------------------------
532 ### Window management cruft.
533
534 class MyWindowMixin (G.Window, HookClient):
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
544 def mywininit(me):
545 """Initialization function. Note that it's not called __init__!"""
546 me.closehook = HookList()
547 HookClient.__init__(me)
548 me.connect('destroy', invoker(me.close))
549
550 def close(me):
551 """Close the window, invoking the closehook and releasing all hooks."""
552 me.closehook.run()
553 me.destroy()
554 me.unhookall()
555
556 class MyWindow (MyWindowMixin):
557 """A version of MyWindowMixin suitable as a single parent class."""
558 def __init__(me, kind = G.WINDOW_TOPLEVEL):
559 G.Window.__init__(me, kind)
560 me.mywininit()
561
562 class 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
570 class TrivialWindow (MyWindow, TrivialWindowMixin):
571 pass
572
573 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
574 """A dialogue box with a closehook and sensible button binding."""
575
576 def __init__(me, title = None, flags = 0, buttons = []):
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 """
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))
591 me.mywininit()
592 me.set_default_response(i - 1)
593 me.connect('response', me.respond)
594
595 def respond(me, hunoz, rid, *hukairz):
596 """Dispatch responses to the appropriate thunks."""
597 if rid >= 0: me.rmap[rid]()
598
599 def makeactiongroup(name, acts):
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 """
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
617 class GridPacker (G.Table):
618 """
619 Like a Table, but with more state: makes filling in the widgets easier.
620 """
621
622 def __init__(me):
623 """Initialize a new GridPacker."""
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)
632
633 def pack(me, w, width = 1, newlinep = False,
634 xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
635 xpad = 0, ypad = 0):
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 """
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
656
657 def labelled(me, lab, w, newlinep = False, **kw):
658 """
659 Packs a labelled widget.
660
661 Other arguments are as for pack. Returns W.
662 """
663 label = G.Label(lab + ' ')
664 label.set_alignment(1.0, 0)
665 me.pack(label, newlinep = newlinep, xopt = G.FILL)
666 me.pack(w, **kw)
667 return w
668
669 def info(me, label, text = None, len = 18, **kw):
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()
677 if text is not None: e.set_text(text)
678 e.set_width_chars(len)
679 e.set_selectable(True)
680 e.set_alignment(0.0, 0.5)
681 me.labelled(label, e, **kw)
682 return e
683
684 class WindowSlot (HookClient):
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 """
691 def __init__(me, createfunc):
692 """
693 Constructor: CREATEFUNC must return a new Window which supports the
694 closehook protocol.
695 """
696 HookClient.__init__(me)
697 me.createfunc = createfunc
698 me.window = None
699
700 def open(me):
701 """Opens the window, creating it if necessary."""
702 if me.window:
703 raise_window(me.window)
704 else:
705 me.window = me.createfunc()
706 me.hook(me.window.closehook, me.closed)
707
708 def closed(me):
709 """Handles the window being closed."""
710 me.unhook(me.window.closehook)
711 me.window = None
712
713 class MyTreeView (G.TreeView):
714 def __init__(me, model):
715 G.TreeView.__init__(me, model)
716 me.set_rules_hint(True)
717
718 class 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.
725 rx_num = RX.compile(r'^[-+]?\d+$')
726
727 ## The colour red.
728 c_red = GDK.color_parse('#ff6666')
729
730 class ValidationError (Exception):
731 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
732 pass
733
734 class ValidatingEntry (G.Entry):
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
742 def __init__(me, valid, text = '', size = -1, *arg, **kw):
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 """
750 G.Entry.__init__(me, *arg, **kw)
751 me.connect("changed", me._check)
752 me.connect("state-changed", me._check)
753 if callable(valid):
754 me.validate = valid
755 else:
756 me.validate = RX.compile(valid).match
757 me.ensure_style()
758 if size != -1: me.set_width_chars(size)
759 me.set_activates_default(True)
760 me.set_text(text)
761 me._check()
762
763 def _check(me, *hunoz):
764 """Check the current text and update validp and the text colour."""
765 if me.validate(G.Entry.get_text(me)):
766 me.validp = True
767 set_entry_bg(me, None)
768 else:
769 me.validp = False
770 set_entry_bg(me, me.is_sensitive() and c_red or None)
771
772 def get_text(me):
773 """
774 Return the text in the Entry if it's valid. If it isn't, raise
775 ValidationError.
776 """
777 if not me.validp:
778 raise ValidationError()
779 return G.Entry.get_text(me)
780
781 def numericvalidate(min = None, max = None):
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 """
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
792 ###--------------------------------------------------------------------------
793 ### Various minor dialog boxen.
794
795 GPL = """\
796 TrIPE is free software: you can redistribute it and/or modify it under
797 the terms of the GNU General Public License as published by the Free
798 Software Foundation; either version 3 of the License, or (at your
799 option) any later version.
800
801 TrIPE is distributed in the hope that it will be useful, but WITHOUT
802 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
803 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
804 for more details.
805
806 You should have received a copy of the GNU General Public License
807 along with TrIPE. If not, see <https://www.gnu.org/licenses/>."""
808
809 class AboutBox (G.AboutDialog, TrivialWindowMixin):
810 """The program `About' box."""
811 def __init__(me):
812 G.AboutDialog.__init__(me)
813 me.mywininit()
814 me.set_name('TrIPEmon')
815 me.set_version(T.VERSION)
816 me.set_license(GPL)
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)
821 me.show()
822 def respond(me, hunoz, rid, *hukairz):
823 if rid == G.RESPONSE_CANCEL:
824 me.close()
825 aboutbox = WindowSlot(AboutBox)
826
827 def moanbox(msg):
828 """Report an error message in a window."""
829 d = G.Dialog('Error from %s' % M.quis,
830 flags = G.DIALOG_MODAL,
831 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
832 label = G.Label(msg)
833 label.set_padding(20, 20)
834 d.vbox.pack_start(label, True, True, 0)
835 label.show()
836 d.run()
837 d.destroy()
838
839 def unimplemented(*hunoz):
840 """Indicator of laziness."""
841 moanbox("I've not written that bit yet.")
842
843 ###--------------------------------------------------------------------------
844 ### Logging windows.
845
846 class LogModel (G.ListStore):
847 """
848 A simple list of log messages, usable as the model for a TreeView.
849
850 The column headings are stored in the `cols' attribute.
851 """
852
853 def __init__(me, columns):
854 """
855 COLUMNS must be a list of column name strings. We add a time column to
856 the left.
857 """
858 me.cols = ('Time',) + columns
859 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
860
861 def add(me, *entries):
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)
869
870 class 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
878 class WarningLogModel (LogModel):
879 """
880 Log model for warnings.
881
882 We split the category out into a separate column.
883 """
884 def __init__(me):
885 LogModel.__init__(me, ('Category', 'Message'))
886 def notify(me, tag, *rest):
887 """Call with a new warning message."""
888 me.add(tag, ' '.join([T.quotify(w) for w in rest]))
889
890 class LogViewer (TrivialWindow):
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
902 def __init__(me, model):
903 """
904 Create a log viewer showing the LogModel MODEL.
905 """
906 TrivialWindow.__init__(me)
907 me.model = model
908 scr = MyScrolledWindow()
909 me.list = MyTreeView(me.model)
910 i = 0
911 for c in me.model.cols:
912 crt = G.CellRendererText()
913 me.list.append_column(G.TreeViewColumn(c, crt, text = i))
914 i += 1
915 crt.set_property('family', 'monospace')
916 me.set_default_size(440, 256)
917 scr.add(me.list)
918 me.add(scr)
919 me.show_all()
920
921 ###--------------------------------------------------------------------------
922 ### Pinging peers.
923
924 class 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
941 class 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,
970 lambda p: T.defer(me._q.put, (p, 'ADD', None)))
971 me.hook(monitor.peers.delhook,
972 lambda p: T.defer(me._q.put, (p, 'KILL', None)))
973 if conn.connectedp(): me.connected()
974
975 def _connected(me):
976 """Respond to connection: start pinging thngs."""
977 me._timer = GL.timeout_add(1000, me._timerfunc)
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."""
986 GL.source_remove(me._timer)
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
1038 class 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()
1058 me.vbox.pack_start(table, True, True, 0)
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))
1069 me.l_tunnel = table.labelled('Tunnel', combo_box_text(),
1070 newlinep = True, width = 3)
1071 me.tuns = ['(Default)'] + conn.tunnels()
1072 for t in me.tuns:
1073 me.l_tunnel.append_text(t)
1074 me.l_tunnel.set_active(0)
1075
1076 def tickybox_sensitivity(tickybox, target):
1077 tickybox.connect('toggled',
1078 lambda t: target.set_sensitive (t.get_active()))
1079
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)
1091
1092 me.c_cork = G.CheckButton('Cork')
1093 table.pack(me.c_cork, newlinep = True, width = 4, xopt = G.FILL)
1094
1095 me.c_mobile = G.CheckButton('Mobile')
1096 table.pack(me.c_mobile, newlinep = True, width = 4, xopt = G.FILL)
1097
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)
1102
1103 me.show_all()
1104
1105 def ok(me):
1106 """Handle an OK press: create the peer."""
1107 try:
1108 t = me.l_tunnel.get_active()
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,
1115 cork = me.c_cork.get_active() or None,
1116 mobile = me.c_mobile.get_active() or None,
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))
1121 except ValidationError:
1122 GDK.beep()
1123 return
1124
1125 @incr
1126 def _addpeer(me, *args, **kw):
1127 """Coroutine function: actually do the ADD command."""
1128 try:
1129 conn.add(*args, **kw)
1130 me.destroy()
1131 except T.TripeError, exc:
1132 T.defer(moanbox, ' '.join(exc))
1133
1134 class ServInfo (TrivialWindow):
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):
1145 TrivialWindow.__init__(me)
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
1188 class 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 == '+')
1204 me.vbox.pack_start(ticky, True, True, 0)
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.
1221
1222 def xlate_time(t):
1223 """Translate a TrIPE-format time to something human-readable."""
1224 if t == 'NEVER': return '(never)'
1225 YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1226 ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1227 ago = MATH.floor(ago); unit = 's'
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)
1234 def xlate_bytes(b):
1235 """Translate a raw byte count into something a human might want to read."""
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.
1246 statsxlate = \
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
1257 def format_stat(format, dict):
1258 if callable(format): return format(dict)
1259 else: return format % dict
1260
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.
1264 cryptolayout = \
1265 [('Diffie-Hellman group',
1266 '%(kx-group)s '
1267 '(%(kx-group-order-bits)s-bit order, '
1268 '%(kx-group-elt-bits)s-bit elements)'),
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'])))]
1285
1286 statslayout = \
1287 [('Start time', '%(start-time)s'),
1288 ('Private key', '%(current-key)s')] + \
1289 cryptolayout + \
1290 [('Last key-exchange', '%(last-keyexch-time)s'),
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',
1297 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1298 ('Rejected packets', '%(rejected-packets)s')]
1299
1300 class PeerWindow (TrivialWindow):
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
1318 TrivialWindow.__init__(me)
1319 me.set_title('TrIPE statistics: %s' % peer.name)
1320 me.peer = peer
1321
1322 table = GridPacker()
1323 me.add(table)
1324
1325 ## Utility for adding fields.
1326 me.e = {}
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.
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)
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)
1347 me.hook(me.closehook, me.stopupdate)
1348 me.hook(me.peer.deadhook, me.dead)
1349 me.hook(me.peer.changehook, me.change)
1350 me.hook(me.peer.pinghook, me.ping)
1351 me.cr = None
1352 me.doupdate = True
1353 me.tryupdate()
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.
1360 me.show_all()
1361
1362 def change(me):
1363 """Update the display in response to a notification."""
1364 me.e['Interface'].set_text(me.peer.ifname)
1365 me.e['Address'].set_text(me.peer.addr)
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])
1378 stat.update(me.peer.__dict__)
1379 for label, format in statslayout:
1380 me.e[label].set_text(format_stat(format, stat))
1381 GL.timeout_add(1000, lambda: me.cr.switch() and False)
1382 me.cr.parent.switch()
1383 me.cr = None
1384
1385 def tryupdate(me):
1386 """Start the updater coroutine, if it's not going already."""
1387 if me.cr is None:
1388 me.cr = T.Coroutine(me._update,
1389 name = 'update-peer-window %s' % me.peer.name)
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
1396 def dead(me):
1397 """Called when the peer is killed."""
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()
1401
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
1414 class CryptoInfo (TrivialWindow):
1415 """Simple display of cryptographic algorithms in use."""
1416 def __init__(me):
1417 TrivialWindow.__init__(me)
1418 me.set_title('Cryptographic algorithms')
1419 T.aside(me.populate)
1420 def populate(me):
1421 table = GridPacker()
1422 me.add(table)
1423
1424 crypto = conn.algs()
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
1430
1431 me.show_all()
1432
1433 ###--------------------------------------------------------------------------
1434 ### Main monitor window.
1435
1436 class MonitorWindow (MyWindow):
1437
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.
1467 MyWindow.__init__(me)
1468 me.set_title('TrIPE monitor')
1469
1470 ## Hook onto diagnostic outputs.
1471 me.warnings = WarningLogModel()
1472 me.hook(conn.warnhook, me.warnings.notify)
1473 me.trace = TraceLogModel()
1474 me.hook(conn.tracehook, me.trace.notify)
1475
1476 ## Make slots to store the various ancillary singleton windows.
1477 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1478 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1479 me.traceopts = WindowSlot(lambda: TraceOptions())
1480 me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1481 me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1482 me.servinfo = WindowSlot(lambda: ServInfo())
1483
1484 ## Main window structure.
1485 vbox = G.VBox()
1486 me.add(vbox)
1487
1488 ## UI manager makes our menus. (We're too cheap to have a toolbar.)
1489 me.ui = G.UIManager()
1490 actgroup = makeactiongroup('monitor',
1491 [('file-menu', '_File', None, None),
1492 ('connect', '_Connect', '<Control>C', conn.connect),
1493 ('disconnect', '_Disconnect', '<Control>D',
1494 lambda: conn.disconnect(None)),
1495 ('quit', '_Quit', '<Control>Q', me.close),
1496 ('server-menu', '_Server', None, None),
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),
1504 ('logs-menu', '_Logs', None, None),
1505 ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1506 ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1507 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1508 ('help-menu', '_Help', None, None),
1509 ('about', '_About tripemon...', None, aboutbox.open),
1510 ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1511 ('kill-peer', '_Kill peer', None, me.killpeer),
1512 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1513
1514 ## Menu structures.
1515 uidef = '''
1516 <ui>
1517 <menubar>
1518 <menu action="file-menu">
1519 <menuitem action="quit"/>
1520 </menu>
1521 <menu action="server-menu">
1522 <menuitem action="connect"/>
1523 <menuitem action="disconnect"/>
1524 <separator/>
1525 <menuitem action="server-version"/>
1526 <menuitem action="crypto-algs"/>
1527 <menuitem action="add-peer"/>
1528 <menuitem action="conn-peer"/>
1529 <menuitem action="daemon"/>
1530 <menuitem action="reload-keys"/>
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"/>
1545 <menuitem action="conn-peer"/>
1546 <menuitem action="kill-peer"/>
1547 <menuitem action="force-kx"/>
1548 </popup>
1549 </ui>
1550 '''
1551
1552 ## Populate the UI manager.
1553 me.ui.insert_action_group(actgroup, 0)
1554 me.ui.add_ui_from_string(uidef)
1555
1556 ## Construct the menu bar.
1557 vbox.pack_start(me.ui.get_widget('/menubar'), False, True, 0)
1558 me.add_accel_group(me.ui.get_accel_group())
1559
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.
1569 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1570 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1571 me.hook(monitor.peers.addhook, me.addpeer)
1572 me.hook(monitor.peers.delhook, me.delpeer)
1573 me.hook(monitor.autopeershook, me.apchange)
1574
1575 ## Construct the list viewer and put it in a scrolling window.
1576 scr = MyScrolledWindow()
1577 me.list = MyTreeView(me.listmodel)
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)
1598 vbox.pack_start(scr, True, True, 0)
1599
1600 ## Construct the status bar, and listen on hooks which report changes to
1601 ## connection status.
1602 me.status = G.Statusbar()
1603 vbox.pack_start(me.status, False, True, 0)
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)
1610
1611 def addpeer(me, peer):
1612 """Hook: announces that PEER has been added."""
1613 peer.i = me.listmodel.append([peer.name, peer.addr,
1614 '???', 'green', '???', 'green'])
1615 peer.win = WindowSlot(lambda: PeerWindow(peer))
1616 me.hook(peer.pinghook, me._ping)
1617 me.hook(peer.changehook, lambda: me._change(peer))
1618 me.apchange()
1619
1620 def delpeer(me, peer):
1621 """Hook: announces that PEER has been removed."""
1622 me.listmodel.remove(peer.i)
1623 me.unhook(peer.pinghook)
1624 me.apchange()
1625
1626 def path_peer(me, path):
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)
1662 ## Tick the peers which are actually connected.
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
1697 T.Coroutine(me._addautopeer_hack,
1698 name = '_addautopeerhack %s' % peer).switch(peer)
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:
1707 T.defer(moanbox, ' '.join(exc.args))
1708 me.apchange()
1709
1710 def activate(me, l, path, col):
1711 """
1712 Handle a double-click on a peer in the main list: open a PeerInfo window.
1713 """
1714 peer = me.path_peer(path)
1715 peer.win.open()
1716
1717 def buttonpress(me, l, ev):
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 """
1724 if ev.button == 3:
1725 x, y = int(ev.x), int(ev.y)
1726 r = me.list.get_path_at_pos(x, y)
1727 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1728 me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1729 r is not None)
1730 me.ui.get_widget('/peer-popup/conn-peer'). \
1731 set_sensitive(bool(monitor.autopeers))
1732 if r:
1733 me.menupeer = me.path_peer(r[0])
1734 else:
1735 me.menupeer = None
1736 me.ui.get_widget('/peer-popup').popup(
1737 None, None, None, ev.button, ev.time)
1738
1739 def killpeer(me):
1740 """Kill a peer from the popup menu."""
1741 cr(conn.kill, me.menupeer.name)()
1742
1743 def forcekx(me):
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'
1754 else:
1755 me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1756 me.listmodel[p.i][colourcol] = 'black'
1757
1758 def _change(me, p):
1759 """Hook: notified when the peer changes state."""
1760 me.listmodel[p.i][1] = p.addr
1761
1762 def setstatus(me, status):
1763 """Update the message in the status bar."""
1764 me.status.pop(0)
1765 me.status.push(0, status)
1766
1767 def notify(me, note, *rest):
1768 """Hook: invoked when interesting notifications occur."""
1769 if note == 'DAEMON':
1770 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1771
1772 def connected(me):
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())
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)
1786 me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1787 set_sensitive(bool(monitor.autopeers))
1788 me.ui.get_widget('/menubar/server-menu/daemon'). \
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 """
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',
1802 '/menubar/server-menu/conn-peer',
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)
1807 if reason: moanbox(reason)
1808
1809 ###--------------------------------------------------------------------------
1810 ### Main program.
1811
1812 def 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
1831 def init(opts):
1832 """Initialization."""
1833
1834 global conn, monitor, pinger
1835
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()
1843
1844 def main():
1845
1846 ## Main window.
1847 root = MonitorWindow()
1848 conn.connect()
1849 root.show_all()
1850
1851 ## Main loop.
1852 HookClient().hook(root.closehook, exit)
1853 conn.mainloop()
1854
1855 if __name__ == '__main__':
1856 opts = parse_options()
1857 init(opts)
1858 main()
1859
1860 ###----- That's all, folks --------------------------------------------------