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 it under
14 ### the terms of the GNU General Public License as published by the Free
15 ### Software Foundation; either version 3 of the License, or (at your
16 ### option) any later version.
18 ### TrIPE is distributed in the hope that it will be useful, but WITHOUT
19 ### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
20 ### FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
23 ### You should have received a copy of the GNU General Public License
24 ### along with TrIPE. If not, see <https://www.gnu.org/licenses/>.
26 ###--------------------------------------------------------------------------
32 from sys import argv, exit, stdin, stdout, stderr, exc_info, excepthook
34 from os import environ
37 from optparse import OptionParser
40 from cStringIO import StringIO
43 if OS.getenv('TRIPEMON_FORCE_GI'): raise ImportError()
50 GDK.KEY_Escape = G.keysyms.Escape
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()
314 me.__dict__.update(conn.algs(name))
317 def update(me, hunoz = None):
318 """Update the peer, fetching information about it from the server."""
319 me._setaddr(conn.addr(me.name))
320 me.ifname = conn.ifname(me.name)
321 me.__dict__.update(conn.peerinfo(me.name))
324 def _setaddr(me, addr):
325 """Set the peer's address."""
326 if addr[0] == 'INET':
327 af, ipaddr, port = addr
329 name, _ = S.getnameinfo((ipaddr, int(port)),
330 S.NI_NUMERICSERV | S.NI_NAMEREQD)
332 me.addr = '%s %s:%s' % (af, ipaddr, port)
334 me.addr = '%s %s:%s [%s]' % (af, name, port, ipaddr)
336 me.addr = ' '.join(addr)
338 def setaddr(me, addr):
339 """Informs the object of a change to its address to ADDR."""
343 def setifname(me, newname):
344 """Informs the object of a change to its interface name to NEWNAME."""
348 class Service (MonitorObject):
350 Represents a service.
352 Additional attributes are:
354 * version = the service version
356 def __init__(me, name, version):
357 MonitorObject.__init__(me, name)
360 def update(me, version):
361 """Tell the Service that its version has changed to VERSION."""
365 class MonitorList (object):
367 Maintains a collection of MonitorObjects.
369 The MonitorList can be indexed by name to retrieve the individual objects;
370 iteration generates the individual objects. More complicated operations
371 can be done on the `table' dictionary directly.
373 Hooks addhook(OBJ) and delhook(OBJ) are invoked when objects are added or
376 Subclass responsibilities:
378 * list(): return a list of (NAME, INFO) pairs.
380 * make(NAME, INFO): returns a new MonitorObject for the given NAME; INFO
381 is from the output of list().
385 """Initialize a new MonitorList."""
387 me.addhook = HookList()
388 me.delhook = HookList()
392 Refresh the list of objects:
394 We add new object which have appeared, delete ones which have vanished,
395 and update any which persist.
398 for name, stuff in me.list():
401 for name in me.table.copy():
405 def add(me, name, stuff):
407 Add a new object created by make(NAME, STUFF) if it doesn't already
408 exist. If it does, update it.
410 if name not in me.table:
411 obj = me.make(name, stuff)
415 me.table[name].update(stuff)
417 def remove(me, name):
419 Remove the object called NAME from the list.
421 The object becomes dead.
429 def __getitem__(me, name):
430 """Retrieve the object called NAME."""
431 return me.table[name]
434 """Iterate over the objects."""
435 return me.table.itervalues()
437 class PeerList (MonitorList):
438 """The list of the known peers."""
440 return [(name, None) for name in conn.list()]
441 def make(me, name, stuff):
444 class ServiceList (MonitorList):
445 """The list of the registered services."""
447 return conn.svclist()
448 def make(me, name, stuff):
449 return Service(name, stuff)
451 class Monitor (HookClient):
453 The main monitor: keeps track of the changes happening to the server.
455 Exports the peers, services MonitorLists, and a (plain Python) list
456 autopeers of peers which the connect service knows how to start by name.
460 * autopeershook(): invoked when the auto-peers list changes.
463 """Initialize the Monitor."""
464 HookClient.__init__(me)
465 me.peers = PeerList()
466 me.services = ServiceList()
467 me.hook(conn.connecthook, me._connected)
468 me.hook(conn.notehook, me._notify)
469 me.autopeershook = HookList()
473 """Handle a successful connection by starting the setup coroutine."""
478 """Coroutine function: initialize for a new connection."""
482 me._updateautopeers()
484 def _updateautopeers(me):
485 """Update the auto-peers list from the connect service."""
486 if 'connect' in me.services.table:
487 me.autopeers = [' '.join(line)
488 for line in conn.svcsubmit('connect', 'list-active')]
492 me.autopeershook.run()
494 def _notify(me, code, *rest):
496 Handle notifications from the server.
498 ADD, KILL and NEWIFNAME notifications get passed up to the PeerList;
499 SVCCLAIM and SVCRELEASE get passed up to the ServiceList. Finally,
500 peerdb-update notifications from the watch service cause us to refresh
504 T.aside(me.peers.add, rest[0], None)
506 T.aside(me.peers.remove, rest[0])
507 elif code == 'NEWIFNAME':
509 me.peers[rest[0]].setifname(rest[2])
512 elif code == 'NEWADDR':
514 me.peers[rest[0]].setaddr(rest[1:])
517 elif code == 'SVCCLAIM':
518 T.aside(me.services.add, rest[0], rest[1])
519 if rest[0] == 'connect':
520 T.aside(me._updateautopeers)
521 elif code == 'SVCRELEASE':
522 T.aside(me.services.remove, rest[0])
523 if rest[0] == 'connect':
524 T.aside(me._updateautopeers)
527 if rest[0] == 'watch' and \
528 rest[1] == 'peerdb-update':
529 T.aside(me._updateautopeers)
531 ###--------------------------------------------------------------------------
532 ### Window management cruft.
534 class MyWindowMixin (G.Window, HookClient):
536 Mixin for windows which call a closehook when they're destroyed. It's also
537 a hookclient, and will release its hooks when it's destroyed.
541 * closehook(): called when the window is closed.
545 """Initialization function. Note that it's not called __init__!"""
546 me.closehook = HookList()
547 HookClient.__init__(me)
548 me.connect('destroy', invoker(me.close))
551 """Close the window, invoking the closehook and releasing all hooks."""
556 class MyWindow (MyWindowMixin):
557 """A version of MyWindowMixin suitable as a single parent class."""
558 def __init__(me, kind = G.WINDOW_TOPLEVEL):
559 G.Window.__init__(me, kind)
562 class TrivialWindowMixin (MyWindowMixin):
563 """A simple window which you can close with Escape."""
565 super(TrivialWindowMixin, me).mywininit()
566 me.connect('key-press-event', me._keypress)
567 def _keypress(me, _, ev):
568 if ev.keyval == GDK.KEY_Escape: me.destroy()
570 class TrivialWindow (MyWindow, TrivialWindowMixin):
573 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
574 """A dialogue box with a closehook and sensible button binding."""
576 def __init__(me, title = None, flags = 0, buttons = []):
578 The BUTTONS are a list of (STOCKID, THUNK) pairs: call the appropriate
579 THUNK when the button is pressed. The other arguments are just like
590 G.Dialog.__init__(me, title, None, flags, tuple(br))
592 me.set_default_response(i - 1)
593 me.connect('response', me.respond)
595 def respond(me, hunoz, rid, *hukairz):
596 """Dispatch responses to the appropriate thunks."""
597 if rid >= 0: me.rmap[rid]()
599 def makeactiongroup(name, acts):
601 Creates an ActionGroup called NAME.
603 ACTS is a list of tuples containing:
605 * ACT: an action name
606 * LABEL: the label string for the action
607 * ACCEL: accelerator string, or None
608 * FUNC: thunk to call when the action is invoked
610 actgroup = G.ActionGroup(name)
611 for act, label, accel, func in acts:
612 a = G.Action(act, label, None, None)
613 if func: a.connect('activate', invoker(func))
614 actgroup.add_action_with_accel(a, accel)
617 class GridPacker (G.Table):
619 Like a Table, but with more state: makes filling in the widgets easier.
623 """Initialize a new GridPacker."""
629 me.set_border_width(4)
630 me.set_col_spacings(4)
631 me.set_row_spacings(4)
633 def pack(me, w, width = 1, newlinep = False,
634 xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
639 W is the widget to add. XOPY, YOPT, XPAD and YPAD are as for Table.
640 WIDTH is how many cells to take up horizontally. NEWLINEP is whether to
641 start a new line for this widget. Returns W.
647 right = me.col + width
648 if bot > me.rows or right > me.cols:
649 if bot > me.rows: me.rows = bot
650 if right > me.cols: me.cols = right
651 me.resize(me.rows, me.cols)
652 me.attach(w, me.col, me.col + width, me.row, me.row + 1,
653 xopt, yopt, xpad, ypad)
657 def labelled(me, lab, w, newlinep = False, **kw):
659 Packs a labelled widget.
661 Other arguments are as for pack. Returns W.
663 label = G.Label(lab + ' ')
664 label.set_alignment(1.0, 0)
665 me.pack(label, newlinep = newlinep, xopt = G.FILL)
669 def info(me, label, text = None, len = 18, **kw):
671 Packs an information widget with a label.
673 LABEL is the label; TEXT is the initial text; LEN is the estimated length
674 in characters. Returns the entry widget.
677 if text is not None: e.set_text(text)
678 e.set_width_chars(len)
679 e.set_selectable(True)
680 e.set_alignment(0.0, 0.5)
681 me.labelled(label, e, **kw)
684 class WindowSlot (HookClient):
686 A place to store a window -- specificially a MyWindowMixin.
688 If the window is destroyed, remember this; when we come to open the window,
689 raise it if it already exists; otherwise make a new one.
691 def __init__(me, createfunc):
693 Constructor: CREATEFUNC must return a new Window which supports the
696 HookClient.__init__(me)
697 me.createfunc = createfunc
701 """Opens the window, creating it if necessary."""
703 raise_window(me.window)
705 me.window = me.createfunc()
706 me.hook(me.window.closehook, me.closed)
709 """Handles the window being closed."""
710 me.unhook(me.window.closehook)
713 class MyTreeView (G.TreeView):
714 def __init__(me, model):
715 G.TreeView.__init__(me, model)
716 me.set_rules_hint(True)
718 class MyScrolledWindow (G.ScrolledWindow):
720 G.ScrolledWindow.__init__(me)
721 me.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
722 me.set_shadow_type(G.SHADOW_IN)
724 ## Matches a signed integer.
725 rx_num = RX.compile(r'^[-+]?\d+$')
728 c_red = GDK.color_parse('#ff6666')
730 class ValidationError (Exception):
731 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
734 class ValidatingEntry (G.Entry):
736 Like an Entry, but makes the text go red if the contents are invalid.
738 If get_text is called, and the text is invalid, ValidationError is raised.
739 The attribute validp reflects whether the contents are currently valid.
742 def __init__(me, valid, text = '', size = -1, *arg, **kw):
744 Make a validating Entry.
746 VALID is a regular expression or a predicate on strings. TEXT is the
747 default text to insert. SIZE is the size of the box to set, in
748 characters (ish). Other arguments are passed to Entry.
750 G.Entry.__init__(me, *arg, **kw)
751 me.connect("changed", me._check)
752 me.connect("state-changed", me._check)
756 me.validate = RX.compile(valid).match
758 if size != -1: me.set_width_chars(size)
759 me.set_activates_default(True)
763 def _check(me, *hunoz):
764 """Check the current text and update validp and the text colour."""
765 if me.validate(G.Entry.get_text(me)):
767 set_entry_bg(me, None)
770 set_entry_bg(me, me.is_sensitive() and c_red or None)
774 Return the text in the Entry if it's valid. If it isn't, raise
778 raise ValidationError()
779 return G.Entry.get_text(me)
781 def numericvalidate(min = None, max = None):
783 Return a validation function for numbers.
785 Entry must consist of an optional sign followed by digits, and the
786 resulting integer must be within the given bounds.
788 return lambda x: (rx_num.match(x) and
789 (min is None or long(x) >= min) and
790 (max is None or long(x) <= max))
792 ###--------------------------------------------------------------------------
793 ### Various minor dialog boxen.
796 TrIPE is free software: you can redistribute it and/or modify it under
797 the terms of the GNU General Public License as published by the Free
798 Software Foundation; either version 3 of the License, or (at your
799 option) any later version.
801 TrIPE is distributed in the hope that it will be useful, but WITHOUT
802 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
803 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
806 You should have received a copy of the GNU General Public License
807 along with TrIPE. If not, see <https://www.gnu.org/licenses/>."""
809 class AboutBox (G.AboutDialog, TrivialWindowMixin):
810 """The program `About' box."""
812 G.AboutDialog.__init__(me)
814 me.set_name('TrIPEmon')
815 me.set_version(T.VERSION)
817 me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
818 me.set_comments('A graphical monitor for the TrIPE VPN server')
819 me.set_copyright('Copyright © 2006-2008 Straylight/Edgeware')
820 me.connect('response', me.respond)
822 def respond(me, hunoz, rid, *hukairz):
823 if rid == G.RESPONSE_CANCEL:
825 aboutbox = WindowSlot(AboutBox)
828 """Report an error message in a window."""
829 d = G.Dialog('Error from %s' % M.quis,
830 flags = G.DIALOG_MODAL,
831 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
833 label.set_padding(20, 20)
834 d.vbox.pack_start(label, True, True, 0)
839 def unimplemented(*hunoz):
840 """Indicator of laziness."""
841 moanbox("I've not written that bit yet.")
843 ###--------------------------------------------------------------------------
846 class LogModel (G.ListStore):
848 A simple list of log messages, usable as the model for a TreeView.
850 The column headings are stored in the `cols' attribute.
853 def __init__(me, columns):
855 COLUMNS must be a list of column name strings. We add a time column to
858 me.cols = ('Time',) + columns
859 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
861 def add(me, *entries):
863 Adds a new log message, with a timestamp.
865 The ENTRIES are the contents for the list columns.
867 now = TIME.strftime('%Y-%m-%d %H:%M:%S')
868 me.append((now, ) + entries)
870 class TraceLogModel (LogModel):
871 """Log model for trace messages."""
873 LogModel.__init__(me, ('Message',))
874 def notify(me, line):
875 """Call with a new trace message."""
878 class WarningLogModel (LogModel):
880 Log model for warnings.
882 We split the category out into a separate column.
885 LogModel.__init__(me, ('Category', 'Message'))
886 def notify(me, tag, *rest):
887 """Call with a new warning message."""
888 me.add(tag, ' '.join([T.quotify(w) for w in rest]))
890 class LogViewer (TrivialWindow):
894 Its contents are a TreeView showing the log.
898 * model: an appropriate LogModel
899 * list: a TreeView widget to display the log
902 def __init__(me, model):
904 Create a log viewer showing the LogModel MODEL.
906 TrivialWindow.__init__(me)
908 scr = MyScrolledWindow()
909 me.list = MyTreeView(me.model)
911 for c in me.model.cols:
912 crt = G.CellRendererText()
913 me.list.append_column(G.TreeViewColumn(c, crt, text = i))
915 crt.set_property('family', 'monospace')
916 me.set_default_size(440, 256)
921 ###--------------------------------------------------------------------------
924 class pingstate (struct):
926 Information kept for each peer by the Pinger.
928 Important attributes:
930 * peer = the peer name
931 * command = PING or EPING
932 * n = how many pings we've sent so far
933 * ngood = how many returned
934 * nmiss = how many didn't return
935 * nmissrun = how many pings since the last good one
936 * tlast = round-trip time for the last (good) ping
937 * ttot = total roung trip time
941 class Pinger (T.Coroutine, HookClient):
943 Coroutine which pings known peers and collects statistics.
945 Interesting attributes:
947 * _map: dict mapping peer names to Peer objects
948 * _q: event queue for notifying pinger coroutine
949 * _timer: gobject timer for waking the coroutine
954 Initialize the pinger.
956 We watch the monitor's PeerList to track which peers we should ping. We
957 maintain an event queue and put all the events on that.
959 The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
960 where CMD is 'PING' or 'EPING'.
962 T.Coroutine.__init__(me)
963 HookClient.__init__(me)
967 me.hook(conn.connecthook, me._connected)
968 me.hook(conn.disconnecthook, me._disconnected)
969 me.hook(monitor.peers.addhook,
970 lambda p: T.defer(me._q.put, (p, 'ADD', None)))
971 me.hook(monitor.peers.delhook,
972 lambda p: T.defer(me._q.put, (p, 'KILL', None)))
973 if conn.connectedp(): me.connected()
976 """Respond to connection: start pinging thngs."""
977 me._timer = GL.timeout_add(1000, me._timerfunc)
980 """Timer function: put a timer event on the queue."""
981 me._q.put((None, 'TIMER', None))
984 def _disconnected(me, reason):
985 """Respond to disconnection: stop pinging."""
986 GL.source_remove(me._timer)
990 Coroutine function: read events from the queue and process them.
994 * (PEER, 'KILL', None): remove PEER from the interesting peers list
995 * (PEER, 'ADD', None): add PEER to the list
996 * (PEER, 'INFO', TOKENS): result from a PING command
997 * (None, 'TIMER', None): interval timer went off: send more pings
1000 tag, code, stuff = me._q.get()
1005 elif not conn.connectedp():
1010 for cmd in 'PING', 'EPING':
1011 ps = pingstate(command = cmd, peer = p,
1012 n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1013 tlast = 0, ttot = 0)
1016 elif code == 'INFO':
1018 if stuff[0] == 'ping-ok':
1028 ps.peer.pinghook.run(ps.peer, ps.command, ps)
1029 elif code == 'TIMER':
1030 for name, p in me._map.iteritems():
1031 for cmd, ps in p.ping.iteritems():
1032 conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
1033 cmd, '-background', conn.bgtag(), '--', name]))
1035 ###--------------------------------------------------------------------------
1036 ### Random dialogue boxes.
1038 class AddPeerDialog (MyDialog):
1040 Let the user create a new peer the low-level way.
1042 Interesting attributes:
1044 * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
1048 """Initialize the dialogue."""
1049 MyDialog.__init__(me, 'Add peer',
1050 buttons = [(G.STOCK_CANCEL, me.destroy),
1051 (G.STOCK_OK, me.ok)])
1056 """Coroutine function: background setup for AddPeerDialog."""
1057 table = GridPacker()
1058 me.vbox.pack_start(table, True, True, 0)
1059 me.e_name = table.labelled('Name',
1060 ValidatingEntry(r'^[^\s.:]+$', '', 16),
1062 me.e_addr = table.labelled('Address',
1063 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
1065 me.e_port = table.labelled('Port',
1066 ValidatingEntry(numericvalidate(0, 65535),
1069 me.l_tunnel = table.labelled('Tunnel', combo_box_text(),
1070 newlinep = True, width = 3)
1071 me.tuns = ['(Default)'] + conn.tunnels()
1073 me.l_tunnel.append_text(t)
1074 me.l_tunnel.set_active(0)
1076 def tickybox_sensitivity(tickybox, target):
1077 tickybox.connect('toggled',
1078 lambda t: target.set_sensitive (t.get_active()))
1080 def optional_entry(label, rx_valid, width):
1081 c = G.CheckButton(label)
1082 table.pack(c, newlinep = True, xopt = G.FILL)
1083 e = ValidatingEntry(rx_valid, '', width)
1084 e.set_sensitive(False)
1085 tickybox_sensitivity(c, e)
1086 table.pack(e, width = 3)
1089 me.c_keepalive, me.e_keepalive = \
1090 optional_entry('Keepalives', r'^\d+[hms]?$', 5)
1092 me.c_cork = G.CheckButton('Cork')
1093 table.pack(me.c_cork, newlinep = True, width = 4, xopt = G.FILL)
1095 me.c_mobile = G.CheckButton('Mobile')
1096 table.pack(me.c_mobile, newlinep = True, width = 4, xopt = G.FILL)
1098 me.c_peerkey, me.e_peerkey = \
1099 optional_entry('Peer key tag', r'^[^.:\s]+$', 16)
1100 me.c_privkey, me.e_privkey = \
1101 optional_entry('Private key tag', r'^[^.:\s]+$', 16)
1106 """Handle an OK press: create the peer."""
1108 t = me.l_tunnel.get_active()
1109 me._addpeer(me.e_name.get_text(),
1110 me.e_addr.get_text(),
1111 me.e_port.get_text(),
1112 keepalive = (me.c_keepalive.get_active() and
1113 me.e_keepalive.get_text() or None),
1114 tunnel = t and me.tuns[t] or None,
1115 cork = me.c_cork.get_active() or None,
1116 mobile = me.c_mobile.get_active() or None,
1117 key = (me.c_peerkey.get_active() and
1118 me.e_peerkey.get_text() or None),
1119 priv = (me.c_privkey.get_active() and
1120 me.e_privkey.get_text() or None))
1121 except ValidationError:
1126 def _addpeer(me, *args, **kw):
1127 """Coroutine function: actually do the ADD command."""
1129 conn.add(*args, **kw)
1131 except T.TripeError, exc:
1132 T.defer(moanbox, ' '.join(exc))
1134 class ServInfo (TrivialWindow):
1136 Show information about the server and available services.
1138 Interesting attributes:
1140 * e: maps SERVINFO keys to entry widgets
1141 * svcs: Gtk ListStore describing services (columns are name and version)
1145 TrivialWindow.__init__(me)
1146 me.set_title('TrIPE server info')
1147 table = GridPacker()
1150 def add(label, tag, text = None, **kw):
1151 me.e[tag] = table.info(label, text, **kw)
1152 add('Implementation', 'implementation')
1153 add('Version', 'version', newlinep = True)
1154 me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1155 me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1156 scr = MyScrolledWindow()
1157 lb = MyTreeView(me.svcs)
1159 for title in 'Service', 'Version':
1160 lb.append_column(G.TreeViewColumn(
1161 title, G.CellRendererText(), text = i))
1163 for svc in monitor.services:
1164 me.svcs.append([svc.name, svc.version])
1166 table.pack(scr, width = 2, newlinep = True,
1167 yopt = G.EXPAND | G.FILL | G.SHRINK)
1169 me.hook(conn.connecthook, me.update)
1170 me.hook(monitor.services.addhook, me.addsvc)
1171 me.hook(monitor.services.delhook, me.delsvc)
1174 def addsvc(me, svc):
1175 me.svcs.append([svc.name, svc.version])
1177 def delsvc(me, svc):
1178 for i in xrange(len(me.svcs)):
1179 if me.svcs[i][0] == svc.name:
1180 me.svcs.remove(me.svcs.get_iter(i))
1184 info = conn.servinfo()
1186 me.e[i].set_text(info[i])
1188 class TraceOptions (MyDialog):
1189 """Tracing options window."""
1191 MyDialog.__init__(me, title = 'Tracing options',
1192 buttons = [(G.STOCK_CLOSE, me.destroy),
1193 (G.STOCK_OK, cr(me.ok))])
1199 for ch, st, desc in conn.trace():
1200 if ch.isupper(): continue
1201 text = desc[0].upper() + desc[1:]
1202 ticky = G.CheckButton(text)
1203 ticky.set_active(st == '+')
1204 me.vbox.pack_start(ticky, True, True, 0)
1205 me.opts.append((ch, ticky))
1210 for ch, ticky in me.opts:
1211 if ticky.get_active():
1215 setting = ''.join(on) + '-' + ''.join(off)
1219 ###--------------------------------------------------------------------------
1223 """Translate a TrIPE-format time to something human-readable."""
1224 if t == 'NEVER': return '(never)'
1225 YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1226 ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1227 ago = MATH.floor(ago); unit = 's'
1228 for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1232 return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1233 (YY, MM, DD, hh, mm, ss, ago, unit)
1235 """Translate a raw byte count into something a human might want to read."""
1242 return '%d %s' % (b, suff)
1244 ## How to translate peer stats. Maps the stat name to a translation
1247 [('start-time', xlate_time),
1248 ('last-packet-time', xlate_time),
1249 ('last-keyexch-time', xlate_time),
1250 ('bytes-in', xlate_bytes),
1251 ('bytes-out', xlate_bytes),
1252 ('keyexch-bytes-in', xlate_bytes),
1253 ('keyexch-bytes-out', xlate_bytes),
1254 ('ip-bytes-in', xlate_bytes),
1255 ('ip-bytes-out', xlate_bytes)]
1257 def format_stat(format, dict):
1258 if callable(format): return format(dict)
1259 else: return format % dict
1261 ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
1262 ## the label to give the entry box; FORMAT is the format string to write into
1265 [('Diffie-Hellman group',
1267 '(%(kx-group-order-bits)s-bit order, '
1268 '%(kx-group-elt-bits)s-bit elements)'),
1269 ('Bulk crypto transform',
1270 '%(bulk-transform)s (%(bulk-overhead)s byte overhead)'),
1271 ('Data encryption', lambda d: '%s (%s; %s)' % (
1273 '%d-bit key' % (8*int(d['cipher-keysz'])),
1274 d.get('cipher-blksz', '0') == '0'
1276 or '%d-bit block' % (8*int(d['cipher-blksz'])))),
1277 ('Message authentication', lambda d: '%s (%s; %s)' % (
1279 d.get('mac-keysz') is None
1281 or '%d-bit key' % (8*int(d['mac-keysz'])),
1282 '%d-bit tag' % (8*int(d['mac-tagsz'])))),
1283 ('Hash', lambda d: '%s (%d-bit output)' %
1284 (d['hash'], 8*int(d['hash-sz'])))]
1287 [('Start time', '%(start-time)s'),
1288 ('Private key', '%(current-key)s')] + \
1290 [('Last key-exchange', '%(last-keyexch-time)s'),
1291 ('Last packet', '%(last-packet-time)s'),
1293 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1294 ('Key-exchange in/out',
1295 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1297 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1298 ('Rejected packets', '%(rejected-packets)s')]
1300 class PeerWindow (TrivialWindow):
1302 Show information about a peer.
1304 This gives a graphical view of the server's peer statistics.
1306 Interesting attributes:
1308 * e: dict mapping keys (mostly matching label widget texts, though pings
1309 use command names) to entry widgets so that we can update them easily
1310 * peer: the peer this window shows information about
1311 * cr: the info-fetching coroutine, or None if crrrently disconnected
1312 * doupate: whether the info-fetching corouting should continue running
1315 def __init__(me, peer):
1316 """Construct a PeerWindow, showing information about PEER."""
1318 TrivialWindow.__init__(me)
1319 me.set_title('TrIPE statistics: %s' % peer.name)
1322 table = GridPacker()
1325 ## Utility for adding fields.
1327 def add(label, text = None, key = None):
1328 if key is None: key = label
1329 me.e[key] = table.info(label, text, len = 42, newlinep = True)
1331 ## Build the dialogue box.
1332 add('Peer name', peer.name)
1333 add('Tunnel', peer.tunnel)
1334 add('Interface', peer.ifname)
1336 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1337 add('Address', peer.addr)
1338 add('Transport pings', key = 'PING')
1339 add('Encrypted pings', key = 'EPING')
1341 for label, format in statslayout:
1344 ## Hook onto various interesting events.
1345 me.hook(conn.connecthook, me.tryupdate)
1346 me.hook(conn.disconnecthook, me.stopupdate)
1347 me.hook(me.closehook, me.stopupdate)
1348 me.hook(me.peer.deadhook, me.dead)
1349 me.hook(me.peer.changehook, me.change)
1350 me.hook(me.peer.pinghook, me.ping)
1355 ## Format the ping statistics.
1356 for cmd, ps in me.peer.ping.iteritems():
1357 me.ping(me.peer, cmd, ps)
1359 ## And show the window.
1363 """Update the display in response to a notification."""
1364 me.e['Interface'].set_text(me.peer.ifname)
1365 me.e['Address'].set_text(me.peer.addr)
1369 Main display-updating coroutine.
1371 This does an update, sleeps for a while, and starts again. If the
1372 me.doupdate flag goes low, we stop the loop.
1374 while me.peer.alivep and conn.connectedp() and me.doupdate:
1375 stat = conn.stats(me.peer.name)
1376 for s, trans in statsxlate:
1377 stat[s] = trans(stat[s])
1378 stat.update(me.peer.__dict__)
1379 for label, format in statslayout:
1380 me.e[label].set_text(format_stat(format, stat))
1381 GL.timeout_add(1000, lambda: me.cr.switch() and False)
1382 me.cr.parent.switch()
1386 """Start the updater coroutine, if it's not going already."""
1388 me.cr = T.Coroutine(me._update,
1389 name = 'update-peer-window %s' % me.peer.name)
1392 def stopupdate(me, *hunoz, **hukairz):
1393 """Stop the update coroutine, by setting me.doupdate."""
1397 """Called when the peer is killed."""
1398 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1399 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1402 def ping(me, peer, cmd, ps):
1403 """Called when a ping result for the peer is reported."""
1404 s = '%d/%d' % (ps.ngood, ps.n)
1406 s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1408 s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1409 me.e[ps.command].set_text(s)
1411 ###--------------------------------------------------------------------------
1412 ### Cryptographic status.
1414 class CryptoInfo (TrivialWindow):
1415 """Simple display of cryptographic algorithms in use."""
1417 TrivialWindow.__init__(me)
1418 me.set_title('Cryptographic algorithms')
1419 T.aside(me.populate)
1421 table = GridPacker()
1424 crypto = conn.algs()
1426 for label, format in cryptolayout:
1427 table.info(label, format_stat(format, crypto),
1428 len = 42, newlinep = not firstp)
1433 ###--------------------------------------------------------------------------
1434 ### Main monitor window.
1436 class MonitorWindow (MyWindow):
1439 The main monitor window.
1441 This class creates, populates and maintains the main monitor window.
1445 * warnings, trace: log models for server output
1446 * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1447 WindowSlot objects for ancillary windows
1448 * ui: Gtk UIManager object for the menu system
1449 * apmenu: pair of identical autoconnecting peer menus
1450 * listmodel: Gtk ListStore for connected peers; contains peer name,
1451 address, and ping times (transport and encrypted, value and colour)
1452 * status: Gtk Statusbar at the bottom of the window
1453 * _kidding: an unpleasant backchannel between the apchange method (which
1454 builds the apmenus) and the menu handler, forced on us by a Gtk
1457 Also installs attributes on Peer objects:
1459 * i: index of peer's entry in listmodel
1460 * win: WindowSlot object for the peer's PeerWindow
1464 """Construct the window."""
1467 MyWindow.__init__(me)
1468 me.set_title('TrIPE monitor')
1470 ## Hook onto diagnostic outputs.
1471 me.warnings = WarningLogModel()
1472 me.hook(conn.warnhook, me.warnings.notify)
1473 me.trace = TraceLogModel()
1474 me.hook(conn.tracehook, me.trace.notify)
1476 ## Make slots to store the various ancillary singleton windows.
1477 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1478 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1479 me.traceopts = WindowSlot(lambda: TraceOptions())
1480 me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1481 me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1482 me.servinfo = WindowSlot(lambda: ServInfo())
1484 ## Main window structure.
1488 ## UI manager makes our menus. (We're too cheap to have a toolbar.)
1489 me.ui = G.UIManager()
1490 actgroup = makeactiongroup('monitor',
1491 [('file-menu', '_File', None, None),
1492 ('connect', '_Connect', '<Control>C', conn.connect),
1493 ('disconnect', '_Disconnect', '<Control>D',
1494 lambda: conn.disconnect(None)),
1495 ('quit', '_Quit', '<Control>Q', me.close),
1496 ('server-menu', '_Server', None, None),
1497 ('daemon', 'Run in _background', None, cr(conn.daemon)),
1498 ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1499 ('crypto-algs', 'Cryptographic algorithms',
1500 '<Control>Y', me.cryptoinfo.open),
1501 ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1502 ('server-quit', 'Terminate server', None, cr(conn.quit)),
1503 ('conn-peer', 'Connect peer', None, None),
1504 ('logs-menu', '_Logs', None, None),
1505 ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1506 ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1507 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1508 ('help-menu', '_Help', None, None),
1509 ('about', '_About tripemon...', None, aboutbox.open),
1510 ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1511 ('kill-peer', '_Kill peer', None, me.killpeer),
1512 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1518 <menu action="file-menu">
1519 <menuitem action="quit"/>
1521 <menu action="server-menu">
1522 <menuitem action="connect"/>
1523 <menuitem action="disconnect"/>
1525 <menuitem action="server-version"/>
1526 <menuitem action="crypto-algs"/>
1527 <menuitem action="add-peer"/>
1528 <menuitem action="conn-peer"/>
1529 <menuitem action="daemon"/>
1530 <menuitem action="reload-keys"/>
1532 <menuitem action="server-quit"/>
1534 <menu action="logs-menu">
1535 <menuitem action="show-warnings"/>
1536 <menuitem action="show-trace"/>
1537 <menuitem action="trace-options"/>
1539 <menu action="help-menu">
1540 <menuitem action="about"/>
1543 <popup name="peer-popup">
1544 <menuitem action="add-peer"/>
1545 <menuitem action="conn-peer"/>
1546 <menuitem action="kill-peer"/>
1547 <menuitem action="force-kx"/>
1552 ## Populate the UI manager.
1553 me.ui.insert_action_group(actgroup, 0)
1554 me.ui.add_ui_from_string(uidef)
1556 ## Construct the menu bar.
1557 vbox.pack_start(me.ui.get_widget('/menubar'), False, True, 0)
1558 me.add_accel_group(me.ui.get_accel_group())
1560 ## Construct and attach the auto-peers menu. (This is a horrible bodge
1561 ## because we can't attach the same submenu in two different places.)
1562 me.apmenu = G.Menu(), G.Menu()
1563 me.ui.get_widget('/menubar/server-menu/conn-peer') \
1564 .set_submenu(me.apmenu[0])
1565 me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1567 ## Construct the main list model, and listen on hooks which report
1568 ## changes to the available peers.
1569 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1570 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1571 me.hook(monitor.peers.addhook, me.addpeer)
1572 me.hook(monitor.peers.delhook, me.delpeer)
1573 me.hook(monitor.autopeershook, me.apchange)
1575 ## Construct the list viewer and put it in a scrolling window.
1576 scr = MyScrolledWindow()
1577 me.list = MyTreeView(me.listmodel)
1578 me.list.append_column(G.TreeViewColumn('Peer name',
1579 G.CellRendererText(),
1581 me.list.append_column(G.TreeViewColumn('Address',
1582 G.CellRendererText(),
1584 me.list.append_column(G.TreeViewColumn('T-ping',
1585 G.CellRendererText(),
1588 me.list.append_column(G.TreeViewColumn('E-ping',
1589 G.CellRendererText(),
1592 me.list.get_column(1).set_expand(True)
1593 me.list.connect('row-activated', me.activate)
1594 me.list.connect('button-press-event', me.buttonpress)
1595 me.list.set_reorderable(True)
1596 me.list.get_selection().set_mode(G.SELECTION_NONE)
1598 vbox.pack_start(scr, True, True, 0)
1600 ## Construct the status bar, and listen on hooks which report changes to
1601 ## connection status.
1602 me.status = G.Statusbar()
1603 vbox.pack_start(me.status, False, True, 0)
1604 me.hook(conn.connecthook, cr(me.connected))
1605 me.hook(conn.disconnecthook, me.disconnected)
1606 me.hook(conn.notehook, me.notify)
1608 ## Set a plausible default window size.
1609 me.set_default_size(512, 180)
1611 def addpeer(me, peer):
1612 """Hook: announces that PEER has been added."""
1613 peer.i = me.listmodel.append([peer.name, peer.addr,
1614 '???', 'green', '???', 'green'])
1615 peer.win = WindowSlot(lambda: PeerWindow(peer))
1616 me.hook(peer.pinghook, me._ping)
1617 me.hook(peer.changehook, lambda: me._change(peer))
1620 def delpeer(me, peer):
1621 """Hook: announces that PEER has been removed."""
1622 me.listmodel.remove(peer.i)
1623 me.unhook(peer.pinghook)
1626 def path_peer(me, path):
1627 """Return the peer corresponding to a given list-model PATH."""
1628 return monitor.peers[me.listmodel[path][0]]
1632 Hook: announces that a change has been made to the peers available for
1633 automated connection.
1635 This populates both auto-peer menus and keeps them in sync. (As
1636 mentioned above, we can't attach the same submenu to two separate parent
1637 menu items. So we end up with two identical menus instead. Yes, this
1641 ## The set_active method of a CheckMenuItem works by maybe activating the
1642 ## menu item. This signals our handler. But we don't actually want to
1643 ## signal the handler unless the user actually frobbed the item. So the
1644 ## _kidding flag is used as an underhanded way of telling the handler
1645 ## that we don't actually want it to do anything. Of course, this sucks
1649 ## Iterate over the two menus.
1652 existing = menu.get_children()
1653 if monitor.autopeers is None:
1655 ## No peers, so empty out the menu.
1656 for item in existing:
1661 ## Insert the new items into the menu. (XXX this seems buggy XXX)
1662 ## Tick the peers which are actually connected.
1664 for peer in monitor.autopeers:
1665 if j < len(existing) and \
1666 existing[j].get_child().get_text() == peer:
1670 item = G.CheckMenuItem(peer, use_underline = False)
1671 item.connect('activate', invoker(me._addautopeer, peer))
1672 menu.insert(item, i)
1673 item.set_active(peer in monitor.peers.table)
1676 ## Make all the menu items visible.
1679 ## Set the parent menu items sensitive if and only if there are any peers
1681 for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1682 me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1684 ## And now allow the handler to do its business normally.
1687 def _addautopeer(me, peer):
1689 Automatically connect an auto-peer.
1691 This method is invoked from the main coroutine. Since the actual
1692 connection needs to issue administration commands, we must spawn a new
1693 child coroutine for it.
1697 T.Coroutine(me._addautopeer_hack,
1698 name = '_addautopeerhack %s' % peer).switch(peer)
1700 def _addautopeer_hack(me, peer):
1701 """Make an automated connection to PEER in response to a user click."""
1705 T._simple(conn.svcsubmit('connect', 'active', peer))
1706 except T.TripeError, exc:
1707 T.defer(moanbox, ' '.join(exc.args))
1710 def activate(me, l, path, col):
1712 Handle a double-click on a peer in the main list: open a PeerInfo window.
1714 peer = me.path_peer(path)
1717 def buttonpress(me, l, ev):
1719 Handle a mouse click on the main list.
1721 Currently we're only interested in button-3, which pops up the peer menu.
1722 For future reference, we stash the peer that was clicked in me.menupeer.
1725 x, y = int(ev.x), int(ev.y)
1726 r = me.list.get_path_at_pos(x, y)
1727 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1728 me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1730 me.ui.get_widget('/peer-popup/conn-peer'). \
1731 set_sensitive(bool(monitor.autopeers))
1733 me.menupeer = me.path_peer(r[0])
1736 me.ui.get_widget('/peer-popup').popup(
1737 None, None, None, ev.button, ev.time)
1740 """Kill a peer from the popup menu."""
1741 cr(conn.kill, me.menupeer.name)()
1744 """Kickstart a key-exchange from the popup menu."""
1745 cr(conn.forcekx, me.menupeer.name)()
1747 _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1748 def _ping(me, p, cmd, ps):
1749 """Hook: responds to ping reports."""
1750 textcol, colourcol = me._columnmap[cmd]
1752 me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1753 me.listmodel[p.i][colourcol] = 'red'
1755 me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1756 me.listmodel[p.i][colourcol] = 'black'
1759 """Hook: notified when the peer changes state."""
1760 me.listmodel[p.i][1] = p.addr
1762 def setstatus(me, status):
1763 """Update the message in the status bar."""
1765 me.status.push(0, status)
1767 def notify(me, note, *rest):
1768 """Hook: invoked when interesting notifications occur."""
1769 if note == 'DAEMON':
1770 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1774 Hook: invoked when a connection is made to the server.
1776 Make options which require a server connection sensitive.
1778 me.setstatus('Connected (port %s)' % conn.port())
1779 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1780 for i in ('/menubar/server-menu/disconnect',
1781 '/menubar/server-menu/server-version',
1782 '/menubar/server-menu/add-peer',
1783 '/menubar/server-menu/server-quit',
1784 '/menubar/logs-menu/trace-options'):
1785 me.ui.get_widget(i).set_sensitive(True)
1786 me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1787 set_sensitive(bool(monitor.autopeers))
1788 me.ui.get_widget('/menubar/server-menu/daemon'). \
1789 set_sensitive(conn.servinfo()['daemon'] == 'nil')
1791 def disconnected(me, reason):
1793 Hook: invoked when the connection to the server is lost.
1795 Make most options insensitive.
1797 me.setstatus('Disconnected')
1798 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1799 for i in ('/menubar/server-menu/disconnect',
1800 '/menubar/server-menu/server-version',
1801 '/menubar/server-menu/add-peer',
1802 '/menubar/server-menu/conn-peer',
1803 '/menubar/server-menu/daemon',
1804 '/menubar/server-menu/server-quit',
1805 '/menubar/logs-menu/trace-options'):
1806 me.ui.get_widget(i).set_sensitive(False)
1807 if reason: moanbox(reason)
1809 ###--------------------------------------------------------------------------
1812 def parse_options():
1814 Parse command-line options.
1816 Process the boring ones. Return all of them, for later.
1818 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1819 version = '%prog (tripe version 1.0.0)')
1820 op.add_option('-a', '--admin-socket',
1821 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1822 help = 'Select socket to connect to [default %default]')
1823 op.add_option('-d', '--directory',
1824 metavar = 'DIR', dest = 'dir', default = T.configdir,
1825 help = 'Select current diretory [default %default]')
1826 opts, args = op.parse_args()
1827 if args: op.error('no arguments permitted')
1832 """Initialization."""
1834 global conn, monitor, pinger
1836 ## Try to establish a connection.
1837 conn = Connection(opts.tripesock)
1839 ## Make the main interesting coroutines and objects.
1847 root = MonitorWindow()
1852 HookClient().hook(root.closehook, exit)
1855 if __name__ == '__main__':
1856 opts = parse_options()
1860 ###----- That's all, folks --------------------------------------------------