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 def _(*hunoz, **hukairz):
87 T.Coroutine(xwrap(func)).switch(*args, **kw)
91 """Decorator: runs its function in a coroutine of its own."""
92 return lambda *args, **kw: T.Coroutine(func).switch(*args, **kw)
94 ###--------------------------------------------------------------------------
95 ### Random bits of infrastructure.
97 ## Program name, shorn of extraneous stuff.
102 class HookList (object):
104 Notification hook list.
106 Other objects can add functions onto the hook list. When the hook list is
107 run, the functions are called in the order in which they were registered.
111 """Basic initialization: create the hook list."""
114 def add(me, func, obj):
115 """Add FUNC to the list of hook functions."""
116 me.list.append((obj, func))
119 """Remove hook functions registered with the given OBJ."""
126 def run(me, *args, **kw):
127 """Invoke the hook functions with arguments *ARGS and **KW."""
128 for o, hook in me.list:
129 rc = hook(*args, **kw)
130 if rc is not None: return rc
133 class HookClient (object):
135 Mixin for classes which are clients of hooks.
137 It keeps track of the hooks it's a client of, and has the ability to
138 extricate itself from all of them. This is useful because weak objects
139 don't seem to work well.
142 """Basic initialization."""
145 def hook(me, hk, func):
146 """Add FUNC to the hook list HK."""
151 """Remove myself from the hook list HK."""
156 """Remove myself from all hook lists."""
161 class struct (object):
162 """A very simple dumb data container object."""
163 def __init__(me, **kw):
164 me.__dict__.update(kw)
166 ## Matches ISO date format yyyy-mm-ddThh:mm:ss.
167 rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
169 ###--------------------------------------------------------------------------
172 class GIOWatcher (object):
174 Monitor I/O events using glib.
176 def __init__(me, conn, mc = GO.main_context_default()):
180 def connected(me, sock):
181 me._watch = GO.io_add_watch(sock, GO.IO_IN,
182 lambda *hunoz: me._conn.receive())
183 def disconnected(me):
184 GO.source_remove(me._watch)
187 me._mc.iteration(True)
189 class Connection (T.TripeCommandDispatcher):
191 The main connection to the server.
193 The improvement over the TripeCommandDispatcher is that the Connection
194 provides hooklists for NOTE, WARN and TRACE messages, and for connect and
197 This class knows about the Glib I/O dispatcher system, and plugs into it.
201 * connecthook(): a connection to the server has been established
202 * disconnecthook(): the connection has been dropped
203 * notehook(TOKEN, ...): server issued a notification
204 * warnhook(TOKEN, ...): server issued a warning
205 * tracehook(TOKEN, ...): server issued a trace message
208 def __init__(me, socket):
209 """Create a new Connection."""
210 T.TripeCommandDispatcher.__init__(me, socket)
211 me.connecthook = HookList()
212 me.disconnecthook = HookList()
213 me.notehook = HookList()
214 me.warnhook = HookList()
215 me.tracehook = HookList()
216 me.handler['NOTE'] = lambda _, *rest: me.notehook.run(*rest)
217 me.handler['WARN'] = lambda _, *rest: me.warnhook.run(*rest)
218 me.handler['TRACE'] = lambda _, *rest: me.tracehook.run(*rest)
219 me.iowatch = GIOWatcher(me)
222 """Handles reconnection to the server, and signals the hook."""
223 T.TripeCommandDispatcher.connected(me)
226 def disconnected(me, reason):
227 """Handles disconnection from the server, and signals the hook."""
228 me.disconnecthook.run(reason)
229 T.TripeCommandDispatcher.disconnected(me, reason)
231 ###--------------------------------------------------------------------------
232 ### Watching the peers go by.
234 class MonitorObject (object):
236 An object with hooks it uses to notify others of changes in its state.
237 These are the objects tracked by the MonitorList class.
239 The object has a name, an `aliveness' state indicated by the `alivep' flag,
244 * changehook(): the object has changed its state
245 * deadhook(): the object has been destroyed
247 Subclass responsibilities:
249 * update(INFO): update internal state based on the provided INFO, and run
253 def __init__(me, name):
254 """Initialize the object with the given NAME."""
256 me.deadhook = HookList()
257 me.changehook = HookList()
261 """Mark the object as dead; invoke the deadhook."""
265 class Peer (MonitorObject):
267 An object representing a connected peer.
269 As well as the standard hooks, a peer has a pinghook, which isn't used
270 directly by this class.
274 * pinghook(): invoked by the Pinger (q.v.) when ping statistics change
276 Attributes provided are:
278 * addr = a vaguely human-readable representation of the peer's address
279 * ifname = the peer's interface name
280 * tunnel = the kind of tunnel the peer is using
281 * keepalive = the peer's keepalive interval in seconds
282 * ping['EPING'] and ping['PING'] = pingstate statistics (maintained by
286 def __init__(me, name):
287 """Initialize the object with the given name."""
288 MonitorObject.__init__(me, name)
289 me.pinghook = HookList()
292 def update(me, hunoz = None):
293 """Update the peer, fetching information about it from the server."""
294 addr = conn.addr(me.name)
295 if addr[0] == 'INET':
296 ipaddr, port = addr[1:]
298 name = S.gethostbyaddr(ipaddr)[0]
299 me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
301 me.addr = 'INET %s:%s' % (ipaddr, port)
303 me.addr = ' '.join(addr)
304 me.ifname = conn.ifname(me.name)
305 me.__dict__.update(conn.peerinfo(me.name))
308 def setifname(me, newname):
309 """Informs the object of a change to its interface name to NEWNAME."""
313 class Service (MonitorObject):
315 Represents a service.
317 Additional attributes are:
319 * version = the service version
321 def __init__(me, name, version):
322 MonitorObject.__init__(me, name)
325 def update(me, version):
326 """Tell the Service that its version has changed to VERSION."""
330 class MonitorList (object):
332 Maintains a collection of MonitorObjects.
334 The MonitorList can be indexed by name to retrieve the individual objects;
335 iteration generates the individual objects. More complicated operations
336 can be done on the `table' dictionary directly.
338 Hooks addhook(OBJ) and delhook(OBJ) are invoked when objects are added or
341 Subclass responsibilities:
343 * list(): return a list of (NAME, INFO) pairs.
345 * make(NAME, INFO): returns a new MonitorObject for the given NAME; INFO
346 is from the output of list().
350 """Initialize a new MonitorList."""
352 me.addhook = HookList()
353 me.delhook = HookList()
357 Refresh the list of objects:
359 We add new object which have appeared, delete ones which have vanished,
360 and update any which persist.
363 for name, stuff in me.list():
366 for name in me.table.copy():
370 def add(me, name, stuff):
372 Add a new object created by make(NAME, STUFF) if it doesn't already
373 exist. If it does, update it.
375 if name not in me.table:
376 obj = me.make(name, stuff)
380 me.table[name].update(stuff)
382 def remove(me, name):
384 Remove the object called NAME from the list.
386 The object becomes dead.
394 def __getitem__(me, name):
395 """Retrieve the object called NAME."""
396 return me.table[name]
399 """Iterate over the objects."""
400 return me.table.itervalues()
402 class PeerList (MonitorList):
403 """The list of the known peers."""
405 return [(name, None) for name in conn.list()]
406 def make(me, name, stuff):
409 class ServiceList (MonitorList):
410 """The list of the registered services."""
412 return conn.svclist()
413 def make(me, name, stuff):
414 return Service(name, stuff)
416 class Monitor (HookClient):
418 The main monitor: keeps track of the changes happening to the server.
420 Exports the peers, services MonitorLists, and a (plain Python) list
421 autopeers of peers which the connect service knows how to start by name.
425 * autopeershook(): invoked when the auto-peers list changes.
428 """Initialize the Monitor."""
429 HookClient.__init__(me)
430 me.peers = PeerList()
431 me.services = ServiceList()
432 me.hook(conn.connecthook, me._connected)
433 me.hook(conn.notehook, me._notify)
434 me.autopeershook = HookList()
438 """Handle a successful connection by starting the setup coroutine."""
443 """Coroutine function: initialize for a new connection."""
447 me._updateautopeers()
449 def _updateautopeers(me):
450 """Update the auto-peers list from the connect service."""
451 if 'connect' in me.services.table:
452 me.autopeers = [' '.join(line)
453 for line in conn.svcsubmit('connect', 'list')]
457 me.autopeershook.run()
459 def _notify(me, code, *rest):
461 Handle notifications from the server.
463 ADD, KILL and NEWIFNAME notifications get passed up to the PeerList;
464 SVCCLAIM and SVCRELEASE get passed up to the ServiceList. Finally,
465 peerdb-update notifications from the watch service cause us to refresh
469 T.aside(me.peers.add, rest[0], None)
471 T.aside(me.peers.remove, rest[0])
472 elif code == 'NEWIFNAME':
474 me.peers[rest[0]].setifname(rest[2])
477 elif code == 'SVCCLAIM':
478 T.aside(me.services.add, rest[0], rest[1])
479 if rest[0] == 'connect':
480 T.aside(me._updateautopeers)
481 elif code == 'SVCRELEASE':
482 T.aside(me.services.remove, rest[0])
483 if rest[0] == 'connect':
484 T.aside(me._updateautopeers)
487 if rest[0] == 'watch' and \
488 rest[1] == 'peerdb-update':
489 T.aside(me._updateautopeers)
491 ###--------------------------------------------------------------------------
492 ### Window management cruft.
494 class MyWindowMixin (G.Window, HookClient):
496 Mixin for windows which call a closehook when they're destroyed. It's also
497 a hookclient, and will release its hooks when it's destroyed.
501 * closehook(): called when the window is closed.
505 """Initialization function. Note that it's not called __init__!"""
506 me.closehook = HookList()
507 HookClient.__init__(me)
508 me.connect('destroy', invoker(me.close))
511 """Close the window, invoking the closehook and releasing all hooks."""
516 class MyWindow (MyWindowMixin):
517 """A version of MyWindowMixin suitable as a single parent class."""
518 def __init__(me, kind = G.WINDOW_TOPLEVEL):
519 G.Window.__init__(me, kind)
522 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
523 """A dialogue box with a closehook and sensible button binding."""
525 def __init__(me, title = None, flags = 0, buttons = []):
527 The BUTTONS are a list of (STOCKID, THUNK) pairs: call the appropriate
528 THUNK when the button is pressed. The other arguments are just like
539 G.Dialog.__init__(me, title, None, flags, tuple(br))
541 me.set_default_response(i - 1)
542 me.connect('response', me.respond)
544 def respond(me, hunoz, rid, *hukairz):
545 """Dispatch responses to the appropriate thunks."""
546 if rid >= 0: me.rmap[rid]()
548 def makeactiongroup(name, acts):
550 Creates an ActionGroup called NAME.
552 ACTS is a list of tuples containing:
554 * ACT: an action name
555 * LABEL: the label string for the action
556 * ACCEL: accelerator string, or None
557 * FUNC: thunk to call when the action is invoked
559 actgroup = G.ActionGroup(name)
560 for act, label, accel, func in acts:
561 a = G.Action(act, label, None, None)
562 if func: a.connect('activate', invoker(func))
563 actgroup.add_action_with_accel(a, accel)
566 class GridPacker (G.Table):
568 Like a Table, but with more state: makes filling in the widgets easier.
572 """Initialize a new GridPacker."""
578 me.set_border_width(4)
579 me.set_col_spacings(4)
580 me.set_row_spacings(4)
582 def pack(me, w, width = 1, newlinep = False,
583 xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
588 W is the widget to add. XOPY, YOPT, XPAD and YPAD are as for Table.
589 WIDTH is how many cells to take up horizontally. NEWLINEP is whether to
590 start a new line for this widget. Returns W.
596 right = me.col + width
597 if bot > me.rows or right > me.cols:
598 if bot > me.rows: me.rows = bot
599 if right > me.cols: me.cols = right
600 me.resize(me.rows, me.cols)
601 me.attach(w, me.col, me.col + width, me.row, me.row + 1,
602 xopt, yopt, xpad, ypad)
606 def labelled(me, lab, w, newlinep = False, **kw):
608 Packs a labelled widget.
610 Other arguments are as for pack. Returns W.
612 label = G.Label(lab + ' ')
613 label.set_alignment(1.0, 0)
614 me.pack(label, newlinep = newlinep, xopt = G.FILL)
618 def info(me, label, text = None, len = 18, **kw):
620 Packs an information widget with a label.
622 LABEL is the label; TEXT is the initial text; LEN is the estimated length
623 in characters. Returns the entry widget.
626 if text is not None: e.set_text(text)
627 e.set_width_chars(len)
628 e.set_selectable(True)
629 e.set_alignment(0.0, 0.5)
630 me.labelled(label, e, **kw)
633 class WindowSlot (HookClient):
635 A place to store a window -- specificially a MyWindowMixin.
637 If the window is destroyed, remember this; when we come to open the window,
638 raise it if it already exists; otherwise make a new one.
640 def __init__(me, createfunc):
642 Constructor: CREATEFUNC must return a new Window which supports the
645 HookClient.__init__(me)
646 me.createfunc = createfunc
650 """Opens the window, creating it if necessary."""
652 me.window.window.raise_()
654 me.window = me.createfunc()
655 me.hook(me.window.closehook, me.closed)
658 """Handles the window being closed."""
659 me.unhook(me.window.closehook)
662 class MyTreeView (G.TreeView):
663 def __init__(me, model):
664 G.TreeView.__init__(me, model)
665 me.set_rules_hint(True)
667 class MyScrolledWindow (G.ScrolledWindow):
669 G.ScrolledWindow.__init__(me)
670 me.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
671 me.set_shadow_type(G.SHADOW_IN)
673 ## Matches a signed integer.
674 rx_num = RX.compile(r'^[-+]?\d+$')
677 c_red = GDK.color_parse('red')
679 class ValidationError (Exception):
680 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
683 class ValidatingEntry (G.Entry):
685 Like an Entry, but makes the text go red if the contents are invalid.
687 If get_text is called, and the text is invalid, ValidationError is raised.
688 The attribute validp reflects whether the contents are currently valid.
691 def __init__(me, valid, text = '', size = -1, *arg, **kw):
693 Make a validating Entry.
695 VALID is a regular expression or a predicate on strings. TEXT is the
696 default text to insert. SIZE is the size of the box to set, in
697 characters (ish). Other arguments are passed to Entry.
699 G.Entry.__init__(me, *arg, **kw)
700 me.connect("changed", me.check)
704 me.validate = RX.compile(valid).match
706 me.c_ok = me.get_style().text[G.STATE_NORMAL]
708 if size != -1: me.set_width_chars(size)
709 me.set_activates_default(True)
713 def check(me, *hunoz):
714 """Check the current text and update validp and the text colour."""
715 if me.validate(G.Entry.get_text(me)):
717 me.modify_text(G.STATE_NORMAL, me.c_ok)
720 me.modify_text(G.STATE_NORMAL, me.c_bad)
724 Return the text in the Entry if it's valid. If it isn't, raise
728 raise ValidationError
729 return G.Entry.get_text(me)
731 def numericvalidate(min = None, max = None):
733 Return a validation function for numbers.
735 Entry must consist of an optional sign followed by digits, and the
736 resulting integer must be within the given bounds.
738 return lambda x: (rx_num.match(x) and
739 (min is None or long(x) >= min) and
740 (max is None or long(x) <= max))
742 ###--------------------------------------------------------------------------
743 ### Various minor dialog boxen.
745 GPL = """This program is free software; you can redistribute it and/or modify
746 it under the terms of the GNU General Public License as published by
747 the Free Software Foundation; either version 2 of the License, or
748 (at your option) any later version.
750 This program is distributed in the hope that it will be useful,
751 but WITHOUT ANY WARRANTY; without even the implied warranty of
752 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
753 GNU General Public License for more details.
755 You should have received a copy of the GNU General Public License
756 along with this program; if not, write to the Free Software Foundation,
757 Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
759 class AboutBox (G.AboutDialog, MyWindowMixin):
760 """The program `About' box."""
762 G.AboutDialog.__init__(me)
764 me.set_name('TrIPEmon')
765 me.set_version(T.VERSION)
767 me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
768 me.set_comments('A graphical monitor for the TrIPE VPN server')
769 me.set_copyright('Copyright © 2006-2008 Straylight/Edgeware')
770 me.connect('response', me.respond)
772 def respond(me, hunoz, rid, *hukairz):
773 if rid == G.RESPONSE_CANCEL:
775 aboutbox = WindowSlot(AboutBox)
778 """Report an error message in a window."""
779 d = G.Dialog('Error from %s' % M.quis,
780 flags = G.DIALOG_MODAL,
781 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
783 label.set_padding(20, 20)
784 d.vbox.pack_start(label)
789 def unimplemented(*hunoz):
790 """Indicator of laziness."""
791 moanbox("I've not written that bit yet.")
793 ###--------------------------------------------------------------------------
796 class LogModel (G.ListStore):
798 A simple list of log messages, usable as the model for a TreeView.
800 The column headings are stored in the `cols' attribute.
803 def __init__(me, columns):
805 COLUMNS must be a list of column name strings. We add a time column to
808 me.cols = ('Time',) + columns
809 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
811 def add(me, *entries):
813 Adds a new log message, with a timestamp.
815 The ENTRIES are the contents for the list columns.
817 now = TIME.strftime('%Y-%m-%d %H:%M:%S')
818 me.append((now, ) + entries)
820 class TraceLogModel (LogModel):
821 """Log model for trace messages."""
823 LogModel.__init__(me, ('Message',))
824 def notify(me, line):
825 """Call with a new trace message."""
828 class WarningLogModel (LogModel):
830 Log model for warnings.
832 We split the category out into a separate column.
835 LogModel.__init__(me, ('Category', 'Message'))
836 def notify(me, tag, *rest):
837 """Call with a new warning message."""
838 me.add(tag, ' '.join([T.quotify(w) for w in rest]))
840 class LogViewer (MyWindow):
844 Its contents are a TreeView showing the log.
848 * model: an appropriate LogModel
849 * list: a TreeView widget to display the log
852 def __init__(me, model):
854 Create a log viewer showing the LogModel MODEL.
856 MyWindow.__init__(me)
858 scr = MyScrolledWindow()
859 me.list = MyTreeView(me.model)
861 for c in me.model.cols:
862 crt = G.CellRendererText()
863 me.list.append_column(G.TreeViewColumn(c, crt, text = i))
865 crt.set_property('family', 'monospace')
866 me.set_default_size(440, 256)
871 ###--------------------------------------------------------------------------
874 class pingstate (struct):
876 Information kept for each peer by the Pinger.
878 Important attributes:
880 * peer = the peer name
881 * command = PING or EPING
882 * n = how many pings we've sent so far
883 * ngood = how many returned
884 * nmiss = how many didn't return
885 * nmissrun = how many pings since the last good one
886 * tlast = round-trip time for the last (good) ping
887 * ttot = total roung trip time
891 class Pinger (T.Coroutine, HookClient):
893 Coroutine which pings known peers and collects statistics.
895 Interesting attributes:
897 * _map: dict mapping peer names to Peer objects
898 * _q: event queue for notifying pinger coroutine
899 * _timer: gobject timer for waking the coroutine
904 Initialize the pinger.
906 We watch the monitor's PeerList to track which peers we should ping. We
907 maintain an event queue and put all the events on that.
909 The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
910 where CMD is 'PING' or 'EPING'.
912 T.Coroutine.__init__(me)
913 HookClient.__init__(me)
917 me.hook(conn.connecthook, me._connected)
918 me.hook(conn.disconnecthook, me._disconnected)
919 me.hook(monitor.peers.addhook,
920 lambda p: T.defer(me._q.put, (p, 'ADD', None)))
921 me.hook(monitor.peers.delhook,
922 lambda p: T.defer(me._q.put, (p, 'KILL', None)))
923 if conn.connectedp(): me.connected()
926 """Respond to connection: start pinging thngs."""
927 me._timer = GO.timeout_add(1000, me._timerfunc)
930 """Timer function: put a timer event on the queue."""
931 me._q.put((None, 'TIMER', None))
934 def _disconnected(me, reason):
935 """Respond to disconnection: stop pinging."""
936 GO.source_remove(me._timer)
940 Coroutine function: read events from the queue and process them.
944 * (PEER, 'KILL', None): remove PEER from the interesting peers list
945 * (PEER, 'ADD', None): add PEER to the list
946 * (PEER, 'INFO', TOKENS): result from a PING command
947 * (None, 'TIMER', None): interval timer went off: send more pings
950 tag, code, stuff = me._q.get()
955 elif not conn.connectedp():
960 for cmd in 'PING', 'EPING':
961 ps = pingstate(command = cmd, peer = p,
962 n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
968 if stuff[0] == 'ping-ok':
978 ps.peer.pinghook.run(ps.peer, ps.command, ps)
979 elif code == 'TIMER':
980 for name, p in me._map.iteritems():
981 for cmd, ps in p.ping.iteritems():
982 conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
983 cmd, '-background', conn.bgtag(), '--', name]))
985 ###--------------------------------------------------------------------------
986 ### Random dialogue boxes.
988 class AddPeerDialog (MyDialog):
990 Let the user create a new peer the low-level way.
992 Interesting attributes:
994 * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
998 """Initialize the dialogue."""
999 MyDialog.__init__(me, 'Add peer',
1000 buttons = [(G.STOCK_CANCEL, me.destroy),
1001 (G.STOCK_OK, me.ok)])
1006 """Coroutine function: background setup for AddPeerDialog."""
1007 table = GridPacker()
1008 me.vbox.pack_start(table)
1009 me.e_name = table.labelled('Name',
1010 ValidatingEntry(r'^[^\s.:]+$', '', 16),
1012 me.e_addr = table.labelled('Address',
1013 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
1015 me.e_port = table.labelled('Port',
1016 ValidatingEntry(numericvalidate(0, 65535),
1019 me.c_keepalive = G.CheckButton('Keepalives')
1020 me.l_tunnel = table.labelled('Tunnel',
1021 G.combo_box_new_text(),
1022 newlinep = True, width = 3)
1023 me.tuns = conn.tunnels()
1025 me.l_tunnel.append_text(t)
1026 me.l_tunnel.set_active(0)
1027 table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
1028 me.c_keepalive.connect('toggled',
1029 lambda t: me.e_keepalive.set_sensitive\
1031 me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
1032 me.e_keepalive.set_sensitive(False)
1033 table.pack(me.e_keepalive, width = 3)
1037 """Handle an OK press: create the peer."""
1039 if me.c_keepalive.get_active():
1040 ka = me.e_keepalive.get_text()
1043 t = me.l_tunnel.get_active()
1048 me._addpeer(me.e_name.get_text(),
1049 me.e_addr.get_text(),
1050 me.e_port.get_text(),
1053 except ValidationError:
1058 def _addpeer(me, name, addr, port, keepalive, tunnel):
1059 """Coroutine function: actually do the ADD command."""
1061 conn.add(name, addr, port, keepalive = keepalive, tunnel = tunnel)
1063 except T.TripeError, exc:
1064 T.defer(moanbox, ' '.join(exc))
1066 class ServInfo (MyWindow):
1068 Show information about the server and available services.
1070 Interesting attributes:
1072 * e: maps SERVINFO keys to entry widgets
1073 * svcs: Gtk ListStore describing services (columns are name and version)
1077 MyWindow.__init__(me)
1078 me.set_title('TrIPE server info')
1079 table = GridPacker()
1082 def add(label, tag, text = None, **kw):
1083 me.e[tag] = table.info(label, text, **kw)
1084 add('Implementation', 'implementation')
1085 add('Version', 'version', newlinep = True)
1086 me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1087 me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1088 scr = MyScrolledWindow()
1089 lb = MyTreeView(me.svcs)
1091 for title in 'Service', 'Version':
1092 lb.append_column(G.TreeViewColumn(
1093 title, G.CellRendererText(), text = i))
1095 for svc in monitor.services:
1096 me.svcs.append([svc.name, svc.version])
1098 table.pack(scr, width = 2, newlinep = True,
1099 yopt = G.EXPAND | G.FILL | G.SHRINK)
1101 me.hook(conn.connecthook, me.update)
1102 me.hook(monitor.services.addhook, me.addsvc)
1103 me.hook(monitor.services.delhook, me.delsvc)
1106 def addsvc(me, svc):
1107 me.svcs.append([svc.name, svc.version])
1109 def delsvc(me, svc):
1110 for i in xrange(len(me.svcs)):
1111 if me.svcs[i][0] == svc.name:
1112 me.svcs.remove(me.svcs.get_iter(i))
1116 info = conn.servinfo()
1118 me.e[i].set_text(info[i])
1120 class TraceOptions (MyDialog):
1121 """Tracing options window."""
1123 MyDialog.__init__(me, title = 'Tracing options',
1124 buttons = [(G.STOCK_CLOSE, me.destroy),
1125 (G.STOCK_OK, cr(me.ok))])
1131 for ch, st, desc in conn.trace():
1132 if ch.isupper(): continue
1133 text = desc[0].upper() + desc[1:]
1134 ticky = G.CheckButton(text)
1135 ticky.set_active(st == '+')
1136 me.vbox.pack_start(ticky)
1137 me.opts.append((ch, ticky))
1142 for ch, ticky in me.opts:
1143 if ticky.get_active():
1147 setting = ''.join(on) + '-' + ''.join(off)
1151 ###--------------------------------------------------------------------------
1155 """Translate a TrIPE-format time to something human-readable."""
1156 if t == 'NEVER': return '(never)'
1157 YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1158 ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1159 ago = MATH.floor(ago); unit = 's'
1160 for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1164 return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1165 (YY, MM, DD, hh, mm, ss, ago, unit)
1167 """Translate a number of bytes into something a human might want to read."""
1174 return '%d %s' % (b, suff)
1176 ## How to translate peer stats. Maps the stat name to a translation
1179 [('start-time', xlate_time),
1180 ('last-packet-time', xlate_time),
1181 ('last-keyexch-time', xlate_time),
1182 ('bytes-in', xlate_bytes),
1183 ('bytes-out', xlate_bytes),
1184 ('keyexch-bytes-in', xlate_bytes),
1185 ('keyexch-bytes-out', xlate_bytes),
1186 ('ip-bytes-in', xlate_bytes),
1187 ('ip-bytes-out', xlate_bytes)]
1189 ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
1190 ## the label to give the entry box; FORMAT is the format string to write into
1193 [('Start time', '%(start-time)s'),
1194 ('Last key-exchange', '%(last-keyexch-time)s'),
1195 ('Last packet', '%(last-packet-time)s'),
1197 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1198 ('Key-exchange in/out',
1199 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1201 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1202 ('Rejected packets', '%(rejected-packets)s')]
1204 class PeerWindow (MyWindow):
1206 Show information about a peer.
1208 This gives a graphical view of the server's peer statistics.
1210 Interesting attributes:
1212 * e: dict mapping keys (mostly matching label widget texts, though pings
1213 use command names) to entry widgets so that we can update them easily
1214 * peer: the peer this window shows information about
1215 * cr: the info-fetching coroutine, or None if crrrently disconnected
1216 * doupate: whether the info-fetching corouting should continue running
1219 def __init__(me, peer):
1220 """Construct a PeerWindow, showing information about PEER."""
1222 MyWindow.__init__(me)
1223 me.set_title('TrIPE statistics: %s' % peer.name)
1226 table = GridPacker()
1229 ## Utility for adding fields.
1231 def add(label, text = None, key = None):
1232 if key is None: key = label
1233 me.e[key] = table.info(label, text, len = 42, newlinep = True)
1235 ## Build the dialogue box.
1236 add('Peer name', peer.name)
1237 add('Tunnel', peer.tunnel)
1238 add('Interface', peer.ifname)
1240 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1241 add('Address', peer.addr)
1242 add('Transport pings', key = 'PING')
1243 add('Encrypted pings', key = 'EPING')
1245 for label, format in statslayout:
1248 ## Hook onto various interesting events.
1249 me.hook(conn.connecthook, me.tryupdate)
1250 me.hook(conn.disconnecthook, me.stopupdate)
1251 me.hook(me.closehook, me.stopupdate)
1252 me.hook(me.peer.deadhook, me.dead)
1253 me.hook(me.peer.changehook, me.change)
1254 me.hook(me.peer.pinghook, me.ping)
1259 ## Format the ping statistics.
1260 for cmd, ps in me.peer.ping.iteritems():
1261 me.ping(me.peer, cmd, ps)
1263 ## And show the window.
1267 """Update the display in response to a notification."""
1268 me.e['Interface'].set_text(me.peer.ifname)
1272 Main display-updating coroutine.
1274 This does an update, sleeps for a while, and starts again. If the
1275 me.doupdate flag goes low, we stop the loop.
1277 while me.peer.alivep and conn.connectedp() and me.doupdate:
1278 stat = conn.stats(me.peer.name)
1279 for s, trans in statsxlate:
1280 stat[s] = trans(stat[s])
1281 for label, format in statslayout:
1282 me.e[label].set_text(format % stat)
1283 GO.timeout_add(1000, lambda: me.cr.switch() and False)
1284 me.cr.parent.switch()
1288 """Start the updater coroutine, if it's not going already."""
1290 me.cr = T.Coroutine(me._update)
1293 def stopupdate(me, *hunoz, **hukairz):
1294 """Stop the update coroutine, by setting me.doupdate."""
1298 """Called when the peer is killed."""
1299 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1300 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1303 def ping(me, peer, cmd, ps):
1304 """Called when a ping result for the peer is reported."""
1305 s = '%d/%d' % (ps.ngood, ps.n)
1307 s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1309 s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1310 me.e[ps.command].set_text(s)
1312 ###--------------------------------------------------------------------------
1313 ### Cryptographic status.
1315 class CryptoInfo (MyWindow):
1316 """Simple display of cryptographic algorithms in use."""
1318 MyWindow.__init__(me)
1319 me.set_title('Cryptographic algorithms')
1320 T.aside(me.populate)
1322 table = GridPacker()
1325 crypto = conn.algs()
1326 table.info('Diffie-Hellman group',
1327 '%s (%d-bit order, %d-bit elements)' %
1328 (crypto['kx-group'],
1329 int(crypto['kx-group-order-bits']),
1330 int(crypto['kx-group-elt-bits'])),
1332 table.info('Data encryption',
1333 '%s (%d-bit key; %s)' %
1335 int(crypto['cipher-keysz']) * 8,
1336 crypto['cipher-blksz'] == '0'
1338 or '%d-bit block' % (int(crypto['cipher-blksz']) * 8)),
1340 table.info('Message authentication',
1341 '%s (%d-bit key; %d-bit tag)' %
1343 int(crypto['mac-keysz']) * 8,
1344 int(crypto['mac-tagsz']) * 8),
1346 table.info('Hash function',
1347 '%s (%d-bit output)' %
1349 int(crypto['hash-sz']) * 8),
1354 ###--------------------------------------------------------------------------
1355 ### Main monitor window.
1357 class MonitorWindow (MyWindow):
1360 The main monitor window.
1362 This class creates, populates and maintains the main monitor window.
1366 * warnings, trace: log models for server output
1367 * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1368 WindowSlot objects for ancillary windows
1369 * ui: Gtk UIManager object for the menu system
1370 * apmenu: pair of identical autoconnecting peer menus
1371 * listmodel: Gtk ListStore for connected peers; contains peer name,
1372 address, and ping times (transport and encrypted, value and colour)
1373 * status: Gtk Statusbar at the bottom of the window
1374 * _kidding: an unpleasant backchannel between the apchange method (which
1375 builds the apmenus) and the menu handler, forced on us by a Gtk
1378 Also installs attributes on Peer objects:
1380 * i: index of peer's entry in listmodel
1381 * win: WindowSlot object for the peer's PeerWindow
1385 """Construct the window."""
1388 MyWindow.__init__(me)
1389 me.set_title('TrIPE monitor')
1391 ## Hook onto diagnostic outputs.
1392 me.warnings = WarningLogModel()
1393 me.hook(conn.warnhook, me.warnings.notify)
1394 me.trace = TraceLogModel()
1395 me.hook(conn.tracehook, me.trace.notify)
1397 ## Make slots to store the various ancillary singleton windows.
1398 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1399 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1400 me.traceopts = WindowSlot(lambda: TraceOptions())
1401 me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1402 me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1403 me.servinfo = WindowSlot(lambda: ServInfo())
1405 ## Main window structure.
1409 ## UI manager makes our menus. (We're too cheap to have a toolbar.)
1410 me.ui = G.UIManager()
1411 actgroup = makeactiongroup('monitor',
1412 [('file-menu', '_File', None, None),
1413 ('connect', '_Connect', '<Control>C', conn.connect),
1414 ('disconnect', '_Disconnect', '<Control>D',
1415 lambda: conn.disconnect(None)),
1416 ('quit', '_Quit', '<Control>Q', me.close),
1417 ('server-menu', '_Server', None, None),
1418 ('daemon', 'Run in _background', None, cr(conn.daemon)),
1419 ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1420 ('crypto-algs', 'Cryptographic algorithms',
1421 '<Control>Y', me.cryptoinfo.open),
1422 ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1423 ('server-quit', 'Terminate server', None, cr(conn.quit)),
1424 ('conn-peer', 'Connect peer', None, None),
1425 ('logs-menu', '_Logs', None, None),
1426 ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1427 ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1428 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1429 ('help-menu', '_Help', None, None),
1430 ('about', '_About tripemon...', None, aboutbox.open),
1431 ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1432 ('kill-peer', '_Kill peer', None, me.killpeer),
1433 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1439 <menu action="file-menu">
1440 <menuitem action="quit"/>
1442 <menu action="server-menu">
1443 <menuitem action="connect"/>
1444 <menuitem action="disconnect"/>
1446 <menuitem action="server-version"/>
1447 <menuitem action="crypto-algs"/>
1448 <menuitem action="add-peer"/>
1449 <menuitem action="conn-peer"/>
1450 <menuitem action="daemon"/>
1451 <menuitem action="reload-keys"/>
1453 <menuitem action="server-quit"/>
1455 <menu action="logs-menu">
1456 <menuitem action="show-warnings"/>
1457 <menuitem action="show-trace"/>
1458 <menuitem action="trace-options"/>
1460 <menu action="help-menu">
1461 <menuitem action="about"/>
1464 <popup name="peer-popup">
1465 <menuitem action="add-peer"/>
1466 <menuitem action="conn-peer"/>
1467 <menuitem action="kill-peer"/>
1468 <menuitem action="force-kx"/>
1473 ## Populate the UI manager.
1474 me.ui.insert_action_group(actgroup, 0)
1475 me.ui.add_ui_from_string(uidef)
1477 ## Construct the menu bar.
1478 vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
1479 me.add_accel_group(me.ui.get_accel_group())
1481 ## Construct and attach the auto-peers menu. (This is a horrible bodge
1482 ## because we can't attach the same submenu in two different places.)
1483 me.apmenu = G.Menu(), G.Menu()
1484 me.ui.get_widget('/menubar/server-menu/conn-peer') \
1485 .set_submenu(me.apmenu[0])
1486 me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1488 ## Construct the main list model, and listen on hooks which report
1489 ## changes to the available peers.
1490 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1491 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1492 me.hook(monitor.peers.addhook, me.addpeer)
1493 me.hook(monitor.peers.delhook, me.delpeer)
1494 me.hook(monitor.autopeershook, me.apchange)
1496 ## Construct the list viewer and put it in a scrolling window.
1497 scr = MyScrolledWindow()
1498 me.list = MyTreeView(me.listmodel)
1499 me.list.append_column(G.TreeViewColumn('Peer name',
1500 G.CellRendererText(),
1502 me.list.append_column(G.TreeViewColumn('Address',
1503 G.CellRendererText(),
1505 me.list.append_column(G.TreeViewColumn('T-ping',
1506 G.CellRendererText(),
1509 me.list.append_column(G.TreeViewColumn('E-ping',
1510 G.CellRendererText(),
1513 me.list.get_column(1).set_expand(True)
1514 me.list.connect('row-activated', me.activate)
1515 me.list.connect('button-press-event', me.buttonpress)
1516 me.list.set_reorderable(True)
1517 me.list.get_selection().set_mode(G.SELECTION_NONE)
1519 vbox.pack_start(scr)
1521 ## Construct the status bar, and listen on hooks which report changes to
1522 ## connection status.
1523 me.status = G.Statusbar()
1524 vbox.pack_start(me.status, expand = False)
1525 me.hook(conn.connecthook, cr(me.connected))
1526 me.hook(conn.disconnecthook, me.disconnected)
1527 me.hook(conn.notehook, me.notify)
1529 ## Set a plausible default window size.
1530 me.set_default_size(512, 180)
1532 def addpeer(me, peer):
1533 """Hook: announces that PEER has been added."""
1534 peer.i = me.listmodel.append([peer.name, peer.addr,
1535 '???', 'green', '???', 'green'])
1536 peer.win = WindowSlot(lambda: PeerWindow(peer))
1537 me.hook(peer.pinghook, me._ping)
1540 def delpeer(me, peer):
1541 """Hook: announces that PEER has been removed."""
1542 me.listmodel.remove(peer.i)
1543 me.unhook(peer.pinghook)
1546 def path_peer(me, path):
1547 """Return the peer corresponding to a given list-model PATH."""
1548 return monitor.peers[me.listmodel[path][0]]
1552 Hook: announces that a change has been made to the peers available for
1553 automated connection.
1555 This populates both auto-peer menus and keeps them in sync. (As
1556 mentioned above, we can't attach the same submenu to two separate parent
1557 menu items. So we end up with two identical menus instead. Yes, this
1561 ## The set_active method of a CheckMenuItem works by maybe activating the
1562 ## menu item. This signals our handler. But we don't actually want to
1563 ## signal the handler unless the user actually frobbed the item. So the
1564 ## _kidding flag is used as an underhanded way of telling the handler
1565 ## that we don't actually want it to do anything. Of course, this sucks
1569 ## Iterate over the two menus.
1572 existing = menu.get_children()
1573 if monitor.autopeers is None:
1575 ## No peers, so empty out the menu.
1576 for item in existing:
1581 ## Insert the new items into the menu. (XXX this seems buggy XXX)
1582 ## Tick the peers which are actually connected.
1584 for peer in monitor.autopeers:
1585 if j < len(existing) and \
1586 existing[j].get_child().get_text() == peer:
1590 item = G.CheckMenuItem(peer, use_underline = False)
1591 item.connect('activate', invoker(me._addautopeer, peer))
1592 menu.insert(item, i)
1593 item.set_active(peer in monitor.peers.table)
1596 ## Make all the menu items visible.
1599 ## Set the parent menu items sensitive if and only if there are any peers
1601 for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1602 me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1604 ## And now allow the handler to do its business normally.
1607 def _addautopeer(me, peer):
1609 Automatically connect an auto-peer.
1611 This method is invoked from the main coroutine. Since the actual
1612 connection needs to issue administration commands, we must spawn a new
1613 child coroutine for it.
1617 T.Coroutine(me._addautopeer_hack).switch(peer)
1619 def _addautopeer_hack(me, peer):
1620 """Make an automated connection to PEER in response to a user click."""
1624 T._simple(conn.svcsubmit('connect', 'active', peer))
1625 except T.TripeError, exc:
1626 T.defer(moanbox, ' '.join(exc.args))
1629 def activate(me, l, path, col):
1631 Handle a double-click on a peer in the main list: open a PeerInfo window.
1633 peer = me.path_peer(path)
1636 def buttonpress(me, l, ev):
1638 Handle a mouse click on the main list.
1640 Currently we're only interested in button-3, which pops up the peer menu.
1641 For future reference, we stash the peer that was clicked in me.menupeer.
1644 x, y = int(ev.x), int(ev.y)
1645 r = me.list.get_path_at_pos(x, y)
1646 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1647 me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1649 me.ui.get_widget('/peer-popup/conn-peer'). \
1650 set_sensitive(bool(monitor.autopeers))
1652 me.menupeer = me.path_peer(r[0])
1655 me.ui.get_widget('/peer-popup').popup(
1656 None, None, None, ev.button, ev.time)
1659 """Kill a peer from the popup menu."""
1660 cr(conn.kill, me.menupeer.name)()
1663 """Kickstart a key-exchange from the popup menu."""
1664 cr(conn.forcekx, me.menupeer.name)()
1666 _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1667 def _ping(me, p, cmd, ps):
1668 """Hook: responds to ping reports."""
1669 textcol, colourcol = me._columnmap[cmd]
1671 me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1672 me.listmodel[p.i][colourcol] = 'red'
1674 me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1675 me.listmodel[p.i][colourcol] = 'black'
1677 def setstatus(me, status):
1678 """Update the message in the status bar."""
1680 me.status.push(0, status)
1682 def notify(me, note, *rest):
1683 """Hook: invoked when interesting notifications occur."""
1684 if note == 'DAEMON':
1685 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1689 Hook: invoked when a connection is made to the server.
1691 Make options which require a server connection sensitive.
1693 me.setstatus('Connected (port %s)' % conn.port())
1694 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1695 for i in ('/menubar/server-menu/disconnect',
1696 '/menubar/server-menu/server-version',
1697 '/menubar/server-menu/add-peer',
1698 '/menubar/server-menu/server-quit',
1699 '/menubar/logs-menu/trace-options'):
1700 me.ui.get_widget(i).set_sensitive(True)
1701 me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1702 set_sensitive(bool(monitor.autopeers))
1703 me.ui.get_widget('/menubar/server-menu/daemon'). \
1704 set_sensitive(conn.servinfo()['daemon'] == 'nil')
1706 def disconnected(me, reason):
1708 Hook: invoked when the connection to the server is lost.
1710 Make most options insensitive.
1712 me.setstatus('Disconnected')
1713 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1714 for i in ('/menubar/server-menu/disconnect',
1715 '/menubar/server-menu/server-version',
1716 '/menubar/server-menu/add-peer',
1717 '/menubar/server-menu/conn-peer',
1718 '/menubar/server-menu/daemon',
1719 '/menubar/server-menu/server-quit',
1720 '/menubar/logs-menu/trace-options'):
1721 me.ui.get_widget(i).set_sensitive(False)
1722 if reason: moanbox(reason)
1724 ###--------------------------------------------------------------------------
1727 def parse_options():
1729 Parse command-line options.
1731 Process the boring ones. Return all of them, for later.
1733 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1734 version = '%prog (tripe version 1.0.0)')
1735 op.add_option('-a', '--admin-socket',
1736 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1737 help = 'Select socket to connect to [default %default]')
1738 op.add_option('-d', '--directory',
1739 metavar = 'DIR', dest = 'dir', default = T.configdir,
1740 help = 'Select current diretory [default %default]')
1741 opts, args = op.parse_args()
1742 if args: op.error('no arguments permitted')
1747 """Initialization."""
1749 global conn, monitor, pinger
1751 ## Try to establish a connection.
1752 conn = Connection(opts.tripesock)
1754 ## Make the main interesting coroutines and objects.
1762 root = MonitorWindow()
1767 HookClient().hook(root.closehook, exit)
1770 if __name__ == '__main__':
1771 opts = parse_options()
1775 ###----- That's all, folks --------------------------------------------------