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.
This commit is contained in:
parent
ce60030fe5
commit
8f7961064a
177
tools/debug-proxy.py
Executable file
177
tools/debug-proxy.py
Executable file
@ -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
|
||||
Loading…
Reference in New Issue
Block a user