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 += '; %.2f ms (last %.1f ms)' % (ping.ttot/ping.ngood, ping.tlast);
912 me.e[ping.cmd].set_text(s)
914 #----- Add peer -------------------------------------------------------------
916 class AddPeerCommand (SimpleBackgroundCommand):
917 def __init__(me, conn, dlg, name, addr, port,
918 keepalive = None, tunnel = None):
922 me.keepalive = keepalive
925 cmd.write('ADD %s' % name)
926 cmd.write(' -background %s' % jobid())
927 if keepalive is not None: cmd.write(' -keepalive %s' % keepalive)
928 if tunnel is not None: cmd.write(' -tunnel %s' % tunnel)
929 cmd.write(' INET %s %s' % (addr, port))
930 SimpleBackgroundCommand.__init__(me, conn, cmd.getvalue())
931 me.hook(me.donehook, invoker(dlg.destroy))
933 token, msg = getword(str(err))
934 if token in ('resolve-error', 'resolver-timeout'):
935 moanbox("Unable to resolve hostname `%s'" % me.addr)
936 elif token == 'peer-create-fail':
937 moanbox("Couldn't create new peer `%s'" % me.name)
938 elif token == 'peer-exists':
939 moanbox("Peer `%s' already exists" % me.name)
941 moanbox("Unexpected error from server command `ADD': %s" % err)
943 class AddPeerDialog (MyDialog):
944 def __init__(me, monitor):
945 MyDialog.__init__(me, 'Add peer',
946 buttons = [(G.STOCK_CANCEL, me.destroy),
947 (G.STOCK_OK, me.ok)])
950 me.vbox.pack_start(table)
951 me.e_name = table.labelled('Name',
952 ValidatingEntry(r'^[^\s.:]+$', '', 16),
954 me.e_addr = table.labelled('Address',
955 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
957 me.e_port = table.labelled('Port',
958 ValidatingEntry(numericvalidate(0, 65535),
961 me.c_keepalive = G.CheckButton('Keepalives')
962 me.l_tunnel = table.labelled('Tunnel',
963 G.combo_box_new_text(),
964 newlinep = True, width = 3)
965 me.tuns = me.mon.simplecmd('TUNNELS')
967 me.l_tunnel.append_text(t)
968 me.l_tunnel.set_active(0)
969 table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
970 me.c_keepalive.connect('toggled',
971 lambda t: me.e_keepalive.set_sensitive\
973 me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
974 me.e_keepalive.set_sensitive(False)
975 table.pack(me.e_keepalive, width = 3)
979 if me.c_keepalive.get_active():
980 ka = me.e_keepalive.get_text()
983 t = me.l_tunnel.get_active()
988 AddPeerCommand(me.mon, me,
989 me.e_name.get_text(),
990 me.e_addr.get_text(),
991 me.e_port.get_text(),
994 except ValidationError:
998 #----- The server monitor ---------------------------------------------------
1000 class PingCommand (SimpleBackgroundCommand):
1001 def __init__(me, conn, cmd, peer, func):
1004 SimpleBackgroundCommand.__init__ \
1005 (me, conn, '%s -background %s %s' % (cmd, jobid(), peer.name))
1007 tok, rest = getword(me.info[0])
1008 if tok == 'ping-ok':
1009 me.func(me.peer, float(rest))
1011 me.func(me.peer, None)
1013 def fail(me, err): me.unhookall()
1014 def lost(me): me.unhookall()
1016 class MonitorWindow (MyWindow):
1018 def __init__(me, monitor):
1019 MyWindow.__init__(me)
1020 me.set_title('TrIPE monitor')
1022 me.hook(me.mon.errorhook, me.report)
1023 me.warnings = WarningLogModel()
1024 me.hook(me.mon.warnhook, me.warnings.notify)
1025 me.trace = TraceLogModel()
1026 me.hook(me.mon.tracehook, me.trace.notify)
1028 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1029 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1030 me.traceopts = WindowSlot(lambda: TraceOptions(me.mon))
1031 me.addpeerwin = WindowSlot(lambda: AddPeerDialog(me.mon))
1032 me.servinfo = WindowSlot(lambda: ServInfo(me.mon))
1037 me.ui = G.UIManager()
1038 def cmd(c): return lambda: me.mon.simplecmd(c)
1039 actgroup = makeactiongroup('monitor',
1040 [('file-menu', '_File', None, None),
1041 ('connect', '_Connect', '<Alt>C', me.mon.connect),
1042 ('disconnect', '_Disconnect', '<Alt>D', me.mon.disconnect),
1043 ('quit', '_Quit', '<Alt>Q', me.close),
1044 ('server-menu', '_Server', None, None),
1045 ('daemon', 'Run in _background', None, cmd('DAEMON')),
1046 ('server-version', 'Server version', '<Alt>V', me.servinfo.open),
1047 ('reload-keys', 'Reload keys', '<Alt>R', cmd('RELOAD')),
1048 ('server-quit', 'Terminate server', None, cmd('QUIT')),
1049 ('logs-menu', '_Logs', None, None),
1050 ('show-warnings', 'Show _warnings', '<Alt>W', me.warnview.open),
1051 ('show-trace', 'Show _trace', '<Alt>T', me.traceview.open),
1052 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1053 ('help-menu', '_Help', None, None),
1054 ('about', '_About tripemon...', None, aboutbox.open),
1055 ('add-peer', '_Add peer...', '<Alt>A', me.addpeerwin.open),
1056 ('kill-peer', '_Kill peer', None, me.killpeer),
1057 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1061 <menu action="file-menu">
1062 <menuitem action="quit"/>
1064 <menu action="server-menu">
1065 <menuitem action="connect"/>
1066 <menuitem action="disconnect"/>
1068 <menuitem action="server-version"/>
1069 <menuitem action="add-peer"/>
1070 <menuitem action="daemon"/>
1071 <menuitem action="reload-keys"/>
1073 <menuitem action="server-quit"/>
1075 <menu action="logs-menu">
1076 <menuitem action="show-warnings"/>
1077 <menuitem action="show-trace"/>
1078 <menuitem action="trace-options"/>
1080 <menu action="help-menu">
1081 <menuitem action="about"/>
1084 <popup name="peer-popup">
1085 <menuitem action="add-peer"/>
1086 <menuitem action="kill-peer"/>
1087 <menuitem action="force-kx"/>
1091 me.ui.insert_action_group(actgroup, 0)
1092 me.ui.add_ui_from_string(uidef)
1093 vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
1094 me.add_accel_group(me.ui.get_accel_group())
1095 me.status = G.Statusbar()
1097 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1098 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1099 me.hook(me.mon.addpeerhook, me.addpeer)
1100 me.hook(me.mon.delpeerhook, me.delpeer)
1102 scr = G.ScrolledWindow()
1103 scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
1104 me.list = G.TreeView(me.listmodel)
1105 me.list.append_column(G.TreeViewColumn('Peer name',
1106 G.CellRendererText(),
1108 me.list.append_column(G.TreeViewColumn('Address',
1109 G.CellRendererText(),
1111 me.list.append_column(G.TreeViewColumn('T-ping',
1112 G.CellRendererText(),
1115 me.list.append_column(G.TreeViewColumn('E-ping',
1116 G.CellRendererText(),
1119 me.list.get_column(1).set_expand(True)
1120 me.list.connect('row-activated', me.activate)
1121 me.list.connect('button-press-event', me.buttonpress)
1122 me.list.set_reorderable(True)
1123 me.list.get_selection().set_mode(G.SELECTION_NONE)
1125 vbox.pack_start(scr)
1127 vbox.pack_start(me.status, expand = False)
1128 me.hook(me.mon.connecthook, me.connected)
1129 me.hook(me.mon.disconnecthook, me.disconnected)
1130 me.hook(me.mon.notehook, me.notify)
1132 me.set_default_size(420, 180)
1136 def addpeer(me, peer):
1137 peer.i = me.listmodel.append([peer.name, peer.addr,
1138 '???', 'green', '???', 'green'])
1139 peer.win = WindowSlot(lambda: PeerWindow(me.mon, peer))
1140 peer.pinghook = HookList()
1141 peer.ping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1142 tlast = 0, ttot = 0,
1143 tcol = 2, ccol = 3, cmd = 'Transport pings')
1144 peer.eping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1145 tlast = 0, ttot = 0,
1146 tcol = 4, ccol = 5, cmd = 'Encrypted pings')
1147 def delpeer(me, peer):
1148 me.listmodel.remove(peer.i)
1149 def path_peer(me, path):
1150 return me.mon.peers[me.listmodel[path][0]]
1152 def activate(me, l, path, col):
1153 peer = me.path_peer(path)
1155 def buttonpress(me, l, ev):
1157 r = me.list.get_path_at_pos(ev.x, ev.y)
1158 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1159 me.ui.get_widget(i).set_sensitive(me.mon.connectedp and
1162 me.menupeer = me.path_peer(r[0])
1165 me.ui.get_widget('/peer-popup').popup(None, None, None,
1169 me.mon.simplecmd('KILL %s' % me.menupeer.name)
1171 me.mon.simplecmd('FORCEKX %s' % me.menupeer.name)
1174 if me.pinger is not None:
1175 GO.source_remove(me.pinger)
1176 me.pinger = GO.timeout_add(10000, me.ping)
1179 if me.pinger is not None:
1180 GO.source_remove(me.pinger)
1183 for name in me.mon.peers:
1184 p = me.mon.peers[name]
1185 PingCommand(me.mon, 'PING', p, lambda p, t: me.pong(p, p.ping, t))
1186 PingCommand(me.mon, 'EPING', p, lambda p, t: me.pong(p, p.eping, t))
1188 def pong(me, p, ping, t):
1193 me.listmodel[p.i][ping.tcol] = '(miss %d)' % ping.nmissrun
1194 me.listmodel[p.i][ping.ccol] = 'red'
1200 me.listmodel[p.i][ping.tcol] = '%.1f ms' % t
1201 me.listmodel[p.i][ping.ccol] = 'black'
1203 def setstatus(me, status):
1205 me.status.push(0, status)
1206 def notify(me, note, rest):
1207 if note == 'DAEMON':
1208 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1210 me.setstatus('Connected (port %s)' % me.mon.simplecmd('PORT')[0])
1211 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1212 for i in ('/menubar/server-menu/disconnect',
1213 '/menubar/server-menu/server-version',
1214 '/menubar/server-menu/add-peer',
1215 '/menubar/server-menu/server-quit',
1216 '/menubar/logs-menu/trace-options'):
1217 me.ui.get_widget(i).set_sensitive(True)
1218 me.ui.get_widget('/menubar/server-menu/daemon'). \
1219 set_sensitive(parseinfo(me.mon.simplecmd('SERVINFO'))['daemon'] ==
1222 def disconnected(me):
1223 me.setstatus('Disconnected')
1224 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1225 for i in ('/menubar/server-menu/disconnect',
1226 '/menubar/server-menu/server-version',
1227 '/menubar/server-menu/add-peer',
1228 '/menubar/server-menu/daemon',
1229 '/menubar/server-menu/server-quit',
1230 '/menubar/logs-menu/trace-options'):
1231 me.ui.get_widget(i).set_sensitive(False)
1234 if me.pinger is not None:
1235 GO.source_remove(me.pinger)
1236 def report(me, msg):
1240 #----- Parse options --------------------------------------------------------
1242 def version(fp = stdout):
1243 """Print the program's version number."""
1244 fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION))
1247 """Print a brief usage message for the program."""
1248 fp.write('Usage: %s [-d DIR] [-a SOCK]\n' % quis)
1252 if 'TRIPEDIR' in environ:
1253 tripedir = environ['TRIPEDIR']
1254 tripesock = '%s/%s' % (socketdir, 'tripesock')
1257 opts, args = O.getopt(argv[1:],
1259 ['help', 'version', 'usage',
1260 'directory=', 'admin-socket='])
1261 except O.GetoptError, exc:
1266 if o in ('-h', '--help'):
1271 Graphical monitor for TrIPE VPN.
1275 -h, --help Show this help message.
1276 -v, --version Show the version number.
1277 -u, --usage Show pointlessly short usage string.
1279 -d, --directory=DIR Use TrIPE directory DIR.
1280 -a, --admin-socket=FILE Select socket to connect to."""
1282 elif o in ('-v', '--version'):
1285 elif o in ('-u', '--usage'):
1288 elif o in ('-d', '--directory'):
1290 elif o in ('-a', '--admin-socket'):
1293 raise "can't happen!"
1299 mon = Monitor(tripesock)
1300 root = MonitorWindow(mon)
1301 HookClient().hook(root.closehook, exit)
1304 if __name__ == '__main__':
1307 #----- That's all, folks ----------------------------------------------------