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 addr = conn.addr(me.name)
297 if addr[0] == 'INET':
298 ipaddr, port = addr[1:]
300 name = S.gethostbyaddr(ipaddr)[0]
301 me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
303 me.addr = 'INET %s:%s' % (ipaddr, port)
305 me.addr = ' '.join(addr)
306 me.ifname = conn.ifname(me.name)
307 me.__dict__.update(conn.peerinfo(me.name))
310 def setifname(me, newname):
311 """Informs the object of a change to its interface name to NEWNAME."""
315 class Service (MonitorObject):
317 Represents a service.
319 Additional attributes are:
321 * version = the service version
323 def __init__(me, name, version):
324 MonitorObject.__init__(me, name)
327 def update(me, version):
328 """Tell the Service that its version has changed to VERSION."""
332 class MonitorList (object):
334 Maintains a collection of MonitorObjects.
336 The MonitorList can be indexed by name to retrieve the individual objects;
337 iteration generates the individual objects. More complicated operations
338 can be done on the `table' dictionary directly.
340 Hooks addhook(OBJ) and delhook(OBJ) are invoked when objects are added or
343 Subclass responsibilities:
345 * list(): return a list of (NAME, INFO) pairs.
347 * make(NAME, INFO): returns a new MonitorObject for the given NAME; INFO
348 is from the output of list().
352 """Initialize a new MonitorList."""
354 me.addhook = HookList()
355 me.delhook = HookList()
359 Refresh the list of objects:
361 We add new object which have appeared, delete ones which have vanished,
362 and update any which persist.
365 for name, stuff in me.list():
368 for name in me.table.copy():
372 def add(me, name, stuff):
374 Add a new object created by make(NAME, STUFF) if it doesn't already
375 exist. If it does, update it.
377 if name not in me.table:
378 obj = me.make(name, stuff)
382 me.table[name].update(stuff)
384 def remove(me, name):
386 Remove the object called NAME from the list.
388 The object becomes dead.
396 def __getitem__(me, name):
397 """Retrieve the object called NAME."""
398 return me.table[name]
401 """Iterate over the objects."""
402 return me.table.itervalues()
404 class PeerList (MonitorList):
405 """The list of the known peers."""
407 return [(name, None) for name in conn.list()]
408 def make(me, name, stuff):
411 class ServiceList (MonitorList):
412 """The list of the registered services."""
414 return conn.svclist()
415 def make(me, name, stuff):
416 return Service(name, stuff)
418 class Monitor (HookClient):
420 The main monitor: keeps track of the changes happening to the server.
422 Exports the peers, services MonitorLists, and a (plain Python) list
423 autopeers of peers which the connect service knows how to start by name.
427 * autopeershook(): invoked when the auto-peers list changes.
430 """Initialize the Monitor."""
431 HookClient.__init__(me)
432 me.peers = PeerList()
433 me.services = ServiceList()
434 me.hook(conn.connecthook, me._connected)
435 me.hook(conn.notehook, me._notify)
436 me.autopeershook = HookList()
440 """Handle a successful connection by starting the setup coroutine."""
445 """Coroutine function: initialize for a new connection."""
449 me._updateautopeers()
451 def _updateautopeers(me):
452 """Update the auto-peers list from the connect service."""
453 if 'connect' in me.services.table:
454 me.autopeers = [' '.join(line)
455 for line in conn.svcsubmit('connect', 'list')]
459 me.autopeershook.run()
461 def _notify(me, code, *rest):
463 Handle notifications from the server.
465 ADD, KILL and NEWIFNAME notifications get passed up to the PeerList;
466 SVCCLAIM and SVCRELEASE get passed up to the ServiceList. Finally,
467 peerdb-update notifications from the watch service cause us to refresh
471 T.aside(me.peers.add, rest[0], None)
473 T.aside(me.peers.remove, rest[0])
474 elif code == 'NEWIFNAME':
476 me.peers[rest[0]].setifname(rest[2])
479 elif code == 'SVCCLAIM':
480 T.aside(me.services.add, rest[0], rest[1])
481 if rest[0] == 'connect':
482 T.aside(me._updateautopeers)
483 elif code == 'SVCRELEASE':
484 T.aside(me.services.remove, rest[0])
485 if rest[0] == 'connect':
486 T.aside(me._updateautopeers)
489 if rest[0] == 'watch' and \
490 rest[1] == 'peerdb-update':
491 T.aside(me._updateautopeers)
493 ###--------------------------------------------------------------------------
494 ### Window management cruft.
496 class MyWindowMixin (G.Window, HookClient):
498 Mixin for windows which call a closehook when they're destroyed. It's also
499 a hookclient, and will release its hooks when it's destroyed.
503 * closehook(): called when the window is closed.
507 """Initialization function. Note that it's not called __init__!"""
508 me.closehook = HookList()
509 HookClient.__init__(me)
510 me.connect('destroy', invoker(me.close))
513 """Close the window, invoking the closehook and releasing all hooks."""
518 class MyWindow (MyWindowMixin):
519 """A version of MyWindowMixin suitable as a single parent class."""
520 def __init__(me, kind = G.WINDOW_TOPLEVEL):
521 G.Window.__init__(me, kind)
524 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
525 """A dialogue box with a closehook and sensible button binding."""
527 def __init__(me, title = None, flags = 0, buttons = []):
529 The BUTTONS are a list of (STOCKID, THUNK) pairs: call the appropriate
530 THUNK when the button is pressed. The other arguments are just like
541 G.Dialog.__init__(me, title, None, flags, tuple(br))
543 me.set_default_response(i - 1)
544 me.connect('response', me.respond)
546 def respond(me, hunoz, rid, *hukairz):
547 """Dispatch responses to the appropriate thunks."""
548 if rid >= 0: me.rmap[rid]()
550 def makeactiongroup(name, acts):
552 Creates an ActionGroup called NAME.
554 ACTS is a list of tuples containing:
556 * ACT: an action name
557 * LABEL: the label string for the action
558 * ACCEL: accelerator string, or None
559 * FUNC: thunk to call when the action is invoked
561 actgroup = G.ActionGroup(name)
562 for act, label, accel, func in acts:
563 a = G.Action(act, label, None, None)
564 if func: a.connect('activate', invoker(func))
565 actgroup.add_action_with_accel(a, accel)
568 class GridPacker (G.Table):
570 Like a Table, but with more state: makes filling in the widgets easier.
574 """Initialize a new GridPacker."""
580 me.set_border_width(4)
581 me.set_col_spacings(4)
582 me.set_row_spacings(4)
584 def pack(me, w, width = 1, newlinep = False,
585 xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
590 W is the widget to add. XOPY, YOPT, XPAD and YPAD are as for Table.
591 WIDTH is how many cells to take up horizontally. NEWLINEP is whether to
592 start a new line for this widget. Returns W.
598 right = me.col + width
599 if bot > me.rows or right > me.cols:
600 if bot > me.rows: me.rows = bot
601 if right > me.cols: me.cols = right
602 me.resize(me.rows, me.cols)
603 me.attach(w, me.col, me.col + width, me.row, me.row + 1,
604 xopt, yopt, xpad, ypad)
608 def labelled(me, lab, w, newlinep = False, **kw):
610 Packs a labelled widget.
612 Other arguments are as for pack. Returns W.
614 label = G.Label(lab + ' ')
615 label.set_alignment(1.0, 0)
616 me.pack(label, newlinep = newlinep, xopt = G.FILL)
620 def info(me, label, text = None, len = 18, **kw):
622 Packs an information widget with a label.
624 LABEL is the label; TEXT is the initial text; LEN is the estimated length
625 in characters. Returns the entry widget.
628 if text is not None: e.set_text(text)
629 e.set_width_chars(len)
630 e.set_selectable(True)
631 e.set_alignment(0.0, 0.5)
632 me.labelled(label, e, **kw)
635 class WindowSlot (HookClient):
637 A place to store a window -- specificially a MyWindowMixin.
639 If the window is destroyed, remember this; when we come to open the window,
640 raise it if it already exists; otherwise make a new one.
642 def __init__(me, createfunc):
644 Constructor: CREATEFUNC must return a new Window which supports the
647 HookClient.__init__(me)
648 me.createfunc = createfunc
652 """Opens the window, creating it if necessary."""
654 me.window.window.raise_()
656 me.window = me.createfunc()
657 me.hook(me.window.closehook, me.closed)
660 """Handles the window being closed."""
661 me.unhook(me.window.closehook)
664 class MyTreeView (G.TreeView):
665 def __init__(me, model):
666 G.TreeView.__init__(me, model)
667 me.set_rules_hint(True)
669 class MyScrolledWindow (G.ScrolledWindow):
671 G.ScrolledWindow.__init__(me)
672 me.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
673 me.set_shadow_type(G.SHADOW_IN)
675 ## Matches a signed integer.
676 rx_num = RX.compile(r'^[-+]?\d+$')
679 c_red = GDK.color_parse('red')
681 class ValidationError (Exception):
682 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
685 class ValidatingEntry (G.Entry):
687 Like an Entry, but makes the text go red if the contents are invalid.
689 If get_text is called, and the text is invalid, ValidationError is raised.
690 The attribute validp reflects whether the contents are currently valid.
693 def __init__(me, valid, text = '', size = -1, *arg, **kw):
695 Make a validating Entry.
697 VALID is a regular expression or a predicate on strings. TEXT is the
698 default text to insert. SIZE is the size of the box to set, in
699 characters (ish). Other arguments are passed to Entry.
701 G.Entry.__init__(me, *arg, **kw)
702 me.connect("changed", me.check)
706 me.validate = RX.compile(valid).match
708 me.c_ok = me.get_style().text[G.STATE_NORMAL]
710 if size != -1: me.set_width_chars(size)
711 me.set_activates_default(True)
715 def check(me, *hunoz):
716 """Check the current text and update validp and the text colour."""
717 if me.validate(G.Entry.get_text(me)):
719 me.modify_text(G.STATE_NORMAL, me.c_ok)
722 me.modify_text(G.STATE_NORMAL, me.c_bad)
726 Return the text in the Entry if it's valid. If it isn't, raise
730 raise ValidationError
731 return G.Entry.get_text(me)
733 def numericvalidate(min = None, max = None):
735 Return a validation function for numbers.
737 Entry must consist of an optional sign followed by digits, and the
738 resulting integer must be within the given bounds.
740 return lambda x: (rx_num.match(x) and
741 (min is None or long(x) >= min) and
742 (max is None or long(x) <= max))
744 ###--------------------------------------------------------------------------
745 ### Various minor dialog boxen.
747 GPL = """This program is free software; you can redistribute it and/or modify
748 it under the terms of the GNU General Public License as published by
749 the Free Software Foundation; either version 2 of the License, or
750 (at your option) any later version.
752 This program is distributed in the hope that it will be useful,
753 but WITHOUT ANY WARRANTY; without even the implied warranty of
754 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
755 GNU General Public License for more details.
757 You should have received a copy of the GNU General Public License
758 along with this program; if not, write to the Free Software Foundation,
759 Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
761 class AboutBox (G.AboutDialog, MyWindowMixin):
762 """The program `About' box."""
764 G.AboutDialog.__init__(me)
766 me.set_name('TrIPEmon')
767 me.set_version(T.VERSION)
769 me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
770 me.set_comments('A graphical monitor for the TrIPE VPN server')
771 me.set_copyright('Copyright © 2006-2008 Straylight/Edgeware')
772 me.connect('response', me.respond)
774 def respond(me, hunoz, rid, *hukairz):
775 if rid == G.RESPONSE_CANCEL:
777 aboutbox = WindowSlot(AboutBox)
780 """Report an error message in a window."""
781 d = G.Dialog('Error from %s' % M.quis,
782 flags = G.DIALOG_MODAL,
783 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
785 label.set_padding(20, 20)
786 d.vbox.pack_start(label)
791 def unimplemented(*hunoz):
792 """Indicator of laziness."""
793 moanbox("I've not written that bit yet.")
795 ###--------------------------------------------------------------------------
798 class LogModel (G.ListStore):
800 A simple list of log messages, usable as the model for a TreeView.
802 The column headings are stored in the `cols' attribute.
805 def __init__(me, columns):
807 COLUMNS must be a list of column name strings. We add a time column to
810 me.cols = ('Time',) + columns
811 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
813 def add(me, *entries):
815 Adds a new log message, with a timestamp.
817 The ENTRIES are the contents for the list columns.
819 now = TIME.strftime('%Y-%m-%d %H:%M:%S')
820 me.append((now, ) + entries)
822 class TraceLogModel (LogModel):
823 """Log model for trace messages."""
825 LogModel.__init__(me, ('Message',))
826 def notify(me, line):
827 """Call with a new trace message."""
830 class WarningLogModel (LogModel):
832 Log model for warnings.
834 We split the category out into a separate column.
837 LogModel.__init__(me, ('Category', 'Message'))
838 def notify(me, tag, *rest):
839 """Call with a new warning message."""
840 me.add(tag, ' '.join([T.quotify(w) for w in rest]))
842 class LogViewer (MyWindow):
846 Its contents are a TreeView showing the log.
850 * model: an appropriate LogModel
851 * list: a TreeView widget to display the log
854 def __init__(me, model):
856 Create a log viewer showing the LogModel MODEL.
858 MyWindow.__init__(me)
860 scr = MyScrolledWindow()
861 me.list = MyTreeView(me.model)
863 for c in me.model.cols:
864 crt = G.CellRendererText()
865 me.list.append_column(G.TreeViewColumn(c, crt, text = i))
867 crt.set_property('family', 'monospace')
868 me.set_default_size(440, 256)
873 ###--------------------------------------------------------------------------
876 class pingstate (struct):
878 Information kept for each peer by the Pinger.
880 Important attributes:
882 * peer = the peer name
883 * command = PING or EPING
884 * n = how many pings we've sent so far
885 * ngood = how many returned
886 * nmiss = how many didn't return
887 * nmissrun = how many pings since the last good one
888 * tlast = round-trip time for the last (good) ping
889 * ttot = total roung trip time
893 class Pinger (T.Coroutine, HookClient):
895 Coroutine which pings known peers and collects statistics.
897 Interesting attributes:
899 * _map: dict mapping peer names to Peer objects
900 * _q: event queue for notifying pinger coroutine
901 * _timer: gobject timer for waking the coroutine
906 Initialize the pinger.
908 We watch the monitor's PeerList to track which peers we should ping. We
909 maintain an event queue and put all the events on that.
911 The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
912 where CMD is 'PING' or 'EPING'.
914 T.Coroutine.__init__(me)
915 HookClient.__init__(me)
919 me.hook(conn.connecthook, me._connected)
920 me.hook(conn.disconnecthook, me._disconnected)
921 me.hook(monitor.peers.addhook,
922 lambda p: T.defer(me._q.put, (p, 'ADD', None)))
923 me.hook(monitor.peers.delhook,
924 lambda p: T.defer(me._q.put, (p, 'KILL', None)))
925 if conn.connectedp(): me.connected()
928 """Respond to connection: start pinging thngs."""
929 me._timer = GO.timeout_add(1000, me._timerfunc)
932 """Timer function: put a timer event on the queue."""
933 me._q.put((None, 'TIMER', None))
936 def _disconnected(me, reason):
937 """Respond to disconnection: stop pinging."""
938 GO.source_remove(me._timer)
942 Coroutine function: read events from the queue and process them.
946 * (PEER, 'KILL', None): remove PEER from the interesting peers list
947 * (PEER, 'ADD', None): add PEER to the list
948 * (PEER, 'INFO', TOKENS): result from a PING command
949 * (None, 'TIMER', None): interval timer went off: send more pings
952 tag, code, stuff = me._q.get()
957 elif not conn.connectedp():
962 for cmd in 'PING', 'EPING':
963 ps = pingstate(command = cmd, peer = p,
964 n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
970 if stuff[0] == 'ping-ok':
980 ps.peer.pinghook.run(ps.peer, ps.command, ps)
981 elif code == 'TIMER':
982 for name, p in me._map.iteritems():
983 for cmd, ps in p.ping.iteritems():
984 conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
985 cmd, '-background', conn.bgtag(), '--', name]))
987 ###--------------------------------------------------------------------------
988 ### Random dialogue boxes.
990 class AddPeerDialog (MyDialog):
992 Let the user create a new peer the low-level way.
994 Interesting attributes:
996 * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
1000 """Initialize the dialogue."""
1001 MyDialog.__init__(me, 'Add peer',
1002 buttons = [(G.STOCK_CANCEL, me.destroy),
1003 (G.STOCK_OK, me.ok)])
1008 """Coroutine function: background setup for AddPeerDialog."""
1009 table = GridPacker()
1010 me.vbox.pack_start(table)
1011 me.e_name = table.labelled('Name',
1012 ValidatingEntry(r'^[^\s.:]+$', '', 16),
1014 me.e_addr = table.labelled('Address',
1015 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
1017 me.e_port = table.labelled('Port',
1018 ValidatingEntry(numericvalidate(0, 65535),
1021 me.c_keepalive = G.CheckButton('Keepalives')
1022 me.l_tunnel = table.labelled('Tunnel',
1023 G.combo_box_new_text(),
1024 newlinep = True, width = 3)
1025 me.tuns = conn.tunnels()
1027 me.l_tunnel.append_text(t)
1028 me.l_tunnel.set_active(0)
1029 table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
1030 me.c_keepalive.connect('toggled',
1031 lambda t: me.e_keepalive.set_sensitive\
1033 me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
1034 me.e_keepalive.set_sensitive(False)
1035 table.pack(me.e_keepalive, width = 3)
1039 """Handle an OK press: create the peer."""
1041 if me.c_keepalive.get_active():
1042 ka = me.e_keepalive.get_text()
1045 t = me.l_tunnel.get_active()
1050 me._addpeer(me.e_name.get_text(),
1051 me.e_addr.get_text(),
1052 me.e_port.get_text(),
1055 except ValidationError:
1060 def _addpeer(me, name, addr, port, keepalive, tunnel):
1061 """Coroutine function: actually do the ADD command."""
1063 conn.add(name, addr, port, keepalive = keepalive, tunnel = tunnel)
1065 except T.TripeError, exc:
1066 T.defer(moanbox, ' '.join(exc))
1068 class ServInfo (MyWindow):
1070 Show information about the server and available services.
1072 Interesting attributes:
1074 * e: maps SERVINFO keys to entry widgets
1075 * svcs: Gtk ListStore describing services (columns are name and version)
1079 MyWindow.__init__(me)
1080 me.set_title('TrIPE server info')
1081 table = GridPacker()
1084 def add(label, tag, text = None, **kw):
1085 me.e[tag] = table.info(label, text, **kw)
1086 add('Implementation', 'implementation')
1087 add('Version', 'version', newlinep = True)
1088 me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1089 me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1090 scr = MyScrolledWindow()
1091 lb = MyTreeView(me.svcs)
1093 for title in 'Service', 'Version':
1094 lb.append_column(G.TreeViewColumn(
1095 title, G.CellRendererText(), text = i))
1097 for svc in monitor.services:
1098 me.svcs.append([svc.name, svc.version])
1100 table.pack(scr, width = 2, newlinep = True,
1101 yopt = G.EXPAND | G.FILL | G.SHRINK)
1103 me.hook(conn.connecthook, me.update)
1104 me.hook(monitor.services.addhook, me.addsvc)
1105 me.hook(monitor.services.delhook, me.delsvc)
1108 def addsvc(me, svc):
1109 me.svcs.append([svc.name, svc.version])
1111 def delsvc(me, svc):
1112 for i in xrange(len(me.svcs)):
1113 if me.svcs[i][0] == svc.name:
1114 me.svcs.remove(me.svcs.get_iter(i))
1118 info = conn.servinfo()
1120 me.e[i].set_text(info[i])
1122 class TraceOptions (MyDialog):
1123 """Tracing options window."""
1125 MyDialog.__init__(me, title = 'Tracing options',
1126 buttons = [(G.STOCK_CLOSE, me.destroy),
1127 (G.STOCK_OK, cr(me.ok))])
1133 for ch, st, desc in conn.trace():
1134 if ch.isupper(): continue
1135 text = desc[0].upper() + desc[1:]
1136 ticky = G.CheckButton(text)
1137 ticky.set_active(st == '+')
1138 me.vbox.pack_start(ticky)
1139 me.opts.append((ch, ticky))
1144 for ch, ticky in me.opts:
1145 if ticky.get_active():
1149 setting = ''.join(on) + '-' + ''.join(off)
1153 ###--------------------------------------------------------------------------
1157 """Translate a TrIPE-format time to something human-readable."""
1158 if t == 'NEVER': return '(never)'
1159 YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1160 ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1161 ago = MATH.floor(ago); unit = 's'
1162 for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1166 return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1167 (YY, MM, DD, hh, mm, ss, ago, unit)
1169 """Translate a number of bytes into something a human might want to read."""
1176 return '%d %s' % (b, suff)
1178 ## How to translate peer stats. Maps the stat name to a translation
1181 [('start-time', xlate_time),
1182 ('last-packet-time', xlate_time),
1183 ('last-keyexch-time', xlate_time),
1184 ('bytes-in', xlate_bytes),
1185 ('bytes-out', xlate_bytes),
1186 ('keyexch-bytes-in', xlate_bytes),
1187 ('keyexch-bytes-out', xlate_bytes),
1188 ('ip-bytes-in', xlate_bytes),
1189 ('ip-bytes-out', xlate_bytes)]
1191 ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
1192 ## the label to give the entry box; FORMAT is the format string to write into
1195 [('Start time', '%(start-time)s'),
1196 ('Last key-exchange', '%(last-keyexch-time)s'),
1197 ('Last packet', '%(last-packet-time)s'),
1199 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1200 ('Key-exchange in/out',
1201 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1203 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1204 ('Rejected packets', '%(rejected-packets)s')]
1206 class PeerWindow (MyWindow):
1208 Show information about a peer.
1210 This gives a graphical view of the server's peer statistics.
1212 Interesting attributes:
1214 * e: dict mapping keys (mostly matching label widget texts, though pings
1215 use command names) to entry widgets so that we can update them easily
1216 * peer: the peer this window shows information about
1217 * cr: the info-fetching coroutine, or None if crrrently disconnected
1218 * doupate: whether the info-fetching corouting should continue running
1221 def __init__(me, peer):
1222 """Construct a PeerWindow, showing information about PEER."""
1224 MyWindow.__init__(me)
1225 me.set_title('TrIPE statistics: %s' % peer.name)
1228 table = GridPacker()
1231 ## Utility for adding fields.
1233 def add(label, text = None, key = None):
1234 if key is None: key = label
1235 me.e[key] = table.info(label, text, len = 42, newlinep = True)
1237 ## Build the dialogue box.
1238 add('Peer name', peer.name)
1239 add('Tunnel', peer.tunnel)
1240 add('Interface', peer.ifname)
1242 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1243 add('Address', peer.addr)
1244 add('Transport pings', key = 'PING')
1245 add('Encrypted pings', key = 'EPING')
1247 for label, format in statslayout:
1250 ## Hook onto various interesting events.
1251 me.hook(conn.connecthook, me.tryupdate)
1252 me.hook(conn.disconnecthook, me.stopupdate)
1253 me.hook(me.closehook, me.stopupdate)
1254 me.hook(me.peer.deadhook, me.dead)
1255 me.hook(me.peer.changehook, me.change)
1256 me.hook(me.peer.pinghook, me.ping)
1261 ## Format the ping statistics.
1262 for cmd, ps in me.peer.ping.iteritems():
1263 me.ping(me.peer, cmd, ps)
1265 ## And show the window.
1269 """Update the display in response to a notification."""
1270 me.e['Interface'].set_text(me.peer.ifname)
1274 Main display-updating coroutine.
1276 This does an update, sleeps for a while, and starts again. If the
1277 me.doupdate flag goes low, we stop the loop.
1279 while me.peer.alivep and conn.connectedp() and me.doupdate:
1280 stat = conn.stats(me.peer.name)
1281 for s, trans in statsxlate:
1282 stat[s] = trans(stat[s])
1283 for label, format in statslayout:
1284 me.e[label].set_text(format % stat)
1285 GO.timeout_add(1000, lambda: me.cr.switch() and False)
1286 me.cr.parent.switch()
1290 """Start the updater coroutine, if it's not going already."""
1292 me.cr = T.Coroutine(me._update,
1293 name = 'update-peer-window %s' % me.peer.name)
1296 def stopupdate(me, *hunoz, **hukairz):
1297 """Stop the update coroutine, by setting me.doupdate."""
1301 """Called when the peer is killed."""
1302 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1303 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1306 def ping(me, peer, cmd, ps):
1307 """Called when a ping result for the peer is reported."""
1308 s = '%d/%d' % (ps.ngood, ps.n)
1310 s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1312 s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1313 me.e[ps.command].set_text(s)
1315 ###--------------------------------------------------------------------------
1316 ### Cryptographic status.
1318 class CryptoInfo (MyWindow):
1319 """Simple display of cryptographic algorithms in use."""
1321 MyWindow.__init__(me)
1322 me.set_title('Cryptographic algorithms')
1323 T.aside(me.populate)
1325 table = GridPacker()
1328 crypto = conn.algs()
1329 table.info('Diffie-Hellman group',
1330 '%s (%d-bit order, %d-bit elements)' %
1331 (crypto['kx-group'],
1332 int(crypto['kx-group-order-bits']),
1333 int(crypto['kx-group-elt-bits'])),
1335 table.info('Data encryption',
1336 '%s (%d-bit key; %s)' %
1338 int(crypto['cipher-keysz']) * 8,
1339 crypto['cipher-blksz'] == '0'
1341 or '%d-bit block' % (int(crypto['cipher-blksz']) * 8)),
1343 table.info('Message authentication',
1344 '%s (%d-bit key; %d-bit tag)' %
1346 int(crypto['mac-keysz']) * 8,
1347 int(crypto['mac-tagsz']) * 8),
1349 table.info('Hash function',
1350 '%s (%d-bit output)' %
1352 int(crypto['hash-sz']) * 8),
1357 ###--------------------------------------------------------------------------
1358 ### Main monitor window.
1360 class MonitorWindow (MyWindow):
1363 The main monitor window.
1365 This class creates, populates and maintains the main monitor window.
1369 * warnings, trace: log models for server output
1370 * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1371 WindowSlot objects for ancillary windows
1372 * ui: Gtk UIManager object for the menu system
1373 * apmenu: pair of identical autoconnecting peer menus
1374 * listmodel: Gtk ListStore for connected peers; contains peer name,
1375 address, and ping times (transport and encrypted, value and colour)
1376 * status: Gtk Statusbar at the bottom of the window
1377 * _kidding: an unpleasant backchannel between the apchange method (which
1378 builds the apmenus) and the menu handler, forced on us by a Gtk
1381 Also installs attributes on Peer objects:
1383 * i: index of peer's entry in listmodel
1384 * win: WindowSlot object for the peer's PeerWindow
1388 """Construct the window."""
1391 MyWindow.__init__(me)
1392 me.set_title('TrIPE monitor')
1394 ## Hook onto diagnostic outputs.
1395 me.warnings = WarningLogModel()
1396 me.hook(conn.warnhook, me.warnings.notify)
1397 me.trace = TraceLogModel()
1398 me.hook(conn.tracehook, me.trace.notify)
1400 ## Make slots to store the various ancillary singleton windows.
1401 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1402 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1403 me.traceopts = WindowSlot(lambda: TraceOptions())
1404 me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1405 me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1406 me.servinfo = WindowSlot(lambda: ServInfo())
1408 ## Main window structure.
1412 ## UI manager makes our menus. (We're too cheap to have a toolbar.)
1413 me.ui = G.UIManager()
1414 actgroup = makeactiongroup('monitor',
1415 [('file-menu', '_File', None, None),
1416 ('connect', '_Connect', '<Control>C', conn.connect),
1417 ('disconnect', '_Disconnect', '<Control>D',
1418 lambda: conn.disconnect(None)),
1419 ('quit', '_Quit', '<Control>Q', me.close),
1420 ('server-menu', '_Server', None, None),
1421 ('daemon', 'Run in _background', None, cr(conn.daemon)),
1422 ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1423 ('crypto-algs', 'Cryptographic algorithms',
1424 '<Control>Y', me.cryptoinfo.open),
1425 ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1426 ('server-quit', 'Terminate server', None, cr(conn.quit)),
1427 ('conn-peer', 'Connect peer', None, None),
1428 ('logs-menu', '_Logs', None, None),
1429 ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1430 ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1431 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1432 ('help-menu', '_Help', None, None),
1433 ('about', '_About tripemon...', None, aboutbox.open),
1434 ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1435 ('kill-peer', '_Kill peer', None, me.killpeer),
1436 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1442 <menu action="file-menu">
1443 <menuitem action="quit"/>
1445 <menu action="server-menu">
1446 <menuitem action="connect"/>
1447 <menuitem action="disconnect"/>
1449 <menuitem action="server-version"/>
1450 <menuitem action="crypto-algs"/>
1451 <menuitem action="add-peer"/>
1452 <menuitem action="conn-peer"/>
1453 <menuitem action="daemon"/>
1454 <menuitem action="reload-keys"/>
1456 <menuitem action="server-quit"/>
1458 <menu action="logs-menu">
1459 <menuitem action="show-warnings"/>
1460 <menuitem action="show-trace"/>
1461 <menuitem action="trace-options"/>
1463 <menu action="help-menu">
1464 <menuitem action="about"/>
1467 <popup name="peer-popup">
1468 <menuitem action="add-peer"/>
1469 <menuitem action="conn-peer"/>
1470 <menuitem action="kill-peer"/>
1471 <menuitem action="force-kx"/>
1476 ## Populate the UI manager.
1477 me.ui.insert_action_group(actgroup, 0)
1478 me.ui.add_ui_from_string(uidef)
1480 ## Construct the menu bar.
1481 vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
1482 me.add_accel_group(me.ui.get_accel_group())
1484 ## Construct and attach the auto-peers menu. (This is a horrible bodge
1485 ## because we can't attach the same submenu in two different places.)
1486 me.apmenu = G.Menu(), G.Menu()
1487 me.ui.get_widget('/menubar/server-menu/conn-peer') \
1488 .set_submenu(me.apmenu[0])
1489 me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1491 ## Construct the main list model, and listen on hooks which report
1492 ## changes to the available peers.
1493 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1494 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1495 me.hook(monitor.peers.addhook, me.addpeer)
1496 me.hook(monitor.peers.delhook, me.delpeer)
1497 me.hook(monitor.autopeershook, me.apchange)
1499 ## Construct the list viewer and put it in a scrolling window.
1500 scr = MyScrolledWindow()
1501 me.list = MyTreeView(me.listmodel)
1502 me.list.append_column(G.TreeViewColumn('Peer name',
1503 G.CellRendererText(),
1505 me.list.append_column(G.TreeViewColumn('Address',
1506 G.CellRendererText(),
1508 me.list.append_column(G.TreeViewColumn('T-ping',
1509 G.CellRendererText(),
1512 me.list.append_column(G.TreeViewColumn('E-ping',
1513 G.CellRendererText(),
1516 me.list.get_column(1).set_expand(True)
1517 me.list.connect('row-activated', me.activate)
1518 me.list.connect('button-press-event', me.buttonpress)
1519 me.list.set_reorderable(True)
1520 me.list.get_selection().set_mode(G.SELECTION_NONE)
1522 vbox.pack_start(scr)
1524 ## Construct the status bar, and listen on hooks which report changes to
1525 ## connection status.
1526 me.status = G.Statusbar()
1527 vbox.pack_start(me.status, expand = False)
1528 me.hook(conn.connecthook, cr(me.connected))
1529 me.hook(conn.disconnecthook, me.disconnected)
1530 me.hook(conn.notehook, me.notify)
1532 ## Set a plausible default window size.
1533 me.set_default_size(512, 180)
1535 def addpeer(me, peer):
1536 """Hook: announces that PEER has been added."""
1537 peer.i = me.listmodel.append([peer.name, peer.addr,
1538 '???', 'green', '???', 'green'])
1539 peer.win = WindowSlot(lambda: PeerWindow(peer))
1540 me.hook(peer.pinghook, me._ping)
1543 def delpeer(me, peer):
1544 """Hook: announces that PEER has been removed."""
1545 me.listmodel.remove(peer.i)
1546 me.unhook(peer.pinghook)
1549 def path_peer(me, path):
1550 """Return the peer corresponding to a given list-model PATH."""
1551 return monitor.peers[me.listmodel[path][0]]
1555 Hook: announces that a change has been made to the peers available for
1556 automated connection.
1558 This populates both auto-peer menus and keeps them in sync. (As
1559 mentioned above, we can't attach the same submenu to two separate parent
1560 menu items. So we end up with two identical menus instead. Yes, this
1564 ## The set_active method of a CheckMenuItem works by maybe activating the
1565 ## menu item. This signals our handler. But we don't actually want to
1566 ## signal the handler unless the user actually frobbed the item. So the
1567 ## _kidding flag is used as an underhanded way of telling the handler
1568 ## that we don't actually want it to do anything. Of course, this sucks
1572 ## Iterate over the two menus.
1575 existing = menu.get_children()
1576 if monitor.autopeers is None:
1578 ## No peers, so empty out the menu.
1579 for item in existing:
1584 ## Insert the new items into the menu. (XXX this seems buggy XXX)
1585 ## Tick the peers which are actually connected.
1587 for peer in monitor.autopeers:
1588 if j < len(existing) and \
1589 existing[j].get_child().get_text() == peer:
1593 item = G.CheckMenuItem(peer, use_underline = False)
1594 item.connect('activate', invoker(me._addautopeer, peer))
1595 menu.insert(item, i)
1596 item.set_active(peer in monitor.peers.table)
1599 ## Make all the menu items visible.
1602 ## Set the parent menu items sensitive if and only if there are any peers
1604 for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1605 me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1607 ## And now allow the handler to do its business normally.
1610 def _addautopeer(me, peer):
1612 Automatically connect an auto-peer.
1614 This method is invoked from the main coroutine. Since the actual
1615 connection needs to issue administration commands, we must spawn a new
1616 child coroutine for it.
1620 T.Coroutine(me._addautopeer_hack,
1621 name = '_addautopeerhack %s' % peer).switch(peer)
1623 def _addautopeer_hack(me, peer):
1624 """Make an automated connection to PEER in response to a user click."""
1628 T._simple(conn.svcsubmit('connect', 'active', peer))
1629 except T.TripeError, exc:
1630 T.defer(moanbox, ' '.join(exc.args))
1633 def activate(me, l, path, col):
1635 Handle a double-click on a peer in the main list: open a PeerInfo window.
1637 peer = me.path_peer(path)
1640 def buttonpress(me, l, ev):
1642 Handle a mouse click on the main list.
1644 Currently we're only interested in button-3, which pops up the peer menu.
1645 For future reference, we stash the peer that was clicked in me.menupeer.
1648 x, y = int(ev.x), int(ev.y)
1649 r = me.list.get_path_at_pos(x, y)
1650 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1651 me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1653 me.ui.get_widget('/peer-popup/conn-peer'). \
1654 set_sensitive(bool(monitor.autopeers))
1656 me.menupeer = me.path_peer(r[0])
1659 me.ui.get_widget('/peer-popup').popup(
1660 None, None, None, ev.button, ev.time)
1663 """Kill a peer from the popup menu."""
1664 cr(conn.kill, me.menupeer.name)()
1667 """Kickstart a key-exchange from the popup menu."""
1668 cr(conn.forcekx, me.menupeer.name)()
1670 _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1671 def _ping(me, p, cmd, ps):
1672 """Hook: responds to ping reports."""
1673 textcol, colourcol = me._columnmap[cmd]
1675 me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1676 me.listmodel[p.i][colourcol] = 'red'
1678 me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1679 me.listmodel[p.i][colourcol] = 'black'
1681 def setstatus(me, status):
1682 """Update the message in the status bar."""
1684 me.status.push(0, status)
1686 def notify(me, note, *rest):
1687 """Hook: invoked when interesting notifications occur."""
1688 if note == 'DAEMON':
1689 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1693 Hook: invoked when a connection is made to the server.
1695 Make options which require a server connection sensitive.
1697 me.setstatus('Connected (port %s)' % conn.port())
1698 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1699 for i in ('/menubar/server-menu/disconnect',
1700 '/menubar/server-menu/server-version',
1701 '/menubar/server-menu/add-peer',
1702 '/menubar/server-menu/server-quit',
1703 '/menubar/logs-menu/trace-options'):
1704 me.ui.get_widget(i).set_sensitive(True)
1705 me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1706 set_sensitive(bool(monitor.autopeers))
1707 me.ui.get_widget('/menubar/server-menu/daemon'). \
1708 set_sensitive(conn.servinfo()['daemon'] == 'nil')
1710 def disconnected(me, reason):
1712 Hook: invoked when the connection to the server is lost.
1714 Make most options insensitive.
1716 me.setstatus('Disconnected')
1717 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1718 for i in ('/menubar/server-menu/disconnect',
1719 '/menubar/server-menu/server-version',
1720 '/menubar/server-menu/add-peer',
1721 '/menubar/server-menu/conn-peer',
1722 '/menubar/server-menu/daemon',
1723 '/menubar/server-menu/server-quit',
1724 '/menubar/logs-menu/trace-options'):
1725 me.ui.get_widget(i).set_sensitive(False)
1726 if reason: moanbox(reason)
1728 ###--------------------------------------------------------------------------
1731 def parse_options():
1733 Parse command-line options.
1735 Process the boring ones. Return all of them, for later.
1737 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1738 version = '%prog (tripe version 1.0.0)')
1739 op.add_option('-a', '--admin-socket',
1740 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1741 help = 'Select socket to connect to [default %default]')
1742 op.add_option('-d', '--directory',
1743 metavar = 'DIR', dest = 'dir', default = T.configdir,
1744 help = 'Select current diretory [default %default]')
1745 opts, args = op.parse_args()
1746 if args: op.error('no arguments permitted')
1751 """Initialization."""
1753 global conn, monitor, pinger
1755 ## Try to establish a connection.
1756 conn = Connection(opts.tripesock)
1758 ## Make the main interesting coroutines and objects.
1766 root = MonitorWindow()
1771 HookClient().hook(root.closehook, exit)
1774 if __name__ == '__main__':
1775 opts = parse_options()
1779 ###----- That's all, folks --------------------------------------------------