| 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 math as M |
| 11 | import sets as SET |
| 12 | import getopt as O |
| 13 | import time as T |
| 14 | import sre as RX |
| 15 | from cStringIO import StringIO |
| 16 | |
| 17 | import pygtk |
| 18 | pygtk.require('2.0') |
| 19 | import gtk as G |
| 20 | import gobject as GO |
| 21 | import gtk.gdk as GDK |
| 22 | |
| 23 | #----- Configuration -------------------------------------------------------- |
| 24 | |
| 25 | tripedir = "@configdir@" |
| 26 | socketdir = "@socketdir@" |
| 27 | PACKAGE = "@PACKAGE@" |
| 28 | VERSION = "@VERSION@" |
| 29 | |
| 30 | debug = False |
| 31 | |
| 32 | #----- Utility functions ---------------------------------------------------- |
| 33 | |
| 34 | ## Program name, shorn of extraneous stuff. |
| 35 | quis = OS.path.basename(argv[0]) |
| 36 | |
| 37 | def moan(msg): |
| 38 | """Report a message to standard error.""" |
| 39 | stderr.write('%s: %s\n' % (quis, msg)) |
| 40 | |
| 41 | def die(msg, rc = 1): |
| 42 | """Report a message to standard error and exit.""" |
| 43 | moan(msg) |
| 44 | exit(rc) |
| 45 | |
| 46 | rx_space = RX.compile(r'\s+') |
| 47 | rx_ordinary = RX.compile(r'[^\\\'\"\s]+') |
| 48 | rx_weird = RX.compile(r'([\\\'])') |
| 49 | rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$') |
| 50 | rx_num = RX.compile(r'^[-+]?\d+$') |
| 51 | |
| 52 | c_red = GDK.color_parse('red') |
| 53 | |
| 54 | def getword(s): |
| 55 | """Pull a word from the front of S, handling quoting according to the |
| 56 | tripe-admin(5) rules. Returns the word and the rest of S, or (None, None) |
| 57 | if there are no more words left.""" |
| 58 | i = 0 |
| 59 | m = rx_space.match(s, i) |
| 60 | if m: i = m.end() |
| 61 | r = '' |
| 62 | q = None |
| 63 | if i >= len(s): |
| 64 | return None, None |
| 65 | while i < len(s) and (q or not s[i].isspace()): |
| 66 | m = rx_ordinary.match(s, i) |
| 67 | if m: |
| 68 | r += m.group() |
| 69 | i = m.end() |
| 70 | elif s[i] == '\\': |
| 71 | r += s[i + 1] |
| 72 | i += 2 |
| 73 | elif s[i] == q: |
| 74 | q = None |
| 75 | i += 1 |
| 76 | elif not q and s[i] == '`' or s[i] == "'": |
| 77 | q = "'" |
| 78 | i += 1 |
| 79 | elif not q and s[i] == '"': |
| 80 | q = '"' |
| 81 | i += 1 |
| 82 | else: |
| 83 | r += s[i] |
| 84 | i += 1 |
| 85 | if q: |
| 86 | raise SyntaxError, 'missing close quote' |
| 87 | m = rx_space.match(s, i) |
| 88 | if m: i = m.end() |
| 89 | return r, s[i:] |
| 90 | |
| 91 | def quotify(s): |
| 92 | """Quote S according to the tripe-admin(5) rules.""" |
| 93 | m = rx_ordinary.match(s) |
| 94 | if m and m.end() == len(s): |
| 95 | return s |
| 96 | else: |
| 97 | return "'" + rx_weird.sub(r'\\\1', s) + "'" |
| 98 | |
| 99 | #----- Random bits of infrastructure ---------------------------------------- |
| 100 | |
| 101 | class struct (object): |
| 102 | """Simple object which stores attributes and has a sensible construction |
| 103 | syntax.""" |
| 104 | def __init__(me, **kw): |
| 105 | me.__dict__.update(kw) |
| 106 | |
| 107 | class peerinfo (struct): pass |
| 108 | class pingstate (struct): pass |
| 109 | |
| 110 | def invoker(func): |
| 111 | """Return a function which throws away its arguments and calls FUNC. (If |
| 112 | for loops worked by binding rather than assignment then we wouldn't need |
| 113 | this kludge.""" |
| 114 | return lambda *hunoz, **hukairz: func() |
| 115 | |
| 116 | class HookList (object): |
| 117 | """I maintain a list of functions, and provide the ability to call them |
| 118 | when something interesting happens. The functions are called in the order |
| 119 | they were added to the list, with all the arguments. If a function returns |
| 120 | a non-None result, no further functions are called.""" |
| 121 | def __init__(me): |
| 122 | me.list = [] |
| 123 | def add(me, func, obj): |
| 124 | me.list.append((obj, func)) |
| 125 | def prune(me, obj): |
| 126 | new = [] |
| 127 | for o, f in me.list: |
| 128 | if o is not obj: |
| 129 | new.append((o, f)) |
| 130 | me.list = new |
| 131 | def run(me, *args, **kw): |
| 132 | for o, hook in me.list: |
| 133 | rc = hook(*args, **kw) |
| 134 | if rc is not None: return rc |
| 135 | return None |
| 136 | |
| 137 | class HookClient (object): |
| 138 | def __init__(me): |
| 139 | me.hooks = SET.Set() |
| 140 | def hook(me, hk, func): |
| 141 | hk.add(func, me) |
| 142 | me.hooks.add(hk) |
| 143 | def unhook(me, hk): |
| 144 | hk.prune(me) |
| 145 | me.hooks.discard(hk) |
| 146 | def unhookall(me): |
| 147 | for hk in me.hooks: |
| 148 | hk.prune(me) |
| 149 | me.hooks.clear() |
| 150 | ##def __del__(me): |
| 151 | ## print '%s dying' % me |
| 152 | |
| 153 | #----- Connections and commands --------------------------------------------- |
| 154 | |
| 155 | class ConnException (Exception): |
| 156 | """Some sort of problem occurred while communicating with the tripe |
| 157 | server.""" |
| 158 | pass |
| 159 | |
| 160 | class Error (ConnException): |
| 161 | """A command caused the server to issue a FAIL message.""" |
| 162 | pass |
| 163 | |
| 164 | class ConnectionFailed (ConnException): |
| 165 | """The connection failed while communicating with the server.""" |
| 166 | |
| 167 | jobid_seq = 0 |
| 168 | def jobid(): |
| 169 | """Return a job tag. Used for background commands.""" |
| 170 | global jobid_seq |
| 171 | jobid_seq += 1 |
| 172 | return 'bg-%d' % jobid_seq |
| 173 | |
| 174 | class BackgroundCommand (HookClient): |
| 175 | def __init__(me, conn, cmd): |
| 176 | HookClient.__init__(me) |
| 177 | me.conn = conn |
| 178 | me.tag = None |
| 179 | me.cmd = cmd |
| 180 | me.donehook = HookList() |
| 181 | me.losthook = HookList() |
| 182 | me.info = [] |
| 183 | me.submit() |
| 184 | me.hook(me.conn.disconnecthook, me.lost) |
| 185 | def submit(me): |
| 186 | me.conn.bgcommand(me.cmd, me) |
| 187 | def lost(me): |
| 188 | me.losthook.run() |
| 189 | me.unhookall() |
| 190 | def fail(me, msg): |
| 191 | me.conn.error("Unexpected error from server command `%s': %s" % |
| 192 | (me.cmd % msg)) |
| 193 | me.unhookall() |
| 194 | def ok(me): |
| 195 | me.donehook.run(me.info) |
| 196 | me.unhookall() |
| 197 | |
| 198 | class SimpleBackgroundCommand (BackgroundCommand): |
| 199 | def submit(me): |
| 200 | try: |
| 201 | BackgroundCommand.submit(me) |
| 202 | except ConnectionFailed, err: |
| 203 | me.conn.error('Unexpected error communicating with server: %s' % msg) |
| 204 | raise |
| 205 | |
| 206 | class Connection (HookClient): |
| 207 | |
| 208 | """I represent a connection to the TrIPE server. I provide facilities for |
| 209 | sending commands and receiving replies. The connection is notional: the |
| 210 | underlying socket connection can come and go under our feet. |
| 211 | |
| 212 | Useful attributes: |
| 213 | connectedp: whether the connection is active |
| 214 | connecthook: called when we have connected |
| 215 | disconnecthook: called if we have disconnected |
| 216 | notehook: called with asynchronous notifications |
| 217 | errorhook: called if there was a command error""" |
| 218 | |
| 219 | def __init__(me, sockname): |
| 220 | """Make a new connection to the server listening to SOCKNAME. In fact, |
| 221 | we're initially disconnected, to allow the caller to get his life in |
| 222 | order before opening the floodgates.""" |
| 223 | HookClient.__init__(me) |
| 224 | me.sockname = sockname |
| 225 | me.sock = None |
| 226 | me.connectedp = False |
| 227 | me.connecthook = HookList() |
| 228 | me.disconnecthook = HookList() |
| 229 | me.errorhook = HookList() |
| 230 | me.inbuf = '' |
| 231 | me.info = [] |
| 232 | me.waitingp = False |
| 233 | me.bgcmd = None |
| 234 | me.bgmap = {} |
| 235 | def connect(me): |
| 236 | "Connect to the server. Runs connecthook if it works.""" |
| 237 | if me.sock: return |
| 238 | sock = S.socket(S.AF_UNIX, S.SOCK_STREAM) |
| 239 | try: |
| 240 | sock.connect(me.sockname) |
| 241 | except S.error, err: |
| 242 | me.error('error opening connection: %s' % err[1]) |
| 243 | me.disconnecthook.run() |
| 244 | return |
| 245 | sock.setblocking(0) |
| 246 | me.socketwatch = GO.io_add_watch(sock, GO.IO_IN, me.ready) |
| 247 | me.sock = sock |
| 248 | me.connectedp = True |
| 249 | me.connecthook.run() |
| 250 | def disconnect(me): |
| 251 | "Disconnects from the server. Runs disconnecthook." |
| 252 | if not me.sock: return |
| 253 | GO.source_remove(me.socketwatch) |
| 254 | me.sock.close() |
| 255 | me.sock = None |
| 256 | me.connectedp = False |
| 257 | me.disconnecthook.run() |
| 258 | def error(me, msg): |
| 259 | """Reports an error on the connection.""" |
| 260 | me.errorhook.run(msg) |
| 261 | |
| 262 | def bgcommand(me, cmd, bg): |
| 263 | """Sends a background command and feeds it properly.""" |
| 264 | try: |
| 265 | me.bgcmd = bg |
| 266 | err = me.docommand(cmd) |
| 267 | if err: |
| 268 | bg.fail(err) |
| 269 | finally: |
| 270 | me.bgcmd = None |
| 271 | def command(me, cmd): |
| 272 | """Sends a command to the server. Returns a list of INFO responses. Do |
| 273 | not use this for backgrounded commands: create a BackgroundCommand |
| 274 | instead. Raises apprpopriate exceptions on error, but doesn't send |
| 275 | report them to the errorhook.""" |
| 276 | err = me.docommand(cmd) |
| 277 | if err: |
| 278 | raise Error, err |
| 279 | return me.info |
| 280 | def docommand(me, cmd): |
| 281 | if not me.sock: |
| 282 | raise ConnException, 'not connected' |
| 283 | if debug: print ">>> %s" % cmd |
| 284 | me.sock.sendall(cmd + '\n') |
| 285 | me.waitingp = True |
| 286 | me.info = [] |
| 287 | try: |
| 288 | me.sock.setblocking(1) |
| 289 | while True: |
| 290 | rc, err = me.collect() |
| 291 | if rc: break |
| 292 | finally: |
| 293 | me.waitingp = False |
| 294 | me.sock.setblocking(0) |
| 295 | if len(me.inbuf) > 0: |
| 296 | GO.idle_add(lambda: me.flushbuf() and False) |
| 297 | return err |
| 298 | def simplecmd(me, cmd): |
| 299 | """Like command(), but reports errors via the errorhook as well as |
| 300 | raising exceptions.""" |
| 301 | try: |
| 302 | i = me.command(cmd) |
| 303 | except Error, msg: |
| 304 | me.error("Unexpected error from server command `%s': %s" % (cmd, msg)) |
| 305 | raise |
| 306 | except ConnectionFailed, msg: |
| 307 | me.error("Unexpected error communicating with server: %s" % msg); |
| 308 | raise |
| 309 | return i |
| 310 | def ready(me, sock, condition): |
| 311 | try: |
| 312 | me.collect() |
| 313 | except ConnException, msg: |
| 314 | me.error("Error watching server connection: %s" % msg) |
| 315 | if me.sock: |
| 316 | me.disconnect() |
| 317 | me.connect() |
| 318 | return True |
| 319 | def collect(me): |
| 320 | data = me.sock.recv(16384) |
| 321 | if data == '': |
| 322 | me.disconnect() |
| 323 | raise ConnectionFailed, 'server disconnected' |
| 324 | me.inbuf += data |
| 325 | return me.flushbuf() |
| 326 | def flushbuf(me): |
| 327 | while True: |
| 328 | nl = me.inbuf.find('\n') |
| 329 | if nl < 0: break |
| 330 | line = me.inbuf[:nl] |
| 331 | if debug: print "<<< %s" % line |
| 332 | me.inbuf = me.inbuf[nl + 1:] |
| 333 | tag, line = getword(line) |
| 334 | rc, err = me.parseline(tag, line) |
| 335 | if rc: return rc, err |
| 336 | return False, None |
| 337 | def parseline(me, code, line): |
| 338 | if code == 'BGDETACH': |
| 339 | if not me.bgcmd: |
| 340 | raise ConnectionFailed, 'unexpected detach' |
| 341 | me.bgcmd.tag = line |
| 342 | me.bgmap[line] = me.bgcmd |
| 343 | me.waitingp = False |
| 344 | me.bgcmd = None |
| 345 | return True, None |
| 346 | elif code == 'BGINFO': |
| 347 | tag, line = getword(line) |
| 348 | me.bgmap[tag].info.append(line) |
| 349 | return False, None |
| 350 | elif code == 'BGFAIL': |
| 351 | tag, line = getword(line) |
| 352 | me.bgmap[tag].fail(line) |
| 353 | del me.bgmap[tag] |
| 354 | return False, None |
| 355 | elif code == 'BGOK': |
| 356 | tag, line = getword(line) |
| 357 | me.bgmap[tag].ok() |
| 358 | del me.bgmap[tag] |
| 359 | return False, None |
| 360 | elif code == 'INFO': |
| 361 | if not me.waitingp or me.bgcmd: |
| 362 | raise ConnectionFailed, 'unexpected INFO response' |
| 363 | me.info.append(line) |
| 364 | return False, None |
| 365 | elif code == 'OK': |
| 366 | if not me.waitingp or me.bgcmd: |
| 367 | raise ConnectionFailed, 'unexpected OK response' |
| 368 | return True, None |
| 369 | elif code == 'FAIL': |
| 370 | if not me.waitingp: |
| 371 | raise ConnectionFailed, 'unexpected FAIL response' |
| 372 | return True, line |
| 373 | else: |
| 374 | raise ConnectionFailed, 'unknown response code `%s' % code |
| 375 | |
| 376 | class Monitor (Connection): |
| 377 | """I monitor a TrIPE server, noticing when it changes state and keeping |
| 378 | track of its peers. I also provide facilities for sending the server |
| 379 | commands and collecting the answers. |
| 380 | |
| 381 | Useful attributes: |
| 382 | addpeerhook: called with a new Peer when the server adds one |
| 383 | delpeerhook: called with a Peer when the server kills one |
| 384 | tracehook: called with a trace message |
| 385 | warnhook: called with a warning message |
| 386 | peers: mapping from names to Peer objects""" |
| 387 | def __init__(me, sockname): |
| 388 | """Initializes the monitor.""" |
| 389 | Connection.__init__(me, sockname) |
| 390 | me.addpeerhook = HookList() |
| 391 | me.delpeerhook = HookList() |
| 392 | me.tracehook = HookList() |
| 393 | me.warnhook = HookList() |
| 394 | me.notehook = HookList() |
| 395 | me.hook(me.connecthook, me.connected) |
| 396 | me.delay = [] |
| 397 | me.peers = {} |
| 398 | def addpeer(me, peer): |
| 399 | if peer not in me.peers: |
| 400 | p = Peer(me, peer) |
| 401 | me.peers[peer] = p |
| 402 | me.addpeerhook.run(p) |
| 403 | def delpeer(me, peer): |
| 404 | if peer in me.peers: |
| 405 | p = me.peers[peer] |
| 406 | me.delpeerhook.run(p) |
| 407 | p.dead() |
| 408 | del me.peers[peer] |
| 409 | def updatelist(me, peers): |
| 410 | newmap = {} |
| 411 | for p in peers: |
| 412 | newmap[p] = True |
| 413 | if p not in me.peers: |
| 414 | me.addpeer(p) |
| 415 | oldpeers = me.peers.copy() |
| 416 | for p in oldpeers: |
| 417 | if p not in newmap: |
| 418 | me.delpeer(p) |
| 419 | def connected(me): |
| 420 | try: |
| 421 | me.simplecmd('WATCH -A+wnt') |
| 422 | me.updatelist([s.strip() for s in me.simplecmd('LIST')]) |
| 423 | except ConnException: |
| 424 | me.disconnect() |
| 425 | return |
| 426 | def parseline(me, code, line): |
| 427 | ## Delay async messages until the current command is done. Otherwise the |
| 428 | ## handler for the async message might send another command before this |
| 429 | ## one's complete, and the whole edifice turns to jelly. |
| 430 | ## |
| 431 | ## No, this isn't the server's fault. If we rely on the server to delay |
| 432 | ## notifications then there's a race between when we send a command and |
| 433 | ## when the server gets it. |
| 434 | if me.waitingp and code in ('TRACE', 'WARN', 'NOTE'): |
| 435 | if len(me.delay) == 0: GO.idle_add(me.flushdelay) |
| 436 | me.delay.append((code, line)) |
| 437 | elif code == 'TRACE': |
| 438 | me.tracehook.run(line) |
| 439 | elif code == 'WARN': |
| 440 | me.warnhook.run(line) |
| 441 | elif code == 'NOTE': |
| 442 | note, line = getword(line) |
| 443 | me.notehook.run(note, line) |
| 444 | if note == 'ADD': |
| 445 | me.addpeer(getword(line)[0]) |
| 446 | elif note == 'KILL': |
| 447 | me.delpeer(line) |
| 448 | else: |
| 449 | ## Well, I asked for it. |
| 450 | pass |
| 451 | else: |
| 452 | return Connection.parseline(me, code, line) |
| 453 | return False, None |
| 454 | def flushdelay(me): |
| 455 | delay = me.delay |
| 456 | me.delay = [] |
| 457 | for tag, line in delay: |
| 458 | me.parseline(tag, line) |
| 459 | return False |
| 460 | |
| 461 | def parseinfo(info): |
| 462 | """Parse key=value output into a dictionary.""" |
| 463 | d = {} |
| 464 | for i in info: |
| 465 | for w in i.split(' '): |
| 466 | q = w.index('=') |
| 467 | d[w[:q]] = w[q + 1:] |
| 468 | return d |
| 469 | |
| 470 | class Peer (object): |
| 471 | """I represent a TrIPE peer. Useful attributes are: |
| 472 | |
| 473 | name: peer's name |
| 474 | addr: human-friendly representation of the peer's address |
| 475 | ifname: interface associated with the peer |
| 476 | alivep: true if the peer hasn't been killed |
| 477 | deadhook: called with no arguments when the peer is killed""" |
| 478 | def __init__(me, monitor, name): |
| 479 | me.mon = monitor |
| 480 | me.name = name |
| 481 | addr = me.mon.simplecmd('ADDR %s' % name)[0].split(' ') |
| 482 | if addr[0] == 'INET': |
| 483 | ipaddr, port = addr[1:] |
| 484 | try: |
| 485 | name = S.gethostbyaddr(ipaddr)[0] |
| 486 | me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr) |
| 487 | except S.herror: |
| 488 | me.addr = 'INET %s:%s' % (ipaddr, port) |
| 489 | else: |
| 490 | me.addr = ' '.join(addr) |
| 491 | me.ifname = me.mon.simplecmd('IFNAME %s' % me.name)[0] |
| 492 | me.__dict__.update(parseinfo(me.mon.simplecmd('PEERINFO %s' % me.name))) |
| 493 | me.deadhook = HookList() |
| 494 | me.alivep = True |
| 495 | def dead(me): |
| 496 | me.alivep = False |
| 497 | me.deadhook.run() |
| 498 | |
| 499 | #----- Window management cruft ---------------------------------------------- |
| 500 | |
| 501 | class MyWindowMixin (G.Window, HookClient): |
| 502 | """Mixin for windows which call a closehook when they're destroyed.""" |
| 503 | def mywininit(me): |
| 504 | me.closehook = HookList() |
| 505 | HookClient.__init__(me) |
| 506 | me.connect('destroy', invoker(me.close)) |
| 507 | def close(me): |
| 508 | me.closehook.run() |
| 509 | me.destroy() |
| 510 | me.unhookall() |
| 511 | class MyWindow (MyWindowMixin): |
| 512 | """A window which calls a closehook when it's destroyed.""" |
| 513 | def __init__(me, kind = G.WINDOW_TOPLEVEL): |
| 514 | G.Window.__init__(me, kind) |
| 515 | me.mywininit() |
| 516 | class MyDialog (G.Dialog, MyWindowMixin, HookClient): |
| 517 | """A dialogue box with a closehook and sensible button binding.""" |
| 518 | def __init__(me, title = None, flags = 0, buttons = []): |
| 519 | """The buttons are a list of (STOCKID, THUNK) pairs: call the appropriate |
| 520 | THUNK when the button is pressed. The others are just like GTK's Dialog |
| 521 | class.""" |
| 522 | i = 0 |
| 523 | br = [] |
| 524 | me.rmap = [] |
| 525 | for b, f in buttons: |
| 526 | br.append(b) |
| 527 | br.append(i) |
| 528 | me.rmap.append(f) |
| 529 | i += 1 |
| 530 | G.Dialog.__init__(me, title, None, flags, tuple(br)) |
| 531 | HookClient.__init__(me) |
| 532 | me.mywininit() |
| 533 | me.set_default_response(i - 1) |
| 534 | me.connect('response', me.respond) |
| 535 | def respond(me, hunoz, rid, *hukairz): |
| 536 | if rid >= 0: me.rmap[rid]() |
| 537 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 813 | #----- Peer window ---------------------------------------------------------- |
| 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)' |
| 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) |
| 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) |
| 918 | if ping.n: |
| 919 | s += ' (%.1f%%)' % (ping.ngood * 100.0/ping.n) |
| 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 | |
| 924 | #----- Add peer ------------------------------------------------------------- |
| 925 | |
| 926 | class AddPeerCommand (SimpleBackgroundCommand): |
| 927 | def __init__(me, conn, dlg, name, addr, port, |
| 928 | keepalive = None, tunnel = None): |
| 929 | me.name = name |
| 930 | me.addr = addr |
| 931 | me.port = port |
| 932 | me.keepalive = keepalive |
| 933 | me.tunnel = tunnel |
| 934 | cmd = StringIO() |
| 935 | cmd.write('ADD %s' % name) |
| 936 | cmd.write(' -background %s' % jobid()) |
| 937 | if keepalive is not None: cmd.write(' -keepalive %s' % keepalive) |
| 938 | if tunnel is not None: cmd.write(' -tunnel %s' % tunnel) |
| 939 | cmd.write(' INET %s %s' % (addr, port)) |
| 940 | SimpleBackgroundCommand.__init__(me, conn, cmd.getvalue()) |
| 941 | me.hook(me.donehook, invoker(dlg.destroy)) |
| 942 | def fail(me, err): |
| 943 | token, msg = getword(str(err)) |
| 944 | if token in ('resolve-error', 'resolver-timeout'): |
| 945 | moanbox("Unable to resolve hostname `%s'" % me.addr) |
| 946 | elif token == 'peer-create-fail': |
| 947 | moanbox("Couldn't create new peer `%s'" % me.name) |
| 948 | elif token == 'peer-exists': |
| 949 | moanbox("Peer `%s' already exists" % me.name) |
| 950 | else: |
| 951 | moanbox("Unexpected error from server command `ADD': %s" % err) |
| 952 | |
| 953 | class AddPeerDialog (MyDialog): |
| 954 | def __init__(me, monitor): |
| 955 | MyDialog.__init__(me, 'Add peer', |
| 956 | buttons = [(G.STOCK_CANCEL, me.destroy), |
| 957 | (G.STOCK_OK, me.ok)]) |
| 958 | me.mon = monitor |
| 959 | table = GridPacker() |
| 960 | me.vbox.pack_start(table) |
| 961 | me.e_name = table.labelled('Name', |
| 962 | ValidatingEntry(r'^[^\s.:]+$', '', 16), |
| 963 | width = 3) |
| 964 | me.e_addr = table.labelled('Address', |
| 965 | ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24), |
| 966 | newlinep = True) |
| 967 | me.e_port = table.labelled('Port', |
| 968 | ValidatingEntry(numericvalidate(0, 65535), |
| 969 | '22003', |
| 970 | 5)) |
| 971 | me.c_keepalive = G.CheckButton('Keepalives') |
| 972 | me.l_tunnel = table.labelled('Tunnel', |
| 973 | G.combo_box_new_text(), |
| 974 | newlinep = True, width = 3) |
| 975 | me.tuns = me.mon.simplecmd('TUNNELS') |
| 976 | for t in me.tuns: |
| 977 | me.l_tunnel.append_text(t) |
| 978 | me.l_tunnel.set_active(0) |
| 979 | table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL) |
| 980 | me.c_keepalive.connect('toggled', |
| 981 | lambda t: me.e_keepalive.set_sensitive\ |
| 982 | (t.get_active())) |
| 983 | me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5) |
| 984 | me.e_keepalive.set_sensitive(False) |
| 985 | table.pack(me.e_keepalive, width = 3) |
| 986 | me.show_all() |
| 987 | def ok(me): |
| 988 | try: |
| 989 | if me.c_keepalive.get_active(): |
| 990 | ka = me.e_keepalive.get_text() |
| 991 | else: |
| 992 | ka = None |
| 993 | t = me.l_tunnel.get_active() |
| 994 | if t == 0: |
| 995 | tun = None |
| 996 | else: |
| 997 | tun = me.tuns[t] |
| 998 | AddPeerCommand(me.mon, me, |
| 999 | me.e_name.get_text(), |
| 1000 | me.e_addr.get_text(), |
| 1001 | me.e_port.get_text(), |
| 1002 | keepalive = ka, |
| 1003 | tunnel = tun) |
| 1004 | except ValidationError: |
| 1005 | GDK.beep() |
| 1006 | return |
| 1007 | |
| 1008 | #----- The server monitor --------------------------------------------------- |
| 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() |
| 1048 | def cmd(c): return lambda: me.mon.simplecmd(c) |
| 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), |
| 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')), |
| 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"> |
| 1075 | <menuitem action="connect"/> |
| 1076 | <menuitem action="disconnect"/> |
| 1077 | <separator/> |
| 1078 | <menuitem action="server-version"/> |
| 1079 | <menuitem action="add-peer"/> |
| 1080 | <menuitem action="daemon"/> |
| 1081 | <menuitem action="reload-keys"/> |
| 1082 | <separator/> |
| 1083 | <menuitem action="server-quit"/> |
| 1084 | </menu> |
| 1085 | <menu action="logs-menu"> |
| 1086 | <menuitem action="show-warnings"/> |
| 1087 | <menuitem action="show-trace"/> |
| 1088 | <menuitem action="trace-options"/> |
| 1089 | </menu> |
| 1090 | <menu action="help-menu"> |
| 1091 | <menuitem action="about"/> |
| 1092 | </menu> |
| 1093 | </menubar> |
| 1094 | <popup name="peer-popup"> |
| 1095 | <menuitem action="add-peer"/> |
| 1096 | <menuitem action="kill-peer"/> |
| 1097 | <menuitem action="force-kx"/> |
| 1098 | </popup> |
| 1099 | </ui> |
| 1100 | ''' |
| 1101 | me.ui.insert_action_group(actgroup, 0) |
| 1102 | me.ui.add_ui_from_string(uidef) |
| 1103 | vbox.pack_start(me.ui.get_widget('/menubar'), expand = False) |
| 1104 | me.add_accel_group(me.ui.get_accel_group()) |
| 1105 | me.status = G.Statusbar() |
| 1106 | |
| 1107 | me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6) |
| 1108 | me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING) |
| 1109 | me.hook(me.mon.addpeerhook, me.addpeer) |
| 1110 | me.hook(me.mon.delpeerhook, me.delpeer) |
| 1111 | |
| 1112 | scr = G.ScrolledWindow() |
| 1113 | scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC) |
| 1114 | me.list = G.TreeView(me.listmodel) |
| 1115 | me.list.append_column(G.TreeViewColumn('Peer name', |
| 1116 | G.CellRendererText(), |
| 1117 | text = 0)) |
| 1118 | me.list.append_column(G.TreeViewColumn('Address', |
| 1119 | G.CellRendererText(), |
| 1120 | text = 1)) |
| 1121 | me.list.append_column(G.TreeViewColumn('T-ping', |
| 1122 | G.CellRendererText(), |
| 1123 | text = 2, |
| 1124 | foreground = 3)) |
| 1125 | me.list.append_column(G.TreeViewColumn('E-ping', |
| 1126 | G.CellRendererText(), |
| 1127 | text = 4, |
| 1128 | foreground = 5)) |
| 1129 | me.list.get_column(1).set_expand(True) |
| 1130 | me.list.connect('row-activated', me.activate) |
| 1131 | me.list.connect('button-press-event', me.buttonpress) |
| 1132 | me.list.set_reorderable(True) |
| 1133 | me.list.get_selection().set_mode(G.SELECTION_NONE) |
| 1134 | scr.add(me.list) |
| 1135 | vbox.pack_start(scr) |
| 1136 | |
| 1137 | vbox.pack_start(me.status, expand = False) |
| 1138 | me.hook(me.mon.connecthook, me.connected) |
| 1139 | me.hook(me.mon.disconnecthook, me.disconnected) |
| 1140 | me.hook(me.mon.notehook, me.notify) |
| 1141 | me.pinger = None |
| 1142 | me.set_default_size(420, 180) |
| 1143 | me.mon.connect() |
| 1144 | me.show_all() |
| 1145 | |
| 1146 | def addpeer(me, peer): |
| 1147 | peer.i = me.listmodel.append([peer.name, peer.addr, |
| 1148 | '???', 'green', '???', 'green']) |
| 1149 | peer.win = WindowSlot(lambda: PeerWindow(me.mon, peer)) |
| 1150 | peer.pinghook = HookList() |
| 1151 | peer.ping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0, |
| 1152 | tlast = 0, ttot = 0, |
| 1153 | tcol = 2, ccol = 3, cmd = 'Transport pings') |
| 1154 | peer.eping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0, |
| 1155 | tlast = 0, ttot = 0, |
| 1156 | tcol = 4, ccol = 5, cmd = 'Encrypted pings') |
| 1157 | def delpeer(me, peer): |
| 1158 | me.listmodel.remove(peer.i) |
| 1159 | def path_peer(me, path): |
| 1160 | return me.mon.peers[me.listmodel[path][0]] |
| 1161 | |
| 1162 | def activate(me, l, path, col): |
| 1163 | peer = me.path_peer(path) |
| 1164 | peer.win.open() |
| 1165 | def buttonpress(me, l, ev): |
| 1166 | if ev.button == 3: |
| 1167 | r = me.list.get_path_at_pos(ev.x, ev.y) |
| 1168 | for i in '/peer-popup/kill-peer', '/peer-popup/force-kx': |
| 1169 | me.ui.get_widget(i).set_sensitive(me.mon.connectedp and |
| 1170 | r is not None) |
| 1171 | if r: |
| 1172 | me.menupeer = me.path_peer(r[0]) |
| 1173 | else: |
| 1174 | me.menupeer = None |
| 1175 | me.ui.get_widget('/peer-popup').popup(None, None, None, |
| 1176 | ev.button, ev.time) |
| 1177 | |
| 1178 | def killpeer(me): |
| 1179 | me.mon.simplecmd('KILL %s' % me.menupeer.name) |
| 1180 | def forcekx(me): |
| 1181 | me.mon.simplecmd('FORCEKX %s' % me.menupeer.name) |
| 1182 | |
| 1183 | def reping(me): |
| 1184 | if me.pinger is not None: |
| 1185 | GO.source_remove(me.pinger) |
| 1186 | me.pinger = GO.timeout_add(10000, me.ping) |
| 1187 | me.ping() |
| 1188 | def unping(me): |
| 1189 | if me.pinger is not None: |
| 1190 | GO.source_remove(me.pinger) |
| 1191 | me.pinger = None |
| 1192 | def ping(me): |
| 1193 | for name in me.mon.peers: |
| 1194 | p = me.mon.peers[name] |
| 1195 | PingCommand(me.mon, 'PING', p, lambda p, t: me.pong(p, p.ping, t)) |
| 1196 | PingCommand(me.mon, 'EPING', p, lambda p, t: me.pong(p, p.eping, t)) |
| 1197 | return True |
| 1198 | def pong(me, p, ping, t): |
| 1199 | ping.n += 1 |
| 1200 | if t is None: |
| 1201 | ping.nmiss += 1 |
| 1202 | ping.nmissrun += 1 |
| 1203 | me.listmodel[p.i][ping.tcol] = '(miss %d)' % ping.nmissrun |
| 1204 | me.listmodel[p.i][ping.ccol] = 'red' |
| 1205 | else: |
| 1206 | ping.ngood += 1 |
| 1207 | ping.nmissrun = 0 |
| 1208 | ping.tlast = t |
| 1209 | ping.ttot += t |
| 1210 | me.listmodel[p.i][ping.tcol] = '%.1f ms' % t |
| 1211 | me.listmodel[p.i][ping.ccol] = 'black' |
| 1212 | p.pinghook.run() |
| 1213 | def setstatus(me, status): |
| 1214 | me.status.pop(0) |
| 1215 | me.status.push(0, status) |
| 1216 | def notify(me, note, rest): |
| 1217 | if note == 'DAEMON': |
| 1218 | me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False) |
| 1219 | def connected(me): |
| 1220 | me.setstatus('Connected (port %s)' % me.mon.simplecmd('PORT')[0]) |
| 1221 | me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False) |
| 1222 | for i in ('/menubar/server-menu/disconnect', |
| 1223 | '/menubar/server-menu/server-version', |
| 1224 | '/menubar/server-menu/add-peer', |
| 1225 | '/menubar/server-menu/server-quit', |
| 1226 | '/menubar/logs-menu/trace-options'): |
| 1227 | me.ui.get_widget(i).set_sensitive(True) |
| 1228 | me.ui.get_widget('/menubar/server-menu/daemon'). \ |
| 1229 | set_sensitive(parseinfo(me.mon.simplecmd('SERVINFO'))['daemon'] == |
| 1230 | 'nil') |
| 1231 | me.reping() |
| 1232 | def disconnected(me): |
| 1233 | me.setstatus('Disconnected') |
| 1234 | me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True) |
| 1235 | for i in ('/menubar/server-menu/disconnect', |
| 1236 | '/menubar/server-menu/server-version', |
| 1237 | '/menubar/server-menu/add-peer', |
| 1238 | '/menubar/server-menu/daemon', |
| 1239 | '/menubar/server-menu/server-quit', |
| 1240 | '/menubar/logs-menu/trace-options'): |
| 1241 | me.ui.get_widget(i).set_sensitive(False) |
| 1242 | me.unping() |
| 1243 | def destroy(me): |
| 1244 | if me.pinger is not None: |
| 1245 | GO.source_remove(me.pinger) |
| 1246 | def report(me, msg): |
| 1247 | moanbox(msg) |
| 1248 | return True |
| 1249 | |
| 1250 | #----- Parse options -------------------------------------------------------- |
| 1251 | |
| 1252 | def version(fp = stdout): |
| 1253 | """Print the program's version number.""" |
| 1254 | fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION)) |
| 1255 | |
| 1256 | def usage(fp): |
| 1257 | """Print a brief usage message for the program.""" |
| 1258 | fp.write('Usage: %s [-d DIR] [-a SOCK]\n' % quis) |
| 1259 | |
| 1260 | def main(): |
| 1261 | global tripedir |
| 1262 | if 'TRIPEDIR' in environ: |
| 1263 | tripedir = environ['TRIPEDIR'] |
| 1264 | tripesock = '%s/%s' % (socketdir, 'tripesock') |
| 1265 | |
| 1266 | try: |
| 1267 | opts, args = O.getopt(argv[1:], |
| 1268 | 'hvud:a:', |
| 1269 | ['help', 'version', 'usage', |
| 1270 | 'directory=', 'admin-socket=']) |
| 1271 | except O.GetoptError, exc: |
| 1272 | moan(exc) |
| 1273 | usage(stderr) |
| 1274 | exit(1) |
| 1275 | for o, v in opts: |
| 1276 | if o in ('-h', '--help'): |
| 1277 | version(stdout) |
| 1278 | print |
| 1279 | usage(stdout) |
| 1280 | print """ |
| 1281 | Graphical monitor for TrIPE VPN. |
| 1282 | |
| 1283 | Options supported: |
| 1284 | |
| 1285 | -h, --help Show this help message. |
| 1286 | -v, --version Show the version number. |
| 1287 | -u, --usage Show pointlessly short usage string. |
| 1288 | |
| 1289 | -d, --directory=DIR Use TrIPE directory DIR. |
| 1290 | -a, --admin-socket=FILE Select socket to connect to.""" |
| 1291 | exit(0) |
| 1292 | elif o in ('-v', '--version'): |
| 1293 | version(stdout) |
| 1294 | exit(0) |
| 1295 | elif o in ('-u', '--usage'): |
| 1296 | usage(stdout) |
| 1297 | exit(0) |
| 1298 | elif o in ('-d', '--directory'): |
| 1299 | tripedir = v |
| 1300 | elif o in ('-a', '--admin-socket'): |
| 1301 | tripesock = v |
| 1302 | else: |
| 1303 | raise "can't happen!" |
| 1304 | if len(args) > 0: |
| 1305 | usage(stderr) |
| 1306 | exit(1) |
| 1307 | |
| 1308 | OS.chdir(tripedir) |
| 1309 | mon = Monitor(tripesock) |
| 1310 | root = MonitorWindow(mon) |
| 1311 | HookClient().hook(root.closehook, exit) |
| 1312 | G.main() |
| 1313 | |
| 1314 | if __name__ == '__main__': |
| 1315 | main() |
| 1316 | |
| 1317 | #----- That's all, folks ---------------------------------------------------- |