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] in ['INET', 'INET6']:
327 af, ipaddr, port = addr
329 name, _ = S.getnameinfo((ipaddr, int(port)),
330 S.NI_NUMERICSERV | S.NI_NAMEREQD)
332 me.addr = '%s %s%s%s:%s' % (af,
333 af == 'INET6' and '[' or '',
335 af == 'INET6' and ']' or '',
338 me.addr = '%s %s:%s [%s]' % (af, name, port, ipaddr)
340 me.addr = ' '.join(addr)
342 def setaddr(me, addr):
343 """Informs the object of a change to its address to ADDR."""
347 def setifname(me, newname):
348 """Informs the object of a change to its interface name to NEWNAME."""
352 class Service (MonitorObject):
354 Represents a service.
356 Additional attributes are:
358 * version = the service version
360 def __init__(me, name, version):
361 MonitorObject.__init__(me, name)
364 def update(me, version):
365 """Tell the Service that its version has changed to VERSION."""
369 class MonitorList (object):
371 Maintains a collection of MonitorObjects.
373 The MonitorList can be indexed by name to retrieve the individual objects;
374 iteration generates the individual objects. More complicated operations
375 can be done on the `table' dictionary directly.
377 Hooks addhook(OBJ) and delhook(OBJ) are invoked when objects are added or
380 Subclass responsibilities:
382 * list(): return a list of (NAME, INFO) pairs.
384 * make(NAME, INFO): returns a new MonitorObject for the given NAME; INFO
385 is from the output of list().
389 """Initialize a new MonitorList."""
391 me.addhook = HookList()
392 me.delhook = HookList()
396 Refresh the list of objects:
398 We add new object which have appeared, delete ones which have vanished,
399 and update any which persist.
402 for name, stuff in me.list():
405 for name in me.table.copy():
409 def add(me, name, stuff):
411 Add a new object created by make(NAME, STUFF) if it doesn't already
412 exist. If it does, update it.
414 if name not in me.table:
415 obj = me.make(name, stuff)
419 me.table[name].update(stuff)
421 def remove(me, name):
423 Remove the object called NAME from the list.
425 The object becomes dead.
433 def __getitem__(me, name):
434 """Retrieve the object called NAME."""
435 return me.table[name]
438 """Iterate over the objects."""
439 return me.table.itervalues()
441 class PeerList (MonitorList):
442 """The list of the known peers."""
444 return [(name, None) for name in conn.list()]
445 def make(me, name, stuff):
448 class ServiceList (MonitorList):
449 """The list of the registered services."""
451 return conn.svclist()
452 def make(me, name, stuff):
453 return Service(name, stuff)
455 class Monitor (HookClient):
457 The main monitor: keeps track of the changes happening to the server.
459 Exports the peers, services MonitorLists, and a (plain Python) list
460 autopeers of peers which the connect service knows how to start by name.
464 * autopeershook(): invoked when the auto-peers list changes.
467 """Initialize the Monitor."""
468 HookClient.__init__(me)
469 me.peers = PeerList()
470 me.services = ServiceList()
471 me.hook(conn.connecthook, me._connected)
472 me.hook(conn.notehook, me._notify)
473 me.autopeershook = HookList()
477 """Handle a successful connection by starting the setup coroutine."""
482 """Coroutine function: initialize for a new connection."""
486 me._updateautopeers()
488 def _updateautopeers(me):
489 """Update the auto-peers list from the connect service."""
490 if 'connect' in me.services.table:
491 me.autopeers = [' '.join(line)
492 for line in conn.svcsubmit('connect', 'list-active')]
496 me.autopeershook.run()
498 def _notify(me, code, *rest):
500 Handle notifications from the server.
502 ADD, KILL and NEWIFNAME notifications get passed up to the PeerList;
503 SVCCLAIM and SVCRELEASE get passed up to the ServiceList. Finally,
504 peerdb-update notifications from the watch service cause us to refresh
508 T.aside(me.peers.add, rest[0], None)
510 T.aside(me.peers.remove, rest[0])
511 elif code == 'NEWIFNAME':
513 me.peers[rest[0]].setifname(rest[2])
516 elif code == 'NEWADDR':
518 me.peers[rest[0]].setaddr(rest[1:])
521 elif code == 'SVCCLAIM':
522 T.aside(me.services.add, rest[0], rest[1])
523 if rest[0] == 'connect':
524 T.aside(me._updateautopeers)
525 elif code == 'SVCRELEASE':
526 T.aside(me.services.remove, rest[0])
527 if rest[0] == 'connect':
528 T.aside(me._updateautopeers)
531 if rest[0] == 'watch' and \
532 rest[1] == 'peerdb-update':
533 T.aside(me._updateautopeers)
535 ###--------------------------------------------------------------------------
536 ### Window management cruft.
538 class MyWindowMixin (G.Window, HookClient):
540 Mixin for windows which call a closehook when they're destroyed. It's also
541 a hookclient, and will release its hooks when it's destroyed.
545 * closehook(): called when the window is closed.
549 """Initialization function. Note that it's not called __init__!"""
550 me.closehook = HookList()
551 HookClient.__init__(me)
552 me.connect('destroy', invoker(me.close))
555 """Close the window, invoking the closehook and releasing all hooks."""
560 class MyWindow (MyWindowMixin):
561 """A version of MyWindowMixin suitable as a single parent class."""
562 def __init__(me, kind = G.WINDOW_TOPLEVEL):
563 G.Window.__init__(me, kind)
566 class TrivialWindowMixin (MyWindowMixin):
567 """A simple window which you can close with Escape."""
569 super(TrivialWindowMixin, me).mywininit()
570 me.connect('key-press-event', me._keypress)
571 def _keypress(me, _, ev):
572 if ev.keyval == GDK.KEY_Escape: me.destroy()
574 class TrivialWindow (MyWindow, TrivialWindowMixin):
577 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
578 """A dialogue box with a closehook and sensible button binding."""
580 def __init__(me, title = None, flags = 0, buttons = []):
582 The BUTTONS are a list of (STOCKID, THUNK) pairs: call the appropriate
583 THUNK when the button is pressed. The other arguments are just like
594 G.Dialog.__init__(me, title, None, flags, tuple(br))
596 me.set_default_response(i - 1)
597 me.connect('response', me.respond)
599 def respond(me, hunoz, rid, *hukairz):
600 """Dispatch responses to the appropriate thunks."""
601 if rid >= 0: me.rmap[rid]()
603 def makeactiongroup(name, acts):
605 Creates an ActionGroup called NAME.
607 ACTS is a list of tuples containing:
609 * ACT: an action name
610 * LABEL: the label string for the action
611 * ACCEL: accelerator string, or None
612 * FUNC: thunk to call when the action is invoked
614 actgroup = G.ActionGroup(name)
615 for act, label, accel, func in acts:
616 a = G.Action(act, label, None, None)
617 if func: a.connect('activate', invoker(func))
618 actgroup.add_action_with_accel(a, accel)
621 class GridPacker (G.Table):
623 Like a Table, but with more state: makes filling in the widgets easier.
627 """Initialize a new GridPacker."""
633 me.set_border_width(4)
634 me.set_col_spacings(4)
635 me.set_row_spacings(4)
637 def pack(me, w, width = 1, newlinep = False,
638 xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
643 W is the widget to add. XOPY, YOPT, XPAD and YPAD are as for Table.
644 WIDTH is how many cells to take up horizontally. NEWLINEP is whether to
645 start a new line for this widget. Returns W.
651 right = me.col + width
652 if bot > me.rows or right > me.cols:
653 if bot > me.rows: me.rows = bot
654 if right > me.cols: me.cols = right
655 me.resize(me.rows, me.cols)
656 me.attach(w, me.col, me.col + width, me.row, me.row + 1,
657 xopt, yopt, xpad, ypad)
661 def labelled(me, lab, w, newlinep = False, **kw):
663 Packs a labelled widget.
665 Other arguments are as for pack. Returns W.
667 label = G.Label(lab + ' ')
668 label.set_alignment(1.0, 0)
669 me.pack(label, newlinep = newlinep, xopt = G.FILL)
673 def info(me, label, text = None, len = 18, **kw):
675 Packs an information widget with a label.
677 LABEL is the label; TEXT is the initial text; LEN is the estimated length
678 in characters. Returns the entry widget.
681 if text is not None: e.set_text(text)
682 e.set_width_chars(len)
683 e.set_selectable(True)
684 e.set_alignment(0.0, 0.5)
685 me.labelled(label, e, **kw)
688 class WindowSlot (HookClient):
690 A place to store a window -- specificially a MyWindowMixin.
692 If the window is destroyed, remember this; when we come to open the window,
693 raise it if it already exists; otherwise make a new one.
695 def __init__(me, createfunc):
697 Constructor: CREATEFUNC must return a new Window which supports the
700 HookClient.__init__(me)
701 me.createfunc = createfunc
705 """Opens the window, creating it if necessary."""
707 raise_window(me.window)
709 me.window = me.createfunc()
710 me.hook(me.window.closehook, me.closed)
713 """Handles the window being closed."""
714 me.unhook(me.window.closehook)
717 class MyTreeView (G.TreeView):
718 def __init__(me, model):
719 G.TreeView.__init__(me, model)
720 me.set_rules_hint(True)
722 class MyScrolledWindow (G.ScrolledWindow):
724 G.ScrolledWindow.__init__(me)
725 me.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
726 me.set_shadow_type(G.SHADOW_IN)
728 ## Matches a signed integer.
729 rx_num = RX.compile(r'^[-+]?\d+$')
732 c_red = GDK.color_parse('#ff6666')
734 class ValidationError (Exception):
735 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
738 class ValidatingEntry (G.Entry):
740 Like an Entry, but makes the text go red if the contents are invalid.
742 If get_text is called, and the text is invalid, ValidationError is raised.
743 The attribute validp reflects whether the contents are currently valid.
746 def __init__(me, valid, text = '', size = -1, *arg, **kw):
748 Make a validating Entry.
750 VALID is a regular expression or a predicate on strings. TEXT is the
751 default text to insert. SIZE is the size of the box to set, in
752 characters (ish). Other arguments are passed to Entry.
754 G.Entry.__init__(me, *arg, **kw)
755 me.connect("changed", me._check)
756 me.connect("state-changed", me._check)
760 me.validate = RX.compile(valid).match
762 if size != -1: me.set_width_chars(size)
763 me.set_activates_default(True)
767 def _check(me, *hunoz):
768 """Check the current text and update validp and the text colour."""
769 if me.validate(G.Entry.get_text(me)):
771 set_entry_bg(me, None)
774 set_entry_bg(me, me.is_sensitive() and c_red or None)
778 Return the text in the Entry if it's valid. If it isn't, raise
782 raise ValidationError()
783 return G.Entry.get_text(me)
785 def numericvalidate(min = None, max = None):
787 Return a validation function for numbers.
789 Entry must consist of an optional sign followed by digits, and the
790 resulting integer must be within the given bounds.
792 return lambda x: (rx_num.match(x) and
793 (min is None or long(x) >= min) and
794 (max is None or long(x) <= max))
796 ###--------------------------------------------------------------------------
797 ### Various minor dialog boxen.
800 TrIPE is free software: you can redistribute it and/or modify it under
801 the terms of the GNU General Public License as published by the Free
802 Software Foundation; either version 3 of the License, or (at your
803 option) any later version.
805 TrIPE is distributed in the hope that it will be useful, but WITHOUT
806 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
807 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
810 You should have received a copy of the GNU General Public License
811 along with TrIPE. If not, see <https://www.gnu.org/licenses/>."""
813 class AboutBox (G.AboutDialog, TrivialWindowMixin):
814 """The program `About' box."""
816 G.AboutDialog.__init__(me)
818 me.set_name('TrIPEmon')
819 me.set_version(T.VERSION)
821 me.set_authors(['Mark Wooding <mdw@distorted.org.uk>'])
822 me.set_comments('A graphical monitor for the TrIPE VPN server')
823 me.set_copyright('Copyright © 2006-2008 Straylight/Edgeware')
824 me.connect('response', me.respond)
826 def respond(me, hunoz, rid, *hukairz):
827 if rid == G.RESPONSE_CANCEL:
829 aboutbox = WindowSlot(AboutBox)
832 """Report an error message in a window."""
833 d = G.Dialog('Error from %s' % M.quis,
834 flags = G.DIALOG_MODAL,
835 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
837 label.set_padding(20, 20)
838 d.vbox.pack_start(label, True, True, 0)
843 def unimplemented(*hunoz):
844 """Indicator of laziness."""
845 moanbox("I've not written that bit yet.")
847 ###--------------------------------------------------------------------------
850 class LogModel (G.ListStore):
852 A simple list of log messages, usable as the model for a TreeView.
854 The column headings are stored in the `cols' attribute.
857 def __init__(me, columns):
859 COLUMNS must be a list of column name strings. We add a time column to
862 me.cols = ('Time',) + columns
863 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
865 def add(me, *entries):
867 Adds a new log message, with a timestamp.
869 The ENTRIES are the contents for the list columns.
871 now = TIME.strftime('%Y-%m-%d %H:%M:%S')
872 me.append((now, ) + entries)
874 class TraceLogModel (LogModel):
875 """Log model for trace messages."""
877 LogModel.__init__(me, ('Message',))
878 def notify(me, line):
879 """Call with a new trace message."""
882 class WarningLogModel (LogModel):
884 Log model for warnings.
886 We split the category out into a separate column.
889 LogModel.__init__(me, ('Category', 'Message'))
890 def notify(me, tag, *rest):
891 """Call with a new warning message."""
892 me.add(tag, ' '.join([T.quotify(w) for w in rest]))
894 class LogViewer (TrivialWindow):
898 Its contents are a TreeView showing the log.
902 * model: an appropriate LogModel
903 * list: a TreeView widget to display the log
906 def __init__(me, model):
908 Create a log viewer showing the LogModel MODEL.
910 TrivialWindow.__init__(me)
912 scr = MyScrolledWindow()
913 me.list = MyTreeView(me.model)
915 for c in me.model.cols:
916 crt = G.CellRendererText()
917 me.list.append_column(G.TreeViewColumn(c, crt, text = i))
919 crt.set_property('family', 'monospace')
920 me.set_default_size(440, 256)
925 ###--------------------------------------------------------------------------
928 class pingstate (struct):
930 Information kept for each peer by the Pinger.
932 Important attributes:
934 * peer = the peer name
935 * command = PING or EPING
936 * n = how many pings we've sent so far
937 * ngood = how many returned
938 * nmiss = how many didn't return
939 * nmissrun = how many pings since the last good one
940 * tlast = round-trip time for the last (good) ping
941 * ttot = total roung trip time
945 class Pinger (T.Coroutine, HookClient):
947 Coroutine which pings known peers and collects statistics.
949 Interesting attributes:
951 * _map: dict mapping peer names to Peer objects
952 * _q: event queue for notifying pinger coroutine
953 * _timer: gobject timer for waking the coroutine
958 Initialize the pinger.
960 We watch the monitor's PeerList to track which peers we should ping. We
961 maintain an event queue and put all the events on that.
963 The statistics for a PEER are held in the Peer object, in PEER.ping[CMD],
964 where CMD is 'PING' or 'EPING'.
966 T.Coroutine.__init__(me)
967 HookClient.__init__(me)
971 me.hook(conn.connecthook, me._connected)
972 me.hook(conn.disconnecthook, me._disconnected)
973 me.hook(monitor.peers.addhook,
974 lambda p: T.defer(me._q.put, (p, 'ADD', None)))
975 me.hook(monitor.peers.delhook,
976 lambda p: T.defer(me._q.put, (p, 'KILL', None)))
977 if conn.connectedp(): me.connected()
980 """Respond to connection: start pinging thngs."""
981 me._timer = GL.timeout_add(1000, me._timerfunc)
984 """Timer function: put a timer event on the queue."""
985 me._q.put((None, 'TIMER', None))
988 def _disconnected(me, reason):
989 """Respond to disconnection: stop pinging."""
990 GL.source_remove(me._timer)
994 Coroutine function: read events from the queue and process them.
998 * (PEER, 'KILL', None): remove PEER from the interesting peers list
999 * (PEER, 'ADD', None): add PEER to the list
1000 * (PEER, 'INFO', TOKENS): result from a PING command
1001 * (None, 'TIMER', None): interval timer went off: send more pings
1004 tag, code, stuff = me._q.get()
1009 elif not conn.connectedp():
1014 for cmd in 'PING', 'EPING':
1015 ps = pingstate(command = cmd, peer = p,
1016 n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1017 tlast = 0, ttot = 0)
1020 elif code == 'INFO':
1022 if stuff[0] == 'ping-ok':
1032 ps.peer.pinghook.run(ps.peer, ps.command, ps)
1033 elif code == 'TIMER':
1034 for name, p in me._map.iteritems():
1035 for cmd, ps in p.ping.iteritems():
1036 conn.rawcommand(T.TripeAsynchronousCommand(me._q, ps, [
1037 cmd, '-background', conn.bgtag(), '--', name]))
1039 ###--------------------------------------------------------------------------
1040 ### Random dialogue boxes.
1042 class AddPeerDialog (MyDialog):
1044 Let the user create a new peer the low-level way.
1046 Interesting attributes:
1048 * e_name, e_addr, e_port, c_keepalive, l_tunnel: widgets in the dialog
1051 AFS = ['ANY', 'INET', 'INET6']
1054 """Initialize the dialogue."""
1055 MyDialog.__init__(me, 'Add peer',
1056 buttons = [(G.STOCK_CANCEL, me.destroy),
1057 (G.STOCK_OK, me.ok)])
1062 """Coroutine function: background setup for AddPeerDialog."""
1063 table = GridPacker()
1064 me.vbox.pack_start(table, True, True, 0)
1065 me.e_name = table.labelled('Name',
1066 ValidatingEntry(r'^[^\s:]+$', '', 16),
1068 me.l_af = table.labelled('Family', combo_box_text(),
1069 newlinep = True, width = 3)
1071 me.l_af.append_text(af)
1072 me.l_af.set_active(0)
1073 me.e_addr = table.labelled('Address',
1074 ValidatingEntry(r'^[a-zA-Z0-9.-:]+$', '', 24),
1076 me.e_port = table.labelled('Port',
1077 ValidatingEntry(numericvalidate(0, 65535),
1080 me.l_tunnel = table.labelled('Tunnel', combo_box_text(),
1081 newlinep = True, width = 3)
1082 me.tuns = ['(Default)'] + conn.tunnels()
1084 me.l_tunnel.append_text(t)
1085 me.l_tunnel.set_active(0)
1087 def tickybox_sensitivity(tickybox, target):
1088 tickybox.connect('toggled',
1089 lambda t: target.set_sensitive (t.get_active()))
1091 def optional_entry(label, rx_valid, width):
1092 c = G.CheckButton(label)
1093 table.pack(c, newlinep = True, xopt = G.FILL)
1094 e = ValidatingEntry(rx_valid, '', width)
1095 e.set_sensitive(False)
1096 tickybox_sensitivity(c, e)
1097 table.pack(e, width = 3)
1100 me.c_keepalive, me.e_keepalive = \
1101 optional_entry('Keepalives', r'^\d+[hms]?$', 5)
1103 me.c_cork = G.CheckButton('Cork')
1104 table.pack(me.c_cork, newlinep = True, width = 4, xopt = G.FILL)
1106 me.c_mobile = G.CheckButton('Mobile')
1107 table.pack(me.c_mobile, newlinep = True, width = 4, xopt = G.FILL)
1109 me.c_ephem = G.CheckButton('Ephemeral')
1110 table.pack(me.c_ephem, newlinep = True, width = 4, xopt = G.FILL)
1112 me.c_peerkey, me.e_peerkey = \
1113 optional_entry('Peer key tag', r'^[^.:\s]+$', 16)
1114 me.c_privkey, me.e_privkey = \
1115 optional_entry('Private key tag', r'^[^.:\s]+$', 16)
1117 me.c_knock, me.e_knock = \
1118 optional_entry('Knock string', r'^[^:\s]+$', 16)
1123 """Handle an OK press: create the peer."""
1125 t = me.l_tunnel.get_active()
1126 afix = me.l_af.get_active()
1127 me._addpeer(me.e_name.get_text(),
1129 me.e_addr.get_text(),
1130 me.e_port.get_text(),
1131 keepalive = (me.c_keepalive.get_active() and
1132 me.e_keepalive.get_text() or None),
1133 tunnel = t and me.tuns[t] or None,
1134 cork = me.c_cork.get_active() or None,
1135 mobile = me.c_mobile.get_active() or None,
1136 ephemeral = me.c_ephem.get_active() or None,
1137 key = (me.c_peerkey.get_active() and
1138 me.e_peerkey.get_text() or None),
1139 priv = (me.c_privkey.get_active() and
1140 me.e_privkey.get_text() or None),
1141 knock = (me.c_knock.get_active() and
1142 me.e_knock.get_text() or None))
1143 except ValidationError:
1148 def _addpeer(me, *args, **kw):
1149 """Coroutine function: actually do the ADD command."""
1151 conn.add(*args, **kw)
1153 except T.TripeError, exc:
1154 T.defer(moanbox, ' '.join(exc))
1156 class ServInfo (TrivialWindow):
1158 Show information about the server and available services.
1160 Interesting attributes:
1162 * e: maps SERVINFO keys to entry widgets
1163 * svcs: Gtk ListStore describing services (columns are name and version)
1167 TrivialWindow.__init__(me)
1168 me.set_title('TrIPE server info')
1169 table = GridPacker()
1172 def add(label, tag, text = None, **kw):
1173 me.e[tag] = table.info(label, text, **kw)
1174 add('Implementation', 'implementation')
1175 add('Version', 'version', newlinep = True)
1176 me.svcs = G.ListStore(*(GO.TYPE_STRING,) * 2)
1177 me.svcs.set_sort_column_id(0, G.SORT_ASCENDING)
1178 scr = MyScrolledWindow()
1179 lb = MyTreeView(me.svcs)
1181 for title in 'Service', 'Version':
1182 lb.append_column(G.TreeViewColumn(
1183 title, G.CellRendererText(), text = i))
1185 for svc in monitor.services:
1186 me.svcs.append([svc.name, svc.version])
1188 table.pack(scr, width = 2, newlinep = True,
1189 yopt = G.EXPAND | G.FILL | G.SHRINK)
1191 me.hook(conn.connecthook, me.update)
1192 me.hook(monitor.services.addhook, me.addsvc)
1193 me.hook(monitor.services.delhook, me.delsvc)
1196 def addsvc(me, svc):
1197 me.svcs.append([svc.name, svc.version])
1199 def delsvc(me, svc):
1200 for i in xrange(len(me.svcs)):
1201 if me.svcs[i][0] == svc.name:
1202 me.svcs.remove(me.svcs.get_iter(i))
1206 info = conn.servinfo()
1208 me.e[i].set_text(info[i])
1210 class TraceOptions (MyDialog):
1211 """Tracing options window."""
1213 MyDialog.__init__(me, title = 'Tracing options',
1214 buttons = [(G.STOCK_CLOSE, me.destroy),
1215 (G.STOCK_OK, cr(me.ok))])
1221 for ch, st, desc in conn.trace():
1222 if ch.isupper(): continue
1223 text = desc[0].upper() + desc[1:]
1224 ticky = G.CheckButton(text)
1225 ticky.set_active(st == '+')
1226 me.vbox.pack_start(ticky, True, True, 0)
1227 me.opts.append((ch, ticky))
1232 for ch, ticky in me.opts:
1233 if ticky.get_active():
1237 setting = ''.join(on) + '-' + ''.join(off)
1241 ###--------------------------------------------------------------------------
1245 """Translate a TrIPE-format time to something human-readable."""
1246 if t == 'NEVER': return '(never)'
1247 YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
1248 ago = TIME.time() - TIME.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
1249 ago = MATH.floor(ago); unit = 's'
1250 for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
1254 return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
1255 (YY, MM, DD, hh, mm, ss, ago, unit)
1257 """Translate a raw byte count into something a human might want to read."""
1264 return '%d %s' % (b, suff)
1266 ## How to translate peer stats. Maps the stat name to a translation
1269 [('start-time', xlate_time),
1270 ('last-packet-time', xlate_time),
1271 ('last-keyexch-time', xlate_time),
1272 ('bytes-in', xlate_bytes),
1273 ('bytes-out', xlate_bytes),
1274 ('keyexch-bytes-in', xlate_bytes),
1275 ('keyexch-bytes-out', xlate_bytes),
1276 ('ip-bytes-in', xlate_bytes),
1277 ('ip-bytes-out', xlate_bytes)]
1279 def format_stat(format, dict):
1280 if callable(format): return format(dict)
1281 else: return format % dict
1283 ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
1284 ## the label to give the entry box; FORMAT is the format string to write into
1287 [('Diffie-Hellman group',
1289 '(%(kx-group-order-bits)s-bit order, '
1290 '%(kx-group-elt-bits)s-bit elements)'),
1291 ('Bulk crypto transform',
1292 '%(bulk-transform)s (%(bulk-overhead)s byte overhead)'),
1293 ('Data encryption', lambda d: '%s (%s; %s)' % (
1295 '%d-bit key' % (8*int(d['cipher-keysz'])),
1296 d.get('cipher-blksz', '0') == '0'
1298 or '%d-bit block' % (8*int(d['cipher-blksz'])))),
1299 ('Message authentication', lambda d: '%s (%s; %s)' % (
1301 d.get('mac-keysz') is None
1303 or '%d-bit key' % (8*int(d['mac-keysz'])),
1304 '%d-bit tag' % (8*int(d['mac-tagsz'])))),
1305 ('Hash', lambda d: '%s (%d-bit output)' %
1306 (d['hash'], 8*int(d['hash-sz'])))]
1309 [('Start time', '%(start-time)s'),
1310 ('Private key', '%(current-key)s')] + \
1312 [('Last key-exchange', '%(last-keyexch-time)s'),
1313 ('Last packet', '%(last-packet-time)s'),
1315 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
1316 ('Key-exchange in/out',
1317 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
1319 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-out)s (%(ip-bytes-out)s)'),
1320 ('Rejected packets', '%(rejected-packets)s')]
1322 class PeerWindow (TrivialWindow):
1324 Show information about a peer.
1326 This gives a graphical view of the server's peer statistics.
1328 Interesting attributes:
1330 * e: dict mapping keys (mostly matching label widget texts, though pings
1331 use command names) to entry widgets so that we can update them easily
1332 * peer: the peer this window shows information about
1333 * cr: the info-fetching coroutine, or None if crrrently disconnected
1334 * doupate: whether the info-fetching corouting should continue running
1337 def __init__(me, peer):
1338 """Construct a PeerWindow, showing information about PEER."""
1340 TrivialWindow.__init__(me)
1341 me.set_title('TrIPE statistics: %s' % peer.name)
1344 table = GridPacker()
1347 ## Utility for adding fields.
1349 def add(label, text = None, key = None):
1350 if key is None: key = label
1351 me.e[key] = table.info(label, text, len = 42, newlinep = True)
1353 ## Build the dialogue box.
1354 add('Peer name', peer.name)
1355 add('Tunnel', peer.tunnel)
1356 add('Interface', peer.ifname)
1358 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
1359 add('Address', peer.addr)
1360 add('Transport pings', key = 'PING')
1361 add('Encrypted pings', key = 'EPING')
1363 for label, format in statslayout:
1366 ## Hook onto various interesting events.
1367 me.hook(conn.connecthook, me.tryupdate)
1368 me.hook(conn.disconnecthook, me.stopupdate)
1369 me.hook(me.closehook, me.stopupdate)
1370 me.hook(me.peer.deadhook, me.dead)
1371 me.hook(me.peer.changehook, me.change)
1372 me.hook(me.peer.pinghook, me.ping)
1377 ## Format the ping statistics.
1378 for cmd, ps in me.peer.ping.iteritems():
1379 me.ping(me.peer, cmd, ps)
1381 ## And show the window.
1385 """Update the display in response to a notification."""
1386 me.e['Interface'].set_text(me.peer.ifname)
1387 me.e['Address'].set_text(me.peer.addr)
1391 Main display-updating coroutine.
1393 This does an update, sleeps for a while, and starts again. If the
1394 me.doupdate flag goes low, we stop the loop.
1396 while me.peer.alivep and conn.connectedp() and me.doupdate:
1397 stat = conn.stats(me.peer.name)
1398 for s, trans in statsxlate:
1399 stat[s] = trans(stat[s])
1400 stat.update(me.peer.__dict__)
1401 for label, format in statslayout:
1402 me.e[label].set_text(format_stat(format, stat))
1403 GL.timeout_add(1000, lambda: me.cr.switch() and False)
1404 me.cr.parent.switch()
1408 """Start the updater coroutine, if it's not going already."""
1410 me.cr = T.Coroutine(me._update,
1411 name = 'update-peer-window %s' % me.peer.name)
1414 def stopupdate(me, *hunoz, **hukairz):
1415 """Stop the update coroutine, by setting me.doupdate."""
1419 """Called when the peer is killed."""
1420 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
1421 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
1424 def ping(me, peer, cmd, ps):
1425 """Called when a ping result for the peer is reported."""
1426 s = '%d/%d' % (ps.ngood, ps.n)
1428 s += ' (%.1f%%)' % (ps.ngood * 100.0/ps.n)
1430 s += '; %.2f ms (last %.1f ms)' % (ps.ttot/ps.ngood, ps.tlast);
1431 me.e[ps.command].set_text(s)
1433 ###--------------------------------------------------------------------------
1434 ### Cryptographic status.
1436 class CryptoInfo (TrivialWindow):
1437 """Simple display of cryptographic algorithms in use."""
1439 TrivialWindow.__init__(me)
1440 me.set_title('Cryptographic algorithms')
1441 T.aside(me.populate)
1443 table = GridPacker()
1446 crypto = conn.algs()
1448 for label, format in cryptolayout:
1449 table.info(label, format_stat(format, crypto),
1450 len = 42, newlinep = not firstp)
1455 ###--------------------------------------------------------------------------
1456 ### Main monitor window.
1458 class MonitorWindow (MyWindow):
1461 The main monitor window.
1463 This class creates, populates and maintains the main monitor window.
1467 * warnings, trace: log models for server output
1468 * warnview, traceview, traceopts, addpeerwin, cryptoinfo, servinfo:
1469 WindowSlot objects for ancillary windows
1470 * ui: Gtk UIManager object for the menu system
1471 * apmenu: pair of identical autoconnecting peer menus
1472 * listmodel: Gtk ListStore for connected peers; contains peer name,
1473 address, and ping times (transport and encrypted, value and colour)
1474 * status: Gtk Statusbar at the bottom of the window
1475 * _kidding: an unpleasant backchannel between the apchange method (which
1476 builds the apmenus) and the menu handler, forced on us by a Gtk
1479 Also installs attributes on Peer objects:
1481 * i: index of peer's entry in listmodel
1482 * win: WindowSlot object for the peer's PeerWindow
1486 """Construct the window."""
1489 MyWindow.__init__(me)
1490 me.set_title('TrIPE monitor')
1492 ## Hook onto diagnostic outputs.
1493 me.warnings = WarningLogModel()
1494 me.hook(conn.warnhook, me.warnings.notify)
1495 me.trace = TraceLogModel()
1496 me.hook(conn.tracehook, me.trace.notify)
1498 ## Make slots to store the various ancillary singleton windows.
1499 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1500 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1501 me.traceopts = WindowSlot(lambda: TraceOptions())
1502 me.addpeerwin = WindowSlot(lambda: AddPeerDialog())
1503 me.cryptoinfo = WindowSlot(lambda: CryptoInfo())
1504 me.servinfo = WindowSlot(lambda: ServInfo())
1506 ## Main window structure.
1510 ## UI manager makes our menus. (We're too cheap to have a toolbar.)
1511 me.ui = G.UIManager()
1512 actgroup = makeactiongroup('monitor',
1513 [('file-menu', '_File', None, None),
1514 ('connect', '_Connect', '<Control>C', conn.connect),
1515 ('disconnect', '_Disconnect', '<Control>D',
1516 lambda: conn.disconnect(None)),
1517 ('quit', '_Quit', '<Control>Q', me.close),
1518 ('server-menu', '_Server', None, None),
1519 ('daemon', 'Run in _background', None, cr(conn.daemon)),
1520 ('server-version', 'Server version', '<Control>V', me.servinfo.open),
1521 ('crypto-algs', 'Cryptographic algorithms',
1522 '<Control>Y', me.cryptoinfo.open),
1523 ('reload-keys', 'Reload keys', '<Control>R', cr(conn.reload)),
1524 ('server-quit', 'Terminate server', None, cr(conn.quit)),
1525 ('conn-peer', 'Connect peer', None, None),
1526 ('logs-menu', '_Logs', None, None),
1527 ('show-warnings', 'Show _warnings', '<Control>W', me.warnview.open),
1528 ('show-trace', 'Show _trace', '<Control>T', me.traceview.open),
1529 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1530 ('help-menu', '_Help', None, None),
1531 ('about', '_About tripemon...', None, aboutbox.open),
1532 ('add-peer', '_Add peer...', '<Control>A', me.addpeerwin.open),
1533 ('kill-peer', '_Kill peer', None, me.killpeer),
1534 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1540 <menu action="file-menu">
1541 <menuitem action="quit"/>
1543 <menu action="server-menu">
1544 <menuitem action="connect"/>
1545 <menuitem action="disconnect"/>
1547 <menuitem action="server-version"/>
1548 <menuitem action="crypto-algs"/>
1549 <menuitem action="add-peer"/>
1550 <menuitem action="conn-peer"/>
1551 <menuitem action="daemon"/>
1552 <menuitem action="reload-keys"/>
1554 <menuitem action="server-quit"/>
1556 <menu action="logs-menu">
1557 <menuitem action="show-warnings"/>
1558 <menuitem action="show-trace"/>
1559 <menuitem action="trace-options"/>
1561 <menu action="help-menu">
1562 <menuitem action="about"/>
1565 <popup name="peer-popup">
1566 <menuitem action="add-peer"/>
1567 <menuitem action="conn-peer"/>
1568 <menuitem action="kill-peer"/>
1569 <menuitem action="force-kx"/>
1574 ## Populate the UI manager.
1575 me.ui.insert_action_group(actgroup, 0)
1576 me.ui.add_ui_from_string(uidef)
1578 ## Construct the menu bar.
1579 vbox.pack_start(me.ui.get_widget('/menubar'), False, True, 0)
1580 me.add_accel_group(me.ui.get_accel_group())
1582 ## Construct and attach the auto-peers menu. (This is a horrible bodge
1583 ## because we can't attach the same submenu in two different places.)
1584 me.apmenu = G.Menu(), G.Menu()
1585 me.ui.get_widget('/menubar/server-menu/conn-peer') \
1586 .set_submenu(me.apmenu[0])
1587 me.ui.get_widget('/peer-popup/conn-peer').set_submenu(me.apmenu[1])
1589 ## Construct the main list model, and listen on hooks which report
1590 ## changes to the available peers.
1591 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1592 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1593 me.hook(monitor.peers.addhook, me.addpeer)
1594 me.hook(monitor.peers.delhook, me.delpeer)
1595 me.hook(monitor.autopeershook, me.apchange)
1597 ## Construct the list viewer and put it in a scrolling window.
1598 scr = MyScrolledWindow()
1599 me.list = MyTreeView(me.listmodel)
1600 me.list.append_column(G.TreeViewColumn('Peer name',
1601 G.CellRendererText(),
1603 me.list.append_column(G.TreeViewColumn('Address',
1604 G.CellRendererText(),
1606 me.list.append_column(G.TreeViewColumn('T-ping',
1607 G.CellRendererText(),
1610 me.list.append_column(G.TreeViewColumn('E-ping',
1611 G.CellRendererText(),
1614 me.list.get_column(1).set_expand(True)
1615 me.list.connect('row-activated', me.activate)
1616 me.list.connect('button-press-event', me.buttonpress)
1617 me.list.set_reorderable(True)
1618 me.list.get_selection().set_mode(G.SELECTION_NONE)
1620 vbox.pack_start(scr, True, True, 0)
1622 ## Construct the status bar, and listen on hooks which report changes to
1623 ## connection status.
1624 me.status = G.Statusbar()
1625 vbox.pack_start(me.status, False, True, 0)
1626 me.hook(conn.connecthook, cr(me.connected))
1627 me.hook(conn.disconnecthook, me.disconnected)
1628 me.hook(conn.notehook, me.notify)
1630 ## Set a plausible default window size.
1631 me.set_default_size(512, 180)
1633 def addpeer(me, peer):
1634 """Hook: announces that PEER has been added."""
1635 peer.i = me.listmodel.append([peer.name, peer.addr,
1636 '???', 'green', '???', 'green'])
1637 peer.win = WindowSlot(lambda: PeerWindow(peer))
1638 me.hook(peer.pinghook, me._ping)
1639 me.hook(peer.changehook, lambda: me._change(peer))
1642 def delpeer(me, peer):
1643 """Hook: announces that PEER has been removed."""
1644 me.listmodel.remove(peer.i)
1645 me.unhook(peer.pinghook)
1648 def path_peer(me, path):
1649 """Return the peer corresponding to a given list-model PATH."""
1650 return monitor.peers[me.listmodel[path][0]]
1654 Hook: announces that a change has been made to the peers available for
1655 automated connection.
1657 This populates both auto-peer menus and keeps them in sync. (As
1658 mentioned above, we can't attach the same submenu to two separate parent
1659 menu items. So we end up with two identical menus instead. Yes, this
1663 ## The set_active method of a CheckMenuItem works by maybe activating the
1664 ## menu item. This signals our handler. But we don't actually want to
1665 ## signal the handler unless the user actually frobbed the item. So the
1666 ## _kidding flag is used as an underhanded way of telling the handler
1667 ## that we don't actually want it to do anything. Of course, this sucks
1671 ## Iterate over the two menus.
1674 existing = menu.get_children()
1675 if monitor.autopeers is None:
1677 ## No peers, so empty out the menu.
1678 for item in existing:
1683 ## Insert the new items into the menu. (XXX this seems buggy XXX)
1684 ## Tick the peers which are actually connected.
1686 for peer in monitor.autopeers:
1687 if j < len(existing) and \
1688 existing[j].get_child().get_text() == peer:
1692 item = G.CheckMenuItem(peer, use_underline = False)
1693 item.connect('activate', invoker(me._addautopeer, peer))
1694 menu.insert(item, i)
1695 item.set_active(peer in monitor.peers.table)
1698 ## Make all the menu items visible.
1701 ## Set the parent menu items sensitive if and only if there are any peers
1703 for name in ['/menubar/server-menu/conn-peer', '/peer-popup/conn-peer']:
1704 me.ui.get_widget(name).set_sensitive(bool(monitor.autopeers))
1706 ## And now allow the handler to do its business normally.
1709 def _addautopeer(me, peer):
1711 Automatically connect an auto-peer.
1713 This method is invoked from the main coroutine. Since the actual
1714 connection needs to issue administration commands, we must spawn a new
1715 child coroutine for it.
1719 T.Coroutine(me._addautopeer_hack,
1720 name = '_addautopeerhack %s' % peer).switch(peer)
1722 def _addautopeer_hack(me, peer):
1723 """Make an automated connection to PEER in response to a user click."""
1727 T._simple(conn.svcsubmit('connect', 'active', peer))
1728 except T.TripeError, exc:
1729 T.defer(moanbox, ' '.join(exc.args))
1732 def activate(me, l, path, col):
1734 Handle a double-click on a peer in the main list: open a PeerInfo window.
1736 peer = me.path_peer(path)
1739 def buttonpress(me, l, ev):
1741 Handle a mouse click on the main list.
1743 Currently we're only interested in button-3, which pops up the peer menu.
1744 For future reference, we stash the peer that was clicked in me.menupeer.
1747 x, y = int(ev.x), int(ev.y)
1748 r = me.list.get_path_at_pos(x, y)
1749 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1750 me.ui.get_widget(i).set_sensitive(conn.connectedp() and
1752 me.ui.get_widget('/peer-popup/conn-peer'). \
1753 set_sensitive(bool(monitor.autopeers))
1755 me.menupeer = me.path_peer(r[0])
1758 me.ui.get_widget('/peer-popup').popup(
1759 None, None, None, ev.button, ev.time)
1762 """Kill a peer from the popup menu."""
1763 cr(conn.kill, me.menupeer.name)()
1766 """Kickstart a key-exchange from the popup menu."""
1767 cr(conn.forcekx, me.menupeer.name)()
1769 _columnmap = {'PING': (2, 3), 'EPING': (4, 5)}
1770 def _ping(me, p, cmd, ps):
1771 """Hook: responds to ping reports."""
1772 textcol, colourcol = me._columnmap[cmd]
1774 me.listmodel[p.i][textcol] = '(miss %d)' % ps.nmissrun
1775 me.listmodel[p.i][colourcol] = 'red'
1777 me.listmodel[p.i][textcol] = '%.1f ms' % ps.tlast
1778 me.listmodel[p.i][colourcol] = 'black'
1781 """Hook: notified when the peer changes state."""
1782 me.listmodel[p.i][1] = p.addr
1784 def setstatus(me, status):
1785 """Update the message in the status bar."""
1787 me.status.push(0, status)
1789 def notify(me, note, *rest):
1790 """Hook: invoked when interesting notifications occur."""
1791 if note == 'DAEMON':
1792 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1796 Hook: invoked when a connection is made to the server.
1798 Make options which require a server connection sensitive.
1800 me.setstatus('Connected (port %s)' % conn.port())
1801 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1802 for i in ('/menubar/server-menu/disconnect',
1803 '/menubar/server-menu/server-version',
1804 '/menubar/server-menu/add-peer',
1805 '/menubar/server-menu/server-quit',
1806 '/menubar/logs-menu/trace-options'):
1807 me.ui.get_widget(i).set_sensitive(True)
1808 me.ui.get_widget('/menubar/server-menu/conn-peer'). \
1809 set_sensitive(bool(monitor.autopeers))
1810 me.ui.get_widget('/menubar/server-menu/daemon'). \
1811 set_sensitive(conn.servinfo()['daemon'] == 'nil')
1813 def disconnected(me, reason):
1815 Hook: invoked when the connection to the server is lost.
1817 Make most options insensitive.
1819 me.setstatus('Disconnected')
1820 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1821 for i in ('/menubar/server-menu/disconnect',
1822 '/menubar/server-menu/server-version',
1823 '/menubar/server-menu/add-peer',
1824 '/menubar/server-menu/conn-peer',
1825 '/menubar/server-menu/daemon',
1826 '/menubar/server-menu/server-quit',
1827 '/menubar/logs-menu/trace-options'):
1828 me.ui.get_widget(i).set_sensitive(False)
1829 if reason: moanbox(reason)
1831 ###--------------------------------------------------------------------------
1834 def parse_options():
1836 Parse command-line options.
1838 Process the boring ones. Return all of them, for later.
1840 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
1841 version = '%prog (tripe version 1.0.0)')
1842 op.add_option('-a', '--admin-socket',
1843 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
1844 help = 'Select socket to connect to [default %default]')
1845 op.add_option('-d', '--directory',
1846 metavar = 'DIR', dest = 'dir', default = T.configdir,
1847 help = 'Select current diretory [default %default]')
1848 opts, args = op.parse_args()
1849 if args: op.error('no arguments permitted')
1854 """Initialization."""
1856 global conn, monitor, pinger
1858 ## Try to establish a connection.
1859 conn = Connection(opts.tripesock)
1861 ## Make the main interesting coroutines and objects.
1869 root = MonitorWindow()
1874 HookClient().hook(root.closehook, exit)
1877 if __name__ == '__main__':
1878 opts = parse_options()
1882 ###----- That's all, folks --------------------------------------------------