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 GDK.KEY_Escape = G.keysyms.Escape
52 def raise_window(w): w.window.raise_()
53 combo_box_text = G.combo_box_new_text
54 def set_entry_bg(e, c): e.modify_base(G.STATE_NORMAL, c)
56 from gi.repository import GObject as GO, GLib as GL, Gtk as G, Gdk as GDK
57 G.WINDOW_TOPLEVEL = G.WindowType.TOPLEVEL
58 G.EXPAND = G.AttachOptions.EXPAND
59 G.SHRINK = G.AttachOptions.SHRINK
60 G.FILL = G.AttachOptions.FILL
61 G.SORT_ASCENDING = G.SortType.ASCENDING
62 G.POLICY_AUTOMATIC = G.PolicyType.AUTOMATIC
63 G.SHADOW_IN = G.ShadowType.IN
64 G.SELECTION_NONE = G.SelectionMode.NONE
65 G.DIALOG_MODAL = G.DialogFlags.MODAL
66 G.RESPONSE_CANCEL = G.ResponseType.CANCEL
67 G.RESPONSE_NONE = G.ResponseType.NONE
68 def raise_window(w): getattr(w.get_window(), 'raise')()
69 combo_box_text = G.ComboBoxText
70 def set_entry_bg(e, c): e.modify_bg(G.StateType.NORMAL, c)
72 if OS.getenv('TRIPE_DEBUG_MONITOR') is not None:
75 ###--------------------------------------------------------------------------
76 ### Doing things later.
79 """Report an uncaught exception."""
80 excepthook(*exc_info())
84 Return a function which behaves like FUNC, but reports exceptions via
89 return func(*args, **kw)
97 def invoker(func, *args, **kw):
99 Return a function which throws away its arguments and calls
102 If for loops worked by binding rather than assignment then we wouldn't need
105 return lambda *hunoz, **hukairz: xwrap(func)(*args, **kw)
107 def cr(func, *args, **kw):
108 """Return a function which invokes FUNC(*ARGS, **KW) in a coroutine."""
109 name = T.funargstr(func, args, kw)
110 return lambda *hunoz, **hukairz: \
111 T.Coroutine(xwrap(func), name = name).switch(*args, **kw)
114 """Decorator: runs its function in a coroutine of its own."""
115 return lambda *args, **kw: \
116 (T.Coroutine(func, name = T.funargstr(func, args, kw))
117 .switch(*args, **kw))
119 ###--------------------------------------------------------------------------
120 ### Random bits of infrastructure.
122 ## Program name, shorn of extraneous stuff.
127 class HookList (object):
129 Notification hook list.
131 Other objects can add functions onto the hook list. When the hook list is
132 run, the functions are called in the order in which they were registered.
136 """Basic initialization: create the hook list."""
139 def add(me, func, obj):
140 """Add FUNC to the list of hook functions."""
141 me.list.append((obj, func))
144 """Remove hook functions registered with the given OBJ."""
151 def run(me, *args, **kw):
152 """Invoke the hook functions with arguments *ARGS and **KW."""
153 for o, hook in me.list:
154 rc = hook(*args, **kw)
155 if rc is not None: return rc
158 class HookClient (object):
160 Mixin for classes which are clients of hooks.
162 It keeps track of the hooks it's a client of, and has the ability to
163 extricate itself from all of them. This is useful because weak objects
164 don't seem to work well.
167 """Basic initialization."""
170 def hook(me, hk, func):
171 """Add FUNC to the hook list HK."""
176 """Remove myself from the hook list HK."""
181 """Remove myself from all hook lists."""
186 class struct (object):
187 """A very simple dumb data container object."""
188 def __init__(me, **kw):
189 me.__dict__.update(kw)
191 ## Matches ISO date format yyyy-mm-ddThh:mm:ss.
192 rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
194 ###--------------------------------------------------------------------------
197 class GIOWatcher (object):
199 Monitor I/O events using glib.
201 def __init__(me, conn, mc = GL.main_context_default()):
205 def connected(me, sock):
206 me._watch = GL.io_add_watch(sock, GL.IO_IN,
207 lambda *hunoz: me._conn.receive())
208 def disconnected(me):
209 GL.source_remove(me._watch)
212 me._mc.iteration(True)
214 class Connection (T.TripeCommandDispatcher):
216 The main connection to the server.
218 The improvement over the TripeCommandDispatcher is that the Connection
219 provides hooklists for NOTE, WARN and TRACE messages, and for connect and
222 This class knows about the Glib I/O dispatcher system, and plugs into it.
226 * connecthook(): a connection to the server has been established
227 * disconnecthook(): the connection has been dropped
228 * notehook(TOKEN, ...): server issued a notification
229 * warnhook(TOKEN, ...): server issued a warning
230 * tracehook(TOKEN, ...): server issued a trace message
233 def __init__(me, socket):
234 """Create a new Connection."""
235 T.TripeCommandDispatcher.__init__(me, socket)
236 me.connecthook = HookList()
237 me.disconnecthook = HookList()
238 me.notehook = HookList()
239 me.warnhook = HookList()
240 me.tracehook = HookList()
241 me.handler['NOTE'] = lambda _, *rest: me.notehook.run(*rest)
242 me.handler['WARN'] = lambda _, *rest: me.warnhook.run(*rest)
243 me.handler['TRACE'] = lambda _, *rest: me.tracehook.run(*rest)
244 me.iowatch = GIOWatcher(me)
247 """Handles reconnection to the server, and signals the hook."""
248 T.TripeCommandDispatcher.connected(me)
251 def disconnected(me, reason):
252 """Handles disconnection from the server, and signals the hook."""
253 me.disconnecthook.run(reason)
254 T.TripeCommandDispatcher.disconnected(me, reason)
256 ###--------------------------------------------------------------------------
257 ### Watching the peers go by.
259 class MonitorObject (object):
261 An object with hooks it uses to notify others of changes in its state.
262 These are the objects tracked by the MonitorList class.
264 The object has a name, an `aliveness' state indicated by the `alivep' flag,
269 * changehook(): the object has changed its state
270 * deadhook(): the object has been destroyed
272 Subclass responsibilities:
274 * update(INFO): update internal state based on the provided INFO, and run
278 def __init__(me, name):
279 """Initialize the object with the given NAME."""
281 me.deadhook = HookList()
282 me.changehook = HookList()
286 """Mark the object as dead; invoke the deadhook."""
290 class Peer (MonitorObject):
292 An object representing a connected peer.
294 As well as the standard hooks, a peer has a pinghook, which isn't used
295 directly by this class.
299 * pinghook(): invoked by the Pinger (q.v.) when ping statistics change
301 Attributes provided are:
303 * addr = a vaguely human-readable representation of the peer's address
304 * ifname = the peer's interface name
305 * tunnel = the kind of tunnel the peer is using
306 * keepalive = the peer's keepalive interval in seconds
307 * ping['EPING'] and ping['PING'] = pingstate statistics (maintained by
311 def __init__(me, name):
312 """Initialize the object with the given name."""
313 MonitorObject.__init__(me, name)
314 me.pinghook = HookList()
315 me.__dict__.update(conn.algs(name))
318 def update(me, hunoz = None):
319 """Update the peer, fetching information about it from the server."""
320 me._setaddr(conn.addr(me.name))
321 me.ifname = conn.ifname(me.name)
322 me.__dict__.update(conn.peerinfo(me.name))
325 def _setaddr(me, addr):
326 """Set the peer's address."""
327 if addr[0] == 'INET':
328 ipaddr, port = addr[1:]
330 name = S.gethostbyaddr(ipaddr)[0]
331 me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
333 me.addr = 'INET %s:%s' % (ipaddr, port)
335 me.addr = ' '.join(addr)
337 def setaddr(me, addr):
338 """Informs the object of a change to its address to ADDR."""
342 def setifname(me, newname):
343 """Informs the object of a change to its interface name to NEWNAME."""
347 class Service (MonitorObject):
349 Represents a service.
351 Additional attributes are:
353 * version = the service version
355 def __init__(me, name, version):
356 MonitorObject.__init__(me, name)
359 def update(me, version):
360 """Tell the Service that its version has changed to VERSION."""
364 class MonitorList (object):
366 Maintains a collection of MonitorObjects.
368 The MonitorList can be indexed by name to retrieve the individual objects;
369 iteration generates the individual objects. More complicated operations
370 can be done on the `table' dictionary directly.
372 Hooks addhook(OBJ) and delhook(OBJ) are invoked when objects are added or
375 Subclass responsibilities:
377 * list(): return a list of (NAME, INFO) pairs.
379 * make(NAME, INFO): returns a new MonitorObject for the given NAME; INFO
380 is from the output of list().
384 """Initialize a new MonitorList."""
386 me.addhook = HookList()
387 me.delhook = HookList()
391 Refresh the list of objects:
393 We add new object which have appeared, delete ones which have vanished,
394 and update any which persist.
397 for name, stuff in me.list():
400 for name in me.table.copy():
404 def add(me, name, stuff):
406 Add a new object created by make(NAME, STUFF) if it doesn't already
407 exist. If it does, update it.
409 if name not in me.table:
410 obj = me.make(name, stuff)
414 me.table[name].update(stuff)
416 def remove(me, name):
418 Remove the object called NAME from the list.
420 The object becomes dead.
428 def __getitem__(me, name):
429 """Retrieve the object called NAME."""
430 return me.table[name]
433 """Iterate over the objects."""
434 return me.table.itervalues()
436 class PeerList (MonitorList):
437 """The list of the known peers."""
439 return [(name, None) for name in conn.list()]
440 def make(me, name, stuff):
443 class ServiceList (MonitorList):
444 """The list of the registered services."""
446 return conn.svclist()
447 def make(me, name, stuff):
448 return Service(name, stuff)
450 class Monitor (HookClient):
452 The main monitor: keeps track of the changes happening to the server.
454 Exports the peers, services MonitorLists, and a (plain Python) list
455 autopeers of peers which the connect service knows how to start by name.
459 * autopeershook(): invoked when the auto-peers list changes.
462 """Initialize the Monitor."""
463 HookClient.__init__(me)
464 me.peers = PeerList()
465 me.services = ServiceList()
466 me.hook(conn.connecthook, me._connected)
467 me.hook(conn.notehook, me._notify)
468 me.autopeershook = HookList()
472 """Handle a successful connection by starting the setup coroutine."""
477 """Coroutine function: initialize for a new connection."""
481 me._updateautopeers()
483 def _updateautopeers(me):
484 """Update the auto-peers list from the connect service."""
485 if 'connect' in me.services.table:
486 me.autopeers = [' '.join(line)
487 for line in conn.svcsubmit('connect', 'list-active')]
491 me.autopeershook.run()
493 def _notify(me, code, *rest):
495 Handle notifications from the server.
497 ADD, KILL and NEWIFNAME notifications get passed up to the PeerList;
498 SVCCLAIM and SVCRELEASE get passed up to the ServiceList. Finally,
499 peerdb-update notifications from the watch service cause us to refresh
503 T.aside(me.peers.add, rest[0], None)
505 T.aside(me.peers.remove, rest[0])
506 elif code == 'NEWIFNAME':
508 me.peers[rest[0]].setifname(rest[2])
511 elif code == 'NEWADDR':
513 me.peers[rest[0]].setaddr(rest[1:])
516 elif code == 'SVCCLAIM':
517 T.aside(me.services.add, rest[0], rest[1])
518 if rest[0] == 'connect':
519 T.aside(me._updateautopeers)
520 elif code == 'SVCRELEASE':
521 T.aside(me.services.remove, rest[0])
522 if rest[0] == 'connect':
523 T.aside(me._updateautopeers)
526 if rest[0] == 'watch' and \
527 rest[1] == 'peerdb-update':
528 T.aside(me._updateautopeers)
530 ###--------------------------------------------------------------------------
531 ### Window management cruft.
533 class MyWindowMixin (G.Window, HookClient):
535 Mixin for windows which call a closehook when they're destroyed. It's also
536 a hookclient, and will release its hooks when it's destroyed.
540 * closehook(): called when the window is closed.
544 """Initialization function. Note that it's not called __init__!"""
545 me.closehook = HookList()
546 HookClient.__init__(me)
547 me.connect('destroy', invoker(me.close))
550 """Close the window, invoking the closehook and releasing all hooks."""
555 class MyWindow (MyWindowMixin):
556 """A version of MyWindowMixin suitable as a single parent class."""
557 def __init__(me, kind = G.WINDOW_TOPLEVEL):
558 G.Window.__init__(me, kind)
561 class TrivialWindowMixin (MyWindowMixin):
562 """A simple window which you can close with Escape."""
564 super(TrivialWindowMixin, me).mywininit()
565 me.connect('key-press-event', me._keypress)
566 def _keypress(me, _, ev):
567 if ev.keyval == GDK.KEY_Escape: me.destroy()
569 class TrivialWindow (MyWindow, TrivialWindowMixin):
572 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
573 """A dialogue box with a closehook and sensible button binding."""
575 def __init__(me, title = None, flags = 0, buttons = []):
577 The BUTTONS are a list of (STOCKID, THUNK) pairs: call the appropriate
578 THUNK when the button is pressed. The other arguments are just like
589 G.Dialog.__init__(me, title, None, flags, tuple(br))
591 me.set_default_response(i - 1)
592 me.connect('response', me.respond)
594 def respond(me, hunoz, rid, *hukairz):
595 """Dispatch responses to the appropriate thunks."""
596 if rid >= 0: me.rmap[rid]()
598 def makeactiongroup(name, acts):
600 Creates an ActionGroup called NAME.
602 ACTS is a list of tuples containing:
604 * ACT: an action name
605 * LABEL: the label string for the action
606 * ACCEL: accelerator string, or None
607 * FUNC: thunk to call when the action is invoked
609 actgroup = G.ActionGroup(name)
610 for act, label, accel, func in acts:
611 a = G.Action(act, label, None, None)
612 if func: a.connect('activate', invoker(func))
613 actgroup.add_action_with_accel(a, accel)
616 class GridPacker (G.Table):
618 Like a Table, but with more state: makes filling in the widgets easier.
622 """Initialize a new GridPacker."""
628 me.set_border_width(4)
629 me.set_col_spacings(4)
630 me.set_row_spacings(4)
632 def pack(me, w, width = 1, newlinep = False,
633 xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
638 W is the widget to add. XOPY, YOPT, XPAD and YPAD are as for Table.
639 WIDTH is how many cells to take up horizontally. NEWLINEP is whether to
640 start a new line for this widget. Returns W.
646 right = me.col + width
647 if bot > me.rows or right > me.cols:
648 if bot > me.rows: me.rows = bot
649 if right > me.cols: me.cols = right
650 me.resize(me.rows, me.cols)
651 me.attach(w, me.col, me.col + width, me.row, me.row + 1,
652 xopt, yopt, xpad, ypad)
656 def labelled(me, lab, w, newlinep = False, **kw):
658 Packs a labelled widget.
660 Other arguments are as for pack. Returns W.
662 label = G.Label(lab + ' ')
663 label.set_alignment(1.0, 0)
664 me.pack(label, newlinep = newlinep, xopt = G.FILL)
668 def info(me, label, text = None, len = 18, **kw):
670 Packs an information widget with a label.
672 LABEL is the label; TEXT is the initial text; LEN is the estimated length
673 in characters. Returns the entry widget.
676 if text is not None: e.set_text(text)
677 e.set_width_chars(len)
678 e.set_selectable(True)
679 e.set_alignment(0.0, 0.5)
680 me.labelled(label, e, **kw)
683 class WindowSlot (HookClient):
685 A place to store a window -- specificially a MyWindowMixin.
687 If the window is destroyed, remember this; when we come to open the window,
688 raise it if it already exists; otherwise make a new one.
690 def __init__(me, createfunc):
692 Constructor: CREATEFUNC must return a new Window which supports the
695 HookClient.__init__(me)
696 me.createfunc = createfunc
700 """Opens the window, creating it if necessary."""
702 raise_window(me.window)
704 me.window = me.createfunc()
705 me.hook(me.window.closehook, me.closed)
708 """Handles the window being closed."""
709 me.unhook(me.window.closehook)
712 class MyTreeView (G.TreeView):
713 def __init__(me, model):
714 G.TreeView.__init__(me, model)
715 me.set_rules_hint(True)
717 class MyScrolledWindow (G.ScrolledWindow):
719 G.ScrolledWindow.__init__(me)
720 me.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
721 me.set_shadow_type(G.SHADOW_IN)
723 ## Matches a signed integer.
724 rx_num = RX.compile(r'^[-+]?\d+$')
727 c_red = GDK.color_parse('#ff6666')
729 class ValidationError (Exception):
730 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
733 class ValidatingEntry (G.Entry):
735 Like an Entry, but makes the text go red if the contents are invalid.
737 If get_text is called, and the text is invalid, ValidationError is raised.
738 The attribute validp reflects whether the contents are currently valid.
741 def __init__(me, valid, text = '', size = -1, *arg, **kw):
743 Make a validating Entry.
745 VALID is a regular expression or a predicate on strings. TEXT is the
746 default text to insert. SIZE is the size of the box to set, in
747 characters (ish). Other arguments are passed to Entry.
749 G.Entry.__init__(me, *arg, **kw)
750 me.connect("changed", me._check)
751 me.connect("state-changed", me._check)
755 me.validate = RX.compile(valid).match
757 if size != -1: me.set_width_chars(size)
758 me.set_activates_default(True)
762 def _check(me, *hunoz):
763 """Check the current text and update validp and the text colour."""
764 if me.validate(G.Entry.get_text(me)):
766 set_entry_bg(me, None)
769 set_entry_bg(me, me.is_sensitive() and c_red or None)
773 Return the text in the Entry if it's valid. If it isn't, raise
777 raise ValidationError
778 return G.Entry.get_text(me)
780 def numericvalidate(min = None, max = None):
782 Return a validation function for numbers.
784 Entry must consist of an optional sign followed by digits, and the
785 resulting integer must be within the given bounds.
787 return lambda x: (rx_num.match(x) and
788 (min is None or long(x) >= min) and
789 (max is None or long(x) <= max))
791 ###--------------------------------------------------------------------------
792 ### Various minor dialog boxen.
794 GPL = """This program is free software; you can redistribute it and/or modify
795 it under the terms of the GNU General Public License as published by
796 the Free Software Foundation; either version 2 of the License, or
797 (at your option) any later version.
799 This program is distributed in the hope that it will be useful,
800 but WITHOUT ANY WARRANTY; without even the implied warranty of
801 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
802 GNU General Public License for more details.
804 You should have received a copy of the GNU General Public License
805 along with this program; if not, write to the Free Software Foundation,
806 Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
808 class AboutBox (G.AboutDialog, TrivialWindowMixin):
809 """The program `About' box."""
811 G.AboutDialog.__init__(me)
813 me.set_name('TrIPEmon')
814 me.set_version(T.VERSION)
816 me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
817 me.set_comments('A graphical monitor for the TrIPE VPN server')
818 me.set_copyright('Copyright © 2006-2008 Straylight/Edgeware')
819 me.connect('response', me.respond)
821 def respond(me, hunoz, rid, *hukairz):
822 if rid == G.RESPONSE_CANCEL:
824 aboutbox = WindowSlot(AboutBox)
827 """Report an error message in a window."""
828 d = G.Dialog('Error from %s' % M.quis,
829 flags = G.DIALOG_MODAL,
830 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
832 label.set_padding(20, 20)
833 d.vbox.pack_start(label, True, True, 0)
838 def unimplemented(*hunoz):
839 """Indicator of laziness."""
840 moanbox("I've not written that bit yet.")
842 ###--------------------------------------------------------------------------
845 class LogModel (G.ListStore):
847 A simple list of log messages, usable as the model for a TreeView.
849 The column headings are stored in the `cols' attribute.
852 def __init__(me, columns):
854 COLUMNS must be a list of column name strings. We add a time column to
857 me.cols = ('Time',) + columns
858 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
860 def add(me, *entries):
862 Adds a new log message, with a timestamp.
864 The ENTRIES are the contents for the list columns.
866 now = TIME.strftime('%Y-%m-%d %H:%M:%S')
867 me.append((now, ) + entries)
869 class TraceLogModel (LogModel):
870 """Log model for trace messages."""
872 LogModel.__init__(me, ('Message',))
873 def notify(me, line):
874 """Call with a new trace message."""
877 class WarningLogModel (LogModel):
879 Log model for warnings.
881 We split the category out into a separate column.
884 LogModel.__init__(me, ('Category', 'Message'))
885 def notify(me, tag, *rest):
886 """Call with a new warning message."""
887 me.add(tag, ' '.join([T.quotify(w) for w in rest]))
889 class LogViewer (TrivialWindow):
893 Its contents are a TreeView showing the log.
897 * model: an appropriate LogModel
898 * list: a TreeView widget to display the log
901 def __init__(me, model):
903 Create a log viewer showing the LogModel MODEL.
905 TrivialWindow.__init__(me)
907 scr = MyScrolledWindow()
908 me.list = MyTreeView(me.model)
910 for c in me.model.cols:
911 crt = G.CellRendererText()
912 me.list.append_column(G.TreeViewColumn(c, crt, text = i))
914 crt.set_property('family', 'monospace')
915 me.set_default_size(440, 256)
920 ###--------------------------------------------------------------------------
923 class pingstate (struct):
925 Information kept for each peer by the Pinger.
927 Important attributes:
929 * peer = the peer name
930 * command = PING or EPING
931 * n = how many pings we've sent so far
932 * ngood = how many returned
933 * nmiss = how many didn't return
934 * nmissrun = how many pings since the last good one
935 * tlast = round-trip time for the last (good) ping
936 * ttot = total roung trip time
940 class Pinger (T.Coroutine, HookClient):
942 Coroutine which pings known peers and collects statistics.
944 Interesting attributes:
946 * _map: dict mapping peer names to Peer objects
947 * _q: event queue for notifying pinger coroutine
948 * _timer: gobject timer for waking the coroutine
953 Initialize the pinger.
955 We watch the monitor's PeerList to track which peers we should ping. We
956 maintain an event queue and put all the events on that.
958 The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
959 where CMD is 'PING' or 'EPING'.
961 T.Coroutine.__init__(me)
962 HookClient.__init__(me)
966 me.hook(conn.connecthook, me._connected)
967 me.hook(conn.disconnecthook, me._disconnected)
968 me.hook(monitor.peers.addhook,
969 lambda p: T.defer(me._q.put, (p, 'ADD', None)))
970 me.hook(monitor.peers.delhook,
971 lambda p: T.defer(me._q.put, (p, 'KILL', None)))
972 if conn.connectedp(): me.connected()
975 """Respond to connection: start pinging thngs."""
976 me._timer = GL.timeout_add(1000, me._timerfunc)
979 """Timer function: put a timer event on the queue."""
980 me._q.put((None, 'TIMER', None))
983 def _disconnected(me, reason):
984 """Respond to disconnection: stop pinging."""
985 GL.source_remove(me._timer)
989 Coroutine function: read events from the queue and process them.
993 * (PEER, 'KILL', None): remove PEER from the interesting peers list
994 * (PEER, 'ADD', None): add PEER to the list
995 * (PEER, 'INFO', TOKENS): result from a PING command
996 * (None, 'TIMER', None): interval timer went off: send more pings
999 tag, code, stuff = me._q.get()
1004 elif not conn.connectedp():
1009 for cmd in 'PING', 'EPING':
1010 ps = pingstate(command = cmd, peer = p,
1011 n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1012 tlast = 0, ttot = 0)
1015 elif code == 'INFO':
1017 if stuff[0] == 'ping-ok':
1027 ps.peer.pinghook.run(ps.peer, ps.command, ps)
1028 elif code == 'TIMER':
1029 for name, p in me._map.iteritems():
1030 for cmd, ps in p.ping.iteritems():
1031 conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
1032 cmd, '-background', conn.bgtag(), '--', name]))
1034 ###--------------------------------------------------------------------------
1035 ### Random dialogue boxes.
1037 class AddPeerDialog (MyDialog):
1039 Let the user create a new peer the low-level way.
1041 Interesting attributes:
1043 * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
1047 """Initialize the dialogue."""
1048 MyDialog.__init__(me, 'Add peer',
1049 buttons = [(G.STOCK_CANCEL, me.destroy),
1050 (G.STOCK_OK, me.ok)])
1055 """Coroutine function: background setup for AddPeerDialog."""
1056 table = GridPacker()
1057 me.vbox.pack_start(table, True, True, 0)
1058 me.e_name = table.labelled('Name',
1059 ValidatingEntry(r'^[^\s.:]+$', '', 16),
1061 me.e_addr = table.labelled('Address',
1062 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
1064 me.e_port = table.labelled('Port',
1065 ValidatingEntry(numericvalidate(0, 65535),
1068 me.l_tunnel = table.labelled('Tunnel', combo_box_text(),
1069 newlinep = True, width = 3)
1070 me.tuns = conn.tunnels()
1072 me.l_tunnel.append_text(t)
1073 me.l_tunnel.set_active(0)
1075 def tickybox_sensitivity(tickybox, target):
1076 tickybox.connect('toggled',
1077 lambda t: target.set_sensitive (t.get_active()))
1079 me.c_keepalive = G.CheckButton('Keepalives')
1080 table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
1081 me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
1082 me.e_keepalive.set_sensitive(False)
1083 tickybox_sensitivity(me.c_keepalive, me.e_keepalive)
1084 table.pack(me.e_keepalive, width = 3)
1086 me.c_mobile = G.CheckButton('Mobile')
1087 table.pack(me.c_mobile, newlinep = True, width = 4, xopt = G.FILL)
1089 me.c_peerkey = G.CheckButton('Peer key tag')
1090 table.pack(me.c_peerkey, newlinep = True, xopt = G.FILL)
1091 me.e_peerkey = ValidatingEntry(r'^[^.:\s]+$', '', 16)
1092 me.e_peerkey.set_sensitive(False)
1093 tickybox_sensitivity(me.c_peerkey, me.e_peerkey)
1094 table.pack(me.e_peerkey, width = 3)
1096 me.c_privkey = G.CheckButton('Private key tag')
1097 table.pack(me.c_privkey, newlinep = True, xopt = G.FILL)
1098 me.e_privkey = ValidatingEntry(r'^[^.:\s]+$', '', 16)
1099 me.e_privkey.set_sensitive(False)
1100 tickybox_sensitivity(me.c_privkey, me.e_privkey)
1101 table.pack(me.e_privkey, width = 3)
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 key = (me.c_peerkey.get_active() and
1116 me.e_peerkey.get_text() or None),
1117 priv = (me.c_privkey.get_active() and
1118 me.e_privkey.get_text() or None))
1119 except ValidationError:
1124 def _addpeer(me, *args, **kw):
1125 """Coroutine function: actually do the ADD command."""
1127 conn.add(*args, **kw)
1129 except T.TripeError, exc:
1130 T.defer(moanbox, ' '.join(exc))
1132 class ServInfo (TrivialWindow):
1134 Show information about the server and available services.
1136 Interesting attributes:
1138 * e: maps SERVINFO keys to entry widgets
1139 * svcs: Gtk ListStore describing services (columns are name and version)
1143 TrivialWindow.__init__(me)
1144 me.set_title('TrIPE server info')
1145 table = GridPacker()
1148 def add(label, tag, text = None, **kw):
1149 me.e[tag] = table.info(label, text, **kw)
1150 add('Implementation', 'implementation')
1151 add('Version', 'version', newlinep = True)
1152 me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1153 me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1154 scr = MyScrolledWindow()
1155 lb = MyTreeView(me.svcs)
1157 for title in 'Service', 'Version':
1158 lb.append_column(G.TreeViewColumn(
1159 title, G.CellRendererText(), text = i))
1161 for svc in monitor.services:
1162 me.svcs.append([svc.name, svc.version])
1164 table.pack(scr, width = 2, newlinep = True,
1165 yopt = G.EXPAND | G.FILL | G.SHRINK)
1167 me.hook(conn.connecthook, me.update)
1168 me.hook(monitor.services.addhook, me.addsvc)
1169 me.hook(monitor.services.delhook, me.delsvc)
1172 def addsvc(me, svc):
1173 me.svcs.append([svc.name, svc.version])
1175 def delsvc(me, svc):
1176 for i in xrange(len(me.svcs)):
1177 if me.svcs[i][0] == svc.name:
1178 me.svcs.remove(me.svcs.get_iter(i))
1182 info = conn.servinfo()
1184 me.e[i].set_text(info[i])
1186 class TraceOptions (MyDialog):
1187 """Tracing options window."""
1189 MyDialog.__init__(me, title = 'Tracing options',
1190 buttons = [(G.STOCK_CLOSE, me.destroy),
1191 (G.STOCK_OK, cr(me.ok))])
1197 for ch, st, desc in conn.trace():
1198 if ch.isupper(): continue
1199 text = desc[0].upper() + desc[1:]
1200 ticky = G.CheckButton(text)
1201 ticky.set_active(st == '+')
1202 me.vbox.pack_start(ticky, True, True, 0)
1203 me.opts.append((ch, ticky))
1208 for ch, ticky in me.opts:
1209 if ticky.get_active():
1213 setting = ''.join(on) + '-' + ''.join(off)
1217 ###--------------------------------------------------------------------------
1221 """Translate a TrIPE-format time to something human-readable."""
1222 if t == 'NEVER': return '(never)'
1223 YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1224 ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1225 ago = MATH.floor(ago); unit = 's'
1226 for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1230 return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1231 (YY, MM, DD, hh, mm, ss, ago, unit)
1233 """Translate a number of bytes into something a human might want to read."""
1240 return '%d %s' % (b, suff)
1242 ## How to translate peer stats. Maps the stat name to a translation
1245 [('start-time', xlate_time),
1246 ('last-packet-time', xlate_time),
1247 ('last-keyexch-time', xlate_time),
1248 ('bytes-in', xlate_bytes),
1249 ('bytes-out', xlate_bytes),
1250 ('keyexch-bytes-in', xlate_bytes),
1251 ('keyexch-bytes-out', xlate_bytes),
1252 ('ip-bytes-in', xlate_bytes),
1253 ('ip-bytes-out', xlate_bytes)]
1255 ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
1256 ## the label to give the entry box; FORMAT is the format string to write into
1259 [('Start time', '%(start-time)s'),
1260 ('Private key', '%(current-key)s'),
1261 ('Diffie-Hellman group',
1263 '(%(kx-group-order-bits)s-bit order, '
1264 '%(kx-group-elt-bits)s-bit elements)'),
1266 '%(cipher)s (%(cipher-keysz)s-bit key, %(cipher-blksz)s-bit block)'),
1267 ('Mac', '%(mac)s (%(mac-keysz)s-bit key, %(mac-tagsz)s-bit tag)'),
1268 ('Hash', '%(hash)s (%(hash-sz)s-bit output)'),
1269 ('Last key-exchange', '%(last-keyexch-time)s'),
1270 ('Last packet', '%(last-packet-time)s'),
1272 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1273 ('Key-exchange in/out',
1274 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1276 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1277 ('Rejected packets', '%(rejected-packets)s')]
1279 class PeerWindow (TrivialWindow):
1281 Show information about a peer.
1283 This gives a graphical view of the server's peer statistics.
1285 Interesting attributes:
1287 * e: dict mapping keys (mostly matching label widget texts, though pings
1288 use command names) to entry widgets so that we can update them easily
1289 * peer: the peer this window shows information about
1290 * cr: the info-fetching coroutine, or None if crrrently disconnected
1291 * doupate: whether the info-fetching corouting should continue running
1294 def __init__(me, peer):
1295 """Construct a PeerWindow, showing information about PEER."""
1297 TrivialWindow.__init__(me)
1298 me.set_title('TrIPE statistics: %s' % peer.name)
1301 table = GridPacker()
1304 ## Utility for adding fields.
1306 def add(label, text = None, key = None):
1307 if key is None: key = label
1308 me.e[key] = table.info(label, text, len = 42, newlinep = True)
1310 ## Build the dialogue box.
1311 add('Peer name', peer.name)
1312 add('Tunnel', peer.tunnel)
1313 add('Interface', peer.ifname)
1315 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1316 add('Address', peer.addr)
1317 add('Transport pings', key = 'PING')
1318 add('Encrypted pings', key = 'EPING')
1320 for label, format in statslayout:
1323 ## Hook onto various interesting events.
1324 me.hook(conn.connecthook, me.tryupdate)
1325 me.hook(conn.disconnecthook, me.stopupdate)
1326 me.hook(me.closehook, me.stopupdate)
1327 me.hook(me.peer.deadhook, me.dead)
1328 me.hook(me.peer.changehook, me.change)
1329 me.hook(me.peer.pinghook, me.ping)
1334 ## Format the ping statistics.
1335 for cmd, ps in me.peer.ping.iteritems():
1336 me.ping(me.peer, cmd, ps)
1338 ## And show the window.
1342 """Update the display in response to a notification."""
1343 me.e['Interface'].set_text(me.peer.ifname)
1347 Main display-updating coroutine.
1349 This does an update, sleeps for a while, and starts again. If the
1350 me.doupdate flag goes low, we stop the loop.
1352 while me.peer.alivep and conn.connectedp() and me.doupdate:
1353 stat = conn.stats(me.peer.name)
1354 for s, trans in statsxlate:
1355 stat[s] = trans(stat[s])
1356 stat.update(me.peer.__dict__)
1357 for label, format in statslayout:
1358 me.e[label].set_text(format % stat)
1359 GL.timeout_add(1000, lambda: me.cr.switch() and False)
1360 me.cr.parent.switch()
1364 """Start the updater coroutine, if it's not going already."""
1366 me.cr = T.Coroutine(me._update,
1367 name = 'update-peer-window %s' % me.peer.name)
1370 def stopupdate(me, *hunoz, **hukairz):
1371 """Stop the update coroutine, by setting me.doupdate."""
1375 """Called when the peer is killed."""
1376 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1377 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1380 def ping(me, peer, cmd, ps):
1381 """Called when a ping result for the peer is reported."""
1382 s = '%d/%d' % (ps.ngood, ps.n)
1384 s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1386 s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1387 me.e[ps.command].set_text(s)
1389 ###--------------------------------------------------------------------------
1390 ### Cryptographic status.
1392 class CryptoInfo (TrivialWindow):
1393 """Simple display of cryptographic algorithms in use."""
1395 TrivialWindow.__init__(me)
1396 me.set_title('Cryptographic algorithms')
1397 T.aside(me.populate)
1399 table = GridPacker()
1402 crypto = conn.algs()
1403 table.info('Diffie-Hellman group',
1404 '%s (%d-bit order, %d-bit elements)' %
1405 (crypto['kx-group'],
1406 int(crypto['kx-group-order-bits']),
1407 int(crypto['kx-group-elt-bits'])),
1409 table.info('Data encryption',
1410 '%s (%d-bit key; %s)' %
1412 int(crypto['cipher-keysz']) * 8,
1413 crypto['cipher-blksz'] == '0'
1415 or '%d-bit block' % (int(crypto['cipher-blksz']) * 8)),
1417 table.info('Message authentication',
1418 '%s (%d-bit key; %d-bit tag)' %
1420 int(crypto['mac-keysz']) * 8,
1421 int(crypto['mac-tagsz']) * 8),
1423 table.info('Hash function',
1424 '%s (%d-bit output)' %
1426 int(crypto['hash-sz']) * 8),
1431 ###--------------------------------------------------------------------------
1432 ### Main monitor window.
1434 class MonitorWindow (MyWindow):
1437 The main monitor window.
1439 This class creates, populates and maintains the main monitor window.
1443 * warnings, trace: log models for server output
1444 * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1445 WindowSlot objects for ancillary windows
1446 * ui: Gtk UIManager object for the menu system
1447 * apmenu: pair of identical autoconnecting peer menus
1448 * listmodel: Gtk ListStore for connected peers; contains peer name,
1449 address, and ping times (transport and encrypted, value and colour)
1450 * status: Gtk Statusbar at the bottom of the window
1451 * _kidding: an unpleasant backchannel between the apchange method (which
1452 builds the apmenus) and the menu handler, forced on us by a Gtk
1455 Also installs attributes on Peer objects:
1457 * i: index of peer's entry in listmodel
1458 * win: WindowSlot object for the peer's PeerWindow
1462 """Construct the window."""
1465 MyWindow.__init__(me)
1466 me.set_title('TrIPE monitor')
1468 ## Hook onto diagnostic outputs.
1469 me.warnings = WarningLogModel()
1470 me.hook(conn.warnhook, me.warnings.notify)
1471 me.trace = TraceLogModel()
1472 me.hook(conn.tracehook, me.trace.notify)
1474 ## Make slots to store the various ancillary singleton windows.
1475 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1476 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1477 me.traceopts = WindowSlot(lambda: TraceOptions())
1478 me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1479 me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1480 me.servinfo = WindowSlot(lambda: ServInfo())
1482 ## Main window structure.
1486 ## UI manager makes our menus. (We're too cheap to have a toolbar.)
1487 me.ui = G.UIManager()
1488 actgroup = makeactiongroup('monitor',
1489 [('file-menu', '_File', None, None),
1490 ('connect', '_Connect', '<Control>C', conn.connect),
1491 ('disconnect', '_Disconnect', '<Control>D',
1492 lambda: conn.disconnect(None)),
1493 ('quit', '_Quit', '<Control>Q', me.close),
1494 ('server-menu', '_Server', None, None),
1495 ('daemon', 'Run in _background', None, cr(conn.daemon)),
1496 ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1497 ('crypto-algs', 'Cryptographic algorithms',
1498 '<Control>Y', me.cryptoinfo.open),
1499 ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1500 ('server-quit', 'Terminate server', None, cr(conn.quit)),
1501 ('conn-peer', 'Connect peer', None, None),
1502 ('logs-menu', '_Logs', None, None),
1503 ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1504 ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1505 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1506 ('help-menu', '_Help', None, None),
1507 ('about', '_About tripemon...', None, aboutbox.open),
1508 ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1509 ('kill-peer', '_Kill peer', None, me.killpeer),
1510 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1516 <menu action="file-menu">
1517 <menuitem action="quit"/>
1519 <menu action="server-menu">
1520 <menuitem action="connect"/>
1521 <menuitem action="disconnect"/>
1523 <menuitem action="server-version"/>
1524 <menuitem action="crypto-algs"/>
1525 <menuitem action="add-peer"/>
1526 <menuitem action="conn-peer"/>
1527 <menuitem action="daemon"/>
1528 <menuitem action="reload-keys"/>
1530 <menuitem action="server-quit"/>
1532 <menu action="logs-menu">
1533 <menuitem action="show-warnings"/>
1534 <menuitem action="show-trace"/>
1535 <menuitem action="trace-options"/>
1537 <menu action="help-menu">
1538 <menuitem action="about"/>
1541 <popup name="peer-popup">
1542 <menuitem action="add-peer"/>
1543 <menuitem action="conn-peer"/>
1544 <menuitem action="kill-peer"/>
1545 <menuitem action="force-kx"/>
1550 ## Populate the UI manager.
1551 me.ui.insert_action_group(actgroup, 0)
1552 me.ui.add_ui_from_string(uidef)
1554 ## Construct the menu bar.
1555 vbox.pack_start(me.ui.get_widget('/menubar'), False, True, 0)
1556 me.add_accel_group(me.ui.get_accel_group())
1558 ## Construct and attach the auto-peers menu. (This is a horrible bodge
1559 ## because we can't attach the same submenu in two different places.)
1560 me.apmenu = G.Menu(), G.Menu()
1561 me.ui.get_widget('/menubar/server-menu/conn-peer') \
1562 .set_submenu(me.apmenu[0])
1563 me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1565 ## Construct the main list model, and listen on hooks which report
1566 ## changes to the available peers.
1567 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1568 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1569 me.hook(monitor.peers.addhook, me.addpeer)
1570 me.hook(monitor.peers.delhook, me.delpeer)
1571 me.hook(monitor.autopeershook, me.apchange)
1573 ## Construct the list viewer and put it in a scrolling window.
1574 scr = MyScrolledWindow()
1575 me.list = MyTreeView(me.listmodel)
1576 me.list.append_column(G.TreeViewColumn('Peer name',
1577 G.CellRendererText(),
1579 me.list.append_column(G.TreeViewColumn('Address',
1580 G.CellRendererText(),
1582 me.list.append_column(G.TreeViewColumn('T-ping',
1583 G.CellRendererText(),
1586 me.list.append_column(G.TreeViewColumn('E-ping',
1587 G.CellRendererText(),
1590 me.list.get_column(1).set_expand(True)
1591 me.list.connect('row-activated', me.activate)
1592 me.list.connect('button-press-event', me.buttonpress)
1593 me.list.set_reorderable(True)
1594 me.list.get_selection().set_mode(G.SELECTION_NONE)
1596 vbox.pack_start(scr, True, True, 0)
1598 ## Construct the status bar, and listen on hooks which report changes to
1599 ## connection status.
1600 me.status = G.Statusbar()
1601 vbox.pack_start(me.status, False, True, 0)
1602 me.hook(conn.connecthook, cr(me.connected))
1603 me.hook(conn.disconnecthook, me.disconnected)
1604 me.hook(conn.notehook, me.notify)
1606 ## Set a plausible default window size.
1607 me.set_default_size(512, 180)
1609 def addpeer(me, peer):
1610 """Hook: announces that PEER has been added."""
1611 peer.i = me.listmodel.append([peer.name, peer.addr,
1612 '???', 'green', '???', 'green'])
1613 peer.win = WindowSlot(lambda: PeerWindow(peer))
1614 me.hook(peer.pinghook, me._ping)
1617 def delpeer(me, peer):
1618 """Hook: announces that PEER has been removed."""
1619 me.listmodel.remove(peer.i)
1620 me.unhook(peer.pinghook)
1623 def path_peer(me, path):
1624 """Return the peer corresponding to a given list-model PATH."""
1625 return monitor.peers[me.listmodel[path][0]]
1629 Hook: announces that a change has been made to the peers available for
1630 automated connection.
1632 This populates both auto-peer menus and keeps them in sync. (As
1633 mentioned above, we can't attach the same submenu to two separate parent
1634 menu items. So we end up with two identical menus instead. Yes, this
1638 ## The set_active method of a CheckMenuItem works by maybe activating the
1639 ## menu item. This signals our handler. But we don't actually want to
1640 ## signal the handler unless the user actually frobbed the item. So the
1641 ## _kidding flag is used as an underhanded way of telling the handler
1642 ## that we don't actually want it to do anything. Of course, this sucks
1646 ## Iterate over the two menus.
1649 existing = menu.get_children()
1650 if monitor.autopeers is None:
1652 ## No peers, so empty out the menu.
1653 for item in existing:
1658 ## Insert the new items into the menu. (XXX this seems buggy XXX)
1659 ## Tick the peers which are actually connected.
1661 for peer in monitor.autopeers:
1662 if j < len(existing) and \
1663 existing[j].get_child().get_text() == peer:
1667 item = G.CheckMenuItem(peer, use_underline = False)
1668 item.connect('activate', invoker(me._addautopeer, peer))
1669 menu.insert(item, i)
1670 item.set_active(peer in monitor.peers.table)
1673 ## Make all the menu items visible.
1676 ## Set the parent menu items sensitive if and only if there are any peers
1678 for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1679 me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1681 ## And now allow the handler to do its business normally.
1684 def _addautopeer(me, peer):
1686 Automatically connect an auto-peer.
1688 This method is invoked from the main coroutine. Since the actual
1689 connection needs to issue administration commands, we must spawn a new
1690 child coroutine for it.
1694 T.Coroutine(me._addautopeer_hack,
1695 name = '_addautopeerhack %s' % peer).switch(peer)
1697 def _addautopeer_hack(me, peer):
1698 """Make an automated connection to PEER in response to a user click."""
1702 T._simple(conn.svcsubmit('connect', 'active', peer))
1703 except T.TripeError, exc:
1704 T.defer(moanbox, ' '.join(exc.args))
1707 def activate(me, l, path, col):
1709 Handle a double-click on a peer in the main list: open a PeerInfo window.
1711 peer = me.path_peer(path)
1714 def buttonpress(me, l, ev):
1716 Handle a mouse click on the main list.
1718 Currently we're only interested in button-3, which pops up the peer menu.
1719 For future reference, we stash the peer that was clicked in me.menupeer.
1722 x, y = int(ev.x), int(ev.y)
1723 r = me.list.get_path_at_pos(x, y)
1724 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1725 me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1727 me.ui.get_widget('/peer-popup/conn-peer'). \
1728 set_sensitive(bool(monitor.autopeers))
1730 me.menupeer = me.path_peer(r[0])
1733 me.ui.get_widget('/peer-popup').popup(
1734 None, None, None, ev.button, ev.time)
1737 """Kill a peer from the popup menu."""
1738 cr(conn.kill, me.menupeer.name)()
1741 """Kickstart a key-exchange from the popup menu."""
1742 cr(conn.forcekx, me.menupeer.name)()
1744 _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1745 def _ping(me, p, cmd, ps):
1746 """Hook: responds to ping reports."""
1747 textcol, colourcol = me._columnmap[cmd]
1749 me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1750 me.listmodel[p.i][colourcol] = 'red'
1752 me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1753 me.listmodel[p.i][colourcol] = 'black'
1755 def setstatus(me, status):
1756 """Update the message in the status bar."""
1758 me.status.push(0, status)
1760 def notify(me, note, *rest):
1761 """Hook: invoked when interesting notifications occur."""
1762 if note == 'DAEMON':
1763 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1767 Hook: invoked when a connection is made to the server.
1769 Make options which require a server connection sensitive.
1771 me.setstatus('Connected (port %s)' % conn.port())
1772 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1773 for i in ('/menubar/server-menu/disconnect',
1774 '/menubar/server-menu/server-version',
1775 '/menubar/server-menu/add-peer',
1776 '/menubar/server-menu/server-quit',
1777 '/menubar/logs-menu/trace-options'):
1778 me.ui.get_widget(i).set_sensitive(True)
1779 me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1780 set_sensitive(bool(monitor.autopeers))
1781 me.ui.get_widget('/menubar/server-menu/daemon'). \
1782 set_sensitive(conn.servinfo()['daemon'] == 'nil')
1784 def disconnected(me, reason):
1786 Hook: invoked when the connection to the server is lost.
1788 Make most options insensitive.
1790 me.setstatus('Disconnected')
1791 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1792 for i in ('/menubar/server-menu/disconnect',
1793 '/menubar/server-menu/server-version',
1794 '/menubar/server-menu/add-peer',
1795 '/menubar/server-menu/conn-peer',
1796 '/menubar/server-menu/daemon',
1797 '/menubar/server-menu/server-quit',
1798 '/menubar/logs-menu/trace-options'):
1799 me.ui.get_widget(i).set_sensitive(False)
1800 if reason: moanbox(reason)
1802 ###--------------------------------------------------------------------------
1805 def parse_options():
1807 Parse command-line options.
1809 Process the boring ones. Return all of them, for later.
1811 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1812 version = '%prog (tripe version 1.0.0)')
1813 op.add_option('-a', '--admin-socket',
1814 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1815 help = 'Select socket to connect to [default %default]')
1816 op.add_option('-d', '--directory',
1817 metavar = 'DIR', dest = 'dir', default = T.configdir,
1818 help = 'Select current diretory [default %default]')
1819 opts, args = op.parse_args()
1820 if args: op.error('no arguments permitted')
1825 """Initialization."""
1827 global conn, monitor, pinger
1829 ## Try to establish a connection.
1830 conn = Connection(opts.tripesock)
1832 ## Make the main interesting coroutines and objects.
1840 root = MonitorWindow()
1845 HookClient().hook(root.closehook, exit)
1848 if __name__ == '__main__':
1849 opts = parse_options()
1853 ###----- That's all, folks --------------------------------------------------