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 class WindowSlot (HookClient):
538 """A place to store a window. If the window is destroyed, remember this;
539 when we come to open the window, raise it if it already exists; otherwise
541 def __init__(me, createfunc):
542 """Constructor: CREATEFUNC must return a new Window which supports the
543 closehook protocol."""
544 HookClient.__init__(me)
545 me.createfunc = createfunc
548 """Opens the window, creating it if necessary."""
550 me.window.window.raise_()
552 me.window = me.createfunc()
553 me.hook(me.window.closehook, me.closed)
555 me.unhook(me.window.closehook)
558 class ValidationError (Exception):
559 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
561 class ValidatingEntry (G.Entry):
562 """Like an Entry, but makes the text go red if the contents are invalid.
563 If get_text is called, and the text is invalid, ValidationError is
565 def __init__(me, valid, text = '', size = -1, *arg, **kw):
566 """Make an Entry. VALID is a regular expression or a predicate on
567 strings. TEXT is the default text to insert. SIZE is the size of the
568 box to set, in characters (ish). Other arguments are passed to Entry."""
569 G.Entry.__init__(me, *arg, **kw)
570 me.connect("changed", me.check)
574 me.validate = RX.compile(valid).match
576 me.c_ok = me.get_style().text[G.STATE_NORMAL]
578 if size != -1: me.set_width_chars(size)
579 me.set_activates_default(True)
582 def check(me, *hunoz):
583 if me.validate(G.Entry.get_text(me)):
585 me.modify_text(G.STATE_NORMAL, me.c_ok)
588 me.modify_text(G.STATE_NORMAL, me.c_bad)
591 raise ValidationError
592 return G.Entry.get_text(me)
594 def numericvalidate(min = None, max = None):
595 """Validation function for numbers. Entry must consist of an optional sign
596 followed by digits, and the resulting integer must be within the given
598 return lambda x: (rx_num.match(x) and
599 (min is None or long(x) >= min) and
600 (max is None or long(x) <= max))
602 #----- Various minor dialog boxen -------------------------------------------
604 GPL = """This program is free software; you can redistribute it and/or modify
605 it under the terms of the GNU General Public License as published by
606 the Free Software Foundation; either version 2 of the License, or
607 (at your option) any later version.
609 This program is distributed in the hope that it will be useful,
610 but WITHOUT ANY WARRANTY; without even the implied warranty of
611 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
612 GNU General Public License for more details.
614 You should have received a copy of the GNU General Public License
615 along with this program; if not, write to the Free Software Foundation,
616 Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
618 class AboutBox (G.AboutDialog, MyWindowMixin):
619 """The program `About' box."""
621 G.AboutDialog.__init__(me)
623 me.set_name('TrIPEmon')
624 me.set_version(VERSION)
626 me.set_authors(['Mark Wooding'])
627 me.connect('unmap', invoker(me.close))
629 aboutbox = WindowSlot(AboutBox)
632 """Report an error message in a window."""
633 d = G.Dialog('Error from %s' % quis,
634 flags = G.DIALOG_MODAL,
635 buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
637 label.set_padding(20, 20)
638 d.vbox.pack_start(label)
643 #----- Logging windows ------------------------------------------------------
645 class LogModel (G.ListStore):
646 """A simple list of log messages."""
647 def __init__(me, columns):
648 """Call with a list of column names. All must be strings. We add a time
649 column to the left."""
650 me.cols = ('Time',) + columns
651 G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
652 def add(me, *entries):
653 """Adds a new log message, with a timestamp."""
654 now = T.strftime('%Y-%m-%d %H:%M:%S')
655 me.append((now,) + entries)
657 class TraceLogModel (LogModel):
658 """Log model for trace messages."""
660 LogModel.__init__(me, ('Message',))
661 def notify(me, line):
662 """Call with a new trace message."""
665 class WarningLogModel (LogModel):
666 """Log model for warnings. We split the category out into a separate
669 LogModel.__init__(me, ('Category', 'Message'))
670 def notify(me, line):
671 """Call with a new warning message."""
672 me.add(*getword(line))
674 class LogViewer (MyWindow):
675 """Log viewer window. Nothing very exciting."""
676 def __init__(me, model):
677 MyWindow.__init__(me)
679 scr = G.ScrolledWindow()
680 scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
681 me.list = G.TreeView(me.model)
682 me.closehook = HookList()
684 for c in me.model.cols:
685 me.list.append_column(G.TreeViewColumn(c,
686 G.CellRendererText(),
689 me.set_default_size(440, 256)
694 def makeactiongroup(name, acts):
695 """Creates an ActionGroup called NAME. ACTS is a list of tuples
698 LABEL: the label string for the action
699 ACCEL: accelerator string, or None
700 FUNC: thunk to call when the action is invoked"""
701 actgroup = G.ActionGroup(name)
702 for act, label, accel, func in acts:
703 a = G.Action(act, label, None, None)
704 if func: a.connect('activate', invoker(func))
705 actgroup.add_action_with_accel(a, accel)
708 class TraceOptions (MyDialog):
709 """Tracing options window."""
710 def __init__(me, monitor):
711 MyDialog.__init__(me, title = 'Tracing options',
712 buttons = [(G.STOCK_CLOSE, me.destroy),
713 (G.STOCK_OK, me.ok)])
716 for o in me.mon.simplecmd('TRACE'):
719 text = o[3].upper() + o[4:]
720 if char.isupper(): continue
721 ticky = G.CheckButton(text)
722 ticky.set_active(onp != ' ')
723 me.vbox.pack_start(ticky)
724 me.opts.append((char, ticky))
729 for char, ticky in me.opts:
730 if ticky.get_active():
734 setting = ''.join(on) + '-' + ''.join(off)
735 me.mon.simplecmd('TRACE %s' % setting)
738 def unimplemented(*hunoz):
739 """Indicator of laziness."""
740 moanbox("I've not written that bit yet.")
742 class GridPacker (G.Table):
743 """Like a Table, but with more state: makes filling in the widgets
751 me.set_border_width(4)
752 me.set_col_spacings(4)
753 me.set_row_spacings(4)
754 def pack(me, w, width = 1, newlinep = False,
755 xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
757 """Packs a new widget. W is the widget to add. XOPY, YOPT, XPAD and
758 YPAD are as for Table. WIDTH is how many cells to take up horizontally.
759 NEWLINEP is whether to start a new line for this widget. Returns W."""
764 right = me.col + width
765 if bot > me.rows or right > me.cols:
766 if bot > me.rows: me.rows = bot
767 if right > me.cols: me.cols = right
768 me.resize(me.rows, me.cols)
769 me.attach(w, me.col, me.col + width, me.row, me.row + 1,
770 xopt, yopt, xpad, ypad)
773 def labelled(me, lab, w, newlinep = False, **kw):
774 """Packs a labelled widget. Other arguments are as for pack. Returns
777 label.set_alignment(1.0, 0)
778 me.pack(label, newlinep = newlinep, xopt = G.FILL)
781 def info(me, label, text = None, len = 18, **kw):
783 if text is not None: e.set_text(text)
784 e.set_width_chars(len)
785 e.set_editable(False)
786 me.labelled(label, e, **kw)
790 """Translate a time in tripe's stats format to something a human might
791 actually want to read."""
792 if t == 'NEVER': return '(never)'
793 Y, M, D, h, m, s = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
794 return '%04d:%02d:%02d %02d:%02d:%02d' % (Y, M, D, h, m, s)
796 """Translate a number of bytes into something a human might want to read."""
803 return '%d %s' % (b, suff)
805 ## How to translate peer stats. Maps the stat name to a translation
808 [('start-time', xlate_time),
809 ('last-packet-time', xlate_time),
810 ('last-keyexch-time', xlate_time),
811 ('bytes-in', xlate_bytes),
812 ('bytes-out', xlate_bytes),
813 ('keyexch-bytes-in', xlate_bytes),
814 ('keyexch-bytes-out', xlate_bytes),
815 ('ip-bytes-in', xlate_bytes),
816 ('ip-bytes-out', xlate_bytes)]
818 ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is
819 ## the label to give the entry box; FORMAT is the format string to write into
822 [('Start time', '%(start-time)s'),
823 ('Last key-exchange', '%(last-keyexch-time)s'),
824 ('Last packet', '%(last-packet-time)s'),
826 '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
827 ('Key-exchange in/out',
828 '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
830 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-in)s (%(ip-bytes-in)s)'),
831 ('Rejected packets', '%(rejected-packets)s')]
833 class PeerWindow (MyWindow):
834 """Show information about a peer."""
835 def __init__(me, monitor, peer):
836 MyWindow.__init__(me)
837 me.set_title('TrIPE statistics: %s' % peer.name)
843 def add(label, text = None):
844 me.e[label] = table.info(label, text, len = 42, newlinep = True)
845 add('Peer name', peer.name)
846 add('Tunnel', peer.tunnel)
847 add('Interface', peer.ifname)
849 (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
850 add('Address', peer.addr)
851 add('Transport pings')
852 add('Encrypted pings')
853 for label, format in statslayout: add(label)
855 me.hook(me.mon.connecthook, me.tryupdate)
856 me.hook(me.mon.disconnecthook, me.stopupdate)
857 me.hook(me.closehook, me.stopupdate)
858 me.hook(me.peer.deadhook, me.dead)
859 me.hook(me.peer.pinghook, me.ping)
864 if not me.peer.alivep or not me.mon.connectedp: return False
865 stat = parseinfo(me.mon.simplecmd('STATS %s' % me.peer.name))
866 for s, trans in statsxlate:
867 stat[s] = trans(stat[s])
868 for label, format in statslayout:
869 me.e[label].set_text(format % stat)
872 if me.timeout is None and me.update():
873 me.timeout = GO.timeout_add(1000, me.update)
875 if me.timeout is not None:
876 GO.source_remove(me.timeout)
879 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
880 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
883 for ping in me.peer.ping, me.peer.eping:
884 s = '%d/%d' % (ping.ngood, ping.n)
886 s += '; %.2f ms (last %.1f ms)' % (ping.ttot/ping.ngood, ping.tlast);
887 me.e[ping.cmd].set_text(s)
889 class AddPeerCommand (SimpleBackgroundCommand):
890 def __init__(me, conn, dlg, name, addr, port,
891 keepalive = None, tunnel = None):
895 me.keepalive = keepalive
898 cmd.write('ADD %s' % name)
899 cmd.write(' -background %s' % jobid())
900 if keepalive is not None: cmd.write(' -keepalive %s' % keepalive)
901 if tunnel is not None: cmd.write(' -tunnel %s' % tunnel)
902 cmd.write(' INET %s %s' % (addr, port))
903 SimpleBackgroundCommand.__init__(me, conn, cmd.getvalue())
904 me.hook(me.donehook, invoker(dlg.destroy))
906 token, msg = getword(str(err))
907 if token in ('resolve-error', 'resolver-timeout'):
908 moanbox("Unable to resolve hostname `%s'" % me.addr)
909 elif token == 'peer-create-fail':
910 moanbox("Couldn't create new peer `%s'" % me.name)
911 elif token == 'peer-exists':
912 moanbox("Peer `%s' already exists" % me.name)
914 moanbox("Unexpected error from server command `ADD': %s" % err)
916 class AddPeerDialog (MyDialog):
917 def __init__(me, monitor):
918 MyDialog.__init__(me, 'Add peer',
919 buttons = [(G.STOCK_CANCEL, me.destroy),
920 (G.STOCK_OK, me.ok)])
923 me.vbox.pack_start(table)
924 me.e_name = table.labelled('Name',
925 ValidatingEntry(r'^[^\s.:]+$', '', 16),
927 me.e_addr = table.labelled('Address',
928 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
930 me.e_port = table.labelled('Port',
931 ValidatingEntry(numericvalidate(0, 65535),
934 me.c_keepalive = G.CheckButton('Keepalives')
935 me.l_tunnel = table.labelled('Tunnel',
936 G.combo_box_new_text(),
937 newlinep = True, width = 3)
938 me.tuns = me.mon.simplecmd('TUNNELS')
940 me.l_tunnel.append_text(t)
941 me.l_tunnel.set_active(0)
942 table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
943 me.c_keepalive.connect('toggled',
944 lambda t: me.e_keepalive.set_sensitive\
946 me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
947 me.e_keepalive.set_sensitive(False)
948 table.pack(me.e_keepalive, width = 3)
952 if me.c_keepalive.get_active():
953 ka = me.e_keepalive.get_text()
956 t = me.l_tunnel.get_active()
961 AddPeerCommand(me.mon, me,
962 me.e_name.get_text(),
963 me.e_addr.get_text(),
964 me.e_port.get_text(),
967 except ValidationError:
971 class ServInfo (MyWindow):
972 def __init__(me, monitor):
973 MyWindow.__init__(me)
974 me.set_title('TrIPE server info')
976 me.table = GridPacker()
979 def add(label, tag, text = None, **kw):
980 me.e[tag] = me.table.info(label, text, **kw)
981 add('Implementation', 'implementation')
982 add('Version', 'version', newlinep = True)
984 me.hook(me.mon.connecthook, me.update)
987 info = parseinfo(me.mon.simplecmd('SERVINFO'))
989 me.e[i].set_text(info[i])
991 class PingCommand (SimpleBackgroundCommand):
992 def __init__(me, conn, cmd, peer, func):
995 SimpleBackgroundCommand.__init__ \
996 (me, conn, '%s -background %s %s' % (cmd, jobid(), peer.name))
998 tok, rest = getword(me.info[0])
1000 me.func(me.peer, float(rest))
1002 me.func(me.peer, None)
1004 def fail(me, err): me.unhookall()
1005 def lost(me): me.unhookall()
1007 class MonitorWindow (MyWindow):
1009 def __init__(me, monitor):
1010 MyWindow.__init__(me)
1011 me.set_title('TrIPE monitor')
1013 me.hook(me.mon.errorhook, me.report)
1014 me.warnings = WarningLogModel()
1015 me.hook(me.mon.warnhook, me.warnings.notify)
1016 me.trace = TraceLogModel()
1017 me.hook(me.mon.tracehook, me.trace.notify)
1019 me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
1020 me.traceview = WindowSlot(lambda: LogViewer(me.trace))
1021 me.traceopts = WindowSlot(lambda: TraceOptions(me.mon))
1022 me.addpeerwin = WindowSlot(lambda: AddPeerDialog(me.mon))
1023 me.servinfo = WindowSlot(lambda: ServInfo(me.mon))
1028 me.ui = G.UIManager()
1029 actgroup = makeactiongroup('monitor',
1030 [('file-menu', '_File', None, None),
1031 ('connect', '_Connect', '<Alt>C', me.mon.connect),
1032 ('disconnect', '_Disconnect', '<Alt>D', me.mon.disconnect),
1033 ('quit', '_Quit', '<Alt>Q', me.close),
1034 ('server-menu', '_Server', None, None),
1035 ('daemon', 'Run in _background', None,
1036 lambda: me.mon.simplecmd('DAEMON')),
1037 ('server-version', 'Server version', None, me.servinfo.open),
1038 ('server-quit', 'Terminate server', None,
1039 lambda: me.mon.simplecmd('QUIT')),
1040 ('logs-menu', '_Logs', None, None),
1041 ('show-warnings', 'Show _warnings', '<Alt>W', me.warnview.open),
1042 ('show-trace', 'Show _trace', '<Alt>T', me.traceview.open),
1043 ('trace-options', 'Trace _options...', None, me.traceopts.open),
1044 ('help-menu', '_Help', None, None),
1045 ('about', '_About tripemon...', None, aboutbox.open),
1046 ('add-peer', '_Add peer...', '<Alt>A', me.addpeerwin.open),
1047 ('kill-peer', '_Kill peer', None, me.killpeer),
1048 ('force-kx', 'Force key e_xchange', None, me.forcekx)])
1052 <menu action="file-menu">
1053 <menuitem action="quit"/>
1055 <menu action="server-menu">
1056 <menuitem action="connect"/>
1057 <menuitem action="disconnect"/>
1059 <menuitem action="add-peer"/>
1060 <menuitem action="daemon"/>
1061 <menuitem action="server-version"/>
1063 <menuitem action="server-quit"/>
1065 <menu action="logs-menu">
1066 <menuitem action="show-warnings"/>
1067 <menuitem action="show-trace"/>
1068 <menuitem action="trace-options"/>
1070 <menu action="help-menu">
1071 <menuitem action="about"/>
1074 <popup name="peer-popup">
1075 <menuitem action="add-peer"/>
1076 <menuitem action="kill-peer"/>
1077 <menuitem action="force-kx"/>
1081 me.ui.insert_action_group(actgroup, 0)
1082 me.ui.add_ui_from_string(uidef)
1083 vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
1084 me.add_accel_group(me.ui.get_accel_group())
1085 me.status = G.Statusbar()
1087 me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
1088 me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
1089 me.hook(me.mon.addpeerhook, me.addpeer)
1090 me.hook(me.mon.delpeerhook, me.delpeer)
1092 scr = G.ScrolledWindow()
1093 scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
1094 me.list = G.TreeView(me.listmodel)
1095 me.list.append_column(G.TreeViewColumn('Peer name',
1096 G.CellRendererText(),
1098 me.list.append_column(G.TreeViewColumn('Address',
1099 G.CellRendererText(),
1101 me.list.append_column(G.TreeViewColumn('T-ping',
1102 G.CellRendererText(),
1105 me.list.append_column(G.TreeViewColumn('E-ping',
1106 G.CellRendererText(),
1109 me.list.get_column(1).set_expand(True)
1110 me.list.connect('row-activated', me.activate)
1111 me.list.connect('button-press-event', me.buttonpress)
1112 me.list.set_reorderable(True)
1113 me.list.get_selection().set_mode(G.SELECTION_NONE)
1115 vbox.pack_start(scr)
1117 vbox.pack_start(me.status, expand = False)
1118 me.hook(me.mon.connecthook, me.connected)
1119 me.hook(me.mon.disconnecthook, me.disconnected)
1120 me.hook(me.mon.notehook, me.notify)
1122 me.set_default_size(420, 180)
1126 def addpeer(me, peer):
1127 peer.i = me.listmodel.append([peer.name, peer.addr,
1128 '???', 'green', '???', 'green'])
1129 peer.win = WindowSlot(lambda: PeerWindow(me.mon, peer))
1130 peer.pinghook = HookList()
1131 peer.ping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1132 tlast = 0, ttot = 0,
1133 tcol = 2, ccol = 3, cmd = 'Transport pings')
1134 peer.eping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
1135 tlast = 0, ttot = 0,
1136 tcol = 4, ccol = 5, cmd = 'Encrypted pings')
1137 def delpeer(me, peer):
1138 me.listmodel.remove(peer.i)
1139 def path_peer(me, path):
1140 return me.mon.peers[me.listmodel[path][0]]
1142 def activate(me, l, path, col):
1143 peer = me.path_peer(path)
1145 def buttonpress(me, l, ev):
1147 r = me.list.get_path_at_pos(ev.x, ev.y)
1148 for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
1149 me.ui.get_widget(i).set_sensitive(me.mon.connectedp and
1152 me.menupeer = me.path_peer(r[0])
1155 me.ui.get_widget('/peer-popup').popup(None, None, None,
1159 me.mon.simplecmd('KILL %s' % me.menupeer.name)
1161 me.mon.simplecmd('FORCEKX %s' % me.menupeer.name)
1164 if me.pinger is not None:
1165 GO.source_remove(me.pinger)
1166 me.pinger = GO.timeout_add(10000, me.ping)
1169 if me.pinger is not None:
1170 GO.source_remove(me.pinger)
1173 for name in me.mon.peers:
1174 p = me.mon.peers[name]
1175 PingCommand(me.mon, 'PING', p, lambda p, t: me.pong(p, p.ping, t))
1176 PingCommand(me.mon, 'EPING', p, lambda p, t: me.pong(p, p.eping, t))
1178 def pong(me, p, ping, t):
1183 me.listmodel[p.i][ping.tcol] = '(miss %d)' % ping.nmissrun
1184 me.listmodel[p.i][ping.ccol] = 'red'
1190 me.listmodel[p.i][ping.tcol] = '%.1f ms' % t
1191 me.listmodel[p.i][ping.ccol] = 'black'
1193 def setstatus(me, status):
1195 me.status.push(0, status)
1196 def notify(me, note, rest):
1197 if note == 'DAEMON':
1198 me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
1200 me.setstatus('Connected (port %s)' % me.mon.simplecmd('PORT')[0])
1201 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
1202 for i in ('/menubar/server-menu/disconnect',
1203 '/menubar/server-menu/server-version',
1204 '/menubar/server-menu/add-peer',
1205 '/menubar/server-menu/server-quit',
1206 '/menubar/logs-menu/trace-options'):
1207 me.ui.get_widget(i).set_sensitive(True)
1208 me.ui.get_widget('/menubar/server-menu/daemon'). \
1209 set_sensitive(parseinfo(me.mon.simplecmd('SERVINFO'))['daemon'] ==
1212 def disconnected(me):
1213 me.setstatus('Disconnected')
1214 me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
1215 for i in ('/menubar/server-menu/disconnect',
1216 '/menubar/server-menu/server-version',
1217 '/menubar/server-menu/add-peer',
1218 '/menubar/server-menu/daemon',
1219 '/menubar/server-menu/server-quit',
1220 '/menubar/logs-menu/trace-options'):
1221 me.ui.get_widget(i).set_sensitive(False)
1224 if me.pinger is not None:
1225 GO.source_remove(me.pinger)
1226 def report(me, msg):
1230 #----- Parse options --------------------------------------------------------
1232 def version(fp = stdout):
1233 """Print the program's version number."""
1234 fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION))
1237 """Print a brief usage message for the program."""
1238 fp.write('Usage: %s [-d DIR] [-a SOCK]\n' % quis)
1242 if 'TRIPEDIR' in environ:
1243 tripedir = environ['TRIPEDIR']
1244 tripesock = '%s/%s' % (socketdir, 'tripesock')
1247 opts, args = O.getopt(argv[1:],
1249 ['help', 'version', 'usage',
1250 'directory=', 'admin-socket='])
1251 except O.GetoptError, exc:
1256 if o in ('-h', '--help'):
1261 Graphical monitor for TrIPE VPN.
1265 -h, --help Show this help message.
1266 -v, --version Show the version number.
1267 -u, --usage Show pointlessly short usage string.
1269 -d, --directory=DIR Use TrIPE directory DIR.
1270 -a, --admin-socket=FILE Select socket to connect to."""
1272 elif o in ('-v', '--version'):
1275 elif o in ('-u', '--usage'):
1278 elif o in ('-d', '--directory'):
1280 elif o in ('-a', '--admin-socket'):
1283 raise "can't happen!"
1289 mon = Monitor(tripesock)
1290 root = MonitorWindow(mon)
1291 HookClient().hook(root.closehook, exit)
1294 if __name__ == '__main__':