X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/tripe/blobdiff_plain/984b6d3310be68c87d1f278102bc4a5ef61645ff..11ad66c29764521f87f0dd399a1e592147c7af36:/mon/tripemon.in diff --git a/mon/tripemon.in b/mon/tripemon.in index e666ab9f..a0c0f5e4 100644 --- a/mon/tripemon.in +++ b/mon/tripemon.in @@ -10,19 +10,18 @@ ### ### This file is part of Trivial IP Encryption (TrIPE). ### -### TrIPE is free software; you can redistribute it and/or modify -### it under the terms of the GNU General Public License as published by -### the Free Software Foundation; either version 2 of the License, or -### (at your option) any later version. +### TrIPE is free software: you can redistribute it and/or modify it under +### the terms of the GNU General Public License as published by the Free +### Software Foundation; either version 3 of the License, or (at your +### option) any later version. ### -### TrIPE is distributed in the hope that it will be useful, -### but WITHOUT ANY WARRANTY; without even the implied warranty of -### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -### GNU General Public License for more details. +### TrIPE is distributed in the hope that it will be useful, but WITHOUT +### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +### FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +### for more details. ### ### You should have received a copy of the GNU General Public License -### along with TrIPE; if not, write to the Free Software Foundation, -### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +### along with TrIPE. If not, see . ###-------------------------------------------------------------------------- ### Dependencies. @@ -40,11 +39,34 @@ import time as TIME import re as RX from cStringIO import StringIO -import pygtk -pygtk.require('2.0') -import gtk as G -import gobject as GO -import gtk.gdk as GDK +try: + if OS.getenv('TRIPEMON_FORCE_GI'): raise ImportError + import pygtk + pygtk.require('2.0') + import gtk as G + import gobject as GO + import gtk.gdk as GDK + GL = GO + GDK.KEY_Escape = G.keysyms.Escape + def raise_window(w): w.window.raise_() + combo_box_text = G.combo_box_new_text + def set_entry_bg(e, c): e.modify_base(G.STATE_NORMAL, c) +except ImportError: + from gi.repository import GObject as GO, GLib as GL, Gtk as G, Gdk as GDK + G.WINDOW_TOPLEVEL = G.WindowType.TOPLEVEL + G.EXPAND = G.AttachOptions.EXPAND + G.SHRINK = G.AttachOptions.SHRINK + G.FILL = G.AttachOptions.FILL + G.SORT_ASCENDING = G.SortType.ASCENDING + G.POLICY_AUTOMATIC = G.PolicyType.AUTOMATIC + G.SHADOW_IN = G.ShadowType.IN + G.SELECTION_NONE = G.SelectionMode.NONE + G.DIALOG_MODAL = G.DialogFlags.MODAL + G.RESPONSE_CANCEL = G.ResponseType.CANCEL + G.RESPONSE_NONE = G.ResponseType.NONE + def raise_window(w): getattr(w.get_window(), 'raise')() + combo_box_text = G.ComboBoxText + def set_entry_bg(e, c): e.modify_bg(G.StateType.NORMAL, c) if OS.getenv('TRIPE_DEBUG_MONITOR') is not None: T._debug = 1 @@ -56,46 +78,6 @@ def uncaught(): """Report an uncaught exception.""" excepthook(*exc_info()) -_idles = [] -def _runidles(): - """Invoke the functions on the idles queue.""" - global _idles - while _idles: - old = _idles - _idles = [] - for func, args, kw in old: - try: - func(*args, **kw) - except: - uncaught() - return False - -def idly(func, *args, **kw): - """Invoke FUNC(*ARGS, **KW) at some later point in time.""" - if not _idles: - GO.idle_add(_runidles) - _idles.append((func, args, kw)) - -_asides = T.Queue() -def _runasides(): - """ - Coroutine function: reads (FUNC, ARGS, KW) triples from a queue and invokes - FUNC(*ARGS, **KW) - """ - while True: - func, args, kw = _asides.get() - try: - func(*args, **kw) - except: - uncaught() - -def aside(func, *args, **kw): - """ - Arrange for FUNC(*ARGS, **KW) to be performed at some point in the future, - and not from the main coroutine. - """ - idly(_asides.put, (func, args, kw)) - def xwrap(func): """ Return a function which behaves like FUNC, but reports exceptions via @@ -123,13 +105,15 @@ def invoker(func, *args, **kw): def cr(func, *args, **kw): """Return a function which invokes FUNC(*ARGS, **KW) in a coroutine.""" - def _(*hunoz, **hukairz): - T.Coroutine(xwrap(func)).switch(*args, **kw) - return _ + name = T.funargstr(func, args, kw) + return lambda *hunoz, **hukairz: \ + T.Coroutine(xwrap(func), name = name).switch(*args, **kw) def incr(func): """Decorator: runs its function in a coroutine of its own.""" - return lambda *args, **kw: T.Coroutine(func).switch(*args, **kw) + return lambda *args, **kw: \ + (T.Coroutine(func, name = T.funargstr(func, args, kw)) + .switch(*args, **kw)) ###-------------------------------------------------------------------------- ### Random bits of infrastructure. @@ -170,12 +154,6 @@ class HookList (object): if rc is not None: return rc return None - def runidly(me, *args, **kw): - """ - Invoke the hook functions as for run, but at some point in the future. - """ - idly(me.run, *args, **kw) - class HookClient (object): """ Mixin for classes which are clients of hooks. @@ -215,6 +193,23 @@ rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$') ###-------------------------------------------------------------------------- ### Connections. +class GIOWatcher (object): + """ + Monitor I/O events using glib. + """ + def __init__(me, conn, mc = GL.main_context_default()): + me._conn = conn + me._watch = None + me._mc = mc + def connected(me, sock): + me._watch = GL.io_add_watch(sock, GL.IO_IN, + lambda *hunoz: me._conn.receive()) + def disconnected(me): + GL.source_remove(me._watch) + me._watch = None + def iterate(me): + me._mc.iteration(True) + class Connection (T.TripeCommandDispatcher): """ The main connection to the server. @@ -245,18 +240,15 @@ class Connection (T.TripeCommandDispatcher): me.handler['NOTE'] = lambda _, *rest: me.notehook.run(*rest) me.handler['WARN'] = lambda _, *rest: me.warnhook.run(*rest) me.handler['TRACE'] = lambda _, *rest: me.tracehook.run(*rest) - me._watch = None + me.iowatch = GIOWatcher(me) def connected(me): """Handles reconnection to the server, and signals the hook.""" T.TripeCommandDispatcher.connected(me) - me._watch = GO.io_add_watch(me.sock, GO.IO_IN, invoker(me.receive)) me.connecthook.run() def disconnected(me, reason): """Handles disconnection from the server, and signals the hook.""" - GO.source_remove(me._watch) - me._watch = None me.disconnecthook.run(reason) T.TripeCommandDispatcher.disconnected(me, reason) @@ -319,11 +311,18 @@ class Peer (MonitorObject): """Initialize the object with the given name.""" MonitorObject.__init__(me, name) me.pinghook = HookList() + me.__dict__.update(conn.algs(name)) me.update() def update(me, hunoz = None): """Update the peer, fetching information about it from the server.""" - addr = conn.addr(me.name) + me._setaddr(conn.addr(me.name)) + me.ifname = conn.ifname(me.name) + me.__dict__.update(conn.peerinfo(me.name)) + me.changehook.run() + + def _setaddr(me, addr): + """Set the peer's address.""" if addr[0] == 'INET': ipaddr, port = addr[1:] try: @@ -333,8 +332,10 @@ class Peer (MonitorObject): me.addr = 'INET %s:%s' % (ipaddr, port) else: me.addr = ' '.join(addr) - me.ifname = conn.ifname(me.name) - me.__dict__.update(conn.peerinfo(me.name)) + + def setaddr(me, addr): + """Informs the object of a change to its address to ADDR.""" + me._setaddr(addr) me.changehook.run() def setifname(me, newname): @@ -482,7 +483,7 @@ class Monitor (HookClient): """Update the auto-peers list from the connect service.""" if 'connect' in me.services.table: me.autopeers = [' '.join(line) - for line in conn.svcsubmit('connect', 'list')] + for line in conn.svcsubmit('connect', 'list-active')] me.autopeers.sort() else: me.autopeers = None @@ -498,27 +499,32 @@ class Monitor (HookClient): the auto-peers list. """ if code == 'ADD': - aside(me.peers.add, rest[0], None) + T.aside(me.peers.add, rest[0], None) elif code == 'KILL': - aside(me.peers.remove, rest[0]) + T.aside(me.peers.remove, rest[0]) elif code == 'NEWIFNAME': try: me.peers[rest[0]].setifname(rest[2]) except KeyError: pass + elif code == 'NEWADDR': + try: + me.peers[rest[0]].setaddr(rest[1:]) + except KeyError: + pass elif code == 'SVCCLAIM': - aside(me.services.add, rest[0], rest[1]) + T.aside(me.services.add, rest[0], rest[1]) if rest[0] == 'connect': - aside(me._updateautopeers) + T.aside(me._updateautopeers) elif code == 'SVCRELEASE': - aside(me.services.remove, rest[0]) + T.aside(me.services.remove, rest[0]) if rest[0] == 'connect': - aside(me._updateautopeers) + T.aside(me._updateautopeers) elif code == 'USER': if not rest: return if rest[0] == 'watch' and \ rest[1] == 'peerdb-update': - aside(me._updateautopeers) + T.aside(me._updateautopeers) ###-------------------------------------------------------------------------- ### Window management cruft. @@ -551,6 +557,17 @@ class MyWindow (MyWindowMixin): G.Window.__init__(me, kind) me.mywininit() +class TrivialWindowMixin (MyWindowMixin): + """A simple window which you can close with Escape.""" + def mywininit(me): + super(TrivialWindowMixin, me).mywininit() + me.connect('key-press-event', me._keypress) + def _keypress(me, _, ev): + if ev.keyval == GDK.KEY_Escape: me.destroy() + +class TrivialWindow (MyWindow, TrivialWindowMixin): + pass + class MyDialog (G.Dialog, MyWindowMixin, HookClient): """A dialogue box with a closehook and sensible button binding.""" @@ -681,7 +698,7 @@ class WindowSlot (HookClient): def open(me): """Opens the window, creating it if necessary.""" if me.window: - me.window.window.raise_() + raise_window(me.window) else: me.window = me.createfunc() me.hook(me.window.closehook, me.closed) @@ -706,7 +723,7 @@ class MyScrolledWindow (G.ScrolledWindow): rx_num = RX.compile(r'^[-+]?\d+$') ## The colour red. -c_red = GDK.color_parse('red') +c_red = GDK.color_parse('#ff6666') class ValidationError (Exception): """Raised by ValidatingEntry.get_text() if the text isn't valid.""" @@ -729,27 +746,26 @@ class ValidatingEntry (G.Entry): characters (ish). Other arguments are passed to Entry. """ G.Entry.__init__(me, *arg, **kw) - me.connect("changed", me.check) + me.connect("changed", me._check) + me.connect("state-changed", me._check) if callable(valid): me.validate = valid else: me.validate = RX.compile(valid).match me.ensure_style() - me.c_ok = me.get_style().text[G.STATE_NORMAL] - me.c_bad = c_red if size != -1: me.set_width_chars(size) me.set_activates_default(True) me.set_text(text) - me.check() + me._check() - def check(me, *hunoz): + def _check(me, *hunoz): """Check the current text and update validp and the text colour.""" if me.validate(G.Entry.get_text(me)): me.validp = True - me.modify_text(G.STATE_NORMAL, me.c_ok) + set_entry_bg(me, None) else: me.validp = False - me.modify_text(G.STATE_NORMAL, me.c_bad) + set_entry_bg(me, me.is_sensitive() and c_red or None) def get_text(me): """ @@ -774,21 +790,21 @@ def numericvalidate(min = None, max = None): ###-------------------------------------------------------------------------- ### Various minor dialog boxen. -GPL = """This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. +GPL = """\ +TrIPE is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation; either version 3 of the License, or (at your +option) any later version. -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. +TrIPE is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software Foundation, -Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.""" +along with TrIPE. If not, see .""" -class AboutBox (G.AboutDialog, MyWindowMixin): +class AboutBox (G.AboutDialog, TrivialWindowMixin): """The program `About' box.""" def __init__(me): G.AboutDialog.__init__(me) @@ -813,7 +829,7 @@ def moanbox(msg): buttons = ((G.STOCK_OK, G.RESPONSE_NONE))) label = G.Label(msg) label.set_padding(20, 20) - d.vbox.pack_start(label) + d.vbox.pack_start(label, True, True, 0) label.show() d.run() d.destroy() @@ -869,7 +885,7 @@ class WarningLogModel (LogModel): """Call with a new warning message.""" me.add(tag, ' '.join([T.quotify(w) for w in rest])) -class LogViewer (MyWindow): +class LogViewer (TrivialWindow): """ A log viewer window. @@ -885,7 +901,7 @@ class LogViewer (MyWindow): """ Create a log viewer showing the LogModel MODEL. """ - MyWindow.__init__(me) + TrivialWindow.__init__(me) me.model = model scr = MyScrolledWindow() me.list = MyTreeView(me.model) @@ -949,14 +965,14 @@ class Pinger (T.Coroutine, HookClient): me.hook(conn.connecthook, me._connected) me.hook(conn.disconnecthook, me._disconnected) me.hook(monitor.peers.addhook, - lambda p: idly(me._q.put, (p, 'ADD', None))) + lambda p: T.defer(me._q.put, (p, 'ADD', None))) me.hook(monitor.peers.delhook, - lambda p: idly(me._q.put, (p, 'KILL', None))) + lambda p: T.defer(me._q.put, (p, 'KILL', None))) if conn.connectedp(): me.connected() def _connected(me): """Respond to connection: start pinging thngs.""" - me._timer = GO.timeout_add(1000, me._timerfunc) + me._timer = GL.timeout_add(1000, me._timerfunc) def _timerfunc(me): """Timer function: put a timer event on the queue.""" @@ -965,7 +981,7 @@ class Pinger (T.Coroutine, HookClient): def _disconnected(me, reason): """Respond to disconnection: stop pinging.""" - GO.source_remove(me._timer) + GL.source_remove(me._timer) def run(me): """ @@ -1037,7 +1053,7 @@ class AddPeerDialog (MyDialog): def _setup(me): """Coroutine function: background setup for AddPeerDialog.""" table = GridPacker() - me.vbox.pack_start(table) + me.vbox.pack_start(table, True, True, 0) me.e_name = table.labelled('Name', ValidatingEntry(r'^[^\s.:]+$', '', 16), width = 3) @@ -1048,54 +1064,71 @@ class AddPeerDialog (MyDialog): ValidatingEntry(numericvalidate(0, 65535), '4070', 5)) - me.c_keepalive = G.CheckButton('Keepalives') - me.l_tunnel = table.labelled('Tunnel', - G.combo_box_new_text(), + me.l_tunnel = table.labelled('Tunnel', combo_box_text(), newlinep = True, width = 3) me.tuns = conn.tunnels() for t in me.tuns: me.l_tunnel.append_text(t) me.l_tunnel.set_active(0) + + def tickybox_sensitivity(tickybox, target): + tickybox.connect('toggled', + lambda t: target.set_sensitive (t.get_active())) + + me.c_keepalive = G.CheckButton('Keepalives') table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL) - me.c_keepalive.connect('toggled', - lambda t: me.e_keepalive.set_sensitive\ - (t.get_active())) me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5) me.e_keepalive.set_sensitive(False) + tickybox_sensitivity(me.c_keepalive, me.e_keepalive) table.pack(me.e_keepalive, width = 3) + + me.c_mobile = G.CheckButton('Mobile') + table.pack(me.c_mobile, newlinep = True, width = 4, xopt = G.FILL) + + me.c_peerkey = G.CheckButton('Peer key tag') + table.pack(me.c_peerkey, newlinep = True, xopt = G.FILL) + me.e_peerkey = ValidatingEntry(r'^[^.:\s]+$', '', 16) + me.e_peerkey.set_sensitive(False) + tickybox_sensitivity(me.c_peerkey, me.e_peerkey) + table.pack(me.e_peerkey, width = 3) + + me.c_privkey = G.CheckButton('Private key tag') + table.pack(me.c_privkey, newlinep = True, xopt = G.FILL) + me.e_privkey = ValidatingEntry(r'^[^.:\s]+$', '', 16) + me.e_privkey.set_sensitive(False) + tickybox_sensitivity(me.c_privkey, me.e_privkey) + table.pack(me.e_privkey, width = 3) + me.show_all() def ok(me): """Handle an OK press: create the peer.""" try: - if me.c_keepalive.get_active(): - ka = me.e_keepalive.get_text() - else: - ka = None t = me.l_tunnel.get_active() - if t == 0: - tun = None - else: - tun = me.tuns[t] - me._addpeer(me.e_name.get_text(), - me.e_addr.get_text(), - me.e_port.get_text(), - ka, - tun) + me._addpeer(me.e_name.get_text(), + me.e_addr.get_text(), + me.e_port.get_text(), + keepalive = (me.c_keepalive.get_active() and + me.e_keepalive.get_text() or None), + tunnel = t and me.tuns[t] or None, + key = (me.c_peerkey.get_active() and + me.e_peerkey.get_text() or None), + priv = (me.c_privkey.get_active() and + me.e_privkey.get_text() or None)) except ValidationError: GDK.beep() return @incr - def _addpeer(me, name, addr, port, keepalive, tunnel): + def _addpeer(me, *args, **kw): """Coroutine function: actually do the ADD command.""" try: - conn.add(name, addr, port, keepalive = keepalive, tunnel = tunnel) + conn.add(*args, **kw) me.destroy() except T.TripeError, exc: - idly(moanbox, ' '.join(exc)) + T.defer(moanbox, ' '.join(exc)) -class ServInfo (MyWindow): +class ServInfo (TrivialWindow): """ Show information about the server and available services. @@ -1106,7 +1139,7 @@ class ServInfo (MyWindow): """ def __init__(me): - MyWindow.__init__(me) + TrivialWindow.__init__(me) me.set_title('TrIPE server info') table = GridPacker() me.add(table) @@ -1165,7 +1198,7 @@ class TraceOptions (MyDialog): text = desc[0].upper() + desc[1:] ticky = G.CheckButton(text) ticky.set_active(st == '+') - me.vbox.pack_start(ticky) + me.vbox.pack_start(ticky, True, True, 0) me.opts.append((ch, ticky)) me.show_all() def ok(me): @@ -1223,6 +1256,15 @@ statsxlate = \ ## the entry. statslayout = \ [('Start time', '%(start-time)s'), + ('Private key', '%(current-key)s'), + ('Diffie-Hellman group', + '%(kx-group)s ' + '(%(kx-group-order-bits)s-bit order, ' + '%(kx-group-elt-bits)s-bit elements)'), + ('Cipher', + '%(cipher)s (%(cipher-keysz)s-bit key, %(cipher-blksz)s-bit block)'), + ('Mac', '%(mac)s (%(mac-keysz)s-bit key, %(mac-tagsz)s-bit tag)'), + ('Hash', '%(hash)s (%(hash-sz)s-bit output)'), ('Last key-exchange', '%(last-keyexch-time)s'), ('Last packet', '%(last-packet-time)s'), ('Packets in/out', @@ -1233,7 +1275,7 @@ statslayout = \ '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'), ('Rejected packets', '%(rejected-packets)s')] -class PeerWindow (MyWindow): +class PeerWindow (TrivialWindow): """ Show information about a peer. @@ -1251,7 +1293,7 @@ class PeerWindow (MyWindow): def __init__(me, peer): """Construct a PeerWindow, showing information about PEER.""" - MyWindow.__init__(me) + TrivialWindow.__init__(me) me.set_title('TrIPE statistics: %s' % peer.name) me.peer = peer @@ -1310,16 +1352,18 @@ class PeerWindow (MyWindow): stat = conn.stats(me.peer.name) for s, trans in statsxlate: stat[s] = trans(stat[s]) + stat.update(me.peer.__dict__) for label, format in statslayout: me.e[label].set_text(format % stat) - GO.timeout_add(1000, lambda: me.cr.switch() and False) + GL.timeout_add(1000, lambda: me.cr.switch() and False) me.cr.parent.switch() me.cr = None def tryupdate(me): """Start the updater coroutine, if it's not going already.""" if me.cr is None: - me.cr = T.Coroutine(me._update) + me.cr = T.Coroutine(me._update, + name = 'update-peer-window %s' % me.peer.name) me.cr.switch() def stopupdate(me, *hunoz, **hukairz): @@ -1344,12 +1388,12 @@ class PeerWindow (MyWindow): ###-------------------------------------------------------------------------- ### Cryptographic status. -class CryptoInfo (MyWindow): +class CryptoInfo (TrivialWindow): """Simple display of cryptographic algorithms in use.""" def __init__(me): - MyWindow.__init__(me) + TrivialWindow.__init__(me) me.set_title('Cryptographic algorithms') - aside(me.populate) + T.aside(me.populate) def populate(me): table = GridPacker() me.add(table) @@ -1507,7 +1551,7 @@ class MonitorWindow (MyWindow): me.ui.add_ui_from_string(uidef) ## Construct the menu bar. - vbox.pack_start(me.ui.get_widget('/menubar'), expand = False) + vbox.pack_start(me.ui.get_widget('/menubar'), False, True, 0) me.add_accel_group(me.ui.get_accel_group()) ## Construct and attach the auto-peers menu. (This is a horrible bodge @@ -1548,12 +1592,12 @@ class MonitorWindow (MyWindow): me.list.set_reorderable(True) me.list.get_selection().set_mode(G.SELECTION_NONE) scr.add(me.list) - vbox.pack_start(scr) + vbox.pack_start(scr, True, True, 0) ## Construct the status bar, and listen on hooks which report changes to ## connection status. me.status = G.Statusbar() - vbox.pack_start(me.status, expand = False) + vbox.pack_start(me.status, False, True, 0) me.hook(conn.connecthook, cr(me.connected)) me.hook(conn.disconnecthook, me.disconnected) me.hook(conn.notehook, me.notify) @@ -1611,7 +1655,7 @@ class MonitorWindow (MyWindow): else: ## Insert the new items into the menu. (XXX this seems buggy XXX) - ## Tick the peers which are actually connected. + ## Tick the peers which are actually connected. i = j = 0 for peer in monitor.autopeers: if j < len(existing) and \ @@ -1646,7 +1690,8 @@ class MonitorWindow (MyWindow): """ if me._kidding: return - T.Coroutine(me._addautopeer_hack).switch(peer) + T.Coroutine(me._addautopeer_hack, + name = '_addautopeerhack %s' % peer).switch(peer) def _addautopeer_hack(me, peer): """Make an automated connection to PEER in response to a user click.""" @@ -1655,7 +1700,7 @@ class MonitorWindow (MyWindow): try: T._simple(conn.svcsubmit('connect', 'active', peer)) except T.TripeError, exc: - idly(moanbox, ' '.join(exc.args)) + T.defer(moanbox, ' '.join(exc.args)) me.apchange() def activate(me, l, path, col): @@ -1780,9 +1825,6 @@ def init(opts): global conn, monitor, pinger - ## Run jobs put off for later. - T.Coroutine(_runasides).switch() - ## Try to establish a connection. conn = Connection(opts.tripesock) @@ -1800,7 +1842,7 @@ def main(): ## Main loop. HookClient().hook(root.closehook, exit) - G.main() + conn.mainloop() if __name__ == '__main__': opts = parse_options()