2 ### -*- mode: python; coding: utf-8 -*-
4 ### Graphical monitor for tripe server
6 ### (c) 2007 Straylight/Edgeware
9 ###----- Licensing notice ---------------------------------------------------
11 ### This file is part of Trivial IP Encryption (TrIPE).
13 ### TrIPE is free software; you can redistribute it and/or modify
14 ### it under the terms of the GNU General Public License as published by
15 ### the Free Software Foundation; either version 2 of the License, or
16 ### (at your option) any later version.
18 ### TrIPE is distributed in the hope that it will be useful,
19 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ### GNU General Public License for more details.
23 ### You should have received a copy of the GNU General Public License
24 ### along with TrIPE; if not, write to the Free Software Foundation,
25 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
27 ###--------------------------------------------------------------------------
33 from sys import argv, exit, stdin, stdout, stderr, exc_info, excepthook
35 from os import environ
38 from optparse import OptionParser
41 from cStringIO import StringIO
49 if OS.getenv('TRIPE_DEBUG_MONITOR') is not None:
52 ###--------------------------------------------------------------------------
53 ### Doing things later.
56 """Report an uncaught exception."""
57 excepthook(*exc_info())
61 """Invoke the functions on the idles queue."""
66 for func, args, kw in old:
73 def idly(func, *args, **kw):
74 """Invoke FUNC(*ARGS, **KW) at some later point in time."""
76 GO.idle_add(_runidles)
77 _idles.append((func, args, kw))
82 Coroutine function: reads (FUNC, ARGS, KW) triples from a queue and invokes
86 func, args, kw = _asides.get()
92 def aside(func, *args, **kw):
94 Arrange for FUNC(*ARGS, **KW) to be performed at some point in the future,
95 and not from the main coroutine.
97 idly(_asides.put, (func, args, kw))
101 Return a function which behaves like FUNC, but reports exceptions via
106 return func(*args, **kw)
114 def invoker(func, *args, **kw):
116 Return a function which throws away its arguments and calls
119 If for loops worked by binding rather than assignment then we wouldn't need
122 return lambda *hunoz, **hukairz: xwrap(func)(*args, **kw)
124 def cr(func, *args, **kw):
125 """Return a function which invokes FUNC(*ARGS, **KW) in a coroutine."""
126 def _(*hunoz, **hukairz):
127 T.Coroutine(xwrap(func)).switch(*args, **kw)
131 """Decorator: runs its function in a coroutine of its own."""
132 return lambda *args, **kw: T.Coroutine(func).switch(*args, **kw)
134 ###--------------------------------------------------------------------------
135 ### Random bits of infrastructure.
137 ## Program name, shorn of extraneous stuff.
142 class HookList (object):
144 Notification hook list.
146 Other objects can add functions onto the hook list. When the hook list is
147 run, the functions are called in the order in which they were registered.
151 """Basic initialization: create the hook list."""
154 def add(me, func, obj):
155 """Add FUNC to the list of hook functions."""
156 me.list.append((obj, func))
159 """Remove hook functions registered with the given OBJ."""
166 def run(me, *args, **kw):
167 """Invoke the hook functions with arguments *ARGS and **KW."""
168 for o, hook in me.list:
169 rc = hook(*args, **kw)
170 if rc is not None: return rc
173 def runidly(me, *args, **kw):
175 Invoke the hook functions as for run, but at some point in the future.
177 idly(me.run, *args, **kw)
179 class HookClient (object):
181 Mixin for classes which are clients of hooks.
183 It keeps track of the hooks it's a client of, and has the ability to
184 extricate itself from all of them. This is useful because weak objects
185 don't seem to work well.
188 """Basic initialization."""
191 def hook(me, hk, func):
192 """Add FUNC to the hook list HK."""
197 """Remove myself from the hook list HK."""
202 """Remove myself from all hook lists."""
207 class struct (object):
208 """A very simple dumb data container object."""
209 def __init__(me, **kw):
210 me.__dict__.update(kw)
212 ## Matches ISO date format yyyy-mm-ddThh:mm:ss.
213 rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
215 ###--------------------------------------------------------------------------
218 class Connection (T.TripeCommandDispatcher):
220 The main connection to the server.
222 The improvement over the TripeCommandDispatcher is that the Connection
223 provides hooklists for NOTE, WARN and TRACE messages, and for connect and
226 This class knows about the Glib I/O dispatcher system, and plugs into it.
230 * connecthook(): a connection to the server has been established
231 * disconnecthook(): the connection has been dropped
232 * notehook(TOKEN, ...): server issued a notification
233 * warnhook(TOKEN, ...): server issued a warning
234 * tracehook(TOKEN, ...): server issued a trace message
237 def __init__(me, socket):
238 """Create a new Connection."""
239 T.TripeCommandDispatcher.__init__(me, socket)
240 me.connecthook = HookList()
241 me.disconnecthook = HookList()
242 me.notehook = HookList()
243 me.warnhook = HookList()
244 me.tracehook = HookList()
245 me.handler['NOTE'] = lambda _, *rest: me.notehook.run(*rest)
246 me.handler['WARN'] = lambda _, *rest: me.warnhook.run(*rest)
247 me.handler['TRACE'] = lambda _, *rest: me.tracehook.run(*rest)
251 """Handles reconnection to the server, and signals the hook."""
252 T.TripeCommandDispatcher.connected(me)
253 me._watch = GO.io_add_watch(me.sock, GO.IO_IN, invoker(me.receive))
256 def disconnected(me, reason):
257 """Handles disconnection from the server, and signals the hook."""
258 GO.source_remove(me._watch)
260 me.disconnecthook.run(reason)
261 T.TripeCommandDispatcher.disconnected(me, reason)
263 ###--------------------------------------------------------------------------
264 ### Watching the peers go by.
266 class MonitorObject (object):
268 An object with hooks it uses to notify others of changes in its state.
269 These are the objects tracked by the MonitorList class.
271 The object has a name, an `aliveness' state indicated by the `alivep' flag,
276 * changehook(): the object has changed its state
277 * deadhook(): the object has been destroyed
279 Subclass responsibilities:
281 * update(INFO): update internal state based on the provided INFO, and run
285 def __init__(me, name):
286 """Initialize the object with the given NAME."""
288 me.deadhook = HookList()
289 me.changehook = HookList()
293 """Mark the object as dead; invoke the deadhook."""
297 class Peer (MonitorObject):
299 An object representing a connected peer.
301 As well as the standard hooks, a peer has a pinghook, which isn't used
302 directly by this class.
306 * pinghook(): invoked by the Pinger (q.v.) when ping statistics change
308 Attributes provided are:
310 * addr = a vaguely human-readable representation of the peer's address
311 * ifname = the peer's interface name
312 * tunnel = the kind of tunnel the peer is using
313 * keepalive = the peer's keepalive interval in seconds
314 * ping['EPING'] and ping['PING'] = pingstate statistics (maintained by
318 def __init__(me, name):
319 """Initialize the object with the given name."""
320 MonitorObject.__init__(me, name)
321 me.pinghook = HookList()
324 def update(me, hunoz = None):
325 """Update the peer, fetching information about it from the server."""
326 addr = conn.addr(me.name)
327 if addr[0] == 'INET':
328 ipaddr, port = addr[1:]
330 name = S.gethostbyaddr(ipaddr)[0]
331 me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
333 me.addr = 'INET %s:%s' % (ipaddr, port)
335 me.addr = ' '.join(addr)
336 me.ifname = conn.ifname(me.name)
337 me.__dict__.update(conn.peerinfo(me.name))
340 def setifname(me, newname):
341 """Informs the object of a change to its interface name to NEWNAME."""
345 class Service (MonitorObject):
347 Represents a service.
349 Additional attributes are:
351 * version = the service version
353 def __init__(me, name, version):
354 MonitorObject.__init__(me, name)
357 def update(me, version):
358 """Tell the Service that its version has changed to VERSION."""
362 class MonitorList (object):
364 Maintains a collection of MonitorObjects.
366 The MonitorList can be indexed by name to retrieve the individual objects;
367 iteration generates the individual objects. More complicated operations
368 can be done on the `table' dictionary directly.
370 Hooks addhook(OBJ) and delhook(OBJ) are invoked when objects are added or
373 Subclass responsibilities:
375 * list(): return a list of (NAME, INFO) pairs.
377 * make(NAME, INFO): returns a new MonitorObject for the given NAME; INFO
378 is from the output of list().
382 """Initialize a new MonitorList."""
384 me.addhook = HookList()
385 me.delhook = HookList()
389 Refresh the list of objects:
391 We add new object which have appeared, delete ones which have vanished,
392 and update any which persist.
395 for name, stuff in me.list():
398 for name in me.table.copy():
402 def add(me, name, stuff):
404 Add a new object created by make(NAME, STUFF) if it doesn't already
405 exist. If it does, update it.
407 if name not in me.table:
408 obj = me.make(name, stuff)
412 me.table[name].update(stuff)
414 def remove(me, name):
416 Remove the object called NAME from the list.
418 The object becomes dead.
426 def __getitem__(me, name):
427 """Retrieve the object called NAME."""
428 return me.table[name]
431 """Iterate over the objects."""
432 return me.table.itervalues()
434 class PeerList (MonitorList):
435 """The list of the known peers."""
437 return [(name, None) for name in conn.list()]
438 def make(me, name, stuff):
441 class ServiceList (MonitorList):
442 """The list of the registered services."""
444 return conn.svclist()
445 def make(me, name, stuff):
446 return Service(name, stuff)
448 class Monitor (HookClient):
450 The main monitor: keeps track of the changes happening to the server.
452 Exports the peers, services MonitorLists, and a (plain Python) list
453 autopeers of peers which the connect service knows how to start by name.
457 * autopeershook(): invoked when the auto-peers list changes.
460 """Initialize the Monitor."""
461 HookClient.__init__(me)
462 me.peers = PeerList()
463 me.services = ServiceList()
464 me.hook(conn.connecthook, me._connected)
465 me.hook(conn.notehook, me._notify)
466 me.autopeershook = HookList()
470 """Handle a successful connection by starting the setup coroutine."""
475 """Coroutine function: initialize for a new connection."""
479 me._updateautopeers()
481 def _updateautopeers(me):
482 """Update the auto-peers list from the connect service."""
483 if 'connect' in me.services.table:
484 me.autopeers = [' '.join(line)
485 for line in conn.svcsubmit('connect', 'list')]
489 me.autopeershook.run()
491 def _notify(me, code, *rest):
493 Handle notifications from the server.
495 ADD, KILL and NEWIFNAME notifications get passed up to the PeerList;
496 SVCCLAIM and SVCRELEASE get passed up to the ServiceList. Finally,
497 peerdb-update notifications from the watch service cause us to refresh
501 aside(me.peers.add, rest[0], None)
503 aside(me.peers.remove, rest[0])
504 elif code == 'NEWIFNAME':
506 me.peers[rest[0]].setifname(rest[2])
509 elif code == 'SVCCLAIM':
510 aside(me.services.add, rest[0], rest[1])
511 if rest[0] == 'connect':
512 aside(me._updateautopeers)
513 elif code == 'SVCRELEASE':
514 aside(me.services.remove, rest[0])
515 if rest[0] == 'connect':
516 aside(me._updateautopeers)
519 if rest[0] == 'watch' and \
520 rest[1] == 'peerdb-update':
521 aside(me._updateautopeers)
523 ###--------------------------------------------------------------------------
524 ### Window management cruft.
526 class MyWindowMixin (G.Window, HookClient):
528 Mixin for windows which call a closehook when they're destroyed. It's also
529 a hookclient, and will release its hooks when it's destroyed.
533 * closehook(): called when the window is closed.
537 """Initialization function. Note that it's not called __init__!"""
538 me.closehook = HookList()
539 HookClient.__init__(me)
540 me.connect('destroy', invoker(me.close))
543 """Close the window, invoking the closehook and releasing all hooks."""
548 class MyWindow (MyWindowMixin):
549 """A version of MyWindowMixin suitable as a single parent class."""
550 def __init__(me, kind = G.WINDOW_TOPLEVEL):
551 G.Window.__init__(me, kind)
554 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
555 """A dialogue box with a closehook and sensible button binding."""
557 def __init__(me, title = None, flags = 0, buttons = []):
559 The BUTTONS are a list of (STOCKID, THUNK) pairs: call the appropriate
560 THUNK when the button is pressed. The other arguments are just like
571 G.Dialog.__init__(me, title, None, flags, tuple(br))
573 me.set_default_response(i - 1)
574 me.connect('response', me.respond)
576 def respond(me, hunoz, rid, *hukairz):
577 """Dispatch responses to the appropriate thunks."""
578 if rid >= 0: me.rmap[rid]()
580 def makeactiongroup(name, acts):
582 Creates an ActionGroup called NAME.
584 ACTS is a list of tuples containing:
586 * ACT: an action name
587 * LABEL: the label string for the action
588 * ACCEL: accelerator string, or None
589 * FUNC: thunk to call when the action is invoked
591 actgroup = G.ActionGroup(name)
592 for act, label, accel, func in acts:
593 a = G.Action(act, label, None, None)
594 if func: a.connect('activate', invoker(func))
595 actgroup.add_action_with_accel(a, accel)
598 class GridPacker (G.Table):
600 Like a Table, but with more state: makes filling in the widgets easier.
604 """Initialize a new GridPacker."""
610 me.set_border_width(4)
611 me.set_col_spacings(4)
612 me.set_row_spacings(4)
614 def pack(me, w, width = 1, newlinep = False,
615 xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
620 W is the widget to add. XOPY, YOPT, XPAD and YPAD are as for Table.
621 WIDTH is how many cells to take up horizontally. NEWLINEP is whether to
622 start a new line for this widget. Returns W.
628 right = me.col + width
629 if bot > me.rows or right > me.cols:
630 if bot > me.rows: me.rows = bot
631 if right > me.cols: me.cols = right
632 me.resize(me.rows, me.cols)
633 me.attach(w, me.col, me.col + width, me.row, me.row + 1,
634 xopt, yopt, xpad, ypad)
638 def labelled(me, lab, w, newlinep = False, **kw):
640 Packs a labelled widget.
642 Other arguments are as for pack. Returns W.
644 label = G.Label(lab + ' ')
645 label.set_alignment(1.0, 0)
646 me.pack(label, newlinep = newlinep, xopt = G.FILL)
650 def info(me, label, text = None, len = 18, **kw):
652 Packs an information widget with a label.
654 LABEL is the label; TEXT is the initial text; LEN is the estimated length
655 in characters. Returns the entry widget.
658 if text is not None: e.set_text(text)
659 e.set_width_chars(len)
660 e.set_selectable(True)
661 e.set_alignment(0.0, 0.5)
662 me.labelled(label, e, **kw)
665 class WindowSlot (HookClient):
667 A place to store a window -- specificially a MyWindowMixin.
669 If the window is destroyed, remember this; when we come to open the window,
670 raise it if it already exists; otherwise make a new one.
672 def __init__(me, createfunc):
674 Constructor: CREATEFUNC must return a new Window which supports the
677 HookClient.__init__(me)
678 me.createfunc = createfunc
682 """Opens the window, creating it if necessary."""
684 me.window.window.raise_()
686 me.window = me.createfunc()
687 me.hook(me.window.closehook, me.closed)
690 """Handles the window being closed."""
691 me.unhook(me.window.closehook)
694 class MyTreeView (G.TreeView):
695 def __init__(me, model):
696 G.TreeView.__init__(me, model)
697 me.set_rules_hint(True)
699 class MyScrolledWindow (G.ScrolledWindow):
701 G.ScrolledWindow.__init__(me)
702 me.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
703 me.set_shadow_type(G.SHADOW_IN)
705 ## Matches a signed integer.
706 rx_num = RX.compile(r'^[-+]?\d+$')
709 c_red = GDK.color_parse('red')
711 class ValidationError (Exception):
712 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
715 class ValidatingEntry (G.Entry):
717 Like an Entry, but makes the text go red if the contents are invalid.
719 If get_text is called, and the text is invalid, ValidationError is raised.
720 The attribute validp reflects whether the contents are currently valid.
723 def __init__(me, valid, text = '', size = -1, *arg, **kw):
725 Make a validating Entry.
727 VALID is a regular expression or a predicate on strings. TEXT is the
728 default text to insert. SIZE is the size of the box to set, in
729 characters (ish). Other arguments are passed to Entry.
731 G.Entry.__init__(me, *arg, **kw)
732 me.connect("changed", me.check)
736 me.validate = RX.compile(valid).match
738 me.c_ok = me.get_style().text[G.STATE_NORMAL]
740 if size != -1: me.set_width_chars(size)
741 me.set_activates_default(True)
745 def check(me, *hunoz):
746 """Check the current text and update validp and the text colour."""
747 if me.validate(G.Entry.get_text(me)):
749 me.modify_text(G.STATE_NORMAL, me.c_ok)
752 me.modify_text(G.STATE_NORMAL, me.c_bad)
756 Return the text in the Entry if it's valid. If it isn't, raise
760 raise ValidationError
761 return G.Entry.get_text(me)
763 def numericvalidate(min = None, max = None):
765 Return a validation function for numbers.
767 Entry must consist of an optional sign followed by digits, and the
768 resulting integer must be within the given bounds.
770 return lambda x: (rx_num.match(x) and
771 (min is None or long(x) >= min) and
772 (max is None or long(x) <= max))
774 ###--------------------------------------------------------------------------
775 ### Various minor dialog boxen.
777 GPL = """This program is free software; you can redistribute it and/or modify
778 it under the terms of the GNU General Public License as published by
779 the Free Software Foundation; either version 2 of the License, or
780 (at your option) any later version.
782 This program is distributed in the hope that it will be useful,
783 but WITHOUT ANY WARRANTY; without even the implied warranty of
784 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
785 GNU General Public License for more details.
787 You should have received a copy of the GNU General Public License
788 along with this program; if not, write to the Free Software Foundation,
789 Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
791 class AboutBox (G.AboutDialog, MyWindowMixin):
792 """The program `About' box."""
794 G.AboutDialog.__init__(me)
796 me.set_name('TrIPEmon')
797 me.set_version(T.VERSION)
799 me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
800 me.set_comments('A graphical monitor for the TrIPE VPN server')
801 me.set_copyright('Copyright © 2006-2008 Straylight/Edgeware')
802 me.connect('response', me.respond)
804 def respond(me, hunoz, rid, *hukairz):
805 if rid == G.RESPONSE_CANCEL:
807 aboutbox = WindowSlot(AboutBox)
810 """Report an error message in a window."""
811 d = G.Dialog('Error from %s' % M.quis,
812 flags = G.DIALOG_MODAL,
813 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
815 label.set_padding(20, 20)
816 d.vbox.pack_start(label)
821 def unimplemented(*hunoz):
822 """Indicator of laziness."""
823 moanbox("I've not written that bit yet.")
825 ###--------------------------------------------------------------------------
828 class LogModel (G.ListStore):
830 A simple list of log messages, usable as the model for a TreeView.
832 The column headings are stored in the `cols' attribute.
835 def __init__(me, columns):
837 COLUMNS must be a list of column name strings. We add a time column to
840 me.cols = ('Time',) + columns
841 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
843 def add(me, *entries):
845 Adds a new log message, with a timestamp.
847 The ENTRIES are the contents for the list columns.
849 now = TIME.strftime('%Y-%m-%d %H:%M:%S')
850 me.append((now, ) + entries)
852 class TraceLogModel (LogModel):
853 """Log model for trace messages."""
855 LogModel.__init__(me, ('Message',))
856 def notify(me, line):
857 """Call with a new trace message."""
860 class WarningLogModel (LogModel):
862 Log model for warnings.
864 We split the category out into a separate column.
867 LogModel.__init__(me, ('Category', 'Message'))
868 def notify(me, tag, *rest):
869 """Call with a new warning message."""
870 me.add(tag, ' '.join([T.quotify(w) for w in rest]))
872 class LogViewer (MyWindow):
876 Its contents are a TreeView showing the log.
880 * model: an appropriate LogModel
881 * list: a TreeView widget to display the log
884 def __init__(me, model):
886 Create a log viewer showing the LogModel MODEL.
888 MyWindow.__init__(me)
890 scr = MyScrolledWindow()
891 me.list = MyTreeView(me.model)
893 for c in me.model.cols:
894 crt = G.CellRendererText()
895 me.list.append_column(G.TreeViewColumn(c, crt, text = i))
897 crt.set_property('family', 'monospace')
898 me.set_default_size(440, 256)
903 ###--------------------------------------------------------------------------
906 class pingstate (struct):
908 Information kept for each peer by the Pinger.
910 Important attributes:
912 * peer = the peer name
913 * command = PING or EPING
914 * n = how many pings we've sent so far
915 * ngood = how many returned
916 * nmiss = how many didn't return
917 * nmissrun = how many pings since the last good one
918 * tlast = round-trip time for the last (good) ping
919 * ttot = total roung trip time
923 class Pinger (T.Coroutine, HookClient):
925 Coroutine which pings known peers and collects statistics.
927 Interesting attributes:
929 * _map: dict mapping peer names to Peer objects
930 * _q: event queue for notifying pinger coroutine
931 * _timer: gobject timer for waking the coroutine
936 Initialize the pinger.
938 We watch the monitor's PeerList to track which peers we should ping. We
939 maintain an event queue and put all the events on that.
941 The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
942 where CMD is 'PING' or 'EPING'.
944 T.Coroutine.__init__(me)
945 HookClient.__init__(me)
949 me.hook(conn.connecthook, me._connected)
950 me.hook(conn.disconnecthook, me._disconnected)
951 me.hook(monitor.peers.addhook,
952 lambda p: idly(me._q.put, (p, 'ADD', None)))
953 me.hook(monitor.peers.delhook,
954 lambda p: idly(me._q.put, (p, 'KILL', None)))
955 if conn.connectedp(): me.connected()
958 """Respond to connection: start pinging thngs."""
959 me._timer = GO.timeout_add(1000, me._timerfunc)
962 """Timer function: put a timer event on the queue."""
963 me._q.put((None, 'TIMER', None))
966 def _disconnected(me, reason):
967 """Respond to disconnection: stop pinging."""
968 GO.source_remove(me._timer)
972 Coroutine function: read events from the queue and process them.
976 * (PEER, 'KILL', None): remove PEER from the interesting peers list
977 * (PEER, 'ADD', None): add PEER to the list
978 * (PEER, 'INFO', TOKENS): result from a PING command
979 * (None, 'TIMER', None): interval timer went off: send more pings
982 tag, code, stuff = me._q.get()
987 elif not conn.connectedp():
992 for cmd in 'PING', 'EPING':
993 ps = pingstate(command = cmd, peer = p,
994 n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1000 if stuff[0] == 'ping-ok':
1010 ps.peer.pinghook.run(ps.peer, ps.command, ps)
1011 elif code == 'TIMER':
1012 for name, p in me._map.iteritems():
1013 for cmd, ps in p.ping.iteritems():
1014 conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
1015 cmd, '-background', conn.bgtag(), '--', name]))
1017 ###--------------------------------------------------------------------------
1018 ### Random dialogue boxes.
1020 class AddPeerDialog (MyDialog):
1022 Let the user create a new peer the low-level way.
1024 Interesting attributes:
1026 * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
1030 """Initialize the dialogue."""
1031 MyDialog.__init__(me, 'Add peer',
1032 buttons = [(G.STOCK_CANCEL, me.destroy),
1033 (G.STOCK_OK, me.ok)])
1038 """Coroutine function: background setup for AddPeerDialog."""
1039 table = GridPacker()
1040 me.vbox.pack_start(table)
1041 me.e_name = table.labelled('Name',
1042 ValidatingEntry(r'^[^\s.:]+$', '', 16),
1044 me.e_addr = table.labelled('Address',
1045 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
1047 me.e_port = table.labelled('Port',
1048 ValidatingEntry(numericvalidate(0, 65535),
1051 me.c_keepalive = G.CheckButton('Keepalives')
1052 me.l_tunnel = table.labelled('Tunnel',
1053 G.combo_box_new_text(),
1054 newlinep = True, width = 3)
1055 me.tuns = conn.tunnels()
1057 me.l_tunnel.append_text(t)
1058 me.l_tunnel.set_active(0)
1059 table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
1060 me.c_keepalive.connect('toggled',
1061 lambda t: me.e_keepalive.set_sensitive\
1063 me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
1064 me.e_keepalive.set_sensitive(False)
1065 table.pack(me.e_keepalive, width = 3)
1069 """Handle an OK press: create the peer."""
1071 if me.c_keepalive.get_active():
1072 ka = me.e_keepalive.get_text()
1075 t = me.l_tunnel.get_active()
1080 me._addpeer(me.e_name.get_text(),
1081 me.e_addr.get_text(),
1082 me.e_port.get_text(),
1085 except ValidationError:
1090 def _addpeer(me, name, addr, port, keepalive, tunnel):
1091 """Coroutine function: actually do the ADD command."""
1093 conn.add(name, addr, port, keepalive = keepalive, tunnel = tunnel)
1095 except T.TripeError, exc:
1096 idly(moanbox, ' '.join(exc))
1098 class ServInfo (MyWindow):
1100 Show information about the server and available services.
1102 Interesting attributes:
1104 * e: maps SERVINFO keys to entry widgets
1105 * svcs: Gtk ListStore describing services (columns are name and version)
1109 MyWindow.__init__(me)
1110 me.set_title('TrIPE server info')
1111 table = GridPacker()
1114 def add(label, tag, text = None, **kw):
1115 me.e[tag] = table.info(label, text, **kw)
1116 add('Implementation', 'implementation')
1117 add('Version', 'version', newlinep = True)
1118 me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1119 me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1120 scr = MyScrolledWindow()
1121 lb = MyTreeView(me.svcs)
1123 for title in 'Service', 'Version':
1124 lb.append_column(G.TreeViewColumn(
1125 title, G.CellRendererText(), text = i))
1127 for svc in monitor.services:
1128 me.svcs.append([svc.name, svc.version])
1130 table.pack(scr, width = 2, newlinep = True,
1131 yopt = G.EXPAND | G.FILL | G.SHRINK)
1133 me.hook(conn.connecthook, me.update)
1134 me.hook(monitor.services.addhook, me.addsvc)
1135 me.hook(monitor.services.delhook, me.delsvc)
1138 def addsvc(me, svc):
1139 me.svcs.append([svc.name, svc.version])
1141 def delsvc(me, svc):
1142 for i in xrange(len(me.svcs)):
1143 if me.svcs[i][0] == svc.name:
1144 me.svcs.remove(me.svcs.get_iter(i))
1148 info = conn.servinfo()
1150 me.e[i].set_text(info[i])
1152 class TraceOptions (MyDialog):
1153 """Tracing options window."""
1155 MyDialog.__init__(me, title = 'Tracing options',
1156 buttons = [(G.STOCK_CLOSE, me.destroy),
1157 (G.STOCK_OK, cr(me.ok))])
1163 for ch, st, desc in conn.trace():
1164 if ch.isupper(): continue
1165 text = desc[0].upper() + desc[1:]
1166 ticky = G.CheckButton(text)
1167 ticky.set_active(st == '+')
1168 me.vbox.pack_start(ticky)
1169 me.opts.append((ch, ticky))
1174 for ch, ticky in me.opts:
1175 if ticky.get_active():
1179 setting = ''.join(on) + '-' + ''.join(off)
1183 ###--------------------------------------------------------------------------
1187 """Translate a TrIPE-format time to something human-readable."""
1188 if t == 'NEVER': return '(never)'
1189 YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1190 ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1191 ago = MATH.floor(ago); unit = 's'
1192 for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1196 return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1197 (YY, MM, DD, hh, mm, ss, ago, unit)
1199 """Translate a number of bytes into something a human might want to read."""
1206 return '%d %s' % (b, suff)
1208 ## How to translate peer stats. Maps the stat name to a translation
1211 [('start-time', xlate_time),
1212 ('last-packet-time', xlate_time),
1213 ('last-keyexch-time', xlate_time),
1214 ('bytes-in', xlate_bytes),
1215 ('bytes-out', xlate_bytes),
1216 ('keyexch-bytes-in', xlate_bytes),
1217 ('keyexch-bytes-out', xlate_bytes),
1218 ('ip-bytes-in', xlate_bytes),
1219 ('ip-bytes-out', xlate_bytes)]
1221 ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
1222 ## the label to give the entry box; FORMAT is the format string to write into
1225 [('Start time', '%(start-time)s'),
1226 ('Last key-exchange', '%(last-keyexch-time)s'),
1227 ('Last packet', '%(last-packet-time)s'),
1229 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1230 ('Key-exchange in/out',
1231 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1233 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1234 ('Rejected packets', '%(rejected-packets)s')]
1236 class PeerWindow (MyWindow):
1238 Show information about a peer.
1240 This gives a graphical view of the server's peer statistics.
1242 Interesting attributes:
1244 * e: dict mapping keys (mostly matching label widget texts, though pings
1245 use command names) to entry widgets so that we can update them easily
1246 * peer: the peer this window shows information about
1247 * cr: the info-fetching coroutine, or None if crrrently disconnected
1248 * doupate: whether the info-fetching corouting should continue running
1251 def __init__(me, peer):
1252 """Construct a PeerWindow, showing information about PEER."""
1254 MyWindow.__init__(me)
1255 me.set_title('TrIPE statistics: %s' % peer.name)
1258 table = GridPacker()
1261 ## Utility for adding fields.
1263 def add(label, text = None, key = None):
1264 if key is None: key = label
1265 me.e[key] = table.info(label, text, len = 42, newlinep = True)
1267 ## Build the dialogue box.
1268 add('Peer name', peer.name)
1269 add('Tunnel', peer.tunnel)
1270 add('Interface', peer.ifname)
1272 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1273 add('Address', peer.addr)
1274 add('Transport pings', key = 'PING')
1275 add('Encrypted pings', key = 'EPING')
1277 for label, format in statslayout:
1280 ## Hook onto various interesting events.
1281 me.hook(conn.connecthook, me.tryupdate)
1282 me.hook(conn.disconnecthook, me.stopupdate)
1283 me.hook(me.closehook, me.stopupdate)
1284 me.hook(me.peer.deadhook, me.dead)
1285 me.hook(me.peer.changehook, me.change)
1286 me.hook(me.peer.pinghook, me.ping)
1291 ## Format the ping statistics.
1292 for cmd, ps in me.peer.ping.iteritems():
1293 me.ping(me.peer, cmd, ps)
1295 ## And show the window.
1299 """Update the display in response to a notification."""
1300 me.e['Interface'].set_text(me.peer.ifname)
1304 Main display-updating coroutine.
1306 This does an update, sleeps for a while, and starts again. If the
1307 me.doupdate flag goes low, we stop the loop.
1309 while me.peer.alivep and conn.connectedp() and me.doupdate:
1310 stat = conn.stats(me.peer.name)
1311 for s, trans in statsxlate:
1312 stat[s] = trans(stat[s])
1313 for label, format in statslayout:
1314 me.e[label].set_text(format % stat)
1315 GO.timeout_add(1000, lambda: me.cr.switch() and False)
1316 me.cr.parent.switch()
1320 """Start the updater coroutine, if it's not going already."""
1322 me.cr = T.Coroutine(me._update)
1325 def stopupdate(me, *hunoz, **hukairz):
1326 """Stop the update coroutine, by setting me.doupdate."""
1330 """Called when the peer is killed."""
1331 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1332 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1335 def ping(me, peer, cmd, ps):
1336 """Called when a ping result for the peer is reported."""
1337 s = '%d/%d' % (ps.ngood, ps.n)
1339 s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1341 s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1342 me.e[ps.command].set_text(s)
1344 ###--------------------------------------------------------------------------
1345 ### Cryptographic status.
1347 class CryptoInfo (MyWindow):
1348 """Simple display of cryptographic algorithms in use."""
1350 MyWindow.__init__(me)
1351 me.set_title('Cryptographic algorithms')
1354 table = GridPacker()
1357 crypto = conn.algs()
1358 table.info('Diffie-Hellman group',
1359 '%s (%d-bit order, %d-bit elements)' %
1360 (crypto['kx-group'],
1361 int(crypto['kx-group-order-bits']),
1362 int(crypto['kx-group-elt-bits'])),
1364 table.info('Data encryption',
1365 '%s (%d-bit key; %s)' %
1367 int(crypto['cipher-keysz']) * 8,
1368 crypto['cipher-blksz'] == '0'
1370 or '%d-bit block' % (int(crypto['cipher-blksz']) * 8)),
1372 table.info('Message authentication',
1373 '%s (%d-bit key; %d-bit tag)' %
1375 int(crypto['mac-keysz']) * 8,
1376 int(crypto['mac-tagsz']) * 8),
1378 table.info('Hash function',
1379 '%s (%d-bit output)' %
1381 int(crypto['hash-sz']) * 8),
1386 ###--------------------------------------------------------------------------
1387 ### Main monitor window.
1389 class MonitorWindow (MyWindow):
1392 The main monitor window.
1394 This class creates, populates and maintains the main monitor window.
1398 * warnings, trace: log models for server output
1399 * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1400 WindowSlot objects for ancillary windows
1401 * ui: Gtk UIManager object for the menu system
1402 * apmenu: pair of identical autoconnecting peer menus
1403 * listmodel: Gtk ListStore for connected peers; contains peer name,
1404 address, and ping times (transport and encrypted, value and colour)
1405 * status: Gtk Statusbar at the bottom of the window
1406 * _kidding: an unpleasant backchannel between the apchange method (which
1407 builds the apmenus) and the menu handler, forced on us by a Gtk
1410 Also installs attributes on Peer objects:
1412 * i: index of peer's entry in listmodel
1413 * win: WindowSlot object for the peer's PeerWindow
1417 """Construct the window."""
1420 MyWindow.__init__(me)
1421 me.set_title('TrIPE monitor')
1423 ## Hook onto diagnostic outputs.
1424 me.warnings = WarningLogModel()
1425 me.hook(conn.warnhook, me.warnings.notify)
1426 me.trace = TraceLogModel()
1427 me.hook(conn.tracehook, me.trace.notify)
1429 ## Make slots to store the various ancillary singleton windows.
1430 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1431 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1432 me.traceopts = WindowSlot(lambda: TraceOptions())
1433 me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1434 me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1435 me.servinfo = WindowSlot(lambda: ServInfo())
1437 ## Main window structure.
1441 ## UI manager makes our menus. (We're too cheap to have a toolbar.)
1442 me.ui = G.UIManager()
1443 actgroup = makeactiongroup('monitor',
1444 [('file-menu', '_File', None, None),
1445 ('connect', '_Connect', '<Control>C', conn.connect),
1446 ('disconnect', '_Disconnect', '<Control>D',
1447 lambda: conn.disconnect(None)),
1448 ('quit', '_Quit', '<Control>Q', me.close),
1449 ('server-menu', '_Server', None, None),
1450 ('daemon', 'Run in _background', None, cr(conn.daemon)),
1451 ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1452 ('crypto-algs', 'Cryptographic algorithms',
1453 '<Control>Y', me.cryptoinfo.open),
1454 ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1455 ('server-quit', 'Terminate server', None, cr(conn.quit)),
1456 ('conn-peer', 'Connect peer', None, None),
1457 ('logs-menu', '_Logs', None, None),
1458 ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1459 ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1460 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1461 ('help-menu', '_Help', None, None),
1462 ('about', '_About tripemon...', None, aboutbox.open),
1463 ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1464 ('kill-peer', '_Kill peer', None, me.killpeer),
1465 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1471 <menu action="file-menu">
1472 <menuitem action="quit"/>
1474 <menu action="server-menu">
1475 <menuitem action="connect"/>
1476 <menuitem action="disconnect"/>
1478 <menuitem action="server-version"/>
1479 <menuitem action="crypto-algs"/>
1480 <menuitem action="add-peer"/>
1481 <menuitem action="conn-peer"/>
1482 <menuitem action="daemon"/>
1483 <menuitem action="reload-keys"/>
1485 <menuitem action="server-quit"/>
1487 <menu action="logs-menu">
1488 <menuitem action="show-warnings"/>
1489 <menuitem action="show-trace"/>
1490 <menuitem action="trace-options"/>
1492 <menu action="help-menu">
1493 <menuitem action="about"/>
1496 <popup name="peer-popup">
1497 <menuitem action="add-peer"/>
1498 <menuitem action="conn-peer"/>
1499 <menuitem action="kill-peer"/>
1500 <menuitem action="force-kx"/>
1505 ## Populate the UI manager.
1506 me.ui.insert_action_group(actgroup, 0)
1507 me.ui.add_ui_from_string(uidef)
1509 ## Construct the menu bar.
1510 vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
1511 me.add_accel_group(me.ui.get_accel_group())
1513 ## Construct and attach the auto-peers menu. (This is a horrible bodge
1514 ## because we can't attach the same submenu in two different places.)
1515 me.apmenu = G.Menu(), G.Menu()
1516 me.ui.get_widget('/menubar/server-menu/conn-peer') \
1517 .set_submenu(me.apmenu[0])
1518 me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1520 ## Construct the main list model, and listen on hooks which report
1521 ## changes to the available peers.
1522 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1523 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1524 me.hook(monitor.peers.addhook, me.addpeer)
1525 me.hook(monitor.peers.delhook, me.delpeer)
1526 me.hook(monitor.autopeershook, me.apchange)
1528 ## Construct the list viewer and put it in a scrolling window.
1529 scr = MyScrolledWindow()
1530 me.list = MyTreeView(me.listmodel)
1531 me.list.append_column(G.TreeViewColumn('Peer name',
1532 G.CellRendererText(),
1534 me.list.append_column(G.TreeViewColumn('Address',
1535 G.CellRendererText(),
1537 me.list.append_column(G.TreeViewColumn('T-ping',
1538 G.CellRendererText(),
1541 me.list.append_column(G.TreeViewColumn('E-ping',
1542 G.CellRendererText(),
1545 me.list.get_column(1).set_expand(True)
1546 me.list.connect('row-activated', me.activate)
1547 me.list.connect('button-press-event', me.buttonpress)
1548 me.list.set_reorderable(True)
1549 me.list.get_selection().set_mode(G.SELECTION_NONE)
1551 vbox.pack_start(scr)
1553 ## Construct the status bar, and listen on hooks which report changes to
1554 ## connection status.
1555 me.status = G.Statusbar()
1556 vbox.pack_start(me.status, expand = False)
1557 me.hook(conn.connecthook, cr(me.connected))
1558 me.hook(conn.disconnecthook, me.disconnected)
1559 me.hook(conn.notehook, me.notify)
1561 ## Set a plausible default window size.
1562 me.set_default_size(512, 180)
1564 def addpeer(me, peer):
1565 """Hook: announces that PEER has been added."""
1566 peer.i = me.listmodel.append([peer.name, peer.addr,
1567 '???', 'green', '???', 'green'])
1568 peer.win = WindowSlot(lambda: PeerWindow(peer))
1569 me.hook(peer.pinghook, me._ping)
1572 def delpeer(me, peer):
1573 """Hook: announces that PEER has been removed."""
1574 me.listmodel.remove(peer.i)
1575 me.unhook(peer.pinghook)
1578 def path_peer(me, path):
1579 """Return the peer corresponding to a given list-model PATH."""
1580 return monitor.peers[me.listmodel[path][0]]
1584 Hook: announces that a change has been made to the peers available for
1585 automated connection.
1587 This populates both auto-peer menus and keeps them in sync. (As
1588 mentioned above, we can't attach the same submenu to two separate parent
1589 menu items. So we end up with two identical menus instead. Yes, this
1593 ## The set_active method of a CheckMenuItem works by maybe activating the
1594 ## menu item. This signals our handler. But we don't actually want to
1595 ## signal the handler unless the user actually frobbed the item. So the
1596 ## _kidding flag is used as an underhanded way of telling the handler
1597 ## that we don't actually want it to do anything. Of course, this sucks
1601 ## Iterate over the two menus.
1604 existing = menu.get_children()
1605 if monitor.autopeers is None:
1607 ## No peers, so empty out the menu.
1608 for item in existing:
1613 ## Insert the new items into the menu. (XXX this seems buggy XXX)
1614 ## Tick the peers which are actually connected.
1616 for peer in monitor.autopeers:
1617 if j < len(existing) and \
1618 existing[j].get_child().get_text() == peer:
1622 item = G.CheckMenuItem(peer, use_underline = False)
1623 item.connect('activate', invoker(me._addautopeer, peer))
1624 menu.insert(item, i)
1625 item.set_active(peer in monitor.peers.table)
1628 ## Make all the menu items visible.
1631 ## Set the parent menu items sensitive if and only if there are any peers
1633 for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1634 me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1636 ## And now allow the handler to do its business normally.
1639 def _addautopeer(me, peer):
1641 Automatically connect an auto-peer.
1643 This method is invoked from the main coroutine. Since the actual
1644 connection needs to issue administration commands, we must spawn a new
1645 child coroutine for it.
1649 T.Coroutine(me._addautopeer_hack).switch(peer)
1651 def _addautopeer_hack(me, peer):
1652 """Make an automated connection to PEER in response to a user click."""
1656 T._simple(conn.svcsubmit('connect', 'active', peer))
1657 except T.TripeError, exc:
1658 idly(moanbox, ' '.join(exc.args))
1661 def activate(me, l, path, col):
1663 Handle a double-click on a peer in the main list: open a PeerInfo window.
1665 peer = me.path_peer(path)
1668 def buttonpress(me, l, ev):
1670 Handle a mouse click on the main list.
1672 Currently we're only interested in button-3, which pops up the peer menu.
1673 For future reference, we stash the peer that was clicked in me.menupeer.
1676 x, y = int(ev.x), int(ev.y)
1677 r = me.list.get_path_at_pos(x, y)
1678 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1679 me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1681 me.ui.get_widget('/peer-popup/conn-peer'). \
1682 set_sensitive(bool(monitor.autopeers))
1684 me.menupeer = me.path_peer(r[0])
1687 me.ui.get_widget('/peer-popup').popup(
1688 None, None, None, ev.button, ev.time)
1691 """Kill a peer from the popup menu."""
1692 cr(conn.kill, me.menupeer.name)()
1695 """Kickstart a key-exchange from the popup menu."""
1696 cr(conn.forcekx, me.menupeer.name)()
1698 _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1699 def _ping(me, p, cmd, ps):
1700 """Hook: responds to ping reports."""
1701 textcol, colourcol = me._columnmap[cmd]
1703 me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1704 me.listmodel[p.i][colourcol] = 'red'
1706 me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1707 me.listmodel[p.i][colourcol] = 'black'
1709 def setstatus(me, status):
1710 """Update the message in the status bar."""
1712 me.status.push(0, status)
1714 def notify(me, note, *rest):
1715 """Hook: invoked when interesting notifications occur."""
1716 if note == 'DAEMON':
1717 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1721 Hook: invoked when a connection is made to the server.
1723 Make options which require a server connection sensitive.
1725 me.setstatus('Connected (port %s)' % conn.port())
1726 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1727 for i in ('/menubar/server-menu/disconnect',
1728 '/menubar/server-menu/server-version',
1729 '/menubar/server-menu/add-peer',
1730 '/menubar/server-menu/server-quit',
1731 '/menubar/logs-menu/trace-options'):
1732 me.ui.get_widget(i).set_sensitive(True)
1733 me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1734 set_sensitive(bool(monitor.autopeers))
1735 me.ui.get_widget('/menubar/server-menu/daemon'). \
1736 set_sensitive(conn.servinfo()['daemon'] == 'nil')
1738 def disconnected(me, reason):
1740 Hook: invoked when the connection to the server is lost.
1742 Make most options insensitive.
1744 me.setstatus('Disconnected')
1745 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1746 for i in ('/menubar/server-menu/disconnect',
1747 '/menubar/server-menu/server-version',
1748 '/menubar/server-menu/add-peer',
1749 '/menubar/server-menu/conn-peer',
1750 '/menubar/server-menu/daemon',
1751 '/menubar/server-menu/server-quit',
1752 '/menubar/logs-menu/trace-options'):
1753 me.ui.get_widget(i).set_sensitive(False)
1754 if reason: moanbox(reason)
1756 ###--------------------------------------------------------------------------
1759 def parse_options():
1761 Parse command-line options.
1763 Process the boring ones. Return all of them, for later.
1765 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1766 version = '%prog (tripe version 1.0.0)')
1767 op.add_option('-a', '--admin-socket',
1768 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1769 help = 'Select socket to connect to [default %default]')
1770 op.add_option('-d', '--directory',
1771 metavar = 'DIR', dest = 'dir', default = T.configdir,
1772 help = 'Select current diretory [default %default]')
1773 opts, args = op.parse_args()
1774 if args: op.error('no arguments permitted')
1779 """Initialization."""
1781 global conn, monitor, pinger
1783 ## Run jobs put off for later.
1784 T.Coroutine(_runasides).switch()
1786 ## Try to establish a connection.
1787 conn = Connection(opts.tripesock)
1789 ## Make the main interesting coroutines and objects.
1797 root = MonitorWindow()
1802 HookClient().hook(root.closehook, exit)
1805 if __name__ == '__main__':
1806 opts = parse_options()
1810 ###----- That's all, folks --------------------------------------------------