4 #----- Dependencies ---------------------------------------------------------
7 from sys import argv, exit, stdin, stdout, stderr
15 from cStringIO import StringIO
23 #----- Configuration --------------------------------------------------------
25 tripedir = "@configdir@"
26 socketdir = "@socketdir@"
32 #----- Utility functions ----------------------------------------------------
34 ## Program name, shorn of extraneous stuff.
35 quis = OS.path.basename(argv[0])
38 """Report a message to standard error."""
39 stderr.write('%s: %s\n' % (quis, msg))
42 """Report a message to standard error and exit."""
46 rx_space = RX.compile(r'\s+')
47 rx_ordinary = RX.compile(r'[^\\\'\"\s]+')
48 rx_weird = RX.compile(r'([\\\'])')
49 rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
50 rx_num = RX.compile(r'^[-+]?\d+$')
52 c_red = GDK.color_parse('red')
55 """Pull a word from the front of S, handling quoting according to the
56 tripe-admin(5) rules. Returns the word and the rest of S, or (None, None)
57 if there are no more words left."""
59 m = rx_space.match(s, i)
65 while i < len(s) and (q or not s[i].isspace()):
66 m = rx_ordinary.match(s, i)
76 elif not q and s[i] == '`' or s[i] == "'":
79 elif not q and s[i] == '"':
86 raise SyntaxError, 'missing close quote'
87 m = rx_space.match(s, i)
92 """Quote S according to the tripe-admin(5) rules."""
93 m = rx_ordinary.match(s)
94 if m and m.end() == len(s):
97 return "'" + rx_weird.sub(r'\\\1', s) + "'"
99 #----- Random bits of infrastructure ----------------------------------------
101 class struct (object):
102 """Simple object which stores attributes and has a sensible construction
104 def __init__(me, **kw):
105 me.__dict__.update(kw)
107 class peerinfo (struct): pass
108 class pingstate (struct): pass
111 """Return a function which throws away its arguments and calls FUNC. (If
112 for loops worked by binding rather than assignment then we wouldn't need
114 return lambda *hunoz, **hukairz: func()
116 class HookList (object):
117 """I maintain a list of functions, and provide the ability to call them
118 when something interesting happens. The functions are called in the order
119 they were added to the list, with all the arguments. If a function returns
120 a non-None result, no further functions are called."""
123 def add(me, func, obj):
124 me.list.append((obj, func))
131 def run(me, *args, **kw):
132 for o, hook in me.list:
133 rc = hook(*args, **kw)
134 if rc is not None: return rc
137 class HookClient (object):
140 def hook(me, hk, func):
151 ## print '%s dying' % me
153 #----- Connections and commands ---------------------------------------------
155 class ConnException (Exception):
156 """Some sort of problem occurred while communicating with the tripe
160 class Error (ConnException):
161 """A command caused the server to issue a FAIL message."""
164 class ConnectionFailed (ConnException):
165 """The connection failed while communicating with the server."""
169 """Return a job tag. Used for background commands."""
172 return 'bg-%d' % jobid_seq
174 class BackgroundCommand (HookClient):
175 def __init__(me, conn, cmd):
176 HookClient.__init__(me)
180 me.donehook = HookList()
181 me.losthook = HookList()
184 me.hook(me.conn.disconnecthook, me.lost)
186 me.conn.bgcommand(me.cmd, me)
191 me.conn.error("Unexpected error from server command `%s': %s" %
195 me.donehook.run(me.info)
198 class SimpleBackgroundCommand (BackgroundCommand):
201 BackgroundCommand.submit(me)
202 except ConnectionFailed, err:
203 me.conn.error('Unexpected error communicating with server: %s' % msg)
206 class Connection (HookClient):
208 """I represent a connection to the TrIPE server. I provide facilities for
209 sending commands and receiving replies. The connection is notional: the
210 underlying socket connection can come and go under our feet.
213 connectedp: whether the connection is active
214 connecthook: called when we have connected
215 disconnecthook: called if we have disconnected
216 notehook: called with asynchronous notifications
217 errorhook: called if there was a command error"""
219 def __init__(me, sockname):
220 """Make a new connection to the server listening to SOCKNAME. In fact,
221 we're initially disconnected, to allow the caller to get his life in
222 order before opening the floodgates."""
223 HookClient.__init__(me)
224 me.sockname = sockname
226 me.connectedp = False
227 me.connecthook = HookList()
228 me.disconnecthook = HookList()
229 me.errorhook = HookList()
236 "Connect to the server. Runs connecthook if it works."""
238 sock = S.socket(S.AF_UNIX, S.SOCK_STREAM)
240 sock.connect(me.sockname)
242 me.error('error opening connection: %s' % err[1])
243 me.disconnecthook.run()
246 me.socketwatch = GO.io_add_watch(sock, GO.IO_IN, me.ready)
251 "Disconnects from the server. Runs disconnecthook."
252 if not me.sock: return
253 GO.source_remove(me.socketwatch)
256 me.connectedp = False
257 me.disconnecthook.run()
259 """Reports an error on the connection."""
260 me.errorhook.run(msg)
262 def bgcommand(me, cmd, bg):
263 """Sends a background command and feeds it properly."""
266 err = me.docommand(cmd)
271 def command(me, cmd):
272 """Sends a command to the server. Returns a list of INFO responses. Do
273 not use this for backgrounded commands: create a BackgroundCommand
274 instead. Raises apprpopriate exceptions on error, but doesn't send
275 report them to the errorhook."""
276 err = me.docommand(cmd)
280 def docommand(me, cmd):
282 raise ConnException, 'not connected'
283 if debug: print ">>> %s" % cmd
284 me.sock.sendall(cmd + '\n')
288 me.sock.setblocking(1)
290 rc, err = me.collect()
294 me.sock.setblocking(0)
295 if len(me.inbuf) > 0:
296 GO.idle_add(lambda: me.flushbuf() and False)
298 def simplecmd(me, cmd):
299 """Like command(), but reports errors via the errorhook as well as
300 raising exceptions."""
304 me.error("Unexpected error from server command `%s': %s" % (cmd, msg))
306 except ConnectionFailed, msg:
307 me.error("Unexpected error communicating with server: %s" % msg);
310 def ready(me, sock, condition):
313 except ConnException, msg:
314 me.error("Error watching server connection: %s" % msg)
320 data = me.sock.recv(16384)
323 raise ConnectionFailed, 'server disconnected'
328 nl = me.inbuf.find('\n')
331 if debug: print "<<< %s" % line
332 me.inbuf = me.inbuf[nl + 1:]
333 tag, line = getword(line)
334 rc, err = me.parseline(tag, line)
335 if rc: return rc, err
337 def parseline(me, code, line):
338 if code == 'BGDETACH':
340 raise ConnectionFailed, 'unexpected detach'
342 me.bgmap[line] = me.bgcmd
346 elif code == 'BGINFO':
347 tag, line = getword(line)
348 me.bgmap[tag].info.append(line)
350 elif code == 'BGFAIL':
351 tag, line = getword(line)
352 me.bgmap[tag].fail(line)
356 tag, line = getword(line)
361 if not me.waitingp or me.bgcmd:
362 raise ConnectionFailed, 'unexpected INFO response'
366 if not me.waitingp or me.bgcmd:
367 raise ConnectionFailed, 'unexpected OK response'
371 raise ConnectionFailed, 'unexpected FAIL response'
374 raise ConnectionFailed, 'unknown response code `%s' % code
376 class Monitor (Connection):
377 """I monitor a TrIPE server, noticing when it changes state and keeping
378 track of its peers. I also provide facilities for sending the server
379 commands and collecting the answers.
382 addpeerhook: called with a new Peer when the server adds one
383 delpeerhook: called with a Peer when the server kills one
384 tracehook: called with a trace message
385 warnhook: called with a warning message
386 peers: mapping from names to Peer objects"""
387 def __init__(me, sockname):
388 """Initializes the monitor."""
389 Connection.__init__(me, sockname)
390 me.addpeerhook = HookList()
391 me.delpeerhook = HookList()
392 me.tracehook = HookList()
393 me.warnhook = HookList()
394 me.notehook = HookList()
395 me.hook(me.connecthook, me.connected)
398 def addpeer(me, peer):
399 if peer not in me.peers:
402 me.addpeerhook.run(p)
403 def delpeer(me, peer):
406 me.delpeerhook.run(p)
409 def updatelist(me, peers):
413 if p not in me.peers:
415 oldpeers = me.peers.copy()
421 me.simplecmd('WATCH -A+wnt')
422 me.updatelist([s.strip() for s in me.simplecmd('LIST')])
423 except ConnException:
426 def parseline(me, code, line):
427 ## Delay async messages until the current command is done. Otherwise the
428 ## handler for the async message might send another command before this
429 ## one's complete, and the whole edifice turns to jelly.
431 ## No, this isn't the server's fault. If we rely on the server to delay
432 ## notifications then there's a race between when we send a command and
433 ## when the server gets it.
434 if me.waitingp and code in ('TRACE', 'WARN', 'NOTE'):
435 if len(me.delay) == 0: GO.idle_add(me.flushdelay)
436 me.delay.append((code, line))
437 elif code == 'TRACE':
438 me.tracehook.run(line)
440 me.warnhook.run(line)
442 note, line = getword(line)
443 me.notehook.run(note, line)
445 me.addpeer(getword(line)[0])
449 ## Well, I asked for it.
452 return Connection.parseline(me, code, line)
457 for tag, line in delay:
458 me.parseline(tag, line)
462 """Parse key=value output into a dictionary."""
465 for w in i.split(' '):
471 """I represent a TrIPE peer. Useful attributes are:
474 addr: human-friendly representation of the peer's address
475 ifname: interface associated with the peer
476 alivep: true if the peer hasn't been killed
477 deadhook: called with no arguments when the peer is killed"""
478 def __init__(me, monitor, name):
481 addr = me.mon.simplecmd('ADDR %s' % name)[0].split(' ')
482 if addr[0] == 'INET':
483 ipaddr, port = addr[1:]
485 name = S.gethostbyaddr(ipaddr)[0]
486 me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
488 me.addr = 'INET %s:%s' % (ipaddr, port)
490 me.addr = ' '.join(addr)
491 me.ifname = me.mon.simplecmd('IFNAME %s' % me.name)[0]
492 me.__dict__.update(parseinfo(me.mon.simplecmd('PEERINFO %s' % me.name)))
493 me.deadhook = HookList()
499 #----- Window management cruft ----------------------------------------------
501 class MyWindowMixin (G.Window, HookClient):
502 """Mixin for windows which call a closehook when they're destroyed."""
504 me.closehook = HookList()
505 HookClient.__init__(me)
506 me.connect('destroy', invoker(me.close))
511 class MyWindow (MyWindowMixin):
512 """A window which calls a closehook when it's destroyed."""
513 def __init__(me, kind = G.WINDOW_TOPLEVEL):
514 G.Window.__init__(me, kind)
516 class MyDialog (G.Dialog, MyWindowMixin, HookClient):
517 """A dialogue box with a closehook and sensible button binding."""
518 def __init__(me, title = None, flags = 0, buttons = []):
519 """The buttons are a list of (STOCKID, THUNK) pairs: call the appropriate
520 THUNK when the button is pressed. The others are just like GTK's Dialog
530 G.Dialog.__init__(me, title, None, flags, tuple(br))
531 HookClient.__init__(me)
533 me.set_default_response(i - 1)
534 me.connect('response', me.respond)
535 def respond(me, hunoz, rid, *hukairz):
536 if rid >= 0: me.rmap[rid]()
538 def makeactiongroup(name, acts):
539 """Creates an ActionGroup called NAME. ACTS is a list of tuples
542 LABEL: the label string for the action
543 ACCEL: accelerator string, or None
544 FUNC: thunk to call when the action is invoked"""
545 actgroup = G.ActionGroup(name)
546 for act, label, accel, func in acts:
547 a = G.Action(act, label, None, None)
548 if func: a.connect('activate', invoker(func))
549 actgroup.add_action_with_accel(a, accel)
552 class GridPacker (G.Table):
553 """Like a Table, but with more state: makes filling in the widgets
561 me.set_border_width(4)
562 me.set_col_spacings(4)
563 me.set_row_spacings(4)
564 def pack(me, w, width = 1, newlinep = False,
565 xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
567 """Packs a new widget. W is the widget to add. XOPY, YOPT, XPAD and
568 YPAD are as for Table. WIDTH is how many cells to take up horizontally.
569 NEWLINEP is whether to start a new line for this widget. Returns W."""
574 right = me.col + width
575 if bot > me.rows or right > me.cols:
576 if bot > me.rows: me.rows = bot
577 if right > me.cols: me.cols = right
578 me.resize(me.rows, me.cols)
579 me.attach(w, me.col, me.col + width, me.row, me.row + 1,
580 xopt, yopt, xpad, ypad)
583 def labelled(me, lab, w, newlinep = False, **kw):
584 """Packs a labelled widget. Other arguments are as for pack. Returns
587 label.set_alignment(1.0, 0)
588 me.pack(label, newlinep = newlinep, xopt = G.FILL)
591 def info(me, label, text = None, len = 18, **kw):
592 """Packs an information widget with a label. LABEL is the label; TEXT is
593 the initial text; LEN is the estimated length in characters. Returns the
596 if text is not None: e.set_text(text)
597 e.set_width_chars(len)
598 e.set_editable(False)
599 me.labelled(label, e, **kw)
602 class WindowSlot (HookClient):
603 """A place to store a window. If the window is destroyed, remember this;
604 when we come to open the window, raise it if it already exists; otherwise
606 def __init__(me, createfunc):
607 """Constructor: CREATEFUNC must return a new Window which supports the
608 closehook protocol."""
609 HookClient.__init__(me)
610 me.createfunc = createfunc
613 """Opens the window, creating it if necessary."""
615 me.window.window.raise_()
617 me.window = me.createfunc()
618 me.hook(me.window.closehook, me.closed)
620 me.unhook(me.window.closehook)
623 class ValidationError (Exception):
624 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
626 class ValidatingEntry (G.Entry):
627 """Like an Entry, but makes the text go red if the contents are invalid.
628 If get_text is called, and the text is invalid, ValidationError is
630 def __init__(me, valid, text = '', size = -1, *arg, **kw):
631 """Make an Entry. VALID is a regular expression or a predicate on
632 strings. TEXT is the default text to insert. SIZE is the size of the
633 box to set, in characters (ish). Other arguments are passed to Entry."""
634 G.Entry.__init__(me, *arg, **kw)
635 me.connect("changed", me.check)
639 me.validate = RX.compile(valid).match
641 me.c_ok = me.get_style().text[G.STATE_NORMAL]
643 if size != -1: me.set_width_chars(size)
644 me.set_activates_default(True)
647 def check(me, *hunoz):
648 if me.validate(G.Entry.get_text(me)):
650 me.modify_text(G.STATE_NORMAL, me.c_ok)
653 me.modify_text(G.STATE_NORMAL, me.c_bad)
656 raise ValidationError
657 return G.Entry.get_text(me)
659 def numericvalidate(min = None, max = None):
660 """Validation function for numbers. Entry must consist of an optional sign
661 followed by digits, and the resulting integer must be within the given
663 return lambda x: (rx_num.match(x) and
664 (min is None or long(x) >= min) and
665 (max is None or long(x) <= max))
667 #----- Various minor dialog boxen -------------------------------------------
669 GPL = """This program is free software; you can redistribute it and/or modify
670 it under the terms of the GNU General Public License as published by
671 the Free Software Foundation; either version 2 of the License, or
672 (at your option) any later version.
674 This program is distributed in the hope that it will be useful,
675 but WITHOUT ANY WARRANTY; without even the implied warranty of
676 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
677 GNU General Public License for more details.
679 You should have received a copy of the GNU General Public License
680 along with this program; if not, write to the Free Software Foundation,
681 Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
683 class AboutBox (G.AboutDialog, MyWindowMixin):
684 """The program `About' box."""
686 G.AboutDialog.__init__(me)
688 me.set_name('TrIPEmon')
689 me.set_version(VERSION)
691 me.set_authors(['Mark Wooding'])
692 me.connect('unmap', invoker(me.close))
694 aboutbox = WindowSlot(AboutBox)
697 """Report an error message in a window."""
698 d = G.Dialog('Error from %s' % quis,
699 flags = G.DIALOG_MODAL,
700 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
702 label.set_padding(20, 20)
703 d.vbox.pack_start(label)
708 def unimplemented(*hunoz):
709 """Indicator of laziness."""
710 moanbox("I've not written that bit yet.")
712 class ServInfo (MyWindow):
713 def __init__(me, monitor):
714 MyWindow.__init__(me)
715 me.set_title('TrIPE server info')
717 me.table = GridPacker()
720 def add(label, tag, text = None, **kw):
721 me.e[tag] = me.table.info(label, text, **kw)
722 add('Implementation', 'implementation')
723 add('Version', 'version', newlinep = True)
725 me.hook(me.mon.connecthook, me.update)
728 info = parseinfo(me.mon.simplecmd('SERVINFO'))
730 me.e[i].set_text(info[i])
732 class TraceOptions (MyDialog):
733 """Tracing options window."""
734 def __init__(me, monitor):
735 MyDialog.__init__(me, title = 'Tracing options',
736 buttons = [(G.STOCK_CLOSE, me.destroy),
737 (G.STOCK_OK, me.ok)])
740 for o in me.mon.simplecmd('TRACE'):
743 text = o[3].upper() + o[4:]
744 if char.isupper(): continue
745 ticky = G.CheckButton(text)
746 ticky.set_active(onp != ' ')
747 me.vbox.pack_start(ticky)
748 me.opts.append((char, ticky))
753 for char, ticky in me.opts:
754 if ticky.get_active():
758 setting = ''.join(on) + '-' + ''.join(off)
759 me.mon.simplecmd('TRACE %s' % setting)
762 #----- Logging windows ------------------------------------------------------
764 class LogModel (G.ListStore):
765 """A simple list of log messages."""
766 def __init__(me, columns):
767 """Call with a list of column names. All must be strings. We add a time
768 column to the left."""
769 me.cols = ('Time',) + columns
770 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
771 def add(me, *entries):
772 """Adds a new log message, with a timestamp."""
773 now = T.strftime('%Y-%m-%d %H:%M:%S')
774 me.append((now,) + entries)
776 class TraceLogModel (LogModel):
777 """Log model for trace messages."""
779 LogModel.__init__(me, ('Message',))
780 def notify(me, line):
781 """Call with a new trace message."""
784 class WarningLogModel (LogModel):
785 """Log model for warnings. We split the category out into a separate
788 LogModel.__init__(me, ('Category', 'Message'))
789 def notify(me, line):
790 """Call with a new warning message."""
791 me.add(*getword(line))
793 class LogViewer (MyWindow):
794 """Log viewer window. Nothing very exciting."""
795 def __init__(me, model):
796 MyWindow.__init__(me)
798 scr = G.ScrolledWindow()
799 scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
800 me.list = G.TreeView(me.model)
801 me.closehook = HookList()
803 for c in me.model.cols:
804 me.list.append_column(G.TreeViewColumn(c,
805 G.CellRendererText(),
808 me.set_default_size(440, 256)
813 #----- Peer window ----------------------------------------------------------
816 """Translate a time in tripe's stats format to something a human might
817 actually want to read."""
818 if t == 'NEVER': return '(never)'
819 YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
820 ago = T.time() - T.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1))
821 ago = M.floor(ago); unit = 's'
822 for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]:
826 return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \
827 (YY, MM, DD, hh, mm, ss, ago, unit)
829 """Translate a number of bytes into something a human might want to read."""
836 return '%d %s' % (b, suff)
838 ## How to translate peer stats. Maps the stat name to a translation
841 [('start-time', xlate_time),
842 ('last-packet-time', xlate_time),
843 ('last-keyexch-time', xlate_time),
844 ('bytes-in', xlate_bytes),
845 ('bytes-out', xlate_bytes),
846 ('keyexch-bytes-in', xlate_bytes),
847 ('keyexch-bytes-out', xlate_bytes),
848 ('ip-bytes-in', xlate_bytes),
849 ('ip-bytes-out', xlate_bytes)]
851 ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
852 ## the label to give the entry box; FORMAT is the format string to write into
855 [('Start time', '%(start-time)s'),
856 ('Last key-exchange', '%(last-keyexch-time)s'),
857 ('Last packet', '%(last-packet-time)s'),
859 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
860 ('Key-exchange in/out',
861 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
863 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-in)s (%(ip-bytes-in)s)'),
864 ('Rejected packets', '%(rejected-packets)s')]
866 class PeerWindow (MyWindow):
867 """Show information about a peer."""
868 def __init__(me, monitor, peer):
869 MyWindow.__init__(me)
870 me.set_title('TrIPE statistics: %s' % peer.name)
876 def add(label, text = None):
877 me.e[label] = table.info(label, text, len = 42, newlinep = True)
878 add('Peer name', peer.name)
879 add('Tunnel', peer.tunnel)
880 add('Interface', peer.ifname)
882 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
883 add('Address', peer.addr)
884 add('Transport pings')
885 add('Encrypted pings')
886 for label, format in statslayout: add(label)
888 me.hook(me.mon.connecthook, me.tryupdate)
889 me.hook(me.mon.disconnecthook, me.stopupdate)
890 me.hook(me.closehook, me.stopupdate)
891 me.hook(me.peer.deadhook, me.dead)
892 me.hook(me.peer.pinghook, me.ping)
897 if not me.peer.alivep or not me.mon.connectedp: return False
898 stat = parseinfo(me.mon.simplecmd('STATS %s' % me.peer.name))
899 for s, trans in statsxlate:
900 stat[s] = trans(stat[s])
901 for label, format in statslayout:
902 me.e[label].set_text(format % stat)
905 if me.timeout is None and me.update():
906 me.timeout = GO.timeout_add(1000, me.update)
908 if me.timeout is not None:
909 GO.source_remove(me.timeout)
912 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
913 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
916 for ping in me.peer.ping, me.peer.eping:
917 s = '%d/%d' % (ping.ngood, ping.n)
919 s += ' (%.1f%%)' % (ping.ngood * 100.0/ping.n)
921 s += '; %.2f ms (last %.1f ms)' % (ping.ttot/ping.ngood, ping.tlast);
922 me.e[ping.cmd].set_text(s)
924 #----- Add peer -------------------------------------------------------------
926 class AddPeerCommand (SimpleBackgroundCommand):
927 def __init__(me, conn, dlg, name, addr, port,
928 keepalive = None, tunnel = None):
932 me.keepalive = keepalive
935 cmd.write('ADD %s' % name)
936 cmd.write(' -background %s' % jobid())
937 if keepalive is not None: cmd.write(' -keepalive %s' % keepalive)
938 if tunnel is not None: cmd.write(' -tunnel %s' % tunnel)
939 cmd.write(' INET %s %s' % (addr, port))
940 SimpleBackgroundCommand.__init__(me, conn, cmd.getvalue())
941 me.hook(me.donehook, invoker(dlg.destroy))
943 token, msg = getword(str(err))
944 if token in ('resolve-error', 'resolver-timeout'):
945 moanbox("Unable to resolve hostname `%s'" % me.addr)
946 elif token == 'peer-create-fail':
947 moanbox("Couldn't create new peer `%s'" % me.name)
948 elif token == 'peer-exists':
949 moanbox("Peer `%s' already exists" % me.name)
951 moanbox("Unexpected error from server command `ADD': %s" % err)
953 class AddPeerDialog (MyDialog):
954 def __init__(me, monitor):
955 MyDialog.__init__(me, 'Add peer',
956 buttons = [(G.STOCK_CANCEL, me.destroy),
957 (G.STOCK_OK, me.ok)])
960 me.vbox.pack_start(table)
961 me.e_name = table.labelled('Name',
962 ValidatingEntry(r'^[^\s.:]+$', '', 16),
964 me.e_addr = table.labelled('Address',
965 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
967 me.e_port = table.labelled('Port',
968 ValidatingEntry(numericvalidate(0, 65535),
971 me.c_keepalive = G.CheckButton('Keepalives')
972 me.l_tunnel = table.labelled('Tunnel',
973 G.combo_box_new_text(),
974 newlinep = True, width = 3)
975 me.tuns = me.mon.simplecmd('TUNNELS')
977 me.l_tunnel.append_text(t)
978 me.l_tunnel.set_active(0)
979 table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
980 me.c_keepalive.connect('toggled',
981 lambda t: me.e_keepalive.set_sensitive\
983 me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
984 me.e_keepalive.set_sensitive(False)
985 table.pack(me.e_keepalive, width = 3)
989 if me.c_keepalive.get_active():
990 ka = me.e_keepalive.get_text()
993 t = me.l_tunnel.get_active()
998 AddPeerCommand(me.mon, me,
999 me.e_name.get_text(),
1000 me.e_addr.get_text(),
1001 me.e_port.get_text(),
1004 except ValidationError:
1008 #----- The server monitor ---------------------------------------------------
1010 class PingCommand (SimpleBackgroundCommand):
1011 def __init__(me, conn, cmd, peer, func):
1014 SimpleBackgroundCommand.__init__ \
1015 (me, conn, '%s -background %s %s' % (cmd, jobid(), peer.name))
1017 tok, rest = getword(me.info[0])
1018 if tok == 'ping-ok':
1019 me.func(me.peer, float(rest))
1021 me.func(me.peer, None)
1023 def fail(me, err): me.unhookall()
1024 def lost(me): me.unhookall()
1026 class MonitorWindow (MyWindow):
1028 def __init__(me, monitor):
1029 MyWindow.__init__(me)
1030 me.set_title('TrIPE monitor')
1032 me.hook(me.mon.errorhook, me.report)
1033 me.warnings = WarningLogModel()
1034 me.hook(me.mon.warnhook, me.warnings.notify)
1035 me.trace = TraceLogModel()
1036 me.hook(me.mon.tracehook, me.trace.notify)
1038 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1039 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1040 me.traceopts = WindowSlot(lambda: TraceOptions(me.mon))
1041 me.addpeerwin = WindowSlot(lambda: AddPeerDialog(me.mon))
1042 me.servinfo = WindowSlot(lambda: ServInfo(me.mon))
1047 me.ui = G.UIManager()
1048 def cmd(c): return lambda: me.mon.simplecmd(c)
1049 actgroup = makeactiongroup('monitor',
1050 [('file-menu', '_File', None, None),
1051 ('connect', '_Connect', '<Alt>C', me.mon.connect),
1052 ('disconnect', '_Disconnect', '<Alt>D', me.mon.disconnect),
1053 ('quit', '_Quit', '<Alt>Q', me.close),
1054 ('server-menu', '_Server', None, None),
1055 ('daemon', 'Run in _background', None, cmd('DAEMON')),
1056 ('server-version', 'Server version', '<Alt>V', me.servinfo.open),
1057 ('reload-keys', 'Reload keys', '<Alt>R', cmd('RELOAD')),
1058 ('server-quit', 'Terminate server', None, cmd('QUIT')),
1059 ('logs-menu', '_Logs', None, None),
1060 ('show-warnings', 'Show _warnings', '<Alt>W', me.warnview.open),
1061 ('show-trace', 'Show _trace', '<Alt>T', me.traceview.open),
1062 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1063 ('help-menu', '_Help', None, None),
1064 ('about', '_About tripemon...', None, aboutbox.open),
1065 ('add-peer', '_Add peer...', '<Alt>A', me.addpeerwin.open),
1066 ('kill-peer', '_Kill peer', None, me.killpeer),
1067 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1071 <menu action="file-menu">
1072 <menuitem action="quit"/>
1074 <menu action="server-menu">
1075 <menuitem action="connect"/>
1076 <menuitem action="disconnect"/>
1078 <menuitem action="server-version"/>
1079 <menuitem action="add-peer"/>
1080 <menuitem action="daemon"/>
1081 <menuitem action="reload-keys"/>
1083 <menuitem action="server-quit"/>
1085 <menu action="logs-menu">
1086 <menuitem action="show-warnings"/>
1087 <menuitem action="show-trace"/>
1088 <menuitem action="trace-options"/>
1090 <menu action="help-menu">
1091 <menuitem action="about"/>
1094 <popup name="peer-popup">
1095 <menuitem action="add-peer"/>
1096 <menuitem action="kill-peer"/>
1097 <menuitem action="force-kx"/>
1101 me.ui.insert_action_group(actgroup, 0)
1102 me.ui.add_ui_from_string(uidef)
1103 vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
1104 me.add_accel_group(me.ui.get_accel_group())
1105 me.status = G.Statusbar()
1107 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1108 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1109 me.hook(me.mon.addpeerhook, me.addpeer)
1110 me.hook(me.mon.delpeerhook, me.delpeer)
1112 scr = G.ScrolledWindow()
1113 scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
1114 me.list = G.TreeView(me.listmodel)
1115 me.list.append_column(G.TreeViewColumn('Peer name',
1116 G.CellRendererText(),
1118 me.list.append_column(G.TreeViewColumn('Address',
1119 G.CellRendererText(),
1121 me.list.append_column(G.TreeViewColumn('T-ping',
1122 G.CellRendererText(),
1125 me.list.append_column(G.TreeViewColumn('E-ping',
1126 G.CellRendererText(),
1129 me.list.get_column(1).set_expand(True)
1130 me.list.connect('row-activated', me.activate)
1131 me.list.connect('button-press-event', me.buttonpress)
1132 me.list.set_reorderable(True)
1133 me.list.get_selection().set_mode(G.SELECTION_NONE)
1135 vbox.pack_start(scr)
1137 vbox.pack_start(me.status, expand = False)
1138 me.hook(me.mon.connecthook, me.connected)
1139 me.hook(me.mon.disconnecthook, me.disconnected)
1140 me.hook(me.mon.notehook, me.notify)
1142 me.set_default_size(420, 180)
1146 def addpeer(me, peer):
1147 peer.i = me.listmodel.append([peer.name, peer.addr,
1148 '???', 'green', '???', 'green'])
1149 peer.win = WindowSlot(lambda: PeerWindow(me.mon, peer))
1150 peer.pinghook = HookList()
1151 peer.ping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1152 tlast = 0, ttot = 0,
1153 tcol = 2, ccol = 3, cmd = 'Transport pings')
1154 peer.eping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1155 tlast = 0, ttot = 0,
1156 tcol = 4, ccol = 5, cmd = 'Encrypted pings')
1157 def delpeer(me, peer):
1158 me.listmodel.remove(peer.i)
1159 def path_peer(me, path):
1160 return me.mon.peers[me.listmodel[path][0]]
1162 def activate(me, l, path, col):
1163 peer = me.path_peer(path)
1165 def buttonpress(me, l, ev):
1167 r = me.list.get_path_at_pos(ev.x, ev.y)
1168 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1169 me.ui.get_widget(i).set_sensitive(me.mon.connectedp and
1172 me.menupeer = me.path_peer(r[0])
1175 me.ui.get_widget('/peer-popup').popup(None, None, None,
1179 me.mon.simplecmd('KILL %s' % me.menupeer.name)
1181 me.mon.simplecmd('FORCEKX %s' % me.menupeer.name)
1184 if me.pinger is not None:
1185 GO.source_remove(me.pinger)
1186 me.pinger = GO.timeout_add(10000, me.ping)
1189 if me.pinger is not None:
1190 GO.source_remove(me.pinger)
1193 for name in me.mon.peers:
1194 p = me.mon.peers[name]
1195 PingCommand(me.mon, 'PING', p, lambda p, t: me.pong(p, p.ping, t))
1196 PingCommand(me.mon, 'EPING', p, lambda p, t: me.pong(p, p.eping, t))
1198 def pong(me, p, ping, t):
1203 me.listmodel[p.i][ping.tcol] = '(miss %d)' % ping.nmissrun
1204 me.listmodel[p.i][ping.ccol] = 'red'
1210 me.listmodel[p.i][ping.tcol] = '%.1f ms' % t
1211 me.listmodel[p.i][ping.ccol] = 'black'
1213 def setstatus(me, status):
1215 me.status.push(0, status)
1216 def notify(me, note, rest):
1217 if note == 'DAEMON':
1218 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1220 me.setstatus('Connected (port %s)' % me.mon.simplecmd('PORT')[0])
1221 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1222 for i in ('/menubar/server-menu/disconnect',
1223 '/menubar/server-menu/server-version',
1224 '/menubar/server-menu/add-peer',
1225 '/menubar/server-menu/server-quit',
1226 '/menubar/logs-menu/trace-options'):
1227 me.ui.get_widget(i).set_sensitive(True)
1228 me.ui.get_widget('/menubar/server-menu/daemon'). \
1229 set_sensitive(parseinfo(me.mon.simplecmd('SERVINFO'))['daemon'] ==
1232 def disconnected(me):
1233 me.setstatus('Disconnected')
1234 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1235 for i in ('/menubar/server-menu/disconnect',
1236 '/menubar/server-menu/server-version',
1237 '/menubar/server-menu/add-peer',
1238 '/menubar/server-menu/daemon',
1239 '/menubar/server-menu/server-quit',
1240 '/menubar/logs-menu/trace-options'):
1241 me.ui.get_widget(i).set_sensitive(False)
1244 if me.pinger is not None:
1245 GO.source_remove(me.pinger)
1246 def report(me, msg):
1250 #----- Parse options --------------------------------------------------------
1252 def version(fp = stdout):
1253 """Print the program's version number."""
1254 fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION))
1257 """Print a brief usage message for the program."""
1258 fp.write('Usage: %s [-d DIR] [-a SOCK]\n' % quis)
1262 if 'TRIPEDIR' in environ:
1263 tripedir = environ['TRIPEDIR']
1264 tripesock = environ.get('TRIPESOCK', '%s/%s' % (socketdir, 'tripesock'))
1267 opts, args = O.getopt(argv[1:],
1269 ['help', 'version', 'usage',
1270 'directory=', 'admin-socket='])
1271 except O.GetoptError, exc:
1276 if o in ('-h', '--help'):
1281 Graphical monitor for TrIPE VPN.
1285 -h, --help Show this help message.
1286 -v, --version Show the version number.
1287 -u, --usage Show pointlessly short usage string.
1289 -d, --directory=DIR Use TrIPE directory DIR.
1290 -a, --admin-socket=FILE Select socket to connect to."""
1292 elif o in ('-v', '--version'):
1295 elif o in ('-u', '--usage'):
1298 elif o in ('-d', '--directory'):
1300 elif o in ('-a', '--admin-socket'):
1303 raise "can't happen!"
1309 mon = Monitor(tripesock)
1310 root = MonitorWindow(mon)
1311 HookClient().hook(root.closehook, exit)
1314 if __name__ == '__main__':
1317 #----- That's all, folks ----------------------------------------------------