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 Return a function which behaves like FUNC, but reports exceptions via
66 return func(*args, **kw)
74 def invoker(func, *args, **kw):
76 Return a function which throws away its arguments and calls
79 If for loops worked by binding rather than assignment then we wouldn't need
82 return lambda *hunoz, **hukairz: xwrap(func)(*args, **kw)
84 def cr(func, *args, **kw):
85 """Return a function which invokes FUNC(*ARGS, **KW) in a coroutine."""
86 name = T.funargstr(func, args, kw)
87 return lambda *hunoz, **hukairz: \
88 T.Coroutine(xwrap(func), name = name).switch(*args, **kw)
91 """Decorator: runs its function in a coroutine of its own."""
92 return lambda *args, **kw: \
93 (T.Coroutine(func, name = T.funargstr(func, args, kw))
96 ###--------------------------------------------------------------------------
97 ### Random bits of infrastructure.
99 ## Program name, shorn of extraneous stuff.
104 class HookList (object):
106 Notification hook list.
108 Other objects can add functions onto the hook list. When the hook list is
109 run, the functions are called in the order in which they were registered.
113 """Basic initialization: create the hook list."""
116 def add(me, func, obj):
117 """Add FUNC to the list of hook functions."""
118 me.list.append((obj, func))
121 """Remove hook functions registered with the given OBJ."""
128 def run(me, *args, **kw):
129 """Invoke the hook functions with arguments *ARGS and **KW."""
130 for o, hook in me.list:
131 rc = hook(*args, **kw)
132 if rc is not None: return rc
135 class HookClient (object):
137 Mixin for classes which are clients of hooks.
139 It keeps track of the hooks it's a client of, and has the ability to
140 extricate itself from all of them. This is useful because weak objects
141 don't seem to work well.
144 """Basic initialization."""
147 def hook(me, hk, func):
148 """Add FUNC to the hook list HK."""
153 """Remove myself from the hook list HK."""
158 """Remove myself from all hook lists."""
163 class struct (object):
164 """A very simple dumb data container object."""
165 def __init__(me, **kw):
166 me.__dict__.update(kw)
168 ## Matches ISO date format yyyy-mm-ddThh:mm:ss.
169 rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
171 ###--------------------------------------------------------------------------
174 class GIOWatcher (object):
176 Monitor I/O events using glib.
178 def __init__(me, conn, mc = GO.main_context_default()):
182 def connected(me, sock):
183 me._watch = GO.io_add_watch(sock, GO.IO_IN,
184 lambda *hunoz: me._conn.receive())
185 def disconnected(me):
186 GO.source_remove(me._watch)
189 me._mc.iteration(True)
191 class Connection (T.TripeCommandDispatcher):
193 The main connection to the server.
195 The improvement over the TripeCommandDispatcher is that the Connection
196 provides hooklists for NOTE, WARN and TRACE messages, and for connect and
199 This class knows about the Glib I/O dispatcher system, and plugs into it.
203 * connecthook(): a connection to the server has been established
204 * disconnecthook(): the connection has been dropped
205 * notehook(TOKEN, ...): server issued a notification
206 * warnhook(TOKEN, ...): server issued a warning
207 * tracehook(TOKEN, ...): server issued a trace message
210 def __init__(me, socket):
211 """Create a new Connection."""
212 T.TripeCommandDispatcher.__init__(me, socket)
213 me.connecthook = HookList()
214 me.disconnecthook = HookList()
215 me.notehook = HookList()
216 me.warnhook = HookList()
217 me.tracehook = HookList()
218 me.handler['NOTE'] = lambda _, *rest: me.notehook.run(*rest)
219 me.handler['WARN'] = lambda _, *rest: me.warnhook.run(*rest)
220 me.handler['TRACE'] = lambda _, *rest: me.tracehook.run(*rest)
221 me.iowatch = GIOWatcher(me)
224 """Handles reconnection to the server, and signals the hook."""
225 T.TripeCommandDispatcher.connected(me)
228 def disconnected(me, reason):
229 """Handles disconnection from the server, and signals the hook."""
230 me.disconnecthook.run(reason)
231 T.TripeCommandDispatcher.disconnected(me, reason)
233 ###--------------------------------------------------------------------------
234 ### Watching the peers go by.
236 class MonitorObject (object):
238 An object with hooks it uses to notify others of changes in its state.
239 These are the objects tracked by the MonitorList class.
241 The object has a name, an `aliveness' state indicated by the `alivep' flag,
246 * changehook(): the object has changed its state
247 * deadhook(): the object has been destroyed
249 Subclass responsibilities:
251 * update(INFO): update internal state based on the provided INFO, and run
255 def __init__(me, name):
256 """Initialize the object with the given NAME."""
258 me.deadhook = HookList()
259 me.changehook = HookList()
263 """Mark the object as dead; invoke the deadhook."""
267 class Peer (MonitorObject):
269 An object representing a connected peer.
271 As well as the standard hooks, a peer has a pinghook, which isn't used
272 directly by this class.
276 * pinghook(): invoked by the Pinger (q.v.) when ping statistics change
278 Attributes provided are:
280 * addr = a vaguely human-readable representation of the peer's address
281 * ifname = the peer's interface name
282 * tunnel = the kind of tunnel the peer is using
283 * keepalive = the peer's keepalive interval in seconds
284 * ping['EPING'] and ping['PING'] = pingstate statistics (maintained by
288 def __init__(me, name):
289 """Initialize the object with the given name."""
290 MonitorObject.__init__(me, name)
291 me.pinghook = HookList()
294 def update(me, hunoz = None):
295 """Update the peer, fetching information about it from the server."""
296 me._setaddr(conn.addr(me.name))
297 me.ifname = conn.ifname(me.name)
298 me.__dict__.update(conn.peerinfo(me.name))
301 def _setaddr(me, addr):
302 """Set the peer's address."""
303 if addr[0] == 'INET':
304 ipaddr, port = addr[1:]
306 name = S.gethostbyaddr(ipaddr)[0]
307 me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
309 me.addr = 'INET %s:%s' % (ipaddr, port)
311 me.addr = ' '.join(addr)
313 def setaddr(me, addr):
314 """Informs the object of a change to its address to ADDR."""
318 def setifname(me, newname):
319 """Informs the object of a change to its interface name to NEWNAME."""
323 class Service (MonitorObject):
325 Represents a service.
327 Additional attributes are:
329 * version = the service version
331 def __init__(me, name, version):
332 MonitorObject.__init__(me, name)
335 def update(me, version):
336 """Tell the Service that its version has changed to VERSION."""
340 class MonitorList (object):
342 Maintains a collection of MonitorObjects.
344 The MonitorList can be indexed by name to retrieve the individual objects;
345 iteration generates the individual objects. More complicated operations
346 can be done on the `table' dictionary directly.
348 Hooks addhook(OBJ) and delhook(OBJ) are invoked when objects are added or
351 Subclass responsibilities:
353 * list(): return a list of (NAME, INFO) pairs.
355 * make(NAME, INFO): returns a new MonitorObject for the given NAME; INFO
356 is from the output of list().
360 """Initialize a new MonitorList."""
362 me.addhook = HookList()
363 me.delhook = HookList()
367 Refresh the list of objects:
369 We add new object which have appeared, delete ones which have vanished,
370 and update any which persist.
373 for name, stuff in me.list():
376 for name in me.table.copy():
380 def add(me, name, stuff):
382 Add a new object created by make(NAME, STUFF) if it doesn't already
383 exist. If it does, update it.
385 if name not in me.table:
386 obj = me.make(name, stuff)
390 me.table[name].update(stuff)
392 def remove(me, name):
394 Remove the object called NAME from the list.
396 The object becomes dead.
404 def __getitem__(me, name):
405 """Retrieve the object called NAME."""
406 return me.table[name]
409 """Iterate over the objects."""
410 return me.table.itervalues()
412 class PeerList (MonitorList):
413 """The list of the known peers."""
415 return [(name, None) for name in conn.list()]
416 def make(me, name, stuff):
419 class ServiceList (MonitorList):
420 """The list of the registered services."""
422 return conn.svclist()
423 def make(me, name, stuff):
424 return Service(name, stuff)
426 class Monitor (HookClient):
428 The main monitor: keeps track of the changes happening to the server.
430 Exports the peers, services MonitorLists, and a (plain Python) list
431 autopeers of peers which the connect service knows how to start by name.
435 * autopeershook(): invoked when the auto-peers list changes.
438 """Initialize the Monitor."""
439 HookClient.__init__(me)
440 me.peers = PeerList()
441 me.services = ServiceList()
442 me.hook(conn.connecthook, me._connected)
443 me.hook(conn.notehook, me._notify)
444 me.autopeershook = HookList()
448 """Handle a successful connection by starting the setup coroutine."""
453 """Coroutine function: initialize for a new connection."""
457 me._updateautopeers()
459 def _updateautopeers(me):
460 """Update the auto-peers list from the connect service."""
461 if 'connect' in me.services.table:
462 me.autopeers = [' '.join(line)
463 for line in conn.svcsubmit('connect', 'list-active')]
467 me.autopeershook.run()
469 def _notify(me, code, *rest):
471 Handle notifications from the server.
473 ADD, KILL and NEWIFNAME notifications get passed up to the PeerList;
474 SVCCLAIM and SVCRELEASE get passed up to the ServiceList. Finally,
475 peerdb-update notifications from the watch service cause us to refresh
479 T.aside(me.peers.add, rest[0], None)
481 T.aside(me.peers.remove, rest[0])
482 elif code == 'NEWIFNAME':
484 me.peers[rest[0]].setifname(rest[2])
487 elif code == 'NEWADDR':
489 me.peers[rest[0]].setaddr(rest[1:])
492 elif code == 'SVCCLAIM':
493 T.aside(me.services.add, rest[0], rest[1])
494 if rest[0] == 'connect':
495 T.aside(me._updateautopeers)
496 elif code == 'SVCRELEASE':
497 T.aside(me.services.remove, rest[0])
498 if rest[0] == 'connect':
499 T.aside(me._updateautopeers)
502 if rest[0] == 'watch' and \
503 rest[1] == 'peerdb-update':
504 T.aside(me._updateautopeers)
506 ###--------------------------------------------------------------------------
507 ### Window management cruft.
509 class MyWindowMixin (G.Window, HookClient):
511 Mixin for windows which call a closehook when they're destroyed. It's also
512 a hookclient, and will release its hooks when it's destroyed.
516 * closehook(): called when the window is closed.
520 """Initialization function. Note that it's not called __init__!"""
521 me.closehook = HookList()
522 HookClient.__init__(me)
523 me.connect('destroy', invoker(me.close))
526 """Close the window, invoking the closehook and releasing all hooks."""
531 class MyWindow (MyWindowMixin):
532 """A version of MyWindowMixin suitable as a single parent class."""
533 def __init__(me, kind = G.WINDOW_TOPLEVEL):
534 G.Window.__init__(me, kind)
537 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
538 """A dialogue box with a closehook and sensible button binding."""
540 def __init__(me, title = None, flags = 0, buttons = []):
542 The BUTTONS are a list of (STOCKID, THUNK) pairs: call the appropriate
543 THUNK when the button is pressed. The other arguments are just like
554 G.Dialog.__init__(me, title, None, flags, tuple(br))
556 me.set_default_response(i - 1)
557 me.connect('response', me.respond)
559 def respond(me, hunoz, rid, *hukairz):
560 """Dispatch responses to the appropriate thunks."""
561 if rid >= 0: me.rmap[rid]()
563 def makeactiongroup(name, acts):
565 Creates an ActionGroup called NAME.
567 ACTS is a list of tuples containing:
569 * ACT: an action name
570 * LABEL: the label string for the action
571 * ACCEL: accelerator string, or None
572 * FUNC: thunk to call when the action is invoked
574 actgroup = G.ActionGroup(name)
575 for act, label, accel, func in acts:
576 a = G.Action(act, label, None, None)
577 if func: a.connect('activate', invoker(func))
578 actgroup.add_action_with_accel(a, accel)
581 class GridPacker (G.Table):
583 Like a Table, but with more state: makes filling in the widgets easier.
587 """Initialize a new GridPacker."""
593 me.set_border_width(4)
594 me.set_col_spacings(4)
595 me.set_row_spacings(4)
597 def pack(me, w, width = 1, newlinep = False,
598 xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
603 W is the widget to add. XOPY, YOPT, XPAD and YPAD are as for Table.
604 WIDTH is how many cells to take up horizontally. NEWLINEP is whether to
605 start a new line for this widget. Returns W.
611 right = me.col + width
612 if bot > me.rows or right > me.cols:
613 if bot > me.rows: me.rows = bot
614 if right > me.cols: me.cols = right
615 me.resize(me.rows, me.cols)
616 me.attach(w, me.col, me.col + width, me.row, me.row + 1,
617 xopt, yopt, xpad, ypad)
621 def labelled(me, lab, w, newlinep = False, **kw):
623 Packs a labelled widget.
625 Other arguments are as for pack. Returns W.
627 label = G.Label(lab + ' ')
628 label.set_alignment(1.0, 0)
629 me.pack(label, newlinep = newlinep, xopt = G.FILL)
633 def info(me, label, text = None, len = 18, **kw):
635 Packs an information widget with a label.
637 LABEL is the label; TEXT is the initial text; LEN is the estimated length
638 in characters. Returns the entry widget.
641 if text is not None: e.set_text(text)
642 e.set_width_chars(len)
643 e.set_selectable(True)
644 e.set_alignment(0.0, 0.5)
645 me.labelled(label, e, **kw)
648 class WindowSlot (HookClient):
650 A place to store a window -- specificially a MyWindowMixin.
652 If the window is destroyed, remember this; when we come to open the window,
653 raise it if it already exists; otherwise make a new one.
655 def __init__(me, createfunc):
657 Constructor: CREATEFUNC must return a new Window which supports the
660 HookClient.__init__(me)
661 me.createfunc = createfunc
665 """Opens the window, creating it if necessary."""
667 me.window.window.raise_()
669 me.window = me.createfunc()
670 me.hook(me.window.closehook, me.closed)
673 """Handles the window being closed."""
674 me.unhook(me.window.closehook)
677 class MyTreeView (G.TreeView):
678 def __init__(me, model):
679 G.TreeView.__init__(me, model)
680 me.set_rules_hint(True)
682 class MyScrolledWindow (G.ScrolledWindow):
684 G.ScrolledWindow.__init__(me)
685 me.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
686 me.set_shadow_type(G.SHADOW_IN)
688 ## Matches a signed integer.
689 rx_num = RX.compile(r'^[-+]?\d+$')
692 c_red = GDK.color_parse('#ff6666')
694 class ValidationError (Exception):
695 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
698 class ValidatingEntry (G.Entry):
700 Like an Entry, but makes the text go red if the contents are invalid.
702 If get_text is called, and the text is invalid, ValidationError is raised.
703 The attribute validp reflects whether the contents are currently valid.
706 def __init__(me, valid, text = '', size = -1, *arg, **kw):
708 Make a validating Entry.
710 VALID is a regular expression or a predicate on strings. TEXT is the
711 default text to insert. SIZE is the size of the box to set, in
712 characters (ish). Other arguments are passed to Entry.
714 G.Entry.__init__(me, *arg, **kw)
715 me.connect("changed", me._check)
716 me.connect("state-changed", me._check)
720 me.validate = RX.compile(valid).match
722 if size != -1: me.set_width_chars(size)
723 me.set_activates_default(True)
727 def _check(me, *hunoz):
728 """Check the current text and update validp and the text colour."""
729 if me.validate(G.Entry.get_text(me)):
731 me.modify_base(G.STATE_NORMAL, None)
734 me.modify_base(G.STATE_NORMAL, me.is_sensitive() and c_red or None)
738 Return the text in the Entry if it's valid. If it isn't, raise
742 raise ValidationError
743 return G.Entry.get_text(me)
745 def numericvalidate(min = None, max = None):
747 Return a validation function for numbers.
749 Entry must consist of an optional sign followed by digits, and the
750 resulting integer must be within the given bounds.
752 return lambda x: (rx_num.match(x) and
753 (min is None or long(x) >= min) and
754 (max is None or long(x) <= max))
756 ###--------------------------------------------------------------------------
757 ### Various minor dialog boxen.
759 GPL = """This program is free software; you can redistribute it and/or modify
760 it under the terms of the GNU General Public License as published by
761 the Free Software Foundation; either version 2 of the License, or
762 (at your option) any later version.
764 This program is distributed in the hope that it will be useful,
765 but WITHOUT ANY WARRANTY; without even the implied warranty of
766 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
767 GNU General Public License for more details.
769 You should have received a copy of the GNU General Public License
770 along with this program; if not, write to the Free Software Foundation,
771 Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
773 class AboutBox (G.AboutDialog, MyWindowMixin):
774 """The program `About' box."""
776 G.AboutDialog.__init__(me)
778 me.set_name('TrIPEmon')
779 me.set_version(T.VERSION)
781 me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
782 me.set_comments('A graphical monitor for the TrIPE VPN server')
783 me.set_copyright('Copyright © 2006-2008 Straylight/Edgeware')
784 me.connect('response', me.respond)
786 def respond(me, hunoz, rid, *hukairz):
787 if rid == G.RESPONSE_CANCEL:
789 aboutbox = WindowSlot(AboutBox)
792 """Report an error message in a window."""
793 d = G.Dialog('Error from %s' % M.quis,
794 flags = G.DIALOG_MODAL,
795 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
797 label.set_padding(20, 20)
798 d.vbox.pack_start(label)
803 def unimplemented(*hunoz):
804 """Indicator of laziness."""
805 moanbox("I've not written that bit yet.")
807 ###--------------------------------------------------------------------------
810 class LogModel (G.ListStore):
812 A simple list of log messages, usable as the model for a TreeView.
814 The column headings are stored in the `cols' attribute.
817 def __init__(me, columns):
819 COLUMNS must be a list of column name strings. We add a time column to
822 me.cols = ('Time',) + columns
823 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
825 def add(me, *entries):
827 Adds a new log message, with a timestamp.
829 The ENTRIES are the contents for the list columns.
831 now = TIME.strftime('%Y-%m-%d %H:%M:%S')
832 me.append((now, ) + entries)
834 class TraceLogModel (LogModel):
835 """Log model for trace messages."""
837 LogModel.__init__(me, ('Message',))
838 def notify(me, line):
839 """Call with a new trace message."""
842 class WarningLogModel (LogModel):
844 Log model for warnings.
846 We split the category out into a separate column.
849 LogModel.__init__(me, ('Category', 'Message'))
850 def notify(me, tag, *rest):
851 """Call with a new warning message."""
852 me.add(tag, ' '.join([T.quotify(w) for w in rest]))
854 class LogViewer (MyWindow):
858 Its contents are a TreeView showing the log.
862 * model: an appropriate LogModel
863 * list: a TreeView widget to display the log
866 def __init__(me, model):
868 Create a log viewer showing the LogModel MODEL.
870 MyWindow.__init__(me)
872 scr = MyScrolledWindow()
873 me.list = MyTreeView(me.model)
875 for c in me.model.cols:
876 crt = G.CellRendererText()
877 me.list.append_column(G.TreeViewColumn(c, crt, text = i))
879 crt.set_property('family', 'monospace')
880 me.set_default_size(440, 256)
885 ###--------------------------------------------------------------------------
888 class pingstate (struct):
890 Information kept for each peer by the Pinger.
892 Important attributes:
894 * peer = the peer name
895 * command = PING or EPING
896 * n = how many pings we've sent so far
897 * ngood = how many returned
898 * nmiss = how many didn't return
899 * nmissrun = how many pings since the last good one
900 * tlast = round-trip time for the last (good) ping
901 * ttot = total roung trip time
905 class Pinger (T.Coroutine, HookClient):
907 Coroutine which pings known peers and collects statistics.
909 Interesting attributes:
911 * _map: dict mapping peer names to Peer objects
912 * _q: event queue for notifying pinger coroutine
913 * _timer: gobject timer for waking the coroutine
918 Initialize the pinger.
920 We watch the monitor's PeerList to track which peers we should ping. We
921 maintain an event queue and put all the events on that.
923 The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
924 where CMD is 'PING' or 'EPING'.
926 T.Coroutine.__init__(me)
927 HookClient.__init__(me)
931 me.hook(conn.connecthook, me._connected)
932 me.hook(conn.disconnecthook, me._disconnected)
933 me.hook(monitor.peers.addhook,
934 lambda p: T.defer(me._q.put, (p, 'ADD', None)))
935 me.hook(monitor.peers.delhook,
936 lambda p: T.defer(me._q.put, (p, 'KILL', None)))
937 if conn.connectedp(): me.connected()
940 """Respond to connection: start pinging thngs."""
941 me._timer = GO.timeout_add(1000, me._timerfunc)
944 """Timer function: put a timer event on the queue."""
945 me._q.put((None, 'TIMER', None))
948 def _disconnected(me, reason):
949 """Respond to disconnection: stop pinging."""
950 GO.source_remove(me._timer)
954 Coroutine function: read events from the queue and process them.
958 * (PEER, 'KILL', None): remove PEER from the interesting peers list
959 * (PEER, 'ADD', None): add PEER to the list
960 * (PEER, 'INFO', TOKENS): result from a PING command
961 * (None, 'TIMER', None): interval timer went off: send more pings
964 tag, code, stuff = me._q.get()
969 elif not conn.connectedp():
974 for cmd in 'PING', 'EPING':
975 ps = pingstate(command = cmd, peer = p,
976 n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
982 if stuff[0] == 'ping-ok':
992 ps.peer.pinghook.run(ps.peer, ps.command, ps)
993 elif code == 'TIMER':
994 for name, p in me._map.iteritems():
995 for cmd, ps in p.ping.iteritems():
996 conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
997 cmd, '-background', conn.bgtag(), '--', name]))
999 ###--------------------------------------------------------------------------
1000 ### Random dialogue boxes.
1002 class AddPeerDialog (MyDialog):
1004 Let the user create a new peer the low-level way.
1006 Interesting attributes:
1008 * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
1012 """Initialize the dialogue."""
1013 MyDialog.__init__(me, 'Add peer',
1014 buttons = [(G.STOCK_CANCEL, me.destroy),
1015 (G.STOCK_OK, me.ok)])
1020 """Coroutine function: background setup for AddPeerDialog."""
1021 table = GridPacker()
1022 me.vbox.pack_start(table)
1023 me.e_name = table.labelled('Name',
1024 ValidatingEntry(r'^[^\s.:]+$', '', 16),
1026 me.e_addr = table.labelled('Address',
1027 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
1029 me.e_port = table.labelled('Port',
1030 ValidatingEntry(numericvalidate(0, 65535),
1033 me.c_keepalive = G.CheckButton('Keepalives')
1034 me.l_tunnel = table.labelled('Tunnel',
1035 G.combo_box_new_text(),
1036 newlinep = True, width = 3)
1037 me.tuns = conn.tunnels()
1039 me.l_tunnel.append_text(t)
1040 me.l_tunnel.set_active(0)
1041 table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
1042 me.c_keepalive.connect('toggled',
1043 lambda t: me.e_keepalive.set_sensitive\
1045 me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
1046 me.e_keepalive.set_sensitive(False)
1047 table.pack(me.e_keepalive, width = 3)
1051 """Handle an OK press: create the peer."""
1053 if me.c_keepalive.get_active():
1054 ka = me.e_keepalive.get_text()
1057 t = me.l_tunnel.get_active()
1062 me._addpeer(me.e_name.get_text(),
1063 me.e_addr.get_text(),
1064 me.e_port.get_text(),
1067 except ValidationError:
1072 def _addpeer(me, name, addr, port, keepalive, tunnel):
1073 """Coroutine function: actually do the ADD command."""
1075 conn.add(name, addr, port, keepalive = keepalive, tunnel = tunnel)
1077 except T.TripeError, exc:
1078 T.defer(moanbox, ' '.join(exc))
1080 class ServInfo (MyWindow):
1082 Show information about the server and available services.
1084 Interesting attributes:
1086 * e: maps SERVINFO keys to entry widgets
1087 * svcs: Gtk ListStore describing services (columns are name and version)
1091 MyWindow.__init__(me)
1092 me.set_title('TrIPE server info')
1093 table = GridPacker()
1096 def add(label, tag, text = None, **kw):
1097 me.e[tag] = table.info(label, text, **kw)
1098 add('Implementation', 'implementation')
1099 add('Version', 'version', newlinep = True)
1100 me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1101 me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1102 scr = MyScrolledWindow()
1103 lb = MyTreeView(me.svcs)
1105 for title in 'Service', 'Version':
1106 lb.append_column(G.TreeViewColumn(
1107 title, G.CellRendererText(), text = i))
1109 for svc in monitor.services:
1110 me.svcs.append([svc.name, svc.version])
1112 table.pack(scr, width = 2, newlinep = True,
1113 yopt = G.EXPAND | G.FILL | G.SHRINK)
1115 me.hook(conn.connecthook, me.update)
1116 me.hook(monitor.services.addhook, me.addsvc)
1117 me.hook(monitor.services.delhook, me.delsvc)
1120 def addsvc(me, svc):
1121 me.svcs.append([svc.name, svc.version])
1123 def delsvc(me, svc):
1124 for i in xrange(len(me.svcs)):
1125 if me.svcs[i][0] == svc.name:
1126 me.svcs.remove(me.svcs.get_iter(i))
1130 info = conn.servinfo()
1132 me.e[i].set_text(info[i])
1134 class TraceOptions (MyDialog):
1135 """Tracing options window."""
1137 MyDialog.__init__(me, title = 'Tracing options',
1138 buttons = [(G.STOCK_CLOSE, me.destroy),
1139 (G.STOCK_OK, cr(me.ok))])
1145 for ch, st, desc in conn.trace():
1146 if ch.isupper(): continue
1147 text = desc[0].upper() + desc[1:]
1148 ticky = G.CheckButton(text)
1149 ticky.set_active(st == '+')
1150 me.vbox.pack_start(ticky)
1151 me.opts.append((ch, ticky))
1156 for ch, ticky in me.opts:
1157 if ticky.get_active():
1161 setting = ''.join(on) + '-' + ''.join(off)
1165 ###--------------------------------------------------------------------------
1169 """Translate a TrIPE-format time to something human-readable."""
1170 if t == 'NEVER': return '(never)'
1171 YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1172 ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1173 ago = MATH.floor(ago); unit = 's'
1174 for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1178 return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1179 (YY, MM, DD, hh, mm, ss, ago, unit)
1181 """Translate a number of bytes into something a human might want to read."""
1188 return '%d %s' % (b, suff)
1190 ## How to translate peer stats. Maps the stat name to a translation
1193 [('start-time', xlate_time),
1194 ('last-packet-time', xlate_time),
1195 ('last-keyexch-time', xlate_time),
1196 ('bytes-in', xlate_bytes),
1197 ('bytes-out', xlate_bytes),
1198 ('keyexch-bytes-in', xlate_bytes),
1199 ('keyexch-bytes-out', xlate_bytes),
1200 ('ip-bytes-in', xlate_bytes),
1201 ('ip-bytes-out', xlate_bytes)]
1203 ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
1204 ## the label to give the entry box; FORMAT is the format string to write into
1207 [('Start time', '%(start-time)s'),
1208 ('Last key-exchange', '%(last-keyexch-time)s'),
1209 ('Last packet', '%(last-packet-time)s'),
1211 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1212 ('Key-exchange in/out',
1213 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1215 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1216 ('Rejected packets', '%(rejected-packets)s')]
1218 class PeerWindow (MyWindow):
1220 Show information about a peer.
1222 This gives a graphical view of the server's peer statistics.
1224 Interesting attributes:
1226 * e: dict mapping keys (mostly matching label widget texts, though pings
1227 use command names) to entry widgets so that we can update them easily
1228 * peer: the peer this window shows information about
1229 * cr: the info-fetching coroutine, or None if crrrently disconnected
1230 * doupate: whether the info-fetching corouting should continue running
1233 def __init__(me, peer):
1234 """Construct a PeerWindow, showing information about PEER."""
1236 MyWindow.__init__(me)
1237 me.set_title('TrIPE statistics: %s' % peer.name)
1240 table = GridPacker()
1243 ## Utility for adding fields.
1245 def add(label, text = None, key = None):
1246 if key is None: key = label
1247 me.e[key] = table.info(label, text, len = 42, newlinep = True)
1249 ## Build the dialogue box.
1250 add('Peer name', peer.name)
1251 add('Tunnel', peer.tunnel)
1252 add('Interface', peer.ifname)
1254 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1255 add('Address', peer.addr)
1256 add('Transport pings', key = 'PING')
1257 add('Encrypted pings', key = 'EPING')
1259 for label, format in statslayout:
1262 ## Hook onto various interesting events.
1263 me.hook(conn.connecthook, me.tryupdate)
1264 me.hook(conn.disconnecthook, me.stopupdate)
1265 me.hook(me.closehook, me.stopupdate)
1266 me.hook(me.peer.deadhook, me.dead)
1267 me.hook(me.peer.changehook, me.change)
1268 me.hook(me.peer.pinghook, me.ping)
1273 ## Format the ping statistics.
1274 for cmd, ps in me.peer.ping.iteritems():
1275 me.ping(me.peer, cmd, ps)
1277 ## And show the window.
1281 """Update the display in response to a notification."""
1282 me.e['Interface'].set_text(me.peer.ifname)
1286 Main display-updating coroutine.
1288 This does an update, sleeps for a while, and starts again. If the
1289 me.doupdate flag goes low, we stop the loop.
1291 while me.peer.alivep and conn.connectedp() and me.doupdate:
1292 stat = conn.stats(me.peer.name)
1293 for s, trans in statsxlate:
1294 stat[s] = trans(stat[s])
1295 for label, format in statslayout:
1296 me.e[label].set_text(format % stat)
1297 GO.timeout_add(1000, lambda: me.cr.switch() and False)
1298 me.cr.parent.switch()
1302 """Start the updater coroutine, if it's not going already."""
1304 me.cr = T.Coroutine(me._update,
1305 name = 'update-peer-window %s' % me.peer.name)
1308 def stopupdate(me, *hunoz, **hukairz):
1309 """Stop the update coroutine, by setting me.doupdate."""
1313 """Called when the peer is killed."""
1314 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1315 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1318 def ping(me, peer, cmd, ps):
1319 """Called when a ping result for the peer is reported."""
1320 s = '%d/%d' % (ps.ngood, ps.n)
1322 s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1324 s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1325 me.e[ps.command].set_text(s)
1327 ###--------------------------------------------------------------------------
1328 ### Cryptographic status.
1330 class CryptoInfo (MyWindow):
1331 """Simple display of cryptographic algorithms in use."""
1333 MyWindow.__init__(me)
1334 me.set_title('Cryptographic algorithms')
1335 T.aside(me.populate)
1337 table = GridPacker()
1340 crypto = conn.algs()
1341 table.info('Diffie-Hellman group',
1342 '%s (%d-bit order, %d-bit elements)' %
1343 (crypto['kx-group'],
1344 int(crypto['kx-group-order-bits']),
1345 int(crypto['kx-group-elt-bits'])),
1347 table.info('Data encryption',
1348 '%s (%d-bit key; %s)' %
1350 int(crypto['cipher-keysz']) * 8,
1351 crypto['cipher-blksz'] == '0'
1353 or '%d-bit block' % (int(crypto['cipher-blksz']) * 8)),
1355 table.info('Message authentication',
1356 '%s (%d-bit key; %d-bit tag)' %
1358 int(crypto['mac-keysz']) * 8,
1359 int(crypto['mac-tagsz']) * 8),
1361 table.info('Hash function',
1362 '%s (%d-bit output)' %
1364 int(crypto['hash-sz']) * 8),
1369 ###--------------------------------------------------------------------------
1370 ### Main monitor window.
1372 class MonitorWindow (MyWindow):
1375 The main monitor window.
1377 This class creates, populates and maintains the main monitor window.
1381 * warnings, trace: log models for server output
1382 * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1383 WindowSlot objects for ancillary windows
1384 * ui: Gtk UIManager object for the menu system
1385 * apmenu: pair of identical autoconnecting peer menus
1386 * listmodel: Gtk ListStore for connected peers; contains peer name,
1387 address, and ping times (transport and encrypted, value and colour)
1388 * status: Gtk Statusbar at the bottom of the window
1389 * _kidding: an unpleasant backchannel between the apchange method (which
1390 builds the apmenus) and the menu handler, forced on us by a Gtk
1393 Also installs attributes on Peer objects:
1395 * i: index of peer's entry in listmodel
1396 * win: WindowSlot object for the peer's PeerWindow
1400 """Construct the window."""
1403 MyWindow.__init__(me)
1404 me.set_title('TrIPE monitor')
1406 ## Hook onto diagnostic outputs.
1407 me.warnings = WarningLogModel()
1408 me.hook(conn.warnhook, me.warnings.notify)
1409 me.trace = TraceLogModel()
1410 me.hook(conn.tracehook, me.trace.notify)
1412 ## Make slots to store the various ancillary singleton windows.
1413 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1414 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1415 me.traceopts = WindowSlot(lambda: TraceOptions())
1416 me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1417 me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1418 me.servinfo = WindowSlot(lambda: ServInfo())
1420 ## Main window structure.
1424 ## UI manager makes our menus. (We're too cheap to have a toolbar.)
1425 me.ui = G.UIManager()
1426 actgroup = makeactiongroup('monitor',
1427 [('file-menu', '_File', None, None),
1428 ('connect', '_Connect', '<Control>C', conn.connect),
1429 ('disconnect', '_Disconnect', '<Control>D',
1430 lambda: conn.disconnect(None)),
1431 ('quit', '_Quit', '<Control>Q', me.close),
1432 ('server-menu', '_Server', None, None),
1433 ('daemon', 'Run in _background', None, cr(conn.daemon)),
1434 ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1435 ('crypto-algs', 'Cryptographic algorithms',
1436 '<Control>Y', me.cryptoinfo.open),
1437 ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1438 ('server-quit', 'Terminate server', None, cr(conn.quit)),
1439 ('conn-peer', 'Connect peer', None, None),
1440 ('logs-menu', '_Logs', None, None),
1441 ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1442 ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1443 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1444 ('help-menu', '_Help', None, None),
1445 ('about', '_About tripemon...', None, aboutbox.open),
1446 ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1447 ('kill-peer', '_Kill peer', None, me.killpeer),
1448 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1454 <menu action="file-menu">
1455 <menuitem action="quit"/>
1457 <menu action="server-menu">
1458 <menuitem action="connect"/>
1459 <menuitem action="disconnect"/>
1461 <menuitem action="server-version"/>
1462 <menuitem action="crypto-algs"/>
1463 <menuitem action="add-peer"/>
1464 <menuitem action="conn-peer"/>
1465 <menuitem action="daemon"/>
1466 <menuitem action="reload-keys"/>
1468 <menuitem action="server-quit"/>
1470 <menu action="logs-menu">
1471 <menuitem action="show-warnings"/>
1472 <menuitem action="show-trace"/>
1473 <menuitem action="trace-options"/>
1475 <menu action="help-menu">
1476 <menuitem action="about"/>
1479 <popup name="peer-popup">
1480 <menuitem action="add-peer"/>
1481 <menuitem action="conn-peer"/>
1482 <menuitem action="kill-peer"/>
1483 <menuitem action="force-kx"/>
1488 ## Populate the UI manager.
1489 me.ui.insert_action_group(actgroup, 0)
1490 me.ui.add_ui_from_string(uidef)
1492 ## Construct the menu bar.
1493 vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
1494 me.add_accel_group(me.ui.get_accel_group())
1496 ## Construct and attach the auto-peers menu. (This is a horrible bodge
1497 ## because we can't attach the same submenu in two different places.)
1498 me.apmenu = G.Menu(), G.Menu()
1499 me.ui.get_widget('/menubar/server-menu/conn-peer') \
1500 .set_submenu(me.apmenu[0])
1501 me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1503 ## Construct the main list model, and listen on hooks which report
1504 ## changes to the available peers.
1505 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1506 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1507 me.hook(monitor.peers.addhook, me.addpeer)
1508 me.hook(monitor.peers.delhook, me.delpeer)
1509 me.hook(monitor.autopeershook, me.apchange)
1511 ## Construct the list viewer and put it in a scrolling window.
1512 scr = MyScrolledWindow()
1513 me.list = MyTreeView(me.listmodel)
1514 me.list.append_column(G.TreeViewColumn('Peer name',
1515 G.CellRendererText(),
1517 me.list.append_column(G.TreeViewColumn('Address',
1518 G.CellRendererText(),
1520 me.list.append_column(G.TreeViewColumn('T-ping',
1521 G.CellRendererText(),
1524 me.list.append_column(G.TreeViewColumn('E-ping',
1525 G.CellRendererText(),
1528 me.list.get_column(1).set_expand(True)
1529 me.list.connect('row-activated', me.activate)
1530 me.list.connect('button-press-event', me.buttonpress)
1531 me.list.set_reorderable(True)
1532 me.list.get_selection().set_mode(G.SELECTION_NONE)
1534 vbox.pack_start(scr)
1536 ## Construct the status bar, and listen on hooks which report changes to
1537 ## connection status.
1538 me.status = G.Statusbar()
1539 vbox.pack_start(me.status, expand = False)
1540 me.hook(conn.connecthook, cr(me.connected))
1541 me.hook(conn.disconnecthook, me.disconnected)
1542 me.hook(conn.notehook, me.notify)
1544 ## Set a plausible default window size.
1545 me.set_default_size(512, 180)
1547 def addpeer(me, peer):
1548 """Hook: announces that PEER has been added."""
1549 peer.i = me.listmodel.append([peer.name, peer.addr,
1550 '???', 'green', '???', 'green'])
1551 peer.win = WindowSlot(lambda: PeerWindow(peer))
1552 me.hook(peer.pinghook, me._ping)
1555 def delpeer(me, peer):
1556 """Hook: announces that PEER has been removed."""
1557 me.listmodel.remove(peer.i)
1558 me.unhook(peer.pinghook)
1561 def path_peer(me, path):
1562 """Return the peer corresponding to a given list-model PATH."""
1563 return monitor.peers[me.listmodel[path][0]]
1567 Hook: announces that a change has been made to the peers available for
1568 automated connection.
1570 This populates both auto-peer menus and keeps them in sync. (As
1571 mentioned above, we can't attach the same submenu to two separate parent
1572 menu items. So we end up with two identical menus instead. Yes, this
1576 ## The set_active method of a CheckMenuItem works by maybe activating the
1577 ## menu item. This signals our handler. But we don't actually want to
1578 ## signal the handler unless the user actually frobbed the item. So the
1579 ## _kidding flag is used as an underhanded way of telling the handler
1580 ## that we don't actually want it to do anything. Of course, this sucks
1584 ## Iterate over the two menus.
1587 existing = menu.get_children()
1588 if monitor.autopeers is None:
1590 ## No peers, so empty out the menu.
1591 for item in existing:
1596 ## Insert the new items into the menu. (XXX this seems buggy XXX)
1597 ## Tick the peers which are actually connected.
1599 for peer in monitor.autopeers:
1600 if j < len(existing) and \
1601 existing[j].get_child().get_text() == peer:
1605 item = G.CheckMenuItem(peer, use_underline = False)
1606 item.connect('activate', invoker(me._addautopeer, peer))
1607 menu.insert(item, i)
1608 item.set_active(peer in monitor.peers.table)
1611 ## Make all the menu items visible.
1614 ## Set the parent menu items sensitive if and only if there are any peers
1616 for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1617 me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1619 ## And now allow the handler to do its business normally.
1622 def _addautopeer(me, peer):
1624 Automatically connect an auto-peer.
1626 This method is invoked from the main coroutine. Since the actual
1627 connection needs to issue administration commands, we must spawn a new
1628 child coroutine for it.
1632 T.Coroutine(me._addautopeer_hack,
1633 name = '_addautopeerhack %s' % peer).switch(peer)
1635 def _addautopeer_hack(me, peer):
1636 """Make an automated connection to PEER in response to a user click."""
1640 T._simple(conn.svcsubmit('connect', 'active', peer))
1641 except T.TripeError, exc:
1642 T.defer(moanbox, ' '.join(exc.args))
1645 def activate(me, l, path, col):
1647 Handle a double-click on a peer in the main list: open a PeerInfo window.
1649 peer = me.path_peer(path)
1652 def buttonpress(me, l, ev):
1654 Handle a mouse click on the main list.
1656 Currently we're only interested in button-3, which pops up the peer menu.
1657 For future reference, we stash the peer that was clicked in me.menupeer.
1660 x, y = int(ev.x), int(ev.y)
1661 r = me.list.get_path_at_pos(x, y)
1662 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1663 me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1665 me.ui.get_widget('/peer-popup/conn-peer'). \
1666 set_sensitive(bool(monitor.autopeers))
1668 me.menupeer = me.path_peer(r[0])
1671 me.ui.get_widget('/peer-popup').popup(
1672 None, None, None, ev.button, ev.time)
1675 """Kill a peer from the popup menu."""
1676 cr(conn.kill, me.menupeer.name)()
1679 """Kickstart a key-exchange from the popup menu."""
1680 cr(conn.forcekx, me.menupeer.name)()
1682 _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1683 def _ping(me, p, cmd, ps):
1684 """Hook: responds to ping reports."""
1685 textcol, colourcol = me._columnmap[cmd]
1687 me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1688 me.listmodel[p.i][colourcol] = 'red'
1690 me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1691 me.listmodel[p.i][colourcol] = 'black'
1693 def setstatus(me, status):
1694 """Update the message in the status bar."""
1696 me.status.push(0, status)
1698 def notify(me, note, *rest):
1699 """Hook: invoked when interesting notifications occur."""
1700 if note == 'DAEMON':
1701 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1705 Hook: invoked when a connection is made to the server.
1707 Make options which require a server connection sensitive.
1709 me.setstatus('Connected (port %s)' % conn.port())
1710 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1711 for i in ('/menubar/server-menu/disconnect',
1712 '/menubar/server-menu/server-version',
1713 '/menubar/server-menu/add-peer',
1714 '/menubar/server-menu/server-quit',
1715 '/menubar/logs-menu/trace-options'):
1716 me.ui.get_widget(i).set_sensitive(True)
1717 me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1718 set_sensitive(bool(monitor.autopeers))
1719 me.ui.get_widget('/menubar/server-menu/daemon'). \
1720 set_sensitive(conn.servinfo()['daemon'] == 'nil')
1722 def disconnected(me, reason):
1724 Hook: invoked when the connection to the server is lost.
1726 Make most options insensitive.
1728 me.setstatus('Disconnected')
1729 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1730 for i in ('/menubar/server-menu/disconnect',
1731 '/menubar/server-menu/server-version',
1732 '/menubar/server-menu/add-peer',
1733 '/menubar/server-menu/conn-peer',
1734 '/menubar/server-menu/daemon',
1735 '/menubar/server-menu/server-quit',
1736 '/menubar/logs-menu/trace-options'):
1737 me.ui.get_widget(i).set_sensitive(False)
1738 if reason: moanbox(reason)
1740 ###--------------------------------------------------------------------------
1743 def parse_options():
1745 Parse command-line options.
1747 Process the boring ones. Return all of them, for later.
1749 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1750 version = '%prog (tripe version 1.0.0)')
1751 op.add_option('-a', '--admin-socket',
1752 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1753 help = 'Select socket to connect to [default %default]')
1754 op.add_option('-d', '--directory',
1755 metavar = 'DIR', dest = 'dir', default = T.configdir,
1756 help = 'Select current diretory [default %default]')
1757 opts, args = op.parse_args()
1758 if args: op.error('no arguments permitted')
1763 """Initialization."""
1765 global conn, monitor, pinger
1767 ## Try to establish a connection.
1768 conn = Connection(opts.tripesock)
1770 ## Make the main interesting coroutines and objects.
1778 root = MonitorWindow()
1783 HookClient().hook(root.closehook, exit)
1786 if __name__ == '__main__':
1787 opts = parse_options()
1791 ###----- That's all, folks --------------------------------------------------