From ce5aceb7d9fc0e0b6cc4347658266e5bebd3d162 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Fri, 10 Aug 2012 19:08:16 -0400 Subject: [PATCH 001/191] Improved exception handling in heartbeatThread and namespaceThread --- CHANGES.rst | 6 ++ README.rst | 42 ++++++++++---- TODO.rst | 1 - setup.cfg | 5 ++ socketIO_client/__init__.py | 106 +++++++++++++++++++++--------------- socketIO_client/tests.py | 7 ++- 6 files changed, 110 insertions(+), 57 deletions(-) create mode 100644 setup.cfg diff --git a/CHANGES.rst b/CHANGES.rst index 94e8c72..8afe01b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +0.3 +--- +- Added support for secure connections +- Added socketIO.wait() +- Improved exception handling in heartbeatThread and namespaceThread + 0.2 --- - Added support for callbacks and channels thanks to Paul Kienzle diff --git a/README.rst b/README.rst index 853cf00..bba2b1d 100644 --- a/README.rst +++ b/README.rst @@ -38,27 +38,29 @@ Emit. :: socketIO = SocketIO('localhost', 8000) socketIO.emit('aaa', {'bbb': 'ccc'}) + socketIO.wait(seconds=1) Emit with callback. :: from socketIO_client import SocketIO - def on_response(arg1, arg2, arg3, arg4): - print arg1, arg2, arg3, arg4 + def on_response(*args): + print args socketIO = SocketIO('localhost', 8000) socketIO.emit('aaa', {'bbb': 'ccc'}, on_response) - socketIO.wait() + socketIO.wait(forCallbacks=True) Define events. :: from socketIO_client import SocketIO - def on_ddd(arg1, arg2, arg3, arg4): - print arg1, arg2, arg3, arg4 + def on_ddd(*args): + print args socketIO = SocketIO('localhost', 8000) socketIO.on('ddd', on_ddd) + socketIO.wait() Define events in a namespace. :: @@ -66,10 +68,11 @@ Define events in a namespace. :: class Namespace(BaseNamespace): - def on_ddd(self, arg1, arg2): - self.socketIO.emit('eee', {'fff': arg1 + arg2}) + def on_ddd(self, *args): + self.socketIO.emit('eee', {'fff': 'ggg'}) socketIO = SocketIO('localhost', 8000, Namespace) + socketIO.wait() Define standard events. :: @@ -90,12 +93,31 @@ Define standard events. :: print '[Message] %s: %s' % (id, message) socketIO = SocketIO('localhost', 8000, Namespace) + socketIO.wait() Define different behavior for different channels on a single socket. :: - mainSocket = SocketIO('localhost', 8000, MainNamespace()) - chatSocket = mainSocket.connect('/chat', ChatNamespace()) - newsSocket = mainSocket.connect('/news', NewsNamespace()) + from socketIO_client import SocketIO, BaseNamespace + + class MainNamespace(BaseNamespace): + + def on_aaa(self, *args): + print 'aaa', args + + class ChatNamespace(BaseNamespace): + + def on_bbb(self, *args): + print 'bbb', args + + class NewsNamespace(BaseNamespace): + + def on_ccc(self, *args): + print 'ccc', args + + mainSocket = SocketIO('localhost', 8000, MainNamespace) + chatSocket = mainSocket.connect('/chat', ChatNamespace) + newsSocket = mainSocket.connect('/news', NewsNamespace) + mainSocket.wait() License diff --git a/TODO.rst b/TODO.rst index f8bee38..e69de29 100644 --- a/TODO.rst +++ b/TODO.rst @@ -1 +0,0 @@ -- Consider enabling multiple callbacks for a single event diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8a2014f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[nosetests] +detailed-errors=TRUE +with-coverage=TRUE +cover-package=socketIO_client +cover-erase=TRUE diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index a3313b0..e32996d 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,16 +1,17 @@ import websocket from anyjson import dumps, loads from threading import Thread, Event +from time import sleep from urllib import urlopen -__version__ = '0.2' +__version__ = '0.3' PROTOCOL = 1 # SocketIO protocol version -class BaseNamespace(object): +class BaseNamespace(object): # pragma: no cover def __init__(self, socketIO): self.socketIO = socketIO @@ -47,10 +48,11 @@ class SocketIO(object): messageID = 0 - def __init__(self, host, port, Namespace=BaseNamespace): + def __init__(self, host, port, Namespace=BaseNamespace, secure=False): self.host = host self.port = int(port) self.namespace = Namespace(self) + self.secure = secure self.__connect() heartbeatInterval = self.heartbeatTimeout - 2 @@ -63,7 +65,7 @@ class SocketIO(object): self.namespaceThread = ListenerThread(self) self.namespaceThread.start() - def __del__(self): + def __del__(self): # pragma: no cover self.heartbeatThread.cancel() self.namespaceThread.cancel() self.connection.close() @@ -71,10 +73,11 @@ class SocketIO(object): def __connect(self): baseURL = '%s:%d/socket.io/%s' % (self.host, self.port, PROTOCOL) try: - response = urlopen('http://%s/' % baseURL) - except IOError: + response = urlopen('%s://%s/' % ( + 'https' if self.secure else 'http', baseURL)) + except IOError: # pragma: no cover raise SocketIOError('Could not start connection') - if 200 != response.getcode(): + if 200 != response.getcode(): # pragma: no cover raise SocketIOError('Could not establish connection') responseParts = response.readline().split(':') self.sessionID = responseParts[0] @@ -82,26 +85,22 @@ class SocketIO(object): self.connectionTimeout = int(responseParts[2]) self.supportedTransports = responseParts[3].split(',') if 'websocket' not in self.supportedTransports: - raise SocketIOError('Could not parse handshake') - socketURL = 'ws://%s/websocket/%s' % (baseURL, self.sessionID) + raise SocketIOError('Could not parse handshake') # pragma: no cover + socketURL = '%s://%s/websocket/%s' % ( + 'wss' if self.secure else 'ws', baseURL, self.sessionID) self.connection = websocket.create_connection(socketURL) def _recv_packet(self): - packetID, channelName, data = None, None, None - try: - packet = self.connection.recv() - packetParts = packet.split(':', 3) - except (websocket.WebSocketException, AttributeError): - return 0, packetID, channelName, data + code, packetID, channelName, data = -1, None, None, None + packet = self.connection.recv() + packetParts = packet.split(':', 3) packetCount = len(packetParts) if 4 == packetCount: code, packetID, channelName, data = packetParts elif 3 == packetCount: code, packetID, channelName = packetParts - elif 1 == packetCount: + elif 1 == packetCount: # pragma: no cover code = packetParts[0] - else: - raise ValueError('Could not parse packet:\n' + packet) return int(code), packetID, channelName, data def _send_packet(self, code, channelName='', data='', callback=None): @@ -115,6 +114,8 @@ class SocketIO(object): self._send_packet(0, channelName) if channelName: del self.channelByName[channelName] + else: + self.__del__() @property def connected(self): @@ -129,8 +130,8 @@ class SocketIO(object): def _send_heartbeat(self): try: self._send_packet(2) - except TypeError: - pass + except: + self.__del__() def message(self, messageData, callback=None, channelName=''): if isinstance(messageData, basestring): @@ -174,8 +175,17 @@ class SocketIO(object): def on(self, eventName, callback): self.callbackByEvent[eventName] = callback - def wait(self): - self.namespaceThread.wait() + def wait(self, seconds=None, forCallbacks=False): + if forCallbacks: + self.namespaceThread.wait_for_callbacks(seconds) + elif seconds: + sleep(seconds) + else: + try: + while self.connected: + sleep(1) + except KeyboardInterrupt: + pass class Channel(object): @@ -210,31 +220,37 @@ class ListenerThread(Thread): super(ListenerThread, self).__init__() self.socketIO = socketIO self.done = Event() - self.waiting = Event() + self.waitingForCallbacks = Event() self.callbackByMessageID = {} self.get_callback = self.socketIO.get_callback def run(self): while not self.done.is_set(): - code, packetID, channelName, data = self.socketIO._recv_packet() - delegate = { - 0: self.on_disconnect, - 1: self.on_connect, - 2: self.on_heartbeat, - 3: self.on_message, - 4: self.on_json, - 5: self.on_event, - 6: self.on_acknowledgment, - 7: self.on_error, - }[code] + try: + code, packetID, channelName, data = self.socketIO._recv_packet() + except: + continue + try: + delegate = { + 0: self.on_disconnect, + 1: self.on_connect, + 2: self.on_heartbeat, + 3: self.on_message, + 4: self.on_json, + 5: self.on_event, + 6: self.on_acknowledgment, + 7: self.on_error, + }[code] + except KeyError: + continue delegate(packetID, channelName, data) def cancel(self): self.done.set() - def wait(self): - self.waiting.set() - self.join() + def wait_for_callbacks(self, seconds): + self.waitingForCallbacks.set() + self.join(seconds) def set_callback(self, messageID, callback): self.callbackByMessageID[messageID] = callback @@ -268,9 +284,6 @@ class ListenerThread(Thread): def on_acknowledgment(self, packetID, channelName, data): dataParts = data.split('+', 1) messageID = int(dataParts[0]) - print data - print dataParts - print dataParts[1] arguments = loads(dataParts[1]) or [] try: callback = self.callbackByMessageID[messageID] @@ -279,7 +292,8 @@ class ListenerThread(Thread): else: del self.callbackByMessageID[messageID] callback(*arguments) - if self.waiting.is_set() and not len(self.callbackByMessageID): + callbackCount = len(self.callbackByMessageID) + if self.waitingForCallbacks.is_set() and not callbackCount: self.cancel() def on_error(self, packetID, channelName, data): @@ -302,10 +316,12 @@ class RhythmicThread(Thread): self.done = Event() def run(self): - self.done.wait(self.intervalInSeconds) - while not self.done.is_set(): - self.rhythmicFunction(*self.args, **self.kw) - self.done.wait(self.intervalInSeconds) + try: + while not self.done.is_set(): + self.rhythmicFunction(*self.args, **self.kw) + self.done.wait(self.intervalInSeconds) + except: + pass def cancel(self): self.done.set() diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index 241d37e..a85c48e 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -9,6 +9,11 @@ ON_RESPONSE_CALLED = False class TestSocketIO(TestCase): + def test_disconnect(self): + socketIO = SocketIO('localhost', 8000) + socketIO.disconnect() + self.assertEqual(socketIO.connected, False) + def test_emit(self): socketIO = SocketIO('localhost', 8000, Namespace) socketIO.emit('aaa', PAYLOAD) @@ -20,7 +25,7 @@ class TestSocketIO(TestCase): ON_RESPONSE_CALLED = False socketIO = SocketIO('localhost', 8000) socketIO.emit('aaa', PAYLOAD, on_response) - socketIO.wait() + socketIO.wait(forCallbacks=True) self.assertEqual(ON_RESPONSE_CALLED, True) def test_events(self): From ad01b9b89ed451bad7194e2024e4fbc33db70cf6 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 12 Aug 2012 08:39:39 -0400 Subject: [PATCH 002/191] It is safer to specify version manually in setup.py --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 1938d55..2651b7a 100755 --- a/setup.py +++ b/setup.py @@ -1,8 +1,6 @@ import os from setuptools import setup, find_packages -from socketIO_client import __version__ - here = os.path.abspath(os.path.dirname(__file__)) README = open(os.path.join(here, 'README.rst')).read() @@ -11,7 +9,7 @@ CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( name='socketIO-client', - version=__version__, + version='0.3', description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', From ecae8b0288d74ffe16466a47c7f4f1dddf36d0b4 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 7 Jan 2013 16:46:12 -0500 Subject: [PATCH 003/191] Updated TODO --- README.rst | 17 +++++++++++------ TODO.rst | 7 +++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index bba2b1d..5ed4179 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ Emit. :: socketIO = SocketIO('localhost', 8000) socketIO.emit('aaa', {'bbb': 'ccc'}) - socketIO.wait(seconds=1) + socketIO.wait(seconds=1) # Exit after one second Emit with callback. :: @@ -49,7 +49,7 @@ Emit with callback. :: socketIO = SocketIO('localhost', 8000) socketIO.emit('aaa', {'bbb': 'ccc'}, on_response) - socketIO.wait(forCallbacks=True) + socketIO.wait(forCallbacks=True) # Exit after callbacks run Define events. :: @@ -60,7 +60,7 @@ Define events. :: socketIO = SocketIO('localhost', 8000) socketIO.on('ddd', on_ddd) - socketIO.wait() + socketIO.wait() # Loop until CTRL-C Define events in a namespace. :: @@ -72,7 +72,7 @@ Define events in a namespace. :: self.socketIO.emit('eee', {'fff': 'ggg'}) socketIO = SocketIO('localhost', 8000, Namespace) - socketIO.wait() + socketIO.wait() # Loop until CTRL-C Define standard events. :: @@ -93,7 +93,7 @@ Define standard events. :: print '[Message] %s: %s' % (id, message) socketIO = SocketIO('localhost', 8000, Namespace) - socketIO.wait() + socketIO.wait() # Loop until CTRL-C Define different behavior for different channels on a single socket. :: @@ -117,8 +117,13 @@ Define different behavior for different channels on a single socket. :: mainSocket = SocketIO('localhost', 8000, MainNamespace) chatSocket = mainSocket.connect('/chat', ChatNamespace) newsSocket = mainSocket.connect('/news', NewsNamespace) - mainSocket.wait() + mainSocket.wait() # Loop until CTRL-C +Open secure websockets (HTTPS / WSS) behind a proxy. :: + + SocketIO('localhost', 8000, + secure=True, + proxies={'http': 'http://proxy.example.com:8080'}) License ------- diff --git a/TODO.rst b/TODO.rst index e69de29..e3e5408 100644 --- a/TODO.rst +++ b/TODO.rst @@ -0,0 +1,7 @@ +Let user define a proxy #5 +Let user emit without arguments #5 +Review forks + Integrate Zac's fork #6 + Integrate Sajal's fork #7 + Integrate Francis's fork #10 + Investigate #8 From 6f12132e88c60a81fdfa53c26c6e7e5e088e8cbe Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 7 Jan 2013 17:14:15 -0500 Subject: [PATCH 004/191] Updated setup.py --- TODO.rst | 10 +++++----- setup.py | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/TODO.rst b/TODO.rst index e3e5408..bf51bc3 100644 --- a/TODO.rst +++ b/TODO.rst @@ -1,7 +1,7 @@ Let user define a proxy #5 Let user emit without arguments #5 -Review forks - Integrate Zac's fork #6 - Integrate Sajal's fork #7 - Integrate Francis's fork #10 - Investigate #8 +Integrate Zac's fork #6 +Integrate Sajal's fork #7 +Integrate Francis's fork #10 +Investigate issue #8 +Integrate Paul's fork diff --git a/setup.py b/setup.py index 2651b7a..0ca72de 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( name='socketIO-client', - version='0.3', + version='0.4', description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', @@ -24,6 +24,7 @@ setup( url='https://github.com/invisibleroads/socketIO-client', install_requires=[ 'anyjson', + 'gevent-socketio', 'websocket-client', ], packages=find_packages(), From 0a5b069cddfbfcaf37970f3991b226a6653483fb Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sat, 9 Feb 2013 19:12:21 -0800 Subject: [PATCH 005/191] Added test to check that child threads die when parent dies --- CHANGES.rst | 3 + README.rst | 13 +- TODO.rst | 7 +- serve_tests.py | 17 ++- setup.py | 1 - socketIO_client/__init__.py | 263 ++++++++++++++++++++---------------- socketIO_client/tests.py | 26 +++- 7 files changed, 193 insertions(+), 137 deletions(-) mode change 100755 => 100644 serve_tests.py mode change 100755 => 100644 setup.py diff --git a/CHANGES.rst b/CHANGES.rst index 8afe01b..1e5d0f7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,6 @@ +0.4 +--- + 0.3 --- - Added support for secure connections diff --git a/README.rst b/README.rst index 5ed4179..504e0d5 100644 --- a/README.rst +++ b/README.rst @@ -35,10 +35,9 @@ Activate isolated environment. :: Emit. :: from socketIO_client import SocketIO - - socketIO = SocketIO('localhost', 8000) - socketIO.emit('aaa', {'bbb': 'ccc'}) - socketIO.wait(seconds=1) # Exit after one second + with SocketIO('localhost', 8000) as socketIO: + socketIO.emit('aaa') + socketIO.wait(1) # Wait a second Emit with callback. :: @@ -47,9 +46,9 @@ Emit with callback. :: def on_response(*args): print args - socketIO = SocketIO('localhost', 8000) - socketIO.emit('aaa', {'bbb': 'ccc'}, on_response) - socketIO.wait(forCallbacks=True) # Exit after callbacks run + with SocketIO('localhost', 8000) as socketIO: + socketIO.emit('aaa', {'bbb': 'ccc'}, on_response) + socketIO.wait(seconds=1, forCallbacks=True) # Wait for callback Define events. :: diff --git a/TODO.rst b/TODO.rst index bf51bc3..e362fcd 100644 --- a/TODO.rst +++ b/TODO.rst @@ -1,5 +1,8 @@ -Let user define a proxy #5 -Let user emit without arguments #5 ++ Fix unittests ++ Fix exceptions when websocket server disappears + +Fix thread exceptions + Integrate Zac's fork #6 Integrate Sajal's fork #7 Integrate Francis's fork #10 diff --git a/serve_tests.py b/serve_tests.py old mode 100755 new mode 100644 index 47c946a..faea52e --- a/serve_tests.py +++ b/serve_tests.py @@ -1,7 +1,14 @@ 'Launch this server in another terminal window before running tests' -from socketio import socketio_manage -from socketio.namespace import BaseNamespace -from socketio.server import SocketIOServer +import sys +try: + from socketio import socketio_manage + from socketio.namespace import BaseNamespace + from socketio.server import SocketIOServer +except ImportError: + from setuptools.command import easy_install + easy_install.main(['-U', 'gevent-socketio']) + print('\nPlease run the script again to launch the test server.') + sys.exit(1) class Namespace(BaseNamespace): @@ -25,5 +32,7 @@ class Application(object): if __name__ == '__main__': - socketIOServer = SocketIOServer(('0.0.0.0', 8000), Application()) + port = 8000 + print 'Starting server at port %s' % port + socketIOServer = SocketIOServer(('0.0.0.0', port), Application()) socketIOServer.serve_forever() diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 0ca72de..0f79923 --- a/setup.py +++ b/setup.py @@ -24,7 +24,6 @@ setup( url='https://github.com/invisibleroads/socketIO-client', install_requires=[ 'anyjson', - 'gevent-socketio', 'websocket-client', ], packages=find_packages(), diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index e32996d..493748a 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,11 +1,16 @@ -import websocket +import sys +import traceback + +import socket from anyjson import dumps, loads +from functools import partial from threading import Thread, Event from time import sleep from urllib import urlopen +from websocket import WebSocketConnectionClosedException, create_connection -__version__ = '0.3' +__version__ = '0.4' PROTOCOL = 1 # SocketIO protocol version @@ -16,7 +21,7 @@ class BaseNamespace(object): # pragma: no cover def __init__(self, socketIO): self.socketIO = socketIO - def on_connect(self, socketIO): + def on_connect(self): pass def on_disconnect(self): @@ -46,54 +51,69 @@ class BaseNamespace(object): # pragma: no cover class SocketIO(object): - messageID = 0 + _messageID = 0 - def __init__(self, host, port, Namespace=BaseNamespace, secure=False): - self.host = host - self.port = int(port) - self.namespace = Namespace(self) - self.secure = secure - self.__connect() + def __init__(self, host, port, Namespace=BaseNamespace, secure=False, proxies=None): + self._host = host + self._port = int(port) + self._namespace = Namespace(self) + self._secure = secure + self._proxies = proxies + self._connect() - heartbeatInterval = self.heartbeatTimeout - 2 - self.heartbeatThread = RhythmicThread(heartbeatInterval, - self._send_heartbeat) - self.heartbeatThread.start() + heartbeatInterval = self._heartbeatTimeout - 2 + self._heartbeatThread = RhythmicThread(heartbeatInterval, self._send_heartbeat) + self._heartbeatThread.start() - self.channelByName = {} - self.callbackByEvent = {} - self.namespaceThread = ListenerThread(self) - self.namespaceThread.start() + self._channelByName = {} + self._callbackByEvent = {} + self._namespaceThread = ListenerThread(self._recv_packet, self._get_callback) + self._namespaceThread.start() - def __del__(self): # pragma: no cover - self.heartbeatThread.cancel() - self.namespaceThread.cancel() - self.connection.close() + def __enter__(self): + return self - def __connect(self): - baseURL = '%s:%d/socket.io/%s' % (self.host, self.port, PROTOCOL) + def __exit__(self, exc_type, exc_value, traceback): + self.__del__() + + def __del__(self): + self._heartbeatThread.cancel() + self._namespaceThread.cancel() + self._connection.close() + + def _connect(self): + baseURL = '%s:%d/socket.io/%s' % (self._host, self._port, PROTOCOL) try: response = urlopen('%s://%s/' % ( - 'https' if self.secure else 'http', baseURL)) + 'https' if self._secure else 'http', baseURL), + proxies=self._proxies) except IOError: # pragma: no cover raise SocketIOError('Could not start connection') if 200 != response.getcode(): # pragma: no cover raise SocketIOError('Could not establish connection') responseParts = response.readline().split(':') - self.sessionID = responseParts[0] - self.heartbeatTimeout = int(responseParts[1]) - self.connectionTimeout = int(responseParts[2]) - self.supportedTransports = responseParts[3].split(',') - if 'websocket' not in self.supportedTransports: + self._sessionID = responseParts[0] + self._heartbeatTimeout = int(responseParts[1]) + self._connectionTimeout = int(responseParts[2]) + self._supportedTransports = responseParts[3].split(',') + if 'websocket' not in self._supportedTransports: raise SocketIOError('Could not parse handshake') # pragma: no cover socketURL = '%s://%s/websocket/%s' % ( - 'wss' if self.secure else 'ws', baseURL, self.sessionID) - self.connection = websocket.create_connection(socketURL) + 'wss' if self._secure else 'ws', baseURL, self._sessionID) + self._connection = create_connection(socketURL) def _recv_packet(self): code, packetID, channelName, data = -1, None, None, None - packet = self.connection.recv() - packetParts = packet.split(':', 3) + try: + packet = self._connection.recv() + except WebSocketConnectionClosedException: + raise SocketIOConnectionError('Lost connection (Connection closed)') + except socket.timeout: + raise SocketIOConnectionError('Lost connection (Connection timed out)') + try: + packetParts = packet.split(':', 3) + except AttributeError: + raise SocketIOPacketError('Received invalid packet (%s)' % packet) packetCount = len(packetParts) if 4 == packetCount: code, packetID, channelName, data = packetParts @@ -104,34 +124,36 @@ class SocketIO(object): return int(code), packetID, channelName, data def _send_packet(self, code, channelName='', data='', callback=None): - self.connection.send(':'.join([ - str(code), - self.set_callback(callback) if callback else '', - channelName, - data])) + callbackNumber = self._set_callback(callback) if callback else '' + packetParts = [str(code), callbackNumber, channelName, data] + try: + self._connection.send(':'.join(packetParts)) + except socket.error: + raise SocketIOPacketError('Could not send packet') def disconnect(self, channelName=''): self._send_packet(0, channelName) if channelName: - del self.channelByName[channelName] + del self._channelByName[channelName] else: self.__del__() @property def connected(self): - return self.connection.connected + return self._connection.connected def connect(self, channelName, Namespace=BaseNamespace): channel = Channel(self, channelName, Namespace) - self.channelByName[channelName] = channel + self._channelByName[channelName] = channel self._send_packet(1, channelName) return channel def _send_heartbeat(self): try: self._send_packet(2) - except: - self.__del__() + except SocketIOPacketError: + print 'Could not send heartbeat' + pass def message(self, messageData, callback=None, channelName=''): if isinstance(messageData, basestring): @@ -144,40 +166,39 @@ class SocketIO(object): def emit(self, eventName, *eventArguments, **eventKeywords): code = 5 - if callable(eventArguments[-1]): + callback = None + if eventArguments and callable(eventArguments[-1]): callback = eventArguments[-1] eventArguments = eventArguments[:-1] - else: - callback = None channelName = eventKeywords.get('channelName', '') data = dumps(dict(name=eventName, args=eventArguments)) self._send_packet(code, channelName, data, callback) - def get_callback(self, channelName, eventName): + def _get_callback(self, channelName, eventName): 'Get callback associated with channelName and eventName' - socketIO = self.channelByName[channelName] if channelName else self + socketIO = self._channelByName[channelName] if channelName else self try: - return socketIO.callbackByEvent[eventName] + return socketIO._callbackByEvent[eventName] except KeyError: pass - namespace = socketIO.namespace def callback_(*eventArguments): - return namespace.on_(eventName, *eventArguments) - return getattr(namespace, name_callback(eventName), callback_) + return socketIO._namespace.on_(eventName, *eventArguments) + callbackName = 'on_' + eventName.replace(' ', '_') + return getattr(socketIO._namespace, callbackName, callback_) - def set_callback(self, callback): + def _set_callback(self, callback): 'Set callback that will be called after receiving an acknowledgment' - self.messageID += 1 - self.namespaceThread.set_callback(self.messageID, callback) - return '%s+' % self.messageID + self._messageID += 1 + self._namespaceThread.set_callback(self._messageID, callback) + return '%s+' % self._messageID def on(self, eventName, callback): - self.callbackByEvent[eventName] = callback + self._callbackByEvent[eventName] = callback def wait(self, seconds=None, forCallbacks=False): if forCallbacks: - self.namespaceThread.wait_for_callbacks(seconds) + self._namespaceThread.wait_for_callbacks(seconds) elif seconds: sleep(seconds) else: @@ -191,24 +212,22 @@ class SocketIO(object): class Channel(object): def __init__(self, socketIO, channelName, Namespace): - self.socketIO = socketIO - self.channelName = channelName - self.namespace = Namespace(self) - self.callbackByEvent = {} + self._socketIO = socketIO + self._channelName = channelName + self._namespace = Namespace(self) + self._callbackByEvent = {} def disconnect(self): - self.socketIO.disconnect(self.channelName) + self._socketIO.disconnect(self._channelName) def emit(self, eventName, *eventArguments): - self.socketIO.emit(eventName, *eventArguments, - channelName=self.channelName) + self._socketIO.emit(eventName, *eventArguments, channelName=self._channelName) def message(self, messageData, callback=None): - self.socketIO.message(messageData, callback, - channelName=self.channelName) + self._socketIO.message(messageData, callback, channelName=self._channelName) def on(self, eventName, eventCallback): - self.callbackByEvent[eventName] = eventCallback + self._callbackByEvent[eventName] = eventCallback class ListenerThread(Thread): @@ -216,34 +235,43 @@ class ListenerThread(Thread): daemon = True - def __init__(self, socketIO): + def __init__(self, recv_packet, get_callback): super(ListenerThread, self).__init__() - self.socketIO = socketIO self.done = Event() self.waitingForCallbacks = Event() self.callbackByMessageID = {} - self.get_callback = self.socketIO.get_callback + self.recv_packet = recv_packet + self.get_callback = get_callback def run(self): - while not self.done.is_set(): - try: - code, packetID, channelName, data = self.socketIO._recv_packet() - except: - continue - try: - delegate = { - 0: self.on_disconnect, - 1: self.on_connect, - 2: self.on_heartbeat, - 3: self.on_message, - 4: self.on_json, - 5: self.on_event, - 6: self.on_acknowledgment, - 7: self.on_error, - }[code] - except KeyError: - continue - delegate(packetID, channelName, data) + try: + while not self.done.is_set(): + try: + code, packetID, channelName, data = self.recv_packet() + except SocketIOConnectionError, error: + print error + return + except SocketIOPacketError, error: + print error + continue + get_channel_callback = partial(self.get_callback, channelName) + try: + delegate = { + 0: self.on_disconnect, + 1: self.on_connect, + 2: self.on_heartbeat, + 3: self.on_message, + 4: self.on_json, + 5: self.on_event, + 6: self.on_acknowledgment, + 7: self.on_error, + }[code] + except KeyError: + continue + delegate(packetID, get_channel_callback, data) + except: + exc_type, exc_value, exc_traceback = sys.exc_info() + open('tracebacks.log', 'a+t').write('\n'.join(traceback.format_tb(exc_traceback))) def cancel(self): self.done.set() @@ -255,33 +283,28 @@ class ListenerThread(Thread): def set_callback(self, messageID, callback): self.callbackByMessageID[messageID] = callback - def on_disconnect(self, packetID, channelName, data): - callback = self.get_callback(channelName, 'disconnect') - callback() + def on_disconnect(self, packetID, get_channel_callback, data): + get_channel_callback('disconnect')() - def on_connect(self, packetID, channelName, data): - callback = self.get_callback(channelName, 'connect') - callback(self.socketIO) + def on_connect(self, packetID, get_channel_callback, data): + get_channel_callback('connect')() - def on_heartbeat(self, packetID, channelName, data): + def on_heartbeat(self, packetID, get_channel_callback, data): pass - def on_message(self, packetID, channelName, data): - callback = self.get_callback(channelName, 'message') - callback(data) + def on_message(self, packetID, get_channel_callback, data): + get_channel_callback('message')(data) - def on_json(self, packetID, channelName, data): - callback = self.get_callback(channelName, 'message') - callback(loads(data)) + def on_json(self, packetID, get_channel_callback, data): + get_channel_callback('message')(loads(data)) - def on_event(self, packetID, channelName, data): + def on_event(self, packetID, get_channel_callback, data): valueByName = loads(data) eventName = valueByName['name'] eventArguments = valueByName['args'] - callback = self.get_callback(channelName, eventName) - callback(*eventArguments) + get_channel_callback(eventName)(*eventArguments) - def on_acknowledgment(self, packetID, channelName, data): + def on_acknowledgment(self, packetID, get_channel_callback, data): dataParts = data.split('+', 1) messageID = int(dataParts[0]) arguments = loads(dataParts[1]) or [] @@ -296,21 +319,20 @@ class ListenerThread(Thread): if self.waitingForCallbacks.is_set() and not callbackCount: self.cancel() - def on_error(self, packetID, channelName, data): + def on_error(self, packetID, get_channel_callback, data): reason, advice = data.split('+', 1) - callback = self.get_callback(channelName, 'error') - callback(reason, advice) + get_channel_callback('error')(reason, advice) class RhythmicThread(Thread): - 'Execute rhythmicFunction every few seconds' + 'Execute call every few seconds' daemon = True - def __init__(self, intervalInSeconds, rhythmicFunction, *args, **kw): + def __init__(self, intervalInSeconds, call, *args, **kw): super(RhythmicThread, self).__init__() self.intervalInSeconds = intervalInSeconds - self.rhythmicFunction = rhythmicFunction + self.call = call self.args = args self.kw = kw self.done = Event() @@ -318,10 +340,11 @@ class RhythmicThread(Thread): def run(self): try: while not self.done.is_set(): - self.rhythmicFunction(*self.args, **self.kw) + self.call(*self.args, **self.kw) self.done.wait(self.intervalInSeconds) except: - pass + exc_type, exc_value, exc_traceback = sys.exc_info() + open('tracebacks.log', 'a+t').write('\n'.join(traceback.format_tb(exc_traceback))) def cancel(self): self.done.set() @@ -331,5 +354,9 @@ class SocketIOError(Exception): pass -def name_callback(eventName): - return 'on_' + eventName.replace(' ', '_') +class SocketIOConnectionError(SocketIOError): + pass + + +class SocketIOPacketError(SocketIOError): + pass diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index a85c48e..53a6cf6 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -15,10 +15,16 @@ class TestSocketIO(TestCase): self.assertEqual(socketIO.connected, False) def test_emit(self): + socketIO = SocketIO('localhost', 8000, Namespace) + socketIO.emit('aaa') + sleep(0.5) + self.assertEqual(socketIO._namespace.payload, '') + + def test_emit_with_payload(self): socketIO = SocketIO('localhost', 8000, Namespace) socketIO.emit('aaa', PAYLOAD) sleep(0.5) - self.assertEqual(socketIO.namespace.payload, PAYLOAD) + self.assertEqual(socketIO._namespace.payload, PAYLOAD) def test_emit_with_callback(self): global ON_RESPONSE_CALLED @@ -43,16 +49,26 @@ class TestSocketIO(TestCase): newsSocket = mainSocket.connect('/news', Namespace) newsSocket.emit('aaa', PAYLOAD) sleep(0.5) - self.assertNotEqual(mainSocket.namespace.payload, PAYLOAD) - self.assertNotEqual(chatSocket.namespace.payload, PAYLOAD) - self.assertEqual(newsSocket.namespace.payload, PAYLOAD) + self.assertNotEqual(mainSocket._namespace.payload, PAYLOAD) + self.assertNotEqual(chatSocket._namespace.payload, PAYLOAD) + self.assertEqual(newsSocket._namespace.payload, PAYLOAD) + + def test_delete(self): + socketIO = SocketIO('localhost', 8000) + childThreads = [ + socketIO._heartbeatThread, + socketIO._namespaceThread, + ] + del socketIO + for childThread in childThreads: + self.assertEqual(True, childThread.done.is_set()) class Namespace(BaseNamespace): payload = None - def on_ddd(self, data): + def on_ddd(self, data=''): self.payload = data From 4f2187c536610ef275bc1b554a773b8b72dede20 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 11 Feb 2013 08:32:22 -0800 Subject: [PATCH 006/191] Added experiments --- TODO.rst | 3 +- experiments/callableweakref-tests.py | 170 ++++++++++++ experiments/callableweakref.py | 52 ++++ experiments/t0.py | 6 + experiments/t1.py | 7 + experiments/t10.py | 16 ++ experiments/t11.py | 21 ++ experiments/t12.py | 32 +++ experiments/t2.py | 19 ++ experiments/t3.py | 19 ++ experiments/t4.py | 26 ++ experiments/t5.py | 30 ++ socketIO_client/__init__.py | 391 +++++++++++++-------------- 13 files changed, 595 insertions(+), 197 deletions(-) create mode 100644 experiments/callableweakref-tests.py create mode 100644 experiments/callableweakref.py create mode 100644 experiments/t0.py create mode 100644 experiments/t1.py create mode 100644 experiments/t10.py create mode 100644 experiments/t11.py create mode 100644 experiments/t12.py create mode 100644 experiments/t2.py create mode 100644 experiments/t3.py create mode 100644 experiments/t4.py create mode 100644 experiments/t5.py diff --git a/TODO.rst b/TODO.rst index e362fcd..b15199b 100644 --- a/TODO.rst +++ b/TODO.rst @@ -1,7 +1,8 @@ + Fix unittests + Fix exceptions when websocket server disappears - Fix thread exceptions + Finish creating low-level _SocketIO to eliminate cyclic references + Move namespace and callback handling to _ListenerThread Integrate Zac's fork #6 Integrate Sajal's fork #7 diff --git a/experiments/callableweakref-tests.py b/experiments/callableweakref-tests.py new file mode 100644 index 0000000..c7a95ec --- /dev/null +++ b/experiments/callableweakref-tests.py @@ -0,0 +1,170 @@ +# Note 1: The expected behavior of weakref to an instance is that it should +# return None if no other strong references to the instance exist, +# signalling that the instance can be safely garbage collected. +# +# Note 2: The expected behavior of weakref to a method is that it should +# return None if no other strong references to the parent instance exist, +# signalling that the parent instance can be safely garbage collected. +# +# Note 3: The IPython interpreter stores its own references and will +# produce results different from those of the default Python interpreter. +import callableweakref +import weakref +import unittest + + +class TestCallableWeakRef(unittest.TestCase): + + def test_instanceDirectWeakref_dies_on_arrival(self): + 'Assert that weakref works as expected' + instanceDirectWeakref = weakref.ref(Instance()) + assert instanceDirectWeakref() is None + + def test_instanceIndirectWeakref_dies_when_instance_dies(self): + 'Assert that weakref works as expected' + instance = Instance() + instanceIndirectWeakref = weakref.ref(instance) + assert instanceIndirectWeakref() is instance + del instance + assert instanceIndirectWeakref() is None + + def test_boundMethodDirectWeakref_dies_on_arrival(self): + 'Assert that weakref does not work as expected' + instance = Instance() + boundMethodDirectWeakref = weakref.ref(instance.call) + assert boundMethodDirectWeakref() is None # Should be instance + + def test_boundMethodIndirectWeakref_lives_when_instance_dies(self): + 'Assert that weakref works as expected' + instance = Instance() + boundMethod = instance.call + boundMethodIndirectWeakref = weakref.ref(boundMethod) + assert boundMethodIndirectWeakref() is boundMethod + del instance + assert boundMethodIndirectWeakref() is boundMethod + del boundMethod + assert boundMethodIndirectWeakref() is None + + def test_unboundMethodDirectWeakref_dies_on_arrival(self): + 'Assert that weakref does not work as expected' + unboundMethodDirectWeakref = weakref.ref(Instance.call) + assert unboundMethodDirectWeakref() is None # Should be Instance.call + + def test_unboundMethodIndirectWeakref_dies_when_unboundMethod_dies(self): + 'Assert that weakref works as expected' + unboundMethod = Instance.call + unboundMethodIndirectWeakref = weakref.ref(unboundMethod) + assert unboundMethodIndirectWeakref() is unboundMethod + del unboundMethod + assert unboundMethodIndirectWeakref() is None + + def test_classFunctionDirectWeakref_dies_on_arrival(self): + 'Assert that weakref works as expected' + classFunctionDirectWeakref = weakref.ref(Instance()) + assert classFunctionDirectWeakref() is None + + def test_classFunctionIndirectWeakref_dies_when_classFunction_dies(self): + 'Assert that weakref works as expected' + classFunction = Instance() + classFunctionIndirectWeakref = weakref.ref(classFunction) + assert classFunctionIndirectWeakref() is classFunction + del classFunction + assert classFunctionIndirectWeakref() is None + + def test_normalFunctionDirectWeakref_dies_when_normal_function_dies(self): + 'Assert that weakref works as expected' + call = lambda: None + normalFunctionDirectWeakref = weakref.ref(call) + assert normalFunctionDirectWeakref() is call + del call + assert normalFunctionDirectWeakref() is None + + def test_normalFunctionIndirectWeakref_dies_when_normal_function_dies(self): + 'Assert that weakref works as expected' + call = lambda: None + normalFunction = call + normalFunctionIndirectWeakref = weakref.ref(normalFunction) + assert normalFunctionIndirectWeakref() is normalFunction + del normalFunction + assert normalFunctionIndirectWeakref() is call + del call + assert normalFunctionIndirectWeakref() is None + + # Assert that CallableWeakref works as expected for callables + + def test_boundMethodDirectCallableWeakref_dies_when_instance_dies(self): + instance = Instance() + boundMethodDirectCallableWeakref = callableweakref.ref(instance.call) + self.assertEqual(boundMethodDirectCallableWeakref(), instance.call) + del instance + assert boundMethodDirectCallableWeakref() is None + + def test_boundMethodIndirectCallableWeakref_dies_when_instance_dies(self): + instance = Instance() + boundMethod = instance.call + boundMethodIndirectCallableWeakref = callableweakref.ref(boundMethod) + self.assertEqual(boundMethodIndirectCallableWeakref(), boundMethod) + del instance + self.assertEqual(boundMethodIndirectCallableWeakref(), boundMethod) + del boundMethod + assert boundMethodIndirectCallableWeakref() is None + + def test_unboundMethodDirectCallableWeakref_lives_on_arrival(self): + unboundMethodDirectCallableWeakref = callableweakref.ref(Instance.call) + self.assertEqual(unboundMethodDirectCallableWeakref(), Instance.call) + + def test_unboundMethodIndirectCallableWeakref_lives_on_arrival(self): + unboundMethod = Instance.call + unboundMethodIndirectCallableWeakref = callableweakref.ref(unboundMethod) + self.assertEqual(unboundMethodIndirectCallableWeakref(), unboundMethod) + + def test_classMethodIndirectCallableWeakref_lives_on_arrival(self): + classMethod = Instance.call_classmethod + classMethodIndirectCallableWeakref = callableweakref.ref(classMethod) + self.assertEqual(classMethodIndirectCallableWeakref(), classMethod) + + def test_staticMethodIndirectCallableWeakref_lives_on_arrival(self): + staticMethod = Instance.call_staticmethod + staticMethodIndirectCallableWeakref = callableweakref.ref(staticMethod) + self.assertEqual(staticMethodIndirectCallableWeakref(), staticMethod) + + def test_classFunctionDirectCallableWeakref_dies_on_arrival(self): + classFunctionDirectCallableWeakref = callableweakref.ref(Instance()) + assert classFunctionDirectCallableWeakref() is None + + def test_classFunctionIndirectCallableWeakref_dies_when_classFunction_dies(self): + classFunction = Instance() + classFunctionIndirectCallableWeakref = callableweakref.ref(classFunction) + self.assertEqual(classFunctionIndirectCallableWeakref(), classFunction.__call__) + del classFunction + assert classFunctionIndirectCallableWeakref() is None + + def test_normalFunctionDirectCallableWeakref_lives_on_arrival(self): + call = lambda: None + normalFunctionDirectCallableWeakref = callableweakref.ref(call) + self.assertEqual(normalFunctionDirectCallableWeakref(), call) + + def test_normalFunctionIndirectCallableWeakref_lives_on_arrival(self): + call = lambda: None + normalFunction = call + normalFunctionIndirectWeakref = callableweakref.ref(normalFunction) + self.assertEqual(normalFunctionIndirectWeakref(), normalFunction) + del normalFunction + self.assertEqual(normalFunctionIndirectWeakref(), call) + + +class Instance(object): + + def __call__(self): + pass + + def call(self): + pass + + @classmethod + def call_classmethod(Class): + pass + + @staticmethod + def call_staticmethod(): + pass diff --git a/experiments/callableweakref.py b/experiments/callableweakref.py new file mode 100644 index 0000000..788592c --- /dev/null +++ b/experiments/callableweakref.py @@ -0,0 +1,52 @@ +import types +import weakref + + +def ref(function): + return CallableWeakReference(function) + + +class CallableWeakReference(object): + + def __init__(self, function): + 'Create a weak reference to a callable' + try: + if function.im_self: + # We have a bound method or class method + self._reference = weakref.ref(function.im_self) + else: + # We have an unbound method + self._reference = None + self._function = function.im_func + self._class = function.im_class + except AttributeError: + try: + function.func_code + # We have a normal function or static method + self._reference = None + self._function = function + self._class = None + except AttributeError: + function = function.__call__ + # We have a class masquerading as a function + self._reference = weakref.ref(function.im_self) + self._function = function.im_func + self._class = function.im_class + + def __call__(self): + if self.dead: + return + if self._reference: + # We have a bound method + return types.MethodType(self._function, self._reference(), self._class) + elif self._class: + return types.MethodType(self._function, None, self._class) + else: + return self._function + + @property + def dead(self): + if self._reference and not self._reference(): + # We have a bound method whose parent reference has died + return True + return False diff --git a/experiments/t0.py b/experiments/t0.py new file mode 100644 index 0000000..3980052 --- /dev/null +++ b/experiments/t0.py @@ -0,0 +1,6 @@ +from socketIO_client import SocketIO + +s = SocketIO('localhost', 8000) +del s +from time import sleep +sleep(3) diff --git a/experiments/t1.py b/experiments/t1.py new file mode 100644 index 0000000..0955ed6 --- /dev/null +++ b/experiments/t1.py @@ -0,0 +1,7 @@ +class O(object): + + def __del__(self): + print '__del__()' + + +o = O() diff --git a/experiments/t10.py b/experiments/t10.py new file mode 100644 index 0000000..24733d9 --- /dev/null +++ b/experiments/t10.py @@ -0,0 +1,16 @@ +class A(object): + + def __init__(self): + self.b = B(self) + + def __del__(self): + print '__del__()' + + +class B(object): + + def __init__(self, a): + self.a = a + + +a = A() diff --git a/experiments/t11.py b/experiments/t11.py new file mode 100644 index 0000000..11121de --- /dev/null +++ b/experiments/t11.py @@ -0,0 +1,21 @@ +class A(object): + + def __init__(self): + self.c = Common() + self.b = B(self.c) + + def __del__(self): + print '__del__()' + + +class B(object): + + def __init__(self, c): + self.c = c + + +class Common(object): + pass + + +a = A() diff --git a/experiments/t12.py b/experiments/t12.py new file mode 100644 index 0000000..b4e9bb3 --- /dev/null +++ b/experiments/t12.py @@ -0,0 +1,32 @@ +# Will the destructor of a class be called if its two children have cyclic +# references to each other? Yes. + + +class Parent(object): + + def __init__(self): + self.child = Child() + + def __del__(self): + print 'Parent.__del__()' + + +class Child(object): + + def __init__(self): + self.grandChild = GrandChild(self) + + def __del__(self): + print 'Child.__del__()' + + +class GrandChild(object): + + def __init__(self, parent): + self.parent = parent + + def __del__(self): + print 'GrandChild.__del__()' + + +parent = Parent() diff --git a/experiments/t2.py b/experiments/t2.py new file mode 100644 index 0000000..3ad124c --- /dev/null +++ b/experiments/t2.py @@ -0,0 +1,19 @@ +import weakref + + +class O(object): + + def __init__(self): + self.p = P(self) + + def __del__(self): + print '__del__()' + + +class P(object): + + def __init__(self, parent): + self.parent = weakref.ref(parent) + + +o = O() diff --git a/experiments/t3.py b/experiments/t3.py new file mode 100644 index 0000000..2de3123 --- /dev/null +++ b/experiments/t3.py @@ -0,0 +1,19 @@ +class O(object): + + def __init__(self): + self.p = P(self.f) + + def __del__(self): + print '__del__()' + + def f(self): + pass + + +class P(object): + + def __init__(self, parentMethod): + self.parentMethod = parentMethod + + +o = O() diff --git a/experiments/t4.py b/experiments/t4.py new file mode 100644 index 0000000..da46dfe --- /dev/null +++ b/experiments/t4.py @@ -0,0 +1,26 @@ +import weakref + + +class O(object): + + def __init__(self): + self.p = P(self.f) + + def __del__(self): + print '__del__()' + + def f(self): + pass + + +class P(object): + + def __init__(self, parentMethod): + self.parentMethod = weakref.ref(parentMethod) + + def show(self): + print self.parentMethod + + +o = O() +o.p.show() # Dead on arrival diff --git a/experiments/t5.py b/experiments/t5.py new file mode 100644 index 0000000..a2ff710 --- /dev/null +++ b/experiments/t5.py @@ -0,0 +1,30 @@ +import callableweakref + + +class O(object): + + def __init__(self): + self.p = P(self.f) + + def __del__(self): + print '__del__()' + + def f(self): + print 'f()' + + +class P(object): + + def __init__(self, parentMethod): + self.parentMethod = callableweakref.ref(parentMethod) + + def show(self): + print self.parentMethod + + def run(self): + self.parentMethod()() + + +o = O() +o.p.show() +o.p.run() diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 493748a..9ae06a2 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,18 +1,13 @@ -import sys -import traceback - import socket +import weakref from anyjson import dumps, loads from functools import partial -from threading import Thread, Event +from threading import Event, Thread from time import sleep from urllib import urlopen from websocket import WebSocketConnectionClosedException, create_connection -__version__ = '0.4' - - PROTOCOL = 1 # SocketIO protocol version @@ -49,112 +44,64 @@ class BaseNamespace(object): # pragma: no cover print '[Reconnect]', args +class Channel(object): + + def __init__(self, socketIO, channelName, Namespace): + self._socketIO = weakref.proxy(socketIO) + self._channelName = channelName + self._namespace = Namespace(self) + self._callbackByEvent = {} + + def disconnect(self): + self._socketIO.disconnect(self._channelName) + + def emit(self, eventName, *eventArguments): + self._socketIO.emit(eventName, *eventArguments, channelName=self._channelName) + + def message(self, messageData, callback=None): + self._socketIO.message(messageData, callback, channelName=self._channelName) + + def on(self, eventName, eventCallback): + self._callbackByEvent[eventName] = eventCallback + + class SocketIO(object): - _messageID = 0 - def __init__(self, host, port, Namespace=BaseNamespace, secure=False, proxies=None): - self._host = host - self._port = int(port) - self._namespace = Namespace(self) - self._secure = secure - self._proxies = proxies - self._connect() + self._socketIO = _SocketIO(host, port, secure, proxies) - heartbeatInterval = self._heartbeatTimeout - 2 - self._heartbeatThread = RhythmicThread(heartbeatInterval, self._send_heartbeat) + self._heartbeatThread = _RhythmicThread( + self._socketIO.heartbeatTimeout, + self._socketIO.send_heartbeat) self._heartbeatThread.start() - self._channelByName = {} - self._callbackByEvent = {} - self._namespaceThread = ListenerThread(self._recv_packet, self._get_callback) + self._namespace = Namespace(self._socketIO) + self._namespaceThread = _ListenerThread(self._socketIO) self._namespaceThread.start() def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): - self.__del__() + self.disconnect() def __del__(self): - self._heartbeatThread.cancel() - self._namespaceThread.cancel() - self._connection.close() - - def _connect(self): - baseURL = '%s:%d/socket.io/%s' % (self._host, self._port, PROTOCOL) - try: - response = urlopen('%s://%s/' % ( - 'https' if self._secure else 'http', baseURL), - proxies=self._proxies) - except IOError: # pragma: no cover - raise SocketIOError('Could not start connection') - if 200 != response.getcode(): # pragma: no cover - raise SocketIOError('Could not establish connection') - responseParts = response.readline().split(':') - self._sessionID = responseParts[0] - self._heartbeatTimeout = int(responseParts[1]) - self._connectionTimeout = int(responseParts[2]) - self._supportedTransports = responseParts[3].split(',') - if 'websocket' not in self._supportedTransports: - raise SocketIOError('Could not parse handshake') # pragma: no cover - socketURL = '%s://%s/websocket/%s' % ( - 'wss' if self._secure else 'ws', baseURL, self._sessionID) - self._connection = create_connection(socketURL) - - def _recv_packet(self): - code, packetID, channelName, data = -1, None, None, None - try: - packet = self._connection.recv() - except WebSocketConnectionClosedException: - raise SocketIOConnectionError('Lost connection (Connection closed)') - except socket.timeout: - raise SocketIOConnectionError('Lost connection (Connection timed out)') - try: - packetParts = packet.split(':', 3) - except AttributeError: - raise SocketIOPacketError('Received invalid packet (%s)' % packet) - packetCount = len(packetParts) - if 4 == packetCount: - code, packetID, channelName, data = packetParts - elif 3 == packetCount: - code, packetID, channelName = packetParts - elif 1 == packetCount: # pragma: no cover - code = packetParts[0] - return int(code), packetID, channelName, data - - def _send_packet(self, code, channelName='', data='', callback=None): - callbackNumber = self._set_callback(callback) if callback else '' - packetParts = [str(code), callbackNumber, channelName, data] - try: - self._connection.send(':'.join(packetParts)) - except socket.error: - raise SocketIOPacketError('Could not send packet') + self.disconnect() def disconnect(self, channelName=''): self._send_packet(0, channelName) if channelName: - del self._channelByName[channelName] + del self.channelByName[channelName] else: - self.__del__() - - @property - def connected(self): - return self._connection.connected + self._heartbeatThread.cancel() + self._namespaceThread.cancel() def connect(self, channelName, Namespace=BaseNamespace): channel = Channel(self, channelName, Namespace) - self._channelByName[channelName] = channel - self._send_packet(1, channelName) + self.channelByName[channelName] = channel + self.send_packet(1, channelName) return channel - def _send_heartbeat(self): - try: - self._send_packet(2) - except SocketIOPacketError: - print 'Could not send heartbeat' - pass - def message(self, messageData, callback=None, channelName=''): if isinstance(messageData, basestring): code = 3 @@ -174,25 +121,6 @@ class SocketIO(object): data = dumps(dict(name=eventName, args=eventArguments)) self._send_packet(code, channelName, data, callback) - def _get_callback(self, channelName, eventName): - 'Get callback associated with channelName and eventName' - socketIO = self._channelByName[channelName] if channelName else self - try: - return socketIO._callbackByEvent[eventName] - except KeyError: - pass - - def callback_(*eventArguments): - return socketIO._namespace.on_(eventName, *eventArguments) - callbackName = 'on_' + eventName.replace(' ', '_') - return getattr(socketIO._namespace, callbackName, callback_) - - def _set_callback(self, callback): - 'Set callback that will be called after receiving an acknowledgment' - self._messageID += 1 - self._namespaceThread.set_callback(self._messageID, callback) - return '%s+' % self._messageID - def on(self, eventName, callback): self._callbackByEvent[eventName] = callback @@ -203,75 +131,179 @@ class SocketIO(object): sleep(seconds) else: try: - while self.connected: + while self._socketIO.connected: sleep(1) except KeyboardInterrupt: pass -class Channel(object): +class _SocketIO(object): + 'Low-level interface to remove cyclic references in child threads' - def __init__(self, socketIO, channelName, Namespace): - self._socketIO = socketIO - self._channelName = channelName - self._namespace = Namespace(self) - self._callbackByEvent = {} + messageID = 0 - def disconnect(self): - self._socketIO.disconnect(self._channelName) + def __init__(self, host, port, secure=False, proxies=None): + self.connect(host, port, secure, proxies) + self.callbackByMessageID = {} + self.callbackByEvent = {} + self.channelByName = {} - def emit(self, eventName, *eventArguments): - self._socketIO.emit(eventName, *eventArguments, channelName=self._channelName) + def __del__(self): + self.connection.close() - def message(self, messageData, callback=None): - self._socketIO.message(messageData, callback, channelName=self._channelName) + def connect(self, host, port, secure, proxies): + baseURL = '%s:%d/socket.io/%s' % (host, port, PROTOCOL) + targetScheme = 'https' if secure else 'http' + targetURL = '%s://%s/' % (targetScheme, baseURL) + try: + response = urlopen(targetURL, proxies=proxies) + except IOError: # pragma: no cover + raise SocketIOError('Could not start connection') + if 200 != response.getcode(): # pragma: no cover + raise SocketIOError('Could not establish connection') + responseParts = response.readline().split(':') + sessionID = responseParts[0] + heartbeatTimeout = int(responseParts[1]) + # connectionTimeout = int(responseParts[2]) + supportedTransports = responseParts[3].split(',') + if 'websocket' not in supportedTransports: + raise SocketIOError('Could not parse handshake') # pragma: no cover + socketScheme = 'wss' if secure else 'ws' + socketURL = '%s://%s/websocket/%s' % (socketScheme, baseURL, sessionID) + self.connection = create_connection(socketURL) + self.heartbeatInterval = heartbeatTimeout - 2 - def on(self, eventName, eventCallback): - self._callbackByEvent[eventName] = eventCallback + def recv_packet(self): + code, packetID, channelName, data = -1, None, None, None + try: + packet = self.connection.recv() + except WebSocketConnectionClosedException: + raise SocketIOConnectionError('Lost connection (Connection closed)') + except socket.timeout: + raise SocketIOConnectionError('Lost connection (Connection timed out)') + try: + packetParts = packet.split(':', 3) + except AttributeError: + raise SocketIOPacketError('Received invalid packet (%s)' % packet) + packetCount = len(packetParts) + if 4 == packetCount: + code, packetID, channelName, data = packetParts + elif 3 == packetCount: + code, packetID, channelName = packetParts + elif 1 == packetCount: # pragma: no cover + code = packetParts[0] + return int(code), packetID, channelName, data + + def send_packet(self, code, channelName='', data='', callback=None): + callbackNumber = self.set_messageID_callback(callback) if callback else '' + packetParts = [str(code), callbackNumber, channelName, data] + try: + self.connection.send(':'.join(packetParts)) + except socket.error: + raise SocketIOPacketError('Could not send packet') + + def set_messageID_callback(self, callback): + 'Set callback that will be called after receiving an acknowledgment' + self.messageID += 1 + self.callbackByMessageID[self.messageID] = callback + return '%s+' % self.messageID + + def get_messageID_callback(self, messageID): + 'Get callback associated with messageID' + try: + callback = self.callbackByMessageID[messageID] + del self.callbackByMessageID[messageID] + return callback + except KeyError: + return + + @property + def has_messageID_callback(self): + return True if self.callbackByMessageID else False + + def get_event_callback(self, channelName, eventName): + 'Get callback associated with channelName and eventName' + _socketIO = self.channelByName[channelName] if channelName else self + try: + return _socketIO.callbackByEvent[eventName] + except KeyError: + pass + + def callback_(*eventArguments): + return _socketIO.namespace.on_(eventName, *eventArguments) + callbackName = 'on_' + eventName.replace(' ', '_') + return getattr(_socketIO.namespace, callbackName, callback_) + + @property + def connected(self): + return self.connection.connected + + def send_heartbeat(self): + try: + self.send_packet(2) + except SocketIOPacketError: + print 'Could not send heartbeat' + pass -class ListenerThread(Thread): +class _RhythmicThread(Thread): + 'Execute call every few seconds' + + daemon = True + + def __init__(self, intervalInSeconds, call, *args, **kw): + super(_RhythmicThread, self).__init__() + self.intervalInSeconds = intervalInSeconds + self.call + self.args = args + self.kw = kw + self.done = Event() + + def run(self): + while not self.done.is_set(): + self.call(*self.args, **self.kw) + self.done.wait(self.intervalInSeconds) + + def cancel(self): + self.done.set() + + +class _ListenerThread(Thread): 'Process messages from SocketIO server' daemon = True - def __init__(self, recv_packet, get_callback): - super(ListenerThread, self).__init__() + def __init__(self, _socketIO, get_event_callback, ): + super(_ListenerThread, self).__init__() + self._socketIO = _socketIO self.done = Event() self.waitingForCallbacks = Event() - self.callbackByMessageID = {} - self.recv_packet = recv_packet - self.get_callback = get_callback def run(self): - try: - while not self.done.is_set(): - try: - code, packetID, channelName, data = self.recv_packet() - except SocketIOConnectionError, error: - print error - return - except SocketIOPacketError, error: - print error - continue - get_channel_callback = partial(self.get_callback, channelName) - try: - delegate = { - 0: self.on_disconnect, - 1: self.on_connect, - 2: self.on_heartbeat, - 3: self.on_message, - 4: self.on_json, - 5: self.on_event, - 6: self.on_acknowledgment, - 7: self.on_error, - }[code] - except KeyError: - continue - delegate(packetID, get_channel_callback, data) - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - open('tracebacks.log', 'a+t').write('\n'.join(traceback.format_tb(exc_traceback))) + while not self.done.is_set(): + try: + code, packetID, channelName, data = self._socketIO.recv_packet() + except SocketIOConnectionError, error: + print error + return + except SocketIOPacketError, error: + print error + continue + get_channel_callback = partial(self._socketIO.get_event_callback, channelName) + try: + delegate = { + 0: self.on_disconnect, + 1: self.on_connect, + 2: self.on_heartbeat, + 3: self.on_message, + 4: self.on_json, + 5: self.on_event, + 6: self.on_acknowledgment, + 7: self.on_error, + }[code] + except KeyError: + continue + delegate(packetID, get_channel_callback, data) def cancel(self): self.done.set() @@ -280,9 +312,6 @@ class ListenerThread(Thread): self.waitingForCallbacks.set() self.join(seconds) - def set_callback(self, messageID, callback): - self.callbackByMessageID[messageID] = callback - def on_disconnect(self, packetID, get_channel_callback, data): get_channel_callback('disconnect')() @@ -308,48 +337,18 @@ class ListenerThread(Thread): dataParts = data.split('+', 1) messageID = int(dataParts[0]) arguments = loads(dataParts[1]) or [] - try: - callback = self.callbackByMessageID[messageID] - except KeyError: - pass - else: - del self.callbackByMessageID[messageID] - callback(*arguments) - callbackCount = len(self.callbackByMessageID) - if self.waitingForCallbacks.is_set() and not callbackCount: - self.cancel() + callback = self._socketIO.get_messageID_callback(messageID) + if not callback: + return + callback(*arguments) + if self.waitingForCallbacks.is_set() and not self._socketIO.has_messageID_callback: + self.cancel() def on_error(self, packetID, get_channel_callback, data): reason, advice = data.split('+', 1) get_channel_callback('error')(reason, advice) -class RhythmicThread(Thread): - 'Execute call every few seconds' - - daemon = True - - def __init__(self, intervalInSeconds, call, *args, **kw): - super(RhythmicThread, self).__init__() - self.intervalInSeconds = intervalInSeconds - self.call = call - self.args = args - self.kw = kw - self.done = Event() - - def run(self): - try: - while not self.done.is_set(): - self.call(*self.args, **self.kw) - self.done.wait(self.intervalInSeconds) - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - open('tracebacks.log', 'a+t').write('\n'.join(traceback.format_tb(exc_traceback))) - - def cancel(self): - self.done.set() - - class SocketIOError(Exception): pass From 01bfefdd8ba4bc597b16d6cbf84ea365d2081125 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 13 Feb 2013 08:27:58 -0800 Subject: [PATCH 007/191] Trying to refactor the damn code to remove cyclic references --- README.rst | 27 +-- socketIO_client/__init__.py | 381 +++++++++++++++++++----------------- socketIO_client/tests.py | 33 ++-- 3 files changed, 233 insertions(+), 208 deletions(-) diff --git a/README.rst b/README.rst index 504e0d5..a1984c2 100644 --- a/README.rst +++ b/README.rst @@ -70,7 +70,8 @@ Define events in a namespace. :: def on_ddd(self, *args): self.socketIO.emit('eee', {'fff': 'ggg'}) - socketIO = SocketIO('localhost', 8000, Namespace) + socketIO = SocketIO('localhost', 8000) + socketIO.define(Namespace) socketIO.wait() # Loop until CTRL-C Define standard events. :: @@ -79,7 +80,7 @@ Define standard events. :: class Namespace(BaseNamespace): - def on_connect(self, socketIO): + def on_connect(self): print '[Connected]' def on_disconnect(self): @@ -91,32 +92,36 @@ Define standard events. :: def on_message(self, id, message): print '[Message] %s: %s' % (id, message) - socketIO = SocketIO('localhost', 8000, Namespace) + socketIO = SocketIO('localhost', 8000) + socketIO.define(Namespace) socketIO.wait() # Loop until CTRL-C -Define different behavior for different channels on a single socket. :: +Define different namespaces on a single socket. :: from socketIO_client import SocketIO, BaseNamespace - class MainNamespace(BaseNamespace): + class MainNamespace(Channel): def on_aaa(self, *args): print 'aaa', args - class ChatNamespace(BaseNamespace): + class ChatNamespace(Channel): def on_bbb(self, *args): print 'bbb', args - class NewsNamespace(BaseNamespace): + class NewsNamespace(Channel): def on_ccc(self, *args): print 'ccc', args - mainSocket = SocketIO('localhost', 8000, MainNamespace) - chatSocket = mainSocket.connect('/chat', ChatNamespace) - newsSocket = mainSocket.connect('/news', NewsNamespace) - mainSocket.wait() # Loop until CTRL-C + socketIO = SocketIO('localhost', 8000) + socketIO.define(MainNamespace) + chatSocket = socketIO.define(ChatNamespace, '/chat') + chatSocket.emit('bbb') + newsSocket = socketIO.define(NewsNamespace, '/news') + newsSocket.emit('ccc') + socketIO.wait() # Loop until CTRL-C Open secure websockets (HTTPS / WSS) behind a proxy. :: diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 9ae06a2..b247fb4 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,7 +1,5 @@ import socket -import weakref from anyjson import dumps, loads -from functools import partial from threading import Event, Thread from time import sleep from urllib import urlopen @@ -13,8 +11,8 @@ PROTOCOL = 1 # SocketIO protocol version class BaseNamespace(object): # pragma: no cover - def __init__(self, socketIO): - self.socketIO = socketIO + def __init__(self, _socketIO): + self._socketIO = _socketIO def on_connect(self): pass @@ -44,40 +42,21 @@ class BaseNamespace(object): # pragma: no cover print '[Reconnect]', args -class Channel(object): - - def __init__(self, socketIO, channelName, Namespace): - self._socketIO = weakref.proxy(socketIO) - self._channelName = channelName - self._namespace = Namespace(self) - self._callbackByEvent = {} - - def disconnect(self): - self._socketIO.disconnect(self._channelName) - - def emit(self, eventName, *eventArguments): - self._socketIO.emit(eventName, *eventArguments, channelName=self._channelName) - - def message(self, messageData, callback=None): - self._socketIO.message(messageData, callback, channelName=self._channelName) - - def on(self, eventName, eventCallback): - self._callbackByEvent[eventName] = eventCallback - - class SocketIO(object): - def __init__(self, host, port, Namespace=BaseNamespace, secure=False, proxies=None): + def __init__(self, host, port, secure=False, proxies=None): self._socketIO = _SocketIO(host, port, secure, proxies) + self._channelByPath = {} - self._heartbeatThread = _RhythmicThread( + self._rhythmicThread = _RhythmicThread( self._socketIO.heartbeatTimeout, self._socketIO.send_heartbeat) - self._heartbeatThread.start() + self._rhythmicThread.start() - self._namespace = Namespace(self._socketIO) - self._namespaceThread = _ListenerThread(self._socketIO) - self._namespaceThread.start() + self._listenerThread = _ListenerThread( + self._socketIO, + self._channelByPath) + self._listenerThread.start() def __enter__(self): return self @@ -88,20 +67,30 @@ class SocketIO(object): def __del__(self): self.disconnect() - def disconnect(self, channelName=''): - self._send_packet(0, channelName) - if channelName: - del self.channelByName[channelName] - else: - self._heartbeatThread.cancel() - self._namespaceThread.cancel() + @property + def connected(self): + return self._socketIO.connection.connected - def connect(self, channelName, Namespace=BaseNamespace): - channel = Channel(self, channelName, Namespace) - self.channelByName[channelName] = channel - self.send_packet(1, channelName) + def disconnect(self, channelPath=''): + self._socketIO.disconnect(channelPath) + if channelPath: + del self._channelByPath[channelPath] + else: + self._rhythmicThread.cancel() + self._listenerThread.cancel() + + def define(self, Namespace, channelPath=''): + self._socketIO.connect(channelPath) + channel = Channel(self._socketIO, Namespace, channelPath) + self._channelByPath[channelPath] = channel return channel + def get_namespace(self, channelPath=''): + return self._channelByPath[channelPath].get_namespace() + + def on(self, eventName, eventCallback): + self._callbackByEvent[eventName] = callback + def message(self, messageData, callback=None, channelName=''): if isinstance(messageData, basestring): code = 3 @@ -111,7 +100,8 @@ class SocketIO(object): data = dumps(messageData) self._send_packet(code, channelName, data, callback) - def emit(self, eventName, *eventArguments, **eventKeywords): + def emit(self, eventName, *eventArguments): + self._socketIO.emit(eventName, *eventArguments) code = 5 callback = None if eventArguments and callable(eventArguments[-1]): @@ -121,12 +111,9 @@ class SocketIO(object): data = dumps(dict(name=eventName, args=eventArguments)) self._send_packet(code, channelName, data, callback) - def on(self, eventName, callback): - self._callbackByEvent[eventName] = callback - def wait(self, seconds=None, forCallbacks=False): if forCallbacks: - self._namespaceThread.wait_for_callbacks(seconds) + self.__listenerThread.wait_for_callbacks(seconds) elif seconds: sleep(seconds) else: @@ -137,115 +124,6 @@ class SocketIO(object): pass -class _SocketIO(object): - 'Low-level interface to remove cyclic references in child threads' - - messageID = 0 - - def __init__(self, host, port, secure=False, proxies=None): - self.connect(host, port, secure, proxies) - self.callbackByMessageID = {} - self.callbackByEvent = {} - self.channelByName = {} - - def __del__(self): - self.connection.close() - - def connect(self, host, port, secure, proxies): - baseURL = '%s:%d/socket.io/%s' % (host, port, PROTOCOL) - targetScheme = 'https' if secure else 'http' - targetURL = '%s://%s/' % (targetScheme, baseURL) - try: - response = urlopen(targetURL, proxies=proxies) - except IOError: # pragma: no cover - raise SocketIOError('Could not start connection') - if 200 != response.getcode(): # pragma: no cover - raise SocketIOError('Could not establish connection') - responseParts = response.readline().split(':') - sessionID = responseParts[0] - heartbeatTimeout = int(responseParts[1]) - # connectionTimeout = int(responseParts[2]) - supportedTransports = responseParts[3].split(',') - if 'websocket' not in supportedTransports: - raise SocketIOError('Could not parse handshake') # pragma: no cover - socketScheme = 'wss' if secure else 'ws' - socketURL = '%s://%s/websocket/%s' % (socketScheme, baseURL, sessionID) - self.connection = create_connection(socketURL) - self.heartbeatInterval = heartbeatTimeout - 2 - - def recv_packet(self): - code, packetID, channelName, data = -1, None, None, None - try: - packet = self.connection.recv() - except WebSocketConnectionClosedException: - raise SocketIOConnectionError('Lost connection (Connection closed)') - except socket.timeout: - raise SocketIOConnectionError('Lost connection (Connection timed out)') - try: - packetParts = packet.split(':', 3) - except AttributeError: - raise SocketIOPacketError('Received invalid packet (%s)' % packet) - packetCount = len(packetParts) - if 4 == packetCount: - code, packetID, channelName, data = packetParts - elif 3 == packetCount: - code, packetID, channelName = packetParts - elif 1 == packetCount: # pragma: no cover - code = packetParts[0] - return int(code), packetID, channelName, data - - def send_packet(self, code, channelName='', data='', callback=None): - callbackNumber = self.set_messageID_callback(callback) if callback else '' - packetParts = [str(code), callbackNumber, channelName, data] - try: - self.connection.send(':'.join(packetParts)) - except socket.error: - raise SocketIOPacketError('Could not send packet') - - def set_messageID_callback(self, callback): - 'Set callback that will be called after receiving an acknowledgment' - self.messageID += 1 - self.callbackByMessageID[self.messageID] = callback - return '%s+' % self.messageID - - def get_messageID_callback(self, messageID): - 'Get callback associated with messageID' - try: - callback = self.callbackByMessageID[messageID] - del self.callbackByMessageID[messageID] - return callback - except KeyError: - return - - @property - def has_messageID_callback(self): - return True if self.callbackByMessageID else False - - def get_event_callback(self, channelName, eventName): - 'Get callback associated with channelName and eventName' - _socketIO = self.channelByName[channelName] if channelName else self - try: - return _socketIO.callbackByEvent[eventName] - except KeyError: - pass - - def callback_(*eventArguments): - return _socketIO.namespace.on_(eventName, *eventArguments) - callbackName = 'on_' + eventName.replace(' ', '_') - return getattr(_socketIO.namespace, callbackName, callback_) - - @property - def connected(self): - return self.connection.connected - - def send_heartbeat(self): - try: - self.send_packet(2) - except SocketIOPacketError: - print 'Could not send heartbeat' - pass - - class _RhythmicThread(Thread): 'Execute call every few seconds' @@ -273,23 +151,32 @@ class _ListenerThread(Thread): daemon = True - def __init__(self, _socketIO, get_event_callback, ): + def __init__(self, _socketIO, _channelByPath): super(_ListenerThread, self).__init__() self._socketIO = _socketIO + self._channelByPath = _channelByPath self.done = Event() - self.waitingForCallbacks = Event() + self.waiting = Event() + + def cancel(self): + self.done.set() + + def wait_for_callbacks(self, seconds): + self.waiting.set() + # Block callingThread until listenerThread terminates + self.join(seconds) def run(self): while not self.done.is_set(): try: - code, packetID, channelName, data = self._socketIO.recv_packet() + code, packetID, channelPath, data = self._socketIO.recv_packet() except SocketIOConnectionError, error: print error return except SocketIOPacketError, error: print error continue - get_channel_callback = partial(self._socketIO.get_event_callback, channelName) + channel = self._channelByPath[channelPath] try: delegate = { 0: self.on_disconnect, @@ -303,50 +190,174 @@ class _ListenerThread(Thread): }[code] except KeyError: continue - delegate(packetID, get_channel_callback, data) + delegate(packetID, channel._get_eventCallback, data) - def cancel(self): - self.done.set() + def on_disconnect(self, packetID, get_eventCallback, data): + get_eventCallback('disconnect')() - def wait_for_callbacks(self, seconds): - self.waitingForCallbacks.set() - self.join(seconds) + def on_connect(self, packetID, get_eventCallback, data): + get_eventCallback('connect')() - def on_disconnect(self, packetID, get_channel_callback, data): - get_channel_callback('disconnect')() + def on_heartbeat(self, packetID, get_eventCallback, data): + get_eventCallback('heartbeat')() - def on_connect(self, packetID, get_channel_callback, data): - get_channel_callback('connect')() + def on_message(self, packetID, get_eventCallback, data): + get_eventCallback('message')(data) - def on_heartbeat(self, packetID, get_channel_callback, data): - pass + def on_json(self, packetID, get_eventCallback, data): + get_eventCallback('message')(loads(data)) - def on_message(self, packetID, get_channel_callback, data): - get_channel_callback('message')(data) - - def on_json(self, packetID, get_channel_callback, data): - get_channel_callback('message')(loads(data)) - - def on_event(self, packetID, get_channel_callback, data): + def on_event(self, packetID, get_eventCallback, data): valueByName = loads(data) eventName = valueByName['name'] eventArguments = valueByName['args'] - get_channel_callback(eventName)(*eventArguments) + get_eventCallback(eventName)(*eventArguments) - def on_acknowledgment(self, packetID, get_channel_callback, data): + def on_acknowledgment(self, packetID, get_eventCallback, data): dataParts = data.split('+', 1) messageID = int(dataParts[0]) arguments = loads(dataParts[1]) or [] - callback = self._socketIO.get_messageID_callback(messageID) + callback = self._socketIO.get_messageCallback(messageID) if not callback: return callback(*arguments) - if self.waitingForCallbacks.is_set() and not self._socketIO.has_messageID_callback: + if self.waiting.is_set() and not self._socketIO.has_messageCallback: self.cancel() - def on_error(self, packetID, get_channel_callback, data): + def on_error(self, packetID, get_eventCallback, data): reason, advice = data.split('+', 1) - get_channel_callback('error')(reason, advice) + get_eventCallback('error')(reason, advice) + + +class _SocketIO(object): + 'Low-level interface to remove cyclic references in child threads' + + messageID = 0 + self.callbackByMessageID = {} + self.callbackByEvent = {} + + + def __init__(self, host, port, secure, proxies): + baseURL = '%s:%d/socket.io/%s' % (host, port, PROTOCOL) + targetScheme = 'https' if secure else 'http' + targetURL = '%s://%s/' % (targetScheme, baseURL) + try: + response = urlopen(targetURL, proxies=proxies) + except IOError: # pragma: no cover + raise SocketIOError('Could not start connection') + if 200 != response.getcode(): # pragma: no cover + raise SocketIOError('Could not establish connection') + responseParts = response.readline().split(':') + sessionID = responseParts[0] + heartbeatTimeout = int(responseParts[1]) + # connectionTimeout = int(responseParts[2]) + supportedTransports = responseParts[3].split(',') + if 'websocket' not in supportedTransports: + raise SocketIOError('Could not parse handshake') # pragma: no cover + socketScheme = 'wss' if secure else 'ws' + socketURL = '%s://%s/websocket/%s' % (socketScheme, baseURL, sessionID) + self.connection = create_connection(socketURL) + self.heartbeatInterval = heartbeatTimeout - 2 + + def __del__(self): + self.connection.close() + + def get_channel(self, channelPath): + + def get_channel(self, channelName): + return self.channelByName[channelName] + pass + + def recv_packet(self): + code, packetID, channelName, data = -1, None, None, None + try: + packet = self.connection.recv() + except WebSocketConnectionClosedException: + raise SocketIOConnectionError('Lost connection (Connection closed)') + except socket.timeout: + raise SocketIOConnectionError('Lost connection (Connection timed out)') + try: + packetParts = packet.split(':', 3) + except AttributeError: + raise SocketIOPacketError('Received invalid packet (%s)' % packet) + packetCount = len(packetParts) + if 4 == packetCount: + code, packetID, channelName, data = packetParts + elif 3 == packetCount: + code, packetID, channelName = packetParts + elif 1 == packetCount: # pragma: no cover + code = packetParts[0] + return int(code), packetID, channelName, data + + def send_packet(self, code, channelName='', data='', callback=None): + callbackNumber = self.set_messageCallback(callback) if callback else '' + packetParts = [str(code), callbackNumber, channelName, data] + try: + self.connection.send(':'.join(packetParts)) + except socket.error: + raise SocketIOPacketError('Could not send packet') + + def disconnect(self, channelPath): + self.send_packet(0, channelPath) + + def connect(self, channelPath): + self.send_packet(1, channelPath) + + def send_heartbeat(self): + try: + self.send_packet(2) + except SocketIOPacketError: + print 'Could not send heartbeat' + pass + + def set_messageCallback(self, callback): + 'Set callback that will be called after receiving an acknowledgment' + self.messageID += 1 + self.callbackByMessageID[self.messageID] = callback + return '%s+' % self.messageID + + def get_messageCallback(self, messageID): + try: + callback = self.callbackByMessageID[messageID] + del self.callbackByMessageID[messageID] + return callback + except KeyError: + return + + @property + def has_messageCallback(self): + return True if self.callbackByMessageID else False + + +class Channel(object): + + def __init__(self, _socketIO, Namespace, channelPath): + self._socketIO = _socketIO + self._namespace = Namespace(_socketIO) + self._channelPath = channelPath + self._callbackByEvent = {} + + def on(self, eventName, eventCallback): + self._callbackByEvent[eventName] = eventCallback + + def message(self, messageData, messageCallback=None): + self._socketIO.message(messageData, messageCallback, channelPath=self._channelPath) + + def emit(self, eventName, *eventArguments): + self._socketIO.emit(eventName, *eventArguments, channelPath=self._channelPath) + + def get_namespace(self): + return self._namespace + + def _get_eventCallback(self, eventName): + # Check callbacks defined by on() + try: + return self._callbackByEvent[eventName] + except KeyError: + pass + # Check callbacks defined explicitly or use on_() + defaultCallback = lambda *eventArguments: self.get_namespace().on_(eventName, *eventArguments) + return getattr(self, 'on_' + eventName.replace(' ', '_'), defaultCallback) class SocketIOError(Exception): diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index 53a6cf6..1cbf3b6 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -13,18 +13,26 @@ class TestSocketIO(TestCase): socketIO = SocketIO('localhost', 8000) socketIO.disconnect() self.assertEqual(socketIO.connected, False) + childThreads = [ + socketIO._rhythmicThread, + socketIO._listenerThread, + ] + for childThread in childThreads: + self.assertEqual(True, childThread.done.is_set()) def test_emit(self): - socketIO = SocketIO('localhost', 8000, Namespace) + socketIO = SocketIO('localhost', 8000) + socketIO.define(Namespace) socketIO.emit('aaa') sleep(0.5) - self.assertEqual(socketIO._namespace.payload, '') + self.assertEqual(socketIO.get_namespace().payload, '') def test_emit_with_payload(self): - socketIO = SocketIO('localhost', 8000, Namespace) + socketIO = SocketIO('localhost', 8000) + socketIO.define(Namespace) socketIO.emit('aaa', PAYLOAD) sleep(0.5) - self.assertEqual(socketIO._namespace.payload, PAYLOAD) + self.assertEqual(socketIO.get_namespace().payload, PAYLOAD) def test_emit_with_callback(self): global ON_RESPONSE_CALLED @@ -44,20 +52,21 @@ class TestSocketIO(TestCase): self.assertEqual(ON_RESPONSE_CALLED, True) def test_channels(self): - mainSocket = SocketIO('localhost', 8000, Namespace) - chatSocket = mainSocket.connect('/chat', Namespace) - newsSocket = mainSocket.connect('/news', Namespace) + socketIO = SocketIO('localhost', 8000) + mainSocket = socketIO.define(Namespace) + chatSocket = socketIO.define(Namespace, '/chat') + newsSocket = socketIO.define(Namespace, '/news') newsSocket.emit('aaa', PAYLOAD) sleep(0.5) - self.assertNotEqual(mainSocket._namespace.payload, PAYLOAD) - self.assertNotEqual(chatSocket._namespace.payload, PAYLOAD) - self.assertEqual(newsSocket._namespace.payload, PAYLOAD) + self.assertNotEqual(mainSocket.get_namespace().payload, PAYLOAD) + self.assertNotEqual(chatSocket.get_namespace().payload, PAYLOAD) + self.assertEqual(newsSocket.get_namespace().payload, PAYLOAD) def test_delete(self): socketIO = SocketIO('localhost', 8000) childThreads = [ - socketIO._heartbeatThread, - socketIO._namespaceThread, + socketIO._rhythmicThread, + socketIO._listenerThread, ] del socketIO for childThread in childThreads: From 0368202f2bfc961be48fdfe55b4429b2baf06149 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Tue, 19 Feb 2013 08:10:35 -0800 Subject: [PATCH 008/191] Passed tests --- experiments/callableweakref-tests.py | 170 --------------------------- experiments/callableweakref.py | 52 -------- experiments/t0.py | 6 - experiments/t1.py | 7 -- experiments/t10.py | 16 --- experiments/t11.py | 21 ---- experiments/t12.py | 32 ----- experiments/t2.py | 19 --- experiments/t3.py | 19 --- experiments/t4.py | 26 ---- experiments/t5.py | 30 ----- socketIO_client/__init__.py | 166 ++++++++++++++------------ socketIO_client/tests.py | 3 +- 13 files changed, 92 insertions(+), 475 deletions(-) delete mode 100644 experiments/callableweakref-tests.py delete mode 100644 experiments/callableweakref.py delete mode 100644 experiments/t0.py delete mode 100644 experiments/t1.py delete mode 100644 experiments/t10.py delete mode 100644 experiments/t11.py delete mode 100644 experiments/t12.py delete mode 100644 experiments/t2.py delete mode 100644 experiments/t3.py delete mode 100644 experiments/t4.py delete mode 100644 experiments/t5.py diff --git a/experiments/callableweakref-tests.py b/experiments/callableweakref-tests.py deleted file mode 100644 index c7a95ec..0000000 --- a/experiments/callableweakref-tests.py +++ /dev/null @@ -1,170 +0,0 @@ -# Note 1: The expected behavior of weakref to an instance is that it should -# return None if no other strong references to the instance exist, -# signalling that the instance can be safely garbage collected. -# -# Note 2: The expected behavior of weakref to a method is that it should -# return None if no other strong references to the parent instance exist, -# signalling that the parent instance can be safely garbage collected. -# -# Note 3: The IPython interpreter stores its own references and will -# produce results different from those of the default Python interpreter. -import callableweakref -import weakref -import unittest - - -class TestCallableWeakRef(unittest.TestCase): - - def test_instanceDirectWeakref_dies_on_arrival(self): - 'Assert that weakref works as expected' - instanceDirectWeakref = weakref.ref(Instance()) - assert instanceDirectWeakref() is None - - def test_instanceIndirectWeakref_dies_when_instance_dies(self): - 'Assert that weakref works as expected' - instance = Instance() - instanceIndirectWeakref = weakref.ref(instance) - assert instanceIndirectWeakref() is instance - del instance - assert instanceIndirectWeakref() is None - - def test_boundMethodDirectWeakref_dies_on_arrival(self): - 'Assert that weakref does not work as expected' - instance = Instance() - boundMethodDirectWeakref = weakref.ref(instance.call) - assert boundMethodDirectWeakref() is None # Should be instance - - def test_boundMethodIndirectWeakref_lives_when_instance_dies(self): - 'Assert that weakref works as expected' - instance = Instance() - boundMethod = instance.call - boundMethodIndirectWeakref = weakref.ref(boundMethod) - assert boundMethodIndirectWeakref() is boundMethod - del instance - assert boundMethodIndirectWeakref() is boundMethod - del boundMethod - assert boundMethodIndirectWeakref() is None - - def test_unboundMethodDirectWeakref_dies_on_arrival(self): - 'Assert that weakref does not work as expected' - unboundMethodDirectWeakref = weakref.ref(Instance.call) - assert unboundMethodDirectWeakref() is None # Should be Instance.call - - def test_unboundMethodIndirectWeakref_dies_when_unboundMethod_dies(self): - 'Assert that weakref works as expected' - unboundMethod = Instance.call - unboundMethodIndirectWeakref = weakref.ref(unboundMethod) - assert unboundMethodIndirectWeakref() is unboundMethod - del unboundMethod - assert unboundMethodIndirectWeakref() is None - - def test_classFunctionDirectWeakref_dies_on_arrival(self): - 'Assert that weakref works as expected' - classFunctionDirectWeakref = weakref.ref(Instance()) - assert classFunctionDirectWeakref() is None - - def test_classFunctionIndirectWeakref_dies_when_classFunction_dies(self): - 'Assert that weakref works as expected' - classFunction = Instance() - classFunctionIndirectWeakref = weakref.ref(classFunction) - assert classFunctionIndirectWeakref() is classFunction - del classFunction - assert classFunctionIndirectWeakref() is None - - def test_normalFunctionDirectWeakref_dies_when_normal_function_dies(self): - 'Assert that weakref works as expected' - call = lambda: None - normalFunctionDirectWeakref = weakref.ref(call) - assert normalFunctionDirectWeakref() is call - del call - assert normalFunctionDirectWeakref() is None - - def test_normalFunctionIndirectWeakref_dies_when_normal_function_dies(self): - 'Assert that weakref works as expected' - call = lambda: None - normalFunction = call - normalFunctionIndirectWeakref = weakref.ref(normalFunction) - assert normalFunctionIndirectWeakref() is normalFunction - del normalFunction - assert normalFunctionIndirectWeakref() is call - del call - assert normalFunctionIndirectWeakref() is None - - # Assert that CallableWeakref works as expected for callables - - def test_boundMethodDirectCallableWeakref_dies_when_instance_dies(self): - instance = Instance() - boundMethodDirectCallableWeakref = callableweakref.ref(instance.call) - self.assertEqual(boundMethodDirectCallableWeakref(), instance.call) - del instance - assert boundMethodDirectCallableWeakref() is None - - def test_boundMethodIndirectCallableWeakref_dies_when_instance_dies(self): - instance = Instance() - boundMethod = instance.call - boundMethodIndirectCallableWeakref = callableweakref.ref(boundMethod) - self.assertEqual(boundMethodIndirectCallableWeakref(), boundMethod) - del instance - self.assertEqual(boundMethodIndirectCallableWeakref(), boundMethod) - del boundMethod - assert boundMethodIndirectCallableWeakref() is None - - def test_unboundMethodDirectCallableWeakref_lives_on_arrival(self): - unboundMethodDirectCallableWeakref = callableweakref.ref(Instance.call) - self.assertEqual(unboundMethodDirectCallableWeakref(), Instance.call) - - def test_unboundMethodIndirectCallableWeakref_lives_on_arrival(self): - unboundMethod = Instance.call - unboundMethodIndirectCallableWeakref = callableweakref.ref(unboundMethod) - self.assertEqual(unboundMethodIndirectCallableWeakref(), unboundMethod) - - def test_classMethodIndirectCallableWeakref_lives_on_arrival(self): - classMethod = Instance.call_classmethod - classMethodIndirectCallableWeakref = callableweakref.ref(classMethod) - self.assertEqual(classMethodIndirectCallableWeakref(), classMethod) - - def test_staticMethodIndirectCallableWeakref_lives_on_arrival(self): - staticMethod = Instance.call_staticmethod - staticMethodIndirectCallableWeakref = callableweakref.ref(staticMethod) - self.assertEqual(staticMethodIndirectCallableWeakref(), staticMethod) - - def test_classFunctionDirectCallableWeakref_dies_on_arrival(self): - classFunctionDirectCallableWeakref = callableweakref.ref(Instance()) - assert classFunctionDirectCallableWeakref() is None - - def test_classFunctionIndirectCallableWeakref_dies_when_classFunction_dies(self): - classFunction = Instance() - classFunctionIndirectCallableWeakref = callableweakref.ref(classFunction) - self.assertEqual(classFunctionIndirectCallableWeakref(), classFunction.__call__) - del classFunction - assert classFunctionIndirectCallableWeakref() is None - - def test_normalFunctionDirectCallableWeakref_lives_on_arrival(self): - call = lambda: None - normalFunctionDirectCallableWeakref = callableweakref.ref(call) - self.assertEqual(normalFunctionDirectCallableWeakref(), call) - - def test_normalFunctionIndirectCallableWeakref_lives_on_arrival(self): - call = lambda: None - normalFunction = call - normalFunctionIndirectWeakref = callableweakref.ref(normalFunction) - self.assertEqual(normalFunctionIndirectWeakref(), normalFunction) - del normalFunction - self.assertEqual(normalFunctionIndirectWeakref(), call) - - -class Instance(object): - - def __call__(self): - pass - - def call(self): - pass - - @classmethod - def call_classmethod(Class): - pass - - @staticmethod - def call_staticmethod(): - pass diff --git a/experiments/callableweakref.py b/experiments/callableweakref.py deleted file mode 100644 index 788592c..0000000 --- a/experiments/callableweakref.py +++ /dev/null @@ -1,52 +0,0 @@ -import types -import weakref - - -def ref(function): - return CallableWeakReference(function) - - -class CallableWeakReference(object): - - def __init__(self, function): - 'Create a weak reference to a callable' - try: - if function.im_self: - # We have a bound method or class method - self._reference = weakref.ref(function.im_self) - else: - # We have an unbound method - self._reference = None - self._function = function.im_func - self._class = function.im_class - except AttributeError: - try: - function.func_code - # We have a normal function or static method - self._reference = None - self._function = function - self._class = None - except AttributeError: - function = function.__call__ - # We have a class masquerading as a function - self._reference = weakref.ref(function.im_self) - self._function = function.im_func - self._class = function.im_class - - def __call__(self): - if self.dead: - return - if self._reference: - # We have a bound method - return types.MethodType(self._function, self._reference(), self._class) - elif self._class: - return types.MethodType(self._function, None, self._class) - else: - return self._function - - @property - def dead(self): - if self._reference and not self._reference(): - # We have a bound method whose parent reference has died - return True - return False diff --git a/experiments/t0.py b/experiments/t0.py deleted file mode 100644 index 3980052..0000000 --- a/experiments/t0.py +++ /dev/null @@ -1,6 +0,0 @@ -from socketIO_client import SocketIO - -s = SocketIO('localhost', 8000) -del s -from time import sleep -sleep(3) diff --git a/experiments/t1.py b/experiments/t1.py deleted file mode 100644 index 0955ed6..0000000 --- a/experiments/t1.py +++ /dev/null @@ -1,7 +0,0 @@ -class O(object): - - def __del__(self): - print '__del__()' - - -o = O() diff --git a/experiments/t10.py b/experiments/t10.py deleted file mode 100644 index 24733d9..0000000 --- a/experiments/t10.py +++ /dev/null @@ -1,16 +0,0 @@ -class A(object): - - def __init__(self): - self.b = B(self) - - def __del__(self): - print '__del__()' - - -class B(object): - - def __init__(self, a): - self.a = a - - -a = A() diff --git a/experiments/t11.py b/experiments/t11.py deleted file mode 100644 index 11121de..0000000 --- a/experiments/t11.py +++ /dev/null @@ -1,21 +0,0 @@ -class A(object): - - def __init__(self): - self.c = Common() - self.b = B(self.c) - - def __del__(self): - print '__del__()' - - -class B(object): - - def __init__(self, c): - self.c = c - - -class Common(object): - pass - - -a = A() diff --git a/experiments/t12.py b/experiments/t12.py deleted file mode 100644 index b4e9bb3..0000000 --- a/experiments/t12.py +++ /dev/null @@ -1,32 +0,0 @@ -# Will the destructor of a class be called if its two children have cyclic -# references to each other? Yes. - - -class Parent(object): - - def __init__(self): - self.child = Child() - - def __del__(self): - print 'Parent.__del__()' - - -class Child(object): - - def __init__(self): - self.grandChild = GrandChild(self) - - def __del__(self): - print 'Child.__del__()' - - -class GrandChild(object): - - def __init__(self, parent): - self.parent = parent - - def __del__(self): - print 'GrandChild.__del__()' - - -parent = Parent() diff --git a/experiments/t2.py b/experiments/t2.py deleted file mode 100644 index 3ad124c..0000000 --- a/experiments/t2.py +++ /dev/null @@ -1,19 +0,0 @@ -import weakref - - -class O(object): - - def __init__(self): - self.p = P(self) - - def __del__(self): - print '__del__()' - - -class P(object): - - def __init__(self, parent): - self.parent = weakref.ref(parent) - - -o = O() diff --git a/experiments/t3.py b/experiments/t3.py deleted file mode 100644 index 2de3123..0000000 --- a/experiments/t3.py +++ /dev/null @@ -1,19 +0,0 @@ -class O(object): - - def __init__(self): - self.p = P(self.f) - - def __del__(self): - print '__del__()' - - def f(self): - pass - - -class P(object): - - def __init__(self, parentMethod): - self.parentMethod = parentMethod - - -o = O() diff --git a/experiments/t4.py b/experiments/t4.py deleted file mode 100644 index da46dfe..0000000 --- a/experiments/t4.py +++ /dev/null @@ -1,26 +0,0 @@ -import weakref - - -class O(object): - - def __init__(self): - self.p = P(self.f) - - def __del__(self): - print '__del__()' - - def f(self): - pass - - -class P(object): - - def __init__(self, parentMethod): - self.parentMethod = weakref.ref(parentMethod) - - def show(self): - print self.parentMethod - - -o = O() -o.p.show() # Dead on arrival diff --git a/experiments/t5.py b/experiments/t5.py deleted file mode 100644 index a2ff710..0000000 --- a/experiments/t5.py +++ /dev/null @@ -1,30 +0,0 @@ -import callableweakref - - -class O(object): - - def __init__(self): - self.p = P(self.f) - - def __del__(self): - print '__del__()' - - def f(self): - print 'f()' - - -class P(object): - - def __init__(self, parentMethod): - self.parentMethod = callableweakref.ref(parentMethod) - - def show(self): - print self.parentMethod - - def run(self): - self.parentMethod()() - - -o = O() -o.p.show() -o.p.run() diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index b247fb4..b850b1c 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -47,9 +47,10 @@ class SocketIO(object): def __init__(self, host, port, secure=False, proxies=None): self._socketIO = _SocketIO(host, port, secure, proxies) self._channelByPath = {} + self.define(BaseNamespace) # Define default namespace self._rhythmicThread = _RhythmicThread( - self._socketIO.heartbeatTimeout, + self._socketIO.heartbeatInterval, self._socketIO.send_heartbeat) self._rhythmicThread.start() @@ -65,14 +66,15 @@ class SocketIO(object): self.disconnect() def __del__(self): - self.disconnect() + self.disconnect(force=True) @property def connected(self): - return self._socketIO.connection.connected + return self._socketIO.connected - def disconnect(self, channelPath=''): - self._socketIO.disconnect(channelPath) + def disconnect(self, channelPath='', force=False): + if self.connected: + self._socketIO.disconnect(channelPath, force) if channelPath: del self._channelByPath[channelPath] else: @@ -88,37 +90,23 @@ class SocketIO(object): def get_namespace(self, channelPath=''): return self._channelByPath[channelPath].get_namespace() - def on(self, eventName, eventCallback): - self._callbackByEvent[eventName] = callback + def on(self, eventName, eventCallback, channelPath=''): + return self._channelByPath[channelPath].on(eventName, eventCallback) - def message(self, messageData, callback=None, channelName=''): - if isinstance(messageData, basestring): - code = 3 - data = messageData - else: - code = 4 - data = dumps(messageData) - self._send_packet(code, channelName, data, callback) + def message(self, messageData, messageCallback=None, channelPath=''): + self._socketIO.message(messageData, messageCallback, channelPath) - def emit(self, eventName, *eventArguments): - self._socketIO.emit(eventName, *eventArguments) - code = 5 - callback = None - if eventArguments and callable(eventArguments[-1]): - callback = eventArguments[-1] - eventArguments = eventArguments[:-1] - channelName = eventKeywords.get('channelName', '') - data = dumps(dict(name=eventName, args=eventArguments)) - self._send_packet(code, channelName, data, callback) + def emit(self, eventName, *eventArguments, **eventKeywords): + self._socketIO.emit(eventName, *eventArguments, **eventKeywords) def wait(self, seconds=None, forCallbacks=False): if forCallbacks: - self.__listenerThread.wait_for_callbacks(seconds) + self._listenerThread.wait_for_callbacks(seconds) elif seconds: sleep(seconds) else: try: - while self._socketIO.connected: + while self.connected: sleep(1) except KeyboardInterrupt: pass @@ -132,7 +120,7 @@ class _RhythmicThread(Thread): def __init__(self, intervalInSeconds, call, *args, **kw): super(_RhythmicThread, self).__init__() self.intervalInSeconds = intervalInSeconds - self.call + self.call = call self.args = args self.kw = kw self.done = Event() @@ -176,8 +164,8 @@ class _ListenerThread(Thread): except SocketIOPacketError, error: print error continue - channel = self._channelByPath[channelPath] try: + channel = self._channelByPath[channelPath] delegate = { 0: self.on_disconnect, 1: self.on_connect, @@ -199,7 +187,8 @@ class _ListenerThread(Thread): get_eventCallback('connect')() def on_heartbeat(self, packetID, get_eventCallback, data): - get_eventCallback('heartbeat')() + # get_eventCallback('heartbeat')() + pass def on_message(self, packetID, get_eventCallback, data): get_eventCallback('message')(data) @@ -217,10 +206,10 @@ class _ListenerThread(Thread): dataParts = data.split('+', 1) messageID = int(dataParts[0]) arguments = loads(dataParts[1]) or [] - callback = self._socketIO.get_messageCallback(messageID) - if not callback: + messageCallback = self._socketIO.get_messageCallback(messageID) + if not messageCallback: return - callback(*arguments) + messageCallback(*arguments) if self.waiting.is_set() and not self._socketIO.has_messageCallback: self.cancel() @@ -233,9 +222,6 @@ class _SocketIO(object): 'Low-level interface to remove cyclic references in child threads' messageID = 0 - self.callbackByMessageID = {} - self.callbackByEvent = {} - def __init__(self, host, port, secure, proxies): baseURL = '%s:%d/socket.io/%s' % (host, port, PROTOCOL) @@ -258,47 +244,18 @@ class _SocketIO(object): socketURL = '%s://%s/websocket/%s' % (socketScheme, baseURL, sessionID) self.connection = create_connection(socketURL) self.heartbeatInterval = heartbeatTimeout - 2 + self.callbackByMessageID = {} def __del__(self): - self.connection.close() + self.disconnect(force=True) - def get_channel(self, channelPath): - - def get_channel(self, channelName): - return self.channelByName[channelName] - pass - - def recv_packet(self): - code, packetID, channelName, data = -1, None, None, None - try: - packet = self.connection.recv() - except WebSocketConnectionClosedException: - raise SocketIOConnectionError('Lost connection (Connection closed)') - except socket.timeout: - raise SocketIOConnectionError('Lost connection (Connection timed out)') - try: - packetParts = packet.split(':', 3) - except AttributeError: - raise SocketIOPacketError('Received invalid packet (%s)' % packet) - packetCount = len(packetParts) - if 4 == packetCount: - code, packetID, channelName, data = packetParts - elif 3 == packetCount: - code, packetID, channelName = packetParts - elif 1 == packetCount: # pragma: no cover - code = packetParts[0] - return int(code), packetID, channelName, data - - def send_packet(self, code, channelName='', data='', callback=None): - callbackNumber = self.set_messageCallback(callback) if callback else '' - packetParts = [str(code), callbackNumber, channelName, data] - try: - self.connection.send(':'.join(packetParts)) - except socket.error: - raise SocketIOPacketError('Could not send packet') - - def disconnect(self, channelPath): - self.send_packet(0, channelPath) + def disconnect(self, channelPath='', force=False): + if not self.connected: + return + if channelPath: + self.send_packet(0, channelPath) + elif not force: + self.connection.close() def connect(self, channelPath): self.send_packet(1, channelPath) @@ -310,6 +267,26 @@ class _SocketIO(object): print 'Could not send heartbeat' pass + def message(self, messageData, messageCallback, channelPath): + if isinstance(messageData, basestring): + code = 3 + data = messageData + else: + code = 4 + data = dumps(messageData) + self.send_packet(code, channelPath, data, messageCallback) + + def emit(self, eventName, *eventArguments, **eventKeywords): + code = 5 + if eventArguments and callable(eventArguments[-1]): + messageCallback = eventArguments[-1] + eventArguments = eventArguments[:-1] + else: + messageCallback = None + channelPath = eventKeywords.get('channelPath', '') + data = dumps(dict(name=eventName, args=eventArguments)) + self.send_packet(code, channelPath, data, messageCallback) + def set_messageCallback(self, callback): 'Set callback that will be called after receiving an acknowledgment' self.messageID += 1 @@ -328,6 +305,41 @@ class _SocketIO(object): def has_messageCallback(self): return True if self.callbackByMessageID else False + def recv_packet(self): + code, packetID, channelPath, data = -1, None, None, None + try: + packet = self.connection.recv() + except WebSocketConnectionClosedException: + raise SocketIOConnectionError('Lost connection (Connection closed)') + except socket.timeout: + raise SocketIOConnectionError('Lost connection (Connection timed out)') + except socket.error: + raise SocketIOConnectionError('Lost connection') + try: + packetParts = packet.split(':', 3) + except AttributeError: + raise SocketIOPacketError('Received invalid packet (%s)' % packet) + packetCount = len(packetParts) + if 4 == packetCount: + code, packetID, channelPath, data = packetParts + elif 3 == packetCount: + code, packetID, channelPath = packetParts + elif 1 == packetCount: # pragma: no cover + code = packetParts[0] + return int(code), packetID, channelPath, data + + def send_packet(self, code, channelPath='', data='', messageCallback=None): + callbackNumber = self.set_messageCallback(messageCallback) if messageCallback else '' + packetParts = [str(code), callbackNumber, channelPath, data] + try: + self.connection.send(':'.join(packetParts)) + except socket.error: + raise SocketIOPacketError('Could not send packet') + + @property + def connected(self): + return self.connection.connected + class Channel(object): @@ -356,8 +368,10 @@ class Channel(object): except KeyError: pass # Check callbacks defined explicitly or use on_() - defaultCallback = lambda *eventArguments: self.get_namespace().on_(eventName, *eventArguments) - return getattr(self, 'on_' + eventName.replace(' ', '_'), defaultCallback) + eventNamespace = self.get_namespace() + callbackName = 'on_' + eventName.replace(' ', '_') + defaultCallback = lambda *eventArguments: eventNamespace.on_(eventName, *eventArguments) + return getattr(eventNamespace, callbackName, defaultCallback) class SocketIOError(Exception): diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index 1cbf3b6..6442e1e 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -12,7 +12,7 @@ class TestSocketIO(TestCase): def test_disconnect(self): socketIO = SocketIO('localhost', 8000) socketIO.disconnect() - self.assertEqual(socketIO.connected, False) + self.assertEqual(False, socketIO.connected) childThreads = [ socketIO._rhythmicThread, socketIO._listenerThread, @@ -78,6 +78,7 @@ class Namespace(BaseNamespace): payload = None def on_ddd(self, data=''): + print '[Event] ddd(%s)' % data self.payload = data From 4f39d1f92282f84e43df8fc026a804a6bcac13fb Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 21 Feb 2013 09:08:58 -0800 Subject: [PATCH 009/191] Added tests for Channel.message() --- socketIO_client/__init__.py | 35 ++++++++++++++++++++++------------- socketIO_client/tests.py | 28 +++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index b850b1c..920fa48 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -87,11 +87,14 @@ class SocketIO(object): self._channelByPath[channelPath] = channel return channel + def get_channel(self, channelPath=''): + return self._channelByPath[channelPath] + def get_namespace(self, channelPath=''): - return self._channelByPath[channelPath].get_namespace() + return self.get_channel(channelPath).get_namespace() def on(self, eventName, eventCallback, channelPath=''): - return self._channelByPath[channelPath].on(eventName, eventCallback) + return self.get_channel(channelPath).on(eventName, eventCallback) def message(self, messageData, messageCallback=None, channelPath=''): self._socketIO.message(messageData, messageCallback, channelPath) @@ -166,17 +169,22 @@ class _ListenerThread(Thread): continue try: channel = self._channelByPath[channelPath] + except KeyError: + print 'Received unexpected channelPath (%s)' % channelPath + continue + try: delegate = { - 0: self.on_disconnect, - 1: self.on_connect, - 2: self.on_heartbeat, - 3: self.on_message, - 4: self.on_json, - 5: self.on_event, - 6: self.on_acknowledgment, - 7: self.on_error, + '0': self.on_disconnect, + '1': self.on_connect, + '2': self.on_heartbeat, + '3': self.on_message, + '4': self.on_json, + '5': self.on_event, + '6': self.on_acknowledgment, + '7': self.on_error, }[code] except KeyError: + print 'Received unexpected code (%s)' % code continue delegate(packetID, channel._get_eventCallback, data) @@ -250,6 +258,7 @@ class _SocketIO(object): self.disconnect(force=True) def disconnect(self, channelPath='', force=False): + 'Set force=True to skip closing the websocket' if not self.connected: return if channelPath: @@ -306,7 +315,6 @@ class _SocketIO(object): return True if self.callbackByMessageID else False def recv_packet(self): - code, packetID, channelPath, data = -1, None, None, None try: packet = self.connection.recv() except WebSocketConnectionClosedException: @@ -320,13 +328,14 @@ class _SocketIO(object): except AttributeError: raise SocketIOPacketError('Received invalid packet (%s)' % packet) packetCount = len(packetParts) + code, packetID, channelPath, data = None, None, None, None if 4 == packetCount: code, packetID, channelPath, data = packetParts elif 3 == packetCount: code, packetID, channelPath = packetParts - elif 1 == packetCount: # pragma: no cover + elif 1 == packetCount: code = packetParts[0] - return int(code), packetID, channelPath, data + return code, packetID, channelPath, data def send_packet(self, code, channelPath='', data='', messageCallback=None): callbackNumber = self.set_messageCallback(messageCallback) if messageCallback else '' diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index 6442e1e..a662c45 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -24,14 +24,14 @@ class TestSocketIO(TestCase): socketIO = SocketIO('localhost', 8000) socketIO.define(Namespace) socketIO.emit('aaa') - sleep(0.5) + sleep(0.1) self.assertEqual(socketIO.get_namespace().payload, '') def test_emit_with_payload(self): socketIO = SocketIO('localhost', 8000) socketIO.define(Namespace) socketIO.emit('aaa', PAYLOAD) - sleep(0.5) + sleep(0.1) self.assertEqual(socketIO.get_namespace().payload, PAYLOAD) def test_emit_with_callback(self): @@ -42,13 +42,21 @@ class TestSocketIO(TestCase): socketIO.wait(forCallbacks=True) self.assertEqual(ON_RESPONSE_CALLED, True) + def test_message(self): + global ON_RESPONSE_CALLED + ON_RESPONSE_CALLED = False + socketIO = SocketIO('localhost', 8000) + socketIO.message(PAYLOAD, on_response) + socketIO.wait(forCallbacks=True) + self.assertEqual(ON_RESPONSE_CALLED, True) + def test_events(self): global ON_RESPONSE_CALLED ON_RESPONSE_CALLED = False socketIO = SocketIO('localhost', 8000) socketIO.on('ddd', on_response) socketIO.emit('aaa', PAYLOAD) - sleep(0.5) + sleep(0.1) self.assertEqual(ON_RESPONSE_CALLED, True) def test_channels(self): @@ -56,12 +64,22 @@ class TestSocketIO(TestCase): mainSocket = socketIO.define(Namespace) chatSocket = socketIO.define(Namespace, '/chat') newsSocket = socketIO.define(Namespace, '/news') - newsSocket.emit('aaa', PAYLOAD) - sleep(0.5) self.assertNotEqual(mainSocket.get_namespace().payload, PAYLOAD) self.assertNotEqual(chatSocket.get_namespace().payload, PAYLOAD) + self.assertNotEqual(newsSocket.get_namespace().payload, PAYLOAD) + newsSocket.emit('aaa', PAYLOAD) + sleep(0.1) self.assertEqual(newsSocket.get_namespace().payload, PAYLOAD) + def test_channels_with_callback(self): + global ON_RESPONSE_CALLED + ON_RESPONSE_CALLED = False + socketIO = SocketIO('localhost', 8000) + mainSocket = socketIO.get_channel() + mainSocket.message(PAYLOAD, on_response) + sleep(0.1) + self.assertEqual(ON_RESPONSE_CALLED, True) + def test_delete(self): socketIO = SocketIO('localhost', 8000) childThreads = [ From efc79c9ad3bf317997a56832fdf7c4e299602c9f Mon Sep 17 00:00:00 2001 From: Lukas Klein Date: Mon, 11 Mar 2013 23:11:41 +0100 Subject: [PATCH 010/191] Fallback if args is empty --- socketIO_client/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index e32996d..c6b3ce0 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -277,7 +277,7 @@ class ListenerThread(Thread): def on_event(self, packetID, channelName, data): valueByName = loads(data) eventName = valueByName['name'] - eventArguments = valueByName['args'] + eventArguments = valueByName.get('args', []) callback = self.get_callback(channelName, eventName) callback(*eventArguments) From f10f59204fc8e6217d1e787e2a20a6c5a0a5b4ba Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 8 Apr 2013 16:37:03 +0100 Subject: [PATCH 011/191] support python setup.py test --- serve_tests.py | 29 ----------------------------- setup.py | 1 + socketIO_client/tests.py | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 29 deletions(-) delete mode 100755 serve_tests.py diff --git a/serve_tests.py b/serve_tests.py deleted file mode 100755 index 47c946a..0000000 --- a/serve_tests.py +++ /dev/null @@ -1,29 +0,0 @@ -'Launch this server in another terminal window before running tests' -from socketio import socketio_manage -from socketio.namespace import BaseNamespace -from socketio.server import SocketIOServer - - -class Namespace(BaseNamespace): - - def on_aaa(self, *args): - self.socket.send_packet(dict( - type='event', - name='ddd', - args=args, - endpoint=self.ns_name)) - - -class Application(object): - - def __call__(self, environ, start_response): - socketio_manage(environ, { - '': Namespace, - '/chat': Namespace, - '/news': Namespace, - }) - - -if __name__ == '__main__': - socketIOServer = SocketIOServer(('0.0.0.0', 8000), Application()) - socketIOServer.serve_forever() diff --git a/setup.py b/setup.py index 2651b7a..03141e4 100755 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ setup( 'anyjson', 'websocket-client', ], + test_suite="nose.collector", packages=find_packages(), include_package_data=True, zip_safe=True) diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index a85c48e..96e5863 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -3,6 +3,46 @@ from time import sleep from unittest import TestCase +from socketio import socketio_manage +from socketio.namespace import BaseNamespace as SIOBaseNameSpace +from socketio.server import SocketIOServer + +from multiprocessing import Process + + +class SIONamespace(SIOBaseNameSpace): + + def on_aaa(self, *args): + self.socket.send_packet(dict( + type='event', + name='ddd', + args=args, + endpoint=self.ns_name)) + + +class Application(object): + + def __call__(self, environ, start_response): + socketio_manage(environ, { + '': SIONamespace, + '/chat': SIONamespace, + '/news': SIONamespace, + }) + + +socketIOServer = SocketIOServer(('0.0.0.0', 8000), Application()) + +p = Process(target=socketIOServer.serve_forever) + + +def setup_module(module): + p.start() + + +def teardown_module(module): + p.terminate() + + PAYLOAD = {'bbb': 'ccc'} ON_RESPONSE_CALLED = False From f1d0a49fb3999bf39faf6552c47f7b4fe8dcb5f9 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 8 Apr 2013 16:39:37 +0100 Subject: [PATCH 012/191] add travis configuration --- .travis.yml | 11 +++++++++++ requirements.txt | 7 +++++++ 2 files changed, 18 insertions(+) create mode 100644 .travis.yml create mode 100644 requirements.txt diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0090a78 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: python +python: + - 2.6 + - 2.7 + - 3.3 +matrix: + allow_failures: + - python: 3.3 +install: + - pip install -r requirements.txt --use-mirrors +script: python setup.py test diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5393f02 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +#Install +anyjson +websocket-client +#Test +nose +nose-cov +gevent-socketio From b9f7bed8e94d3e80933b131ef4595e17b1e36142 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 8 Apr 2013 16:46:51 +0100 Subject: [PATCH 013/191] add missing requirements --- .travis.yml | 1 + requirements.txt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0090a78..96f83ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,6 @@ matrix: allow_failures: - python: 3.3 install: + - sudo apt-get build-dep -qq python-gevent - pip install -r requirements.txt --use-mirrors script: python setup.py test diff --git a/requirements.txt b/requirements.txt index 5393f02..296a9a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ websocket-client nose nose-cov gevent-socketio +multiprocessing + From 2d0f636b5d23f1712aff09c687471fb6c812ca08 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 8 Apr 2013 17:04:09 +0100 Subject: [PATCH 014/191] multiprocessing is a standard lib for 2.6+ --- requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 296a9a8..5393f02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,3 @@ websocket-client nose nose-cov gevent-socketio -multiprocessing - From 187c6fbca10762074d2feabe7cfcd64fa9dbbf7d Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 14 Apr 2013 23:23:30 -0700 Subject: [PATCH 015/191] Reviewed code --- .gitignore | 9 +++++---- LICENSE | 2 +- TODO.rst | 13 +++++++++---- socketIO_client/__init__.py | 10 +++++----- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 15e22ab..6cf8c00 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ *~ +*.sw[op] +*.py[cod] +*.egg *.egg-info -*.pyc -*.swo -*.swp -.coverage build dist +sdist +.coverage diff --git a/LICENSE b/LICENSE index 2e7adea..4e68cea 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012 Roy Hyunjin Han and contributors +Copyright (c) 2013 Roy Hyunjin Han and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/TODO.rst b/TODO.rst index b15199b..4da9fa4 100644 --- a/TODO.rst +++ b/TODO.rst @@ -1,9 +1,14 @@ ++ Identify what needs to be done + + check __init__.py + + check tests.py + + check serve_tests.py + Fix unittests + Fix exceptions when websocket server disappears -Fix thread exceptions - Finish creating low-level _SocketIO to eliminate cyclic references - Move namespace and callback handling to _ListenerThread - ++ Fix thread exceptions + + Finish creating low-level _SocketIO to eliminate cyclic references + + Move namespace and callback handling to _ListenerThread += Resolve pull requests +Resolve issues Integrate Zac's fork #6 Integrate Sajal's fork #7 Integrate Francis's fork #10 diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 920fa48..e5b50be 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,15 +1,16 @@ import socket from anyjson import dumps, loads -from threading import Event, Thread +from threading import Thread, Event from time import sleep from urllib import urlopen from websocket import WebSocketConnectionClosedException, create_connection -PROTOCOL = 1 # SocketIO protocol version +PROTOCOL = 1 # socket.io protocol version class BaseNamespace(object): # pragma: no cover + 'Define socket.io behavior' def __init__(self, _socketIO): self._socketIO = _socketIO @@ -138,7 +139,7 @@ class _RhythmicThread(Thread): class _ListenerThread(Thread): - 'Process messages from SocketIO server' + 'Process messages from socket.io server' daemon = True @@ -195,7 +196,6 @@ class _ListenerThread(Thread): get_eventCallback('connect')() def on_heartbeat(self, packetID, get_eventCallback, data): - # get_eventCallback('heartbeat')() pass def on_message(self, packetID, get_eventCallback, data): @@ -207,7 +207,7 @@ class _ListenerThread(Thread): def on_event(self, packetID, get_eventCallback, data): valueByName = loads(data) eventName = valueByName['name'] - eventArguments = valueByName['args'] + eventArguments = valueByName.get('args', []) get_eventCallback(eventName)(*eventArguments) def on_acknowledgment(self, packetID, get_eventCallback, data): From ee91f821719a313f071ca51d10f6b8f116ba40ec Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 15 Apr 2013 01:15:21 -0700 Subject: [PATCH 016/191] Preparing to merge changes from zratic --- README.rst | 15 +++--- serve_tests.py | 15 ++---- socketIO_client/__init__.py | 3 +- socketIO_client/tests.py | 93 +++++++++++++++---------------------- 4 files changed, 52 insertions(+), 74 deletions(-) diff --git a/README.rst b/README.rst index a1984c2..93571c7 100644 --- a/README.rst +++ b/README.rst @@ -86,11 +86,11 @@ Define standard events. :: def on_disconnect(self): print '[Disconnected]' - def on_error(self, name, message): - print '[Error] %s: %s' % (name, message) + def on_error(self, reason, advice): + print '[Error] %s' % advice - def on_message(self, id, message): - print '[Message] %s: %s' % (id, message) + def on_message(self, messageData): + print '[Message] %s' % messageData socketIO = SocketIO('localhost', 8000) socketIO.define(Namespace) @@ -100,17 +100,17 @@ Define different namespaces on a single socket. :: from socketIO_client import SocketIO, BaseNamespace - class MainNamespace(Channel): + class MainNamespace(BaseNamespace): def on_aaa(self, *args): print 'aaa', args - class ChatNamespace(Channel): + class ChatNamespace(BaseNamespace): def on_bbb(self, *args): print 'bbb', args - class NewsNamespace(Channel): + class NewsNamespace(BaseNamespace): def on_ccc(self, *args): print 'ccc', args @@ -129,6 +129,7 @@ Open secure websockets (HTTPS / WSS) behind a proxy. :: secure=True, proxies={'http': 'http://proxy.example.com:8080'}) + License ------- This software is available under the MIT License. diff --git a/serve_tests.py b/serve_tests.py index faea52e..24df950 100644 --- a/serve_tests.py +++ b/serve_tests.py @@ -1,5 +1,4 @@ 'Launch this server in another terminal window before running tests' -import sys try: from socketio import socketio_manage from socketio.namespace import BaseNamespace @@ -8,17 +7,13 @@ except ImportError: from setuptools.command import easy_install easy_install.main(['-U', 'gevent-socketio']) print('\nPlease run the script again to launch the test server.') - sys.exit(1) + import sys; sys.exit(1) class Namespace(BaseNamespace): def on_aaa(self, *args): - self.socket.send_packet(dict( - type='event', - name='ddd', - args=args, - endpoint=self.ns_name)) + self.emit('aaa_response', *args) class Application(object): @@ -32,7 +27,7 @@ class Application(object): if __name__ == '__main__': - port = 8000 - print 'Starting server at port %s' % port - socketIOServer = SocketIOServer(('0.0.0.0', port), Application()) + from socketIO_client.tests import PORT + print 'Starting server at port %s' % PORT + socketIOServer = SocketIOServer(('0.0.0.0', PORT), Application()) socketIOServer.serve_forever() diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index e5b50be..b3268ca 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -286,7 +286,6 @@ class _SocketIO(object): self.send_packet(code, channelPath, data, messageCallback) def emit(self, eventName, *eventArguments, **eventKeywords): - code = 5 if eventArguments and callable(eventArguments[-1]): messageCallback = eventArguments[-1] eventArguments = eventArguments[:-1] @@ -294,7 +293,7 @@ class _SocketIO(object): messageCallback = None channelPath = eventKeywords.get('channelPath', '') data = dumps(dict(name=eventName, args=eventArguments)) - self.send_packet(code, channelPath, data, messageCallback) + self.send_packet(5, channelPath, data, messageCallback) def set_messageCallback(self, callback): 'Set callback that will be called after receiving an acknowledgment' diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index a662c45..cbeaca9 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -3,67 +3,53 @@ from time import sleep from unittest import TestCase -PAYLOAD = {'bbb': 'ccc'} ON_RESPONSE_CALLED = False +PORT = 8000 +PAYLOAD = {'xxx': 'yyy'} class TestSocketIO(TestCase): - def test_disconnect(self): - socketIO = SocketIO('localhost', 8000) - socketIO.disconnect() - self.assertEqual(False, socketIO.connected) - childThreads = [ - socketIO._rhythmicThread, - socketIO._listenerThread, - ] - for childThread in childThreads: - self.assertEqual(True, childThread.done.is_set()) - - def test_emit(self): - socketIO = SocketIO('localhost', 8000) - socketIO.define(Namespace) - socketIO.emit('aaa') - sleep(0.1) - self.assertEqual(socketIO.get_namespace().payload, '') - - def test_emit_with_payload(self): - socketIO = SocketIO('localhost', 8000) - socketIO.define(Namespace) - socketIO.emit('aaa', PAYLOAD) - sleep(0.1) - self.assertEqual(socketIO.get_namespace().payload, PAYLOAD) - - def test_emit_with_callback(self): + def setUp(self): global ON_RESPONSE_CALLED ON_RESPONSE_CALLED = False - socketIO = SocketIO('localhost', 8000) - socketIO.emit('aaa', PAYLOAD, on_response) - socketIO.wait(forCallbacks=True) + self.socketIO = SocketIO('localhost', PORT) + + def tearDown(self): + del self.socketIO + + def test_emit(self): + self.socketIO.define(Namespace) + self.socketIO.emit('aaa') + sleep(0.1) + self.assertEqual(self.socketIO.get_namespace().payload, '') + + def test_emit_with_payload(self): + self.socketIO.define(Namespace) + self.socketIO.emit('aaa', PAYLOAD) + sleep(0.1) + self.assertEqual(self.socketIO.get_namespace().payload, PAYLOAD) + + def test_emit_with_callback(self): + self.socketIO.emit('aaa', PAYLOAD, on_response) + self.socketIO.wait(forCallbacks=True) self.assertEqual(ON_RESPONSE_CALLED, True) def test_message(self): - global ON_RESPONSE_CALLED - ON_RESPONSE_CALLED = False - socketIO = SocketIO('localhost', 8000) - socketIO.message(PAYLOAD, on_response) - socketIO.wait(forCallbacks=True) + self.socketIO.message(PAYLOAD, on_response) + self.socketIO.wait(forCallbacks=True) self.assertEqual(ON_RESPONSE_CALLED, True) def test_events(self): - global ON_RESPONSE_CALLED - ON_RESPONSE_CALLED = False - socketIO = SocketIO('localhost', 8000) - socketIO.on('ddd', on_response) - socketIO.emit('aaa', PAYLOAD) + self.socketIO.on('aaa_response', on_response) + self.socketIO.emit('aaa', PAYLOAD) sleep(0.1) self.assertEqual(ON_RESPONSE_CALLED, True) def test_channels(self): - socketIO = SocketIO('localhost', 8000) - mainSocket = socketIO.define(Namespace) - chatSocket = socketIO.define(Namespace, '/chat') - newsSocket = socketIO.define(Namespace, '/news') + mainSocket = self.socketIO.define(Namespace) + chatSocket = self.socketIO.define(Namespace, '/chat') + newsSocket = self.socketIO.define(Namespace, '/news') self.assertNotEqual(mainSocket.get_namespace().payload, PAYLOAD) self.assertNotEqual(chatSocket.get_namespace().payload, PAYLOAD) self.assertNotEqual(newsSocket.get_namespace().payload, PAYLOAD) @@ -72,31 +58,28 @@ class TestSocketIO(TestCase): self.assertEqual(newsSocket.get_namespace().payload, PAYLOAD) def test_channels_with_callback(self): - global ON_RESPONSE_CALLED - ON_RESPONSE_CALLED = False - socketIO = SocketIO('localhost', 8000) - mainSocket = socketIO.get_channel() + mainSocket = self.socketIO.get_channel() mainSocket.message(PAYLOAD, on_response) sleep(0.1) self.assertEqual(ON_RESPONSE_CALLED, True) - def test_delete(self): - socketIO = SocketIO('localhost', 8000) + def test_disconnect(self): childThreads = [ - socketIO._rhythmicThread, - socketIO._listenerThread, + self.socketIO._rhythmicThread, + self.socketIO._listenerThread, ] - del socketIO + self.socketIO.disconnect() for childThread in childThreads: self.assertEqual(True, childThread.done.is_set()) + self.assertEqual(False, self.socketIO.connected) class Namespace(BaseNamespace): payload = None - def on_ddd(self, data=''): - print '[Event] ddd(%s)' % data + def on_aaa_response(self, data=''): + print '[Event] aaa_response(%s)' % data self.payload = data From 1669e90177de9bba4084bd650b1d5caf756bf2c5 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 15 Apr 2013 09:01:33 -0700 Subject: [PATCH 017/191] Updated TODO --- TODO.goals | 9 +++++++++ TODO.rst | 16 ---------------- 2 files changed, 9 insertions(+), 16 deletions(-) create mode 100644 TODO.goals delete mode 100644 TODO.rst diff --git a/TODO.goals b/TODO.goals new file mode 100644 index 0000000..7ae3dbd --- /dev/null +++ b/TODO.goals @@ -0,0 +1,9 @@ += Resolve pull requests + = Resolve pull request #6 +Resolve issues + Investigate issue #8 +Examine forks + Integrate Zac's fork #6 + Integrate Sajal's fork #7 + Integrate Francis's fork #10 + Integrate Paul's fork diff --git a/TODO.rst b/TODO.rst deleted file mode 100644 index 4da9fa4..0000000 --- a/TODO.rst +++ /dev/null @@ -1,16 +0,0 @@ -+ Identify what needs to be done - + check __init__.py - + check tests.py - + check serve_tests.py -+ Fix unittests -+ Fix exceptions when websocket server disappears -+ Fix thread exceptions - + Finish creating low-level _SocketIO to eliminate cyclic references - + Move namespace and callback handling to _ListenerThread -= Resolve pull requests -Resolve issues -Integrate Zac's fork #6 -Integrate Sajal's fork #7 -Integrate Francis's fork #10 -Investigate issue #8 -Integrate Paul's fork From 133bd6f3098e1c9067eb124fbde04b184187ad08 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 18 Apr 2013 07:32:46 -0700 Subject: [PATCH 018/191] Added emit with callback in serve_tests --- TODO.goals | 3 +++ serve_tests.py | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/TODO.goals b/TODO.goals index 7ae3dbd..502f59f 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,5 +1,8 @@ = Resolve pull requests = Resolve pull request #6 + Add unit test for handling server callback + + Add emit with callback in serve_tests + Use json with ensure_ascii=False Resolve issues Investigate issue #8 Examine forks diff --git a/serve_tests.py b/serve_tests.py index 24df950..35bf41d 100644 --- a/serve_tests.py +++ b/serve_tests.py @@ -4,10 +4,11 @@ try: from socketio.namespace import BaseNamespace from socketio.server import SocketIOServer except ImportError: + import sys from setuptools.command import easy_install easy_install.main(['-U', 'gevent-socketio']) print('\nPlease run the script again to launch the test server.') - import sys; sys.exit(1) + sys.exit(1) class Namespace(BaseNamespace): @@ -15,8 +16,13 @@ class Namespace(BaseNamespace): def on_aaa(self, *args): self.emit('aaa_response', *args) + def on_bbb(self, *args): + def callback(*args): + self.emit('callback_response', *args) + self.emit('bbb_response', *args, callback=callback) -class Application(object): + +class App(object): def __call__(self, environ, start_response): socketio_manage(environ, { @@ -29,5 +35,8 @@ class Application(object): if __name__ == '__main__': from socketIO_client.tests import PORT print 'Starting server at port %s' % PORT - socketIOServer = SocketIOServer(('0.0.0.0', PORT), Application()) - socketIOServer.serve_forever() + try: + server = SocketIOServer(('0.0.0.0', PORT), App()) + server.serve_forever() + except KeyboardInterrupt: + pass From f725889de70358c226d579dd671d253466b373a0 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 18 Apr 2013 08:15:47 -0700 Subject: [PATCH 019/191] Assumed that websockets use unicode and not ascii --- TODO.goals | 2 +- socketIO_client/__init__.py | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/TODO.goals b/TODO.goals index 502f59f..9730d82 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,8 +1,8 @@ = Resolve pull requests = Resolve pull request #6 Add unit test for handling server callback + + Use json with ensure_ascii=False + Add emit with callback in serve_tests - Use json with ensure_ascii=False Resolve issues Investigate issue #8 Examine forks diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index b3268ca..36124fe 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,5 +1,5 @@ import socket -from anyjson import dumps, loads +from json import dumps, loads from threading import Thread, Event from time import sleep from urllib import urlopen @@ -46,6 +46,13 @@ class BaseNamespace(object): # pragma: no cover class SocketIO(object): def __init__(self, host, port, secure=False, proxies=None): + """ + Create a socket.io client that connects to a socket.io server + at the specified host and port. Set secure=True to use HTTPS/WSS. + + SocketIO('localhost', 8000, secure=True, + proxies={'https': 'https://proxy.example.com:8080'}) + """ self._socketIO = _SocketIO(host, port, secure, proxies) self._channelByPath = {} self.define(BaseNamespace) # Define default namespace @@ -67,15 +74,15 @@ class SocketIO(object): self.disconnect() def __del__(self): - self.disconnect(force=True) + self.disconnect(closeSocket=False) @property def connected(self): return self._socketIO.connected - def disconnect(self, channelPath='', force=False): + def disconnect(self, channelPath='', closeSocket=True): if self.connected: - self._socketIO.disconnect(channelPath, force) + self._socketIO.disconnect(channelPath, closeSocket) if channelPath: del self._channelByPath[channelPath] else: @@ -255,15 +262,14 @@ class _SocketIO(object): self.callbackByMessageID = {} def __del__(self): - self.disconnect(force=True) + self.disconnect(closeSocket=False) - def disconnect(self, channelPath='', force=False): - 'Set force=True to skip closing the websocket' + def disconnect(self, channelPath='', closeSocket=True): if not self.connected: return if channelPath: self.send_packet(0, channelPath) - elif not force: + elif closeSocket: self.connection.close() def connect(self, channelPath): @@ -282,7 +288,7 @@ class _SocketIO(object): data = messageData else: code = 4 - data = dumps(messageData) + data = dumps(messageData, ensure_ascii=False) self.send_packet(code, channelPath, data, messageCallback) def emit(self, eventName, *eventArguments, **eventKeywords): @@ -292,7 +298,7 @@ class _SocketIO(object): else: messageCallback = None channelPath = eventKeywords.get('channelPath', '') - data = dumps(dict(name=eventName, args=eventArguments)) + data = dumps(dict(name=eventName, args=eventArguments), ensure_ascii=False) self.send_packet(5, channelPath, data, messageCallback) def set_messageCallback(self, callback): From 75d1fa2adc3950c149896e7fbca71e44407efcc9 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 18 Apr 2013 08:58:52 -0700 Subject: [PATCH 020/191] Merged Channel functionality into BaseNamespace --- CHANGES.rst | 2 + socketIO_client/__init__.py | 141 +++++++++++++++++------------------- socketIO_client/tests.py | 24 +++--- 3 files changed, 79 insertions(+), 88 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1e5d0f7..6ff0115 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,7 @@ 0.4 --- +- Merged Channel functionality into BaseNamespace thanks to Alexandre Bourget +- Added support for unicode thanks to Zac Lee 0.3 --- diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 36124fe..6102be9 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -12,8 +12,10 @@ PROTOCOL = 1 # socket.io protocol version class BaseNamespace(object): # pragma: no cover 'Define socket.io behavior' - def __init__(self, _socketIO): + def __init__(self, _socketIO, namespacePath): self._socketIO = _socketIO + self._namespacePath = namespacePath + self._callbackByEvent = {} def on_connect(self): pass @@ -27,7 +29,7 @@ class BaseNamespace(object): # pragma: no cover def on_message(self, messageData): print '[Message] %s' % messageData - def on_(self, eventName, *eventArguments): + def on_default(self, eventName, *eventArguments): print '[Event] %s%s' % (eventName, eventArguments) def on_open(self, *args): @@ -42,6 +44,29 @@ class BaseNamespace(object): # pragma: no cover def on_reconnect(self, *args): print '[Reconnect]', args + def message(self, messageData, messageCallback=None): + self._socketIO.message( + messageData, messageCallback, namespacePath=self._namespacePath) + + def emit(self, eventName, *eventArguments): + self._socketIO.emit( + eventName, *eventArguments, namespacePath=self._namespacePath) + + def on(self, eventName, eventCallback): + self._callbackByEvent[eventName] = eventCallback + + def _get_eventCallback(self, eventName): + # Check callbacks defined by on() + try: + return self._callbackByEvent[eventName] + except KeyError: + pass + + # Check callbacks defined explicitly or use on_default() + def callback(*eventArguments): + return self.on_default(eventName, *eventArguments) + return getattr(self, 'on_' + eventName.replace(' ', '_'), callback) + class SocketIO(object): @@ -54,7 +79,7 @@ class SocketIO(object): proxies={'https': 'https://proxy.example.com:8080'}) """ self._socketIO = _SocketIO(host, port, secure, proxies) - self._channelByPath = {} + self._namespaceByPath = {} self.define(BaseNamespace) # Define default namespace self._rhythmicThread = _RhythmicThread( @@ -64,7 +89,7 @@ class SocketIO(object): self._listenerThread = _ListenerThread( self._socketIO, - self._channelByPath) + self._namespaceByPath) self._listenerThread.start() def __enter__(self): @@ -80,32 +105,29 @@ class SocketIO(object): def connected(self): return self._socketIO.connected - def disconnect(self, channelPath='', closeSocket=True): + def disconnect(self, namespacePath='', closeSocket=True): if self.connected: - self._socketIO.disconnect(channelPath, closeSocket) - if channelPath: - del self._channelByPath[channelPath] + self._socketIO.disconnect(namespacePath, closeSocket) + if namespacePath: + del self._namespaceByPath[namespacePath] else: self._rhythmicThread.cancel() self._listenerThread.cancel() - def define(self, Namespace, channelPath=''): - self._socketIO.connect(channelPath) - channel = Channel(self._socketIO, Namespace, channelPath) - self._channelByPath[channelPath] = channel - return channel + def define(self, Namespace, namespacePath=''): + self._socketIO.connect(namespacePath) + namespace = Namespace(self._socketIO, namespacePath) + self._namespaceByPath[namespacePath] = namespace + return namespace - def get_channel(self, channelPath=''): - return self._channelByPath[channelPath] + def get_namespace(self, namespacePath=''): + return self._namespaceByPath[namespacePath] - def get_namespace(self, channelPath=''): - return self.get_channel(channelPath).get_namespace() + def on(self, eventName, eventCallback, namespacePath=''): + return self.get_namespace(namespacePath).on(eventName, eventCallback) - def on(self, eventName, eventCallback, channelPath=''): - return self.get_channel(channelPath).on(eventName, eventCallback) - - def message(self, messageData, messageCallback=None, channelPath=''): - self._socketIO.message(messageData, messageCallback, channelPath) + def message(self, messageData, messageCallback=None, namespacePath=''): + self._socketIO.message(messageData, messageCallback, namespacePath) def emit(self, eventName, *eventArguments, **eventKeywords): self._socketIO.emit(eventName, *eventArguments, **eventKeywords) @@ -150,10 +172,10 @@ class _ListenerThread(Thread): daemon = True - def __init__(self, _socketIO, _channelByPath): + def __init__(self, _socketIO, _namespaceByPath): super(_ListenerThread, self).__init__() self._socketIO = _socketIO - self._channelByPath = _channelByPath + self._namespaceByPath = _namespaceByPath self.done = Event() self.waiting = Event() @@ -168,7 +190,7 @@ class _ListenerThread(Thread): def run(self): while not self.done.is_set(): try: - code, packetID, channelPath, data = self._socketIO.recv_packet() + code, packetID, namespacePath, data = self._socketIO.recv_packet() except SocketIOConnectionError, error: print error return @@ -176,9 +198,9 @@ class _ListenerThread(Thread): print error continue try: - channel = self._channelByPath[channelPath] + namespace = self._namespaceByPath[namespacePath] except KeyError: - print 'Received unexpected channelPath (%s)' % channelPath + print 'Received unexpected namespacePath (%s)' % namespacePath continue try: delegate = { @@ -194,7 +216,7 @@ class _ListenerThread(Thread): except KeyError: print 'Received unexpected code (%s)' % code continue - delegate(packetID, channel._get_eventCallback, data) + delegate(packetID, namespace._get_eventCallback, data) def on_disconnect(self, packetID, get_eventCallback, data): get_eventCallback('disconnect')() @@ -264,16 +286,16 @@ class _SocketIO(object): def __del__(self): self.disconnect(closeSocket=False) - def disconnect(self, channelPath='', closeSocket=True): + def disconnect(self, namespacePath='', closeSocket=True): if not self.connected: return - if channelPath: - self.send_packet(0, channelPath) + if namespacePath: + self.send_packet(0, namespacePath) elif closeSocket: self.connection.close() - def connect(self, channelPath): - self.send_packet(1, channelPath) + def connect(self, namespacePath): + self.send_packet(1, namespacePath) def send_heartbeat(self): try: @@ -282,14 +304,14 @@ class _SocketIO(object): print 'Could not send heartbeat' pass - def message(self, messageData, messageCallback, channelPath): + def message(self, messageData, messageCallback, namespacePath): if isinstance(messageData, basestring): code = 3 data = messageData else: code = 4 data = dumps(messageData, ensure_ascii=False) - self.send_packet(code, channelPath, data, messageCallback) + self.send_packet(code, namespacePath, data, messageCallback) def emit(self, eventName, *eventArguments, **eventKeywords): if eventArguments and callable(eventArguments[-1]): @@ -297,9 +319,9 @@ class _SocketIO(object): eventArguments = eventArguments[:-1] else: messageCallback = None - channelPath = eventKeywords.get('channelPath', '') + namespacePath = eventKeywords.get('namespacePath', '') data = dumps(dict(name=eventName, args=eventArguments), ensure_ascii=False) - self.send_packet(5, channelPath, data, messageCallback) + self.send_packet(5, namespacePath, data, messageCallback) def set_messageCallback(self, callback): 'Set callback that will be called after receiving an acknowledgment' @@ -333,18 +355,18 @@ class _SocketIO(object): except AttributeError: raise SocketIOPacketError('Received invalid packet (%s)' % packet) packetCount = len(packetParts) - code, packetID, channelPath, data = None, None, None, None + code, packetID, namespacePath, data = None, None, None, None if 4 == packetCount: - code, packetID, channelPath, data = packetParts + code, packetID, namespacePath, data = packetParts elif 3 == packetCount: - code, packetID, channelPath = packetParts + code, packetID, namespacePath = packetParts elif 1 == packetCount: code = packetParts[0] - return code, packetID, channelPath, data + return code, packetID, namespacePath, data - def send_packet(self, code, channelPath='', data='', messageCallback=None): + def send_packet(self, code, namespacePath='', data='', messageCallback=None): callbackNumber = self.set_messageCallback(messageCallback) if messageCallback else '' - packetParts = [str(code), callbackNumber, channelPath, data] + packetParts = [str(code), callbackNumber, namespacePath, data] try: self.connection.send(':'.join(packetParts)) except socket.error: @@ -355,39 +377,6 @@ class _SocketIO(object): return self.connection.connected -class Channel(object): - - def __init__(self, _socketIO, Namespace, channelPath): - self._socketIO = _socketIO - self._namespace = Namespace(_socketIO) - self._channelPath = channelPath - self._callbackByEvent = {} - - def on(self, eventName, eventCallback): - self._callbackByEvent[eventName] = eventCallback - - def message(self, messageData, messageCallback=None): - self._socketIO.message(messageData, messageCallback, channelPath=self._channelPath) - - def emit(self, eventName, *eventArguments): - self._socketIO.emit(eventName, *eventArguments, channelPath=self._channelPath) - - def get_namespace(self): - return self._namespace - - def _get_eventCallback(self, eventName): - # Check callbacks defined by on() - try: - return self._callbackByEvent[eventName] - except KeyError: - pass - # Check callbacks defined explicitly or use on_() - eventNamespace = self.get_namespace() - callbackName = 'on_' + eventName.replace(' ', '_') - defaultCallback = lambda *eventArguments: eventNamespace.on_(eventName, *eventArguments) - return getattr(eventNamespace, callbackName, defaultCallback) - - class SocketIOError(Exception): pass diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index cbeaca9..cc9d3e7 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -46,20 +46,20 @@ class TestSocketIO(TestCase): sleep(0.1) self.assertEqual(ON_RESPONSE_CALLED, True) - def test_channels(self): - mainSocket = self.socketIO.define(Namespace) - chatSocket = self.socketIO.define(Namespace, '/chat') - newsSocket = self.socketIO.define(Namespace, '/news') - self.assertNotEqual(mainSocket.get_namespace().payload, PAYLOAD) - self.assertNotEqual(chatSocket.get_namespace().payload, PAYLOAD) - self.assertNotEqual(newsSocket.get_namespace().payload, PAYLOAD) - newsSocket.emit('aaa', PAYLOAD) + def test_namespaces(self): + mainNamespace = self.socketIO.define(Namespace) + chatNamespace = self.socketIO.define(Namespace, '/chat') + newsNamespace = self.socketIO.define(Namespace, '/news') + self.assertNotEqual(mainNamespace.payload, PAYLOAD) + self.assertNotEqual(chatNamespace.payload, PAYLOAD) + self.assertNotEqual(newsNamespace.payload, PAYLOAD) + newsNamespace.emit('aaa', PAYLOAD) sleep(0.1) - self.assertEqual(newsSocket.get_namespace().payload, PAYLOAD) + self.assertEqual(newsNamespace.payload, PAYLOAD) - def test_channels_with_callback(self): - mainSocket = self.socketIO.get_channel() - mainSocket.message(PAYLOAD, on_response) + def test_namespaces_with_callback(self): + mainNamespace = self.socketIO.get_namespace() + mainNamespace.message(PAYLOAD, on_response) sleep(0.1) self.assertEqual(ON_RESPONSE_CALLED, True) From 25922c5b03e8483c954b928e925edb2759fbd152 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 18 Apr 2013 09:12:20 -0700 Subject: [PATCH 021/191] Updated README --- README.rst | 22 +++++++++++++--------- socketIO_client/__init__.py | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 93571c7..02080ca 100644 --- a/README.rst +++ b/README.rst @@ -4,9 +4,9 @@ Here is a socket.io_ client library for Python. You can use it to write test co Thanks to rod_ for the `StackOverflow question and answer`__ on which this code is based. -Thanks to liris_ for websocket-client_ and to guille_ for the `socket.io specification`_. +Thanks to `Hiroki Ohtani`_ for websocket-client_, `Guillermo Rauch`_ for the `socket.io specification`_ and `Alexandre Bourget`_ for gevent-socketio_. -Thanks to `Paul Kienzle`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_ for submitting code to expand support of the socket.io protocol. +Thanks to `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_ for submitting code to expand support of the socket.io protocol. Installation @@ -68,7 +68,7 @@ Define events in a namespace. :: class Namespace(BaseNamespace): def on_ddd(self, *args): - self.socketIO.emit('eee', {'fff': 'ggg'}) + self.emit('eee', {'fff': 'ggg'}) socketIO = SocketIO('localhost', 8000) socketIO.define(Namespace) @@ -117,10 +117,10 @@ Define different namespaces on a single socket. :: socketIO = SocketIO('localhost', 8000) socketIO.define(MainNamespace) - chatSocket = socketIO.define(ChatNamespace, '/chat') - chatSocket.emit('bbb') - newsSocket = socketIO.define(NewsNamespace, '/news') - newsSocket.emit('ccc') + chatNamespace = socketIO.define(ChatNamespace, '/chat') + chatNamespace.emit('bbb') + newsNamespace = socketIO.define(NewsNamespace, '/news') + newsNamespace.emit('ccc') socketIO.wait() # Loop until CTRL-C Open secure websockets (HTTPS / WSS) behind a proxy. :: @@ -139,10 +139,14 @@ This software is available under the MIT License. .. _rod: http://stackoverflow.com/users/370115/rod .. _StackOverflowQA: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client __ StackOverflowQA_ -.. _liris: https://github.com/liris .. _websocket-client: https://github.com/liris/websocket-client -.. _guille: https://github.com/guille .. _socket.io specification: https://github.com/LearnBoost/socket.io-spec +.. _gevent-socketio: https://github.com/abourget/gevent-socketio +.. _Hiroki Ohtani: https://github.com/liris +.. _Guillermo Rauch: https://github.com/guille +.. _Alexandre Bourget: https://github.com/abourget .. _Paul Kienzle: https://github.com/pkienzle .. _Josh VanderLinden: https://github.com/codekoala .. _Ian Fitzpatrick: https://github.com/GraphEffect +.. _Zac Lee: https://github.com/zratic +.. _Lucas Klein: https://github.com/lukashed diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 6102be9..faa0a1f 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -73,7 +73,7 @@ class SocketIO(object): def __init__(self, host, port, secure=False, proxies=None): """ Create a socket.io client that connects to a socket.io server - at the specified host and port. Set secure=True to use HTTPS/WSS. + at the specified host and port. Set secure=True to use HTTPS / WSS. SocketIO('localhost', 8000, secure=True, proxies={'https': 'https://proxy.example.com:8080'}) From fc2ddfe46b286cb93aaf1cfccd1d4bf507b75ccf Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 18 Apr 2013 09:25:37 -0700 Subject: [PATCH 022/191] Updated README --- README.rst | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 02080ca..8f7bc34 100644 --- a/README.rst +++ b/README.rst @@ -2,12 +2,6 @@ socketIO-client =============== Here is a socket.io_ client library for Python. You can use it to write test code for your socket.io_ server. -Thanks to rod_ for the `StackOverflow question and answer`__ on which this code is based. - -Thanks to `Hiroki Ohtani`_ for websocket-client_, `Guillermo Rauch`_ for the `socket.io specification`_ and `Alexandre Bourget`_ for gevent-socketio_. - -Thanks to `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_ for submitting code to expand support of the socket.io protocol. - Installation ------------ @@ -135,18 +129,31 @@ License This software is available under the MIT License. +Credits +------- +- `Guillermo Rauch`_ wrote the `socket.io specification`_. +- `Hiroki Ohtani`_ wrote websocket-client_. +- rod_ wrote a `prototype for a python client to a socket.io server`_ on StackOverflow. +- `Alexandre Bourget`_ wrote gevent-socketio_, which is a socket.io server written in python. +- `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_ submitted code to expand support of the socket.io protocol. + + .. _socket.io: http://socket.io -.. _rod: http://stackoverflow.com/users/370115/rod -.. _StackOverflowQA: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client -__ StackOverflowQA_ -.. _websocket-client: https://github.com/liris/websocket-client -.. _socket.io specification: https://github.com/LearnBoost/socket.io-spec -.. _gevent-socketio: https://github.com/abourget/gevent-socketio -.. _Hiroki Ohtani: https://github.com/liris + .. _Guillermo Rauch: https://github.com/guille +.. _socket.io specification: https://github.com/LearnBoost/socket.io-spec + +.. _Hiroki Ohtani: https://github.com/liris +.. _websocket-client: https://github.com/liris/websocket-client + +.. _rod: http://stackoverflow.com/users/370115/rod +.. _prototype for a python client to a socket.io server: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client + .. _Alexandre Bourget: https://github.com/abourget +.. _gevent-socketio: https://github.com/abourget/gevent-socketio + .. _Paul Kienzle: https://github.com/pkienzle +.. _Zac Lee: https://github.com/zratic .. _Josh VanderLinden: https://github.com/codekoala .. _Ian Fitzpatrick: https://github.com/GraphEffect -.. _Zac Lee: https://github.com/zratic .. _Lucas Klein: https://github.com/lukashed From 61918597b70edc0c7925d55dc66f6bdab4e9768a Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 18 Apr 2013 09:26:23 -0700 Subject: [PATCH 023/191] Updated README --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 8f7bc34..4f3e123 100644 --- a/README.rst +++ b/README.rst @@ -133,8 +133,8 @@ Credits ------- - `Guillermo Rauch`_ wrote the `socket.io specification`_. - `Hiroki Ohtani`_ wrote websocket-client_. -- rod_ wrote a `prototype for a python client to a socket.io server`_ on StackOverflow. -- `Alexandre Bourget`_ wrote gevent-socketio_, which is a socket.io server written in python. +- rod_ wrote a `prototype for a Python client to a socket.io server`_ on StackOverflow. +- `Alexandre Bourget`_ wrote gevent-socketio_, which is a socket.io server written in Python. - `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_ submitted code to expand support of the socket.io protocol. @@ -147,7 +147,7 @@ Credits .. _websocket-client: https://github.com/liris/websocket-client .. _rod: http://stackoverflow.com/users/370115/rod -.. _prototype for a python client to a socket.io server: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client +.. _prototype for a Python client to a socket.io server: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client .. _Alexandre Bourget: https://github.com/abourget .. _gevent-socketio: https://github.com/abourget/gevent-socketio From 18d2a1ea8c8940910b859de867112b387a49e03d Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 24 Apr 2013 09:58:24 -0700 Subject: [PATCH 024/191] Added support for acks thanks to zratic --- socketIO_client/__init__.py | 178 ++++++++++++++++++++---------------- socketIO_client/tests.py | 69 +++++++------- 2 files changed, 138 insertions(+), 109 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index faa0a1f..18cb1fe 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -12,9 +12,9 @@ PROTOCOL = 1 # socket.io protocol version class BaseNamespace(object): # pragma: no cover 'Define socket.io behavior' - def __init__(self, _socketIO, namespacePath): + def __init__(self, _socketIO, path): self._socketIO = _socketIO - self._namespacePath = namespacePath + self._path = path self._callbackByEvent = {} def on_connect(self): @@ -26,11 +26,16 @@ class BaseNamespace(object): # pragma: no cover def on_error(self, reason, advice): print '[Error] %s' % advice - def on_message(self, messageData): - print '[Message] %s' % messageData + def on_message(self, data): + print '[Message] %s' % data - def on_default(self, eventName, *eventArguments): - print '[Event] %s%s' % (eventName, eventArguments) + def on_default(self, event, *args): + callback, args = find_callback(args) + arguments = [str(_) for _ in args] + if callback: + arguments.append('callback(*args)') + callback() + print '[Event] %s(%s)' % (event, ', '.join(arguments)) def on_open(self, *args): print '[Open]', args @@ -44,28 +49,25 @@ class BaseNamespace(object): # pragma: no cover def on_reconnect(self, *args): print '[Reconnect]', args - def message(self, messageData, messageCallback=None): - self._socketIO.message( - messageData, messageCallback, namespacePath=self._namespacePath) + def message(self, data, callback=None): + self._socketIO.message(data, callback, path=self._path) - def emit(self, eventName, *eventArguments): - self._socketIO.emit( - eventName, *eventArguments, namespacePath=self._namespacePath) + def emit(self, event, *args, **kw): + kw['path'] = self._path + self._socketIO.emit(event, *args, **kw) - def on(self, eventName, eventCallback): - self._callbackByEvent[eventName] = eventCallback + def on(self, event, callback): + self._callbackByEvent[event] = callback - def _get_eventCallback(self, eventName): + def _get_eventCallback(self, event): # Check callbacks defined by on() try: - return self._callbackByEvent[eventName] + return self._callbackByEvent[event] except KeyError: pass - # Check callbacks defined explicitly or use on_default() - def callback(*eventArguments): - return self.on_default(eventName, *eventArguments) - return getattr(self, 'on_' + eventName.replace(' ', '_'), callback) + callback = lambda *args: self.on_default(event, *args) + return getattr(self, 'on_' + event.replace(' ', '_'), callback) class SocketIO(object): @@ -99,38 +101,38 @@ class SocketIO(object): self.disconnect() def __del__(self): - self.disconnect(closeSocket=False) + self.disconnect(close=False) @property def connected(self): return self._socketIO.connected - def disconnect(self, namespacePath='', closeSocket=True): + def disconnect(self, path='', close=True): if self.connected: - self._socketIO.disconnect(namespacePath, closeSocket) - if namespacePath: - del self._namespaceByPath[namespacePath] + self._socketIO.disconnect(path, close) + if path: + del self._namespaceByPath[path] else: self._rhythmicThread.cancel() self._listenerThread.cancel() - def define(self, Namespace, namespacePath=''): - self._socketIO.connect(namespacePath) - namespace = Namespace(self._socketIO, namespacePath) - self._namespaceByPath[namespacePath] = namespace + def define(self, Namespace, path=''): + self._socketIO.connect(path) + namespace = Namespace(self._socketIO, path) + self._namespaceByPath[path] = namespace return namespace - def get_namespace(self, namespacePath=''): - return self._namespaceByPath[namespacePath] + def get_namespace(self, path=''): + return self._namespaceByPath[path] - def on(self, eventName, eventCallback, namespacePath=''): - return self.get_namespace(namespacePath).on(eventName, eventCallback) + def on(self, event, callback, path=''): + return self.get_namespace(path).on(event, callback) - def message(self, messageData, messageCallback=None, namespacePath=''): - self._socketIO.message(messageData, messageCallback, namespacePath) + def message(self, data, callback=None, path=''): + self._socketIO.message(data, callback, path) - def emit(self, eventName, *eventArguments, **eventKeywords): - self._socketIO.emit(eventName, *eventArguments, **eventKeywords) + def emit(self, event, *args, **kw): + self._socketIO.emit(event, *args, **kw) def wait(self, seconds=None, forCallbacks=False): if forCallbacks: @@ -187,10 +189,13 @@ class _ListenerThread(Thread): # Block callingThread until listenerThread terminates self.join(seconds) + def get_ackCallback(self, packetID): + return lambda *args: self._socketIO.ack(packetID, *args) + def run(self): while not self.done.is_set(): try: - code, packetID, namespacePath, data = self._socketIO.recv_packet() + code, packetID, path, data = self._socketIO.recv_packet() except SocketIOConnectionError, error: print error return @@ -198,9 +203,9 @@ class _ListenerThread(Thread): print error continue try: - namespace = self._namespaceByPath[namespacePath] + namespace = self._namespaceByPath[path] except KeyError: - print 'Received unexpected namespacePath (%s)' % namespacePath + print 'Received unexpected path (%s)' % path continue try: delegate = { @@ -228,25 +233,33 @@ class _ListenerThread(Thread): pass def on_message(self, packetID, get_eventCallback, data): - get_eventCallback('message')(data) + args = [data] + if packetID: + args.append(self.get_ackCallback(packetID)) + get_eventCallback('message')(args) def on_json(self, packetID, get_eventCallback, data): - get_eventCallback('message')(loads(data)) + args = [loads(data)] + if packetID: + args.append(self.get_ackCallback(packetID)) + get_eventCallback('message')(args) def on_event(self, packetID, get_eventCallback, data): valueByName = loads(data) - eventName = valueByName['name'] - eventArguments = valueByName.get('args', []) - get_eventCallback(eventName)(*eventArguments) + event = valueByName['name'] + args = valueByName.get('args', []) + if packetID: + args.append(self.get_ackCallback(packetID)) + get_eventCallback(event)(*args) def on_acknowledgment(self, packetID, get_eventCallback, data): dataParts = data.split('+', 1) messageID = int(dataParts[0]) - arguments = loads(dataParts[1]) or [] - messageCallback = self._socketIO.get_messageCallback(messageID) - if not messageCallback: + args = loads(dataParts[1]) or [] + callback = self._socketIO.get_messageCallback(messageID) + if not callback: return - messageCallback(*arguments) + callback(*args) if self.waiting.is_set() and not self._socketIO.has_messageCallback: self.cancel() @@ -284,18 +297,18 @@ class _SocketIO(object): self.callbackByMessageID = {} def __del__(self): - self.disconnect(closeSocket=False) + self.disconnect(close=False) - def disconnect(self, namespacePath='', closeSocket=True): + def disconnect(self, path='', close=True): if not self.connected: return - if namespacePath: - self.send_packet(0, namespacePath) - elif closeSocket: + if path: + self.send_packet(0, path) + elif close: self.connection.close() - def connect(self, namespacePath): - self.send_packet(1, namespacePath) + def connect(self, path): + self.send_packet(1, path) def send_heartbeat(self): try: @@ -304,24 +317,25 @@ class _SocketIO(object): print 'Could not send heartbeat' pass - def message(self, messageData, messageCallback, namespacePath): - if isinstance(messageData, basestring): + def message(self, data, callback, path): + if isinstance(data, basestring): code = 3 - data = messageData + packetData = data else: code = 4 - data = dumps(messageData, ensure_ascii=False) - self.send_packet(code, namespacePath, data, messageCallback) + packetData = dumps(data, ensure_ascii=False) + self.send_packet(code, path, packetData, callback) - def emit(self, eventName, *eventArguments, **eventKeywords): - if eventArguments and callable(eventArguments[-1]): - messageCallback = eventArguments[-1] - eventArguments = eventArguments[:-1] - else: - messageCallback = None - namespacePath = eventKeywords.get('namespacePath', '') - data = dumps(dict(name=eventName, args=eventArguments), ensure_ascii=False) - self.send_packet(5, namespacePath, data, messageCallback) + def emit(self, event, *args, **kw): + callback, args = find_callback(args, kw) + packetData = dumps(dict(name=event, args=args), ensure_ascii=False) + path = kw.get('path', '') + self.send_packet(5, path, packetData, callback) + + def ack(self, packetID, *args): + packetID = packetID.rstrip('+') + packetData = '%s+%s' % (packetID, dumps(args, ensure_ascii=False)) if args else packetID + self.send_packet(6, data=packetData) def set_messageCallback(self, callback): 'Set callback that will be called after receiving an acknowledgment' @@ -355,18 +369,18 @@ class _SocketIO(object): except AttributeError: raise SocketIOPacketError('Received invalid packet (%s)' % packet) packetCount = len(packetParts) - code, packetID, namespacePath, data = None, None, None, None + code, packetID, path, data = None, None, None, None if 4 == packetCount: - code, packetID, namespacePath, data = packetParts + code, packetID, path, data = packetParts elif 3 == packetCount: - code, packetID, namespacePath = packetParts + code, packetID, path = packetParts elif 1 == packetCount: code = packetParts[0] - return code, packetID, namespacePath, data + return code, packetID, path, data - def send_packet(self, code, namespacePath='', data='', messageCallback=None): - callbackNumber = self.set_messageCallback(messageCallback) if messageCallback else '' - packetParts = [str(code), callbackNumber, namespacePath, data] + def send_packet(self, code, path='', data='', callback=None): + packetID = self.set_messageCallback(callback) if callback else '' + packetParts = [str(code), packetID, path, data] try: self.connection.send(':'.join(packetParts)) except socket.error: @@ -387,3 +401,13 @@ class SocketIOConnectionError(SocketIOError): class SocketIOPacketError(SocketIOError): pass + + +def find_callback(args, kw=None): + 'Return callback whether passed as a last argument or as a keyword' + if args and callable(args[-1]): + return args[-1], args[:-1] + try: + return kw['callback'], args + except (KeyError, TypeError): + return None, args diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index cc9d3e7..92ffe5f 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -1,9 +1,8 @@ -from socketIO_client import SocketIO, BaseNamespace +from socketIO_client import SocketIO, BaseNamespace, find_callback from time import sleep from unittest import TestCase -ON_RESPONSE_CALLED = False PORT = 8000 PAYLOAD = {'xxx': 'yyy'} @@ -11,13 +10,28 @@ PAYLOAD = {'xxx': 'yyy'} class TestSocketIO(TestCase): def setUp(self): - global ON_RESPONSE_CALLED - ON_RESPONSE_CALLED = False self.socketIO = SocketIO('localhost', PORT) + self.called_on_response = False def tearDown(self): del self.socketIO + def on_response(self, *args): + self.called_on_response = True + callback, args = find_callback(args) + if callback: + callback(*args) + + def test_disconnect(self): + childThreads = [ + self.socketIO._rhythmicThread, + self.socketIO._listenerThread, + ] + self.socketIO.disconnect() + for childThread in childThreads: + self.assertEqual(True, childThread.done.is_set()) + self.assertEqual(False, self.socketIO.connected) + def test_emit(self): self.socketIO.define(Namespace) self.socketIO.emit('aaa') @@ -31,20 +45,26 @@ class TestSocketIO(TestCase): self.assertEqual(self.socketIO.get_namespace().payload, PAYLOAD) def test_emit_with_callback(self): - self.socketIO.emit('aaa', PAYLOAD, on_response) - self.socketIO.wait(forCallbacks=True) - self.assertEqual(ON_RESPONSE_CALLED, True) + self.socketIO.emit('aaa', PAYLOAD, self.on_response) + self.socketIO.wait(seconds=0.1, forCallbacks=True) + self.assertEqual(self.called_on_response, True) - def test_message(self): - self.socketIO.message(PAYLOAD, on_response) - self.socketIO.wait(forCallbacks=True) - self.assertEqual(ON_RESPONSE_CALLED, True) - - def test_events(self): - self.socketIO.on('aaa_response', on_response) + def test_emit_with_event(self): + self.socketIO.on('aaa_response', self.on_response) self.socketIO.emit('aaa', PAYLOAD) sleep(0.1) - self.assertEqual(ON_RESPONSE_CALLED, True) + self.assertEqual(self.called_on_response, True) + + def test_message(self): + self.socketIO.message(PAYLOAD, self.on_response) + self.socketIO.wait(seconds=0.1, forCallbacks=True) + self.assertEqual(self.called_on_response, True) + + def test_ack(self): + self.socketIO.on('bbb_response', self.on_response) + self.socketIO.emit('bbb', PAYLOAD) + sleep(0.1) + self.assertEqual(self.called_on_response, True) def test_namespaces(self): mainNamespace = self.socketIO.define(Namespace) @@ -59,19 +79,9 @@ class TestSocketIO(TestCase): def test_namespaces_with_callback(self): mainNamespace = self.socketIO.get_namespace() - mainNamespace.message(PAYLOAD, on_response) + mainNamespace.message(PAYLOAD, self.on_response) sleep(0.1) - self.assertEqual(ON_RESPONSE_CALLED, True) - - def test_disconnect(self): - childThreads = [ - self.socketIO._rhythmicThread, - self.socketIO._listenerThread, - ] - self.socketIO.disconnect() - for childThread in childThreads: - self.assertEqual(True, childThread.done.is_set()) - self.assertEqual(False, self.socketIO.connected) + self.assertEqual(self.called_on_response, True) class Namespace(BaseNamespace): @@ -81,8 +91,3 @@ class Namespace(BaseNamespace): def on_aaa_response(self, data=''): print '[Event] aaa_response(%s)' % data self.payload = data - - -def on_response(*args): - global ON_RESPONSE_CALLED - ON_RESPONSE_CALLED = True From 77a8e72c1f9a4fd7ea92d8b77f7af3a5b120fcef Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Fri, 26 Apr 2013 09:34:47 -0700 Subject: [PATCH 025/191] Fixes #6 --- README.rst | 64 ++++++------- TODO.goals | 4 +- serve_tests.js | 71 +++++++++++++++ serve_tests.py | 42 --------- socketIO_client/__init__.py | 87 ++++++++++++------ socketIO_client/tests.py | 177 ++++++++++++++++++++++++++---------- 6 files changed, 286 insertions(+), 159 deletions(-) create mode 100644 serve_tests.js delete mode 100644 serve_tests.py diff --git a/README.rst b/README.rst index 4f3e123..3c348b8 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,7 @@ Installation source $VIRTUAL_ENV/bin/activate # Install package - easy_install -U socketIO-client + pip install -U socketIO-client Usage @@ -29,31 +29,33 @@ Activate isolated environment. :: Emit. :: from socketIO_client import SocketIO + with SocketIO('localhost', 8000) as socketIO: socketIO.emit('aaa') - socketIO.wait(1) # Wait a second + socketIO.wait(seconds=1) Emit with callback. :: from socketIO_client import SocketIO - def on_response(*args): - print args + def on_bbb_response(*args): + print 'on_bbb_response', args with SocketIO('localhost', 8000) as socketIO: - socketIO.emit('aaa', {'bbb': 'ccc'}, on_response) - socketIO.wait(seconds=1, forCallbacks=True) # Wait for callback + socketIO.emit('bbb', {'xxx': 'yyy'}, on_bbb_response) + socketIO.wait_for_callbacks(seconds=1) Define events. :: from socketIO_client import SocketIO - def on_ddd(*args): - print args + def on_aaa_response(*args): + print 'on_aaa_response', args socketIO = SocketIO('localhost', 8000) - socketIO.on('ddd', on_ddd) - socketIO.wait() # Loop until CTRL-C + socketIO.on('aaa_response', on_aaa_response) + socketIO.emit('aaa') + socketIO.wait(seconds=1) Define events in a namespace. :: @@ -61,12 +63,14 @@ Define events in a namespace. :: class Namespace(BaseNamespace): - def on_ddd(self, *args): - self.emit('eee', {'fff': 'ggg'}) + def on_aaa_response(self, *args): + print 'on_aaa_response', args + self.emit('bbb') socketIO = SocketIO('localhost', 8000) socketIO.define(Namespace) - socketIO.wait() # Loop until CTRL-C + socketIO.emit('aaa') + socketIO.wait(seconds=1) Define standard events. :: @@ -77,51 +81,37 @@ Define standard events. :: def on_connect(self): print '[Connected]' - def on_disconnect(self): - print '[Disconnected]' - - def on_error(self, reason, advice): - print '[Error] %s' % advice - - def on_message(self, messageData): - print '[Message] %s' % messageData - socketIO = SocketIO('localhost', 8000) socketIO.define(Namespace) - socketIO.wait() # Loop until CTRL-C + socketIO.wait(seconds=1) Define different namespaces on a single socket. :: from socketIO_client import SocketIO, BaseNamespace - class MainNamespace(BaseNamespace): - - def on_aaa(self, *args): - print 'aaa', args - class ChatNamespace(BaseNamespace): - def on_bbb(self, *args): - print 'bbb', args + def on_aaa_response(self, *args): + print 'on_aaa_response', args class NewsNamespace(BaseNamespace): - def on_ccc(self, *args): - print 'ccc', args + def on_aaa_response(self, *args): + print 'on_aaa_response', args socketIO = SocketIO('localhost', 8000) - socketIO.define(MainNamespace) chatNamespace = socketIO.define(ChatNamespace, '/chat') - chatNamespace.emit('bbb') newsNamespace = socketIO.define(NewsNamespace, '/news') - newsNamespace.emit('ccc') - socketIO.wait() # Loop until CTRL-C + + chatNamespace.emit('aaa') + newsNamespace.emit('aaa') + socketIO.wait(seconds=1) Open secure websockets (HTTPS / WSS) behind a proxy. :: SocketIO('localhost', 8000, secure=True, - proxies={'http': 'http://proxy.example.com:8080'}) + proxies={'https': 'https://proxy.example.com:8080'}) License diff --git a/TODO.goals b/TODO.goals index 9730d82..d76ca64 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,6 +1,6 @@ = Resolve pull requests - = Resolve pull request #6 - Add unit test for handling server callback + + Resolve pull request #6 + + Add unit test for handling server callback + Use json with ensure_ascii=False + Add emit with callback in serve_tests Resolve issues diff --git a/serve_tests.js b/serve_tests.js new file mode 100644 index 0000000..82c8024 --- /dev/null +++ b/serve_tests.js @@ -0,0 +1,71 @@ +var io = require('socket.io').listen(8000); + +var main = io.of('').on('connection', function(socket) { + socket.on('message', function(data, fn) { + if (fn) { // Client expects a callback + if (data) { + fn(data); + } else { + fn(); + } + } else if (typeof data === 'object') { + socket.json.send(data ? data : 'message_response'); // object or null + } else { + socket.send(data ? data : 'message_response'); // string or '' + } + }); + socket.on('emit', function() { + socket.emit('emit_response'); + }); + socket.on('emit_with_payload', function(payload) { + socket.emit('emit_with_payload_response', payload); + }); + socket.on('emit_with_multiple_payloads', function(payload, payload) { + socket.emit('emit_with_multiple_payloads_response', payload, payload); + }); + socket.on('emit_with_callback', function(fn) { + fn(); + }); + socket.on('emit_with_callback_with_payload', function(fn) { + fn(PAYLOAD); + }); + socket.on('emit_with_callback_with_multiple_payloads', function(fn) { + fn(PAYLOAD, PAYLOAD); + }); + socket.on('emit_with_event', function(payload) { + socket.emit('emit_with_event_response', payload); + }); + socket.on('ack', function(payload) { + socket.emit('ack_response', payload, function(payload) { + socket.emit('ack_callback_response', payload); + }); + }); + socket.on('aaa', function() { + socket.emit('aaa_response', PAYLOAD); + }); + socket.on('bbb', function(payload, fn) { + if (fn) { + fn(payload); + } + }); +}); + +var chat = io.of('/chat').on('connection', function (socket) { + socket.on('emit_with_payload', function(payload) { + socket.emit('emit_with_payload_response', payload); + }); + socket.on('aaa', function() { + socket.emit('aaa_response', 'in chat'); + }); +}); + +var news = io.of('/news').on('connection', function (socket) { + socket.on('emit_with_payload', function(payload) { + socket.emit('emit_with_payload_response', payload); + }); + socket.on('aaa', function() { + socket.emit('aaa_response', 'in news'); + }); +}); + +var PAYLOAD = {'xxx': 'yyy'}; diff --git a/serve_tests.py b/serve_tests.py deleted file mode 100644 index 35bf41d..0000000 --- a/serve_tests.py +++ /dev/null @@ -1,42 +0,0 @@ -'Launch this server in another terminal window before running tests' -try: - from socketio import socketio_manage - from socketio.namespace import BaseNamespace - from socketio.server import SocketIOServer -except ImportError: - import sys - from setuptools.command import easy_install - easy_install.main(['-U', 'gevent-socketio']) - print('\nPlease run the script again to launch the test server.') - sys.exit(1) - - -class Namespace(BaseNamespace): - - def on_aaa(self, *args): - self.emit('aaa_response', *args) - - def on_bbb(self, *args): - def callback(*args): - self.emit('callback_response', *args) - self.emit('bbb_response', *args, callback=callback) - - -class App(object): - - def __call__(self, environ, start_response): - socketio_manage(environ, { - '': Namespace, - '/chat': Namespace, - '/news': Namespace, - }) - - -if __name__ == '__main__': - from socketIO_client.tests import PORT - print 'Starting server at port %s' % PORT - try: - server = SocketIOServer(('0.0.0.0', PORT), App()) - server.serve_forever() - except KeyboardInterrupt: - pass diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 18cb1fe..5d3499d 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -16,25 +16,39 @@ class BaseNamespace(object): # pragma: no cover self._socketIO = _socketIO self._path = path self._callbackByEvent = {} + self.initialize() + + def initialize(self): + 'Initialize custom variables here; you can override this method' + pass def on_connect(self): + 'Called when socket is connecting; you can override this method' pass def on_disconnect(self): + 'Called when socket is disconnecting; you can override this method' pass def on_error(self, reason, advice): + 'Called when server sends an error; you can override this method' print '[Error] %s' % advice def on_message(self, data): + 'Called when server sends a message; you can override this method' print '[Message] %s' % data - def on_default(self, event, *args): + def on_event(self, event, *args): + """ + Called when server emits an event; you can override this method. + Called only if the program cannot find a more specific event handler, + such as one defined by namespace.on('my_event', my_function). + """ callback, args = find_callback(args) - arguments = [str(_) for _ in args] + arguments = [repr(_) for _ in args] if callback: arguments.append('callback(*args)') - callback() + callback(*args) print '[Event] %s(%s)' % (event, ', '.join(arguments)) def on_open(self, *args): @@ -49,7 +63,7 @@ class BaseNamespace(object): # pragma: no cover def on_reconnect(self, *args): print '[Reconnect]', args - def message(self, data, callback=None): + def message(self, data='', callback=None): self._socketIO.message(data, callback, path=self._path) def emit(self, event, *args, **kw): @@ -57,6 +71,7 @@ class BaseNamespace(object): # pragma: no cover self._socketIO.emit(event, *args, **kw) def on(self, event, callback): + 'Define a callback to handle a custom event emitted by the server' self._callbackByEvent[event] = callback def _get_eventCallback(self, event): @@ -65,8 +80,8 @@ class BaseNamespace(object): # pragma: no cover return self._callbackByEvent[event] except KeyError: pass - # Check callbacks defined explicitly or use on_default() - callback = lambda *args: self.on_default(event, *args) + # Check callbacks defined explicitly or use on_event() + callback = lambda *args: self.on_event(event, *args) return getattr(self, 'on_' + event.replace(' ', '_'), callback) @@ -117,7 +132,8 @@ class SocketIO(object): self._listenerThread.cancel() def define(self, Namespace, path=''): - self._socketIO.connect(path) + if path: + self._socketIO.connect(path) namespace = Namespace(self._socketIO, path) self._namespaceByPath[path] = namespace return namespace @@ -128,17 +144,15 @@ class SocketIO(object): def on(self, event, callback, path=''): return self.get_namespace(path).on(event, callback) - def message(self, data, callback=None, path=''): + def message(self, data='', callback=None, path=''): self._socketIO.message(data, callback, path) def emit(self, event, *args, **kw): self._socketIO.emit(event, *args, **kw) - def wait(self, seconds=None, forCallbacks=False): - if forCallbacks: - self._listenerThread.wait_for_callbacks(seconds) - elif seconds: - sleep(seconds) + def wait(self, seconds=None): + if seconds: + self._listenerThread.wait(seconds) else: try: while self.connected: @@ -146,6 +160,9 @@ class SocketIO(object): except KeyboardInterrupt: pass + def wait_for_callbacks(self, seconds=None): + self._listenerThread.wait_for_callbacks(seconds) + class _RhythmicThread(Thread): 'Execute call every few seconds' @@ -179,15 +196,18 @@ class _ListenerThread(Thread): self._socketIO = _socketIO self._namespaceByPath = _namespaceByPath self.done = Event() - self.waiting = Event() + self.ready = Event() + self.ready.set() def cancel(self): self.done.set() + def wait(self, seconds): + self.done.wait(seconds) + def wait_for_callbacks(self, seconds): - self.waiting.set() - # Block callingThread until listenerThread terminates - self.join(seconds) + self.ready.clear() + self.ready.wait(seconds) def get_ackCallback(self, packetID): return lambda *args: self._socketIO.ack(packetID, *args) @@ -215,7 +235,7 @@ class _ListenerThread(Thread): '3': self.on_message, '4': self.on_json, '5': self.on_event, - '6': self.on_acknowledgment, + '6': self.on_ack, '7': self.on_error, }[code] except KeyError: @@ -236,13 +256,13 @@ class _ListenerThread(Thread): args = [data] if packetID: args.append(self.get_ackCallback(packetID)) - get_eventCallback('message')(args) + get_eventCallback('message')(*args) def on_json(self, packetID, get_eventCallback, data): args = [loads(data)] if packetID: args.append(self.get_ackCallback(packetID)) - get_eventCallback('message')(args) + get_eventCallback('message')(*args) def on_event(self, packetID, get_eventCallback, data): valueByName = loads(data) @@ -252,16 +272,16 @@ class _ListenerThread(Thread): args.append(self.get_ackCallback(packetID)) get_eventCallback(event)(*args) - def on_acknowledgment(self, packetID, get_eventCallback, data): + def on_ack(self, packetID, get_eventCallback, data): dataParts = data.split('+', 1) messageID = int(dataParts[0]) - args = loads(dataParts[1]) or [] + args = loads(dataParts[1]) if len(dataParts) > 1 else [] callback = self._socketIO.get_messageCallback(messageID) if not callback: return callback(*args) - if self.waiting.is_set() and not self._socketIO.has_messageCallback: - self.cancel() + if not self._socketIO.has_messageCallback: + self.ready.set() def on_error(self, packetID, get_eventCallback, data): reason, advice = data.split('+', 1) @@ -289,7 +309,7 @@ class _SocketIO(object): # connectionTimeout = int(responseParts[2]) supportedTransports = responseParts[3].split(',') if 'websocket' not in supportedTransports: - raise SocketIOError('Could not parse handshake') # pragma: no cover + raise SocketIOError('Could not parse handshake') socketScheme = 'wss' if secure else 'ws' socketURL = '%s://%s/websocket/%s' % (socketScheme, baseURL, sessionID) self.connection = create_connection(socketURL) @@ -334,7 +354,10 @@ class _SocketIO(object): def ack(self, packetID, *args): packetID = packetID.rstrip('+') - packetData = '%s+%s' % (packetID, dumps(args, ensure_ascii=False)) if args else packetID + packetData = '%s+%s' % ( + packetID, + dumps(args, ensure_ascii=False), + ) if args else packetID self.send_packet(6, data=packetData) def set_messageCallback(self, callback): @@ -359,11 +382,14 @@ class _SocketIO(object): try: packet = self.connection.recv() except WebSocketConnectionClosedException: - raise SocketIOConnectionError('Lost connection (Connection closed)') + text = 'Lost connection (Connection closed)' + raise SocketIOConnectionError(text) except socket.timeout: - raise SocketIOConnectionError('Lost connection (Connection timed out)') + text = 'Lost connection (Connection timed out)' + raise SocketIOConnectionError(text) except socket.error: - raise SocketIOConnectionError('Lost connection') + text = 'Lost connection' + raise SocketIOConnectionError(text) try: packetParts = packet.split(':', 3) except AttributeError: @@ -382,7 +408,8 @@ class _SocketIO(object): packetID = self.set_messageCallback(callback) if callback else '' packetParts = [str(code), packetID, path, data] try: - self.connection.send(':'.join(packetParts)) + packet = ':'.join(packetParts) + self.connection.send(packet) except socket.error: raise SocketIOPacketError('Could not send packet') diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index 92ffe5f..bdbfbd5 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -1,16 +1,17 @@ from socketIO_client import SocketIO, BaseNamespace, find_callback -from time import sleep from unittest import TestCase +HOST = 'localhost' PORT = 8000 +DATA = 'xxx' PAYLOAD = {'xxx': 'yyy'} class TestSocketIO(TestCase): def setUp(self): - self.socketIO = SocketIO('localhost', PORT) + self.socketIO = SocketIO(HOST, PORT) self.called_on_response = False def tearDown(self): @@ -18,76 +19,156 @@ class TestSocketIO(TestCase): def on_response(self, *args): self.called_on_response = True - callback, args = find_callback(args) - if callback: - callback(*args) + for arg in args: + if isinstance(arg, dict): + self.assertEqual(arg, PAYLOAD) + else: + self.assertEqual(arg, DATA) + + def is_connected(self, socketIO, connected): + childThreads = [ + socketIO._rhythmicThread, + socketIO._listenerThread, + ] + for childThread in childThreads: + self.assertEqual(not connected, childThread.done.is_set()) + self.assertEqual(connected, socketIO.connected) def test_disconnect(self): - childThreads = [ - self.socketIO._rhythmicThread, - self.socketIO._listenerThread, - ] + 'Terminate child threads after disconnect' + self.is_connected(self.socketIO, True) self.socketIO.disconnect() - for childThread in childThreads: - self.assertEqual(True, childThread.done.is_set()) - self.assertEqual(False, self.socketIO.connected) + self.is_connected(self.socketIO, False) + # Use context manager + with SocketIO(HOST, PORT) as self.socketIO: + self.is_connected(self.socketIO, True) + self.is_connected(self.socketIO, False) + + def test_message(self): + 'Message' + self.socketIO.define(Namespace) + self.socketIO.message() + self.socketIO.wait(0.1) + namespace = self.socketIO.get_namespace() + self.assertEqual(namespace.response, 'message_response') + + def test_message_with_data(self): + 'Message with data' + self.socketIO.define(Namespace) + self.socketIO.message(DATA) + self.socketIO.wait(0.1) + namespace = self.socketIO.get_namespace() + self.assertEqual(namespace.response, DATA) + + def test_message_with_payload(self): + 'Message with payload' + self.socketIO.define(Namespace) + self.socketIO.message(PAYLOAD) + self.socketIO.wait(0.1) + namespace = self.socketIO.get_namespace() + self.assertEqual(namespace.response, PAYLOAD) + + def test_message_with_callback(self): + 'Message with callback' + self.socketIO.message(callback=self.on_response) + self.socketIO.wait_for_callbacks(seconds=0.1) + self.assertEqual(self.called_on_response, True) + + def test_message_with_callback_with_data(self): + 'Message with callback with data' + self.socketIO.message(DATA, self.on_response) + self.socketIO.wait_for_callbacks(seconds=0.1) + self.assertEqual(self.called_on_response, True) def test_emit(self): + 'Emit' self.socketIO.define(Namespace) - self.socketIO.emit('aaa') - sleep(0.1) - self.assertEqual(self.socketIO.get_namespace().payload, '') + self.socketIO.emit('emit') + self.socketIO.wait(0.1) + self.assertEqual(self.socketIO.get_namespace().argsByEvent, { + 'emit_response': (), + }) def test_emit_with_payload(self): + 'Emit with payload' self.socketIO.define(Namespace) - self.socketIO.emit('aaa', PAYLOAD) - sleep(0.1) - self.assertEqual(self.socketIO.get_namespace().payload, PAYLOAD) + self.socketIO.emit('emit_with_payload', PAYLOAD) + self.socketIO.wait(0.1) + self.assertEqual(self.socketIO.get_namespace().argsByEvent, { + 'emit_with_payload_response': (PAYLOAD,), + }) + + def test_emit_with_multiple_payloads(self): + 'Emit with multiple payloads' + self.socketIO.define(Namespace) + self.socketIO.emit('emit_with_multiple_payloads', PAYLOAD, PAYLOAD) + self.socketIO.wait(0.1) + self.assertEqual(self.socketIO.get_namespace().argsByEvent, { + 'emit_with_multiple_payloads_response': (PAYLOAD, PAYLOAD), + }) def test_emit_with_callback(self): - self.socketIO.emit('aaa', PAYLOAD, self.on_response) - self.socketIO.wait(seconds=0.1, forCallbacks=True) + 'Emit with callback' + self.socketIO.emit('emit_with_callback', self.on_response) + self.socketIO.wait_for_callbacks(seconds=0.1) + self.assertEqual(self.called_on_response, True) + + def test_emit_with_callback_with_payload(self): + 'Emit with callback with payload' + self.socketIO.emit('emit_with_callback_with_payload', + self.on_response) + self.socketIO.wait_for_callbacks(seconds=0.1) + self.assertEqual(self.called_on_response, True) + + def test_emit_with_callback_with_multiple_payloads(self): + 'Emit with callback with multiple payloads' + self.socketIO.emit('emit_with_callback_with_multiple_payloads', + self.on_response) + self.socketIO.wait_for_callbacks(seconds=0.1) self.assertEqual(self.called_on_response, True) def test_emit_with_event(self): - self.socketIO.on('aaa_response', self.on_response) - self.socketIO.emit('aaa', PAYLOAD) - sleep(0.1) - self.assertEqual(self.called_on_response, True) - - def test_message(self): - self.socketIO.message(PAYLOAD, self.on_response) - self.socketIO.wait(seconds=0.1, forCallbacks=True) + 'Emit to trigger an event' + self.socketIO.on('emit_with_event_response', self.on_response) + self.socketIO.emit('emit_with_event', PAYLOAD) + self.socketIO.wait_for_callbacks(0.1) self.assertEqual(self.called_on_response, True) def test_ack(self): - self.socketIO.on('bbb_response', self.on_response) - self.socketIO.emit('bbb', PAYLOAD) - sleep(0.1) - self.assertEqual(self.called_on_response, True) + 'Trigger server callback' + self.socketIO.define(Namespace) + self.socketIO.emit('ack', PAYLOAD) + self.socketIO.wait(0.1) + self.assertEqual(self.socketIO.get_namespace().argsByEvent, { + 'ack_response': (PAYLOAD,), + 'ack_callback_response': (PAYLOAD,), + }) def test_namespaces(self): + 'Behave differently in different namespaces' mainNamespace = self.socketIO.define(Namespace) chatNamespace = self.socketIO.define(Namespace, '/chat') newsNamespace = self.socketIO.define(Namespace, '/news') - self.assertNotEqual(mainNamespace.payload, PAYLOAD) - self.assertNotEqual(chatNamespace.payload, PAYLOAD) - self.assertNotEqual(newsNamespace.payload, PAYLOAD) - newsNamespace.emit('aaa', PAYLOAD) - sleep(0.1) - self.assertEqual(newsNamespace.payload, PAYLOAD) - - def test_namespaces_with_callback(self): - mainNamespace = self.socketIO.get_namespace() - mainNamespace.message(PAYLOAD, self.on_response) - sleep(0.1) - self.assertEqual(self.called_on_response, True) + newsNamespace.emit('emit_with_payload', PAYLOAD) + self.socketIO.wait(0.1) + self.assertEqual(mainNamespace.argsByEvent, {}) + self.assertEqual(chatNamespace.argsByEvent, {}) + self.assertEqual(newsNamespace.argsByEvent, { + 'emit_with_payload_response': (PAYLOAD,), + }) class Namespace(BaseNamespace): - payload = None + def initialize(self): + self.response = None + self.argsByEvent = {} - def on_aaa_response(self, data=''): - print '[Event] aaa_response(%s)' % data - self.payload = data + def on_message(self, data): + self.response = data + + def on_event(self, event, *args): + callback, args = find_callback(args) + if callback: + callback(*args) + self.argsByEvent[event] = args From a84f2f40bb1cf722f780685ffc9f8028f0dbc08b Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Fri, 26 Apr 2013 09:39:42 -0700 Subject: [PATCH 026/191] Updated CHANGES --- CHANGES.rst | 5 +++-- TODO.goals | 5 ----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6ff0115..eeae8ed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,13 +1,14 @@ 0.4 --- +- Added support for server-side callbacks thanks to Zac Lee +- Added low-level _SocketIO to remove cyclic references - Merged Channel functionality into BaseNamespace thanks to Alexandre Bourget -- Added support for unicode thanks to Zac Lee 0.3 --- - Added support for secure connections - Added socketIO.wait() -- Improved exception handling in heartbeatThread and namespaceThread +- Improved exception handling in _RhythmicThread and _ListenerThread 0.2 --- diff --git a/TODO.goals b/TODO.goals index d76ca64..84b159c 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,12 +1,7 @@ = Resolve pull requests - + Resolve pull request #6 - + Add unit test for handling server callback - + Use json with ensure_ascii=False - + Add emit with callback in serve_tests Resolve issues Investigate issue #8 Examine forks - Integrate Zac's fork #6 Integrate Sajal's fork #7 Integrate Francis's fork #10 Integrate Paul's fork From 1fc03165414efdeca4ec78a3bec94313c267dd92 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Fri, 26 Apr 2013 09:54:10 -0700 Subject: [PATCH 027/191] Fixes #15 --- socketIO_client/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 5d3499d..5bd75c4 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -218,7 +218,8 @@ class _ListenerThread(Thread): code, packetID, path, data = self._socketIO.recv_packet() except SocketIOConnectionError, error: print error - return + self.cancel() + break except SocketIOPacketError, error: print error continue From f9e093efbbc58fac7b83e65252457f9d4e102bb1 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Fri, 26 Apr 2013 10:19:41 -0700 Subject: [PATCH 028/191] Fixes #7 and #16 --- CHANGES.rst | 1 + setup.py | 2 +- socketIO_client/__init__.py | 17 ++++++++++------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index eeae8ed..ac96c37 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,6 @@ 0.4 --- +- Added support for custom headers and proxies thanks to Rui and Sajal - Added support for server-side callbacks thanks to Zac Lee - Added low-level _SocketIO to remove cyclic references - Merged Channel functionality into BaseNamespace thanks to Alexandre Bourget diff --git a/setup.py b/setup.py index 0f79923..c5f8a26 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ setup( author_email='rhh@crosscompute.com', url='https://github.com/invisibleroads/socketIO-client', install_requires=[ - 'anyjson', + 'requests', 'websocket-client', ], packages=find_packages(), diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 5bd75c4..8a3b27c 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,8 +1,8 @@ +import requests import socket from json import dumps, loads from threading import Thread, Event from time import sleep -from urllib import urlopen from websocket import WebSocketConnectionClosedException, create_connection @@ -87,7 +87,7 @@ class BaseNamespace(object): # pragma: no cover class SocketIO(object): - def __init__(self, host, port, secure=False, proxies=None): + def __init__(self, host, port, secure=False, headers=None, proxies=None): """ Create a socket.io client that connects to a socket.io server at the specified host and port. Set secure=True to use HTTPS / WSS. @@ -95,7 +95,7 @@ class SocketIO(object): SocketIO('localhost', 8000, secure=True, proxies={'https': 'https://proxy.example.com:8080'}) """ - self._socketIO = _SocketIO(host, port, secure, proxies) + self._socketIO = _SocketIO(host, port, secure, headers, proxies) self._namespaceByPath = {} self.define(BaseNamespace) # Define default namespace @@ -294,17 +294,20 @@ class _SocketIO(object): messageID = 0 - def __init__(self, host, port, secure, proxies): + def __init__(self, host, port, secure, headers, proxies): baseURL = '%s:%d/socket.io/%s' % (host, port, PROTOCOL) targetScheme = 'https' if secure else 'http' targetURL = '%s://%s/' % (targetScheme, baseURL) try: - response = urlopen(targetURL, proxies=proxies) + response = requests.get( + targetURL, + headers=headers, + proxies=proxies) except IOError: # pragma: no cover raise SocketIOError('Could not start connection') - if 200 != response.getcode(): # pragma: no cover + if 200 != response.status_code: # pragma: no cover raise SocketIOError('Could not establish connection') - responseParts = response.readline().split(':') + responseParts = response.text.split(':') sessionID = responseParts[0] heartbeatTimeout = int(responseParts[1]) # connectionTimeout = int(responseParts[2]) From 01f1d0d06c08cdb275d7c42382d4d7631c6616a8 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Fri, 26 Apr 2013 10:27:51 -0700 Subject: [PATCH 029/191] Updated README --- README.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.rst b/README.rst index 3c348b8..8093fcc 100644 --- a/README.rst +++ b/README.rst @@ -109,10 +109,20 @@ Define different namespaces on a single socket. :: Open secure websockets (HTTPS / WSS) behind a proxy. :: + from socketIO_client import SocketIO + SocketIO('localhost', 8000, secure=True, proxies={'https': 'https://proxy.example.com:8080'}) +Specify custom headers thanks to the `requests`_ library. :: + + from socketIO_client import SocketIO + from base64 import b64encode + + SocketIO('localhost', 8000, + headers={'Authorization': 'Basic ' + b64encode('username:password')}) + License ------- @@ -129,6 +139,7 @@ Credits .. _socket.io: http://socket.io +.. _requests: http://python-requests.org .. _Guillermo Rauch: https://github.com/guille .. _socket.io specification: https://github.com/LearnBoost/socket.io-spec From 9a13cd5fb706a85b9248ebc12fbd2e1dffe8c81d Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 8 May 2013 00:36:09 -0700 Subject: [PATCH 030/191] Fixes #17 --- README.rst | 6 ++---- setup.py | 2 +- socketIO_client/__init__.py | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 8093fcc..a96918d 100644 --- a/README.rst +++ b/README.rst @@ -67,8 +67,7 @@ Define events in a namespace. :: print 'on_aaa_response', args self.emit('bbb') - socketIO = SocketIO('localhost', 8000) - socketIO.define(Namespace) + socketIO = SocketIO('localhost', 8000, Namespace) socketIO.emit('aaa') socketIO.wait(seconds=1) @@ -81,8 +80,7 @@ Define standard events. :: def on_connect(self): print '[Connected]' - socketIO = SocketIO('localhost', 8000) - socketIO.define(Namespace) + socketIO = SocketIO('localhost', 8000, Namespace) socketIO.wait(seconds=1) Define different namespaces on a single socket. :: diff --git a/setup.py b/setup.py index c5f8a26..b8d6c4f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( name='socketIO-client', - version='0.4', + version='0.5', description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 8a3b27c..59b107f 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -87,7 +87,7 @@ class BaseNamespace(object): # pragma: no cover class SocketIO(object): - def __init__(self, host, port, secure=False, headers=None, proxies=None): + def __init__(self, host, port, Namespace=BaseNamespace, secure=False, headers=None, proxies=None): """ Create a socket.io client that connects to a socket.io server at the specified host and port. Set secure=True to use HTTPS / WSS. @@ -97,7 +97,7 @@ class SocketIO(object): """ self._socketIO = _SocketIO(host, port, secure, headers, proxies) self._namespaceByPath = {} - self.define(BaseNamespace) # Define default namespace + self.define(Namespace) self._rhythmicThread = _RhythmicThread( self._socketIO.heartbeatInterval, From 9cb0c5157336a0772c5daed176ba6b53a8b304d9 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 18 Jul 2013 18:27:42 -0700 Subject: [PATCH 031/191] Updated TODO --- TODO.goals | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/TODO.goals b/TODO.goals index 84b159c..28edde8 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,7 +1,19 @@ = Resolve pull requests + Resolve pull request #10 + Resolve pull request #13 + Resolve pull request #14 + Resolve pull request #18 + Resolve pull request #20 + Resolve pull request #21 + Resolve pull request #23 Resolve issues - Investigate issue #8 -Examine forks - Integrate Sajal's fork #7 - Integrate Francis's fork #10 - Integrate Paul's fork + Resolve issue #8 + Resolve issue #10 + Resolve issue #11 + Resolve issue #13 + Resolve issue #14 + Resolve issue #18 + Resolve issue #20 + Resolve issue #21 + Resolve issue #22 + Resolve issue #23 From 3c04878dfe33a0017ce0cb3cb851bd8c22ea6fff Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Fri, 11 Oct 2013 06:50:48 -0700 Subject: [PATCH 032/191] Updated MANIFEST.in --- CHANGES.rst | 4 ++++ MANIFEST.in | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index ac96c37..acd8d36 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,7 @@ +0.5 +--- +- + 0.4 --- - Added support for custom headers and proxies thanks to Rui and Sajal diff --git a/MANIFEST.in b/MANIFEST.in index 23be747..346209d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -recursive-include socketIOClient * +recursive-include socketIO_client * include *.rst global-exclude *.pyc From d97afb9d2a85c536ff30281a3f65cc7329f61f41 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Fri, 1 Nov 2013 16:14:23 -0700 Subject: [PATCH 033/191] Updated goals --- TODO.goals | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/TODO.goals b/TODO.goals index 28edde8..8cd11fc 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,19 +1,8 @@ -= Resolve pull requests - Resolve pull request #10 - Resolve pull request #13 - Resolve pull request #14 - Resolve pull request #18 - Resolve pull request #20 - Resolve pull request #21 - Resolve pull request #23 -Resolve issues - Resolve issue #8 - Resolve issue #10 - Resolve issue #11 - Resolve issue #13 - Resolve issue #14 - Resolve issue #18 - Resolve issue #20 - Resolve issue #21 - Resolve issue #22 - Resolve issue #23 += Investigate coroutine replacement for threads +Make client robust if server disconnects +Signal on_disconnect +Add on_noop +Add query_string +Add more transports +Replace print with logging +Test with django-socketio From 096d41c072517e6e78ffcc97ba113bd54273b7fe Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 3 Nov 2013 01:01:29 -0800 Subject: [PATCH 034/191] Fixes #23 #21 #20 #18 --- TODO.goals | 16 +- socketIO_client/__init__.py | 774 ++++++++++++++++++++---------------- socketIO_client/tests.py | 62 +-- 3 files changed, 467 insertions(+), 385 deletions(-) diff --git a/TODO.goals b/TODO.goals index 8cd11fc..87f2b73 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,8 +1,8 @@ -= Investigate coroutine replacement for threads -Make client robust if server disconnects -Signal on_disconnect -Add on_noop -Add query_string -Add more transports -Replace print with logging -Test with django-socketio += Test with django-socketio ++ Investigate coroutine replacement for threads ++ Make client robust if server disconnects ++ Signal on_disconnect ++ Add on_noop ++ Add query_string ++ Add more transports ++ Replace print with logging diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 59b107f..6dbbc7f 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,47 +1,66 @@ +import logging +import json import requests import socket -from json import dumps, loads -from threading import Thread, Event -from time import sleep -from websocket import WebSocketConnectionClosedException, create_connection +import time +import websocket +from collections import namedtuple -PROTOCOL = 1 # socket.io protocol version +_Session = namedtuple('_Session', [ + 'id', + 'heartbeat_timeout', + 'server_supported_transports', +]) +_log = logging.getLogger(__name__) +TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling' +PROTOCOL_VERSION = 1 -class BaseNamespace(object): # pragma: no cover - 'Define socket.io behavior' +class BaseNamespace(object): + 'Define client behavior' - def __init__(self, _socketIO, path): - self._socketIO = _socketIO + def __init__(self, _transport, path): + self._transport = _transport self._path = path - self._callbackByEvent = {} + self._callback_by_event = {} self.initialize() def initialize(self): 'Initialize custom variables here; you can override this method' pass + def message(self, data='', callback=None): + self._transport.message(self._path, data, callback) + + def emit(self, event, *args, **kw): + callback, args = find_callback(args, kw) + self._transport.emit(self._path, event, args, callback) + + def on(self, event, callback): + 'Define a callback to handle a custom event emitted by the server' + self._callback_by_event[event] = callback + def on_connect(self): - 'Called when socket is connecting; you can override this method' - pass + 'Called after server connects; you can override this method' + _log.debug('[connect]') def on_disconnect(self): - 'Called when socket is disconnecting; you can override this method' - pass + 'Called after server disconnects; you can override this method' + _log.debug('[disconnect]') - def on_error(self, reason, advice): - 'Called when server sends an error; you can override this method' - print '[Error] %s' % advice + def on_heartbeat(self): + 'Called after server sends a heartbeat; you can override this method' + _log.debug('[heartbeat]') def on_message(self, data): - 'Called when server sends a message; you can override this method' - print '[Message] %s' % data + 'Called after server sends a message; you can override this method' + _log.info('[message] %s', data) def on_event(self, event, *args): """ - Called when server emits an event; you can override this method. - Called only if the program cannot find a more specific event handler, + Called after server sends an event; you can override this method. + Called only if a custom event handler does not exist, such as one defined by namespace.on('my_event', my_function). """ callback, args = find_callback(args) @@ -49,389 +68,387 @@ class BaseNamespace(object): # pragma: no cover if callback: arguments.append('callback(*args)') callback(*args) - print '[Event] %s(%s)' % (event, ', '.join(arguments)) + _log.info('[event] %s(%s)', event, ', '.join(arguments)) + + def on_error(self, reason, advice): + 'Called after server sends an error; you can override this method' + _log.info('[error] %s', advice) + + def on_noop(self): + 'Called after server sends a noop; you can override this method' + _log.info('[noop]') def on_open(self, *args): - print '[Open]', args + _log.info('[open] %s', args) def on_close(self, *args): - print '[Close]', args + _log.info('[close] %s', args) def on_retry(self, *args): - print '[Retry]', args + _log.info('[retry] %s', args) def on_reconnect(self, *args): - print '[Reconnect]', args + _log.info('[reconnect] %s', args) - def message(self, data='', callback=None): - self._socketIO.message(data, callback, path=self._path) - - def emit(self, event, *args, **kw): - kw['path'] = self._path - self._socketIO.emit(event, *args, **kw) - - def on(self, event, callback): - 'Define a callback to handle a custom event emitted by the server' - self._callbackByEvent[event] = callback - - def _get_eventCallback(self, event): + def _find_event_callback(self, event): # Check callbacks defined by on() try: - return self._callbackByEvent[event] + return self._callback_by_event[event] except KeyError: pass # Check callbacks defined explicitly or use on_event() - callback = lambda *args: self.on_event(event, *args) - return getattr(self, 'on_' + event.replace(' ', '_'), callback) + return getattr( + self, + 'on_' + event.replace(' ', '_'), + lambda *args: self.on_event(event, *args)) class SocketIO(object): - def __init__(self, host, port, Namespace=BaseNamespace, secure=False, headers=None, proxies=None): + def __init__( + self, host, port, Namespace=BaseNamespace, secure=False, + wait_for_connection=True, transports=TRANSPORTS, **kw): """ Create a socket.io client that connects to a socket.io server - at the specified host and port. Set secure=True to use HTTPS / WSS. + at the specified host and port. + - Define the behavior of the client by specifying a custom Namespace. + - Set secure=True to use HTTPS / WSS. + - Set wait_for_connection=True to block until we have a connection. + - List the transports you want to use (%s). + - Pass query params, headers, cookies, proxies as keyword arguments. - SocketIO('localhost', 8000, secure=True, - proxies={'https': 'https://proxy.example.com:8080'}) - """ - self._socketIO = _SocketIO(host, port, secure, headers, proxies) - self._namespaceByPath = {} + SocketIO('localhost', 8000, proxies={ + 'https': 'https://proxy.example.com:8080'}) + """ % ', '.join(TRANSPORTS) + self.base_url = '%s:%d/socket.io/%s' % (host, port, PROTOCOL_VERSION) + self.secure = secure + self.wait_for_connection = wait_for_connection + self._namespace_by_path = {} + self.client_supported_transports = transports + self.kw = kw self.define(Namespace) - self._rhythmicThread = _RhythmicThread( - self._socketIO.heartbeatInterval, - self._socketIO.send_heartbeat) - self._rhythmicThread.start() - - self._listenerThread = _ListenerThread( - self._socketIO, - self._namespaceByPath) - self._listenerThread.start() - def __enter__(self): return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, *exception_pack): self.disconnect() def __del__(self): - self.disconnect(close=False) - - @property - def connected(self): - return self._socketIO.connected - - def disconnect(self, path='', close=True): - if self.connected: - self._socketIO.disconnect(path, close) - if path: - del self._namespaceByPath[path] - else: - self._rhythmicThread.cancel() - self._listenerThread.cancel() + self.disconnect() def define(self, Namespace, path=''): if path: - self._socketIO.connect(path) - namespace = Namespace(self._socketIO, path) - self._namespaceByPath[path] = namespace + self._transport.connect(path) + namespace = Namespace(self._transport, path) + self._namespace_by_path[path] = namespace return namespace - def get_namespace(self, path=''): - return self._namespaceByPath[path] - def on(self, event, callback, path=''): return self.get_namespace(path).on(event, callback) def message(self, data='', callback=None, path=''): - self._socketIO.message(data, callback, path) + self._transport.message(path, data, callback) def emit(self, event, *args, **kw): - self._socketIO.emit(event, *args, **kw) + path = kw.get('path', '') + callback, args = find_callback(args, kw) + self._transport.emit(path, event, args, callback) - def wait(self, seconds=None): - if seconds: - self._listenerThread.wait(seconds) - else: - try: - while self.connected: - sleep(1) - except KeyboardInterrupt: - pass - - def wait_for_callbacks(self, seconds=None): - self._listenerThread.wait_for_callbacks(seconds) - - -class _RhythmicThread(Thread): - 'Execute call every few seconds' - - daemon = True - - def __init__(self, intervalInSeconds, call, *args, **kw): - super(_RhythmicThread, self).__init__() - self.intervalInSeconds = intervalInSeconds - self.call = call - self.args = args - self.kw = kw - self.done = Event() - - def run(self): - while not self.done.is_set(): - self.call(*self.args, **self.kw) - self.done.wait(self.intervalInSeconds) - - def cancel(self): - self.done.set() - - -class _ListenerThread(Thread): - 'Process messages from socket.io server' - - daemon = True - - def __init__(self, _socketIO, _namespaceByPath): - super(_ListenerThread, self).__init__() - self._socketIO = _socketIO - self._namespaceByPath = _namespaceByPath - self.done = Event() - self.ready = Event() - self.ready.set() - - def cancel(self): - self.done.set() - - def wait(self, seconds): - self.done.wait(seconds) - - def wait_for_callbacks(self, seconds): - self.ready.clear() - self.ready.wait(seconds) - - def get_ackCallback(self, packetID): - return lambda *args: self._socketIO.ack(packetID, *args) - - def run(self): - while not self.done.is_set(): - try: - code, packetID, path, data = self._socketIO.recv_packet() - except SocketIOConnectionError, error: - print error - self.cancel() - break - except SocketIOPacketError, error: - print error - continue - try: - namespace = self._namespaceByPath[path] - except KeyError: - print 'Received unexpected path (%s)' % path - continue - try: - delegate = { - '0': self.on_disconnect, - '1': self.on_connect, - '2': self.on_heartbeat, - '3': self.on_message, - '4': self.on_json, - '5': self.on_event, - '6': self.on_ack, - '7': self.on_error, - }[code] - except KeyError: - print 'Received unexpected code (%s)' % code - continue - delegate(packetID, namespace._get_eventCallback, data) - - def on_disconnect(self, packetID, get_eventCallback, data): - get_eventCallback('disconnect')() - - def on_connect(self, packetID, get_eventCallback, data): - get_eventCallback('connect')() - - def on_heartbeat(self, packetID, get_eventCallback, data): - pass - - def on_message(self, packetID, get_eventCallback, data): - args = [data] - if packetID: - args.append(self.get_ackCallback(packetID)) - get_eventCallback('message')(*args) - - def on_json(self, packetID, get_eventCallback, data): - args = [loads(data)] - if packetID: - args.append(self.get_ackCallback(packetID)) - get_eventCallback('message')(*args) - - def on_event(self, packetID, get_eventCallback, data): - valueByName = loads(data) - event = valueByName['name'] - args = valueByName.get('args', []) - if packetID: - args.append(self.get_ackCallback(packetID)) - get_eventCallback(event)(*args) - - def on_ack(self, packetID, get_eventCallback, data): - dataParts = data.split('+', 1) - messageID = int(dataParts[0]) - args = loads(dataParts[1]) if len(dataParts) > 1 else [] - callback = self._socketIO.get_messageCallback(messageID) - if not callback: - return - callback(*args) - if not self._socketIO.has_messageCallback: - self.ready.set() - - def on_error(self, packetID, get_eventCallback, data): - reason, advice = data.split('+', 1) - get_eventCallback('error')(reason, advice) - - -class _SocketIO(object): - 'Low-level interface to remove cyclic references in child threads' - - messageID = 0 - - def __init__(self, host, port, secure, headers, proxies): - baseURL = '%s:%d/socket.io/%s' % (host, port, PROTOCOL) - targetScheme = 'https' if secure else 'http' - targetURL = '%s://%s/' % (targetScheme, baseURL) + def wait(self, seconds=None, for_callbacks=False): try: - response = requests.get( - targetURL, - headers=headers, - proxies=proxies) - except IOError: # pragma: no cover - raise SocketIOError('Could not start connection') - if 200 != response.status_code: # pragma: no cover - raise SocketIOError('Could not establish connection') - responseParts = response.text.split(':') - sessionID = responseParts[0] - heartbeatTimeout = int(responseParts[1]) - # connectionTimeout = int(responseParts[2]) - supportedTransports = responseParts[3].split(',') - if 'websocket' not in supportedTransports: - raise SocketIOError('Could not parse handshake') - socketScheme = 'wss' if secure else 'ws' - socketURL = '%s://%s/websocket/%s' % (socketScheme, baseURL, sessionID) - self.connection = create_connection(socketURL) - self.heartbeatInterval = heartbeatTimeout - 2 - self.callbackByMessageID = {} - - def __del__(self): - self.disconnect(close=False) - - def disconnect(self, path='', close=True): - if not self.connected: - return - if path: - self.send_packet(0, path) - elif close: - self.connection.close() - - def connect(self, path): - self.send_packet(1, path) - - def send_heartbeat(self): - try: - self.send_packet(2) - except SocketIOPacketError: - print 'Could not send heartbeat' + warning_screen = _yield_warning_screen(seconds, sleep=1) + for elapsed_time in warning_screen: + try: + if for_callbacks and not self._transport.has_ack_callback: + break + try: + self._process_packet(self._transport.recv_packet()) + except _TimeoutError: + pass + except _PacketError as error: + _log.warn('[packet error] %s', error) + self.heartbeat_pacemaker.send(elapsed_time) + except SocketIOConnectionError as error: + self.disconnect() + warning = Exception('[connection error] %s' % error) + warning_screen.throw(warning) + except KeyboardInterrupt: pass - def message(self, data, callback, path): - if isinstance(data, basestring): - code = 3 - packetData = data - else: - code = 4 - packetData = dumps(data, ensure_ascii=False) - self.send_packet(code, path, packetData, callback) + def wait_for_callbacks(self, seconds=None): + self.wait(seconds, for_callbacks=True) - def emit(self, event, *args, **kw): - callback, args = find_callback(args, kw) - packetData = dumps(dict(name=event, args=args), ensure_ascii=False) - path = kw.get('path', '') - self.send_packet(5, path, packetData, callback) - - def ack(self, packetID, *args): - packetID = packetID.rstrip('+') - packetData = '%s+%s' % ( - packetID, - dumps(args, ensure_ascii=False), - ) if args else packetID - self.send_packet(6, data=packetData) - - def set_messageCallback(self, callback): - 'Set callback that will be called after receiving an acknowledgment' - self.messageID += 1 - self.callbackByMessageID[self.messageID] = callback - return '%s+' % self.messageID - - def get_messageCallback(self, messageID): - try: - callback = self.callbackByMessageID[messageID] - del self.callbackByMessageID[messageID] - return callback - except KeyError: - return - - @property - def has_messageCallback(self): - return True if self.callbackByMessageID else False - - def recv_packet(self): - try: - packet = self.connection.recv() - except WebSocketConnectionClosedException: - text = 'Lost connection (Connection closed)' - raise SocketIOConnectionError(text) - except socket.timeout: - text = 'Lost connection (Connection timed out)' - raise SocketIOConnectionError(text) - except socket.error: - text = 'Lost connection' - raise SocketIOConnectionError(text) - try: - packetParts = packet.split(':', 3) - except AttributeError: - raise SocketIOPacketError('Received invalid packet (%s)' % packet) - packetCount = len(packetParts) - code, packetID, path, data = None, None, None, None - if 4 == packetCount: - code, packetID, path, data = packetParts - elif 3 == packetCount: - code, packetID, path = packetParts - elif 1 == packetCount: - code = packetParts[0] - return code, packetID, path, data - - def send_packet(self, code, path='', data='', callback=None): - packetID = self.set_messageCallback(callback) if callback else '' - packetParts = [str(code), packetID, path, data] - try: - packet = ':'.join(packetParts) - self.connection.send(packet) - except socket.error: - raise SocketIOPacketError('Could not send packet') + def disconnect(self, path=''): + if self.connected: + self._transport.disconnect(path) + namespace = self._namespace_by_path[path] + namespace.on_disconnect() + if path: + del self._namespace_by_path[path] @property def connected(self): - return self.connection.connected + return self.__transport.connected + + @property + def _transport(self): + try: + if self.connected: + return self.__transport + except AttributeError: + pass + warning_screen = _yield_warning_screen(seconds=None, sleep=1) + for elapsed_time in warning_screen: + try: + self.__transport = self._get_transport() + break + except SocketIOConnectionError as error: + if not self.wait_for_connection: + raise + warning = Exception('[waiting for connection] %s' % error) + warning_screen.throw(warning) + return self.__transport + + def _get_transport(self): + self.session = _get_session(self.secure, self.base_url, **self.kw) + _log.debug('[transports available] %s', ' '.join( + self.session.server_supported_transports)) + # Initialize heartbeat_pacemaker + self.heartbeat_pacemaker = self._make_heartbeat_pacemaker( + heartbeat_interval=self.session.heartbeat_timeout - 2) + self.heartbeat_pacemaker.next() + # Negotiate transport + transport = _negotiate_transport( + self.client_supported_transports, self.session, + self.secure, self.base_url, **self.kw) + # Update namespaces + for namespace in self._namespace_by_path.values(): + namespace._transport = transport + return transport + + def _make_heartbeat_pacemaker(self, heartbeat_interval): + heartbeat_time = 0 + while True: + elapsed_time = (yield) + if elapsed_time - heartbeat_time > heartbeat_interval: + heartbeat_time = elapsed_time + self._transport.send_heartbeat() + + def _process_packet(self, packet): + code, packet_id, path, data = packet + namespace = self.get_namespace(path) + delegate = self._get_delegate(code) + delegate(packet_id, data, namespace._find_event_callback) + + def get_namespace(self, path=''): + try: + return self._namespace_by_path[path] + except KeyError: + raise _PacketError('unexpected namespace path (%s)' % path) + + def _get_delegate(self, code): + try: + return { + '0': self._on_disconnect, + '1': self._on_connect, + '2': self._on_heartbeat, + '3': self._on_message, + '4': self._on_json, + '5': self._on_event, + '6': self._on_ack, + '7': self._on_error, + '8': self._on_noop, + }[code] + except KeyError: + raise _PacketError('unexpected code (%s)' % code) + + def _on_disconnect(self, packet_id, data, find_event_callback): + find_event_callback('disconnect')() + + def _on_connect(self, packet_id, data, find_event_callback): + find_event_callback('connect')() + + def _on_heartbeat(self, packet_id, data, find_event_callback): + find_event_callback('heartbeat')() + + def _on_message(self, packet_id, data, find_event_callback): + args = [data] + if packet_id: + args.append(self._prepare_to_send_ack(packet_id)) + find_event_callback('message')(*args) + + def _on_json(self, packet_id, data, find_event_callback): + args = [json.loads(data)] + if packet_id: + args.append(self._prepare_to_send_ack(packet_id)) + find_event_callback('message')(*args) + + def _on_event(self, packet_id, data, find_event_callback): + value_by_name = json.loads(data) + event = value_by_name['name'] + args = value_by_name.get('args', []) + if packet_id: + args.append(self._prepare_to_send_ack(packet_id)) + find_event_callback(event)(*args) + + def _on_ack(self, packet_id, data, find_event_callback): + data_parts = data.split('+', 1) + packet_id = data_parts[0] + try: + ack_callback = self._transport.get_ack_callback(packet_id) + except KeyError: + return + args = json.loads(data_parts[1]) if len(data_parts) > 1 else [] + ack_callback(*args) + + def _on_error(self, packet_id, data, find_event_callback): + reason, advice = data.split('+', 1) + find_event_callback('error')(reason, advice) + + def _on_noop(self, packet_id, data, find_event_callback): + find_event_callback('noop')() + + def _prepare_to_send_ack(self, packet_id): + 'Return function that acknowledges the server' + return lambda *args: self._transport.ack(packet_id, *args) class SocketIOError(Exception): pass +class _TimeoutError(Exception): + pass + + +class _PacketError(SocketIOError): + pass + + class SocketIOConnectionError(SocketIOError): pass -class SocketIOPacketError(SocketIOError): - pass +class _AbstractTransport(object): + + def __init__(self): + self._packet_id = 0 + self._callback_by_packet_id = {} + + def disconnect(self, path=''): + if not self.connected: + return + if path: + self.send_packet(0, path) + else: + self.connection.close() + + def connect(self, path): + self.send_packet(1, path) + + def send_heartbeat(self): + self.send_packet(2) + + def message(self, path, data, callback): + if isinstance(data, basestring): + code = 3 + else: + code = 4 + data = json.dumps(data, ensure_ascii=False) + self.send_packet(code, path, data, callback) + + def emit(self, path, event, args, callback): + data = json.dumps(dict(name=event, args=args), ensure_ascii=False) + self.send_packet(5, path, data, callback) + + def ack(self, packet_id, *args): + packet_id = packet_id.rstrip('+') + data = '%s+%s' % ( + packet_id, + json.dumps(args, ensure_ascii=False), + ) if args else packet_id + self.send_packet(6, data=data) + + def noop(self): + self.send_packet(8) + + def send_packet(self, code, path='', data='', callback=None): + packet_id = self.set_ack_callback(callback) if callback else '' + packet_parts = str(code), packet_id, path, data + packet_text = ':'.join(packet_parts) + self.send(packet_text) + _log.debug('[packet sent] %s', packet_text) + + def recv_packet(self): + code, packet_id, path, data = None, None, None, None + packet_text = self.recv() + _log.debug('[packet received] %s', packet_text) + try: + packet_parts = packet_text.split(':', 3) + except AttributeError: + raise _PacketError('invalid packet (%s)' % packet_text) + packet_count = len(packet_parts) + if 4 == packet_count: + code, packet_id, path, data = packet_parts + elif 3 == packet_count: + code, packet_id, path = packet_parts + elif 1 == packet_count: + code = packet_parts[0] + return code, packet_id, path, data + + def set_ack_callback(self, callback): + 'Set callback to be called after server sends an acknowledgment' + self._packet_id += 1 + self._callback_by_packet_id[str(self._packet_id)] = callback + return '%s+' % self._packet_id + + def get_ack_callback(self, packet_id): + 'Get callback to be called after server sends an acknowledgment' + callback = self._callback_by_packet_id[packet_id] + del self._callback_by_packet_id[packet_id] + return callback + + @property + def has_ack_callback(self): + return True if self._callback_by_packet_id else False + + +class _WebsocketTransport(_AbstractTransport): + + def __init__(self, session, secure, base_url, **kw): + super(_WebsocketTransport, self).__init__() + url = '%s://%s/websocket/%s' % ( + 'wss' if secure else 'ws', + base_url, session.id) + _log.debug('[transport selected] %s', url) + try: + self.connection = websocket.create_connection(url) + except socket.timeout as error: + raise SocketIOConnectionError(error) + except socket.error as error: + raise SocketIOConnectionError(error) + self.connection.settimeout(1) + + @property + def connected(self): + return self.connection.connected + + def recv(self): + try: + return self.connection.recv() + except socket.timeout: + raise _TimeoutError + except socket.error as error: + raise SocketIOConnectionError(error) + except websocket.WebSocketConnectionClosedException: + raise SocketIOConnectionError('server closed connection') + + def send(self, packet_text): + try: + self.connection.send(packet_text) + except socket.error: + raise SocketIOConnectionError('could not send %s' % packet_text) def find_callback(args, kw=None): @@ -442,3 +459,68 @@ def find_callback(args, kw=None): return kw['callback'], args except (KeyError, TypeError): return None, args + + +def _yield_warning_screen(seconds=None, sleep=0): + last_warning = None + for elapsed_time in _yield_elapsed_time(seconds): + try: + yield elapsed_time + except Exception as warning: + warning = str(warning) + if last_warning != warning: + last_warning = warning + _log.warn(warning) + time.sleep(sleep) + + +def _yield_elapsed_time(seconds=None): + if seconds is None: + while True: + yield float('inf') + start_time = time.time() + while time.time() - start_time < seconds: + yield time.time() - start_time + + +def _get_session(secure, base_url, **kw): + server_url = '%s://%s/' % ('https' if secure else 'http', base_url) + try: + response = requests.get(server_url, **kw) + except requests.exceptions.ConnectionError: + raise SocketIOConnectionError('could not start connection') + status = response.status_code + if 200 != status: + raise SocketIOConnectionError('unexpected status code (%s)' % status) + response_parts = response.text.split(':') + return _Session( + id=response_parts[0], + heartbeat_timeout=int(response_parts[1]), + server_supported_transports=response_parts[3].split(',')) + + +def _negotiate_transport( + client_supported_transports, session, + secure, base_url, **kw): + server_supported_transports = session.server_supported_transports + for supported_transport in client_supported_transports: + if supported_transport in server_supported_transports: + return { + 'websocket': _WebsocketTransport, + # 'xhr-polling': + # 'jsonp-polling': + }[supported_transport](session, secure, base_url, **kw) + raise SocketIOError(' '.join([ + 'could not negotiate a transport:', + 'client supports %s but' % ', '.join(client_supported_transports), + 'server supports %s' % ', '.join(server_supported_transports), + ])) + + +if __name__ == '__main__': + requests_log = logging.getLogger('requests') + requests_log.setLevel(logging.WARNING) + logging.basicConfig(level=logging.DEBUG) + socketIO = SocketIO('localhost', 8000) + socketIO.emit('aaa') + socketIO.wait() diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index bdbfbd5..d16e7cd 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -1,3 +1,4 @@ +import logging from socketIO_client import SocketIO, BaseNamespace, find_callback from unittest import TestCase @@ -6,6 +7,7 @@ HOST = 'localhost' PORT = 8000 DATA = 'xxx' PAYLOAD = {'xxx': 'yyy'} +logging.basicConfig(level=logging.DEBUG) class TestSocketIO(TestCase): @@ -25,24 +27,18 @@ class TestSocketIO(TestCase): else: self.assertEqual(arg, DATA) - def is_connected(self, socketIO, connected): - childThreads = [ - socketIO._rhythmicThread, - socketIO._listenerThread, - ] - for childThread in childThreads: - self.assertEqual(not connected, childThread.done.is_set()) - self.assertEqual(connected, socketIO.connected) - def test_disconnect(self): - 'Terminate child threads after disconnect' - self.is_connected(self.socketIO, True) + 'Disconnect' + self.assertTrue(self.socketIO.connected) self.socketIO.disconnect() - self.is_connected(self.socketIO, False) + self.assertFalse(self.socketIO.connected) # Use context manager - with SocketIO(HOST, PORT) as self.socketIO: - self.is_connected(self.socketIO, True) - self.is_connected(self.socketIO, False) + with SocketIO(HOST, PORT, Namespace) as self.socketIO: + namespace = self.socketIO.get_namespace() + self.assertFalse(namespace.called_on_disconnect) + self.assertTrue(self.socketIO.connected) + self.assertTrue(namespace.called_on_disconnect) + self.assertFalse(self.socketIO.connected) def test_message(self): 'Message' @@ -72,20 +68,20 @@ class TestSocketIO(TestCase): 'Message with callback' self.socketIO.message(callback=self.on_response) self.socketIO.wait_for_callbacks(seconds=0.1) - self.assertEqual(self.called_on_response, True) + self.assertTrue(self.called_on_response) def test_message_with_callback_with_data(self): 'Message with callback with data' self.socketIO.message(DATA, self.on_response) self.socketIO.wait_for_callbacks(seconds=0.1) - self.assertEqual(self.called_on_response, True) + self.assertTrue(self.called_on_response) def test_emit(self): 'Emit' self.socketIO.define(Namespace) self.socketIO.emit('emit') self.socketIO.wait(0.1) - self.assertEqual(self.socketIO.get_namespace().argsByEvent, { + self.assertEqual(self.socketIO.get_namespace().args_by_event, { 'emit_response': (), }) @@ -94,7 +90,7 @@ class TestSocketIO(TestCase): self.socketIO.define(Namespace) self.socketIO.emit('emit_with_payload', PAYLOAD) self.socketIO.wait(0.1) - self.assertEqual(self.socketIO.get_namespace().argsByEvent, { + self.assertEqual(self.socketIO.get_namespace().args_by_event, { 'emit_with_payload_response': (PAYLOAD,), }) @@ -103,7 +99,7 @@ class TestSocketIO(TestCase): self.socketIO.define(Namespace) self.socketIO.emit('emit_with_multiple_payloads', PAYLOAD, PAYLOAD) self.socketIO.wait(0.1) - self.assertEqual(self.socketIO.get_namespace().argsByEvent, { + self.assertEqual(self.socketIO.get_namespace().args_by_event, { 'emit_with_multiple_payloads_response': (PAYLOAD, PAYLOAD), }) @@ -111,35 +107,35 @@ class TestSocketIO(TestCase): 'Emit with callback' self.socketIO.emit('emit_with_callback', self.on_response) self.socketIO.wait_for_callbacks(seconds=0.1) - self.assertEqual(self.called_on_response, True) + self.assertTrue(self.called_on_response) def test_emit_with_callback_with_payload(self): 'Emit with callback with payload' self.socketIO.emit('emit_with_callback_with_payload', self.on_response) self.socketIO.wait_for_callbacks(seconds=0.1) - self.assertEqual(self.called_on_response, True) + self.assertTrue(self.called_on_response) def test_emit_with_callback_with_multiple_payloads(self): 'Emit with callback with multiple payloads' self.socketIO.emit('emit_with_callback_with_multiple_payloads', self.on_response) self.socketIO.wait_for_callbacks(seconds=0.1) - self.assertEqual(self.called_on_response, True) + self.assertTrue(self.called_on_response) def test_emit_with_event(self): 'Emit to trigger an event' self.socketIO.on('emit_with_event_response', self.on_response) self.socketIO.emit('emit_with_event', PAYLOAD) - self.socketIO.wait_for_callbacks(0.1) - self.assertEqual(self.called_on_response, True) + self.socketIO.wait(0.1) + self.assertTrue(self.called_on_response) def test_ack(self): 'Trigger server callback' self.socketIO.define(Namespace) self.socketIO.emit('ack', PAYLOAD) self.socketIO.wait(0.1) - self.assertEqual(self.socketIO.get_namespace().argsByEvent, { + self.assertEqual(self.socketIO.get_namespace().args_by_event, { 'ack_response': (PAYLOAD,), 'ack_callback_response': (PAYLOAD,), }) @@ -151,9 +147,9 @@ class TestSocketIO(TestCase): newsNamespace = self.socketIO.define(Namespace, '/news') newsNamespace.emit('emit_with_payload', PAYLOAD) self.socketIO.wait(0.1) - self.assertEqual(mainNamespace.argsByEvent, {}) - self.assertEqual(chatNamespace.argsByEvent, {}) - self.assertEqual(newsNamespace.argsByEvent, { + self.assertEqual(mainNamespace.args_by_event, {}) + self.assertEqual(chatNamespace.args_by_event, {}) + self.assertEqual(newsNamespace.args_by_event, { 'emit_with_payload_response': (PAYLOAD,), }) @@ -162,7 +158,11 @@ class Namespace(BaseNamespace): def initialize(self): self.response = None - self.argsByEvent = {} + self.args_by_event = {} + self.called_on_disconnect = False + + def on_disconnect(self): + self.called_on_disconnect = True def on_message(self, data): self.response = data @@ -171,4 +171,4 @@ class Namespace(BaseNamespace): callback, args = find_callback(args) if callback: callback(*args) - self.argsByEvent[event] = args + self.args_by_event[event] = args From 018e5660494f1be2775ed8543b09c1de74631427 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 3 Nov 2013 11:59:54 -0800 Subject: [PATCH 035/191] Debugging alternate transports --- serve_tests.js | 1 + socketIO_client/__init__.py | 219 ++++--------------------- socketIO_client/exceptions.py | 14 ++ socketIO_client/tests.py | 86 ++++++---- socketIO_client/transports.py | 296 ++++++++++++++++++++++++++++++++++ 5 files changed, 400 insertions(+), 216 deletions(-) create mode 100644 socketIO_client/exceptions.py create mode 100644 socketIO_client/transports.py diff --git a/serve_tests.js b/serve_tests.js index 82c8024..f69247a 100644 --- a/serve_tests.js +++ b/serve_tests.js @@ -15,6 +15,7 @@ var main = io.of('').on('connection', function(socket) { } }); socket.on('emit', function() { + console.log('hey'); socket.emit('emit_response'); }); socket.on('emit_with_payload', function(payload) { diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 6dbbc7f..2759f5a 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,20 +1,21 @@ import logging import json import requests -import socket import time -import websocket from collections import namedtuple +from .exceptions import SocketIOConnectionError, _TimeoutError, _PacketError +from .transports import _get_response, _negotiate_transport, TRANSPORTS -_Session = namedtuple('_Session', [ + +_SocketIOSession = namedtuple('_SocketIOSession', [ 'id', 'heartbeat_timeout', 'server_supported_transports', ]) _log = logging.getLogger(__name__) -TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling' PROTOCOL_VERSION = 1 +RETRY_INTERVAL_IN_SECONDS = 1 class BaseNamespace(object): @@ -157,22 +158,26 @@ class SocketIO(object): def wait(self, seconds=None, for_callbacks=False): try: - warning_screen = _yield_warning_screen(seconds, sleep=1) + warning_screen = _yield_warning_screen(seconds) for elapsed_time in warning_screen: try: if for_callbacks and not self._transport.has_ack_callback: break try: - self._process_packet(self._transport.recv_packet()) + packet = self._transport.recv_packet().next() + self._process_packet(packet) except _TimeoutError: pass - except _PacketError as error: - _log.warn('[packet error] %s', error) + except _PacketError as e: + _log.warn('[packet error] %s', e) self.heartbeat_pacemaker.send(elapsed_time) - except SocketIOConnectionError as error: + except SocketIOConnectionError as e: + try: + warning = Exception('[connection error] %s' % e) + warning_screen.throw(warning) + except StopIteration: + _log.warn(warning) self.disconnect() - warning = Exception('[connection error] %s' % error) - warning_screen.throw(warning) except KeyboardInterrupt: pass @@ -198,29 +203,33 @@ class SocketIO(object): return self.__transport except AttributeError: pass - warning_screen = _yield_warning_screen(seconds=None, sleep=1) + warning_screen = _yield_warning_screen(seconds=None) for elapsed_time in warning_screen: try: self.__transport = self._get_transport() break - except SocketIOConnectionError as error: + except SocketIOConnectionError as e: if not self.wait_for_connection: raise - warning = Exception('[waiting for connection] %s' % error) - warning_screen.throw(warning) + try: + warning = Exception('[waiting for connection] %s' % e) + warning_screen.throw(warning) + except StopIteration: + _log.warn(warning) return self.__transport def _get_transport(self): - self.session = _get_session(self.secure, self.base_url, **self.kw) + socketIO_session = _get_socketIO_session( + self.secure, self.base_url, **self.kw) _log.debug('[transports available] %s', ' '.join( - self.session.server_supported_transports)) + socketIO_session.server_supported_transports)) # Initialize heartbeat_pacemaker self.heartbeat_pacemaker = self._make_heartbeat_pacemaker( - heartbeat_interval=self.session.heartbeat_timeout - 2) + heartbeat_interval=socketIO_session.heartbeat_timeout - 2) self.heartbeat_pacemaker.next() # Negotiate transport transport = _negotiate_transport( - self.client_supported_transports, self.session, + self.client_supported_transports, socketIO_session, self.secure, self.base_url, **self.kw) # Update namespaces for namespace in self._namespace_by_path.values(): @@ -314,143 +323,6 @@ class SocketIO(object): return lambda *args: self._transport.ack(packet_id, *args) -class SocketIOError(Exception): - pass - - -class _TimeoutError(Exception): - pass - - -class _PacketError(SocketIOError): - pass - - -class SocketIOConnectionError(SocketIOError): - pass - - -class _AbstractTransport(object): - - def __init__(self): - self._packet_id = 0 - self._callback_by_packet_id = {} - - def disconnect(self, path=''): - if not self.connected: - return - if path: - self.send_packet(0, path) - else: - self.connection.close() - - def connect(self, path): - self.send_packet(1, path) - - def send_heartbeat(self): - self.send_packet(2) - - def message(self, path, data, callback): - if isinstance(data, basestring): - code = 3 - else: - code = 4 - data = json.dumps(data, ensure_ascii=False) - self.send_packet(code, path, data, callback) - - def emit(self, path, event, args, callback): - data = json.dumps(dict(name=event, args=args), ensure_ascii=False) - self.send_packet(5, path, data, callback) - - def ack(self, packet_id, *args): - packet_id = packet_id.rstrip('+') - data = '%s+%s' % ( - packet_id, - json.dumps(args, ensure_ascii=False), - ) if args else packet_id - self.send_packet(6, data=data) - - def noop(self): - self.send_packet(8) - - def send_packet(self, code, path='', data='', callback=None): - packet_id = self.set_ack_callback(callback) if callback else '' - packet_parts = str(code), packet_id, path, data - packet_text = ':'.join(packet_parts) - self.send(packet_text) - _log.debug('[packet sent] %s', packet_text) - - def recv_packet(self): - code, packet_id, path, data = None, None, None, None - packet_text = self.recv() - _log.debug('[packet received] %s', packet_text) - try: - packet_parts = packet_text.split(':', 3) - except AttributeError: - raise _PacketError('invalid packet (%s)' % packet_text) - packet_count = len(packet_parts) - if 4 == packet_count: - code, packet_id, path, data = packet_parts - elif 3 == packet_count: - code, packet_id, path = packet_parts - elif 1 == packet_count: - code = packet_parts[0] - return code, packet_id, path, data - - def set_ack_callback(self, callback): - 'Set callback to be called after server sends an acknowledgment' - self._packet_id += 1 - self._callback_by_packet_id[str(self._packet_id)] = callback - return '%s+' % self._packet_id - - def get_ack_callback(self, packet_id): - 'Get callback to be called after server sends an acknowledgment' - callback = self._callback_by_packet_id[packet_id] - del self._callback_by_packet_id[packet_id] - return callback - - @property - def has_ack_callback(self): - return True if self._callback_by_packet_id else False - - -class _WebsocketTransport(_AbstractTransport): - - def __init__(self, session, secure, base_url, **kw): - super(_WebsocketTransport, self).__init__() - url = '%s://%s/websocket/%s' % ( - 'wss' if secure else 'ws', - base_url, session.id) - _log.debug('[transport selected] %s', url) - try: - self.connection = websocket.create_connection(url) - except socket.timeout as error: - raise SocketIOConnectionError(error) - except socket.error as error: - raise SocketIOConnectionError(error) - self.connection.settimeout(1) - - @property - def connected(self): - return self.connection.connected - - def recv(self): - try: - return self.connection.recv() - except socket.timeout: - raise _TimeoutError - except socket.error as error: - raise SocketIOConnectionError(error) - except websocket.WebSocketConnectionClosedException: - raise SocketIOConnectionError('server closed connection') - - def send(self, packet_text): - try: - self.connection.send(packet_text) - except socket.error: - raise SocketIOConnectionError('could not send %s' % packet_text) - - def find_callback(args, kw=None): 'Return callback whether passed as a last argument or as a keyword' if args and callable(args[-1]): @@ -461,7 +333,7 @@ def find_callback(args, kw=None): return None, args -def _yield_warning_screen(seconds=None, sleep=0): +def _yield_warning_screen(seconds=None): last_warning = None for elapsed_time in _yield_elapsed_time(seconds): try: @@ -471,7 +343,7 @@ def _yield_warning_screen(seconds=None, sleep=0): if last_warning != warning: last_warning = warning _log.warn(warning) - time.sleep(sleep) + time.sleep(RETRY_INTERVAL_IN_SECONDS) def _yield_elapsed_time(seconds=None): @@ -483,40 +355,19 @@ def _yield_elapsed_time(seconds=None): yield time.time() - start_time -def _get_session(secure, base_url, **kw): +def _get_socketIO_session(secure, base_url, **kw): server_url = '%s://%s/' % ('https' if secure else 'http', base_url) try: - response = requests.get(server_url, **kw) - except requests.exceptions.ConnectionError: - raise SocketIOConnectionError('could not start connection') - status = response.status_code - if 200 != status: - raise SocketIOConnectionError('unexpected status code (%s)' % status) + response = _get_response(requests.get, server_url, **kw) + except _TimeoutError as e: + raise SocketIOConnectionError(e) response_parts = response.text.split(':') - return _Session( + return _SocketIOSession( id=response_parts[0], heartbeat_timeout=int(response_parts[1]), server_supported_transports=response_parts[3].split(',')) -def _negotiate_transport( - client_supported_transports, session, - secure, base_url, **kw): - server_supported_transports = session.server_supported_transports - for supported_transport in client_supported_transports: - if supported_transport in server_supported_transports: - return { - 'websocket': _WebsocketTransport, - # 'xhr-polling': - # 'jsonp-polling': - }[supported_transport](session, secure, base_url, **kw) - raise SocketIOError(' '.join([ - 'could not negotiate a transport:', - 'client supports %s but' % ', '.join(client_supported_transports), - 'server supports %s' % ', '.join(server_supported_transports), - ])) - - if __name__ == '__main__': requests_log = logging.getLogger('requests') requests_log.setLevel(logging.WARNING) diff --git a/socketIO_client/exceptions.py b/socketIO_client/exceptions.py new file mode 100644 index 0000000..a614636 --- /dev/null +++ b/socketIO_client/exceptions.py @@ -0,0 +1,14 @@ +class SocketIOError(Exception): + pass + + +class SocketIOConnectionError(SocketIOError): + pass + + +class _TimeoutError(Exception): + pass + + +class _PacketError(SocketIOError): + pass diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index d16e7cd..6986694 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -1,7 +1,9 @@ import logging -from socketIO_client import SocketIO, BaseNamespace, find_callback from unittest import TestCase +from . import SocketIO, BaseNamespace, find_callback +from .transports import TIMEOUT_IN_SECONDS + HOST = 'localhost' PORT = 8000 @@ -10,22 +12,21 @@ PAYLOAD = {'xxx': 'yyy'} logging.basicConfig(level=logging.DEBUG) -class TestSocketIO(TestCase): +class BaseMixin(TestCase): def setUp(self): - self.socketIO = SocketIO(HOST, PORT) self.called_on_response = False def tearDown(self): del self.socketIO def on_response(self, *args): - self.called_on_response = True for arg in args: if isinstance(arg, dict): self.assertEqual(arg, PAYLOAD) else: self.assertEqual(arg, DATA) + self.called_on_response = True def test_disconnect(self): 'Disconnect' @@ -42,100 +43,97 @@ class TestSocketIO(TestCase): def test_message(self): 'Message' - self.socketIO.define(Namespace) + namespace = self.socketIO.define(Namespace) self.socketIO.message() - self.socketIO.wait(0.1) - namespace = self.socketIO.get_namespace() + self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(namespace.response, 'message_response') def test_message_with_data(self): 'Message with data' - self.socketIO.define(Namespace) + namespace = self.socketIO.define(Namespace) self.socketIO.message(DATA) - self.socketIO.wait(0.1) - namespace = self.socketIO.get_namespace() + self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(namespace.response, DATA) def test_message_with_payload(self): 'Message with payload' - self.socketIO.define(Namespace) + namespace = self.socketIO.define(Namespace) self.socketIO.message(PAYLOAD) - self.socketIO.wait(0.1) - namespace = self.socketIO.get_namespace() + self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(namespace.response, PAYLOAD) def test_message_with_callback(self): 'Message with callback' self.socketIO.message(callback=self.on_response) - self.socketIO.wait_for_callbacks(seconds=0.1) + self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_message_with_callback_with_data(self): 'Message with callback with data' self.socketIO.message(DATA, self.on_response) - self.socketIO.wait_for_callbacks(seconds=0.1) + self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit(self): 'Emit' - self.socketIO.define(Namespace) + namespace = self.socketIO.define(Namespace) self.socketIO.emit('emit') - self.socketIO.wait(0.1) - self.assertEqual(self.socketIO.get_namespace().args_by_event, { + self.socketIO.wait(self.wait_time_in_seconds) + self.assertEqual(namespace.args_by_event, { 'emit_response': (), }) def test_emit_with_payload(self): 'Emit with payload' - self.socketIO.define(Namespace) + namespace = self.socketIO.define(Namespace) self.socketIO.emit('emit_with_payload', PAYLOAD) - self.socketIO.wait(0.1) - self.assertEqual(self.socketIO.get_namespace().args_by_event, { + self.socketIO.wait(self.wait_time_in_seconds) + self.assertEqual(namespace.args_by_event, { 'emit_with_payload_response': (PAYLOAD,), }) def test_emit_with_multiple_payloads(self): 'Emit with multiple payloads' - self.socketIO.define(Namespace) + namespace = self.socketIO.define(Namespace) self.socketIO.emit('emit_with_multiple_payloads', PAYLOAD, PAYLOAD) - self.socketIO.wait(0.1) - self.assertEqual(self.socketIO.get_namespace().args_by_event, { + self.socketIO.wait(self.wait_time_in_seconds) + self.assertEqual(namespace.args_by_event, { 'emit_with_multiple_payloads_response': (PAYLOAD, PAYLOAD), }) def test_emit_with_callback(self): 'Emit with callback' self.socketIO.emit('emit_with_callback', self.on_response) - self.socketIO.wait_for_callbacks(seconds=0.1) + self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit_with_callback_with_payload(self): 'Emit with callback with payload' self.socketIO.emit('emit_with_callback_with_payload', self.on_response) - self.socketIO.wait_for_callbacks(seconds=0.1) + self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit_with_callback_with_multiple_payloads(self): 'Emit with callback with multiple payloads' self.socketIO.emit('emit_with_callback_with_multiple_payloads', self.on_response) - self.socketIO.wait_for_callbacks(seconds=0.1) + self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit_with_event(self): 'Emit to trigger an event' self.socketIO.on('emit_with_event_response', self.on_response) self.socketIO.emit('emit_with_event', PAYLOAD) - self.socketIO.wait(0.1) + self.socketIO.wait(self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_ack(self): 'Trigger server callback' - self.socketIO.define(Namespace) + namespace = self.socketIO.define(Namespace) self.socketIO.emit('ack', PAYLOAD) - self.socketIO.wait(0.1) - self.assertEqual(self.socketIO.get_namespace().args_by_event, { + self.socketIO.wait(self.wait_time_in_seconds) + self.assertEqual(namespace.args_by_event, { 'ack_response': (PAYLOAD,), 'ack_callback_response': (PAYLOAD,), }) @@ -146,7 +144,7 @@ class TestSocketIO(TestCase): chatNamespace = self.socketIO.define(Namespace, '/chat') newsNamespace = self.socketIO.define(Namespace, '/news') newsNamespace.emit('emit_with_payload', PAYLOAD) - self.socketIO.wait(0.1) + self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(mainNamespace.args_by_event, {}) self.assertEqual(chatNamespace.args_by_event, {}) self.assertEqual(newsNamespace.args_by_event, { @@ -154,6 +152,30 @@ class TestSocketIO(TestCase): }) +class Test_WebsocketTransport(BaseMixin): + + def setUp(self): + super(Test_WebsocketTransport, self).setUp() + self.socketIO = SocketIO(HOST, PORT, transports=['websocket']) + self.wait_time_in_seconds = 0.1 + + +class Test_XHR_PollingTransport(BaseMixin): + + def setUp(self): + super(Test_XHR_PollingTransport, self).setUp() + self.socketIO = SocketIO(HOST, PORT, transports=['xhr-polling']) + self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1 + + +class Test_JSONP_PollingTransport(BaseMixin): + + def setUp(self): + super(Test_JSONP_PollingTransport, self).setUp() + self.socketIO = SocketIO(HOST, PORT, transports=['jsonp-polling']) + self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1 + + class Namespace(BaseNamespace): def initialize(self): diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py new file mode 100644 index 0000000..c93ced6 --- /dev/null +++ b/socketIO_client/transports.py @@ -0,0 +1,296 @@ +import json +import logging +import re +import requests +import socket +import time +import websocket +from itertools import izip + +from .exceptions import SocketIOError, SocketIOConnectionError, _TimeoutError + + +TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling' +BOUNDARY = u'\ufffd'.encode('utf-8') +TIMEOUT_IN_SECONDS = 2 +_log = logging.getLogger(__name__) + + +class _AbstractTransport(object): + + def __init__(self): + self._packet_id = 0 + self._callback_by_packet_id = {} + + def disconnect(self, path=''): + if not self.connected: + return + if path: + self.send_packet(0, path) + else: + self.close() + + def connect(self, path): + self.send_packet(1, path) + + def send_heartbeat(self): + self.send_packet(2) + + def message(self, path, data, callback): + if isinstance(data, basestring): + code = 3 + else: + code = 4 + data = json.dumps(data, ensure_ascii=False) + self.send_packet(code, path, data, callback) + + def emit(self, path, event, args, callback): + data = json.dumps(dict(name=event, args=args), ensure_ascii=False) + self.send_packet(5, path, data, callback) + + def ack(self, packet_id, *args): + packet_id = packet_id.rstrip('+') + data = '%s+%s' % ( + packet_id, + json.dumps(args, ensure_ascii=False), + ) if args else packet_id + self.send_packet(6, data=data) + + def noop(self): + self.send_packet(8) + + def send_packet(self, code, path='', data='', callback=None): + packet_id = self.set_ack_callback(callback) if callback else '' + packet_parts = str(code), packet_id, path, data + packet_text = ':'.join(packet_parts) + self.send(packet_text) + _log.debug('[packet sent] %s', packet_text) + + def recv_packet(self): + code, packet_id, path, data = None, None, None, None + for packet_text in self.recv(): + _log.debug('[packet received] %s', packet_text) + try: + packet_parts = packet_text.split(':', 3) + except AttributeError: + _log.warn('[packet error] %s', packet_text) + continue + packet_count = len(packet_parts) + if 4 == packet_count: + code, packet_id, path, data = packet_parts + elif 3 == packet_count: + code, packet_id, path = packet_parts + elif 1 == packet_count: + code = packet_parts[0] + yield code, packet_id, path, data + + def set_ack_callback(self, callback): + 'Set callback to be called after server sends an acknowledgment' + self._packet_id += 1 + self._callback_by_packet_id[str(self._packet_id)] = callback + return '%s+' % self._packet_id + + def get_ack_callback(self, packet_id): + 'Get callback to be called after server sends an acknowledgment' + callback = self._callback_by_packet_id[packet_id] + del self._callback_by_packet_id[packet_id] + return callback + + @property + def has_ack_callback(self): + return True if self._callback_by_packet_id else False + + +class _WebsocketTransport(_AbstractTransport): + + def __init__(self, socketIO_session, secure, base_url, **kw): + super(_WebsocketTransport, self).__init__() + url = '%s://%s/websocket/%s' % ( + 'wss' if secure else 'ws', + base_url, socketIO_session.id) + try: + self._connection = websocket.create_connection(url) + except socket.timeout as e: + raise SocketIOConnectionError(e) + except socket.error as e: + raise SocketIOConnectionError(e) + self._connection.settimeout(TIMEOUT_IN_SECONDS) + + @property + def connected(self): + return self._connection.connected + + def send(self, packet_text): + try: + self._connection.send(packet_text) + except socket.error: + raise SocketIOConnectionError('could not send %s' % packet_text) + + def recv(self): + try: + yield self._connection.recv() + except socket.timeout: + raise _TimeoutError + except socket.error as e: + raise SocketIOConnectionError(e) + except websocket.WebSocketConnectionClosedException as e: + raise SocketIOConnectionError('connection closed (%s)' % e) + + def close(self): + self._connection.close() + + +class _XHR_PollingTransport(_AbstractTransport): + + def __init__(self, socketIO_session, secure, base_url, **kw): + super(_XHR_PollingTransport, self).__init__() + self._url = '%s://%s/xhr-polling/%s' % ( + 'https' if secure else 'http', + base_url, socketIO_session.id) + self._connected = True + self._http_session = _prepare_http_session(kw) + + @property + def connected(self): + return self._connected + + @property + def _params(self): + return dict(t=time.time()) + + def send(self, packet_text): + _get_response( + self._http_session.post, + self._url, + params=self._params, + data=packet_text, + timeout=TIMEOUT_IN_SECONDS) + + def recv(self): + response = _get_response( + self._http_session.get, + self._url, + params=self._params, + timeout=TIMEOUT_IN_SECONDS) + encoded_text = response.text.encode('utf-8') + if not encoded_text.startswith(BOUNDARY): + yield encoded_text.decode('utf-8') + for packet_text in _yield_text_from_framed_data(encoded_text): + yield packet_text + + def close(self): + _get_response( + self._http_session.get, + self._url, + params=dict(self._params.items() + [('disconnect', True)])) + self._connected = False + + +class _JSONP_PollingTransport(_AbstractTransport): + + DATA_PATTERN = re.compile(r'io.j\[(\d+)\]\("(.*)"\);') + + def __init__(self, socketIO_session, secure, base_url, **kw): + super(_JSONP_PollingTransport, self).__init__() + self._url = '%s://%s/jsonp-polling/%s' % ( + 'https' if secure else 'http', + base_url, socketIO_session.id) + self._connected = True + self._http_session = _prepare_http_session(kw) + self._jsonp_id = 0 + + @property + def connected(self): + return self._connected + + @property + def _params(self): + return dict(t=time.time(), jsonp=self._jsonp_id) + + def send(self, packet_text): + _get_response( + self._http_session.post, + self._url, + params=self._params, + data='d=%s' % requests.utils.quote(packet_text), + headers={'content-type': 'application/x-www-form-urlencoded'}, + timeout=TIMEOUT_IN_SECONDS) + + def recv(self): + response = _get_response( + self._http_session.get, + self._url, + params=self._params, + headers={'content-type': 'application/javascript'}, + timeout=TIMEOUT_IN_SECONDS) + encoded_text = response.text.encode('utf-8') + if not encoded_text.startswith(BOUNDARY): + self._jsonp_id, encoded_data = self.DATA_PATTERN.match( + encoded_text).groups() + yield encoded_data.decode('utf-8') + for packet_text in _yield_text_from_framed_data(encoded_text): + yield packet_text + + def close(self): + _get_response( + self._http_session.get, + self._url, + params=dict(self._params.items() + [('disconnect', True)])) + self._connected = False + + +def _negotiate_transport( + client_supported_transports, session, + secure, base_url, **kw): + server_supported_transports = session.server_supported_transports + for supported_transport in client_supported_transports: + if supported_transport in server_supported_transports: + _log.debug('[transport selected] %s', supported_transport) + return { + 'websocket': _WebsocketTransport, + 'xhr-polling': _XHR_PollingTransport, + 'jsonp-polling': _JSONP_PollingTransport, + }[supported_transport](session, secure, base_url, **kw) + raise SocketIOError(' '.join([ + 'could not negotiate a transport:', + 'client supports %s but' % ', '.join(client_supported_transports), + 'server supports %s' % ', '.join(server_supported_transports), + ])) + + +def _yield_text_from_framed_data(framed_data): + parts = [x.decode('utf-8') for x in framed_data.split(BOUNDARY)] + for text_length, text in izip(parts[1::2], parts[2::2]): + if text_length == str(len(text)): + yield text + warning = 'invalid declared length=%s for packet_text=%s' % ( + text_length, text) + _log.warn('[packet error] %s', warning) + + +def _get_response(request, *args, **kw): + try: + response = request(*args, **kw) + except requests.exceptions.Timeout as e: + raise _TimeoutError(e) + except requests.exceptions.ConnectionError as e: + raise SocketIOConnectionError(e) + except requests.exceptions.SSLError as e: + raise SocketIOConnectionError('could not negotiate SSL (%s)' % e) + status = response.status_code + if 200 != status: + raise SocketIOConnectionError('unexpected status code (%s)' % status) + return response + + +def _prepare_http_session(kw): + http_session = requests.Session() + http_session.headers.update(kw.get('headers', {})) + http_session.auth = kw.get('auth') + http_session.proxies.update(kw.get('proxies', {})) + http_session.hooks.update(kw.get('hooks', {})) + http_session.params.update(kw.get('params', {})) + http_session.verify = kw.get('verify') + http_session.cert = kw.get('cert') + http_session.cookies.update(kw.get('cookies', {})) + return http_session From 28cfadc5938757abedb079cc33d1bf92639044a8 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 3 Nov 2013 15:03:34 -0800 Subject: [PATCH 036/191] Fixed _XHR_PollingTransport --- serve_tests.js | 1 - socketIO_client/__init__.py | 47 +++++++++++++++-------------------- socketIO_client/tests.py | 14 +++++------ socketIO_client/transports.py | 27 ++++++++++++-------- 4 files changed, 44 insertions(+), 45 deletions(-) diff --git a/serve_tests.js b/serve_tests.js index f69247a..82c8024 100644 --- a/serve_tests.js +++ b/serve_tests.js @@ -15,7 +15,6 @@ var main = io.of('').on('connection', function(socket) { } }); socket.on('emit', function() { - console.log('hey'); socket.emit('emit_response'); }); socket.on('emit_with_payload', function(payload) { diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 2759f5a..a114906 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -23,7 +23,7 @@ class BaseNamespace(object): def __init__(self, _transport, path): self._transport = _transport - self._path = path + self.path = path self._callback_by_event = {} self.initialize() @@ -32,11 +32,11 @@ class BaseNamespace(object): pass def message(self, data='', callback=None): - self._transport.message(self._path, data, callback) + self._transport.message(self.path, data, callback) def emit(self, event, *args, **kw): callback, args = find_callback(args, kw) - self._transport.emit(self._path, event, args, callback) + self._transport.emit(self.path, event, args, callback) def on(self, event, callback): 'Define a callback to handle a custom event emitted by the server' @@ -44,19 +44,19 @@ class BaseNamespace(object): def on_connect(self): 'Called after server connects; you can override this method' - _log.debug('[connect]') + _log.debug('%s [connect]', self.path) def on_disconnect(self): 'Called after server disconnects; you can override this method' - _log.debug('[disconnect]') + _log.debug('%s [disconnect]', self.path) def on_heartbeat(self): 'Called after server sends a heartbeat; you can override this method' - _log.debug('[heartbeat]') + _log.debug('%s [heartbeat]', self.path) def on_message(self, data): 'Called after server sends a message; you can override this method' - _log.info('[message] %s', data) + _log.info('%s [message] %s', self.path, data) def on_event(self, event, *args): """ @@ -69,27 +69,27 @@ class BaseNamespace(object): if callback: arguments.append('callback(*args)') callback(*args) - _log.info('[event] %s(%s)', event, ', '.join(arguments)) + _log.info('%s [event] %s(%s)', self.path, event, ', '.join(arguments)) def on_error(self, reason, advice): 'Called after server sends an error; you can override this method' - _log.info('[error] %s', advice) + _log.info('%s [error] %s', self.path, advice) def on_noop(self): 'Called after server sends a noop; you can override this method' - _log.info('[noop]') + _log.info('%s [noop]', self.path) def on_open(self, *args): - _log.info('[open] %s', args) + _log.info('%s [open] %s', self.path, args) def on_close(self, *args): - _log.info('[close] %s', args) + _log.info('%s [close] %s', self.path, args) def on_retry(self, *args): - _log.info('[retry] %s', args) + _log.info('%s [retry] %s', self.path, args) def on_reconnect(self, *args): - _log.info('[reconnect] %s', args) + _log.info('%s [reconnect] %s', self.path, args) def _find_event_callback(self, event): # Check callbacks defined by on() @@ -142,6 +142,7 @@ class SocketIO(object): if path: self._transport.connect(path) namespace = Namespace(self._transport, path) + namespace.on_connect() self._namespace_by_path[path] = namespace return namespace @@ -164,12 +165,13 @@ class SocketIO(object): if for_callbacks and not self._transport.has_ack_callback: break try: - packet = self._transport.recv_packet().next() - self._process_packet(packet) + for packet in self._transport.recv_packet(): + try: + self._process_packet(packet) + except _PacketError as e: + _log.warn('[packet error] %s', e) except _TimeoutError: pass - except _PacketError as e: - _log.warn('[packet error] %s', e) self.heartbeat_pacemaker.send(elapsed_time) except SocketIOConnectionError as e: try: @@ -366,12 +368,3 @@ def _get_socketIO_session(secure, base_url, **kw): id=response_parts[0], heartbeat_timeout=int(response_parts[1]), server_supported_transports=response_parts[3].split(',')) - - -if __name__ == '__main__': - requests_log = logging.getLogger('requests') - requests_log.setLevel(logging.WARNING) - logging.basicConfig(level=logging.DEBUG) - socketIO = SocketIO('localhost', 8000) - socketIO.emit('aaa') - socketIO.wait() diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index 6986694..a57605a 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -140,14 +140,14 @@ class BaseMixin(TestCase): def test_namespaces(self): 'Behave differently in different namespaces' - mainNamespace = self.socketIO.define(Namespace) - chatNamespace = self.socketIO.define(Namespace, '/chat') - newsNamespace = self.socketIO.define(Namespace, '/news') - newsNamespace.emit('emit_with_payload', PAYLOAD) + main_namespace = self.socketIO.define(Namespace) + chat_namespace = self.socketIO.define(Namespace, '/chat') + news_namespace = self.socketIO.define(Namespace, '/news') + news_namespace.emit('emit_with_payload', PAYLOAD) self.socketIO.wait(self.wait_time_in_seconds) - self.assertEqual(mainNamespace.args_by_event, {}) - self.assertEqual(chatNamespace.args_by_event, {}) - self.assertEqual(newsNamespace.args_by_event, { + self.assertEqual(main_namespace.args_by_event, {}) + self.assertEqual(chat_namespace.args_by_event, {}) + self.assertEqual(news_namespace.args_by_event, { 'emit_with_payload_response': (PAYLOAD,), }) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index c93ced6..6000960 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -149,6 +149,9 @@ class _XHR_PollingTransport(_AbstractTransport): base_url, socketIO_session.id) self._connected = True self._http_session = _prepare_http_session(kw) + # Create connection + for packet_text in self.recv_packet(): + pass @property def connected(self): @@ -156,7 +159,7 @@ class _XHR_PollingTransport(_AbstractTransport): @property def _params(self): - return dict(t=time.time()) + return dict(t=int(time.time())) def send(self, packet_text): _get_response( @@ -197,7 +200,10 @@ class _JSONP_PollingTransport(_AbstractTransport): base_url, socketIO_session.id) self._connected = True self._http_session = _prepare_http_session(kw) - self._jsonp_id = 0 + self._id = 0 + # Create connection + for packet_text in self.recv_packet(): + pass @property def connected(self): @@ -205,7 +211,7 @@ class _JSONP_PollingTransport(_AbstractTransport): @property def _params(self): - return dict(t=time.time(), jsonp=self._jsonp_id) + return dict(t=int(time.time()), i=self._id) def send(self, packet_text): _get_response( @@ -221,11 +227,11 @@ class _JSONP_PollingTransport(_AbstractTransport): self._http_session.get, self._url, params=self._params, - headers={'content-type': 'application/javascript'}, + headers={'content-type': 'text/javascript; charset=UTF-8'}, timeout=TIMEOUT_IN_SECONDS) encoded_text = response.text.encode('utf-8') if not encoded_text.startswith(BOUNDARY): - self._jsonp_id, encoded_data = self.DATA_PATTERN.match( + self._id, encoded_data = self.DATA_PATTERN.match( encoded_text).groups() yield encoded_data.decode('utf-8') for packet_text in _yield_text_from_framed_data(encoded_text): @@ -261,11 +267,12 @@ def _negotiate_transport( def _yield_text_from_framed_data(framed_data): parts = [x.decode('utf-8') for x in framed_data.split(BOUNDARY)] for text_length, text in izip(parts[1::2], parts[2::2]): - if text_length == str(len(text)): - yield text - warning = 'invalid declared length=%s for packet_text=%s' % ( - text_length, text) - _log.warn('[packet error] %s', warning) + if text_length != str(len(text)): + warning = 'invalid declared length=%s for packet_text=%s' % ( + text_length, text) + _log.warn('[packet error] %s', warning) + continue + yield text def _get_response(request, *args, **kw): From 0751dc3484cc7a431d5c9d7a906c065a60c27d8b Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 3 Nov 2013 17:16:59 -0800 Subject: [PATCH 037/191] Fixed _JSONP_PollingTransport --- TODO.goals | 9 +-------- socketIO_client/tests.py | 8 ++++---- socketIO_client/transports.py | 38 +++++++++++++++++++++-------------- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/TODO.goals b/TODO.goals index 87f2b73..77bf64e 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,8 +1 @@ -= Test with django-socketio -+ Investigate coroutine replacement for threads -+ Make client robust if server disconnects -+ Signal on_disconnect -+ Add on_noop -+ Add query_string -+ Add more transports -+ Replace print with logging +# America/Los_Angeles 11/3/2013 diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index a57605a..007dc40 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -12,7 +12,7 @@ PAYLOAD = {'xxx': 'yyy'} logging.basicConfig(level=logging.DEBUG) -class BaseMixin(TestCase): +class BaseMixin(object): def setUp(self): self.called_on_response = False @@ -152,7 +152,7 @@ class BaseMixin(TestCase): }) -class Test_WebsocketTransport(BaseMixin): +class Test_WebsocketTransport(TestCase, BaseMixin): def setUp(self): super(Test_WebsocketTransport, self).setUp() @@ -160,7 +160,7 @@ class Test_WebsocketTransport(BaseMixin): self.wait_time_in_seconds = 0.1 -class Test_XHR_PollingTransport(BaseMixin): +class Test_XHR_PollingTransport(TestCase, BaseMixin): def setUp(self): super(Test_XHR_PollingTransport, self).setUp() @@ -168,7 +168,7 @@ class Test_XHR_PollingTransport(BaseMixin): self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1 -class Test_JSONP_PollingTransport(BaseMixin): +class Test_JSONP_PollingTransport(TestCase, BaseMixin): def setUp(self): super(Test_JSONP_PollingTransport, self).setUp() diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 6000960..609694a 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -11,7 +11,7 @@ from .exceptions import SocketIOError, SocketIOConnectionError, _TimeoutError TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling' -BOUNDARY = u'\ufffd'.encode('utf-8') +BOUNDARY = u'\ufffd' TIMEOUT_IN_SECONDS = 2 _log = logging.getLogger(__name__) @@ -175,10 +175,11 @@ class _XHR_PollingTransport(_AbstractTransport): self._url, params=self._params, timeout=TIMEOUT_IN_SECONDS) - encoded_text = response.text.encode('utf-8') - if not encoded_text.startswith(BOUNDARY): - yield encoded_text.decode('utf-8') - for packet_text in _yield_text_from_framed_data(encoded_text): + response_text = response.text + if not response_text.startswith(BOUNDARY): + yield response_text + return + for packet_text in _yield_text_from_framed_data(response_text): yield packet_text def close(self): @@ -191,7 +192,7 @@ class _XHR_PollingTransport(_AbstractTransport): class _JSONP_PollingTransport(_AbstractTransport): - DATA_PATTERN = re.compile(r'io.j\[(\d+)\]\("(.*)"\);') + RESPONSE_PATTERN = re.compile(r'io.j\[(\d+)\]\("(.*)"\);') def __init__(self, socketIO_session, secure, base_url, **kw): super(_JSONP_PollingTransport, self).__init__() @@ -218,23 +219,30 @@ class _JSONP_PollingTransport(_AbstractTransport): self._http_session.post, self._url, params=self._params, - data='d=%s' % requests.utils.quote(packet_text), + data='d=%s' % requests.utils.quote(json.dumps(packet_text)), headers={'content-type': 'application/x-www-form-urlencoded'}, timeout=TIMEOUT_IN_SECONDS) def recv(self): + 'Decode the JavaScript response so that we can parse it as JSON' response = _get_response( self._http_session.get, self._url, params=self._params, headers={'content-type': 'text/javascript; charset=UTF-8'}, timeout=TIMEOUT_IN_SECONDS) - encoded_text = response.text.encode('utf-8') - if not encoded_text.startswith(BOUNDARY): - self._id, encoded_data = self.DATA_PATTERN.match( - encoded_text).groups() - yield encoded_data.decode('utf-8') - for packet_text in _yield_text_from_framed_data(encoded_text): + response_text = response.text + try: + self._id, response_text = self.RESPONSE_PATTERN.match( + response_text).groups() + except AttributeError: + _log.warn('[packet error] %s', response_text) + return + if not response_text.startswith(BOUNDARY): + yield response_text.decode('unicode_escape') + return + for packet_text in _yield_text_from_framed_data( + response_text, parse=lambda x: x.decode('unicode_escape')): yield packet_text def close(self): @@ -264,8 +272,8 @@ def _negotiate_transport( ])) -def _yield_text_from_framed_data(framed_data): - parts = [x.decode('utf-8') for x in framed_data.split(BOUNDARY)] +def _yield_text_from_framed_data(framed_data, parse=lambda x: x): + parts = [parse(x) for x in framed_data.split(BOUNDARY)] for text_length, text in izip(parts[1::2], parts[2::2]): if text_length != str(len(text)): warning = 'invalid declared length=%s for packet_text=%s' % ( From be05a0699f43b127432a52ad3a46bf149ad506a3 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 3 Nov 2013 17:23:09 -0800 Subject: [PATCH 038/191] Updated README --- README.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index a96918d..42b504e 100644 --- a/README.rst +++ b/README.rst @@ -113,13 +113,15 @@ Open secure websockets (HTTPS / WSS) behind a proxy. :: secure=True, proxies={'https': 'https://proxy.example.com:8080'}) -Specify custom headers thanks to the `requests`_ library. :: +Specify params, headers and cookies thanks to the `requests`_ library. :: from socketIO_client import SocketIO from base64 import b64encode SocketIO('localhost', 8000, - headers={'Authorization': 'Basic ' + b64encode('username:password')}) + params={'q': 'qqq'}, + headers={'Authorization': 'Basic ' + b64encode('username:password')}, + cookies={'a': 'aaa'}) License From 7c18bc5dfc36969c6a47fe0f57f4c4f2a6827e59 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 3 Nov 2013 17:35:32 -0800 Subject: [PATCH 039/191] Added credits --- README.rst | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 42b504e..235e077 100644 --- a/README.rst +++ b/README.rst @@ -135,8 +135,10 @@ Credits - `Hiroki Ohtani`_ wrote websocket-client_. - rod_ wrote a `prototype for a Python client to a socket.io server`_ on StackOverflow. - `Alexandre Bourget`_ wrote gevent-socketio_, which is a socket.io server written in Python. -- `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_ submitted code to expand support of the socket.io protocol. - +- `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_, `Rui Chicoria`_ submitted code to expand support of the socket.io protocol. +- `Guy Zmo`_ and `Francis Bull`_ wrote prototypes to support xhr-polling and jsonp-polling. +- `Eric Chen`_, `Denis Zinevich`_, `Thiago Hersan`_ suggested ways to make the connection more robust. + .. _socket.io: http://socket.io .. _requests: http://python-requests.org @@ -153,8 +155,15 @@ Credits .. _Alexandre Bourget: https://github.com/abourget .. _gevent-socketio: https://github.com/abourget/gevent-socketio +.. _Guy Zmo: https://github.com/guyzmo +.. _Francis Bull: https://github.com/franbull .. _Paul Kienzle: https://github.com/pkienzle .. _Zac Lee: https://github.com/zratic .. _Josh VanderLinden: https://github.com/codekoala .. _Ian Fitzpatrick: https://github.com/GraphEffect .. _Lucas Klein: https://github.com/lukashed +.. _Rui Chicoria: https://github.com/rchicoria + +.. _Eric Chen: https://github.com/taiyangc +.. _Denis Zinevich: https://github.com/dzinevich +.. _Thiago Hersan: https://github.com/thiagohersan From 2c381142d4f1ca2c9441700610b7b69d20e453fe Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 3 Nov 2013 17:50:03 -0800 Subject: [PATCH 040/191] Updated CHANGES --- CHANGES.rst | 6 +++++- README.rst | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index acd8d36..84ccc57 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ 0.5 --- -- +- Rewrote library to use coroutines instead of threads to save memory +- Improved connection resilience +- Added support for xhr-polling thanks to Francis Bull +- Added support for jsonp-polling thanks to Bernard Pratz +- Added support for query params and cookies 0.4 --- diff --git a/README.rst b/README.rst index 235e077..0a9bb4e 100644 --- a/README.rst +++ b/README.rst @@ -136,7 +136,7 @@ Credits - rod_ wrote a `prototype for a Python client to a socket.io server`_ on StackOverflow. - `Alexandre Bourget`_ wrote gevent-socketio_, which is a socket.io server written in Python. - `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_, `Rui Chicoria`_ submitted code to expand support of the socket.io protocol. -- `Guy Zmo`_ and `Francis Bull`_ wrote prototypes to support xhr-polling and jsonp-polling. +- `Bernard Pratz`_ and `Francis Bull`_ wrote prototypes to support xhr-polling and jsonp-polling. - `Eric Chen`_, `Denis Zinevich`_, `Thiago Hersan`_ suggested ways to make the connection more robust. @@ -155,7 +155,7 @@ Credits .. _Alexandre Bourget: https://github.com/abourget .. _gevent-socketio: https://github.com/abourget/gevent-socketio -.. _Guy Zmo: https://github.com/guyzmo +.. _Bernard Pratz: https://github.com/guyzmo .. _Francis Bull: https://github.com/franbull .. _Paul Kienzle: https://github.com/pkienzle .. _Zac Lee: https://github.com/zratic From 1a3e1270dd8f01856bb8bc1bb25c5315a90c24aa Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 3 Nov 2013 18:28:57 -0800 Subject: [PATCH 041/191] Raw unicode literals are not supported in Python3 --- setup.py | 1 + socketIO_client/transports.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b8d6c4f..2fb7721 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ setup( url='https://github.com/invisibleroads/socketIO-client', install_requires=[ 'requests', + 'six', 'websocket-client', ], packages=find_packages(), diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 609694a..79aac94 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -2,6 +2,7 @@ import json import logging import re import requests +import six import socket import time import websocket @@ -11,7 +12,7 @@ from .exceptions import SocketIOError, SocketIOConnectionError, _TimeoutError TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling' -BOUNDARY = u'\ufffd' +BOUNDARY = six.u('\ufffd') TIMEOUT_IN_SECONDS = 2 _log = logging.getLogger(__name__) From 13b482e075c116a56864efca3a38f226aa3f126f Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 6 Nov 2013 09:57:28 -0800 Subject: [PATCH 042/191] Fixes #24 --- setup.py | 2 +- socketIO_client/__init__.py | 18 ++++++++--------- socketIO_client/exceptions.py | 6 +++--- socketIO_client/transports.py | 38 +++++++++++++++++++++-------------- 4 files changed, 36 insertions(+), 28 deletions(-) diff --git a/setup.py b/setup.py index 2fb7721..9afabd2 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( name='socketIO-client', - version='0.5', + version='0.5.1', description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index a114906..f166cc3 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -4,7 +4,7 @@ import requests import time from collections import namedtuple -from .exceptions import SocketIOConnectionError, _TimeoutError, _PacketError +from .exceptions import ConnectionError, TimeoutError, PacketError from .transports import _get_response, _negotiate_transport, TRANSPORTS @@ -168,12 +168,12 @@ class SocketIO(object): for packet in self._transport.recv_packet(): try: self._process_packet(packet) - except _PacketError as e: + except PacketError as e: _log.warn('[packet error] %s', e) - except _TimeoutError: + except TimeoutError: pass self.heartbeat_pacemaker.send(elapsed_time) - except SocketIOConnectionError as e: + except ConnectionError as e: try: warning = Exception('[connection error] %s' % e) warning_screen.throw(warning) @@ -210,7 +210,7 @@ class SocketIO(object): try: self.__transport = self._get_transport() break - except SocketIOConnectionError as e: + except ConnectionError as e: if not self.wait_for_connection: raise try: @@ -256,7 +256,7 @@ class SocketIO(object): try: return self._namespace_by_path[path] except KeyError: - raise _PacketError('unexpected namespace path (%s)' % path) + raise PacketError('unexpected namespace path (%s)' % path) def _get_delegate(self, code): try: @@ -272,7 +272,7 @@ class SocketIO(object): '8': self._on_noop, }[code] except KeyError: - raise _PacketError('unexpected code (%s)' % code) + raise PacketError('unexpected code (%s)' % code) def _on_disconnect(self, packet_id, data, find_event_callback): find_event_callback('disconnect')() @@ -361,8 +361,8 @@ def _get_socketIO_session(secure, base_url, **kw): server_url = '%s://%s/' % ('https' if secure else 'http', base_url) try: response = _get_response(requests.get, server_url, **kw) - except _TimeoutError as e: - raise SocketIOConnectionError(e) + except TimeoutError as e: + raise ConnectionError(e) response_parts = response.text.split(':') return _SocketIOSession( id=response_parts[0], diff --git a/socketIO_client/exceptions.py b/socketIO_client/exceptions.py index a614636..ed2b4d2 100644 --- a/socketIO_client/exceptions.py +++ b/socketIO_client/exceptions.py @@ -2,13 +2,13 @@ class SocketIOError(Exception): pass -class SocketIOConnectionError(SocketIOError): +class ConnectionError(SocketIOError): pass -class _TimeoutError(Exception): +class TimeoutError(SocketIOError): pass -class _PacketError(SocketIOError): +class PacketError(SocketIOError): pass diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 79aac94..833289a 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -8,12 +8,12 @@ import time import websocket from itertools import izip -from .exceptions import SocketIOError, SocketIOConnectionError, _TimeoutError +from .exceptions import SocketIOError, ConnectionError, TimeoutError TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling' BOUNDARY = six.u('\ufffd') -TIMEOUT_IN_SECONDS = 2 +TIMEOUT_IN_SECONDS = 3 _log = logging.getLogger(__name__) @@ -112,9 +112,9 @@ class _WebsocketTransport(_AbstractTransport): try: self._connection = websocket.create_connection(url) except socket.timeout as e: - raise SocketIOConnectionError(e) + raise ConnectionError(e) except socket.error as e: - raise SocketIOConnectionError(e) + raise ConnectionError(e) self._connection.settimeout(TIMEOUT_IN_SECONDS) @property @@ -124,18 +124,26 @@ class _WebsocketTransport(_AbstractTransport): def send(self, packet_text): try: self._connection.send(packet_text) - except socket.error: - raise SocketIOConnectionError('could not send %s' % packet_text) + except websocket.WebSocketTimeoutException as e: + message = 'timed out while sending %s (%s)' % (packet_text, e) + _log.warn(message) + raise TimeoutError(e) + except socket.error as e: + message = 'disconnected while sending %s (%s)' % (packet_text, e) + _log.warn(message) + raise ConnectionError(message) def recv(self): try: yield self._connection.recv() - except socket.timeout: - raise _TimeoutError - except socket.error as e: - raise SocketIOConnectionError(e) + except websocket.WebSocketTimeoutException as e: + raise TimeoutError(e) + except websocket.SSLError as e: + raise ConnectionError(e) except websocket.WebSocketConnectionClosedException as e: - raise SocketIOConnectionError('connection closed (%s)' % e) + raise ConnectionError('connection closed (%s)' % e) + except socket.error as e: + raise ConnectionError(e) def close(self): self._connection.close() @@ -288,14 +296,14 @@ def _get_response(request, *args, **kw): try: response = request(*args, **kw) except requests.exceptions.Timeout as e: - raise _TimeoutError(e) + raise TimeoutError(e) except requests.exceptions.ConnectionError as e: - raise SocketIOConnectionError(e) + raise ConnectionError(e) except requests.exceptions.SSLError as e: - raise SocketIOConnectionError('could not negotiate SSL (%s)' % e) + raise ConnectionError('could not negotiate SSL (%s)' % e) status = response.status_code if 200 != status: - raise SocketIOConnectionError('unexpected status code (%s)' % status) + raise ConnectionError('unexpected status code (%s)' % status) return response From 65c4160c8e34d4a30177c4309080fa43348ad378 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 6 Nov 2013 10:26:49 -0800 Subject: [PATCH 043/191] Fixes #25 --- TODO.goals | 1 + socketIO_client/__init__.py | 35 ++++++++++++++++++++--------------- socketIO_client/transports.py | 8 ++++---- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/TODO.goals b/TODO.goals index 77bf64e..6721fe8 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1 +1,2 @@ # America/Los_Angeles 11/3/2013 +Add test for server ack callback in namespace diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index f166cc3..1cfd900 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -250,7 +250,7 @@ class SocketIO(object): code, packet_id, path, data = packet namespace = self.get_namespace(path) delegate = self._get_delegate(code) - delegate(packet_id, data, namespace._find_event_callback) + delegate(packet, namespace._find_event_callback) def get_namespace(self, path=''): try: @@ -274,36 +274,40 @@ class SocketIO(object): except KeyError: raise PacketError('unexpected code (%s)' % code) - def _on_disconnect(self, packet_id, data, find_event_callback): + def _on_disconnect(self, packet, find_event_callback): find_event_callback('disconnect')() - def _on_connect(self, packet_id, data, find_event_callback): + def _on_connect(self, packet, find_event_callback): find_event_callback('connect')() - def _on_heartbeat(self, packet_id, data, find_event_callback): + def _on_heartbeat(self, packet, find_event_callback): find_event_callback('heartbeat')() - def _on_message(self, packet_id, data, find_event_callback): + def _on_message(self, packet, find_event_callback): + code, packet_id, path, data = packet args = [data] if packet_id: - args.append(self._prepare_to_send_ack(packet_id)) + args.append(self._prepare_to_send_ack(path, packet_id)) find_event_callback('message')(*args) - def _on_json(self, packet_id, data, find_event_callback): + def _on_json(self, packet, find_event_callback): + code, packet_id, path, data = packet args = [json.loads(data)] if packet_id: - args.append(self._prepare_to_send_ack(packet_id)) + args.append(self._prepare_to_send_ack(path, packet_id)) find_event_callback('message')(*args) - def _on_event(self, packet_id, data, find_event_callback): + def _on_event(self, packet, find_event_callback): + code, packet_id, path, data = packet value_by_name = json.loads(data) event = value_by_name['name'] args = value_by_name.get('args', []) if packet_id: - args.append(self._prepare_to_send_ack(packet_id)) + args.append(self._prepare_to_send_ack(path, packet_id)) find_event_callback(event)(*args) - def _on_ack(self, packet_id, data, find_event_callback): + def _on_ack(self, packet, find_event_callback): + code, packet_id, path, data = packet data_parts = data.split('+', 1) packet_id = data_parts[0] try: @@ -313,16 +317,17 @@ class SocketIO(object): args = json.loads(data_parts[1]) if len(data_parts) > 1 else [] ack_callback(*args) - def _on_error(self, packet_id, data, find_event_callback): + def _on_error(self, packet, find_event_callback): + code, packet_id, path, data = packet reason, advice = data.split('+', 1) find_event_callback('error')(reason, advice) - def _on_noop(self, packet_id, data, find_event_callback): + def _on_noop(self, packet, find_event_callback): find_event_callback('noop')() - def _prepare_to_send_ack(self, packet_id): + def _prepare_to_send_ack(self, path, packet_id): 'Return function that acknowledges the server' - return lambda *args: self._transport.ack(packet_id, *args) + return lambda *args: self._transport.ack(path, packet_id, *args) def find_callback(args, kw=None): diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 833289a..a3469e5 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -49,16 +49,16 @@ class _AbstractTransport(object): data = json.dumps(dict(name=event, args=args), ensure_ascii=False) self.send_packet(5, path, data, callback) - def ack(self, packet_id, *args): + def ack(self, path, packet_id, *args): packet_id = packet_id.rstrip('+') data = '%s+%s' % ( packet_id, json.dumps(args, ensure_ascii=False), ) if args else packet_id - self.send_packet(6, data=data) + self.send_packet(6, path, data) - def noop(self): - self.send_packet(8) + def noop(self, path=''): + self.send_packet(8, path) def send_packet(self, code, path='', data='', callback=None): packet_id = self.set_ack_callback(callback) if callback else '' From e75fbeb2e3747f0c259448faf065312bdc86a076 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 6 Nov 2013 18:29:26 -0800 Subject: [PATCH 044/191] Added test to cover #25 --- serve_tests.js | 5 +++++ socketIO_client/tests.py | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/serve_tests.js b/serve_tests.js index 82c8024..9107d4a 100644 --- a/serve_tests.js +++ b/serve_tests.js @@ -57,6 +57,11 @@ var chat = io.of('/chat').on('connection', function (socket) { socket.on('aaa', function() { socket.emit('aaa_response', 'in chat'); }); + socket.on('ack', function(payload) { + socket.emit('ack_response', payload, function(payload) { + socket.emit('ack_callback_response', payload); + }); + }); }); var news = io.of('/news').on('connection', function (socket) { diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index 007dc40..db96990 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -138,7 +138,7 @@ class BaseMixin(object): 'ack_callback_response': (PAYLOAD,), }) - def test_namespaces(self): + def test_namespace_emit(self): 'Behave differently in different namespaces' main_namespace = self.socketIO.define(Namespace) chat_namespace = self.socketIO.define(Namespace, '/chat') @@ -151,6 +151,16 @@ class BaseMixin(object): 'emit_with_payload_response': (PAYLOAD,), }) + def test_namespace_ack(self): + 'Trigger server callback' + chat_namespace = self.socketIO.define(Namespace, '/chat') + chat_namespace.emit('ack', PAYLOAD) + self.socketIO.wait(self.wait_time_in_seconds) + self.assertEqual(chat_namespace.args_by_event, { + 'ack_response': (PAYLOAD,), + 'ack_callback_response': (PAYLOAD,), + }) + class Test_WebsocketTransport(TestCase, BaseMixin): From 81c6d513a57ed6caaf7640d67604f8b9b07c07c8 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 6 Nov 2013 18:35:09 -0800 Subject: [PATCH 045/191] Updated CHANGES --- CHANGES.rst | 5 +++++ README.rst | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 84ccc57..5b0b164 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +0.5.1 +----- +- Added error handling in the event of websocket timeout +- Fixed sending acknowledgments in custom namespaces thanks to Travis Odom + 0.5 --- - Rewrote library to use coroutines instead of threads to save memory diff --git a/README.rst b/README.rst index 0a9bb4e..36535a6 100644 --- a/README.rst +++ b/README.rst @@ -135,7 +135,7 @@ Credits - `Hiroki Ohtani`_ wrote websocket-client_. - rod_ wrote a `prototype for a Python client to a socket.io server`_ on StackOverflow. - `Alexandre Bourget`_ wrote gevent-socketio_, which is a socket.io server written in Python. -- `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_, `Rui Chicoria`_ submitted code to expand support of the socket.io protocol. +- `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_, `Rui Chicoria`_, `Travis Odom`_ submitted code to expand support of the socket.io protocol. - `Bernard Pratz`_ and `Francis Bull`_ wrote prototypes to support xhr-polling and jsonp-polling. - `Eric Chen`_, `Denis Zinevich`_, `Thiago Hersan`_ suggested ways to make the connection more robust. @@ -163,6 +163,7 @@ Credits .. _Ian Fitzpatrick: https://github.com/GraphEffect .. _Lucas Klein: https://github.com/lukashed .. _Rui Chicoria: https://github.com/rchicoria +.. _Travis Odom: https://github.com/burstaholic .. _Eric Chen: https://github.com/taiyangc .. _Denis Zinevich: https://github.com/dzinevich From 1a16e98d324a056893f796bbc1f0ee37a73746a8 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 7 Nov 2013 06:55:37 -0800 Subject: [PATCH 046/191] Updated README --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 36535a6..f02ce9d 100644 --- a/README.rst +++ b/README.rst @@ -98,11 +98,11 @@ Define different namespaces on a single socket. :: print 'on_aaa_response', args socketIO = SocketIO('localhost', 8000) - chatNamespace = socketIO.define(ChatNamespace, '/chat') - newsNamespace = socketIO.define(NewsNamespace, '/news') + chat_namespace = socketIO.define(ChatNamespace, '/chat') + news_namespace = socketIO.define(NewsNamespace, '/news') - chatNamespace.emit('aaa') - newsNamespace.emit('aaa') + chat_namespace.emit('aaa') + news_namespace.emit('aaa') socketIO.wait(seconds=1) Open secure websockets (HTTPS / WSS) behind a proxy. :: From 579d0493d83d035f519dc6fa10b10581eed78eda Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Tue, 12 Nov 2013 09:51:05 -0800 Subject: [PATCH 047/191] Added instructions for debugging information --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index f02ce9d..227a7a9 100644 --- a/README.rst +++ b/README.rst @@ -26,6 +26,11 @@ Activate isolated environment. :: VIRTUAL_ENV=$HOME/.virtualenv source $VIRTUAL_ENV/bin/activate +For debugging information, run these commands first. :: + + import logging + logging.basicConfig(level=logging.DEBUG) + Emit. :: from socketIO_client import SocketIO From 43261b2347ebf92c3dc29a8b11b12ce0e3a0483d Mon Sep 17 00:00:00 2001 From: Travis Odom Date: Sat, 9 Nov 2013 20:11:26 -0600 Subject: [PATCH 048/191] Always send heartbeats to prevent server timeout --- socketIO_client/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 1cfd900..efe9103 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -354,10 +354,10 @@ def _yield_warning_screen(seconds=None): def _yield_elapsed_time(seconds=None): + start_time = time.time() if seconds is None: while True: - yield float('inf') - start_time = time.time() + yield time.time() - start_time while time.time() - start_time < seconds: yield time.time() - start_time From e7529e54873ca02ba2aa7534e60469863908cd9d Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 17 Nov 2013 13:40:36 -0800 Subject: [PATCH 049/191] Replaced secure=True with host='https://example.com' --- CHANGES.rst | 5 ++++ README.rst | 11 ++++---- TODO.goals | 3 +- TODO.log | 4 +++ setup.py | 2 +- socketIO_client/__init__.py | 52 ++++++++++++++++++++++------------- socketIO_client/transports.py | 16 +++++------ 7 files changed, 57 insertions(+), 36 deletions(-) create mode 100644 TODO.log diff --git a/CHANGES.rst b/CHANGES.rst index 5b0b164..41058f9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +0.5.2 +----- +- Replaced secure=True with host='https://example.com' +- Fixed sending heartbeats thanks to Travis Odom + 0.5.1 ----- - Added error handling in the event of websocket timeout diff --git a/README.rst b/README.rst index 227a7a9..22e9215 100644 --- a/README.rst +++ b/README.rst @@ -110,15 +110,13 @@ Define different namespaces on a single socket. :: news_namespace.emit('aaa') socketIO.wait(seconds=1) -Open secure websockets (HTTPS / WSS) behind a proxy. :: +Connect via SSL. :: from socketIO_client import SocketIO - SocketIO('localhost', 8000, - secure=True, - proxies={'https': 'https://proxy.example.com:8080'}) + SocketIO('https://localhost') -Specify params, headers and cookies thanks to the `requests`_ library. :: +Specify params, headers, cookies, proxies thanks to the `requests`_ library. :: from socketIO_client import SocketIO from base64 import b64encode @@ -126,7 +124,8 @@ Specify params, headers and cookies thanks to the `requests`_ library. :: SocketIO('localhost', 8000, params={'q': 'qqq'}, headers={'Authorization': 'Basic ' + b64encode('username:password')}, - cookies={'a': 'aaa'}) + cookies={'a': 'aaa'}, + proxies={'https': 'https://proxy.example.com:8080'}) License diff --git a/TODO.goals b/TODO.goals index 6721fe8..95743f6 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,2 +1 @@ -# America/Los_Angeles 11/3/2013 -Add test for server ack callback in namespace +# America/Los_Angeles 11/17/2013 \ No newline at end of file diff --git a/TODO.log b/TODO.log new file mode 100644 index 0000000..4da62af --- /dev/null +++ b/TODO.log @@ -0,0 +1,4 @@ +# UTC 11/17/2013 ++ Beware of scheme included in URL [11/17/2013] ++ Add test for server ack callback in namespace [11/17/2013] ++ Set port automatically if it is not automatically specified [11/17/2013] \ No newline at end of file diff --git a/setup.py b/setup.py index 9afabd2..507323d 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( name='socketIO-client', - version='0.5.1', + version='0.5.2', description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index efe9103..de37c0e 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -3,6 +3,7 @@ import json import requests import time from collections import namedtuple +from urlparse import urlparse from .exceptions import ConnectionError, TimeoutError, PacketError from .transports import _get_response, _negotiate_transport, TRANSPORTS @@ -105,24 +106,26 @@ class BaseNamespace(object): class SocketIO(object): + """Create a socket.io client that connects to a socket.io server + at the specified host and port. + + - Define the behavior of the client by specifying a custom Namespace. + - Prefix host with https:// to use SSL. + - Set wait_for_connection=True to block until we have a connection. + - Specify the transports you want to use. + - Pass query params, headers, cookies, proxies as keyword arguments. + + SocketIO('localhost', 8000, + params={'q': 'qqq'}, + headers={'Authorization': 'Basic ' + b64encode('username:password')}, + cookies={'a': 'aaa'}, + proxies={'https': 'https://proxy.example.com:8080'}) + """ def __init__( - self, host, port, Namespace=BaseNamespace, secure=False, + self, host, port=None, Namespace=BaseNamespace, wait_for_connection=True, transports=TRANSPORTS, **kw): - """ - Create a socket.io client that connects to a socket.io server - at the specified host and port. - - Define the behavior of the client by specifying a custom Namespace. - - Set secure=True to use HTTPS / WSS. - - Set wait_for_connection=True to block until we have a connection. - - List the transports you want to use (%s). - - Pass query params, headers, cookies, proxies as keyword arguments. - - SocketIO('localhost', 8000, proxies={ - 'https': 'https://proxy.example.com:8080'}) - """ % ', '.join(TRANSPORTS) - self.base_url = '%s:%d/socket.io/%s' % (host, port, PROTOCOL_VERSION) - self.secure = secure + self.is_secure, self.base_url = _parse_host(host, port) self.wait_for_connection = wait_for_connection self._namespace_by_path = {} self.client_supported_transports = transports @@ -222,7 +225,7 @@ class SocketIO(object): def _get_transport(self): socketIO_session = _get_socketIO_session( - self.secure, self.base_url, **self.kw) + self.is_secure, self.base_url, **self.kw) _log.debug('[transports available] %s', ' '.join( socketIO_session.server_supported_transports)) # Initialize heartbeat_pacemaker @@ -232,7 +235,7 @@ class SocketIO(object): # Negotiate transport transport = _negotiate_transport( self.client_supported_transports, socketIO_session, - self.secure, self.base_url, **self.kw) + self.is_secure, self.base_url, **self.kw) # Update namespaces for namespace in self._namespace_by_path.values(): namespace._transport = transport @@ -340,6 +343,17 @@ def find_callback(args, kw=None): return None, args +def _parse_host(host, port): + if not host.startswith('http'): + host = 'http://' + host + url_pack = urlparse(host) + is_secure = url_pack.scheme == 'https' + port = port or url_pack.port or (443 if is_secure else 80) + base_url = '%s:%d%s/socket.io/%s' % ( + url_pack.hostname, port, url_pack.path, PROTOCOL_VERSION) + return is_secure, base_url + + def _yield_warning_screen(seconds=None): last_warning = None for elapsed_time in _yield_elapsed_time(seconds): @@ -362,8 +376,8 @@ def _yield_elapsed_time(seconds=None): yield time.time() - start_time -def _get_socketIO_session(secure, base_url, **kw): - server_url = '%s://%s/' % ('https' if secure else 'http', base_url) +def _get_socketIO_session(is_secure, base_url, **kw): + server_url = '%s://%s/' % ('https' if is_secure else 'http', base_url) try: response = _get_response(requests.get, server_url, **kw) except TimeoutError as e: diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index a3469e5..758f34e 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -104,10 +104,10 @@ class _AbstractTransport(object): class _WebsocketTransport(_AbstractTransport): - def __init__(self, socketIO_session, secure, base_url, **kw): + def __init__(self, socketIO_session, is_secure, base_url, **kw): super(_WebsocketTransport, self).__init__() url = '%s://%s/websocket/%s' % ( - 'wss' if secure else 'ws', + 'wss' if is_secure else 'ws', base_url, socketIO_session.id) try: self._connection = websocket.create_connection(url) @@ -151,10 +151,10 @@ class _WebsocketTransport(_AbstractTransport): class _XHR_PollingTransport(_AbstractTransport): - def __init__(self, socketIO_session, secure, base_url, **kw): + def __init__(self, socketIO_session, is_secure, base_url, **kw): super(_XHR_PollingTransport, self).__init__() self._url = '%s://%s/xhr-polling/%s' % ( - 'https' if secure else 'http', + 'https' if is_secure else 'http', base_url, socketIO_session.id) self._connected = True self._http_session = _prepare_http_session(kw) @@ -203,10 +203,10 @@ class _JSONP_PollingTransport(_AbstractTransport): RESPONSE_PATTERN = re.compile(r'io.j\[(\d+)\]\("(.*)"\);') - def __init__(self, socketIO_session, secure, base_url, **kw): + def __init__(self, socketIO_session, is_secure, base_url, **kw): super(_JSONP_PollingTransport, self).__init__() self._url = '%s://%s/jsonp-polling/%s' % ( - 'https' if secure else 'http', + 'https' if is_secure else 'http', base_url, socketIO_session.id) self._connected = True self._http_session = _prepare_http_session(kw) @@ -264,7 +264,7 @@ class _JSONP_PollingTransport(_AbstractTransport): def _negotiate_transport( client_supported_transports, session, - secure, base_url, **kw): + is_secure, base_url, **kw): server_supported_transports = session.server_supported_transports for supported_transport in client_supported_transports: if supported_transport in server_supported_transports: @@ -273,7 +273,7 @@ def _negotiate_transport( 'websocket': _WebsocketTransport, 'xhr-polling': _XHR_PollingTransport, 'jsonp-polling': _JSONP_PollingTransport, - }[supported_transport](session, secure, base_url, **kw) + }[supported_transport](session, is_secure, base_url, **kw) raise SocketIOError(' '.join([ 'could not negotiate a transport:', 'client supports %s but' % ', '.join(client_supported_transports), From 1506e224584d178c17567b824f99d494ed599950 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 18 Nov 2013 09:45:09 -0800 Subject: [PATCH 050/191] Set heartbeat_interval to half of the heartbeat_timeout --- README.rst | 7 +++++++ TODO.goals | 3 ++- socketIO_client/__init__.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 22e9215..1e9f420 100644 --- a/README.rst +++ b/README.rst @@ -127,6 +127,13 @@ Specify params, headers, cookies, proxies thanks to the `requests`_ library. :: cookies={'a': 'aaa'}, proxies={'https': 'https://proxy.example.com:8080'}) +Wait forever. :: + + from socketIO_client import SocketIO + + socketIO = SocketIO('localhost') + socketIO.wait() + License ------- diff --git a/TODO.goals b/TODO.goals index 95743f6..0ffe609 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1 +1,2 @@ -# America/Los_Angeles 11/17/2013 \ No newline at end of file +# America/Los_Angeles 11/17/2013 +_ Investigate heartbeat timeouts diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index de37c0e..af65fca 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -230,7 +230,7 @@ class SocketIO(object): socketIO_session.server_supported_transports)) # Initialize heartbeat_pacemaker self.heartbeat_pacemaker = self._make_heartbeat_pacemaker( - heartbeat_interval=socketIO_session.heartbeat_timeout - 2) + heartbeat_interval=socketIO_session.heartbeat_timeout / 2) self.heartbeat_pacemaker.next() # Negotiate transport transport = _negotiate_transport( From 5ccb32f338d8816a2d655b99b2510024385cd321 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 18 Nov 2013 10:43:31 -0800 Subject: [PATCH 051/191] Exit the loop if the client wants to disconnect; fixes #30 --- CHANGES.rst | 5 +++++ serve_tests.js | 3 +++ setup.py | 2 +- socketIO_client/__init__.py | 19 +++++++++++++++++-- socketIO_client/tests.py | 21 +++++++++++++++++---- socketIO_client/transports.py | 3 +++ 6 files changed, 46 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 41058f9..7e91a66 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +0.5.3 +----- +- Exit the wait loop if the client wants to disconnect +- Set heartbeat_interval to be half of the heartbeat_timeout + 0.5.2 ----- - Replaced secure=True with host='https://example.com' diff --git a/serve_tests.js b/serve_tests.js index 9107d4a..6c2edfc 100644 --- a/serve_tests.js +++ b/serve_tests.js @@ -48,6 +48,9 @@ var main = io.of('').on('connection', function(socket) { fn(payload); } }); + socket.on('wait_with_disconnect', function() { + socket.emit('wait_with_disconnect_response'); + }); }); var chat = io.of('/chat').on('connection', function (socket) { diff --git a/setup.py b/setup.py index 507323d..639cb7d 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( name='socketIO-client', - version='0.5.2', + version='0.5.3', description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index af65fca..5faa483 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -39,6 +39,9 @@ class BaseNamespace(object): callback, args = find_callback(args, kw) self._transport.emit(self.path, event, args, callback) + def disconnect(self): + self._transport.disconnect(self.path) + def on(self, event, callback): 'Define a callback to handle a custom event emitted by the server' self._callback_by_event[event] = callback @@ -161,12 +164,14 @@ class SocketIO(object): self._transport.emit(path, event, args, callback) def wait(self, seconds=None, for_callbacks=False): + """Wait in a loop and process events as defined in the namespaces. + + - Omit seconds, i.e. call wait() without arguments, to wait forever. + """ try: warning_screen = _yield_warning_screen(seconds) for elapsed_time in warning_screen: try: - if for_callbacks and not self._transport.has_ack_callback: - break try: for packet in self._transport.recv_packet(): try: @@ -175,6 +180,8 @@ class SocketIO(object): _log.warn('[packet error] %s', e) except TimeoutError: pass + if self._stop_waiting(for_callbacks): + break self.heartbeat_pacemaker.send(elapsed_time) except ConnectionError as e: try: @@ -186,6 +193,14 @@ class SocketIO(object): except KeyboardInterrupt: pass + def _stop_waiting(self, for_callbacks): + # Use __transport to make sure that we do not reconnect inadvertently + if for_callbacks and not self.__transport.has_ack_callback: + return True + if self.__transport._wants_to_disconnect: + return True + return False + def wait_for_callbacks(self, seconds=None): self.wait(seconds, for_callbacks=True) diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index db96990..dfebecb 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -1,4 +1,5 @@ import logging +import time from unittest import TestCase from . import SocketIO, BaseNamespace, find_callback @@ -109,15 +110,15 @@ class BaseMixin(object): def test_emit_with_callback_with_payload(self): 'Emit with callback with payload' - self.socketIO.emit('emit_with_callback_with_payload', - self.on_response) + self.socketIO.emit( + 'emit_with_callback_with_payload', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit_with_callback_with_multiple_payloads(self): 'Emit with callback with multiple payloads' - self.socketIO.emit('emit_with_callback_with_multiple_payloads', - self.on_response) + self.socketIO.emit( + 'emit_with_callback_with_multiple_payloads', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) @@ -138,6 +139,15 @@ class BaseMixin(object): 'ack_callback_response': (PAYLOAD,), }) + def test_wait_with_disconnect(self): + 'Exit loop when the client wants to disconnect' + self.socketIO.define(Namespace) + self.socketIO.emit('wait_with_disconnect') + timeout_in_seconds = 5 + start_time = time.time() + self.socketIO.wait(timeout_in_seconds) + self.assertTrue(time.time() - start_time < timeout_in_seconds) + def test_namespace_emit(self): 'Behave differently in different namespaces' main_namespace = self.socketIO.define(Namespace) @@ -204,3 +214,6 @@ class Namespace(BaseNamespace): if callback: callback(*args) self.args_by_event[event] = args + + def on_wait_with_disconnect_response(self): + self.disconnect() diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 758f34e..b28a1ee 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -22,8 +22,11 @@ class _AbstractTransport(object): def __init__(self): self._packet_id = 0 self._callback_by_packet_id = {} + self._wants_to_disconnect = False def disconnect(self, path=''): + if not path: + self._wants_to_disconnect = True if not self.connected: return if path: From 38e72dc304c31eeb291b31ea9aeac70cae059662 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 20 Nov 2013 08:07:07 -0800 Subject: [PATCH 052/191] Fixed calling on_connect() so that it is called only once; fixes #31 --- CHANGES.rst | 3 ++- README.rst | 3 ++- TODO.goals | 3 +-- TODO.log | 3 ++- socketIO_client/__init__.py | 28 ++++++++++++++++------------ socketIO_client/transports.py | 19 ++++++++++++++----- 6 files changed, 37 insertions(+), 22 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7e91a66..90ee4b2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,7 @@ 0.5.3 ----- -- Exit the wait loop if the client wants to disconnect +- Updated wait loop to exit if the client wants to disconnect +- Fixed calling on_connect() so that it is called only once - Set heartbeat_interval to be half of the heartbeat_timeout 0.5.2 diff --git a/README.rst b/README.rst index 1e9f420..64dc5cd 100644 --- a/README.rst +++ b/README.rst @@ -148,7 +148,7 @@ Credits - `Alexandre Bourget`_ wrote gevent-socketio_, which is a socket.io server written in Python. - `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_, `Rui Chicoria`_, `Travis Odom`_ submitted code to expand support of the socket.io protocol. - `Bernard Pratz`_ and `Francis Bull`_ wrote prototypes to support xhr-polling and jsonp-polling. -- `Eric Chen`_, `Denis Zinevich`_, `Thiago Hersan`_ suggested ways to make the connection more robust. +- `Eric Chen`_, `Denis Zinevich`_, `Thiago Hersan`_, `Nayef Copty`_ suggested ways to make the connection more robust. .. _socket.io: http://socket.io @@ -179,3 +179,4 @@ Credits .. _Eric Chen: https://github.com/taiyangc .. _Denis Zinevich: https://github.com/dzinevich .. _Thiago Hersan: https://github.com/thiagohersan +.. _Nayef Copty: https://github.com/nayefc diff --git a/TODO.goals b/TODO.goals index 0ffe609..cb82706 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,2 +1 @@ -# America/Los_Angeles 11/17/2013 -_ Investigate heartbeat timeouts +# US/Pacific 11/19/2013 \ No newline at end of file diff --git a/TODO.log b/TODO.log index 4da62af..70f9984 100644 --- a/TODO.log +++ b/TODO.log @@ -1,4 +1,5 @@ -# UTC 11/17/2013 +# UTC 11/19/2013 ++ Add nayefc to acknowledgments [11/19/2013] + Beware of scheme included in URL [11/17/2013] + Add test for server ack callback in namespace [11/17/2013] + Set port automatically if it is not automatically specified [11/17/2013] \ No newline at end of file diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 5faa483..106accc 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -148,7 +148,6 @@ class SocketIO(object): if path: self._transport.connect(path) namespace = Namespace(self._transport, path) - namespace.on_connect() self._namespace_by_path[path] = namespace return namespace @@ -173,11 +172,7 @@ class SocketIO(object): for elapsed_time in warning_screen: try: try: - for packet in self._transport.recv_packet(): - try: - self._process_packet(packet) - except PacketError as e: - _log.warn('[packet error] %s', e) + self._process_events() except TimeoutError: pass if self._stop_waiting(for_callbacks): @@ -193,6 +188,21 @@ class SocketIO(object): except KeyboardInterrupt: pass + def _process_events(self): + for packet in self._transport.recv_packet(): + try: + self._process_packet(packet) + except PacketError as e: + _log.warn('[packet error] %s', e) + + def _process_packet(self, packet): + logging.debug('xxx') + logging.debug(packet) + code, packet_id, path, data = packet + namespace = self.get_namespace(path) + delegate = self._get_delegate(code) + delegate(packet, namespace._find_event_callback) + def _stop_waiting(self, for_callbacks): # Use __transport to make sure that we do not reconnect inadvertently if for_callbacks and not self.__transport.has_ack_callback: @@ -264,12 +274,6 @@ class SocketIO(object): heartbeat_time = elapsed_time self._transport.send_heartbeat() - def _process_packet(self, packet): - code, packet_id, path, data = packet - namespace = self.get_namespace(path) - delegate = self._get_delegate(code) - delegate(packet, namespace._find_event_callback) - def get_namespace(self, path=''): try: return self._namespace_by_path[path] diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index b28a1ee..ed4e59c 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -23,6 +23,7 @@ class _AbstractTransport(object): self._packet_id = 0 self._callback_by_packet_id = {} self._wants_to_disconnect = False + self._packets = [] def disconnect(self, path=''): if not path: @@ -71,7 +72,11 @@ class _AbstractTransport(object): _log.debug('[packet sent] %s', packet_text) def recv_packet(self): - code, packet_id, path, data = None, None, None, None + try: + while self._packets: + yield self._packets.pop(0) + except IndexError: + pass for packet_text in self.recv(): _log.debug('[packet received] %s', packet_text) try: @@ -79,6 +84,7 @@ class _AbstractTransport(object): except AttributeError: _log.warn('[packet error] %s', packet_text) continue + code, packet_id, path, data = None, None, None, None packet_count = len(packet_parts) if 4 == packet_count: code, packet_id, path, data = packet_parts @@ -88,6 +94,9 @@ class _AbstractTransport(object): code = packet_parts[0] yield code, packet_id, path, data + def _enqueue_packet(self, packet): + self._packets.append(packet) + def set_ack_callback(self, callback): 'Set callback to be called after server sends an acknowledgment' self._packet_id += 1 @@ -162,8 +171,8 @@ class _XHR_PollingTransport(_AbstractTransport): self._connected = True self._http_session = _prepare_http_session(kw) # Create connection - for packet_text in self.recv_packet(): - pass + for packet in self.recv_packet(): + self._enqueue_packet(packet) @property def connected(self): @@ -215,8 +224,8 @@ class _JSONP_PollingTransport(_AbstractTransport): self._http_session = _prepare_http_session(kw) self._id = 0 # Create connection - for packet_text in self.recv_packet(): - pass + for packet in self.recv_packet(): + self._enqueue_packet(packet) @property def connected(self): From 3c73bc9b5592718103ca5a096f6da49614ad93ef Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 20 Nov 2013 08:16:03 -0800 Subject: [PATCH 053/191] Added TODO --- TODO.goals | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.goals b/TODO.goals index cb82706..d63b1d2 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1 +1,2 @@ -# US/Pacific 11/19/2013 \ No newline at end of file +# US/Pacific 11/19/2013 +Include graingert's pull request on travis.yml From 68d9639a566d54c0ba52fd76b117aef1fecf2b5e Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 20 Nov 2013 08:17:44 -0800 Subject: [PATCH 054/191] Removed extra debugging statements --- socketIO_client/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 106accc..aee4543 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -196,8 +196,6 @@ class SocketIO(object): _log.warn('[packet error] %s', e) def _process_packet(self, packet): - logging.debug('xxx') - logging.debug(packet) code, packet_id, path, data = packet namespace = self.get_namespace(path) delegate = self._get_delegate(code) From 9c1766b80681e49e98f6de3ded654b9863c87a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Sureau?= Date: Thu, 21 Nov 2013 16:20:45 +0100 Subject: [PATCH 055/191] Automatically reconnect transport to existing namespaces --- socketIO_client/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index aee4543..3bdecf1 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -260,8 +260,9 @@ class SocketIO(object): self.client_supported_transports, socketIO_session, self.is_secure, self.base_url, **self.kw) # Update namespaces - for namespace in self._namespace_by_path.values(): + for path, namespace in self._namespace_by_path.iteritems(): namespace._transport = transport + transport.connect(path) return transport def _make_heartbeat_pacemaker(self, heartbeat_interval): From 13ffc8cf7f9b13a07eeb0aa45c7f2acca42c7e16 Mon Sep 17 00:00:00 2001 From: Marcus Cobden Date: Mon, 9 Dec 2013 09:56:36 +0000 Subject: [PATCH 056/191] Do not suppress KeyboardInterrupt exceptions The encompassing application should be responsible for handling the KeyboardInterrupt exception --- socketIO_client/__init__.py | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3bdecf1..bd0df20 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -167,26 +167,23 @@ class SocketIO(object): - Omit seconds, i.e. call wait() without arguments, to wait forever. """ - try: - warning_screen = _yield_warning_screen(seconds) - for elapsed_time in warning_screen: + warning_screen = _yield_warning_screen(seconds) + for elapsed_time in warning_screen: + try: try: - try: - self._process_events() - except TimeoutError: - pass - if self._stop_waiting(for_callbacks): - break - self.heartbeat_pacemaker.send(elapsed_time) - except ConnectionError as e: - try: - warning = Exception('[connection error] %s' % e) - warning_screen.throw(warning) - except StopIteration: - _log.warn(warning) - self.disconnect() - except KeyboardInterrupt: - pass + self._process_events() + except TimeoutError: + pass + if self._stop_waiting(for_callbacks): + break + self.heartbeat_pacemaker.send(elapsed_time) + except ConnectionError as e: + try: + warning = Exception('[connection error] %s' % e) + warning_screen.throw(warning) + except StopIteration: + _log.warn(warning) + self.disconnect() def _process_events(self): for packet in self._transport.recv_packet(): From d05683262bbd6ac011927d43c0c9a27d9b0313b6 Mon Sep 17 00:00:00 2001 From: Marcus Cobden Date: Mon, 9 Dec 2013 10:16:22 +0000 Subject: [PATCH 057/191] Pass on wait timeout to underlying transports --- socketIO_client/__init__.py | 6 +++--- socketIO_client/transports.py | 16 +++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3bdecf1..9a7f436 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -172,7 +172,7 @@ class SocketIO(object): for elapsed_time in warning_screen: try: try: - self._process_events() + self._process_events(timeout=seconds) except TimeoutError: pass if self._stop_waiting(for_callbacks): @@ -188,8 +188,8 @@ class SocketIO(object): except KeyboardInterrupt: pass - def _process_events(self): - for packet in self._transport.recv_packet(): + def _process_events(self, timeout=None): + for packet in self._transport.recv_packet(timeout=timeout): try: self._process_packet(packet) except PacketError as e: diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index ed4e59c..e257f41 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -71,13 +71,13 @@ class _AbstractTransport(object): self.send(packet_text) _log.debug('[packet sent] %s', packet_text) - def recv_packet(self): + def recv_packet(self, timeout=None): try: while self._packets: yield self._packets.pop(0) except IndexError: pass - for packet_text in self.recv(): + for packet_text in self.recv(timeout=timeout): _log.debug('[packet received] %s', packet_text) try: packet_parts = packet_text.split(':', 3) @@ -145,7 +145,9 @@ class _WebsocketTransport(_AbstractTransport): _log.warn(message) raise ConnectionError(message) - def recv(self): + def recv(self, timeout=None): + if timeout: + self._connection.settimeout(timeout) try: yield self._connection.recv() except websocket.WebSocketTimeoutException as e: @@ -190,12 +192,12 @@ class _XHR_PollingTransport(_AbstractTransport): data=packet_text, timeout=TIMEOUT_IN_SECONDS) - def recv(self): + def recv(self, timeout=None): response = _get_response( self._http_session.get, self._url, params=self._params, - timeout=TIMEOUT_IN_SECONDS) + timeout=timeout or TIMEOUT_IN_SECONDS) response_text = response.text if not response_text.startswith(BOUNDARY): yield response_text @@ -244,14 +246,14 @@ class _JSONP_PollingTransport(_AbstractTransport): headers={'content-type': 'application/x-www-form-urlencoded'}, timeout=TIMEOUT_IN_SECONDS) - def recv(self): + def recv(self, timeout=None): 'Decode the JavaScript response so that we can parse it as JSON' response = _get_response( self._http_session.get, self._url, params=self._params, headers={'content-type': 'text/javascript; charset=UTF-8'}, - timeout=TIMEOUT_IN_SECONDS) + timeout=timeout or TIMEOUT_IN_SECONDS) response_text = response.text try: self._id, response_text = self.RESPONSE_PATTERN.match( From 09199c10df97b87b9d61c0316cc14b658de3fab6 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Tue, 17 Dec 2013 18:29:46 +0100 Subject: [PATCH 058/191] Add context aware logmessages to the socketio client --- socketIO_client/__init__.py | 38 +++++++++++++++++++++-------------- socketIO_client/transports.py | 19 +++++++++++------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3bdecf1..877b1b3 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -28,6 +28,9 @@ class BaseNamespace(object): self._callback_by_event = {} self.initialize() + def _log(self, level, msg, *attrs): + _log.log(level, '%s: %s' % (self._transport._url, msg), *attrs) + def initialize(self): 'Initialize custom variables here; you can override this method' pass @@ -48,19 +51,19 @@ class BaseNamespace(object): def on_connect(self): 'Called after server connects; you can override this method' - _log.debug('%s [connect]', self.path) + self._log(logging.DEBUG, '%s [connect]', self.path) def on_disconnect(self): 'Called after server disconnects; you can override this method' - _log.debug('%s [disconnect]', self.path) + self._log(logging.DEBUG, '%s [disconnect]', self.path) def on_heartbeat(self): 'Called after server sends a heartbeat; you can override this method' - _log.debug('%s [heartbeat]', self.path) + self._log(logging.DEBUG, '%s [heartbeat]', self.path) def on_message(self, data): 'Called after server sends a message; you can override this method' - _log.info('%s [message] %s', self.path, data) + self._log(logging.INFO, '%s [message] %s', self.path, data) def on_event(self, event, *args): """ @@ -73,27 +76,28 @@ class BaseNamespace(object): if callback: arguments.append('callback(*args)') callback(*args) - _log.info('%s [event] %s(%s)', self.path, event, ', '.join(arguments)) + self._log(logging.INFO, '%s [event] %s(%s)', self.path, event, + ', '.join(arguments)) def on_error(self, reason, advice): 'Called after server sends an error; you can override this method' - _log.info('%s [error] %s', self.path, advice) + self._log(logging.INFO, '%s [error] %s', self.path, advice) def on_noop(self): 'Called after server sends a noop; you can override this method' - _log.info('%s [noop]', self.path) + self._log(logging.INFO, '%s [noop]', self.path) def on_open(self, *args): - _log.info('%s [open] %s', self.path, args) + self._log(logging.INFO, '%s [open] %s', self.path, args) def on_close(self, *args): - _log.info('%s [close] %s', self.path, args) + self._log(logging.INFO, '%s [close] %s', self.path, args) def on_retry(self, *args): - _log.info('%s [retry] %s', self.path, args) + self._log(logging.INFO, '%s [retry] %s', self.path, args) def on_reconnect(self, *args): - _log.info('%s [reconnect] %s', self.path, args) + self._log(logging.INFO, '%s [reconnect] %s', self.path, args) def _find_event_callback(self, event): # Check callbacks defined by on() @@ -135,6 +139,10 @@ class SocketIO(object): self.kw = kw self.define(Namespace) + def log(self, level, msg, *attrs): + _log.log(level, '%s: %s' % (self.base_url, msg), + *attrs) + def __enter__(self): return self @@ -183,7 +191,7 @@ class SocketIO(object): warning = Exception('[connection error] %s' % e) warning_screen.throw(warning) except StopIteration: - _log.warn(warning) + self.log(logging.WARNING, warning) self.disconnect() except KeyboardInterrupt: pass @@ -193,7 +201,7 @@ class SocketIO(object): try: self._process_packet(packet) except PacketError as e: - _log.warn('[packet error] %s', e) + self.log(logging.WARNING, '[packet error] %s', e) def _process_packet(self, packet): code, packet_id, path, data = packet @@ -243,13 +251,13 @@ class SocketIO(object): warning = Exception('[waiting for connection] %s' % e) warning_screen.throw(warning) except StopIteration: - _log.warn(warning) + self.log(logging.WARNING, warning) return self.__transport def _get_transport(self): socketIO_session = _get_socketIO_session( self.is_secure, self.base_url, **self.kw) - _log.debug('[transports available] %s', ' '.join( + self.log(logging.DEBUG, '[transports available] %s', ' '.join( socketIO_session.server_supported_transports)) # Initialize heartbeat_pacemaker self.heartbeat_pacemaker = self._make_heartbeat_pacemaker( diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index ed4e59c..372a11c 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -25,6 +25,9 @@ class _AbstractTransport(object): self._wants_to_disconnect = False self._packets = [] + def _log(self, level, msg, *attrs): + _log.log(level, '[%s] %s' % (self._url, msg), *attrs) + def disconnect(self, path=''): if not path: self._wants_to_disconnect = True @@ -69,7 +72,7 @@ class _AbstractTransport(object): packet_parts = str(code), packet_id, path, data packet_text = ':'.join(packet_parts) self.send(packet_text) - _log.debug('[packet sent] %s', packet_text) + self._log(logging.DEBUG, '[packet sent] %s', packet_text) def recv_packet(self): try: @@ -78,11 +81,11 @@ class _AbstractTransport(object): except IndexError: pass for packet_text in self.recv(): - _log.debug('[packet received] %s', packet_text) + self._log(logging.DEBUG, '[packet received] %s', packet_text) try: packet_parts = packet_text.split(':', 3) except AttributeError: - _log.warn('[packet error] %s', packet_text) + self._log(logging.WARNING, '[packet error] %s', packet_text) continue code, packet_id, path, data = None, None, None, None packet_count = len(packet_parts) @@ -121,6 +124,7 @@ class _WebsocketTransport(_AbstractTransport): url = '%s://%s/websocket/%s' % ( 'wss' if is_secure else 'ws', base_url, socketIO_session.id) + self._url = url try: self._connection = websocket.create_connection(url) except socket.timeout as e: @@ -138,11 +142,11 @@ class _WebsocketTransport(_AbstractTransport): self._connection.send(packet_text) except websocket.WebSocketTimeoutException as e: message = 'timed out while sending %s (%s)' % (packet_text, e) - _log.warn(message) + self._log(logging.WARNING, message) raise TimeoutError(e) except socket.error as e: message = 'disconnected while sending %s (%s)' % (packet_text, e) - _log.warn(message) + self._log(logging.WARNING, message) raise ConnectionError(message) def recv(self): @@ -257,7 +261,7 @@ class _JSONP_PollingTransport(_AbstractTransport): self._id, response_text = self.RESPONSE_PATTERN.match( response_text).groups() except AttributeError: - _log.warn('[packet error] %s', response_text) + self._log(self.WARNING, '[packet error] %s', response_text) return if not response_text.startswith(BOUNDARY): yield response_text.decode('unicode_escape') @@ -280,7 +284,8 @@ def _negotiate_transport( server_supported_transports = session.server_supported_transports for supported_transport in client_supported_transports: if supported_transport in server_supported_transports: - _log.debug('[transport selected] %s', supported_transport) + _log.debug('[%s] [transport selected] %s', base_url, + supported_transport) return { 'websocket': _WebsocketTransport, 'xhr-polling': _XHR_PollingTransport, From fa454b5950af23dd6b24312dc7e771eb9ec432fa Mon Sep 17 00:00:00 2001 From: Marcus Cobden Date: Mon, 23 Dec 2013 13:46:37 +0000 Subject: [PATCH 059/191] Catch AttributeError in client connected check If the client has never connected, there will be no __transport attribute. --- socketIO_client/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3bdecf1..c6b85f2 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -222,7 +222,12 @@ class SocketIO(object): @property def connected(self): - return self.__transport.connected + try: + transport = self.__transport + except AttributeError: + return False + else: + return transport.connected @property def _transport(self): From 6bef8c00099a7aaaa2c15342956aae03fc8d7701 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 1 Jan 2014 14:48:15 +0100 Subject: [PATCH 060/191] heartbeat_time independent from elapse_time --- socketIO_client/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3bdecf1..d75e048 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -177,7 +177,7 @@ class SocketIO(object): pass if self._stop_waiting(for_callbacks): break - self.heartbeat_pacemaker.send(elapsed_time) + self.heartbeat_pacemaker.next() except ConnectionError as e: try: warning = Exception('[connection error] %s' % e) @@ -266,11 +266,11 @@ class SocketIO(object): return transport def _make_heartbeat_pacemaker(self, heartbeat_interval): - heartbeat_time = 0 + heartbeat_time = time.time() while True: - elapsed_time = (yield) - if elapsed_time - heartbeat_time > heartbeat_interval: - heartbeat_time = elapsed_time + yield + if time.time() - heartbeat_time > heartbeat_interval: + heartbeat_time = time.time() self._transport.send_heartbeat() def get_namespace(self, path=''): From e16652c4cce1e4f7d76eb36386e0a9f2ea0d9cf4 Mon Sep 17 00:00:00 2001 From: Brad Campbell Date: Fri, 10 Jan 2014 18:29:57 -0500 Subject: [PATCH 061/191] Make on_reconnect callback functional Previously, if the socket.io connection got interrupted and reconnected the on_connect callback got called again. I noticed that there is a on_reconnect function to overide in the base namespace class, but since there is no socket.io command corresponding to on_reconnect it never gets called. This commit adds a check to see if we have been connected before, and if so, calls the on_reconnect callback instead of the on_connect callback agin. --- socketIO_client/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3bdecf1..fc224e3 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -25,6 +25,7 @@ class BaseNamespace(object): def __init__(self, _transport, path): self._transport = _transport self.path = path + self.was_connected = False self._callback_by_event = {} self.initialize() @@ -101,6 +102,15 @@ class BaseNamespace(object): return self._callback_by_event[event] except KeyError: pass + + # Convert connect to reconnect if we have seen connect + # already. + if event == 'connect': + if self.was_connected == False: + self.was_connected = True + else: + event = 'reconnect' + # Check callbacks defined explicitly or use on_event() return getattr( self, From 8f97e73bacf7dffc7135e87fd9902e243bcb1503 Mon Sep 17 00:00:00 2001 From: drewhutchison Date: Wed, 12 Feb 2014 15:07:36 -0700 Subject: [PATCH 062/191] check for exit condition before transport reconnects --- socketIO_client/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3bdecf1..1bccef5 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -170,13 +170,13 @@ class SocketIO(object): try: warning_screen = _yield_warning_screen(seconds) for elapsed_time in warning_screen: + if self._stop_waiting(for_callbacks): + break try: try: self._process_events() except TimeoutError: pass - if self._stop_waiting(for_callbacks): - break self.heartbeat_pacemaker.send(elapsed_time) except ConnectionError as e: try: From c82a8301bbea43e729375bea8d0cdfba25c40c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Sureau?= Date: Tue, 25 Feb 2014 14:56:44 +0100 Subject: [PATCH 063/191] Use HTTP headers for Websocket connection --- socketIO_client/transports.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index ed4e59c..95f3c67 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -121,8 +121,13 @@ class _WebsocketTransport(_AbstractTransport): url = '%s://%s/websocket/%s' % ( 'wss' if is_secure else 'ws', base_url, socketIO_session.id) + + http_session = _prepare_http_session(kw) + req = http_session.prepare_request(requests.Request('GET', url)) + headers = ['%s: %s' % item for item in req.headers.iteritems()] + try: - self._connection = websocket.create_connection(url) + self._connection = websocket.create_connection(url, header=headers) except socket.timeout as e: raise ConnectionError(e) except socket.error as e: From 6a5a35d84d634c24afe8f0cbe77f76979fdd45aa Mon Sep 17 00:00:00 2001 From: fredericsureau Date: Fri, 21 Mar 2014 18:02:03 +0100 Subject: [PATCH 064/191] Force packets encoding to utf-8 --- socketIO_client/transports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index ed4e59c..f75e802 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -66,7 +66,7 @@ class _AbstractTransport(object): def send_packet(self, code, path='', data='', callback=None): packet_id = self.set_ack_callback(callback) if callback else '' - packet_parts = str(code), packet_id, path, data + packet_parts = str(code), packet_id, path, unicode(data).encode('utf-8') packet_text = ':'.join(packet_parts) self.send(packet_text) _log.debug('[packet sent] %s', packet_text) From f182df8580899afd7bc28fdff9d77804975e884a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Sureau?= Date: Tue, 25 Mar 2014 16:18:19 +0100 Subject: [PATCH 065/191] Implement transports fallback --- socketIO_client/__init__.py | 80 ++++++++++++++++++++++++++--------- socketIO_client/transports.py | 19 --------- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3bdecf1..7012cfc 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -6,7 +6,7 @@ from collections import namedtuple from urlparse import urlparse from .exceptions import ConnectionError, TimeoutError, PacketError -from .transports import _get_response, _negotiate_transport, TRANSPORTS +from .transports import _get_response, TRANSPORTS, _WebsocketTransport, _XHR_PollingTransport, _JSONP_PollingTransport _SocketIOSession = namedtuple('_SocketIOSession', [ @@ -127,9 +127,11 @@ class SocketIO(object): def __init__( self, host, port=None, Namespace=BaseNamespace, - wait_for_connection=True, transports=TRANSPORTS, **kw): + wait_for_connection=True, transports=TRANSPORTS, + try_multiple_transports=True, **kw): self.is_secure, self.base_url = _parse_host(host, port) self.wait_for_connection = wait_for_connection + self.try_multiple_transports = try_multiple_transports self._namespace_by_path = {} self.client_supported_transports = transports self.kw = kw @@ -231,39 +233,79 @@ class SocketIO(object): return self.__transport except AttributeError: pass + warning_screen = _yield_warning_screen(seconds=None) + + # Transport handshake + if self.wait_for_connection: + for elapsed_time in warning_screen: + try: + socketIO_session = _get_socketIO_session(self.is_secure, self.base_url, **self.kw) + break + except ConnectionError as e: + try: + warning = Exception('[waiting for handshake] %s' % e) + warning_screen.throw(warning) + except StopIteration: + _log.warn(warning) + + else: + socketIO_session = _get_socketIO_session(self.is_secure, self.base_url, **self.kw) + + supported_transports = self._get_supported_transports(socketIO_session) + _log.debug('[negociated transports] %s', supported_transports) + + # Initialize heartbeat_pacemaker + self.heartbeat_pacemaker = self._make_heartbeat_pacemaker(socketIO_session.heartbeat_timeout / 2) + self.heartbeat_pacemaker.next() + for elapsed_time in warning_screen: + _log.debug('supported_transports = %s', supported_transports) try: - self.__transport = self._get_transport() + _log.debug('[trying transport] %s', supported_transports[0]) + self.__transport = self._get_transport(socketIO_session, supported_transports[0]) break except ConnectionError as e: - if not self.wait_for_connection: + last_transport = len(supported_transports) <= 1 + if not self.wait_for_connection and (not self.try_multiple_transports or last_transport): raise + try: warning = Exception('[waiting for connection] %s' % e) warning_screen.throw(warning) except StopIteration: _log.warn(warning) + + if self.try_multiple_transports and not last_transport: + supported_transports.pop(0) + + self._update_namespaces(self.__transport) return self.__transport - def _get_transport(self): - socketIO_session = _get_socketIO_session( - self.is_secure, self.base_url, **self.kw) - _log.debug('[transports available] %s', ' '.join( - socketIO_session.server_supported_transports)) - # Initialize heartbeat_pacemaker - self.heartbeat_pacemaker = self._make_heartbeat_pacemaker( - heartbeat_interval=socketIO_session.heartbeat_timeout / 2) - self.heartbeat_pacemaker.next() - # Negotiate transport - transport = _negotiate_transport( - self.client_supported_transports, socketIO_session, - self.is_secure, self.base_url, **self.kw) - # Update namespaces + def _get_supported_transports(self, session): + _log.debug('[transports available] %s', ' '.join(session.server_supported_transports)) + supported_transports = [t for t in self.client_supported_transports if t in session.server_supported_transports] + + if not supported_transports: + raise SocketIOError(' '.join([ + 'could not negotiate a transport:', + 'client supports %s but' % ', '.join(client_supported_transports), + 'server supports %s' % ', '.join(server_supported_transports), + ])) + + return supported_transports + + def _get_transport(self, session, transport_name): + return { + 'websocket': _WebsocketTransport, + 'xhr-polling': _XHR_PollingTransport, + 'jsonp-polling': _JSONP_PollingTransport, + }[transport_name](session, self.is_secure, self.base_url, **self.kw) + + def _update_namespaces(self, transport): for path, namespace in self._namespace_by_path.iteritems(): namespace._transport = transport transport.connect(path) - return transport def _make_heartbeat_pacemaker(self, heartbeat_interval): heartbeat_time = 0 diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index ed4e59c..5d911a5 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -274,25 +274,6 @@ class _JSONP_PollingTransport(_AbstractTransport): self._connected = False -def _negotiate_transport( - client_supported_transports, session, - is_secure, base_url, **kw): - server_supported_transports = session.server_supported_transports - for supported_transport in client_supported_transports: - if supported_transport in server_supported_transports: - _log.debug('[transport selected] %s', supported_transport) - return { - 'websocket': _WebsocketTransport, - 'xhr-polling': _XHR_PollingTransport, - 'jsonp-polling': _JSONP_PollingTransport, - }[supported_transport](session, is_secure, base_url, **kw) - raise SocketIOError(' '.join([ - 'could not negotiate a transport:', - 'client supports %s but' % ', '.join(client_supported_transports), - 'server supports %s' % ', '.join(server_supported_transports), - ])) - - def _yield_text_from_framed_data(framed_data, parse=lambda x: x): parts = [parse(x) for x in framed_data.split(BOUNDARY)] for text_length, text in izip(parts[1::2], parts[2::2]): From c35316f1582de2c25b9e3c0ec99ba356b444806a Mon Sep 17 00:00:00 2001 From: Krenair Date: Sun, 11 May 2014 15:45:40 +0100 Subject: [PATCH 066/191] Run under Python 3 This will make the code I wrote in : http://lists.wikimedia.org/pipermail/wikitech-l/2014-May/076373.html actually work in modern versions of Python. --- socketIO_client/__init__.py | 9 ++++++--- socketIO_client/transports.py | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3bdecf1..2e56665 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -3,7 +3,10 @@ import json import requests import time from collections import namedtuple -from urlparse import urlparse +try: + from urlparse import urlparse +except: + from urllib.parse import urlparse from .exceptions import ConnectionError, TimeoutError, PacketError from .transports import _get_response, _negotiate_transport, TRANSPORTS @@ -254,13 +257,13 @@ class SocketIO(object): # Initialize heartbeat_pacemaker self.heartbeat_pacemaker = self._make_heartbeat_pacemaker( heartbeat_interval=socketIO_session.heartbeat_timeout / 2) - self.heartbeat_pacemaker.next() + next(self.heartbeat_pacemaker) # Negotiate transport transport = _negotiate_transport( self.client_supported_transports, socketIO_session, self.is_secure, self.base_url, **self.kw) # Update namespaces - for path, namespace in self._namespace_by_path.iteritems(): + for path, namespace in self._namespace_by_path.items(): namespace._transport = transport transport.connect(path) return transport diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index ed4e59c..e43e644 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -6,7 +6,10 @@ import six import socket import time import websocket -from itertools import izip +try: + from itertools import izip +except: + izip = zip from .exceptions import SocketIOError, ConnectionError, TimeoutError From 3d12aa8a28972473fd1ccc64a4c424a58ee79b35 Mon Sep 17 00:00:00 2001 From: Krenair Date: Sun, 11 May 2014 16:32:29 +0100 Subject: [PATCH 067/191] Check for ImportError rather than any exception --- socketIO_client/__init__.py | 2 +- socketIO_client/transports.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 2e56665..82c5e51 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -5,7 +5,7 @@ import time from collections import namedtuple try: from urlparse import urlparse -except: +except ImportError: from urllib.parse import urlparse from .exceptions import ConnectionError, TimeoutError, PacketError diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index e43e644..c522666 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -8,7 +8,7 @@ import time import websocket try: from itertools import izip -except: +except ImportError: izip = zip from .exceptions import SocketIOError, ConnectionError, TimeoutError From 20b15872ee00bf3656f34f565f7f1ac756a3ce61 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Thu, 26 Jun 2014 14:42:25 +0200 Subject: [PATCH 068/191] Fixed logging.WARNING typo --- socketIO_client/transports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 372a11c..46a6736 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -261,7 +261,7 @@ class _JSONP_PollingTransport(_AbstractTransport): self._id, response_text = self.RESPONSE_PATTERN.match( response_text).groups() except AttributeError: - self._log(self.WARNING, '[packet error] %s', response_text) + self._log(logging.WARNING, '[packet error] %s', response_text) return if not response_text.startswith(BOUNDARY): yield response_text.decode('unicode_escape') From 9285f3da029e413018d65b90ac9cb12b6c18b6de Mon Sep 17 00:00:00 2001 From: Patrick Huber Date: Mon, 28 Jul 2014 09:06:51 +0200 Subject: [PATCH 069/191] allow to pass in a different resource than just 'socket.io' --- README.rst | 9 +++++---- socketIO_client/__init__.py | 13 ++++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 64dc5cd..cd5bb11 100644 --- a/README.rst +++ b/README.rst @@ -121,7 +121,8 @@ Specify params, headers, cookies, proxies thanks to the `requests`_ library. :: from socketIO_client import SocketIO from base64 import b64encode - SocketIO('localhost', 8000, + SocketIO('localhost', 8000, + resource='my.io', params={'q': 'qqq'}, headers={'Authorization': 'Basic ' + b64encode('username:password')}, cookies={'a': 'aaa'}, @@ -149,7 +150,7 @@ Credits - `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_, `Rui Chicoria`_, `Travis Odom`_ submitted code to expand support of the socket.io protocol. - `Bernard Pratz`_ and `Francis Bull`_ wrote prototypes to support xhr-polling and jsonp-polling. - `Eric Chen`_, `Denis Zinevich`_, `Thiago Hersan`_, `Nayef Copty`_ suggested ways to make the connection more robust. - + .. _socket.io: http://socket.io .. _requests: http://python-requests.org @@ -175,8 +176,8 @@ Credits .. _Lucas Klein: https://github.com/lukashed .. _Rui Chicoria: https://github.com/rchicoria .. _Travis Odom: https://github.com/burstaholic - .. _Eric Chen: https://github.com/taiyangc -.. _Denis Zinevich: https://github.com/dzinevich +.. _Denis Zinevich: https://github.com/dzinevich .. _Thiago Hersan: https://github.com/thiagohersan .. _Nayef Copty: https://github.com/nayefc +.. _Patrick Huber: https://github.com/stackmagic diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3bdecf1..00ca9c3 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -20,6 +20,7 @@ RETRY_INTERVAL_IN_SECONDS = 1 class BaseNamespace(object): + 'Define client behavior' def __init__(self, _transport, path): @@ -109,6 +110,7 @@ class BaseNamespace(object): class SocketIO(object): + """Create a socket.io client that connects to a socket.io server at the specified host and port. @@ -119,6 +121,7 @@ class SocketIO(object): - Pass query params, headers, cookies, proxies as keyword arguments. SocketIO('localhost', 8000, + resource='my.io', params={'q': 'qqq'}, headers={'Authorization': 'Basic ' + b64encode('username:password')}, cookies={'a': 'aaa'}, @@ -127,8 +130,8 @@ class SocketIO(object): def __init__( self, host, port=None, Namespace=BaseNamespace, - wait_for_connection=True, transports=TRANSPORTS, **kw): - self.is_secure, self.base_url = _parse_host(host, port) + wait_for_connection=True, transports=TRANSPORTS, resource='socket.io', **kw): + self.is_secure, self.base_url = _parse_host(host, port, resource) self.wait_for_connection = wait_for_connection self._namespace_by_path = {} self.client_supported_transports = transports @@ -361,14 +364,14 @@ def find_callback(args, kw=None): return None, args -def _parse_host(host, port): +def _parse_host(host, port, resource): if not host.startswith('http'): host = 'http://' + host url_pack = urlparse(host) is_secure = url_pack.scheme == 'https' port = port or url_pack.port or (443 if is_secure else 80) - base_url = '%s:%d%s/socket.io/%s' % ( - url_pack.hostname, port, url_pack.path, PROTOCOL_VERSION) + base_url = '%s:%d%s/%s/%s' % ( + url_pack.hostname, port, url_pack.path, resource, PROTOCOL_VERSION) return is_secure, base_url From 557edb862028fcbec1a61d053d38812e4d85a1dc Mon Sep 17 00:00:00 2001 From: jorgen Date: Mon, 15 Sep 2014 21:39:11 +0200 Subject: [PATCH 070/191] SSL Timeout error --- setup.py | 2 +- socketIO_client/transports.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 639cb7d..76b5152 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( name='socketIO-client', - version='0.5.3', + version='0.5.3b', description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index ed4e59c..a49c0d9 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -151,7 +151,10 @@ class _WebsocketTransport(_AbstractTransport): except websocket.WebSocketTimeoutException as e: raise TimeoutError(e) except websocket.SSLError as e: - raise ConnectionError(e) + if e.message == "The read operation timed out": + raise TimeoutError(e) + else: + raise ConnectionError(e) except websocket.WebSocketConnectionClosedException as e: raise ConnectionError('connection closed (%s)' % e) except socket.error as e: From 9a80988e263e7c46b2f06b76ff3a5ac14a74ee6b Mon Sep 17 00:00:00 2001 From: antonzy Date: Mon, 22 Sep 2014 17:08:44 +0300 Subject: [PATCH 071/191] Migrate to 3.4 --- socketIO_client/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3bdecf1..f785919 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -3,7 +3,7 @@ import json import requests import time from collections import namedtuple -from urlparse import urlparse +from urllib import parse from .exceptions import ConnectionError, TimeoutError, PacketError from .transports import _get_response, _negotiate_transport, TRANSPORTS @@ -254,13 +254,13 @@ class SocketIO(object): # Initialize heartbeat_pacemaker self.heartbeat_pacemaker = self._make_heartbeat_pacemaker( heartbeat_interval=socketIO_session.heartbeat_timeout / 2) - self.heartbeat_pacemaker.next() + next(self.heartbeat_pacemaker) # Negotiate transport transport = _negotiate_transport( self.client_supported_transports, socketIO_session, self.is_secure, self.base_url, **self.kw) # Update namespaces - for path, namespace in self._namespace_by_path.iteritems(): + for path, namespace in self._namespace_by_path.items(): namespace._transport = transport transport.connect(path) return transport @@ -364,7 +364,7 @@ def find_callback(args, kw=None): def _parse_host(host, port): if not host.startswith('http'): host = 'http://' + host - url_pack = urlparse(host) + url_pack = parse.urlparse(host) is_secure = url_pack.scheme == 'https' port = port or url_pack.port or (443 if is_secure else 80) base_url = '%s:%d%s/socket.io/%s' % ( From e108c8a12a07b052de30298c967ebf077c61eba9 Mon Sep 17 00:00:00 2001 From: antonzy Date: Mon, 22 Sep 2014 17:12:08 +0300 Subject: [PATCH 072/191] Migrate to 3.4 --- socketIO_client/transports.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index ed4e59c..5701e92 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -6,7 +6,6 @@ import six import socket import time import websocket -from itertools import izip from .exceptions import SocketIOError, ConnectionError, TimeoutError @@ -295,7 +294,7 @@ def _negotiate_transport( def _yield_text_from_framed_data(framed_data, parse=lambda x: x): parts = [parse(x) for x in framed_data.split(BOUNDARY)] - for text_length, text in izip(parts[1::2], parts[2::2]): + for text_length, text in zip(parts[1::2], parts[2::2]): if text_length != str(len(text)): warning = 'invalid declared length=%s for packet_text=%s' % ( text_length, text) From b0d67b98af947b6023605a71b24ccb8dc0298aac Mon Sep 17 00:00:00 2001 From: jorgen Date: Tue, 23 Sep 2014 06:27:27 +0200 Subject: [PATCH 073/191] Version bump --- setup.py | 2 +- socketIO_client/__init__.py | 35 ++++++++++++++++------------------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/setup.py b/setup.py index 76b5152..b42b4a3 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( name='socketIO-client', - version='0.5.3b', + version='0.5.3.2', description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3bdecf1..bd0df20 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -167,26 +167,23 @@ class SocketIO(object): - Omit seconds, i.e. call wait() without arguments, to wait forever. """ - try: - warning_screen = _yield_warning_screen(seconds) - for elapsed_time in warning_screen: + warning_screen = _yield_warning_screen(seconds) + for elapsed_time in warning_screen: + try: try: - try: - self._process_events() - except TimeoutError: - pass - if self._stop_waiting(for_callbacks): - break - self.heartbeat_pacemaker.send(elapsed_time) - except ConnectionError as e: - try: - warning = Exception('[connection error] %s' % e) - warning_screen.throw(warning) - except StopIteration: - _log.warn(warning) - self.disconnect() - except KeyboardInterrupt: - pass + self._process_events() + except TimeoutError: + pass + if self._stop_waiting(for_callbacks): + break + self.heartbeat_pacemaker.send(elapsed_time) + except ConnectionError as e: + try: + warning = Exception('[connection error] %s' % e) + warning_screen.throw(warning) + except StopIteration: + _log.warn(warning) + self.disconnect() def _process_events(self): for packet in self._transport.recv_packet(): From 72541725451a4153090cf55f98f2bd1e7f298a9a Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 17 Nov 2014 18:43:25 -0500 Subject: [PATCH 074/191] Save experiments --- experiments/app.js | 16 ++++++++++ experiments/client0.py | 16 ++++++++++ experiments/client1.py | 52 +++++++++++++++++++++++++++++++++ experiments/index.html | 8 +++++ experiments/interpretation.log | Bin 0 -> 3414 bytes experiments/proxy.js | 29 ++++++++++++++++++ experiments/proxy.log | Bin 0 -> 4311 bytes 7 files changed, 121 insertions(+) create mode 100644 experiments/app.js create mode 100644 experiments/client0.py create mode 100644 experiments/client1.py create mode 100644 experiments/index.html create mode 100644 experiments/interpretation.log create mode 100644 experiments/proxy.js create mode 100644 experiments/proxy.log diff --git a/experiments/app.js b/experiments/app.js new file mode 100644 index 0000000..8beb129 --- /dev/null +++ b/experiments/app.js @@ -0,0 +1,16 @@ +var app = require('express')(); +var server = require('http').Server(app); +var io = require('socket.io')(server); + +server.listen(9000); + +app.get('/', function (req, res) { + res.sendFile(__dirname + '/index.html'); +}); + +io.on('connection', function (socket) { + socket.emit('news', { hello: 'world' }); + socket.on('my other event', function (data) { + console.log(data); + }); +}); diff --git a/experiments/client0.py b/experiments/client0.py new file mode 100644 index 0000000..fe849a5 --- /dev/null +++ b/experiments/client0.py @@ -0,0 +1,16 @@ +class SocketIO(object): + + def __init__(self, host, port): + pass + + def on(self, event, callback): + pass + + +def on_news(self, data): + print(data) + self.emit('my other event', {'my': 'data'}) + + +s = SocketIO('localhost', 9000) +s.on('news', on_news) diff --git a/experiments/client1.py b/experiments/client1.py new file mode 100644 index 0000000..c2c6290 --- /dev/null +++ b/experiments/client1.py @@ -0,0 +1,52 @@ +import requests + + +def get_packets(content): + packets = [] + index = 0 + content_length = len(content) + while index < content_length: + index, packet_length = read_packet_length(content, index) + index, packet = read_packet(content, index, packet_length) + packets.append((packet[0], packet[1:])) + return packets + + +def read_packet_length(content, index): + while ord(content[index]) != 0: + index += 1 + index += 1 + packet_length_string = '' + while ord(content[index]) != 255: + packet_length_string += str(ord(content[index])) + index += 1 + return index, int(packet_length_string) + + +def read_packet(content, index, packet_length): + while ord(content[index]) == 255: + index += 1 + packet = content[index:index + packet_length] + return index + packet_length, packet + + +base_url = 'http://localhost:9000' +session = requests.Session() +# Establish engine.io connection +response = session.get( + base_url + '/socket.io/?EIO=3&transport=polling&t=1416156610842-0') +packets = get_packets(response.content) +for packet_type, packet in packets: + print packet_type, packet +packet_type, packet = packets[0] +import json +packet_json = json.loads(packet) +print packet_json +print packet_json['pingInterval'] +print packet_json['pingTimeout'] +print packet_json['sid'] +# Establish socket.io connection +# Receive socket.io event +# Send socket.io event +# Send socket.io ping +# Receive socket.io pong diff --git a/experiments/index.html b/experiments/index.html new file mode 100644 index 0000000..90f066c --- /dev/null +++ b/experiments/index.html @@ -0,0 +1,8 @@ + + diff --git a/experiments/interpretation.log b/experiments/interpretation.log new file mode 100644 index 0000000000000000000000000000000000000000..2afb8e24769945acbdaacf98e77d1814320474d3 GIT binary patch literal 3414 zcmd5;+fExX5alKT`3lPq2qXgb-m-}lRa8wSUYeFmFN*T8>{`i+&3f52K@sB9s(w;` zp=0j`Ok<@=Qc543XvW_0`1qU|_s%+>E<5KJeRN##@S2IEm><9CoOavxp2&l=$apT= z8BdZp9qfrVb*M*O&!fb5tU9T&bP%TuK0;Z}1rK?G#w;&lo}xCgwdu!T;IG2zv+;-( zA{b>b`)mv2nvmb7g5|K5)Ou%~^N-yR=bfXs-FM&mXl-}<=a28?9v5+hTe$l*Ity-S zc=@Gp$M(hNlarJ8_z>S_gFJ{>fm=O%$F9nwIzKrqbSp(JbdJX>Y@5l(sExNDqEJ*R}1^>qsww;rOM)m5zHD z)Y2gwGDtZ`fKx6|I1KV2oT8vzLm+PvCEyW52+?e45LOXb8a?~I%;oX^QC7wflZ-(G zg7!n6rYsZ(wV5@L=Sw3kuXtIGdTyQW0TbFL9%#V41bIDI+dw=L{E56&gR@|T5=|Td z;>KiWh`s?ta>&}oj{LWJIAwPQKD@_6mLwdgf5-DA!VmqDQ%0(8z-CrCuc$ghO+{D- zR=L$OGgmbUc#^wX+=jGwU{u`pr9HDxQ+oA%sLIy@uS?1VsS;NYJWZ+ryOb!b*#Kru z%;>7pgoE~>U=_iD88l(g%~PD^^7a2k!WP;-~5p8Z&!W>Ny7elDr3SBAD(FF7v1tOR)&rKkE)gzQVx1>l)HO%QGx{OpO(N(16 z`FBKQJO2SG|A6i9MU!7^)sO?6l+voa$O?2VJw&HP$VVfP1o+|8m5A=w?N=kZUOr4j zw-+M%(lFSr@A-A-RbWtiYlJny;4B2g^jR|1`#Q+I>XOE0yDrI9l5I}2t;D)w!!UHn v#Tp#+0{Ij^kazw3eJzh!6{Qu9X#*IpA$aD-^!~aeO%o literal 0 HcmV?d00001 diff --git a/experiments/proxy.js b/experiments/proxy.js new file mode 100644 index 0000000..4dec919 --- /dev/null +++ b/experiments/proxy.js @@ -0,0 +1,29 @@ +var proxy = require('http-proxy').createProxyServer({ + target: {host: 'localhost', port: 9000} +}); +var server = require('http').createServer(function(req, res) { + console.log('[REQUEST] ' + req.url); + if (req.method == 'POST') { + var body = ''; + req.on('data', function (data) { + body += data; + }); + req.on('end', function () { + print_body('[REQUEST.BODY] ', body); + }); + } + var _write = res.write; + res.write = function(data) { + print_body('[RESPONSE.BODY] ', data); + _write.call(res, data); + } + proxy.web(req, res); +}); +function print_body(header, body) { + var text = String(body); + console.log(header + text); + for (var i = 0; i < text.length; i++) { + console.log('body[%s] = %s = %s', i, text[i], text.charCodeAt(i)); + } +} +server.listen(8000); diff --git a/experiments/proxy.log b/experiments/proxy.log new file mode 100644 index 0000000000000000000000000000000000000000..45d08726609b2eb8b35e47b7493851c07f711015 GIT binary patch literal 4311 zcmcJS-EP}B5QWpUK!2XX&_%A|M3h8|GGHvSXbR+Jy_=u53&R&qEF|7ImTM~sf*_B! z=p*$NmOL|RIhE5E2=+p4G#HT|59e^`;o<&YC;Laoqt5P|;kUn6)!AiP?@X)RuYc|Q zvs$0=+y23zpKjI5@qD$YmUVwo&1Td2`BvRmnKDWnqhgyS-PnJB^ylD@qy3%V2XFr# zbv9nV{POwV_&!=qCs8jNe4HGPKdG~m4=a6>9>3q)+j|#nN7sw<<#%1#IO)S)(GT&-`ai1k@)kVtK6zs6Y4!51oKoNR$9uyClt%CBVLM5v6z~ z39wV2qzF3!_OTzE)lY!^-bW1QfO^kG3=^fG77}>|{5<{^{SzGT6=pkqZ z^swAApby4;O3zE;i2qT=?4nd=3CE9|Ll+#RlXDE-PmO z1915rz{Oe&$i<3HyIOLv3j^hV4+|&<`^FOYx$mcmZ_d73b5S0C}ig(NerH!1jQmt$CnoIFABv=n(4X;c@y{KNmGi4PBUB zfms~73fSU2rIsH0hFW^)E$ZmvK9c}E{47BcU-Wn6gzn5=2HpxdooSOh0^9;iVYNdO0E$HsDjN!g+6K< zQ4C}oF(JT3Ws_}0BPHL6Eb@)8E*VFV(q%Z*g?tm5l58VQ3Bko@P+YVmx(o|JAM;J< zEwW8GHn~Rn0j>#e4rCg!I5Le$YH^ciq>Ui?Hyeo`jn!pv{L4DTe+IoEN~uWfbDyixcU&03Sf1MJDxPj5duTmA{7^G~nu QNB^Jq$Vq8cS3fT0f8?ro>i_@% literal 0 HcmV?d00001 From 64d1c010a923b00259318f36354cd19b97556768 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Tue, 18 Nov 2014 10:42:57 -0500 Subject: [PATCH 075/191] Update __init__.py --- socketIO_client/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index f785919..261acca 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -3,7 +3,10 @@ import json import requests import time from collections import namedtuple -from urllib import parse +try: + from urllib import parse as parse_url +except ImportError: + from urlparse import urlparse as parse_url from .exceptions import ConnectionError, TimeoutError, PacketError from .transports import _get_response, _negotiate_transport, TRANSPORTS @@ -364,7 +367,7 @@ def find_callback(args, kw=None): def _parse_host(host, port): if not host.startswith('http'): host = 'http://' + host - url_pack = parse.urlparse(host) + url_pack = parse_url(host) is_secure = url_pack.scheme == 'https' port = port or url_pack.port or (443 if is_secure else 80) base_url = '%s:%d%s/socket.io/%s' % ( From 895d2447fdf9a6bb081f7ea153be64176af44ea7 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Tue, 18 Nov 2014 10:45:22 -0500 Subject: [PATCH 076/191] Experiment with socket.io 1.2.0 --- experiments/client1.py | 41 +++++++++++++++++++++++++++++---- experiments/interpretation.log | Bin 3414 -> 3436 bytes 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/experiments/client1.py b/experiments/client1.py index c2c6290..e88abf8 100644 --- a/experiments/client1.py +++ b/experiments/client1.py @@ -1,4 +1,6 @@ +import json import requests +import time def get_packets(content): @@ -8,7 +10,9 @@ def get_packets(content): while index < content_length: index, packet_length = read_packet_length(content, index) index, packet = read_packet(content, index, packet_length) - packets.append((packet[0], packet[1:])) + packet_type = int(packet[0]) + packet_payload = packet[1:] + packets.append((packet_type, packet_payload)) return packets @@ -30,23 +34,52 @@ def read_packet(content, index, packet_length): return index + packet_length, packet +request_counter = 0 + + +def get_timestamp(): + global request_counter + timestamp = '%s-%s' % (int(time.time() * 1000), request_counter) + request_counter += 1 + return timestamp + + base_url = 'http://localhost:9000' session = requests.Session() # Establish engine.io connection -response = session.get( - base_url + '/socket.io/?EIO=3&transport=polling&t=1416156610842-0') +url = base_url + '/socket.io/' +response = session.get(url, params={ + 'EIO': 3, + 'transport': 'polling', + 't': get_timestamp(), +}) +print response.url + + packets = get_packets(response.content) for packet_type, packet in packets: print packet_type, packet packet_type, packet = packets[0] -import json packet_json = json.loads(packet) print packet_json print packet_json['pingInterval'] print packet_json['pingTimeout'] print packet_json['sid'] +assert packet_type == 0 + + # Establish socket.io connection +session.get + + +# [REQUEST] /socket.io/?EIO=3&transport=polling&t=1416156610865-1&sid=OXdRaq1cUWs5v3TVAAAF # Receive socket.io event + + # Send socket.io event +# [REQUEST] /socket.io/?EIO=3&transport=polling&t=1416156610887-2&sid=OXdRaq1cUWs5v3TVAAAF + + # Send socket.io ping +# [REQUEST] /socket.io/?EIO=3&transport=polling&t=1416156635868-4&sid=OXdRaq1cUWs5v3TVAAAF # Receive socket.io pong diff --git a/experiments/interpretation.log b/experiments/interpretation.log index 2afb8e24769945acbdaacf98e77d1814320474d3..ea51ac8d4150cd24d3bb94a58513ace66b5687e6 100644 GIT binary patch delta 67 zcmca6^+t+2I>p~`GD0}mLraWe5y#W8s-fXX7qNbCxI4^?* It+}L`0ma%A>Hq)$ delta 50 zcmaDObxn#bI>5~j()Tn3w;FcmU`8C`5U!6K8(Ij?UH=8|Lv0K2{r A-T(jq From c3428e0b68128b54444c6cf99a3ac24895fba15e Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Tue, 25 Nov 2014 06:01:46 -0500 Subject: [PATCH 077/191] Working through parse error on send --- experiments/client1.py | 145 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 133 insertions(+), 12 deletions(-) diff --git a/experiments/client1.py b/experiments/client1.py index e88abf8..55fd870 100644 --- a/experiments/client1.py +++ b/experiments/client1.py @@ -3,6 +3,8 @@ import requests import time +# def unwrap_payload +# def decode_payload(payload) def get_packets(content): packets = [] index = 0 @@ -44,9 +46,10 @@ def get_timestamp(): return timestamp +print '*** Connect' base_url = 'http://localhost:9000' session = requests.Session() -# Establish engine.io connection +print session.cookies.items() url = base_url + '/socket.io/' response = session.get(url, params={ 'EIO': 3, @@ -54,8 +57,7 @@ response = session.get(url, params={ 't': get_timestamp(), }) print response.url - - +print session.cookies.items() packets = get_packets(response.content) for packet_type, packet in packets: print packet_type, packet @@ -68,18 +70,137 @@ print packet_json['sid'] assert packet_type == 0 -# Establish socket.io connection -session.get +"" +base_url = 'http://localhost:9000' +url = base_url + '/socket.io/' +response = session.get(url, params={ + 'EIO': 3, + 'transport': 'polling', + 't': get_timestamp(), + 'sid': packet_json['sid'], +}) +print response.url +print session.cookies.items() +packets = get_packets(response.content) +for packet_type, packet in packets: + print 'engineIO_packet_type = %s' % packet_type + print 'socketIO_packet_type = %s' % packet[0] + print 'packet = %s' % packet[1:] +# from IPython import embed; embed() -# [REQUEST] /socket.io/?EIO=3&transport=polling&t=1416156610865-1&sid=OXdRaq1cUWs5v3TVAAAF -# Receive socket.io event +# def wrap_payload +def encode_payload(packs): + parts = [] + for packet_type, packet in packs: + content = str(packet_type) + str(packet) + parts.append(make_header(content) + content) + return ''.join(parts) -# Send socket.io event -# [REQUEST] /socket.io/?EIO=3&transport=polling&t=1416156610887-2&sid=OXdRaq1cUWs5v3TVAAAF +def make_header(content): + length_string = str(len(content)) + print length_string + header_digits = [0] + for index in xrange(len(length_string)): + header_digits.append(ord(length_string[index]) - 48) + header_digits.append(255) + return ''.join(chr(x) for x in header_digits) -# Send socket.io ping -# [REQUEST] /socket.io/?EIO=3&transport=polling&t=1416156635868-4&sid=OXdRaq1cUWs5v3TVAAAF -# Receive socket.io pong +# print '***' +# base_url = 'http://localhost:9000' +# url = base_url + '/socket.io/' +# response = session.get(url, params={ + # 'EIO': 3, + # 'transport': 'polling', + # 't': get_timestamp(), + # 'sid': packet_json['sid'], +# }) +# print response.url +# print response.content +# packets = get_packets(response.content) +# for packet_type, packet in packets: + # print packet_type, packet + + +print '*** Send event' +packets = [ + (4, '2["my other event",{"my":"data"}]'), +] +payload = encode_payload(packets) +print payload +base_url = 'http://localhost:9000' +url = base_url + '/socket.io/' +print session.cookies.items() +response = session.post(url, params={ + 'EIO': 3, + 'transport': 'polling', + 't': get_timestamp(), + 'sid': packet_json['sid'], +}, data=payload) +print response.url +print response.content + +from time import sleep +sleep(10) + + +print '*** Send event' +packets = [ + (4, '2["my other event",{"my":"data"}]'), +] +payload = encode_payload(packets) +print payload +base_url = 'http://localhost:9000' +url = base_url + '/socket.io/' +print session.cookies.items() +response = session.post(url, params={ + 'EIO': 3, + 'transport': 'polling', + 't': get_timestamp(), + 'sid': packet_json['sid'], +}, data=payload) +print response.url +print response.content + + +""" +print '*** Send ping' +packets = [ + (2, ''), +] +payload = encode_payload(packets) +print payload +base_url = 'http://localhost:9000' +url = base_url + '/socket.io/' +response = session.post(url, params={ + 'EIO': 3, + 'transport': 'polling', + 't': get_timestamp(), + 'sid': packet_json['sid'], +}, data=payload) +print response.url +print response.content +# packets = get_packets(response.content) +# for packet_type, packet in packets: + # print packet_type, packet + + +print '*** Send ping' +packets = [ + (2, ''), +] +payload = encode_payload(packets) +print payload +base_url = 'http://localhost:9000' +url = base_url + '/socket.io/' +response = session.post(url, params={ + 'EIO': 3, + 'transport': 'polling', + 't': get_timestamp(), + 'sid': packet_json['sid'], +}, data=payload) +print response.url +print response.content +""" From a0b061cbdca09fe153264163d27632e34ce9160c Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 27 Nov 2014 02:14:51 -0500 Subject: [PATCH 078/191] Use single quotes --- socketIO_client/transports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index d971ae2..172ca15 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -150,7 +150,7 @@ class _WebsocketTransport(_AbstractTransport): except websocket.WebSocketTimeoutException as e: raise TimeoutError(e) except websocket.SSLError as e: - if e.message == "The read operation timed out": + if 'timed out' in e.message: raise TimeoutError(e) else: raise ConnectionError(e) From cbdf009d46e53ed8de046a30f08896752bd63c5e Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 30 Nov 2014 05:17:22 -0500 Subject: [PATCH 079/191] Add content-type --- experiments/app.js | 1 + experiments/client1.py | 31 +++++++++++++++++++------------ experiments/proxy.js | 3 ++- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/experiments/app.js b/experiments/app.js index 8beb129..4df4754 100644 --- a/experiments/app.js +++ b/experiments/app.js @@ -1,3 +1,4 @@ +// DEBUG=* node app.js var app = require('express')(); var server = require('http').Server(app); var io = require('socket.io')(server); diff --git a/experiments/client1.py b/experiments/client1.py index 55fd870..0a270d4 100644 --- a/experiments/client1.py +++ b/experiments/client1.py @@ -46,8 +46,10 @@ def get_timestamp(): return timestamp +base_url = 'http://localhost:8000' + + print '*** Connect' -base_url = 'http://localhost:9000' session = requests.Session() print session.cookies.items() url = base_url + '/socket.io/' @@ -70,8 +72,6 @@ print packet_json['sid'] assert packet_type == 0 -"" -base_url = 'http://localhost:9000' url = base_url + '/socket.io/' response = session.get(url, params={ 'EIO': 3, @@ -105,11 +105,14 @@ def make_header(content): for index in xrange(len(length_string)): header_digits.append(ord(length_string[index]) - 48) header_digits.append(255) + print '---' + for x in header_digits: + print str(x) + print '---' return ''.join(chr(x) for x in header_digits) # print '***' -# base_url = 'http://localhost:9000' # url = base_url + '/socket.io/' # response = session.get(url, params={ # 'EIO': 3, @@ -130,7 +133,6 @@ packets = [ ] payload = encode_payload(packets) print payload -base_url = 'http://localhost:9000' url = base_url + '/socket.io/' print session.cookies.items() response = session.post(url, params={ @@ -138,7 +140,9 @@ response = session.post(url, params={ 'transport': 'polling', 't': get_timestamp(), 'sid': packet_json['sid'], -}, data=payload) +}, data=payload, headers={ + 'content-type': 'application/octet-stream', +}) print response.url print response.content @@ -152,7 +156,6 @@ packets = [ ] payload = encode_payload(packets) print payload -base_url = 'http://localhost:9000' url = base_url + '/socket.io/' print session.cookies.items() response = session.post(url, params={ @@ -160,7 +163,9 @@ response = session.post(url, params={ 'transport': 'polling', 't': get_timestamp(), 'sid': packet_json['sid'], -}, data=payload) +}, data=payload, headers={ + 'content-type': 'application/octet-stream', +}) print response.url print response.content @@ -172,14 +177,15 @@ packets = [ ] payload = encode_payload(packets) print payload -base_url = 'http://localhost:9000' url = base_url + '/socket.io/' response = session.post(url, params={ 'EIO': 3, 'transport': 'polling', 't': get_timestamp(), 'sid': packet_json['sid'], -}, data=payload) +}, data=payload, headers={ + 'content-type': 'application/octet-stream', +}) print response.url print response.content # packets = get_packets(response.content) @@ -193,14 +199,15 @@ packets = [ ] payload = encode_payload(packets) print payload -base_url = 'http://localhost:9000' url = base_url + '/socket.io/' response = session.post(url, params={ 'EIO': 3, 'transport': 'polling', 't': get_timestamp(), 'sid': packet_json['sid'], -}, data=payload) +}, data=payload, headers={ + 'content-type': 'application/octet-stream', +}) print response.url print response.content """ diff --git a/experiments/proxy.js b/experiments/proxy.js index 4dec919..756da12 100644 --- a/experiments/proxy.js +++ b/experiments/proxy.js @@ -2,7 +2,8 @@ var proxy = require('http-proxy').createProxyServer({ target: {host: 'localhost', port: 9000} }); var server = require('http').createServer(function(req, res) { - console.log('[REQUEST] ' + req.url); + console.log('[REQUEST.%s] %s', req.method, req.url); + console.log(req['headers']); if (req.method == 'POST') { var body = ''; req.on('data', function (data) { From eda747ec4e084567ab3c48951a32f40b326569ec Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 30 Nov 2014 05:58:02 -0500 Subject: [PATCH 080/191] Use GET request to get pong --- experiments/client1.py | 28 +++++++++++++------------ experiments/{client0.py => client2.py} | 8 +------ experiments/socketIO_client/__init__.py | 7 +++++++ 3 files changed, 23 insertions(+), 20 deletions(-) rename experiments/{client0.py => client2.py} (54%) create mode 100644 experiments/socketIO_client/__init__.py diff --git a/experiments/client1.py b/experiments/client1.py index 0a270d4..68fdaba 100644 --- a/experiments/client1.py +++ b/experiments/client1.py @@ -46,13 +46,13 @@ def get_timestamp(): return timestamp -base_url = 'http://localhost:8000' +base_url = 'http://localhost:9000' +url = base_url + '/socket.io/' print '*** Connect' session = requests.Session() print session.cookies.items() -url = base_url + '/socket.io/' response = session.get(url, params={ 'EIO': 3, 'transport': 'polling', @@ -72,7 +72,6 @@ print packet_json['sid'] assert packet_type == 0 -url = base_url + '/socket.io/' response = session.get(url, params={ 'EIO': 3, 'transport': 'polling', @@ -113,7 +112,6 @@ def make_header(content): # print '***' -# url = base_url + '/socket.io/' # response = session.get(url, params={ # 'EIO': 3, # 'transport': 'polling', @@ -133,7 +131,6 @@ packets = [ ] payload = encode_payload(packets) print payload -url = base_url + '/socket.io/' print session.cookies.items() response = session.post(url, params={ 'EIO': 3, @@ -156,7 +153,6 @@ packets = [ ] payload = encode_payload(packets) print payload -url = base_url + '/socket.io/' print session.cookies.items() response = session.post(url, params={ 'EIO': 3, @@ -170,14 +166,12 @@ print response.url print response.content -""" print '*** Send ping' packets = [ (2, ''), ] payload = encode_payload(packets) print payload -url = base_url + '/socket.io/' response = session.post(url, params={ 'EIO': 3, 'transport': 'polling', @@ -188,9 +182,6 @@ response = session.post(url, params={ }) print response.url print response.content -# packets = get_packets(response.content) -# for packet_type, packet in packets: - # print packet_type, packet print '*** Send ping' @@ -199,7 +190,6 @@ packets = [ ] payload = encode_payload(packets) print payload -url = base_url + '/socket.io/' response = session.post(url, params={ 'EIO': 3, 'transport': 'polling', @@ -210,4 +200,16 @@ response = session.post(url, params={ }) print response.url print response.content -""" + + +response = session.get(url, params={ + 'EIO': 3, + 'transport': 'polling', + 't': get_timestamp(), + 'sid': packet_json['sid'], +}) +print response.url +print session.cookies.items() +packets = get_packets(response.content) +for packet_type, packet in packets: + print packet_type, packet diff --git a/experiments/client0.py b/experiments/client2.py similarity index 54% rename from experiments/client0.py rename to experiments/client2.py index fe849a5..9b91509 100644 --- a/experiments/client0.py +++ b/experiments/client2.py @@ -1,10 +1,4 @@ -class SocketIO(object): - - def __init__(self, host, port): - pass - - def on(self, event, callback): - pass +from socketIO_client import SocketIO def on_news(self, data): diff --git a/experiments/socketIO_client/__init__.py b/experiments/socketIO_client/__init__.py new file mode 100644 index 0000000..42266b7 --- /dev/null +++ b/experiments/socketIO_client/__init__.py @@ -0,0 +1,7 @@ +class SocketIO(object): + + def __init__(self, host, port): + pass + + def on(self, event, callback): + pass From 37bd6786d735247f28b794594aa79a88cab1a701 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Sat, 20 Dec 2014 16:28:22 -0800 Subject: [PATCH 081/191] Checkpoint --- socketIO_client/__init__.py | 27 ++++++++++++++++------- socketIO_client/parser.py | 41 +++++++++++++++++++++++++++++++++++ socketIO_client/transports.py | 5 ++++- 3 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 socketIO_client/parser.py diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index e966e3a..e4d5fa1 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,8 +1,10 @@ +from collections import namedtuple import logging import json +import parser import requests import time -from collections import namedtuple + try: from urllib import parse as parse_url except ImportError: @@ -367,8 +369,7 @@ def _parse_host(host, port): url_pack = parse_url(host) is_secure = url_pack.scheme == 'https' port = port or url_pack.port or (443 if is_secure else 80) - base_url = '%s:%d%s/socket.io/%s' % ( - url_pack.hostname, port, url_pack.path, PROTOCOL_VERSION) + base_url = '%s:%d%s/socket.io/%s' % (url_pack.hostname, port, url_pack.path, PROTOCOL_VERSION) return is_secure, base_url @@ -395,13 +396,23 @@ def _yield_elapsed_time(seconds=None): def _get_socketIO_session(is_secure, base_url, **kw): - server_url = '%s://%s/' % ('https' if is_secure else 'http', base_url) + server_url = '%s://%s/?transport=polling' % ('https' if is_secure else 'http', base_url) + _log.debug('[session] %s', server_url) try: response = _get_response(requests.get, server_url, **kw) except TimeoutError as e: raise ConnectionError(e) - response_parts = response.text.split(':') + + _log.debug("[response] %s", response.text); + decoded = parser.decode_response(response); + _log.debug("[decoded] %s", repr(decoded)); + return _SocketIOSession( - id=response_parts[0], - heartbeat_timeout=int(response_parts[1]), - server_supported_transports=response_parts[3].split(',')) + id = decoded["payload"]["sid"], + heartbeat_timeout = int(decoded["payload"]["pingInterval"]), + server_supported_transports = ["xhr-polling"]);#decoded["payload"]["upgrades"]); + + #return _SocketIOSession( + # id=response_parts[0], + # heartbeat_timeout=int(response_parts[1]), + # server_supported_transports=response_parts[3].split(',')) diff --git a/socketIO_client/parser.py b/socketIO_client/parser.py new file mode 100644 index 0000000..2231481 --- /dev/null +++ b/socketIO_client/parser.py @@ -0,0 +1,41 @@ +import logging +import json + +_log = logging.getLogger(__name__) + +""" Decodes a response from requests lib. +""" +def decode_response(response): + # TODO(sean): Should we use the 'raw' stream instead? + raw_bytes = response.content; + packet_type = "string" if ord(raw_bytes[0]) == 0 else "binary"; + _log.debug("Packet type: %s" % packet_type); + + if packet_type is "string": + length_bytes = []; + offset = 1; + while ord(raw_bytes[offset]) is not 255: + length_bytes.append(ord(raw_bytes[offset])); + offset += 1; + offset += 1; + + length = 0; + base = 1; + for digit in reversed(length_bytes): + length += (int(digit) * base); + base *= 10; + _log.debug("Packet length: %d" % length); + + message_type = raw_bytes[offset]; + offset += 1; + + message = {"type": message_type, "payload": json.loads(raw_bytes[offset:offset + length - 1])}; + _log.debug("Message: %s" % repr(message)); + return message; + else: + pass; + + return ""; + +def decode_packet(packet): + pass; diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 172ca15..e1a0064 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -1,5 +1,6 @@ import json import logging +import parser import re import requests import six @@ -79,6 +80,7 @@ class _AbstractTransport(object): for packet_text in self.recv(): _log.debug('[packet received] %s', packet_text) try: + #packet = parser.decode_response(packet_text); packet_parts = packet_text.split(':', 3) except AttributeError: _log.warn('[packet error] %s', packet_text) @@ -167,7 +169,7 @@ class _XHR_PollingTransport(_AbstractTransport): def __init__(self, socketIO_session, is_secure, base_url, **kw): super(_XHR_PollingTransport, self).__init__() - self._url = '%s://%s/xhr-polling/%s' % ( + self._url = '%s://%s/?transport=polling&sid=%s' % ( 'https' if is_secure else 'http', base_url, socketIO_session.id) self._connected = True @@ -198,6 +200,7 @@ class _XHR_PollingTransport(_AbstractTransport): self._url, params=self._params, timeout=TIMEOUT_IN_SECONDS) + #response_text = response.content response_text = response.text if not response_text.startswith(BOUNDARY): yield response_text From 63dea4f7a33f855b74a05589c956f2dae85c594f Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 22 Dec 2014 12:38:40 +0900 Subject: [PATCH 082/191] Wrote basic connect() --- experiments/client1.py | 4 +- experiments/socketIO_client/__init__.py | 80 ++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/experiments/client1.py b/experiments/client1.py index 68fdaba..4880a14 100644 --- a/experiments/client1.py +++ b/experiments/client1.py @@ -3,7 +3,6 @@ import requests import time -# def unwrap_payload # def decode_payload(payload) def get_packets(content): packets = [] @@ -88,6 +87,9 @@ for packet_type, packet in packets: # from IPython import embed; embed() +# def wrap_payload +# def pack_packets +# def pack_packs # def wrap_payload def encode_payload(packs): parts = [] diff --git a/experiments/socketIO_client/__init__.py b/experiments/socketIO_client/__init__.py index 42266b7..cd94394 100644 --- a/experiments/socketIO_client/__init__.py +++ b/experiments/socketIO_client/__init__.py @@ -1,7 +1,83 @@ -class SocketIO(object): +import json +import requests +import time + + +class EngineIO(object): + + _path = 'engine.io' + _engine_io_protocol = 3 + _request_index = 0 def __init__(self, host, port): - pass + url = 'http://%s:%s/%s/' % (host, port, self._path) + session = requests.Session() + response = session.get(url, params={ + 'EIO': self._engine_io_protocol, + 'transport': 'polling', + 't': self._get_timestamp(), + }) + packs = _decode_content(response.content) + packet_type, packet = packs[0] + assert packet_type == 0 + packet_json = json.loads(packet) + print(packet_json) + response = session.get(url, params={ + 'EIO': self._engine_io_protocol, + 'transport': 'polling', + 't': self._get_timestamp(), + 'sid': packet_json['sid'], + }) + packs = _decode_content(response.content) + for packet_type, packet in packs: + print 'engineIO_packet_type = %s' % packet_type + print 'socketIO_packet_type = %s' % packet[0] + print 'packet = %s' % packet[1:] + + def _get_timestamp(self): + timestamp = '%s-%s' % (int(time.time() * 1000), self._request_index) + self._request_index += 1 + return timestamp + + +class SocketIO(EngineIO): + + _path = 'socket.io' + _socket_io_protocol = 4 + + def __init__(self, host, port): + super(SocketIO, self).__init__(host, port) def on(self, event, callback): pass + + +def _decode_content(content): + packs = [] + index = 0 + content_length = len(content) + while index < content_length: + index, packet_length = _read_packet_length(content, index) + index, packet = _read_packet(content, index, packet_length) + packet_type = int(packet[0]) + packet_payload = packet[1:] + packs.append((packet_type, packet_payload)) + return packs + + +def _read_packet_length(content, index): + while ord(content[index]) != 0: + index += 1 + index += 1 + packet_length_string = '' + while ord(content[index]) != 255: + packet_length_string += str(ord(content[index])) + index += 1 + return index, int(packet_length_string) + + +def _read_packet(content, index, packet_length): + while ord(content[index]) == 255: + index += 1 + packet = content[index:index + packet_length] + return index + packet_length, packet From d64e947aac64f0c0b5001feb4294a832d494c22d Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Mon, 22 Dec 2014 00:36:09 -0800 Subject: [PATCH 083/191] Checkpoint for updates. Namespaces are working. Events are working. Reconnects working. Disconnects working. Need to implemenet ACKs / callbacks, WebSocket transport, and JSONP transport --- socketIO_client/__init__.py | 124 ++++++++++++++++++++------- socketIO_client/parser.py | 117 +++++++++++++++++++++---- socketIO_client/transports.py | 155 +++++++++++++++++++++++----------- 3 files changed, 302 insertions(+), 94 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index e4d5fa1..bd3556a 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,6 +1,8 @@ from collections import namedtuple +import copy import logging import json +import multiprocessing import parser import requests import time @@ -16,7 +18,8 @@ from .transports import _get_response, _negotiate_transport, TRANSPORTS _SocketIOSession = namedtuple('_SocketIOSession', [ 'id', - 'heartbeat_timeout', + 'heartbeat_interval', + 'connection_timeout', 'server_supported_transports', ]) _log = logging.getLogger(__name__) @@ -42,7 +45,11 @@ class BaseNamespace(object): def emit(self, event, *args, **kw): callback, args = find_callback(args, kw) - self._transport.emit(self.path, event, args, callback) + + if callback is not None: + _log.warn("Callback was specified but is not supported."); + + self._transport.emit(self.path, event, args, None) def disconnect(self): self._transport.disconnect(self.path) @@ -138,6 +145,16 @@ class SocketIO(object): self._namespace_by_path = {} self.client_supported_transports = transports self.kw = kw + # These two fields work to control the heartbeat thread. + self.heartbeat_terminator = None; + self.heartbeat_thread = None; + # Saved session information. + self.session = None; + # This is stores the set of paths (namespaces) that need to be + # reconnected to. + self.reconnect_paths = {}; + # This sets of a chain of events that attempts to connect to + # the server at the base namespace. self.define(Namespace) def __enter__(self): @@ -145,9 +162,17 @@ class SocketIO(object): def __exit__(self, *exception_pack): self.disconnect() + self._terminate_heartbeat(); def __del__(self): self.disconnect() + self._terminate_heartbeat(); + + def _terminate_heartbeat(self): + if self.heartbeat_terminator is not None: + self.heartbeat_terminator.set(); + #time.sleep(self.session.heartbeat_interval); + self.heartbeat_thread.join(); def define(self, Namespace, path=''): if path: @@ -167,6 +192,19 @@ class SocketIO(object): callback, args = find_callback(args, kw) self._transport.emit(path, event, args, callback) + def reconnect(self): + """Reconnects to a set of namespaces. + + """ + for path in self.reconnect_paths: + # We avoid reconnecting to the default namespace because + # socketIO_client connects to that already. + if (len(self.reconnect_paths) > 1 and path is ''): + continue; + _log.debug("Reconnecting to path: %s" % repr(path)) + self._transport.connect(path); + self.reconnect_paths = {}; + def wait(self, seconds=None, for_callbacks=False): """Wait in a loop and process events as defined in the namespaces. @@ -181,14 +219,28 @@ class SocketIO(object): pass if self._stop_waiting(for_callbacks): break - self.heartbeat_pacemaker.send(elapsed_time) + + # We will end up here in the case that we + # disconnected, then reconnected AND we were + # successful. + if len(self.reconnect_paths) > 0: + self.reconnect(); except ConnectionError as e: try: + # This is where we end up if the connection was + # severed. The client will disconnect here. + if len(self.reconnect_paths) is 0: + self.reconnect_paths = copy.deepcopy(self._namespace_by_path); + + self._terminate_heartbeat(); + warning = Exception('[connection error] %s' % e) + self._transport._connected = False; warning_screen.throw(warning) except StopIteration: _log.warn(warning) self.disconnect() + _log.debug("[wait canceled]"); def _process_events(self): for packet in self._transport.recv_packet(): @@ -249,31 +301,29 @@ class SocketIO(object): return self.__transport def _get_transport(self): - socketIO_session = _get_socketIO_session( - self.is_secure, self.base_url, **self.kw) - _log.debug('[transports available] %s', ' '.join( - socketIO_session.server_supported_transports)) - # Initialize heartbeat_pacemaker - self.heartbeat_pacemaker = self._make_heartbeat_pacemaker( - heartbeat_interval=socketIO_session.heartbeat_timeout / 2) - next(self.heartbeat_pacemaker) + self.session = _get_socketIO_session(self.is_secure, self.base_url, **self.kw) + _log.debug('[transports available] %s', ' '.join(self.session.server_supported_transports)) + # Negotiate transport transport = _negotiate_transport( - self.client_supported_transports, socketIO_session, + self.client_supported_transports, self.session, self.is_secure, self.base_url, **self.kw) # Update namespaces for path, namespace in self._namespace_by_path.items(): namespace._transport = transport transport.connect(path) - return transport + + transport.set_timeout(self.session.connection_timeout); - def _make_heartbeat_pacemaker(self, heartbeat_interval): - heartbeat_time = 0 - while True: - elapsed_time = (yield) - if elapsed_time - heartbeat_time > heartbeat_interval: - heartbeat_time = elapsed_time - self._transport.send_heartbeat() + # Start the heartbeat pacemaker (PING). + _log.debug("[start heartbeat pacemaker]"); + self.heartbeat_terminator = multiprocessing.Event(); + self.heartbeat_thread = multiprocessing.Process( + target = _make_heartbeat_pacemaker, + args = (self.heartbeat_terminator, transport, self.session.heartbeat_interval / 2)); + self.heartbeat_thread.start(); + + return transport def get_namespace(self, path=''): try: @@ -369,7 +419,7 @@ def _parse_host(host, port): url_pack = parse_url(host) is_secure = url_pack.scheme == 'https' port = port or url_pack.port or (443 if is_secure else 80) - base_url = '%s:%d%s/socket.io/%s' % (url_pack.hostname, port, url_pack.path, PROTOCOL_VERSION) + base_url = '%s:%d%s/socket.io' % (url_pack.hostname, port, url_pack.path) return is_secure, base_url @@ -396,7 +446,8 @@ def _yield_elapsed_time(seconds=None): def _get_socketIO_session(is_secure, base_url, **kw): - server_url = '%s://%s/?transport=polling' % ('https' if is_secure else 'http', base_url) + server_url = '%s://%s/?EIO=%d&transport=polling' \ + % ('https' if is_secure else 'http', base_url, parser.ENGINE_PROTOCOL) _log.debug('[session] %s', server_url) try: response = _get_response(requests.get, server_url, **kw) @@ -404,15 +455,28 @@ def _get_socketIO_session(is_secure, base_url, **kw): raise ConnectionError(e) _log.debug("[response] %s", response.text); - decoded = parser.decode_response(response); - _log.debug("[decoded] %s", repr(decoded)); + packet = parser.decode_response(response); + _log.debug("[decoded] %s", repr(packet)); + + if packet.type is not parser.PacketType.OPEN: + _log.warn("Got unexpected packet during connection handshake: %d" % packet.type); + return None; + + handshake = json.loads(packet.payload); return _SocketIOSession( - id = decoded["payload"]["sid"], - heartbeat_timeout = int(decoded["payload"]["pingInterval"]), + id = handshake["sid"], + heartbeat_interval = int(handshake["pingInterval"]) / 1000, + connection_timeout = int(handshake["pingTimeout"]) / 1000, server_supported_transports = ["xhr-polling"]);#decoded["payload"]["upgrades"]); - #return _SocketIOSession( - # id=response_parts[0], - # heartbeat_timeout=int(response_parts[1]), - # server_supported_transports=response_parts[3].split(',')) +def _make_heartbeat_pacemaker(terminator, transport, heartbeat_interval): + while True: + if terminator.wait(heartbeat_interval): + break; + _log.debug("[hearbeat]"); + try: + transport.send_heartbeat(); + except: + pass; + _log.debug("[heartbeat terminated]"); diff --git a/socketIO_client/parser.py b/socketIO_client/parser.py index 2231481..6896c40 100644 --- a/socketIO_client/parser.py +++ b/socketIO_client/parser.py @@ -1,21 +1,94 @@ +from enum import Enum import logging import json _log = logging.getLogger(__name__) -""" Decodes a response from requests lib. -""" -def decode_response(response): - # TODO(sean): Should we use the 'raw' stream instead? - raw_bytes = response.content; - packet_type = "string" if ord(raw_bytes[0]) == 0 else "binary"; - _log.debug("Packet type: %s" % packet_type); +ENGINE_PROTOCOL = 3; - if packet_type is "string": +class PacketType(Enum): + OPEN = 0; + CLOSE = 1; + PING = 2; + PONG = 3; + MESSAGE = 4; + UPGRADE = 5; + NOOP = 6; + +class MessageType(Enum): + CONNECT = 0; + DISCONNECT = 1; + EVENT = 2; + ACK = 3; + ERROR = 4; + BINARY_EVENT = 5; + BINARY_ACK = 6; + +class Packet(): + def __init__(self, packet_type, payload): + self.type = packet_type; + self.payload = payload; + +class Message(): + def __init__(self, message_type, message, path = ""): + self.type = message_type; + if isinstance(message, basestring): + try: + self.message = json.loads(message); + except: + self.message = message; + else: + self.message = message; + + self.path = path; + + def encode_as_json(self): + """Encodes a Message to be sent to socket.io server. + + Assumes the message payload will be dumped as a json string. + """ + if self.path == "": + return str(self.type) + json.dumps(self.message); + return str(self.type) + self.path + "," + json.dumps(self.message); + + def encode_as_string(self): + """Same as the encode_as_string method except it doesn't encode things as a JSON string""" + if self.path == "": + return str(self.type) + self.message; + return str(self.type) + self.path + "," + self.message; + +def decode_message(payload): + """ Decodes a message encoded via socket.io + """ + + message_type = int(payload[0]); + message = payload[1:]; + + return Message(message_type, message); + +def decode_response(response): + """Decodes a response from requests lib. + + """ + # TODO(sean): Should we use the 'raw' stream instead? + return decode_packet(response.content); + +def decode_packet(packet): + """Decodes a packet sent via engine.io. + + If the packet is a message, this method assumes the message was + encoded by socket.io and will parse it as such. + + """ + + packet_format = "string" if ord(packet[0]) == 0 else "binary"; + _log.debug("Packet type: %s" % packet_format); + + if packet_format is "string": length_bytes = []; offset = 1; - while ord(raw_bytes[offset]) is not 255: - length_bytes.append(ord(raw_bytes[offset])); + while ord(packet[offset]) is not 255: + length_bytes.append(ord(packet[offset])); offset += 1; offset += 1; @@ -26,16 +99,30 @@ def decode_response(response): base *= 10; _log.debug("Packet length: %d" % length); - message_type = raw_bytes[offset]; + packet_type = int(packet[offset]); offset += 1; - message = {"type": message_type, "payload": json.loads(raw_bytes[offset:offset + length - 1])}; - _log.debug("Message: %s" % repr(message)); - return message; + payload = packet[offset:offset + length - 1]; + _log.debug("Payload: %s" % repr(payload)); + + if packet_type is PacketType.MESSAGE: + message = decode_message(payload); + payload = message; + + return Packet(packet_type, payload); else: pass; return ""; -def decode_packet(packet): pass; + +def encode_packet_string(code, path, data): + """Encodes packet to be sent to socket.io server. + """ + + code_length = len(str(code)); + data_length = len(data); + length = code_length + data_length; + + return str(length) + ":" + str(code) + str(data); diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index e1a0064..a2f37e8 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -1,6 +1,7 @@ import json import logging import parser +from parser import Message, MessageType, PacketType import re import requests import six @@ -13,7 +14,7 @@ from .exceptions import SocketIOError, ConnectionError, TimeoutError TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling' BOUNDARY = six.u('\ufffd') -TIMEOUT_IN_SECONDS = 3 +TIMEOUT_IN_SECONDS = 300 _log = logging.getLogger(__name__) @@ -24,6 +25,10 @@ class _AbstractTransport(object): self._callback_by_packet_id = {} self._wants_to_disconnect = False self._packets = [] + self._timeout = TIMEOUT_IN_SECONDS; + + def set_timeout(self, timeout): + self._timeout = timeout; def disconnect(self, path=''): if not path: @@ -31,15 +36,20 @@ class _AbstractTransport(object): if not self.connected: return if path: - self.send_packet(0, path) + self.send_packet(PacketType.CLOSE, path) else: self.close() def connect(self, path): - self.send_packet(1, path) + if path != "": + _log.debug("Connecting to path: %s" % path); + data = Message(MessageType.CONNECT, path).encode_as_string(); + self.send_packet(PacketType.MESSAGE, path, data); + else: + self.send_packet(PacketType.OPEN, path, data); def send_heartbeat(self): - self.send_packet(2) + self.send_packet(PacketType.PING) def message(self, path, data, callback): if isinstance(data, basestring): @@ -50,8 +60,8 @@ class _AbstractTransport(object): self.send_packet(code, path, data, callback) def emit(self, path, event, args, callback): - data = json.dumps(dict(name=event, args=args), ensure_ascii=False) - self.send_packet(5, path, data, callback) + message = Message(MessageType.EVENT, [event, args], path); + self.send_packet(PacketType.MESSAGE, path, message.encode_as_json(), callback) def ack(self, path, packet_id, *args): packet_id = packet_id.rstrip('+') @@ -59,15 +69,13 @@ class _AbstractTransport(object): packet_id, json.dumps(args, ensure_ascii=False), ) if args else packet_id - self.send_packet(6, path, data) + #self.send_packet(6, path, data) def noop(self, path=''): - self.send_packet(8, path) + self.send_packet(PacketType.NOOP, path) def send_packet(self, code, path='', data='', callback=None): - packet_id = self.set_ack_callback(callback) if callback else '' - packet_parts = str(code), packet_id, path, data - packet_text = ':'.join(packet_parts) + packet_text = parser.encode_packet_string(code, path, data); self.send(packet_text) _log.debug('[packet sent] %s', packet_text) @@ -77,22 +85,48 @@ class _AbstractTransport(object): yield self._packets.pop(0) except IndexError: pass - for packet_text in self.recv(): - _log.debug('[packet received] %s', packet_text) + for response in self.recv(): + _log.debug('[packet received] %s', response.text); try: - #packet = parser.decode_response(packet_text); - packet_parts = packet_text.split(':', 3) + packet = parser.decode_response(response); except AttributeError: - _log.warn('[packet error] %s', packet_text) + _log.warn('[packet error] %s', response.text) continue - code, packet_id, path, data = None, None, None, None - packet_count = len(packet_parts) - if 4 == packet_count: - code, packet_id, path, data = packet_parts - elif 3 == packet_count: - code, packet_id, path = packet_parts - elif 1 == packet_count: - code = packet_parts[0] + code, packet_id, path, data = None, None, '', None + + if packet.type is PacketType.OPEN: + code = '1'; + continue; + elif packet.type is PacketType.CLOSE: + code = '0'; + elif packet.type is PacketType.PING: + code = '2'; + elif packet.type is PacketType.PONG: + code = '2'; + elif packet.type is PacketType.UPGRADE: + _log.warn("Don't know how to handle upgrade packets"); + yield code, packet_id, path, data; + elif packet.type is PacketType.NOOP: + code = '8'; + elif packet.type is PacketType.MESSAGE: + if packet.payload.type is MessageType.CONNECT: + code = '1'; + elif packet.payload.type is MessageType.DISCONNECT: + code = '0'; + elif packet.payload.type is MessageType.EVENT: + code = '5'; + data = json.dumps({"name": packet.payload.message[0], "args": []}); + elif packet.payload.type is MessageType.ACK: + code = '6'; + elif packet.payload.type is MessageType.ERROR: + code = '7'; + else: + _log.warn("Don't know how to handle message type: %d" % packet.payload.type); + yield code, packet_id, path, data; + else: + _log.warn("Don't know how to handle packet type: %d" % packet.type); + yield code, packet_id, path, data; + yield code, packet_id, path, data def _enqueue_packet(self, packet): @@ -169,14 +203,16 @@ class _XHR_PollingTransport(_AbstractTransport): def __init__(self, socketIO_session, is_secure, base_url, **kw): super(_XHR_PollingTransport, self).__init__() - self._url = '%s://%s/?transport=polling&sid=%s' % ( + self._url = '%s://%s/?EIO=%d&transport=polling&sid=%s' % ( 'https' if is_secure else 'http', - base_url, socketIO_session.id) + base_url, parser.ENGINE_PROTOCOL, socketIO_session.id) self._connected = True self._http_session = _prepare_http_session(kw) + self._waiting = False; + # Create connection - for packet in self.recv_packet(): - self._enqueue_packet(packet) + #for packet in self.recv_packet(): + # self._enqueue_packet(packet) @property def connected(self): @@ -184,35 +220,54 @@ class _XHR_PollingTransport(_AbstractTransport): @property def _params(self): - return dict(t=int(time.time())) + return dict(t=int(time.time() * 1000)) def send(self, packet_text): - _get_response( - self._http_session.post, - self._url, - params=self._params, - data=packet_text, - timeout=TIMEOUT_IN_SECONDS) + uri = self._url + "&" + '&'.join("%s=%s" % (k, v) for (k, v) in self._params.iteritems()); + response = None; + try: + response = requests.post(uri, data=packet_text); + except requests.exceptions.Timeout as e: + message = 'timed out while sending %s (%s)' % (packet_text, e) + _log.warn(message) + raise TimeoutError(e) + except requests.exceptions.ConnectionError as e: + message = 'disconnected while sending %s (%s)' % (packet_text, e) + _log.warn(message) + raise ConnectionError(message) + except requests.exceptions.SSLError as e: + raise ConnectionError('could not negotiate SSL (%s)' % e) + status = response.status_code + if 200 != status: + raise ConnectionError('unexpected status code (%s)' % status) + return response def recv(self): + if self._waiting: + return; + + self._waiting = True; response = _get_response( self._http_session.get, self._url, - params=self._params, - timeout=TIMEOUT_IN_SECONDS) - #response_text = response.content - response_text = response.text - if not response_text.startswith(BOUNDARY): - yield response_text - return - for packet_text in _yield_text_from_framed_data(response_text): - yield packet_text + params = self._params, + timeout = self._timeout) + + self._waiting = False; + if response is None: + return; + + response_text = response; + #response_text = response.text + #if not response_text.startswith(BOUNDARY): + yield response_text + return + #for packet_text in _yield_text_from_framed_data(response_text): + # yield packet_text def close(self): - _get_response( - self._http_session.get, - self._url, - params=dict(self._params.items() + [('disconnect', True)])) + self.send_packet(41) + self.send_packet(1) self._connected = False @@ -310,8 +365,10 @@ def _yield_text_from_framed_data(framed_data, parse=lambda x: x): def _get_response(request, *args, **kw): + response = None; try: - response = request(*args, **kw) + response = request(*args, **kw); + response.close(); except requests.exceptions.Timeout as e: raise TimeoutError(e) except requests.exceptions.ConnectionError as e: From 57971b5f71f2724630ec81b6dd5c3e0c0ce1e360 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Mon, 22 Dec 2014 16:29:58 -0800 Subject: [PATCH 084/191] Added support for websockets via upgrade paradigm. Also added support for series of packets in responses rather than assuming single packets each time. Added support for all message fields in socket.io protocol --- socketIO_client/__init__.py | 104 +++++++++++++++------- socketIO_client/parser.py | 99 ++++++++++++++++++--- socketIO_client/transports.py | 157 ++++++++++------------------------ 3 files changed, 205 insertions(+), 155 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index bd3556a..36e2496 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -4,6 +4,7 @@ import logging import json import multiprocessing import parser +from parser import Message, Packet, MessageType, PacketType import requests import time @@ -13,7 +14,7 @@ except ImportError: from urlparse import urlparse as parse_url from .exceptions import ConnectionError, TimeoutError, PacketError -from .transports import _get_response, _negotiate_transport, TRANSPORTS +from .transports import _get_response _SocketIOSession = namedtuple('_SocketIOSession', [ @@ -139,11 +140,10 @@ class SocketIO(object): def __init__( self, host, port=None, Namespace=BaseNamespace, - wait_for_connection=True, transports=TRANSPORTS, **kw): + wait_for_connection=True, **kw): self.is_secure, self.base_url = _parse_host(host, port) self.wait_for_connection = wait_for_connection self._namespace_by_path = {} - self.client_supported_transports = transports self.kw = kw # These two fields work to control the heartbeat thread. self.heartbeat_terminator = None; @@ -250,10 +250,15 @@ class SocketIO(object): _log.warn('[packet error] %s', e) def _process_packet(self, packet): - code, packet_id, path, data = packet + code, packet_id, path, data, p = packet namespace = self.get_namespace(path) - delegate = self._get_delegate(code) - delegate(packet, namespace._find_event_callback) + delegate = None; + try: + delegate = self._get_delegate(code) + except: + pass; + if delegate is not None: + delegate(packet, namespace._find_event_callback) def _stop_waiting(self, for_callbacks): # Use __transport to make sure that we do not reconnect inadvertently @@ -300,14 +305,40 @@ class SocketIO(object): _log.warn(warning) return self.__transport - def _get_transport(self): - self.session = _get_socketIO_session(self.is_secure, self.base_url, **self.kw) - _log.debug('[transports available] %s', ' '.join(self.session.server_supported_transports)) + def _upgrade(self): + websocket = transports.WebsocketTransport(self.session, self.is_secure, self.base_url, **self.kw); + websocket.send_packet(PacketType.PING, "", "probe"); + for packet in websocket.recv_packet(): + _log.debug("[websocket] Packet: %s" % str(packet)); + (code, packet_id, path, data, p) = packet; + if code == PacketType.PONG: + packet = p; + _log.debug("[PONG] %s" % repr(packet)); - # Negotiate transport - transport = _negotiate_transport( - self.client_supported_transports, self.session, - self.is_secure, self.base_url, **self.kw) + self.heartbeat_terminator.set(); + + # Technically we would need to pause the current + # transport (which should be polling in this + # implementation), but since we haven't actually + # started a polling yet, we can upgrade without that. + _log.debug("[upgrading] Sending upgrade request"); + websocket.send_packet(PacketType.UPGRADE); + self._start_heartbeat(websocket); + return websocket; + + def _start_heartbeat(self, transport): + _log.debug("[start heartbeat pacemaker]"); + self.heartbeat_terminator = multiprocessing.Event(); + self.heartbeat_thread = multiprocessing.Process( + target = _make_heartbeat_pacemaker, + args = (self.heartbeat_terminator, transport, self.session.heartbeat_interval / 2)); + self.heartbeat_thread.start(); + + def _get_transport(self): + self.session = _get_socketIO_session(self.is_secure, self.base_url, **self.kw); + + # Negotiate initial transport + transport = transports.XHR_PollingTransport(self.session, self.is_secure, self.base_url, **self.kw); # Update namespaces for path, namespace in self._namespace_by_path.items(): namespace._transport = transport @@ -316,12 +347,17 @@ class SocketIO(object): transport.set_timeout(self.session.connection_timeout); # Start the heartbeat pacemaker (PING). - _log.debug("[start heartbeat pacemaker]"); - self.heartbeat_terminator = multiprocessing.Event(); - self.heartbeat_thread = multiprocessing.Process( - target = _make_heartbeat_pacemaker, - args = (self.heartbeat_terminator, transport, self.session.heartbeat_interval / 2)); - self.heartbeat_thread.start(); + self._start_heartbeat(transport); + + # If websocket is available, upgrade to it immediately. + # TODO(sean): We could run this on a separate thread for + # maximum efficiency although that would require some + # synchronization to ensure buffers are flushed, etc. + if "websocket" in self.session.server_supported_transports: + try: + return self._upgrade(); + except: + pass; return transport @@ -371,10 +407,19 @@ class SocketIO(object): find_event_callback('message')(*args) def _on_event(self, packet, find_event_callback): - code, packet_id, path, data = packet - value_by_name = json.loads(data) - event = value_by_name['name'] - args = value_by_name.get('args', []) + code, packet_id, path, data, p = packet + packet = p; + + + # Accoding to the documentation + # (https://github.com/automattic/socket.io-protocol#event), + # the event name is the first entry in the message array, and + # the arguments are the rest of the entries. + event = packet.payload.message[0]; + args = packet.payload.message[1:] if len(packet.payload.message) > 1 else []; + + _log.debug("[event] %s (%s)" % (repr(event), repr(args))); + if packet_id: args.append(self._prepare_to_send_ack(path, packet_id)) find_event_callback(event)(*args) @@ -455,12 +500,11 @@ def _get_socketIO_session(is_secure, base_url, **kw): raise ConnectionError(e) _log.debug("[response] %s", response.text); - packet = parser.decode_response(response); - _log.debug("[decoded] %s", repr(packet)); - - if packet.type is not parser.PacketType.OPEN: - _log.warn("Got unexpected packet during connection handshake: %d" % packet.type); - return None; + for packet in parser.decode_response(response): + _log.debug("[decoded] %s", str(packet)); + if packet.type is not parser.PacketType.OPEN: + _log.warn("Got unexpected packet during connection handshake: %d" % packet.type); + return None; handshake = json.loads(packet.payload); @@ -468,7 +512,7 @@ def _get_socketIO_session(is_secure, base_url, **kw): id = handshake["sid"], heartbeat_interval = int(handshake["pingInterval"]) / 1000, connection_timeout = int(handshake["pingTimeout"]) / 1000, - server_supported_transports = ["xhr-polling"]);#decoded["payload"]["upgrades"]); + server_supported_transports = handshake["upgrades"]); def _make_heartbeat_pacemaker(terminator, transport, heartbeat_interval): while True: diff --git a/socketIO_client/parser.py b/socketIO_client/parser.py index 6896c40..3e657d0 100644 --- a/socketIO_client/parser.py +++ b/socketIO_client/parser.py @@ -29,8 +29,11 @@ class Packet(): self.type = packet_type; self.payload = payload; + def __str__(self): + return "PACKET{type: " + str(self.type) + ", payload: " + str(self.payload) + "}"; + class Message(): - def __init__(self, message_type, message, path = ""): + def __init__(self, message_type, message, path = "", attachments = "", message_id = None): self.type = message_type; if isinstance(message, basestring): try: @@ -41,6 +44,23 @@ class Message(): self.message = message; self.path = path; + self.attachments = attachments; + self.id = message_id; + + def __str__(self): + if self.id is not None: + return "MESSAGE{" + \ + "id: " + str(self.id) + ", " + \ + "type: " + str(self.type) + ", " + \ + "message: " + str(self.message) + ", " + \ + "path: " + self.path + \ + "}"; + else: + return "MESSAGE{" + \ + "type: " + str(self.type) + ", " + \ + "message: " + str(self.message) + ", " + \ + "path: " + self.path + \ + "}"; def encode_as_json(self): """Encodes a Message to be sent to socket.io server. @@ -61,17 +81,78 @@ def decode_message(payload): """ Decodes a message encoded via socket.io """ - message_type = int(payload[0]); - message = payload[1:]; + _log.debug("[decode payload] %s" % repr(payload)); - return Message(message_type, message); + i = 0; + message_type = int(payload[i]); + message = ""; + path = ""; + attachments = ""; + message_id = None; + + i += 1; + + if len(payload) > i: + if message_type == MessageType.BINARY_EVENT or message_type == MessageType.BINARY_ACK: + while (payload[i] != "-"): + attachments += payload[i]; + i += 1; + + if len(payload) > i: + # This is kind of odd, but it is how socket.io-parser works (see + # https://github.com/Automattic/socket.io-parser/blob/master/index.js#L292 + # @0ae9a4f). + if payload[i] == "/": + if "," in payload: + split_point = payload.index(","); + path = payload[i:split_point]; + i += split_point; + else: + path = payload[i:]; + i += len(path); + + if len(payload) > i: + # This is the same pecularity as above. + if "," in payload[i:]: + split_point = payload.index(","); + message_id = int(payload[i:split_point]); + i += split_point; + + if len(payload) > i: + message = payload[i:]; + + return Message(message_type, message, path, attachments, message_id); def decode_response(response): """Decodes a response from requests lib. """ # TODO(sean): Should we use the 'raw' stream instead? - return decode_packet(response.content); + if isinstance(response, basestring): + _log.debug("[decode response (string)] Response: %s" % str(response)); + packet = decode_packet_string(response); + yield packet; + else: + content = response.content; + total_length = len(content); + processed = 0; + while processed < total_length: + _log.debug("[decode response] Content: %s" % str(content)); + (read, packet) = decode_packet(content); + content = content[read:]; + processed += read; + yield packet; + + +def decode_packet_string(packet): + packet_type = int(packet[0]); + payload = packet[1:]; + + if packet_type == PacketType.MESSAGE: + message = decode_message(payload); + payload = message; + + return Packet(packet_type, payload); def decode_packet(packet): """Decodes a packet sent via engine.io. @@ -108,15 +189,11 @@ def decode_packet(packet): if packet_type is PacketType.MESSAGE: message = decode_message(payload); payload = message; - - return Packet(packet_type, payload); + + return offset + length, Packet(packet_type, payload); else: pass; - return ""; - - pass; - def encode_packet_string(code, path, data): """Encodes packet to be sent to socket.io server. """ diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index a2f37e8..83eb019 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -45,6 +45,19 @@ class _AbstractTransport(object): _log.debug("Connecting to path: %s" % path); data = Message(MessageType.CONNECT, path).encode_as_string(); self.send_packet(PacketType.MESSAGE, path, data); + + # Wait for response. + responded = False; + while not responded: + for packet in self.recv_packet(): + _log.debug("[connect wait] Waiting for confirmation"); + (code, packet_id, ignore, data, p) = packet; + packet = p; + if (packet.type == PacketType.MESSAGE + and packet.payload.type == MessageType.CONNECT + and packet.payload.path == path): + _log.debug("Connected to path: %s" % path); + responded = True; else: self.send_packet(PacketType.OPEN, path, data); @@ -85,27 +98,20 @@ class _AbstractTransport(object): yield self._packets.pop(0) except IndexError: pass - for response in self.recv(): - _log.debug('[packet received] %s', response.text); - try: - packet = parser.decode_response(response); - except AttributeError: - _log.warn('[packet error] %s', response.text) - continue + for packet in self.recv(): code, packet_id, path, data = None, None, '', None if packet.type is PacketType.OPEN: code = '1'; - continue; elif packet.type is PacketType.CLOSE: code = '0'; elif packet.type is PacketType.PING: code = '2'; elif packet.type is PacketType.PONG: - code = '2'; + code = PacketType.PONG; elif packet.type is PacketType.UPGRADE: _log.warn("Don't know how to handle upgrade packets"); - yield code, packet_id, path, data; + yield code, packet_id, path, data, packet; elif packet.type is PacketType.NOOP: code = '8'; elif packet.type is PacketType.MESSAGE: @@ -122,12 +128,12 @@ class _AbstractTransport(object): code = '7'; else: _log.warn("Don't know how to handle message type: %d" % packet.payload.type); - yield code, packet_id, path, data; + yield code, packet_id, path, data, packet; else: _log.warn("Don't know how to handle packet type: %d" % packet.type); - yield code, packet_id, path, data; + yield code, packet_id, path, data, packet; - yield code, packet_id, path, data + yield code, packet_id, path, data, packet def _enqueue_packet(self, packet): self._packets.append(packet) @@ -149,15 +155,17 @@ class _AbstractTransport(object): return True if self._callback_by_packet_id else False -class _WebsocketTransport(_AbstractTransport): +class WebsocketTransport(_AbstractTransport): def __init__(self, socketIO_session, is_secure, base_url, **kw): - super(_WebsocketTransport, self).__init__() - url = '%s://%s/websocket/%s' % ( + super(WebsocketTransport, self).__init__() + + self._url = '%s://%s/?EIO=%d&transport=websocket&sid=%s' % ( 'wss' if is_secure else 'ws', - base_url, socketIO_session.id) + base_url, parser.ENGINE_PROTOCOL, socketIO_session.id) + try: - self._connection = websocket.create_connection(url) + self._connection = websocket.create_connection(self._url) except socket.timeout as e: raise ConnectionError(e) except socket.error as e: @@ -168,6 +176,11 @@ class _WebsocketTransport(_AbstractTransport): def connected(self): return self._connection.connected + def send_packet(self, code, path="", data='', callback=None): + packet_text = Message(code, data).encode_as_string(); + self.send(packet_text) + _log.debug('[packet sent] %s', packet_text) + def send(self, packet_text): try: self._connection.send(packet_text) @@ -182,7 +195,14 @@ class _WebsocketTransport(_AbstractTransport): def recv(self): try: - yield self._connection.recv() + response = self._connection.recv(); + try: + for packet in parser.decode_response(response): + _log.debug('[websocket packet received] %s', str(packet)); + yield packet; + except AttributeError: + _log.warn('[packet error] %s', repr(response)) + return; except websocket.WebSocketTimeoutException as e: raise TimeoutError(e) except websocket.SSLError as e: @@ -199,10 +219,10 @@ class _WebsocketTransport(_AbstractTransport): self._connection.close() -class _XHR_PollingTransport(_AbstractTransport): +class XHR_PollingTransport(_AbstractTransport): def __init__(self, socketIO_session, is_secure, base_url, **kw): - super(_XHR_PollingTransport, self).__init__() + super(XHR_PollingTransport, self).__init__() self._url = '%s://%s/?EIO=%d&transport=polling&sid=%s' % ( 'https' if is_secure else 'http', base_url, parser.ENGINE_PROTOCOL, socketIO_session.id) @@ -210,10 +230,6 @@ class _XHR_PollingTransport(_AbstractTransport): self._http_session = _prepare_http_session(kw) self._waiting = False; - # Create connection - #for packet in self.recv_packet(): - # self._enqueue_packet(packet) - @property def connected(self): return self._connected @@ -257,102 +273,15 @@ class _XHR_PollingTransport(_AbstractTransport): if response is None: return; - response_text = response; - #response_text = response.text - #if not response_text.startswith(BOUNDARY): - yield response_text + for packet in parser.decode_response(response): + yield packet; return - #for packet_text in _yield_text_from_framed_data(response_text): - # yield packet_text def close(self): self.send_packet(41) self.send_packet(1) self._connected = False - -class _JSONP_PollingTransport(_AbstractTransport): - - RESPONSE_PATTERN = re.compile(r'io.j\[(\d+)\]\("(.*)"\);') - - def __init__(self, socketIO_session, is_secure, base_url, **kw): - super(_JSONP_PollingTransport, self).__init__() - self._url = '%s://%s/jsonp-polling/%s' % ( - 'https' if is_secure else 'http', - base_url, socketIO_session.id) - self._connected = True - self._http_session = _prepare_http_session(kw) - self._id = 0 - # Create connection - for packet in self.recv_packet(): - self._enqueue_packet(packet) - - @property - def connected(self): - return self._connected - - @property - def _params(self): - return dict(t=int(time.time()), i=self._id) - - def send(self, packet_text): - _get_response( - self._http_session.post, - self._url, - params=self._params, - data='d=%s' % requests.utils.quote(json.dumps(packet_text)), - headers={'content-type': 'application/x-www-form-urlencoded'}, - timeout=TIMEOUT_IN_SECONDS) - - def recv(self): - 'Decode the JavaScript response so that we can parse it as JSON' - response = _get_response( - self._http_session.get, - self._url, - params=self._params, - headers={'content-type': 'text/javascript; charset=UTF-8'}, - timeout=TIMEOUT_IN_SECONDS) - response_text = response.text - try: - self._id, response_text = self.RESPONSE_PATTERN.match( - response_text).groups() - except AttributeError: - _log.warn('[packet error] %s', response_text) - return - if not response_text.startswith(BOUNDARY): - yield response_text.decode('unicode_escape') - return - for packet_text in _yield_text_from_framed_data( - response_text, parse=lambda x: x.decode('unicode_escape')): - yield packet_text - - def close(self): - _get_response( - self._http_session.get, - self._url, - params=dict(self._params.items() + [('disconnect', True)])) - self._connected = False - - -def _negotiate_transport( - client_supported_transports, session, - is_secure, base_url, **kw): - server_supported_transports = session.server_supported_transports - for supported_transport in client_supported_transports: - if supported_transport in server_supported_transports: - _log.debug('[transport selected] %s', supported_transport) - return { - 'websocket': _WebsocketTransport, - 'xhr-polling': _XHR_PollingTransport, - 'jsonp-polling': _JSONP_PollingTransport, - }[supported_transport](session, is_secure, base_url, **kw) - raise SocketIOError(' '.join([ - 'could not negotiate a transport:', - 'client supports %s but' % ', '.join(client_supported_transports), - 'server supports %s' % ', '.join(server_supported_transports), - ])) - - def _yield_text_from_framed_data(framed_data, parse=lambda x: x): parts = [parse(x) for x in framed_data.split(BOUNDARY)] for text_length, text in zip(parts[1::2], parts[2::2]): From 07a5cc4c63d85839ce838c93776bcf233032a172 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Mon, 22 Dec 2014 16:49:16 -0800 Subject: [PATCH 085/191] Events are working on a per-namespace basis now --- socketIO_client/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 36e2496..794e158 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -410,7 +410,6 @@ class SocketIO(object): code, packet_id, path, data, p = packet packet = p; - # Accoding to the documentation # (https://github.com/automattic/socket.io-protocol#event), # the event name is the first entry in the message array, and @@ -420,9 +419,12 @@ class SocketIO(object): _log.debug("[event] %s (%s)" % (repr(event), repr(args))); - if packet_id: + if packet.payload.id is not None: args.append(self._prepare_to_send_ack(path, packet_id)) - find_event_callback(event)(*args) + try: + self._namespace_by_path[packet.payload.path]._find_event_callback(event)(*args); + except KeyError: + _log.error("Could not handle event for unknown path: %s" % packet.payload.path); def _on_ack(self, packet, find_event_callback): code, packet_id, path, data = packet From 16c64437d318bcac87e1ff21b9d7304cf2b4327e Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Mon, 22 Dec 2014 17:43:42 -0800 Subject: [PATCH 086/191] Medium-sized refactor. Converted all methods away from old 'code' paradigm to use the PacketType and MessageType enums directly --- socketIO_client/__init__.py | 151 +++++++++++++++++++--------------- socketIO_client/transports.py | 38 +-------- 2 files changed, 87 insertions(+), 102 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 794e158..4552103 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -67,14 +67,6 @@ class BaseNamespace(object): 'Called after server disconnects; you can override this method' _log.debug('%s [disconnect]', self.path) - def on_heartbeat(self): - 'Called after server sends a heartbeat; you can override this method' - _log.debug('%s [heartbeat]', self.path) - - def on_message(self, data): - 'Called after server sends a message; you can override this method' - _log.info('%s [message] %s', self.path, data) - def on_event(self, event, *args): """ Called after server sends an event; you can override this method. @@ -88,14 +80,22 @@ class BaseNamespace(object): callback(*args) _log.info('%s [event] %s(%s)', self.path, event, ', '.join(arguments)) - def on_error(self, reason, advice): + def on_error(self, reason): 'Called after server sends an error; you can override this method' - _log.info('%s [error] %s', self.path, advice) + _log.info('%s [error] %s', self.path, reason) def on_noop(self): 'Called after server sends a noop; you can override this method' _log.info('%s [noop]', self.path) + def on_ping(self): + 'Called after server sends a ping; you can override this method' + _log.info('%s [ping]', self.path) + + def on_pong(self): + 'Called after server sends a pong; you can override this method' + _log.info('%s [pong]', self.path) + def on_open(self, *args): _log.info('%s [open] %s', self.path, args) @@ -249,13 +249,49 @@ class SocketIO(object): except PacketError as e: _log.warn('[packet error] %s', e) + def _get_message_delegate(self, code): + try: + return { + MessageType.CONNECT: self._on_connect, + MessageType.DISCONNECT: self._on_disconnect, + MessageType.EVENT: self._on_event, + MessageType.ACK: self._on_ack, + MessageType.ERROR: self._on_error, + MessageType.BINARY_EVENT: self._on_binary_event, + MessageType.BINARY_ACK: self._on_binary_ack + }[code] + except KeyError: + raise PacketError('unexpected code (%s)' % code) + + def _get_packet_delegate(self, code): + try: + return { + PacketType.OPEN: self._on_open, + PacketType.CLOSE: self._on_close, + PacketType.PING: self._on_ping, + PacketType.PONG: self._on_pong, + #PacketType.MESSAGE: self._on_message, Handled by other delegates + PacketType.UPGRADE: self._on_upgrade, + PacketType.NOOP: self._on_noop + }[code] + except KeyError: + raise PacketError('unexpected code (%s)' % code) + def _process_packet(self, packet): - code, packet_id, path, data, p = packet + _log.debug("[process packet] %s" % str(packet)); + path = packet.payload.path if packet.type == PacketType.MESSAGE else ""; namespace = self.get_namespace(path) + code = packet.payload.type if packet.type == PacketType.MESSAGE else packet.type; + delegate = None; try: - delegate = self._get_delegate(code) - except: + if packet.type == PacketType.MESSAGE: + _log.debug("[process packet] Handling message"); + delegate = self._get_message_delegate(packet.payload.type); + else: + delegate = self._get_packet_delegate(packet.type); + except Exception as e: + _log.warn("[process packet] Could not find delegate for packet: " + str(e)); pass; if delegate is not None: delegate(packet, namespace._find_event_callback) @@ -310,9 +346,7 @@ class SocketIO(object): websocket.send_packet(PacketType.PING, "", "probe"); for packet in websocket.recv_packet(): _log.debug("[websocket] Packet: %s" % str(packet)); - (code, packet_id, path, data, p) = packet; - if code == PacketType.PONG: - packet = p; + if packet.type == PacketType.PONG: _log.debug("[PONG] %s" % repr(packet)); self.heartbeat_terminator.set(); @@ -367,49 +401,39 @@ class SocketIO(object): except KeyError: raise PacketError('unexpected namespace path (%s)' % path) - def _get_delegate(self, code): - try: - return { - '0': self._on_disconnect, - '1': self._on_connect, - '2': self._on_heartbeat, - '3': self._on_message, - '4': self._on_json, - '5': self._on_event, - '6': self._on_ack, - '7': self._on_error, - '8': self._on_noop, - }[code] - except KeyError: - raise PacketError('unexpected code (%s)' % code) + ################################################################# + # Handlers for EngineIO packet types (PacketType in parser.py) + ################################################################# - def _on_disconnect(self, packet, find_event_callback): - find_event_callback('disconnect')() + def _on_open(self, packet, find_event_callback): + find_event_callback('open')() + + def _on_close(self, packet, find_event_callback): + find_event_callback('close')() + + def _on_ping(self, packet, find_event_callback): + find_event_callback('ping')() + + def _on_pong(self, packet, find_event_callback): + find_event_callback('pong')() + + def _on_upgrade(self, packet, find_event_callback): + find_event_callback('close')() + + def _on_noop(self, packet, find_event_callback): + find_event_callback('noop')() + + ################################################################# + # Handlers for SocketIO "packet" types (MessageType in parser.py) + ################################################################# def _on_connect(self, packet, find_event_callback): find_event_callback('connect')() - def _on_heartbeat(self, packet, find_event_callback): - find_event_callback('heartbeat')() - - def _on_message(self, packet, find_event_callback): - code, packet_id, path, data = packet - args = [data] - if packet_id: - args.append(self._prepare_to_send_ack(path, packet_id)) - find_event_callback('message')(*args) - - def _on_json(self, packet, find_event_callback): - code, packet_id, path, data = packet - args = [json.loads(data)] - if packet_id: - args.append(self._prepare_to_send_ack(path, packet_id)) - find_event_callback('message')(*args) + def _on_disconnect(self, packet, find_event_callback): + find_event_callback('disconnect')() def _on_event(self, packet, find_event_callback): - code, packet_id, path, data, p = packet - packet = p; - # Accoding to the documentation # (https://github.com/automattic/socket.io-protocol#event), # the event name is the first entry in the message array, and @@ -421,29 +445,26 @@ class SocketIO(object): if packet.payload.id is not None: args.append(self._prepare_to_send_ack(path, packet_id)) - try: - self._namespace_by_path[packet.payload.path]._find_event_callback(event)(*args); - except KeyError: - _log.error("Could not handle event for unknown path: %s" % packet.payload.path); + find_event_callback(event)(*args); def _on_ack(self, packet, find_event_callback): - code, packet_id, path, data = packet - data_parts = data.split('+', 1) - packet_id = data_parts[0] + event = packet.payload.message[0]; + args = packet.payload.message[1:] if len(packet.payload.message) > 1 else []; + packet_id = packet.payload.id; try: ack_callback = self._transport.get_ack_callback(packet_id) except KeyError: return - args = json.loads(data_parts[1]) if len(data_parts) > 1 else [] ack_callback(*args) def _on_error(self, packet, find_event_callback): - code, packet_id, path, data = packet - reason, advice = data.split('+', 1) - find_event_callback('error')(reason, advice) + find_event_callback('error')(packet.payload.message) - def _on_noop(self, packet, find_event_callback): - find_event_callback('noop')() + def _on_binary_event(self, packet, find_event_callback): + raise PacketError("Don't know how to handle binary events yet"); + + def _on_binary_ack(self, packet, find_event_callback): + raise PacketError("Don't know how to handle binary acks yet"); def _prepare_to_send_ack(self, path, packet_id): 'Return function that acknowledges the server' diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 83eb019..32a03f1 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -51,8 +51,6 @@ class _AbstractTransport(object): while not responded: for packet in self.recv_packet(): _log.debug("[connect wait] Waiting for confirmation"); - (code, packet_id, ignore, data, p) = packet; - packet = p; if (packet.type == PacketType.MESSAGE and packet.payload.type == MessageType.CONNECT and packet.payload.path == path): @@ -99,41 +97,7 @@ class _AbstractTransport(object): except IndexError: pass for packet in self.recv(): - code, packet_id, path, data = None, None, '', None - - if packet.type is PacketType.OPEN: - code = '1'; - elif packet.type is PacketType.CLOSE: - code = '0'; - elif packet.type is PacketType.PING: - code = '2'; - elif packet.type is PacketType.PONG: - code = PacketType.PONG; - elif packet.type is PacketType.UPGRADE: - _log.warn("Don't know how to handle upgrade packets"); - yield code, packet_id, path, data, packet; - elif packet.type is PacketType.NOOP: - code = '8'; - elif packet.type is PacketType.MESSAGE: - if packet.payload.type is MessageType.CONNECT: - code = '1'; - elif packet.payload.type is MessageType.DISCONNECT: - code = '0'; - elif packet.payload.type is MessageType.EVENT: - code = '5'; - data = json.dumps({"name": packet.payload.message[0], "args": []}); - elif packet.payload.type is MessageType.ACK: - code = '6'; - elif packet.payload.type is MessageType.ERROR: - code = '7'; - else: - _log.warn("Don't know how to handle message type: %d" % packet.payload.type); - yield code, packet_id, path, data, packet; - else: - _log.warn("Don't know how to handle packet type: %d" % packet.type); - yield code, packet_id, path, data, packet; - - yield code, packet_id, path, data, packet + yield packet def _enqueue_packet(self, packet): self._packets.append(packet) From 5c1d38ac861d0a8d6f5ac279882ad18929aabb2d Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Mon, 22 Dec 2014 18:39:58 -0800 Subject: [PATCH 087/191] Implemented ack callbacks --- socketIO_client/__init__.py | 2 +- socketIO_client/parser.py | 63 +++++++++++++++++++++++++++-------- socketIO_client/transports.py | 26 +++++++++------ 3 files changed, 66 insertions(+), 25 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 4552103..05492d9 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -444,7 +444,7 @@ class SocketIO(object): _log.debug("[event] %s (%s)" % (repr(event), repr(args))); if packet.payload.id is not None: - args.append(self._prepare_to_send_ack(path, packet_id)) + args.append(self._prepare_to_send_ack(packet.payload.path, packet.payload.id)) find_event_callback(event)(*args); def _on_ack(self, packet, find_event_callback): diff --git a/socketIO_client/parser.py b/socketIO_client/parser.py index 3e657d0..8fe06b1 100644 --- a/socketIO_client/parser.py +++ b/socketIO_client/parser.py @@ -32,6 +32,27 @@ class Packet(): def __str__(self): return "PACKET{type: " + str(self.type) + ", payload: " + str(self.payload) + "}"; + def encode_as_string(self, for_websocket = False): + data = ""; + path = ""; + if self.type == PacketType.MESSAGE: + data = self.payload.encode_as_string(); + path = self.payload.path; + else: + data = self.payload; + + code_length = len(str(self.type)); + data_length = len(data); + length = code_length + data_length; + + encoded = ""; + if for_websocket: + encoded = str(self.type) + str(data); + else: + encoded = str(length) + ":" + str(self.type) + str(data); + + return encoded; + class Message(): def __init__(self, message_type, message, path = "", attachments = "", message_id = None): self.type = message_type; @@ -67,15 +88,30 @@ class Message(): Assumes the message payload will be dumped as a json string. """ + data = json.dumps(self.message); + if self.id is not None: + data = str(self.id) + json.dumps(self.message); + if self.path == "": - return str(self.type) + json.dumps(self.message); - return str(self.type) + self.path + "," + json.dumps(self.message); + return str(self.type) + data; + return str(self.type) + self.path + "," + data; def encode_as_string(self): """Same as the encode_as_string method except it doesn't encode things as a JSON string""" + data = self.message; + if self.id is not None: + data = str(self.id) + self.message; + if self.path == "": - return str(self.type) + self.message; - return str(self.type) + self.path + "," + self.message; + return str(self.type) + data; + return str(self.type) + self.path + "," + data; + +def _is_integer(s): + try: + int(s); + except ValueError: + return False; + return True; def decode_message(payload): """ Decodes a message encoded via socket.io @@ -112,11 +148,15 @@ def decode_message(payload): i += len(path); if len(payload) > i: - # This is the same pecularity as above. - if "," in payload[i:]: - split_point = payload.index(","); - message_id = int(payload[i:split_point]); - i += split_point; + # This is another oddity. According to the socket.io-parser we + # need to loop over the next chars until we stop finding ints + # to determine if there is a message id. + message_id_str = ""; + while _is_integer(payload[i]): + message_id_str += payload[i]; + i += 1; + if message_id_str != "": + message_id = int(message_id_str); if len(payload) > i: message = payload[i:]; @@ -198,8 +238,3 @@ def encode_packet_string(code, path, data): """Encodes packet to be sent to socket.io server. """ - code_length = len(str(code)); - data_length = len(data); - length = code_length + data_length; - - return str(length) + ":" + str(code) + str(data); diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 32a03f1..65be157 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -1,7 +1,7 @@ import json import logging import parser -from parser import Message, MessageType, PacketType +from parser import Message, Packet, MessageType, PacketType import re import requests import six @@ -75,18 +75,16 @@ class _AbstractTransport(object): self.send_packet(PacketType.MESSAGE, path, message.encode_as_json(), callback) def ack(self, path, packet_id, *args): - packet_id = packet_id.rstrip('+') - data = '%s+%s' % ( - packet_id, - json.dumps(args, ensure_ascii=False), - ) if args else packet_id - #self.send_packet(6, path, data) + _log.debug("[ack] Sending ACK for packet: %d" % packet_id); + message = Message(MessageType.ACK, "", path, "", packet_id); + packet = Packet(PacketType.MESSAGE, message); + self.send_engineio_packet(packet) def noop(self, path=''): self.send_packet(PacketType.NOOP, path) def send_packet(self, code, path='', data='', callback=None): - packet_text = parser.encode_packet_string(code, path, data); + packet_text = Packet(code, data).encode_as_string(); self.send(packet_text) _log.debug('[packet sent] %s', packet_text) @@ -140,11 +138,19 @@ class WebsocketTransport(_AbstractTransport): def connected(self): return self._connection.connected - def send_packet(self, code, path="", data='', callback=None): - packet_text = Message(code, data).encode_as_string(); + def send_message(self, message, callback = None): + packet_text = message.encode_as_string(); self.send(packet_text) _log.debug('[packet sent] %s', packet_text) + def send_engineio_packet(self, packet, callback=None): + packet_text = packet.encode_as_string(for_websocket = True); + self.send(packet_text) + _log.debug('[packet sent] %s', packet_text) + + def send_packet(self, code, path="", data='', callback=None): + self.send_message(Message(code, data), callback); + def send(self, packet_text): try: self._connection.send(packet_text) From 2d1257bf8f241a2b831198c1b92bf3d4e625b7f1 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Mon, 22 Dec 2014 21:46:05 -0800 Subject: [PATCH 088/191] Client now attempts to reconnect forever on server disconnect and correctly reconnects --- socketIO_client/__init__.py | 55 ++++++++++++++++++++++++++--------- socketIO_client/parser.py | 5 ++-- socketIO_client/transports.py | 19 ++++++------ 3 files changed, 55 insertions(+), 24 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 05492d9..30a8643 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -145,19 +145,26 @@ class SocketIO(object): self.wait_for_connection = wait_for_connection self._namespace_by_path = {} self.kw = kw + + self.__transport = None; + # These two fields work to control the heartbeat thread. self.heartbeat_terminator = None; self.heartbeat_thread = None; + # Saved session information. self.session = None; + # This is stores the set of paths (namespaces) that need to be # reconnected to. self.reconnect_paths = {}; + # This sets of a chain of events that attempts to connect to # the server at the base namespace. self.define(Namespace) def __enter__(self): + _log.debug("[enter]"); return self def __exit__(self, *exception_pack): @@ -193,9 +200,19 @@ class SocketIO(object): self._transport.emit(path, event, args, callback) def reconnect(self): - """Reconnects to a set of namespaces. + """Reconnects the client. + + Reconnects to the server and connects to the previously + connected set of namespaces. """ + _log.debug(" [reconnect attempt]"); + + # Reconnect to the server. + if self.__transport is not None: + self.__transport.close(); + self.__transport = self._get_transport(); + for path in self.reconnect_paths: # We avoid reconnecting to the default namespace because # socketIO_client connects to that already. @@ -203,6 +220,10 @@ class SocketIO(object): continue; _log.debug("Reconnecting to path: %s" % repr(path)) self._transport.connect(path); + # Restore paths. + self._namespace_by_path = copy.copy(self.reconnect_paths); + for namespace in self._namespace_by_path: + self._namespace_by_path[namespace]._transport = self.__transport; self.reconnect_paths = {}; def wait(self, seconds=None, for_callbacks=False): @@ -212,6 +233,15 @@ class SocketIO(object): """ warning_screen = _yield_warning_screen(seconds) for elapsed_time in warning_screen: + # We will end up here in the case that we + # disconnected. + if len(self.reconnect_paths) > 0: + try: + self.reconnect(); + except ConnectionError as e: + time.sleep(1); + continue; + try: try: self._process_events() @@ -220,26 +250,23 @@ class SocketIO(object): if self._stop_waiting(for_callbacks): break - # We will end up here in the case that we - # disconnected, then reconnected AND we were - # successful. - if len(self.reconnect_paths) > 0: - self.reconnect(); except ConnectionError as e: try: # This is where we end up if the connection was # severed. The client will disconnect here. if len(self.reconnect_paths) is 0: - self.reconnect_paths = copy.deepcopy(self._namespace_by_path); + self.reconnect_paths = copy.copy(self._namespace_by_path); - self._terminate_heartbeat(); + self._terminate_heartbeat(); + + for namespace in self.reconnect_paths: + self.disconnect(namespace); warning = Exception('[connection error] %s' % e) - self._transport._connected = False; warning_screen.throw(warning) except StopIteration: _log.warn(warning) - self.disconnect() + _log.debug("[wait canceled]"); def _process_events(self): @@ -317,18 +344,19 @@ class SocketIO(object): @property def connected(self): - return self.__transport.connected + return self.__transport.connected if self.__transport is not None else False; @property def _transport(self): try: - if self.connected: + if self.__transport is not None and self.connected: return self.__transport except AttributeError: pass warning_screen = _yield_warning_screen(seconds=None) for elapsed_time in warning_screen: try: + _log.debug("[create transport]"); self.__transport = self._get_transport() break except ConnectionError as e: @@ -349,7 +377,7 @@ class SocketIO(object): if packet.type == PacketType.PONG: _log.debug("[PONG] %s" % repr(packet)); - self.heartbeat_terminator.set(); + self._terminate_heartbeat(); # Technically we would need to pause the current # transport (which should be polling in this @@ -391,6 +419,7 @@ class SocketIO(object): try: return self._upgrade(); except: + _log.warn("[websocket] Failed to upgrade to websocket") pass; return transport diff --git a/socketIO_client/parser.py b/socketIO_client/parser.py index 8fe06b1..39fcb3d 100644 --- a/socketIO_client/parser.py +++ b/socketIO_client/parser.py @@ -35,7 +35,7 @@ class Packet(): def encode_as_string(self, for_websocket = False): data = ""; path = ""; - if self.type == PacketType.MESSAGE: + if self.type == PacketType.MESSAGE and not isinstance(self.payload, basestring): data = self.payload.encode_as_string(); path = self.payload.path; else: @@ -230,8 +230,9 @@ def decode_packet(packet): message = decode_message(payload); payload = message; - return offset + length, Packet(packet_type, payload); + return offset + length - 1, Packet(packet_type, payload); else: + import ipdb; ipdb.set_trace(); pass; def encode_packet_string(code, path, data): diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 65be157..2c70cf4 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -41,7 +41,7 @@ class _AbstractTransport(object): self.close() def connect(self, path): - if path != "": + if True or path != "": _log.debug("Connecting to path: %s" % path); data = Message(MessageType.CONNECT, path).encode_as_string(); self.send_packet(PacketType.MESSAGE, path, data); @@ -50,11 +50,11 @@ class _AbstractTransport(object): responded = False; while not responded: for packet in self.recv_packet(): - _log.debug("[connect wait] Waiting for confirmation"); + _log.debug("[connect wait] Waiting for confirmation of connect to: %s" % path); if (packet.type == PacketType.MESSAGE and packet.payload.type == MessageType.CONNECT and packet.payload.path == path): - _log.debug("Connected to path: %s" % path); + _log.debug("[connect] Connected to path: %s" % path); responded = True; else: self.send_packet(PacketType.OPEN, path, data); @@ -78,7 +78,7 @@ class _AbstractTransport(object): _log.debug("[ack] Sending ACK for packet: %d" % packet_id); message = Message(MessageType.ACK, "", path, "", packet_id); packet = Packet(PacketType.MESSAGE, message); - self.send_engineio_packet(packet) + self.send_engineio_packet(packet); def noop(self, path=''): self.send_packet(PacketType.NOOP, path) @@ -86,7 +86,6 @@ class _AbstractTransport(object): def send_packet(self, code, path='', data='', callback=None): packet_text = Packet(code, data).encode_as_string(); self.send(packet_text) - _log.debug('[packet sent] %s', packet_text) def recv_packet(self): try: @@ -127,6 +126,7 @@ class WebsocketTransport(_AbstractTransport): base_url, parser.ENGINE_PROTOCOL, socketIO_session.id) try: + _log.debug("[websocket] Connecting"); self._connection = websocket.create_connection(self._url) except socket.timeout as e: raise ConnectionError(e) @@ -141,17 +141,16 @@ class WebsocketTransport(_AbstractTransport): def send_message(self, message, callback = None): packet_text = message.encode_as_string(); self.send(packet_text) - _log.debug('[packet sent] %s', packet_text) def send_engineio_packet(self, packet, callback=None): packet_text = packet.encode_as_string(for_websocket = True); self.send(packet_text) - _log.debug('[packet sent] %s', packet_text) def send_packet(self, code, path="", data='', callback=None): self.send_message(Message(code, data), callback); def send(self, packet_text): + _log.debug("[websocket] send: " + str(packet_text)); try: self._connection.send(packet_text) except websocket.WebSocketTimeoutException as e: @@ -186,7 +185,8 @@ class WebsocketTransport(_AbstractTransport): raise ConnectionError(e) def close(self): - self._connection.close() + self._connection.close(); + self._connected = False; class XHR_PollingTransport(_AbstractTransport): @@ -207,8 +207,9 @@ class XHR_PollingTransport(_AbstractTransport): @property def _params(self): return dict(t=int(time.time() * 1000)) - + def send(self, packet_text): + _log.debug("[xhr] send: " + str(packet_text)); uri = self._url + "&" + '&'.join("%s=%s" % (k, v) for (k, v) in self._params.iteritems()); response = None; try: From f4a96c72d036292ce1d5111b59843a06fc90f393 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Mon, 22 Dec 2014 22:08:15 -0800 Subject: [PATCH 089/191] Updates to get XHR polling working correctly. Mostly encode/decode issues. --- socketIO_client/__init__.py | 3 +-- socketIO_client/parser.py | 5 ----- socketIO_client/transports.py | 27 ++++++++++++++++++++------- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 30a8643..fd9ebf7 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -178,7 +178,6 @@ class SocketIO(object): def _terminate_heartbeat(self): if self.heartbeat_terminator is not None: self.heartbeat_terminator.set(); - #time.sleep(self.session.heartbeat_interval); self.heartbeat_thread.join(); def define(self, Namespace, path=''): @@ -239,7 +238,7 @@ class SocketIO(object): try: self.reconnect(); except ConnectionError as e: - time.sleep(1); + time.sleep(RETRY_INTERVAL_IN_SECONDS); continue; try: diff --git a/socketIO_client/parser.py b/socketIO_client/parser.py index 39fcb3d..4edd487 100644 --- a/socketIO_client/parser.py +++ b/socketIO_client/parser.py @@ -234,8 +234,3 @@ def decode_packet(packet): else: import ipdb; ipdb.set_trace(); pass; - -def encode_packet_string(code, path, data): - """Encodes packet to be sent to socket.io server. - """ - diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 2c70cf4..b13738a 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -50,12 +50,15 @@ class _AbstractTransport(object): responded = False; while not responded: for packet in self.recv_packet(): - _log.debug("[connect wait] Waiting for confirmation of connect to: %s" % path); - if (packet.type == PacketType.MESSAGE - and packet.payload.type == MessageType.CONNECT - and packet.payload.path == path): - _log.debug("[connect] Connected to path: %s" % path); - responded = True; + if not responded: + _log.debug("[connect wait] Waiting for confirmation of connect to: %s" % path); + if (packet.type == PacketType.MESSAGE + and packet.payload.type == MessageType.CONNECT + and packet.payload.path == path): + _log.debug("[connect] Connected to path: %s" % path); + responded = True; + else: + self._packets.append(packet); else: self.send_packet(PacketType.OPEN, path, data); @@ -167,7 +170,7 @@ class WebsocketTransport(_AbstractTransport): response = self._connection.recv(); try: for packet in parser.decode_response(response): - _log.debug('[websocket packet received] %s', str(packet)); + _log.debug('[websocket] Packet received: %s', str(packet)); yield packet; except AttributeError: _log.warn('[packet error] %s', repr(response)) @@ -207,6 +210,10 @@ class XHR_PollingTransport(_AbstractTransport): @property def _params(self): return dict(t=int(time.time() * 1000)) + + def send_engineio_packet(self, packet, callback=None): + packet_text = packet.encode_as_string(for_websocket = False); + self.send(packet_text) def send(self, packet_text): _log.debug("[xhr] send: " + str(packet_text)); @@ -233,6 +240,11 @@ class XHR_PollingTransport(_AbstractTransport): if self._waiting: return; + # Yield any packets that were not processed before. + for packet in self._packets: + _log.debug('[xhr] Packet received: %s', str(packet)); + yield packet; + self._waiting = True; response = _get_response( self._http_session.get, @@ -245,6 +257,7 @@ class XHR_PollingTransport(_AbstractTransport): return; for packet in parser.decode_response(response): + _log.debug('[xhr] Packet received: %s', str(packet)); yield packet; return From bdb92f8da6a642ce5f54c5a6c2990c3ef08f648c Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Tue, 23 Dec 2014 02:18:33 -0800 Subject: [PATCH 090/191] Added 4 space indents to server_tests.js. Also added less ambiguous method definitions for multiple arg methods --- serve_tests.js | 130 ++++++++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/serve_tests.js b/serve_tests.js index 6c2edfc..90c58cf 100644 --- a/serve_tests.js +++ b/serve_tests.js @@ -1,79 +1,79 @@ var io = require('socket.io').listen(8000); var main = io.of('').on('connection', function(socket) { - socket.on('message', function(data, fn) { - if (fn) { // Client expects a callback - if (data) { - fn(data); - } else { - fn(); - } - } else if (typeof data === 'object') { - socket.json.send(data ? data : 'message_response'); // object or null - } else { - socket.send(data ? data : 'message_response'); // string or '' - } - }); - socket.on('emit', function() { - socket.emit('emit_response'); - }); - socket.on('emit_with_payload', function(payload) { - socket.emit('emit_with_payload_response', payload); - }); - socket.on('emit_with_multiple_payloads', function(payload, payload) { - socket.emit('emit_with_multiple_payloads_response', payload, payload); - }); - socket.on('emit_with_callback', function(fn) { - fn(); - }); - socket.on('emit_with_callback_with_payload', function(fn) { - fn(PAYLOAD); - }); - socket.on('emit_with_callback_with_multiple_payloads', function(fn) { - fn(PAYLOAD, PAYLOAD); - }); - socket.on('emit_with_event', function(payload) { - socket.emit('emit_with_event_response', payload); - }); - socket.on('ack', function(payload) { - socket.emit('ack_response', payload, function(payload) { - socket.emit('ack_callback_response', payload); + socket.on('message', function(data, fn) { + if (fn) { // Client expects a callback + if (data) { + fn(data); + } else { + fn(); + } + } else if (typeof data === 'object') { + socket.json.send(data ? data : 'message_response'); // object or null + } else { + socket.send(data ? data : 'message_response'); // string or '' + } + }); + socket.on('emit', function() { + socket.emit('emit_response'); + }); + socket.on('emit_with_payload', function(payload) { + socket.emit('emit_with_payload_response', payload); + }); + socket.on('emit_with_multiple_payloads', function(payload1, payload2) { + socket.emit('emit_with_multiple_payloads_response', payload1, payload2); + }); + socket.on('emit_with_callback', function(fn) { + fn(); + }); + socket.on('emit_with_callback_with_payload', function(fn) { + fn(PAYLOAD); + }); + socket.on('emit_with_callback_with_multiple_payloads', function(fn) { + fn(PAYLOAD, PAYLOAD); + }); + socket.on('emit_with_event', function(payload) { + socket.emit('emit_with_event_response', payload); + }); + socket.on('ack', function(payload) { + socket.emit('ack_response', payload, function(payload) { + socket.emit('ack_callback_response', payload); + }); + }); + socket.on('aaa', function() { + socket.emit('aaa_response', PAYLOAD); + }); + socket.on('bbb', function(payload, fn) { + if (fn) { + fn(payload); + } + }); + socket.on('wait_with_disconnect', function() { + socket.emit('wait_with_disconnect_response'); }); - }); - socket.on('aaa', function() { - socket.emit('aaa_response', PAYLOAD); - }); - socket.on('bbb', function(payload, fn) { - if (fn) { - fn(payload); - } - }); - socket.on('wait_with_disconnect', function() { - socket.emit('wait_with_disconnect_response'); - }); }); var chat = io.of('/chat').on('connection', function (socket) { - socket.on('emit_with_payload', function(payload) { - socket.emit('emit_with_payload_response', payload); - }); - socket.on('aaa', function() { - socket.emit('aaa_response', 'in chat'); - }); - socket.on('ack', function(payload) { - socket.emit('ack_response', payload, function(payload) { - socket.emit('ack_callback_response', payload); + socket.on('emit_with_payload', function(payload) { + socket.emit('emit_with_payload_response', payload); + }); + socket.on('aaa', function() { + socket.emit('aaa_response', 'in chat'); + }); + socket.on('ack', function(payload) { + socket.emit('ack_response', payload, function(payload) { + socket.emit('ack_callback_response', payload); + }); }); - }); }); var news = io.of('/news').on('connection', function (socket) { - socket.on('emit_with_payload', function(payload) { - socket.emit('emit_with_payload_response', payload); - }); - socket.on('aaa', function() { - socket.emit('aaa_response', 'in news'); - }); + socket.on('emit_with_payload', function(payload) { + socket.emit('emit_with_payload_response', payload); + }); + socket.on('aaa', function() { + socket.emit('aaa_response', 'in news'); + }); }); var PAYLOAD = {'xxx': 'yyy'}; From 66e563acc31c8e4f22b365668dec71d2e798d8c8 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Tue, 23 Dec 2014 02:19:41 -0800 Subject: [PATCH 091/191] Updated implementation to allow callbacks and arg handling that is consistent with reference javascript implementation. --- socketIO_client/__init__.py | 33 ++++++++++++++++++++--------- socketIO_client/transports.py | 40 +++++++++++++++++++---------------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index fd9ebf7..8196327 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -27,7 +27,6 @@ _log = logging.getLogger(__name__) PROTOCOL_VERSION = 1 RETRY_INTERVAL_IN_SECONDS = 1 - class BaseNamespace(object): 'Define client behavior' @@ -46,11 +45,7 @@ class BaseNamespace(object): def emit(self, event, *args, **kw): callback, args = find_callback(args, kw) - - if callback is not None: - _log.warn("Callback was specified but is not supported."); - - self._transport.emit(self.path, event, args, None) + self._transport.emit(self.path, event, args, callback) def disconnect(self): self._transport.disconnect(self.path) @@ -231,7 +226,7 @@ class SocketIO(object): - Omit seconds, i.e. call wait() without arguments, to wait forever. """ warning_screen = _yield_warning_screen(seconds) - for elapsed_time in warning_screen: + for elapsed_time in warning_screen: # We will end up here in the case that we # disconnected. if len(self.reconnect_paths) > 0: @@ -266,6 +261,7 @@ class SocketIO(object): except StopIteration: _log.warn(warning) + self._terminate_heartbeat(); _log.debug("[wait canceled]"); def _process_events(self): @@ -476,13 +472,30 @@ class SocketIO(object): find_event_callback(event)(*args); def _on_ack(self, packet, find_event_callback): - event = packet.payload.message[0]; - args = packet.payload.message[1:] if len(packet.payload.message) > 1 else []; + """Handles ACK from server. + + There are two types of ACKs. The first is when this client + requests that the server responds with an ACK upon execution + of a remote function (specified in the server via + socketio.on()). + + The second type is when the server requests that this client + acknowledges that a local function has been executed. + + Both are handled the same way from the client's standpoint, + but in latter case the server will actually send along the + event name and the args, but they are currently ignored. + + """ + + #event = packet.payload.message[0]; packet_id = packet.payload.id; try: - ack_callback = self._transport.get_ack_callback(packet_id) + ack_callback = self._transport.get_ack_callback(str(packet_id)) except KeyError: + _log.warn("Could not find callback function for packet id: %d" % packet_id); return + args = packet.payload.message[1:] if len(packet.payload.message) > 1 else []; ack_callback(*args) def _on_error(self, packet, find_event_callback): diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index b13738a..18e27ae 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -65,28 +65,30 @@ class _AbstractTransport(object): def send_heartbeat(self): self.send_packet(PacketType.PING) - def message(self, path, data, callback): - if isinstance(data, basestring): - code = 3 - else: - code = 4 - data = json.dumps(data, ensure_ascii=False) - self.send_packet(code, path, data, callback) - def emit(self, path, event, args, callback): - message = Message(MessageType.EVENT, [event, args], path); - self.send_packet(PacketType.MESSAGE, path, message.encode_as_json(), callback) + message_id = self.set_ack_callback(callback) if callback else None; + message = ""; + if len(args) > 0: + data = list(args); + data.insert(0, event); + message = Message(MessageType.EVENT, data, path, message_id = message_id); + else: + message = Message(MessageType.EVENT, [event], path, message_id = message_id); + self.send_packet(PacketType.MESSAGE, path, message.encode_as_json()) def ack(self, path, packet_id, *args): _log.debug("[ack] Sending ACK for packet: %d" % packet_id); - message = Message(MessageType.ACK, "", path, "", packet_id); + data = ""; + if len(args) > 0: + data = args; + message = Message(MessageType.ACK, data, path, "", packet_id); packet = Packet(PacketType.MESSAGE, message); self.send_engineio_packet(packet); def noop(self, path=''): self.send_packet(PacketType.NOOP, path) - def send_packet(self, code, path='', data='', callback=None): + def send_packet(self, code, path='', data=''): packet_text = Packet(code, data).encode_as_string(); self.send(packet_text) @@ -105,11 +107,13 @@ class _AbstractTransport(object): def set_ack_callback(self, callback): 'Set callback to be called after server sends an acknowledgment' self._packet_id += 1 + _log.debug("Setting ACK for packet id: %d (%s) [%s]" % (self._packet_id, str(callback), str(self))); self._callback_by_packet_id[str(self._packet_id)] = callback - return '%s+' % self._packet_id + return self._packet_id def get_ack_callback(self, packet_id): 'Get callback to be called after server sends an acknowledgment' + _log.debug("Searching for ACK for packet id: %s [%s]" % (packet_id, str(self))); callback = self._callback_by_packet_id[packet_id] del self._callback_by_packet_id[packet_id] return callback @@ -141,16 +145,16 @@ class WebsocketTransport(_AbstractTransport): def connected(self): return self._connection.connected - def send_message(self, message, callback = None): + def send_message(self, message): packet_text = message.encode_as_string(); self.send(packet_text) - def send_engineio_packet(self, packet, callback=None): + def send_engineio_packet(self, packet): packet_text = packet.encode_as_string(for_websocket = True); self.send(packet_text) - def send_packet(self, code, path="", data='', callback=None): - self.send_message(Message(code, data), callback); + def send_packet(self, code, path="", data=''): + self.send_message(Message(code, data)); def send(self, packet_text): _log.debug("[websocket] send: " + str(packet_text)); @@ -211,7 +215,7 @@ class XHR_PollingTransport(_AbstractTransport): def _params(self): return dict(t=int(time.time() * 1000)) - def send_engineio_packet(self, packet, callback=None): + def send_engineio_packet(self, packet): packet_text = packet.encode_as_string(for_websocket = False); self.send(packet_text) From 6f8e76adfd6cdeaafc039bdd91193e086ee8ac80 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Tue, 23 Dec 2014 02:20:26 -0800 Subject: [PATCH 092/191] Added check to parser encoder to automatically encode json if the message data is not a string --- socketIO_client/parser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/socketIO_client/parser.py b/socketIO_client/parser.py index 4edd487..27a2d7f 100644 --- a/socketIO_client/parser.py +++ b/socketIO_client/parser.py @@ -98,6 +98,8 @@ class Message(): def encode_as_string(self): """Same as the encode_as_string method except it doesn't encode things as a JSON string""" + if not isinstance(self.message, basestring): + return self.encode_as_json(); data = self.message; if self.id is not None: data = str(self.id) + self.message; From 2361706c4e92e00e7e30418228767d3b8ecfad7c Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Tue, 23 Dec 2014 02:24:52 -0800 Subject: [PATCH 093/191] Updated tests slightly for newer client implementation. Tests on 'message'-like methods have been removed as the newer paradigm considers message and events the same. Timeouts on the non-websocket transports have been lowered to enable faster tests. Explicit transports have been removed from constructors since that has also been removed from the code. --- socketIO_client/tests.py | 60 ++++++++-------------------------------- 1 file changed, 11 insertions(+), 49 deletions(-) diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index dfebecb..ea20282 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -3,8 +3,6 @@ import time from unittest import TestCase from . import SocketIO, BaseNamespace, find_callback -from .transports import TIMEOUT_IN_SECONDS - HOST = 'localhost' PORT = 8000 @@ -42,39 +40,6 @@ class BaseMixin(object): self.assertTrue(namespace.called_on_disconnect) self.assertFalse(self.socketIO.connected) - def test_message(self): - 'Message' - namespace = self.socketIO.define(Namespace) - self.socketIO.message() - self.socketIO.wait(self.wait_time_in_seconds) - self.assertEqual(namespace.response, 'message_response') - - def test_message_with_data(self): - 'Message with data' - namespace = self.socketIO.define(Namespace) - self.socketIO.message(DATA) - self.socketIO.wait(self.wait_time_in_seconds) - self.assertEqual(namespace.response, DATA) - - def test_message_with_payload(self): - 'Message with payload' - namespace = self.socketIO.define(Namespace) - self.socketIO.message(PAYLOAD) - self.socketIO.wait(self.wait_time_in_seconds) - self.assertEqual(namespace.response, PAYLOAD) - - def test_message_with_callback(self): - 'Message with callback' - self.socketIO.message(callback=self.on_response) - self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) - self.assertTrue(self.called_on_response) - - def test_message_with_callback_with_data(self): - 'Message with callback with data' - self.socketIO.message(DATA, self.on_response) - self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) - self.assertTrue(self.called_on_response) - def test_emit(self): 'Emit' namespace = self.socketIO.define(Namespace) @@ -104,22 +69,22 @@ class BaseMixin(object): def test_emit_with_callback(self): 'Emit with callback' - self.socketIO.emit('emit_with_callback', self.on_response) - self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) + self.socketIO.emit('emit_with_callback', callback = self.on_response) + self.socketIO.wait_for_callbacks(seconds = self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit_with_callback_with_payload(self): 'Emit with callback with payload' self.socketIO.emit( 'emit_with_callback_with_payload', self.on_response) - self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) + self.socketIO.wait_for_callbacks(seconds = self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit_with_callback_with_multiple_payloads(self): 'Emit with callback with multiple payloads' self.socketIO.emit( 'emit_with_callback_with_multiple_payloads', self.on_response) - self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) + self.socketIO.wait_for_callbacks(seconds = self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit_with_event(self): @@ -171,30 +136,27 @@ class BaseMixin(object): 'ack_callback_response': (PAYLOAD,), }) - -class Test_WebsocketTransport(TestCase, BaseMixin): +class Test_WebsocketTransport(BaseMixin, TestCase): def setUp(self): super(Test_WebsocketTransport, self).setUp() - self.socketIO = SocketIO(HOST, PORT, transports=['websocket']) + self.socketIO = SocketIO(HOST, PORT) self.wait_time_in_seconds = 0.1 -class Test_XHR_PollingTransport(TestCase, BaseMixin): +class Test_XHR_PollingTransport(BaseMixin, TestCase): def setUp(self): super(Test_XHR_PollingTransport, self).setUp() - self.socketIO = SocketIO(HOST, PORT, transports=['xhr-polling']) - self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1 - + self.socketIO = SocketIO(HOST, PORT) + self.wait_time_in_seconds = 1 class Test_JSONP_PollingTransport(TestCase, BaseMixin): def setUp(self): super(Test_JSONP_PollingTransport, self).setUp() - self.socketIO = SocketIO(HOST, PORT, transports=['jsonp-polling']) - self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1 - + self.socketIO = SocketIO(HOST, PORT, transports = ['jsonp-polling']) + self.wait_time_in_seconds = 1; class Namespace(BaseNamespace): From 455890a7000175826d0d2b680661c5aa3afab2e1 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Tue, 23 Dec 2014 02:37:53 -0800 Subject: [PATCH 094/191] Added more documentation --- socketIO_client/__init__.py | 47 +++++++++++++++++++++++++++++++------ socketIO_client/parser.py | 27 +++++++++++++++++---- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 8196327..a2b6c8c 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -16,7 +16,6 @@ except ImportError: from .exceptions import ConnectionError, TimeoutError, PacketError from .transports import _get_response - _SocketIOSession = namedtuple('_SocketIOSession', [ 'id', 'heartbeat_interval', @@ -24,7 +23,6 @@ _SocketIOSession = namedtuple('_SocketIOSession', [ 'server_supported_transports', ]) _log = logging.getLogger(__name__) -PROTOCOL_VERSION = 1 RETRY_INTERVAL_IN_SECONDS = 1 class BaseNamespace(object): @@ -171,6 +169,9 @@ class SocketIO(object): self._terminate_heartbeat(); def _terminate_heartbeat(self): + """Terminates the heartbeat thread. + + """ if self.heartbeat_terminator is not None: self.heartbeat_terminator.set(); self.heartbeat_thread.join(); @@ -365,6 +366,21 @@ class SocketIO(object): return self.__transport def _upgrade(self): + """Attempts to upgrade the connection to a websocket. + + This method will execute the update process outline here: + https://github.com/Automattic/engine.io-protocol#transport-upgrading + + To summarize, we first send a PING packet with the string + 'probe' appended as data. This signals to the server that we + want to probe the ability to upgrade. If the server has this + functionality, it responds with a PONG packet and the 'probe' + string. + + We then send an UPGRADE packet, restart the heartbeat thread, + and return. + + """ websocket = transports.WebsocketTransport(self.session, self.is_secure, self.base_url, **self.kw); websocket.send_packet(PacketType.PING, "", "probe"); for packet in websocket.recv_packet(): @@ -384,6 +400,19 @@ class SocketIO(object): return websocket; def _start_heartbeat(self, transport): + """Starts the heartbeat thread. + + The heartbeat thread ensures that our connection is never + severed. This effectively spawns a thread that sits in an + infinite loop. The thread waits + self.session.heartbeat_interval / 2 (gleaned from the server) + seconds, then sends a heartbeat packet (PING). + + The thread is implemented using a multiprocessing.Event to + perform the wait, so it doesn't waste any cpu cycles while + it's waiting. + + """ _log.debug("[start heartbeat pacemaker]"); self.heartbeat_terminator = multiprocessing.Event(); self.heartbeat_thread = multiprocessing.Process( @@ -458,10 +487,14 @@ class SocketIO(object): find_event_callback('disconnect')() def _on_event(self, packet, find_event_callback): - # Accoding to the documentation - # (https://github.com/automattic/socket.io-protocol#event), - # the event name is the first entry in the message array, and - # the arguments are the rest of the entries. + """This delegate is called when there is an EVENT packet. + + Accoding to the documentation + (https://github.com/automattic/socket.io-protocol#event), the + event name is the first entry in the message array, and the + arguments are the rest of the entries. + + """ event = packet.payload.message[0]; args = packet.payload.message[1:] if len(packet.payload.message) > 1 else []; @@ -556,7 +589,7 @@ def _yield_elapsed_time(seconds=None): def _get_socketIO_session(is_secure, base_url, **kw): server_url = '%s://%s/?EIO=%d&transport=polling' \ - % ('https' if is_secure else 'http', base_url, parser.ENGINE_PROTOCOL) + % ('https' if is_secure else 'http', base_url, parser.ENGINEIO_PROTOCOL) _log.debug('[session] %s', server_url) try: response = _get_response(requests.get, server_url, **kw) diff --git a/socketIO_client/parser.py b/socketIO_client/parser.py index 27a2d7f..070cf14 100644 --- a/socketIO_client/parser.py +++ b/socketIO_client/parser.py @@ -4,7 +4,7 @@ import json _log = logging.getLogger(__name__) -ENGINE_PROTOCOL = 3; +ENGINEIO_PROTOCOL = 3; class PacketType(Enum): OPEN = 0; @@ -25,6 +25,8 @@ class MessageType(Enum): BINARY_ACK = 6; class Packet(): + """ Represents a 'packet' from the engine.io protocol. + """ def __init__(self, packet_type, payload): self.type = packet_type; self.payload = payload; @@ -33,6 +35,16 @@ class Packet(): return "PACKET{type: " + str(self.type) + ", payload: " + str(self.payload) + "}"; def encode_as_string(self, for_websocket = False): + """Returns the packet encoded according to the engine.io-protocol. + + Reference: (https://github.com/Automattic/engine.io-protocol). + + It's worth noting that websockets have their own framing and + encoding mechanism so if the packet is going to be transmitted + via websockets, we encode it slightly differently per the + documentation. + + """ data = ""; path = ""; if self.type == PacketType.MESSAGE and not isinstance(self.payload, basestring): @@ -54,6 +66,8 @@ class Packet(): return encoded; class Message(): + """Represents a 'message' from the socket.io protocol. + """ def __init__(self, message_type, message, path = "", attachments = "", message_id = None): self.type = message_type; if isinstance(message, basestring): @@ -84,9 +98,8 @@ class Message(): "}"; def encode_as_json(self): - """Encodes a Message to be sent to socket.io server. - - Assumes the message payload will be dumped as a json string. + """Encodes a JSON Message to be sent to socket.io server. + """ data = json.dumps(self.message); if self.id is not None: @@ -97,7 +110,11 @@ class Message(): return str(self.type) + self.path + "," + data; def encode_as_string(self): - """Same as the encode_as_string method except it doesn't encode things as a JSON string""" + """Encodes a Message to be to a socket.io server. + + This will call encode_as_json if the message is not a string. + + """ if not isinstance(self.message, basestring): return self.encode_as_json(); data = self.message; From 6932d4c2d1b89d57a4c6524d5526f2c064cdf908 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Tue, 23 Dec 2014 19:15:50 -0800 Subject: [PATCH 095/191] Added connection error handling for all send / recv related methods so that we can reconnect on all connection errors --- socketIO_client/__init__.py | 103 ++++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 39 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index a2b6c8c..5026cb9 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -28,8 +28,8 @@ RETRY_INTERVAL_IN_SECONDS = 1 class BaseNamespace(object): 'Define client behavior' - def __init__(self, _transport, path): - self._transport = _transport + def __init__(self, client, path): + self._client = client; self.path = path self._callback_by_event = {} self.initialize() @@ -38,15 +38,12 @@ class BaseNamespace(object): 'Initialize custom variables here; you can override this method' pass - def message(self, data='', callback=None): - self._transport.message(self.path, data, callback) - def emit(self, event, *args, **kw): callback, args = find_callback(args, kw) - self._transport.emit(self.path, event, args, callback) + self._client.emit(self.path, event, args, callback) def disconnect(self): - self._transport.disconnect(self.path) + self._client.disconnect(self.path) def on(self, event, callback): 'Define a callback to handle a custom event emitted by the server' @@ -168,31 +165,31 @@ class SocketIO(object): self.disconnect() self._terminate_heartbeat(); - def _terminate_heartbeat(self): - """Terminates the heartbeat thread. - - """ - if self.heartbeat_terminator is not None: - self.heartbeat_terminator.set(); - self.heartbeat_thread.join(); - def define(self, Namespace, path=''): - if path: - self._transport.connect(path) - namespace = Namespace(self._transport, path) + _log.debug("[define] Path: %s" % path); + namespace = Namespace(self, path) self._namespace_by_path[path] = namespace + + if path: + try: + self._transport.connect(path); + except ConnectionError as e: + _log.warn("[define] Connection error: %s" % str(e)); + self._handle_severed_connection(); + return namespace def on(self, event, callback, path=''): return self.get_namespace(path).on(event, callback) - def message(self, data='', callback=None, path=''): - self._transport.message(path, data, callback) - def emit(self, event, *args, **kw): path = kw.get('path', '') callback, args = find_callback(args, kw) - self._transport.emit(path, event, args, callback) + try: + self._transport.emit(path, event, args, callback); + except ConnectionError as e: + _log.warn("[emit] Connection error: %s" % str(e)); + self._handle_severed_connection(); def reconnect(self): """Reconnects the client. @@ -205,8 +202,8 @@ class SocketIO(object): # Reconnect to the server. if self.__transport is not None: - self.__transport.close(); - self.__transport = self._get_transport(); + self.__transport.close(); + self.__transport = None; for path in self.reconnect_paths: # We avoid reconnecting to the default namespace because @@ -217,10 +214,21 @@ class SocketIO(object): self._transport.connect(path); # Restore paths. self._namespace_by_path = copy.copy(self.reconnect_paths); - for namespace in self._namespace_by_path: - self._namespace_by_path[namespace]._transport = self.__transport; self.reconnect_paths = {}; + def _handle_severed_connection(self): + """Handles severed (unexpectedly terminated) connections + + """ + self._terminate_heartbeat(); + if len(self.reconnect_paths) is 0: + self.reconnect_paths = copy.copy(self._namespace_by_path); + + self._terminate_heartbeat(); + + for namespace in self.reconnect_paths: + self.disconnect(namespace); + def wait(self, seconds=None, for_callbacks=False): """Wait in a loop and process events as defined in the namespaces. @@ -247,16 +255,7 @@ class SocketIO(object): except ConnectionError as e: try: - # This is where we end up if the connection was - # severed. The client will disconnect here. - if len(self.reconnect_paths) is 0: - self.reconnect_paths = copy.copy(self._namespace_by_path); - - self._terminate_heartbeat(); - - for namespace in self.reconnect_paths: - self.disconnect(namespace); - + self._handle_severed_connection(); warning = Exception('[connection error] %s' % e) warning_screen.throw(warning) except StopIteration: @@ -332,7 +331,11 @@ class SocketIO(object): def disconnect(self, path=''): if self.connected: - self._transport.disconnect(path) + try: + self._transport.disconnect(path) + except ConnectionError as e: + _log.warn("[disconnect] Connection error: %s" % str(e)); + namespace = self._namespace_by_path[path] namespace.on_disconnect() if path: @@ -340,7 +343,7 @@ class SocketIO(object): @property def connected(self): - return self.__transport.connected if self.__transport is not None else False; + return self.__transport.connected if self.__transport is not None else False; @property def _transport(self): @@ -399,6 +402,14 @@ class SocketIO(object): self._start_heartbeat(websocket); return websocket; + def _terminate_heartbeat(self): + """Terminates the heartbeat thread. + + """ + if self.heartbeat_terminator is not None: + self.heartbeat_terminator.set(); + self.heartbeat_thread.join(); + def _start_heartbeat(self, transport): """Starts the heartbeat thread. @@ -413,6 +424,11 @@ class SocketIO(object): it's waiting. """ + + if self.heartbeat_thread is not None and not self.heartbeat_terminator.is_set(): + _log.warn("[start hearbeat] heartbeat already started... terminating old heartbeat"); + self._terminate_heartbeat(); + _log.debug("[start heartbeat pacemaker]"); self.heartbeat_terminator = multiprocessing.Event(); self.heartbeat_thread = multiprocessing.Process( @@ -542,8 +558,14 @@ class SocketIO(object): def _prepare_to_send_ack(self, path, packet_id): 'Return function that acknowledges the server' - return lambda *args: self._transport.ack(path, packet_id, *args) + return lambda *args: _send_ack(self, path, packet_id, *args); +def _send_ack(socketio, path, packet_id, *args): + try: + socketio._transport.ack(path, packet_id, *args); + except ConnectionError as e: + _log.warn("[send ack] Connection error: %s" % str(e)); + socketio._handle_severed_connection(); def find_callback(args, kw=None): 'Return callback whether passed as a last argument or as a keyword' @@ -618,6 +640,9 @@ def _make_heartbeat_pacemaker(terminator, transport, heartbeat_interval): _log.debug("[hearbeat]"); try: transport.send_heartbeat(); + except requests.exceptions.ConnectionError as e: + message = "[heartbeat] disconnected while sending PING"; + _log.warn(message); except: pass; _log.debug("[heartbeat terminated]"); From a1f37e8e60ec94f3a857d95248bae62adb9fd4ff Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Tue, 23 Dec 2014 21:38:42 -0800 Subject: [PATCH 096/191] Added an event queue so that emitted events that failed can be automatically retried upon reconnection. Also fixed a potential infinite loop bug wrt reconnecting automatically --- socketIO_client/__init__.py | 87 +++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 5026cb9..6c5b41c 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -38,9 +38,8 @@ class BaseNamespace(object): 'Initialize custom variables here; you can override this method' pass - def emit(self, event, *args, **kw): - callback, args = find_callback(args, kw) - self._client.emit(self.path, event, args, callback) + def emit(self, event_name, *args, **kw): + self._client.emit(event_name, path = self.path, *args, **kw) def disconnect(self): self._client.disconnect(self.path) @@ -110,6 +109,15 @@ class BaseNamespace(object): 'on_' + event.replace(' ', '_'), lambda *args: self.on_event(event, *args)) +class SocketIOEvent(object): + def __init__(self, path, name, args, callback): + self.path = path; + self.name = name; + self.args = args; + self.callback = callback; + + def __str__(self): + return str(self.path) + "/" + str(self.name) + "(" + str(self.args) + ")(" + str(self.callback) + ")"; class SocketIO(object): """Create a socket.io client that connects to a socket.io server @@ -152,6 +160,12 @@ class SocketIO(object): # This sets of a chain of events that attempts to connect to # the server at the base namespace. self.define(Namespace) + self._transport.connect(""); + + # Events that fail to emit due to connection errors will be + # placed in this 'queue' and re-sent automatically upon + # reconnect. + self._event_retry_queue = []; def __enter__(self): _log.debug("[enter]"); @@ -182,11 +196,24 @@ class SocketIO(object): def on(self, event, callback, path=''): return self.get_namespace(path).on(event, callback) - def emit(self, event, *args, **kw): + def _emit_event(self, event): + """Emits an Emittable object. + + This function enables automatically re-emitting events that + failed due to connection errors. + + """ + self._transport.emit(event.path, event.name, event.args, event.callback); + + def emit(self, event_name, *args, **kw): path = kw.get('path', '') callback, args = find_callback(args, kw) + event = SocketIOEvent(path, event_name, args, callback); + self._event_retry_queue.append(event); try: - self._transport.emit(path, event, args, callback); + #import ipdb; ipdb.set_trace(); + self._emit_event(event); + self._event_retry_queue.pop(); except ConnectionError as e: _log.warn("[emit] Connection error: %s" % str(e)); self._handle_severed_connection(); @@ -205,6 +232,11 @@ class SocketIO(object): self.__transport.close(); self.__transport = None; + # We call the _create_transport directly ahead of the loop + # below so that self._tranport will not result in an infinite + # loop. + self._create_transport(); + for path in self.reconnect_paths: # We avoid reconnecting to the default namespace because # socketIO_client connects to that already. @@ -216,6 +248,11 @@ class SocketIO(object): self._namespace_by_path = copy.copy(self.reconnect_paths); self.reconnect_paths = {}; + # Send any pending events. + for event in self._event_retry_queue: + _log.debug("[reconnect] Re-emitting event: %s" % str(event)); + self._emit_event(event); + def _handle_severed_connection(self): """Handles severed (unexpectedly terminated) connections @@ -225,9 +262,10 @@ class SocketIO(object): self.reconnect_paths = copy.copy(self._namespace_by_path); self._terminate_heartbeat(); + self.__transport = None; for namespace in self.reconnect_paths: - self.disconnect(namespace); + self.disconnect(namespace, skip_transport_disconnect = True); def wait(self, seconds=None, for_callbacks=False): """Wait in a loop and process events as defined in the namespaces. @@ -235,16 +273,7 @@ class SocketIO(object): - Omit seconds, i.e. call wait() without arguments, to wait forever. """ warning_screen = _yield_warning_screen(seconds) - for elapsed_time in warning_screen: - # We will end up here in the case that we - # disconnected. - if len(self.reconnect_paths) > 0: - try: - self.reconnect(); - except ConnectionError as e: - time.sleep(RETRY_INTERVAL_IN_SECONDS); - continue; - + for elapsed_time in warning_screen: try: try: self._process_events() @@ -329,15 +358,15 @@ class SocketIO(object): def wait_for_callbacks(self, seconds=None): self.wait(seconds, for_callbacks=True) - def disconnect(self, path=''): - if self.connected: + def disconnect(self, path='', skip_transport_disconnect = False): + if self.connected and not skip_transport_disconnect: try: self._transport.disconnect(path) except ConnectionError as e: _log.warn("[disconnect] Connection error: %s" % str(e)); - namespace = self._namespace_by_path[path] - namespace.on_disconnect() + namespace = self._namespace_by_path[path] + namespace.on_disconnect() if path: del self._namespace_by_path[path] @@ -345,6 +374,10 @@ class SocketIO(object): def connected(self): return self.__transport.connected if self.__transport is not None else False; + def _create_transport(self): + _log.debug("[create transport]"); + self.__transport = self._get_transport(); + @property def _transport(self): try: @@ -355,8 +388,7 @@ class SocketIO(object): warning_screen = _yield_warning_screen(seconds=None) for elapsed_time in warning_screen: try: - _log.debug("[create transport]"); - self.__transport = self._get_transport() + self._create_transport(); break except ConnectionError as e: if not self.wait_for_connection: @@ -366,6 +398,17 @@ class SocketIO(object): warning_screen.throw(warning) except StopIteration: _log.warn(warning) + + continue; + # If we disconnected before, self.reconnected_paths will be + # non-empty. + while len(self.reconnect_paths) > 0: + try: + self.reconnect(); + except ConnectionError as e: + time.sleep(RETRY_INTERVAL_IN_SECONDS); + continue; + return self.__transport def _upgrade(self): From f06bf8e7364d0b9b0b99679c95eb9ed50eff1559 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Tue, 23 Dec 2014 21:39:26 -0800 Subject: [PATCH 097/191] Fixed a typo bug. Also added better disconnect handling in the websockets send method --- socketIO_client/transports.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 18e27ae..96a4045 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -130,7 +130,7 @@ class WebsocketTransport(_AbstractTransport): self._url = '%s://%s/?EIO=%d&transport=websocket&sid=%s' % ( 'wss' if is_secure else 'ws', - base_url, parser.ENGINE_PROTOCOL, socketIO_session.id) + base_url, parser.ENGINEIO_PROTOCOL, socketIO_session.id) try: _log.debug("[websocket] Connecting"); @@ -159,11 +159,16 @@ class WebsocketTransport(_AbstractTransport): def send(self, packet_text): _log.debug("[websocket] send: " + str(packet_text)); try: - self._connection.send(packet_text) + self._connection.ping(); + self._connection.send(packet_text); except websocket.WebSocketTimeoutException as e: message = 'timed out while sending %s (%s)' % (packet_text, e) _log.warn(message) raise TimeoutError(e) + except websocket.WebSocketConnectionClosedException as e: + message = 'disconnected while sending %s (%s)' % (packet_text, e); + _log.warn(message); + raise ConnectionError(message); except socket.error as e: message = 'disconnected while sending %s (%s)' % (packet_text, e) _log.warn(message) @@ -202,7 +207,7 @@ class XHR_PollingTransport(_AbstractTransport): super(XHR_PollingTransport, self).__init__() self._url = '%s://%s/?EIO=%d&transport=polling&sid=%s' % ( 'https' if is_secure else 'http', - base_url, parser.ENGINE_PROTOCOL, socketIO_session.id) + base_url, parser.ENGINEIO_PROTOCOL, socketIO_session.id) self._connected = True self._http_session = _prepare_http_session(kw) self._waiting = False; From dc8e8677410573fc009b2d3f17259f10d49f3231 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Tue, 23 Dec 2014 21:39:48 -0800 Subject: [PATCH 098/191] Added tests that verify restart functionality is working as expected --- socketIO_client/tests.py | 68 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index ea20282..a0e6223 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -1,4 +1,5 @@ import logging +from subprocess import check_call import time from unittest import TestCase @@ -15,6 +16,9 @@ class BaseMixin(object): def setUp(self): self.called_on_response = False + + # Start the server if it's not already started. + check_call("./start-test-server.sh"); def tearDown(self): del self.socketIO @@ -27,6 +31,65 @@ class BaseMixin(object): self.assertEqual(arg, DATA) self.called_on_response = True + def test_server_dies_during_define(self): + 'Server dies in the middle of a send' + self.socketIO.wait(self.wait_time_in_seconds); + self.assertTrue(self.socketIO.connected); + + check_call("./kill-test-server.sh"); + + news_namespace = self.socketIO.define(Namespace, '/news'); + self.assertFalse(self.socketIO.connected); + + check_call("./start-test-server.sh"); + + main_namespace = self.socketIO.define(Namespace); + chat_namespace = self.socketIO.define(Namespace, '/chat'); + news_namespace = self.socketIO.define(Namespace, '/news'); + news_namespace.emit('emit_with_payload', PAYLOAD); + + self.socketIO.wait(self.wait_time_in_seconds); + self.assertEqual(main_namespace.args_by_event, {}); + self.assertEqual(chat_namespace.args_by_event, {}); + self.assertEqual(news_namespace.args_by_event, { + 'emit_with_payload_response': (PAYLOAD,), + }); + + def test_server_dies_during_emit_and_re_emit(self): + 'Server dies in the middle of a send' + namespace = self.socketIO.define(Namespace); + + self.socketIO.wait(self.wait_time_in_seconds); + self.assertTrue(self.socketIO.connected); + + check_call("./kill-test-server.sh"); + + self.socketIO.emit("emit"); + self.assertFalse(self.socketIO.connected); + check_call("./start-test-server.sh"); + + #self.socketIO.emit("emit"); + self.socketIO.wait(self.wait_time_in_seconds); + self.assertEqual(namespace.args_by_event, { + 'emit_response': (), + }); + + def test_server_restart(self): + 'Server restart' + self.assertTrue(self.socketIO.connected); + + check_call("./kill-test-server.sh"); + time.sleep(2); + self.socketIO.wait(self.wait_time_in_seconds) + + self.assertFalse(self.socketIO.connected); + + check_call("./start-test-server.sh"); + time.sleep(2); + self.socketIO.wait(self.wait_time_in_seconds); + + self.assertTrue(self.socketIO.connected); + def test_disconnect(self): 'Disconnect' self.assertTrue(self.socketIO.connected) @@ -143,21 +206,20 @@ class Test_WebsocketTransport(BaseMixin, TestCase): self.socketIO = SocketIO(HOST, PORT) self.wait_time_in_seconds = 0.1 - class Test_XHR_PollingTransport(BaseMixin, TestCase): def setUp(self): super(Test_XHR_PollingTransport, self).setUp() self.socketIO = SocketIO(HOST, PORT) self.wait_time_in_seconds = 1 - +""" class Test_JSONP_PollingTransport(TestCase, BaseMixin): def setUp(self): super(Test_JSONP_PollingTransport, self).setUp() self.socketIO = SocketIO(HOST, PORT, transports = ['jsonp-polling']) self.wait_time_in_seconds = 1; - +""" class Namespace(BaseNamespace): def initialize(self): From b059cdf02c336894ff3b5387163d2f90b65cb609 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Thu, 25 Dec 2014 14:06:32 -0800 Subject: [PATCH 099/191] Re-arranged the call order of connecting to avoid interrupted connections --- socketIO_client/__init__.py | 46 ++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 6c5b41c..3793c60 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -24,6 +24,7 @@ _SocketIOSession = namedtuple('_SocketIOSession', [ ]) _log = logging.getLogger(__name__) RETRY_INTERVAL_IN_SECONDS = 1 +MAX_UPGRADE_RETRIES = 3; class BaseNamespace(object): 'Define client behavior' @@ -75,15 +76,15 @@ class BaseNamespace(object): def on_noop(self): 'Called after server sends a noop; you can override this method' - _log.info('%s [noop]', self.path) + _log.debug('%s [noop]', self.path) def on_ping(self): 'Called after server sends a ping; you can override this method' - _log.info('%s [ping]', self.path) + _log.debug('%s [ping]', self.path) def on_pong(self): 'Called after server sends a pong; you can override this method' - _log.info('%s [pong]', self.path) + _log.debug('%s [pong]', self.path) def on_open(self, *args): _log.info('%s [open] %s', self.path, args) @@ -434,16 +435,15 @@ class SocketIO(object): if packet.type == PacketType.PONG: _log.debug("[PONG] %s" % repr(packet)); - self._terminate_heartbeat(); - # Technically we would need to pause the current # transport (which should be polling in this # implementation), but since we haven't actually # started a polling yet, we can upgrade without that. _log.debug("[upgrading] Sending upgrade request"); websocket.send_packet(PacketType.UPGRADE); - self._start_heartbeat(websocket); return websocket; + else: + self._process_packet(packet) def _terminate_heartbeat(self): """Terminates the heartbeat thread. @@ -484,26 +484,33 @@ class SocketIO(object): # Negotiate initial transport transport = transports.XHR_PollingTransport(self.session, self.is_secure, self.base_url, **self.kw); - # Update namespaces - for path, namespace in self._namespace_by_path.items(): - namespace._transport = transport - transport.connect(path) - transport.set_timeout(self.session.connection_timeout); - # Start the heartbeat pacemaker (PING). - self._start_heartbeat(transport); - # If websocket is available, upgrade to it immediately. # TODO(sean): We could run this on a separate thread for # maximum efficiency although that would require some # synchronization to ensure buffers are flushed, etc. + num_retries = 0; if "websocket" in self.session.server_supported_transports: - try: - return self._upgrade(); - except: - _log.warn("[websocket] Failed to upgrade to websocket") - pass; + while num_retries < MAX_UPGRADE_RETRIES: + try: + transport = self._upgrade(); + break; + except: + pass; + time.sleep(1); + num_retries += 1; + + if num_retries == MAX_UPGRADE_RETRIES: + _log.warn("[websocket] Failed to upgrade to websocket"); + + # Update namespaces + for path, namespace in self._namespace_by_path.items(): + namespace._transport = transport + transport.connect(path) + + # Start the heartbeat pacemaker (PING). + self._start_heartbeat(transport); return transport @@ -669,6 +676,7 @@ def _get_socketIO_session(is_secure, base_url, **kw): return None; handshake = json.loads(packet.payload); + _log.info("socket.io client connected to: %s" % base_url); return _SocketIOSession( id = handshake["sid"], From 5af21af575968eecb82fc20836b22f14b2e13191 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Thu, 25 Dec 2014 22:52:47 -0800 Subject: [PATCH 100/191] Small updates that fix a couple of connection issues --- socketIO_client/__init__.py | 11 ++++++++--- socketIO_client/transports.py | 5 +++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3793c60..2b45c25 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -161,7 +161,6 @@ class SocketIO(object): # This sets of a chain of events that attempts to connect to # the server at the base namespace. self.define(Namespace) - self._transport.connect(""); # Events that fail to emit due to connection errors will be # placed in this 'queue' and re-sent automatically upon @@ -430,6 +429,7 @@ class SocketIO(object): """ websocket = transports.WebsocketTransport(self.session, self.is_secure, self.base_url, **self.kw); websocket.send_packet(PacketType.PING, "", "probe"); + for packet in websocket.recv_packet(): _log.debug("[websocket] Packet: %s" % str(packet)); if packet.type == PacketType.PONG: @@ -486,6 +486,11 @@ class SocketIO(object): transport = transports.XHR_PollingTransport(self.session, self.is_secure, self.base_url, **self.kw); transport.set_timeout(self.session.connection_timeout); + # Wait for the response that we connected. + packet = None; + while packet == None or packet.type != PacketType.MESSAGE or packet.payload.type != MessageType.CONNECT: + packet = transport.recv().next(); + # If websocket is available, upgrade to it immediately. # TODO(sean): We could run this on a separate thread for # maximum efficiency although that would require some @@ -496,7 +501,7 @@ class SocketIO(object): try: transport = self._upgrade(); break; - except: + except Exception, e: pass; time.sleep(1); num_retries += 1; @@ -504,7 +509,7 @@ class SocketIO(object): if num_retries == MAX_UPGRADE_RETRIES: _log.warn("[websocket] Failed to upgrade to websocket"); - # Update namespaces + # Update namespaces for path, namespace in self._namespace_by_path.items(): namespace._transport = transport transport.connect(path) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 96a4045..f62e997 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -133,12 +133,13 @@ class WebsocketTransport(_AbstractTransport): base_url, parser.ENGINEIO_PROTOCOL, socketIO_session.id) try: - _log.debug("[websocket] Connecting"); - self._connection = websocket.create_connection(self._url) + _log.debug("[websocket] Connecting to: %s" % self._url); + self._connection = websocket.create_connection(self._url); except socket.timeout as e: raise ConnectionError(e) except socket.error as e: raise ConnectionError(e) + self._connection.settimeout(TIMEOUT_IN_SECONDS) @property From ef64649a2a4fe22a72dfdd2de96f1b1d5970c15e Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Thu, 25 Dec 2014 23:27:19 -0800 Subject: [PATCH 101/191] Added support for forcing websocket connections if available. Also fixed a small bug that seemed to be causing a race condition on first connections due to a failure to consume a packet on opening the default namespace --- socketIO_client/__init__.py | 12 +++++++++--- socketIO_client/transports.py | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 2b45c25..8188bec 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -138,11 +138,16 @@ class SocketIO(object): """ def __init__( - self, host, port=None, Namespace=BaseNamespace, - wait_for_connection=True, **kw): + self, host, + port = None, + Namespace = BaseNamespace, + wait_for_connection = True, + force_websockets_if_available = True, + **kw): self.is_secure, self.base_url = _parse_host(host, port) self.wait_for_connection = wait_for_connection self._namespace_by_path = {} + self.force_websockets_if_available = force_websockets_if_available; self.kw = kw self.__transport = None; @@ -161,6 +166,7 @@ class SocketIO(object): # This sets of a chain of events that attempts to connect to # the server at the base namespace. self.define(Namespace) + self._transport.connect(""); # Events that fail to emit due to connection errors will be # placed in this 'queue' and re-sent automatically upon @@ -497,7 +503,7 @@ class SocketIO(object): # synchronization to ensure buffers are flushed, etc. num_retries = 0; if "websocket" in self.session.server_supported_transports: - while num_retries < MAX_UPGRADE_RETRIES: + while num_retries < MAX_UPGRADE_RETRIES or self.force_websockets_if_available: try: transport = self._upgrade(); break; diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index f62e997..ad88ebb 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -61,6 +61,7 @@ class _AbstractTransport(object): self._packets.append(packet); else: self.send_packet(PacketType.OPEN, path, data); + self.recv().next(); def send_heartbeat(self): self.send_packet(PacketType.PING) From f44485d3e2369a8628e9dd388e59438c4a3201bc Mon Sep 17 00:00:00 2001 From: Merlijn van Deen Date: Tue, 30 Dec 2014 17:37:37 +0100 Subject: [PATCH 102/191] Python 3: use urllib.parse.urlparse --- socketIO_client/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index e966e3a..360e850 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -4,7 +4,7 @@ import requests import time from collections import namedtuple try: - from urllib import parse as parse_url + from urllib.parse import urlparse as parse_url except ImportError: from urlparse import urlparse as parse_url From 2a43420e1b503100ea51cbe44d4db6f3b447183c Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Wed, 7 Jan 2015 13:14:19 -0800 Subject: [PATCH 103/191] Added scripts to start and kill test server --- kill-test-server.sh | 18 ++++++++++++++++++ start-test-server.sh | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100755 kill-test-server.sh create mode 100755 start-test-server.sh diff --git a/kill-test-server.sh b/kill-test-server.sh new file mode 100755 index 0000000..6823405 --- /dev/null +++ b/kill-test-server.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +pid=$(ps ax | grep node | grep serve_tests | grep -v grep | awk '{print $1}'); +if [[ "$pid" == "" ]]; then + echo "Server not started." + exit 0; +fi + +kill ${pid} + +# Ensure it's dead... Jim +pid=$(ps ax | grep node | grep serve_tests | grep -v grep | awk '{print $1}'); +if [[ "$pid" != "" ]]; then + echo "Server didn't die :(." + exit 1; +else + exit 0; +fi diff --git a/start-test-server.sh b/start-test-server.sh new file mode 100755 index 0000000..05f832b --- /dev/null +++ b/start-test-server.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +pid=$(ps ax | grep node | grep serve_tests | grep -v grep | awk '{print $1}'); +if [[ "$pid" != "" ]]; then + #echo "Server already started." + exit 0; +fi + +dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + +# Start the server (pipe output to server.log) +node ${dir}/serve_tests.js &> ${dir}/server.log & +pid=$!; + +# Wait a second so the server has a chance to start. +sleep 1; + +check=$(ps ax | grep ${pid} | grep -v grep); + +if [[ "$check" == "" ]]; then + echo "Server failed to start"; + exit 1; +else + exit 0; +fi From b9e63ff7cd2ed4ab08d6ece197572cef6f951218 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 25 Jan 2015 10:17:03 +0900 Subject: [PATCH 104/191] Save before flight --- TODO.goals | 14 ++++++++++-- experiments/client2.py | 9 ++++---- experiments/socketIO_client/__init__.py | 29 +++++++++++++++++++++---- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/TODO.goals b/TODO.goals index d63b1d2..ff6edf1 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,2 +1,12 @@ -# US/Pacific 11/19/2013 -Include graingert's pull request on travis.yml +Check that SSL websocket connections work properly +Use continuous integration with TravisCI +Do not connect to blank namespace if overriding default namespace exists #40 +Upgrade to socket.io 1.2 #41 +Write a unit test to make sure that the client can disconnect properly #42 +Check that setting stream=True makes sure that we receive all server events #45 +Revive heartbeat as separate process thanks to sarietta +Credit everyone who took the time to submit an issue or pull request +Support Python 3 + Use urllib.parse.urlparse +Merge sarietta's pull request +Add __version__ diff --git a/experiments/client2.py b/experiments/client2.py index 9b91509..14c0d7a 100644 --- a/experiments/client2.py +++ b/experiments/client2.py @@ -1,10 +1,11 @@ from socketIO_client import SocketIO -def on_news(self, data): - print(data) - self.emit('my other event', {'my': 'data'}) +# def on_news(self, data): + # print(data) + # self.emit('my other event', {'my': 'data'}) s = SocketIO('localhost', 9000) -s.on('news', on_news) +s.emit('whee') +# s.on('news', on_news) diff --git a/experiments/socketIO_client/__init__.py b/experiments/socketIO_client/__init__.py index cd94394..52413f9 100644 --- a/experiments/socketIO_client/__init__.py +++ b/experiments/socketIO_client/__init__.py @@ -11,8 +11,8 @@ class EngineIO(object): def __init__(self, host, port): url = 'http://%s:%s/%s/' % (host, port, self._path) - session = requests.Session() - response = session.get(url, params={ + self.session = requests.Session() + response = self.session.get(url, params={ 'EIO': self._engine_io_protocol, 'transport': 'polling', 't': self._get_timestamp(), @@ -21,12 +21,13 @@ class EngineIO(object): packet_type, packet = packs[0] assert packet_type == 0 packet_json = json.loads(packet) + self._session_id = packet_json['sid'] print(packet_json) - response = session.get(url, params={ + response = self.session.get(url, params={ 'EIO': self._engine_io_protocol, 'transport': 'polling', 't': self._get_timestamp(), - 'sid': packet_json['sid'], + 'sid': self._session_id, }) packs = _decode_content(response.content) for packet_type, packet in packs: @@ -39,6 +40,17 @@ class EngineIO(object): self._request_index += 1 return timestamp + def _message(self, packet): + packet_type = 4 + response = self.session.post(self.url, params={ + 'EIO': self._engine_io_protocol, + 'transport': 'polling', + 't': self._get_timestamp(), + 'sid': self._session_id, + }, data=_encode_content([(packet_type, packet)]), headers={ + 'content-type': 'application/octet-stream', + }) + class SocketIO(EngineIO): @@ -51,6 +63,11 @@ class SocketIO(EngineIO): def on(self, event, callback): pass + def emit(self, event): + packet_type = 2 + packet = json.dumps([event]) + self._message(str(packet_type) + packet) + def _decode_content(content): packs = [] @@ -81,3 +98,7 @@ def _read_packet(content, index, packet_length): index += 1 packet = content[index:index + packet_length] return index + packet_length, packet + + +def _encode_content(packs): + pass From 8e23fc73a5582d9a9b8ff2fcf12da2165650f295 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Sat, 31 Jan 2015 21:54:04 -0800 Subject: [PATCH 105/191] Switched from multiprocessing to thread --- socketIO_client/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 8188bec..3a8ecee 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -2,10 +2,10 @@ from collections import namedtuple import copy import logging import json -import multiprocessing import parser from parser import Message, Packet, MessageType, PacketType import requests +import threading import time try: @@ -479,10 +479,11 @@ class SocketIO(object): self._terminate_heartbeat(); _log.debug("[start heartbeat pacemaker]"); - self.heartbeat_terminator = multiprocessing.Event(); - self.heartbeat_thread = multiprocessing.Process( + self.heartbeat_terminator = threading.Event(); + self.heartbeat_thread = threading.Thread( target = _make_heartbeat_pacemaker, args = (self.heartbeat_terminator, transport, self.session.heartbeat_interval / 2)); + self.heartbeat_thread.daemon = True; self.heartbeat_thread.start(); def _get_transport(self): From cfbaaa21eb5d821481a8a994bf022732841edf23 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Tue, 10 Feb 2015 22:04:38 -0500 Subject: [PATCH 106/191] Update goals --- TODO.goals | 28 +++++++++++++++++++--------- setup.cfg | 10 ++++++---- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/TODO.goals b/TODO.goals index ff6edf1..e3f90f9 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,12 +1,22 @@ -Check that SSL websocket connections work properly -Use continuous integration with TravisCI -Do not connect to blank namespace if overriding default namespace exists #40 -Upgrade to socket.io 1.2 #41 -Write a unit test to make sure that the client can disconnect properly #42 Check that setting stream=True makes sure that we receive all server events #45 -Revive heartbeat as separate process thanks to sarietta -Credit everyone who took the time to submit an issue or pull request -Support Python 3 +Use HTTP headers for Websocket connection #44 +Do not connect to blank namespace if overriding default namespace exists #40 +Pass wait timeout to underlying transports #34 +Make heartbeat time independent from elapsed time #38 + Check that heartbeats are sent even with short wait time #64 + Revive heartbeat as separate process thanks to sarietta +Restore namespace as separate process #50 +Check that we have the correct version of websocket #62 +Fall back to next transport #48 +Check that SSL websocket connections work properly #54 +Write a unit test to make sure that the client can disconnect properly #42 #46 +Make sure that on_reconnect works #61 +Check that the library reconnects despite server restart #63 #53 +Upgrade to socket.io 1.2 #41 #52 + Merge sarietta's pull request +Implement rooms #65 +Run under Python 3 #51 Use urllib.parse.urlparse -Merge sarietta's pull request +Use continuous integration with TravisCI +Credit everyone who took the time to submit an issue or pull request Add __version__ diff --git a/setup.cfg b/setup.cfg index 8a2014f..468f360 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,7 @@ [nosetests] -detailed-errors=TRUE -with-coverage=TRUE -cover-package=socketIO_client -cover-erase=TRUE +detailed-errors = TRUE +with-coverage = TRUE +cover-package = socketIO_client +cover-erase = TRUE + +[easy_install] From fc3a8b5e32d276dd24ea6beba1081ad2a53f51c8 Mon Sep 17 00:00:00 2001 From: Adam Kecer Date: Wed, 11 Feb 2015 18:45:23 +0100 Subject: [PATCH 107/191] Update transports.py Under p3k, this code throws NameError (because of the unicode() function) This fix should be valid under both python2 and python3 --- socketIO_client/transports.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 9172fb1..b33754a 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -68,7 +68,10 @@ class _AbstractTransport(object): def send_packet(self, code, path='', data='', callback=None): packet_id = self.set_ack_callback(callback) if callback else '' - packet_parts = str(code), packet_id, path, unicode(data).encode('utf-8') + try: + packet_parts = str(code), packet_id, path, unicode(data).encode('utf-8') + except NameError: # fix for p3k + packet_parts = str(code), packet_id, path, data packet_text = ':'.join(packet_parts) self.send(packet_text) self._log(logging.DEBUG, '[packet sent] %s', packet_text) From 5f7937dc1e892a267f3f9a7d1151504e5cafbf96 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 15 Feb 2015 08:48:27 -0500 Subject: [PATCH 108/191] Add stream=True #45 --- TODO.goals | 7 +- experiments/app.js | 17 -- experiments/client1.py | 217 ------------------------ experiments/client2.py | 11 -- experiments/index.html | 8 - experiments/interpretation.log | Bin 3436 -> 0 bytes experiments/proxy.js | 30 ---- experiments/proxy.log | Bin 4311 -> 0 bytes experiments/socketIO_client/__init__.py | 104 ------------ serve_tests.js | 7 + setup.cfg | 2 - setup.py | 3 +- socketIO_client/__init__.py | 1 + socketIO_client/tests.py | 13 ++ socketIO_client/transports.py | 3 +- 15 files changed, 29 insertions(+), 394 deletions(-) delete mode 100644 experiments/app.js delete mode 100644 experiments/client1.py delete mode 100644 experiments/client2.py delete mode 100644 experiments/index.html delete mode 100644 experiments/interpretation.log delete mode 100644 experiments/proxy.js delete mode 100644 experiments/proxy.log delete mode 100644 experiments/socketIO_client/__init__.py diff --git a/TODO.goals b/TODO.goals index e3f90f9..ffdff8b 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,13 +1,15 @@ ++ Add __version__ Check that setting stream=True makes sure that we receive all server events #45 Use HTTP headers for Websocket connection #44 Do not connect to blank namespace if overriding default namespace exists #40 Pass wait timeout to underlying transports #34 +Check that we have the correct version of websocket #62 +Fall back to next transport #48 + Make heartbeat time independent from elapsed time #38 Check that heartbeats are sent even with short wait time #64 Revive heartbeat as separate process thanks to sarietta Restore namespace as separate process #50 -Check that we have the correct version of websocket #62 -Fall back to next transport #48 Check that SSL websocket connections work properly #54 Write a unit test to make sure that the client can disconnect properly #42 #46 Make sure that on_reconnect works #61 @@ -19,4 +21,3 @@ Run under Python 3 #51 Use urllib.parse.urlparse Use continuous integration with TravisCI Credit everyone who took the time to submit an issue or pull request -Add __version__ diff --git a/experiments/app.js b/experiments/app.js deleted file mode 100644 index 4df4754..0000000 --- a/experiments/app.js +++ /dev/null @@ -1,17 +0,0 @@ -// DEBUG=* node app.js -var app = require('express')(); -var server = require('http').Server(app); -var io = require('socket.io')(server); - -server.listen(9000); - -app.get('/', function (req, res) { - res.sendFile(__dirname + '/index.html'); -}); - -io.on('connection', function (socket) { - socket.emit('news', { hello: 'world' }); - socket.on('my other event', function (data) { - console.log(data); - }); -}); diff --git a/experiments/client1.py b/experiments/client1.py deleted file mode 100644 index 4880a14..0000000 --- a/experiments/client1.py +++ /dev/null @@ -1,217 +0,0 @@ -import json -import requests -import time - - -# def decode_payload(payload) -def get_packets(content): - packets = [] - index = 0 - content_length = len(content) - while index < content_length: - index, packet_length = read_packet_length(content, index) - index, packet = read_packet(content, index, packet_length) - packet_type = int(packet[0]) - packet_payload = packet[1:] - packets.append((packet_type, packet_payload)) - return packets - - -def read_packet_length(content, index): - while ord(content[index]) != 0: - index += 1 - index += 1 - packet_length_string = '' - while ord(content[index]) != 255: - packet_length_string += str(ord(content[index])) - index += 1 - return index, int(packet_length_string) - - -def read_packet(content, index, packet_length): - while ord(content[index]) == 255: - index += 1 - packet = content[index:index + packet_length] - return index + packet_length, packet - - -request_counter = 0 - - -def get_timestamp(): - global request_counter - timestamp = '%s-%s' % (int(time.time() * 1000), request_counter) - request_counter += 1 - return timestamp - - -base_url = 'http://localhost:9000' -url = base_url + '/socket.io/' - - -print '*** Connect' -session = requests.Session() -print session.cookies.items() -response = session.get(url, params={ - 'EIO': 3, - 'transport': 'polling', - 't': get_timestamp(), -}) -print response.url -print session.cookies.items() -packets = get_packets(response.content) -for packet_type, packet in packets: - print packet_type, packet -packet_type, packet = packets[0] -packet_json = json.loads(packet) -print packet_json -print packet_json['pingInterval'] -print packet_json['pingTimeout'] -print packet_json['sid'] -assert packet_type == 0 - - -response = session.get(url, params={ - 'EIO': 3, - 'transport': 'polling', - 't': get_timestamp(), - 'sid': packet_json['sid'], -}) -print response.url -print session.cookies.items() -packets = get_packets(response.content) -for packet_type, packet in packets: - print 'engineIO_packet_type = %s' % packet_type - print 'socketIO_packet_type = %s' % packet[0] - print 'packet = %s' % packet[1:] -# from IPython import embed; embed() - - -# def wrap_payload -# def pack_packets -# def pack_packs -# def wrap_payload -def encode_payload(packs): - parts = [] - for packet_type, packet in packs: - content = str(packet_type) + str(packet) - parts.append(make_header(content) + content) - return ''.join(parts) - - -def make_header(content): - length_string = str(len(content)) - print length_string - header_digits = [0] - for index in xrange(len(length_string)): - header_digits.append(ord(length_string[index]) - 48) - header_digits.append(255) - print '---' - for x in header_digits: - print str(x) - print '---' - return ''.join(chr(x) for x in header_digits) - - -# print '***' -# response = session.get(url, params={ - # 'EIO': 3, - # 'transport': 'polling', - # 't': get_timestamp(), - # 'sid': packet_json['sid'], -# }) -# print response.url -# print response.content -# packets = get_packets(response.content) -# for packet_type, packet in packets: - # print packet_type, packet - - -print '*** Send event' -packets = [ - (4, '2["my other event",{"my":"data"}]'), -] -payload = encode_payload(packets) -print payload -print session.cookies.items() -response = session.post(url, params={ - 'EIO': 3, - 'transport': 'polling', - 't': get_timestamp(), - 'sid': packet_json['sid'], -}, data=payload, headers={ - 'content-type': 'application/octet-stream', -}) -print response.url -print response.content - -from time import sleep -sleep(10) - - -print '*** Send event' -packets = [ - (4, '2["my other event",{"my":"data"}]'), -] -payload = encode_payload(packets) -print payload -print session.cookies.items() -response = session.post(url, params={ - 'EIO': 3, - 'transport': 'polling', - 't': get_timestamp(), - 'sid': packet_json['sid'], -}, data=payload, headers={ - 'content-type': 'application/octet-stream', -}) -print response.url -print response.content - - -print '*** Send ping' -packets = [ - (2, ''), -] -payload = encode_payload(packets) -print payload -response = session.post(url, params={ - 'EIO': 3, - 'transport': 'polling', - 't': get_timestamp(), - 'sid': packet_json['sid'], -}, data=payload, headers={ - 'content-type': 'application/octet-stream', -}) -print response.url -print response.content - - -print '*** Send ping' -packets = [ - (2, ''), -] -payload = encode_payload(packets) -print payload -response = session.post(url, params={ - 'EIO': 3, - 'transport': 'polling', - 't': get_timestamp(), - 'sid': packet_json['sid'], -}, data=payload, headers={ - 'content-type': 'application/octet-stream', -}) -print response.url -print response.content - - -response = session.get(url, params={ - 'EIO': 3, - 'transport': 'polling', - 't': get_timestamp(), - 'sid': packet_json['sid'], -}) -print response.url -print session.cookies.items() -packets = get_packets(response.content) -for packet_type, packet in packets: - print packet_type, packet diff --git a/experiments/client2.py b/experiments/client2.py deleted file mode 100644 index 14c0d7a..0000000 --- a/experiments/client2.py +++ /dev/null @@ -1,11 +0,0 @@ -from socketIO_client import SocketIO - - -# def on_news(self, data): - # print(data) - # self.emit('my other event', {'my': 'data'}) - - -s = SocketIO('localhost', 9000) -s.emit('whee') -# s.on('news', on_news) diff --git a/experiments/index.html b/experiments/index.html deleted file mode 100644 index 90f066c..0000000 --- a/experiments/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/experiments/interpretation.log b/experiments/interpretation.log deleted file mode 100644 index ea51ac8d4150cd24d3bb94a58513ace66b5687e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3436 zcmd5;-EP`26z-qYmZyk(QMFQO?femt(8M%0^=5R1?nj7=G_fFRh_l3{LRBAa+9T~1 zb{uEm4vPs`*IgKNj-BJ<&);{B`&aD`*X?fa>|MJzFwS!p+){oPvGZ5$i%!!z;+dc1 zDa&{>WpNxO!z11#HgSpLx&(W+S;ZBa45Ng?M@E`4&H@%26Po1_ON^#rDburH;4j0e zv(cF5+#jbf`(z94ntJ)oQ&(r|2#nW`0O>)aavodLxgH2B#>6V_t~h6F)``)4>=^XAd%D zcN{fbaN)TF5UCx`r-iJ|>wk2#x4}M+8u`KDeg%`uHJ?U_S z;{gUSRR~8EQqBy(DdR>k@-sj9g@SSozPLpr1`i)Whjv3JZo@H@}9tV`|TzA7?s1%H*eHDMpXDEaLPdtslt@T&WeB3~1{4wea$LSTLH)QJM@TEZ|F z1L$?Jpsi9Dws8zaEBA*~qYI68=5f}`*ZUU`kr<-`Ss`*kX z_0$@vQ>nSCN@cw&JJRl6sm}4#V0;86yntBfbUX!-hCcU~YjhVFUp9+EqkBRrQW8sA zimIierKnOWH2QaxWZC}#Gyj0?@0F9?DkYHtoP?0F#)vX>D?C`GMZm^m&;|J6(>dGM zdpdbu)p{|qt93+2c55ZF&!JdH_?F|jUe$gPEQ-Aq=IXGpSHfcUP$`AK4Z1ITrndEN z3zkXNEor@_YBy{jntr)jyMxXk9>*u*!k@ml#WBn3w81eg0K?H_x!QAA6JWJtT5wFJ JJLYB;<0n-+KOF!7 diff --git a/experiments/proxy.js b/experiments/proxy.js deleted file mode 100644 index 756da12..0000000 --- a/experiments/proxy.js +++ /dev/null @@ -1,30 +0,0 @@ -var proxy = require('http-proxy').createProxyServer({ - target: {host: 'localhost', port: 9000} -}); -var server = require('http').createServer(function(req, res) { - console.log('[REQUEST.%s] %s', req.method, req.url); - console.log(req['headers']); - if (req.method == 'POST') { - var body = ''; - req.on('data', function (data) { - body += data; - }); - req.on('end', function () { - print_body('[REQUEST.BODY] ', body); - }); - } - var _write = res.write; - res.write = function(data) { - print_body('[RESPONSE.BODY] ', data); - _write.call(res, data); - } - proxy.web(req, res); -}); -function print_body(header, body) { - var text = String(body); - console.log(header + text); - for (var i = 0; i < text.length; i++) { - console.log('body[%s] = %s = %s', i, text[i], text.charCodeAt(i)); - } -} -server.listen(8000); diff --git a/experiments/proxy.log b/experiments/proxy.log deleted file mode 100644 index 45d08726609b2eb8b35e47b7493851c07f711015..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4311 zcmcJS-EP}B5QWpUK!2XX&_%A|M3h8|GGHvSXbR+Jy_=u53&R&qEF|7ImTM~sf*_B! z=p*$NmOL|RIhE5E2=+p4G#HT|59e^`;o<&YC;Laoqt5P|;kUn6)!AiP?@X)RuYc|Q zvs$0=+y23zpKjI5@qD$YmUVwo&1Td2`BvRmnKDWnqhgyS-PnJB^ylD@qy3%V2XFr# zbv9nV{POwV_&!=qCs8jNe4HGPKdG~m4=a6>9>3q)+j|#nN7sw<<#%1#IO)S)(GT&-`ai1k@)kVtK6zs6Y4!51oKoNR$9uyClt%CBVLM5v6z~ z39wV2qzF3!_OTzE)lY!^-bW1QfO^kG3=^fG77}>|{5<{^{SzGT6=pkqZ z^swAApby4;O3zE;i2qT=?4nd=3CE9|Ll+#RlXDE-PmO z1915rz{Oe&$i<3HyIOLv3j^hV4+|&<`^FOYx$mcmZ_d73b5S0C}ig(NerH!1jQmt$CnoIFABv=n(4X;c@y{KNmGi4PBUB zfms~73fSU2rIsH0hFW^)E$ZmvK9c}E{47BcU-Wn6gzn5=2HpxdooSOh0^9;iVYNdO0E$HsDjN!g+6K< zQ4C}oF(JT3Ws_}0BPHL6Eb@)8E*VFV(q%Z*g?tm5l58VQ3Bko@P+YVmx(o|JAM;J< zEwW8GHn~Rn0j>#e4rCg!I5Le$YH^ciq>Ui?Hyeo`jn!pv{L4DTe+IoEN~uWfbDyixcU&03Sf1MJDxPj5duTmA{7^G~nu QNB^Jq$Vq8cS3fT0f8?ro>i_@% diff --git a/experiments/socketIO_client/__init__.py b/experiments/socketIO_client/__init__.py deleted file mode 100644 index 52413f9..0000000 --- a/experiments/socketIO_client/__init__.py +++ /dev/null @@ -1,104 +0,0 @@ -import json -import requests -import time - - -class EngineIO(object): - - _path = 'engine.io' - _engine_io_protocol = 3 - _request_index = 0 - - def __init__(self, host, port): - url = 'http://%s:%s/%s/' % (host, port, self._path) - self.session = requests.Session() - response = self.session.get(url, params={ - 'EIO': self._engine_io_protocol, - 'transport': 'polling', - 't': self._get_timestamp(), - }) - packs = _decode_content(response.content) - packet_type, packet = packs[0] - assert packet_type == 0 - packet_json = json.loads(packet) - self._session_id = packet_json['sid'] - print(packet_json) - response = self.session.get(url, params={ - 'EIO': self._engine_io_protocol, - 'transport': 'polling', - 't': self._get_timestamp(), - 'sid': self._session_id, - }) - packs = _decode_content(response.content) - for packet_type, packet in packs: - print 'engineIO_packet_type = %s' % packet_type - print 'socketIO_packet_type = %s' % packet[0] - print 'packet = %s' % packet[1:] - - def _get_timestamp(self): - timestamp = '%s-%s' % (int(time.time() * 1000), self._request_index) - self._request_index += 1 - return timestamp - - def _message(self, packet): - packet_type = 4 - response = self.session.post(self.url, params={ - 'EIO': self._engine_io_protocol, - 'transport': 'polling', - 't': self._get_timestamp(), - 'sid': self._session_id, - }, data=_encode_content([(packet_type, packet)]), headers={ - 'content-type': 'application/octet-stream', - }) - - -class SocketIO(EngineIO): - - _path = 'socket.io' - _socket_io_protocol = 4 - - def __init__(self, host, port): - super(SocketIO, self).__init__(host, port) - - def on(self, event, callback): - pass - - def emit(self, event): - packet_type = 2 - packet = json.dumps([event]) - self._message(str(packet_type) + packet) - - -def _decode_content(content): - packs = [] - index = 0 - content_length = len(content) - while index < content_length: - index, packet_length = _read_packet_length(content, index) - index, packet = _read_packet(content, index, packet_length) - packet_type = int(packet[0]) - packet_payload = packet[1:] - packs.append((packet_type, packet_payload)) - return packs - - -def _read_packet_length(content, index): - while ord(content[index]) != 0: - index += 1 - index += 1 - packet_length_string = '' - while ord(content[index]) != 255: - packet_length_string += str(ord(content[index])) - index += 1 - return index, int(packet_length_string) - - -def _read_packet(content, index, packet_length): - while ord(content[index]) == 255: - index += 1 - packet = content[index:index + packet_length] - return index + packet_length, packet - - -def _encode_content(packs): - pass diff --git a/serve_tests.js b/serve_tests.js index 6c2edfc..17eef7a 100644 --- a/serve_tests.js +++ b/serve_tests.js @@ -51,6 +51,13 @@ var main = io.of('').on('connection', function(socket) { socket.on('wait_with_disconnect', function() { socket.emit('wait_with_disconnect_response'); }); + /* + socket.on('rapid_fire', function() { + for (var i = 0; i < 100000; i++) { + socket.emit('rapid_fire', i); + } + }); + */ }); var chat = io.of('/chat').on('connection', function (socket) { diff --git a/setup.cfg b/setup.cfg index 468f360..ea7fb18 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,5 +3,3 @@ detailed-errors = TRUE with-coverage = TRUE cover-package = socketIO_client cover-erase = TRUE - -[easy_install] diff --git a/setup.py b/setup.py index b42b4a3..c26f2af 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ import os from setuptools import setup, find_packages +from socketIO_client import __version__ here = os.path.abspath(os.path.dirname(__file__)) @@ -9,7 +10,7 @@ CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( name='socketIO-client', - version='0.5.3.2', + version=__version__, description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 622dae7..bc98d8a 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -12,6 +12,7 @@ from .exceptions import ConnectionError, TimeoutError, PacketError from .transports import _get_response, _negotiate_transport, TRANSPORTS +__version__ = '0.5.4' _SocketIOSession = namedtuple('_SocketIOSession', [ 'id', 'heartbeat_timeout', diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index dfebecb..542339e 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -171,6 +171,15 @@ class BaseMixin(object): 'ack_callback_response': (PAYLOAD,), }) + """ + def test_rapid_fire(self): + 'Capture all server events' + namespace = self.socketIO.define(Namespace) + self.socketIO.emit('rapid_fire') + self.socketIO.wait(30) + self.assertEqual(namespace.messages, range(100000)) + """ + class Test_WebsocketTransport(TestCase, BaseMixin): @@ -202,6 +211,7 @@ class Namespace(BaseNamespace): self.response = None self.args_by_event = {} self.called_on_disconnect = False + self.messages = [] def on_disconnect(self): self.called_on_disconnect = True @@ -217,3 +227,6 @@ class Namespace(BaseNamespace): def on_wait_with_disconnect_response(self): self.disconnect() + + def on_rapid_fire(self, x): + self.messages.append(x) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index b33754a..ebe8d59 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -204,7 +204,8 @@ class _XHR_PollingTransport(_AbstractTransport): self._http_session.get, self._url, params=self._params, - timeout=TIMEOUT_IN_SECONDS) + timeout=TIMEOUT_IN_SECONDS, + stream=True) response_text = response.text if not response_text.startswith(BOUNDARY): yield response_text From 99b29ac8411ba91afe9476253bc78185e34ab815 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 15 Feb 2015 08:54:06 -0500 Subject: [PATCH 109/191] Update goals --- TODO.goals | 4 ++-- TODO.log | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 TODO.log diff --git a/TODO.goals b/TODO.goals index ffdff8b..c8d206f 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,10 +1,9 @@ -+ Add __version__ -Check that setting stream=True makes sure that we receive all server events #45 Use HTTP headers for Websocket connection #44 Do not connect to blank namespace if overriding default namespace exists #40 Pass wait timeout to underlying transports #34 Check that we have the correct version of websocket #62 Fall back to next transport #48 +Release 0.5.4 Make heartbeat time independent from elapsed time #38 Check that heartbeats are sent even with short wait time #64 @@ -21,3 +20,4 @@ Run under Python 3 #51 Use urllib.parse.urlparse Use continuous integration with TravisCI Credit everyone who took the time to submit an issue or pull request +Release 0.6.1 diff --git a/TODO.log b/TODO.log deleted file mode 100644 index 70f9984..0000000 --- a/TODO.log +++ /dev/null @@ -1,5 +0,0 @@ -# UTC 11/19/2013 -+ Add nayefc to acknowledgments [11/19/2013] -+ Beware of scheme included in URL [11/17/2013] -+ Add test for server ack callback in namespace [11/17/2013] -+ Set port automatically if it is not automatically specified [11/17/2013] \ No newline at end of file From 18cf130d9e3c38b61e064241bfe638abde3c8a06 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 15 Feb 2015 10:20:11 -0500 Subject: [PATCH 110/191] Fix #40 --- TODO.goals | 1 - socketIO_client/__init__.py | 109 ++++++++++++++++++++++++++---------- socketIO_client/tests.py | 10 +++- 3 files changed, 88 insertions(+), 32 deletions(-) diff --git a/TODO.goals b/TODO.goals index c8d206f..e2023ce 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,4 +1,3 @@ -Use HTTP headers for Websocket connection #44 Do not connect to blank namespace if overriding default namespace exists #40 Pass wait timeout to underlying transports #34 Check that we have the correct version of websocket #62 diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index bc98d8a..8979801 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -34,9 +34,6 @@ class BaseNamespace(object): self._callback_by_event = {} self.initialize() - def _log(self, level, msg, *attrs): - _log.log(level, '%s: %s' % (self._transport._url, msg), *attrs) - def initialize(self): 'Initialize custom variables here; you can override this method' pass @@ -57,19 +54,19 @@ class BaseNamespace(object): def on_connect(self): 'Called after server connects; you can override this method' - self._log(logging.DEBUG, '%s [connect]', self.path) + pass def on_disconnect(self): 'Called after server disconnects; you can override this method' - self._log(logging.DEBUG, '%s [disconnect]', self.path) + pass def on_heartbeat(self): 'Called after server sends a heartbeat; you can override this method' - self._log(logging.DEBUG, '%s [heartbeat]', self.path) + pass def on_message(self, data): 'Called after server sends a message; you can override this method' - self._log(logging.INFO, '%s [message] %s', self.path, data) + pass def on_event(self, event, *args): """ @@ -78,32 +75,28 @@ class BaseNamespace(object): such as one defined by namespace.on('my_event', my_function). """ callback, args = find_callback(args) - arguments = [repr(_) for _ in args] if callback: - arguments.append('callback(*args)') callback(*args) - self._log(logging.INFO, '%s [event] %s(%s)', self.path, event, - ', '.join(arguments)) def on_error(self, reason, advice): 'Called after server sends an error; you can override this method' - self._log(logging.INFO, '%s [error] %s', self.path, advice) + pass def on_noop(self): 'Called after server sends a noop; you can override this method' - self._log(logging.INFO, '%s [noop]', self.path) + pass def on_open(self, *args): - self._log(logging.INFO, '%s [open] %s', self.path, args) + pass def on_close(self, *args): - self._log(logging.INFO, '%s [close] %s', self.path, args) + pass def on_retry(self, *args): - self._log(logging.INFO, '%s [retry] %s', self.path, args) + pass def on_reconnect(self, *args): - self._log(logging.INFO, '%s [reconnect] %s', self.path, args) + pass def _find_event_callback(self, event): # Check callbacks defined by on() @@ -111,15 +104,12 @@ class BaseNamespace(object): return self._callback_by_event[event] except KeyError: pass - - # Convert connect to reconnect if we have seen connect - # already. + # Convert connect to reconnect if we have seen connect already if event == 'connect': - if self.was_connected == False: + if not self.was_connected: self.was_connected = True else: event = 'reconnect' - # Check callbacks defined explicitly or use on_event() return getattr( self, @@ -127,6 +117,61 @@ class BaseNamespace(object): lambda *args: self.on_event(event, *args)) +class LoggingNamespace(BaseNamespace): + + def _log(self, level, msg, *attrs): + _log.log(level, '%s: %s' % (self._transport._url, msg), *attrs) + + def on_connect(self): + self._log(logging.DEBUG, '%s [connect]', self.path) + super(LoggingNamespace, self).on_connect() + + def on_disconnect(self): + self._log(logging.DEBUG, '%s [disconnect]', self.path) + super(LoggingNamespace, self).on_disconnect() + + def on_heartbeat(self): + self._log(logging.DEBUG, '%s [heartbeat]', self.path) + super(LoggingNamespace, self).on_heartbeat() + + def on_message(self, data): + self._log(logging.INFO, '%s [message] %s', self.path, data) + super(LoggingNamespace, self).on_message(data) + + def on_event(self, event, *args): + callback, args = find_callback(args) + arguments = [repr(_) for _ in args] + if callback: + arguments.append('callback(*args)') + self._log(logging.INFO, '%s [event] %s(%s)', self.path, event, + ', '.join(arguments)) + super(LoggingNamespace, self).on_event(event, *args) + + def on_error(self, reason, advice): + self._log(logging.INFO, '%s [error] %s', self.path, advice) + super(LoggingNamespace, self).on_error(reason, advice) + + def on_noop(self): + self._log(logging.INFO, '%s [noop]', self.path) + super(LoggingNamespace, self).on_noop() + + def on_open(self, *args): + self._log(logging.INFO, '%s [open] %s', self.path, args) + super(LoggingNamespace, self).on_open(*args) + + def on_close(self, *args): + self._log(logging.INFO, '%s [close] %s', self.path, args) + super(LoggingNamespace, self).on_close(*args) + + def on_retry(self, *args): + self._log(logging.INFO, '%s [retry] %s', self.path, args) + super(LoggingNamespace, self).on_retry(*args) + + def on_reconnect(self, *args): + self._log(logging.INFO, '%s [reconnect] %s', self.path, args) + super(LoggingNamespace, self).on_reconnect(*args) + + class SocketIO(object): """Create a socket.io client that connects to a socket.io server @@ -147,14 +192,16 @@ class SocketIO(object): """ def __init__( - self, host, port=None, Namespace=BaseNamespace, - wait_for_connection=True, transports=TRANSPORTS, resource='socket.io', **kw): + self, host, port=None, Namespace=None, + wait_for_connection=True, transports=TRANSPORTS, + resource='socket.io', **kw): self.is_secure, self.base_url = _parse_host(host, port, resource) self.wait_for_connection = wait_for_connection self._namespace_by_path = {} self.client_supported_transports = transports self.kw = kw - self.define(Namespace) + if Namespace: + self.define(Namespace) def log(self, level, msg, *attrs): _log.log(level, '%s: %s' % (self.base_url, msg), @@ -177,6 +224,8 @@ class SocketIO(object): return namespace def on(self, event, callback, path=''): + if path not in self._namespace_by_path: + self.define(BaseNamespace, path) return self.get_namespace(path).on(event, callback) def message(self, data='', callback=None, path=''): @@ -237,10 +286,12 @@ class SocketIO(object): def disconnect(self, path=''): if self.connected: self._transport.disconnect(path) - namespace = self._namespace_by_path[path] - namespace.on_disconnect() - if path: - del self._namespace_by_path[path] + try: + namespace = self._namespace_by_path[path] + namespace.on_disconnect() + except KeyError: + pass + del self._namespace_by_path[path] @property def connected(self): diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index 542339e..829902f 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -2,7 +2,7 @@ import logging import time from unittest import TestCase -from . import SocketIO, BaseNamespace, find_callback +from . import SocketIO, LoggingNamespace, find_callback from .transports import TIMEOUT_IN_SECONDS @@ -31,6 +31,7 @@ class BaseMixin(object): def test_disconnect(self): 'Disconnect' + self.socketIO.define(LoggingNamespace) self.assertTrue(self.socketIO.connected) self.socketIO.disconnect() self.assertFalse(self.socketIO.connected) @@ -65,12 +66,14 @@ class BaseMixin(object): def test_message_with_callback(self): 'Message with callback' + self.socketIO.define(LoggingNamespace) self.socketIO.message(callback=self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_message_with_callback_with_data(self): 'Message with callback with data' + self.socketIO.define(LoggingNamespace) self.socketIO.message(DATA, self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) @@ -104,12 +107,14 @@ class BaseMixin(object): def test_emit_with_callback(self): 'Emit with callback' + self.socketIO.define(LoggingNamespace) self.socketIO.emit('emit_with_callback', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit_with_callback_with_payload(self): 'Emit with callback with payload' + self.socketIO.define(LoggingNamespace) self.socketIO.emit( 'emit_with_callback_with_payload', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) @@ -117,6 +122,7 @@ class BaseMixin(object): def test_emit_with_callback_with_multiple_payloads(self): 'Emit with callback with multiple payloads' + self.socketIO.define(LoggingNamespace) self.socketIO.emit( 'emit_with_callback_with_multiple_payloads', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) @@ -205,7 +211,7 @@ class Test_JSONP_PollingTransport(TestCase, BaseMixin): self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1 -class Namespace(BaseNamespace): +class Namespace(LoggingNamespace): def initialize(self): self.response = None From 0c8dda1316f8c4c0ee6de6f8f2e208e59b887f4d Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 15 Feb 2015 11:18:57 -0500 Subject: [PATCH 111/191] Ensure that recv timeout is less than heartbeat timeout #34 --- socketIO_client/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 899cc32..501d09d 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -238,19 +238,19 @@ class SocketIO(object): def wait(self, seconds=None, for_callbacks=False): """Wait in a loop and process events as defined in the namespaces. - - - Omit seconds, i.e. call wait() without arguments, to wait forever. + Omit seconds, i.e. call wait() without arguments, to wait forever. """ warning_screen = _yield_warning_screen(seconds) + timeout = min(self._heartbeat_interval, seconds) for elapsed_time in warning_screen: if self._stop_waiting(for_callbacks): break try: try: - self._process_events(timeout=seconds) + self._process_events(timeout) except TimeoutError: pass - self.heartbeat_pacemaker.next() + next(self.heartbeat_pacemaker) except ConnectionError as e: try: warning = Exception('[connection error] %s' % e) @@ -330,8 +330,9 @@ class SocketIO(object): self.log(logging.DEBUG, '[transports available] %s', ' '.join( socketIO_session.server_supported_transports)) # Initialize heartbeat_pacemaker + self._heartbeat_interval = socketIO_session.heartbeat_timeout / 2 self.heartbeat_pacemaker = self._make_heartbeat_pacemaker( - heartbeat_interval=socketIO_session.heartbeat_timeout / 2) + heartbeat_interval=self._heartbeat_interval) next(self.heartbeat_pacemaker) # Negotiate transport transport = _negotiate_transport( From 0c6d4cd4d7539e87b71a1ecbacfdc4eb38199fc9 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 15 Feb 2015 11:35:06 -0500 Subject: [PATCH 112/191] Fix #62 --- socketIO_client/transports.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 0cfe6c6..3b77485 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -4,12 +4,19 @@ import re import requests import six import socket +import sys import time import websocket from .exceptions import SocketIOError, ConnectionError, TimeoutError +if not hasattr(websocket, 'create_connection'): + sys.exit("""Incompatible websocket implementation +- Please make sure that you have websocket-client installed +- Please remove other websocket implementations""") + + TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling' BOUNDARY = six.u('\ufffd') TIMEOUT_IN_SECONDS = 3 From f7a5788bf7dacc3fde5cbc3dec144c53b88fe63d Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 15 Feb 2015 11:36:47 -0500 Subject: [PATCH 113/191] Update goals --- TODO.goals | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/TODO.goals b/TODO.goals index e2023ce..0b3d98f 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,17 +1,14 @@ -Do not connect to blank namespace if overriding default namespace exists #40 -Pass wait timeout to underlying transports #34 -Check that we have the correct version of websocket #62 Fall back to next transport #48 -Release 0.5.4 -Make heartbeat time independent from elapsed time #38 - Check that heartbeats are sent even with short wait time #64 - Revive heartbeat as separate process thanks to sarietta -Restore namespace as separate process #50 -Check that SSL websocket connections work properly #54 Write a unit test to make sure that the client can disconnect properly #42 #46 Make sure that on_reconnect works #61 Check that the library reconnects despite server restart #63 #53 +Check that SSL websocket connections work properly #54 +Release 0.5.4 + +Revive heartbeat as separate process thanks to sarietta + Check that heartbeats are sent even with short wait time #64 +Restore namespace as separate process #50 Upgrade to socket.io 1.2 #41 #52 Merge sarietta's pull request Implement rooms #65 From e98f98f9a2e54cafcde3aaaf9cd302ec5e9e48b7 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 15 Feb 2015 13:54:19 -0500 Subject: [PATCH 114/191] Merge #48 --- serve_tests.js | 7 ---- socketIO_client/__init__.py | 73 +++++++++++++++++++++++++---------- socketIO_client/tests.py | 13 ------- socketIO_client/transports.py | 20 ---------- 4 files changed, 53 insertions(+), 60 deletions(-) diff --git a/serve_tests.js b/serve_tests.js index 17eef7a..6c2edfc 100644 --- a/serve_tests.js +++ b/serve_tests.js @@ -51,13 +51,6 @@ var main = io.of('').on('connection', function(socket) { socket.on('wait_with_disconnect', function() { socket.emit('wait_with_disconnect_response'); }); - /* - socket.on('rapid_fire', function() { - for (var i = 0; i < 100000; i++) { - socket.emit('rapid_fire', i); - } - }); - */ }); var chat = io.of('/chat').on('connection', function (socket) { diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 501d09d..709f004 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -8,8 +8,11 @@ try: except ImportError: from urlparse import urlparse as parse_url -from .exceptions import ConnectionError, TimeoutError, PacketError -from .transports import _get_response, _negotiate_transport, TRANSPORTS +from .exceptions import ( + ConnectionError, TimeoutError, PacketError, SocketIOError) +from .transports import ( + _get_response, TRANSPORTS, + _WebsocketTransport, _XHR_PollingTransport, _JSONP_PollingTransport) __version__ = '0.5.4' @@ -24,7 +27,6 @@ RETRY_INTERVAL_IN_SECONDS = 1 class BaseNamespace(object): - 'Define client behavior' def __init__(self, _transport, path): @@ -310,39 +312,70 @@ class SocketIO(object): except AttributeError: pass warning_screen = _yield_warning_screen(seconds=None) + for elapsed_time in warning_screen: try: - self.__transport = self._get_transport() + socketIO_session = _get_socketIO_session( + self.is_secure, self.base_url, **self.kw) break except ConnectionError as e: if not self.wait_for_connection: raise + warning = Exception('[waiting for connection] %s' % e) try: - warning = Exception('[waiting for connection] %s' % e) warning_screen.throw(warning) except StopIteration: self.log(logging.WARNING, warning) - return self.__transport + supported_transports = self._get_supported_transports(socketIO_session) - def _get_transport(self): - socketIO_session = _get_socketIO_session( - self.is_secure, self.base_url, **self.kw) - self.log(logging.DEBUG, '[transports available] %s', ' '.join( - socketIO_session.server_supported_transports)) - # Initialize heartbeat_pacemaker self._heartbeat_interval = socketIO_session.heartbeat_timeout / 2 self.heartbeat_pacemaker = self._make_heartbeat_pacemaker( heartbeat_interval=self._heartbeat_interval) next(self.heartbeat_pacemaker) - # Negotiate transport - transport = _negotiate_transport( - self.client_supported_transports, socketIO_session, - self.is_secure, self.base_url, **self.kw) - # Update namespaces + + for elapsed_time in warning_screen: + self.log( + logging.DEBUG, + '[trying transport] %s', supported_transports[0]) + try: + self.__transport = self._get_transport( + socketIO_session, supported_transports[0]) + break + except ConnectionError as e: + try: + supported_transports.pop(0) + except IndexError: + raise + for path, namespace in self._namespace_by_path.items(): - namespace._transport = transport - transport.connect(path) - return transport + namespace._transport = self.__transport + self.__transport.connect(path) + return self.__transport + + def _get_supported_transports(self, session): + self.log( + logging.DEBUG, + '[transports available] %s', + ' '.join(session.server_supported_transports)) + supported_transports = list(set( + session.server_supported_transports, + ).intersection(self.client_supported_transports)) + if not supported_transports: + raise SocketIOError(' '.join([ + 'could not negotiate a transport:', + 'client supports %s but' % ', '.join( + self.client_supported_transports), + 'server supports %s' % ', '.join( + session.server_supported_transports), + ])) + return supported_transports + + def _get_transport(self, session, transport_name): + return { + 'websocket': _WebsocketTransport, + 'xhr-polling': _XHR_PollingTransport, + 'jsonp-polling': _JSONP_PollingTransport, + }[transport_name](session, self.is_secure, self.base_url, **self.kw) def _make_heartbeat_pacemaker(self, heartbeat_interval): heartbeat_time = time.time() diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index 829902f..400d626 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -177,15 +177,6 @@ class BaseMixin(object): 'ack_callback_response': (PAYLOAD,), }) - """ - def test_rapid_fire(self): - 'Capture all server events' - namespace = self.socketIO.define(Namespace) - self.socketIO.emit('rapid_fire') - self.socketIO.wait(30) - self.assertEqual(namespace.messages, range(100000)) - """ - class Test_WebsocketTransport(TestCase, BaseMixin): @@ -217,7 +208,6 @@ class Namespace(LoggingNamespace): self.response = None self.args_by_event = {} self.called_on_disconnect = False - self.messages = [] def on_disconnect(self): self.called_on_disconnect = True @@ -233,6 +223,3 @@ class Namespace(LoggingNamespace): def on_wait_with_disconnect_response(self): self.disconnect() - - def on_rapid_fire(self, x): - self.messages.append(x) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 3b77485..5e47f94 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -296,26 +296,6 @@ class _JSONP_PollingTransport(_AbstractTransport): self._connected = False -def _negotiate_transport( - client_supported_transports, session, - is_secure, base_url, **kw): - server_supported_transports = session.server_supported_transports - for supported_transport in client_supported_transports: - if supported_transport in server_supported_transports: - _log.debug('[%s] [transport selected] %s', base_url, - supported_transport) - return { - 'websocket': _WebsocketTransport, - 'xhr-polling': _XHR_PollingTransport, - 'jsonp-polling': _JSONP_PollingTransport, - }[supported_transport](session, is_secure, base_url, **kw) - raise SocketIOError(' '.join([ - 'could not negotiate a transport:', - 'client supports %s but' % ', '.join(client_supported_transports), - 'server supports %s' % ', '.join(server_supported_transports), - ])) - - def _yield_text_from_framed_data(framed_data, parse=lambda x: x): parts = [parse(x) for x in framed_data.split(BOUNDARY)] for text_length, text in zip(parts[1::2], parts[2::2]): From 66c11a0a37dcbdd251e61b5e070b6858884268d3 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 15 Feb 2015 16:18:08 -0500 Subject: [PATCH 115/191] Fix #46 --- socketIO_client/__init__.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 4e8df09..2bc3ac4 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -286,14 +286,13 @@ class SocketIO(object): self.wait(seconds, for_callbacks=True) def disconnect(self, path=''): - if self.connected: - self._transport.disconnect(path) - try: - namespace = self._namespace_by_path[path] - namespace.on_disconnect() - except KeyError: - pass - del self._namespace_by_path[path] + self._transport.disconnect(path) + try: + namespace = self._namespace_by_path[path] + namespace.on_disconnect() + del self._namespace_by_path[path] + except KeyError: + pass @property def connected(self): From 4dea8b080ef54ffc6727eae52188f8d017c02c6c Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 15 Feb 2015 17:17:07 -0500 Subject: [PATCH 116/191] Hide unneccessary attributes --- socketIO_client/__init__.py | 44 ++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 2bc3ac4..50c817e 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -197,17 +197,16 @@ class SocketIO(object): self, host, port=None, Namespace=None, wait_for_connection=True, transports=TRANSPORTS, resource='socket.io', **kw): - self.is_secure, self.base_url = _parse_host(host, port, resource) + self.is_secure, self._base_url = _parse_host(host, port, resource) self.wait_for_connection = wait_for_connection self._namespace_by_path = {} - self.client_supported_transports = transports - self.kw = kw + self._client_supported_transports = transports + self._kw = kw if Namespace: self.define(Namespace) - def log(self, level, msg, *attrs): - _log.log(level, '%s: %s' % (self.base_url, msg), - *attrs) + def _log(self, level, msg, *attrs): + _log.log(level, '%s: %s' % (self._base_url, msg), *attrs) def __enter__(self): return self @@ -240,7 +239,8 @@ class SocketIO(object): def wait(self, seconds=None, for_callbacks=False): """Wait in a loop and process events as defined in the namespaces. - Omit seconds, i.e. call wait() without arguments, to wait forever. + + - Omit seconds, i.e. call wait() without arguments, to wait forever. """ warning_screen = _yield_warning_screen(seconds) timeout = min(self._heartbeat_interval, seconds) @@ -252,13 +252,13 @@ class SocketIO(object): self._process_events(timeout) except TimeoutError: pass - next(self.heartbeat_pacemaker) + next(self._heartbeat_pacemaker) except ConnectionError as e: try: warning = Exception('[connection error] %s' % e) warning_screen.throw(warning) except StopIteration: - self.log(logging.WARNING, warning) + self._log(logging.WARNING, warning) self.disconnect() def _process_events(self, timeout=None): @@ -266,7 +266,7 @@ class SocketIO(object): try: self._process_packet(packet) except PacketError as e: - self.log(logging.WARNING, '[packet error] %s', e) + self._log(logging.WARNING, '[packet error] %s', e) def _process_packet(self, packet): code, packet_id, path, data = packet @@ -314,7 +314,7 @@ class SocketIO(object): for elapsed_time in warning_screen: try: socketIO_session = _get_socketIO_session( - self.is_secure, self.base_url, **self.kw) + self.is_secure, self._base_url, **self._kw) break except ConnectionError as e: if not self.wait_for_connection: @@ -323,11 +323,11 @@ class SocketIO(object): try: warning_screen.throw(warning) except StopIteration: - self.log(logging.WARNING, warning) + self._log(logging.WARNING, warning) supported_transports = self._get_supported_transports(socketIO_session) - self.heartbeat_pacemaker = self._make_heartbeat_pacemaker( + self._heartbeat_pacemaker = self._make_heartbeat_pacemaker( heartbeat_timeout=socketIO_session.heartbeat_timeout) - next(self.heartbeat_pacemaker) + next(self._heartbeat_pacemaker) for elapsed_time in warning_screen: try: self.__transport = self._get_transport( @@ -344,30 +344,28 @@ class SocketIO(object): return self.__transport def _get_supported_transports(self, session): - self.log( - logging.DEBUG, - '[transports available] %s', + self._log( logging.DEBUG, '[transports available] %s', ' '.join(session.server_supported_transports)) - supported_transports = list(set( - session.server_supported_transports, - ).intersection(self.client_supported_transports)) + supported_transports = [ + x for x in self._client_supported_transports if + x in session.server_supported_transports] if not supported_transports: raise SocketIOError(' '.join([ 'could not negotiate a transport:', 'client supports %s but' % ', '.join( - self.client_supported_transports), + self._client_supported_transports), 'server supports %s' % ', '.join( session.server_supported_transports), ])) return supported_transports def _get_transport(self, session, transport_name): - self.log(logging.DEBUG, '[trying transport] %s', transport_name) + self._log(logging.DEBUG, '[trying transport] %s', transport_name) return { 'websocket': _WebsocketTransport, 'xhr-polling': _XHR_PollingTransport, 'jsonp-polling': _JSONP_PollingTransport, - }[transport_name](session, self.is_secure, self.base_url, **self.kw) + }[transport_name](session, self.is_secure, self._base_url, **self._kw) def _make_heartbeat_pacemaker(self, heartbeat_timeout): self._heartbeat_interval = heartbeat_timeout / 2 From f8a3d7cbe60bdd7061caa901fdbb494e63a1b934 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 15 Feb 2015 18:25:01 -0500 Subject: [PATCH 117/191] Fix #53 #61 #63 --- socketIO_client/__init__.py | 62 +++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 50c817e..3105a13 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -32,7 +32,7 @@ class BaseNamespace(object): def __init__(self, _transport, path): self._transport = _transport self.path = path - self.was_connected = False + self._was_connected = False self._callback_by_event = {} self.initialize() @@ -108,8 +108,8 @@ class BaseNamespace(object): pass # Convert connect to reconnect if we have seen connect already if event == 'connect': - if not self.was_connected: - self.was_connected = True + if not self._was_connected: + self._was_connected = True else: event = 'reconnect' # Check callbacks defined explicitly or use on_event() @@ -259,7 +259,11 @@ class SocketIO(object): warning_screen.throw(warning) except StopIteration: self._log(logging.WARNING, warning) - self.disconnect() + try: + namespace = self._namespace_by_path[''] + namespace.on_disconnect() + except KeyError: + pass def _process_events(self, timeout=None): for packet in self._transport.recv_packet(timeout): @@ -310,12 +314,35 @@ class SocketIO(object): return self.__transport except AttributeError: pass + socketIO_session = self._get_socketIO_session() + supported_transports = self._get_supported_transports(socketIO_session) + self._heartbeat_pacemaker = self._make_heartbeat_pacemaker( + heartbeat_timeout=socketIO_session.heartbeat_timeout) + next(self._heartbeat_pacemaker) warning_screen = _yield_warning_screen(seconds=None) for elapsed_time in warning_screen: try: - socketIO_session = _get_socketIO_session( - self.is_secure, self._base_url, **self._kw) + self._transport_name = supported_transports.pop(0) + except IndexError: + raise ConnectionError('Could not negotiate a transport') + try: + self.__transport = self._get_transport( + socketIO_session, self._transport_name) break + except ConnectionError: + pass + for path, namespace in self._namespace_by_path.items(): + namespace._transport = self.__transport + if path: + self.__transport.connect(path) + return self.__transport + + def _get_socketIO_session(self): + warning_screen = _yield_warning_screen(seconds=None) + for elapsed_time in warning_screen: + try: + return _get_socketIO_session( + self.is_secure, self._base_url, **self._kw) except ConnectionError as e: if not self.wait_for_connection: raise @@ -324,27 +351,10 @@ class SocketIO(object): warning_screen.throw(warning) except StopIteration: self._log(logging.WARNING, warning) - supported_transports = self._get_supported_transports(socketIO_session) - self._heartbeat_pacemaker = self._make_heartbeat_pacemaker( - heartbeat_timeout=socketIO_session.heartbeat_timeout) - next(self._heartbeat_pacemaker) - for elapsed_time in warning_screen: - try: - self.__transport = self._get_transport( - socketIO_session, supported_transports[0]) - break - except ConnectionError as e: - try: - supported_transports.pop(0) - except IndexError: - raise - for path, namespace in self._namespace_by_path.items(): - namespace._transport = self.__transport - self.__transport.connect(path) - return self.__transport def _get_supported_transports(self, session): - self._log( logging.DEBUG, '[transports available] %s', + self._log( + logging.DEBUG, '[transports available] %s', ' '.join(session.server_supported_transports)) supported_transports = [ x for x in self._client_supported_transports if @@ -360,7 +370,7 @@ class SocketIO(object): return supported_transports def _get_transport(self, session, transport_name): - self._log(logging.DEBUG, '[trying transport] %s', transport_name) + self._log(logging.DEBUG, '[transport chosen] %s', transport_name) return { 'websocket': _WebsocketTransport, 'xhr-polling': _XHR_PollingTransport, From 63a6fca6134816b5094a134904671d8ab421948f Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 08:17:11 -0500 Subject: [PATCH 118/191] Update goals --- TODO.goals | 8 -------- 1 file changed, 8 deletions(-) diff --git a/TODO.goals b/TODO.goals index 0b3d98f..c6ddcaf 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,11 +1,4 @@ -Fall back to next transport #48 - -Write a unit test to make sure that the client can disconnect properly #42 #46 -Make sure that on_reconnect works #61 -Check that the library reconnects despite server restart #63 #53 -Check that SSL websocket connections work properly #54 Release 0.5.4 - Revive heartbeat as separate process thanks to sarietta Check that heartbeats are sent even with short wait time #64 Restore namespace as separate process #50 @@ -13,7 +6,6 @@ Upgrade to socket.io 1.2 #41 #52 Merge sarietta's pull request Implement rooms #65 Run under Python 3 #51 - Use urllib.parse.urlparse Use continuous integration with TravisCI Credit everyone who took the time to submit an issue or pull request Release 0.6.1 From 502e54d7bb70a023619153f8ba269c45461673f8 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 11:09:45 -0500 Subject: [PATCH 119/191] Update acknowledgments for 0.5.4 --- CHANGES.rst => CHANGES.md | 10 +- MANIFEST.in | 2 +- README.md | 156 ++++++++++++++++++++++++++++++ README.rst | 183 ------------------------------------ setup.py | 4 +- socketIO_client/__init__.py | 7 +- 6 files changed, 169 insertions(+), 193 deletions(-) rename CHANGES.rst => CHANGES.md (81%) create mode 100644 README.md delete mode 100644 README.rst diff --git a/CHANGES.rst b/CHANGES.md similarity index 81% rename from CHANGES.rst rename to CHANGES.md index 90ee4b2..c8895d7 100644 --- a/CHANGES.rst +++ b/CHANGES.md @@ -1,3 +1,9 @@ +0.5.4 +----- +- Fixed reconnection in the event of server restart +- Fixed calling on_reconnect() so that it is actually called +- Set default Namespace=None + 0.5.3 ----- - Updated wait loop to exit if the client wants to disconnect @@ -42,7 +48,5 @@ 0.1 --- -- Wrapped code from StackOverflow_ +- Wrapped code from [StackOverflow](http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client) - Added exception handling to destructor in case of connection failure - -.. _StackOverflow: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client diff --git a/MANIFEST.in b/MANIFEST.in index 346209d..4396c68 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ recursive-include socketIO_client * -include *.rst +include *.md global-exclude *.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..a7a7152 --- /dev/null +++ b/README.md @@ -0,0 +1,156 @@ +socketIO-client +=============== +Here is a [socket.io](http://socket.io) client library for Python. You can use it to write test code for your socket.io server. + + +Installation +------------ +Install the package in an isolated environment. + + VIRTUAL_ENV=$HOME/.virtualenv + + # Prepare isolated environment + virtualenv $VIRTUAL_ENV + + # Activate isolated environment + source $VIRTUAL_ENV/bin/activate + + # Install package + pip install -U socketIO-client + + +Usage +----- +Activate isolated environment. + + VIRTUAL_ENV=$HOME/.virtualenv + source $VIRTUAL_ENV/bin/activate + +Launch the your socket.io server. + + node serve_tests.js + +For debugging information, run these commands first. + + import logging + logging.basicConfig(level=logging.DEBUG) + +Emit. + + from socketIO_client import SocketIO, LoggingNamespace + + with SocketIO('localhost', 8000, LoggingNamespace) as socketIO: + socketIO.emit('aaa') + socketIO.wait(seconds=1) + +Emit with callback. + + from socketIO_client import SocketIO, LoggingNamespace + + def on_bbb_response(*args): + print 'on_bbb_response', args + + with SocketIO('localhost', 8000, LoggingNamespace) as socketIO: + socketIO.emit('bbb', {'xxx': 'yyy'}, on_bbb_response) + socketIO.wait_for_callbacks(seconds=1) + +Define events. + + from socketIO_client import SocketIO, LoggingNamespace + + def on_aaa_response(*args): + print 'on_aaa_response', args + + socketIO = SocketIO('localhost', 8000, LoggingNamespace) + socketIO.on('aaa_response', on_aaa_response) + socketIO.emit('aaa') + socketIO.wait(seconds=1) + +Define events in a namespace. + + from socketIO_client import SocketIO, BaseNamespace + + class Namespace(BaseNamespace): + + def on_aaa_response(self, *args): + print 'on_aaa_response', args + self.emit('bbb') + + socketIO = SocketIO('localhost', 8000, Namespace) + socketIO.emit('aaa') + socketIO.wait(seconds=1) + +Define standard events. + + from socketIO_client import SocketIO, BaseNamespace + + class Namespace(BaseNamespace): + + def on_connect(self): + print '[Connected]' + + socketIO = SocketIO('localhost', 8000, Namespace) + socketIO.wait(seconds=1) + +Define different namespaces on a single socket. + + from socketIO_client import SocketIO, BaseNamespace + + class ChatNamespace(BaseNamespace): + + def on_aaa_response(self, *args): + print 'on_aaa_response', args + + class NewsNamespace(BaseNamespace): + + def on_aaa_response(self, *args): + print 'on_aaa_response', args + + socketIO = SocketIO('localhost', 8000) + chat_namespace = socketIO.define(ChatNamespace, '/chat') + news_namespace = socketIO.define(NewsNamespace, '/news') + + chat_namespace.emit('aaa') + news_namespace.emit('aaa') + socketIO.wait(seconds=1) + +Connect via SSL. + + from socketIO_client import SocketIO + + SocketIO('https://localhost', verify=False) + +Specify params, headers, cookies, proxies thanks to the [requests](http://python-requests.org) library. + + from socketIO_client import SocketIO + from base64 import b64encode + + SocketIO('localhost', 8000, + params={'q': 'qqq'}, + headers={'Authorization': 'Basic ' + b64encode('username:password')}, + cookies={'a': 'aaa'}, + proxies={'https': 'https://proxy.example.com:8080'}) + +Wait forever. + + from socketIO_client import SocketIO + + socketIO = SocketIO('localhost') + socketIO.wait() + + +License +------- +This software is available under the MIT License. + + +Credits +------- +- [Guillermo Rauch](https://github.com/guille) wrote the [socket.io specification](https://github.com/LearnBoost/socket.io-spec). +- [Hiroki Ohtani](https://github.com/liris) wrote [websocket-client](https://github.com/liris/websocket-client). +- [rod](http://stackoverflow.com/users/370115/rod) wrote a [prototype for a Python client to a socket.io server](http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client) on StackOverflow. +- [Alexandre Bourget](https://github.com/abourget) wrote [gevent-socketio](https://github.com/abourget/gevent-socketio), which is a socket.io server written in Python. +- [Paul Kienzle](https://github.com/pkienzle), [Zac Lee](https://github.com/zratic), [Josh VanderLinden](https://github.com/codekoala), [Ian Fitzpatrick](https://github.com/GraphEffect), [Lucas Klein](https://github.com/lukashed), [Rui Chicoria](https://github.com/rchicoria), [Travis Odom](https://github.com/burstaholic), [Patrick Huber](https://github.com/stackmagic), [Brad Campbell](https://github.com/bradjc), [Daniel](https://github.com/dabidan), [Sean Arietta](https://github.com/sarietta) submitted code to expand support of the socket.io protocol. +- [Bernard Pratz](https://github.com/guyzmo), [Francis Bull](https://github.com/franbull) wrote prototypes to support xhr-polling and jsonp-polling. +- [Eric Chen](https://github.com/taiyangc), [Denis Zinevich](https://github.com/dzinevich), [Thiago Hersan](https://github.com/thiagohersan), [Nayef Copty](https://github.com/nayefc), [Jörgen Karlsson](https://github.com/jorgen-k), [Branden Ghena](https://github.com/brghena) suggested ways to make the connection more robust. +- [Merlijn van Deen](https://github.com/valhallasw), [Frederic Sureau](https://github.com/fredericsureau), [Marcus Cobden](https://github.com/leth), [Drew Hutchison](https://github.com/drewhutchison), [wuurrd](https://github.com/wuurrd), [Adam Kecer](https://github.com/amfg), [Alex Monk](https://github.com/Krenair), [Vishal](https://github.com/vishalwy), [John Vandenberg](https://github.com/jayvdb) proposed changes that make the library more friendly and practical for you! diff --git a/README.rst b/README.rst deleted file mode 100644 index cd5bb11..0000000 --- a/README.rst +++ /dev/null @@ -1,183 +0,0 @@ -socketIO-client -=============== -Here is a socket.io_ client library for Python. You can use it to write test code for your socket.io_ server. - - -Installation ------------- -:: - - VIRTUAL_ENV=$HOME/.virtualenv - - # Prepare isolated environment - virtualenv $VIRTUAL_ENV - - # Activate isolated environment - source $VIRTUAL_ENV/bin/activate - - # Install package - pip install -U socketIO-client - - -Usage ------ -Activate isolated environment. :: - - VIRTUAL_ENV=$HOME/.virtualenv - source $VIRTUAL_ENV/bin/activate - -For debugging information, run these commands first. :: - - import logging - logging.basicConfig(level=logging.DEBUG) - -Emit. :: - - from socketIO_client import SocketIO - - with SocketIO('localhost', 8000) as socketIO: - socketIO.emit('aaa') - socketIO.wait(seconds=1) - -Emit with callback. :: - - from socketIO_client import SocketIO - - def on_bbb_response(*args): - print 'on_bbb_response', args - - with SocketIO('localhost', 8000) as socketIO: - socketIO.emit('bbb', {'xxx': 'yyy'}, on_bbb_response) - socketIO.wait_for_callbacks(seconds=1) - -Define events. :: - - from socketIO_client import SocketIO - - def on_aaa_response(*args): - print 'on_aaa_response', args - - socketIO = SocketIO('localhost', 8000) - socketIO.on('aaa_response', on_aaa_response) - socketIO.emit('aaa') - socketIO.wait(seconds=1) - -Define events in a namespace. :: - - from socketIO_client import SocketIO, BaseNamespace - - class Namespace(BaseNamespace): - - def on_aaa_response(self, *args): - print 'on_aaa_response', args - self.emit('bbb') - - socketIO = SocketIO('localhost', 8000, Namespace) - socketIO.emit('aaa') - socketIO.wait(seconds=1) - -Define standard events. :: - - from socketIO_client import SocketIO, BaseNamespace - - class Namespace(BaseNamespace): - - def on_connect(self): - print '[Connected]' - - socketIO = SocketIO('localhost', 8000, Namespace) - socketIO.wait(seconds=1) - -Define different namespaces on a single socket. :: - - from socketIO_client import SocketIO, BaseNamespace - - class ChatNamespace(BaseNamespace): - - def on_aaa_response(self, *args): - print 'on_aaa_response', args - - class NewsNamespace(BaseNamespace): - - def on_aaa_response(self, *args): - print 'on_aaa_response', args - - socketIO = SocketIO('localhost', 8000) - chat_namespace = socketIO.define(ChatNamespace, '/chat') - news_namespace = socketIO.define(NewsNamespace, '/news') - - chat_namespace.emit('aaa') - news_namespace.emit('aaa') - socketIO.wait(seconds=1) - -Connect via SSL. :: - - from socketIO_client import SocketIO - - SocketIO('https://localhost') - -Specify params, headers, cookies, proxies thanks to the `requests`_ library. :: - - from socketIO_client import SocketIO - from base64 import b64encode - - SocketIO('localhost', 8000, - resource='my.io', - params={'q': 'qqq'}, - headers={'Authorization': 'Basic ' + b64encode('username:password')}, - cookies={'a': 'aaa'}, - proxies={'https': 'https://proxy.example.com:8080'}) - -Wait forever. :: - - from socketIO_client import SocketIO - - socketIO = SocketIO('localhost') - socketIO.wait() - - -License -------- -This software is available under the MIT License. - - -Credits -------- -- `Guillermo Rauch`_ wrote the `socket.io specification`_. -- `Hiroki Ohtani`_ wrote websocket-client_. -- rod_ wrote a `prototype for a Python client to a socket.io server`_ on StackOverflow. -- `Alexandre Bourget`_ wrote gevent-socketio_, which is a socket.io server written in Python. -- `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_, `Rui Chicoria`_, `Travis Odom`_ submitted code to expand support of the socket.io protocol. -- `Bernard Pratz`_ and `Francis Bull`_ wrote prototypes to support xhr-polling and jsonp-polling. -- `Eric Chen`_, `Denis Zinevich`_, `Thiago Hersan`_, `Nayef Copty`_ suggested ways to make the connection more robust. - - -.. _socket.io: http://socket.io -.. _requests: http://python-requests.org - -.. _Guillermo Rauch: https://github.com/guille -.. _socket.io specification: https://github.com/LearnBoost/socket.io-spec - -.. _Hiroki Ohtani: https://github.com/liris -.. _websocket-client: https://github.com/liris/websocket-client - -.. _rod: http://stackoverflow.com/users/370115/rod -.. _prototype for a Python client to a socket.io server: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client - -.. _Alexandre Bourget: https://github.com/abourget -.. _gevent-socketio: https://github.com/abourget/gevent-socketio - -.. _Bernard Pratz: https://github.com/guyzmo -.. _Francis Bull: https://github.com/franbull -.. _Paul Kienzle: https://github.com/pkienzle -.. _Zac Lee: https://github.com/zratic -.. _Josh VanderLinden: https://github.com/codekoala -.. _Ian Fitzpatrick: https://github.com/GraphEffect -.. _Lucas Klein: https://github.com/lukashed -.. _Rui Chicoria: https://github.com/rchicoria -.. _Travis Odom: https://github.com/burstaholic -.. _Eric Chen: https://github.com/taiyangc -.. _Denis Zinevich: https://github.com/dzinevich -.. _Thiago Hersan: https://github.com/thiagohersan -.. _Nayef Copty: https://github.com/nayefc -.. _Patrick Huber: https://github.com/stackmagic diff --git a/setup.py b/setup.py index c26f2af..ced54e6 100644 --- a/setup.py +++ b/setup.py @@ -4,8 +4,8 @@ from socketIO_client import __version__ here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.rst')).read() -CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() +README = open(os.path.join(here, 'README.md')).read() +CHANGES = open(os.path.join(here, 'CHANGES.md')).read() setup( diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3105a13..3624484 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -9,7 +9,7 @@ except ImportError: from urlparse import urlparse as parse_url from .exceptions import ( - ConnectionError, TimeoutError, PacketError, SocketIOError) + SocketIOError, ConnectionError, TimeoutError, PacketError) from .transports import ( _get_response, TRANSPORTS, _WebsocketTransport, _XHR_PollingTransport, _JSONP_PollingTransport) @@ -182,11 +182,10 @@ class SocketIO(object): - Define the behavior of the client by specifying a custom Namespace. - Prefix host with https:// to use SSL. - Set wait_for_connection=True to block until we have a connection. - - Specify the transports you want to use. + - Specify desired transports=['websocket', 'xhr-polling']. - Pass query params, headers, cookies, proxies as keyword arguments. SocketIO('localhost', 8000, - resource='my.io', params={'q': 'qqq'}, headers={'Authorization': 'Basic ' + b64encode('username:password')}, cookies={'a': 'aaa'}, @@ -390,7 +389,7 @@ class SocketIO(object): try: return self._namespace_by_path[path] except KeyError: - raise PacketError('unexpected namespace path (%s)' % path) + raise PacketError('unhandled namespace path (%s)' % path) def _get_delegate(self, code): try: From 154874cfc9796d4b529ebeabc925368a5db7ea36 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 12:32:52 -0500 Subject: [PATCH 120/191] Update links --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a7a7152..7a918ab 100644 --- a/README.md +++ b/README.md @@ -146,11 +146,11 @@ This software is available under the MIT License. Credits ------- -- [Guillermo Rauch](https://github.com/guille) wrote the [socket.io specification](https://github.com/LearnBoost/socket.io-spec). +- [Guillermo Rauch](https://github.com/rauchg) wrote the [socket.io specification](https://github.com/LearnBoost/socket.io-spec). - [Hiroki Ohtani](https://github.com/liris) wrote [websocket-client](https://github.com/liris/websocket-client). - [rod](http://stackoverflow.com/users/370115/rod) wrote a [prototype for a Python client to a socket.io server](http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client) on StackOverflow. - [Alexandre Bourget](https://github.com/abourget) wrote [gevent-socketio](https://github.com/abourget/gevent-socketio), which is a socket.io server written in Python. -- [Paul Kienzle](https://github.com/pkienzle), [Zac Lee](https://github.com/zratic), [Josh VanderLinden](https://github.com/codekoala), [Ian Fitzpatrick](https://github.com/GraphEffect), [Lucas Klein](https://github.com/lukashed), [Rui Chicoria](https://github.com/rchicoria), [Travis Odom](https://github.com/burstaholic), [Patrick Huber](https://github.com/stackmagic), [Brad Campbell](https://github.com/bradjc), [Daniel](https://github.com/dabidan), [Sean Arietta](https://github.com/sarietta) submitted code to expand support of the socket.io protocol. +- [Paul Kienzle](https://github.com/pkienzle), [Zac Lee](https://github.com/zratic), [Josh VanderLinden](https://github.com/codekoala), [Ian Fitzpatrick](https://github.com/ifitzpatrick), [Lucas Klein](https://github.com/lukasklein), [Rui Chicoria](https://github.com/rchicoria), [Travis Odom](https://github.com/burstaholic), [Patrick Huber](https://github.com/stackmagic), [Brad Campbell](https://github.com/bradjc), [Daniel](https://github.com/dabidan), [Sean Arietta](https://github.com/sarietta) submitted code to expand support of the socket.io protocol. - [Bernard Pratz](https://github.com/guyzmo), [Francis Bull](https://github.com/franbull) wrote prototypes to support xhr-polling and jsonp-polling. - [Eric Chen](https://github.com/taiyangc), [Denis Zinevich](https://github.com/dzinevich), [Thiago Hersan](https://github.com/thiagohersan), [Nayef Copty](https://github.com/nayefc), [Jörgen Karlsson](https://github.com/jorgen-k), [Branden Ghena](https://github.com/brghena) suggested ways to make the connection more robust. -- [Merlijn van Deen](https://github.com/valhallasw), [Frederic Sureau](https://github.com/fredericsureau), [Marcus Cobden](https://github.com/leth), [Drew Hutchison](https://github.com/drewhutchison), [wuurrd](https://github.com/wuurrd), [Adam Kecer](https://github.com/amfg), [Alex Monk](https://github.com/Krenair), [Vishal](https://github.com/vishalwy), [John Vandenberg](https://github.com/jayvdb) proposed changes that make the library more friendly and practical for you! +- [Merlijn van Deen](https://github.com/valhallasw), [Frederic Sureau](https://github.com/fredericsureau), [Marcus Cobden](https://github.com/leth), [Drew Hutchison](https://github.com/drewhutchison), [wuurrd](https://github.com/wuurrd), [Adam Kecer](https://github.com/amfg), [Alex Monk](https://github.com/Krenair), [Vishal P R](https://github.com/vishalwy), [John Vandenberg](https://github.com/jayvdb), [Thomas Grainger](https://github.com/graingert) proposed changes that make the library more friendly and practical for you! From 326b152b0545a43a701be4eb74afd5457d6fc7a4 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 13:44:35 -0500 Subject: [PATCH 121/191] Fix .travis.yml --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 62e6d73..f3dda90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,6 @@ install: - pip install -U --use-mirrors six - pip install -U --use-mirrors websocket-client before_script: - - node serve-tests.js - - sleep 1 + - node serve-tests.js & + - sleep 3 script: nosetests From 97b3325d6f47de863d847c14fc9e56668ed8958d Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 14:12:33 -0500 Subject: [PATCH 122/191] Fix for Python 3 --- .travis.yml | 7 ++++--- setup.py | 9 ++++++--- socketIO_client/transports.py | 25 ++++++++++++++----------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index f3dda90..c3475fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,10 @@ before_install: - sudo apt-get install nodejs install: - npm install -G socket.io@0.9 - - pip install -U --use-mirrors requests - - pip install -U --use-mirrors six - - pip install -U --use-mirrors websocket-client + - pip install -U requests + - pip install -U six + - pip install -U websocket-client + - pip install -U coverage before_script: - node serve-tests.js & - sleep 3 diff --git a/setup.py b/setup.py index ced54e6..192eec0 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ import os from setuptools import setup, find_packages -from socketIO_client import __version__ here = os.path.abspath(os.path.dirname(__file__)) @@ -10,7 +9,7 @@ CHANGES = open(os.path.join(here, 'CHANGES.md')).read() setup( name='socketIO-client', - version=__version__, + version='0.5.4', description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', @@ -28,6 +27,10 @@ setup( 'six', 'websocket-client', ], + tests_require=[ + 'nose', + 'coverage', + ], packages=find_packages(), include_package_data=True, - zip_safe=True) + zip_safe=False) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 5e47f94..ca95777 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -1,3 +1,4 @@ +import codecs import json import logging import re @@ -8,7 +9,7 @@ import sys import time import websocket -from .exceptions import SocketIOError, ConnectionError, TimeoutError +from .exceptions import ConnectionError, TimeoutError if not hasattr(websocket, 'create_connection'): @@ -21,6 +22,11 @@ TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling' BOUNDARY = six.u('\ufffd') TIMEOUT_IN_SECONDS = 3 _log = logging.getLogger(__name__) +escape_unicode = codecs.getdecoder('unicode_escape') +try: + encode_unicode = lambda x: unicode(x).encode('utf-8') +except NameError: + encode_unicode = lambda x: x class _AbstractTransport(object): @@ -75,10 +81,7 @@ class _AbstractTransport(object): def send_packet(self, code, path='', data='', callback=None): packet_id = self.set_ack_callback(callback) if callback else '' - try: - packet_parts = str(code), packet_id, path, unicode(data).encode('utf-8') - except NameError: # fix for p3k - packet_parts = str(code), packet_id, path, data + packet_parts = str(code), packet_id, path, encode_unicode(data) packet_text = ':'.join(packet_parts) self.send(packet_text) self._log(logging.DEBUG, '[packet sent] %s', packet_text) @@ -90,7 +93,7 @@ class _AbstractTransport(object): except IndexError: pass for packet_text in self.recv(timeout=timeout): - _log.debug('[packet received] %s', packet_text) + self._log(logging.DEBUG, '[packet received] %s', packet_text) try: packet_parts = packet_text.split(':', 3) except AttributeError: @@ -136,7 +139,7 @@ class _WebsocketTransport(_AbstractTransport): self._url = url http_session = _prepare_http_session(kw) req = http_session.prepare_request(requests.Request('GET', url)) - headers = ['%s: %s' % item for item in req.headers.iteritems()] + headers = ['%s: %s' % item for item in req.headers.items()] try: self._connection = websocket.create_connection(url, header=headers) except socket.timeout as e: @@ -229,7 +232,7 @@ class _XHR_PollingTransport(_AbstractTransport): _get_response( self._http_session.get, self._url, - params=dict(self._params.items() + [('disconnect', True)])) + params=dict(list(self._params.items()) + [('disconnect', True)])) self._connected = False @@ -282,17 +285,17 @@ class _JSONP_PollingTransport(_AbstractTransport): self._log(logging.WARNING, '[packet error] %s', response_text) return if not response_text.startswith(BOUNDARY): - yield response_text.decode('unicode_escape') + yield escape_unicode(response_text) return for packet_text in _yield_text_from_framed_data( - response_text, parse=lambda x: x.decode('unicode_escape')): + response_text, parse=lambda x: escape_unicode(x)): yield packet_text def close(self): _get_response( self._http_session.get, self._url, - params=dict(self._params.items() + [('disconnect', True)])) + params=dict(list(self._params.items()) + [('disconnect', True)])) self._connected = False From 6627e360d7b5795abe0b248bd4529a0693cbcc13 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 14:23:46 -0500 Subject: [PATCH 123/191] Fix python3 ReferenceError --- README.md | 2 ++ socketIO_client/__init__.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b587fb8..ef3b3ed 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.org/invisibleroads/socketIO-client.svg?branch=master)](https://travis-ci.org/invisibleroads/socketIO-client) + socketIO-client =============== Here is a [socket.io](http://socket.io) client library for Python. You can use it to write test code for your socket.io server. diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 3624484..f06b5cb 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -289,7 +289,10 @@ class SocketIO(object): self.wait(seconds, for_callbacks=True) def disconnect(self, path=''): - self._transport.disconnect(path) + try: + self._transport.disconnect(path) + except ReferenceError: + pass try: namespace = self._namespace_by_path[path] namespace.on_disconnect() From 244c8c6f3fc7141c35b474d5ae5b6cff2eab117c Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 14:34:03 -0500 Subject: [PATCH 124/191] Fix --- socketIO_client/transports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index ca95777..6b441e0 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -288,7 +288,7 @@ class _JSONP_PollingTransport(_AbstractTransport): yield escape_unicode(response_text) return for packet_text in _yield_text_from_framed_data( - response_text, parse=lambda x: escape_unicode(x)): + response_text, escape_unicode): yield packet_text def close(self): From d61163569a516873bafd72464c02c812af256204 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 14:47:18 -0500 Subject: [PATCH 125/191] Fix escape_unicode --- socketIO_client/transports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 6b441e0..d99f591 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -22,7 +22,7 @@ TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling' BOUNDARY = six.u('\ufffd') TIMEOUT_IN_SECONDS = 3 _log = logging.getLogger(__name__) -escape_unicode = codecs.getdecoder('unicode_escape') +escape_unicode = lambda x: codecs.getdecoder('unicode_escape')(x)[0] try: encode_unicode = lambda x: unicode(x).encode('utf-8') except NameError: From 8dbfa58d7c8d87a7bf93b7d03f1e6e9af925bf9e Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 15:00:03 -0500 Subject: [PATCH 126/191] Fix more for python3 --- socketIO_client/transports.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index d99f591..2887efa 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -24,9 +24,11 @@ TIMEOUT_IN_SECONDS = 3 _log = logging.getLogger(__name__) escape_unicode = lambda x: codecs.getdecoder('unicode_escape')(x)[0] try: - encode_unicode = lambda x: unicode(x).encode('utf-8') + unicode except NameError: encode_unicode = lambda x: x +else: + encode_unicode = lambda x: unicode(x).encode('utf-8') class _AbstractTransport(object): @@ -57,7 +59,7 @@ class _AbstractTransport(object): self.send_packet(2) def message(self, path, data, callback): - if isinstance(data, basestring): + if isinstance(data, six.string_types): code = 3 else: code = 4 From d09f808cd3242cf699c90dc1d08b62201e26b8f3 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 15:04:56 -0500 Subject: [PATCH 127/191] Updated change log --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index c8895d7..b4649c1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ - Fixed reconnection in the event of server restart - Fixed calling on_reconnect() so that it is actually called - Set default Namespace=None +- Added support for Python 3.4 0.5.3 ----- From a42704e0ab629d21d5d62f96861048d7c3711004 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 15:24:56 -0500 Subject: [PATCH 128/191] Revert to RST for PyPI --- CHANGES.md => CHANGES.rst | 2 +- MANIFEST.in | 2 +- README.md | 158 ------------------------------------- README.rst | 160 ++++++++++++++++++++++++++++++++++++++ setup.py | 4 +- 5 files changed, 164 insertions(+), 162 deletions(-) rename CHANGES.md => CHANGES.rst (94%) delete mode 100644 README.md create mode 100644 README.rst diff --git a/CHANGES.md b/CHANGES.rst similarity index 94% rename from CHANGES.md rename to CHANGES.rst index b4649c1..eae9b14 100644 --- a/CHANGES.md +++ b/CHANGES.rst @@ -49,5 +49,5 @@ 0.1 --- -- Wrapped code from [StackOverflow](http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client) +- Wrapped `code from StackOverflow `_ - Added exception handling to destructor in case of connection failure diff --git a/MANIFEST.in b/MANIFEST.in index 4396c68..346209d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ recursive-include socketIO_client * -include *.md +include *.rst global-exclude *.pyc diff --git a/README.md b/README.md deleted file mode 100644 index ef3b3ed..0000000 --- a/README.md +++ /dev/null @@ -1,158 +0,0 @@ -[![Build Status](https://travis-ci.org/invisibleroads/socketIO-client.svg?branch=master)](https://travis-ci.org/invisibleroads/socketIO-client) - -socketIO-client -=============== -Here is a [socket.io](http://socket.io) client library for Python. You can use it to write test code for your socket.io server. - - -Installation ------------- -Install the package in an isolated environment. - - VIRTUAL_ENV=$HOME/.virtualenv - - # Prepare isolated environment - virtualenv $VIRTUAL_ENV - - # Activate isolated environment - source $VIRTUAL_ENV/bin/activate - - # Install package - pip install -U socketIO-client - - -Usage ------ -Activate isolated environment. - - VIRTUAL_ENV=$HOME/.virtualenv - source $VIRTUAL_ENV/bin/activate - -Launch your socket.io server. - - node serve-tests.js - -For debugging information, run these commands first. - - import logging - logging.basicConfig(level=logging.DEBUG) - -Emit. - - from socketIO_client import SocketIO, LoggingNamespace - - with SocketIO('localhost', 8000, LoggingNamespace) as socketIO: - socketIO.emit('aaa') - socketIO.wait(seconds=1) - -Emit with callback. - - from socketIO_client import SocketIO, LoggingNamespace - - def on_bbb_response(*args): - print 'on_bbb_response', args - - with SocketIO('localhost', 8000, LoggingNamespace) as socketIO: - socketIO.emit('bbb', {'xxx': 'yyy'}, on_bbb_response) - socketIO.wait_for_callbacks(seconds=1) - -Define events. - - from socketIO_client import SocketIO, LoggingNamespace - - def on_aaa_response(*args): - print 'on_aaa_response', args - - socketIO = SocketIO('localhost', 8000, LoggingNamespace) - socketIO.on('aaa_response', on_aaa_response) - socketIO.emit('aaa') - socketIO.wait(seconds=1) - -Define events in a namespace. - - from socketIO_client import SocketIO, BaseNamespace - - class Namespace(BaseNamespace): - - def on_aaa_response(self, *args): - print 'on_aaa_response', args - self.emit('bbb') - - socketIO = SocketIO('localhost', 8000, Namespace) - socketIO.emit('aaa') - socketIO.wait(seconds=1) - -Define standard events. - - from socketIO_client import SocketIO, BaseNamespace - - class Namespace(BaseNamespace): - - def on_connect(self): - print '[Connected]' - - socketIO = SocketIO('localhost', 8000, Namespace) - socketIO.wait(seconds=1) - -Define different namespaces on a single socket. - - from socketIO_client import SocketIO, BaseNamespace - - class ChatNamespace(BaseNamespace): - - def on_aaa_response(self, *args): - print 'on_aaa_response', args - - class NewsNamespace(BaseNamespace): - - def on_aaa_response(self, *args): - print 'on_aaa_response', args - - socketIO = SocketIO('localhost', 8000) - chat_namespace = socketIO.define(ChatNamespace, '/chat') - news_namespace = socketIO.define(NewsNamespace, '/news') - - chat_namespace.emit('aaa') - news_namespace.emit('aaa') - socketIO.wait(seconds=1) - -Connect via SSL. - - from socketIO_client import SocketIO - - SocketIO('https://localhost', verify=False) - -Specify params, headers, cookies, proxies thanks to the [requests](http://python-requests.org) library. - - from socketIO_client import SocketIO - from base64 import b64encode - - SocketIO('localhost', 8000, - params={'q': 'qqq'}, - headers={'Authorization': 'Basic ' + b64encode('username:password')}, - cookies={'a': 'aaa'}, - proxies={'https': 'https://proxy.example.com:8080'}) - -Wait forever. - - from socketIO_client import SocketIO - - socketIO = SocketIO('localhost') - socketIO.wait() - - -License -------- -This software is available under the MIT License. - - -Credits -------- -- [Guillermo Rauch](https://github.com/rauchg) wrote the [socket.io specification](https://github.com/LearnBoost/socket.io-spec). -- [Hiroki Ohtani](https://github.com/liris) wrote [websocket-client](https://github.com/liris/websocket-client). -- [rod](http://stackoverflow.com/users/370115/rod) wrote a [prototype for a Python client to a socket.io server](http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client) on StackOverflow. -- [Alexandre Bourget](https://github.com/abourget) wrote [gevent-socketio](https://github.com/abourget/gevent-socketio), which is a socket.io server written in Python. -- [Paul Kienzle](https://github.com/pkienzle), [Zac Lee](https://github.com/zratic), [Josh VanderLinden](https://github.com/codekoala), [Ian Fitzpatrick](https://github.com/ifitzpatrick), [Lucas Klein](https://github.com/lukasklein), [Rui Chicoria](https://github.com/rchicoria), [Travis Odom](https://github.com/burstaholic), [Patrick Huber](https://github.com/stackmagic), [Brad Campbell](https://github.com/bradjc), [Daniel](https://github.com/dabidan), [Sean Arietta](https://github.com/sarietta) submitted code to expand support of the socket.io protocol. -- [Bernard Pratz](https://github.com/guyzmo), [Francis Bull](https://github.com/franbull) wrote prototypes to support xhr-polling and jsonp-polling. -- [Eric Chen](https://github.com/taiyangc), [Denis Zinevich](https://github.com/dzinevich), [Thiago Hersan](https://github.com/thiagohersan), [Nayef Copty](https://github.com/nayefc), [Jörgen Karlsson](https://github.com/jorgen-k), [Branden Ghena](https://github.com/brghena) suggested ways to make the connection more robust. -- [Merlijn van Deen](https://github.com/valhallasw), [Frederic Sureau](https://github.com/fredericsureau), [Marcus Cobden](https://github.com/leth), [Drew Hutchison](https://github.com/drewhutchison), [wuurrd](https://github.com/wuurrd), [Adam Kecer](https://github.com/amfg), [Alex Monk](https://github.com/Krenair), [Vishal P R](https://github.com/vishalwy), [John Vandenberg](https://github.com/jayvdb), [Thomas Grainger](https://github.com/graingert) proposed changes that make the library more friendly and practical for you! diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..7bc0f48 --- /dev/null +++ b/README.rst @@ -0,0 +1,160 @@ +.. image:: https://travis-ci.org/invisibleroads/socketIO-client.svg?branch=v0.5.4 + :target: https://travis-ci.org/invisibleroads/socketIO-client + + +socketIO-client +=============== +Here is a `socket.io `_ client library for Python. You can use it to write test code for your socket.io server. + + +Installation +------------ +Install the package in an isolated environment. :: + + VIRTUAL_ENV=$HOME/.virtualenv + + # Prepare isolated environment + virtualenv $VIRTUAL_ENV + + # Activate isolated environment + source $VIRTUAL_ENV/bin/activate + + # Install package + pip install -U socketIO-client + + +Usage +----- +Activate isolated environment. :: + + VIRTUAL_ENV=$HOME/.virtualenv + source $VIRTUAL_ENV/bin/activate + +Launch your socket.io server. :: + + node serve-tests.js + +For debugging information, run these commands first. :: + + import logging + logging.basicConfig(level=logging.DEBUG) + +Emit. :: + + from socketIO_client import SocketIO, LoggingNamespace + + with SocketIO('localhost', 8000, LoggingNamespace) as socketIO: + socketIO.emit('aaa') + socketIO.wait(seconds=1) + +Emit with callback. :: + + from socketIO_client import SocketIO, LoggingNamespace + + def on_bbb_response(*args): + print 'on_bbb_response', args + + with SocketIO('localhost', 8000, LoggingNamespace) as socketIO: + socketIO.emit('bbb', {'xxx': 'yyy'}, on_bbb_response) + socketIO.wait_for_callbacks(seconds=1) + +Define events. :: + + from socketIO_client import SocketIO, LoggingNamespace + + def on_aaa_response(*args): + print 'on_aaa_response', args + + socketIO = SocketIO('localhost', 8000, LoggingNamespace) + socketIO.on('aaa_response', on_aaa_response) + socketIO.emit('aaa') + socketIO.wait(seconds=1) + +Define events in a namespace. :: + + from socketIO_client import SocketIO, BaseNamespace + + class Namespace(BaseNamespace): + + def on_aaa_response(self, *args): + print 'on_aaa_response', args + self.emit('bbb') + + socketIO = SocketIO('localhost', 8000, Namespace) + socketIO.emit('aaa') + socketIO.wait(seconds=1) + +Define standard events. :: + + from socketIO_client import SocketIO, BaseNamespace + + class Namespace(BaseNamespace): + + def on_connect(self): + print '[Connected]' + + socketIO = SocketIO('localhost', 8000, Namespace) + socketIO.wait(seconds=1) + +Define different namespaces on a single socket. :: + + from socketIO_client import SocketIO, BaseNamespace + + class ChatNamespace(BaseNamespace): + + def on_aaa_response(self, *args): + print 'on_aaa_response', args + + class NewsNamespace(BaseNamespace): + + def on_aaa_response(self, *args): + print 'on_aaa_response', args + + socketIO = SocketIO('localhost', 8000) + chat_namespace = socketIO.define(ChatNamespace, '/chat') + news_namespace = socketIO.define(NewsNamespace, '/news') + + chat_namespace.emit('aaa') + news_namespace.emit('aaa') + socketIO.wait(seconds=1) + +Connect via SSL. :: + + from socketIO_client import SocketIO + + SocketIO('https://localhost', verify=False) + +Specify params, headers, cookies, proxies thanks to the `requests `_ library. :: + + from socketIO_client import SocketIO + from base64 import b64encode + + SocketIO('localhost', 8000, + params={'q': 'qqq'}, + headers={'Authorization': 'Basic ' + b64encode('username:password')}, + cookies={'a': 'aaa'}, + proxies={'https': 'https://proxy.example.com:8080'}) + +Wait forever. :: + + from socketIO_client import SocketIO + + socketIO = SocketIO('localhost') + socketIO.wait() + + +License +------- +This software is available under the MIT License. + + +Credits +------- +- `Guillermo Rauch `_ wrote the `socket.io specification `_. +- `Hiroki Ohtani `_ wrote `websocket-client `_. +- `rod `_ wrote a `prototype for a Python client to a socket.io server `_. +- `Alexandre Bourget `_ wrote `gevent-socketio `_, which is a socket.io server written in Python. +- `Paul Kienzle `_, `Zac Lee `_, `Josh VanderLinden `_, `Ian Fitzpatrick `_, `Lucas Klein `_, `Rui Chicoria `_, `Travis Odom `_, `Patrick Huber `_, `Brad Campbell `_, `Daniel `_, `Sean Arietta `_ submitted code to expand support of the socket.io protocol. +- `Bernard Pratz `_, `Francis Bull `_ wrote prototypes to support xhr-polling and jsonp-polling. +- `Eric Chen `_, `Denis Zinevich `_, `Thiago Hersan `_, `Nayef Copty `_, `Jörgen Karlsson `_, `Branden Ghena `_ suggested ways to make the connection more robust. +- `Merlijn van Deen `_, `Frederic Sureau `_, `Marcus Cobden `_, `Drew Hutchison `_, `wuurrd `_, `Adam Kecer `_, `Alex Monk `_, `Vishal P R `_, `John Vandenberg `_, `Thomas Grainger `_ proposed changes that make the library more friendly and practical for you! diff --git a/setup.py b/setup.py index 192eec0..5d320f7 100644 --- a/setup.py +++ b/setup.py @@ -3,8 +3,8 @@ from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.md')).read() -CHANGES = open(os.path.join(here, 'CHANGES.md')).read() +README = open(os.path.join(here, 'README.rst')).read() +CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( From 006d1ac133fc34ab37cf459c4e26ec20e9c2415f Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 15:28:50 -0500 Subject: [PATCH 129/191] Bump version for PyPI --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5d320f7..b98da09 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( name='socketIO-client', - version='0.5.4', + version='0.5.5', description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', From e38628d1789f289900c63b7f336ac4126ef0543f Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 15:30:41 -0500 Subject: [PATCH 130/191] Update change log --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index eae9b14..9fcbea8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -0.5.4 +0.5.5 ----- - Fixed reconnection in the event of server restart - Fixed calling on_reconnect() so that it is actually called From 082c4e21eb262fc3f250b7dc948c0daa8016cb4c Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 16 Feb 2015 15:31:01 -0500 Subject: [PATCH 131/191] Update goals --- TODO.goals | 1 - 1 file changed, 1 deletion(-) diff --git a/TODO.goals b/TODO.goals index c6ddcaf..677aa74 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,4 +1,3 @@ -Release 0.5.4 Revive heartbeat as separate process thanks to sarietta Check that heartbeats are sent even with short wait time #64 Restore namespace as separate process #50 From 24ce2dc6c0629d8410092324e4f23aeadaf2d835 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Tue, 17 Feb 2015 10:53:00 -0500 Subject: [PATCH 132/191] Support Python 3.4 --- .travis.yml | 3 --- TODO.goals | 14 ++++++-------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index c3475fa..e6415c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,6 @@ python: - 2.6 - 2.7 - 3.4 -matrix: - allow_failures: - - python: 3.4 before_install: - sudo apt-get update - sudo apt-get install nodejs diff --git a/TODO.goals b/TODO.goals index 677aa74..a3b2527 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,10 +1,8 @@ -Revive heartbeat as separate process thanks to sarietta - Check that heartbeats are sent even with short wait time #64 -Restore namespace as separate process #50 -Upgrade to socket.io 1.2 #41 #52 +Release 0.6.1 #41 #52 + + Update serve-tests.js + + Integrate serve-tests.js changes from sarietta + = Put tests in index.html + Update tests Merge sarietta's pull request + Revive heartbeat as separate process Implement rooms #65 -Run under Python 3 #51 -Use continuous integration with TravisCI -Credit everyone who took the time to submit an issue or pull request -Release 0.6.1 From cd92f1c0cc5a605af16e04b5677bbd8e6ae1812b Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Tue, 17 Feb 2015 11:25:18 -0500 Subject: [PATCH 133/191] Start from scratch --- CHANGES.rst | 4 + MANIFEST.in | 2 +- README.rst | 7 +- TODO.goals | 4 +- setup.py | 27 +- socketIO_client/__init__.py | 523 +----------------- socketIO_client/exceptions.py | 14 - socketIO_client/tests.py | 225 -------- socketIO_client/tests/__init__.py | 41 ++ socketIO_client/tests/index.html | 4 + .../tests/serve.js | 37 +- socketIO_client/transports.py | 340 ------------ 12 files changed, 105 insertions(+), 1123 deletions(-) delete mode 100644 socketIO_client/exceptions.py delete mode 100644 socketIO_client/tests.py create mode 100644 socketIO_client/tests/__init__.py create mode 100644 socketIO_client/tests/index.html rename serve-tests.js => socketIO_client/tests/serve.js (68%) delete mode 100644 socketIO_client/transports.py diff --git a/CHANGES.rst b/CHANGES.rst index 9fcbea8..b7e7cb1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,7 @@ +0.6.1 +----- +- Upgraded to socket.io protocol 1.x thanks to Sean Arietta and Joe Palmer + 0.5.5 ----- - Fixed reconnection in the event of server restart diff --git a/MANIFEST.in b/MANIFEST.in index 346209d..abf7482 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ recursive-include socketIO_client * -include *.rst +include *.html *.js *.rst global-exclude *.pyc diff --git a/README.rst b/README.rst index 7bc0f48..9dab304 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -.. image:: https://travis-ci.org/invisibleroads/socketIO-client.svg?branch=v0.5.4 +.. image:: https://travis-ci.org/invisibleroads/socketIO-client.svg?branch=v0.6.1 :target: https://travis-ci.org/invisibleroads/socketIO-client @@ -30,9 +30,10 @@ Activate isolated environment. :: VIRTUAL_ENV=$HOME/.virtualenv source $VIRTUAL_ENV/bin/activate -Launch your socket.io server. :: +Launch a socket.io server. :: - node serve-tests.js + PACKAGE_FOLDER=`python -c "import os, socketIO_client; print(os.path.dirname(socketIO_client.__file__))"` + node $PACKAGE_FOLDER/tests/serve.js For debugging information, run these commands first. :: diff --git a/TODO.goals b/TODO.goals index a3b2527..84ab464 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,7 +1,5 @@ Release 0.6.1 #41 #52 - + Update serve-tests.js - + Integrate serve-tests.js changes from sarietta - = Put tests in index.html + Put tests in index.html Update tests Merge sarietta's pull request Revive heartbeat as separate process diff --git a/setup.py b/setup.py index b98da09..bc147cb 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,24 @@ -import os -from setuptools import setup, find_packages +from os.path import abspath, dirname, join +from setuptools import find_packages, setup -here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.rst')).read() -CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() +REQUIREMENTS = [ + 'requests', + 'six', + 'websocket-client', +] +HERE = dirname(abspath(__file__)) +DESCRIPTION = '\n\n'.join(open(join(HERE, _)).read() for _ in [ + 'README.rst', + 'CHANGES.rst', +]) setup( name='socketIO-client', - version='0.5.5', + version='0.6.1', description='A socket.io client library', - long_description=README + '\n\n' + CHANGES, + long_description=DESCRIPTION, license='MIT', classifiers=[ 'Intended Audience :: Developers', @@ -22,11 +29,7 @@ setup( author='Roy Hyunjin Han', author_email='rhh@crosscompute.com', url='https://github.com/invisibleroads/socketIO-client', - install_requires=[ - 'requests', - 'six', - 'websocket-client', - ], + install_requires=REQUIREMENTS, tests_require=[ 'nose', 'coverage', diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index f06b5cb..d5fa6e5 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,522 +1,21 @@ -import logging -import json -import requests -import time -from collections import namedtuple -try: - from urllib.parse import urlparse as parse_url -except ImportError: - from urlparse import urlparse as parse_url - -from .exceptions import ( - SocketIOError, ConnectionError, TimeoutError, PacketError) -from .transports import ( - _get_response, TRANSPORTS, - _WebsocketTransport, _XHR_PollingTransport, _JSONP_PollingTransport) - - __version__ = '0.5.4' -_SocketIOSession = namedtuple('_SocketIOSession', [ - 'id', - 'heartbeat_timeout', - 'server_supported_transports', -]) -_log = logging.getLogger(__name__) -PROTOCOL_VERSION = 1 -RETRY_INTERVAL_IN_SECONDS = 1 + + +class EngineIO(object): + pass + + +class SocketIO(EngineIO): + pass class BaseNamespace(object): - 'Define client behavior' - - def __init__(self, _transport, path): - self._transport = _transport - self.path = path - self._was_connected = False - self._callback_by_event = {} - self.initialize() - - def initialize(self): - 'Initialize custom variables here; you can override this method' - pass - - def message(self, data='', callback=None): - self._transport.message(self.path, data, callback) - - def emit(self, event, *args, **kw): - callback, args = find_callback(args, kw) - self._transport.emit(self.path, event, args, callback) - - def disconnect(self): - self._transport.disconnect(self.path) - - def on(self, event, callback): - 'Define a callback to handle a custom event emitted by the server' - self._callback_by_event[event] = callback - - def on_connect(self): - 'Called after server connects; you can override this method' - pass - - def on_disconnect(self): - 'Called after server disconnects; you can override this method' - pass - - def on_heartbeat(self): - 'Called after server sends a heartbeat; you can override this method' - pass - - def on_message(self, data): - 'Called after server sends a message; you can override this method' - pass - - def on_event(self, event, *args): - """ - Called after server sends an event; you can override this method. - Called only if a custom event handler does not exist, - such as one defined by namespace.on('my_event', my_function). - """ - callback, args = find_callback(args) - if callback: - callback(*args) - - def on_error(self, reason, advice): - 'Called after server sends an error; you can override this method' - pass - - def on_noop(self): - 'Called after server sends a noop; you can override this method' - pass - - def on_open(self, *args): - pass - - def on_close(self, *args): - pass - - def on_retry(self, *args): - pass - - def on_reconnect(self, *args): - pass - - def _find_event_callback(self, event): - # Check callbacks defined by on() - try: - return self._callback_by_event[event] - except KeyError: - pass - # Convert connect to reconnect if we have seen connect already - if event == 'connect': - if not self._was_connected: - self._was_connected = True - else: - event = 'reconnect' - # Check callbacks defined explicitly or use on_event() - return getattr( - self, - 'on_' + event.replace(' ', '_'), - lambda *args: self.on_event(event, *args)) + pass class LoggingNamespace(BaseNamespace): - - def _log(self, level, msg, *attrs): - _log.log(level, '%s: %s' % (self._transport._url, msg), *attrs) - - def on_connect(self): - self._log(logging.DEBUG, '%s [connect]', self.path) - super(LoggingNamespace, self).on_connect() - - def on_disconnect(self): - self._log(logging.DEBUG, '%s [disconnect]', self.path) - super(LoggingNamespace, self).on_disconnect() - - def on_heartbeat(self): - self._log(logging.DEBUG, '%s [heartbeat]', self.path) - super(LoggingNamespace, self).on_heartbeat() - - def on_message(self, data): - self._log(logging.INFO, '%s [message] %s', self.path, data) - super(LoggingNamespace, self).on_message(data) - - def on_event(self, event, *args): - callback, args = find_callback(args) - arguments = [repr(_) for _ in args] - if callback: - arguments.append('callback(*args)') - self._log(logging.INFO, '%s [event] %s(%s)', self.path, event, - ', '.join(arguments)) - super(LoggingNamespace, self).on_event(event, *args) - - def on_error(self, reason, advice): - self._log(logging.INFO, '%s [error] %s', self.path, advice) - super(LoggingNamespace, self).on_error(reason, advice) - - def on_noop(self): - self._log(logging.INFO, '%s [noop]', self.path) - super(LoggingNamespace, self).on_noop() - - def on_open(self, *args): - self._log(logging.INFO, '%s [open] %s', self.path, args) - super(LoggingNamespace, self).on_open(*args) - - def on_close(self, *args): - self._log(logging.INFO, '%s [close] %s', self.path, args) - super(LoggingNamespace, self).on_close(*args) - - def on_retry(self, *args): - self._log(logging.INFO, '%s [retry] %s', self.path, args) - super(LoggingNamespace, self).on_retry(*args) - - def on_reconnect(self, *args): - self._log(logging.INFO, '%s [reconnect] %s', self.path, args) - super(LoggingNamespace, self).on_reconnect(*args) - - -class SocketIO(object): - - """Create a socket.io client that connects to a socket.io server - at the specified host and port. - - - Define the behavior of the client by specifying a custom Namespace. - - Prefix host with https:// to use SSL. - - Set wait_for_connection=True to block until we have a connection. - - Specify desired transports=['websocket', 'xhr-polling']. - - Pass query params, headers, cookies, proxies as keyword arguments. - - SocketIO('localhost', 8000, - params={'q': 'qqq'}, - headers={'Authorization': 'Basic ' + b64encode('username:password')}, - cookies={'a': 'aaa'}, - proxies={'https': 'https://proxy.example.com:8080'}) - """ - - def __init__( - self, host, port=None, Namespace=None, - wait_for_connection=True, transports=TRANSPORTS, - resource='socket.io', **kw): - self.is_secure, self._base_url = _parse_host(host, port, resource) - self.wait_for_connection = wait_for_connection - self._namespace_by_path = {} - self._client_supported_transports = transports - self._kw = kw - if Namespace: - self.define(Namespace) - - def _log(self, level, msg, *attrs): - _log.log(level, '%s: %s' % (self._base_url, msg), *attrs) - - def __enter__(self): - return self - - def __exit__(self, *exception_pack): - self.disconnect() - - def __del__(self): - self.disconnect() - - def define(self, Namespace, path=''): - if path: - self._transport.connect(path) - namespace = Namespace(self._transport, path) - self._namespace_by_path[path] = namespace - return namespace - - def on(self, event, callback, path=''): - if path not in self._namespace_by_path: - self.define(BaseNamespace, path) - return self.get_namespace(path).on(event, callback) - - def message(self, data='', callback=None, path=''): - self._transport.message(path, data, callback) - - def emit(self, event, *args, **kw): - path = kw.get('path', '') - callback, args = find_callback(args, kw) - self._transport.emit(path, event, args, callback) - - def wait(self, seconds=None, for_callbacks=False): - """Wait in a loop and process events as defined in the namespaces. - - - Omit seconds, i.e. call wait() without arguments, to wait forever. - """ - warning_screen = _yield_warning_screen(seconds) - timeout = min(self._heartbeat_interval, seconds) - for elapsed_time in warning_screen: - if self._stop_waiting(for_callbacks): - break - try: - try: - self._process_events(timeout) - except TimeoutError: - pass - next(self._heartbeat_pacemaker) - except ConnectionError as e: - try: - warning = Exception('[connection error] %s' % e) - warning_screen.throw(warning) - except StopIteration: - self._log(logging.WARNING, warning) - try: - namespace = self._namespace_by_path[''] - namespace.on_disconnect() - except KeyError: - pass - - def _process_events(self, timeout=None): - for packet in self._transport.recv_packet(timeout): - try: - self._process_packet(packet) - except PacketError as e: - self._log(logging.WARNING, '[packet error] %s', e) - - def _process_packet(self, packet): - code, packet_id, path, data = packet - namespace = self.get_namespace(path) - delegate = self._get_delegate(code) - delegate(packet, namespace._find_event_callback) - - def _stop_waiting(self, for_callbacks): - # Use __transport to make sure that we do not reconnect inadvertently - if for_callbacks and not self.__transport.has_ack_callback: - return True - if self.__transport._wants_to_disconnect: - return True - return False - - def wait_for_callbacks(self, seconds=None): - self.wait(seconds, for_callbacks=True) - - def disconnect(self, path=''): - try: - self._transport.disconnect(path) - except ReferenceError: - pass - try: - namespace = self._namespace_by_path[path] - namespace.on_disconnect() - del self._namespace_by_path[path] - except KeyError: - pass - - @property - def connected(self): - try: - transport = self.__transport - except AttributeError: - return False - else: - return transport.connected - - @property - def _transport(self): - try: - if self.connected: - return self.__transport - except AttributeError: - pass - socketIO_session = self._get_socketIO_session() - supported_transports = self._get_supported_transports(socketIO_session) - self._heartbeat_pacemaker = self._make_heartbeat_pacemaker( - heartbeat_timeout=socketIO_session.heartbeat_timeout) - next(self._heartbeat_pacemaker) - warning_screen = _yield_warning_screen(seconds=None) - for elapsed_time in warning_screen: - try: - self._transport_name = supported_transports.pop(0) - except IndexError: - raise ConnectionError('Could not negotiate a transport') - try: - self.__transport = self._get_transport( - socketIO_session, self._transport_name) - break - except ConnectionError: - pass - for path, namespace in self._namespace_by_path.items(): - namespace._transport = self.__transport - if path: - self.__transport.connect(path) - return self.__transport - - def _get_socketIO_session(self): - warning_screen = _yield_warning_screen(seconds=None) - for elapsed_time in warning_screen: - try: - return _get_socketIO_session( - self.is_secure, self._base_url, **self._kw) - except ConnectionError as e: - if not self.wait_for_connection: - raise - warning = Exception('[waiting for connection] %s' % e) - try: - warning_screen.throw(warning) - except StopIteration: - self._log(logging.WARNING, warning) - - def _get_supported_transports(self, session): - self._log( - logging.DEBUG, '[transports available] %s', - ' '.join(session.server_supported_transports)) - supported_transports = [ - x for x in self._client_supported_transports if - x in session.server_supported_transports] - if not supported_transports: - raise SocketIOError(' '.join([ - 'could not negotiate a transport:', - 'client supports %s but' % ', '.join( - self._client_supported_transports), - 'server supports %s' % ', '.join( - session.server_supported_transports), - ])) - return supported_transports - - def _get_transport(self, session, transport_name): - self._log(logging.DEBUG, '[transport chosen] %s', transport_name) - return { - 'websocket': _WebsocketTransport, - 'xhr-polling': _XHR_PollingTransport, - 'jsonp-polling': _JSONP_PollingTransport, - }[transport_name](session, self.is_secure, self._base_url, **self._kw) - - def _make_heartbeat_pacemaker(self, heartbeat_timeout): - self._heartbeat_interval = heartbeat_timeout / 2 - heartbeat_time = time.time() - while True: - yield - if time.time() - heartbeat_time > self._heartbeat_interval: - heartbeat_time = time.time() - self._transport.send_heartbeat() - - def get_namespace(self, path=''): - try: - return self._namespace_by_path[path] - except KeyError: - raise PacketError('unhandled namespace path (%s)' % path) - - def _get_delegate(self, code): - try: - return { - '0': self._on_disconnect, - '1': self._on_connect, - '2': self._on_heartbeat, - '3': self._on_message, - '4': self._on_json, - '5': self._on_event, - '6': self._on_ack, - '7': self._on_error, - '8': self._on_noop, - }[code] - except KeyError: - raise PacketError('unexpected code (%s)' % code) - - def _on_disconnect(self, packet, find_event_callback): - find_event_callback('disconnect')() - - def _on_connect(self, packet, find_event_callback): - find_event_callback('connect')() - - def _on_heartbeat(self, packet, find_event_callback): - find_event_callback('heartbeat')() - - def _on_message(self, packet, find_event_callback): - code, packet_id, path, data = packet - args = [data] - if packet_id: - args.append(self._prepare_to_send_ack(path, packet_id)) - find_event_callback('message')(*args) - - def _on_json(self, packet, find_event_callback): - code, packet_id, path, data = packet - args = [json.loads(data)] - if packet_id: - args.append(self._prepare_to_send_ack(path, packet_id)) - find_event_callback('message')(*args) - - def _on_event(self, packet, find_event_callback): - code, packet_id, path, data = packet - value_by_name = json.loads(data) - event = value_by_name['name'] - args = value_by_name.get('args', []) - if packet_id: - args.append(self._prepare_to_send_ack(path, packet_id)) - find_event_callback(event)(*args) - - def _on_ack(self, packet, find_event_callback): - code, packet_id, path, data = packet - data_parts = data.split('+', 1) - packet_id = data_parts[0] - try: - ack_callback = self._transport.get_ack_callback(packet_id) - except KeyError: - return - args = json.loads(data_parts[1]) if len(data_parts) > 1 else [] - ack_callback(*args) - - def _on_error(self, packet, find_event_callback): - code, packet_id, path, data = packet - reason, advice = data.split('+', 1) - find_event_callback('error')(reason, advice) - - def _on_noop(self, packet, find_event_callback): - find_event_callback('noop')() - - def _prepare_to_send_ack(self, path, packet_id): - 'Return function that acknowledges the server' - return lambda *args: self._transport.ack(path, packet_id, *args) + pass def find_callback(args, kw=None): - 'Return callback whether passed as a last argument or as a keyword' - if args and callable(args[-1]): - return args[-1], args[:-1] - try: - return kw['callback'], args - except (KeyError, TypeError): - return None, args - - -def _parse_host(host, port, resource): - if not host.startswith('http'): - host = 'http://' + host - url_pack = parse_url(host) - is_secure = url_pack.scheme == 'https' - port = port or url_pack.port or (443 if is_secure else 80) - base_url = '%s:%d%s/%s/%s' % ( - url_pack.hostname, port, url_pack.path, resource, PROTOCOL_VERSION) - return is_secure, base_url - - -def _yield_warning_screen(seconds=None): - last_warning = None - for elapsed_time in _yield_elapsed_time(seconds): - try: - yield elapsed_time - except Exception as warning: - warning = str(warning) - if last_warning != warning: - last_warning = warning - _log.warn(warning) - time.sleep(RETRY_INTERVAL_IN_SECONDS) - - -def _yield_elapsed_time(seconds=None): - start_time = time.time() - if seconds is None: - while True: - yield time.time() - start_time - while time.time() - start_time < seconds: - yield time.time() - start_time - - -def _get_socketIO_session(is_secure, base_url, **kw): - server_url = '%s://%s/' % ('https' if is_secure else 'http', base_url) - try: - response = _get_response(requests.get, server_url, **kw) - except TimeoutError as e: - raise ConnectionError(e) - response_parts = response.text.split(':') - return _SocketIOSession( - id=response_parts[0], - heartbeat_timeout=int(response_parts[1]), - server_supported_transports=response_parts[3].split(',')) + pass diff --git a/socketIO_client/exceptions.py b/socketIO_client/exceptions.py deleted file mode 100644 index ed2b4d2..0000000 --- a/socketIO_client/exceptions.py +++ /dev/null @@ -1,14 +0,0 @@ -class SocketIOError(Exception): - pass - - -class ConnectionError(SocketIOError): - pass - - -class TimeoutError(SocketIOError): - pass - - -class PacketError(SocketIOError): - pass diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py deleted file mode 100644 index 400d626..0000000 --- a/socketIO_client/tests.py +++ /dev/null @@ -1,225 +0,0 @@ -import logging -import time -from unittest import TestCase - -from . import SocketIO, LoggingNamespace, find_callback -from .transports import TIMEOUT_IN_SECONDS - - -HOST = 'localhost' -PORT = 8000 -DATA = 'xxx' -PAYLOAD = {'xxx': 'yyy'} -logging.basicConfig(level=logging.DEBUG) - - -class BaseMixin(object): - - def setUp(self): - self.called_on_response = False - - def tearDown(self): - del self.socketIO - - def on_response(self, *args): - for arg in args: - if isinstance(arg, dict): - self.assertEqual(arg, PAYLOAD) - else: - self.assertEqual(arg, DATA) - self.called_on_response = True - - def test_disconnect(self): - 'Disconnect' - self.socketIO.define(LoggingNamespace) - self.assertTrue(self.socketIO.connected) - self.socketIO.disconnect() - self.assertFalse(self.socketIO.connected) - # Use context manager - with SocketIO(HOST, PORT, Namespace) as self.socketIO: - namespace = self.socketIO.get_namespace() - self.assertFalse(namespace.called_on_disconnect) - self.assertTrue(self.socketIO.connected) - self.assertTrue(namespace.called_on_disconnect) - self.assertFalse(self.socketIO.connected) - - def test_message(self): - 'Message' - namespace = self.socketIO.define(Namespace) - self.socketIO.message() - self.socketIO.wait(self.wait_time_in_seconds) - self.assertEqual(namespace.response, 'message_response') - - def test_message_with_data(self): - 'Message with data' - namespace = self.socketIO.define(Namespace) - self.socketIO.message(DATA) - self.socketIO.wait(self.wait_time_in_seconds) - self.assertEqual(namespace.response, DATA) - - def test_message_with_payload(self): - 'Message with payload' - namespace = self.socketIO.define(Namespace) - self.socketIO.message(PAYLOAD) - self.socketIO.wait(self.wait_time_in_seconds) - self.assertEqual(namespace.response, PAYLOAD) - - def test_message_with_callback(self): - 'Message with callback' - self.socketIO.define(LoggingNamespace) - self.socketIO.message(callback=self.on_response) - self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) - self.assertTrue(self.called_on_response) - - def test_message_with_callback_with_data(self): - 'Message with callback with data' - self.socketIO.define(LoggingNamespace) - self.socketIO.message(DATA, self.on_response) - self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) - self.assertTrue(self.called_on_response) - - def test_emit(self): - 'Emit' - namespace = self.socketIO.define(Namespace) - self.socketIO.emit('emit') - self.socketIO.wait(self.wait_time_in_seconds) - self.assertEqual(namespace.args_by_event, { - 'emit_response': (), - }) - - def test_emit_with_payload(self): - 'Emit with payload' - namespace = self.socketIO.define(Namespace) - self.socketIO.emit('emit_with_payload', PAYLOAD) - self.socketIO.wait(self.wait_time_in_seconds) - self.assertEqual(namespace.args_by_event, { - 'emit_with_payload_response': (PAYLOAD,), - }) - - def test_emit_with_multiple_payloads(self): - 'Emit with multiple payloads' - namespace = self.socketIO.define(Namespace) - self.socketIO.emit('emit_with_multiple_payloads', PAYLOAD, PAYLOAD) - self.socketIO.wait(self.wait_time_in_seconds) - self.assertEqual(namespace.args_by_event, { - 'emit_with_multiple_payloads_response': (PAYLOAD, PAYLOAD), - }) - - def test_emit_with_callback(self): - 'Emit with callback' - self.socketIO.define(LoggingNamespace) - self.socketIO.emit('emit_with_callback', self.on_response) - self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) - self.assertTrue(self.called_on_response) - - def test_emit_with_callback_with_payload(self): - 'Emit with callback with payload' - self.socketIO.define(LoggingNamespace) - self.socketIO.emit( - 'emit_with_callback_with_payload', self.on_response) - self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) - self.assertTrue(self.called_on_response) - - def test_emit_with_callback_with_multiple_payloads(self): - 'Emit with callback with multiple payloads' - self.socketIO.define(LoggingNamespace) - self.socketIO.emit( - 'emit_with_callback_with_multiple_payloads', self.on_response) - self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) - self.assertTrue(self.called_on_response) - - def test_emit_with_event(self): - 'Emit to trigger an event' - self.socketIO.on('emit_with_event_response', self.on_response) - self.socketIO.emit('emit_with_event', PAYLOAD) - self.socketIO.wait(self.wait_time_in_seconds) - self.assertTrue(self.called_on_response) - - def test_ack(self): - 'Trigger server callback' - namespace = self.socketIO.define(Namespace) - self.socketIO.emit('ack', PAYLOAD) - self.socketIO.wait(self.wait_time_in_seconds) - self.assertEqual(namespace.args_by_event, { - 'ack_response': (PAYLOAD,), - 'ack_callback_response': (PAYLOAD,), - }) - - def test_wait_with_disconnect(self): - 'Exit loop when the client wants to disconnect' - self.socketIO.define(Namespace) - self.socketIO.emit('wait_with_disconnect') - timeout_in_seconds = 5 - start_time = time.time() - self.socketIO.wait(timeout_in_seconds) - self.assertTrue(time.time() - start_time < timeout_in_seconds) - - def test_namespace_emit(self): - 'Behave differently in different namespaces' - main_namespace = self.socketIO.define(Namespace) - chat_namespace = self.socketIO.define(Namespace, '/chat') - news_namespace = self.socketIO.define(Namespace, '/news') - news_namespace.emit('emit_with_payload', PAYLOAD) - self.socketIO.wait(self.wait_time_in_seconds) - self.assertEqual(main_namespace.args_by_event, {}) - self.assertEqual(chat_namespace.args_by_event, {}) - self.assertEqual(news_namespace.args_by_event, { - 'emit_with_payload_response': (PAYLOAD,), - }) - - def test_namespace_ack(self): - 'Trigger server callback' - chat_namespace = self.socketIO.define(Namespace, '/chat') - chat_namespace.emit('ack', PAYLOAD) - self.socketIO.wait(self.wait_time_in_seconds) - self.assertEqual(chat_namespace.args_by_event, { - 'ack_response': (PAYLOAD,), - 'ack_callback_response': (PAYLOAD,), - }) - - -class Test_WebsocketTransport(TestCase, BaseMixin): - - def setUp(self): - super(Test_WebsocketTransport, self).setUp() - self.socketIO = SocketIO(HOST, PORT, transports=['websocket']) - self.wait_time_in_seconds = 0.1 - - -class Test_XHR_PollingTransport(TestCase, BaseMixin): - - def setUp(self): - super(Test_XHR_PollingTransport, self).setUp() - self.socketIO = SocketIO(HOST, PORT, transports=['xhr-polling']) - self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1 - - -class Test_JSONP_PollingTransport(TestCase, BaseMixin): - - def setUp(self): - super(Test_JSONP_PollingTransport, self).setUp() - self.socketIO = SocketIO(HOST, PORT, transports=['jsonp-polling']) - self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1 - - -class Namespace(LoggingNamespace): - - def initialize(self): - self.response = None - self.args_by_event = {} - self.called_on_disconnect = False - - def on_disconnect(self): - self.called_on_disconnect = True - - def on_message(self, data): - self.response = data - - def on_event(self, event, *args): - callback, args = find_callback(args) - if callback: - callback(*args) - self.args_by_event[event] = args - - def on_wait_with_disconnect_response(self): - self.disconnect() diff --git a/socketIO_client/tests/__init__.py b/socketIO_client/tests/__init__.py new file mode 100644 index 0000000..d7f6c61 --- /dev/null +++ b/socketIO_client/tests/__init__.py @@ -0,0 +1,41 @@ +import logging +from unittest import TestCase + +from .. import SocketIO, LoggingNamespace, find_callback + + +HOST = 'localhost' +PORT = 8000 +logging.basicConfig(level=logging.DEBUG) + + +class BaseMixin(object): + + def test_emit(self): + 'Emit' + namespace = self.socketIO.define(Namespace) + self.socketIO.emit('emit') + self.socketIO.wait(self.wait_time_in_seconds) + self.assertEqual(namespace.args_by_event, { + 'emit_response': (), + }) + + +class Test_XHR_PollingTransport(TestCase, BaseMixin): + + def setUp(self): + super(Test_XHR_PollingTransport, self).setUp() + self.socketIO = SocketIO(HOST, PORT, transports=['xhr-polling']) + self.wait_time_in_seconds = 1 + + +class Namespace(LoggingNamespace): + + def initialize(self): + self.args_by_event = {} + + def on_event(self, event, *args): + callback, args = find_callback(args) + if callback: + callback(*args) + self.args_by_event[event] = args diff --git a/socketIO_client/tests/index.html b/socketIO_client/tests/index.html new file mode 100644 index 0000000..86299b8 --- /dev/null +++ b/socketIO_client/tests/index.html @@ -0,0 +1,4 @@ + + diff --git a/serve-tests.js b/socketIO_client/tests/serve.js similarity index 68% rename from serve-tests.js rename to socketIO_client/tests/serve.js index 6c2edfc..7dd9232 100644 --- a/serve-tests.js +++ b/socketIO_client/tests/serve.js @@ -1,17 +1,25 @@ -var io = require('socket.io').listen(8000); +// DEBUG=* node serve.js -var main = io.of('').on('connection', function(socket) { +var app = require('http').createServer(serve).listen(8000); +var io = require('socket.io')(app); +var fs = require('fs'); +var PAYLOAD = {'xxx': 'yyy'}; + +io.on('connection', function(socket) { socket.on('message', function(data, fn) { - if (fn) { // Client expects a callback + if (fn) { + // Client requests callback if (data) { fn(data); } else { fn(); } } else if (typeof data === 'object') { - socket.json.send(data ? data : 'message_response'); // object or null + // Data has type object or is null + socket.json.send(data ? data : 'message_response'); } else { - socket.send(data ? data : 'message_response'); // string or '' + // Data has type string or is '' + socket.send(data ? data : 'message_response'); } }); socket.on('emit', function() { @@ -20,8 +28,8 @@ var main = io.of('').on('connection', function(socket) { socket.on('emit_with_payload', function(payload) { socket.emit('emit_with_payload_response', payload); }); - socket.on('emit_with_multiple_payloads', function(payload, payload) { - socket.emit('emit_with_multiple_payloads_response', payload, payload); + socket.on('emit_with_multiple_payloads', function(payload1, payload2) { + socket.emit('emit_with_multiple_payloads_response', payload1, payload2); }); socket.on('emit_with_callback', function(fn) { fn(); @@ -44,16 +52,14 @@ var main = io.of('').on('connection', function(socket) { socket.emit('aaa_response', PAYLOAD); }); socket.on('bbb', function(payload, fn) { - if (fn) { - fn(payload); - } + if (fn) fn(payload); }); socket.on('wait_with_disconnect', function() { socket.emit('wait_with_disconnect_response'); }); }); -var chat = io.of('/chat').on('connection', function (socket) { +io.of('/chat').on('connection', function(socket) { socket.on('emit_with_payload', function(payload) { socket.emit('emit_with_payload_response', payload); }); @@ -67,7 +73,7 @@ var chat = io.of('/chat').on('connection', function (socket) { }); }); -var news = io.of('/news').on('connection', function (socket) { +io.of('/news').on('connection', function(socket) { socket.on('emit_with_payload', function(payload) { socket.emit('emit_with_payload_response', payload); }); @@ -76,4 +82,9 @@ var news = io.of('/news').on('connection', function (socket) { }); }); -var PAYLOAD = {'xxx': 'yyy'}; +function serve(req, res) { + fs.readFile(__dirname + '/index.html', function(err, data) { + res.writeHead(200); + res.end(data); + }); +} diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py deleted file mode 100644 index 2887efa..0000000 --- a/socketIO_client/transports.py +++ /dev/null @@ -1,340 +0,0 @@ -import codecs -import json -import logging -import re -import requests -import six -import socket -import sys -import time -import websocket - -from .exceptions import ConnectionError, TimeoutError - - -if not hasattr(websocket, 'create_connection'): - sys.exit("""Incompatible websocket implementation -- Please make sure that you have websocket-client installed -- Please remove other websocket implementations""") - - -TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling' -BOUNDARY = six.u('\ufffd') -TIMEOUT_IN_SECONDS = 3 -_log = logging.getLogger(__name__) -escape_unicode = lambda x: codecs.getdecoder('unicode_escape')(x)[0] -try: - unicode -except NameError: - encode_unicode = lambda x: x -else: - encode_unicode = lambda x: unicode(x).encode('utf-8') - - -class _AbstractTransport(object): - - def __init__(self): - self._packet_id = 0 - self._callback_by_packet_id = {} - self._wants_to_disconnect = False - self._packets = [] - - def _log(self, level, msg, *attrs): - _log.log(level, '[%s] %s' % (self._url, msg), *attrs) - - def disconnect(self, path=''): - if not path: - self._wants_to_disconnect = True - if not self.connected: - return - if path: - self.send_packet(0, path) - else: - self.close() - - def connect(self, path): - self.send_packet(1, path) - - def send_heartbeat(self): - self.send_packet(2) - - def message(self, path, data, callback): - if isinstance(data, six.string_types): - code = 3 - else: - code = 4 - data = json.dumps(data, ensure_ascii=False) - self.send_packet(code, path, data, callback) - - def emit(self, path, event, args, callback): - data = json.dumps(dict(name=event, args=args), ensure_ascii=False) - self.send_packet(5, path, data, callback) - - def ack(self, path, packet_id, *args): - packet_id = packet_id.rstrip('+') - data = '%s+%s' % ( - packet_id, - json.dumps(args, ensure_ascii=False), - ) if args else packet_id - self.send_packet(6, path, data) - - def noop(self, path=''): - self.send_packet(8, path) - - def send_packet(self, code, path='', data='', callback=None): - packet_id = self.set_ack_callback(callback) if callback else '' - packet_parts = str(code), packet_id, path, encode_unicode(data) - packet_text = ':'.join(packet_parts) - self.send(packet_text) - self._log(logging.DEBUG, '[packet sent] %s', packet_text) - - def recv_packet(self, timeout=None): - try: - while self._packets: - yield self._packets.pop(0) - except IndexError: - pass - for packet_text in self.recv(timeout=timeout): - self._log(logging.DEBUG, '[packet received] %s', packet_text) - try: - packet_parts = packet_text.split(':', 3) - except AttributeError: - self._log(logging.WARNING, '[packet error] %s', packet_text) - continue - code, packet_id, path, data = None, None, None, None - packet_count = len(packet_parts) - if 4 == packet_count: - code, packet_id, path, data = packet_parts - elif 3 == packet_count: - code, packet_id, path = packet_parts - elif 1 == packet_count: - code = packet_parts[0] - yield code, packet_id, path, data - - def _enqueue_packet(self, packet): - self._packets.append(packet) - - def set_ack_callback(self, callback): - 'Set callback to be called after server sends an acknowledgment' - self._packet_id += 1 - self._callback_by_packet_id[str(self._packet_id)] = callback - return '%s+' % self._packet_id - - def get_ack_callback(self, packet_id): - 'Get callback to be called after server sends an acknowledgment' - callback = self._callback_by_packet_id[packet_id] - del self._callback_by_packet_id[packet_id] - return callback - - @property - def has_ack_callback(self): - return True if self._callback_by_packet_id else False - - -class _WebsocketTransport(_AbstractTransport): - - def __init__(self, socketIO_session, is_secure, base_url, **kw): - super(_WebsocketTransport, self).__init__() - url = '%s://%s/websocket/%s' % ( - 'wss' if is_secure else 'ws', - base_url, socketIO_session.id) - self._url = url - http_session = _prepare_http_session(kw) - req = http_session.prepare_request(requests.Request('GET', url)) - headers = ['%s: %s' % item for item in req.headers.items()] - try: - self._connection = websocket.create_connection(url, header=headers) - except socket.timeout as e: - raise ConnectionError(e) - except socket.error as e: - raise ConnectionError(e) - self._connection.settimeout(TIMEOUT_IN_SECONDS) - - @property - def connected(self): - return self._connection.connected - - def send(self, packet_text): - try: - self._connection.send(packet_text) - except websocket.WebSocketTimeoutException as e: - message = 'timed out while sending %s (%s)' % (packet_text, e) - self._log(logging.WARNING, message) - raise TimeoutError(e) - except socket.error as e: - message = 'disconnected while sending %s (%s)' % (packet_text, e) - self._log(logging.WARNING, message) - raise ConnectionError(message) - - def recv(self, timeout=None): - if timeout: - self._connection.settimeout(timeout) - try: - yield self._connection.recv() - except websocket.WebSocketTimeoutException as e: - raise TimeoutError(e) - except websocket.SSLError as e: - if 'timed out' in e.message: - raise TimeoutError(e) - else: - raise ConnectionError(e) - except websocket.WebSocketConnectionClosedException as e: - raise ConnectionError('connection closed (%s)' % e) - except socket.error as e: - raise ConnectionError(e) - - def close(self): - self._connection.close() - - -class _XHR_PollingTransport(_AbstractTransport): - - def __init__(self, socketIO_session, is_secure, base_url, **kw): - super(_XHR_PollingTransport, self).__init__() - self._url = '%s://%s/xhr-polling/%s' % ( - 'https' if is_secure else 'http', - base_url, socketIO_session.id) - self._connected = True - self._http_session = _prepare_http_session(kw) - # Create connection - for packet in self.recv_packet(): - self._enqueue_packet(packet) - - @property - def connected(self): - return self._connected - - @property - def _params(self): - return dict(t=int(time.time())) - - def send(self, packet_text): - _get_response( - self._http_session.post, - self._url, - params=self._params, - data=packet_text, - timeout=TIMEOUT_IN_SECONDS) - - def recv(self, timeout=None): - response = _get_response( - self._http_session.get, - self._url, - params=self._params, - timeout=timeout or TIMEOUT_IN_SECONDS, - stream=True) - response_text = response.text - if not response_text.startswith(BOUNDARY): - yield response_text - return - for packet_text in _yield_text_from_framed_data(response_text): - yield packet_text - - def close(self): - _get_response( - self._http_session.get, - self._url, - params=dict(list(self._params.items()) + [('disconnect', True)])) - self._connected = False - - -class _JSONP_PollingTransport(_AbstractTransport): - - RESPONSE_PATTERN = re.compile(r'io.j\[(\d+)\]\("(.*)"\);') - - def __init__(self, socketIO_session, is_secure, base_url, **kw): - super(_JSONP_PollingTransport, self).__init__() - self._url = '%s://%s/jsonp-polling/%s' % ( - 'https' if is_secure else 'http', - base_url, socketIO_session.id) - self._connected = True - self._http_session = _prepare_http_session(kw) - self._id = 0 - # Create connection - for packet in self.recv_packet(): - self._enqueue_packet(packet) - - @property - def connected(self): - return self._connected - - @property - def _params(self): - return dict(t=int(time.time()), i=self._id) - - def send(self, packet_text): - _get_response( - self._http_session.post, - self._url, - params=self._params, - data='d=%s' % requests.utils.quote(json.dumps(packet_text)), - headers={'content-type': 'application/x-www-form-urlencoded'}, - timeout=TIMEOUT_IN_SECONDS) - - def recv(self, timeout=None): - 'Decode the JavaScript response so that we can parse it as JSON' - response = _get_response( - self._http_session.get, - self._url, - params=self._params, - headers={'content-type': 'text/javascript; charset=UTF-8'}, - timeout=timeout or TIMEOUT_IN_SECONDS) - response_text = response.text - try: - self._id, response_text = self.RESPONSE_PATTERN.match( - response_text).groups() - except AttributeError: - self._log(logging.WARNING, '[packet error] %s', response_text) - return - if not response_text.startswith(BOUNDARY): - yield escape_unicode(response_text) - return - for packet_text in _yield_text_from_framed_data( - response_text, escape_unicode): - yield packet_text - - def close(self): - _get_response( - self._http_session.get, - self._url, - params=dict(list(self._params.items()) + [('disconnect', True)])) - self._connected = False - - -def _yield_text_from_framed_data(framed_data, parse=lambda x: x): - parts = [parse(x) for x in framed_data.split(BOUNDARY)] - for text_length, text in zip(parts[1::2], parts[2::2]): - if text_length != str(len(text)): - warning = 'invalid declared length=%s for packet_text=%s' % ( - text_length, text) - _log.warn('[packet error] %s', warning) - continue - yield text - - -def _get_response(request, *args, **kw): - try: - response = request(*args, **kw) - except requests.exceptions.Timeout as e: - raise TimeoutError(e) - except requests.exceptions.ConnectionError as e: - raise ConnectionError(e) - except requests.exceptions.SSLError as e: - raise ConnectionError('could not negotiate SSL (%s)' % e) - status = response.status_code - if 200 != status: - raise ConnectionError('unexpected status code (%s)' % status) - return response - - -def _prepare_http_session(kw): - http_session = requests.Session() - http_session.headers.update(kw.get('headers', {})) - http_session.auth = kw.get('auth') - http_session.proxies.update(kw.get('proxies', {})) - http_session.hooks.update(kw.get('hooks', {})) - http_session.params.update(kw.get('params', {})) - http_session.verify = kw.get('verify') - http_session.cert = kw.get('cert') - http_session.cookies.update(kw.get('cookies', {})) - return http_session From d96831de1920f0f7a771f419ae4c3c994305efbf Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Tue, 17 Feb 2015 16:40:43 -0500 Subject: [PATCH 134/191] Restore proxy server --- .travis.yml | 6 ++++-- README.rst | 8 ++++++-- socketIO_client/tests/proxy.js | 30 ++++++++++++++++++++++++++++++ socketIO_client/tests/serve.js | 2 +- 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 socketIO_client/tests/proxy.js diff --git a/.travis.yml b/.travis.yml index e6415c3..af9d4b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,14 @@ before_install: - sudo apt-get update - sudo apt-get install nodejs install: - - npm install -G socket.io@0.9 + - npm install -G socket.io + - npm install -G http-proxy - pip install -U requests - pip install -U six - pip install -U websocket-client - pip install -U coverage before_script: - - node serve-tests.js & + - DEBUG=* node tests/serve.js & + - DEBUG=* node tests/proxy.js & - sleep 3 script: nosetests diff --git a/README.rst b/README.rst index 9dab304..9031bbe 100644 --- a/README.rst +++ b/README.rst @@ -30,10 +30,14 @@ Activate isolated environment. :: VIRTUAL_ENV=$HOME/.virtualenv source $VIRTUAL_ENV/bin/activate -Launch a socket.io server. :: +Launch your server. :: + # Get package folder PACKAGE_FOLDER=`python -c "import os, socketIO_client; print(os.path.dirname(socketIO_client.__file__))"` - node $PACKAGE_FOLDER/tests/serve.js + # Start socket.io server + DEBUG=* node $PACKAGE_FOLDER/tests/serve.js + # Start proxy server in a separate terminal on the same machine + DEBUG=* node $PACKAGE_FOLDER/tests/proxy.js For debugging information, run these commands first. :: diff --git a/socketIO_client/tests/proxy.js b/socketIO_client/tests/proxy.js new file mode 100644 index 0000000..178c752 --- /dev/null +++ b/socketIO_client/tests/proxy.js @@ -0,0 +1,30 @@ +var proxy = require('http-proxy').createProxyServer({ + target: {host: 'localhost', port: 9000} +}); +var server = require('http').createServer(function(req, res) { + console.log('[REQUEST.%s] %s', req.method, req.url); + console.log(req['headers']); + if (req.method == 'POST') { + var body = ''; + req.on('data', function (data) { + body += data; + }); + req.on('end', function () { + print_body('[REQUEST.BODY] ', body); + }); + } + var write = res.write; + res.write = function(data) { + print_body('[RESPONSE.BODY] ', data); + write.call(res, data); + } + proxy.web(req, res); +}); +function print_body(header, body) { + var text = String(body); + console.log(header + text); + for (var i = 0; i < text.length; i++) { + console.log('body[%s] = %s = %s', i, text[i], text.charCodeAt(i)); + } +} +server.listen(8000); diff --git a/socketIO_client/tests/serve.js b/socketIO_client/tests/serve.js index 7dd9232..c3e10c8 100644 --- a/socketIO_client/tests/serve.js +++ b/socketIO_client/tests/serve.js @@ -1,6 +1,6 @@ // DEBUG=* node serve.js -var app = require('http').createServer(serve).listen(8000); +var app = require('http').createServer(serve).listen(9000); var io = require('socket.io')(app); var fs = require('fs'); var PAYLOAD = {'xxx': 'yyy'}; From 5917925cccfd6e3841a42f4f81c8c61153239b7d Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 18 Feb 2015 09:47:52 -0500 Subject: [PATCH 135/191] Connect, emit, ping --- TODO.goals | 10 ++- socketIO_client/__init__.py | 158 +++++++++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 3 deletions(-) diff --git a/TODO.goals b/TODO.goals index 84ab464..50a9c42 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,6 +1,14 @@ Release 0.6.1 #41 #52 + Implement define + Implement wait + Implement on + + Get pong + Change print statements into logging statements + Clean up + Put tests in index.html Update tests - Merge sarietta's pull request Revive heartbeat as separate process + Merge sarietta's pull request Implement rooms #65 diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index d5fa6e5..d065bf2 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,12 +1,114 @@ +# !!! Get pong +# !!! Change print statements into logging statements + + +import json +import requests +import time + + __version__ = '0.5.4' +TRANSPORTS = [] class EngineIO(object): - pass + + _engine_io_protocol = 3 + _request_index = 0 + + def __init__( + self, host, port=None, + resource='engine.io'): + self._url = 'http://%s:%s/%s/' % (host, port, resource) + self._session = requests.Session() + + response = self._session.get(self._url, params={ + 'EIO': self._engine_io_protocol, + 'transport': 'polling', + 't': self._get_timestamp(), + }) + packs = _decode_content(response.content) + packet_type, packet = packs[0] + assert packet_type == 0 + packet_json = json.loads(packet) + print packet_json + # packet_json['pingInterval'] + # packet_json['pingTimeout'] + # packet_json['upgrades'] + self._session_id = packet_json['sid'] + + response = self._session.get(self._url, params={ + 'EIO': self._engine_io_protocol, + 'transport': 'polling', + 't': self._get_timestamp(), + 'sid': self._session_id, + }) + packs = _decode_content(response.content) + for packet_type, packet in packs: + print 'engineIO_packet_type = %s' % packet_type + print 'socketIO_packet_type = %s' % packet[0] + print 'packet = %s' % packet[1:] + + def _get_timestamp(self): + timestamp = '%s-%s' % (int(time.time() * 1000), self._request_index) + self._request_index += 1 + return timestamp + + def _message(self, packet): + packet_type = 4 + response = self._session.post(self._url, params={ + 'EIO': self._engine_io_protocol, + 'transport': 'polling', + 't': self._get_timestamp(), + 'sid': self._session_id, + }, data=_encode_content([(packet_type, packet)]), headers={ + 'content-type': 'application/octet-stream', + }) + packs = _decode_content(response.content) + for packet_type, packet in packs: + print 'engineIO_packet_type = %s' % packet_type + print 'socketIO_packet_type = %s' % packet[0] + print 'packet = %s' % packet[1:] + + def _ping(self): + packet_type = 2 + packet = '' + response = self._session.post(self._url, params={ + 'EIO': self._engine_io_protocol, + 'transport': 'polling', + 't': self._get_timestamp(), + 'sid': self._session_id, + }, data=_encode_content([(packet_type, packet)]), headers={ + 'content-type': 'application/octet-stream', + }) + packs = _decode_content(response.content) + for packet_type, packet in packs: + print 'engineIO_packet_type = %s' % packet_type + print 'socketIO_packet_type = %s' % packet[0] + print 'packet = %s' % packet[1:] class SocketIO(EngineIO): - pass + + def __init__( + self, host, port=None, Namespace=None, + wait_for_connection=True, transports=TRANSPORTS, + resource='socket.io', **kw): + super(SocketIO, self).__init__(host, port, resource) + + def define(self): + pass + + def wait(self): + pass + + def emit(self, event, *args, **kw): + packet_type = 2 + packet = json.dumps([event]) + self._message(str(packet_type) + packet) + + def on(self, event, callback): + pass class BaseNamespace(object): @@ -17,5 +119,57 @@ class LoggingNamespace(BaseNamespace): pass +def _decode_content(content): + print content + packs = [] + index = 0 + content_length = len(content) + while index < content_length: + try: + index, packet_length = _read_packet_length(content, index) + except IndexError: + break + index, packet = _read_packet(content, index, packet_length) + packet_type = int(packet[0]) + packet_payload = packet[1:] + packs.append((packet_type, packet_payload)) + return packs + + +def _encode_content(packs): + parts = [] + for packet_type, packet_payload in packs: + packet = str(packet_type) + str(packet_payload) + parts.append(_make_packet_header(packet) + packet) + return ''.join(parts) + + +def _read_packet_length(content, index): + while ord(content[index]) != 0: + index += 1 + index += 1 + packet_length_string = '' + while ord(content[index]) != 255: + packet_length_string += str(ord(content[index])) + index += 1 + return index, int(packet_length_string) + + +def _read_packet(content, index, packet_length): + while ord(content[index]) == 255: + index += 1 + packet = content[index:index + packet_length] + return index + packet_length, packet + + +def _make_packet_header(packet): + length_string = str(len(packet)) + header_digits = [0] + for index in xrange(len(length_string)): + header_digits.append(ord(length_string[index]) - 48) + header_digits.append(255) + return ''.join(chr(x) for x in header_digits) + + def find_callback(args, kw=None): pass From 98c5ecea7fca206ebd08fd0e13decfe9dd832812 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 18 Feb 2015 13:38:16 -0500 Subject: [PATCH 136/191] Add namespace structure --- socketIO_client/__init__.py | 284 ++++++++++++++++++++++----------- socketIO_client/tests/proxy.js | 5 +- 2 files changed, 199 insertions(+), 90 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index d065bf2..c4962a5 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,115 +1,219 @@ -# !!! Get pong -# !!! Change print statements into logging statements - - import json import requests import time -__version__ = '0.5.4' +__version__ = '0.6.1' TRANSPORTS = [] class EngineIO(object): - _engine_io_protocol = 3 - _request_index = 0 + _engineIO_protocol = 3 + _engineIO_request_index = 0 def __init__( - self, host, port=None, - resource='engine.io'): + self, + host, port=None, Namespace=None, + wait_for_connection=True, transports=TRANSPORTS, + resource='engine.io', **kw): self._url = 'http://%s:%s/%s/' % (host, port, resource) self._session = requests.Session() + print self._url response = self._session.get(self._url, params={ - 'EIO': self._engine_io_protocol, + 'EIO': self._engineIO_protocol, 'transport': 'polling', 't': self._get_timestamp(), }) - packs = _decode_content(response.content) - packet_type, packet = packs[0] - assert packet_type == 0 - packet_json = json.loads(packet) - print packet_json - # packet_json['pingInterval'] - # packet_json['pingTimeout'] - # packet_json['upgrades'] - self._session_id = packet_json['sid'] + print response.url - response = self._session.get(self._url, params={ - 'EIO': self._engine_io_protocol, - 'transport': 'polling', - 't': self._get_timestamp(), - 'sid': self._session_id, - }) - packs = _decode_content(response.content) - for packet_type, packet in packs: - print 'engineIO_packet_type = %s' % packet_type - print 'socketIO_packet_type = %s' % packet[0] - print 'packet = %s' % packet[1:] + engineIO_packets = _decode_content(response.content) + print engineIO_packets + engineIO_packet_type, engineIO_packet_data = engineIO_packets[0] + assert engineIO_packet_type == 0 + engineIO_packet_json = json.loads(engineIO_packet_data) + print engineIO_packet_json + # engineIO_packet_json['pingInterval'] + # engineIO_packet_json['pingTimeout'] + # engineIO_packet_json['upgrades'] + self._session_id = engineIO_packet_json['sid'] + + def wait(self): + while True: + self._process_packets() + + def _process_packets(self): + for engineIO_packet in self._recv_packet(): + self._process_packet(engineIO_packet) + + def _process_packet(self, packet): + engineIO_packet_type, engineIO_packet_data = packet + print 'engineIO_packet_type = %s' % engineIO_packet_type + engineIO_packet_data_parsed = _parse_engineIO_data( + engineIO_packet_data) + # Launch callbacks + namespace = self.get_namespace() + delegate = { + 0: self._on_open, + 1: self._on_close, + 2: self._on_ping, + 3: self._on_pong, + 4: self._on_message, + 5: self._on_upgrade, + 6: self._on_noop, + }[engineIO_packet_type] + delegate(engineIO_packet_data_parsed, namespace._find_packet_callback) + return engineIO_packet_data + + def get_namespace(self): + pass + + def _on_open(self): + pass + + def _on_close(self): + pass + + def _on_ping(self): + pass + + def _on_pong(self): + pass + + def _on_message(self): + pass + + def _on_upgrade(self): + pass + + def _on_noop(self): + pass def _get_timestamp(self): - timestamp = '%s-%s' % (int(time.time() * 1000), self._request_index) - self._request_index += 1 + timestamp = '%s-%s' % ( + int(time.time() * 1000), self._engineIO_request_index) + self._engineIO_request_index += 1 return timestamp - def _message(self, packet): - packet_type = 4 + def _message(self, engineIO_packet_data): + engineIO_packet_type = 4 response = self._session.post(self._url, params={ - 'EIO': self._engine_io_protocol, + 'EIO': self._engineIO_protocol, 'transport': 'polling', 't': self._get_timestamp(), 'sid': self._session_id, - }, data=_encode_content([(packet_type, packet)]), headers={ + }, data=_encode_content([ + (engineIO_packet_type, engineIO_packet_data), + ]), headers={ 'content-type': 'application/octet-stream', }) - packs = _decode_content(response.content) - for packet_type, packet in packs: - print 'engineIO_packet_type = %s' % packet_type - print 'socketIO_packet_type = %s' % packet[0] - print 'packet = %s' % packet[1:] + engineIO_packets = _decode_content(response.content) + for engineIO_packet_type, engineIO_packet_data in engineIO_packets: + socketIO_packet_type = int(engineIO_packet_data[0]) + socketIO_packet_data = engineIO_packet_data[1:] + print 'engineIO_packet_type = %s' % engineIO_packet_type + print 'socketIO_packet_type = %s' % socketIO_packet_type + print 'socketIO_packet_data = %s' % socketIO_packet_data def _ping(self): - packet_type = 2 - packet = '' + engineIO_packet_type = 2 + engineIO_packet_data = '' response = self._session.post(self._url, params={ - 'EIO': self._engine_io_protocol, + 'EIO': self._engineIO_protocol, 'transport': 'polling', 't': self._get_timestamp(), 'sid': self._session_id, - }, data=_encode_content([(packet_type, packet)]), headers={ + }, data=_encode_content([ + (engineIO_packet_type, engineIO_packet_data), + ]), headers={ 'content-type': 'application/octet-stream', }) - packs = _decode_content(response.content) - for packet_type, packet in packs: - print 'engineIO_packet_type = %s' % packet_type - print 'socketIO_packet_type = %s' % packet[0] - print 'packet = %s' % packet[1:] + engineIO_packets = _decode_content(response.content) + for engineIO_packet_type, engineIO_packet_data in engineIO_packets: + socketIO_packet_type = int(engineIO_packet_data[0]) + socketIO_packet_data = engineIO_packet_data[1:] + print 'engineIO_packet_type = %s' % engineIO_packet_type + print 'socketIO_packet_type = %s' % socketIO_packet_type + print 'socketIO_packet_data = %s' % socketIO_packet_data + + def _recv_packet(self): + response = self._session.get(self._url, params={ + 'EIO': self._engineIO_protocol, + 'transport': 'polling', + 't': self._get_timestamp(), + 'sid': self._session_id, + }) + for engineIO_packet in _decode_content(response.content): + yield engineIO_packet class SocketIO(EngineIO): def __init__( - self, host, port=None, Namespace=None, + self, + host, port=None, Namespace=None, wait_for_connection=True, transports=TRANSPORTS, resource='socket.io', **kw): - super(SocketIO, self).__init__(host, port, resource) + super(SocketIO, self).__init__( + host, port, Namespace, + wait_for_connection, transports, + resource, **kw) - def define(self): - pass - - def wait(self): + def define(self, Namespace, path=''): pass def emit(self, event, *args, **kw): - packet_type = 2 - packet = json.dumps([event]) - self._message(str(packet_type) + packet) + socketIO_packet_type = 2 + socketIO_packet_data = json.dumps([event]) + self._message(str(socketIO_packet_type) + socketIO_packet_data) def on(self, event, callback): pass + def _process_packet(self, packet): + engineIO_packet_data = super(SocketIO, self)._process_packet(packet) + socketIO_packet_type = int(engineIO_packet_data[0]) + socketIO_packet_data = engineIO_packet_data[1:] + print 'socketIO_packet_type = %s' % socketIO_packet_type + socketIO_packet_data_parsed = _parse_socketIO_data( + socketIO_packet_data) + # Launch callbacks + namespace = self.get_namespace() + delegate = { + 0: self._on_connect, + 1: self._on_disconnect, + 2: self._on_event, + 3: self._on_ack, + 4: self._on_error, + 5: self._on_binary_event, + 6: self._on_binary_ack, + }[socketIO_packet_type] + delegate( + socketIO_packet_data_parsed, namespace._find_packet_callback) + return socketIO_packet_data + + def _on_connect(self): + pass + + def _on_disconnect(self): + pass + + def _on_event(self): + pass + + def _on_ack(self): + pass + + def _on_error(self): + pass + + def _on_binary_event(self): + pass + + def _on_binary_ack(self): + pass + class BaseNamespace(object): pass @@ -121,52 +225,54 @@ class LoggingNamespace(BaseNamespace): def _decode_content(content): print content - packs = [] - index = 0 + packets = [] + content_index = 0 content_length = len(content) - while index < content_length: + while content_index < content_length: try: - index, packet_length = _read_packet_length(content, index) + content_index, packet_length = _read_packet_length( + content, content_index) except IndexError: break - index, packet = _read_packet(content, index, packet_length) - packet_type = int(packet[0]) - packet_payload = packet[1:] - packs.append((packet_type, packet_payload)) - return packs + content_index, packet_string = _read_packet_string( + content, content_index, packet_length) + packet_type = int(packet_string[0]) + packet_data = packet_string[1:] + packets.append((packet_type, packet_data)) + return packets -def _encode_content(packs): +def _encode_content(packets): parts = [] - for packet_type, packet_payload in packs: - packet = str(packet_type) + str(packet_payload) - parts.append(_make_packet_header(packet) + packet) + for packet_type, packet_data in packets: + packet_string = str(packet_type) + str(packet_data) + parts.append(_make_packet_header(packet_string) + packet_string) return ''.join(parts) -def _read_packet_length(content, index): - while ord(content[index]) != 0: - index += 1 - index += 1 +def _read_packet_length(content, content_index): + while ord(content[content_index]) != 0: + content_index += 1 + content_index += 1 packet_length_string = '' - while ord(content[index]) != 255: - packet_length_string += str(ord(content[index])) - index += 1 - return index, int(packet_length_string) + while ord(content[content_index]) != 255: + packet_length_string += str(ord(content[content_index])) + content_index += 1 + return content_index, int(packet_length_string) -def _read_packet(content, index, packet_length): - while ord(content[index]) == 255: - index += 1 - packet = content[index:index + packet_length] - return index + packet_length, packet +def _read_packet_string(content, content_index, packet_length): + while ord(content[content_index]) == 255: + content_index += 1 + packet_string = content[content_index:content_index + packet_length] + return content_index + packet_length, packet_string -def _make_packet_header(packet): - length_string = str(len(packet)) +def _make_packet_header(packet_string): + length_string = str(len(packet_string)) header_digits = [0] - for index in xrange(len(length_string)): - header_digits.append(ord(length_string[index]) - 48) + for i in xrange(len(length_string)): + header_digits.append(ord(length_string[i]) - 48) header_digits.append(255) return ''.join(chr(x) for x in header_digits) diff --git a/socketIO_client/tests/proxy.js b/socketIO_client/tests/proxy.js index 178c752..f5fbfca 100644 --- a/socketIO_client/tests/proxy.js +++ b/socketIO_client/tests/proxy.js @@ -23,8 +23,11 @@ var server = require('http').createServer(function(req, res) { function print_body(header, body) { var text = String(body); console.log(header + text); + if (text.charCodeAt(0) != 0) return; for (var i = 0; i < text.length; i++) { - console.log('body[%s] = %s = %s', i, text[i], text.charCodeAt(i)); + var character_code = text.charCodeAt(i); + console.log('body[%s] = %s = %s', i, text[i], character_code); + if (character_code == 65533) break; } } server.listen(8000); From 38e6038b8fa945baec0b13945749f2cab03eaf9a Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 18 Feb 2015 17:49:47 -0500 Subject: [PATCH 137/191] Expand namespace structure --- socketIO_client/__init__.py | 109 ++++++++++++++++++++++------------ socketIO_client/exceptions.py | 6 ++ 2 files changed, 77 insertions(+), 38 deletions(-) create mode 100644 socketIO_client/exceptions.py diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index c4962a5..af83295 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -2,6 +2,8 @@ import json import requests import time +from .exceptions import PacketError + __version__ = '0.6.1' TRANSPORTS = [] @@ -54,40 +56,51 @@ class EngineIO(object): engineIO_packet_data) # Launch callbacks namespace = self.get_namespace() - delegate = { - 0: self._on_open, - 1: self._on_close, - 2: self._on_ping, - 3: self._on_pong, - 4: self._on_message, - 5: self._on_upgrade, - 6: self._on_noop, - }[engineIO_packet_type] + try: + delegate = { + 0: self._on_open, + 1: self._on_close, + 2: self._on_ping, + 3: self._on_pong, + 4: self._on_message, + 5: self._on_upgrade, + 6: self._on_noop, + }[engineIO_packet_type] + except KeyError: + raise PacketError( + 'unexpected engine.io packet type (%s)' % engineIO_packet_type) delegate(engineIO_packet_data_parsed, namespace._find_packet_callback) return engineIO_packet_data + def define(self, Namespace): + self._namespace = namespace = Namespace(self) + return namespace + def get_namespace(self): + try: + return self._namespace + except AttributeError: + raise PacketError('undefined engine.io namespace') + + def _on_open(self, data_parsed, find_packet_callback): pass - def _on_open(self): + def _on_close(self, data_parsed, find_packet_callback): pass - def _on_close(self): + def _on_ping(self, data_parsed, find_packet_callback): pass - def _on_ping(self): + def _on_pong(self, data_parsed, find_packet_callback): pass - def _on_pong(self): + def _on_message(self, data_parsed, find_packet_callback): pass - def _on_message(self): + def _on_upgrade(self, data_parsed, find_packet_callback): pass - def _on_upgrade(self): - pass - - def _on_noop(self): + def _on_noop(self, data_parsed, find_packet_callback): pass def _get_timestamp(self): @@ -161,7 +174,16 @@ class SocketIO(EngineIO): resource, **kw) def define(self, Namespace, path=''): - pass + if path: + self._connect(path) + self._namespace_by_path[path] = namespace = Namespace(self, path) + return namespace + + def get_namespace(self, path=''): + try: + return self._namespace_by_path[path] + except KeyError: + raise PacketError('undefined socket.io namespace (%s)' % path) def emit(self, event, *args, **kw): socketIO_packet_type = 2 @@ -180,38 +202,41 @@ class SocketIO(EngineIO): socketIO_packet_data) # Launch callbacks namespace = self.get_namespace() - delegate = { - 0: self._on_connect, - 1: self._on_disconnect, - 2: self._on_event, - 3: self._on_ack, - 4: self._on_error, - 5: self._on_binary_event, - 6: self._on_binary_ack, - }[socketIO_packet_type] - delegate( - socketIO_packet_data_parsed, namespace._find_packet_callback) + try: + delegate = { + 0: self._on_connect, + 1: self._on_disconnect, + 2: self._on_event, + 3: self._on_ack, + 4: self._on_error, + 5: self._on_binary_event, + 6: self._on_binary_ack, + }[socketIO_packet_type] + except KeyError: + raise PacketError( + 'unexpected socket.io packet type (%s)' % socketIO_packet_type) + delegate(socketIO_packet_data_parsed, namespace._find_packet_callback) return socketIO_packet_data - def _on_connect(self): + def _on_connect(self, data_parsed, find_packet_callback): pass - def _on_disconnect(self): + def _on_disconnect(self, data_parsed, find_packet_callback): pass - def _on_event(self): + def _on_event(self, data_parsed, find_packet_callback): pass - def _on_ack(self): + def _on_ack(self, data_parsed, find_packet_callback): pass - def _on_error(self): + def _on_error(self, data_parsed, find_packet_callback): pass - def _on_binary_event(self): + def _on_binary_event(self, data_parsed, find_packet_callback): pass - def _on_binary_ack(self): + def _on_binary_ack(self, data_parsed, find_packet_callback): pass @@ -223,6 +248,10 @@ class LoggingNamespace(BaseNamespace): pass +def find_callback(args, kw=None): + pass + + def _decode_content(content): print content packets = [] @@ -277,5 +306,9 @@ def _make_packet_header(packet_string): return ''.join(chr(x) for x in header_digits) -def find_callback(args, kw=None): +def _parse_engineIO_data(): + pass + + +def _parse_socketIO_data(): pass diff --git a/socketIO_client/exceptions.py b/socketIO_client/exceptions.py new file mode 100644 index 0000000..b90fe20 --- /dev/null +++ b/socketIO_client/exceptions.py @@ -0,0 +1,6 @@ +class SocketIOError(Exception): + pass + + +class PacketError(SocketIOError): + pass From 911c04cbf10ab3c68c74db1c0562787efd9eb900 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 18 Feb 2015 19:22:09 -0500 Subject: [PATCH 138/191] Add logging --- README.rst | 2 +- socketIO_client/__init__.py | 132 +++++++++++++++++++++++++++--- socketIO_client/tests/__init__.py | 4 +- 3 files changed, 122 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index 9031bbe..0287329 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -.. image:: https://travis-ci.org/invisibleroads/socketIO-client.svg?branch=v0.6.1 +.. image:: https://travis-ci.org/invisibleroads/socketIO-client.svg?branch=0.6.1 :target: https://travis-ci.org/invisibleroads/socketIO-client diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index af83295..589370d 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,4 +1,5 @@ import json +import logging import requests import time @@ -6,6 +7,7 @@ from .exceptions import PacketError __version__ = '0.6.1' +_log = logging.getLogger(__name__) TRANSPORTS = [] @@ -31,7 +33,6 @@ class EngineIO(object): print response.url engineIO_packets = _decode_content(response.content) - print engineIO_packets engineIO_packet_type, engineIO_packet_data = engineIO_packets[0] assert engineIO_packet_type == 0 engineIO_packet_json = json.loads(engineIO_packet_data) @@ -41,6 +42,9 @@ class EngineIO(object): # engineIO_packet_json['upgrades'] self._session_id = engineIO_packet_json['sid'] + if Namespace: + self.define(Namespace) + def wait(self): while True: self._process_packets() @@ -168,6 +172,7 @@ class SocketIO(EngineIO): host, port=None, Namespace=None, wait_for_connection=True, transports=TRANSPORTS, resource='socket.io', **kw): + self._namespace_by_path = {} super(SocketIO, self).__init__( host, port, Namespace, wait_for_connection, transports, @@ -219,10 +224,10 @@ class SocketIO(EngineIO): return socketIO_packet_data def _on_connect(self, data_parsed, find_packet_callback): - pass + find_packet_callback('connect')() def _on_disconnect(self, data_parsed, find_packet_callback): - pass + find_packet_callback('disconnect')() def _on_event(self, data_parsed, find_packet_callback): pass @@ -240,20 +245,121 @@ class SocketIO(EngineIO): pass -class BaseNamespace(object): - pass +class EngineIONamespace(object): + 'Define engine.io client behavior' + + def __init__(self, io): + self._io = io + self._callback_by_event = {} + self.initialize() + + def initialize(self): + 'Initialize custom variables here; you can override this method' + pass + + def on_event(self, event, *args): + """ + Called after server sends an event; you can override this method. + Called only if a custom event handler does not exist, + such as one defined by namespace.on('my_event', my_function). + """ + callback, args = find_callback(args) + if callback: + callback(*args) + + def _find_packet_callback(self, event): + # Check callbacks defined by on() + try: + return self._callback_by_event[event] + except KeyError: + pass + # Check callbacks defined explicitly or use on_event() + return getattr( + self, 'on_' + event.replace(' ', '_'), + lambda *args: self.on_event(event, *args)) -class LoggingNamespace(BaseNamespace): - pass +class SocketIONamespace(EngineIONamespace): + 'Define socket.io client behavior' + + def __init__(self, io, path): + self.path = path + super(SocketIONamespace, self).__init__(io) + + def on_connect(self): + 'Called after server connects; you can override this method' + + def on_reconnect(self): + 'Called after server reconnects; you can override this method' + + def on_disconnect(self): + 'Called after server disconnects; you can override this method' + + def _find_packet_callback(self, event): + # Interpret events + if event == 'connect': + if not hasattr(self, '_was_connected'): + self._was_connected = True + else: + event = 'reconnect' + return super(SocketIONamespace, self)._find_packet_callback(event) + + +class LoggingMixin(object): + + def _log(self, level, msg, *attrs): + _log.log(level, '%s: %s' % (self._io._url, msg), *attrs) + + +class LoggingEngineIONamespace(EngineIONamespace, LoggingMixin): + + def on_event(self, event, *args): + callback, args = find_callback(args) + arguments = [repr(_) for _ in args] + if callback: + arguments.append('callback(*args)') + self._log( + logging.INFO, '[event] %s(%s)', + event, ', '.join(arguments)) + super(LoggingEngineIONamespace, self).on_event(event, *args) + + +class LoggingSocketIONamespace(SocketIONamespace, LoggingMixin): + + def on_event(self, event, *args): + callback, args = find_callback(args) + arguments = [repr(_) for _ in args] + if callback: + arguments.append('callback(*args)') + self._log( + logging.INFO, '%s [event] %s(%s)', self.path, + event, ', '.join(arguments)) + super(LoggingSocketIONamespace, self).on_event(event, *args) + + def on_connect(self): + self._log(logging.DEBUG, '%s [connect]', self.path) + super(LoggingSocketIONamespace, self).on_connect() + + def on_reconnect(self): + self._log(logging.DEBUG, '%s [reconnect]', self.path) + super(LoggingSocketIONamespace, self).on_reconnect() + + def on_disconnect(self): + self._log(logging.DEBUG, '%s [disconnect]', self.path) + super(LoggingSocketIONamespace, self).on_disconnect() def find_callback(args, kw=None): - pass + 'Return callback whether passed as a last argument or as a keyword' + if args and callable(args[-1]): + return args[-1], args[:-1] + try: + return kw['callback'], args + except (KeyError, TypeError): + return None, args def _decode_content(content): - print content packets = [] content_index = 0 content_length = len(content) @@ -306,9 +412,9 @@ def _make_packet_header(packet_string): return ''.join(chr(x) for x in header_digits) -def _parse_engineIO_data(): - pass +def _parse_engineIO_data(data): + return data -def _parse_socketIO_data(): - pass +def _parse_socketIO_data(data): + return data diff --git a/socketIO_client/tests/__init__.py b/socketIO_client/tests/__init__.py index d7f6c61..ed194c3 100644 --- a/socketIO_client/tests/__init__.py +++ b/socketIO_client/tests/__init__.py @@ -1,7 +1,7 @@ import logging from unittest import TestCase -from .. import SocketIO, LoggingNamespace, find_callback +from .. import SocketIO, LoggingSocketIONamespace, find_callback HOST = 'localhost' @@ -29,7 +29,7 @@ class Test_XHR_PollingTransport(TestCase, BaseMixin): self.wait_time_in_seconds = 1 -class Namespace(LoggingNamespace): +class Namespace(LoggingSocketIONamespace): def initialize(self): self.args_by_event = {} From b70cb7ffe5e4e51f70f5eea079ff8fc73559b3c0 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 19 Feb 2015 09:17:22 -0500 Subject: [PATCH 139/191] Fix #68 --- .travis.yml | 3 --- socketIO_client/transports.py | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index c3475fa..e6415c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,6 @@ python: - 2.6 - 2.7 - 3.4 -matrix: - allow_failures: - - python: 3.4 before_install: - sudo apt-get update - sudo apt-get install nodejs diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 2887efa..b6cc4e3 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -7,7 +7,7 @@ import six import socket import sys import time -import websocket +import websocket._exceptions from .exceptions import ConnectionError, TimeoutError @@ -148,6 +148,8 @@ class _WebsocketTransport(_AbstractTransport): raise ConnectionError(e) except socket.error as e: raise ConnectionError(e) + except websocket._exceptions.WebSocketException as e: + raise ConnectionError(e) self._connection.settimeout(TIMEOUT_IN_SECONDS) @property From 8289624c47c5dd7579aab4e8cbb817ef090bfbdb Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 19 Feb 2015 14:16:13 -0500 Subject: [PATCH 140/191] Revive heartbeat thread to exit recv with pong thanks to sarietta --- TODO.goals | 3 - socketIO_client/__init__.py | 153 +++++++++++++++++++++++------- socketIO_client/exceptions.py | 8 ++ socketIO_client/tests/__init__.py | 1 + socketIO_client/transports.py | 17 ++++ 5 files changed, 147 insertions(+), 35 deletions(-) create mode 100644 socketIO_client/transports.py diff --git a/TODO.goals b/TODO.goals index 50a9c42..c163d11 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,6 +1,4 @@ Release 0.6.1 #41 #52 - Implement define - Implement wait Implement on Get pong @@ -9,6 +7,5 @@ Release 0.6.1 #41 #52 Put tests in index.html Update tests - Revive heartbeat as separate process Merge sarietta's pull request Implement rooms #65 diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 589370d..1860de4 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,14 +1,17 @@ import json import logging import requests +import threading import time -from .exceptions import PacketError +from .exceptions import PacketError, TimeoutError +from .transports import _get_response __version__ = '0.6.1' _log = logging.getLogger(__name__) TRANSPORTS = [] +RETRY_INTERVAL_IN_SECONDS = 1 class EngineIO(object): @@ -22,10 +25,10 @@ class EngineIO(object): wait_for_connection=True, transports=TRANSPORTS, resource='engine.io', **kw): self._url = 'http://%s:%s/%s/' % (host, port, resource) - self._session = requests.Session() + self._http_session = requests.Session() print self._url - response = self._session.get(self._url, params={ + response = self._http_session.get(self._url, params={ 'EIO': self._engineIO_protocol, 'transport': 'polling', 't': self._get_timestamp(), @@ -35,23 +38,48 @@ class EngineIO(object): engineIO_packets = _decode_content(response.content) engineIO_packet_type, engineIO_packet_data = engineIO_packets[0] assert engineIO_packet_type == 0 - engineIO_packet_json = json.loads(engineIO_packet_data) - print engineIO_packet_json - # engineIO_packet_json['pingInterval'] - # engineIO_packet_json['pingTimeout'] - # engineIO_packet_json['upgrades'] - self._session_id = engineIO_packet_json['sid'] + packet_json = json.loads(engineIO_packet_data) + print packet_json + # packet_json['upgrades'] + self._ping_interval = packet_json['pingInterval'] / float(1000) + self._ping_timeout = packet_json['pingTimeout'] / float(1000) + self._session_id = packet_json['sid'] if Namespace: self.define(Namespace) - def wait(self): - while True: - self._process_packets() + self._heartbeat_thread = HeartbeatThread( + send_heartbeat=self._ping, + relax_interval_in_seconds=self._ping_interval, + hurry_interval_in_seconds=1) + self._heartbeat_thread.start() + + def define(self, Namespace): + self._namespace = namespace = Namespace(self) + return namespace + + def get_namespace(self): + try: + return self._namespace + except AttributeError: + raise PacketError('undefined engine.io namespace') + + def wait(self, seconds=None): + self._heartbeat_thread.hurry() + warning_screen = _yield_warning_screen(seconds) + for elapsed_time in warning_screen: + try: + self._process_packets() + except TimeoutError: + pass + self._heartbeat_thread.relax() def _process_packets(self): for engineIO_packet in self._recv_packet(): - self._process_packet(engineIO_packet) + try: + self._process_packet(engineIO_packet) + except PacketError as e: + self._log(logging.WARNING, '[packet error] %s', e) def _process_packet(self, packet): engineIO_packet_type, engineIO_packet_data = packet @@ -74,17 +102,8 @@ class EngineIO(object): raise PacketError( 'unexpected engine.io packet type (%s)' % engineIO_packet_type) delegate(engineIO_packet_data_parsed, namespace._find_packet_callback) - return engineIO_packet_data - - def define(self, Namespace): - self._namespace = namespace = Namespace(self) - return namespace - - def get_namespace(self): - try: - return self._namespace - except AttributeError: - raise PacketError('undefined engine.io namespace') + if engineIO_packet_type is 4: + return engineIO_packet_data def _on_open(self, data_parsed, find_packet_callback): pass @@ -115,7 +134,7 @@ class EngineIO(object): def _message(self, engineIO_packet_data): engineIO_packet_type = 4 - response = self._session.post(self._url, params={ + response = self._http_session.post(self._url, params={ 'EIO': self._engineIO_protocol, 'transport': 'polling', 't': self._get_timestamp(), @@ -125,6 +144,7 @@ class EngineIO(object): ]), headers={ 'content-type': 'application/octet-stream', }) + print 'message()' engineIO_packets = _decode_content(response.content) for engineIO_packet_type, engineIO_packet_data in engineIO_packets: socketIO_packet_type = int(engineIO_packet_data[0]) @@ -136,7 +156,7 @@ class EngineIO(object): def _ping(self): engineIO_packet_type = 2 engineIO_packet_data = '' - response = self._session.post(self._url, params={ + response = self._http_session.post(self._url, params={ 'EIO': self._engineIO_protocol, 'transport': 'polling', 't': self._get_timestamp(), @@ -146,6 +166,7 @@ class EngineIO(object): ]), headers={ 'content-type': 'application/octet-stream', }) + print 'ping()' engineIO_packets = _decode_content(response.content) for engineIO_packet_type, engineIO_packet_data in engineIO_packets: socketIO_packet_type = int(engineIO_packet_data[0]) @@ -155,15 +176,22 @@ class EngineIO(object): print 'socketIO_packet_data = %s' % socketIO_packet_data def _recv_packet(self): - response = self._session.get(self._url, params={ - 'EIO': self._engineIO_protocol, - 'transport': 'polling', - 't': self._get_timestamp(), - 'sid': self._session_id, - }) + response = _get_response( + self._http_session.get, + self._url, + params={ + 'EIO': self._engineIO_protocol, + 'transport': 'polling', + 't': self._get_timestamp(), + 'sid': self._session_id, + }, + timeout=self._ping_timeout) for engineIO_packet in _decode_content(response.content): yield engineIO_packet + def _log(self, level, msg, *attrs): + _log.log(level, '%s: %s' % (self._url, msg), *attrs) + class SocketIO(EngineIO): @@ -200,6 +228,8 @@ class SocketIO(EngineIO): def _process_packet(self, packet): engineIO_packet_data = super(SocketIO, self)._process_packet(packet) + if engineIO_packet_data is None: + return socketIO_packet_type = int(engineIO_packet_data[0]) socketIO_packet_data = engineIO_packet_data[1:] print 'socketIO_packet_type = %s' % socketIO_packet_type @@ -220,6 +250,8 @@ class SocketIO(EngineIO): except KeyError: raise PacketError( 'unexpected socket.io packet type (%s)' % socketIO_packet_type) + print socketIO_packet_data_parsed + print namespace._find_packet_callback delegate(socketIO_packet_data_parsed, namespace._find_packet_callback) return socketIO_packet_data @@ -418,3 +450,60 @@ def _parse_engineIO_data(data): def _parse_socketIO_data(data): return data + + +def _yield_warning_screen(seconds=None): + last_warning = None + for elapsed_time in _yield_elapsed_time(seconds): + try: + yield elapsed_time + except Exception as warning: + warning = str(warning) + if last_warning != warning: + last_warning = warning + _log.warn(warning) + time.sleep(RETRY_INTERVAL_IN_SECONDS) + + +def _yield_elapsed_time(seconds=None): + start_time = time.time() + if seconds is None: + while True: + yield time.time() - start_time + while time.time() - start_time < seconds: + yield time.time() - start_time + + +class HeartbeatThread(threading.Thread): + + daemon = True + + def __init__( + self, send_heartbeat, + relax_interval_in_seconds, + hurry_interval_in_seconds): + super(HeartbeatThread, self).__init__() + self._send_heartbeat = send_heartbeat + self._relax_interval_in_seconds = relax_interval_in_seconds + self._hurry_interval_in_seconds = hurry_interval_in_seconds + self._adrenaline = threading.Event() + self._rest = threading.Event() + self._stop = threading.Event() + + def run(self): + while not self._stop.is_set(): + self._send_heartbeat() + if self._adrenaline.is_set(): + interval_in_seconds = self._hurry_interval_in_seconds + else: + interval_in_seconds = self._relax_interval_in_seconds + self._rest.wait(interval_in_seconds) + + def relax(self): + self._adrenaline.clear() + + def hurry(self): + self._adrenaline.set() + + def stop(self): + self._stop.set() diff --git a/socketIO_client/exceptions.py b/socketIO_client/exceptions.py index b90fe20..ed2b4d2 100644 --- a/socketIO_client/exceptions.py +++ b/socketIO_client/exceptions.py @@ -2,5 +2,13 @@ class SocketIOError(Exception): pass +class ConnectionError(SocketIOError): + pass + + +class TimeoutError(SocketIOError): + pass + + class PacketError(SocketIOError): pass diff --git a/socketIO_client/tests/__init__.py b/socketIO_client/tests/__init__.py index ed194c3..8143a05 100644 --- a/socketIO_client/tests/__init__.py +++ b/socketIO_client/tests/__init__.py @@ -35,6 +35,7 @@ class Namespace(LoggingSocketIONamespace): self.args_by_event = {} def on_event(self, event, *args): + print 'xxx *** xxx' callback, args = find_callback(args) if callback: callback(*args) diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py new file mode 100644 index 0000000..7620855 --- /dev/null +++ b/socketIO_client/transports.py @@ -0,0 +1,17 @@ +import requests +from .exceptions import ConnectionError, TimeoutError + + +def _get_response(request, *args, **kw): + try: + response = request(*args, **kw) + except requests.exceptions.Timeout as e: + raise TimeoutError(e) + except requests.exceptions.ConnectionError as e: + raise ConnectionError(e) + except requests.exceptions.SSLError as e: + raise ConnectionError('could not negotiate SSL (%s)' % e) + status_code = response.status_code + if 200 != status_code: + raise ConnectionError('unexpected status code (%s)' % status_code) + return response From f34c7014fb766e8ec2e30d199ce7622d3e8c8abb Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 19 Feb 2015 21:39:09 -0500 Subject: [PATCH 141/191] Passed a unit test --- .travis.yml | 2 +- socketIO_client/__init__.py | 309 ++++++++++++++++++++---------------- 2 files changed, 170 insertions(+), 141 deletions(-) diff --git a/.travis.yml b/.travis.yml index af9d4b0..2d8f75d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ before_install: - sudo apt-get update - sudo apt-get install nodejs install: - - npm install -G socket.io + - npm install -G socket.io@1.3.1 - npm install -G http-proxy - pip install -U requests - pip install -U six diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 1860de4..99a9a2d 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -3,6 +3,7 @@ import logging import requests import threading import time +from collections import namedtuple from .exceptions import PacketError, TimeoutError from .transports import _get_response @@ -10,10 +11,116 @@ from .transports import _get_response __version__ = '0.6.1' _log = logging.getLogger(__name__) +EngineIOData = namedtuple('EngineIOData', ['data']) +SocketIOData = namedtuple('SocketIOData', ['path', 'ack_id', 'event', 'args']) TRANSPORTS = [] RETRY_INTERVAL_IN_SECONDS = 1 +class EngineIONamespace(object): + 'Define engine.io client behavior' + + def __init__(self, io): + self._io = io + self._callback_by_event = {} + self.initialize() + + def initialize(self): + 'Initialize custom variables here; you can override this method' + pass + + def on_event(self, event, *args): + """ + Called after server sends an event; you can override this method. + Called only if a custom event handler does not exist, + such as one defined by namespace.on('my_event', my_function). + """ + callback, args = find_callback(args) + if callback: + callback(*args) + + def _find_packet_callback(self, event): + # Check callbacks defined by on() + try: + return self._callback_by_event[event] + except KeyError: + pass + # Check callbacks defined explicitly or use on_event() + return getattr( + self, 'on_' + event.replace(' ', '_'), + lambda *args: self.on_event(event, *args)) + + +class SocketIONamespace(EngineIONamespace): + 'Define socket.io client behavior' + + def __init__(self, io, path): + self.path = path + super(SocketIONamespace, self).__init__(io) + + def on_connect(self): + 'Called after server connects; you can override this method' + + def on_reconnect(self): + 'Called after server reconnects; you can override this method' + + def on_disconnect(self): + 'Called after server disconnects; you can override this method' + + def _find_packet_callback(self, event): + # Interpret events + if event == 'connect': + if not hasattr(self, '_was_connected'): + self._was_connected = True + else: + event = 'reconnect' + return super(SocketIONamespace, self)._find_packet_callback(event) + + +class LoggingMixin(object): + + def _log(self, level, msg, *attrs): + _log.log(level, '%s: %s' % (self._io._url, msg), *attrs) + + +class LoggingEngineIONamespace(EngineIONamespace, LoggingMixin): + + def on_event(self, event, *args): + callback, args = find_callback(args) + arguments = [repr(_) for _ in args] + if callback: + arguments.append('callback(*args)') + self._log( + logging.INFO, '[event] %s(%s)', + event, ', '.join(arguments)) + super(LoggingEngineIONamespace, self).on_event(event, *args) + + +class LoggingSocketIONamespace(SocketIONamespace, LoggingMixin): + + def on_event(self, event, *args): + callback, args = find_callback(args) + arguments = [repr(_) for _ in args] + if callback: + arguments.append('callback(*args)') + self._log( + logging.INFO, '%s [event] %s(%s)', self.path, + event, ', '.join(arguments)) + super(LoggingSocketIONamespace, self).on_event(event, *args) + + def on_connect(self): + self._log(logging.DEBUG, '%s [connect]', self.path) + super(LoggingSocketIONamespace, self).on_connect() + + def on_reconnect(self): + self._log(logging.DEBUG, '%s [reconnect]', self.path) + super(LoggingSocketIONamespace, self).on_reconnect() + + def on_disconnect(self): + self._log(logging.DEBUG, '%s [disconnect]', self.path) + super(LoggingSocketIONamespace, self).on_disconnect() + + class EngineIO(object): _engineIO_protocol = 3 @@ -38,12 +145,12 @@ class EngineIO(object): engineIO_packets = _decode_content(response.content) engineIO_packet_type, engineIO_packet_data = engineIO_packets[0] assert engineIO_packet_type == 0 - packet_json = json.loads(engineIO_packet_data) - print packet_json - # packet_json['upgrades'] - self._ping_interval = packet_json['pingInterval'] / float(1000) - self._ping_timeout = packet_json['pingTimeout'] / float(1000) - self._session_id = packet_json['sid'] + value_by_name = json.loads(engineIO_packet_data) + print(value_by_name) + # value_by_name['upgrades'] + self._ping_interval = value_by_name['pingInterval'] / float(1000) + self._ping_timeout = value_by_name['pingTimeout'] / float(1000) + self._session_id = value_by_name['sid'] if Namespace: self.define(Namespace) @@ -250,8 +357,6 @@ class SocketIO(EngineIO): except KeyError: raise PacketError( 'unexpected socket.io packet type (%s)' % socketIO_packet_type) - print socketIO_packet_data_parsed - print namespace._find_packet_callback delegate(socketIO_packet_data_parsed, namespace._find_packet_callback) return socketIO_packet_data @@ -262,7 +367,11 @@ class SocketIO(EngineIO): find_packet_callback('disconnect')() def _on_event(self, data_parsed, find_packet_callback): - pass + args = data_parsed.args + if data_parsed.ack_id: + args.append(self._prepare_to_send_ack( + data_parsed.path, data_parsed.ack_id)) + find_packet_callback(data_parsed.event)(*args) def _on_ack(self, data_parsed, find_packet_callback): pass @@ -276,109 +385,44 @@ class SocketIO(EngineIO): def _on_binary_ack(self, data_parsed, find_packet_callback): pass - -class EngineIONamespace(object): - 'Define engine.io client behavior' - - def __init__(self, io): - self._io = io - self._callback_by_event = {} - self.initialize() - - def initialize(self): - 'Initialize custom variables here; you can override this method' - pass - - def on_event(self, event, *args): - """ - Called after server sends an event; you can override this method. - Called only if a custom event handler does not exist, - such as one defined by namespace.on('my_event', my_function). - """ - callback, args = find_callback(args) - if callback: - callback(*args) - - def _find_packet_callback(self, event): - # Check callbacks defined by on() - try: - return self._callback_by_event[event] - except KeyError: - pass - # Check callbacks defined explicitly or use on_event() - return getattr( - self, 'on_' + event.replace(' ', '_'), - lambda *args: self.on_event(event, *args)) + def _prepare_to_send_ack(self, path, ack_id): + 'Return function that acknowledges the server' + return lambda *args: self._ack(path, ack_id, *args) -class SocketIONamespace(EngineIONamespace): - 'Define socket.io client behavior' +class HeartbeatThread(threading.Thread): - def __init__(self, io, path): - self.path = path - super(SocketIONamespace, self).__init__(io) + daemon = True - def on_connect(self): - 'Called after server connects; you can override this method' + def __init__( + self, send_heartbeat, + relax_interval_in_seconds, + hurry_interval_in_seconds): + super(HeartbeatThread, self).__init__() + self._send_heartbeat = send_heartbeat + self._relax_interval_in_seconds = relax_interval_in_seconds + self._hurry_interval_in_seconds = hurry_interval_in_seconds + self._adrenaline = threading.Event() + self._rest = threading.Event() + self._stop = threading.Event() - def on_reconnect(self): - 'Called after server reconnects; you can override this method' - - def on_disconnect(self): - 'Called after server disconnects; you can override this method' - - def _find_packet_callback(self, event): - # Interpret events - if event == 'connect': - if not hasattr(self, '_was_connected'): - self._was_connected = True + def run(self): + while not self._stop.is_set(): + self._send_heartbeat() + if self._adrenaline.is_set(): + interval_in_seconds = self._hurry_interval_in_seconds else: - event = 'reconnect' - return super(SocketIONamespace, self)._find_packet_callback(event) + interval_in_seconds = self._relax_interval_in_seconds + self._rest.wait(interval_in_seconds) + def relax(self): + self._adrenaline.clear() -class LoggingMixin(object): + def hurry(self): + self._adrenaline.set() - def _log(self, level, msg, *attrs): - _log.log(level, '%s: %s' % (self._io._url, msg), *attrs) - - -class LoggingEngineIONamespace(EngineIONamespace, LoggingMixin): - - def on_event(self, event, *args): - callback, args = find_callback(args) - arguments = [repr(_) for _ in args] - if callback: - arguments.append('callback(*args)') - self._log( - logging.INFO, '[event] %s(%s)', - event, ', '.join(arguments)) - super(LoggingEngineIONamespace, self).on_event(event, *args) - - -class LoggingSocketIONamespace(SocketIONamespace, LoggingMixin): - - def on_event(self, event, *args): - callback, args = find_callback(args) - arguments = [repr(_) for _ in args] - if callback: - arguments.append('callback(*args)') - self._log( - logging.INFO, '%s [event] %s(%s)', self.path, - event, ', '.join(arguments)) - super(LoggingSocketIONamespace, self).on_event(event, *args) - - def on_connect(self): - self._log(logging.DEBUG, '%s [connect]', self.path) - super(LoggingSocketIONamespace, self).on_connect() - - def on_reconnect(self): - self._log(logging.DEBUG, '%s [reconnect]', self.path) - super(LoggingSocketIONamespace, self).on_reconnect() - - def on_disconnect(self): - self._log(logging.DEBUG, '%s [disconnect]', self.path) - super(LoggingSocketIONamespace, self).on_disconnect() + def stop(self): + self._stop.set() def find_callback(args, kw=None): @@ -445,11 +489,31 @@ def _make_packet_header(packet_string): def _parse_engineIO_data(data): - return data + return EngineIOData(data=data) def _parse_socketIO_data(data): - return data + if data.startswith('/'): + try: + path, data = data.split(',', 1) + except ValueError: + path = data + data = '' + else: + path = '' + try: + ack_id = int(data[0]) + data = data[1:] + except (ValueError, IndexError): + ack_id = None + if data: + x = json.loads(data) + event = x[0] + args = x[1:] + else: + event = '' + args = [] + return SocketIOData(path=path, ack_id=ack_id, event=event, args=args) def _yield_warning_screen(seconds=None): @@ -472,38 +536,3 @@ def _yield_elapsed_time(seconds=None): yield time.time() - start_time while time.time() - start_time < seconds: yield time.time() - start_time - - -class HeartbeatThread(threading.Thread): - - daemon = True - - def __init__( - self, send_heartbeat, - relax_interval_in_seconds, - hurry_interval_in_seconds): - super(HeartbeatThread, self).__init__() - self._send_heartbeat = send_heartbeat - self._relax_interval_in_seconds = relax_interval_in_seconds - self._hurry_interval_in_seconds = hurry_interval_in_seconds - self._adrenaline = threading.Event() - self._rest = threading.Event() - self._stop = threading.Event() - - def run(self): - while not self._stop.is_set(): - self._send_heartbeat() - if self._adrenaline.is_set(): - interval_in_seconds = self._hurry_interval_in_seconds - else: - interval_in_seconds = self._relax_interval_in_seconds - self._rest.wait(interval_in_seconds) - - def relax(self): - self._adrenaline.clear() - - def hurry(self): - self._adrenaline.set() - - def stop(self): - self._stop.set() From ef43467870bb18fedaa56228d7786fae312c5c28 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 19 Feb 2015 21:45:32 -0500 Subject: [PATCH 142/191] Fix travis --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2d8f75d..5fb9d1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ install: - pip install -U websocket-client - pip install -U coverage before_script: - - DEBUG=* node tests/serve.js & - - DEBUG=* node tests/proxy.js & + - DEBUG=* node socketIO_client/tests/serve.js & + - DEBUG=* node socketIO_client/tests/proxy.js & - sleep 3 script: nosetests From 7e105e13b50981a8548b0d430d2ae1c8f232cc3a Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 19 Feb 2015 21:49:35 -0500 Subject: [PATCH 143/191] Add parentheses to print statements --- README.rst | 12 ++++++------ socketIO_client/__init__.py | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.rst b/README.rst index 0287329..48313f1 100644 --- a/README.rst +++ b/README.rst @@ -57,7 +57,7 @@ Emit with callback. :: from socketIO_client import SocketIO, LoggingNamespace def on_bbb_response(*args): - print 'on_bbb_response', args + print('on_bbb_response', args) with SocketIO('localhost', 8000, LoggingNamespace) as socketIO: socketIO.emit('bbb', {'xxx': 'yyy'}, on_bbb_response) @@ -68,7 +68,7 @@ Define events. :: from socketIO_client import SocketIO, LoggingNamespace def on_aaa_response(*args): - print 'on_aaa_response', args + print('on_aaa_response', args) socketIO = SocketIO('localhost', 8000, LoggingNamespace) socketIO.on('aaa_response', on_aaa_response) @@ -82,7 +82,7 @@ Define events in a namespace. :: class Namespace(BaseNamespace): def on_aaa_response(self, *args): - print 'on_aaa_response', args + print('on_aaa_response', args) self.emit('bbb') socketIO = SocketIO('localhost', 8000, Namespace) @@ -96,7 +96,7 @@ Define standard events. :: class Namespace(BaseNamespace): def on_connect(self): - print '[Connected]' + print('[Connected]') socketIO = SocketIO('localhost', 8000, Namespace) socketIO.wait(seconds=1) @@ -108,12 +108,12 @@ Define different namespaces on a single socket. :: class ChatNamespace(BaseNamespace): def on_aaa_response(self, *args): - print 'on_aaa_response', args + print('on_aaa_response', args) class NewsNamespace(BaseNamespace): def on_aaa_response(self, *args): - print 'on_aaa_response', args + print('on_aaa_response', args) socketIO = SocketIO('localhost', 8000) chat_namespace = socketIO.define(ChatNamespace, '/chat') diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 99a9a2d..8ffe9de 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -133,14 +133,14 @@ class EngineIO(object): resource='engine.io', **kw): self._url = 'http://%s:%s/%s/' % (host, port, resource) self._http_session = requests.Session() - print self._url + print(self._url) response = self._http_session.get(self._url, params={ 'EIO': self._engineIO_protocol, 'transport': 'polling', 't': self._get_timestamp(), }) - print response.url + print(response.url) engineIO_packets = _decode_content(response.content) engineIO_packet_type, engineIO_packet_data = engineIO_packets[0] @@ -190,7 +190,7 @@ class EngineIO(object): def _process_packet(self, packet): engineIO_packet_type, engineIO_packet_data = packet - print 'engineIO_packet_type = %s' % engineIO_packet_type + print('engineIO_packet_type = %s' % engineIO_packet_type) engineIO_packet_data_parsed = _parse_engineIO_data( engineIO_packet_data) # Launch callbacks @@ -251,14 +251,14 @@ class EngineIO(object): ]), headers={ 'content-type': 'application/octet-stream', }) - print 'message()' + print('message()') engineIO_packets = _decode_content(response.content) for engineIO_packet_type, engineIO_packet_data in engineIO_packets: socketIO_packet_type = int(engineIO_packet_data[0]) socketIO_packet_data = engineIO_packet_data[1:] - print 'engineIO_packet_type = %s' % engineIO_packet_type - print 'socketIO_packet_type = %s' % socketIO_packet_type - print 'socketIO_packet_data = %s' % socketIO_packet_data + print('engineIO_packet_type = %s' % engineIO_packet_type) + print('socketIO_packet_type = %s' % socketIO_packet_type) + print('socketIO_packet_data = %s' % socketIO_packet_data) def _ping(self): engineIO_packet_type = 2 @@ -273,14 +273,14 @@ class EngineIO(object): ]), headers={ 'content-type': 'application/octet-stream', }) - print 'ping()' + print('ping()') engineIO_packets = _decode_content(response.content) for engineIO_packet_type, engineIO_packet_data in engineIO_packets: socketIO_packet_type = int(engineIO_packet_data[0]) socketIO_packet_data = engineIO_packet_data[1:] - print 'engineIO_packet_type = %s' % engineIO_packet_type - print 'socketIO_packet_type = %s' % socketIO_packet_type - print 'socketIO_packet_data = %s' % socketIO_packet_data + print('engineIO_packet_type = %s' % engineIO_packet_type) + print('socketIO_packet_type = %s' % socketIO_packet_type) + print('socketIO_packet_data = %s' % socketIO_packet_data) def _recv_packet(self): response = _get_response( @@ -339,7 +339,7 @@ class SocketIO(EngineIO): return socketIO_packet_type = int(engineIO_packet_data[0]) socketIO_packet_data = engineIO_packet_data[1:] - print 'socketIO_packet_type = %s' % socketIO_packet_type + print('socketIO_packet_type = %s' % socketIO_packet_type) socketIO_packet_data_parsed = _parse_socketIO_data( socketIO_packet_data) # Launch callbacks From 8641818cf27fc90bea7543e8243ae9a0e6855f7d Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 19 Feb 2015 21:53:14 -0500 Subject: [PATCH 144/191] Remove print statement --- TODO.goals | 2 +- socketIO_client/tests/__init__.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/TODO.goals b/TODO.goals index c163d11..09ff239 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,7 +1,7 @@ Release 0.6.1 #41 #52 Implement on - Get pong + Send pong Change print statements into logging statements Clean up diff --git a/socketIO_client/tests/__init__.py b/socketIO_client/tests/__init__.py index 8143a05..ed194c3 100644 --- a/socketIO_client/tests/__init__.py +++ b/socketIO_client/tests/__init__.py @@ -35,7 +35,6 @@ class Namespace(LoggingSocketIONamespace): self.args_by_event = {} def on_event(self, event, *args): - print 'xxx *** xxx' callback, args = find_callback(args) if callback: callback(*args) From 23844f9e7660197b52f05b0dca1e01e0b11a02c6 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 19 Feb 2015 22:46:09 -0500 Subject: [PATCH 145/191] Fix Python 3 compatibility issues --- socketIO_client/__init__.py | 26 +++++++++++++++----------- socketIO_client/compat.py | 13 +++++++++++++ 2 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 socketIO_client/compat.py diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 8ffe9de..61306f3 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -5,6 +5,7 @@ import threading import time from collections import namedtuple +from .compat import get_byte, get_character, get_unicode from .exceptions import PacketError, TimeoutError from .transports import _get_response @@ -145,7 +146,7 @@ class EngineIO(object): engineIO_packets = _decode_content(response.content) engineIO_packet_type, engineIO_packet_data = engineIO_packets[0] assert engineIO_packet_type == 0 - value_by_name = json.loads(engineIO_packet_data) + value_by_name = json.loads(get_unicode(engineIO_packet_data)) print(value_by_name) # value_by_name['upgrades'] self._ping_interval = value_by_name['pingInterval'] / float(1000) @@ -254,7 +255,7 @@ class EngineIO(object): print('message()') engineIO_packets = _decode_content(response.content) for engineIO_packet_type, engineIO_packet_data in engineIO_packets: - socketIO_packet_type = int(engineIO_packet_data[0]) + socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) socketIO_packet_data = engineIO_packet_data[1:] print('engineIO_packet_type = %s' % engineIO_packet_type) print('socketIO_packet_type = %s' % socketIO_packet_type) @@ -276,7 +277,7 @@ class EngineIO(object): print('ping()') engineIO_packets = _decode_content(response.content) for engineIO_packet_type, engineIO_packet_data in engineIO_packets: - socketIO_packet_type = int(engineIO_packet_data[0]) + socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) socketIO_packet_data = engineIO_packet_data[1:] print('engineIO_packet_type = %s' % engineIO_packet_type) print('socketIO_packet_type = %s' % socketIO_packet_type) @@ -337,7 +338,7 @@ class SocketIO(EngineIO): engineIO_packet_data = super(SocketIO, self)._process_packet(packet) if engineIO_packet_data is None: return - socketIO_packet_type = int(engineIO_packet_data[0]) + socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) socketIO_packet_data = engineIO_packet_data[1:] print('socketIO_packet_type = %s' % socketIO_packet_type) socketIO_packet_data_parsed = _parse_socketIO_data( @@ -447,7 +448,7 @@ def _decode_content(content): break content_index, packet_string = _read_packet_string( content, content_index, packet_length) - packet_type = int(packet_string[0]) + packet_type = int(get_character(packet_string, 0)) packet_data = packet_string[1:] packets.append((packet_type, packet_data)) return packets @@ -462,18 +463,20 @@ def _encode_content(packets): def _read_packet_length(content, content_index): - while ord(content[content_index]) != 0: + while get_byte(content, content_index) != 0: content_index += 1 content_index += 1 packet_length_string = '' - while ord(content[content_index]) != 255: - packet_length_string += str(ord(content[content_index])) + byte = get_byte(content, content_index) + while byte != 255: + packet_length_string += str(byte) content_index += 1 + byte = get_byte(content, content_index) return content_index, int(packet_length_string) def _read_packet_string(content, content_index, packet_length): - while ord(content[content_index]) == 255: + while get_byte(content, content_index) == 255: content_index += 1 packet_string = content[content_index:content_index + packet_length] return content_index + packet_length, packet_string @@ -482,17 +485,18 @@ def _read_packet_string(content, content_index, packet_length): def _make_packet_header(packet_string): length_string = str(len(packet_string)) header_digits = [0] - for i in xrange(len(length_string)): + for i in range(len(length_string)): header_digits.append(ord(length_string[i]) - 48) header_digits.append(255) return ''.join(chr(x) for x in header_digits) def _parse_engineIO_data(data): - return EngineIOData(data=data) + return EngineIOData(data=get_unicode(data)) def _parse_socketIO_data(data): + data = get_unicode(data) if data.startswith('/'): try: path, data = data.split(',', 1) diff --git a/socketIO_client/compat.py b/socketIO_client/compat.py new file mode 100644 index 0000000..1408e19 --- /dev/null +++ b/socketIO_client/compat.py @@ -0,0 +1,13 @@ +import six + + +def get_byte(x, index): + return six.indexbytes(x, index) + + +def get_character(x, index): + return chr(six.indexbytes(x, index)) + + +def get_unicode(x): + return x.decode('utf-8') From 96014d3d537d8e0341584f00eb9c95b418134c1d Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Fri, 20 Feb 2015 15:38:53 -0500 Subject: [PATCH 146/191] Start separating transport --- TODO.goals | 2 - socketIO_client/__init__.py | 334 ++++++++++++++++++++++-------- socketIO_client/tests/__init__.py | 97 +++++++++ socketIO_client/tests/index.html | 21 ++ socketIO_client/tests/proxy.js | 3 + socketIO_client/tests/serve.js | 12 +- 6 files changed, 374 insertions(+), 95 deletions(-) diff --git a/TODO.goals b/TODO.goals index 09ff239..619a558 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,6 +1,4 @@ Release 0.6.1 #41 #52 - Implement on - Send pong Change print statements into logging statements Clean up diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 61306f3..d390fda 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -6,14 +6,13 @@ import time from collections import namedtuple from .compat import get_byte, get_character, get_unicode -from .exceptions import PacketError, TimeoutError +from .exceptions import ConnectionError, TimeoutError, PacketError from .transports import _get_response __version__ = '0.6.1' _log = logging.getLogger(__name__) -EngineIOData = namedtuple('EngineIOData', ['data']) -SocketIOData = namedtuple('SocketIOData', ['path', 'ack_id', 'event', 'args']) +SocketIOData = namedtuple('SocketIOData', ['path', 'ack_id', 'args']) TRANSPORTS = [] RETRY_INTERVAL_IN_SECONDS = 1 @@ -27,18 +26,40 @@ class EngineIONamespace(object): self.initialize() def initialize(self): - 'Initialize custom variables here; you can override this method' - pass + """Initialize custom variables here. + You can override this method.""" - def on_event(self, event, *args): - """ - Called after server sends an event; you can override this method. - Called only if a custom event handler does not exist, - such as one defined by namespace.on('my_event', my_function). - """ - callback, args = find_callback(args) - if callback: - callback(*args) + def on(self, event, callback): + 'Define a callback to handle an event emitted by the server' + self._callback_by_event[event] = callback + + def on_open(self): + """Called after engine.io connects. + You can override this method.""" + + def on_close(self): + """Called after engine.io disconnects. + You can override this method.""" + + def on_ping(self, data): + """Called after engine.io sends a ping packet. + You can override this method.""" + + def on_pong(self, data): + """Called after engine.io sends a pong packet. + You can override this method.""" + + def on_message(self, data): + """Called after engine.io sends a message packet. + You can override this method.""" + + def on_upgrade(self): + """Called after engine.io sends an upgrade packet. + You can override this method.""" + + def on_noop(self): + """Called after engine.io sends a noop packet. + You can override this method.""" def _find_packet_callback(self, event): # Check callbacks defined by on() @@ -46,10 +67,8 @@ class EngineIONamespace(object): return self._callback_by_event[event] except KeyError: pass - # Check callbacks defined explicitly or use on_event() - return getattr( - self, 'on_' + event.replace(' ', '_'), - lambda *args: self.on_event(event, *args)) + # Check callbacks defined explicitly + return getattr(self, 'on_' + event) class SocketIONamespace(EngineIONamespace): @@ -60,13 +79,45 @@ class SocketIONamespace(EngineIONamespace): super(SocketIONamespace, self).__init__(io) def on_connect(self): - 'Called after server connects; you can override this method' + """Called after socket.io connects. + You can override this method.""" def on_reconnect(self): - 'Called after server reconnects; you can override this method' + """Called after socket.io reconnects. + You can override this method.""" def on_disconnect(self): - 'Called after server disconnects; you can override this method' + """Called after socket.io disconnects. + You can override this method.""" + + def on_event(self, event, *args): + """ + Called if there is no matching event handler. + You can override this method. + There are three ways to define an event handler: + + - Call socketIO.on() + + socketIO = SocketIO('localhost', 8000) + socketIO.on('my_event', my_function) + + - Call namespace.on() + + namespace = socketIO.get_namespace() + namespace.on('my_event', my_function) + + - Define namespace.on_xxx + + class Namespace(SocketIONamespace): + + def on_my_event(self, *args): + my_function(*args) + + socketIO.define(Namespace)""" + + def on_error(self, data): + """Called after socket.io sends an error packet. + You can override this method.""" def _find_packet_callback(self, event): # Interpret events @@ -75,7 +126,15 @@ class SocketIONamespace(EngineIONamespace): self._was_connected = True else: event = 'reconnect' - return super(SocketIONamespace, self)._find_packet_callback(event) + # Check callbacks defined by on() + try: + return self._callback_by_event[event] + except KeyError: + pass + # Check callbacks defined explicitly or use on_event() + return getattr( + self, 'on_' + event.replace(' ', '_'), + lambda *args: self.on_event(event, *args)) class LoggingMixin(object): @@ -134,21 +193,17 @@ class EngineIO(object): resource='engine.io', **kw): self._url = 'http://%s:%s/%s/' % (host, port, resource) self._http_session = requests.Session() - print(self._url) response = self._http_session.get(self._url, params={ 'EIO': self._engineIO_protocol, 'transport': 'polling', 't': self._get_timestamp(), }) - print(response.url) - - engineIO_packets = _decode_content(response.content) + engineIO_packets = _decode_engineIO_content(response.content) engineIO_packet_type, engineIO_packet_data = engineIO_packets[0] assert engineIO_packet_type == 0 value_by_name = json.loads(get_unicode(engineIO_packet_data)) - print(value_by_name) - # value_by_name['upgrades'] + # 'websocket' in value_by_name['upgrades'] self._ping_interval = value_by_name['pingInterval'] / float(1000) self._ping_timeout = value_by_name['pingTimeout'] / float(1000) self._session_id = value_by_name['sid'] @@ -162,6 +217,22 @@ class EngineIO(object): hurry_interval_in_seconds=1) self._heartbeat_thread.start() + @property + def connected(self): + try: + transport = self.__transport + except AttributeError: + return False + else: + return transport.connected + + def on(self, event, callback): + try: + namespace = self.get_namespace() + except PacketError: + namespace = self.define(EngineIONamespace) + return namespace.on(event, callback) + def define(self, Namespace): self._namespace = namespace = Namespace(self) return namespace @@ -172,16 +243,36 @@ class EngineIO(object): except AttributeError: raise PacketError('undefined engine.io namespace') - def wait(self, seconds=None): + def wait(self, seconds=None, **kw): + 'Wait in a loop and react to events as defined in the namespaces' self._heartbeat_thread.hurry() warning_screen = _yield_warning_screen(seconds) for elapsed_time in warning_screen: + if self._should_stop_waiting(**kw): + break try: - self._process_packets() - except TimeoutError: - pass + try: + self._process_packets() + except TimeoutError: + pass + except ConnectionError as e: + try: + warning = Exception('[connection error] %s' % e) + warning_screen.throw(warning) + except StopIteration: + self._log(logging.WARNING, warning) + try: + namespace = self.get_namespace() + namespace.on_disconnect() + except PacketError: + pass + self._heartbeat_thread.relax() + def _should_stop_waiting(self): + # Use __transport to make sure that we do not reconnect inadvertently + return self.__transport._wants_to_disconnect + def _process_packets(self): for engineIO_packet in self._recv_packet(): try: @@ -192,8 +283,6 @@ class EngineIO(object): def _process_packet(self, packet): engineIO_packet_type, engineIO_packet_data = packet print('engineIO_packet_type = %s' % engineIO_packet_type) - engineIO_packet_data_parsed = _parse_engineIO_data( - engineIO_packet_data) # Launch callbacks namespace = self.get_namespace() try: @@ -209,30 +298,31 @@ class EngineIO(object): except KeyError: raise PacketError( 'unexpected engine.io packet type (%s)' % engineIO_packet_type) - delegate(engineIO_packet_data_parsed, namespace._find_packet_callback) + delegate(engineIO_packet_data, namespace._find_packet_callback) if engineIO_packet_type is 4: return engineIO_packet_data - def _on_open(self, data_parsed, find_packet_callback): - pass + def _on_open(self, data, find_packet_callback): + find_packet_callback('open')() - def _on_close(self, data_parsed, find_packet_callback): - pass + def _on_close(self, data, find_packet_callback): + find_packet_callback('close')() - def _on_ping(self, data_parsed, find_packet_callback): - pass + def _on_ping(self, data, find_packet_callback): + self._pong(data) + find_packet_callback('ping')(data) - def _on_pong(self, data_parsed, find_packet_callback): - pass + def _on_pong(self, data, find_packet_callback): + find_packet_callback('pong')(data) - def _on_message(self, data_parsed, find_packet_callback): - pass + def _on_message(self, data, find_packet_callback): + find_packet_callback('message')(data) - def _on_upgrade(self, data_parsed, find_packet_callback): - pass + def _on_upgrade(self, data, find_packet_callback): + find_packet_callback('upgrade')() - def _on_noop(self, data_parsed, find_packet_callback): - pass + def _on_noop(self, data, find_packet_callback): + find_packet_callback('noop')() def _get_timestamp(self): timestamp = '%s-%s' % ( @@ -247,13 +337,14 @@ class EngineIO(object): 'transport': 'polling', 't': self._get_timestamp(), 'sid': self._session_id, - }, data=_encode_content([ + }, data=_encode_engineIO_content([ (engineIO_packet_type, engineIO_packet_data), ]), headers={ 'content-type': 'application/octet-stream', }) print('message()') - engineIO_packets = _decode_content(response.content) + print(response.content) + engineIO_packets = _decode_engineIO_content(response.content) for engineIO_packet_type, engineIO_packet_data in engineIO_packets: socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) socketIO_packet_data = engineIO_packet_data[1:] @@ -261,21 +352,43 @@ class EngineIO(object): print('socketIO_packet_type = %s' % socketIO_packet_type) print('socketIO_packet_data = %s' % socketIO_packet_data) - def _ping(self): + def _ping(self, engineIO_packet_data=''): engineIO_packet_type = 2 - engineIO_packet_data = '' response = self._http_session.post(self._url, params={ 'EIO': self._engineIO_protocol, 'transport': 'polling', 't': self._get_timestamp(), 'sid': self._session_id, - }, data=_encode_content([ + }, data=_encode_engineIO_content([ (engineIO_packet_type, engineIO_packet_data), ]), headers={ 'content-type': 'application/octet-stream', }) print('ping()') - engineIO_packets = _decode_content(response.content) + print(response.content) + engineIO_packets = _decode_engineIO_content(response.content) + for engineIO_packet_type, engineIO_packet_data in engineIO_packets: + socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) + socketIO_packet_data = engineIO_packet_data[1:] + print('engineIO_packet_type = %s' % engineIO_packet_type) + print('socketIO_packet_type = %s' % socketIO_packet_type) + print('socketIO_packet_data = %s' % socketIO_packet_data) + + def _pong(self, engineIO_packet_data=''): + engineIO_packet_type = 3 + response = self._http_session.post(self._url, params={ + 'EIO': self._engineIO_protocol, + 'transport': 'polling', + 't': self._get_timestamp(), + 'sid': self._session_id, + }, data=_encode_engineIO_content([ + (engineIO_packet_type, engineIO_packet_data), + ]), headers={ + 'content-type': 'application/octet-stream', + }) + print('pong()') + print(response.content) + engineIO_packets = _decode_engineIO_content(response.content) for engineIO_packet_type, engineIO_packet_data in engineIO_packets: socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) socketIO_packet_data = engineIO_packet_data[1:] @@ -294,7 +407,7 @@ class EngineIO(object): 'sid': self._session_id, }, timeout=self._ping_timeout) - for engineIO_packet in _decode_content(response.content): + for engineIO_packet in _decode_engineIO_content(response.content): yield engineIO_packet def _log(self, level, msg, *attrs): @@ -309,11 +422,20 @@ class SocketIO(EngineIO): wait_for_connection=True, transports=TRANSPORTS, resource='socket.io', **kw): self._namespace_by_path = {} + self._callback_by_ack_id = {} + self._ack_id = 0 super(SocketIO, self).__init__( host, port, Namespace, wait_for_connection, transports, resource, **kw) + def on(self, event, callback, path=''): + try: + namespace = self.get_namespace(path) + except PacketError: + namespace = self.define(SocketIONamespace, path) + return namespace.on(event, callback) + def define(self, Namespace, path=''): if path: self._connect(path) @@ -326,13 +448,45 @@ class SocketIO(EngineIO): except KeyError: raise PacketError('undefined socket.io namespace (%s)' % path) + def wait(self, seconds=None, for_callbacks=False): + super(SocketIO, self).wait(seconds, for_callbacks=for_callbacks) + + def wait_for_callbacks(self, seconds=None): + self.wait(seconds, for_callbacks=True) + + def _should_stop_waiting(self, for_callbacks): + # Use __transport to make sure that we do not reconnect inadvertently + if for_callbacks and not self.__transport.has_ack_callback: + return True + return super(SocketIO, self)._should_stop_waiting() + def emit(self, event, *args, **kw): + path = kw.get('path', '') + callback, args = find_callback(args, kw) + self._emit(path, event, args, callback) + + def _emit(self, path, event, args, callback): socketIO_packet_type = 2 - socketIO_packet_data = json.dumps([event]) + + socketIO_packet_data = json.dumps([event] + list(args)) + if callback: + ack_id = self._set_ack_callback(callback) if callback else '' + socketIO_packet_data = str(ack_id) + socketIO_packet_data + if path: + socketIO_packet_data = path + ',' + socketIO_packet_data + self._message(str(socketIO_packet_type) + socketIO_packet_data) - def on(self, event, callback): - pass + def _set_ack_callback(self, callback): + self._ack_id += 1 + self._callback_by_ack_id[self._ack_id] = callback + return self._ack_id + + def send(self, data='', callback=None): + args = [data] + if callback: + args.append(callback) + self.emit('message', *args) def _process_packet(self, packet): engineIO_packet_data = super(SocketIO, self)._process_packet(packet) @@ -341,8 +495,6 @@ class SocketIO(EngineIO): socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) socketIO_packet_data = engineIO_packet_data[1:] print('socketIO_packet_type = %s' % socketIO_packet_type) - socketIO_packet_data_parsed = _parse_socketIO_data( - socketIO_packet_data) # Launch callbacks namespace = self.get_namespace() try: @@ -358,33 +510,46 @@ class SocketIO(EngineIO): except KeyError: raise PacketError( 'unexpected socket.io packet type (%s)' % socketIO_packet_type) - delegate(socketIO_packet_data_parsed, namespace._find_packet_callback) + delegate(socketIO_packet_data, namespace._find_packet_callback) return socketIO_packet_data - def _on_connect(self, data_parsed, find_packet_callback): + def _on_connect(self, data, find_packet_callback): find_packet_callback('connect')() - def _on_disconnect(self, data_parsed, find_packet_callback): + def _on_disconnect(self, data, find_packet_callback): find_packet_callback('disconnect')() - def _on_event(self, data_parsed, find_packet_callback): + def _on_event(self, data, find_packet_callback): + data_parsed = _parse_socketIO_data(data) args = data_parsed.args + try: + event = args.pop(0) + except IndexError: + raise PacketError('missing event name') if data_parsed.ack_id: args.append(self._prepare_to_send_ack( data_parsed.path, data_parsed.ack_id)) - find_packet_callback(data_parsed.event)(*args) + find_packet_callback(event)(*args) - def _on_ack(self, data_parsed, find_packet_callback): - pass + def _on_ack(self, data, find_packet_callback): + data_parsed = _parse_socketIO_data(data) + try: + ack_callback = self._get_ack_callback(data_parsed.ack_id) + except KeyError: + return + ack_callback(*data_parsed.args) - def _on_error(self, data_parsed, find_packet_callback): - pass + def _get_ack_callback(self, ack_id): + return self._callback_by_ack_id.pop(ack_id) - def _on_binary_event(self, data_parsed, find_packet_callback): - pass + def _on_error(self, data, find_packet_callback): + find_packet_callback('error')(data) - def _on_binary_ack(self, data_parsed, find_packet_callback): - pass + def _on_binary_event(self, data, find_packet_callback): + self._log(logging.WARNING, '[not implemented] binary event') + + def _on_binary_ack(self, data, find_packet_callback): + self._log(logging.WARNING, '[not implemented] binary ack') def _prepare_to_send_ack(self, path, ack_id): 'Return function that acknowledges the server' @@ -421,6 +586,8 @@ class HeartbeatThread(threading.Thread): def hurry(self): self._adrenaline.set() + self._rest.set() + self._rest.clear() def stop(self): self._stop.set() @@ -436,7 +603,7 @@ def find_callback(args, kw=None): return None, args -def _decode_content(content): +def _decode_engineIO_content(content): packets = [] content_index = 0 content_length = len(content) @@ -454,7 +621,7 @@ def _decode_content(content): return packets -def _encode_content(packets): +def _encode_engineIO_content(packets): parts = [] for packet_type, packet_data in packets: packet_string = str(packet_type) + str(packet_data) @@ -491,10 +658,6 @@ def _make_packet_header(packet_string): return ''.join(chr(x) for x in header_digits) -def _parse_engineIO_data(data): - return EngineIOData(data=get_unicode(data)) - - def _parse_socketIO_data(data): data = get_unicode(data) if data.startswith('/'): @@ -510,14 +673,11 @@ def _parse_socketIO_data(data): data = data[1:] except (ValueError, IndexError): ack_id = None - if data: - x = json.loads(data) - event = x[0] - args = x[1:] - else: - event = '' + try: + args = json.loads(data) + except ValueError: args = [] - return SocketIOData(path=path, ack_id=ack_id, event=event, args=args) + return SocketIOData(path=path, ack_id=ack_id, args=args) def _yield_warning_screen(seconds=None): diff --git a/socketIO_client/tests/__init__.py b/socketIO_client/tests/__init__.py index ed194c3..75be733 100644 --- a/socketIO_client/tests/__init__.py +++ b/socketIO_client/tests/__init__.py @@ -6,11 +6,19 @@ from .. import SocketIO, LoggingSocketIONamespace, find_callback HOST = 'localhost' PORT = 8000 +DATA = 'xxx' +PAYLOAD = {'xxx': 'yyy'} logging.basicConfig(level=logging.DEBUG) class BaseMixin(object): + def setUp(self): + self.called_on_response = False + + def tearDown(self): + del self.socketIO + def test_emit(self): 'Emit' namespace = self.socketIO.define(Namespace) @@ -20,6 +28,84 @@ class BaseMixin(object): 'emit_response': (), }) + def test_emit_with_payload(self): + 'Emit with payload' + namespace = self.socketIO.define(Namespace) + self.socketIO.emit('emit_with_payload', PAYLOAD) + self.socketIO.wait(self.wait_time_in_seconds) + self.assertEqual(namespace.args_by_event, { + 'emit_with_payload_response': (PAYLOAD,), + }) + + def test_emit_with_multiple_payloads(self): + 'Emit with multiple payloads' + namespace = self.socketIO.define(Namespace) + self.socketIO.emit('emit_with_multiple_payloads', PAYLOAD, PAYLOAD) + self.socketIO.wait(self.wait_time_in_seconds) + self.assertEqual(namespace.args_by_event, { + 'emit_with_multiple_payloads_response': (PAYLOAD, PAYLOAD), + }) + + def test_emit_with_callback(self): + 'Emit with callback' + self.socketIO.define(LoggingSocketIONamespace) + self.socketIO.emit('emit_with_callback', self.on_response) + self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) + self.assertTrue(self.called_on_response) + + def test_emit_with_callback_with_payload(self): + 'Emit with callback with payload' + self.socketIO.define(LoggingSocketIONamespace) + self.socketIO.emit( + 'emit_with_callback_with_payload', self.on_response) + self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) + self.assertTrue(self.called_on_response) + + def test_emit_with_callback_with_multiple_payloads(self): + 'Emit with callback with multiple payloads' + self.socketIO.define(LoggingSocketIONamespace) + self.socketIO.emit( + 'emit_with_callback_with_multiple_payloads', self.on_response) + self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) + self.assertTrue(self.called_on_response) + + def test_emit_with_event(self): + 'Emit to trigger an event' + self.socketIO.on('emit_with_event_response', self.on_response) + self.socketIO.emit('emit_with_event', PAYLOAD) + self.socketIO.wait(self.wait_time_in_seconds) + self.assertTrue(self.called_on_response) + + def test_send(self): + 'Send' + namespace = self.socketIO.define(Namespace) + self.socketIO.send() + self.socketIO.wait(self.wait_time_in_seconds) + self.assertEqual(namespace.response, 'message_response') + + def test_send_with_data(self): + 'Send with data' + namespace = self.socketIO.define(Namespace) + self.socketIO.send(DATA) + self.socketIO.wait(self.wait_time_in_seconds) + self.assertEqual(namespace.response, DATA) + + def on_response(self, *args): + for arg in args: + if isinstance(arg, dict): + self.assertEqual(arg, PAYLOAD) + else: + self.assertEqual(arg, DATA) + self.called_on_response = True + + +# class Test_WebsocketTransport(TestCase, BaseMixin): + + # def setUp(self): + # super(Test_WebsocketTransport, self).setUp() + # self.socketIO = SocketIO(HOST, PORT, transports=['websocket']) + # self.wait_time_in_seconds = 0.1 + class Test_XHR_PollingTransport(TestCase, BaseMixin): @@ -32,10 +118,21 @@ class Test_XHR_PollingTransport(TestCase, BaseMixin): class Namespace(LoggingSocketIONamespace): def initialize(self): + self.called_on_disconnect = False self.args_by_event = {} + self.response = None + + def on_disconnect(self): + self.called_on_disconnect = True + + def on_wait_with_disconnect_response(self): + self.disconnect() def on_event(self, event, *args): callback, args = find_callback(args) if callback: callback(*args) self.args_by_event[event] = args + + def on_message(self, data): + self.response = data diff --git a/socketIO_client/tests/index.html b/socketIO_client/tests/index.html index 86299b8..df37df0 100644 --- a/socketIO_client/tests/index.html +++ b/socketIO_client/tests/index.html @@ -1,4 +1,25 @@ diff --git a/socketIO_client/tests/proxy.js b/socketIO_client/tests/proxy.js index f5fbfca..6179788 100644 --- a/socketIO_client/tests/proxy.js +++ b/socketIO_client/tests/proxy.js @@ -1,5 +1,8 @@ var proxy = require('http-proxy').createProxyServer({ target: {host: 'localhost', port: 9000} +}).on('error', function(err, req, res) { + console.log('[ERROR] %s', err); + res.end(); }); var server = require('http').createServer(function(req, res) { console.log('[REQUEST.%s] %s', req.method, req.url); diff --git a/socketIO_client/tests/serve.js b/socketIO_client/tests/serve.js index c3e10c8..40af3e1 100644 --- a/socketIO_client/tests/serve.js +++ b/socketIO_client/tests/serve.js @@ -43,9 +43,9 @@ io.on('connection', function(socket) { socket.on('emit_with_event', function(payload) { socket.emit('emit_with_event_response', payload); }); - socket.on('ack', function(payload) { - socket.emit('ack_response', payload, function(payload) { - socket.emit('ack_callback_response', payload); + socket.on('trigger_server_expects_callback', function(payload) { + socket.emit('server_expects_callback', payload, function(payload) { + socket.emit('server_received_callback', payload); }); }); socket.on('aaa', function() { @@ -66,9 +66,9 @@ io.of('/chat').on('connection', function(socket) { socket.on('aaa', function() { socket.emit('aaa_response', 'in chat'); }); - socket.on('ack', function(payload) { - socket.emit('ack_response', payload, function(payload) { - socket.emit('ack_callback_response', payload); + socket.on('trigger_server_expects_callback', function(payload) { + socket.emit('server_expects_callback', payload, function(payload) { + socket.emit('server_received_callback', payload); }); }); }); From 9afe5c2f2a7d40f321e6f062e40733d0045507a2 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Fri, 20 Feb 2015 18:06:31 -0500 Subject: [PATCH 147/191] Isolate transport --- socketIO_client/__init__.py | 237 +++++++++++++++++++--------------- socketIO_client/compat.py | 4 + socketIO_client/transports.py | 45 +++++++ 3 files changed, 183 insertions(+), 103 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index d390fda..0b29107 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -5,9 +5,9 @@ import threading import time from collections import namedtuple -from .compat import get_byte, get_character, get_unicode +from . import transports +from .compat import get_byte, get_character, get_unicode, parse_url from .exceptions import ConnectionError, TimeoutError, PacketError -from .transports import _get_response __version__ = '0.6.1' @@ -183,7 +183,6 @@ class LoggingSocketIONamespace(SocketIONamespace, LoggingMixin): class EngineIO(object): - _engineIO_protocol = 3 _engineIO_request_index = 0 def __init__( @@ -191,31 +190,22 @@ class EngineIO(object): host, port=None, Namespace=None, wait_for_connection=True, transports=TRANSPORTS, resource='engine.io', **kw): - self._url = 'http://%s:%s/%s/' % (host, port, resource) + self._is_secure, self._url = _parse_host(host, port, resource) + self._wait_for_connection = wait_for_connection + self._client_transports = transports + self._kw = kw self._http_session = requests.Session() - - response = self._http_session.get(self._url, params={ - 'EIO': self._engineIO_protocol, - 'transport': 'polling', - 't': self._get_timestamp(), - }) - engineIO_packets = _decode_engineIO_content(response.content) - engineIO_packet_type, engineIO_packet_data = engineIO_packets[0] - assert engineIO_packet_type == 0 - value_by_name = json.loads(get_unicode(engineIO_packet_data)) - # 'websocket' in value_by_name['upgrades'] - self._ping_interval = value_by_name['pingInterval'] / float(1000) - self._ping_timeout = value_by_name['pingTimeout'] / float(1000) - self._session_id = value_by_name['sid'] - if Namespace: self.define(Namespace) - self._heartbeat_thread = HeartbeatThread( - send_heartbeat=self._ping, - relax_interval_in_seconds=self._ping_interval, - hurry_interval_in_seconds=1) - self._heartbeat_thread.start() + def __enter__(self): + return self + + def __exit__(self, *exception_pack): + self.close() + + def __del__(self): + self.close() @property def connected(self): @@ -266,7 +256,6 @@ class EngineIO(object): namespace.on_disconnect() except PacketError: pass - self._heartbeat_thread.relax() def _should_stop_waiting(self): @@ -274,7 +263,7 @@ class EngineIO(object): return self.__transport._wants_to_disconnect def _process_packets(self): - for engineIO_packet in self._recv_packet(): + for engineIO_packet in self._transport.recv_packet(): try: self._process_packet(engineIO_packet) except PacketError as e: @@ -332,89 +321,100 @@ class EngineIO(object): def _message(self, engineIO_packet_data): engineIO_packet_type = 4 - response = self._http_session.post(self._url, params={ - 'EIO': self._engineIO_protocol, - 'transport': 'polling', - 't': self._get_timestamp(), - 'sid': self._session_id, - }, data=_encode_engineIO_content([ - (engineIO_packet_type, engineIO_packet_data), - ]), headers={ - 'content-type': 'application/octet-stream', - }) - print('message()') - print(response.content) - engineIO_packets = _decode_engineIO_content(response.content) - for engineIO_packet_type, engineIO_packet_data in engineIO_packets: - socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) - socketIO_packet_data = engineIO_packet_data[1:] - print('engineIO_packet_type = %s' % engineIO_packet_type) - print('socketIO_packet_type = %s' % socketIO_packet_type) - print('socketIO_packet_data = %s' % socketIO_packet_data) + self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) def _ping(self, engineIO_packet_data=''): engineIO_packet_type = 2 - response = self._http_session.post(self._url, params={ - 'EIO': self._engineIO_protocol, - 'transport': 'polling', - 't': self._get_timestamp(), - 'sid': self._session_id, - }, data=_encode_engineIO_content([ - (engineIO_packet_type, engineIO_packet_data), - ]), headers={ - 'content-type': 'application/octet-stream', - }) - print('ping()') - print(response.content) - engineIO_packets = _decode_engineIO_content(response.content) - for engineIO_packet_type, engineIO_packet_data in engineIO_packets: - socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) - socketIO_packet_data = engineIO_packet_data[1:] - print('engineIO_packet_type = %s' % engineIO_packet_type) - print('socketIO_packet_type = %s' % socketIO_packet_type) - print('socketIO_packet_data = %s' % socketIO_packet_data) + self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) def _pong(self, engineIO_packet_data=''): engineIO_packet_type = 3 - response = self._http_session.post(self._url, params={ - 'EIO': self._engineIO_protocol, - 'transport': 'polling', - 't': self._get_timestamp(), - 'sid': self._session_id, - }, data=_encode_engineIO_content([ - (engineIO_packet_type, engineIO_packet_data), - ]), headers={ - 'content-type': 'application/octet-stream', - }) - print('pong()') - print(response.content) - engineIO_packets = _decode_engineIO_content(response.content) - for engineIO_packet_type, engineIO_packet_data in engineIO_packets: - socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) - socketIO_packet_data = engineIO_packet_data[1:] - print('engineIO_packet_type = %s' % engineIO_packet_type) - print('socketIO_packet_type = %s' % socketIO_packet_type) - print('socketIO_packet_data = %s' % socketIO_packet_data) + self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) - def _recv_packet(self): - response = _get_response( - self._http_session.get, - self._url, - params={ - 'EIO': self._engineIO_protocol, - 'transport': 'polling', - 't': self._get_timestamp(), - 'sid': self._session_id, - }, - timeout=self._ping_timeout) - for engineIO_packet in _decode_engineIO_content(response.content): - yield engineIO_packet + @property + def _transport(self): + try: + if self.connected: + return self.__transport + except AttributeError: + pass + self._get_engineIO_session() + self._negotiate_transport() + self._reset_heartbeat() + self._connect_namespaces() + return self.__transport + + def _get_engineIO_session(self): + url = '%s://%s/' % ('https' if self.is_secure else 'http', self._url) + warning_screen = _yield_warning_screen() + for elapsed_time in warning_screen: + try: + engineIO_packet_type, engineIO_packet_data = next( + XHR_PollingTransport().recv_packet()) + + response = _get_response(self._http_session.get, url, params={ + 'EIO': self._engineIO_protocol, + 'transport': 'polling', + 't': self._get_timestamp(), + }, **self._kw) + except (TimeoutError, ConnectionError) as e: + if not self._wait_for_connection: + raise + warning = Exception('[waiting for connection] %s' % e) + warning_screen.throw(warning) + engineIO_packets = _decode_engineIO_content(response.content) + engineIO_packet_type, engineIO_packet_data = engineIO_packets[0] + assert engineIO_packet_type == 0 + value_by_name = json.loads(get_unicode(engineIO_packet_data)) + self._session_id = value_by_name['sid'] + self._ping_interval = value_by_name['pingInterval'] / float(1000) + self._ping_timeout = value_by_name['pingTimeout'] / float(1000) + self._transport_upgrades = value_by_name['upgrades'] + + def _negotiate_transport(self): + self.__transport = self._get_transport('xhr-polling') + + def _reset_heartbeat(self): + try: + self._heartbeat_thread.stop() + except AttributeError: + pass + self._heartbeat_thread = HeartbeatThread( + send_heartbeat=self.__transport._ping, + relax_interval_in_seconds=self._ping_interval, + hurry_interval_in_seconds=1) + self._heartbeat_thread.start() + + def _connect_namespaces(self): + pass + + def _get_transport(self, transport_name): + self._log(logging.DEBUG, '[transport chosen] %s', transport_name) + return { + 'xhr-polling': transports.XHR_PollingTransport, + }[transport_name]() def _log(self, level, msg, *attrs): _log.log(level, '%s: %s' % (self._url, msg), *attrs) class SocketIO(EngineIO): + """Create a socket.io client that connects to a socket.io server + at the specified host and port. + + - Define the behavior of the client by specifying a custom Namespace. + - Prefix host with https:// to use SSL. + - Set wait_for_connection=True to block until we have a connection. + - Specify desired transports=['websocket', 'xhr-polling']. + - Pass query params, headers, cookies, proxies as keyword arguments. + + SocketIO( + 'localhost', 8000, + params={'q': 'qqq'}, + headers={'Authorization': 'Basic ' + b64encode('username:password')}, + cookies={'a': 'aaa'}, + proxies={'https': 'https://proxy.example.com:8080'}) + """ def __init__( self, @@ -429,6 +429,14 @@ class SocketIO(EngineIO): wait_for_connection, transports, resource, **kw) + def __exit__(self, *exception_pack): + self.disconnect() + super(SocketIO, self).__exit__(*exception_pack) + + def __del__(self): + self.disconnect() + super(SocketIO, self).__del__() + def on(self, event, callback, path=''): try: namespace = self.get_namespace(path) @@ -555,6 +563,12 @@ class SocketIO(EngineIO): 'Return function that acknowledges the server' return lambda *args: self._ack(path, ack_id, *args) + def _connect_namespaces(self): + for path, namespace in self._namespace_by_path.items(): + namespace._transport = self.__transport + if path: + self.__transport.connect(path) + class HeartbeatThread(threading.Thread): @@ -573,13 +587,19 @@ class HeartbeatThread(threading.Thread): self._stop = threading.Event() def run(self): - while not self._stop.is_set(): - self._send_heartbeat() - if self._adrenaline.is_set(): - interval_in_seconds = self._hurry_interval_in_seconds - else: - interval_in_seconds = self._relax_interval_in_seconds - self._rest.wait(interval_in_seconds) + try: + while not self._stop.is_set(): + try: + self._send_heartbeat() + except TimeoutError: + pass + if self._adrenaline.is_set(): + interval_in_seconds = self._hurry_interval_in_seconds + else: + interval_in_seconds = self._relax_interval_in_seconds + self._rest.wait(interval_in_seconds) + except ConnectionError: + pass def relax(self): self._adrenaline.clear() @@ -590,6 +610,7 @@ class HeartbeatThread(threading.Thread): self._rest.clear() def stop(self): + self._rest.set() self._stop.set() @@ -603,6 +624,16 @@ def find_callback(args, kw=None): return None, args +def _parse_host(host, port, resource): + if not host.startswith('http'): + host = 'http://' + host + url_pack = parse_url(host) + is_secure = url_pack.scheme == 'https' + port = port or url_pack.port or (443 if is_secure else 80) + url = '%s:%d%s/%s' % (url_pack.hostname, port, url_pack.path, resource) + return is_secure, url + + def _decode_engineIO_content(content): packets = [] content_index = 0 diff --git a/socketIO_client/compat.py b/socketIO_client/compat.py index 1408e19..012993c 100644 --- a/socketIO_client/compat.py +++ b/socketIO_client/compat.py @@ -1,4 +1,8 @@ import six +try: + from urllib.parse import urlparse as parse_url +except ImportError: + from urlparse import urlparse as parse_url def get_byte(x, index): diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 7620855..fe641a7 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -2,6 +2,51 @@ import requests from .exceptions import ConnectionError, TimeoutError +ENGINEIO_PROTOCOL = 3 + + +class AbstractTransport(object): + pass + + +class XHR_PollingTransport(AbstractTransport): + + # pass http session + # pass session id (can be none) + # pass timeout + + def send_packet(self, engineIO_packet_type, engineIO_packet_data): + _get_response() + + assert ok + + response = self._http_session.post(self._url, params={ + 'EIO': self._engineIO_protocol, + 'transport': 'polling', + 't': self._get_timestamp(), + 'sid': self._session_id, + }, data=_encode_engineIO_content([ + (engineIO_packet_type, engineIO_packet_data), + ]), headers={ + 'content-type': 'application/octet-stream', + }) + assert response.content == 'ok' + + def _recv_packet(self): + response = _get_response( + self._http_session.get, + self._url, + params={ + 'EIO': self._engineIO_protocol, + 'transport': 'polling', + 't': self._get_timestamp(), + 'sid': self._session_id, + }, + timeout=self._ping_timeout) + for engineIO_packet in _decode_engineIO_content(response.content): + yield engineIO_packet + + def _get_response(request, *args, **kw): try: response = request(*args, **kw) From 5ecc5fb36c5ec43308016833bd08825b846d827f Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sat, 21 Feb 2015 13:20:28 -0500 Subject: [PATCH 148/191] Split into modules --- socketIO_client/__init__.py | 648 +++++-------------- socketIO_client/heartbeats.py | 47 ++ socketIO_client/logs.py | 38 ++ socketIO_client/namespaces.py | 198 ++++++ socketIO_client/parsers.py | 93 +++ socketIO_client/{compat.py => symmetries.py} | 6 +- socketIO_client/transports.py | 105 ++- 7 files changed, 625 insertions(+), 510 deletions(-) create mode 100644 socketIO_client/heartbeats.py create mode 100644 socketIO_client/logs.py create mode 100644 socketIO_client/namespaces.py create mode 100644 socketIO_client/parsers.py rename socketIO_client/{compat.py => symmetries.py} (79%) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 0b29107..2d1cc66 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,211 +1,34 @@ import json -import logging -import requests -import threading -import time -from collections import namedtuple -from . import transports -from .compat import get_byte, get_character, get_unicode, parse_url from .exceptions import ConnectionError, TimeoutError, PacketError +from .heartbeats import HeartbeatThread +from .logs import LoggingMixin +from .namespaces import EngineIONamespace, SocketIONamespace, find_callback +from .parsers import parse_host, parse_socketIO_data +from .symmetries import encode_string, get_character +from .transports import XHR_PollingTransport, prepare_http_session, TRANSPORTS +__all__ = 'SocketIO', 'SocketIONamespace' __version__ = '0.6.1' -_log = logging.getLogger(__name__) -SocketIOData = namedtuple('SocketIOData', ['path', 'ack_id', 'args']) -TRANSPORTS = [] -RETRY_INTERVAL_IN_SECONDS = 1 -class EngineIONamespace(object): - 'Define engine.io client behavior' - - def __init__(self, io): - self._io = io - self._callback_by_event = {} - self.initialize() - - def initialize(self): - """Initialize custom variables here. - You can override this method.""" - - def on(self, event, callback): - 'Define a callback to handle an event emitted by the server' - self._callback_by_event[event] = callback - - def on_open(self): - """Called after engine.io connects. - You can override this method.""" - - def on_close(self): - """Called after engine.io disconnects. - You can override this method.""" - - def on_ping(self, data): - """Called after engine.io sends a ping packet. - You can override this method.""" - - def on_pong(self, data): - """Called after engine.io sends a pong packet. - You can override this method.""" - - def on_message(self, data): - """Called after engine.io sends a message packet. - You can override this method.""" - - def on_upgrade(self): - """Called after engine.io sends an upgrade packet. - You can override this method.""" - - def on_noop(self): - """Called after engine.io sends a noop packet. - You can override this method.""" - - def _find_packet_callback(self, event): - # Check callbacks defined by on() - try: - return self._callback_by_event[event] - except KeyError: - pass - # Check callbacks defined explicitly - return getattr(self, 'on_' + event) - - -class SocketIONamespace(EngineIONamespace): - 'Define socket.io client behavior' - - def __init__(self, io, path): - self.path = path - super(SocketIONamespace, self).__init__(io) - - def on_connect(self): - """Called after socket.io connects. - You can override this method.""" - - def on_reconnect(self): - """Called after socket.io reconnects. - You can override this method.""" - - def on_disconnect(self): - """Called after socket.io disconnects. - You can override this method.""" - - def on_event(self, event, *args): - """ - Called if there is no matching event handler. - You can override this method. - There are three ways to define an event handler: - - - Call socketIO.on() - - socketIO = SocketIO('localhost', 8000) - socketIO.on('my_event', my_function) - - - Call namespace.on() - - namespace = socketIO.get_namespace() - namespace.on('my_event', my_function) - - - Define namespace.on_xxx - - class Namespace(SocketIONamespace): - - def on_my_event(self, *args): - my_function(*args) - - socketIO.define(Namespace)""" - - def on_error(self, data): - """Called after socket.io sends an error packet. - You can override this method.""" - - def _find_packet_callback(self, event): - # Interpret events - if event == 'connect': - if not hasattr(self, '_was_connected'): - self._was_connected = True - else: - event = 'reconnect' - # Check callbacks defined by on() - try: - return self._callback_by_event[event] - except KeyError: - pass - # Check callbacks defined explicitly or use on_event() - return getattr( - self, 'on_' + event.replace(' ', '_'), - lambda *args: self.on_event(event, *args)) - - -class LoggingMixin(object): - - def _log(self, level, msg, *attrs): - _log.log(level, '%s: %s' % (self._io._url, msg), *attrs) - - -class LoggingEngineIONamespace(EngineIONamespace, LoggingMixin): - - def on_event(self, event, *args): - callback, args = find_callback(args) - arguments = [repr(_) for _ in args] - if callback: - arguments.append('callback(*args)') - self._log( - logging.INFO, '[event] %s(%s)', - event, ', '.join(arguments)) - super(LoggingEngineIONamespace, self).on_event(event, *args) - - -class LoggingSocketIONamespace(SocketIONamespace, LoggingMixin): - - def on_event(self, event, *args): - callback, args = find_callback(args) - arguments = [repr(_) for _ in args] - if callback: - arguments.append('callback(*args)') - self._log( - logging.INFO, '%s [event] %s(%s)', self.path, - event, ', '.join(arguments)) - super(LoggingSocketIONamespace, self).on_event(event, *args) - - def on_connect(self): - self._log(logging.DEBUG, '%s [connect]', self.path) - super(LoggingSocketIONamespace, self).on_connect() - - def on_reconnect(self): - self._log(logging.DEBUG, '%s [reconnect]', self.path) - super(LoggingSocketIONamespace, self).on_reconnect() - - def on_disconnect(self): - self._log(logging.DEBUG, '%s [disconnect]', self.path) - super(LoggingSocketIONamespace, self).on_disconnect() - - -class EngineIO(object): - - _engineIO_request_index = 0 +class EngineIO(LoggingMixin): def __init__( - self, - host, port=None, Namespace=None, + self, host, port=None, Namespace=None, wait_for_connection=True, transports=TRANSPORTS, resource='engine.io', **kw): - self._is_secure, self._url = _parse_host(host, port, resource) + self._is_secure, self._url = parse_host(host, port, resource) self._wait_for_connection = wait_for_connection self._client_transports = transports - self._kw = kw - self._http_session = requests.Session() + self._http_session = prepare_http_session(kw) + self._log_name = self._url + self._wants_to_close = False if Namespace: self.define(Namespace) - def __enter__(self): - return self - - def __exit__(self, *exception_pack): - self.close() - - def __del__(self): - self.close() + # Connect @property def connected(self): @@ -216,6 +39,81 @@ class EngineIO(object): else: return transport.connected + @property + def _transport(self): + try: + if self.connected: + return self.__transport + except AttributeError: + pass + self._get_engineIO_session() + self._negotiate_transport() + self._reset_heartbeat() + self._connect_namespaces() + return self.__transport + + def _get_engineIO_session(self): + warning_screen = self._yield_warning_screen() + for elapsed_time in warning_screen: + transport = XHR_PollingTransport( + self._http_session, self._is_secure, self._url) + try: + engineIO_packet_type, engineIO_packet_data = next( + transport.recv_packet()) + + except (TimeoutError, ConnectionError) as e: + if not self._wait_for_connection: + raise + warning = Exception('[waiting for connection] %s' % e) + warning_screen.throw(warning) + assert engineIO_packet_type == 0 + value_by_name = json.loads(encode_string(engineIO_packet_data)) + self._session_id = value_by_name['sid'] + self._ping_interval = value_by_name['pingInterval'] / float(1000) + self._ping_timeout = value_by_name['pingTimeout'] / float(1000) + self._transport_upgrades = value_by_name['upgrades'] + + def _negotiate_transport(self): + self.__transport = self._get_transport('xhr-polling') + + def _reset_heartbeat(self): + try: + self._heartbeat_thread.stop() + except AttributeError: + pass + self._heartbeat_thread = HeartbeatThread( + send_heartbeat=self.__transport._ping, + relax_interval_in_seconds=self._ping_interval, + hurry_interval_in_seconds=1) + self._heartbeat_thread.start() + + def _connect_namespaces(self): + pass + + def _get_transport(self, transport_name): + self._debug('[transport selected] %s', transport_name) + SelectedTransport = { + 'xhr-polling': XHR_PollingTransport, + }[transport_name] + return SelectedTransport( + self._http_session, self._is_secure, self._url, + self._engineIO_session) + + def __enter__(self): + return self + + def __exit__(self, *exception_pack): + self._close() + + def __del__(self): + self._close() + + # Define + + def define(self, Namespace): + self._namespace = namespace = Namespace(self) + return namespace + def on(self, event, callback): try: namespace = self.get_namespace() @@ -223,20 +121,51 @@ class EngineIO(object): namespace = self.define(EngineIONamespace) return namespace.on(event, callback) - def define(self, Namespace): - self._namespace = namespace = Namespace(self) - return namespace - def get_namespace(self): try: return self._namespace except AttributeError: raise PacketError('undefined engine.io namespace') + # Act + + def _open(self): + engineIO_packet_type = 0 + self._transport.send_packet(engineIO_packet_type, '') + + def _close(self): + self._wants_to_close = True + if not self.connected: + return + engineIO_packet_type = 1 + self._transport.send_packet(engineIO_packet_type, '') + + def _ping(self, engineIO_packet_data=''): + engineIO_packet_type = 2 + self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) + + def _pong(self, engineIO_packet_data=''): + engineIO_packet_type = 3 + self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) + + def _message(self, engineIO_packet_data): + engineIO_packet_type = 4 + self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) + + def _upgrade(self): + engineIO_packet_type = 5 + self._transport.send_packet(engineIO_packet_type, '') + + def _noop(self): + engineIO_packet_type = 6 + self._transport.send_packet(engineIO_packet_type, '') + + # React + def wait(self, seconds=None, **kw): 'Wait in a loop and react to events as defined in the namespaces' self._heartbeat_thread.hurry() - warning_screen = _yield_warning_screen(seconds) + warning_screen = self._yield_warning_screen(seconds) for elapsed_time in warning_screen: if self._should_stop_waiting(**kw): break @@ -250,7 +179,7 @@ class EngineIO(object): warning = Exception('[connection error] %s' % e) warning_screen.throw(warning) except StopIteration: - self._log(logging.WARNING, warning) + self._warn(warning) try: namespace = self.get_namespace() namespace.on_disconnect() @@ -260,14 +189,14 @@ class EngineIO(object): def _should_stop_waiting(self): # Use __transport to make sure that we do not reconnect inadvertently - return self.__transport._wants_to_disconnect + return self._wants_to_close def _process_packets(self): for engineIO_packet in self._transport.recv_packet(): try: self._process_packet(engineIO_packet) except PacketError as e: - self._log(logging.WARNING, '[packet error] %s', e) + self._warn('[packet error] %s', e) def _process_packet(self, packet): engineIO_packet_type, engineIO_packet_data = packet @@ -313,90 +242,6 @@ class EngineIO(object): def _on_noop(self, data, find_packet_callback): find_packet_callback('noop')() - def _get_timestamp(self): - timestamp = '%s-%s' % ( - int(time.time() * 1000), self._engineIO_request_index) - self._engineIO_request_index += 1 - return timestamp - - def _message(self, engineIO_packet_data): - engineIO_packet_type = 4 - self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) - - def _ping(self, engineIO_packet_data=''): - engineIO_packet_type = 2 - self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) - - def _pong(self, engineIO_packet_data=''): - engineIO_packet_type = 3 - self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) - - @property - def _transport(self): - try: - if self.connected: - return self.__transport - except AttributeError: - pass - self._get_engineIO_session() - self._negotiate_transport() - self._reset_heartbeat() - self._connect_namespaces() - return self.__transport - - def _get_engineIO_session(self): - url = '%s://%s/' % ('https' if self.is_secure else 'http', self._url) - warning_screen = _yield_warning_screen() - for elapsed_time in warning_screen: - try: - engineIO_packet_type, engineIO_packet_data = next( - XHR_PollingTransport().recv_packet()) - - response = _get_response(self._http_session.get, url, params={ - 'EIO': self._engineIO_protocol, - 'transport': 'polling', - 't': self._get_timestamp(), - }, **self._kw) - except (TimeoutError, ConnectionError) as e: - if not self._wait_for_connection: - raise - warning = Exception('[waiting for connection] %s' % e) - warning_screen.throw(warning) - engineIO_packets = _decode_engineIO_content(response.content) - engineIO_packet_type, engineIO_packet_data = engineIO_packets[0] - assert engineIO_packet_type == 0 - value_by_name = json.loads(get_unicode(engineIO_packet_data)) - self._session_id = value_by_name['sid'] - self._ping_interval = value_by_name['pingInterval'] / float(1000) - self._ping_timeout = value_by_name['pingTimeout'] / float(1000) - self._transport_upgrades = value_by_name['upgrades'] - - def _negotiate_transport(self): - self.__transport = self._get_transport('xhr-polling') - - def _reset_heartbeat(self): - try: - self._heartbeat_thread.stop() - except AttributeError: - pass - self._heartbeat_thread = HeartbeatThread( - send_heartbeat=self.__transport._ping, - relax_interval_in_seconds=self._ping_interval, - hurry_interval_in_seconds=1) - self._heartbeat_thread.start() - - def _connect_namespaces(self): - pass - - def _get_transport(self, transport_name): - self._log(logging.DEBUG, '[transport chosen] %s', transport_name) - return { - 'xhr-polling': transports.XHR_PollingTransport, - }[transport_name]() - - def _log(self, level, msg, *attrs): - _log.log(level, '%s: %s' % (self._url, msg), *attrs) - class SocketIO(EngineIO): """Create a socket.io client that connects to a socket.io server @@ -417,18 +262,24 @@ class SocketIO(EngineIO): """ def __init__( - self, - host, port=None, Namespace=None, + self, host, port=None, Namespace=None, wait_for_connection=True, transports=TRANSPORTS, resource='socket.io', **kw): self._namespace_by_path = {} self._callback_by_ack_id = {} self._ack_id = 0 super(SocketIO, self).__init__( - host, port, Namespace, - wait_for_connection, transports, + host, port, Namespace, wait_for_connection, transports, resource, **kw) + # Connect + + def _connect_namespaces(self): + for path, namespace in self._namespace_by_path.items(): + namespace._transport = self.__transport + if path: + self.__transport.connect(path) + def __exit__(self, *exception_pack): self.disconnect() super(SocketIO, self).__exit__(*exception_pack) @@ -437,12 +288,7 @@ class SocketIO(EngineIO): self.disconnect() super(SocketIO, self).__del__() - def on(self, event, callback, path=''): - try: - namespace = self.get_namespace(path) - except PacketError: - namespace = self.define(SocketIONamespace, path) - return namespace.on(event, callback) + # Define def define(self, Namespace, path=''): if path: @@ -450,23 +296,26 @@ class SocketIO(EngineIO): self._namespace_by_path[path] = namespace = Namespace(self, path) return namespace + def on(self, event, callback, path=''): + try: + namespace = self.get_namespace(path) + except PacketError: + namespace = self.define(SocketIONamespace, path) + return namespace.on(event, callback) + def get_namespace(self, path=''): try: return self._namespace_by_path[path] except KeyError: raise PacketError('undefined socket.io namespace (%s)' % path) - def wait(self, seconds=None, for_callbacks=False): - super(SocketIO, self).wait(seconds, for_callbacks=for_callbacks) + # Act - def wait_for_callbacks(self, seconds=None): - self.wait(seconds, for_callbacks=True) + def connect(): + pass - def _should_stop_waiting(self, for_callbacks): - # Use __transport to make sure that we do not reconnect inadvertently - if for_callbacks and not self.__transport.has_ack_callback: - return True - return super(SocketIO, self)._should_stop_waiting() + def disconnect(): + pass def emit(self, event, *args, **kw): path = kw.get('path', '') @@ -496,6 +345,20 @@ class SocketIO(EngineIO): args.append(callback) self.emit('message', *args) + # React + + def wait(self, seconds=None, for_callbacks=False): + super(SocketIO, self).wait(seconds, for_callbacks=for_callbacks) + + def wait_for_callbacks(self, seconds=None): + self.wait(seconds, for_callbacks=True) + + def _should_stop_waiting(self, for_callbacks): + # Use __transport to make sure that we do not reconnect inadvertently + if for_callbacks and not self.__transport.has_ack_callback: + return True + return super(SocketIO, self)._should_stop_waiting() + def _process_packet(self, packet): engineIO_packet_data = super(SocketIO, self)._process_packet(packet) if engineIO_packet_data is None: @@ -528,7 +391,7 @@ class SocketIO(EngineIO): find_packet_callback('disconnect')() def _on_event(self, data, find_packet_callback): - data_parsed = _parse_socketIO_data(data) + data_parsed = parse_socketIO_data(data) args = data_parsed.args try: event = args.pop(0) @@ -540,194 +403,25 @@ class SocketIO(EngineIO): find_packet_callback(event)(*args) def _on_ack(self, data, find_packet_callback): - data_parsed = _parse_socketIO_data(data) + data_parsed = parse_socketIO_data(data) try: ack_callback = self._get_ack_callback(data_parsed.ack_id) except KeyError: return ack_callback(*data_parsed.args) - def _get_ack_callback(self, ack_id): - return self._callback_by_ack_id.pop(ack_id) - def _on_error(self, data, find_packet_callback): find_packet_callback('error')(data) def _on_binary_event(self, data, find_packet_callback): - self._log(logging.WARNING, '[not implemented] binary event') + self._warn('[not implemented] binary event') def _on_binary_ack(self, data, find_packet_callback): - self._log(logging.WARNING, '[not implemented] binary ack') + self._warn('[not implemented] binary ack') def _prepare_to_send_ack(self, path, ack_id): 'Return function that acknowledges the server' return lambda *args: self._ack(path, ack_id, *args) - def _connect_namespaces(self): - for path, namespace in self._namespace_by_path.items(): - namespace._transport = self.__transport - if path: - self.__transport.connect(path) - - -class HeartbeatThread(threading.Thread): - - daemon = True - - def __init__( - self, send_heartbeat, - relax_interval_in_seconds, - hurry_interval_in_seconds): - super(HeartbeatThread, self).__init__() - self._send_heartbeat = send_heartbeat - self._relax_interval_in_seconds = relax_interval_in_seconds - self._hurry_interval_in_seconds = hurry_interval_in_seconds - self._adrenaline = threading.Event() - self._rest = threading.Event() - self._stop = threading.Event() - - def run(self): - try: - while not self._stop.is_set(): - try: - self._send_heartbeat() - except TimeoutError: - pass - if self._adrenaline.is_set(): - interval_in_seconds = self._hurry_interval_in_seconds - else: - interval_in_seconds = self._relax_interval_in_seconds - self._rest.wait(interval_in_seconds) - except ConnectionError: - pass - - def relax(self): - self._adrenaline.clear() - - def hurry(self): - self._adrenaline.set() - self._rest.set() - self._rest.clear() - - def stop(self): - self._rest.set() - self._stop.set() - - -def find_callback(args, kw=None): - 'Return callback whether passed as a last argument or as a keyword' - if args and callable(args[-1]): - return args[-1], args[:-1] - try: - return kw['callback'], args - except (KeyError, TypeError): - return None, args - - -def _parse_host(host, port, resource): - if not host.startswith('http'): - host = 'http://' + host - url_pack = parse_url(host) - is_secure = url_pack.scheme == 'https' - port = port or url_pack.port or (443 if is_secure else 80) - url = '%s:%d%s/%s' % (url_pack.hostname, port, url_pack.path, resource) - return is_secure, url - - -def _decode_engineIO_content(content): - packets = [] - content_index = 0 - content_length = len(content) - while content_index < content_length: - try: - content_index, packet_length = _read_packet_length( - content, content_index) - except IndexError: - break - content_index, packet_string = _read_packet_string( - content, content_index, packet_length) - packet_type = int(get_character(packet_string, 0)) - packet_data = packet_string[1:] - packets.append((packet_type, packet_data)) - return packets - - -def _encode_engineIO_content(packets): - parts = [] - for packet_type, packet_data in packets: - packet_string = str(packet_type) + str(packet_data) - parts.append(_make_packet_header(packet_string) + packet_string) - return ''.join(parts) - - -def _read_packet_length(content, content_index): - while get_byte(content, content_index) != 0: - content_index += 1 - content_index += 1 - packet_length_string = '' - byte = get_byte(content, content_index) - while byte != 255: - packet_length_string += str(byte) - content_index += 1 - byte = get_byte(content, content_index) - return content_index, int(packet_length_string) - - -def _read_packet_string(content, content_index, packet_length): - while get_byte(content, content_index) == 255: - content_index += 1 - packet_string = content[content_index:content_index + packet_length] - return content_index + packet_length, packet_string - - -def _make_packet_header(packet_string): - length_string = str(len(packet_string)) - header_digits = [0] - for i in range(len(length_string)): - header_digits.append(ord(length_string[i]) - 48) - header_digits.append(255) - return ''.join(chr(x) for x in header_digits) - - -def _parse_socketIO_data(data): - data = get_unicode(data) - if data.startswith('/'): - try: - path, data = data.split(',', 1) - except ValueError: - path = data - data = '' - else: - path = '' - try: - ack_id = int(data[0]) - data = data[1:] - except (ValueError, IndexError): - ack_id = None - try: - args = json.loads(data) - except ValueError: - args = [] - return SocketIOData(path=path, ack_id=ack_id, args=args) - - -def _yield_warning_screen(seconds=None): - last_warning = None - for elapsed_time in _yield_elapsed_time(seconds): - try: - yield elapsed_time - except Exception as warning: - warning = str(warning) - if last_warning != warning: - last_warning = warning - _log.warn(warning) - time.sleep(RETRY_INTERVAL_IN_SECONDS) - - -def _yield_elapsed_time(seconds=None): - start_time = time.time() - if seconds is None: - while True: - yield time.time() - start_time - while time.time() - start_time < seconds: - yield time.time() - start_time + def _get_ack_callback(self, ack_id): + return self._callback_by_ack_id.pop(ack_id) diff --git a/socketIO_client/heartbeats.py b/socketIO_client/heartbeats.py new file mode 100644 index 0000000..f1de194 --- /dev/null +++ b/socketIO_client/heartbeats.py @@ -0,0 +1,47 @@ +from threading import Thread, Event + +from .exceptions import ConnectionError, TimeoutError + + +class HeartbeatThread(Thread): + + daemon = True + + def __init__( + self, send_heartbeat, + relax_interval_in_seconds, + hurry_interval_in_seconds): + super(HeartbeatThread, self).__init__() + self._send_heartbeat = send_heartbeat + self._relax_interval_in_seconds = relax_interval_in_seconds + self._hurry_interval_in_seconds = hurry_interval_in_seconds + self._adrenaline = Event() + self._rest = Event() + self._stop = Event() + + def run(self): + try: + while not self._stop.is_set(): + try: + self._send_heartbeat() + except TimeoutError: + pass + if self._adrenaline.is_set(): + interval_in_seconds = self._hurry_interval_in_seconds + else: + interval_in_seconds = self._relax_interval_in_seconds + self._rest.wait(interval_in_seconds) + except ConnectionError: + pass + + def relax(self): + self._adrenaline.clear() + + def hurry(self): + self._adrenaline.set() + self._rest.set() + self._rest.clear() + + def stop(self): + self._rest.set() + self._stop.set() diff --git a/socketIO_client/logs.py b/socketIO_client/logs.py new file mode 100644 index 0000000..ff37d19 --- /dev/null +++ b/socketIO_client/logs.py @@ -0,0 +1,38 @@ +import logging +import time + + +class LoggingMixin(object): + + def _log(self, level, msg, *attrs): + logging.log(level, '%s %s' % (self._log_name, msg), *attrs) + + def _debug(self, msg, *attrs): + self._log(logging.DEBUG, msg, *attrs) + + def _info(self, msg, *attrs): + self._log(logging.INFO, msg, *attrs) + + def _warn(self, msg, *attrs): + self._log(logging.WARNING, msg, *attrs) + + def _yield_warning_screen(self, seconds=None): + last_warning = None + for elapsed_time in _yield_elapsed_time(seconds): + try: + yield elapsed_time + except Exception as warning: + warning = str(warning) + if last_warning != warning: + last_warning = warning + self._warn(warning) + time.sleep(1) + + +def _yield_elapsed_time(seconds=None): + start_time = time.time() + if seconds is None: + while True: + yield time.time() - start_time + while time.time() - start_time < seconds: + yield time.time() - start_time diff --git a/socketIO_client/namespaces.py b/socketIO_client/namespaces.py new file mode 100644 index 0000000..db96007 --- /dev/null +++ b/socketIO_client/namespaces.py @@ -0,0 +1,198 @@ +from .logs import LoggingMixin + + +class EngineIONamespace(LoggingMixin): + 'Define engine.io client behavior' + + def __init__(self, io): + self._io = io + self._callback_by_event = {} + self._log_name = io._url + self.initialize() + + def initialize(self): + """Initialize custom variables here. + You can override this method.""" + + def on(self, event, callback): + 'Define a callback to handle an event emitted by the server' + self._callback_by_event[event] = callback + + def on_open(self): + """Called after engine.io connects. + You can override this method.""" + + def on_close(self): + """Called after engine.io disconnects. + You can override this method.""" + + def on_ping(self, data): + """Called after engine.io sends a ping packet. + You can override this method.""" + + def on_pong(self, data): + """Called after engine.io sends a pong packet. + You can override this method.""" + + def on_message(self, data): + """Called after engine.io sends a message packet. + You can override this method.""" + + def on_upgrade(self): + """Called after engine.io sends an upgrade packet. + You can override this method.""" + + def on_noop(self): + """Called after engine.io sends a noop packet. + You can override this method.""" + + def _find_packet_callback(self, event): + # Check callbacks defined by on() + try: + return self._callback_by_event[event] + except KeyError: + pass + # Check callbacks defined explicitly + return getattr(self, 'on_' + event) + + +class SocketIONamespace(EngineIONamespace): + 'Define socket.io client behavior' + + def __init__(self, io, path): + self.path = path + super(SocketIONamespace, self).__init__(io) + + def on_connect(self): + """Called after socket.io connects. + You can override this method.""" + + def on_reconnect(self): + """Called after socket.io reconnects. + You can override this method.""" + + def on_disconnect(self): + """Called after socket.io disconnects. + You can override this method.""" + + def on_event(self, event, *args): + """ + Called if there is no matching event handler. + You can override this method. + There are three ways to define an event handler: + + - Call socketIO.on() + + socketIO = SocketIO('localhost', 8000) + socketIO.on('my_event', my_function) + + - Call namespace.on() + + namespace = socketIO.get_namespace() + namespace.on('my_event', my_function) + + - Define namespace.on_xxx + + class Namespace(SocketIONamespace): + + def on_my_event(self, *args): + my_function(*args) + + socketIO.define(Namespace)""" + + def on_error(self, data): + """Called after socket.io sends an error packet. + You can override this method.""" + + def _find_packet_callback(self, event): + # Interpret events + if event == 'connect': + if not hasattr(self, '_was_connected'): + self._was_connected = True + else: + event = 'reconnect' + # Check callbacks defined by on() + try: + return self._callback_by_event[event] + except KeyError: + pass + # Check callbacks defined explicitly or use on_event() + return getattr( + self, 'on_' + event.replace(' ', '_'), + lambda *args: self.on_event(event, *args)) + + +class LoggingEngineIONamespace(EngineIONamespace): + + def on_open(self): + self._debug('[open]') + super(LoggingEngineIONamespace, self).on_open() + + def on_close(self): + self._debug('[close]') + super(LoggingEngineIONamespace, self).on_close() + + def on_ping(self, data): + self._debug('[ping] %s' % data) + super(LoggingEngineIONamespace, self).on_ping(data) + + def on_pong(self, data): + self._debug('[pong] %s' % data) + super(LoggingEngineIONamespace, self).on_pong(data) + + def on_message(self, data): + self._debug('[message] %s' % data) + super(LoggingEngineIONamespace, self).on_message(data) + + def on_upgrade(self): + self._debug('[upgrade]') + super(LoggingEngineIONamespace, self).on_upgrade() + + def on_noop(self): + self._debug('[noop]') + super(LoggingEngineIONamespace, self).on_noop() + + def on_event(self, event, *args): + callback, args = find_callback(args) + arguments = [repr(_) for _ in args] + if callback: + arguments.append('callback(*args)') + self._info('[event] %s(%s)', event, ', '.join(arguments)) + super(LoggingEngineIONamespace, self).on_event(event, *args) + + +class LoggingSocketIONamespace(SocketIONamespace): + + def on_connect(self): + self._debug('%s [connect]' % self.path) + super(LoggingSocketIONamespace, self).on_connect() + + def on_reconnect(self): + self._debug('%s [reconnect]' % self.path) + super(LoggingSocketIONamespace, self).on_reconnect() + + def on_disconnect(self): + self._debug('%s [disconnect]' % self.path) + super(LoggingSocketIONamespace, self).on_disconnect() + + def on_event(self, event, *args): + callback, args = find_callback(args) + arguments = [repr(_) for _ in args] + if callback: + arguments.append('callback(*args)') + self._info('%s [event] %s(%s)', self.path, event, ', '.join(arguments)) + super(LoggingSocketIONamespace, self).on_event(event, *args) + + def on_error(self, data): + self._debug('%s [error] %s' % (self.path, data)) + super(LoggingSocketIONamespace, self).on_error() + + +def find_callback(args, kw=None): + 'Return callback whether passed as a last argument or as a keyword' + if args and callable(args[-1]): + return args[-1], args[:-1] + try: + return kw['callback'], args + except (KeyError, TypeError): + return None, args diff --git a/socketIO_client/parsers.py b/socketIO_client/parsers.py new file mode 100644 index 0000000..7a5a9d7 --- /dev/null +++ b/socketIO_client/parsers.py @@ -0,0 +1,93 @@ +import json +from collections import namedtuple + +from .symmetries import encode_string, get_byte, get_character, parse_url + + +SocketIOData = namedtuple('SocketIOData', ['path', 'ack_id', 'args']) + + +def parse_host(host, port, resource): + if not host.startswith('http'): + host = 'http://' + host + url_pack = parse_url(host) + is_secure = url_pack.scheme == 'https' + port = port or url_pack.port or (443 if is_secure else 80) + url = '%s:%d%s/%s' % (url_pack.hostname, port, url_pack.path, resource) + return is_secure, url + + +def encode_engineIO_content(engineIO_packets): + parts = [] + for packet_type, packet_data in engineIO_packets: + packet_string = str(packet_type) + encode_string(packet_data) + parts.append(_make_packet_header(packet_string) + packet_string) + return ''.join(parts) + + +def decode_engineIO_content(content): + content_index = 0 + content_length = len(content) + while content_index < content_length: + try: + content_index, packet_length = _read_packet_length( + content, content_index) + except IndexError: + break + content_index, packet_string = _read_packet_string( + content, content_index, packet_length) + engineIO_packet_type = int(get_character(packet_string, 0)) + engineIO_packet_data = packet_string[1:] + yield engineIO_packet_type, engineIO_packet_data + + +def parse_socketIO_data(data): + data = encode_string(data) + if data.startswith('/'): + try: + path, data = data.split(',', 1) + except ValueError: + path = data + data = '' + else: + path = '' + try: + ack_id_string, data = data.split('[', 1) + data = '[' + data + ack_id = int(ack_id_string) + except (ValueError, IndexError): + ack_id = None + try: + args = json.loads(data) + except ValueError: + args = [] + return SocketIOData(path=path, ack_id=ack_id, args=args) + + +def _make_packet_header(packet_string): + length_string = str(len(packet_string)) + header_digits = [0] + for i in range(len(length_string)): + header_digits.append(ord(length_string[i]) - 48) + header_digits.append(255) + return ''.join(chr(x) for x in header_digits) + + +def _read_packet_length(content, content_index): + while get_byte(content, content_index) != 0: + content_index += 1 + content_index += 1 + packet_length_string = '' + byte = get_byte(content, content_index) + while byte != 255: + packet_length_string += str(byte) + content_index += 1 + byte = get_byte(content, content_index) + return content_index, int(packet_length_string) + + +def _read_packet_string(content, content_index, packet_length): + while get_byte(content, content_index) == 255: + content_index += 1 + packet_string = content[content_index:content_index + packet_length] + return content_index + packet_length, packet_string diff --git a/socketIO_client/compat.py b/socketIO_client/symmetries.py similarity index 79% rename from socketIO_client/compat.py rename to socketIO_client/symmetries.py index 012993c..b061a20 100644 --- a/socketIO_client/compat.py +++ b/socketIO_client/symmetries.py @@ -13,5 +13,9 @@ def get_character(x, index): return chr(six.indexbytes(x, index)) -def get_unicode(x): +def encode_string(x): + return x.encode('utf-8') + + +def decode_string(x): return x.decode('utf-8') diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index fe641a7..f917b8a 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -1,53 +1,81 @@ import requests +import time + from .exceptions import ConnectionError, TimeoutError +from .parsers import decode_engineIO_content, encode_engineIO_content ENGINEIO_PROTOCOL = 3 +TRANSPORTS = 'websocket', 'xhr-polling' class AbstractTransport(object): - pass + + def __init__(self, http_session, is_secure, url, engineIO_session=None): + self.http_session = http_session + self.is_secure = is_secure + self.url = url + self.engineIO_session = engineIO_session + + def send_packet(self, engineIO_packet_type, engineIO_packet_data): + pass + + def recv_packet(self): + pass class XHR_PollingTransport(AbstractTransport): - # pass http session - # pass session id (can be none) - # pass timeout + def __init__(self, http_session, is_secure, url, engineIO_session=None): + super(XHR_PollingTransport, self).__init__( + http_session, is_secure, url, engineIO_session) + self.http_url = '%s://%s/' % ('https' if is_secure else 'http', url) + self.params = { + 'EIO': ENGINEIO_PROTOCOL, + 'transport': 'polling', + } + if engineIO_session: + self.request_index = 1 + self.kw_get = dict(timeout=engineIO_session.ping_timeout) + self.kw_post = dict(headers={ + 'content-type': 'application/octet-stream', + }) + self.params['sid'] = engineIO_session.id + else: + self.request_index = 0 + self.kw_get = {} + self.kw_post = {} + + def recv_packet(self): + params = dict(self.params) + params['t'] = self.get_timestamp() + response = get_response( + self.http_session.get, + self.http_url, + params=params, + **self.kw_get) + return decode_engineIO_content(response.content) def send_packet(self, engineIO_packet_type, engineIO_packet_data): - _get_response() - - assert ok - - response = self._http_session.post(self._url, params={ - 'EIO': self._engineIO_protocol, - 'transport': 'polling', - 't': self._get_timestamp(), - 'sid': self._session_id, - }, data=_encode_engineIO_content([ - (engineIO_packet_type, engineIO_packet_data), - ]), headers={ - 'content-type': 'application/octet-stream', - }) + params = dict(self.params) + params['t'] = self.get_timestamp() + response = get_response( + self.http_session.post, + self.http_url, + params=params, + data=encode_engineIO_content([ + (engineIO_packet_type, engineIO_packet_data), + ]), + **self.kw_post) assert response.content == 'ok' - def _recv_packet(self): - response = _get_response( - self._http_session.get, - self._url, - params={ - 'EIO': self._engineIO_protocol, - 'transport': 'polling', - 't': self._get_timestamp(), - 'sid': self._session_id, - }, - timeout=self._ping_timeout) - for engineIO_packet in _decode_engineIO_content(response.content): - yield engineIO_packet + def get_timestamp(self): + timestamp = '%s-%s' % (int(time.time() * 1000), self.request_index) + self.request_index += 1 + return timestamp -def _get_response(request, *args, **kw): +def get_response(request, *args, **kw): try: response = request(*args, **kw) except requests.exceptions.Timeout as e: @@ -60,3 +88,16 @@ def _get_response(request, *args, **kw): if 200 != status_code: raise ConnectionError('unexpected status code (%s)' % status_code) return response + + +def prepare_http_session(kw): + http_session = requests.Session() + http_session.headers.update(kw.get('headers', {})) + http_session.auth = kw.get('auth') + http_session.proxies.update(kw.get('proxies', {})) + http_session.hooks.update(kw.get('hooks', {})) + http_session.params.update(kw.get('params', {})) + http_session.verify = kw.get('verify') + http_session.cert = kw.get('cert') + http_session.cookies.update(kw.get('cookies', {})) + return http_session From ebd18b70175b5c0da4db687dbd5abfa1361c2b47 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 22 Feb 2015 14:17:24 -0500 Subject: [PATCH 149/191] Prepare to check against master branch --- socketIO_client/__init__.py | 72 +++++++++++++++++------------------ socketIO_client/namespaces.py | 16 ++++++++ socketIO_client/parsers.py | 25 +++++++++++- 3 files changed, 75 insertions(+), 38 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 2d1cc66..8ece708 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,11 +1,11 @@ -import json - from .exceptions import ConnectionError, TimeoutError, PacketError from .heartbeats import HeartbeatThread from .logs import LoggingMixin from .namespaces import EngineIONamespace, SocketIONamespace, find_callback -from .parsers import parse_host, parse_socketIO_data -from .symmetries import encode_string, get_character +from .parsers import ( + parse_host, parse_engineIO_session, + parse_socketIO_data, format_socketIO_data) +from .symmetries import get_character from .transports import XHR_PollingTransport, prepare_http_session, TRANSPORTS @@ -46,7 +46,7 @@ class EngineIO(LoggingMixin): return self.__transport except AttributeError: pass - self._get_engineIO_session() + self._engineIO_session = self._get_engineIO_session() self._negotiate_transport() self._reset_heartbeat() self._connect_namespaces() @@ -60,18 +60,13 @@ class EngineIO(LoggingMixin): try: engineIO_packet_type, engineIO_packet_data = next( transport.recv_packet()) - except (TimeoutError, ConnectionError) as e: if not self._wait_for_connection: raise warning = Exception('[waiting for connection] %s' % e) warning_screen.throw(warning) assert engineIO_packet_type == 0 - value_by_name = json.loads(encode_string(engineIO_packet_data)) - self._session_id = value_by_name['sid'] - self._ping_interval = value_by_name['pingInterval'] / float(1000) - self._ping_timeout = value_by_name['pingTimeout'] / float(1000) - self._transport_upgrades = value_by_name['upgrades'] + return parse_engineIO_session(engineIO_packet_data) def _negotiate_transport(self): self.__transport = self._get_transport('xhr-polling') @@ -83,7 +78,7 @@ class EngineIO(LoggingMixin): pass self._heartbeat_thread = HeartbeatThread( send_heartbeat=self.__transport._ping, - relax_interval_in_seconds=self._ping_interval, + relax_interval_in_seconds=self._engineIO_session.ping_interval, hurry_interval_in_seconds=1) self._heartbeat_thread.start() @@ -129,6 +124,9 @@ class EngineIO(LoggingMixin): # Act + def send(self, engineIO_packet_data): + self._message(engineIO_packet_data) + def _open(self): engineIO_packet_type = 0 self._transport.send_packet(engineIO_packet_type, '') @@ -188,7 +186,6 @@ class EngineIO(LoggingMixin): self._heartbeat_thread.relax() def _should_stop_waiting(self): - # Use __transport to make sure that we do not reconnect inadvertently return self._wants_to_close def _process_packets(self): @@ -278,7 +275,7 @@ class SocketIO(EngineIO): for path, namespace in self._namespace_by_path.items(): namespace._transport = self.__transport if path: - self.__transport.connect(path) + self.connect(path) def __exit__(self, *exception_pack): self.disconnect() @@ -311,34 +308,29 @@ class SocketIO(EngineIO): # Act - def connect(): - pass + def connect(self, path): + socketIO_packet_type = 0 + socketIO_packet_data = format_socketIO_data(path) + self._message(str(socketIO_packet_type) + socketIO_packet_data) - def disconnect(): - pass + def disconnect(self, path=''): + socketIO_packet_type = 1 + socketIO_packet_data = format_socketIO_data(path) + self._message(str(socketIO_packet_type) + socketIO_packet_data) + try: + namespace = self._namespace_by_path.pop(path) + namespace.on_disconnect() + except KeyError: + pass def emit(self, event, *args, **kw): path = kw.get('path', '') callback, args = find_callback(args, kw) - self._emit(path, event, args, callback) - - def _emit(self, path, event, args, callback): + ack_id = self._set_ack_callback(callback) if callback else None socketIO_packet_type = 2 - - socketIO_packet_data = json.dumps([event] + list(args)) - if callback: - ack_id = self._set_ack_callback(callback) if callback else '' - socketIO_packet_data = str(ack_id) + socketIO_packet_data - if path: - socketIO_packet_data = path + ',' + socketIO_packet_data - + socketIO_packet_data = format_socketIO_data(path, ack_id, args) self._message(str(socketIO_packet_type) + socketIO_packet_data) - def _set_ack_callback(self, callback): - self._ack_id += 1 - self._callback_by_ack_id[self._ack_id] = callback - return self._ack_id - def send(self, data='', callback=None): args = [data] if callback: @@ -354,8 +346,7 @@ class SocketIO(EngineIO): self.wait(seconds, for_callbacks=True) def _should_stop_waiting(self, for_callbacks): - # Use __transport to make sure that we do not reconnect inadvertently - if for_callbacks and not self.__transport.has_ack_callback: + if for_callbacks and not self._has_ack_callback: return True return super(SocketIO, self)._should_stop_waiting() @@ -423,5 +414,14 @@ class SocketIO(EngineIO): 'Return function that acknowledges the server' return lambda *args: self._ack(path, ack_id, *args) + def _set_ack_callback(self, callback): + self._ack_id += 1 + self._callback_by_ack_id[self._ack_id] = callback + return self._ack_id + def _get_ack_callback(self, ack_id): return self._callback_by_ack_id.pop(ack_id) + + @property + def _has_ack_callback(self): + return True if self._callback_by_ack_id else False diff --git a/socketIO_client/namespaces.py b/socketIO_client/namespaces.py index db96007..c622e65 100644 --- a/socketIO_client/namespaces.py +++ b/socketIO_client/namespaces.py @@ -18,6 +18,10 @@ class EngineIONamespace(LoggingMixin): 'Define a callback to handle an event emitted by the server' self._callback_by_event[event] = callback + def send(self, data): + 'Send a message' + self._io.send(data) + def on_open(self): """Called after engine.io connects. You can override this method.""" @@ -63,6 +67,18 @@ class SocketIONamespace(EngineIONamespace): self.path = path super(SocketIONamespace, self).__init__(io) + def connect(self): + self._io.connect(self.path) + + def disconnect(self): + self._io.disconnect(self.path) + + def emit(self, event, *args, **kw): + self._io.emit(event, *args, **kw) + + def send(self, data='', callback=None): + self._io.send(data, callback) + def on_connect(self): """Called after socket.io connects. You can override this method.""" diff --git a/socketIO_client/parsers.py b/socketIO_client/parsers.py index 7a5a9d7..e748164 100644 --- a/socketIO_client/parsers.py +++ b/socketIO_client/parsers.py @@ -1,9 +1,12 @@ import json from collections import namedtuple -from .symmetries import encode_string, get_byte, get_character, parse_url +from .symmetries import ( + decode_string, encode_string, get_byte, get_character, parse_url) +EngineIOSession = namedtuple('EngineIOSession', [ + 'id', 'ping_interval', 'ping_timeout', 'transport_upgrades']) SocketIOData = namedtuple('SocketIOData', ['path', 'ack_id', 'args']) @@ -17,6 +20,15 @@ def parse_host(host, port, resource): return is_secure, url +def parse_engineIO_session(engineIO_packet_data): + d = json.loads(decode_string(engineIO_packet_data)) + return EngineIOSession( + id=d['sid'], + ping_interval=d['pingInterval'] / float(1000), + ping_timeout=d['pingTimeout'] / float(1000), + transport_upgrades=d['upgrades']) + + def encode_engineIO_content(engineIO_packets): parts = [] for packet_type, packet_data in engineIO_packets: @@ -42,7 +54,7 @@ def decode_engineIO_content(content): def parse_socketIO_data(data): - data = encode_string(data) + data = decode_string(data) if data.startswith('/'): try: path, data = data.split(',', 1) @@ -64,6 +76,15 @@ def parse_socketIO_data(data): return SocketIOData(path=path, ack_id=ack_id, args=args) +def format_socketIO_data(path=None, ack_id=None, args=None): + socketIO_packet_data = json.dumps(args) if args else '' + if ack_id is not None: + socketIO_packet_data = str(ack_id) + socketIO_packet_data + if path: + socketIO_packet_data = path + ',' + socketIO_packet_data + return socketIO_packet_data + + def _make_packet_header(packet_string): length_string = str(len(packet_string)) header_digits = [0] From 9fe0c059262322af1d16f91c5cf008cdce364fef Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 22 Feb 2015 15:39:43 -0500 Subject: [PATCH 150/191] Restore tests --- TODO.goals | 9 ++--- socketIO_client/__init__.py | 16 ++++++-- socketIO_client/parsers.py | 2 +- socketIO_client/tests/__init__.py | 65 +++++++++++++++++++++++++++---- socketIO_client/transports.py | 4 +- 5 files changed, 77 insertions(+), 19 deletions(-) diff --git a/TODO.goals b/TODO.goals index 619a558..1c863a8 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,9 +1,8 @@ Release 0.6.1 #41 #52 - Send pong - Change print statements into logging statements - Clean up - - Put tests in index.html Update tests Merge sarietta's pull request +Add Websocket transport +Consider logging packets sent and received Implement rooms #65 +Implement binary event +Implement binary ack diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 8ece708..eb84e60 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -314,9 +314,14 @@ class SocketIO(EngineIO): self._message(str(socketIO_packet_type) + socketIO_packet_data) def disconnect(self, path=''): - socketIO_packet_type = 1 - socketIO_packet_data = format_socketIO_data(path) - self._message(str(socketIO_packet_type) + socketIO_packet_data) + if not self.connected: + return + if path: + socketIO_packet_type = 1 + socketIO_packet_data = format_socketIO_data(path) + self._message(str(socketIO_packet_type) + socketIO_packet_data) + else: + self._close() try: namespace = self._namespace_by_path.pop(path) namespace.on_disconnect() @@ -337,6 +342,11 @@ class SocketIO(EngineIO): args.append(callback) self.emit('message', *args) + def _ack(self, path, ack_id, *args): + socketIO_packet_type = 3 + socketIO_packet_data = format_socketIO_data(path, ack_id, args) + self._message(str(socketIO_packet_type) + socketIO_packet_data) + # React def wait(self, seconds=None, for_callbacks=False): diff --git a/socketIO_client/parsers.py b/socketIO_client/parsers.py index e748164..a095d8a 100644 --- a/socketIO_client/parsers.py +++ b/socketIO_client/parsers.py @@ -77,7 +77,7 @@ def parse_socketIO_data(data): def format_socketIO_data(path=None, ack_id=None, args=None): - socketIO_packet_data = json.dumps(args) if args else '' + socketIO_packet_data = json.dumps(args, ensure_ascii=False) if args else '' if ack_id is not None: socketIO_packet_data = str(ack_id) + socketIO_packet_data if path: diff --git a/socketIO_client/tests/__init__.py b/socketIO_client/tests/__init__.py index 75be733..a5ab078 100644 --- a/socketIO_client/tests/__init__.py +++ b/socketIO_client/tests/__init__.py @@ -1,4 +1,5 @@ import logging +import time from unittest import TestCase from .. import SocketIO, LoggingSocketIONamespace, find_callback @@ -19,6 +20,20 @@ class BaseMixin(object): def tearDown(self): del self.socketIO + def test_disconnect(self): + 'Disconnect' + self.socketIO.define(LoggingSocketIONamespace) + self.assertTrue(self.socketIO.connected) + self.socketIO.disconnect() + self.assertFalse(self.socketIO.connected) + # Use context manager + with SocketIO(HOST, PORT, Namespace) as self.socketIO: + namespace = self.socketIO.get_namespace() + self.assertFalse(namespace.called_on_disconnect) + self.assertTrue(self.socketIO.connected) + self.assertTrue(namespace.called_on_disconnect) + self.assertFalse(self.socketIO.connected) + def test_emit(self): 'Emit' namespace = self.socketIO.define(Namespace) @@ -90,6 +105,48 @@ class BaseMixin(object): self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(namespace.response, DATA) + def test_ack(self): + 'Respond to a server callback request' + namespace = self.socketIO.define(Namespace) + self.socketIO.emit('trigger_server_expects_callback', PAYLOAD) + self.socketIO.wait(self.wait_time_in_seconds) + self.assertEqual(namespace.args_by_event, { + 'server_expects_callback': (PAYLOAD,), + 'server_received_callback': (PAYLOAD,), + }) + + def test_wait_with_disconnect(self): + 'Exit loop when the client wants to disconnect' + self.socketIO.define(Namespace) + self.socketIO.emit('wait_with_disconnect') + timeout_in_seconds = 5 + start_time = time.time() + self.socketIO.wait(timeout_in_seconds) + self.assertTrue(time.time() - start_time < timeout_in_seconds) + + def test_namespace_emit(self): + 'Behave differently in different namespaces' + main_namespace = self.socketIO.define(Namespace) + chat_namespace = self.socketIO.define(Namespace, '/chat') + news_namespace = self.socketIO.define(Namespace, '/news') + news_namespace.emit('emit_with_payload', PAYLOAD) + self.socketIO.wait(self.wait_time_in_seconds) + self.assertEqual(main_namespace.args_by_event, {}) + self.assertEqual(chat_namespace.args_by_event, {}) + self.assertEqual(news_namespace.args_by_event, { + 'emit_with_payload_response': (PAYLOAD,), + }) + + def test_namespace_ack(self): + 'Trigger server callback' + chat_namespace = self.socketIO.define(Namespace, '/chat') + chat_namespace.emit('trigger_server_expects_callback', PAYLOAD) + self.socketIO.wait(self.wait_time_in_seconds) + self.assertEqual(chat_namespace.args_by_event, { + 'server_expects_callback': (PAYLOAD,), + 'server_received_callback': (PAYLOAD,), + }) + def on_response(self, *args): for arg in args: if isinstance(arg, dict): @@ -99,14 +156,6 @@ class BaseMixin(object): self.called_on_response = True -# class Test_WebsocketTransport(TestCase, BaseMixin): - - # def setUp(self): - # super(Test_WebsocketTransport, self).setUp() - # self.socketIO = SocketIO(HOST, PORT, transports=['websocket']) - # self.wait_time_in_seconds = 0.1 - - class Test_XHR_PollingTransport(TestCase, BaseMixin): def setUp(self): diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index f917b8a..12a97b8 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -17,10 +17,10 @@ class AbstractTransport(object): self.url = url self.engineIO_session = engineIO_session - def send_packet(self, engineIO_packet_type, engineIO_packet_data): + def recv_packet(self): pass - def recv_packet(self): + def send_packet(self, engineIO_packet_type, engineIO_packet_data): pass From dec8e09327c900b4bbf5475b051f29c0d491c046 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 22 Feb 2015 17:30:35 -0500 Subject: [PATCH 151/191] Prepare to run tests --- .travis.yml | 2 +- README.rst | 2 +- socketIO_client/__init__.py | 33 +++++++++++++++++++-------------- socketIO_client/parsers.py | 31 ++++++++++++++++++++++--------- 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5fb9d1f..b0bc004 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ before_install: - sudo apt-get update - sudo apt-get install nodejs install: - - npm install -G socket.io@1.3.1 + - npm install -G socket.io - npm install -G http-proxy - pip install -U requests - pip install -U six diff --git a/README.rst b/README.rst index 48313f1..582cf58 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ Activate isolated environment. :: VIRTUAL_ENV=$HOME/.virtualenv source $VIRTUAL_ENV/bin/activate -Launch your server. :: +Launch your socket.io server. :: # Get package folder PACKAGE_FOLDER=`python -c "import os, socketIO_client; print(os.path.dirname(socketIO_client.__file__))"` diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index eb84e60..e791b51 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -4,7 +4,8 @@ from .logs import LoggingMixin from .namespaces import EngineIONamespace, SocketIONamespace, find_callback from .parsers import ( parse_host, parse_engineIO_session, - parse_socketIO_data, format_socketIO_data) + format_socketIO_packet_data, parse_socketIO_packet_data, + get_namespace_path) from .symmetries import get_character from .transports import XHR_PollingTransport, prepare_http_session, TRANSPORTS @@ -69,17 +70,22 @@ class EngineIO(LoggingMixin): return parse_engineIO_session(engineIO_packet_data) def _negotiate_transport(self): - self.__transport = self._get_transport('xhr-polling') + transport_name = 'xhr-polling' + self.__transport = self._get_transport(transport_name) + self._transport_name = transport_name def _reset_heartbeat(self): try: self._heartbeat_thread.stop() except AttributeError: pass + ping_interval = self._engineIO_session.ping_interval self._heartbeat_thread = HeartbeatThread( - send_heartbeat=self.__transport._ping, - relax_interval_in_seconds=self._engineIO_session.ping_interval, - hurry_interval_in_seconds=1) + send_heartbeat=self._ping, + relax_interval_in_seconds=ping_interval, + hurry_interval_in_seconds=1 if self._transport_name in [ + 'xhr-polling', + ] else ping_interval) self._heartbeat_thread.start() def _connect_namespaces(self): @@ -197,7 +203,6 @@ class EngineIO(LoggingMixin): def _process_packet(self, packet): engineIO_packet_type, engineIO_packet_data = packet - print('engineIO_packet_type = %s' % engineIO_packet_type) # Launch callbacks namespace = self.get_namespace() try: @@ -310,7 +315,7 @@ class SocketIO(EngineIO): def connect(self, path): socketIO_packet_type = 0 - socketIO_packet_data = format_socketIO_data(path) + socketIO_packet_data = format_socketIO_packet_data(path) self._message(str(socketIO_packet_type) + socketIO_packet_data) def disconnect(self, path=''): @@ -318,7 +323,7 @@ class SocketIO(EngineIO): return if path: socketIO_packet_type = 1 - socketIO_packet_data = format_socketIO_data(path) + socketIO_packet_data = format_socketIO_packet_data(path) self._message(str(socketIO_packet_type) + socketIO_packet_data) else: self._close() @@ -333,7 +338,7 @@ class SocketIO(EngineIO): callback, args = find_callback(args, kw) ack_id = self._set_ack_callback(callback) if callback else None socketIO_packet_type = 2 - socketIO_packet_data = format_socketIO_data(path, ack_id, args) + socketIO_packet_data = format_socketIO_packet_data(path, ack_id, args) self._message(str(socketIO_packet_type) + socketIO_packet_data) def send(self, data='', callback=None): @@ -344,7 +349,7 @@ class SocketIO(EngineIO): def _ack(self, path, ack_id, *args): socketIO_packet_type = 3 - socketIO_packet_data = format_socketIO_data(path, ack_id, args) + socketIO_packet_data = format_socketIO_packet_data(path, ack_id, args) self._message(str(socketIO_packet_type) + socketIO_packet_data) # React @@ -366,9 +371,9 @@ class SocketIO(EngineIO): return socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) socketIO_packet_data = engineIO_packet_data[1:] - print('socketIO_packet_type = %s' % socketIO_packet_type) # Launch callbacks - namespace = self.get_namespace() + path = get_namespace_path(socketIO_packet_data) + namespace = self.get_namespace(path) try: delegate = { 0: self._on_connect, @@ -392,7 +397,7 @@ class SocketIO(EngineIO): find_packet_callback('disconnect')() def _on_event(self, data, find_packet_callback): - data_parsed = parse_socketIO_data(data) + data_parsed = parse_socketIO_packet_data(data) args = data_parsed.args try: event = args.pop(0) @@ -404,7 +409,7 @@ class SocketIO(EngineIO): find_packet_callback(event)(*args) def _on_ack(self, data, find_packet_callback): - data_parsed = parse_socketIO_data(data) + data_parsed = parse_socketIO_packet_data(data) try: ack_callback = self._get_ack_callback(data_parsed.ack_id) except KeyError: diff --git a/socketIO_client/parsers.py b/socketIO_client/parsers.py index a095d8a..ba1a4e6 100644 --- a/socketIO_client/parsers.py +++ b/socketIO_client/parsers.py @@ -53,8 +53,17 @@ def decode_engineIO_content(content): yield engineIO_packet_type, engineIO_packet_data -def parse_socketIO_data(data): - data = decode_string(data) +def format_socketIO_packet_data(path=None, ack_id=None, args=None): + socketIO_packet_data = json.dumps(args, ensure_ascii=False) if args else '' + if ack_id is not None: + socketIO_packet_data = str(ack_id) + socketIO_packet_data + if path: + socketIO_packet_data = path + ',' + socketIO_packet_data + return socketIO_packet_data + + +def parse_socketIO_packet_data(socketIO_packet_data): + data = decode_string(socketIO_packet_data) if data.startswith('/'): try: path, data = data.split(',', 1) @@ -76,13 +85,17 @@ def parse_socketIO_data(data): return SocketIOData(path=path, ack_id=ack_id, args=args) -def format_socketIO_data(path=None, ack_id=None, args=None): - socketIO_packet_data = json.dumps(args, ensure_ascii=False) if args else '' - if ack_id is not None: - socketIO_packet_data = str(ack_id) + socketIO_packet_data - if path: - socketIO_packet_data = path + ',' + socketIO_packet_data - return socketIO_packet_data +def get_namespace_path(socketIO_packet_data): + if '/' != get_character(socketIO_packet_data, 0): + return '' + # Loop incrementally in case there is binary data + parts = [] + for i in range(len(socketIO_packet_data)): + character = get_character(socketIO_packet_data, i) + if ',' == character: + break + parts.append(character) + return ''.join(parts) def _make_packet_header(packet_string): From 5806f23492918f8d04fe52dfb4486f3893942172 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 22 Feb 2015 18:59:05 -0500 Subject: [PATCH 152/191] Fail unit tests without errors --- socketIO_client/__init__.py | 25 +++++++++++++++---------- socketIO_client/parsers.py | 2 +- socketIO_client/tests/__init__.py | 16 +++++++++------- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index e791b51..e472083 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,7 +1,9 @@ from .exceptions import ConnectionError, TimeoutError, PacketError from .heartbeats import HeartbeatThread from .logs import LoggingMixin -from .namespaces import EngineIONamespace, SocketIONamespace, find_callback +from .namespaces import ( + EngineIONamespace, SocketIONamespace, LoggingSocketIONamespace, + find_callback) from .parsers import ( parse_host, parse_engineIO_session, format_socketIO_packet_data, parse_socketIO_packet_data, @@ -12,6 +14,8 @@ from .transports import XHR_PollingTransport, prepare_http_session, TRANSPORTS __all__ = 'SocketIO', 'SocketIONamespace' __version__ = '0.6.1' +BaseNamespace = SocketIONamespace +LoggingNamespace = LoggingSocketIONamespace class EngineIO(LoggingMixin): @@ -34,24 +38,23 @@ class EngineIO(LoggingMixin): @property def connected(self): try: - transport = self.__transport + return self._connected except AttributeError: return False - else: - return transport.connected @property def _transport(self): try: if self.connected: - return self.__transport + return self._transport_instance except AttributeError: pass self._engineIO_session = self._get_engineIO_session() self._negotiate_transport() - self._reset_heartbeat() self._connect_namespaces() - return self.__transport + self._connected = True + self._reset_heartbeat() + return self._transport_instance def _get_engineIO_session(self): warning_screen = self._yield_warning_screen() @@ -61,6 +64,7 @@ class EngineIO(LoggingMixin): try: engineIO_packet_type, engineIO_packet_data = next( transport.recv_packet()) + break except (TimeoutError, ConnectionError) as e: if not self._wait_for_connection: raise @@ -71,7 +75,7 @@ class EngineIO(LoggingMixin): def _negotiate_transport(self): transport_name = 'xhr-polling' - self.__transport = self._get_transport(transport_name) + self._transport_instance = self._get_transport(transport_name) self._transport_name = transport_name def _reset_heartbeat(self): @@ -143,6 +147,7 @@ class EngineIO(LoggingMixin): return engineIO_packet_type = 1 self._transport.send_packet(engineIO_packet_type, '') + self._connected = False def _ping(self, engineIO_packet_data=''): engineIO_packet_type = 2 @@ -278,7 +283,7 @@ class SocketIO(EngineIO): def _connect_namespaces(self): for path, namespace in self._namespace_by_path.items(): - namespace._transport = self.__transport + namespace._transport = self._transport_instance if path: self.connect(path) @@ -294,7 +299,7 @@ class SocketIO(EngineIO): def define(self, Namespace, path=''): if path: - self._connect(path) + self.connect(path) self._namespace_by_path[path] = namespace = Namespace(self, path) return namespace diff --git a/socketIO_client/parsers.py b/socketIO_client/parsers.py index ba1a4e6..1ee9f2f 100644 --- a/socketIO_client/parsers.py +++ b/socketIO_client/parsers.py @@ -86,7 +86,7 @@ def parse_socketIO_packet_data(socketIO_packet_data): def get_namespace_path(socketIO_packet_data): - if '/' != get_character(socketIO_packet_data, 0): + if not socketIO_packet_data.startswith(b'/'): return '' # Loop incrementally in case there is binary data parts = [] diff --git a/socketIO_client/tests/__init__.py b/socketIO_client/tests/__init__.py index a5ab078..d0e7e8f 100644 --- a/socketIO_client/tests/__init__.py +++ b/socketIO_client/tests/__init__.py @@ -2,7 +2,7 @@ import logging import time from unittest import TestCase -from .. import SocketIO, LoggingSocketIONamespace, find_callback +from .. import SocketIO, LoggingNamespace, find_callback HOST = 'localhost' @@ -15,14 +15,16 @@ logging.basicConfig(level=logging.DEBUG) class BaseMixin(object): def setUp(self): + super(BaseMixin, self).setUp() self.called_on_response = False def tearDown(self): + super(BaseMixin, self).tearDown() del self.socketIO def test_disconnect(self): 'Disconnect' - self.socketIO.define(LoggingSocketIONamespace) + self.socketIO.define(LoggingNamespace) self.assertTrue(self.socketIO.connected) self.socketIO.disconnect() self.assertFalse(self.socketIO.connected) @@ -63,14 +65,14 @@ class BaseMixin(object): def test_emit_with_callback(self): 'Emit with callback' - self.socketIO.define(LoggingSocketIONamespace) + self.socketIO.define(LoggingNamespace) self.socketIO.emit('emit_with_callback', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit_with_callback_with_payload(self): 'Emit with callback with payload' - self.socketIO.define(LoggingSocketIONamespace) + self.socketIO.define(LoggingNamespace) self.socketIO.emit( 'emit_with_callback_with_payload', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) @@ -78,7 +80,7 @@ class BaseMixin(object): def test_emit_with_callback_with_multiple_payloads(self): 'Emit with callback with multiple payloads' - self.socketIO.define(LoggingSocketIONamespace) + self.socketIO.define(LoggingNamespace) self.socketIO.emit( 'emit_with_callback_with_multiple_payloads', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) @@ -156,7 +158,7 @@ class BaseMixin(object): self.called_on_response = True -class Test_XHR_PollingTransport(TestCase, BaseMixin): +class Test_XHR_PollingTransport(BaseMixin, TestCase): def setUp(self): super(Test_XHR_PollingTransport, self).setUp() @@ -164,7 +166,7 @@ class Test_XHR_PollingTransport(TestCase, BaseMixin): self.wait_time_in_seconds = 1 -class Namespace(LoggingSocketIONamespace): +class Namespace(LoggingNamespace): def initialize(self): self.called_on_disconnect = False From 4fa6dcbe8db2b58d0b3bf0f82ea7a209a46971aa Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 22 Feb 2015 19:55:31 -0500 Subject: [PATCH 153/191] Debug missed emit --- socketIO_client/__init__.py | 21 +++++++++++++++++---- socketIO_client/tests/__init__.py | 7 +++++++ socketIO_client/transports.py | 19 ++++++++++++++----- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index e472083..1a97d89 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -50,9 +50,11 @@ class EngineIO(LoggingMixin): except AttributeError: pass self._engineIO_session = self._get_engineIO_session() - self._negotiate_transport() + self._transport_instance = self._negotiate_transport() self._connect_namespaces() self._connected = True + for engineIO_packet in self._transport_instance.recv_packet(): + self._transport_instance.queue_packet(engineIO_packet) self._reset_heartbeat() return self._transport_instance @@ -74,9 +76,8 @@ class EngineIO(LoggingMixin): return parse_engineIO_session(engineIO_packet_data) def _negotiate_transport(self): - transport_name = 'xhr-polling' - self._transport_instance = self._get_transport(transport_name) - self._transport_name = transport_name + self._transport_name = 'xhr-polling' + return self._get_transport(self._transport_name) def _reset_heartbeat(self): try: @@ -158,6 +159,7 @@ class EngineIO(LoggingMixin): self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) def _message(self, engineIO_packet_data): + print('_message %s' % str(engineIO_packet_data)) engineIO_packet_type = 4 self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) @@ -208,6 +210,7 @@ class EngineIO(LoggingMixin): def _process_packet(self, packet): engineIO_packet_type, engineIO_packet_data = packet + print 'engineIO_packet_data=%s' % engineIO_packet_data # Launch callbacks namespace = self.get_namespace() try: @@ -224,6 +227,8 @@ class EngineIO(LoggingMixin): raise PacketError( 'unexpected engine.io packet type (%s)' % engineIO_packet_type) delegate(engineIO_packet_data, namespace._find_packet_callback) + print '*** in engine.io, engineIO_packet_type=%s' % engineIO_packet_type + print '*** in engine.io, engineIO_packet_data=%s' % engineIO_packet_data if engineIO_packet_type is 4: return engineIO_packet_data @@ -339,12 +344,19 @@ class SocketIO(EngineIO): pass def emit(self, event, *args, **kw): + print 'z1' path = kw.get('path', '') + print 'z2' callback, args = find_callback(args, kw) + print 'z3' ack_id = self._set_ack_callback(callback) if callback else None + print 'z4' socketIO_packet_type = 2 + print 'z5' socketIO_packet_data = format_socketIO_packet_data(path, ack_id, args) + print 'z6' self._message(str(socketIO_packet_type) + socketIO_packet_data) + print 'z7' def send(self, data='', callback=None): args = [data] @@ -372,6 +384,7 @@ class SocketIO(EngineIO): def _process_packet(self, packet): engineIO_packet_data = super(SocketIO, self)._process_packet(packet) + print '*** in socket.io, engineIO_packet_data=%s' % engineIO_packet_data if engineIO_packet_data is None: return socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) diff --git a/socketIO_client/tests/__init__.py b/socketIO_client/tests/__init__.py index d0e7e8f..239de78 100644 --- a/socketIO_client/tests/__init__.py +++ b/socketIO_client/tests/__init__.py @@ -39,7 +39,9 @@ class BaseMixin(object): def test_emit(self): 'Emit' namespace = self.socketIO.define(Namespace) + print 'a' self.socketIO.emit('emit') + print 'b' self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(namespace.args_by_event, { 'emit_response': (), @@ -169,21 +171,26 @@ class Test_XHR_PollingTransport(BaseMixin, TestCase): class Namespace(LoggingNamespace): def initialize(self): + print('xxx initialize') self.called_on_disconnect = False self.args_by_event = {} self.response = None def on_disconnect(self): + print('xxx on_disconnect') self.called_on_disconnect = True def on_wait_with_disconnect_response(self): + print('xxx on_wait_with_disconnect_response') self.disconnect() def on_event(self, event, *args): + print('xxx on_event') callback, args = find_callback(args) if callback: callback(*args) self.args_by_event[event] = args def on_message(self, data): + print('xxx on_message') self.response = data diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 12a97b8..0ca398d 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -16,13 +16,19 @@ class AbstractTransport(object): self.is_secure = is_secure self.url = url self.engineIO_session = engineIO_session + self.engineIO_packets = [] def recv_packet(self): - pass + while self.engineIO_packets: + yield self.engineIO_packets.pop(0) def send_packet(self, engineIO_packet_type, engineIO_packet_data): pass + def queue_packet(self, engineIO_packet): + print 'queue_packet %s' % str(engineIO_packet) + self.engineIO_packets.append(engineIO_packet) + class XHR_PollingTransport(AbstractTransport): @@ -47,18 +53,21 @@ class XHR_PollingTransport(AbstractTransport): self.kw_post = {} def recv_packet(self): + for engineIO_packet in super(XHR_PollingTransport, self).recv_packet(): + yield engineIO_packet params = dict(self.params) - params['t'] = self.get_timestamp() + params['t'] = self._get_timestamp() response = get_response( self.http_session.get, self.http_url, params=params, **self.kw_get) - return decode_engineIO_content(response.content) + for engineIO_packet in decode_engineIO_content(response.content): + yield engineIO_packet def send_packet(self, engineIO_packet_type, engineIO_packet_data): params = dict(self.params) - params['t'] = self.get_timestamp() + params['t'] = self._get_timestamp() response = get_response( self.http_session.post, self.http_url, @@ -69,7 +78,7 @@ class XHR_PollingTransport(AbstractTransport): **self.kw_post) assert response.content == 'ok' - def get_timestamp(self): + def _get_timestamp(self): timestamp = '%s-%s' % (int(time.time() * 1000), self.request_index) self.request_index += 1 return timestamp From 3d8efe0eb484cb21d19bba9765ef4123a9ac271a Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 22 Feb 2015 20:29:12 -0500 Subject: [PATCH 154/191] Fix emit bug --- socketIO_client/__init__.py | 38 +++++++++++++++++------------------ socketIO_client/parsers.py | 1 + socketIO_client/transports.py | 10 +-------- 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 1a97d89..ff6230b 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -21,7 +21,7 @@ LoggingNamespace = LoggingSocketIONamespace class EngineIO(LoggingMixin): def __init__( - self, host, port=None, Namespace=None, + self, host, port=None, Namespace=EngineIONamespace, wait_for_connection=True, transports=TRANSPORTS, resource='engine.io', **kw): self._is_secure, self._url = parse_host(host, port, resource) @@ -30,32 +30,29 @@ class EngineIO(LoggingMixin): self._http_session = prepare_http_session(kw) self._log_name = self._url self._wants_to_close = False + self._connected = False if Namespace: self.define(Namespace) + # self._transport # Connect - @property - def connected(self): - try: - return self._connected - except AttributeError: - return False - @property def _transport(self): - try: - if self.connected: - return self._transport_instance - except AttributeError: - pass + print 't1' + if self._connected: + return self._transport_instance + print 't2' self._engineIO_session = self._get_engineIO_session() + print 't3' self._transport_instance = self._negotiate_transport() + print 't4' self._connect_namespaces() + print 't5' self._connected = True - for engineIO_packet in self._transport_instance.recv_packet(): - self._transport_instance.queue_packet(engineIO_packet) + print 't6 starting heartbeat' self._reset_heartbeat() + print 't7' return self._transport_instance def _get_engineIO_session(self): @@ -144,7 +141,7 @@ class EngineIO(LoggingMixin): def _close(self): self._wants_to_close = True - if not self.connected: + if not self._connected: return engineIO_packet_type = 1 self._transport.send_packet(engineIO_packet_type, '') @@ -274,7 +271,7 @@ class SocketIO(EngineIO): """ def __init__( - self, host, port=None, Namespace=None, + self, host, port=None, Namespace=SocketIONamespace, wait_for_connection=True, transports=TRANSPORTS, resource='socket.io', **kw): self._namespace_by_path = {} @@ -329,7 +326,7 @@ class SocketIO(EngineIO): self._message(str(socketIO_packet_type) + socketIO_packet_data) def disconnect(self, path=''): - if not self.connected: + if not self._connected: return if path: socketIO_packet_type = 1 @@ -351,10 +348,11 @@ class SocketIO(EngineIO): print 'z3' ack_id = self._set_ack_callback(callback) if callback else None print 'z4' + args = [event] + list(args) socketIO_packet_type = 2 - print 'z5' + print 'z5 path=%s ack_id=%s args=%s' % (path, ack_id, str(args)) socketIO_packet_data = format_socketIO_packet_data(path, ack_id, args) - print 'z6' + print 'z6 emitting %s' % str(socketIO_packet_data) self._message(str(socketIO_packet_type) + socketIO_packet_data) print 'z7' diff --git a/socketIO_client/parsers.py b/socketIO_client/parsers.py index 1ee9f2f..957116c 100644 --- a/socketIO_client/parsers.py +++ b/socketIO_client/parsers.py @@ -59,6 +59,7 @@ def format_socketIO_packet_data(path=None, ack_id=None, args=None): socketIO_packet_data = str(ack_id) + socketIO_packet_data if path: socketIO_packet_data = path + ',' + socketIO_packet_data + print 'format_socketIO_packet_data = %s' % socketIO_packet_data return socketIO_packet_data diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 0ca398d..6cd012c 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -16,19 +16,13 @@ class AbstractTransport(object): self.is_secure = is_secure self.url = url self.engineIO_session = engineIO_session - self.engineIO_packets = [] def recv_packet(self): - while self.engineIO_packets: - yield self.engineIO_packets.pop(0) + pass def send_packet(self, engineIO_packet_type, engineIO_packet_data): pass - def queue_packet(self, engineIO_packet): - print 'queue_packet %s' % str(engineIO_packet) - self.engineIO_packets.append(engineIO_packet) - class XHR_PollingTransport(AbstractTransport): @@ -53,8 +47,6 @@ class XHR_PollingTransport(AbstractTransport): self.kw_post = {} def recv_packet(self): - for engineIO_packet in super(XHR_PollingTransport, self).recv_packet(): - yield engineIO_packet params = dict(self.params) params['t'] = self._get_timestamp() response = get_response( From f67efd71196c99f76d4993660824d550a63e590c Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 22 Feb 2015 20:31:52 -0500 Subject: [PATCH 155/191] Remove print statements --- socketIO_client/__init__.py | 19 ------------------- socketIO_client/parsers.py | 1 - socketIO_client/tests/__init__.py | 7 ------- 3 files changed, 27 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index ff6230b..6ad6c0e 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -39,20 +39,13 @@ class EngineIO(LoggingMixin): @property def _transport(self): - print 't1' if self._connected: return self._transport_instance - print 't2' self._engineIO_session = self._get_engineIO_session() - print 't3' self._transport_instance = self._negotiate_transport() - print 't4' self._connect_namespaces() - print 't5' self._connected = True - print 't6 starting heartbeat' self._reset_heartbeat() - print 't7' return self._transport_instance def _get_engineIO_session(self): @@ -156,7 +149,6 @@ class EngineIO(LoggingMixin): self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) def _message(self, engineIO_packet_data): - print('_message %s' % str(engineIO_packet_data)) engineIO_packet_type = 4 self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) @@ -207,7 +199,6 @@ class EngineIO(LoggingMixin): def _process_packet(self, packet): engineIO_packet_type, engineIO_packet_data = packet - print 'engineIO_packet_data=%s' % engineIO_packet_data # Launch callbacks namespace = self.get_namespace() try: @@ -224,8 +215,6 @@ class EngineIO(LoggingMixin): raise PacketError( 'unexpected engine.io packet type (%s)' % engineIO_packet_type) delegate(engineIO_packet_data, namespace._find_packet_callback) - print '*** in engine.io, engineIO_packet_type=%s' % engineIO_packet_type - print '*** in engine.io, engineIO_packet_data=%s' % engineIO_packet_data if engineIO_packet_type is 4: return engineIO_packet_data @@ -341,20 +330,13 @@ class SocketIO(EngineIO): pass def emit(self, event, *args, **kw): - print 'z1' path = kw.get('path', '') - print 'z2' callback, args = find_callback(args, kw) - print 'z3' ack_id = self._set_ack_callback(callback) if callback else None - print 'z4' args = [event] + list(args) socketIO_packet_type = 2 - print 'z5 path=%s ack_id=%s args=%s' % (path, ack_id, str(args)) socketIO_packet_data = format_socketIO_packet_data(path, ack_id, args) - print 'z6 emitting %s' % str(socketIO_packet_data) self._message(str(socketIO_packet_type) + socketIO_packet_data) - print 'z7' def send(self, data='', callback=None): args = [data] @@ -382,7 +364,6 @@ class SocketIO(EngineIO): def _process_packet(self, packet): engineIO_packet_data = super(SocketIO, self)._process_packet(packet) - print '*** in socket.io, engineIO_packet_data=%s' % engineIO_packet_data if engineIO_packet_data is None: return socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) diff --git a/socketIO_client/parsers.py b/socketIO_client/parsers.py index 957116c..1ee9f2f 100644 --- a/socketIO_client/parsers.py +++ b/socketIO_client/parsers.py @@ -59,7 +59,6 @@ def format_socketIO_packet_data(path=None, ack_id=None, args=None): socketIO_packet_data = str(ack_id) + socketIO_packet_data if path: socketIO_packet_data = path + ',' + socketIO_packet_data - print 'format_socketIO_packet_data = %s' % socketIO_packet_data return socketIO_packet_data diff --git a/socketIO_client/tests/__init__.py b/socketIO_client/tests/__init__.py index 239de78..d0e7e8f 100644 --- a/socketIO_client/tests/__init__.py +++ b/socketIO_client/tests/__init__.py @@ -39,9 +39,7 @@ class BaseMixin(object): def test_emit(self): 'Emit' namespace = self.socketIO.define(Namespace) - print 'a' self.socketIO.emit('emit') - print 'b' self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(namespace.args_by_event, { 'emit_response': (), @@ -171,26 +169,21 @@ class Test_XHR_PollingTransport(BaseMixin, TestCase): class Namespace(LoggingNamespace): def initialize(self): - print('xxx initialize') self.called_on_disconnect = False self.args_by_event = {} self.response = None def on_disconnect(self): - print('xxx on_disconnect') self.called_on_disconnect = True def on_wait_with_disconnect_response(self): - print('xxx on_wait_with_disconnect_response') self.disconnect() def on_event(self, event, *args): - print('xxx on_event') callback, args = find_callback(args) if callback: callback(*args) self.args_by_event[event] = args def on_message(self, data): - print('xxx on_message') self.response = data From 34c56a3da3e0eefae32a36988205ef4f95f99288 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 22 Feb 2015 20:45:27 -0500 Subject: [PATCH 156/191] Fix broken test --- socketIO_client/__init__.py | 13 ++++++------- socketIO_client/tests/__init__.py | 9 ++++----- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 6ad6c0e..d00b655 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -30,21 +30,20 @@ class EngineIO(LoggingMixin): self._http_session = prepare_http_session(kw) self._log_name = self._url self._wants_to_close = False - self._connected = False + self.connected = False if Namespace: self.define(Namespace) - # self._transport # Connect @property def _transport(self): - if self._connected: + if self.connected: return self._transport_instance self._engineIO_session = self._get_engineIO_session() self._transport_instance = self._negotiate_transport() self._connect_namespaces() - self._connected = True + self.connected = True self._reset_heartbeat() return self._transport_instance @@ -134,11 +133,11 @@ class EngineIO(LoggingMixin): def _close(self): self._wants_to_close = True - if not self._connected: + if not self.connected: return engineIO_packet_type = 1 self._transport.send_packet(engineIO_packet_type, '') - self._connected = False + self.connected = False def _ping(self, engineIO_packet_data=''): engineIO_packet_type = 2 @@ -315,7 +314,7 @@ class SocketIO(EngineIO): self._message(str(socketIO_packet_type) + socketIO_packet_data) def disconnect(self, path=''): - if not self._connected: + if not self.connected: return if path: socketIO_packet_type = 1 diff --git a/socketIO_client/tests/__init__.py b/socketIO_client/tests/__init__.py index d0e7e8f..135215d 100644 --- a/socketIO_client/tests/__init__.py +++ b/socketIO_client/tests/__init__.py @@ -24,13 +24,14 @@ class BaseMixin(object): def test_disconnect(self): 'Disconnect' - self.socketIO.define(LoggingNamespace) + self.socketIO.emit('whee') self.assertTrue(self.socketIO.connected) self.socketIO.disconnect() self.assertFalse(self.socketIO.connected) # Use context manager with SocketIO(HOST, PORT, Namespace) as self.socketIO: namespace = self.socketIO.get_namespace() + namespace.emit('whee') self.assertFalse(namespace.called_on_disconnect) self.assertTrue(self.socketIO.connected) self.assertTrue(namespace.called_on_disconnect) @@ -65,14 +66,12 @@ class BaseMixin(object): def test_emit_with_callback(self): 'Emit with callback' - self.socketIO.define(LoggingNamespace) self.socketIO.emit('emit_with_callback', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit_with_callback_with_payload(self): 'Emit with callback with payload' - self.socketIO.define(LoggingNamespace) self.socketIO.emit( 'emit_with_callback_with_payload', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) @@ -80,7 +79,6 @@ class BaseMixin(object): def test_emit_with_callback_with_multiple_payloads(self): 'Emit with callback with multiple payloads' - self.socketIO.define(LoggingNamespace) self.socketIO.emit( 'emit_with_callback_with_multiple_payloads', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) @@ -162,7 +160,8 @@ class Test_XHR_PollingTransport(BaseMixin, TestCase): def setUp(self): super(Test_XHR_PollingTransport, self).setUp() - self.socketIO = SocketIO(HOST, PORT, transports=['xhr-polling']) + self.socketIO = SocketIO(HOST, PORT, LoggingNamespace, transports=[ + 'xhr-polling']) self.wait_time_in_seconds = 1 From 5146b2f0bc4e463bb762f74ac7511e475adfac37 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 22 Feb 2015 20:58:08 -0500 Subject: [PATCH 157/191] Fix another test --- socketIO_client/__init__.py | 5 +++-- socketIO_client/namespaces.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index d00b655..7e75955 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -337,11 +337,12 @@ class SocketIO(EngineIO): socketIO_packet_data = format_socketIO_packet_data(path, ack_id, args) self._message(str(socketIO_packet_type) + socketIO_packet_data) - def send(self, data='', callback=None): + def send(self, data='', callback=None, **kw): + path = kw.get('path', '') args = [data] if callback: args.append(callback) - self.emit('message', *args) + self.emit('message', *args, path=path) def _ack(self, path, ack_id, *args): socketIO_packet_type = 3 diff --git a/socketIO_client/namespaces.py b/socketIO_client/namespaces.py index c622e65..e69421b 100644 --- a/socketIO_client/namespaces.py +++ b/socketIO_client/namespaces.py @@ -74,7 +74,7 @@ class SocketIONamespace(EngineIONamespace): self._io.disconnect(self.path) def emit(self, event, *args, **kw): - self._io.emit(event, *args, **kw) + self._io.emit(event, path=self.path, *args, **kw) def send(self, data='', callback=None): self._io.send(data, callback) From 922de4d160bfc9560cd898ec31f17e65c61a3f65 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 23 Feb 2015 02:18:18 -0500 Subject: [PATCH 158/191] Fix ack --- socketIO_client/__init__.py | 17 ++++++++++------- socketIO_client/tests/__init__.py | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 7e75955..113c3bb 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -23,10 +23,11 @@ class EngineIO(LoggingMixin): def __init__( self, host, port=None, Namespace=EngineIONamespace, wait_for_connection=True, transports=TRANSPORTS, - resource='engine.io', **kw): + resource='engine.io', hurry_interval_in_seconds=1, **kw): self._is_secure, self._url = parse_host(host, port, resource) self._wait_for_connection = wait_for_connection self._client_transports = transports + self._hurry_interval_in_seconds = hurry_interval_in_seconds self._http_session = prepare_http_session(kw) self._log_name = self._url self._wants_to_close = False @@ -74,12 +75,14 @@ class EngineIO(LoggingMixin): except AttributeError: pass ping_interval = self._engineIO_session.ping_interval + if self._transport_name.endswith('-polling'): + hurry_interval_in_seconds = self._hurry_interval_in_seconds + else: + hurry_interval_in_seconds = ping_interval self._heartbeat_thread = HeartbeatThread( send_heartbeat=self._ping, relax_interval_in_seconds=ping_interval, - hurry_interval_in_seconds=1 if self._transport_name in [ - 'xhr-polling', - ] else ping_interval) + hurry_interval_in_seconds=hurry_interval_in_seconds) self._heartbeat_thread.start() def _connect_namespaces(self): @@ -261,13 +264,13 @@ class SocketIO(EngineIO): def __init__( self, host, port=None, Namespace=SocketIONamespace, wait_for_connection=True, transports=TRANSPORTS, - resource='socket.io', **kw): + resource='socket.io', hurry_interval_in_seconds=1, **kw): self._namespace_by_path = {} self._callback_by_ack_id = {} self._ack_id = 0 super(SocketIO, self).__init__( host, port, Namespace, wait_for_connection, transports, - resource, **kw) + resource, hurry_interval_in_seconds, **kw) # Connect @@ -400,7 +403,7 @@ class SocketIO(EngineIO): event = args.pop(0) except IndexError: raise PacketError('missing event name') - if data_parsed.ack_id: + if data_parsed.ack_id is not None: args.append(self._prepare_to_send_ack( data_parsed.path, data_parsed.ack_id)) find_packet_callback(event)(*args) diff --git a/socketIO_client/tests/__init__.py b/socketIO_client/tests/__init__.py index 135215d..b17e1e5 100644 --- a/socketIO_client/tests/__init__.py +++ b/socketIO_client/tests/__init__.py @@ -138,7 +138,7 @@ class BaseMixin(object): }) def test_namespace_ack(self): - 'Trigger server callback' + 'Respond to a server callback request within a namespace' chat_namespace = self.socketIO.define(Namespace, '/chat') chat_namespace.emit('trigger_server_expects_callback', PAYLOAD) self.socketIO.wait(self.wait_time_in_seconds) From 8f9da7f4d0b3d8f5556c08512e5ecf4a95b95c07 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 23 Feb 2015 02:25:40 -0500 Subject: [PATCH 159/191] Update goals --- TODO.goals | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TODO.goals b/TODO.goals index 1c863a8..716f490 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,5 +1,8 @@ Release 0.6.1 #41 #52 - Update tests + Fix connection status code 400 error + Rename connected to opened + Add retry to engineio commands + Run README commands Merge sarietta's pull request Add Websocket transport Consider logging packets sent and received From 0d1a2b9ca28338195b944e10f3d71edaa8ab0b22 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 23 Feb 2015 08:56:00 -0500 Subject: [PATCH 160/191] Retry failed commands --- TODO.goals | 4 +--- socketIO_client/__init__.py | 32 +++++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/TODO.goals b/TODO.goals index 716f490..d01b1ff 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,10 +1,8 @@ Release 0.6.1 #41 #52 - Fix connection status code 400 error - Rename connected to opened - Add retry to engineio commands Run README commands Merge sarietta's pull request Add Websocket transport + Update proxy to include websocket depending on argument Consider logging packets sent and received Implement rooms #65 Implement binary event diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 113c3bb..2ede079 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -18,6 +18,17 @@ BaseNamespace = SocketIONamespace LoggingNamespace = LoggingSocketIONamespace +def retry(f): + def wrap(*args, **kw): + self = args[0] + try: + return f(*args, **kw) + except (TimeoutError, ConnectionError): + self._opened = False + return f(*args, **kw) + return wrap + + class EngineIO(LoggingMixin): def __init__( @@ -28,10 +39,10 @@ class EngineIO(LoggingMixin): self._wait_for_connection = wait_for_connection self._client_transports = transports self._hurry_interval_in_seconds = hurry_interval_in_seconds - self._http_session = prepare_http_session(kw) + self._kw = kw self._log_name = self._url self._wants_to_close = False - self.connected = False + self._opened = False if Namespace: self.define(Namespace) @@ -39,17 +50,18 @@ class EngineIO(LoggingMixin): @property def _transport(self): - if self.connected: + if self._opened: return self._transport_instance self._engineIO_session = self._get_engineIO_session() self._transport_instance = self._negotiate_transport() self._connect_namespaces() - self.connected = True + self._opened = True self._reset_heartbeat() return self._transport_instance def _get_engineIO_session(self): warning_screen = self._yield_warning_screen() + self._http_session = prepare_http_session(self._kw) for elapsed_time in warning_screen: transport = XHR_PollingTransport( self._http_session, self._is_secure, self._url) @@ -130,34 +142,40 @@ class EngineIO(LoggingMixin): def send(self, engineIO_packet_data): self._message(engineIO_packet_data) + @retry def _open(self): engineIO_packet_type = 0 self._transport.send_packet(engineIO_packet_type, '') def _close(self): self._wants_to_close = True - if not self.connected: + if not self._opened: return engineIO_packet_type = 1 self._transport.send_packet(engineIO_packet_type, '') - self.connected = False + self._opened = False + @retry def _ping(self, engineIO_packet_data=''): engineIO_packet_type = 2 self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) + @retry def _pong(self, engineIO_packet_data=''): engineIO_packet_type = 3 self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) + @retry def _message(self, engineIO_packet_data): engineIO_packet_type = 4 self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) + @retry def _upgrade(self): engineIO_packet_type = 5 self._transport.send_packet(engineIO_packet_type, '') + @retry def _noop(self): engineIO_packet_type = 6 self._transport.send_packet(engineIO_packet_type, '') @@ -317,7 +335,7 @@ class SocketIO(EngineIO): self._message(str(socketIO_packet_type) + socketIO_packet_data) def disconnect(self, path=''): - if not self.connected: + if not self._opened: return if path: socketIO_packet_type = 1 From 444386f2716eedc01e8b64a3db0422537a4611e9 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 23 Feb 2015 09:39:29 -0500 Subject: [PATCH 161/191] Test README commands --- README.rst | 3 ++- TODO.goals | 3 +++ socketIO_client/__init__.py | 8 ++++++++ socketIO_client/namespaces.py | 22 ++++++++++++++-------- socketIO_client/tests/__init__.py | 2 -- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 582cf58..967514e 100644 --- a/README.rst +++ b/README.rst @@ -42,6 +42,7 @@ Launch your socket.io server. :: For debugging information, run these commands first. :: import logging + logging.getLogger('requests').setLevel(logging.WARNING) logging.basicConfig(level=logging.DEBUG) Emit. :: @@ -144,7 +145,7 @@ Wait forever. :: from socketIO_client import SocketIO - socketIO = SocketIO('localhost') + socketIO = SocketIO('localhost', 8000) socketIO.wait() diff --git a/TODO.goals b/TODO.goals index d01b1ff..66afe84 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,8 +1,11 @@ Release 0.6.1 #41 #52 Run README commands + Fix for Python 3 Merge sarietta's pull request Add Websocket transport Update proxy to include websocket depending on argument + Use prepared request to get headers from http_session + Include https://github.com/invisibleroads/socketIO-client/issues/68 Consider logging packets sent and received Implement rooms #65 Implement binary event diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 2ede079..70731dc 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -45,6 +45,7 @@ class EngineIO(LoggingMixin): self._opened = False if Namespace: self.define(Namespace) + self._transport # Connect @@ -149,6 +150,7 @@ class EngineIO(LoggingMixin): def _close(self): self._wants_to_close = True + self._heartbeat_thread.stop() if not self._opened: return engineIO_packet_type = 1 @@ -169,6 +171,7 @@ class EngineIO(LoggingMixin): def _message(self, engineIO_packet_data): engineIO_packet_type = 4 self._transport.send_packet(engineIO_packet_type, engineIO_packet_data) + self._debug('[socket.io packet sent] %s', engineIO_packet_data) @retry def _upgrade(self): @@ -292,6 +295,10 @@ class SocketIO(EngineIO): # Connect + @property + def connected(self): + return self._opened + def _connect_namespaces(self): for path, namespace in self._namespace_by_path.items(): namespace._transport = self._transport_instance @@ -387,6 +394,7 @@ class SocketIO(EngineIO): engineIO_packet_data = super(SocketIO, self)._process_packet(packet) if engineIO_packet_data is None: return + self._debug('[socket.io packet received] %s', engineIO_packet_data) socketIO_packet_type = int(get_character(engineIO_packet_data, 0)) socketIO_packet_data = engineIO_packet_data[1:] # Launch callbacks diff --git a/socketIO_client/namespaces.py b/socketIO_client/namespaces.py index e69421b..84cd65c 100644 --- a/socketIO_client/namespaces.py +++ b/socketIO_client/namespaces.py @@ -149,15 +149,15 @@ class LoggingEngineIONamespace(EngineIONamespace): super(LoggingEngineIONamespace, self).on_close() def on_ping(self, data): - self._debug('[ping] %s' % data) + self._debug('[ping] %s', data) super(LoggingEngineIONamespace, self).on_ping(data) def on_pong(self, data): - self._debug('[pong] %s' % data) + self._debug('[pong] %s', data) super(LoggingEngineIONamespace, self).on_pong(data) def on_message(self, data): - self._debug('[message] %s' % data) + self._debug('[message] %s', data) super(LoggingEngineIONamespace, self).on_message(data) def on_upgrade(self): @@ -180,15 +180,15 @@ class LoggingEngineIONamespace(EngineIONamespace): class LoggingSocketIONamespace(SocketIONamespace): def on_connect(self): - self._debug('%s [connect]' % self.path) + self._debug('%s[connect]', _make_logging_header(self.path)) super(LoggingSocketIONamespace, self).on_connect() def on_reconnect(self): - self._debug('%s [reconnect]' % self.path) + self._debug('%s[reconnect]', _make_logging_header(self.path)) super(LoggingSocketIONamespace, self).on_reconnect() def on_disconnect(self): - self._debug('%s [disconnect]' % self.path) + self._debug('%s[disconnect]', _make_logging_header(self.path)) super(LoggingSocketIONamespace, self).on_disconnect() def on_event(self, event, *args): @@ -196,11 +196,13 @@ class LoggingSocketIONamespace(SocketIONamespace): arguments = [repr(_) for _ in args] if callback: arguments.append('callback(*args)') - self._info('%s [event] %s(%s)', self.path, event, ', '.join(arguments)) + self._info( + '%s[event] %s(%s)', _make_logging_header(self.path), event, + ', '.join(arguments)) super(LoggingSocketIONamespace, self).on_event(event, *args) def on_error(self, data): - self._debug('%s [error] %s' % (self.path, data)) + self._debug('%s[error] %s', _make_logging_header(self.path), data) super(LoggingSocketIONamespace, self).on_error() @@ -212,3 +214,7 @@ def find_callback(args, kw=None): return kw['callback'], args except (KeyError, TypeError): return None, args + + +def _make_logging_header(path): + return path + ' ' if path else '' diff --git a/socketIO_client/tests/__init__.py b/socketIO_client/tests/__init__.py index b17e1e5..2ce4856 100644 --- a/socketIO_client/tests/__init__.py +++ b/socketIO_client/tests/__init__.py @@ -24,14 +24,12 @@ class BaseMixin(object): def test_disconnect(self): 'Disconnect' - self.socketIO.emit('whee') self.assertTrue(self.socketIO.connected) self.socketIO.disconnect() self.assertFalse(self.socketIO.connected) # Use context manager with SocketIO(HOST, PORT, Namespace) as self.socketIO: namespace = self.socketIO.get_namespace() - namespace.emit('whee') self.assertFalse(namespace.called_on_disconnect) self.assertTrue(self.socketIO.connected) self.assertTrue(namespace.called_on_disconnect) From af91855b187034e576373bb8cd1efb4d99828065 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 23 Feb 2015 10:54:33 -0500 Subject: [PATCH 162/191] Fix for Python 3 --- TODO.goals | 3 +-- socketIO_client/__init__.py | 4 ++-- socketIO_client/heartbeats.py | 8 ++++---- socketIO_client/parsers.py | 12 ++++++------ socketIO_client/transports.py | 2 +- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/TODO.goals b/TODO.goals index 66afe84..bde212b 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,11 +1,10 @@ Release 0.6.1 #41 #52 - Run README commands - Fix for Python 3 Merge sarietta's pull request Add Websocket transport Update proxy to include websocket depending on argument Use prepared request to get headers from http_session Include https://github.com/invisibleroads/socketIO-client/issues/68 +Add test for on_reconnect using sarietta's bash scripts Consider logging packets sent and received Implement rooms #65 Implement binary event diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 70731dc..7518384 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -84,7 +84,7 @@ class EngineIO(LoggingMixin): def _reset_heartbeat(self): try: - self._heartbeat_thread.stop() + self._heartbeat_thread.halt() except AttributeError: pass ping_interval = self._engineIO_session.ping_interval @@ -150,7 +150,7 @@ class EngineIO(LoggingMixin): def _close(self): self._wants_to_close = True - self._heartbeat_thread.stop() + self._heartbeat_thread.halt() if not self._opened: return engineIO_packet_type = 1 diff --git a/socketIO_client/heartbeats.py b/socketIO_client/heartbeats.py index f1de194..db2599d 100644 --- a/socketIO_client/heartbeats.py +++ b/socketIO_client/heartbeats.py @@ -17,11 +17,11 @@ class HeartbeatThread(Thread): self._hurry_interval_in_seconds = hurry_interval_in_seconds self._adrenaline = Event() self._rest = Event() - self._stop = Event() + self._halt = Event() def run(self): try: - while not self._stop.is_set(): + while not self._halt.is_set(): try: self._send_heartbeat() except TimeoutError: @@ -42,6 +42,6 @@ class HeartbeatThread(Thread): self._rest.set() self._rest.clear() - def stop(self): + def halt(self): self._rest.set() - self._stop.set() + self._halt.set() diff --git a/socketIO_client/parsers.py b/socketIO_client/parsers.py index 1ee9f2f..c962d77 100644 --- a/socketIO_client/parsers.py +++ b/socketIO_client/parsers.py @@ -30,11 +30,11 @@ def parse_engineIO_session(engineIO_packet_data): def encode_engineIO_content(engineIO_packets): - parts = [] + content = bytearray() for packet_type, packet_data in engineIO_packets: - packet_string = str(packet_type) + encode_string(packet_data) - parts.append(_make_packet_header(packet_string) + packet_string) - return ''.join(parts) + packet_string = encode_string(str(packet_type) + packet_data) + content.extend(_make_packet_header(packet_string) + packet_string) + return content def decode_engineIO_content(content): @@ -100,11 +100,11 @@ def get_namespace_path(socketIO_packet_data): def _make_packet_header(packet_string): length_string = str(len(packet_string)) - header_digits = [0] + header_digits = bytearray([0]) for i in range(len(length_string)): header_digits.append(ord(length_string[i]) - 48) header_digits.append(255) - return ''.join(chr(x) for x in header_digits) + return header_digits def _read_packet_length(content, content_index): diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 6cd012c..dc187cc 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -68,7 +68,7 @@ class XHR_PollingTransport(AbstractTransport): (engineIO_packet_type, engineIO_packet_data), ]), **self.kw_post) - assert response.content == 'ok' + assert response.content == b'ok' def _get_timestamp(self): timestamp = '%s-%s' % (int(time.time() * 1000), self.request_index) From 1f9d97fc3341b2f5283837eb37c4b740fcaae7ce Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 23 Feb 2015 10:56:25 -0500 Subject: [PATCH 163/191] Fix #66 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bc147cb..51ab595 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ DESCRIPTION = '\n\n'.join(open(join(HERE, _)).read() for _ in [ 'CHANGES.rst', ]) setup( - name='socketIO-client', + name='socketIO_client', version='0.6.1', description='A socket.io client library', long_description=DESCRIPTION, From 55b88b51d52b3f326ba1517a579a7fde78921659 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 23 Feb 2015 11:10:18 -0500 Subject: [PATCH 164/191] Update travis image url --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 967514e..7edc2c3 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -.. image:: https://travis-ci.org/invisibleroads/socketIO-client.svg?branch=0.6.1 +.. image:: https://travis-ci.org/invisibleroads/socketIO-client.svg?branch=master :target: https://travis-ci.org/invisibleroads/socketIO-client From 64fad75d4c452ee10dc4e58be8b1518d8a166a19 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 23 Feb 2015 11:11:59 -0500 Subject: [PATCH 165/191] Fix #41 #52 --- TODO.goals | 2 -- 1 file changed, 2 deletions(-) diff --git a/TODO.goals b/TODO.goals index bde212b..9fced70 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,5 +1,3 @@ -Release 0.6.1 #41 #52 - Merge sarietta's pull request Add Websocket transport Update proxy to include websocket depending on argument Use prepared request to get headers from http_session From 57da37e9958278a733c5fc2d11135413a3604a8c Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 23 Feb 2015 11:48:19 -0500 Subject: [PATCH 166/191] Update README --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 7edc2c3..3cad020 100644 --- a/README.rst +++ b/README.rst @@ -33,7 +33,8 @@ Activate isolated environment. :: Launch your socket.io server. :: # Get package folder - PACKAGE_FOLDER=`python -c "import os, socketIO_client; print(os.path.dirname(socketIO_client.__file__))"` + PACKAGE_FOLDER=`python -c "import os, socketIO_client;\ + print(os.path.dirname(socketIO_client.__file__))"` # Start socket.io server DEBUG=* node $PACKAGE_FOLDER/tests/serve.js # Start proxy server in a separate terminal on the same machine @@ -156,7 +157,7 @@ This software is available under the MIT License. Credits ------- -- `Guillermo Rauch `_ wrote the `socket.io specification `_. +- `Guillermo Rauch `_ wrote the `socket.io specification `_. - `Hiroki Ohtani `_ wrote `websocket-client `_. - `rod `_ wrote a `prototype for a Python client to a socket.io server `_. - `Alexandre Bourget `_ wrote `gevent-socketio `_, which is a socket.io server written in Python. From 601ba59971fd98647dc7d44cb5f52ab03e08c3a1 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 2 Mar 2015 08:59:47 -0500 Subject: [PATCH 167/191] Add bountysource link --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 3cad020..ee301dd 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,9 @@ .. image:: https://travis-ci.org/invisibleroads/socketIO-client.svg?branch=master :target: https://travis-ci.org/invisibleroads/socketIO-client +.. image:: https://www.bountysource.com/badge/tracker?tracker_id=388415 + :target: https://www.bountysource.com/trackers/388415-invisibleroads-socketio-client?utm_source=388415&utm_medium=shield&utm_campaign=TRACKER_BADGE + socketIO-client =============== From 978a669d162b627005cc285d91656a872913dd09 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 15 Apr 2015 07:17:23 -0400 Subject: [PATCH 168/191] Add websocket transport --- .travis.yml | 3 +- README.rst | 6 +- TODO.goals | 8 +- socketIO_client/__init__.py | 47 +++++++---- socketIO_client/heartbeats.py | 8 +- socketIO_client/parsers.py | 30 ++++--- socketIO_client/symmetries.py | 14 ++-- socketIO_client/tests/__init__.py | 15 +++- socketIO_client/tests/index.html | 2 +- socketIO_client/tests/serve.js | 14 +++- socketIO_client/tests/ssl.crt | 21 +++++ socketIO_client/tests/ssl.key | 28 +++++++ socketIO_client/transports.py | 132 ++++++++++++++++++++++++------ 13 files changed, 250 insertions(+), 78 deletions(-) create mode 100644 socketIO_client/tests/ssl.crt create mode 100644 socketIO_client/tests/ssl.key diff --git a/.travis.yml b/.travis.yml index b0bc004..1db3eb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,13 +8,12 @@ before_install: - sudo apt-get install nodejs install: - npm install -G socket.io - - npm install -G http-proxy + - npm install -G yargs - pip install -U requests - pip install -U six - pip install -U websocket-client - pip install -U coverage before_script: - DEBUG=* node socketIO_client/tests/serve.js & - - DEBUG=* node socketIO_client/tests/proxy.js & - sleep 3 script: nosetests diff --git a/README.rst b/README.rst index ee301dd..9a20e0b 100644 --- a/README.rst +++ b/README.rst @@ -1,9 +1,6 @@ .. image:: https://travis-ci.org/invisibleroads/socketIO-client.svg?branch=master :target: https://travis-ci.org/invisibleroads/socketIO-client -.. image:: https://www.bountysource.com/badge/tracker?tracker_id=388415 - :target: https://www.bountysource.com/trackers/388415-invisibleroads-socketio-client?utm_source=388415&utm_medium=shield&utm_campaign=TRACKER_BADGE - socketIO-client =============== @@ -139,7 +136,8 @@ Specify params, headers, cookies, proxies thanks to the `requests