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(me, 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')]
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('red')
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)
719 me.validate = RX.compile(valid).match
721 me.c_ok = me.get_style().text[G.STATE_NORMAL]
723 if size != -1: me.set_width_chars(size)
724 me.set_activates_default(True)
728 def check(me, *hunoz):
729 """Check the current text and update validp and the text colour."""
730 if me.validate(G.Entry.get_text(me)):
732 me.modify_text(G.STATE_NORMAL, me.c_ok)
735 me.modify_text(G.STATE_NORMAL, me.c_bad)
739 Return the text in the Entry if it's valid. If it isn't, raise
743 raise ValidationError
744 return G.Entry.get_text(me)
746 def numericvalidate(min = None, max = None):
748 Return a validation function for numbers.
750 Entry must consist of an optional sign followed by digits, and the
751 resulting integer must be within the given bounds.
753 return lambda x: (rx_num.match(x) and
754 (min is None or long(x) >= min) and
755 (max is None or long(x) <= max))
757 ###--------------------------------------------------------------------------
758 ### Various minor dialog boxen.
760 GPL = """This program is free software; you can redistribute it and/or modify
761 it under the terms of the GNU General Public License as published by
762 the Free Software Foundation; either version 2 of the License, or
763 (at your option) any later version.
765 This program is distributed in the hope that it will be useful,
766 but WITHOUT ANY WARRANTY; without even the implied warranty of
767 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
768 GNU General Public License for more details.
770 You should have received a copy of the GNU General Public License
771 along with this program; if not, write to the Free Software Foundation,
772 Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
774 class AboutBox (G.AboutDialog, MyWindowMixin):
775 """The program `About' box."""
777 G.AboutDialog.__init__(me)
779 me.set_name('TrIPEmon')
780 me.set_version(T.VERSION)
782 me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
783 me.set_comments('A graphical monitor for the TrIPE VPN server')
784 me.set_copyright('Copyright © 2006-2008 Straylight/Edgeware')
785 me.connect('response', me.respond)
787 def respond(me, hunoz, rid, *hukairz):
788 if rid == G.RESPONSE_CANCEL:
790 aboutbox = WindowSlot(AboutBox)
793 """Report an error message in a window."""
794 d = G.Dialog('Error from %s' % M.quis,
795 flags = G.DIALOG_MODAL,
796 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
798 label.set_padding(20, 20)
799 d.vbox.pack_start(label)
804 def unimplemented(*hunoz):
805 """Indicator of laziness."""
806 moanbox("I've not written that bit yet.")
808 ###--------------------------------------------------------------------------
811 class LogModel (G.ListStore):
813 A simple list of log messages, usable as the model for a TreeView.
815 The column headings are stored in the `cols' attribute.
818 def __init__(me, columns):
820 COLUMNS must be a list of column name strings. We add a time column to
823 me.cols = ('Time',) + columns
824 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
826 def add(me, *entries):
828 Adds a new log message, with a timestamp.
830 The ENTRIES are the contents for the list columns.
832 now = TIME.strftime('%Y-%m-%d %H:%M:%S')
833 me.append((now, ) + entries)
835 class TraceLogModel (LogModel):
836 """Log model for trace messages."""
838 LogModel.__init__(me, ('Message',))
839 def notify(me, line):
840 """Call with a new trace message."""
843 class WarningLogModel (LogModel):
845 Log model for warnings.
847 We split the category out into a separate column.
850 LogModel.__init__(me, ('Category', 'Message'))
851 def notify(me, tag, *rest):
852 """Call with a new warning message."""
853 me.add(tag, ' '.join([T.quotify(w) for w in rest]))
855 class LogViewer (MyWindow):
859 Its contents are a TreeView showing the log.
863 * model: an appropriate LogModel
864 * list: a TreeView widget to display the log
867 def __init__(me, model):
869 Create a log viewer showing the LogModel MODEL.
871 MyWindow.__init__(me)
873 scr = MyScrolledWindow()
874 me.list = MyTreeView(me.model)
876 for c in me.model.cols:
877 crt = G.CellRendererText()
878 me.list.append_column(G.TreeViewColumn(c, crt, text = i))
880 crt.set_property('family', 'monospace')
881 me.set_default_size(440, 256)
886 ###--------------------------------------------------------------------------
889 class pingstate (struct):
891 Information kept for each peer by the Pinger.
893 Important attributes:
895 * peer = the peer name
896 * command = PING or EPING
897 * n = how many pings we've sent so far
898 * ngood = how many returned
899 * nmiss = how many didn't return
900 * nmissrun = how many pings since the last good one
901 * tlast = round-trip time for the last (good) ping
902 * ttot = total roung trip time
906 class Pinger (T.Coroutine, HookClient):
908 Coroutine which pings known peers and collects statistics.
910 Interesting attributes:
912 * _map: dict mapping peer names to Peer objects
913 * _q: event queue for notifying pinger coroutine
914 * _timer: gobject timer for waking the coroutine
919 Initialize the pinger.
921 We watch the monitor's PeerList to track which peers we should ping. We
922 maintain an event queue and put all the events on that.
924 The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
925 where CMD is 'PING' or 'EPING'.
927 T.Coroutine.__init__(me)
928 HookClient.__init__(me)
932 me.hook(conn.connecthook, me._connected)
933 me.hook(conn.disconnecthook, me._disconnected)
934 me.hook(monitor.peers.addhook,
935 lambda p: T.defer(me._q.put, (p, 'ADD', None)))
936 me.hook(monitor.peers.delhook,
937 lambda p: T.defer(me._q.put, (p, 'KILL', None)))
938 if conn.connectedp(): me.connected()
941 """Respond to connection: start pinging thngs."""
942 me._timer = GO.timeout_add(1000, me._timerfunc)
945 """Timer function: put a timer event on the queue."""
946 me._q.put((None, 'TIMER', None))
949 def _disconnected(me, reason):
950 """Respond to disconnection: stop pinging."""
951 GO.source_remove(me._timer)
955 Coroutine function: read events from the queue and process them.
959 * (PEER, 'KILL', None): remove PEER from the interesting peers list
960 * (PEER, 'ADD', None): add PEER to the list
961 * (PEER, 'INFO', TOKENS): result from a PING command
962 * (None, 'TIMER', None): interval timer went off: send more pings
965 tag, code, stuff = me._q.get()
970 elif not conn.connectedp():
975 for cmd in 'PING', 'EPING':
976 ps = pingstate(command = cmd, peer = p,
977 n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
983 if stuff[0] == 'ping-ok':
993 ps.peer.pinghook.run(ps.peer, ps.command, ps)
994 elif code == 'TIMER':
995 for name, p in me._map.iteritems():
996 for cmd, ps in p.ping.iteritems():
997 conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
998 cmd, '-background', conn.bgtag(), '--', name]))
1000 ###--------------------------------------------------------------------------
1001 ### Random dialogue boxes.
1003 class AddPeerDialog (MyDialog):
1005 Let the user create a new peer the low-level way.
1007 Interesting attributes:
1009 * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
1013 """Initialize the dialogue."""
1014 MyDialog.__init__(me, 'Add peer',
1015 buttons = [(G.STOCK_CANCEL, me.destroy),
1016 (G.STOCK_OK, me.ok)])
1021 """Coroutine function: background setup for AddPeerDialog."""
1022 table = GridPacker()
1023 me.vbox.pack_start(table)
1024 me.e_name = table.labelled('Name',
1025 ValidatingEntry(r'^[^\s.:]+$', '', 16),
1027 me.e_addr = table.labelled('Address',
1028 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
1030 me.e_port = table.labelled('Port',
1031 ValidatingEntry(numericvalidate(0, 65535),
1034 me.c_keepalive = G.CheckButton('Keepalives')
1035 me.l_tunnel = table.labelled('Tunnel',
1036 G.combo_box_new_text(),
1037 newlinep = True, width = 3)
1038 me.tuns = conn.tunnels()
1040 me.l_tunnel.append_text(t)
1041 me.l_tunnel.set_active(0)
1042 table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
1043 me.c_keepalive.connect('toggled',
1044 lambda t: me.e_keepalive.set_sensitive\
1046 me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
1047 me.e_keepalive.set_sensitive(False)
1048 table.pack(me.e_keepalive, width = 3)
1052 """Handle an OK press: create the peer."""
1054 if me.c_keepalive.get_active():
1055 ka = me.e_keepalive.get_text()
1058 t = me.l_tunnel.get_active()
1063 me._addpeer(me.e_name.get_text(),
1064 me.e_addr.get_text(),
1065 me.e_port.get_text(),
1068 except ValidationError:
1073 def _addpeer(me, name, addr, port, keepalive, tunnel):
1074 """Coroutine function: actually do the ADD command."""
1076 conn.add(name, addr, port, keepalive = keepalive, tunnel = tunnel)
1078 except T.TripeError, exc:
1079 T.defer(moanbox, ' '.join(exc))
1081 class ServInfo (MyWindow):
1083 Show information about the server and available services.
1085 Interesting attributes:
1087 * e: maps SERVINFO keys to entry widgets
1088 * svcs: Gtk ListStore describing services (columns are name and version)
1092 MyWindow.__init__(me)
1093 me.set_title('TrIPE server info')
1094 table = GridPacker()
1097 def add(label, tag, text = None, **kw):
1098 me.e[tag] = table.info(label, text, **kw)
1099 add('Implementation', 'implementation')
1100 add('Version', 'version', newlinep = True)
1101 me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1102 me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1103 scr = MyScrolledWindow()
1104 lb = MyTreeView(me.svcs)
1106 for title in 'Service', 'Version':
1107 lb.append_column(G.TreeViewColumn(
1108 title, G.CellRendererText(), text = i))
1110 for svc in monitor.services:
1111 me.svcs.append([svc.name, svc.version])
1113 table.pack(scr, width = 2, newlinep = True,
1114 yopt = G.EXPAND | G.FILL | G.SHRINK)
1116 me.hook(conn.connecthook, me.update)
1117 me.hook(monitor.services.addhook, me.addsvc)
1118 me.hook(monitor.services.delhook, me.delsvc)
1121 def addsvc(me, svc):
1122 me.svcs.append([svc.name, svc.version])
1124 def delsvc(me, svc):
1125 for i in xrange(len(me.svcs)):
1126 if me.svcs[i][0] == svc.name:
1127 me.svcs.remove(me.svcs.get_iter(i))
1131 info = conn.servinfo()
1133 me.e[i].set_text(info[i])
1135 class TraceOptions (MyDialog):
1136 """Tracing options window."""
1138 MyDialog.__init__(me, title = 'Tracing options',
1139 buttons = [(G.STOCK_CLOSE, me.destroy),
1140 (G.STOCK_OK, cr(me.ok))])
1146 for ch, st, desc in conn.trace():
1147 if ch.isupper(): continue
1148 text = desc[0].upper() + desc[1:]
1149 ticky = G.CheckButton(text)
1150 ticky.set_active(st == '+')
1151 me.vbox.pack_start(ticky)
1152 me.opts.append((ch, ticky))
1157 for ch, ticky in me.opts:
1158 if ticky.get_active():
1162 setting = ''.join(on) + '-' + ''.join(off)
1166 ###--------------------------------------------------------------------------
1170 """Translate a TrIPE-format time to something human-readable."""
1171 if t == 'NEVER': return '(never)'
1172 YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1173 ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1174 ago = MATH.floor(ago); unit = 's'
1175 for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1179 return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1180 (YY, MM, DD, hh, mm, ss, ago, unit)
1182 """Translate a number of bytes into something a human might want to read."""
1189 return '%d %s' % (b, suff)
1191 ## How to translate peer stats. Maps the stat name to a translation
1194 [('start-time', xlate_time),
1195 ('last-packet-time', xlate_time),
1196 ('last-keyexch-time', xlate_time),
1197 ('bytes-in', xlate_bytes),
1198 ('bytes-out', xlate_bytes),
1199 ('keyexch-bytes-in', xlate_bytes),
1200 ('keyexch-bytes-out', xlate_bytes),
1201 ('ip-bytes-in', xlate_bytes),
1202 ('ip-bytes-out', xlate_bytes)]
1204 ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
1205 ## the label to give the entry box; FORMAT is the format string to write into
1208 [('Start time', '%(start-time)s'),
1209 ('Last key-exchange', '%(last-keyexch-time)s'),
1210 ('Last packet', '%(last-packet-time)s'),
1212 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1213 ('Key-exchange in/out',
1214 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1216 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1217 ('Rejected packets', '%(rejected-packets)s')]
1219 class PeerWindow (MyWindow):
1221 Show information about a peer.
1223 This gives a graphical view of the server's peer statistics.
1225 Interesting attributes:
1227 * e: dict mapping keys (mostly matching label widget texts, though pings
1228 use command names) to entry widgets so that we can update them easily
1229 * peer: the peer this window shows information about
1230 * cr: the info-fetching coroutine, or None if crrrently disconnected
1231 * doupate: whether the info-fetching corouting should continue running
1234 def __init__(me, peer):
1235 """Construct a PeerWindow, showing information about PEER."""
1237 MyWindow.__init__(me)
1238 me.set_title('TrIPE statistics: %s' % peer.name)
1241 table = GridPacker()
1244 ## Utility for adding fields.
1246 def add(label, text = None, key = None):
1247 if key is None: key = label
1248 me.e[key] = table.info(label, text, len = 42, newlinep = True)
1250 ## Build the dialogue box.
1251 add('Peer name', peer.name)
1252 add('Tunnel', peer.tunnel)
1253 add('Interface', peer.ifname)
1255 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1256 add('Address', peer.addr)
1257 add('Transport pings', key = 'PING')
1258 add('Encrypted pings', key = 'EPING')
1260 for label, format in statslayout:
1263 ## Hook onto various interesting events.
1264 me.hook(conn.connecthook, me.tryupdate)
1265 me.hook(conn.disconnecthook, me.stopupdate)
1266 me.hook(me.closehook, me.stopupdate)
1267 me.hook(me.peer.deadhook, me.dead)
1268 me.hook(me.peer.changehook, me.change)
1269 me.hook(me.peer.pinghook, me.ping)
1274 ## Format the ping statistics.
1275 for cmd, ps in me.peer.ping.iteritems():
1276 me.ping(me.peer, cmd, ps)
1278 ## And show the window.
1282 """Update the display in response to a notification."""
1283 me.e['Interface'].set_text(me.peer.ifname)
1287 Main display-updating coroutine.
1289 This does an update, sleeps for a while, and starts again. If the
1290 me.doupdate flag goes low, we stop the loop.
1292 while me.peer.alivep and conn.connectedp() and me.doupdate:
1293 stat = conn.stats(me.peer.name)
1294 for s, trans in statsxlate:
1295 stat[s] = trans(stat[s])
1296 for label, format in statslayout:
1297 me.e[label].set_text(format % stat)
1298 GO.timeout_add(1000, lambda: me.cr.switch() and False)
1299 me.cr.parent.switch()
1303 """Start the updater coroutine, if it's not going already."""
1305 me.cr = T.Coroutine(me._update,
1306 name = 'update-peer-window %s' % me.peer.name)
1309 def stopupdate(me, *hunoz, **hukairz):
1310 """Stop the update coroutine, by setting me.doupdate."""
1314 """Called when the peer is killed."""
1315 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1316 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1319 def ping(me, peer, cmd, ps):
1320 """Called when a ping result for the peer is reported."""
1321 s = '%d/%d' % (ps.ngood, ps.n)
1323 s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1325 s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1326 me.e[ps.command].set_text(s)
1328 ###--------------------------------------------------------------------------
1329 ### Cryptographic status.
1331 class CryptoInfo (MyWindow):
1332 """Simple display of cryptographic algorithms in use."""
1334 MyWindow.__init__(me)
1335 me.set_title('Cryptographic algorithms')
1336 T.aside(me.populate)
1338 table = GridPacker()
1341 crypto = conn.algs()
1342 table.info('Diffie-Hellman group',
1343 '%s (%d-bit order, %d-bit elements)' %
1344 (crypto['kx-group'],
1345 int(crypto['kx-group-order-bits']),
1346 int(crypto['kx-group-elt-bits'])),
1348 table.info('Data encryption',
1349 '%s (%d-bit key; %s)' %
1351 int(crypto['cipher-keysz']) * 8,
1352 crypto['cipher-blksz'] == '0'
1354 or '%d-bit block' % (int(crypto['cipher-blksz']) * 8)),
1356 table.info('Message authentication',
1357 '%s (%d-bit key; %d-bit tag)' %
1359 int(crypto['mac-keysz']) * 8,
1360 int(crypto['mac-tagsz']) * 8),
1362 table.info('Hash function',
1363 '%s (%d-bit output)' %
1365 int(crypto['hash-sz']) * 8),
1370 ###--------------------------------------------------------------------------
1371 ### Main monitor window.
1373 class MonitorWindow (MyWindow):
1376 The main monitor window.
1378 This class creates, populates and maintains the main monitor window.
1382 * warnings, trace: log models for server output
1383 * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1384 WindowSlot objects for ancillary windows
1385 * ui: Gtk UIManager object for the menu system
1386 * apmenu: pair of identical autoconnecting peer menus
1387 * listmodel: Gtk ListStore for connected peers; contains peer name,
1388 address, and ping times (transport and encrypted, value and colour)
1389 * status: Gtk Statusbar at the bottom of the window
1390 * _kidding: an unpleasant backchannel between the apchange method (which
1391 builds the apmenus) and the menu handler, forced on us by a Gtk
1394 Also installs attributes on Peer objects:
1396 * i: index of peer's entry in listmodel
1397 * win: WindowSlot object for the peer's PeerWindow
1401 """Construct the window."""
1404 MyWindow.__init__(me)
1405 me.set_title('TrIPE monitor')
1407 ## Hook onto diagnostic outputs.
1408 me.warnings = WarningLogModel()
1409 me.hook(conn.warnhook, me.warnings.notify)
1410 me.trace = TraceLogModel()
1411 me.hook(conn.tracehook, me.trace.notify)
1413 ## Make slots to store the various ancillary singleton windows.
1414 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1415 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1416 me.traceopts = WindowSlot(lambda: TraceOptions())
1417 me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1418 me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1419 me.servinfo = WindowSlot(lambda: ServInfo())
1421 ## Main window structure.
1425 ## UI manager makes our menus. (We're too cheap to have a toolbar.)
1426 me.ui = G.UIManager()
1427 actgroup = makeactiongroup('monitor',
1428 [('file-menu', '_File', None, None),
1429 ('connect', '_Connect', '<Control>C', conn.connect),
1430 ('disconnect', '_Disconnect', '<Control>D',
1431 lambda: conn.disconnect(None)),
1432 ('quit', '_Quit', '<Control>Q', me.close),
1433 ('server-menu', '_Server', None, None),
1434 ('daemon', 'Run in _background', None, cr(conn.daemon)),
1435 ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1436 ('crypto-algs', 'Cryptographic algorithms',
1437 '<Control>Y', me.cryptoinfo.open),
1438 ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1439 ('server-quit', 'Terminate server', None, cr(conn.quit)),
1440 ('conn-peer', 'Connect peer', None, None),
1441 ('logs-menu', '_Logs', None, None),
1442 ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1443 ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1444 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1445 ('help-menu', '_Help', None, None),
1446 ('about', '_About tripemon...', None, aboutbox.open),
1447 ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1448 ('kill-peer', '_Kill peer', None, me.killpeer),
1449 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1455 <menu action="file-menu">
1456 <menuitem action="quit"/>
1458 <menu action="server-menu">
1459 <menuitem action="connect"/>
1460 <menuitem action="disconnect"/>
1462 <menuitem action="server-version"/>
1463 <menuitem action="crypto-algs"/>
1464 <menuitem action="add-peer"/>
1465 <menuitem action="conn-peer"/>
1466 <menuitem action="daemon"/>
1467 <menuitem action="reload-keys"/>
1469 <menuitem action="server-quit"/>
1471 <menu action="logs-menu">
1472 <menuitem action="show-warnings"/>
1473 <menuitem action="show-trace"/>
1474 <menuitem action="trace-options"/>
1476 <menu action="help-menu">
1477 <menuitem action="about"/>
1480 <popup name="peer-popup">
1481 <menuitem action="add-peer"/>
1482 <menuitem action="conn-peer"/>
1483 <menuitem action="kill-peer"/>
1484 <menuitem action="force-kx"/>
1489 ## Populate the UI manager.
1490 me.ui.insert_action_group(actgroup, 0)
1491 me.ui.add_ui_from_string(uidef)
1493 ## Construct the menu bar.
1494 vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
1495 me.add_accel_group(me.ui.get_accel_group())
1497 ## Construct and attach the auto-peers menu. (This is a horrible bodge
1498 ## because we can't attach the same submenu in two different places.)
1499 me.apmenu = G.Menu(), G.Menu()
1500 me.ui.get_widget('/menubar/server-menu/conn-peer') \
1501 .set_submenu(me.apmenu[0])
1502 me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1504 ## Construct the main list model, and listen on hooks which report
1505 ## changes to the available peers.
1506 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1507 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1508 me.hook(monitor.peers.addhook, me.addpeer)
1509 me.hook(monitor.peers.delhook, me.delpeer)
1510 me.hook(monitor.autopeershook, me.apchange)
1512 ## Construct the list viewer and put it in a scrolling window.
1513 scr = MyScrolledWindow()
1514 me.list = MyTreeView(me.listmodel)
1515 me.list.append_column(G.TreeViewColumn('Peer name',
1516 G.CellRendererText(),
1518 me.list.append_column(G.TreeViewColumn('Address',
1519 G.CellRendererText(),
1521 me.list.append_column(G.TreeViewColumn('T-ping',
1522 G.CellRendererText(),
1525 me.list.append_column(G.TreeViewColumn('E-ping',
1526 G.CellRendererText(),
1529 me.list.get_column(1).set_expand(True)
1530 me.list.connect('row-activated', me.activate)
1531 me.list.connect('button-press-event', me.buttonpress)
1532 me.list.set_reorderable(True)
1533 me.list.get_selection().set_mode(G.SELECTION_NONE)
1535 vbox.pack_start(scr)
1537 ## Construct the status bar, and listen on hooks which report changes to
1538 ## connection status.
1539 me.status = G.Statusbar()
1540 vbox.pack_start(me.status, expand = False)
1541 me.hook(conn.connecthook, cr(me.connected))
1542 me.hook(conn.disconnecthook, me.disconnected)
1543 me.hook(conn.notehook, me.notify)
1545 ## Set a plausible default window size.
1546 me.set_default_size(512, 180)
1548 def addpeer(me, peer):
1549 """Hook: announces that PEER has been added."""
1550 peer.i = me.listmodel.append([peer.name, peer.addr,
1551 '???', 'green', '???', 'green'])
1552 peer.win = WindowSlot(lambda: PeerWindow(peer))
1553 me.hook(peer.pinghook, me._ping)
1556 def delpeer(me, peer):
1557 """Hook: announces that PEER has been removed."""
1558 me.listmodel.remove(peer.i)
1559 me.unhook(peer.pinghook)
1562 def path_peer(me, path):
1563 """Return the peer corresponding to a given list-model PATH."""
1564 return monitor.peers[me.listmodel[path][0]]
1568 Hook: announces that a change has been made to the peers available for
1569 automated connection.
1571 This populates both auto-peer menus and keeps them in sync. (As
1572 mentioned above, we can't attach the same submenu to two separate parent
1573 menu items. So we end up with two identical menus instead. Yes, this
1577 ## The set_active method of a CheckMenuItem works by maybe activating the
1578 ## menu item. This signals our handler. But we don't actually want to
1579 ## signal the handler unless the user actually frobbed the item. So the
1580 ## _kidding flag is used as an underhanded way of telling the handler
1581 ## that we don't actually want it to do anything. Of course, this sucks
1585 ## Iterate over the two menus.
1588 existing = menu.get_children()
1589 if monitor.autopeers is None:
1591 ## No peers, so empty out the menu.
1592 for item in existing:
1597 ## Insert the new items into the menu. (XXX this seems buggy XXX)
1598 ## Tick the peers which are actually connected.
1600 for peer in monitor.autopeers:
1601 if j < len(existing) and \
1602 existing[j].get_child().get_text() == peer:
1606 item = G.CheckMenuItem(peer, use_underline = False)
1607 item.connect('activate', invoker(me._addautopeer, peer))
1608 menu.insert(item, i)
1609 item.set_active(peer in monitor.peers.table)
1612 ## Make all the menu items visible.
1615 ## Set the parent menu items sensitive if and only if there are any peers
1617 for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1618 me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1620 ## And now allow the handler to do its business normally.
1623 def _addautopeer(me, peer):
1625 Automatically connect an auto-peer.
1627 This method is invoked from the main coroutine. Since the actual
1628 connection needs to issue administration commands, we must spawn a new
1629 child coroutine for it.
1633 T.Coroutine(me._addautopeer_hack,
1634 name = '_addautopeerhack %s' % peer).switch(peer)
1636 def _addautopeer_hack(me, peer):
1637 """Make an automated connection to PEER in response to a user click."""
1641 T._simple(conn.svcsubmit('connect', 'active', peer))
1642 except T.TripeError, exc:
1643 T.defer(moanbox, ' '.join(exc.args))
1646 def activate(me, l, path, col):
1648 Handle a double-click on a peer in the main list: open a PeerInfo window.
1650 peer = me.path_peer(path)
1653 def buttonpress(me, l, ev):
1655 Handle a mouse click on the main list.
1657 Currently we're only interested in button-3, which pops up the peer menu.
1658 For future reference, we stash the peer that was clicked in me.menupeer.
1661 x, y = int(ev.x), int(ev.y)
1662 r = me.list.get_path_at_pos(x, y)
1663 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1664 me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1666 me.ui.get_widget('/peer-popup/conn-peer'). \
1667 set_sensitive(bool(monitor.autopeers))
1669 me.menupeer = me.path_peer(r[0])
1672 me.ui.get_widget('/peer-popup').popup(
1673 None, None, None, ev.button, ev.time)
1676 """Kill a peer from the popup menu."""
1677 cr(conn.kill, me.menupeer.name)()
1680 """Kickstart a key-exchange from the popup menu."""
1681 cr(conn.forcekx, me.menupeer.name)()
1683 _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1684 def _ping(me, p, cmd, ps):
1685 """Hook: responds to ping reports."""
1686 textcol, colourcol = me._columnmap[cmd]
1688 me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1689 me.listmodel[p.i][colourcol] = 'red'
1691 me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1692 me.listmodel[p.i][colourcol] = 'black'
1694 def setstatus(me, status):
1695 """Update the message in the status bar."""
1697 me.status.push(0, status)
1699 def notify(me, note, *rest):
1700 """Hook: invoked when interesting notifications occur."""
1701 if note == 'DAEMON':
1702 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1706 Hook: invoked when a connection is made to the server.
1708 Make options which require a server connection sensitive.
1710 me.setstatus('Connected (port %s)' % conn.port())
1711 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1712 for i in ('/menubar/server-menu/disconnect',
1713 '/menubar/server-menu/server-version',
1714 '/menubar/server-menu/add-peer',
1715 '/menubar/server-menu/server-quit',
1716 '/menubar/logs-menu/trace-options'):
1717 me.ui.get_widget(i).set_sensitive(True)
1718 me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1719 set_sensitive(bool(monitor.autopeers))
1720 me.ui.get_widget('/menubar/server-menu/daemon'). \
1721 set_sensitive(conn.servinfo()['daemon'] == 'nil')
1723 def disconnected(me, reason):
1725 Hook: invoked when the connection to the server is lost.
1727 Make most options insensitive.
1729 me.setstatus('Disconnected')
1730 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1731 for i in ('/menubar/server-menu/disconnect',
1732 '/menubar/server-menu/server-version',
1733 '/menubar/server-menu/add-peer',
1734 '/menubar/server-menu/conn-peer',
1735 '/menubar/server-menu/daemon',
1736 '/menubar/server-menu/server-quit',
1737 '/menubar/logs-menu/trace-options'):
1738 me.ui.get_widget(i).set_sensitive(False)
1739 if reason: moanbox(reason)
1741 ###--------------------------------------------------------------------------
1744 def parse_options():
1746 Parse command-line options.
1748 Process the boring ones. Return all of them, for later.
1750 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1751 version = '%prog (tripe version 1.0.0)')
1752 op.add_option('-a', '--admin-socket',
1753 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1754 help = 'Select socket to connect to [default %default]')
1755 op.add_option('-d', '--directory',
1756 metavar = 'DIR', dest = 'dir', default = T.configdir,
1757 help = 'Select current diretory [default %default]')
1758 opts, args = op.parse_args()
1759 if args: op.error('no arguments permitted')
1764 """Initialization."""
1766 global conn, monitor, pinger
1768 ## Try to establish a connection.
1769 conn = Connection(opts.tripesock)
1771 ## Make the main interesting coroutines and objects.
1779 root = MonitorWindow()
1784 HookClient().hook(root.closehook, exit)
1787 if __name__ == '__main__':
1788 opts = parse_options()
1792 ###----- That's all, folks --------------------------------------------------