Browse Source

Fix admin transport reporting and broaden transport docs

pull/1567/head
tech-zjf 2 months ago
parent
commit
c3e75d8f53
  1. 26
      src/socketio/admin.py
  2. 26
      src/socketio/async_admin.py
  3. 13
      src/socketio/async_client.py
  4. 10
      src/socketio/async_server.py
  5. 19
      src/socketio/async_simple_client.py
  6. 5
      src/socketio/base_client.py
  7. 5
      src/socketio/base_server.py
  8. 13
      src/socketio/client.py
  9. 9
      src/socketio/server.py
  10. 19
      src/socketio/simple_client.py
  11. 35
      tests/async/test_admin.py
  12. 35
      tests/common/test_admin.py

26
src/socketio/admin.py

@ -208,7 +208,7 @@ class InstrumentedServer:
serialized_socket, serialized_socket,
datetime.fromtimestamp(t, timezone.utc).isoformat(), datetime.fromtimestamp(t, timezone.utc).isoformat(),
), namespace=self.admin_namespace) ), namespace=self.admin_namespace)
if not self.sio.eio._get_socket(eio_sid).upgraded: if self._get_transport(eio_sid) == 'polling':
self.sio.start_background_task( self.sio.start_background_task(
self._check_for_upgrade, eio_sid, sid, namespace) self._check_for_upgrade, eio_sid, sid, namespace)
elif event == 'disconnect': elif event == 'disconnect':
@ -233,11 +233,12 @@ class InstrumentedServer:
for _ in range(5): for _ in range(5):
self.sio.sleep(5) self.sio.sleep(5)
try: try:
if self.sio.eio._get_socket(eio_sid).upgraded: transport = self._get_transport(eio_sid)
if transport != 'polling':
self.sio.emit('socket_updated', { self.sio.emit('socket_updated', {
'id': sid, 'id': sid,
'nsp': namespace, 'nsp': namespace,
'transport': 'websocket', 'transport': transport,
}, namespace=self.admin_namespace) }, namespace=self.admin_namespace)
break break
except KeyError: except KeyError:
@ -345,12 +346,23 @@ class InstrumentedServer:
), namespace=self.admin_namespace) ), namespace=self.admin_namespace)
return socket.__send_ping() return socket.__send_ping()
def _get_transport(self, eio_sid, default=None):
transport = getattr(self.sio.eio, 'transport', None)
try:
if callable(transport):
return transport(eio_sid)
socket = self.sio.eio._get_socket(eio_sid)
except KeyError:
return default
return 'websocket' if socket.upgraded else 'polling'
def _emit_server_stats(self): def _emit_server_stats(self):
start_time = time.time() start_time = time.time()
namespaces = list(self.sio.handlers.keys()) namespaces = list(self.sio.handlers.keys())
namespaces.sort() namespaces.sort()
while not self.stop_stats_event.is_set(): while not self.stop_stats_event.is_set():
self.sio.sleep(self.server_stats_interval) self.sio.sleep(self.server_stats_interval)
eio_sids = list(self.sio.eio.sockets)
self.sio.emit('server_stats', { self.sio.emit('server_stats', {
'serverId': self.server_id, 'serverId': self.server_id,
'hostname': HOSTNAME, 'hostname': HOSTNAME,
@ -358,8 +370,9 @@ class InstrumentedServer:
'uptime': time.time() - start_time, 'uptime': time.time() - start_time,
'clientsCount': len(self.sio.eio.sockets), 'clientsCount': len(self.sio.eio.sockets),
'pollingClientsCount': len( 'pollingClientsCount': len(
[s for s in self.sio.eio.sockets.values() [eio_sid for eio_sid in eio_sids
if not s.upgraded]), if self._get_transport(
eio_sid, default=None) == 'polling']),
'aggregatedEvents': self.event_buffer.get_and_clear(), 'aggregatedEvents': self.event_buffer.get_and_clear(),
'namespaces': [{ 'namespaces': [{
'name': nsp, 'name': nsp,
@ -371,14 +384,13 @@ class InstrumentedServer:
def serialize_socket(self, sid, namespace, eio_sid=None): def serialize_socket(self, sid, namespace, eio_sid=None):
if eio_sid is None: # pragma: no cover if eio_sid is None: # pragma: no cover
eio_sid = self.sio.manager.eio_sid_from_sid(sid) eio_sid = self.sio.manager.eio_sid_from_sid(sid)
socket = self.sio.eio._get_socket(eio_sid)
environ = self.sio.environ.get(eio_sid, {}) environ = self.sio.environ.get(eio_sid, {})
tm = self.sio.manager._timestamps[sid] if sid in \ tm = self.sio.manager._timestamps[sid] if sid in \
self.sio.manager._timestamps else 0 self.sio.manager._timestamps else 0
return { return {
'id': sid, 'id': sid,
'clientId': eio_sid, 'clientId': eio_sid,
'transport': 'websocket' if socket.upgraded else 'polling', 'transport': self._get_transport(eio_sid),
'nsp': namespace, 'nsp': namespace,
'data': {}, 'data': {},
'handshake': { 'handshake': {

26
src/socketio/async_admin.py

@ -194,7 +194,7 @@ class InstrumentedAsyncServer:
serialized_socket, serialized_socket,
datetime.fromtimestamp(t, timezone.utc).isoformat(), datetime.fromtimestamp(t, timezone.utc).isoformat(),
), namespace=self.admin_namespace) ), namespace=self.admin_namespace)
if not self.sio.eio._get_socket(eio_sid).upgraded: if self._get_transport(eio_sid) == 'polling':
self.sio.start_background_task( self.sio.start_background_task(
self._check_for_upgrade, eio_sid, sid, namespace) self._check_for_upgrade, eio_sid, sid, namespace)
elif event == 'disconnect': elif event == 'disconnect':
@ -220,11 +220,12 @@ class InstrumentedAsyncServer:
for _ in range(5): for _ in range(5):
await self.sio.sleep(5) await self.sio.sleep(5)
try: try:
if self.sio.eio._get_socket(eio_sid).upgraded: transport = self._get_transport(eio_sid)
if transport != 'polling':
await self.sio.emit('socket_updated', { await self.sio.emit('socket_updated', {
'id': sid, 'id': sid,
'nsp': namespace, 'nsp': namespace,
'transport': 'websocket', 'transport': transport,
}, namespace=self.admin_namespace) }, namespace=self.admin_namespace)
break break
except KeyError: except KeyError:
@ -332,12 +333,23 @@ class InstrumentedAsyncServer:
), namespace=self.admin_namespace) ), namespace=self.admin_namespace)
return await socket.__send_ping() return await socket.__send_ping()
def _get_transport(self, eio_sid, default=None):
transport = getattr(self.sio.eio, 'transport', None)
try:
if callable(transport):
return transport(eio_sid)
socket = self.sio.eio._get_socket(eio_sid)
except KeyError:
return default
return 'websocket' if socket.upgraded else 'polling'
async def _emit_server_stats(self): async def _emit_server_stats(self):
start_time = time.time() start_time = time.time()
namespaces = list(self.sio.handlers.keys()) namespaces = list(self.sio.handlers.keys())
namespaces.sort() namespaces.sort()
while not self.stop_stats_event.is_set(): while not self.stop_stats_event.is_set():
await self.sio.sleep(self.server_stats_interval) await self.sio.sleep(self.server_stats_interval)
eio_sids = list(self.sio.eio.sockets)
await self.sio.emit('server_stats', { await self.sio.emit('server_stats', {
'serverId': self.server_id, 'serverId': self.server_id,
'hostname': HOSTNAME, 'hostname': HOSTNAME,
@ -345,8 +357,9 @@ class InstrumentedAsyncServer:
'uptime': time.time() - start_time, 'uptime': time.time() - start_time,
'clientsCount': len(self.sio.eio.sockets), 'clientsCount': len(self.sio.eio.sockets),
'pollingClientsCount': len( 'pollingClientsCount': len(
[s for s in self.sio.eio.sockets.values() [eio_sid for eio_sid in eio_sids
if not s.upgraded]), if self._get_transport(
eio_sid, default=None) == 'polling']),
'aggregatedEvents': self.event_buffer.get_and_clear(), 'aggregatedEvents': self.event_buffer.get_and_clear(),
'namespaces': [{ 'namespaces': [{
'name': nsp, 'name': nsp,
@ -362,14 +375,13 @@ class InstrumentedAsyncServer:
def serialize_socket(self, sid, namespace, eio_sid=None): def serialize_socket(self, sid, namespace, eio_sid=None):
if eio_sid is None: # pragma: no cover if eio_sid is None: # pragma: no cover
eio_sid = self.sio.manager.eio_sid_from_sid(sid) eio_sid = self.sio.manager.eio_sid_from_sid(sid)
socket = self.sio.eio._get_socket(eio_sid)
environ = self.sio.environ.get(eio_sid, {}) environ = self.sio.environ.get(eio_sid, {})
tm = self.sio.manager._timestamps[sid] if sid in \ tm = self.sio.manager._timestamps[sid] if sid in \
self.sio.manager._timestamps else 0 self.sio.manager._timestamps else 0
return { return {
'id': sid, 'id': sid,
'clientId': eio_sid, 'clientId': eio_sid,
'transport': 'websocket' if socket.upgraded else 'polling', 'transport': self._get_transport(eio_sid),
'nsp': namespace, 'nsp': namespace,
'data': {}, 'data': {},
'handshake': { 'handshake': {

13
src/socketio/async_client.py

@ -16,7 +16,7 @@ class AsyncClient(base_client.BaseClient):
"""A Socket.IO client for asyncio. """A Socket.IO client for asyncio.
This class implements a fully compliant Socket.IO web client with support This class implements a fully compliant Socket.IO web client with support
for websocket and long-polling transports. for Engine.IO transports, including websocket and long-polling.
:param reconnection: ``True`` if the client should automatically attempt to :param reconnection: ``True`` if the client should automatically attempt to
reconnect to the server after an interruption, or reconnect to the server after an interruption, or
@ -91,10 +91,13 @@ class AsyncClient(base_client.BaseClient):
more string key/value pairs. If a function is provided, more string key/value pairs. If a function is provided,
the client will invoke it to obtain the authentication the client will invoke it to obtain the authentication
data each time a connection or reconnection is attempted. data each time a connection or reconnection is attempted.
:param transports: The list of allowed transports. Valid transports :param transports: The list of allowed transports. Commonly
are ``'polling'`` and ``'websocket'``. If not ``'polling'`` and ``'websocket'`` are available.
given, the polling transport is connected first, Additional transports (for example
then an upgrade to websocket is attempted. ``'webtransport'``) may be available when
supported by the underlying Engine.IO client. If
not given, the polling transport is connected
first, then an upgrade to websocket is attempted.
:param namespaces: The namespaces to connect as a string or list of :param namespaces: The namespaces to connect as a string or list of
strings. If not given, the namespaces that have strings. If not given, the namespaces that have
registered event handlers are connected. registered event handlers are connected.

10
src/socketio/async_server.py

@ -18,7 +18,8 @@ class AsyncServer(base_server.BaseServer):
"""A Socket.IO server for asyncio. """A Socket.IO server for asyncio.
This class implements a fully compliant Socket.IO web server with support This class implements a fully compliant Socket.IO web server with support
for websocket and long-polling transports, compatible with the asyncio for Engine.IO transports, including websocket and long-polling, compatible
with the asyncio
framework. framework.
:param client_manager: The client manager instance that will manage the :param client_manager: The client manager instance that will manage the
@ -104,9 +105,10 @@ class AsyncServer(base_server.BaseServer):
inactive clients are closed. Set to ``False`` to inactive clients are closed. Set to ``False`` to
disable the monitoring task (not recommended). The disable the monitoring task (not recommended). The
default is ``True``. default is ``True``.
:param transports: The list of allowed transports. Valid transports :param transports: The list of allowed transports. By default this is
are ``'polling'`` and ``'websocket'``. Defaults to ``['polling', 'websocket']``. Additional transports
``['polling', 'websocket']``. (for example ``'webtransport'``) may be available when
supported by the underlying Engine.IO server.
:param engineio_logger: To enable Engine.IO logging set to ``True`` or pass :param engineio_logger: To enable Engine.IO logging set to ``True`` or pass
a logger object to use. To disable logging set to a logger object to use. To disable logging set to
``False``. The default is ``False``. Note that ``False``. The default is ``False``. Note that

19
src/socketio/async_simple_client.py

@ -7,7 +7,8 @@ class AsyncSimpleClient:
"""A Socket.IO client. """A Socket.IO client.
This class implements a simple, yet fully compliant Socket.IO web client This class implements a simple, yet fully compliant Socket.IO web client
with support for websocket and long-polling transports. with support for Engine.IO transports, including websocket and
long-polling.
The positional and keyword arguments given in the constructor are passed The positional and keyword arguments given in the constructor are passed
to the underlying :func:`socketio.AsyncClient` object. to the underlying :func:`socketio.AsyncClient` object.
@ -43,10 +44,13 @@ class AsyncSimpleClient:
more string key/value pairs. If a function is provided, more string key/value pairs. If a function is provided,
the client will invoke it to obtain the authentication the client will invoke it to obtain the authentication
data each time a connection or reconnection is attempted. data each time a connection or reconnection is attempted.
:param transports: The list of allowed transports. Valid transports :param transports: The list of allowed transports. Commonly
are ``'polling'`` and ``'websocket'``. If not ``'polling'`` and ``'websocket'`` are available.
given, the polling transport is connected first, Additional transports (for example
then an upgrade to websocket is attempted. ``'webtransport'``) may be available when
supported by the underlying Engine.IO client. If
not given, the polling transport is connected
first, then an upgrade to websocket is attempted.
:param namespace: The namespace to connect to as a string. If not :param namespace: The namespace to connect to as a string. If not
given, the default namespace ``/`` is used. given, the default namespace ``/`` is used.
:param socketio_path: The endpoint where the Socket.IO server is :param socketio_path: The endpoint where the Socket.IO server is
@ -102,8 +106,9 @@ class AsyncSimpleClient:
def transport(self): def transport(self):
"""The name of the transport currently in use. """The name of the transport currently in use.
The transport is returned as a string and can be one of ``polling`` Common values are ``'polling'`` and ``'websocket'``. Additional
and ``websocket``. values may be possible when supported by the underlying Engine.IO
implementation.
""" """
return self.client.transport() if self.client else '' return self.client.transport() if self.client else ''

5
src/socketio/base_client.py

@ -229,8 +229,9 @@ class BaseClient:
def transport(self): def transport(self):
"""Return the name of the transport used by the client. """Return the name of the transport used by the client.
The two possible values returned by this function are ``'polling'`` Common values returned by this function are ``'polling'`` and
and ``'websocket'``. ``'websocket'``. Additional values may be possible when supported by
the underlying Engine.IO implementation.
""" """
return self.eio.transport() return self.eio.transport()

5
src/socketio/base_server.py

@ -193,8 +193,9 @@ class BaseServer:
def transport(self, sid, namespace=None): def transport(self, sid, namespace=None):
"""Return the name of the transport used by the client. """Return the name of the transport used by the client.
The two possible values returned by this function are ``'polling'`` Common values returned by this function are ``'polling'`` and
and ``'websocket'``. ``'websocket'``. Additional values may be possible when supported by
the underlying Engine.IO implementation.
:param sid: The session of the client. :param sid: The session of the client.
:param namespace: The Socket.IO namespace. If this argument is omitted :param namespace: The Socket.IO namespace. If this argument is omitted

13
src/socketio/client.py

@ -11,7 +11,7 @@ class Client(base_client.BaseClient):
"""A Socket.IO client. """A Socket.IO client.
This class implements a fully compliant Socket.IO web client with support This class implements a fully compliant Socket.IO web client with support
for websocket and long-polling transports. for Engine.IO transports, including websocket and long-polling.
:param reconnection: ``True`` if the client should automatically attempt to :param reconnection: ``True`` if the client should automatically attempt to
reconnect to the server after an interruption, or reconnect to the server after an interruption, or
@ -90,10 +90,13 @@ class Client(base_client.BaseClient):
more string key/value pairs. If a function is provided, more string key/value pairs. If a function is provided,
the client will invoke it to obtain the authentication the client will invoke it to obtain the authentication
data each time a connection or reconnection is attempted. data each time a connection or reconnection is attempted.
:param transports: The list of allowed transports. Valid transports :param transports: The list of allowed transports. Commonly
are ``'polling'`` and ``'websocket'``. If not ``'polling'`` and ``'websocket'`` are available.
given, the polling transport is connected first, Additional transports (for example
then an upgrade to websocket is attempted. ``'webtransport'``) may be available when
supported by the underlying Engine.IO client. If
not given, the polling transport is connected
first, then an upgrade to websocket is attempted.
:param namespaces: The namespaces to connect as a string or list of :param namespaces: The namespaces to connect as a string or list of
strings. If not given, the namespaces that have strings. If not given, the namespaces that have
registered event handlers are connected. registered event handlers are connected.

9
src/socketio/server.py

@ -13,7 +13,7 @@ class Server(base_server.BaseServer):
"""A Socket.IO server. """A Socket.IO server.
This class implements a fully compliant Socket.IO web server with support This class implements a fully compliant Socket.IO web server with support
for websocket and long-polling transports. for Engine.IO transports, including websocket and long-polling.
:param client_manager: The client manager instance that will manage the :param client_manager: The client manager instance that will manage the
client list. When this is omitted, the client list client list. When this is omitted, the client list
@ -107,9 +107,10 @@ class Server(base_server.BaseServer):
inactive clients are closed. Set to ``False`` to inactive clients are closed. Set to ``False`` to
disable the monitoring task (not recommended). The disable the monitoring task (not recommended). The
default is ``True``. default is ``True``.
:param transports: The list of allowed transports. Valid transports :param transports: The list of allowed transports. By default this is
are ``'polling'`` and ``'websocket'``. Defaults to ``['polling', 'websocket']``. Additional transports
``['polling', 'websocket']``. (for example ``'webtransport'``) may be available when
supported by the underlying Engine.IO server.
:param engineio_logger: To enable Engine.IO logging set to ``True`` or pass :param engineio_logger: To enable Engine.IO logging set to ``True`` or pass
a logger object to use. To disable logging set to a logger object to use. To disable logging set to
``False``. The default is ``False``. Note that ``False``. The default is ``False``. Note that

19
src/socketio/simple_client.py

@ -7,7 +7,8 @@ class SimpleClient:
"""A Socket.IO client. """A Socket.IO client.
This class implements a simple, yet fully compliant Socket.IO web client This class implements a simple, yet fully compliant Socket.IO web client
with support for websocket and long-polling transports. with support for Engine.IO transports, including websocket and
long-polling.
The positional and keyword arguments given in the constructor are passed The positional and keyword arguments given in the constructor are passed
to the underlying :func:`socketio.Client` object. to the underlying :func:`socketio.Client` object.
@ -42,10 +43,13 @@ class SimpleClient:
more string key/value pairs. If a function is provided, more string key/value pairs. If a function is provided,
the client will invoke it to obtain the authentication the client will invoke it to obtain the authentication
data each time a connection or reconnection is attempted. data each time a connection or reconnection is attempted.
:param transports: The list of allowed transports. Valid transports :param transports: The list of allowed transports. Commonly
are ``'polling'`` and ``'websocket'``. If not ``'polling'`` and ``'websocket'`` are available.
given, the polling transport is connected first, Additional transports (for example
then an upgrade to websocket is attempted. ``'webtransport'``) may be available when
supported by the underlying Engine.IO client. If
not given, the polling transport is connected
first, then an upgrade to websocket is attempted.
:param namespace: The namespace to connect to as a string. If not :param namespace: The namespace to connect to as a string. If not
given, the default namespace ``/`` is used. given, the default namespace ``/`` is used.
:param socketio_path: The endpoint where the Socket.IO server is :param socketio_path: The endpoint where the Socket.IO server is
@ -100,8 +104,9 @@ class SimpleClient:
def transport(self): def transport(self):
"""The name of the transport currently in use. """The name of the transport currently in use.
The transport is returned as a string and can be one of ``polling`` Common values are ``'polling'`` and ``'websocket'``. Additional
and ``websocket``. values may be possible when supported by the underlying Engine.IO
implementation.
""" """
return self.client.transport() if self.client else '' return self.client.transport() if self.client else ''

35
tests/async/test_admin.py

@ -249,6 +249,41 @@ class TestAsyncAdmin:
elif socket['id'] == sid3: elif socket['id'] == sid3:
assert socket['rooms'] == [sid3] assert socket['rooms'] == [sid3]
@with_instrumented_server()
def test_admin_websocket_only_client_transport(self):
with socketio.SimpleClient(reconnection=False) as admin_client:
admin_client.connect(
'http://localhost:8900', namespace='/admin',
transports=['websocket'])
events = self._expect({'config': 1, 'all_sockets': 1,
'server_stats': 2}, admin_client)
assert events['all_sockets'][0]['transport'] == 'websocket'
assert events['server_stats']['pollingClientsCount'] == 0
@with_instrumented_server()
def test_admin_polling_count_ignores_websocket_only_clients(self):
with socketio.SimpleClient(reconnection=False) as ws_client, \
socketio.SimpleClient(reconnection=False) as polling_client, \
socketio.SimpleClient(reconnection=False) as admin_client:
ws_client.connect('http://localhost:8900',
transports=['websocket'])
saved_check_for_upgrade = self.isvr._check_for_upgrade
self.isvr._check_for_upgrade = mock.AsyncMock()
polling_client.connect('http://localhost:8900',
transports=['polling'])
self.isvr._check_for_upgrade = saved_check_for_upgrade
admin_client.connect(
'http://localhost:8900', namespace='/admin',
transports=['websocket'])
events = self._expect({'config': 1, 'all_sockets': 1,
'server_stats': 2}, admin_client)
assert events['server_stats']['clientsCount'] == 3
assert events['server_stats']['pollingClientsCount'] == 1
@with_instrumented_server(mode='production', read_only=True) @with_instrumented_server(mode='production', read_only=True)
def test_admin_connect_production(self): def test_admin_connect_production(self):
with socketio.SimpleClient(reconnection=False) as admin_client: with socketio.SimpleClient(reconnection=False) as admin_client:

35
tests/common/test_admin.py

@ -225,6 +225,41 @@ class TestAdmin:
elif socket['id'] == sid3: elif socket['id'] == sid3:
assert socket['rooms'] == [sid3] assert socket['rooms'] == [sid3]
@with_instrumented_server()
def test_admin_websocket_only_client_transport(self):
with socketio.SimpleClient(reconnection=False) as admin_client:
admin_client.connect(
'http://localhost:8900', namespace='/admin',
transports=['websocket'])
events = self._expect({'config': 1, 'all_sockets': 1,
'server_stats': 2}, admin_client)
assert events['all_sockets'][0]['transport'] == 'websocket'
assert events['server_stats']['pollingClientsCount'] == 0
@with_instrumented_server()
def test_admin_polling_count_ignores_websocket_only_clients(self):
with socketio.SimpleClient(reconnection=False) as ws_client, \
socketio.SimpleClient(reconnection=False) as polling_client, \
socketio.SimpleClient(reconnection=False) as admin_client:
ws_client.connect('http://localhost:8900',
transports=['websocket'])
saved_check_for_upgrade = self.isvr._check_for_upgrade
self.isvr._check_for_upgrade = mock.MagicMock()
polling_client.connect('http://localhost:8900',
transports=['polling'])
self.isvr._check_for_upgrade = saved_check_for_upgrade
admin_client.connect(
'http://localhost:8900', namespace='/admin',
transports=['websocket'])
events = self._expect({'config': 1, 'all_sockets': 1,
'server_stats': 2}, admin_client)
assert events['server_stats']['clientsCount'] == 3
assert events['server_stats']['pollingClientsCount'] == 1
@with_instrumented_server(mode='production', read_only=True) @with_instrumented_server(mode='production', read_only=True)
def test_admin_connect_production(self): def test_admin_connect_production(self):
with socketio.SimpleClient(reconnection=False) as admin_client: with socketio.SimpleClient(reconnection=False) as admin_client:

Loading…
Cancel
Save