chiark / gitweb /
Various little fixes.
[tripe] / tripemon.in
CommitLineData
060ca767 1#! @PYTHON@
2# -*-python-*-
3
4#----- Dependencies ---------------------------------------------------------
5
6import socket as S
7from sys import argv, exit, stdin, stdout, stderr
8import os as OS
9from os import environ
10import sets as SET
11import getopt as O
12import time as T
13import sre as RX
14from cStringIO import StringIO
15
16import pygtk
17pygtk.require('2.0')
18import gtk as G
19import gobject as GO
20import gtk.gdk as GDK
21
22#----- Configuration --------------------------------------------------------
23
24tripedir = "@configdir@"
25socketdir = "@socketdir@"
26PACKAGE = "@PACKAGE@"
27VERSION = "@VERSION@"
28
29debug = False
30
31#----- Utility functions ----------------------------------------------------
32
33## Program name, shorn of extraneous stuff.
34quis = OS.path.basename(argv[0])
35
36def moan(msg):
37 """Report a message to standard error."""
38 stderr.write('%s: %s\n' % (quis, msg))
39
40def die(msg, rc = 1):
41 """Report a message to standard error and exit."""
42 moan(msg)
43 exit(rc)
44
45rx_space = RX.compile(r'\s+')
46rx_ordinary = RX.compile(r'[^\\\'\"\s]+')
47rx_weird = RX.compile(r'([\\\'])')
48rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
49rx_num = RX.compile(r'^[-+]?\d+$')
50
51c_red = GDK.color_parse('red')
52
53def getword(s):
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."""
57 i = 0
58 m = rx_space.match(s, i)
59 if m: i = m.end()
60 r = ''
61 q = None
62 if i >= len(s):
63 return None, None
64 while i < len(s) and (q or not s[i].isspace()):
65 m = rx_ordinary.match(s, i)
66 if m:
67 r += m.group()
68 i = m.end()
69 elif s[i] == '\\':
70 r += s[i + 1]
71 i += 2
72 elif s[i] == q:
73 q = None
74 i += 1
75 elif not q and s[i] == '`' or s[i] == "'":
76 q = "'"
77 i += 1
78 elif not q and s[i] == '"':
79 q = '"'
80 i += 1
81 else:
82 r += s[i]
83 i += 1
84 if q:
85 raise SyntaxError, 'missing close quote'
86 m = rx_space.match(s, i)
87 if m: i = m.end()
88 return r, s[i:]
89
90def quotify(s):
91 """Quote S according to the tripe-admin(5) rules."""
92 m = rx_ordinary.match(s)
93 if m and m.end() == len(s):
94 return s
95 else:
96 return "'" + rx_weird.sub(r'\\\1', s) + "'"
97
98#----- Random bits of infrastructure ----------------------------------------
99
100class struct (object):
101 """Simple object which stores attributes and has a sensible construction
102 syntax."""
103 def __init__(me, **kw):
104 me.__dict__.update(kw)
105
106class peerinfo (struct): pass
107class pingstate (struct): pass
108
109def invoker(func):
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
112 this kludge."""
113 return lambda *hunoz, **hukairz: func()
114
115class 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."""
120 def __init__(me):
121 me.list = []
122 def add(me, func, obj):
123 me.list.append((obj, func))
124 def prune(me, obj):
125 new = []
126 for o, f in me.list:
127 if o is not obj:
128 new.append((o, f))
129 me.list = new
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
134 return None
135
136class HookClient (object):
137 def __init__(me):
138 me.hooks = SET.Set()
139 def hook(me, hk, func):
140 hk.add(func, me)
141 me.hooks.add(hk)
142 def unhook(me, hk):
143 hk.prune(me)
144 me.hooks.discard(hk)
145 def unhookall(me):
146 for hk in me.hooks:
147 hk.prune(me)
148 me.hooks.clear()
149 ##def __del__(me):
150 ## print '%s dying' % me
151
152#----- Connections and commands ---------------------------------------------
153
154class ConnException (Exception):
155 """Some sort of problem occurred while communicating with the tripe
156 server."""
157 pass
158
159class Error (ConnException):
160 """A command caused the server to issue a FAIL message."""
161 pass
162
163class ConnectionFailed (ConnException):
164 """The connection failed while communicating with the server."""
165
166jobid_seq = 0
167def jobid():
168 """Return a job tag. Used for background commands."""
169 global jobid_seq
170 jobid_seq += 1
171 return 'bg-%d' % jobid_seq
172
173class BackgroundCommand (HookClient):
174 def __init__(me, conn, cmd):
175 HookClient.__init__(me)
176 me.conn = conn
177 me.tag = None
178 me.cmd = cmd
179 me.donehook = HookList()
180 me.losthook = HookList()
181 me.info = []
182 me.submit()
183 me.hook(me.conn.disconnecthook, me.lost)
184 def submit(me):
185 me.conn.bgcommand(me.cmd, me)
186 def lost(me):
187 me.losthook.run()
188 me.unhookall()
189 def fail(me, msg):
190 me.conn.error("Unexpected error from server command `%s': %s" %
191 (me.cmd % msg))
192 me.unhookall()
193 def ok(me):
194 me.donehook.run(me.info)
195 me.unhookall()
196
197class SimpleBackgroundCommand (BackgroundCommand):
198 def submit(me):
199 try:
200 BackgroundCommand.submit(me)
201 except ConnectionFailed, err:
202 me.conn.error('Unexpected error communicating with server: %s' % msg)
203 raise
204
205class Connection (HookClient):
206
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.
210
211 Useful attributes:
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"""
217
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
224 me.sock = None
225 me.connectedp = False
226 me.connecthook = HookList()
227 me.disconnecthook = HookList()
228 me.errorhook = HookList()
229 me.inbuf = ''
230 me.info = []
231 me.waitingp = False
232 me.bgcmd = None
233 me.bgmap = {}
234 def connect(me):
235 "Connect to the server. Runs connecthook if it works."""
236 if me.sock: return
237 sock = S.socket(S.AF_UNIX, S.SOCK_STREAM)
238 try:
239 sock.connect(me.sockname)
240 except S.error, err:
241 me.error('error opening connection: %s' % err[1])
242 me.disconnecthook.run()
243 return
244 sock.setblocking(0)
245 me.socketwatch = GO.io_add_watch(sock, GO.IO_IN, me.ready)
246 me.sock = sock
247 me.connectedp = True
248 me.connecthook.run()
249 def disconnect(me):
250 "Disconnects from the server. Runs disconnecthook."
251 if not me.sock: return
252 GO.source_remove(me.socketwatch)
253 me.sock.close()
254 me.sock = None
255 me.connectedp = False
256 me.disconnecthook.run()
257 def error(me, msg):
258 """Reports an error on the connection."""
259 me.errorhook.run(msg)
260
261 def bgcommand(me, cmd, bg):
262 """Sends a background command and feeds it properly."""
263 try:
264 me.bgcmd = bg
265 err = me.docommand(cmd)
266 if err:
267 bg.fail(err)
268 finally:
269 me.bgcmd = None
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)
276 if err:
277 raise Error, err
278 return me.info
279 def docommand(me, cmd):
280 if not me.sock:
281 raise ConnException, 'not connected'
282 if debug: print ">>> %s" % cmd
283 me.sock.sendall(cmd + '\n')
284 me.waitingp = True
285 me.info = []
286 try:
287 me.sock.setblocking(1)
288 while True:
289 rc, err = me.collect()
290 if rc: break
291 finally:
292 me.waitingp = False
293 me.sock.setblocking(0)
294 if len(me.inbuf) > 0:
295 GO.idle_add(lambda: me.flushbuf() and False)
296 return err
297 def simplecmd(me, cmd):
298 """Like command(), but reports errors via the errorhook as well as
299 raising exceptions."""
300 try:
301 i = me.command(cmd)
302 except Error, msg:
303 me.error("Unexpected error from server command `%s': %s" % (cmd, msg))
304 raise
305 except ConnectionFailed, msg:
306 me.error("Unexpected error communicating with server: %s" % msg);
307 raise
308 return i
309 def ready(me, sock, condition):
310 try:
311 me.collect()
312 except ConnException, msg:
313 me.error("Error watching server connection: %s" % msg)
314 if me.sock:
315 me.disconnect()
316 me.connect()
317 return True
318 def collect(me):
319 data = me.sock.recv(16384)
320 if data == '':
321 me.disconnect()
322 raise ConnectionFailed, 'server disconnected'
323 me.inbuf += data
324 return me.flushbuf()
325 def flushbuf(me):
326 while True:
327 nl = me.inbuf.find('\n')
328 if nl < 0: break
329 line = me.inbuf[:nl]
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
335 return False, None
336 def parseline(me, code, line):
337 if code == 'BGDETACH':
338 if not me.bgcmd:
339 raise ConnectionFailed, 'unexpected detach'
340 me.bgcmd.tag = line
341 me.bgmap[line] = me.bgcmd
342 me.waitingp = False
343 me.bgcmd = None
344 return True, None
345 elif code == 'BGINFO':
346 tag, line = getword(line)
347 me.bgmap[tag].info.append(line)
348 return False, None
349 elif code == 'BGFAIL':
350 tag, line = getword(line)
351 me.bgmap[tag].fail(line)
352 del me.bgmap[tag]
353 return False, None
354 elif code == 'BGOK':
355 tag, line = getword(line)
356 me.bgmap[tag].ok()
357 del me.bgmap[tag]
358 return False, None
359 elif code == 'INFO':
360 if not me.waitingp or me.bgcmd:
361 raise ConnectionFailed, 'unexpected INFO response'
362 me.info.append(line)
363 return False, None
364 elif code == 'OK':
365 if not me.waitingp or me.bgcmd:
366 raise ConnectionFailed, 'unexpected OK response'
367 return True, None
368 elif code == 'FAIL':
369 if not me.waitingp:
370 raise ConnectionFailed, 'unexpected FAIL response'
371 return True, line
372 else:
373 raise ConnectionFailed, 'unknown response code `%s' % code
374
375class 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.
379
380 Useful attributes:
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)
395 me.delay = []
396 me.peers = {}
397 def addpeer(me, peer):
398 if peer not in me.peers:
399 p = Peer(me, peer)
400 me.peers[peer] = p
401 me.addpeerhook.run(p)
402 def delpeer(me, peer):
403 if peer in me.peers:
404 p = me.peers[peer]
405 me.delpeerhook.run(p)
406 p.dead()
407 del me.peers[peer]
408 def updatelist(me, peers):
409 newmap = {}
410 for p in peers:
411 newmap[p] = True
412 if p not in me.peers:
413 me.addpeer(p)
414 oldpeers = me.peers.copy()
415 for p in oldpeers:
416 if p not in newmap:
417 me.delpeer(p)
418 def connected(me):
419 try:
420 me.simplecmd('WATCH -A+wnt')
421 me.updatelist([s.strip() for s in me.simplecmd('LIST')])
422 except ConnException:
423 me.disconnect()
424 return
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.
429 ##
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)
438 elif code == 'WARN':
439 me.warnhook.run(line)
440 elif code == 'NOTE':
441 note, line = getword(line)
442 me.notehook.run(note, line)
443 if note == 'ADD':
444 me.addpeer(getword(line)[0])
445 elif note == 'KILL':
446 me.delpeer(line)
447 else:
448 ## Well, I asked for it.
449 pass
450 else:
451 return Connection.parseline(me, code, line)
452 return False, None
453 def flushdelay(me):
454 delay = me.delay
455 me.delay = []
456 for tag, line in delay:
457 me.parseline(tag, line)
458 return False
459
460def parseinfo(info):
461 """Parse key=value output into a dictionary."""
462 d = {}
463 for i in info:
464 for w in i.split(' '):
465 q = w.index('=')
466 d[w[:q]] = w[q + 1:]
467 return d
468
469class Peer (object):
470 """I represent a TrIPE peer. Useful attributes are:
471
472 name: peer's name
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):
478 me.mon = monitor
479 me.name = name
480 addr = me.mon.simplecmd('ADDR %s' % name)[0].split(' ')
481 if addr[0] == 'INET':
482 ipaddr, port = addr[1:]
483 try:
484 name = S.gethostbyaddr(ipaddr)[0]
485 me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
486 except S.herror:
487 me.addr = 'INET %s:%s' % (ipaddr, port)
488 else:
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()
493 me.alivep = True
494 def dead(me):
495 me.alivep = False
496 me.deadhook.run()
497
498#----- Window management cruft ----------------------------------------------
499
500class MyWindowMixin (G.Window, HookClient):
501 """Mixin for windows which call a closehook when they're destroyed."""
502 def mywininit(me):
503 me.closehook = HookList()
504 HookClient.__init__(me)
505 me.connect('destroy', invoker(me.close))
506 def close(me):
507 me.closehook.run()
508 me.destroy()
509 me.unhookall()
510class 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)
514 me.mywininit()
515class 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
520 class."""
521 i = 0
522 br = []
523 me.rmap = []
524 for b, f in buttons:
525 br.append(b)
526 br.append(i)
527 me.rmap.append(f)
528 i += 1
529 G.Dialog.__init__(me, title, None, flags, tuple(br))
530 HookClient.__init__(me)
531 me.mywininit()
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]()
536
ca6eb20c 537def makeactiongroup(name, acts):
538 """Creates an ActionGroup called NAME. ACTS is a list of tuples
539 containing:
540 ACT: an action name
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)
549 return actgroup
550
551class GridPacker (G.Table):
552 """Like a Table, but with more state: makes filling in the widgets
553 easier."""
554 def __init__(me):
555 G.Table.__init__(me)
556 me.row = 0
557 me.col = 0
558 me.rows = 1
559 me.cols = 1
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,
565 xpad = 0, ypad = 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."""
569 if newlinep:
570 me.row += 1
571 me.col = 0
572 bot = me.row + 1
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)
580 me.col += width
581 return w
582 def labelled(me, lab, w, newlinep = False, **kw):
583 """Packs a labelled widget. Other arguments are as for pack. Returns
584 W."""
585 label = G.Label(lab)
586 label.set_alignment(1.0, 0)
587 me.pack(label, newlinep = newlinep, xopt = G.FILL)
588 me.pack(w, **kw)
589 return w
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
593 entry widget."""
594 e = G.Entry()
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)
599 return e
600
060ca767 601class 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
604 make a new one."""
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
610 me.window = None
611 def open(me):
612 """Opens the window, creating it if necessary."""
613 if me.window:
614 me.window.window.raise_()
615 else:
616 me.window = me.createfunc()
617 me.hook(me.window.closehook, me.closed)
618 def closed(me):
619 me.unhook(me.window.closehook)
620 me.window = None
621
622class ValidationError (Exception):
623 """Raised by ValidatingEntry.get_text() if the text isn't valid."""
624 pass
625class 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
628 raised."""
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)
635 if callable(valid):
636 me.validate = valid
637 else:
638 me.validate = RX.compile(valid).match
639 me.ensure_style()
640 me.c_ok = me.get_style().text[G.STATE_NORMAL]
641 me.c_bad = c_red
642 if size != -1: me.set_width_chars(size)
643 me.set_activates_default(True)
644 me.set_text(text)
645 me.check()
646 def check(me, *hunoz):
647 if me.validate(G.Entry.get_text(me)):
648 me.validp = True
649 me.modify_text(G.STATE_NORMAL, me.c_ok)
650 else:
651 me.validp = False
652 me.modify_text(G.STATE_NORMAL, me.c_bad)
653 def get_text(me):
654 if not me.validp:
655 raise ValidationError
656 return G.Entry.get_text(me)
657
658def 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
661 bounds."""
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))
665
666#----- Various minor dialog boxen -------------------------------------------
667
668GPL = """This program is free software; you can redistribute it and/or modify
669it under the terms of the GNU General Public License as published by
670the Free Software Foundation; either version 2 of the License, or
671(at your option) any later version.
672
673This program is distributed in the hope that it will be useful,
674but WITHOUT ANY WARRANTY; without even the implied warranty of
675MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
676GNU General Public License for more details.
677
678You should have received a copy of the GNU General Public License
679along with this program; if not, write to the Free Software Foundation,
680Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
681
682class AboutBox (G.AboutDialog, MyWindowMixin):
683 """The program `About' box."""
684 def __init__(me):
685 G.AboutDialog.__init__(me)
686 me.mywininit()
687 me.set_name('TrIPEmon')
688 me.set_version(VERSION)
689 me.set_license(GPL)
690 me.set_authors(['Mark Wooding'])
691 me.connect('unmap', invoker(me.close))
692 me.show()
693aboutbox = WindowSlot(AboutBox)
694
695def moanbox(msg):
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)))
700 label = G.Label(msg)
701 label.set_padding(20, 20)
702 d.vbox.pack_start(label)
703 label.show()
704 d.run()
705 d.destroy()
706
ca6eb20c 707def unimplemented(*hunoz):
708 """Indicator of laziness."""
709 moanbox("I've not written that bit yet.")
710
711class ServInfo (MyWindow):
712 def __init__(me, monitor):
713 MyWindow.__init__(me)
714 me.set_title('TrIPE server info')
715 me.mon = monitor
716 me.table = GridPacker()
717 me.add(me.table)
718 me.e = {}
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)
723 me.update()
724 me.hook(me.mon.connecthook, me.update)
725 me.show_all()
726 def update(me):
727 info = parseinfo(me.mon.simplecmd('SERVINFO'))
728 for i in me.e:
729 me.e[i].set_text(info[i])
730
731class 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)])
737 me.mon = monitor
738 me.opts = []
739 for o in me.mon.simplecmd('TRACE'):
740 char = o[0]
741 onp = o[1]
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))
748 me.show_all()
749 def ok(me):
750 on = []
751 off = []
752 for char, ticky in me.opts:
753 if ticky.get_active():
754 on.append(char)
755 else:
756 off.append(char)
757 setting = ''.join(on) + '-' + ''.join(off)
758 me.mon.simplecmd('TRACE %s' % setting)
759 me.destroy()
760
060ca767 761#----- Logging windows ------------------------------------------------------
762
763class 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)
774
775class TraceLogModel (LogModel):
776 """Log model for trace messages."""
777 def __init__(me):
778 LogModel.__init__(me, ('Message',))
779 def notify(me, line):
780 """Call with a new trace message."""
781 me.add(line)
782
783class WarningLogModel (LogModel):
784 """Log model for warnings. We split the category out into a separate
785 column."""
786 def __init__(me):
787 LogModel.__init__(me, ('Category', 'Message'))
788 def notify(me, line):
789 """Call with a new warning message."""
790 me.add(*getword(line))
791
792class LogViewer (MyWindow):
793 """Log viewer window. Nothing very exciting."""
794 def __init__(me, model):
795 MyWindow.__init__(me)
796 me.model = model
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()
801 i = 0
802 for c in me.model.cols:
803 me.list.append_column(G.TreeViewColumn(c,
804 G.CellRendererText(),
805 text = i))
806 i += 1
807 me.set_default_size(440, 256)
808 scr.add(me.list)
809 me.add(scr)
810 me.show_all()
811
ca6eb20c 812#----- Peer window ----------------------------------------------------------
060ca767 813
814def xlate_time(t):
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)
820def xlate_bytes(b):
821 """Translate a number of bytes into something a human might want to read."""
822 suff = 'B'
823 b = int(b)
824 for s in 'KMG':
825 if b < 4096: break
826 b /= 1024
827 suff = s
828 return '%d %s' % (b, suff)
829
830## How to translate peer stats. Maps the stat name to a translation
831## function.
832statsxlate = \
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)]
842
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
845## the entry.
846statslayout = \
847 [('Start time', '%(start-time)s'),
848 ('Last key-exchange', '%(last-keyexch-time)s'),
849 ('Last packet', '%(last-packet-time)s'),
850 ('Packets in/out',
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)'),
854 ('IP in/out',
855 '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-in)s (%(ip-bytes-in)s)'),
856 ('Rejected packets', '%(rejected-packets)s')]
857
858class 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)
863 me.mon = monitor
864 me.peer = peer
865 table = GridPacker()
866 me.add(table)
867 me.e = {}
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)
873 add('Keepalives',
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)
879 me.timeout = None
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)
885 me.tryupdate()
886 me.ping()
887 me.show_all()
888 def update(me):
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)
895 return True
896 def tryupdate(me):
897 if me.timeout is None and me.update():
898 me.timeout = GO.timeout_add(1000, me.update)
899 def stopupdate(me):
900 if me.timeout is not None:
901 GO.source_remove(me.timeout)
902 me.timeout = None
903 def dead(me):
904 me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
905 me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
906 me.stopupdate()
907 def ping(me):
908 for ping in me.peer.ping, me.peer.eping:
909 s = '%d/%d' % (ping.ngood, ping.n)
910 if ping.ngood:
911 s += '; %.2f ms (last %.1f ms)' % (ping.ttot/ping.ngood, ping.tlast);
912 me.e[ping.cmd].set_text(s)
913
ca6eb20c 914#----- Add peer -------------------------------------------------------------
915
060ca767 916class AddPeerCommand (SimpleBackgroundCommand):
917 def __init__(me, conn, dlg, name, addr, port,
918 keepalive = None, tunnel = None):
919 me.name = name
920 me.addr = addr
921 me.port = port
922 me.keepalive = keepalive
923 me.tunnel = tunnel
924 cmd = StringIO()
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))
932 def fail(me, err):
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)
940 else:
941 moanbox("Unexpected error from server command `ADD': %s" % err)
942
943class 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)])
948 me.mon = monitor
949 table = GridPacker()
950 me.vbox.pack_start(table)
951 me.e_name = table.labelled('Name',
952 ValidatingEntry(r'^[^\s.:]+$', '', 16),
953 width = 3)
954 me.e_addr = table.labelled('Address',
955 ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
956 newlinep = True)
957 me.e_port = table.labelled('Port',
958 ValidatingEntry(numericvalidate(0, 65535),
959 '22003',
960 5))
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')
966 for t in me.tuns:
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\
972 (t.get_active()))
973 me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
974 me.e_keepalive.set_sensitive(False)
975 table.pack(me.e_keepalive, width = 3)
976 me.show_all()
977 def ok(me):
978 try:
979 if me.c_keepalive.get_active():
980 ka = me.e_keepalive.get_text()
981 else:
982 ka = None
983 t = me.l_tunnel.get_active()
984 if t == 0:
985 tun = None
986 else:
987 tun = me.tuns[t]
988 AddPeerCommand(me.mon, me,
989 me.e_name.get_text(),
990 me.e_addr.get_text(),
991 me.e_port.get_text(),
992 keepalive = ka,
993 tunnel = tun)
994 except ValidationError:
995 GDK.beep()
996 return
997
ca6eb20c 998#----- The server monitor ---------------------------------------------------
060ca767 999
1000class PingCommand (SimpleBackgroundCommand):
1001 def __init__(me, conn, cmd, peer, func):
1002 me.peer = peer
1003 me.func = func
1004 SimpleBackgroundCommand.__init__ \
1005 (me, conn, '%s -background %s %s' % (cmd, jobid(), peer.name))
1006 def ok(me):
1007 tok, rest = getword(me.info[0])
1008 if tok == 'ping-ok':
1009 me.func(me.peer, float(rest))
1010 else:
1011 me.func(me.peer, None)
1012 me.unhookall()
1013 def fail(me, err): me.unhookall()
1014 def lost(me): me.unhookall()
1015
1016class MonitorWindow (MyWindow):
1017
1018 def __init__(me, monitor):
1019 MyWindow.__init__(me)
1020 me.set_title('TrIPE monitor')
1021 me.mon = 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)
1027
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))
1033
1034 vbox = G.VBox()
1035 me.add(vbox)
1036
1037 me.ui = G.UIManager()
ca6eb20c 1038 def cmd(c): me.mon.simplecmd(c)
060ca767 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),
ca6eb20c 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')),
060ca767 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)])
1058 uidef = '''
1059 <ui>
1060 <menubar>
1061 <menu action="file-menu">
1062 <menuitem action="quit"/>
1063 </menu>
1064 <menu action="server-menu">
ca6eb20c 1065 <menuitem action="connect"/>
060ca767 1066 <menuitem action="disconnect"/>
1067 <separator/>
ca6eb20c 1068 <menuitem action="server-version"/>
060ca767 1069 <menuitem action="add-peer"/>
1070 <menuitem action="daemon"/>
ca6eb20c 1071 <menuitem action="reload-keys"/>
060ca767 1072 <separator/>
1073 <menuitem action="server-quit"/>
1074 </menu>
1075 <menu action="logs-menu">
1076 <menuitem action="show-warnings"/>
1077 <menuitem action="show-trace"/>
1078 <menuitem action="trace-options"/>
1079 </menu>
1080 <menu action="help-menu">
1081 <menuitem action="about"/>
1082 </menu>
1083 </menubar>
1084 <popup name="peer-popup">
1085 <menuitem action="add-peer"/>
1086 <menuitem action="kill-peer"/>
1087 <menuitem action="force-kx"/>
1088 </popup>
1089 </ui>
1090 '''
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()
1096
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)
1101
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(),
1107 text = 0))
1108 me.list.append_column(G.TreeViewColumn('Address',
1109 G.CellRendererText(),
1110 text = 1))
1111 me.list.append_column(G.TreeViewColumn('T-ping',
1112 G.CellRendererText(),
1113 text = 2,
1114 foreground = 3))
1115 me.list.append_column(G.TreeViewColumn('E-ping',
1116 G.CellRendererText(),
1117 text = 4,
1118 foreground = 5))
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)
1124 scr.add(me.list)
1125 vbox.pack_start(scr)
1126
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)
1131 me.pinger = None
1132 me.set_default_size(420, 180)
1133 me.mon.connect()
1134 me.show_all()
1135
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]]
1151
1152 def activate(me, l, path, col):
1153 peer = me.path_peer(path)
1154 peer.win.open()
1155 def buttonpress(me, l, ev):
1156 if ev.button == 3:
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
1160 r is not None)
1161 if r:
1162 me.menupeer = me.path_peer(r[0])
1163 else:
1164 me.menupeer = None
1165 me.ui.get_widget('/peer-popup').popup(None, None, None,
1166 ev.button, ev.time)
1167
1168 def killpeer(me):
1169 me.mon.simplecmd('KILL %s' % me.menupeer.name)
1170 def forcekx(me):
1171 me.mon.simplecmd('FORCEKX %s' % me.menupeer.name)
1172
1173 def reping(me):
1174 if me.pinger is not None:
1175 GO.source_remove(me.pinger)
1176 me.pinger = GO.timeout_add(10000, me.ping)
1177 me.ping()
1178 def unping(me):
1179 if me.pinger is not None:
1180 GO.source_remove(me.pinger)
1181 me.pinger = None
1182 def ping(me):
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))
1187 return True
1188 def pong(me, p, ping, t):
1189 ping.n += 1
1190 if t is None:
1191 ping.nmiss += 1
1192 ping.nmissrun += 1
1193 me.listmodel[p.i][ping.tcol] = '(miss %d)' % ping.nmissrun
1194 me.listmodel[p.i][ping.ccol] = 'red'
1195 else:
1196 ping.ngood += 1
1197 ping.nmissrun = 0
1198 ping.tlast = t
1199 ping.ttot += t
1200 me.listmodel[p.i][ping.tcol] = '%.1f ms' % t
1201 me.listmodel[p.i][ping.ccol] = 'black'
1202 p.pinghook.run()
1203 def setstatus(me, status):
1204 me.status.pop(0)
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)
1209 def connected(me):
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'] ==
1220 'nil')
1221 me.reping()
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)
1232 me.unping()
1233 def destroy(me):
1234 if me.pinger is not None:
1235 GO.source_remove(me.pinger)
1236 def report(me, msg):
1237 moanbox(msg)
1238 return True
1239
1240#----- Parse options --------------------------------------------------------
1241
1242def version(fp = stdout):
1243 """Print the program's version number."""
1244 fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION))
1245
1246def usage(fp):
1247 """Print a brief usage message for the program."""
1248 fp.write('Usage: %s [-d DIR] [-a SOCK]\n' % quis)
1249
1250def main():
1251 global tripedir
1252 if 'TRIPEDIR' in environ:
1253 tripedir = environ['TRIPEDIR']
1254 tripesock = '%s/%s' % (socketdir, 'tripesock')
1255
1256 try:
1257 opts, args = O.getopt(argv[1:],
1258 'hvud:a:',
1259 ['help', 'version', 'usage',
1260 'directory=', 'admin-socket='])
1261 except O.GetoptError, exc:
1262 moan(exc)
1263 usage(stderr)
1264 exit(1)
1265 for o, v in opts:
1266 if o in ('-h', '--help'):
1267 version(stdout)
1268 print
1269 usage(stdout)
1270 print """
1271Graphical monitor for TrIPE VPN.
1272
1273Options supported:
1274
1275-h, --help Show this help message.
1276-v, --version Show the version number.
1277-u, --usage Show pointlessly short usage string.
1278
1279-d, --directory=DIR Use TrIPE directory DIR.
1280-a, --admin-socket=FILE Select socket to connect to."""
1281 exit(0)
1282 elif o in ('-v', '--version'):
1283 version(stdout)
1284 exit(0)
1285 elif o in ('-u', '--usage'):
1286 usage(stdout)
1287 exit(0)
1288 elif o in ('-d', '--directory'):
1289 tripedir = v
1290 elif o in ('-a', '--admin-socket'):
1291 tripesock = v
1292 else:
1293 raise "can't happen!"
1294 if len(args) > 0:
1295 usage(stderr)
1296 exit(1)
1297
1298 OS.chdir(tripedir)
1299 mon = Monitor(tripesock)
1300 root = MonitorWindow(mon)
1301 HookClient().hook(root.closehook, exit)
1302 G.main()
1303
1304if __name__ == '__main__':
1305 main()
1306
ca6eb20c 1307#----- That's all, folks ----------------------------------------------------