Commit | Line | Data |
---|---|---|
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 | |
c55f0b7c | 14 | import re as RX |
060ca767 | 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), | |
165efde7 | 969 | '4070', |
060ca767 | 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: | |
e04c2d50 | 1245 | GO.source_remove(me.pinger) |
060ca767 | 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'] | |
179d8129 | 1264 | tripesock = environ.get('TRIPESOCK', '%s/%s' % (socketdir, 'tripesock')) |
060ca767 | 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 | ||
1279 | usage(stdout) | |
1280 | print """ | |
1281 | Graphical monitor for TrIPE VPN. | |
1282 | ||
1283 | Options supported: | |
1284 | ||
e04c2d50 MW |
1285 | -h, --help Show this help message. |
1286 | -v, --version Show the version number. | |
1287 | -u, --usage Show pointlessly short usage string. | |
060ca767 | 1288 | |
e04c2d50 MW |
1289 | -d, --directory=DIR Use TrIPE directory DIR. |
1290 | -a, --admin-socket=FILE Select socket to connect to.""" | |
060ca767 | 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 ---------------------------------------------------- |