codekingpro/portable-devtools
114k
1import asyncio2 3import engineio4 5from . import async_manager6from . import base_server7from . import exceptions8from . import packet9 10# this set is used to keep references to background tasks to prevent them from11# being garbage collected mid-execution. Solution taken from12# https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task13task_reference_holder = set()14 15 16class AsyncServer(base_server.BaseServer):17 """A Socket.IO server for asyncio.18 19 This class implements a fully compliant Socket.IO web server with support20 for websocket and long-polling transports, compatible with the asyncio21 framework.22 23 :param client_manager: The client manager instance that will manage the24 client list. When this is omitted, the client list25 is stored in an in-memory structure, so the use of26 multiple connected servers is not possible.27 :param logger: To enable logging set to ``True`` or pass a logger object to28 use. To disable logging set to ``False``. Note that fatal29 errors are logged even when ``logger`` is ``False``.30 :param json: An alternative json module to use for encoding and decoding31 packets. Custom json modules must have ``dumps`` and ``loads``32 functions that are compatible with the standard library33 versions.34 :param async_handlers: If set to ``True``, event handlers for a client are35 executed in separate threads. To run handlers for a36 client synchronously, set to ``False``. The default37 is ``True``.38 :param always_connect: When set to ``False``, new connections are39 provisory until the connect handler returns40 something other than ``False``, at which point they41 are accepted. When set to ``True``, connections are42 immediately accepted, and then if the connect43 handler returns ``False`` a disconnect is issued.44 Set to ``True`` if you need to emit events from the45 connect handler and your client is confused when it46 receives events before the connection acceptance.47 In any other case use the default of ``False``.48 :param namespaces: a list of namespaces that are accepted, in addition to49 any namespaces for which handlers have been defined. The50 default is `['/']`, which always accepts connections to51 the default namespace. Set to `'*'` to accept all52 namespaces.53 :param kwargs: Connection parameters for the underlying Engine.IO server.54 55 The Engine.IO configuration supports the following settings:56 57 :param async_mode: The asynchronous model to use. See the Deployment58 section in the documentation for a description of the59 available options. Valid async modes are "aiohttp",60 "sanic", "tornado" and "asgi". If this argument is not61 given, "aiohttp" is tried first, followed by "sanic",62 "tornado", and finally "asgi". The first async mode that63 has all its dependencies installed is the one that is64 chosen.65 :param ping_interval: The interval in seconds at which the server pings66 the client. The default is 25 seconds. For advanced67 control, a two element tuple can be given, where68 the first number is the ping interval and the second69 is a grace period added by the server.70 :param ping_timeout: The time in seconds that the client waits for the71 server to respond before disconnecting. The default72 is 20 seconds.73 :param max_http_buffer_size: The maximum size that is accepted for incoming74 messages. The default is 1,000,000 bytes. In75 spite of its name, the value set in this76 argument is enforced for HTTP long-polling and77 WebSocket connections.78 :param allow_upgrades: Whether to allow transport upgrades or not. The79 default is ``True``.80 :param http_compression: Whether to compress packages when using the81 polling transport. The default is ``True``.82 :param compression_threshold: Only compress messages when their byte size83 is greater than this value. The default is84 1024 bytes.85 :param cookie: If set to a string, it is the name of the HTTP cookie the86 server sends back to the client containing the client87 session id. If set to a dictionary, the ``'name'`` key88 contains the cookie name and other keys define cookie89 attributes, where the value of each attribute can be a90 string, a callable with no arguments, or a boolean. If set91 to ``None`` (the default), a cookie is not sent to the92 client.93 :param cors_allowed_origins: Origin or list of origins that are allowed to94 connect to this server. Only the same origin95 is allowed by default. Set this argument to96 ``'*'`` to allow all origins, or to ``[]`` to97 disable CORS handling.98 :param cors_credentials: Whether credentials (cookies, authentication) are99 allowed in requests to this server. The default is100 ``True``.101 :param monitor_clients: If set to ``True``, a background task will ensure102 inactive clients are closed. Set to ``False`` to103 disable the monitoring task (not recommended). The104 default is ``True``.105 :param engineio_logger: To enable Engine.IO logging set to ``True`` or pass106 a logger object to use. To disable logging set to107 ``False``. The default is ``False``. Note that108 fatal errors are logged even when109 ``engineio_logger`` is ``False``.110 """111 def __init__(self, client_manager=None, logger=False, json=None,112 async_handlers=True, namespaces=None, **kwargs):113 if client_manager is None:114 client_manager = async_manager.AsyncManager()115 super().__init__(client_manager=client_manager, logger=logger,116 json=json, async_handlers=async_handlers,117 namespaces=namespaces, **kwargs)118 119 def is_asyncio_based(self):120 return True121 122 def attach(self, app, socketio_path='socket.io'):123 """Attach the Socket.IO server to an application."""124 self.eio.attach(app, socketio_path)125 126 async def emit(self, event, data=None, to=None, room=None, skip_sid=None,127 namespace=None, callback=None, ignore_queue=False):128 """Emit a custom event to one or more connected clients.129 130 :param event: The event name. It can be any string. The event names131 ``'connect'``, ``'message'`` and ``'disconnect'`` are132 reserved and should not be used.133 :param data: The data to send to the client or clients. Data can be of134 type ``str``, ``bytes``, ``list`` or ``dict``. To send135 multiple arguments, use a tuple where each element is of136 one of the types indicated above.137 :param to: The recipient of the message. This can be set to the138 session ID of a client to address only that client, to any139 any custom room created by the application to address all140 the clients in that room, or to a list of custom room141 names. If this argument is omitted the event is broadcasted142 to all connected clients.143 :param room: Alias for the ``to`` parameter.144 :param skip_sid: The session ID of a client to skip when broadcasting145 to a room or to all clients. This can be used to146 prevent a message from being sent to the sender.147 :param namespace: The Socket.IO namespace for the event. If this148 argument is omitted the event is emitted to the149 default namespace.150 :param callback: If given, this function will be called to acknowledge151 the client has received the message. The arguments152 that will be passed to the function are those provided153 by the client. Callback functions can only be used154 when addressing an individual client.155 :param ignore_queue: Only used when a message queue is configured. If156 set to ``True``, the event is emitted to the157 clients directly, without going through the queue.158 This is more efficient, but only works when a159 single server process is used. It is recommended160 to always leave this parameter with its default161 value of ``False``.162 163 Note: this method is not designed to be used concurrently. If multiple164 tasks are emitting at the same time to the same client connection, then165 messages composed of multiple packets may end up being sent in an166 incorrect sequence. Use standard concurrency solutions (such as a Lock167 object) to prevent this situation.168 169 Note 2: this method is a coroutine.170 """171 namespace = namespace or '/'172 room = to or room173 self.logger.info('emitting event "%s" to %s [%s]', event,174 room or 'all', namespace)175 await self.manager.emit(event, data, namespace, room=room,176 skip_sid=skip_sid, callback=callback,177 ignore_queue=ignore_queue)178 179 async def send(self, data, to=None, room=None, skip_sid=None,180 namespace=None, callback=None, ignore_queue=False):181 """Send a message to one or more connected clients.182 183 This function emits an event with the name ``'message'``. Use184 :func:`emit` to issue custom event names.185 186 :param data: The data to send to the client or clients. Data can be of187 type ``str``, ``bytes``, ``list`` or ``dict``. To send188 multiple arguments, use a tuple where each element is of189 one of the types indicated above.190 :param to: The recipient of the message. This can be set to the191 session ID of a client to address only that client, to any192 any custom room created by the application to address all193 the clients in that room, or to a list of custom room194 names. If this argument is omitted the event is broadcasted195 to all connected clients.196 :param room: Alias for the ``to`` parameter.197 :param skip_sid: The session ID of a client to skip when broadcasting198 to a room or to all clients. This can be used to199 prevent a message from being sent to the sender.200 :param namespace: The Socket.IO namespace for the event. If this201 argument is omitted the event is emitted to the202 default namespace.203 :param callback: If given, this function will be called to acknowledge204 the client has received the message. The arguments205 that will be passed to the function are those provided206 by the client. Callback functions can only be used207 when addressing an individual client.208 :param ignore_queue: Only used when a message queue is configured. If209 set to ``True``, the event is emitted to the210 clients directly, without going through the queue.211 This is more efficient, but only works when a212 single server process is used. It is recommended213 to always leave this parameter with its default214 value of ``False``.215 216 Note: this method is a coroutine.217 """218 await self.emit('message', data=data, to=to, room=room,219 skip_sid=skip_sid, namespace=namespace,220 callback=callback, ignore_queue=ignore_queue)221 222 async def call(self, event, data=None, to=None, sid=None, namespace=None,223 timeout=60, ignore_queue=False):224 """Emit a custom event to a client and wait for the response.225 226 This method issues an emit with a callback and waits for the callback227 to be invoked before returning. If the callback isn't invoked before228 the timeout, then a ``TimeoutError`` exception is raised. If the229 Socket.IO connection drops during the wait, this method still waits230 until the specified timeout.231 232 :param event: The event name. It can be any string. The event names233 ``'connect'``, ``'message'`` and ``'disconnect'`` are234 reserved and should not be used.235 :param data: The data to send to the client or clients. Data can be of236 type ``str``, ``bytes``, ``list`` or ``dict``. To send237 multiple arguments, use a tuple where each element is of238 one of the types indicated above.239 :param to: The session ID of the recipient client.240 :param sid: Alias for the ``to`` parameter.241 :param namespace: The Socket.IO namespace for the event. If this242 argument is omitted the event is emitted to the243 default namespace.244 :param timeout: The waiting timeout. If the timeout is reached before245 the client acknowledges the event, then a246 ``TimeoutError`` exception is raised.247 :param ignore_queue: Only used when a message queue is configured. If248 set to ``True``, the event is emitted to the249 client directly, without going through the queue.250 This is more efficient, but only works when a251 single server process is used. It is recommended252 to always leave this parameter with its default253 value of ``False``.254 255 Note: this method is not designed to be used concurrently. If multiple256 tasks are emitting at the same time to the same client connection, then257 messages composed of multiple packets may end up being sent in an258 incorrect sequence. Use standard concurrency solutions (such as a Lock259 object) to prevent this situation.260 261 Note 2: this method is a coroutine.262 """263 if to is None and sid is None:264 raise ValueError('Cannot use call() to broadcast.')265 if not self.async_handlers:266 raise RuntimeError(267 'Cannot use call() when async_handlers is False.')268 callback_event = self.eio.create_event()269 callback_args = []270 271 def event_callback(*args):272 callback_args.append(args)273 callback_event.set()274 275 await self.emit(event, data=data, room=to or sid, namespace=namespace,276 callback=event_callback, ignore_queue=ignore_queue)277 try:278 await asyncio.wait_for(callback_event.wait(), timeout)279 except asyncio.TimeoutError:280 raise exceptions.TimeoutError() from None281 return callback_args[0] if len(callback_args[0]) > 1 \282 else callback_args[0][0] if len(callback_args[0]) == 1 \283 else None284 285 async def enter_room(self, sid, room, namespace=None):286 """Enter a room.287 288 This function adds the client to a room. The :func:`emit` and289 :func:`send` functions can optionally broadcast events to all the290 clients in a room.291 292 :param sid: Session ID of the client.293 :param room: Room name. If the room does not exist it is created.294 :param namespace: The Socket.IO namespace for the event. If this295 argument is omitted the default namespace is used.296 297 Note: this method is a coroutine.298 """299 namespace = namespace or '/'300 self.logger.info('%s is entering room %s [%s]', sid, room, namespace)301 await self.manager.enter_room(sid, namespace, room)302 303 async def leave_room(self, sid, room, namespace=None):304 """Leave a room.305 306 This function removes the client from a room.307 308 :param sid: Session ID of the client.309 :param room: Room name.310 :param namespace: The Socket.IO namespace for the event. If this311 argument is omitted the default namespace is used.312 313 Note: this method is a coroutine.314 """315 namespace = namespace or '/'316 self.logger.info('%s is leaving room %s [%s]', sid, room, namespace)317 await self.manager.leave_room(sid, namespace, room)318 319 async def close_room(self, room, namespace=None):320 """Close a room.321 322 This function removes all the clients from the given room.323 324 :param room: Room name.325 :param namespace: The Socket.IO namespace for the event. If this326 argument is omitted the default namespace is used.327 328 Note: this method is a coroutine.329 """330 namespace = namespace or '/'331 self.logger.info('room %s is closing [%s]', room, namespace)332 await self.manager.close_room(room, namespace)333 334 async def get_session(self, sid, namespace=None):335 """Return the user session for a client.336 337 :param sid: The session id of the client.338 :param namespace: The Socket.IO namespace. If this argument is omitted339 the default namespace is used.340 341 The return value is a dictionary. Modifications made to this342 dictionary are not guaranteed to be preserved. If you want to modify343 the user session, use the ``session`` context manager instead.344 """345 namespace = namespace or '/'346 eio_sid = self.manager.eio_sid_from_sid(sid, namespace)347 eio_session = await self.eio.get_session(eio_sid)348 return eio_session.setdefault(namespace, {})349 350 async def save_session(self, sid, session, namespace=None):351 """Store the user session for a client.352 353 :param sid: The session id of the client.354 :param session: The session dictionary.355 :param namespace: The Socket.IO namespace. If this argument is omitted356 the default namespace is used.357 """358 namespace = namespace or '/'359 eio_sid = self.manager.eio_sid_from_sid(sid, namespace)360 eio_session = await self.eio.get_session(eio_sid)361 eio_session[namespace] = session362 363 def session(self, sid, namespace=None):364 """Return the user session for a client with context manager syntax.365 366 :param sid: The session id of the client.367 368 This is a context manager that returns the user session dictionary for369 the client. Any changes that are made to this dictionary inside the370 context manager block are saved back to the session. Example usage::371 372 @eio.on('connect')373 def on_connect(sid, environ):374 username = authenticate_user(environ)375 if not username:376 return False377 with eio.session(sid) as session:378 session['username'] = username379 380 @eio.on('message')381 def on_message(sid, msg):382 async with eio.session(sid) as session:383 print('received message from ', session['username'])384 """385 class _session_context_manager(object):386 def __init__(self, server, sid, namespace):387 self.server = server388 self.sid = sid389 self.namespace = namespace390 self.session = None391 392 async def __aenter__(self):393 self.session = await self.server.get_session(394 sid, namespace=self.namespace)395 return self.session396 397 async def __aexit__(self, *args):398 await self.server.save_session(sid, self.session,399 namespace=self.namespace)400 401 return _session_context_manager(self, sid, namespace)402 403 async def disconnect(self, sid, namespace=None, ignore_queue=False):404 """Disconnect a client.405 406 :param sid: Session ID of the client.407 :param namespace: The Socket.IO namespace to disconnect. If this408 argument is omitted the default namespace is used.409 :param ignore_queue: Only used when a message queue is configured. If410 set to ``True``, the disconnect is processed411 locally, without broadcasting on the queue. It is412 recommended to always leave this parameter with413 its default value of ``False``.414 415 Note: this method is a coroutine.416 """417 namespace = namespace or '/'418 if ignore_queue:419 delete_it = self.manager.is_connected(sid, namespace)420 else:421 delete_it = await self.manager.can_disconnect(sid, namespace)422 if delete_it:423 self.logger.info('Disconnecting %s [%s]', sid, namespace)424 eio_sid = self.manager.pre_disconnect(sid, namespace=namespace)425 await self._send_packet(eio_sid, self.packet_class(426 packet.DISCONNECT, namespace=namespace))427 await self._trigger_event('disconnect', namespace, sid)428 await self.manager.disconnect(sid, namespace=namespace,429 ignore_queue=True)430 431 async def shutdown(self):432 """Stop Socket.IO background tasks.433 434 This method stops all background activity initiated by the Socket.IO435 server. It must be called before shutting down the web server.436 """437 self.logger.info('Socket.IO is shutting down')438 await self.eio.shutdown()439 440 async def handle_request(self, *args, **kwargs):441 """Handle an HTTP request from the client.442 443 This is the entry point of the Socket.IO application. This function444 returns the HTTP response body to deliver to the client.445 446 Note: this method is a coroutine.447 """448 return await self.eio.handle_request(*args, **kwargs)449 450 def start_background_task(self, target, *args, **kwargs):451 """Start a background task using the appropriate async model.452 453 This is a utility function that applications can use to start a454 background task using the method that is compatible with the455 selected async mode.456 457 :param target: the target function to execute. Must be a coroutine.458 :param args: arguments to pass to the function.459 :param kwargs: keyword arguments to pass to the function.460 461 The return value is a ``asyncio.Task`` object.462 """463 return self.eio.start_background_task(target, *args, **kwargs)464 465 async def sleep(self, seconds=0):466 """Sleep for the requested amount of time using the appropriate async467 model.468 469 This is a utility function that applications can use to put a task to470 sleep without having to worry about using the correct call for the471 selected async mode.472 473 Note: this method is a coroutine.474 """475 return await self.eio.sleep(seconds)476 477 def instrument(self, auth=None, mode='development', read_only=False,478 server_id=None, namespace='/admin',479 server_stats_interval=2):480 """Instrument the Socket.IO server for monitoring with the `Socket.IO481 Admin UI <https://socket.io/docs/v4/admin-ui/>`_.482 483 :param auth: Authentication credentials for Admin UI access. Set to a484 dictionary with the expected login (usually ``username``485 and ``password``) or a list of dictionaries if more than486 one set of credentials need to be available. For more487 complex authentication methods, set to a callable that488 receives the authentication dictionary as an argument and489 returns ``True`` if the user is allowed or ``False``490 otherwise. To disable authentication, set this argument to491 ``False`` (not recommended, never do this on a production492 server).493 :param mode: The reporting mode. The default is ``'development'``,494 which is best used while debugging, as it may have a495 significant performance effect. Set to ``'production'`` to496 reduce the amount of information that is reported to the497 admin UI.498 :param read_only: If set to ``True``, the admin interface will be499 read-only, with no option to modify room assignments500 or disconnect clients. The default is ``False``.501 :param server_id: The server name to use for this server. If this502 argument is omitted, the server generates its own503 name.504 :param namespace: The Socket.IO namespace to use for the admin505 interface. The default is ``/admin``.506 :param server_stats_interval: The interval in seconds at which the507 server emits a summary of it stats to all508 connected admins.509 """510 from .async_admin import InstrumentedAsyncServer511 return InstrumentedAsyncServer(512 self, auth=auth, mode=mode, read_only=read_only,513 server_id=server_id, namespace=namespace,514 server_stats_interval=server_stats_interval)515 516 async def _send_packet(self, eio_sid, pkt):517 """Send a Socket.IO packet to a client."""518 encoded_packet = pkt.encode()519 if isinstance(encoded_packet, list):520 for ep in encoded_packet:521 await self.eio.send(eio_sid, ep)522 else:523 await self.eio.send(eio_sid, encoded_packet)524 525 async def _send_eio_packet(self, eio_sid, eio_pkt):526 """Send a raw Engine.IO packet to a client."""527 await self.eio.send_packet(eio_sid, eio_pkt)528 529 async def _handle_connect(self, eio_sid, namespace, data):530 """Handle a client connection request."""531 namespace = namespace or '/'532 sid = None533 if namespace in self.handlers or namespace in self.namespace_handlers \534 or self.namespaces == '*' or namespace in self.namespaces:535 sid = await self.manager.connect(eio_sid, namespace)536 if sid is None:537 await self._send_packet(eio_sid, self.packet_class(538 packet.CONNECT_ERROR, data='Unable to connect',539 namespace=namespace))540 return541 542 if self.always_connect:543 await self._send_packet(eio_sid, self.packet_class(544 packet.CONNECT, {'sid': sid}, namespace=namespace))545 fail_reason = exceptions.ConnectionRefusedError().error_args546 try:547 if data:548 success = await self._trigger_event(549 'connect', namespace, sid, self.environ[eio_sid], data)550 else:551 try:552 success = await self._trigger_event(553 'connect', namespace, sid, self.environ[eio_sid])554 except TypeError:555 success = await self._trigger_event(556 'connect', namespace, sid, self.environ[eio_sid], None)557 except exceptions.ConnectionRefusedError as exc:558 fail_reason = exc.error_args559 success = False560 561 if success is False:562 if self.always_connect:563 self.manager.pre_disconnect(sid, namespace)564 await self._send_packet(eio_sid, self.packet_class(565 packet.DISCONNECT, data=fail_reason, namespace=namespace))566 else:567 await self._send_packet(eio_sid, self.packet_class(568 packet.CONNECT_ERROR, data=fail_reason,569 namespace=namespace))570 await self.manager.disconnect(sid, namespace, ignore_queue=True)571 elif not self.always_connect:572 await self._send_packet(eio_sid, self.packet_class(573 packet.CONNECT, {'sid': sid}, namespace=namespace))574 575 async def _handle_disconnect(self, eio_sid, namespace):576 """Handle a client disconnect."""577 namespace = namespace or '/'578 sid = self.manager.sid_from_eio_sid(eio_sid, namespace)579 if not self.manager.is_connected(sid, namespace): # pragma: no cover580 return581 self.manager.pre_disconnect(sid, namespace=namespace)582 await self._trigger_event('disconnect', namespace, sid)583 await self.manager.disconnect(sid, namespace, ignore_queue=True)584 585 async def _handle_event(self, eio_sid, namespace, id, data):586 """Handle an incoming client event."""587 namespace = namespace or '/'588 sid = self.manager.sid_from_eio_sid(eio_sid, namespace)589 self.logger.info('received event "%s" from %s [%s]', data[0], sid,590 namespace)591 if not self.manager.is_connected(sid, namespace):592 self.logger.warning('%s is not connected to namespace %s',593 sid, namespace)594 return595 if self.async_handlers:596 task = self.start_background_task(597 self._handle_event_internal, self, sid, eio_sid, data,598 namespace, id)599 task_reference_holder.add(task)600 task.add_done_callback(task_reference_holder.discard)601 else:602 await self._handle_event_internal(self, sid, eio_sid, data,603 namespace, id)604 605 async def _handle_event_internal(self, server, sid, eio_sid, data,606 namespace, id):607 r = await server._trigger_event(data[0], namespace, sid, *data[1:])608 if r != self.not_handled and id is not None:609 # send ACK packet with the response returned by the handler610 # tuples are expanded as multiple arguments611 if r is None:612 data = []613 elif isinstance(r, tuple):614 data = list(r)615 else:616 data = [r]617 await server._send_packet(eio_sid, self.packet_class(618 packet.ACK, namespace=namespace, id=id, data=data))619 620 async def _handle_ack(self, eio_sid, namespace, id, data):621 """Handle ACK packets from the client."""622 namespace = namespace or '/'623 sid = self.manager.sid_from_eio_sid(eio_sid, namespace)624 self.logger.info('received ack from %s [%s]', sid, namespace)625 await self.manager.trigger_callback(sid, id, data)626 627 async def _trigger_event(self, event, namespace, *args):628 """Invoke an application event handler."""629 # first see if we have an explicit handler for the event630 handler, args = self._get_event_handler(event, namespace, args)631 if handler:632 if asyncio.iscoroutinefunction(handler):633 try:634 ret = await handler(*args)635 except asyncio.CancelledError: # pragma: no cover636 ret = None637 else:638 ret = handler(*args)639 return ret640 # or else, forward the event to a namespace handler if one exists641 handler, args = self._get_namespace_handler(namespace, args)642 if handler:643 return await handler.trigger_event(event, *args)644 else:645 return self.not_handled646 647 async def _handle_eio_connect(self, eio_sid, environ):648 """Handle the Engine.IO connection event."""649 if not self.manager_initialized:650 self.manager_initialized = True651 self.manager.initialize()652 self.environ[eio_sid] = environ653 654 async def _handle_eio_message(self, eio_sid, data):655 """Dispatch Engine.IO messages."""656 if eio_sid in self._binary_packet:657 pkt = self._binary_packet[eio_sid]658 if pkt.add_attachment(data):659 del self._binary_packet[eio_sid]660 if pkt.packet_type == packet.BINARY_EVENT:661 await self._handle_event(eio_sid, pkt.namespace, pkt.id,662 pkt.data)663 else:664 await self._handle_ack(eio_sid, pkt.namespace, pkt.id,665 pkt.data)666 else:667 pkt = self.packet_class(encoded_packet=data)668 if pkt.packet_type == packet.CONNECT:669 await self._handle_connect(eio_sid, pkt.namespace, pkt.data)670 elif pkt.packet_type == packet.DISCONNECT:671 await self._handle_disconnect(eio_sid, pkt.namespace)672 elif pkt.packet_type == packet.EVENT:673 await self._handle_event(eio_sid, pkt.namespace, pkt.id,674 pkt.data)675 elif pkt.packet_type == packet.ACK:676 await self._handle_ack(eio_sid, pkt.namespace, pkt.id,677 pkt.data)678 elif pkt.packet_type == packet.BINARY_EVENT or \679 pkt.packet_type == packet.BINARY_ACK:680 self._binary_packet[eio_sid] = pkt681 elif pkt.packet_type == packet.CONNECT_ERROR:682 raise ValueError('Unexpected CONNECT_ERROR packet.')683 else:684 raise ValueError('Unknown packet type.')685 686 async def _handle_eio_disconnect(self, eio_sid):687 """Handle Engine.IO disconnect event."""688 for n in list(self.manager.get_namespaces()).copy():689 await self._handle_disconnect(eio_sid, n)690 if eio_sid in self.environ:691 del self.environ[eio_sid]692 693 def _engineio_server_class(self):694 return engineio.AsyncServer695 