From 8f7961064a2632ecab261962db1dcda839a6b637 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 22 Aug 2012 15:46:23 +0200 Subject: [PATCH 1/3] Add debug proxy helper. Tool sits in front of MPD and Mopidy proxying commands to both. Only the reference backend's replies are passed to the client. All requests are logged, but only the response's unified diff is displayed. Intended use case is quick and simple protocol implementation comparisons. --- tools/debug-proxy.py | 177 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100755 tools/debug-proxy.py diff --git a/tools/debug-proxy.py b/tools/debug-proxy.py new file mode 100755 index 00000000..cf84bd54 --- /dev/null +++ b/tools/debug-proxy.py @@ -0,0 +1,177 @@ +#!/usr/bin/python + +import argparse +import difflib +import sys + +from gevent import select +from gevent import server +from gevent import socket + + +def proxy(client, address, reference_address, actual_address): + """Main handler code that gets called for each connection.""" + client.setblocking(False) + + reference = connect(reference_address) + actual = connect(actual_address) + + if reference and actual: + loop(client, address, reference, actual) + else: + print 'Could not connect to one of the backends.' + + for sock in (client, reference, actual): + close(sock) + + +def connect(address): + """Connect to given address and set socket non blocking.""" + try: + sock = socket.socket() + sock.connect(address) + sock.setblocking(False) + except socket.error: + return None + return sock + + +def close(sock): + """Shutdown and close our sockets.""" + try: + sock.shutdown(socket.SHUT_WR) + sock.close() + except socket.error: + pass + + +def loop(client, address, reference, actual): + """Loop that handles one MPD reqeust/response pair per iteration.""" + + # Consume banners from backends + responses = dict() + disconnected = read([reference, actual], responses, find_response_end_token) + diff(address, '', responses[reference], responses[actual]) + + # We lost the a backend, might as well give up. + if disconnected: + return + + client.sendall(responses[reference]) + + while True: + responses = dict() + + # Get the command from the client. Not sure how an if this will handle + # client sending multiple commands currently :/ + disconnected = read([client], responses, find_request_end_token) + + # We lost the client, might as well give up. + if disconnected: + return + + # Send the entire command to both backends. + reference.sendall(responses[client]) + actual.sendall(responses[client]) + + # Get the entire resonse from both backends. + disconnected = read([reference, actual], responses, find_response_end_token) + + # Send the client the complete reference response + client.sendall(responses[reference]) + + # Compare our responses + diff(address, responses[client], responses[reference], responses[actual]) + + # Give up if we lost a backend. + if disconnected: + return + + +def read(sockets, responses, find_end_token): + """Keep reading from sockets until they disconnet or we find our token.""" + + # This function doesn't go to well with idle when backends are out of sync. + disconnected = False + + for sock in sockets: + responses.setdefault(sock, '') + + while sockets: + for sock in select.select(sockets, [], [])[0]: + data = sock.recv(4096) + responses[sock] += data + + if find_end_token(responses[sock]): + sockets.remove(sock) + + if not data: + sockets.remove(sock) + disconnected = True + + return disconnected + + +def find_response_end_token(data): + """Find token that indicates the response is over.""" + for line in data.splitlines(True): + if line.startswith(('OK', 'ACK')) and line.endswith('\n'): + return True + return False + + +def find_request_end_token(data): + """Find token that indicates that request is over.""" + lines = data.splitlines(True) + if not lines: + return False + elif 'command_list_ok_begin' == lines[0].strip(): + return 'command_list_end' == lines[-1].strip() + else: + return lines[0].endswith('\n') + + +def diff(address, command, reference_response, actual_response): + """Print command from client and a unified diff of the responses.""" + sys.stdout.write('[%s]:%s\n%s' % (address[0], address[1], command)) + for line in difflib.unified_diff(reference_response.splitlines(True), + actual_response.splitlines(True), + fromfile='Reference response', + tofile='Actual response'): + sys.stdout.write(line) + sys.stdout.flush() + + +def parse_args(): + """Handle flag parsing.""" + parser = argparse.ArgumentParser( + description='Proxy and compare MPD protocol interactions.') + parser.add_argument('--listen', default=':6600', type=parse_address, + help='address:port to listen on.') + parser.add_argument('--reference', default=':6601', type=parse_address, + help='address:port for the reference backend.') + parser.add_argument('--actual', default=':6602', type=parse_address, + help='address:port for the actual backend.') + + return parser.parse_args() + + +def parse_address(address): + """Convert host:port or port to address to pass to connect.""" + if ':' not in address: + return ('', int(address)) + host, port = address.rsplit(':', 1) + return (host, int(port)) + + +if __name__ == '__main__': + args = parse_args() + + def handle(client, address): + """Wrapper that adds reference and actual backends to proxy calls.""" + return proxy(client, address, args.reference, args.actual) + + try: + server.StreamServer(args.listen, handle).serve_forever() + except (KeyboardInterrupt, SystemExit): + pass From 4ff5c2e992aea195a80c2d9c83449e9dc5d88094 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 22 Aug 2012 23:16:03 +0200 Subject: [PATCH 2/3] Add color to console output and fix some things from review. --- tools/debug-proxy.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tools/debug-proxy.py b/tools/debug-proxy.py index cf84bd54..3ff6f561 100755 --- a/tools/debug-proxy.py +++ b/tools/debug-proxy.py @@ -4,9 +4,12 @@ import argparse import difflib import sys -from gevent import select -from gevent import server -from gevent import socket +from gevent import select, server, socket + +COLORS = ['\033[1;%dm' % (30+i) for i in range(8)] +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = COLORS +RESET = "\033[0m" +BOLD = "\033[1m" def proxy(client, address, reference_address, actual_address): @@ -53,7 +56,7 @@ def loop(client, address, reference, actual): disconnected = read([reference, actual], responses, find_response_end_token) diff(address, '', responses[reference], responses[actual]) - # We lost the a backend, might as well give up. + # We lost a backend, might as well give up. if disconnected: return @@ -138,7 +141,17 @@ def diff(address, command, reference_response, actual_response): actual_response.splitlines(True), fromfile='Reference response', tofile='Actual response'): + + if line.startswith('+') and not line.startswith('+++'): + sys.stdout.write(GREEN) + elif line.startswith('-') and not line.startswith('---'): + sys.stdout.write(RED) + elif line.startswith('@@'): + sys.stdout.write(CYAN) + sys.stdout.write(line) + sys.stdout.write(RESET) + sys.stdout.flush() From 1649abc410aa8c6b67cded667814ae664fc08666 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 22 Aug 2012 23:42:57 +0200 Subject: [PATCH 3/3] Add debug-proxy to 0.8 changelog. --- docs/changes.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index a4aae058..a2a45960 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,17 @@ Changes This change log is used to track all major changes to Mopidy. +v0.8 (in development) +===================== + +**Changes** + +- Added tools/debug-proxy.py to tee client requests to two backends and diff + responses. Intended as a developer tool for checking for MPD protocol changes + and various client support. Requires gevent, which currently is not a + dependency of Mopidy. + + v0.7.3 (2012-08-11) ===================