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

26
src/socketio/async_admin.py

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

13
src/socketio/async_client.py

@ -16,7 +16,7 @@ class AsyncClient(base_client.BaseClient):
"""A Socket.IO client for asyncio.
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
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,
the client will invoke it to obtain the authentication
data each time a connection or reconnection is attempted.
:param transports: The list of allowed transports. Valid transports
are ``'polling'`` and ``'websocket'``. If not
given, the polling transport is connected first,
then an upgrade to websocket is attempted.
:param transports: The list of allowed transports. Commonly
``'polling'`` and ``'websocket'`` are available.
Additional transports (for example
``'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
strings. If not given, the namespaces that have
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.
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.
: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
disable the monitoring task (not recommended). The
default is ``True``.
:param transports: The list of allowed transports. Valid transports
are ``'polling'`` and ``'websocket'``. Defaults to
``['polling', 'websocket']``.
:param transports: The list of allowed transports. By default this is
``['polling', 'websocket']``. Additional transports
(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
a logger object to use. To disable logging set to
``False``. The default is ``False``. Note that

19
src/socketio/async_simple_client.py

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

5
src/socketio/base_client.py

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

5
src/socketio/base_server.py

@ -193,8 +193,9 @@ class BaseServer:
def transport(self, sid, namespace=None):
"""Return the name of the transport used by the client.
The two possible values returned by this function are ``'polling'``
and ``'websocket'``.
Common values returned by this function are ``'polling'`` and
``'websocket'``. Additional values may be possible when supported by
the underlying Engine.IO implementation.
:param sid: The session of the client.
: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.
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
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,
the client will invoke it to obtain the authentication
data each time a connection or reconnection is attempted.
:param transports: The list of allowed transports. Valid transports
are ``'polling'`` and ``'websocket'``. If not
given, the polling transport is connected first,
then an upgrade to websocket is attempted.
:param transports: The list of allowed transports. Commonly
``'polling'`` and ``'websocket'`` are available.
Additional transports (for example
``'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
strings. If not given, the namespaces that have
registered event handlers are connected.

9
src/socketio/server.py

@ -13,7 +13,7 @@ class Server(base_server.BaseServer):
"""A Socket.IO server.
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
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
disable the monitoring task (not recommended). The
default is ``True``.
:param transports: The list of allowed transports. Valid transports
are ``'polling'`` and ``'websocket'``. Defaults to
``['polling', 'websocket']``.
:param transports: The list of allowed transports. By default this is
``['polling', 'websocket']``. Additional transports
(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
a logger object to use. To disable logging set to
``False``. The default is ``False``. Note that

19
src/socketio/simple_client.py

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

35
tests/async/test_admin.py

@ -249,6 +249,41 @@ class TestAsyncAdmin:
elif socket['id'] == 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)
def test_admin_connect_production(self):
with socketio.SimpleClient(reconnection=False) as admin_client:

35
tests/common/test_admin.py

@ -225,6 +225,41 @@ class TestAdmin:
elif socket['id'] == 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)
def test_admin_connect_production(self):
with socketio.SimpleClient(reconnection=False) as admin_client:

Loading…
Cancel
Save