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 ipaddr, port = addr[1:]
329 name = S.gethostbyaddr(ipaddr)[0]
330 me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
332 me.addr = 'INET %s:%s' % (ipaddr, port)
334 me.addr = ' '.join(addr)
336 def setaddr(me, addr):
337 """Informs the object of a change to its address to ADDR."""
341 def setifname(me, newname):
342 """Informs the object of a change to its interface name to NEWNAME."""
346 class Service (MonitorObject):
348 Represents a service.
350 Additional attributes are:
352 * version = the service version
354 def __init__(me, name, version):
355 MonitorObject.__init__(me, name)
358 def update(me, version):
359 """Tell the Service that its version has changed to VERSION."""
363 class MonitorList (object):
365 Maintains a collection of MonitorObjects.
367 The MonitorList can be indexed by name to retrieve the individual objects;
368 iteration generates the individual objects. More complicated operations
369 can be done on the `table' dictionary directly.
371 Hooks addhook(OBJ) and delhook(OBJ) are invoked when objects are added or
374 Subclass responsibilities:
376 * list(): return a list of (NAME, INFO) pairs.
378 * make(NAME, INFO): returns a new MonitorObject for the given NAME; INFO
379 is from the output of list().
383 """Initialize a new MonitorList."""
385 me.addhook = HookList()
386 me.delhook = HookList()
390 Refresh the list of objects:
392 We add new object which have appeared, delete ones which have vanished,
393 and update any which persist.
396 for name, stuff in me.list():
399 for name in me.table.copy():
403 def add(me, name, stuff):
405 Add a new object created by make(NAME, STUFF) if it doesn't already
406 exist. If it does, update it.
408 if name not in me.table:
409 obj = me.make(name, stuff)
413 me.table[name].update(stuff)
415 def remove(me, name):
417 Remove the object called NAME from the list.
419 The object becomes dead.
427 def __getitem__(me, name):
428 """Retrieve the object called NAME."""
429 return me.table[name]
432 """Iterate over the objects."""
433 return me.table.itervalues()
435 class PeerList (MonitorList):
436 """The list of the known peers."""
438 return [(name, None) for name in conn.list()]
439 def make(me, name, stuff):
442 class ServiceList (MonitorList):
443 """The list of the registered services."""
445 return conn.svclist()
446 def make(me, name, stuff):
447 return Service(name, stuff)
449 class Monitor (HookClient):
451 The main monitor: keeps track of the changes happening to the server.
453 Exports the peers, services MonitorLists, and a (plain Python) list
454 autopeers of peers which the connect service knows how to start by name.
458 * autopeershook(): invoked when the auto-peers list changes.
461 """Initialize the Monitor."""
462 HookClient.__init__(me)
463 me.peers = PeerList()
464 me.services = ServiceList()
465 me.hook(conn.connecthook, me._connected)
466 me.hook(conn.notehook, me._notify)
467 me.autopeershook = HookList()
471 """Handle a successful connection by starting the setup coroutine."""
476 """Coroutine function: initialize for a new connection."""
480 me._updateautopeers()
482 def _updateautopeers(me):
483 """Update the auto-peers list from the connect service."""
484 if 'connect' in me.services.table:
485 me.autopeers = [' '.join(line)
486 for line in conn.svcsubmit('connect', 'list-active')]
490 me.autopeershook.run()
492 def _notify(me, code, *rest):
494 Handle notifications from the server.
496 ADD, KILL and NEWIFNAME notifications get passed up to the PeerList;
497 SVCCLAIM and SVCRELEASE get passed up to the ServiceList. Finally,
498 peerdb-update notifications from the watch service cause us to refresh
502 T.aside(me.peers.add, rest[0], None)
504 T.aside(me.peers.remove, rest[0])
505 elif code == 'NEWIFNAME':
507 me.peers[rest[0]].setifname(rest[2])
510 elif code == 'NEWADDR':
512 me.peers[rest[0]].setaddr(rest[1:])
515 elif code == 'SVCCLAIM':
516 T.aside(me.services.add, rest[0], rest[1])
517 if rest[0] == 'connect':
518 T.aside(me._updateautopeers)
519 elif code == 'SVCRELEASE':
520 T.aside(me.services.remove, rest[0])
521 if rest[0] == 'connect':
522 T.aside(me._updateautopeers)
525 if rest[0] == 'watch' and \
526 rest[1] == 'peerdb-update':
527 T.aside(me._updateautopeers)
529 ###--------------------------------------------------------------------------
530 ### Window management cruft.
532 class MyWindowMixin (G.Window, HookClient):
534 Mixin for windows which call a closehook when they're destroyed. It's also
535 a hookclient, and will release its hooks when it's destroyed.
539 * closehook(): called when the window is closed.
543 """Initialization function. Note that it's not called __init__!"""
544 me.closehook = HookList()
545 HookClient.__init__(me)
546 me.connect('destroy', invoker(me.close))
549 """Close the window, invoking the closehook and releasing all hooks."""
554 class MyWindow (MyWindowMixin):
555 """A version of MyWindowMixin suitable as a single parent class."""
556 def __init__(me, kind = G.WINDOW_TOPLEVEL):
557 G.Window.__init__(me, kind)
560 class TrivialWindowMixin (MyWindowMixin):
561 """A simple window which you can close with Escape."""
563 super(TrivialWindowMixin, me).mywininit()
564 me.connect('key-press-event', me._keypress)
565 def _keypress(me, _, ev):
566 if ev.keyval == GDK.KEY_Escape: me.destroy()
568 class TrivialWindow (MyWindow, TrivialWindowMixin):
571 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
572 """A dialogue box with a closehook and sensible button binding."""
574 def __init__(me, title = None, flags = 0, buttons = []):
576 The BUTTONS are a list of (STOCKID, THUNK) pairs: call the appropriate
577 THUNK when the button is pressed. The other arguments are just like
588 G.Dialog.__init__(me, title, None, flags, tuple(br))
590 me.set_default_response(i - 1)
591 me.connect('response', me.respond)
593 def respond(me, hunoz, rid, *hukairz):
594 """Dispatch responses to the appropriate thunks."""
595 if rid >= 0: me.rmap[rid]()
597 def makeactiongroup(name, acts):
599 Creates an ActionGroup called NAME.
601 ACTS is a list of tuples containing:
603 * ACT: an action name
604 * LABEL: the label string for the action
605 * ACCEL: accelerator string, or None
606 * FUNC: thunk to call when the action is invoked
608 actgroup = G.ActionGroup(name)
609 for act, label, accel, func in acts:
610 a = G.Action(act, label, None, None)
611 if func: a.connect('activate', invoker(func))
612 actgroup.add_action_with_accel(a, accel)
615 class GridPacker (G.Table):
617 Like a Table, but with more state: makes filling in the widgets easier.
621 """Initialize a new GridPacker."""
627 me.set_border_width(4)
628 me.set_col_spacings(4)
629 me.set_row_spacings(4)
631 def pack(me, w, width = 1, newlinep = False,
632 xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
637 W is the widget to add. XOPY, YOPT, XPAD and YPAD are as for Table.
638 WIDTH is how many cells to take up horizontally. NEWLINEP is whether to
639 start a new line for this widget. Returns W.
645 right = me.col + width
646 if bot > me.rows or right > me.cols:
647 if bot > me.rows: me.rows = bot
648 if right > me.cols: me.cols = right
649 me.resize(me.rows, me.cols)
650 me.attach(w, me.col, me.col + width, me.row, me.row + 1,
651 xopt, yopt, xpad, ypad)
655 def labelled(me, lab, w, newlinep = False, **kw):
657 Packs a labelled widget.
659 Other arguments are as for pack. Returns W.
661 label = G.Label(lab + ' ')
662 label.set_alignment(1.0, 0)
663 me.pack(label, newlinep = newlinep, xopt = G.FILL)
667 def info(me, label, text = None, len = 18, **kw):
669 Packs an information widget with a label.
671 LABEL is the label; TEXT is the initial text; LEN is the estimated length
672 in characters. Returns the entry widget.
675 if text is not None: e.set_text(text)
676 e.set_width_chars(len)
677 e.set_selectable(True)
678 e.set_alignment(0.0, 0.5)
679 me.labelled(label, e, **kw)
682 class WindowSlot (HookClient):
684 A place to store a window -- specificially a MyWindowMixin.
686 If the window is destroyed, remember this; when we come to open the window,
687 raise it if it already exists; otherwise make a new one.
689 def __init__(me, createfunc):
691 Constructor: CREATEFUNC must return a new Window which supports the
694 HookClient.__init__(me)
695 me.createfunc = createfunc
699 """Opens the window, creating it if necessary."""
701 raise_window(me.window)
703 me.window = me.createfunc()
704 me.hook(me.window.closehook, me.closed)
707 """Handles the window being closed."""
708 me.unhook(me.window.closehook)
711 class MyTreeView (G.TreeView):
712 def __init__(me, model):
713 G.TreeView.__init__(me, model)
714 me.set_rules_hint(True)
716 class MyScrolledWindow (G.ScrolledWindow):
718 G.ScrolledWindow.__init__(me)
719 me.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
720 me.set_shadow_type(G.SHADOW_IN)
722 ## Matches a signed integer.
723 rx_num = RX.compile(r'^[-+]?\d+$')
726 c_red = GDK.color_parse('#ff6666')
728 class ValidationError (Exception):
729 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
732 class ValidatingEntry (G.Entry):
734 Like an Entry, but makes the text go red if the contents are invalid.
736 If get_text is called, and the text is invalid, ValidationError is raised.
737 The attribute validp reflects whether the contents are currently valid.
740 def __init__(me, valid, text = '', size = -1, *arg, **kw):
742 Make a validating Entry.
744 VALID is a regular expression or a predicate on strings. TEXT is the
745 default text to insert. SIZE is the size of the box to set, in
746 characters (ish). Other arguments are passed to Entry.
748 G.Entry.__init__(me, *arg, **kw)
749 me.connect("changed", me._check)
750 me.connect("state-changed", me._check)
754 me.validate = RX.compile(valid).match
756 if size != -1: me.set_width_chars(size)
757 me.set_activates_default(True)
761 def _check(me, *hunoz):
762 """Check the current text and update validp and the text colour."""
763 if me.validate(G.Entry.get_text(me)):
765 set_entry_bg(me, None)
768 set_entry_bg(me, me.is_sensitive() and c_red or None)
772 Return the text in the Entry if it's valid. If it isn't, raise
776 raise ValidationError
777 return G.Entry.get_text(me)
779 def numericvalidate(min = None, max = None):
781 Return a validation function for numbers.
783 Entry must consist of an optional sign followed by digits, and the
784 resulting integer must be within the given bounds.
786 return lambda x: (rx_num.match(x) and
787 (min is None or long(x) >= min) and
788 (max is None or long(x) <= max))
790 ###--------------------------------------------------------------------------
791 ### Various minor dialog boxen.
794 TrIPE is free software: you can redistribute it and/or modify it under
795 the terms of the GNU General Public License as published by the Free
796 Software Foundation; either version 3 of the License, or (at your
797 option) any later version.
799 TrIPE is distributed in the hope that it will be useful, but WITHOUT
800 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
801 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
804 You should have received a copy of the GNU General Public License
805 along with TrIPE. If not, see <https://www.gnu.org/licenses/>."""
807 class AboutBox (G.AboutDialog, TrivialWindowMixin):
808 """The program `About' box."""
810 G.AboutDialog.__init__(me)
812 me.set_name('TrIPEmon')
813 me.set_version(T.VERSION)
815 me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
816 me.set_comments('A graphical monitor for the TrIPE VPN server')
817 me.set_copyright('Copyright © 2006-2008 Straylight/Edgeware')
818 me.connect('response', me.respond)
820 def respond(me, hunoz, rid, *hukairz):
821 if rid == G.RESPONSE_CANCEL:
823 aboutbox = WindowSlot(AboutBox)
826 """Report an error message in a window."""
827 d = G.Dialog('Error from %s' % M.quis,
828 flags = G.DIALOG_MODAL,
829 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
831 label.set_padding(20, 20)
832 d.vbox.pack_start(label, True, True, 0)
837 def unimplemented(*hunoz):
838 """Indicator of laziness."""
839 moanbox("I've not written that bit yet.")
841 ###--------------------------------------------------------------------------
844 class LogModel (G.ListStore):
846 A simple list of log messages, usable as the model for a TreeView.
848 The column headings are stored in the `cols' attribute.
851 def __init__(me, columns):
853 COLUMNS must be a list of column name strings. We add a time column to
856 me.cols = ('Time',) + columns
857 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
859 def add(me, *entries):
861 Adds a new log message, with a timestamp.
863 The ENTRIES are the contents for the list columns.
865 now = TIME.strftime('%Y-%m-%d %H:%M:%S')
866 me.append((now, ) + entries)
868 class TraceLogModel (LogModel):
869 """Log model for trace messages."""
871 LogModel.__init__(me, ('Message',))
872 def notify(me, line):
873 """Call with a new trace message."""
876 class WarningLogModel (LogModel):
878 Log model for warnings.
880 We split the category out into a separate column.
883 LogModel.__init__(me, ('Category', 'Message'))
884 def notify(me, tag, *rest):
885 """Call with a new warning message."""
886 me.add(tag, ' '.join([T.quotify(w) for w in rest]))
888 class LogViewer (TrivialWindow):
892 Its contents are a TreeView showing the log.
896 * model: an appropriate LogModel
897 * list: a TreeView widget to display the log
900 def __init__(me, model):
902 Create a log viewer showing the LogModel MODEL.
904 TrivialWindow.__init__(me)
906 scr = MyScrolledWindow()
907 me.list = MyTreeView(me.model)
909 for c in me.model.cols:
910 crt = G.CellRendererText()
911 me.list.append_column(G.TreeViewColumn(c, crt, text = i))
913 crt.set_property('family', 'monospace')
914 me.set_default_size(440, 256)
919 ###--------------------------------------------------------------------------
922 class pingstate (struct):
924 Information kept for each peer by the Pinger.
926 Important attributes:
928 * peer = the peer name
929 * command = PING or EPING
930 * n = how many pings we've sent so far
931 * ngood = how many returned
932 * nmiss = how many didn't return
933 * nmissrun = how many pings since the last good one
934 * tlast = round-trip time for the last (good) ping
935 * ttot = total roung trip time
939 class Pinger (T.Coroutine, HookClient):
941 Coroutine which pings known peers and collects statistics.
943 Interesting attributes:
945 * _map: dict mapping peer names to Peer objects
946 * _q: event queue for notifying pinger coroutine
947 * _timer: gobject timer for waking the coroutine
952 Initialize the pinger.
954 We watch the monitor's PeerList to track which peers we should ping. We
955 maintain an event queue and put all the events on that.
957 The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
958 where CMD is 'PING' or 'EPING'.
960 T.Coroutine.__init__(me)
961 HookClient.__init__(me)
965 me.hook(conn.connecthook, me._connected)
966 me.hook(conn.disconnecthook, me._disconnected)
967 me.hook(monitor.peers.addhook,
968 lambda p: T.defer(me._q.put, (p, 'ADD', None)))
969 me.hook(monitor.peers.delhook,
970 lambda p: T.defer(me._q.put, (p, 'KILL', None)))
971 if conn.connectedp(): me.connected()
974 """Respond to connection: start pinging thngs."""
975 me._timer = GL.timeout_add(1000, me._timerfunc)
978 """Timer function: put a timer event on the queue."""
979 me._q.put((None, 'TIMER', None))
982 def _disconnected(me, reason):
983 """Respond to disconnection: stop pinging."""
984 GL.source_remove(me._timer)
988 Coroutine function: read events from the queue and process them.
992 * (PEER, 'KILL', None): remove PEER from the interesting peers list
993 * (PEER, 'ADD', None): add PEER to the list
994 * (PEER, 'INFO', TOKENS): result from a PING command
995 * (None, 'TIMER', None): interval timer went off: send more pings
998 tag, code, stuff = me._q.get()
1003 elif not conn.connectedp():
1008 for cmd in 'PING', 'EPING':
1009 ps = pingstate(command = cmd, peer = p,
1010 n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1011 tlast = 0, ttot = 0)
1014 elif code == 'INFO':
1016 if stuff[0] == 'ping-ok':
1026 ps.peer.pinghook.run(ps.peer, ps.command, ps)
1027 elif code == 'TIMER':
1028 for name, p in me._map.iteritems():
1029 for cmd, ps in p.ping.iteritems():
1030 conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
1031 cmd, '-background', conn.bgtag(), '--', name]))
1033 ###--------------------------------------------------------------------------
1034 ### Random dialogue boxes.
1036 class AddPeerDialog (MyDialog):
1038 Let the user create a new peer the low-level way.
1040 Interesting attributes:
1042 * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
1046 """Initialize the dialogue."""
1047 MyDialog.__init__(me, 'Add peer',
1048 buttons = [(G.STOCK_CANCEL, me.destroy),
1049 (G.STOCK_OK, me.ok)])
1054 """Coroutine function: background setup for AddPeerDialog."""
1055 table = GridPacker()
1056 me.vbox.pack_start(table, True, True, 0)
1057 me.e_name = table.labelled('Name',
1058 ValidatingEntry(r'^[^\s.:]+$', '', 16),
1060 me.e_addr = table.labelled('Address',
1061 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
1063 me.e_port = table.labelled('Port',
1064 ValidatingEntry(numericvalidate(0, 65535),
1067 me.l_tunnel = table.labelled('Tunnel', combo_box_text(),
1068 newlinep = True, width = 3)
1069 me.tuns = ['(Default)'] + conn.tunnels()
1071 me.l_tunnel.append_text(t)
1072 me.l_tunnel.set_active(0)
1074 def tickybox_sensitivity(tickybox, target):
1075 tickybox.connect('toggled',
1076 lambda t: target.set_sensitive (t.get_active()))
1078 me.c_keepalive = G.CheckButton('Keepalives')
1079 table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
1080 me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
1081 me.e_keepalive.set_sensitive(False)
1082 tickybox_sensitivity(me.c_keepalive, me.e_keepalive)
1083 table.pack(me.e_keepalive, width = 3)
1085 me.c_cork = G.CheckButton('Cork')
1086 table.pack(me.c_cork, newlinep = True, width = 4, xopt = G.FILL)
1088 me.c_mobile = G.CheckButton('Mobile')
1089 table.pack(me.c_mobile, newlinep = True, width = 4, xopt = G.FILL)
1091 me.c_peerkey = G.CheckButton('Peer key tag')
1092 table.pack(me.c_peerkey, newlinep = True, xopt = G.FILL)
1093 me.e_peerkey = ValidatingEntry(r'^[^.:\s]+$', '', 16)
1094 me.e_peerkey.set_sensitive(False)
1095 tickybox_sensitivity(me.c_peerkey, me.e_peerkey)
1096 table.pack(me.e_peerkey, width = 3)
1098 me.c_privkey = G.CheckButton('Private key tag')
1099 table.pack(me.c_privkey, newlinep = True, xopt = G.FILL)
1100 me.e_privkey = ValidatingEntry(r'^[^.:\s]+$', '', 16)
1101 me.e_privkey.set_sensitive(False)
1102 tickybox_sensitivity(me.c_privkey, me.e_privkey)
1103 table.pack(me.e_privkey, width = 3)
1108 """Handle an OK press: create the peer."""
1110 t = me.l_tunnel.get_active()
1111 me._addpeer(me.e_name.get_text(),
1112 me.e_addr.get_text(),
1113 me.e_port.get_text(),
1114 keepalive = (me.c_keepalive.get_active() and
1115 me.e_keepalive.get_text() or None),
1116 tunnel = t and me.tuns[t] or None,
1117 cork = me.c_cork.get_active() or None,
1118 mobile = me.c_mobile.get_active() or None,
1119 key = (me.c_peerkey.get_active() and
1120 me.e_peerkey.get_text() or None),
1121 priv = (me.c_privkey.get_active() and
1122 me.e_privkey.get_text() or None))
1123 except ValidationError:
1128 def _addpeer(me, *args, **kw):
1129 """Coroutine function: actually do the ADD command."""
1131 conn.add(*args, **kw)
1133 except T.TripeError, exc:
1134 T.defer(moanbox, ' '.join(exc))
1136 class ServInfo (TrivialWindow):
1138 Show information about the server and available services.
1140 Interesting attributes:
1142 * e: maps SERVINFO keys to entry widgets
1143 * svcs: Gtk ListStore describing services (columns are name and version)
1147 TrivialWindow.__init__(me)
1148 me.set_title('TrIPE server info')
1149 table = GridPacker()
1152 def add(label, tag, text = None, **kw):
1153 me.e[tag] = table.info(label, text, **kw)
1154 add('Implementation', 'implementation')
1155 add('Version', 'version', newlinep = True)
1156 me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1157 me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1158 scr = MyScrolledWindow()
1159 lb = MyTreeView(me.svcs)
1161 for title in 'Service', 'Version':
1162 lb.append_column(G.TreeViewColumn(
1163 title, G.CellRendererText(), text = i))
1165 for svc in monitor.services:
1166 me.svcs.append([svc.name, svc.version])
1168 table.pack(scr, width = 2, newlinep = True,
1169 yopt = G.EXPAND | G.FILL | G.SHRINK)
1171 me.hook(conn.connecthook, me.update)
1172 me.hook(monitor.services.addhook, me.addsvc)
1173 me.hook(monitor.services.delhook, me.delsvc)
1176 def addsvc(me, svc):
1177 me.svcs.append([svc.name, svc.version])
1179 def delsvc(me, svc):
1180 for i in xrange(len(me.svcs)):
1181 if me.svcs[i][0] == svc.name:
1182 me.svcs.remove(me.svcs.get_iter(i))
1186 info = conn.servinfo()
1188 me.e[i].set_text(info[i])
1190 class TraceOptions (MyDialog):
1191 """Tracing options window."""
1193 MyDialog.__init__(me, title = 'Tracing options',
1194 buttons = [(G.STOCK_CLOSE, me.destroy),
1195 (G.STOCK_OK, cr(me.ok))])
1201 for ch, st, desc in conn.trace():
1202 if ch.isupper(): continue
1203 text = desc[0].upper() + desc[1:]
1204 ticky = G.CheckButton(text)
1205 ticky.set_active(st == '+')
1206 me.vbox.pack_start(ticky, True, True, 0)
1207 me.opts.append((ch, ticky))
1212 for ch, ticky in me.opts:
1213 if ticky.get_active():
1217 setting = ''.join(on) + '-' + ''.join(off)
1221 ###--------------------------------------------------------------------------
1225 """Translate a TrIPE-format time to something human-readable."""
1226 if t == 'NEVER': return '(never)'
1227 YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1228 ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1229 ago = MATH.floor(ago); unit = 's'
1230 for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1234 return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1235 (YY, MM, DD, hh, mm, ss, ago, unit)
1237 """Translate a raw byte count into something a human might want to read."""
1244 return '%d %s' % (b, suff)
1246 ## How to translate peer stats. Maps the stat name to a translation
1249 [('start-time', xlate_time),
1250 ('last-packet-time', xlate_time),
1251 ('last-keyexch-time', xlate_time),
1252 ('bytes-in', xlate_bytes),
1253 ('bytes-out', xlate_bytes),
1254 ('keyexch-bytes-in', xlate_bytes),
1255 ('keyexch-bytes-out', xlate_bytes),
1256 ('ip-bytes-in', xlate_bytes),
1257 ('ip-bytes-out', xlate_bytes)]
1259 def format_stat(format, dict):
1260 if callable(format): return format(dict)
1261 else: return format % dict
1263 ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
1264 ## the label to give the entry box; FORMAT is the format string to write into
1267 [('Diffie-Hellman group',
1269 '(%(kx-group-order-bits)s-bit order, '
1270 '%(kx-group-elt-bits)s-bit elements)'),
1271 ('Bulk crypto transform',
1272 '%(bulk-transform)s (%(bulk-overhead)s byte overhead)'),
1273 ('Data encryption', lambda d: '%s (%s; %s)' % (
1275 '%d-bit key' % (8*int(d['cipher-keysz'])),
1276 d.get('cipher-blksz', '0') == '0'
1278 or '%d-bit block' % (8*int(d['cipher-blksz'])))),
1279 ('Message authentication', lambda d: '%s (%s; %s)' % (
1281 d.get('mac-keysz') is None
1283 or '%d-bit key' % (8*int(d['mac-keysz'])),
1284 '%d-bit tag' % (8*int(d['mac-tagsz'])))),
1285 ('Hash', lambda d: '%s (%d-bit output)' %
1286 (d['hash'], 8*int(d['hash-sz'])))]
1289 [('Start time', '%(start-time)s'),
1290 ('Private key', '%(current-key)s')] + \
1292 [('Last key-exchange', '%(last-keyexch-time)s'),
1293 ('Last packet', '%(last-packet-time)s'),
1295 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1296 ('Key-exchange in/out',
1297 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1299 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1300 ('Rejected packets', '%(rejected-packets)s')]
1302 class PeerWindow (TrivialWindow):
1304 Show information about a peer.
1306 This gives a graphical view of the server's peer statistics.
1308 Interesting attributes:
1310 * e: dict mapping keys (mostly matching label widget texts, though pings
1311 use command names) to entry widgets so that we can update them easily
1312 * peer: the peer this window shows information about
1313 * cr: the info-fetching coroutine, or None if crrrently disconnected
1314 * doupate: whether the info-fetching corouting should continue running
1317 def __init__(me, peer):
1318 """Construct a PeerWindow, showing information about PEER."""
1320 TrivialWindow.__init__(me)
1321 me.set_title('TrIPE statistics: %s' % peer.name)
1324 table = GridPacker()
1327 ## Utility for adding fields.
1329 def add(label, text = None, key = None):
1330 if key is None: key = label
1331 me.e[key] = table.info(label, text, len = 42, newlinep = True)
1333 ## Build the dialogue box.
1334 add('Peer name', peer.name)
1335 add('Tunnel', peer.tunnel)
1336 add('Interface', peer.ifname)
1338 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1339 add('Address', peer.addr)
1340 add('Transport pings', key = 'PING')
1341 add('Encrypted pings', key = 'EPING')
1343 for label, format in statslayout:
1346 ## Hook onto various interesting events.
1347 me.hook(conn.connecthook, me.tryupdate)
1348 me.hook(conn.disconnecthook, me.stopupdate)
1349 me.hook(me.closehook, me.stopupdate)
1350 me.hook(me.peer.deadhook, me.dead)
1351 me.hook(me.peer.changehook, me.change)
1352 me.hook(me.peer.pinghook, me.ping)
1357 ## Format the ping statistics.
1358 for cmd, ps in me.peer.ping.iteritems():
1359 me.ping(me.peer, cmd, ps)
1361 ## And show the window.
1365 """Update the display in response to a notification."""
1366 me.e['Interface'].set_text(me.peer.ifname)
1370 Main display-updating coroutine.
1372 This does an update, sleeps for a while, and starts again. If the
1373 me.doupdate flag goes low, we stop the loop.
1375 while me.peer.alivep and conn.connectedp() and me.doupdate:
1376 stat = conn.stats(me.peer.name)
1377 for s, trans in statsxlate:
1378 stat[s] = trans(stat[s])
1379 stat.update(me.peer.__dict__)
1380 for label, format in statslayout:
1381 me.e[label].set_text(format_stat(format, stat))
1382 GL.timeout_add(1000, lambda: me.cr.switch() and False)
1383 me.cr.parent.switch()
1387 """Start the updater coroutine, if it's not going already."""
1389 me.cr = T.Coroutine(me._update,
1390 name = 'update-peer-window %s' % me.peer.name)
1393 def stopupdate(me, *hunoz, **hukairz):
1394 """Stop the update coroutine, by setting me.doupdate."""
1398 """Called when the peer is killed."""
1399 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1400 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1403 def ping(me, peer, cmd, ps):
1404 """Called when a ping result for the peer is reported."""
1405 s = '%d/%d' % (ps.ngood, ps.n)
1407 s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1409 s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1410 me.e[ps.command].set_text(s)
1412 ###--------------------------------------------------------------------------
1413 ### Cryptographic status.
1415 class CryptoInfo (TrivialWindow):
1416 """Simple display of cryptographic algorithms in use."""
1418 TrivialWindow.__init__(me)
1419 me.set_title('Cryptographic algorithms')
1420 T.aside(me.populate)
1422 table = GridPacker()
1425 crypto = conn.algs()
1426 table.info('Diffie-Hellman group',
1427 '%s (%d-bit order, %d-bit elements)' %
1428 (crypto['kx-group'],
1429 int(crypto['kx-group-order-bits']),
1430 int(crypto['kx-group-elt-bits'])),
1432 table.info('Data encryption',
1433 '%s (%d-bit key; %s)' %
1435 int(crypto['cipher-keysz']) * 8,
1436 crypto['cipher-blksz'] == '0'
1438 or '%d-bit block' % (int(crypto['cipher-blksz']) * 8)),
1440 table.info('Message authentication',
1441 '%s (%d-bit key; %d-bit tag)' %
1443 int(crypto['mac-keysz']) * 8,
1444 int(crypto['mac-tagsz']) * 8),
1446 table.info('Hash function',
1447 '%s (%d-bit output)' %
1449 int(crypto['hash-sz']) * 8),
1454 ###--------------------------------------------------------------------------
1455 ### Main monitor window.
1457 class MonitorWindow (MyWindow):
1460 The main monitor window.
1462 This class creates, populates and maintains the main monitor window.
1466 * warnings, trace: log models for server output
1467 * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1468 WindowSlot objects for ancillary windows
1469 * ui: Gtk UIManager object for the menu system
1470 * apmenu: pair of identical autoconnecting peer menus
1471 * listmodel: Gtk ListStore for connected peers; contains peer name,
1472 address, and ping times (transport and encrypted, value and colour)
1473 * status: Gtk Statusbar at the bottom of the window
1474 * _kidding: an unpleasant backchannel between the apchange method (which
1475 builds the apmenus) and the menu handler, forced on us by a Gtk
1478 Also installs attributes on Peer objects:
1480 * i: index of peer's entry in listmodel
1481 * win: WindowSlot object for the peer's PeerWindow
1485 """Construct the window."""
1488 MyWindow.__init__(me)
1489 me.set_title('TrIPE monitor')
1491 ## Hook onto diagnostic outputs.
1492 me.warnings = WarningLogModel()
1493 me.hook(conn.warnhook, me.warnings.notify)
1494 me.trace = TraceLogModel()
1495 me.hook(conn.tracehook, me.trace.notify)
1497 ## Make slots to store the various ancillary singleton windows.
1498 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1499 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1500 me.traceopts = WindowSlot(lambda: TraceOptions())
1501 me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1502 me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1503 me.servinfo = WindowSlot(lambda: ServInfo())
1505 ## Main window structure.
1509 ## UI manager makes our menus. (We're too cheap to have a toolbar.)
1510 me.ui = G.UIManager()
1511 actgroup = makeactiongroup('monitor',
1512 [('file-menu', '_File', None, None),
1513 ('connect', '_Connect', '<Control>C', conn.connect),
1514 ('disconnect', '_Disconnect', '<Control>D',
1515 lambda: conn.disconnect(None)),
1516 ('quit', '_Quit', '<Control>Q', me.close),
1517 ('server-menu', '_Server', None, None),
1518 ('daemon', 'Run in _background', None, cr(conn.daemon)),
1519 ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1520 ('crypto-algs', 'Cryptographic algorithms',
1521 '<Control>Y', me.cryptoinfo.open),
1522 ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1523 ('server-quit', 'Terminate server', None, cr(conn.quit)),
1524 ('conn-peer', 'Connect peer', None, None),
1525 ('logs-menu', '_Logs', None, None),
1526 ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1527 ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1528 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1529 ('help-menu', '_Help', None, None),
1530 ('about', '_About tripemon...', None, aboutbox.open),
1531 ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1532 ('kill-peer', '_Kill peer', None, me.killpeer),
1533 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1539 <menu action="file-menu">
1540 <menuitem action="quit"/>
1542 <menu action="server-menu">
1543 <menuitem action="connect"/>
1544 <menuitem action="disconnect"/>
1546 <menuitem action="server-version"/>
1547 <menuitem action="crypto-algs"/>
1548 <menuitem action="add-peer"/>
1549 <menuitem action="conn-peer"/>
1550 <menuitem action="daemon"/>
1551 <menuitem action="reload-keys"/>
1553 <menuitem action="server-quit"/>
1555 <menu action="logs-menu">
1556 <menuitem action="show-warnings"/>
1557 <menuitem action="show-trace"/>
1558 <menuitem action="trace-options"/>
1560 <menu action="help-menu">
1561 <menuitem action="about"/>
1564 <popup name="peer-popup">
1565 <menuitem action="add-peer"/>
1566 <menuitem action="conn-peer"/>
1567 <menuitem action="kill-peer"/>
1568 <menuitem action="force-kx"/>
1573 ## Populate the UI manager.
1574 me.ui.insert_action_group(actgroup, 0)
1575 me.ui.add_ui_from_string(uidef)
1577 ## Construct the menu bar.
1578 vbox.pack_start(me.ui.get_widget('/menubar'), False, True, 0)
1579 me.add_accel_group(me.ui.get_accel_group())
1581 ## Construct and attach the auto-peers menu. (This is a horrible bodge
1582 ## because we can't attach the same submenu in two different places.)
1583 me.apmenu = G.Menu(), G.Menu()
1584 me.ui.get_widget('/menubar/server-menu/conn-peer') \
1585 .set_submenu(me.apmenu[0])
1586 me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1588 ## Construct the main list model, and listen on hooks which report
1589 ## changes to the available peers.
1590 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1591 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1592 me.hook(monitor.peers.addhook, me.addpeer)
1593 me.hook(monitor.peers.delhook, me.delpeer)
1594 me.hook(monitor.autopeershook, me.apchange)
1596 ## Construct the list viewer and put it in a scrolling window.
1597 scr = MyScrolledWindow()
1598 me.list = MyTreeView(me.listmodel)
1599 me.list.append_column(G.TreeViewColumn('Peer name',
1600 G.CellRendererText(),
1602 me.list.append_column(G.TreeViewColumn('Address',
1603 G.CellRendererText(),
1605 me.list.append_column(G.TreeViewColumn('T-ping',
1606 G.CellRendererText(),
1609 me.list.append_column(G.TreeViewColumn('E-ping',
1610 G.CellRendererText(),
1613 me.list.get_column(1).set_expand(True)
1614 me.list.connect('row-activated', me.activate)
1615 me.list.connect('button-press-event', me.buttonpress)
1616 me.list.set_reorderable(True)
1617 me.list.get_selection().set_mode(G.SELECTION_NONE)
1619 vbox.pack_start(scr, True, True, 0)
1621 ## Construct the status bar, and listen on hooks which report changes to
1622 ## connection status.
1623 me.status = G.Statusbar()
1624 vbox.pack_start(me.status, False, True, 0)
1625 me.hook(conn.connecthook, cr(me.connected))
1626 me.hook(conn.disconnecthook, me.disconnected)
1627 me.hook(conn.notehook, me.notify)
1629 ## Set a plausible default window size.
1630 me.set_default_size(512, 180)
1632 def addpeer(me, peer):
1633 """Hook: announces that PEER has been added."""
1634 peer.i = me.listmodel.append([peer.name, peer.addr,
1635 '???', 'green', '???', 'green'])
1636 peer.win = WindowSlot(lambda: PeerWindow(peer))
1637 me.hook(peer.pinghook, me._ping)
1640 def delpeer(me, peer):
1641 """Hook: announces that PEER has been removed."""
1642 me.listmodel.remove(peer.i)
1643 me.unhook(peer.pinghook)
1646 def path_peer(me, path):
1647 """Return the peer corresponding to a given list-model PATH."""
1648 return monitor.peers[me.listmodel[path][0]]
1652 Hook: announces that a change has been made to the peers available for
1653 automated connection.
1655 This populates both auto-peer menus and keeps them in sync. (As
1656 mentioned above, we can't attach the same submenu to two separate parent
1657 menu items. So we end up with two identical menus instead. Yes, this
1661 ## The set_active method of a CheckMenuItem works by maybe activating the
1662 ## menu item. This signals our handler. But we don't actually want to
1663 ## signal the handler unless the user actually frobbed the item. So the
1664 ## _kidding flag is used as an underhanded way of telling the handler
1665 ## that we don't actually want it to do anything. Of course, this sucks
1669 ## Iterate over the two menus.
1672 existing = menu.get_children()
1673 if monitor.autopeers is None:
1675 ## No peers, so empty out the menu.
1676 for item in existing:
1681 ## Insert the new items into the menu. (XXX this seems buggy XXX)
1682 ## Tick the peers which are actually connected.
1684 for peer in monitor.autopeers:
1685 if j < len(existing) and \
1686 existing[j].get_child().get_text() == peer:
1690 item = G.CheckMenuItem(peer, use_underline = False)
1691 item.connect('activate', invoker(me._addautopeer, peer))
1692 menu.insert(item, i)
1693 item.set_active(peer in monitor.peers.table)
1696 ## Make all the menu items visible.
1699 ## Set the parent menu items sensitive if and only if there are any peers
1701 for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1702 me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1704 ## And now allow the handler to do its business normally.
1707 def _addautopeer(me, peer):
1709 Automatically connect an auto-peer.
1711 This method is invoked from the main coroutine. Since the actual
1712 connection needs to issue administration commands, we must spawn a new
1713 child coroutine for it.
1717 T.Coroutine(me._addautopeer_hack,
1718 name = '_addautopeerhack %s' % peer).switch(peer)
1720 def _addautopeer_hack(me, peer):
1721 """Make an automated connection to PEER in response to a user click."""
1725 T._simple(conn.svcsubmit('connect', 'active', peer))
1726 except T.TripeError, exc:
1727 T.defer(moanbox, ' '.join(exc.args))
1730 def activate(me, l, path, col):
1732 Handle a double-click on a peer in the main list: open a PeerInfo window.
1734 peer = me.path_peer(path)
1737 def buttonpress(me, l, ev):
1739 Handle a mouse click on the main list.
1741 Currently we're only interested in button-3, which pops up the peer menu.
1742 For future reference, we stash the peer that was clicked in me.menupeer.
1745 x, y = int(ev.x), int(ev.y)
1746 r = me.list.get_path_at_pos(x, y)
1747 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1748 me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1750 me.ui.get_widget('/peer-popup/conn-peer'). \
1751 set_sensitive(bool(monitor.autopeers))
1753 me.menupeer = me.path_peer(r[0])
1756 me.ui.get_widget('/peer-popup').popup(
1757 None, None, None, ev.button, ev.time)
1760 """Kill a peer from the popup menu."""
1761 cr(conn.kill, me.menupeer.name)()
1764 """Kickstart a key-exchange from the popup menu."""
1765 cr(conn.forcekx, me.menupeer.name)()
1767 _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1768 def _ping(me, p, cmd, ps):
1769 """Hook: responds to ping reports."""
1770 textcol, colourcol = me._columnmap[cmd]
1772 me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1773 me.listmodel[p.i][colourcol] = 'red'
1775 me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1776 me.listmodel[p.i][colourcol] = 'black'
1778 def setstatus(me, status):
1779 """Update the message in the status bar."""
1781 me.status.push(0, status)
1783 def notify(me, note, *rest):
1784 """Hook: invoked when interesting notifications occur."""
1785 if note == 'DAEMON':
1786 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1790 Hook: invoked when a connection is made to the server.
1792 Make options which require a server connection sensitive.
1794 me.setstatus('Connected (port %s)' % conn.port())
1795 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1796 for i in ('/menubar/server-menu/disconnect',
1797 '/menubar/server-menu/server-version',
1798 '/menubar/server-menu/add-peer',
1799 '/menubar/server-menu/server-quit',
1800 '/menubar/logs-menu/trace-options'):
1801 me.ui.get_widget(i).set_sensitive(True)
1802 me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1803 set_sensitive(bool(monitor.autopeers))
1804 me.ui.get_widget('/menubar/server-menu/daemon'). \
1805 set_sensitive(conn.servinfo()['daemon'] == 'nil')
1807 def disconnected(me, reason):
1809 Hook: invoked when the connection to the server is lost.
1811 Make most options insensitive.
1813 me.setstatus('Disconnected')
1814 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1815 for i in ('/menubar/server-menu/disconnect',
1816 '/menubar/server-menu/server-version',
1817 '/menubar/server-menu/add-peer',
1818 '/menubar/server-menu/conn-peer',
1819 '/menubar/server-menu/daemon',
1820 '/menubar/server-menu/server-quit',
1821 '/menubar/logs-menu/trace-options'):
1822 me.ui.get_widget(i).set_sensitive(False)
1823 if reason: moanbox(reason)
1825 ###--------------------------------------------------------------------------
1828 def parse_options():
1830 Parse command-line options.
1832 Process the boring ones. Return all of them, for later.
1834 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1835 version = '%prog (tripe version 1.0.0)')
1836 op.add_option('-a', '--admin-socket',
1837 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1838 help = 'Select socket to connect to [default %default]')
1839 op.add_option('-d', '--directory',
1840 metavar = 'DIR', dest = 'dir', default = T.configdir,
1841 help = 'Select current diretory [default %default]')
1842 opts, args = op.parse_args()
1843 if args: op.error('no arguments permitted')
1848 """Initialization."""
1850 global conn, monitor, pinger
1852 ## Try to establish a connection.
1853 conn = Connection(opts.tripesock)
1855 ## Make the main interesting coroutines and objects.
1863 root = MonitorWindow()
1868 HookClient().hook(root.closehook, exit)
1871 if __name__ == '__main__':
1872 opts = parse_options()
1876 ###----- That's all, folks --------------------------------------------------