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 |
37941236 |
10 | import math as M |
060ca767 |
11 | import sets as SET |
12 | import getopt as O |
13 | import time as T |
14 | import sre as RX |
15 | from cStringIO import StringIO |
16 | |
17 | import pygtk |
18 | pygtk.require('2.0') |
19 | import gtk as G |
20 | import gobject as GO |
21 | import gtk.gdk as GDK |
22 | |
23 | #----- Configuration -------------------------------------------------------- |
24 | |
25 | tripedir = "@configdir@" |
26 | socketdir = "@socketdir@" |
27 | PACKAGE = "@PACKAGE@" |
28 | VERSION = "@VERSION@" |
29 | |
30 | debug = False |
31 | |
32 | #----- Utility functions ---------------------------------------------------- |
33 | |
34 | ## Program name, shorn of extraneous stuff. |
35 | quis = OS.path.basename(argv[0]) |
36 | |
37 | def moan(msg): |
38 | """Report a message to standard error.""" |
39 | stderr.write('%s: %s\n' % (quis, msg)) |
40 | |
41 | def die(msg, rc = 1): |
42 | """Report a message to standard error and exit.""" |
43 | moan(msg) |
44 | exit(rc) |
45 | |
46 | rx_space = RX.compile(r'\s+') |
47 | rx_ordinary = RX.compile(r'[^\\\'\"\s]+') |
48 | rx_weird = RX.compile(r'([\\\'])') |
49 | rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$') |
50 | rx_num = RX.compile(r'^[-+]?\d+$') |
51 | |
52 | c_red = GDK.color_parse('red') |
53 | |
54 | def getword(s): |
55 | """Pull a word from the front of S, handling quoting according to the |
56 | tripe-admin(5) rules. Returns the word and the rest of S, or (None, None) |
57 | if there are no more words left.""" |
58 | i = 0 |
59 | m = rx_space.match(s, i) |
60 | if m: i = m.end() |
61 | r = '' |
62 | q = None |
63 | if i >= len(s): |
64 | return None, None |
65 | while i < len(s) and (q or not s[i].isspace()): |
66 | m = rx_ordinary.match(s, i) |
67 | if m: |
68 | r += m.group() |
69 | i = m.end() |
70 | elif s[i] == '\\': |
71 | r += s[i + 1] |
72 | i += 2 |
73 | elif s[i] == q: |
74 | q = None |
75 | i += 1 |
76 | elif not q and s[i] == '`' or s[i] == "'": |
77 | q = "'" |
78 | i += 1 |
79 | elif not q and s[i] == '"': |
80 | q = '"' |
81 | i += 1 |
82 | else: |
83 | r += s[i] |
84 | i += 1 |
85 | if q: |
86 | raise SyntaxError, 'missing close quote' |
87 | m = rx_space.match(s, i) |
88 | if m: i = m.end() |
89 | return r, s[i:] |
90 | |
91 | def quotify(s): |
92 | """Quote S according to the tripe-admin(5) rules.""" |
93 | m = rx_ordinary.match(s) |
94 | if m and m.end() == len(s): |
95 | return s |
96 | else: |
97 | return "'" + rx_weird.sub(r'\\\1', s) + "'" |
98 | |
99 | #----- Random bits of infrastructure ---------------------------------------- |
100 | |
101 | class struct (object): |
102 | """Simple object which stores attributes and has a sensible construction |
103 | syntax.""" |
104 | def __init__(me, **kw): |
105 | me.__dict__.update(kw) |
106 | |
107 | class peerinfo (struct): pass |
108 | class pingstate (struct): pass |
109 | |
110 | def invoker(func): |
111 | """Return a function which throws away its arguments and calls FUNC. (If |
112 | for loops worked by binding rather than assignment then we wouldn't need |
113 | this kludge.""" |
114 | return lambda *hunoz, **hukairz: func() |
115 | |
116 | class HookList (object): |
117 | """I maintain a list of functions, and provide the ability to call them |
118 | when something interesting happens. The functions are called in the order |
119 | they were added to the list, with all the arguments. If a function returns |
120 | a non-None result, no further functions are called.""" |
121 | def __init__(me): |
122 | me.list = [] |
123 | def add(me, func, obj): |
124 | me.list.append((obj, func)) |
125 | def prune(me, obj): |
126 | new = [] |
127 | for o, f in me.list: |
128 | if o is not obj: |
129 | new.append((o, f)) |
130 | me.list = new |
131 | def run(me, *args, **kw): |
132 | for o, hook in me.list: |
133 | rc = hook(*args, **kw) |
134 | if rc is not None: return rc |
135 | return None |
136 | |
137 | class HookClient (object): |
138 | def __init__(me): |
139 | me.hooks = SET.Set() |
140 | def hook(me, hk, func): |
141 | hk.add(func, me) |
142 | me.hooks.add(hk) |
143 | def unhook(me, hk): |
144 | hk.prune(me) |
145 | me.hooks.discard(hk) |
146 | def unhookall(me): |
147 | for hk in me.hooks: |
148 | hk.prune(me) |
149 | me.hooks.clear() |
150 | ##def __del__(me): |
151 | ## print '%s dying' % me |
152 | |
153 | #----- Connections and commands --------------------------------------------- |
154 | |
155 | class ConnException (Exception): |
156 | """Some sort of problem occurred while communicating with the tripe |
157 | server.""" |
158 | pass |
159 | |
160 | class Error (ConnException): |
161 | """A command caused the server to issue a FAIL message.""" |
162 | pass |
163 | |
164 | class ConnectionFailed (ConnException): |
165 | """The connection failed while communicating with the server.""" |
166 | |
167 | jobid_seq = 0 |
168 | def jobid(): |
169 | """Return a job tag. Used for background commands.""" |
170 | global jobid_seq |
171 | jobid_seq += 1 |
172 | return 'bg-%d' % jobid_seq |
173 | |
174 | class BackgroundCommand (HookClient): |
175 | def __init__(me, conn, cmd): |
176 | HookClient.__init__(me) |
177 | me.conn = conn |
178 | me.tag = None |
179 | me.cmd = cmd |
180 | me.donehook = HookList() |
181 | me.losthook = HookList() |
182 | me.info = [] |
183 | me.submit() |
184 | me.hook(me.conn.disconnecthook, me.lost) |
185 | def submit(me): |
186 | me.conn.bgcommand(me.cmd, me) |
187 | def lost(me): |
188 | me.losthook.run() |
189 | me.unhookall() |
190 | def fail(me, msg): |
191 | me.conn.error("Unexpected error from server command `%s': %s" % |
192 | (me.cmd % msg)) |
193 | me.unhookall() |
194 | def ok(me): |
195 | me.donehook.run(me.info) |
196 | me.unhookall() |
197 | |
198 | class SimpleBackgroundCommand (BackgroundCommand): |
199 | def submit(me): |
200 | try: |
201 | BackgroundCommand.submit(me) |
202 | except ConnectionFailed, err: |
203 | me.conn.error('Unexpected error communicating with server: %s' % msg) |
204 | raise |
205 | |
206 | class Connection (HookClient): |
207 | |
208 | """I represent a connection to the TrIPE server. I provide facilities for |
209 | sending commands and receiving replies. The connection is notional: the |
210 | underlying socket connection can come and go under our feet. |
211 | |
212 | Useful attributes: |
213 | connectedp: whether the connection is active |
214 | connecthook: called when we have connected |
215 | disconnecthook: called if we have disconnected |
216 | notehook: called with asynchronous notifications |
217 | errorhook: called if there was a command error""" |
218 | |
219 | def __init__(me, sockname): |
220 | """Make a new connection to the server listening to SOCKNAME. In fact, |
221 | we're initially disconnected, to allow the caller to get his life in |
222 | order before opening the floodgates.""" |
223 | HookClient.__init__(me) |
224 | me.sockname = sockname |
225 | me.sock = None |
226 | me.connectedp = False |
227 | me.connecthook = HookList() |
228 | me.disconnecthook = HookList() |
229 | me.errorhook = HookList() |
230 | me.inbuf = '' |
231 | me.info = [] |
232 | me.waitingp = False |
233 | me.bgcmd = None |
234 | me.bgmap = {} |
235 | def connect(me): |
236 | "Connect to the server. Runs connecthook if it works.""" |
237 | if me.sock: return |
238 | sock = S.socket(S.AF_UNIX, S.SOCK_STREAM) |
239 | try: |
240 | sock.connect(me.sockname) |
241 | except S.error, err: |
242 | me.error('error opening connection: %s' % err[1]) |
243 | me.disconnecthook.run() |
244 | return |
245 | sock.setblocking(0) |
246 | me.socketwatch = GO.io_add_watch(sock, GO.IO_IN, me.ready) |
247 | me.sock = sock |
248 | me.connectedp = True |
249 | me.connecthook.run() |
250 | def disconnect(me): |
251 | "Disconnects from the server. Runs disconnecthook." |
252 | if not me.sock: return |
253 | GO.source_remove(me.socketwatch) |
254 | me.sock.close() |
255 | me.sock = None |
256 | me.connectedp = False |
257 | me.disconnecthook.run() |
258 | def error(me, msg): |
259 | """Reports an error on the connection.""" |
260 | me.errorhook.run(msg) |
261 | |
262 | def bgcommand(me, cmd, bg): |
263 | """Sends a background command and feeds it properly.""" |
264 | try: |
265 | me.bgcmd = bg |
266 | err = me.docommand(cmd) |
267 | if err: |
268 | bg.fail(err) |
269 | finally: |
270 | me.bgcmd = None |
271 | def command(me, cmd): |
272 | """Sends a command to the server. Returns a list of INFO responses. Do |
273 | not use this for backgrounded commands: create a BackgroundCommand |
274 | instead. Raises apprpopriate exceptions on error, but doesn't send |
275 | report them to the errorhook.""" |
276 | err = me.docommand(cmd) |
277 | if err: |
278 | raise Error, err |
279 | return me.info |
280 | def docommand(me, cmd): |
281 | if not me.sock: |
282 | raise ConnException, 'not connected' |
283 | if debug: print ">>> %s" % cmd |
284 | me.sock.sendall(cmd + '\n') |
285 | me.waitingp = True |
286 | me.info = [] |
287 | try: |
288 | me.sock.setblocking(1) |
289 | while True: |
290 | rc, err = me.collect() |
291 | if rc: break |
292 | finally: |
293 | me.waitingp = False |
294 | me.sock.setblocking(0) |
295 | if len(me.inbuf) > 0: |
296 | GO.idle_add(lambda: me.flushbuf() and False) |
297 | return err |
298 | def simplecmd(me, cmd): |
299 | """Like command(), but reports errors via the errorhook as well as |
300 | raising exceptions.""" |
301 | try: |
302 | i = me.command(cmd) |
303 | except Error, msg: |
304 | me.error("Unexpected error from server command `%s': %s" % (cmd, msg)) |
305 | raise |
306 | except ConnectionFailed, msg: |
307 | me.error("Unexpected error communicating with server: %s" % msg); |
308 | raise |
309 | return i |
310 | def ready(me, sock, condition): |
311 | try: |
312 | me.collect() |
313 | except ConnException, msg: |
314 | me.error("Error watching server connection: %s" % msg) |
315 | if me.sock: |
316 | me.disconnect() |
317 | me.connect() |
318 | return True |
319 | def collect(me): |
320 | data = me.sock.recv(16384) |
321 | if data == '': |
322 | me.disconnect() |
323 | raise ConnectionFailed, 'server disconnected' |
324 | me.inbuf += data |
325 | return me.flushbuf() |
326 | def flushbuf(me): |
327 | while True: |
328 | nl = me.inbuf.find('\n') |
329 | if nl < 0: break |
330 | line = me.inbuf[:nl] |
331 | if debug: print "<<< %s" % line |
332 | me.inbuf = me.inbuf[nl + 1:] |
333 | tag, line = getword(line) |
334 | rc, err = me.parseline(tag, line) |
335 | if rc: return rc, err |
336 | return False, None |
337 | def parseline(me, code, line): |
338 | if code == 'BGDETACH': |
339 | if not me.bgcmd: |
340 | raise ConnectionFailed, 'unexpected detach' |
341 | me.bgcmd.tag = line |
342 | me.bgmap[line] = me.bgcmd |
343 | me.waitingp = False |
344 | me.bgcmd = None |
345 | return True, None |
346 | elif code == 'BGINFO': |
347 | tag, line = getword(line) |
348 | me.bgmap[tag].info.append(line) |
349 | return False, None |
350 | elif code == 'BGFAIL': |
351 | tag, line = getword(line) |
352 | me.bgmap[tag].fail(line) |
353 | del me.bgmap[tag] |
354 | return False, None |
355 | elif code == 'BGOK': |
356 | tag, line = getword(line) |
357 | me.bgmap[tag].ok() |
358 | del me.bgmap[tag] |
359 | return False, None |
360 | elif code == 'INFO': |
361 | if not me.waitingp or me.bgcmd: |
362 | raise ConnectionFailed, 'unexpected INFO response' |
363 | me.info.append(line) |
364 | return False, None |
365 | elif code == 'OK': |
366 | if not me.waitingp or me.bgcmd: |
367 | raise ConnectionFailed, 'unexpected OK response' |
368 | return True, None |
369 | elif code == 'FAIL': |
370 | if not me.waitingp: |
371 | raise ConnectionFailed, 'unexpected FAIL response' |
372 | return True, line |
373 | else: |
374 | raise ConnectionFailed, 'unknown response code `%s' % code |
375 | |
376 | class Monitor (Connection): |
377 | """I monitor a TrIPE server, noticing when it changes state and keeping |
378 | track of its peers. I also provide facilities for sending the server |
379 | commands and collecting the answers. |
380 | |
381 | Useful attributes: |
382 | addpeerhook: called with a new Peer when the server adds one |
383 | delpeerhook: called with a Peer when the server kills one |
384 | tracehook: called with a trace message |
385 | warnhook: called with a warning message |
386 | peers: mapping from names to Peer objects""" |
387 | def __init__(me, sockname): |
388 | """Initializes the monitor.""" |
389 | Connection.__init__(me, sockname) |
390 | me.addpeerhook = HookList() |
391 | me.delpeerhook = HookList() |
392 | me.tracehook = HookList() |
393 | me.warnhook = HookList() |
394 | me.notehook = HookList() |
395 | me.hook(me.connecthook, me.connected) |
396 | me.delay = [] |
397 | me.peers = {} |
398 | def addpeer(me, peer): |
399 | if peer not in me.peers: |
400 | p = Peer(me, peer) |
401 | me.peers[peer] = p |
402 | me.addpeerhook.run(p) |
403 | def delpeer(me, peer): |
404 | if peer in me.peers: |
405 | p = me.peers[peer] |
406 | me.delpeerhook.run(p) |
407 | p.dead() |
408 | del me.peers[peer] |
409 | def updatelist(me, peers): |
410 | newmap = {} |
411 | for p in peers: |
412 | newmap[p] = True |
413 | if p not in me.peers: |
414 | me.addpeer(p) |
415 | oldpeers = me.peers.copy() |
416 | for p in oldpeers: |
417 | if p not in newmap: |
418 | me.delpeer(p) |
419 | def connected(me): |
420 | try: |
421 | me.simplecmd('WATCH -A+wnt') |
422 | me.updatelist([s.strip() for s in me.simplecmd('LIST')]) |
423 | except ConnException: |
424 | me.disconnect() |
425 | return |
426 | def parseline(me, code, line): |
427 | ## Delay async messages until the current command is done. Otherwise the |
428 | ## handler for the async message might send another command before this |
429 | ## one's complete, and the whole edifice turns to jelly. |
430 | ## |
431 | ## No, this isn't the server's fault. If we rely on the server to delay |
432 | ## notifications then there's a race between when we send a command and |
433 | ## when the server gets it. |
434 | if me.waitingp and code in ('TRACE', 'WARN', 'NOTE'): |
435 | if len(me.delay) == 0: GO.idle_add(me.flushdelay) |
436 | me.delay.append((code, line)) |
437 | elif code == 'TRACE': |
438 | me.tracehook.run(line) |
439 | elif code == 'WARN': |
440 | me.warnhook.run(line) |
441 | elif code == 'NOTE': |
442 | note, line = getword(line) |
443 | me.notehook.run(note, line) |
444 | if note == 'ADD': |
445 | me.addpeer(getword(line)[0]) |
446 | elif note == 'KILL': |
447 | me.delpeer(line) |
448 | else: |
449 | ## Well, I asked for it. |
450 | pass |
451 | else: |
452 | return Connection.parseline(me, code, line) |
453 | return False, None |
454 | def flushdelay(me): |
455 | delay = me.delay |
456 | me.delay = [] |
457 | for tag, line in delay: |
458 | me.parseline(tag, line) |
459 | return False |
460 | |
461 | def parseinfo(info): |
462 | """Parse key=value output into a dictionary.""" |
463 | d = {} |
464 | for i in info: |
465 | for w in i.split(' '): |
466 | q = w.index('=') |
467 | d[w[:q]] = w[q + 1:] |
468 | return d |
469 | |
470 | class Peer (object): |
471 | """I represent a TrIPE peer. Useful attributes are: |
472 | |
473 | name: peer's name |
474 | addr: human-friendly representation of the peer's address |
475 | ifname: interface associated with the peer |
476 | alivep: true if the peer hasn't been killed |
477 | deadhook: called with no arguments when the peer is killed""" |
478 | def __init__(me, monitor, name): |
479 | me.mon = monitor |
480 | me.name = name |
481 | addr = me.mon.simplecmd('ADDR %s' % name)[0].split(' ') |
482 | if addr[0] == 'INET': |
483 | ipaddr, port = addr[1:] |
484 | try: |
485 | name = S.gethostbyaddr(ipaddr)[0] |
486 | me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr) |
487 | except S.herror: |
488 | me.addr = 'INET %s:%s' % (ipaddr, port) |
489 | else: |
490 | me.addr = ' '.join(addr) |
491 | me.ifname = me.mon.simplecmd('IFNAME %s' % me.name)[0] |
492 | me.__dict__.update(parseinfo(me.mon.simplecmd('PEERINFO %s' % me.name))) |
493 | me.deadhook = HookList() |
494 | me.alivep = True |
495 | def dead(me): |
496 | me.alivep = False |
497 | me.deadhook.run() |
498 | |
499 | #----- Window management cruft ---------------------------------------------- |
500 | |
501 | class MyWindowMixin (G.Window, HookClient): |
502 | """Mixin for windows which call a closehook when they're destroyed.""" |
503 | def mywininit(me): |
504 | me.closehook = HookList() |
505 | HookClient.__init__(me) |
506 | me.connect('destroy', invoker(me.close)) |
507 | def close(me): |
508 | me.closehook.run() |
509 | me.destroy() |
510 | me.unhookall() |
511 | class MyWindow (MyWindowMixin): |
512 | """A window which calls a closehook when it's destroyed.""" |
513 | def __init__(me, kind = G.WINDOW_TOPLEVEL): |
514 | G.Window.__init__(me, kind) |
515 | me.mywininit() |
516 | class MyDialog (G.Dialog, MyWindowMixin, HookClient): |
517 | """A dialogue box with a closehook and sensible button binding.""" |
518 | def __init__(me, title = None, flags = 0, buttons = []): |
519 | """The buttons are a list of (STOCKID, THUNK) pairs: call the appropriate |
520 | THUNK when the button is pressed. The others are just like GTK's Dialog |
521 | class.""" |
522 | i = 0 |
523 | br = [] |
524 | me.rmap = [] |
525 | for b, f in buttons: |
526 | br.append(b) |
527 | br.append(i) |
528 | me.rmap.append(f) |
529 | i += 1 |
530 | G.Dialog.__init__(me, title, None, flags, tuple(br)) |
531 | HookClient.__init__(me) |
532 | me.mywininit() |
533 | me.set_default_response(i - 1) |
534 | me.connect('response', me.respond) |
535 | def respond(me, hunoz, rid, *hukairz): |
536 | if rid >= 0: me.rmap[rid]() |
537 | |
ca6eb20c |
538 | def makeactiongroup(name, acts): |
539 | """Creates an ActionGroup called NAME. ACTS is a list of tuples |
540 | containing: |
541 | ACT: an action name |
542 | LABEL: the label string for the action |
543 | ACCEL: accelerator string, or None |
544 | FUNC: thunk to call when the action is invoked""" |
545 | actgroup = G.ActionGroup(name) |
546 | for act, label, accel, func in acts: |
547 | a = G.Action(act, label, None, None) |
548 | if func: a.connect('activate', invoker(func)) |
549 | actgroup.add_action_with_accel(a, accel) |
550 | return actgroup |
551 | |
552 | class GridPacker (G.Table): |
553 | """Like a Table, but with more state: makes filling in the widgets |
554 | easier.""" |
555 | def __init__(me): |
556 | G.Table.__init__(me) |
557 | me.row = 0 |
558 | me.col = 0 |
559 | me.rows = 1 |
560 | me.cols = 1 |
561 | me.set_border_width(4) |
562 | me.set_col_spacings(4) |
563 | me.set_row_spacings(4) |
564 | def pack(me, w, width = 1, newlinep = False, |
565 | xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0, |
566 | xpad = 0, ypad = 0): |
567 | """Packs a new widget. W is the widget to add. XOPY, YOPT, XPAD and |
568 | YPAD are as for Table. WIDTH is how many cells to take up horizontally. |
569 | NEWLINEP is whether to start a new line for this widget. Returns W.""" |
570 | if newlinep: |
571 | me.row += 1 |
572 | me.col = 0 |
573 | bot = me.row + 1 |
574 | right = me.col + width |
575 | if bot > me.rows or right > me.cols: |
576 | if bot > me.rows: me.rows = bot |
577 | if right > me.cols: me.cols = right |
578 | me.resize(me.rows, me.cols) |
579 | me.attach(w, me.col, me.col + width, me.row, me.row + 1, |
580 | xopt, yopt, xpad, ypad) |
581 | me.col += width |
582 | return w |
583 | def labelled(me, lab, w, newlinep = False, **kw): |
584 | """Packs a labelled widget. Other arguments are as for pack. Returns |
585 | W.""" |
586 | label = G.Label(lab) |
587 | label.set_alignment(1.0, 0) |
588 | me.pack(label, newlinep = newlinep, xopt = G.FILL) |
589 | me.pack(w, **kw) |
590 | return w |
591 | def info(me, label, text = None, len = 18, **kw): |
592 | """Packs an information widget with a label. LABEL is the label; TEXT is |
593 | the initial text; LEN is the estimated length in characters. Returns the |
594 | entry widget.""" |
595 | e = G.Entry() |
596 | if text is not None: e.set_text(text) |
597 | e.set_width_chars(len) |
598 | e.set_editable(False) |
599 | me.labelled(label, e, **kw) |
600 | return e |
601 | |
060ca767 |
602 | class WindowSlot (HookClient): |
603 | """A place to store a window. If the window is destroyed, remember this; |
604 | when we come to open the window, raise it if it already exists; otherwise |
605 | make a new one.""" |
606 | def __init__(me, createfunc): |
607 | """Constructor: CREATEFUNC must return a new Window which supports the |
608 | closehook protocol.""" |
609 | HookClient.__init__(me) |
610 | me.createfunc = createfunc |
611 | me.window = None |
612 | def open(me): |
613 | """Opens the window, creating it if necessary.""" |
614 | if me.window: |
615 | me.window.window.raise_() |
616 | else: |
617 | me.window = me.createfunc() |
618 | me.hook(me.window.closehook, me.closed) |
619 | def closed(me): |
620 | me.unhook(me.window.closehook) |
621 | me.window = None |
622 | |
623 | class ValidationError (Exception): |
624 | """Raised by ValidatingEntry.get_text() if the text isn't valid.""" |
625 | pass |
626 | class ValidatingEntry (G.Entry): |
627 | """Like an Entry, but makes the text go red if the contents are invalid. |
628 | If get_text is called, and the text is invalid, ValidationError is |
629 | raised.""" |
630 | def __init__(me, valid, text = '', size = -1, *arg, **kw): |
631 | """Make an Entry. VALID is a regular expression or a predicate on |
632 | strings. TEXT is the default text to insert. SIZE is the size of the |
633 | box to set, in characters (ish). Other arguments are passed to Entry.""" |
634 | G.Entry.__init__(me, *arg, **kw) |
635 | me.connect("changed", me.check) |
636 | if callable(valid): |
637 | me.validate = valid |
638 | else: |
639 | me.validate = RX.compile(valid).match |
640 | me.ensure_style() |
641 | me.c_ok = me.get_style().text[G.STATE_NORMAL] |
642 | me.c_bad = c_red |
643 | if size != -1: me.set_width_chars(size) |
644 | me.set_activates_default(True) |
645 | me.set_text(text) |
646 | me.check() |
647 | def check(me, *hunoz): |
648 | if me.validate(G.Entry.get_text(me)): |
649 | me.validp = True |
650 | me.modify_text(G.STATE_NORMAL, me.c_ok) |
651 | else: |
652 | me.validp = False |
653 | me.modify_text(G.STATE_NORMAL, me.c_bad) |
654 | def get_text(me): |
655 | if not me.validp: |
656 | raise ValidationError |
657 | return G.Entry.get_text(me) |
658 | |
659 | def numericvalidate(min = None, max = None): |
660 | """Validation function for numbers. Entry must consist of an optional sign |
661 | followed by digits, and the resulting integer must be within the given |
662 | bounds.""" |
663 | return lambda x: (rx_num.match(x) and |
664 | (min is None or long(x) >= min) and |
665 | (max is None or long(x) <= max)) |
666 | |
667 | #----- Various minor dialog boxen ------------------------------------------- |
668 | |
669 | GPL = """This program is free software; you can redistribute it and/or modify |
670 | it under the terms of the GNU General Public License as published by |
671 | the Free Software Foundation; either version 2 of the License, or |
672 | (at your option) any later version. |
673 | |
674 | This program is distributed in the hope that it will be useful, |
675 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
676 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
677 | GNU General Public License for more details. |
678 | |
679 | You should have received a copy of the GNU General Public License |
680 | along with this program; if not, write to the Free Software Foundation, |
681 | Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.""" |
682 | |
683 | class AboutBox (G.AboutDialog, MyWindowMixin): |
684 | """The program `About' box.""" |
685 | def __init__(me): |
686 | G.AboutDialog.__init__(me) |
687 | me.mywininit() |
688 | me.set_name('TrIPEmon') |
689 | me.set_version(VERSION) |
690 | me.set_license(GPL) |
691 | me.set_authors(['Mark Wooding']) |
692 | me.connect('unmap', invoker(me.close)) |
693 | me.show() |
694 | aboutbox = WindowSlot(AboutBox) |
695 | |
696 | def moanbox(msg): |
697 | """Report an error message in a window.""" |
698 | d = G.Dialog('Error from %s' % quis, |
699 | flags = G.DIALOG_MODAL, |
700 | buttons = ((G.STOCK_OK, G.RESPONSE_NONE))) |
701 | label = G.Label(msg) |
702 | label.set_padding(20, 20) |
703 | d.vbox.pack_start(label) |
704 | label.show() |
705 | d.run() |
706 | d.destroy() |
707 | |
ca6eb20c |
708 | def unimplemented(*hunoz): |
709 | """Indicator of laziness.""" |
710 | moanbox("I've not written that bit yet.") |
711 | |
712 | class ServInfo (MyWindow): |
713 | def __init__(me, monitor): |
714 | MyWindow.__init__(me) |
715 | me.set_title('TrIPE server info') |
716 | me.mon = monitor |
717 | me.table = GridPacker() |
718 | me.add(me.table) |
719 | me.e = {} |
720 | def add(label, tag, text = None, **kw): |
721 | me.e[tag] = me.table.info(label, text, **kw) |
722 | add('Implementation', 'implementation') |
723 | add('Version', 'version', newlinep = True) |
724 | me.update() |
725 | me.hook(me.mon.connecthook, me.update) |
726 | me.show_all() |
727 | def update(me): |
728 | info = parseinfo(me.mon.simplecmd('SERVINFO')) |
729 | for i in me.e: |
730 | me.e[i].set_text(info[i]) |
731 | |
732 | class TraceOptions (MyDialog): |
733 | """Tracing options window.""" |
734 | def __init__(me, monitor): |
735 | MyDialog.__init__(me, title = 'Tracing options', |
736 | buttons = [(G.STOCK_CLOSE, me.destroy), |
737 | (G.STOCK_OK, me.ok)]) |
738 | me.mon = monitor |
739 | me.opts = [] |
740 | for o in me.mon.simplecmd('TRACE'): |
741 | char = o[0] |
742 | onp = o[1] |
743 | text = o[3].upper() + o[4:] |
744 | if char.isupper(): continue |
745 | ticky = G.CheckButton(text) |
746 | ticky.set_active(onp != ' ') |
747 | me.vbox.pack_start(ticky) |
748 | me.opts.append((char, ticky)) |
749 | me.show_all() |
750 | def ok(me): |
751 | on = [] |
752 | off = [] |
753 | for char, ticky in me.opts: |
754 | if ticky.get_active(): |
755 | on.append(char) |
756 | else: |
757 | off.append(char) |
758 | setting = ''.join(on) + '-' + ''.join(off) |
759 | me.mon.simplecmd('TRACE %s' % setting) |
760 | me.destroy() |
761 | |
060ca767 |
762 | #----- Logging windows ------------------------------------------------------ |
763 | |
764 | class LogModel (G.ListStore): |
765 | """A simple list of log messages.""" |
766 | def __init__(me, columns): |
767 | """Call with a list of column names. All must be strings. We add a time |
768 | column to the left.""" |
769 | me.cols = ('Time',) + columns |
770 | G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols))) |
771 | def add(me, *entries): |
772 | """Adds a new log message, with a timestamp.""" |
773 | now = T.strftime('%Y-%m-%d %H:%M:%S') |
774 | me.append((now,) + entries) |
775 | |
776 | class TraceLogModel (LogModel): |
777 | """Log model for trace messages.""" |
778 | def __init__(me): |
779 | LogModel.__init__(me, ('Message',)) |
780 | def notify(me, line): |
781 | """Call with a new trace message.""" |
782 | me.add(line) |
783 | |
784 | class WarningLogModel (LogModel): |
785 | """Log model for warnings. We split the category out into a separate |
786 | column.""" |
787 | def __init__(me): |
788 | LogModel.__init__(me, ('Category', 'Message')) |
789 | def notify(me, line): |
790 | """Call with a new warning message.""" |
791 | me.add(*getword(line)) |
792 | |
793 | class LogViewer (MyWindow): |
794 | """Log viewer window. Nothing very exciting.""" |
795 | def __init__(me, model): |
796 | MyWindow.__init__(me) |
797 | me.model = model |
798 | scr = G.ScrolledWindow() |
799 | scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC) |
800 | me.list = G.TreeView(me.model) |
801 | me.closehook = HookList() |
802 | i = 0 |
803 | for c in me.model.cols: |
804 | me.list.append_column(G.TreeViewColumn(c, |
805 | G.CellRendererText(), |
806 | text = i)) |
807 | i += 1 |
808 | me.set_default_size(440, 256) |
809 | scr.add(me.list) |
810 | me.add(scr) |
811 | me.show_all() |
812 | |
ca6eb20c |
813 | #----- Peer window ---------------------------------------------------------- |
060ca767 |
814 | |
815 | def xlate_time(t): |
816 | """Translate a time in tripe's stats format to something a human might |
817 | actually want to read.""" |
818 | if t == 'NEVER': return '(never)' |
37941236 |
819 | YY, MM, DD, hh, mm, ss = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6)) |
820 | ago = T.time() - T.mktime((YY, MM, DD, hh, mm, ss, 0, 0, -1)) |
821 | ago = M.floor(ago); unit = 's' |
822 | for n, u in [(60, 'min'), (60, 'hrs'), (24, 'days')]: |
823 | if ago < 2*n: break |
824 | ago /= n |
825 | unit = u |
826 | return '%04d:%02d:%02d %02d:%02d:%02d (%.1f %s ago)' % \ |
827 | (YY, MM, DD, hh, mm, ss, ago, unit) |
060ca767 |
828 | def xlate_bytes(b): |
829 | """Translate a number of bytes into something a human might want to read.""" |
830 | suff = 'B' |
831 | b = int(b) |
832 | for s in 'KMG': |
833 | if b < 4096: break |
834 | b /= 1024 |
835 | suff = s |
836 | return '%d %s' % (b, suff) |
837 | |
838 | ## How to translate peer stats. Maps the stat name to a translation |
839 | ## function. |
840 | statsxlate = \ |
841 | [('start-time', xlate_time), |
842 | ('last-packet-time', xlate_time), |
843 | ('last-keyexch-time', xlate_time), |
844 | ('bytes-in', xlate_bytes), |
845 | ('bytes-out', xlate_bytes), |
846 | ('keyexch-bytes-in', xlate_bytes), |
847 | ('keyexch-bytes-out', xlate_bytes), |
848 | ('ip-bytes-in', xlate_bytes), |
849 | ('ip-bytes-out', xlate_bytes)] |
850 | |
851 | ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is |
852 | ## the label to give the entry box; FORMAT is the format string to write into |
853 | ## the entry. |
854 | statslayout = \ |
855 | [('Start time', '%(start-time)s'), |
856 | ('Last key-exchange', '%(last-keyexch-time)s'), |
857 | ('Last packet', '%(last-packet-time)s'), |
858 | ('Packets in/out', |
859 | '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'), |
860 | ('Key-exchange in/out', |
861 | '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'), |
862 | ('IP in/out', |
863 | '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-in)s (%(ip-bytes-in)s)'), |
864 | ('Rejected packets', '%(rejected-packets)s')] |
865 | |
866 | class PeerWindow (MyWindow): |
867 | """Show information about a peer.""" |
868 | def __init__(me, monitor, peer): |
869 | MyWindow.__init__(me) |
870 | me.set_title('TrIPE statistics: %s' % peer.name) |
871 | me.mon = monitor |
872 | me.peer = peer |
873 | table = GridPacker() |
874 | me.add(table) |
875 | me.e = {} |
876 | def add(label, text = None): |
877 | me.e[label] = table.info(label, text, len = 42, newlinep = True) |
878 | add('Peer name', peer.name) |
879 | add('Tunnel', peer.tunnel) |
880 | add('Interface', peer.ifname) |
881 | add('Keepalives', |
882 | (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive) |
883 | add('Address', peer.addr) |
884 | add('Transport pings') |
885 | add('Encrypted pings') |
886 | for label, format in statslayout: add(label) |
887 | me.timeout = None |
888 | me.hook(me.mon.connecthook, me.tryupdate) |
889 | me.hook(me.mon.disconnecthook, me.stopupdate) |
890 | me.hook(me.closehook, me.stopupdate) |
891 | me.hook(me.peer.deadhook, me.dead) |
892 | me.hook(me.peer.pinghook, me.ping) |
893 | me.tryupdate() |
894 | me.ping() |
895 | me.show_all() |
896 | def update(me): |
897 | if not me.peer.alivep or not me.mon.connectedp: return False |
898 | stat = parseinfo(me.mon.simplecmd('STATS %s' % me.peer.name)) |
899 | for s, trans in statsxlate: |
900 | stat[s] = trans(stat[s]) |
901 | for label, format in statslayout: |
902 | me.e[label].set_text(format % stat) |
903 | return True |
904 | def tryupdate(me): |
905 | if me.timeout is None and me.update(): |
906 | me.timeout = GO.timeout_add(1000, me.update) |
907 | def stopupdate(me): |
908 | if me.timeout is not None: |
909 | GO.source_remove(me.timeout) |
910 | me.timeout = None |
911 | def dead(me): |
912 | me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name) |
913 | me.e['Peer name'].set_text('%s [defunct]' % me.peer.name) |
914 | me.stopupdate() |
915 | def ping(me): |
916 | for ping in me.peer.ping, me.peer.eping: |
917 | s = '%d/%d' % (ping.ngood, ping.n) |
f87966db |
918 | if ping.n: |
919 | s += ' (%.1f%%)' % (ping.ngood * 100.0/ping.n) |
060ca767 |
920 | if ping.ngood: |
921 | s += '; %.2f ms (last %.1f ms)' % (ping.ttot/ping.ngood, ping.tlast); |
922 | me.e[ping.cmd].set_text(s) |
923 | |
ca6eb20c |
924 | #----- Add peer ------------------------------------------------------------- |
925 | |
060ca767 |
926 | class AddPeerCommand (SimpleBackgroundCommand): |
927 | def __init__(me, conn, dlg, name, addr, port, |
928 | keepalive = None, tunnel = None): |
929 | me.name = name |
930 | me.addr = addr |
931 | me.port = port |
932 | me.keepalive = keepalive |
933 | me.tunnel = tunnel |
934 | cmd = StringIO() |
935 | cmd.write('ADD %s' % name) |
936 | cmd.write(' -background %s' % jobid()) |
937 | if keepalive is not None: cmd.write(' -keepalive %s' % keepalive) |
938 | if tunnel is not None: cmd.write(' -tunnel %s' % tunnel) |
939 | cmd.write(' INET %s %s' % (addr, port)) |
940 | SimpleBackgroundCommand.__init__(me, conn, cmd.getvalue()) |
941 | me.hook(me.donehook, invoker(dlg.destroy)) |
942 | def fail(me, err): |
943 | token, msg = getword(str(err)) |
944 | if token in ('resolve-error', 'resolver-timeout'): |
945 | moanbox("Unable to resolve hostname `%s'" % me.addr) |
946 | elif token == 'peer-create-fail': |
947 | moanbox("Couldn't create new peer `%s'" % me.name) |
948 | elif token == 'peer-exists': |
949 | moanbox("Peer `%s' already exists" % me.name) |
950 | else: |
951 | moanbox("Unexpected error from server command `ADD': %s" % err) |
952 | |
953 | class AddPeerDialog (MyDialog): |
954 | def __init__(me, monitor): |
955 | MyDialog.__init__(me, 'Add peer', |
956 | buttons = [(G.STOCK_CANCEL, me.destroy), |
957 | (G.STOCK_OK, me.ok)]) |
958 | me.mon = monitor |
959 | table = GridPacker() |
960 | me.vbox.pack_start(table) |
961 | me.e_name = table.labelled('Name', |
962 | ValidatingEntry(r'^[^\s.:]+$', '', 16), |
963 | width = 3) |
964 | me.e_addr = table.labelled('Address', |
965 | ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24), |
966 | newlinep = True) |
967 | me.e_port = table.labelled('Port', |
968 | ValidatingEntry(numericvalidate(0, 65535), |
969 | '22003', |
970 | 5)) |
971 | me.c_keepalive = G.CheckButton('Keepalives') |
972 | me.l_tunnel = table.labelled('Tunnel', |
973 | G.combo_box_new_text(), |
974 | newlinep = True, width = 3) |
975 | me.tuns = me.mon.simplecmd('TUNNELS') |
976 | for t in me.tuns: |
977 | me.l_tunnel.append_text(t) |
978 | me.l_tunnel.set_active(0) |
979 | table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL) |
980 | me.c_keepalive.connect('toggled', |
981 | lambda t: me.e_keepalive.set_sensitive\ |
982 | (t.get_active())) |
983 | me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5) |
984 | me.e_keepalive.set_sensitive(False) |
985 | table.pack(me.e_keepalive, width = 3) |
986 | me.show_all() |
987 | def ok(me): |
988 | try: |
989 | if me.c_keepalive.get_active(): |
990 | ka = me.e_keepalive.get_text() |
991 | else: |
992 | ka = None |
993 | t = me.l_tunnel.get_active() |
994 | if t == 0: |
995 | tun = None |
996 | else: |
997 | tun = me.tuns[t] |
998 | AddPeerCommand(me.mon, me, |
999 | me.e_name.get_text(), |
1000 | me.e_addr.get_text(), |
1001 | me.e_port.get_text(), |
1002 | keepalive = ka, |
1003 | tunnel = tun) |
1004 | except ValidationError: |
1005 | GDK.beep() |
1006 | return |
1007 | |
ca6eb20c |
1008 | #----- The server monitor --------------------------------------------------- |
060ca767 |
1009 | |
1010 | class PingCommand (SimpleBackgroundCommand): |
1011 | def __init__(me, conn, cmd, peer, func): |
1012 | me.peer = peer |
1013 | me.func = func |
1014 | SimpleBackgroundCommand.__init__ \ |
1015 | (me, conn, '%s -background %s %s' % (cmd, jobid(), peer.name)) |
1016 | def ok(me): |
1017 | tok, rest = getword(me.info[0]) |
1018 | if tok == 'ping-ok': |
1019 | me.func(me.peer, float(rest)) |
1020 | else: |
1021 | me.func(me.peer, None) |
1022 | me.unhookall() |
1023 | def fail(me, err): me.unhookall() |
1024 | def lost(me): me.unhookall() |
1025 | |
1026 | class MonitorWindow (MyWindow): |
1027 | |
1028 | def __init__(me, monitor): |
1029 | MyWindow.__init__(me) |
1030 | me.set_title('TrIPE monitor') |
1031 | me.mon = monitor |
1032 | me.hook(me.mon.errorhook, me.report) |
1033 | me.warnings = WarningLogModel() |
1034 | me.hook(me.mon.warnhook, me.warnings.notify) |
1035 | me.trace = TraceLogModel() |
1036 | me.hook(me.mon.tracehook, me.trace.notify) |
1037 | |
1038 | me.warnview = WindowSlot(lambda: LogViewer(me.warnings)) |
1039 | me.traceview = WindowSlot(lambda: LogViewer(me.trace)) |
1040 | me.traceopts = WindowSlot(lambda: TraceOptions(me.mon)) |
1041 | me.addpeerwin = WindowSlot(lambda: AddPeerDialog(me.mon)) |
1042 | me.servinfo = WindowSlot(lambda: ServInfo(me.mon)) |
1043 | |
1044 | vbox = G.VBox() |
1045 | me.add(vbox) |
1046 | |
1047 | me.ui = G.UIManager() |
804c7d44 |
1048 | def cmd(c): return lambda: me.mon.simplecmd(c) |
060ca767 |
1049 | actgroup = makeactiongroup('monitor', |
1050 | [('file-menu', '_File', None, None), |
1051 | ('connect', '_Connect', '<Alt>C', me.mon.connect), |
1052 | ('disconnect', '_Disconnect', '<Alt>D', me.mon.disconnect), |
1053 | ('quit', '_Quit', '<Alt>Q', me.close), |
1054 | ('server-menu', '_Server', None, None), |
ca6eb20c |
1055 | ('daemon', 'Run in _background', None, cmd('DAEMON')), |
1056 | ('server-version', 'Server version', '<Alt>V', me.servinfo.open), |
1057 | ('reload-keys', 'Reload keys', '<Alt>R', cmd('RELOAD')), |
1058 | ('server-quit', 'Terminate server', None, cmd('QUIT')), |
060ca767 |
1059 | ('logs-menu', '_Logs', None, None), |
1060 | ('show-warnings', 'Show _warnings', '<Alt>W', me.warnview.open), |
1061 | ('show-trace', 'Show _trace', '<Alt>T', me.traceview.open), |
1062 | ('trace-options', 'Trace _options...', None, me.traceopts.open), |
1063 | ('help-menu', '_Help', None, None), |
1064 | ('about', '_About tripemon...', None, aboutbox.open), |
1065 | ('add-peer', '_Add peer...', '<Alt>A', me.addpeerwin.open), |
1066 | ('kill-peer', '_Kill peer', None, me.killpeer), |
1067 | ('force-kx', 'Force key e_xchange', None, me.forcekx)]) |
1068 | uidef = ''' |
1069 | <ui> |
1070 | <menubar> |
1071 | <menu action="file-menu"> |
1072 | <menuitem action="quit"/> |
1073 | </menu> |
1074 | <menu action="server-menu"> |
ca6eb20c |
1075 | <menuitem action="connect"/> |
060ca767 |
1076 | <menuitem action="disconnect"/> |
1077 | <separator/> |
ca6eb20c |
1078 | <menuitem action="server-version"/> |
060ca767 |
1079 | <menuitem action="add-peer"/> |
1080 | <menuitem action="daemon"/> |
ca6eb20c |
1081 | <menuitem action="reload-keys"/> |
060ca767 |
1082 | <separator/> |
1083 | <menuitem action="server-quit"/> |
1084 | </menu> |
1085 | <menu action="logs-menu"> |
1086 | <menuitem action="show-warnings"/> |
1087 | <menuitem action="show-trace"/> |
1088 | <menuitem action="trace-options"/> |
1089 | </menu> |
1090 | <menu action="help-menu"> |
1091 | <menuitem action="about"/> |
1092 | </menu> |
1093 | </menubar> |
1094 | <popup name="peer-popup"> |
1095 | <menuitem action="add-peer"/> |
1096 | <menuitem action="kill-peer"/> |
1097 | <menuitem action="force-kx"/> |
1098 | </popup> |
1099 | </ui> |
1100 | ''' |
1101 | me.ui.insert_action_group(actgroup, 0) |
1102 | me.ui.add_ui_from_string(uidef) |
1103 | vbox.pack_start(me.ui.get_widget('/menubar'), expand = False) |
1104 | me.add_accel_group(me.ui.get_accel_group()) |
1105 | me.status = G.Statusbar() |
1106 | |
1107 | me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6) |
1108 | me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING) |
1109 | me.hook(me.mon.addpeerhook, me.addpeer) |
1110 | me.hook(me.mon.delpeerhook, me.delpeer) |
1111 | |
1112 | scr = G.ScrolledWindow() |
1113 | scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC) |
1114 | me.list = G.TreeView(me.listmodel) |
1115 | me.list.append_column(G.TreeViewColumn('Peer name', |
1116 | G.CellRendererText(), |
1117 | text = 0)) |
1118 | me.list.append_column(G.TreeViewColumn('Address', |
1119 | G.CellRendererText(), |
1120 | text = 1)) |
1121 | me.list.append_column(G.TreeViewColumn('T-ping', |
1122 | G.CellRendererText(), |
1123 | text = 2, |
1124 | foreground = 3)) |
1125 | me.list.append_column(G.TreeViewColumn('E-ping', |
1126 | G.CellRendererText(), |
1127 | text = 4, |
1128 | foreground = 5)) |
1129 | me.list.get_column(1).set_expand(True) |
1130 | me.list.connect('row-activated', me.activate) |
1131 | me.list.connect('button-press-event', me.buttonpress) |
1132 | me.list.set_reorderable(True) |
1133 | me.list.get_selection().set_mode(G.SELECTION_NONE) |
1134 | scr.add(me.list) |
1135 | vbox.pack_start(scr) |
1136 | |
1137 | vbox.pack_start(me.status, expand = False) |
1138 | me.hook(me.mon.connecthook, me.connected) |
1139 | me.hook(me.mon.disconnecthook, me.disconnected) |
1140 | me.hook(me.mon.notehook, me.notify) |
1141 | me.pinger = None |
1142 | me.set_default_size(420, 180) |
1143 | me.mon.connect() |
1144 | me.show_all() |
1145 | |
1146 | def addpeer(me, peer): |
1147 | peer.i = me.listmodel.append([peer.name, peer.addr, |
1148 | '???', 'green', '???', 'green']) |
1149 | peer.win = WindowSlot(lambda: PeerWindow(me.mon, peer)) |
1150 | peer.pinghook = HookList() |
1151 | peer.ping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0, |
1152 | tlast = 0, ttot = 0, |
1153 | tcol = 2, ccol = 3, cmd = 'Transport pings') |
1154 | peer.eping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0, |
1155 | tlast = 0, ttot = 0, |
1156 | tcol = 4, ccol = 5, cmd = 'Encrypted pings') |
1157 | def delpeer(me, peer): |
1158 | me.listmodel.remove(peer.i) |
1159 | def path_peer(me, path): |
1160 | return me.mon.peers[me.listmodel[path][0]] |
1161 | |
1162 | def activate(me, l, path, col): |
1163 | peer = me.path_peer(path) |
1164 | peer.win.open() |
1165 | def buttonpress(me, l, ev): |
1166 | if ev.button == 3: |
1167 | r = me.list.get_path_at_pos(ev.x, ev.y) |
1168 | for i in '/peer-popup/kill-peer', '/peer-popup/force-kx': |
1169 | me.ui.get_widget(i).set_sensitive(me.mon.connectedp and |
1170 | r is not None) |
1171 | if r: |
1172 | me.menupeer = me.path_peer(r[0]) |
1173 | else: |
1174 | me.menupeer = None |
1175 | me.ui.get_widget('/peer-popup').popup(None, None, None, |
1176 | ev.button, ev.time) |
1177 | |
1178 | def killpeer(me): |
1179 | me.mon.simplecmd('KILL %s' % me.menupeer.name) |
1180 | def forcekx(me): |
1181 | me.mon.simplecmd('FORCEKX %s' % me.menupeer.name) |
1182 | |
1183 | def reping(me): |
1184 | if me.pinger is not None: |
1185 | GO.source_remove(me.pinger) |
1186 | me.pinger = GO.timeout_add(10000, me.ping) |
1187 | me.ping() |
1188 | def unping(me): |
1189 | if me.pinger is not None: |
1190 | GO.source_remove(me.pinger) |
1191 | me.pinger = None |
1192 | def ping(me): |
1193 | for name in me.mon.peers: |
1194 | p = me.mon.peers[name] |
1195 | PingCommand(me.mon, 'PING', p, lambda p, t: me.pong(p, p.ping, t)) |
1196 | PingCommand(me.mon, 'EPING', p, lambda p, t: me.pong(p, p.eping, t)) |
1197 | return True |
1198 | def pong(me, p, ping, t): |
1199 | ping.n += 1 |
1200 | if t is None: |
1201 | ping.nmiss += 1 |
1202 | ping.nmissrun += 1 |
1203 | me.listmodel[p.i][ping.tcol] = '(miss %d)' % ping.nmissrun |
1204 | me.listmodel[p.i][ping.ccol] = 'red' |
1205 | else: |
1206 | ping.ngood += 1 |
1207 | ping.nmissrun = 0 |
1208 | ping.tlast = t |
1209 | ping.ttot += t |
1210 | me.listmodel[p.i][ping.tcol] = '%.1f ms' % t |
1211 | me.listmodel[p.i][ping.ccol] = 'black' |
1212 | p.pinghook.run() |
1213 | def setstatus(me, status): |
1214 | me.status.pop(0) |
1215 | me.status.push(0, status) |
1216 | def notify(me, note, rest): |
1217 | if note == 'DAEMON': |
1218 | me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False) |
1219 | def connected(me): |
1220 | me.setstatus('Connected (port %s)' % me.mon.simplecmd('PORT')[0]) |
1221 | me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False) |
1222 | for i in ('/menubar/server-menu/disconnect', |
1223 | '/menubar/server-menu/server-version', |
1224 | '/menubar/server-menu/add-peer', |
1225 | '/menubar/server-menu/server-quit', |
1226 | '/menubar/logs-menu/trace-options'): |
1227 | me.ui.get_widget(i).set_sensitive(True) |
1228 | me.ui.get_widget('/menubar/server-menu/daemon'). \ |
1229 | set_sensitive(parseinfo(me.mon.simplecmd('SERVINFO'))['daemon'] == |
1230 | 'nil') |
1231 | me.reping() |
1232 | def disconnected(me): |
1233 | me.setstatus('Disconnected') |
1234 | me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True) |
1235 | for i in ('/menubar/server-menu/disconnect', |
1236 | '/menubar/server-menu/server-version', |
1237 | '/menubar/server-menu/add-peer', |
1238 | '/menubar/server-menu/daemon', |
1239 | '/menubar/server-menu/server-quit', |
1240 | '/menubar/logs-menu/trace-options'): |
1241 | me.ui.get_widget(i).set_sensitive(False) |
1242 | me.unping() |
1243 | def destroy(me): |
1244 | if me.pinger is not None: |
1245 | GO.source_remove(me.pinger) |
1246 | def report(me, msg): |
1247 | moanbox(msg) |
1248 | return True |
1249 | |
1250 | #----- Parse options -------------------------------------------------------- |
1251 | |
1252 | def version(fp = stdout): |
1253 | """Print the program's version number.""" |
1254 | fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION)) |
1255 | |
1256 | def usage(fp): |
1257 | """Print a brief usage message for the program.""" |
1258 | fp.write('Usage: %s [-d DIR] [-a SOCK]\n' % quis) |
1259 | |
1260 | def main(): |
1261 | global tripedir |
1262 | if 'TRIPEDIR' in environ: |
1263 | tripedir = environ['TRIPEDIR'] |
1264 | tripesock = '%s/%s' % (socketdir, 'tripesock') |
1265 | |
1266 | try: |
1267 | opts, args = O.getopt(argv[1:], |
1268 | 'hvud:a:', |
1269 | ['help', 'version', 'usage', |
1270 | 'directory=', 'admin-socket=']) |
1271 | except O.GetoptError, exc: |
1272 | moan(exc) |
1273 | usage(stderr) |
1274 | exit(1) |
1275 | for o, v in opts: |
1276 | if o in ('-h', '--help'): |
1277 | version(stdout) |
1278 | print |
1279 | usage(stdout) |
1280 | print """ |
1281 | Graphical monitor for TrIPE VPN. |
1282 | |
1283 | Options supported: |
1284 | |
1285 | -h, --help Show this help message. |
1286 | -v, --version Show the version number. |
1287 | -u, --usage Show pointlessly short usage string. |
1288 | |
1289 | -d, --directory=DIR Use TrIPE directory DIR. |
1290 | -a, --admin-socket=FILE Select socket to connect to.""" |
1291 | exit(0) |
1292 | elif o in ('-v', '--version'): |
1293 | version(stdout) |
1294 | exit(0) |
1295 | elif o in ('-u', '--usage'): |
1296 | usage(stdout) |
1297 | exit(0) |
1298 | elif o in ('-d', '--directory'): |
1299 | tripedir = v |
1300 | elif o in ('-a', '--admin-socket'): |
1301 | tripesock = v |
1302 | else: |
1303 | raise "can't happen!" |
1304 | if len(args) > 0: |
1305 | usage(stderr) |
1306 | exit(1) |
1307 | |
1308 | OS.chdir(tripedir) |
1309 | mon = Monitor(tripesock) |
1310 | root = MonitorWindow(mon) |
1311 | HookClient().hook(root.closehook, exit) |
1312 | G.main() |
1313 | |
1314 | if __name__ == '__main__': |
1315 | main() |
1316 | |
ca6eb20c |
1317 | #----- That's all, folks ---------------------------------------------------- |