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
44 if OS.getenv('TRIPEMON_FORCE_GI'): raise ImportError
51 def raise_window(w): w.window.raise_()
52 combo_box_text = G.combo_box_new_text
53 def set_entry_bg(e, c): e.modify_base(G.STATE_NORMAL, c)
55 from gi.repository import GObject as GO, GLib as GL, Gtk as G, Gdk as GDK
56 G.WINDOW_TOPLEVEL = G.WindowType.TOPLEVEL
57 G.EXPAND = G.AttachOptions.EXPAND
58 G.SHRINK = G.AttachOptions.SHRINK
59 G.FILL = G.AttachOptions.FILL
60 G.SORT_ASCENDING = G.SortType.ASCENDING
61 G.POLICY_AUTOMATIC = G.PolicyType.AUTOMATIC
62 G.SHADOW_IN = G.ShadowType.IN
63 G.SELECTION_NONE = G.SelectionMode.NONE
64 G.DIALOG_MODAL = G.DialogFlags.MODAL
65 G.RESPONSE_CANCEL = G.ResponseType.CANCEL
66 G.RESPONSE_NONE = G.ResponseType.NONE
67 def raise_window(w): getattr(w.get_window(), 'raise')()
68 combo_box_text = G.ComboBoxText
69 def set_entry_bg(e, c): e.modify_bg(G.StateType.NORMAL, c)
71 if OS.getenv('TRIPE_DEBUG_MONITOR') is not None:
74 ###--------------------------------------------------------------------------
75 ### Doing things later.
78 """Report an uncaught exception."""
79 excepthook(*exc_info())
83 Return a function which behaves like FUNC, but reports exceptions via
88 return func(*args, **kw)
96 def invoker(func, *args, **kw):
98 Return a function which throws away its arguments and calls
101 If for loops worked by binding rather than assignment then we wouldn't need
104 return lambda *hunoz, **hukairz: xwrap(func)(*args, **kw)
106 def cr(func, *args, **kw):
107 """Return a function which invokes FUNC(*ARGS, **KW) in a coroutine."""
108 name = T.funargstr(func, args, kw)
109 return lambda *hunoz, **hukairz: \
110 T.Coroutine(xwrap(func), name = name).switch(*args, **kw)
113 """Decorator: runs its function in a coroutine of its own."""
114 return lambda *args, **kw: \
115 (T.Coroutine(func, name = T.funargstr(func, args, kw))
116 .switch(*args, **kw))
118 ###--------------------------------------------------------------------------
119 ### Random bits of infrastructure.
121 ## Program name, shorn of extraneous stuff.
126 class HookList (object):
128 Notification hook list.
130 Other objects can add functions onto the hook list. When the hook list is
131 run, the functions are called in the order in which they were registered.
135 """Basic initialization: create the hook list."""
138 def add(me, func, obj):
139 """Add FUNC to the list of hook functions."""
140 me.list.append((obj, func))
143 """Remove hook functions registered with the given OBJ."""
150 def run(me, *args, **kw):
151 """Invoke the hook functions with arguments *ARGS and **KW."""
152 for o, hook in me.list:
153 rc = hook(*args, **kw)
154 if rc is not None: return rc
157 class HookClient (object):
159 Mixin for classes which are clients of hooks.
161 It keeps track of the hooks it's a client of, and has the ability to
162 extricate itself from all of them. This is useful because weak objects
163 don't seem to work well.
166 """Basic initialization."""
169 def hook(me, hk, func):
170 """Add FUNC to the hook list HK."""
175 """Remove myself from the hook list HK."""
180 """Remove myself from all hook lists."""
185 class struct (object):
186 """A very simple dumb data container object."""
187 def __init__(me, **kw):
188 me.__dict__.update(kw)
190 ## Matches ISO date format yyyy-mm-ddThh:mm:ss.
191 rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
193 ###--------------------------------------------------------------------------
196 class GIOWatcher (object):
198 Monitor I/O events using glib.
200 def __init__(me, conn, mc = GL.main_context_default()):
204 def connected(me, sock):
205 me._watch = GL.io_add_watch(sock, GL.IO_IN,
206 lambda *hunoz: me._conn.receive())
207 def disconnected(me):
208 GL.source_remove(me._watch)
211 me._mc.iteration(True)
213 class Connection (T.TripeCommandDispatcher):
215 The main connection to the server.
217 The improvement over the TripeCommandDispatcher is that the Connection
218 provides hooklists for NOTE, WARN and TRACE messages, and for connect and
221 This class knows about the Glib I/O dispatcher system, and plugs into it.
225 * connecthook(): a connection to the server has been established
226 * disconnecthook(): the connection has been dropped
227 * notehook(TOKEN, ...): server issued a notification
228 * warnhook(TOKEN, ...): server issued a warning
229 * tracehook(TOKEN, ...): server issued a trace message
232 def __init__(me, socket):
233 """Create a new Connection."""
234 T.TripeCommandDispatcher.__init__(me, socket)
235 me.connecthook = HookList()
236 me.disconnecthook = HookList()
237 me.notehook = HookList()
238 me.warnhook = HookList()
239 me.tracehook = HookList()
240 me.handler['NOTE'] = lambda _, *rest: me.notehook.run(*rest)
241 me.handler['WARN'] = lambda _, *rest: me.warnhook.run(*rest)
242 me.handler['TRACE'] = lambda _, *rest: me.tracehook.run(*rest)
243 me.iowatch = GIOWatcher(me)
246 """Handles reconnection to the server, and signals the hook."""
247 T.TripeCommandDispatcher.connected(me)
250 def disconnected(me, reason):
251 """Handles disconnection from the server, and signals the hook."""
252 me.disconnecthook.run(reason)
253 T.TripeCommandDispatcher.disconnected(me, reason)
255 ###--------------------------------------------------------------------------
256 ### Watching the peers go by.
258 class MonitorObject (object):
260 An object with hooks it uses to notify others of changes in its state.
261 These are the objects tracked by the MonitorList class.
263 The object has a name, an `aliveness' state indicated by the `alivep' flag,
268 * changehook(): the object has changed its state
269 * deadhook(): the object has been destroyed
271 Subclass responsibilities:
273 * update(INFO): update internal state based on the provided INFO, and run
277 def __init__(me, name):
278 """Initialize the object with the given NAME."""
280 me.deadhook = HookList()
281 me.changehook = HookList()
285 """Mark the object as dead; invoke the deadhook."""
289 class Peer (MonitorObject):
291 An object representing a connected peer.
293 As well as the standard hooks, a peer has a pinghook, which isn't used
294 directly by this class.
298 * pinghook(): invoked by the Pinger (q.v.) when ping statistics change
300 Attributes provided are:
302 * addr = a vaguely human-readable representation of the peer's address
303 * ifname = the peer's interface name
304 * tunnel = the kind of tunnel the peer is using
305 * keepalive = the peer's keepalive interval in seconds
306 * ping['EPING'] and ping['PING'] = pingstate statistics (maintained by
310 def __init__(me, name):
311 """Initialize the object with the given name."""
312 MonitorObject.__init__(me, name)
313 me.pinghook = HookList()
316 def update(me, hunoz = None):
317 """Update the peer, fetching information about it from the server."""
318 me._setaddr(conn.addr(me.name))
319 me.ifname = conn.ifname(me.name)
320 me.__dict__.update(conn.peerinfo(me.name))
323 def _setaddr(me, addr):
324 """Set the peer's address."""
325 if addr[0] == 'INET':
326 ipaddr, port = addr[1:]
328 name = S.gethostbyaddr(ipaddr)[0]
329 me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
331 me.addr = 'INET %s:%s' % (ipaddr, port)
333 me.addr = ' '.join(addr)
335 def setaddr(me, addr):
336 """Informs the object of a change to its address to ADDR."""
340 def setifname(me, newname):
341 """Informs the object of a change to its interface name to NEWNAME."""
345 class Service (MonitorObject):
347 Represents a service.
349 Additional attributes are:
351 * version = the service version
353 def __init__(me, name, version):
354 MonitorObject.__init__(me, name)
357 def update(me, version):
358 """Tell the Service that its version has changed to VERSION."""
362 class MonitorList (object):
364 Maintains a collection of MonitorObjects.
366 The MonitorList can be indexed by name to retrieve the individual objects;
367 iteration generates the individual objects. More complicated operations
368 can be done on the `table' dictionary directly.
370 Hooks addhook(OBJ) and delhook(OBJ) are invoked when objects are added or
373 Subclass responsibilities:
375 * list(): return a list of (NAME, INFO) pairs.
377 * make(NAME, INFO): returns a new MonitorObject for the given NAME; INFO
378 is from the output of list().
382 """Initialize a new MonitorList."""
384 me.addhook = HookList()
385 me.delhook = HookList()
389 Refresh the list of objects:
391 We add new object which have appeared, delete ones which have vanished,
392 and update any which persist.
395 for name, stuff in me.list():
398 for name in me.table.copy():
402 def add(me, name, stuff):
404 Add a new object created by make(NAME, STUFF) if it doesn't already
405 exist. If it does, update it.
407 if name not in me.table:
408 obj = me.make(name, stuff)
412 me.table[name].update(stuff)
414 def remove(me, name):
416 Remove the object called NAME from the list.
418 The object becomes dead.
426 def __getitem__(me, name):
427 """Retrieve the object called NAME."""
428 return me.table[name]
431 """Iterate over the objects."""
432 return me.table.itervalues()
434 class PeerList (MonitorList):
435 """The list of the known peers."""
437 return [(name, None) for name in conn.list()]
438 def make(me, name, stuff):
441 class ServiceList (MonitorList):
442 """The list of the registered services."""
444 return conn.svclist()
445 def make(me, name, stuff):
446 return Service(name, stuff)
448 class Monitor (HookClient):
450 The main monitor: keeps track of the changes happening to the server.
452 Exports the peers, services MonitorLists, and a (plain Python) list
453 autopeers of peers which the connect service knows how to start by name.
457 * autopeershook(): invoked when the auto-peers list changes.
460 """Initialize the Monitor."""
461 HookClient.__init__(me)
462 me.peers = PeerList()
463 me.services = ServiceList()
464 me.hook(conn.connecthook, me._connected)
465 me.hook(conn.notehook, me._notify)
466 me.autopeershook = HookList()
470 """Handle a successful connection by starting the setup coroutine."""
475 """Coroutine function: initialize for a new connection."""
479 me._updateautopeers()
481 def _updateautopeers(me):
482 """Update the auto-peers list from the connect service."""
483 if 'connect' in me.services.table:
484 me.autopeers = [' '.join(line)
485 for line in conn.svcsubmit('connect', 'list-active')]
489 me.autopeershook.run()
491 def _notify(me, code, *rest):
493 Handle notifications from the server.
495 ADD, KILL and NEWIFNAME notifications get passed up to the PeerList;
496 SVCCLAIM and SVCRELEASE get passed up to the ServiceList. Finally,
497 peerdb-update notifications from the watch service cause us to refresh
501 T.aside(me.peers.add, rest[0], None)
503 T.aside(me.peers.remove, rest[0])
504 elif code == 'NEWIFNAME':
506 me.peers[rest[0]].setifname(rest[2])
509 elif code == 'NEWADDR':
511 me.peers[rest[0]].setaddr(rest[1:])
514 elif code == 'SVCCLAIM':
515 T.aside(me.services.add, rest[0], rest[1])
516 if rest[0] == 'connect':
517 T.aside(me._updateautopeers)
518 elif code == 'SVCRELEASE':
519 T.aside(me.services.remove, rest[0])
520 if rest[0] == 'connect':
521 T.aside(me._updateautopeers)
524 if rest[0] == 'watch' and \
525 rest[1] == 'peerdb-update':
526 T.aside(me._updateautopeers)
528 ###--------------------------------------------------------------------------
529 ### Window management cruft.
531 class MyWindowMixin (G.Window, HookClient):
533 Mixin for windows which call a closehook when they're destroyed. It's also
534 a hookclient, and will release its hooks when it's destroyed.
538 * closehook(): called when the window is closed.
542 """Initialization function. Note that it's not called __init__!"""
543 me.closehook = HookList()
544 HookClient.__init__(me)
545 me.connect('destroy', invoker(me.close))
548 """Close the window, invoking the closehook and releasing all hooks."""
553 class MyWindow (MyWindowMixin):
554 """A version of MyWindowMixin suitable as a single parent class."""
555 def __init__(me, kind = G.WINDOW_TOPLEVEL):
556 G.Window.__init__(me, kind)
559 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
560 """A dialogue box with a closehook and sensible button binding."""
562 def __init__(me, title = None, flags = 0, buttons = []):
564 The BUTTONS are a list of (STOCKID, THUNK) pairs: call the appropriate
565 THUNK when the button is pressed. The other arguments are just like
576 G.Dialog.__init__(me, title, None, flags, tuple(br))
578 me.set_default_response(i - 1)
579 me.connect('response', me.respond)
581 def respond(me, hunoz, rid, *hukairz):
582 """Dispatch responses to the appropriate thunks."""
583 if rid >= 0: me.rmap[rid]()
585 def makeactiongroup(name, acts):
587 Creates an ActionGroup called NAME.
589 ACTS is a list of tuples containing:
591 * ACT: an action name
592 * LABEL: the label string for the action
593 * ACCEL: accelerator string, or None
594 * FUNC: thunk to call when the action is invoked
596 actgroup = G.ActionGroup(name)
597 for act, label, accel, func in acts:
598 a = G.Action(act, label, None, None)
599 if func: a.connect('activate', invoker(func))
600 actgroup.add_action_with_accel(a, accel)
603 class GridPacker (G.Table):
605 Like a Table, but with more state: makes filling in the widgets easier.
609 """Initialize a new GridPacker."""
615 me.set_border_width(4)
616 me.set_col_spacings(4)
617 me.set_row_spacings(4)
619 def pack(me, w, width = 1, newlinep = False,
620 xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
625 W is the widget to add. XOPY, YOPT, XPAD and YPAD are as for Table.
626 WIDTH is how many cells to take up horizontally. NEWLINEP is whether to
627 start a new line for this widget. Returns W.
633 right = me.col + width
634 if bot > me.rows or right > me.cols:
635 if bot > me.rows: me.rows = bot
636 if right > me.cols: me.cols = right
637 me.resize(me.rows, me.cols)
638 me.attach(w, me.col, me.col + width, me.row, me.row + 1,
639 xopt, yopt, xpad, ypad)
643 def labelled(me, lab, w, newlinep = False, **kw):
645 Packs a labelled widget.
647 Other arguments are as for pack. Returns W.
649 label = G.Label(lab + ' ')
650 label.set_alignment(1.0, 0)
651 me.pack(label, newlinep = newlinep, xopt = G.FILL)
655 def info(me, label, text = None, len = 18, **kw):
657 Packs an information widget with a label.
659 LABEL is the label; TEXT is the initial text; LEN is the estimated length
660 in characters. Returns the entry widget.
663 if text is not None: e.set_text(text)
664 e.set_width_chars(len)
665 e.set_selectable(True)
666 e.set_alignment(0.0, 0.5)
667 me.labelled(label, e, **kw)
670 class WindowSlot (HookClient):
672 A place to store a window -- specificially a MyWindowMixin.
674 If the window is destroyed, remember this; when we come to open the window,
675 raise it if it already exists; otherwise make a new one.
677 def __init__(me, createfunc):
679 Constructor: CREATEFUNC must return a new Window which supports the
682 HookClient.__init__(me)
683 me.createfunc = createfunc
687 """Opens the window, creating it if necessary."""
689 raise_window(me.window)
691 me.window = me.createfunc()
692 me.hook(me.window.closehook, me.closed)
695 """Handles the window being closed."""
696 me.unhook(me.window.closehook)
699 class MyTreeView (G.TreeView):
700 def __init__(me, model):
701 G.TreeView.__init__(me, model)
702 me.set_rules_hint(True)
704 class MyScrolledWindow (G.ScrolledWindow):
706 G.ScrolledWindow.__init__(me)
707 me.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
708 me.set_shadow_type(G.SHADOW_IN)
710 ## Matches a signed integer.
711 rx_num = RX.compile(r'^[-+]?\d+$')
714 c_red = GDK.color_parse('#ff6666')
716 class ValidationError (Exception):
717 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
720 class ValidatingEntry (G.Entry):
722 Like an Entry, but makes the text go red if the contents are invalid.
724 If get_text is called, and the text is invalid, ValidationError is raised.
725 The attribute validp reflects whether the contents are currently valid.
728 def __init__(me, valid, text = '', size = -1, *arg, **kw):
730 Make a validating Entry.
732 VALID is a regular expression or a predicate on strings. TEXT is the
733 default text to insert. SIZE is the size of the box to set, in
734 characters (ish). Other arguments are passed to Entry.
736 G.Entry.__init__(me, *arg, **kw)
737 me.connect("changed", me._check)
738 me.connect("state-changed", me._check)
742 me.validate = RX.compile(valid).match
744 if size != -1: me.set_width_chars(size)
745 me.set_activates_default(True)
749 def _check(me, *hunoz):
750 """Check the current text and update validp and the text colour."""
751 if me.validate(G.Entry.get_text(me)):
753 set_entry_bg(me, None)
756 set_entry_bg(me, me.is_sensitive() and c_red or None)
760 Return the text in the Entry if it's valid. If it isn't, raise
764 raise ValidationError
765 return G.Entry.get_text(me)
767 def numericvalidate(min = None, max = None):
769 Return a validation function for numbers.
771 Entry must consist of an optional sign followed by digits, and the
772 resulting integer must be within the given bounds.
774 return lambda x: (rx_num.match(x) and
775 (min is None or long(x) >= min) and
776 (max is None or long(x) <= max))
778 ###--------------------------------------------------------------------------
779 ### Various minor dialog boxen.
781 GPL = """This program is free software; you can redistribute it and/or modify
782 it under the terms of the GNU General Public License as published by
783 the Free Software Foundation; either version 2 of the License, or
784 (at your option) any later version.
786 This program is distributed in the hope that it will be useful,
787 but WITHOUT ANY WARRANTY; without even the implied warranty of
788 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
789 GNU General Public License for more details.
791 You should have received a copy of the GNU General Public License
792 along with this program; if not, write to the Free Software Foundation,
793 Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
795 class AboutBox (G.AboutDialog, MyWindowMixin):
796 """The program `About' box."""
798 G.AboutDialog.__init__(me)
800 me.set_name('TrIPEmon')
801 me.set_version(T.VERSION)
803 me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
804 me.set_comments('A graphical monitor for the TrIPE VPN server')
805 me.set_copyright('Copyright © 2006-2008 Straylight/Edgeware')
806 me.connect('response', me.respond)
808 def respond(me, hunoz, rid, *hukairz):
809 if rid == G.RESPONSE_CANCEL:
811 aboutbox = WindowSlot(AboutBox)
814 """Report an error message in a window."""
815 d = G.Dialog('Error from %s' % M.quis,
816 flags = G.DIALOG_MODAL,
817 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
819 label.set_padding(20, 20)
820 d.vbox.pack_start(label, True, True, 0)
825 def unimplemented(*hunoz):
826 """Indicator of laziness."""
827 moanbox("I've not written that bit yet.")
829 ###--------------------------------------------------------------------------
832 class LogModel (G.ListStore):
834 A simple list of log messages, usable as the model for a TreeView.
836 The column headings are stored in the `cols' attribute.
839 def __init__(me, columns):
841 COLUMNS must be a list of column name strings. We add a time column to
844 me.cols = ('Time',) + columns
845 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
847 def add(me, *entries):
849 Adds a new log message, with a timestamp.
851 The ENTRIES are the contents for the list columns.
853 now = TIME.strftime('%Y-%m-%d %H:%M:%S')
854 me.append((now, ) + entries)
856 class TraceLogModel (LogModel):
857 """Log model for trace messages."""
859 LogModel.__init__(me, ('Message',))
860 def notify(me, line):
861 """Call with a new trace message."""
864 class WarningLogModel (LogModel):
866 Log model for warnings.
868 We split the category out into a separate column.
871 LogModel.__init__(me, ('Category', 'Message'))
872 def notify(me, tag, *rest):
873 """Call with a new warning message."""
874 me.add(tag, ' '.join([T.quotify(w) for w in rest]))
876 class LogViewer (MyWindow):
880 Its contents are a TreeView showing the log.
884 * model: an appropriate LogModel
885 * list: a TreeView widget to display the log
888 def __init__(me, model):
890 Create a log viewer showing the LogModel MODEL.
892 MyWindow.__init__(me)
894 scr = MyScrolledWindow()
895 me.list = MyTreeView(me.model)
897 for c in me.model.cols:
898 crt = G.CellRendererText()
899 me.list.append_column(G.TreeViewColumn(c, crt, text = i))
901 crt.set_property('family', 'monospace')
902 me.set_default_size(440, 256)
907 ###--------------------------------------------------------------------------
910 class pingstate (struct):
912 Information kept for each peer by the Pinger.
914 Important attributes:
916 * peer = the peer name
917 * command = PING or EPING
918 * n = how many pings we've sent so far
919 * ngood = how many returned
920 * nmiss = how many didn't return
921 * nmissrun = how many pings since the last good one
922 * tlast = round-trip time for the last (good) ping
923 * ttot = total roung trip time
927 class Pinger (T.Coroutine, HookClient):
929 Coroutine which pings known peers and collects statistics.
931 Interesting attributes:
933 * _map: dict mapping peer names to Peer objects
934 * _q: event queue for notifying pinger coroutine
935 * _timer: gobject timer for waking the coroutine
940 Initialize the pinger.
942 We watch the monitor's PeerList to track which peers we should ping. We
943 maintain an event queue and put all the events on that.
945 The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
946 where CMD is 'PING' or 'EPING'.
948 T.Coroutine.__init__(me)
949 HookClient.__init__(me)
953 me.hook(conn.connecthook, me._connected)
954 me.hook(conn.disconnecthook, me._disconnected)
955 me.hook(monitor.peers.addhook,
956 lambda p: T.defer(me._q.put, (p, 'ADD', None)))
957 me.hook(monitor.peers.delhook,
958 lambda p: T.defer(me._q.put, (p, 'KILL', None)))
959 if conn.connectedp(): me.connected()
962 """Respond to connection: start pinging thngs."""
963 me._timer = GL.timeout_add(1000, me._timerfunc)
966 """Timer function: put a timer event on the queue."""
967 me._q.put((None, 'TIMER', None))
970 def _disconnected(me, reason):
971 """Respond to disconnection: stop pinging."""
972 GL.source_remove(me._timer)
976 Coroutine function: read events from the queue and process them.
980 * (PEER, 'KILL', None): remove PEER from the interesting peers list
981 * (PEER, 'ADD', None): add PEER to the list
982 * (PEER, 'INFO', TOKENS): result from a PING command
983 * (None, 'TIMER', None): interval timer went off: send more pings
986 tag, code, stuff = me._q.get()
991 elif not conn.connectedp():
996 for cmd in 'PING', 'EPING':
997 ps = pingstate(command = cmd, peer = p,
998 n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1002 elif code == 'INFO':
1004 if stuff[0] == 'ping-ok':
1014 ps.peer.pinghook.run(ps.peer, ps.command, ps)
1015 elif code == 'TIMER':
1016 for name, p in me._map.iteritems():
1017 for cmd, ps in p.ping.iteritems():
1018 conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
1019 cmd, '-background', conn.bgtag(), '--', name]))
1021 ###--------------------------------------------------------------------------
1022 ### Random dialogue boxes.
1024 class AddPeerDialog (MyDialog):
1026 Let the user create a new peer the low-level way.
1028 Interesting attributes:
1030 * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
1034 """Initialize the dialogue."""
1035 MyDialog.__init__(me, 'Add peer',
1036 buttons = [(G.STOCK_CANCEL, me.destroy),
1037 (G.STOCK_OK, me.ok)])
1042 """Coroutine function: background setup for AddPeerDialog."""
1043 table = GridPacker()
1044 me.vbox.pack_start(table, True, True, 0)
1045 me.e_name = table.labelled('Name',
1046 ValidatingEntry(r'^[^\s.:]+$', '', 16),
1048 me.e_addr = table.labelled('Address',
1049 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
1051 me.e_port = table.labelled('Port',
1052 ValidatingEntry(numericvalidate(0, 65535),
1055 me.c_keepalive = G.CheckButton('Keepalives')
1056 me.l_tunnel = table.labelled('Tunnel', combo_box_text(),
1057 newlinep = True, width = 3)
1058 me.tuns = conn.tunnels()
1060 me.l_tunnel.append_text(t)
1061 me.l_tunnel.set_active(0)
1062 table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
1063 me.c_keepalive.connect('toggled',
1064 lambda t: me.e_keepalive.set_sensitive\
1066 me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
1067 me.e_keepalive.set_sensitive(False)
1068 table.pack(me.e_keepalive, width = 3)
1072 """Handle an OK press: create the peer."""
1074 if me.c_keepalive.get_active():
1075 ka = me.e_keepalive.get_text()
1078 t = me.l_tunnel.get_active()
1083 me._addpeer(me.e_name.get_text(),
1084 me.e_addr.get_text(),
1085 me.e_port.get_text(),
1088 except ValidationError:
1093 def _addpeer(me, name, addr, port, keepalive, tunnel):
1094 """Coroutine function: actually do the ADD command."""
1096 conn.add(name, addr, port, keepalive = keepalive, tunnel = tunnel)
1098 except T.TripeError, exc:
1099 T.defer(moanbox, ' '.join(exc))
1101 class ServInfo (MyWindow):
1103 Show information about the server and available services.
1105 Interesting attributes:
1107 * e: maps SERVINFO keys to entry widgets
1108 * svcs: Gtk ListStore describing services (columns are name and version)
1112 MyWindow.__init__(me)
1113 me.set_title('TrIPE server info')
1114 table = GridPacker()
1117 def add(label, tag, text = None, **kw):
1118 me.e[tag] = table.info(label, text, **kw)
1119 add('Implementation', 'implementation')
1120 add('Version', 'version', newlinep = True)
1121 me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1122 me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1123 scr = MyScrolledWindow()
1124 lb = MyTreeView(me.svcs)
1126 for title in 'Service', 'Version':
1127 lb.append_column(G.TreeViewColumn(
1128 title, G.CellRendererText(), text = i))
1130 for svc in monitor.services:
1131 me.svcs.append([svc.name, svc.version])
1133 table.pack(scr, width = 2, newlinep = True,
1134 yopt = G.EXPAND | G.FILL | G.SHRINK)
1136 me.hook(conn.connecthook, me.update)
1137 me.hook(monitor.services.addhook, me.addsvc)
1138 me.hook(monitor.services.delhook, me.delsvc)
1141 def addsvc(me, svc):
1142 me.svcs.append([svc.name, svc.version])
1144 def delsvc(me, svc):
1145 for i in xrange(len(me.svcs)):
1146 if me.svcs[i][0] == svc.name:
1147 me.svcs.remove(me.svcs.get_iter(i))
1151 info = conn.servinfo()
1153 me.e[i].set_text(info[i])
1155 class TraceOptions (MyDialog):
1156 """Tracing options window."""
1158 MyDialog.__init__(me, title = 'Tracing options',
1159 buttons = [(G.STOCK_CLOSE, me.destroy),
1160 (G.STOCK_OK, cr(me.ok))])
1166 for ch, st, desc in conn.trace():
1167 if ch.isupper(): continue
1168 text = desc[0].upper() + desc[1:]
1169 ticky = G.CheckButton(text)
1170 ticky.set_active(st == '+')
1171 me.vbox.pack_start(ticky, True, True, 0)
1172 me.opts.append((ch, ticky))
1177 for ch, ticky in me.opts:
1178 if ticky.get_active():
1182 setting = ''.join(on) + '-' + ''.join(off)
1186 ###--------------------------------------------------------------------------
1190 """Translate a TrIPE-format time to something human-readable."""
1191 if t == 'NEVER': return '(never)'
1192 YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1193 ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1194 ago = MATH.floor(ago); unit = 's'
1195 for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1199 return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1200 (YY, MM, DD, hh, mm, ss, ago, unit)
1202 """Translate a number of bytes into something a human might want to read."""
1209 return '%d %s' % (b, suff)
1211 ## How to translate peer stats. Maps the stat name to a translation
1214 [('start-time', xlate_time),
1215 ('last-packet-time', xlate_time),
1216 ('last-keyexch-time', xlate_time),
1217 ('bytes-in', xlate_bytes),
1218 ('bytes-out', xlate_bytes),
1219 ('keyexch-bytes-in', xlate_bytes),
1220 ('keyexch-bytes-out', xlate_bytes),
1221 ('ip-bytes-in', xlate_bytes),
1222 ('ip-bytes-out', xlate_bytes)]
1224 ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
1225 ## the label to give the entry box; FORMAT is the format string to write into
1228 [('Start time', '%(start-time)s'),
1229 ('Last key-exchange', '%(last-keyexch-time)s'),
1230 ('Last packet', '%(last-packet-time)s'),
1232 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1233 ('Key-exchange in/out',
1234 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1236 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1237 ('Rejected packets', '%(rejected-packets)s')]
1239 class PeerWindow (MyWindow):
1241 Show information about a peer.
1243 This gives a graphical view of the server's peer statistics.
1245 Interesting attributes:
1247 * e: dict mapping keys (mostly matching label widget texts, though pings
1248 use command names) to entry widgets so that we can update them easily
1249 * peer: the peer this window shows information about
1250 * cr: the info-fetching coroutine, or None if crrrently disconnected
1251 * doupate: whether the info-fetching corouting should continue running
1254 def __init__(me, peer):
1255 """Construct a PeerWindow, showing information about PEER."""
1257 MyWindow.__init__(me)
1258 me.set_title('TrIPE statistics: %s' % peer.name)
1261 table = GridPacker()
1264 ## Utility for adding fields.
1266 def add(label, text = None, key = None):
1267 if key is None: key = label
1268 me.e[key] = table.info(label, text, len = 42, newlinep = True)
1270 ## Build the dialogue box.
1271 add('Peer name', peer.name)
1272 add('Tunnel', peer.tunnel)
1273 add('Interface', peer.ifname)
1275 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1276 add('Address', peer.addr)
1277 add('Transport pings', key = 'PING')
1278 add('Encrypted pings', key = 'EPING')
1280 for label, format in statslayout:
1283 ## Hook onto various interesting events.
1284 me.hook(conn.connecthook, me.tryupdate)
1285 me.hook(conn.disconnecthook, me.stopupdate)
1286 me.hook(me.closehook, me.stopupdate)
1287 me.hook(me.peer.deadhook, me.dead)
1288 me.hook(me.peer.changehook, me.change)
1289 me.hook(me.peer.pinghook, me.ping)
1294 ## Format the ping statistics.
1295 for cmd, ps in me.peer.ping.iteritems():
1296 me.ping(me.peer, cmd, ps)
1298 ## And show the window.
1302 """Update the display in response to a notification."""
1303 me.e['Interface'].set_text(me.peer.ifname)
1307 Main display-updating coroutine.
1309 This does an update, sleeps for a while, and starts again. If the
1310 me.doupdate flag goes low, we stop the loop.
1312 while me.peer.alivep and conn.connectedp() and me.doupdate:
1313 stat = conn.stats(me.peer.name)
1314 for s, trans in statsxlate:
1315 stat[s] = trans(stat[s])
1316 for label, format in statslayout:
1317 me.e[label].set_text(format % stat)
1318 GL.timeout_add(1000, lambda: me.cr.switch() and False)
1319 me.cr.parent.switch()
1323 """Start the updater coroutine, if it's not going already."""
1325 me.cr = T.Coroutine(me._update,
1326 name = 'update-peer-window %s' % me.peer.name)
1329 def stopupdate(me, *hunoz, **hukairz):
1330 """Stop the update coroutine, by setting me.doupdate."""
1334 """Called when the peer is killed."""
1335 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1336 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1339 def ping(me, peer, cmd, ps):
1340 """Called when a ping result for the peer is reported."""
1341 s = '%d/%d' % (ps.ngood, ps.n)
1343 s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1345 s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1346 me.e[ps.command].set_text(s)
1348 ###--------------------------------------------------------------------------
1349 ### Cryptographic status.
1351 class CryptoInfo (MyWindow):
1352 """Simple display of cryptographic algorithms in use."""
1354 MyWindow.__init__(me)
1355 me.set_title('Cryptographic algorithms')
1356 T.aside(me.populate)
1358 table = GridPacker()
1361 crypto = conn.algs()
1362 table.info('Diffie-Hellman group',
1363 '%s (%d-bit order, %d-bit elements)' %
1364 (crypto['kx-group'],
1365 int(crypto['kx-group-order-bits']),
1366 int(crypto['kx-group-elt-bits'])),
1368 table.info('Data encryption',
1369 '%s (%d-bit key; %s)' %
1371 int(crypto['cipher-keysz']) * 8,
1372 crypto['cipher-blksz'] == '0'
1374 or '%d-bit block' % (int(crypto['cipher-blksz']) * 8)),
1376 table.info('Message authentication',
1377 '%s (%d-bit key; %d-bit tag)' %
1379 int(crypto['mac-keysz']) * 8,
1380 int(crypto['mac-tagsz']) * 8),
1382 table.info('Hash function',
1383 '%s (%d-bit output)' %
1385 int(crypto['hash-sz']) * 8),
1390 ###--------------------------------------------------------------------------
1391 ### Main monitor window.
1393 class MonitorWindow (MyWindow):
1396 The main monitor window.
1398 This class creates, populates and maintains the main monitor window.
1402 * warnings, trace: log models for server output
1403 * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1404 WindowSlot objects for ancillary windows
1405 * ui: Gtk UIManager object for the menu system
1406 * apmenu: pair of identical autoconnecting peer menus
1407 * listmodel: Gtk ListStore for connected peers; contains peer name,
1408 address, and ping times (transport and encrypted, value and colour)
1409 * status: Gtk Statusbar at the bottom of the window
1410 * _kidding: an unpleasant backchannel between the apchange method (which
1411 builds the apmenus) and the menu handler, forced on us by a Gtk
1414 Also installs attributes on Peer objects:
1416 * i: index of peer's entry in listmodel
1417 * win: WindowSlot object for the peer's PeerWindow
1421 """Construct the window."""
1424 MyWindow.__init__(me)
1425 me.set_title('TrIPE monitor')
1427 ## Hook onto diagnostic outputs.
1428 me.warnings = WarningLogModel()
1429 me.hook(conn.warnhook, me.warnings.notify)
1430 me.trace = TraceLogModel()
1431 me.hook(conn.tracehook, me.trace.notify)
1433 ## Make slots to store the various ancillary singleton windows.
1434 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1435 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1436 me.traceopts = WindowSlot(lambda: TraceOptions())
1437 me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1438 me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1439 me.servinfo = WindowSlot(lambda: ServInfo())
1441 ## Main window structure.
1445 ## UI manager makes our menus. (We're too cheap to have a toolbar.)
1446 me.ui = G.UIManager()
1447 actgroup = makeactiongroup('monitor',
1448 [('file-menu', '_File', None, None),
1449 ('connect', '_Connect', '<Control>C', conn.connect),
1450 ('disconnect', '_Disconnect', '<Control>D',
1451 lambda: conn.disconnect(None)),
1452 ('quit', '_Quit', '<Control>Q', me.close),
1453 ('server-menu', '_Server', None, None),
1454 ('daemon', 'Run in _background', None, cr(conn.daemon)),
1455 ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1456 ('crypto-algs', 'Cryptographic algorithms',
1457 '<Control>Y', me.cryptoinfo.open),
1458 ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1459 ('server-quit', 'Terminate server', None, cr(conn.quit)),
1460 ('conn-peer', 'Connect peer', None, None),
1461 ('logs-menu', '_Logs', None, None),
1462 ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1463 ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1464 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1465 ('help-menu', '_Help', None, None),
1466 ('about', '_About tripemon...', None, aboutbox.open),
1467 ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1468 ('kill-peer', '_Kill peer', None, me.killpeer),
1469 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1475 <menu action="file-menu">
1476 <menuitem action="quit"/>
1478 <menu action="server-menu">
1479 <menuitem action="connect"/>
1480 <menuitem action="disconnect"/>
1482 <menuitem action="server-version"/>
1483 <menuitem action="crypto-algs"/>
1484 <menuitem action="add-peer"/>
1485 <menuitem action="conn-peer"/>
1486 <menuitem action="daemon"/>
1487 <menuitem action="reload-keys"/>
1489 <menuitem action="server-quit"/>
1491 <menu action="logs-menu">
1492 <menuitem action="show-warnings"/>
1493 <menuitem action="show-trace"/>
1494 <menuitem action="trace-options"/>
1496 <menu action="help-menu">
1497 <menuitem action="about"/>
1500 <popup name="peer-popup">
1501 <menuitem action="add-peer"/>
1502 <menuitem action="conn-peer"/>
1503 <menuitem action="kill-peer"/>
1504 <menuitem action="force-kx"/>
1509 ## Populate the UI manager.
1510 me.ui.insert_action_group(actgroup, 0)
1511 me.ui.add_ui_from_string(uidef)
1513 ## Construct the menu bar.
1514 vbox.pack_start(me.ui.get_widget('/menubar'), False, True, 0)
1515 me.add_accel_group(me.ui.get_accel_group())
1517 ## Construct and attach the auto-peers menu. (This is a horrible bodge
1518 ## because we can't attach the same submenu in two different places.)
1519 me.apmenu = G.Menu(), G.Menu()
1520 me.ui.get_widget('/menubar/server-menu/conn-peer') \
1521 .set_submenu(me.apmenu[0])
1522 me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1524 ## Construct the main list model, and listen on hooks which report
1525 ## changes to the available peers.
1526 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1527 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1528 me.hook(monitor.peers.addhook, me.addpeer)
1529 me.hook(monitor.peers.delhook, me.delpeer)
1530 me.hook(monitor.autopeershook, me.apchange)
1532 ## Construct the list viewer and put it in a scrolling window.
1533 scr = MyScrolledWindow()
1534 me.list = MyTreeView(me.listmodel)
1535 me.list.append_column(G.TreeViewColumn('Peer name',
1536 G.CellRendererText(),
1538 me.list.append_column(G.TreeViewColumn('Address',
1539 G.CellRendererText(),
1541 me.list.append_column(G.TreeViewColumn('T-ping',
1542 G.CellRendererText(),
1545 me.list.append_column(G.TreeViewColumn('E-ping',
1546 G.CellRendererText(),
1549 me.list.get_column(1).set_expand(True)
1550 me.list.connect('row-activated', me.activate)
1551 me.list.connect('button-press-event', me.buttonpress)
1552 me.list.set_reorderable(True)
1553 me.list.get_selection().set_mode(G.SELECTION_NONE)
1555 vbox.pack_start(scr, True, True, 0)
1557 ## Construct the status bar, and listen on hooks which report changes to
1558 ## connection status.
1559 me.status = G.Statusbar()
1560 vbox.pack_start(me.status, False, True, 0)
1561 me.hook(conn.connecthook, cr(me.connected))
1562 me.hook(conn.disconnecthook, me.disconnected)
1563 me.hook(conn.notehook, me.notify)
1565 ## Set a plausible default window size.
1566 me.set_default_size(512, 180)
1568 def addpeer(me, peer):
1569 """Hook: announces that PEER has been added."""
1570 peer.i = me.listmodel.append([peer.name, peer.addr,
1571 '???', 'green', '???', 'green'])
1572 peer.win = WindowSlot(lambda: PeerWindow(peer))
1573 me.hook(peer.pinghook, me._ping)
1576 def delpeer(me, peer):
1577 """Hook: announces that PEER has been removed."""
1578 me.listmodel.remove(peer.i)
1579 me.unhook(peer.pinghook)
1582 def path_peer(me, path):
1583 """Return the peer corresponding to a given list-model PATH."""
1584 return monitor.peers[me.listmodel[path][0]]
1588 Hook: announces that a change has been made to the peers available for
1589 automated connection.
1591 This populates both auto-peer menus and keeps them in sync. (As
1592 mentioned above, we can't attach the same submenu to two separate parent
1593 menu items. So we end up with two identical menus instead. Yes, this
1597 ## The set_active method of a CheckMenuItem works by maybe activating the
1598 ## menu item. This signals our handler. But we don't actually want to
1599 ## signal the handler unless the user actually frobbed the item. So the
1600 ## _kidding flag is used as an underhanded way of telling the handler
1601 ## that we don't actually want it to do anything. Of course, this sucks
1605 ## Iterate over the two menus.
1608 existing = menu.get_children()
1609 if monitor.autopeers is None:
1611 ## No peers, so empty out the menu.
1612 for item in existing:
1617 ## Insert the new items into the menu. (XXX this seems buggy XXX)
1618 ## Tick the peers which are actually connected.
1620 for peer in monitor.autopeers:
1621 if j < len(existing) and \
1622 existing[j].get_child().get_text() == peer:
1626 item = G.CheckMenuItem(peer, use_underline = False)
1627 item.connect('activate', invoker(me._addautopeer, peer))
1628 menu.insert(item, i)
1629 item.set_active(peer in monitor.peers.table)
1632 ## Make all the menu items visible.
1635 ## Set the parent menu items sensitive if and only if there are any peers
1637 for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1638 me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1640 ## And now allow the handler to do its business normally.
1643 def _addautopeer(me, peer):
1645 Automatically connect an auto-peer.
1647 This method is invoked from the main coroutine. Since the actual
1648 connection needs to issue administration commands, we must spawn a new
1649 child coroutine for it.
1653 T.Coroutine(me._addautopeer_hack,
1654 name = '_addautopeerhack %s' % peer).switch(peer)
1656 def _addautopeer_hack(me, peer):
1657 """Make an automated connection to PEER in response to a user click."""
1661 T._simple(conn.svcsubmit('connect', 'active', peer))
1662 except T.TripeError, exc:
1663 T.defer(moanbox, ' '.join(exc.args))
1666 def activate(me, l, path, col):
1668 Handle a double-click on a peer in the main list: open a PeerInfo window.
1670 peer = me.path_peer(path)
1673 def buttonpress(me, l, ev):
1675 Handle a mouse click on the main list.
1677 Currently we're only interested in button-3, which pops up the peer menu.
1678 For future reference, we stash the peer that was clicked in me.menupeer.
1681 x, y = int(ev.x), int(ev.y)
1682 r = me.list.get_path_at_pos(x, y)
1683 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1684 me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1686 me.ui.get_widget('/peer-popup/conn-peer'). \
1687 set_sensitive(bool(monitor.autopeers))
1689 me.menupeer = me.path_peer(r[0])
1692 me.ui.get_widget('/peer-popup').popup(
1693 None, None, None, ev.button, ev.time)
1696 """Kill a peer from the popup menu."""
1697 cr(conn.kill, me.menupeer.name)()
1700 """Kickstart a key-exchange from the popup menu."""
1701 cr(conn.forcekx, me.menupeer.name)()
1703 _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1704 def _ping(me, p, cmd, ps):
1705 """Hook: responds to ping reports."""
1706 textcol, colourcol = me._columnmap[cmd]
1708 me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1709 me.listmodel[p.i][colourcol] = 'red'
1711 me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1712 me.listmodel[p.i][colourcol] = 'black'
1714 def setstatus(me, status):
1715 """Update the message in the status bar."""
1717 me.status.push(0, status)
1719 def notify(me, note, *rest):
1720 """Hook: invoked when interesting notifications occur."""
1721 if note == 'DAEMON':
1722 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1726 Hook: invoked when a connection is made to the server.
1728 Make options which require a server connection sensitive.
1730 me.setstatus('Connected (port %s)' % conn.port())
1731 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1732 for i in ('/menubar/server-menu/disconnect',
1733 '/menubar/server-menu/server-version',
1734 '/menubar/server-menu/add-peer',
1735 '/menubar/server-menu/server-quit',
1736 '/menubar/logs-menu/trace-options'):
1737 me.ui.get_widget(i).set_sensitive(True)
1738 me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1739 set_sensitive(bool(monitor.autopeers))
1740 me.ui.get_widget('/menubar/server-menu/daemon'). \
1741 set_sensitive(conn.servinfo()['daemon'] == 'nil')
1743 def disconnected(me, reason):
1745 Hook: invoked when the connection to the server is lost.
1747 Make most options insensitive.
1749 me.setstatus('Disconnected')
1750 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1751 for i in ('/menubar/server-menu/disconnect',
1752 '/menubar/server-menu/server-version',
1753 '/menubar/server-menu/add-peer',
1754 '/menubar/server-menu/conn-peer',
1755 '/menubar/server-menu/daemon',
1756 '/menubar/server-menu/server-quit',
1757 '/menubar/logs-menu/trace-options'):
1758 me.ui.get_widget(i).set_sensitive(False)
1759 if reason: moanbox(reason)
1761 ###--------------------------------------------------------------------------
1764 def parse_options():
1766 Parse command-line options.
1768 Process the boring ones. Return all of them, for later.
1770 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1771 version = '%prog (tripe version 1.0.0)')
1772 op.add_option('-a', '--admin-socket',
1773 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1774 help = 'Select socket to connect to [default %default]')
1775 op.add_option('-d', '--directory',
1776 metavar = 'DIR', dest = 'dir', default = T.configdir,
1777 help = 'Select current diretory [default %default]')
1778 opts, args = op.parse_args()
1779 if args: op.error('no arguments permitted')
1784 """Initialization."""
1786 global conn, monitor, pinger
1788 ## Try to establish a connection.
1789 conn = Connection(opts.tripesock)
1791 ## Make the main interesting coroutines and objects.
1799 root = MonitorWindow()
1804 HookClient().hook(root.closehook, exit)
1807 if __name__ == '__main__':
1808 opts = parse_options()
1812 ###----- That's all, folks --------------------------------------------------