060ca767 |
1 | #! @PYTHON@ |
2 | # -*-python-*- |
3 | |
4 | #----- Dependencies --------------------------------------------------------- |
5 | |
6 | import socket as S |
7 | from sys import argv, exit, stdin, stdout, stderr |
8 | import os as OS |
9 | from os import environ |
10 | import sets as SET |
11 | import getopt as O |
12 | import time as T |
13 | import sre as RX |
14 | from cStringIO import StringIO |
15 | |
16 | import pygtk |
17 | pygtk.require('2.0') |
18 | import gtk as G |
19 | import gobject as GO |
20 | import gtk.gdk as GDK |
21 | |
22 | #----- Configuration -------------------------------------------------------- |
23 | |
24 | tripedir = "@configdir@" |
25 | socketdir = "@socketdir@" |
26 | PACKAGE = "@PACKAGE@" |
27 | VERSION = "@VERSION@" |
28 | |
29 | debug = False |
30 | |
31 | #----- Utility functions ---------------------------------------------------- |
32 | |
33 | ## Program name, shorn of extraneous stuff. |
34 | quis = OS.path.basename(argv[0]) |
35 | |
36 | def moan(msg): |
37 | """Report a message to standard error.""" |
38 | stderr.write('%s: %s\n' % (quis, msg)) |
39 | |
40 | def die(msg, rc = 1): |
41 | """Report a message to standard error and exit.""" |
42 | moan(msg) |
43 | exit(rc) |
44 | |
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+$') |
50 | |
51 | c_red = GDK.color_parse('red') |
52 | |
53 | def 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 | |
90 | def 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 | |
100 | class 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 | |
106 | class peerinfo (struct): pass |
107 | class pingstate (struct): pass |
108 | |
109 | def 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 | |
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.""" |
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 | |
136 | class 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 | |
154 | class ConnException (Exception): |
155 | """Some sort of problem occurred while communicating with the tripe |
156 | server.""" |
157 | pass |
158 | |
159 | class Error (ConnException): |
160 | """A command caused the server to issue a FAIL message.""" |
161 | pass |
162 | |
163 | class ConnectionFailed (ConnException): |
164 | """The connection failed while communicating with the server.""" |
165 | |
166 | jobid_seq = 0 |
167 | def 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 | |
173 | class 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 | |
197 | class 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 | |
205 | class 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 | |
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. |
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 | |
460 | def 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 | |
469 | class 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 | |
500 | class 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() |
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) |
514 | me.mywininit() |
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 |
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 | |
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 |
540 | make a new one.""" |
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 |
546 | me.window = None |
547 | def open(me): |
548 | """Opens the window, creating it if necessary.""" |
549 | if me.window: |
550 | me.window.window.raise_() |
551 | else: |
552 | me.window = me.createfunc() |
553 | me.hook(me.window.closehook, me.closed) |
554 | def closed(me): |
555 | me.unhook(me.window.closehook) |
556 | me.window = None |
557 | |
558 | class ValidationError (Exception): |
559 | """Raised by ValidatingEntry.get_text() if the text isn't valid.""" |
560 | pass |
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 |
564 | raised.""" |
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) |
571 | if callable(valid): |
572 | me.validate = valid |
573 | else: |
574 | me.validate = RX.compile(valid).match |
575 | me.ensure_style() |
576 | me.c_ok = me.get_style().text[G.STATE_NORMAL] |
577 | me.c_bad = c_red |
578 | if size != -1: me.set_width_chars(size) |
579 | me.set_activates_default(True) |
580 | me.set_text(text) |
581 | me.check() |
582 | def check(me, *hunoz): |
583 | if me.validate(G.Entry.get_text(me)): |
584 | me.validp = True |
585 | me.modify_text(G.STATE_NORMAL, me.c_ok) |
586 | else: |
587 | me.validp = False |
588 | me.modify_text(G.STATE_NORMAL, me.c_bad) |
589 | def get_text(me): |
590 | if not me.validp: |
591 | raise ValidationError |
592 | return G.Entry.get_text(me) |
593 | |
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 |
597 | bounds.""" |
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)) |
601 | |
602 | #----- Various minor dialog boxen ------------------------------------------- |
603 | |
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. |
608 | |
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. |
613 | |
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.""" |
617 | |
618 | class AboutBox (G.AboutDialog, MyWindowMixin): |
619 | """The program `About' box.""" |
620 | def __init__(me): |
621 | G.AboutDialog.__init__(me) |
622 | me.mywininit() |
623 | me.set_name('TrIPEmon') |
624 | me.set_version(VERSION) |
625 | me.set_license(GPL) |
626 | me.set_authors(['Mark Wooding']) |
627 | me.connect('unmap', invoker(me.close)) |
628 | me.show() |
629 | aboutbox = WindowSlot(AboutBox) |
630 | |
631 | def moanbox(msg): |
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))) |
636 | label = G.Label(msg) |
637 | label.set_padding(20, 20) |
638 | d.vbox.pack_start(label) |
639 | label.show() |
640 | d.run() |
641 | d.destroy() |
642 | |
643 | #----- Logging windows ------------------------------------------------------ |
644 | |
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) |
656 | |
657 | class TraceLogModel (LogModel): |
658 | """Log model for trace messages.""" |
659 | def __init__(me): |
660 | LogModel.__init__(me, ('Message',)) |
661 | def notify(me, line): |
662 | """Call with a new trace message.""" |
663 | me.add(line) |
664 | |
665 | class WarningLogModel (LogModel): |
666 | """Log model for warnings. We split the category out into a separate |
667 | column.""" |
668 | def __init__(me): |
669 | LogModel.__init__(me, ('Category', 'Message')) |
670 | def notify(me, line): |
671 | """Call with a new warning message.""" |
672 | me.add(*getword(line)) |
673 | |
674 | class LogViewer (MyWindow): |
675 | """Log viewer window. Nothing very exciting.""" |
676 | def __init__(me, model): |
677 | MyWindow.__init__(me) |
678 | me.model = model |
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() |
683 | i = 0 |
684 | for c in me.model.cols: |
685 | me.list.append_column(G.TreeViewColumn(c, |
686 | G.CellRendererText(), |
687 | text = i)) |
688 | i += 1 |
689 | me.set_default_size(440, 256) |
690 | scr.add(me.list) |
691 | me.add(scr) |
692 | me.show_all() |
693 | |
694 | def makeactiongroup(name, acts): |
695 | """Creates an ActionGroup called NAME. ACTS is a list of tuples |
696 | containing: |
697 | ACT: an action name |
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) |
706 | return actgroup |
707 | |
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)]) |
714 | me.mon = monitor |
715 | me.opts = [] |
716 | for o in me.mon.simplecmd('TRACE'): |
717 | char = o[0] |
718 | onp = o[1] |
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)) |
725 | me.show_all() |
726 | def ok(me): |
727 | on = [] |
728 | off = [] |
729 | for char, ticky in me.opts: |
730 | if ticky.get_active(): |
731 | on.append(char) |
732 | else: |
733 | off.append(char) |
734 | setting = ''.join(on) + '-' + ''.join(off) |
735 | me.mon.simplecmd('TRACE %s' % setting) |
736 | me.destroy() |
737 | |
738 | def unimplemented(*hunoz): |
739 | """Indicator of laziness.""" |
740 | moanbox("I've not written that bit yet.") |
741 | |
742 | class GridPacker (G.Table): |
743 | """Like a Table, but with more state: makes filling in the widgets |
744 | easier.""" |
745 | def __init__(me): |
746 | G.Table.__init__(me) |
747 | me.row = 0 |
748 | me.col = 0 |
749 | me.rows = 1 |
750 | me.cols = 1 |
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, |
756 | xpad = 0, ypad = 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.""" |
760 | if newlinep: |
761 | me.row += 1 |
762 | me.col = 0 |
763 | bot = me.row + 1 |
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) |
771 | me.col += width |
772 | return w |
773 | def labelled(me, lab, w, newlinep = False, **kw): |
774 | """Packs a labelled widget. Other arguments are as for pack. Returns |
775 | W.""" |
776 | label = G.Label(lab) |
777 | label.set_alignment(1.0, 0) |
778 | me.pack(label, newlinep = newlinep, xopt = G.FILL) |
779 | me.pack(w, **kw) |
780 | return w |
781 | def info(me, label, text = None, len = 18, **kw): |
782 | e = G.Entry() |
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) |
787 | return e |
788 | |
789 | def xlate_time(t): |
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) |
795 | def xlate_bytes(b): |
796 | """Translate a number of bytes into something a human might want to read.""" |
797 | suff = 'B' |
798 | b = int(b) |
799 | for s in 'KMG': |
800 | if b < 4096: break |
801 | b /= 1024 |
802 | suff = s |
803 | return '%d %s' % (b, suff) |
804 | |
805 | ## How to translate peer stats. Maps the stat name to a translation |
806 | ## function. |
807 | statsxlate = \ |
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)] |
817 | |
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 |
820 | ## the entry. |
821 | statslayout = \ |
822 | [('Start time', '%(start-time)s'), |
823 | ('Last key-exchange', '%(last-keyexch-time)s'), |
824 | ('Last packet', '%(last-packet-time)s'), |
825 | ('Packets in/out', |
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)'), |
829 | ('IP in/out', |
830 | '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-in)s (%(ip-bytes-in)s)'), |
831 | ('Rejected packets', '%(rejected-packets)s')] |
832 | |
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) |
838 | me.mon = monitor |
839 | me.peer = peer |
840 | table = GridPacker() |
841 | me.add(table) |
842 | me.e = {} |
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) |
848 | add('Keepalives', |
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) |
854 | me.timeout = None |
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) |
860 | me.tryupdate() |
861 | me.ping() |
862 | me.show_all() |
863 | def update(me): |
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) |
870 | return True |
871 | def tryupdate(me): |
872 | if me.timeout is None and me.update(): |
873 | me.timeout = GO.timeout_add(1000, me.update) |
874 | def stopupdate(me): |
875 | if me.timeout is not None: |
876 | GO.source_remove(me.timeout) |
877 | me.timeout = None |
878 | def dead(me): |
879 | me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name) |
880 | me.e['Peer name'].set_text('%s [defunct]' % me.peer.name) |
881 | me.stopupdate() |
882 | def ping(me): |
883 | for ping in me.peer.ping, me.peer.eping: |
884 | s = '%d/%d' % (ping.ngood, ping.n) |
885 | if ping.ngood: |
886 | s += '; %.2f ms (last %.1f ms)' % (ping.ttot/ping.ngood, ping.tlast); |
887 | me.e[ping.cmd].set_text(s) |
888 | |
889 | class AddPeerCommand (SimpleBackgroundCommand): |
890 | def __init__(me, conn, dlg, name, addr, port, |
891 | keepalive = None, tunnel = None): |
892 | me.name = name |
893 | me.addr = addr |
894 | me.port = port |
895 | me.keepalive = keepalive |
896 | me.tunnel = tunnel |
897 | cmd = StringIO() |
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)) |
905 | def fail(me, err): |
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) |
913 | else: |
914 | moanbox("Unexpected error from server command `ADD': %s" % err) |
915 | |
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)]) |
921 | me.mon = monitor |
922 | table = GridPacker() |
923 | me.vbox.pack_start(table) |
924 | me.e_name = table.labelled('Name', |
925 | ValidatingEntry(r'^[^\s.:]+$', '', 16), |
926 | width = 3) |
927 | me.e_addr = table.labelled('Address', |
928 | ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24), |
929 | newlinep = True) |
930 | me.e_port = table.labelled('Port', |
931 | ValidatingEntry(numericvalidate(0, 65535), |
932 | '22003', |
933 | 5)) |
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') |
939 | for t in me.tuns: |
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\ |
945 | (t.get_active())) |
946 | me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5) |
947 | me.e_keepalive.set_sensitive(False) |
948 | table.pack(me.e_keepalive, width = 3) |
949 | me.show_all() |
950 | def ok(me): |
951 | try: |
952 | if me.c_keepalive.get_active(): |
953 | ka = me.e_keepalive.get_text() |
954 | else: |
955 | ka = None |
956 | t = me.l_tunnel.get_active() |
957 | if t == 0: |
958 | tun = None |
959 | else: |
960 | tun = me.tuns[t] |
961 | AddPeerCommand(me.mon, me, |
962 | me.e_name.get_text(), |
963 | me.e_addr.get_text(), |
964 | me.e_port.get_text(), |
965 | keepalive = ka, |
966 | tunnel = tun) |
967 | except ValidationError: |
968 | GDK.beep() |
969 | return |
970 | |
971 | class ServInfo (MyWindow): |
972 | def __init__(me, monitor): |
973 | MyWindow.__init__(me) |
974 | me.set_title('TrIPE server info') |
975 | me.mon = monitor |
976 | me.table = GridPacker() |
977 | me.add(me.table) |
978 | me.e = {} |
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) |
983 | me.update() |
984 | me.hook(me.mon.connecthook, me.update) |
985 | me.show_all() |
986 | def update(me): |
987 | info = parseinfo(me.mon.simplecmd('SERVINFO')) |
988 | for i in me.e: |
989 | me.e[i].set_text(info[i]) |
990 | |
991 | class PingCommand (SimpleBackgroundCommand): |
992 | def __init__(me, conn, cmd, peer, func): |
993 | me.peer = peer |
994 | me.func = func |
995 | SimpleBackgroundCommand.__init__ \ |
996 | (me, conn, '%s -background %s %s' % (cmd, jobid(), peer.name)) |
997 | def ok(me): |
998 | tok, rest = getword(me.info[0]) |
999 | if tok == 'ping-ok': |
1000 | me.func(me.peer, float(rest)) |
1001 | else: |
1002 | me.func(me.peer, None) |
1003 | me.unhookall() |
1004 | def fail(me, err): me.unhookall() |
1005 | def lost(me): me.unhookall() |
1006 | |
1007 | class MonitorWindow (MyWindow): |
1008 | |
1009 | def __init__(me, monitor): |
1010 | MyWindow.__init__(me) |
1011 | me.set_title('TrIPE monitor') |
1012 | me.mon = 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) |
1018 | |
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)) |
1024 | |
1025 | vbox = G.VBox() |
1026 | me.add(vbox) |
1027 | |
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)]) |
1049 | uidef = ''' |
1050 | <ui> |
1051 | <menubar> |
1052 | <menu action="file-menu"> |
1053 | <menuitem action="quit"/> |
1054 | </menu> |
1055 | <menu action="server-menu"> |
1056 | <menuitem action="connect"/> |
1057 | <menuitem action="disconnect"/> |
1058 | <separator/> |
1059 | <menuitem action="add-peer"/> |
1060 | <menuitem action="daemon"/> |
1061 | <menuitem action="server-version"/> |
1062 | <separator/> |
1063 | <menuitem action="server-quit"/> |
1064 | </menu> |
1065 | <menu action="logs-menu"> |
1066 | <menuitem action="show-warnings"/> |
1067 | <menuitem action="show-trace"/> |
1068 | <menuitem action="trace-options"/> |
1069 | </menu> |
1070 | <menu action="help-menu"> |
1071 | <menuitem action="about"/> |
1072 | </menu> |
1073 | </menubar> |
1074 | <popup name="peer-popup"> |
1075 | <menuitem action="add-peer"/> |
1076 | <menuitem action="kill-peer"/> |
1077 | <menuitem action="force-kx"/> |
1078 | </popup> |
1079 | </ui> |
1080 | ''' |
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() |
1086 | |
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) |
1091 | |
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(), |
1097 | text = 0)) |
1098 | me.list.append_column(G.TreeViewColumn('Address', |
1099 | G.CellRendererText(), |
1100 | text = 1)) |
1101 | me.list.append_column(G.TreeViewColumn('T-ping', |
1102 | G.CellRendererText(), |
1103 | text = 2, |
1104 | foreground = 3)) |
1105 | me.list.append_column(G.TreeViewColumn('E-ping', |
1106 | G.CellRendererText(), |
1107 | text = 4, |
1108 | foreground = 5)) |
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) |
1114 | scr.add(me.list) |
1115 | vbox.pack_start(scr) |
1116 | |
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) |
1121 | me.pinger = None |
1122 | me.set_default_size(420, 180) |
1123 | me.mon.connect() |
1124 | me.show_all() |
1125 | |
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]] |
1141 | |
1142 | def activate(me, l, path, col): |
1143 | peer = me.path_peer(path) |
1144 | peer.win.open() |
1145 | def buttonpress(me, l, ev): |
1146 | if ev.button == 3: |
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 |
1150 | r is not None) |
1151 | if r: |
1152 | me.menupeer = me.path_peer(r[0]) |
1153 | else: |
1154 | me.menupeer = None |
1155 | me.ui.get_widget('/peer-popup').popup(None, None, None, |
1156 | ev.button, ev.time) |
1157 | |
1158 | def killpeer(me): |
1159 | me.mon.simplecmd('KILL %s' % me.menupeer.name) |
1160 | def forcekx(me): |
1161 | me.mon.simplecmd('FORCEKX %s' % me.menupeer.name) |
1162 | |
1163 | def reping(me): |
1164 | if me.pinger is not None: |
1165 | GO.source_remove(me.pinger) |
1166 | me.pinger = GO.timeout_add(10000, me.ping) |
1167 | me.ping() |
1168 | def unping(me): |
1169 | if me.pinger is not None: |
1170 | GO.source_remove(me.pinger) |
1171 | me.pinger = None |
1172 | def ping(me): |
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)) |
1177 | return True |
1178 | def pong(me, p, ping, t): |
1179 | ping.n += 1 |
1180 | if t is None: |
1181 | ping.nmiss += 1 |
1182 | ping.nmissrun += 1 |
1183 | me.listmodel[p.i][ping.tcol] = '(miss %d)' % ping.nmissrun |
1184 | me.listmodel[p.i][ping.ccol] = 'red' |
1185 | else: |
1186 | ping.ngood += 1 |
1187 | ping.nmissrun = 0 |
1188 | ping.tlast = t |
1189 | ping.ttot += t |
1190 | me.listmodel[p.i][ping.tcol] = '%.1f ms' % t |
1191 | me.listmodel[p.i][ping.ccol] = 'black' |
1192 | p.pinghook.run() |
1193 | def setstatus(me, status): |
1194 | me.status.pop(0) |
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) |
1199 | def connected(me): |
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'] == |
1210 | 'nil') |
1211 | me.reping() |
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) |
1222 | me.unping() |
1223 | def destroy(me): |
1224 | if me.pinger is not None: |
1225 | GO.source_remove(me.pinger) |
1226 | def report(me, msg): |
1227 | moanbox(msg) |
1228 | return True |
1229 | |
1230 | #----- Parse options -------------------------------------------------------- |
1231 | |
1232 | def version(fp = stdout): |
1233 | """Print the program's version number.""" |
1234 | fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION)) |
1235 | |
1236 | def usage(fp): |
1237 | """Print a brief usage message for the program.""" |
1238 | fp.write('Usage: %s [-d DIR] [-a SOCK]\n' % quis) |
1239 | |
1240 | def main(): |
1241 | global tripedir |
1242 | if 'TRIPEDIR' in environ: |
1243 | tripedir = environ['TRIPEDIR'] |
1244 | tripesock = '%s/%s' % (socketdir, 'tripesock') |
1245 | |
1246 | try: |
1247 | opts, args = O.getopt(argv[1:], |
1248 | 'hvud:a:', |
1249 | ['help', 'version', 'usage', |
1250 | 'directory=', 'admin-socket=']) |
1251 | except O.GetoptError, exc: |
1252 | moan(exc) |
1253 | usage(stderr) |
1254 | exit(1) |
1255 | for o, v in opts: |
1256 | if o in ('-h', '--help'): |
1257 | version(stdout) |
1258 | print |
1259 | usage(stdout) |
1260 | print """ |
1261 | Graphical monitor for TrIPE VPN. |
1262 | |
1263 | Options supported: |
1264 | |
1265 | -h, --help Show this help message. |
1266 | -v, --version Show the version number. |
1267 | -u, --usage Show pointlessly short usage string. |
1268 | |
1269 | -d, --directory=DIR Use TrIPE directory DIR. |
1270 | -a, --admin-socket=FILE Select socket to connect to.""" |
1271 | exit(0) |
1272 | elif o in ('-v', '--version'): |
1273 | version(stdout) |
1274 | exit(0) |
1275 | elif o in ('-u', '--usage'): |
1276 | usage(stdout) |
1277 | exit(0) |
1278 | elif o in ('-d', '--directory'): |
1279 | tripedir = v |
1280 | elif o in ('-a', '--admin-socket'): |
1281 | tripesock = v |
1282 | else: |
1283 | raise "can't happen!" |
1284 | if len(args) > 0: |
1285 | usage(stderr) |
1286 | exit(1) |
1287 | |
1288 | OS.chdir(tripedir) |
1289 | mon = Monitor(tripesock) |
1290 | root = MonitorWindow(mon) |
1291 | HookClient().hook(root.closehook, exit) |
1292 | G.main() |
1293 | |
1294 | if __name__ == '__main__': |
1295 | main() |
1296 | |