From 37bd6786d735247f28b794594aa79a88cab1a701 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Sat, 20 Dec 2014 16:28:22 -0800 Subject: [PATCH 01/90] 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 d64e947aac64f0c0b5001feb4294a832d494c22d Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Mon, 22 Dec 2014 00:36:09 -0800 Subject: [PATCH 02/90] 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 03/90] 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 04/90] 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 05/90] 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 06/90] 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 07/90] 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 08/90] 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 09/90] 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 10/90] 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 11/90] 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 12/90] 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 13/90] 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 14/90] 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 15/90] 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 16/90] 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 17/90] 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 18/90] 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 19/90] 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 20/90] 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 2a43420e1b503100ea51cbe44d4db6f3b447183c Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Wed, 7 Jan 2015 13:14:19 -0800 Subject: [PATCH 21/90] 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 8e23fc73a5582d9a9b8ff2fcf12da2165650f295 Mon Sep 17 00:00:00 2001 From: Sean Arietta Date: Sat, 31 Jan 2015 21:54:04 -0800 Subject: [PATCH 22/90] 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 cd92f1c0cc5a605af16e04b5677bbd8e6ae1812b Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Tue, 17 Feb 2015 11:25:18 -0500 Subject: [PATCH 23/90] 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 24/90] 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 25/90] 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 26/90] 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 27/90] 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 28/90] 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 29/90] 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 30/90] 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 31/90] 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 32/90] 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 33/90] 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 34/90] 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 35/90] 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 36/90] 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 37/90] 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 38/90] 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 39/90] 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 40/90] 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 41/90] 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 42/90] 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 43/90] 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 44/90] 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 45/90] 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 46/90] 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 47/90] 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 48/90] 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 49/90] 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 50/90] 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 51/90] 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 52/90] 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 53/90] 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 54/90] 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 55/90] 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 56/90] 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 57/90] 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 58/90] 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