4 #----- Dependencies ---------------------------------------------------------
7 from sys import argv, exit, stdin, stdout, stderr
14 from cStringIO import StringIO
22 #----- Configuration --------------------------------------------------------
24 tripedir = "@configdir@"
25 socketdir = "@socketdir@"
31 #----- Utility functions ----------------------------------------------------
33 ## Program name, shorn of extraneous stuff.
34 quis = OS.path.basename(argv[0])
37 """Report a message to standard error."""
38 stderr.write('%s: %s\n' % (quis, msg))
41 """Report a message to standard error and exit."""
45 rx_space = RX.compile(r'\s+')
46 rx_ordinary = RX.compile(r'[^\\\'\"\s]+')
47 rx_weird = RX.compile(r'([\\\'])')
48 rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
49 rx_num = RX.compile(r'^[-+]?\d+$')
51 c_red = GDK.color_parse('red')
54 """Pull a word from the front of S, handling quoting according to the
55 tripe-admin(5) rules. Returns the word and the rest of S, or (None, None)
56 if there are no more words left."""
58 m = rx_space.match(s, i)
64 while i < len(s) and (q or not s[i].isspace()):
65 m = rx_ordinary.match(s, i)
75 elif not q and s[i] == '`' or s[i] == "'":
78 elif not q and s[i] == '"':
85 raise SyntaxError, 'missing close quote'
86 m = rx_space.match(s, i)
91 """Quote S according to the tripe-admin(5) rules."""
92 m = rx_ordinary.match(s)
93 if m and m.end() == len(s):
96 return "'" + rx_weird.sub(r'\\\1', s) + "'"
98 #----- Random bits of infrastructure ----------------------------------------
100 class struct (object):
101 """Simple object which stores attributes and has a sensible construction
103 def __init__(me, **kw):
104 me.__dict__.update(kw)
106 class peerinfo (struct): pass
107 class pingstate (struct): pass
110 """Return a function which throws away its arguments and calls FUNC. (If
111 for loops worked by binding rather than assignment then we wouldn't need
113 return lambda *hunoz, **hukairz: func()
115 class HookList (object):
116 """I maintain a list of functions, and provide the ability to call them
117 when something interesting happens. The functions are called in the order
118 they were added to the list, with all the arguments. If a function returns
119 a non-None result, no further functions are called."""
122 def add(me, func, obj):
123 me.list.append((obj, func))
130 def run(me, *args, **kw):
131 for o, hook in me.list:
132 rc = hook(*args, **kw)
133 if rc is not None: return rc
136 class HookClient (object):
139 def hook(me, hk, func):
150 ## print '%s dying' % me
152 #----- Connections and commands ---------------------------------------------
154 class ConnException (Exception):
155 """Some sort of problem occurred while communicating with the tripe
159 class Error (ConnException):
160 """A command caused the server to issue a FAIL message."""
163 class ConnectionFailed (ConnException):
164 """The connection failed while communicating with the server."""
168 """Return a job tag. Used for background commands."""
171 return 'bg-%d' % jobid_seq
173 class BackgroundCommand (HookClient):
174 def __init__(me, conn, cmd):
175 HookClient.__init__(me)
179 me.donehook = HookList()
180 me.losthook = HookList()
183 me.hook(me.conn.disconnecthook, me.lost)
185 me.conn.bgcommand(me.cmd, me)
190 me.conn.error("Unexpected error from server command `%s': %s" %
194 me.donehook.run(me.info)
197 class SimpleBackgroundCommand (BackgroundCommand):
200 BackgroundCommand.submit(me)
201 except ConnectionFailed, err:
202 me.conn.error('Unexpected error communicating with server: %s' % msg)
205 class Connection (HookClient):
207 """I represent a connection to the TrIPE server. I provide facilities for
208 sending commands and receiving replies. The connection is notional: the
209 underlying socket connection can come and go under our feet.
212 connectedp: whether the connection is active
213 connecthook: called when we have connected
214 disconnecthook: called if we have disconnected
215 notehook: called with asynchronous notifications
216 errorhook: called if there was a command error"""
218 def __init__(me, sockname):
219 """Make a new connection to the server listening to SOCKNAME. In fact,
220 we're initially disconnected, to allow the caller to get his life in
221 order before opening the floodgates."""
222 HookClient.__init__(me)
223 me.sockname = sockname
225 me.connectedp = False
226 me.connecthook = HookList()
227 me.disconnecthook = HookList()
228 me.errorhook = HookList()
235 "Connect to the server. Runs connecthook if it works."""
237 sock = S.socket(S.AF_UNIX, S.SOCK_STREAM)
239 sock.connect(me.sockname)
241 me.error('error opening connection: %s' % err[1])
242 me.disconnecthook.run()
245 me.socketwatch = GO.io_add_watch(sock, GO.IO_IN, me.ready)
250 "Disconnects from the server. Runs disconnecthook."
251 if not me.sock: return
252 GO.source_remove(me.socketwatch)
255 me.connectedp = False
256 me.disconnecthook.run()
258 """Reports an error on the connection."""
259 me.errorhook.run(msg)
261 def bgcommand(me, cmd, bg):
262 """Sends a background command and feeds it properly."""
265 err = me.docommand(cmd)
270 def command(me, cmd):
271 """Sends a command to the server. Returns a list of INFO responses. Do
272 not use this for backgrounded commands: create a BackgroundCommand
273 instead. Raises apprpopriate exceptions on error, but doesn't send
274 report them to the errorhook."""
275 err = me.docommand(cmd)
279 def docommand(me, cmd):
281 raise ConnException, 'not connected'
282 if debug: print ">>> %s" % cmd
283 me.sock.sendall(cmd + '\n')
287 me.sock.setblocking(1)
289 rc, err = me.collect()
293 me.sock.setblocking(0)
294 if len(me.inbuf) > 0:
295 GO.idle_add(lambda: me.flushbuf() and False)
297 def simplecmd(me, cmd):
298 """Like command(), but reports errors via the errorhook as well as
299 raising exceptions."""
303 me.error("Unexpected error from server command `%s': %s" % (cmd, msg))
305 except ConnectionFailed, msg:
306 me.error("Unexpected error communicating with server: %s" % msg);
309 def ready(me, sock, condition):
312 except ConnException, msg:
313 me.error("Error watching server connection: %s" % msg)
319 data = me.sock.recv(16384)
322 raise ConnectionFailed, 'server disconnected'
327 nl = me.inbuf.find('\n')
330 if debug: print "<<< %s" % line
331 me.inbuf = me.inbuf[nl + 1:]
332 tag, line = getword(line)
333 rc, err = me.parseline(tag, line)
334 if rc: return rc, err
336 def parseline(me, code, line):
337 if code == 'BGDETACH':
339 raise ConnectionFailed, 'unexpected detach'
341 me.bgmap[line] = me.bgcmd
345 elif code == 'BGINFO':
346 tag, line = getword(line)
347 me.bgmap[tag].info.append(line)
349 elif code == 'BGFAIL':
350 tag, line = getword(line)
351 me.bgmap[tag].fail(line)
355 tag, line = getword(line)
360 if not me.waitingp or me.bgcmd:
361 raise ConnectionFailed, 'unexpected INFO response'
365 if not me.waitingp or me.bgcmd:
366 raise ConnectionFailed, 'unexpected OK response'
370 raise ConnectionFailed, 'unexpected FAIL response'
373 raise ConnectionFailed, 'unknown response code `%s' % code
375 class Monitor (Connection):
376 """I monitor a TrIPE server, noticing when it changes state and keeping
377 track of its peers. I also provide facilities for sending the server
378 commands and collecting the answers.
381 addpeerhook: called with a new Peer when the server adds one
382 delpeerhook: called with a Peer when the server kills one
383 tracehook: called with a trace message
384 warnhook: called with a warning message
385 peers: mapping from names to Peer objects"""
386 def __init__(me, sockname):
387 """Initializes the monitor."""
388 Connection.__init__(me, sockname)
389 me.addpeerhook = HookList()
390 me.delpeerhook = HookList()
391 me.tracehook = HookList()
392 me.warnhook = HookList()
393 me.notehook = HookList()
394 me.hook(me.connecthook, me.connected)
397 def addpeer(me, peer):
398 if peer not in me.peers:
401 me.addpeerhook.run(p)
402 def delpeer(me, peer):
405 me.delpeerhook.run(p)
408 def updatelist(me, peers):
412 if p not in me.peers:
414 oldpeers = me.peers.copy()
420 me.simplecmd('WATCH -A+wnt')
421 me.updatelist([s.strip() for s in me.simplecmd('LIST')])
422 except ConnException:
425 def parseline(me, code, line):
426 ## Delay async messages until the current command is done. Otherwise the
427 ## handler for the async message might send another command before this
428 ## one's complete, and the whole edifice turns to jelly.
430 ## No, this isn't the server's fault. If we rely on the server to delay
431 ## notifications then there's a race between when we send a command and
432 ## when the server gets it.
433 if me.waitingp and code in ('TRACE', 'WARN', 'NOTE'):
434 if len(me.delay) == 0: GO.idle_add(me.flushdelay)
435 me.delay.append((code, line))
436 elif code == 'TRACE':
437 me.tracehook.run(line)
439 me.warnhook.run(line)
441 note, line = getword(line)
442 me.notehook.run(note, line)
444 me.addpeer(getword(line)[0])
448 ## Well, I asked for it.
451 return Connection.parseline(me, code, line)
456 for tag, line in delay:
457 me.parseline(tag, line)
461 """Parse key=value output into a dictionary."""
464 for w in i.split(' '):
470 """I represent a TrIPE peer. Useful attributes are:
473 addr: human-friendly representation of the peer's address
474 ifname: interface associated with the peer
475 alivep: true if the peer hasn't been killed
476 deadhook: called with no arguments when the peer is killed"""
477 def __init__(me, monitor, name):
480 addr = me.mon.simplecmd('ADDR %s' % name)[0].split(' ')
481 if addr[0] == 'INET':
482 ipaddr, port = addr[1:]
484 name = S.gethostbyaddr(ipaddr)[0]
485 me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
487 me.addr = 'INET %s:%s' % (ipaddr, port)
489 me.addr = ' '.join(addr)
490 me.ifname = me.mon.simplecmd('IFNAME %s' % me.name)[0]
491 me.__dict__.update(parseinfo(me.mon.simplecmd('PEERINFO %s' % me.name)))
492 me.deadhook = HookList()
498 #----- Window management cruft ----------------------------------------------
500 class MyWindowMixin (G.Window, HookClient):
501 """Mixin for windows which call a closehook when they're destroyed."""
503 me.closehook = HookList()
504 HookClient.__init__(me)
505 me.connect('destroy', invoker(me.close))
510 class MyWindow (MyWindowMixin):
511 """A window which calls a closehook when it's destroyed."""
512 def __init__(me, kind = G.WINDOW_TOPLEVEL):
513 G.Window.__init__(me, kind)
515 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
516 """A dialogue box with a closehook and sensible button binding."""
517 def __init__(me, title = None, flags = 0, buttons = []):
518 """The buttons are a list of (STOCKID, THUNK) pairs: call the appropriate
519 THUNK when the button is pressed. The others are just like GTK's Dialog
529 G.Dialog.__init__(me, title, None, flags, tuple(br))
530 HookClient.__init__(me)
532 me.set_default_response(i - 1)
533 me.connect('response', me.respond)
534 def respond(me, hunoz, rid, *hukairz):
535 if rid >= 0: me.rmap[rid]()
537 def makeactiongroup(name, acts):
538 """Creates an ActionGroup called NAME. ACTS is a list of tuples
541 LABEL: the label string for the action
542 ACCEL: accelerator string, or None
543 FUNC: thunk to call when the action is invoked"""
544 actgroup = G.ActionGroup(name)
545 for act, label, accel, func in acts:
546 a = G.Action(act, label, None, None)
547 if func: a.connect('activate', invoker(func))
548 actgroup.add_action_with_accel(a, accel)
551 class GridPacker (G.Table):
552 """Like a Table, but with more state: makes filling in the widgets
560 me.set_border_width(4)
561 me.set_col_spacings(4)
562 me.set_row_spacings(4)
563 def pack(me, w, width = 1, newlinep = False,
564 xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
566 """Packs a new widget. W is the widget to add. XOPY, YOPT, XPAD and
567 YPAD are as for Table. WIDTH is how many cells to take up horizontally.
568 NEWLINEP is whether to start a new line for this widget. Returns W."""
573 right = me.col + width
574 if bot > me.rows or right > me.cols:
575 if bot > me.rows: me.rows = bot
576 if right > me.cols: me.cols = right
577 me.resize(me.rows, me.cols)
578 me.attach(w, me.col, me.col + width, me.row, me.row + 1,
579 xopt, yopt, xpad, ypad)
582 def labelled(me, lab, w, newlinep = False, **kw):
583 """Packs a labelled widget. Other arguments are as for pack. Returns
586 label.set_alignment(1.0, 0)
587 me.pack(label, newlinep = newlinep, xopt = G.FILL)
590 def info(me, label, text = None, len = 18, **kw):
591 """Packs an information widget with a label. LABEL is the label; TEXT is
592 the initial text; LEN is the estimated length in characters. Returns the
595 if text is not None: e.set_text(text)
596 e.set_width_chars(len)
597 e.set_editable(False)
598 me.labelled(label, e, **kw)
601 class WindowSlot (HookClient):
602 """A place to store a window. If the window is destroyed, remember this;
603 when we come to open the window, raise it if it already exists; otherwise
605 def __init__(me, createfunc):
606 """Constructor: CREATEFUNC must return a new Window which supports the
607 closehook protocol."""
608 HookClient.__init__(me)
609 me.createfunc = createfunc
612 """Opens the window, creating it if necessary."""
614 me.window.window.raise_()
616 me.window = me.createfunc()
617 me.hook(me.window.closehook, me.closed)
619 me.unhook(me.window.closehook)
622 class ValidationError (Exception):
623 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
625 class ValidatingEntry (G.Entry):
626 """Like an Entry, but makes the text go red if the contents are invalid.
627 If get_text is called, and the text is invalid, ValidationError is
629 def __init__(me, valid, text = '', size = -1, *arg, **kw):
630 """Make an Entry. VALID is a regular expression or a predicate on
631 strings. TEXT is the default text to insert. SIZE is the size of the
632 box to set, in characters (ish). Other arguments are passed to Entry."""
633 G.Entry.__init__(me, *arg, **kw)
634 me.connect("changed", me.check)
638 me.validate = RX.compile(valid).match
640 me.c_ok = me.get_style().text[G.STATE_NORMAL]
642 if size != -1: me.set_width_chars(size)
643 me.set_activates_default(True)
646 def check(me, *hunoz):
647 if me.validate(G.Entry.get_text(me)):
649 me.modify_text(G.STATE_NORMAL, me.c_ok)
652 me.modify_text(G.STATE_NORMAL, me.c_bad)
655 raise ValidationError
656 return G.Entry.get_text(me)
658 def numericvalidate(min = None, max = None):
659 """Validation function for numbers. Entry must consist of an optional sign
660 followed by digits, and the resulting integer must be within the given
662 return lambda x: (rx_num.match(x) and
663 (min is None or long(x) >= min) and
664 (max is None or long(x) <= max))
666 #----- Various minor dialog boxen -------------------------------------------
668 GPL = """This program is free software; you can redistribute it and/or modify
669 it under the terms of the GNU General Public License as published by
670 the Free Software Foundation; either version 2 of the License, or
671 (at your option) any later version.
673 This program is distributed in the hope that it will be useful,
674 but WITHOUT ANY WARRANTY; without even the implied warranty of
675 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
676 GNU General Public License for more details.
678 You should have received a copy of the GNU General Public License
679 along with this program; if not, write to the Free Software Foundation,
680 Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
682 class AboutBox (G.AboutDialog, MyWindowMixin):
683 """The program `About' box."""
685 G.AboutDialog.__init__(me)
687 me.set_name('TrIPEmon')
688 me.set_version(VERSION)
690 me.set_authors(['Mark Wooding'])
691 me.connect('unmap', invoker(me.close))
693 aboutbox = WindowSlot(AboutBox)
696 """Report an error message in a window."""
697 d = G.Dialog('Error from %s' % quis,
698 flags = G.DIALOG_MODAL,
699 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
701 label.set_padding(20, 20)
702 d.vbox.pack_start(label)
707 def unimplemented(*hunoz):
708 """Indicator of laziness."""
709 moanbox("I've not written that bit yet.")
711 class ServInfo (MyWindow):
712 def __init__(me, monitor):
713 MyWindow.__init__(me)
714 me.set_title('TrIPE server info')
716 me.table = GridPacker()
719 def add(label, tag, text = None, **kw):
720 me.e[tag] = me.table.info(label, text, **kw)
721 add('Implementation', 'implementation')
722 add('Version', 'version', newlinep = True)
724 me.hook(me.mon.connecthook, me.update)
727 info = parseinfo(me.mon.simplecmd('SERVINFO'))
729 me.e[i].set_text(info[i])
731 class TraceOptions (MyDialog):
732 """Tracing options window."""
733 def __init__(me, monitor):
734 MyDialog.__init__(me, title = 'Tracing options',
735 buttons = [(G.STOCK_CLOSE, me.destroy),
736 (G.STOCK_OK, me.ok)])
739 for o in me.mon.simplecmd('TRACE'):
742 text = o[3].upper() + o[4:]
743 if char.isupper(): continue
744 ticky = G.CheckButton(text)
745 ticky.set_active(onp != ' ')
746 me.vbox.pack_start(ticky)
747 me.opts.append((char, ticky))
752 for char, ticky in me.opts:
753 if ticky.get_active():
757 setting = ''.join(on) + '-' + ''.join(off)
758 me.mon.simplecmd('TRACE %s' % setting)
761 #----- Logging windows ------------------------------------------------------
763 class LogModel (G.ListStore):
764 """A simple list of log messages."""
765 def __init__(me, columns):
766 """Call with a list of column names. All must be strings. We add a time
767 column to the left."""
768 me.cols = ('Time',) + columns
769 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
770 def add(me, *entries):
771 """Adds a new log message, with a timestamp."""
772 now = T.strftime('%Y-%m-%d %H:%M:%S')
773 me.append((now,) + entries)
775 class TraceLogModel (LogModel):
776 """Log model for trace messages."""
778 LogModel.__init__(me, ('Message',))
779 def notify(me, line):
780 """Call with a new trace message."""
783 class WarningLogModel (LogModel):
784 """Log model for warnings. We split the category out into a separate
787 LogModel.__init__(me, ('Category', 'Message'))
788 def notify(me, line):
789 """Call with a new warning message."""
790 me.add(*getword(line))
792 class LogViewer (MyWindow):
793 """Log viewer window. Nothing very exciting."""
794 def __init__(me, model):
795 MyWindow.__init__(me)
797 scr = G.ScrolledWindow()
798 scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
799 me.list = G.TreeView(me.model)
800 me.closehook = HookList()
802 for c in me.model.cols:
803 me.list.append_column(G.TreeViewColumn(c,
804 G.CellRendererText(),
807 me.set_default_size(440, 256)
812 #----- Peer window ----------------------------------------------------------
815 """Translate a time in tripe's stats format to something a human might
816 actually want to read."""
817 if t == 'NEVER': return '(never)'
818 Y, M, D, h, m, s = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
819 return '%04d:%02d:%02d %02d:%02d:%02d' % (Y, M, D, h, m, s)
821 """Translate a number of bytes into something a human might want to read."""
828 return '%d %s' % (b, suff)
830 ## How to translate peer stats. Maps the stat name to a translation
833 [('start-time', xlate_time),
834 ('last-packet-time', xlate_time),
835 ('last-keyexch-time', xlate_time),
836 ('bytes-in', xlate_bytes),
837 ('bytes-out', xlate_bytes),
838 ('keyexch-bytes-in', xlate_bytes),
839 ('keyexch-bytes-out', xlate_bytes),
840 ('ip-bytes-in', xlate_bytes),
841 ('ip-bytes-out', xlate_bytes)]
843 ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
844 ## the label to give the entry box; FORMAT is the format string to write into
847 [('Start time', '%(start-time)s'),
848 ('Last key-exchange', '%(last-keyexch-time)s'),
849 ('Last packet', '%(last-packet-time)s'),
851 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
852 ('Key-exchange in/out',
853 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
855 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-in)s (%(ip-bytes-in)s)'),
856 ('Rejected packets', '%(rejected-packets)s')]
858 class PeerWindow (MyWindow):
859 """Show information about a peer."""
860 def __init__(me, monitor, peer):
861 MyWindow.__init__(me)
862 me.set_title('TrIPE statistics: %s' % peer.name)
868 def add(label, text = None):
869 me.e[label] = table.info(label, text, len = 42, newlinep = True)
870 add('Peer name', peer.name)
871 add('Tunnel', peer.tunnel)
872 add('Interface', peer.ifname)
874 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
875 add('Address', peer.addr)
876 add('Transport pings')
877 add('Encrypted pings')
878 for label, format in statslayout: add(label)
880 me.hook(me.mon.connecthook, me.tryupdate)
881 me.hook(me.mon.disconnecthook, me.stopupdate)
882 me.hook(me.closehook, me.stopupdate)
883 me.hook(me.peer.deadhook, me.dead)
884 me.hook(me.peer.pinghook, me.ping)
889 if not me.peer.alivep or not me.mon.connectedp: return False
890 stat = parseinfo(me.mon.simplecmd('STATS %s' % me.peer.name))
891 for s, trans in statsxlate:
892 stat[s] = trans(stat[s])
893 for label, format in statslayout:
894 me.e[label].set_text(format % stat)
897 if me.timeout is None and me.update():
898 me.timeout = GO.timeout_add(1000, me.update)
900 if me.timeout is not None:
901 GO.source_remove(me.timeout)
904 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
905 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
908 for ping in me.peer.ping, me.peer.eping:
909 s = '%d/%d' % (ping.ngood, ping.n)
911 s += ' (%.1f%%)' % (ping.ngood * 100.0/ping.n)
913 s += '; %.2f ms (last %.1f ms)' % (ping.ttot/ping.ngood, ping.tlast);
914 me.e[ping.cmd].set_text(s)
916 #----- Add peer -------------------------------------------------------------
918 class AddPeerCommand (SimpleBackgroundCommand):
919 def __init__(me, conn, dlg, name, addr, port,
920 keepalive = None, tunnel = None):
924 me.keepalive = keepalive
927 cmd.write('ADD %s' % name)
928 cmd.write(' -background %s' % jobid())
929 if keepalive is not None: cmd.write(' -keepalive %s' % keepalive)
930 if tunnel is not None: cmd.write(' -tunnel %s' % tunnel)
931 cmd.write(' INET %s %s' % (addr, port))
932 SimpleBackgroundCommand.__init__(me, conn, cmd.getvalue())
933 me.hook(me.donehook, invoker(dlg.destroy))
935 token, msg = getword(str(err))
936 if token in ('resolve-error', 'resolver-timeout'):
937 moanbox("Unable to resolve hostname `%s'" % me.addr)
938 elif token == 'peer-create-fail':
939 moanbox("Couldn't create new peer `%s'" % me.name)
940 elif token == 'peer-exists':
941 moanbox("Peer `%s' already exists" % me.name)
943 moanbox("Unexpected error from server command `ADD': %s" % err)
945 class AddPeerDialog (MyDialog):
946 def __init__(me, monitor):
947 MyDialog.__init__(me, 'Add peer',
948 buttons = [(G.STOCK_CANCEL, me.destroy),
949 (G.STOCK_OK, me.ok)])
952 me.vbox.pack_start(table)
953 me.e_name = table.labelled('Name',
954 ValidatingEntry(r'^[^\s.:]+$', '', 16),
956 me.e_addr = table.labelled('Address',
957 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
959 me.e_port = table.labelled('Port',
960 ValidatingEntry(numericvalidate(0, 65535),
963 me.c_keepalive = G.CheckButton('Keepalives')
964 me.l_tunnel = table.labelled('Tunnel',
965 G.combo_box_new_text(),
966 newlinep = True, width = 3)
967 me.tuns = me.mon.simplecmd('TUNNELS')
969 me.l_tunnel.append_text(t)
970 me.l_tunnel.set_active(0)
971 table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
972 me.c_keepalive.connect('toggled',
973 lambda t: me.e_keepalive.set_sensitive\
975 me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
976 me.e_keepalive.set_sensitive(False)
977 table.pack(me.e_keepalive, width = 3)
981 if me.c_keepalive.get_active():
982 ka = me.e_keepalive.get_text()
985 t = me.l_tunnel.get_active()
990 AddPeerCommand(me.mon, me,
991 me.e_name.get_text(),
992 me.e_addr.get_text(),
993 me.e_port.get_text(),
996 except ValidationError:
1000 #----- The server monitor ---------------------------------------------------
1002 class PingCommand (SimpleBackgroundCommand):
1003 def __init__(me, conn, cmd, peer, func):
1006 SimpleBackgroundCommand.__init__ \
1007 (me, conn, '%s -background %s %s' % (cmd, jobid(), peer.name))
1009 tok, rest = getword(me.info[0])
1010 if tok == 'ping-ok':
1011 me.func(me.peer, float(rest))
1013 me.func(me.peer, None)
1015 def fail(me, err): me.unhookall()
1016 def lost(me): me.unhookall()
1018 class MonitorWindow (MyWindow):
1020 def __init__(me, monitor):
1021 MyWindow.__init__(me)
1022 me.set_title('TrIPE monitor')
1024 me.hook(me.mon.errorhook, me.report)
1025 me.warnings = WarningLogModel()
1026 me.hook(me.mon.warnhook, me.warnings.notify)
1027 me.trace = TraceLogModel()
1028 me.hook(me.mon.tracehook, me.trace.notify)
1030 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1031 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1032 me.traceopts = WindowSlot(lambda: TraceOptions(me.mon))
1033 me.addpeerwin = WindowSlot(lambda: AddPeerDialog(me.mon))
1034 me.servinfo = WindowSlot(lambda: ServInfo(me.mon))
1039 me.ui = G.UIManager()
1040 def cmd(c): return lambda: me.mon.simplecmd(c)
1041 actgroup = makeactiongroup('monitor',
1042 [('file-menu', '_File', None, None),
1043 ('connect', '_Connect', '<Alt>C', me.mon.connect),
1044 ('disconnect', '_Disconnect', '<Alt>D', me.mon.disconnect),
1045 ('quit', '_Quit', '<Alt>Q', me.close),
1046 ('server-menu', '_Server', None, None),
1047 ('daemon', 'Run in _background', None, cmd('DAEMON')),
1048 ('server-version', 'Server version', '<Alt>V', me.servinfo.open),
1049 ('reload-keys', 'Reload keys', '<Alt>R', cmd('RELOAD')),
1050 ('server-quit', 'Terminate server', None, cmd('QUIT')),
1051 ('logs-menu', '_Logs', None, None),
1052 ('show-warnings', 'Show _warnings', '<Alt>W', me.warnview.open),
1053 ('show-trace', 'Show _trace', '<Alt>T', me.traceview.open),
1054 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1055 ('help-menu', '_Help', None, None),
1056 ('about', '_About tripemon...', None, aboutbox.open),
1057 ('add-peer', '_Add peer...', '<Alt>A', me.addpeerwin.open),
1058 ('kill-peer', '_Kill peer', None, me.killpeer),
1059 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1063 <menu action="file-menu">
1064 <menuitem action="quit"/>
1066 <menu action="server-menu">
1067 <menuitem action="connect"/>
1068 <menuitem action="disconnect"/>
1070 <menuitem action="server-version"/>
1071 <menuitem action="add-peer"/>
1072 <menuitem action="daemon"/>
1073 <menuitem action="reload-keys"/>
1075 <menuitem action="server-quit"/>
1077 <menu action="logs-menu">
1078 <menuitem action="show-warnings"/>
1079 <menuitem action="show-trace"/>
1080 <menuitem action="trace-options"/>
1082 <menu action="help-menu">
1083 <menuitem action="about"/>
1086 <popup name="peer-popup">
1087 <menuitem action="add-peer"/>
1088 <menuitem action="kill-peer"/>
1089 <menuitem action="force-kx"/>
1093 me.ui.insert_action_group(actgroup, 0)
1094 me.ui.add_ui_from_string(uidef)
1095 vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
1096 me.add_accel_group(me.ui.get_accel_group())
1097 me.status = G.Statusbar()
1099 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1100 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1101 me.hook(me.mon.addpeerhook, me.addpeer)
1102 me.hook(me.mon.delpeerhook, me.delpeer)
1104 scr = G.ScrolledWindow()
1105 scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
1106 me.list = G.TreeView(me.listmodel)
1107 me.list.append_column(G.TreeViewColumn('Peer name',
1108 G.CellRendererText(),
1110 me.list.append_column(G.TreeViewColumn('Address',
1111 G.CellRendererText(),
1113 me.list.append_column(G.TreeViewColumn('T-ping',
1114 G.CellRendererText(),
1117 me.list.append_column(G.TreeViewColumn('E-ping',
1118 G.CellRendererText(),
1121 me.list.get_column(1).set_expand(True)
1122 me.list.connect('row-activated', me.activate)
1123 me.list.connect('button-press-event', me.buttonpress)
1124 me.list.set_reorderable(True)
1125 me.list.get_selection().set_mode(G.SELECTION_NONE)
1127 vbox.pack_start(scr)
1129 vbox.pack_start(me.status, expand = False)
1130 me.hook(me.mon.connecthook, me.connected)
1131 me.hook(me.mon.disconnecthook, me.disconnected)
1132 me.hook(me.mon.notehook, me.notify)
1134 me.set_default_size(420, 180)
1138 def addpeer(me, peer):
1139 peer.i = me.listmodel.append([peer.name, peer.addr,
1140 '???', 'green', '???', 'green'])
1141 peer.win = WindowSlot(lambda: PeerWindow(me.mon, peer))
1142 peer.pinghook = HookList()
1143 peer.ping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1144 tlast = 0, ttot = 0,
1145 tcol = 2, ccol = 3, cmd = 'Transport pings')
1146 peer.eping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1147 tlast = 0, ttot = 0,
1148 tcol = 4, ccol = 5, cmd = 'Encrypted pings')
1149 def delpeer(me, peer):
1150 me.listmodel.remove(peer.i)
1151 def path_peer(me, path):
1152 return me.mon.peers[me.listmodel[path][0]]
1154 def activate(me, l, path, col):
1155 peer = me.path_peer(path)
1157 def buttonpress(me, l, ev):
1159 r = me.list.get_path_at_pos(ev.x, ev.y)
1160 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1161 me.ui.get_widget(i).set_sensitive(me.mon.connectedp and
1164 me.menupeer = me.path_peer(r[0])
1167 me.ui.get_widget('/peer-popup').popup(None, None, None,
1171 me.mon.simplecmd('KILL %s' % me.menupeer.name)
1173 me.mon.simplecmd('FORCEKX %s' % me.menupeer.name)
1176 if me.pinger is not None:
1177 GO.source_remove(me.pinger)
1178 me.pinger = GO.timeout_add(10000, me.ping)
1181 if me.pinger is not None:
1182 GO.source_remove(me.pinger)
1185 for name in me.mon.peers:
1186 p = me.mon.peers[name]
1187 PingCommand(me.mon, 'PING', p, lambda p, t: me.pong(p, p.ping, t))
1188 PingCommand(me.mon, 'EPING', p, lambda p, t: me.pong(p, p.eping, t))
1190 def pong(me, p, ping, t):
1195 me.listmodel[p.i][ping.tcol] = '(miss %d)' % ping.nmissrun
1196 me.listmodel[p.i][ping.ccol] = 'red'
1202 me.listmodel[p.i][ping.tcol] = '%.1f ms' % t
1203 me.listmodel[p.i][ping.ccol] = 'black'
1205 def setstatus(me, status):
1207 me.status.push(0, status)
1208 def notify(me, note, rest):
1209 if note == 'DAEMON':
1210 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1212 me.setstatus('Connected (port %s)' % me.mon.simplecmd('PORT')[0])
1213 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1214 for i in ('/menubar/server-menu/disconnect',
1215 '/menubar/server-menu/server-version',
1216 '/menubar/server-menu/add-peer',
1217 '/menubar/server-menu/server-quit',
1218 '/menubar/logs-menu/trace-options'):
1219 me.ui.get_widget(i).set_sensitive(True)
1220 me.ui.get_widget('/menubar/server-menu/daemon'). \
1221 set_sensitive(parseinfo(me.mon.simplecmd('SERVINFO'))['daemon'] ==
1224 def disconnected(me):
1225 me.setstatus('Disconnected')
1226 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1227 for i in ('/menubar/server-menu/disconnect',
1228 '/menubar/server-menu/server-version',
1229 '/menubar/server-menu/add-peer',
1230 '/menubar/server-menu/daemon',
1231 '/menubar/server-menu/server-quit',
1232 '/menubar/logs-menu/trace-options'):
1233 me.ui.get_widget(i).set_sensitive(False)
1236 if me.pinger is not None:
1237 GO.source_remove(me.pinger)
1238 def report(me, msg):
1242 #----- Parse options --------------------------------------------------------
1244 def version(fp = stdout):
1245 """Print the program's version number."""
1246 fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION))
1249 """Print a brief usage message for the program."""
1250 fp.write('Usage: %s [-d DIR] [-a SOCK]\n' % quis)
1254 if 'TRIPEDIR' in environ:
1255 tripedir = environ['TRIPEDIR']
1256 tripesock = '%s/%s' % (socketdir, 'tripesock')
1259 opts, args = O.getopt(argv[1:],
1261 ['help', 'version', 'usage',
1262 'directory=', 'admin-socket='])
1263 except O.GetoptError, exc:
1268 if o in ('-h', '--help'):
1273 Graphical monitor for TrIPE VPN.
1277 -h, --help Show this help message.
1278 -v, --version Show the version number.
1279 -u, --usage Show pointlessly short usage string.
1281 -d, --directory=DIR Use TrIPE directory DIR.
1282 -a, --admin-socket=FILE Select socket to connect to."""
1284 elif o in ('-v', '--version'):
1287 elif o in ('-u', '--usage'):
1290 elif o in ('-d', '--directory'):
1292 elif o in ('-a', '--admin-socket'):
1295 raise "can't happen!"
1301 mon = Monitor(tripesock)
1302 root = MonitorWindow(mon)
1303 HookClient().hook(root.closehook, exit)
1306 if __name__ == '__main__':
1309 #----- That's all, folks ----------------------------------------------------