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 | |
ca6eb20c |
537 | def makeactiongroup(name, acts): |
538 | """Creates an ActionGroup called NAME. ACTS is a list of tuples |
539 | containing: |
540 | ACT: an action name |
541 | LABEL: the label string for the action |
542 | ACCEL: accelerator string, or None |
543 | FUNC: thunk to call when the action is invoked""" |
544 | actgroup = G.ActionGroup(name) |
545 | for act, label, accel, func in acts: |
546 | a = G.Action(act, label, None, None) |
547 | if func: a.connect('activate', invoker(func)) |
548 | actgroup.add_action_with_accel(a, accel) |
549 | return actgroup |
550 | |
551 | class GridPacker (G.Table): |
552 | """Like a Table, but with more state: makes filling in the widgets |
553 | easier.""" |
554 | def __init__(me): |
555 | G.Table.__init__(me) |
556 | me.row = 0 |
557 | me.col = 0 |
558 | me.rows = 1 |
559 | me.cols = 1 |
560 | me.set_border_width(4) |
561 | me.set_col_spacings(4) |
562 | me.set_row_spacings(4) |
563 | def pack(me, w, width = 1, newlinep = False, |
564 | xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0, |
565 | xpad = 0, ypad = 0): |
566 | """Packs a new widget. W is the widget to add. XOPY, YOPT, XPAD and |
567 | YPAD are as for Table. WIDTH is how many cells to take up horizontally. |
568 | NEWLINEP is whether to start a new line for this widget. Returns W.""" |
569 | if newlinep: |
570 | me.row += 1 |
571 | me.col = 0 |
572 | bot = me.row + 1 |
573 | right = me.col + width |
574 | if bot > me.rows or right > me.cols: |
575 | if bot > me.rows: me.rows = bot |
576 | if right > me.cols: me.cols = right |
577 | me.resize(me.rows, me.cols) |
578 | me.attach(w, me.col, me.col + width, me.row, me.row + 1, |
579 | xopt, yopt, xpad, ypad) |
580 | me.col += width |
581 | return w |
582 | def labelled(me, lab, w, newlinep = False, **kw): |
583 | """Packs a labelled widget. Other arguments are as for pack. Returns |
584 | W.""" |
585 | label = G.Label(lab) |
586 | label.set_alignment(1.0, 0) |
587 | me.pack(label, newlinep = newlinep, xopt = G.FILL) |
588 | me.pack(w, **kw) |
589 | return w |
590 | def info(me, label, text = None, len = 18, **kw): |
591 | """Packs an information widget with a label. LABEL is the label; TEXT is |
592 | the initial text; LEN is the estimated length in characters. Returns the |
593 | entry widget.""" |
594 | e = G.Entry() |
595 | if text is not None: e.set_text(text) |
596 | e.set_width_chars(len) |
597 | e.set_editable(False) |
598 | me.labelled(label, e, **kw) |
599 | return e |
600 | |
060ca767 |
601 | class WindowSlot (HookClient): |
602 | """A place to store a window. If the window is destroyed, remember this; |
603 | when we come to open the window, raise it if it already exists; otherwise |
604 | make a new one.""" |
605 | def __init__(me, createfunc): |
606 | """Constructor: CREATEFUNC must return a new Window which supports the |
607 | closehook protocol.""" |
608 | HookClient.__init__(me) |
609 | me.createfunc = createfunc |
610 | me.window = None |
611 | def open(me): |
612 | """Opens the window, creating it if necessary.""" |
613 | if me.window: |
614 | me.window.window.raise_() |
615 | else: |
616 | me.window = me.createfunc() |
617 | me.hook(me.window.closehook, me.closed) |
618 | def closed(me): |
619 | me.unhook(me.window.closehook) |
620 | me.window = None |
621 | |
622 | class ValidationError (Exception): |
623 | """Raised by ValidatingEntry.get_text() if the text isn't valid.""" |
624 | pass |
625 | class ValidatingEntry (G.Entry): |
626 | """Like an Entry, but makes the text go red if the contents are invalid. |
627 | If get_text is called, and the text is invalid, ValidationError is |
628 | raised.""" |
629 | def __init__(me, valid, text = '', size = -1, *arg, **kw): |
630 | """Make an Entry. VALID is a regular expression or a predicate on |
631 | strings. TEXT is the default text to insert. SIZE is the size of the |
632 | box to set, in characters (ish). Other arguments are passed to Entry.""" |
633 | G.Entry.__init__(me, *arg, **kw) |
634 | me.connect("changed", me.check) |
635 | if callable(valid): |
636 | me.validate = valid |
637 | else: |
638 | me.validate = RX.compile(valid).match |
639 | me.ensure_style() |
640 | me.c_ok = me.get_style().text[G.STATE_NORMAL] |
641 | me.c_bad = c_red |
642 | if size != -1: me.set_width_chars(size) |
643 | me.set_activates_default(True) |
644 | me.set_text(text) |
645 | me.check() |
646 | def check(me, *hunoz): |
647 | if me.validate(G.Entry.get_text(me)): |
648 | me.validp = True |
649 | me.modify_text(G.STATE_NORMAL, me.c_ok) |
650 | else: |
651 | me.validp = False |
652 | me.modify_text(G.STATE_NORMAL, me.c_bad) |
653 | def get_text(me): |
654 | if not me.validp: |
655 | raise ValidationError |
656 | return G.Entry.get_text(me) |
657 | |
658 | def numericvalidate(min = None, max = None): |
659 | """Validation function for numbers. Entry must consist of an optional sign |
660 | followed by digits, and the resulting integer must be within the given |
661 | bounds.""" |
662 | return lambda x: (rx_num.match(x) and |
663 | (min is None or long(x) >= min) and |
664 | (max is None or long(x) <= max)) |
665 | |
666 | #----- Various minor dialog boxen ------------------------------------------- |
667 | |
668 | GPL = """This program is free software; you can redistribute it and/or modify |
669 | it under the terms of the GNU General Public License as published by |
670 | the Free Software Foundation; either version 2 of the License, or |
671 | (at your option) any later version. |
672 | |
673 | This program is distributed in the hope that it will be useful, |
674 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
675 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
676 | GNU General Public License for more details. |
677 | |
678 | You should have received a copy of the GNU General Public License |
679 | along with this program; if not, write to the Free Software Foundation, |
680 | Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.""" |
681 | |
682 | class AboutBox (G.AboutDialog, MyWindowMixin): |
683 | """The program `About' box.""" |
684 | def __init__(me): |
685 | G.AboutDialog.__init__(me) |
686 | me.mywininit() |
687 | me.set_name('TrIPEmon') |
688 | me.set_version(VERSION) |
689 | me.set_license(GPL) |
690 | me.set_authors(['Mark Wooding']) |
691 | me.connect('unmap', invoker(me.close)) |
692 | me.show() |
693 | aboutbox = WindowSlot(AboutBox) |
694 | |
695 | def moanbox(msg): |
696 | """Report an error message in a window.""" |
697 | d = G.Dialog('Error from %s' % quis, |
698 | flags = G.DIALOG_MODAL, |
699 | buttons = ((G.STOCK_OK, G.RESPONSE_NONE))) |
700 | label = G.Label(msg) |
701 | label.set_padding(20, 20) |
702 | d.vbox.pack_start(label) |
703 | label.show() |
704 | d.run() |
705 | d.destroy() |
706 | |
ca6eb20c |
707 | def unimplemented(*hunoz): |
708 | """Indicator of laziness.""" |
709 | moanbox("I've not written that bit yet.") |
710 | |
711 | class ServInfo (MyWindow): |
712 | def __init__(me, monitor): |
713 | MyWindow.__init__(me) |
714 | me.set_title('TrIPE server info') |
715 | me.mon = monitor |
716 | me.table = GridPacker() |
717 | me.add(me.table) |
718 | me.e = {} |
719 | def add(label, tag, text = None, **kw): |
720 | me.e[tag] = me.table.info(label, text, **kw) |
721 | add('Implementation', 'implementation') |
722 | add('Version', 'version', newlinep = True) |
723 | me.update() |
724 | me.hook(me.mon.connecthook, me.update) |
725 | me.show_all() |
726 | def update(me): |
727 | info = parseinfo(me.mon.simplecmd('SERVINFO')) |
728 | for i in me.e: |
729 | me.e[i].set_text(info[i]) |
730 | |
731 | class TraceOptions (MyDialog): |
732 | """Tracing options window.""" |
733 | def __init__(me, monitor): |
734 | MyDialog.__init__(me, title = 'Tracing options', |
735 | buttons = [(G.STOCK_CLOSE, me.destroy), |
736 | (G.STOCK_OK, me.ok)]) |
737 | me.mon = monitor |
738 | me.opts = [] |
739 | for o in me.mon.simplecmd('TRACE'): |
740 | char = o[0] |
741 | onp = o[1] |
742 | text = o[3].upper() + o[4:] |
743 | if char.isupper(): continue |
744 | ticky = G.CheckButton(text) |
745 | ticky.set_active(onp != ' ') |
746 | me.vbox.pack_start(ticky) |
747 | me.opts.append((char, ticky)) |
748 | me.show_all() |
749 | def ok(me): |
750 | on = [] |
751 | off = [] |
752 | for char, ticky in me.opts: |
753 | if ticky.get_active(): |
754 | on.append(char) |
755 | else: |
756 | off.append(char) |
757 | setting = ''.join(on) + '-' + ''.join(off) |
758 | me.mon.simplecmd('TRACE %s' % setting) |
759 | me.destroy() |
760 | |
060ca767 |
761 | #----- Logging windows ------------------------------------------------------ |
762 | |
763 | class LogModel (G.ListStore): |
764 | """A simple list of log messages.""" |
765 | def __init__(me, columns): |
766 | """Call with a list of column names. All must be strings. We add a time |
767 | column to the left.""" |
768 | me.cols = ('Time',) + columns |
769 | G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols))) |
770 | def add(me, *entries): |
771 | """Adds a new log message, with a timestamp.""" |
772 | now = T.strftime('%Y-%m-%d %H:%M:%S') |
773 | me.append((now,) + entries) |
774 | |
775 | class TraceLogModel (LogModel): |
776 | """Log model for trace messages.""" |
777 | def __init__(me): |
778 | LogModel.__init__(me, ('Message',)) |
779 | def notify(me, line): |
780 | """Call with a new trace message.""" |
781 | me.add(line) |
782 | |
783 | class WarningLogModel (LogModel): |
784 | """Log model for warnings. We split the category out into a separate |
785 | column.""" |
786 | def __init__(me): |
787 | LogModel.__init__(me, ('Category', 'Message')) |
788 | def notify(me, line): |
789 | """Call with a new warning message.""" |
790 | me.add(*getword(line)) |
791 | |
792 | class LogViewer (MyWindow): |
793 | """Log viewer window. Nothing very exciting.""" |
794 | def __init__(me, model): |
795 | MyWindow.__init__(me) |
796 | me.model = model |
797 | scr = G.ScrolledWindow() |
798 | scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC) |
799 | me.list = G.TreeView(me.model) |
800 | me.closehook = HookList() |
801 | i = 0 |
802 | for c in me.model.cols: |
803 | me.list.append_column(G.TreeViewColumn(c, |
804 | G.CellRendererText(), |
805 | text = i)) |
806 | i += 1 |
807 | me.set_default_size(440, 256) |
808 | scr.add(me.list) |
809 | me.add(scr) |
810 | me.show_all() |
811 | |
ca6eb20c |
812 | #----- Peer window ---------------------------------------------------------- |
060ca767 |
813 | |
814 | def xlate_time(t): |
815 | """Translate a time in tripe's stats format to something a human might |
816 | actually want to read.""" |
817 | if t == 'NEVER': return '(never)' |
818 | Y, M, D, h, m, s = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6)) |
819 | return '%04d:%02d:%02d %02d:%02d:%02d' % (Y, M, D, h, m, s) |
820 | def xlate_bytes(b): |
821 | """Translate a number of bytes into something a human might want to read.""" |
822 | suff = 'B' |
823 | b = int(b) |
824 | for s in 'KMG': |
825 | if b < 4096: break |
826 | b /= 1024 |
827 | suff = s |
828 | return '%d %s' % (b, suff) |
829 | |
830 | ## How to translate peer stats. Maps the stat name to a translation |
831 | ## function. |
832 | statsxlate = \ |
833 | [('start-time', xlate_time), |
834 | ('last-packet-time', xlate_time), |
835 | ('last-keyexch-time', xlate_time), |
836 | ('bytes-in', xlate_bytes), |
837 | ('bytes-out', xlate_bytes), |
838 | ('keyexch-bytes-in', xlate_bytes), |
839 | ('keyexch-bytes-out', xlate_bytes), |
840 | ('ip-bytes-in', xlate_bytes), |
841 | ('ip-bytes-out', xlate_bytes)] |
842 | |
843 | ## How to lay out the stats dialog. Format is (LABEL, FORMAT): LABEL is |
844 | ## the label to give the entry box; FORMAT is the format string to write into |
845 | ## the entry. |
846 | statslayout = \ |
847 | [('Start time', '%(start-time)s'), |
848 | ('Last key-exchange', '%(last-keyexch-time)s'), |
849 | ('Last packet', '%(last-packet-time)s'), |
850 | ('Packets in/out', |
851 | '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'), |
852 | ('Key-exchange in/out', |
853 | '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'), |
854 | ('IP in/out', |
855 | '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-in)s (%(ip-bytes-in)s)'), |
856 | ('Rejected packets', '%(rejected-packets)s')] |
857 | |
858 | class PeerWindow (MyWindow): |
859 | """Show information about a peer.""" |
860 | def __init__(me, monitor, peer): |
861 | MyWindow.__init__(me) |
862 | me.set_title('TrIPE statistics: %s' % peer.name) |
863 | me.mon = monitor |
864 | me.peer = peer |
865 | table = GridPacker() |
866 | me.add(table) |
867 | me.e = {} |
868 | def add(label, text = None): |
869 | me.e[label] = table.info(label, text, len = 42, newlinep = True) |
870 | add('Peer name', peer.name) |
871 | add('Tunnel', peer.tunnel) |
872 | add('Interface', peer.ifname) |
873 | add('Keepalives', |
874 | (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive) |
875 | add('Address', peer.addr) |
876 | add('Transport pings') |
877 | add('Encrypted pings') |
878 | for label, format in statslayout: add(label) |
879 | me.timeout = None |
880 | me.hook(me.mon.connecthook, me.tryupdate) |
881 | me.hook(me.mon.disconnecthook, me.stopupdate) |
882 | me.hook(me.closehook, me.stopupdate) |
883 | me.hook(me.peer.deadhook, me.dead) |
884 | me.hook(me.peer.pinghook, me.ping) |
885 | me.tryupdate() |
886 | me.ping() |
887 | me.show_all() |
888 | def update(me): |
889 | if not me.peer.alivep or not me.mon.connectedp: return False |
890 | stat = parseinfo(me.mon.simplecmd('STATS %s' % me.peer.name)) |
891 | for s, trans in statsxlate: |
892 | stat[s] = trans(stat[s]) |
893 | for label, format in statslayout: |
894 | me.e[label].set_text(format % stat) |
895 | return True |
896 | def tryupdate(me): |
897 | if me.timeout is None and me.update(): |
898 | me.timeout = GO.timeout_add(1000, me.update) |
899 | def stopupdate(me): |
900 | if me.timeout is not None: |
901 | GO.source_remove(me.timeout) |
902 | me.timeout = None |
903 | def dead(me): |
904 | me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name) |
905 | me.e['Peer name'].set_text('%s [defunct]' % me.peer.name) |
906 | me.stopupdate() |
907 | def ping(me): |
908 | for ping in me.peer.ping, me.peer.eping: |
909 | s = '%d/%d' % (ping.ngood, ping.n) |
910 | if ping.ngood: |
911 | s += '; %.2f ms (last %.1f ms)' % (ping.ttot/ping.ngood, ping.tlast); |
912 | me.e[ping.cmd].set_text(s) |
913 | |
ca6eb20c |
914 | #----- Add peer ------------------------------------------------------------- |
915 | |
060ca767 |
916 | class AddPeerCommand (SimpleBackgroundCommand): |
917 | def __init__(me, conn, dlg, name, addr, port, |
918 | keepalive = None, tunnel = None): |
919 | me.name = name |
920 | me.addr = addr |
921 | me.port = port |
922 | me.keepalive = keepalive |
923 | me.tunnel = tunnel |
924 | cmd = StringIO() |
925 | cmd.write('ADD %s' % name) |
926 | cmd.write(' -background %s' % jobid()) |
927 | if keepalive is not None: cmd.write(' -keepalive %s' % keepalive) |
928 | if tunnel is not None: cmd.write(' -tunnel %s' % tunnel) |
929 | cmd.write(' INET %s %s' % (addr, port)) |
930 | SimpleBackgroundCommand.__init__(me, conn, cmd.getvalue()) |
931 | me.hook(me.donehook, invoker(dlg.destroy)) |
932 | def fail(me, err): |
933 | token, msg = getword(str(err)) |
934 | if token in ('resolve-error', 'resolver-timeout'): |
935 | moanbox("Unable to resolve hostname `%s'" % me.addr) |
936 | elif token == 'peer-create-fail': |
937 | moanbox("Couldn't create new peer `%s'" % me.name) |
938 | elif token == 'peer-exists': |
939 | moanbox("Peer `%s' already exists" % me.name) |
940 | else: |
941 | moanbox("Unexpected error from server command `ADD': %s" % err) |
942 | |
943 | class AddPeerDialog (MyDialog): |
944 | def __init__(me, monitor): |
945 | MyDialog.__init__(me, 'Add peer', |
946 | buttons = [(G.STOCK_CANCEL, me.destroy), |
947 | (G.STOCK_OK, me.ok)]) |
948 | me.mon = monitor |
949 | table = GridPacker() |
950 | me.vbox.pack_start(table) |
951 | me.e_name = table.labelled('Name', |
952 | ValidatingEntry(r'^[^\s.:]+$', '', 16), |
953 | width = 3) |
954 | me.e_addr = table.labelled('Address', |
955 | ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24), |
956 | newlinep = True) |
957 | me.e_port = table.labelled('Port', |
958 | ValidatingEntry(numericvalidate(0, 65535), |
959 | '22003', |
960 | 5)) |
961 | me.c_keepalive = G.CheckButton('Keepalives') |
962 | me.l_tunnel = table.labelled('Tunnel', |
963 | G.combo_box_new_text(), |
964 | newlinep = True, width = 3) |
965 | me.tuns = me.mon.simplecmd('TUNNELS') |
966 | for t in me.tuns: |
967 | me.l_tunnel.append_text(t) |
968 | me.l_tunnel.set_active(0) |
969 | table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL) |
970 | me.c_keepalive.connect('toggled', |
971 | lambda t: me.e_keepalive.set_sensitive\ |
972 | (t.get_active())) |
973 | me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5) |
974 | me.e_keepalive.set_sensitive(False) |
975 | table.pack(me.e_keepalive, width = 3) |
976 | me.show_all() |
977 | def ok(me): |
978 | try: |
979 | if me.c_keepalive.get_active(): |
980 | ka = me.e_keepalive.get_text() |
981 | else: |
982 | ka = None |
983 | t = me.l_tunnel.get_active() |
984 | if t == 0: |
985 | tun = None |
986 | else: |
987 | tun = me.tuns[t] |
988 | AddPeerCommand(me.mon, me, |
989 | me.e_name.get_text(), |
990 | me.e_addr.get_text(), |
991 | me.e_port.get_text(), |
992 | keepalive = ka, |
993 | tunnel = tun) |
994 | except ValidationError: |
995 | GDK.beep() |
996 | return |
997 | |
ca6eb20c |
998 | #----- The server monitor --------------------------------------------------- |
060ca767 |
999 | |
1000 | class PingCommand (SimpleBackgroundCommand): |
1001 | def __init__(me, conn, cmd, peer, func): |
1002 | me.peer = peer |
1003 | me.func = func |
1004 | SimpleBackgroundCommand.__init__ \ |
1005 | (me, conn, '%s -background %s %s' % (cmd, jobid(), peer.name)) |
1006 | def ok(me): |
1007 | tok, rest = getword(me.info[0]) |
1008 | if tok == 'ping-ok': |
1009 | me.func(me.peer, float(rest)) |
1010 | else: |
1011 | me.func(me.peer, None) |
1012 | me.unhookall() |
1013 | def fail(me, err): me.unhookall() |
1014 | def lost(me): me.unhookall() |
1015 | |
1016 | class MonitorWindow (MyWindow): |
1017 | |
1018 | def __init__(me, monitor): |
1019 | MyWindow.__init__(me) |
1020 | me.set_title('TrIPE monitor') |
1021 | me.mon = monitor |
1022 | me.hook(me.mon.errorhook, me.report) |
1023 | me.warnings = WarningLogModel() |
1024 | me.hook(me.mon.warnhook, me.warnings.notify) |
1025 | me.trace = TraceLogModel() |
1026 | me.hook(me.mon.tracehook, me.trace.notify) |
1027 | |
1028 | me.warnview = WindowSlot(lambda: LogViewer(me.warnings)) |
1029 | me.traceview = WindowSlot(lambda: LogViewer(me.trace)) |
1030 | me.traceopts = WindowSlot(lambda: TraceOptions(me.mon)) |
1031 | me.addpeerwin = WindowSlot(lambda: AddPeerDialog(me.mon)) |
1032 | me.servinfo = WindowSlot(lambda: ServInfo(me.mon)) |
1033 | |
1034 | vbox = G.VBox() |
1035 | me.add(vbox) |
1036 | |
1037 | me.ui = G.UIManager() |
ca6eb20c |
1038 | def cmd(c): me.mon.simplecmd(c) |
060ca767 |
1039 | actgroup = makeactiongroup('monitor', |
1040 | [('file-menu', '_File', None, None), |
1041 | ('connect', '_Connect', '<Alt>C', me.mon.connect), |
1042 | ('disconnect', '_Disconnect', '<Alt>D', me.mon.disconnect), |
1043 | ('quit', '_Quit', '<Alt>Q', me.close), |
1044 | ('server-menu', '_Server', None, None), |
ca6eb20c |
1045 | ('daemon', 'Run in _background', None, cmd('DAEMON')), |
1046 | ('server-version', 'Server version', '<Alt>V', me.servinfo.open), |
1047 | ('reload-keys', 'Reload keys', '<Alt>R', cmd('RELOAD')), |
1048 | ('server-quit', 'Terminate server', None, cmd('QUIT')), |
060ca767 |
1049 | ('logs-menu', '_Logs', None, None), |
1050 | ('show-warnings', 'Show _warnings', '<Alt>W', me.warnview.open), |
1051 | ('show-trace', 'Show _trace', '<Alt>T', me.traceview.open), |
1052 | ('trace-options', 'Trace _options...', None, me.traceopts.open), |
1053 | ('help-menu', '_Help', None, None), |
1054 | ('about', '_About tripemon...', None, aboutbox.open), |
1055 | ('add-peer', '_Add peer...', '<Alt>A', me.addpeerwin.open), |
1056 | ('kill-peer', '_Kill peer', None, me.killpeer), |
1057 | ('force-kx', 'Force key e_xchange', None, me.forcekx)]) |
1058 | uidef = ''' |
1059 | <ui> |
1060 | <menubar> |
1061 | <menu action="file-menu"> |
1062 | <menuitem action="quit"/> |
1063 | </menu> |
1064 | <menu action="server-menu"> |
ca6eb20c |
1065 | <menuitem action="connect"/> |
060ca767 |
1066 | <menuitem action="disconnect"/> |
1067 | <separator/> |
ca6eb20c |
1068 | <menuitem action="server-version"/> |
060ca767 |
1069 | <menuitem action="add-peer"/> |
1070 | <menuitem action="daemon"/> |
ca6eb20c |
1071 | <menuitem action="reload-keys"/> |
060ca767 |
1072 | <separator/> |
1073 | <menuitem action="server-quit"/> |
1074 | </menu> |
1075 | <menu action="logs-menu"> |
1076 | <menuitem action="show-warnings"/> |
1077 | <menuitem action="show-trace"/> |
1078 | <menuitem action="trace-options"/> |
1079 | </menu> |
1080 | <menu action="help-menu"> |
1081 | <menuitem action="about"/> |
1082 | </menu> |
1083 | </menubar> |
1084 | <popup name="peer-popup"> |
1085 | <menuitem action="add-peer"/> |
1086 | <menuitem action="kill-peer"/> |
1087 | <menuitem action="force-kx"/> |
1088 | </popup> |
1089 | </ui> |
1090 | ''' |
1091 | me.ui.insert_action_group(actgroup, 0) |
1092 | me.ui.add_ui_from_string(uidef) |
1093 | vbox.pack_start(me.ui.get_widget('/menubar'), expand = False) |
1094 | me.add_accel_group(me.ui.get_accel_group()) |
1095 | me.status = G.Statusbar() |
1096 | |
1097 | me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6) |
1098 | me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING) |
1099 | me.hook(me.mon.addpeerhook, me.addpeer) |
1100 | me.hook(me.mon.delpeerhook, me.delpeer) |
1101 | |
1102 | scr = G.ScrolledWindow() |
1103 | scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC) |
1104 | me.list = G.TreeView(me.listmodel) |
1105 | me.list.append_column(G.TreeViewColumn('Peer name', |
1106 | G.CellRendererText(), |
1107 | text = 0)) |
1108 | me.list.append_column(G.TreeViewColumn('Address', |
1109 | G.CellRendererText(), |
1110 | text = 1)) |
1111 | me.list.append_column(G.TreeViewColumn('T-ping', |
1112 | G.CellRendererText(), |
1113 | text = 2, |
1114 | foreground = 3)) |
1115 | me.list.append_column(G.TreeViewColumn('E-ping', |
1116 | G.CellRendererText(), |
1117 | text = 4, |
1118 | foreground = 5)) |
1119 | me.list.get_column(1).set_expand(True) |
1120 | me.list.connect('row-activated', me.activate) |
1121 | me.list.connect('button-press-event', me.buttonpress) |
1122 | me.list.set_reorderable(True) |
1123 | me.list.get_selection().set_mode(G.SELECTION_NONE) |
1124 | scr.add(me.list) |
1125 | vbox.pack_start(scr) |
1126 | |
1127 | vbox.pack_start(me.status, expand = False) |
1128 | me.hook(me.mon.connecthook, me.connected) |
1129 | me.hook(me.mon.disconnecthook, me.disconnected) |
1130 | me.hook(me.mon.notehook, me.notify) |
1131 | me.pinger = None |
1132 | me.set_default_size(420, 180) |
1133 | me.mon.connect() |
1134 | me.show_all() |
1135 | |
1136 | def addpeer(me, peer): |
1137 | peer.i = me.listmodel.append([peer.name, peer.addr, |
1138 | '???', 'green', '???', 'green']) |
1139 | peer.win = WindowSlot(lambda: PeerWindow(me.mon, peer)) |
1140 | peer.pinghook = HookList() |
1141 | peer.ping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0, |
1142 | tlast = 0, ttot = 0, |
1143 | tcol = 2, ccol = 3, cmd = 'Transport pings') |
1144 | peer.eping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0, |
1145 | tlast = 0, ttot = 0, |
1146 | tcol = 4, ccol = 5, cmd = 'Encrypted pings') |
1147 | def delpeer(me, peer): |
1148 | me.listmodel.remove(peer.i) |
1149 | def path_peer(me, path): |
1150 | return me.mon.peers[me.listmodel[path][0]] |
1151 | |
1152 | def activate(me, l, path, col): |
1153 | peer = me.path_peer(path) |
1154 | peer.win.open() |
1155 | def buttonpress(me, l, ev): |
1156 | if ev.button == 3: |
1157 | r = me.list.get_path_at_pos(ev.x, ev.y) |
1158 | for i in '/peer-popup/kill-peer', '/peer-popup/force-kx': |
1159 | me.ui.get_widget(i).set_sensitive(me.mon.connectedp and |
1160 | r is not None) |
1161 | if r: |
1162 | me.menupeer = me.path_peer(r[0]) |
1163 | else: |
1164 | me.menupeer = None |
1165 | me.ui.get_widget('/peer-popup').popup(None, None, None, |
1166 | ev.button, ev.time) |
1167 | |
1168 | def killpeer(me): |
1169 | me.mon.simplecmd('KILL %s' % me.menupeer.name) |
1170 | def forcekx(me): |
1171 | me.mon.simplecmd('FORCEKX %s' % me.menupeer.name) |
1172 | |
1173 | def reping(me): |
1174 | if me.pinger is not None: |
1175 | GO.source_remove(me.pinger) |
1176 | me.pinger = GO.timeout_add(10000, me.ping) |
1177 | me.ping() |
1178 | def unping(me): |
1179 | if me.pinger is not None: |
1180 | GO.source_remove(me.pinger) |
1181 | me.pinger = None |
1182 | def ping(me): |
1183 | for name in me.mon.peers: |
1184 | p = me.mon.peers[name] |
1185 | PingCommand(me.mon, 'PING', p, lambda p, t: me.pong(p, p.ping, t)) |
1186 | PingCommand(me.mon, 'EPING', p, lambda p, t: me.pong(p, p.eping, t)) |
1187 | return True |
1188 | def pong(me, p, ping, t): |
1189 | ping.n += 1 |
1190 | if t is None: |
1191 | ping.nmiss += 1 |
1192 | ping.nmissrun += 1 |
1193 | me.listmodel[p.i][ping.tcol] = '(miss %d)' % ping.nmissrun |
1194 | me.listmodel[p.i][ping.ccol] = 'red' |
1195 | else: |
1196 | ping.ngood += 1 |
1197 | ping.nmissrun = 0 |
1198 | ping.tlast = t |
1199 | ping.ttot += t |
1200 | me.listmodel[p.i][ping.tcol] = '%.1f ms' % t |
1201 | me.listmodel[p.i][ping.ccol] = 'black' |
1202 | p.pinghook.run() |
1203 | def setstatus(me, status): |
1204 | me.status.pop(0) |
1205 | me.status.push(0, status) |
1206 | def notify(me, note, rest): |
1207 | if note == 'DAEMON': |
1208 | me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False) |
1209 | def connected(me): |
1210 | me.setstatus('Connected (port %s)' % me.mon.simplecmd('PORT')[0]) |
1211 | me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False) |
1212 | for i in ('/menubar/server-menu/disconnect', |
1213 | '/menubar/server-menu/server-version', |
1214 | '/menubar/server-menu/add-peer', |
1215 | '/menubar/server-menu/server-quit', |
1216 | '/menubar/logs-menu/trace-options'): |
1217 | me.ui.get_widget(i).set_sensitive(True) |
1218 | me.ui.get_widget('/menubar/server-menu/daemon'). \ |
1219 | set_sensitive(parseinfo(me.mon.simplecmd('SERVINFO'))['daemon'] == |
1220 | 'nil') |
1221 | me.reping() |
1222 | def disconnected(me): |
1223 | me.setstatus('Disconnected') |
1224 | me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True) |
1225 | for i in ('/menubar/server-menu/disconnect', |
1226 | '/menubar/server-menu/server-version', |
1227 | '/menubar/server-menu/add-peer', |
1228 | '/menubar/server-menu/daemon', |
1229 | '/menubar/server-menu/server-quit', |
1230 | '/menubar/logs-menu/trace-options'): |
1231 | me.ui.get_widget(i).set_sensitive(False) |
1232 | me.unping() |
1233 | def destroy(me): |
1234 | if me.pinger is not None: |
1235 | GO.source_remove(me.pinger) |
1236 | def report(me, msg): |
1237 | moanbox(msg) |
1238 | return True |
1239 | |
1240 | #----- Parse options -------------------------------------------------------- |
1241 | |
1242 | def version(fp = stdout): |
1243 | """Print the program's version number.""" |
1244 | fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION)) |
1245 | |
1246 | def usage(fp): |
1247 | """Print a brief usage message for the program.""" |
1248 | fp.write('Usage: %s [-d DIR] [-a SOCK]\n' % quis) |
1249 | |
1250 | def main(): |
1251 | global tripedir |
1252 | if 'TRIPEDIR' in environ: |
1253 | tripedir = environ['TRIPEDIR'] |
1254 | tripesock = '%s/%s' % (socketdir, 'tripesock') |
1255 | |
1256 | try: |
1257 | opts, args = O.getopt(argv[1:], |
1258 | 'hvud:a:', |
1259 | ['help', 'version', 'usage', |
1260 | 'directory=', 'admin-socket=']) |
1261 | except O.GetoptError, exc: |
1262 | moan(exc) |
1263 | usage(stderr) |
1264 | exit(1) |
1265 | for o, v in opts: |
1266 | if o in ('-h', '--help'): |
1267 | version(stdout) |
1268 | print |
1269 | usage(stdout) |
1270 | print """ |
1271 | Graphical monitor for TrIPE VPN. |
1272 | |
1273 | Options supported: |
1274 | |
1275 | -h, --help Show this help message. |
1276 | -v, --version Show the version number. |
1277 | -u, --usage Show pointlessly short usage string. |
1278 | |
1279 | -d, --directory=DIR Use TrIPE directory DIR. |
1280 | -a, --admin-socket=FILE Select socket to connect to.""" |
1281 | exit(0) |
1282 | elif o in ('-v', '--version'): |
1283 | version(stdout) |
1284 | exit(0) |
1285 | elif o in ('-u', '--usage'): |
1286 | usage(stdout) |
1287 | exit(0) |
1288 | elif o in ('-d', '--directory'): |
1289 | tripedir = v |
1290 | elif o in ('-a', '--admin-socket'): |
1291 | tripesock = v |
1292 | else: |
1293 | raise "can't happen!" |
1294 | if len(args) > 0: |
1295 | usage(stderr) |
1296 | exit(1) |
1297 | |
1298 | OS.chdir(tripedir) |
1299 | mon = Monitor(tripesock) |
1300 | root = MonitorWindow(mon) |
1301 | HookClient().hook(root.closehook, exit) |
1302 | G.main() |
1303 | |
1304 | if __name__ == '__main__': |
1305 | main() |
1306 | |
ca6eb20c |
1307 | #----- That's all, folks ---------------------------------------------------- |