mirror of https://github.com/morpheus65535/bazarr
1007 lines
47 KiB
Python
1007 lines
47 KiB
Python
from functools import wraps
|
|
import os
|
|
import sys
|
|
|
|
# make sure gevent-socketio is not installed, as it conflicts with
|
|
# python-socketio
|
|
gevent_socketio_found = True
|
|
try:
|
|
from socketio import socketio_manage # noqa: F401
|
|
except ImportError:
|
|
gevent_socketio_found = False
|
|
if gevent_socketio_found:
|
|
print('The gevent-socketio package is incompatible with this version of '
|
|
'the Flask-SocketIO extension. Please uninstall it, and then '
|
|
'install the latest version of python-socketio in its place.')
|
|
sys.exit(1)
|
|
|
|
import flask
|
|
from flask import _request_ctx_stack, has_request_context, json as flask_json
|
|
from flask.sessions import SessionMixin
|
|
import socketio
|
|
from socketio.exceptions import ConnectionRefusedError # noqa: F401
|
|
from werkzeug.debug import DebuggedApplication
|
|
from werkzeug._reloader import run_with_reloader
|
|
|
|
from .namespace import Namespace
|
|
from .test_client import SocketIOTestClient
|
|
|
|
|
|
class _SocketIOMiddleware(socketio.WSGIApp):
|
|
"""This WSGI middleware simply exposes the Flask application in the WSGI
|
|
environment before executing the request.
|
|
"""
|
|
def __init__(self, socketio_app, flask_app, socketio_path='socket.io'):
|
|
self.flask_app = flask_app
|
|
super(_SocketIOMiddleware, self).__init__(socketio_app,
|
|
flask_app.wsgi_app,
|
|
socketio_path=socketio_path)
|
|
|
|
def __call__(self, environ, start_response):
|
|
environ = environ.copy()
|
|
environ['flask.app'] = self.flask_app
|
|
return super(_SocketIOMiddleware, self).__call__(environ,
|
|
start_response)
|
|
|
|
|
|
class _ManagedSession(dict, SessionMixin):
|
|
"""This class is used for user sessions that are managed by
|
|
Flask-SocketIO. It is simple dict, expanded with the Flask session
|
|
attributes."""
|
|
pass
|
|
|
|
|
|
class SocketIO(object):
|
|
"""Create a Flask-SocketIO server.
|
|
|
|
:param app: The flask application instance. If the application instance
|
|
isn't known at the time this class is instantiated, then call
|
|
``socketio.init_app(app)`` once the application instance is
|
|
available.
|
|
:param manage_session: If set to ``True``, this extension manages the user
|
|
session for Socket.IO events. If set to ``False``,
|
|
Flask's own session management is used. When using
|
|
Flask's cookie based sessions it is recommended that
|
|
you leave this set to the default of ``True``. When
|
|
using server-side sessions, a ``False`` setting
|
|
enables sharing the user session between HTTP routes
|
|
and Socket.IO events.
|
|
:param message_queue: A connection URL for a message queue service the
|
|
server can use for multi-process communication. A
|
|
message queue is not required when using a single
|
|
server process.
|
|
:param channel: The channel name, when using a message queue. If a channel
|
|
isn't specified, a default channel will be used. If
|
|
multiple clusters of SocketIO processes need to use the
|
|
same message queue without interfering with each other,
|
|
then each cluster should use a different channel.
|
|
:param path: The path where the Socket.IO server is exposed. Defaults to
|
|
``'socket.io'``. Leave this as is unless you know what you are
|
|
doing.
|
|
:param resource: Alias to ``path``.
|
|
:param kwargs: Socket.IO and Engine.IO server options.
|
|
|
|
The Socket.IO server options are detailed below:
|
|
|
|
:param client_manager: The client manager instance that will manage the
|
|
client list. When this is omitted, the client list
|
|
is stored in an in-memory structure, so the use of
|
|
multiple connected servers is not possible. In most
|
|
cases, this argument does not need to be set
|
|
explicitly.
|
|
:param logger: To enable logging set to ``True`` or pass a logger object to
|
|
use. To disable logging set to ``False``. The default is
|
|
``False``. Note that fatal errors will be logged even when
|
|
``logger`` is ``False``.
|
|
:param json: An alternative json module to use for encoding and decoding
|
|
packets. Custom json modules must have ``dumps`` and ``loads``
|
|
functions that are compatible with the standard library
|
|
versions. To use the same json encoder and decoder as a Flask
|
|
application, use ``flask.json``.
|
|
:param async_handlers: If set to ``True``, event handlers for a client are
|
|
executed in separate threads. To run handlers for a
|
|
client synchronously, set to ``False``. The default
|
|
is ``True``.
|
|
:param always_connect: When set to ``False``, new connections are
|
|
provisory until the connect handler returns
|
|
something other than ``False``, at which point they
|
|
are accepted. When set to ``True``, connections are
|
|
immediately accepted, and then if the connect
|
|
handler returns ``False`` a disconnect is issued.
|
|
Set to ``True`` if you need to emit events from the
|
|
connect handler and your client is confused when it
|
|
receives events before the connection acceptance.
|
|
In any other case use the default of ``False``.
|
|
|
|
The Engine.IO server configuration supports the following settings:
|
|
|
|
:param async_mode: The asynchronous model to use. See the Deployment
|
|
section in the documentation for a description of the
|
|
available options. Valid async modes are ``threading``,
|
|
``eventlet``, ``gevent`` and ``gevent_uwsgi``. If this
|
|
argument is not given, ``eventlet`` is tried first, then
|
|
``gevent_uwsgi``, then ``gevent``, and finally
|
|
``threading``. The first async mode that has all its
|
|
dependencies installed is then one that is chosen.
|
|
:param ping_interval: The interval in seconds at which the server pings
|
|
the client. The default is 25 seconds. For advanced
|
|
control, a two element tuple can be given, where
|
|
the first number is the ping interval and the second
|
|
is a grace period added by the server.
|
|
:param ping_timeout: The time in seconds that the client waits for the
|
|
server to respond before disconnecting. The default
|
|
is 5 seconds.
|
|
:param max_http_buffer_size: The maximum size of a message when using the
|
|
polling transport. The default is 1,000,000
|
|
bytes.
|
|
:param allow_upgrades: Whether to allow transport upgrades or not. The
|
|
default is ``True``.
|
|
:param http_compression: Whether to compress packages when using the
|
|
polling transport. The default is ``True``.
|
|
:param compression_threshold: Only compress messages when their byte size
|
|
is greater than this value. The default is
|
|
1024 bytes.
|
|
:param cookie: If set to a string, it is the name of the HTTP cookie the
|
|
server sends back to the client containing the client
|
|
session id. If set to a dictionary, the ``'name'`` key
|
|
contains the cookie name and other keys define cookie
|
|
attributes, where the value of each attribute can be a
|
|
string, a callable with no arguments, or a boolean. If set
|
|
to ``None`` (the default), a cookie is not sent to the
|
|
client.
|
|
:param cors_allowed_origins: Origin or list of origins that are allowed to
|
|
connect to this server. Only the same origin
|
|
is allowed by default. Set this argument to
|
|
``'*'`` to allow all origins, or to ``[]`` to
|
|
disable CORS handling.
|
|
:param cors_credentials: Whether credentials (cookies, authentication) are
|
|
allowed in requests to this server. The default is
|
|
``True``.
|
|
:param monitor_clients: If set to ``True``, a background task will ensure
|
|
inactive clients are closed. Set to ``False`` to
|
|
disable the monitoring task (not recommended). The
|
|
default is ``True``.
|
|
: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
|
|
fatal errors are logged even when
|
|
``engineio_logger`` is ``False``.
|
|
"""
|
|
|
|
def __init__(self, app=None, **kwargs):
|
|
self.server = None
|
|
self.server_options = {}
|
|
self.wsgi_server = None
|
|
self.handlers = []
|
|
self.namespace_handlers = []
|
|
self.exception_handlers = {}
|
|
self.default_exception_handler = None
|
|
self.manage_session = True
|
|
# We can call init_app when:
|
|
# - we were given the Flask app instance (standard initialization)
|
|
# - we were not given the app, but we were given a message_queue
|
|
# (standard initialization for auxiliary process)
|
|
# In all other cases we collect the arguments and assume the client
|
|
# will call init_app from an app factory function.
|
|
if app is not None or 'message_queue' in kwargs:
|
|
self.init_app(app, **kwargs)
|
|
else:
|
|
self.server_options.update(kwargs)
|
|
|
|
def init_app(self, app, **kwargs):
|
|
if app is not None:
|
|
if not hasattr(app, 'extensions'):
|
|
app.extensions = {} # pragma: no cover
|
|
app.extensions['socketio'] = self
|
|
self.server_options.update(kwargs)
|
|
self.manage_session = self.server_options.pop('manage_session',
|
|
self.manage_session)
|
|
|
|
if 'client_manager' not in self.server_options:
|
|
url = self.server_options.pop('message_queue', None)
|
|
channel = self.server_options.pop('channel', 'flask-socketio')
|
|
write_only = app is None
|
|
if url:
|
|
if url.startswith(('redis://', "rediss://")):
|
|
queue_class = socketio.RedisManager
|
|
elif url.startswith(('kafka://')):
|
|
queue_class = socketio.KafkaManager
|
|
elif url.startswith('zmq'):
|
|
queue_class = socketio.ZmqManager
|
|
else:
|
|
queue_class = socketio.KombuManager
|
|
queue = queue_class(url, channel=channel,
|
|
write_only=write_only)
|
|
self.server_options['client_manager'] = queue
|
|
|
|
if 'json' in self.server_options and \
|
|
self.server_options['json'] == flask_json:
|
|
# flask's json module is tricky to use because its output
|
|
# changes when it is invoked inside or outside the app context
|
|
# so here to prevent any ambiguities we replace it with wrappers
|
|
# that ensure that the app context is always present
|
|
class FlaskSafeJSON(object):
|
|
@staticmethod
|
|
def dumps(*args, **kwargs):
|
|
with app.app_context():
|
|
return flask_json.dumps(*args, **kwargs)
|
|
|
|
@staticmethod
|
|
def loads(*args, **kwargs):
|
|
with app.app_context():
|
|
return flask_json.loads(*args, **kwargs)
|
|
|
|
self.server_options['json'] = FlaskSafeJSON
|
|
|
|
resource = self.server_options.pop('path', None) or \
|
|
self.server_options.pop('resource', None) or 'socket.io'
|
|
if resource.startswith('/'):
|
|
resource = resource[1:]
|
|
if os.environ.get('FLASK_RUN_FROM_CLI'):
|
|
if self.server_options.get('async_mode') is None:
|
|
self.server_options['async_mode'] = 'threading'
|
|
self.server = socketio.Server(**self.server_options)
|
|
self.async_mode = self.server.async_mode
|
|
for handler in self.handlers:
|
|
self.server.on(handler[0], handler[1], namespace=handler[2])
|
|
for namespace_handler in self.namespace_handlers:
|
|
self.server.register_namespace(namespace_handler)
|
|
|
|
if app is not None:
|
|
# here we attach the SocketIO middlware to the SocketIO object so
|
|
# it can be referenced later if debug middleware needs to be
|
|
# inserted
|
|
self.sockio_mw = _SocketIOMiddleware(self.server, app,
|
|
socketio_path=resource)
|
|
app.wsgi_app = self.sockio_mw
|
|
|
|
def on(self, message, namespace=None):
|
|
"""Decorator to register a SocketIO event handler.
|
|
|
|
This decorator must be applied to SocketIO event handlers. Example::
|
|
|
|
@socketio.on('my event', namespace='/chat')
|
|
def handle_my_custom_event(json):
|
|
print('received json: ' + str(json))
|
|
|
|
:param message: The name of the event. This is normally a user defined
|
|
string, but a few event names are already defined. Use
|
|
``'message'`` to define a handler that takes a string
|
|
payload, ``'json'`` to define a handler that takes a
|
|
JSON blob payload, ``'connect'`` or ``'disconnect'``
|
|
to create handlers for connection and disconnection
|
|
events.
|
|
:param namespace: The namespace on which the handler is to be
|
|
registered. Defaults to the global namespace.
|
|
"""
|
|
namespace = namespace or '/'
|
|
|
|
def decorator(handler):
|
|
@wraps(handler)
|
|
def _handler(sid, *args):
|
|
return self._handle_event(handler, message, namespace, sid,
|
|
*args)
|
|
|
|
if self.server:
|
|
self.server.on(message, _handler, namespace=namespace)
|
|
else:
|
|
self.handlers.append((message, _handler, namespace))
|
|
return handler
|
|
return decorator
|
|
|
|
def on_error(self, namespace=None):
|
|
"""Decorator to define a custom error handler for SocketIO events.
|
|
|
|
This decorator can be applied to a function that acts as an error
|
|
handler for a namespace. This handler will be invoked when a SocketIO
|
|
event handler raises an exception. The handler function must accept one
|
|
argument, which is the exception raised. Example::
|
|
|
|
@socketio.on_error(namespace='/chat')
|
|
def chat_error_handler(e):
|
|
print('An error has occurred: ' + str(e))
|
|
|
|
:param namespace: The namespace for which to register the error
|
|
handler. Defaults to the global namespace.
|
|
"""
|
|
namespace = namespace or '/'
|
|
|
|
def decorator(exception_handler):
|
|
if not callable(exception_handler):
|
|
raise ValueError('exception_handler must be callable')
|
|
self.exception_handlers[namespace] = exception_handler
|
|
return exception_handler
|
|
return decorator
|
|
|
|
def on_error_default(self, exception_handler):
|
|
"""Decorator to define a default error handler for SocketIO events.
|
|
|
|
This decorator can be applied to a function that acts as a default
|
|
error handler for any namespaces that do not have a specific handler.
|
|
Example::
|
|
|
|
@socketio.on_error_default
|
|
def error_handler(e):
|
|
print('An error has occurred: ' + str(e))
|
|
"""
|
|
if not callable(exception_handler):
|
|
raise ValueError('exception_handler must be callable')
|
|
self.default_exception_handler = exception_handler
|
|
return exception_handler
|
|
|
|
def on_event(self, message, handler, namespace=None):
|
|
"""Register a SocketIO event handler.
|
|
|
|
``on_event`` is the non-decorator version of ``'on'``.
|
|
|
|
Example::
|
|
|
|
def on_foo_event(json):
|
|
print('received json: ' + str(json))
|
|
|
|
socketio.on_event('my event', on_foo_event, namespace='/chat')
|
|
|
|
:param message: The name of the event. This is normally a user defined
|
|
string, but a few event names are already defined. Use
|
|
``'message'`` to define a handler that takes a string
|
|
payload, ``'json'`` to define a handler that takes a
|
|
JSON blob payload, ``'connect'`` or ``'disconnect'``
|
|
to create handlers for connection and disconnection
|
|
events.
|
|
:param handler: The function that handles the event.
|
|
:param namespace: The namespace on which the handler is to be
|
|
registered. Defaults to the global namespace.
|
|
"""
|
|
self.on(message, namespace=namespace)(handler)
|
|
|
|
def event(self, *args, **kwargs):
|
|
"""Decorator to register an event handler.
|
|
|
|
This is a simplified version of the ``on()`` method that takes the
|
|
event name from the decorated function.
|
|
|
|
Example usage::
|
|
|
|
@socketio.event
|
|
def my_event(data):
|
|
print('Received data: ', data)
|
|
|
|
The above example is equivalent to::
|
|
|
|
@socketio.on('my_event')
|
|
def my_event(data):
|
|
print('Received data: ', data)
|
|
|
|
A custom namespace can be given as an argument to the decorator::
|
|
|
|
@socketio.event(namespace='/test')
|
|
def my_event(data):
|
|
print('Received data: ', data)
|
|
"""
|
|
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
|
|
# the decorator was invoked without arguments
|
|
# args[0] is the decorated function
|
|
return self.on(args[0].__name__)(args[0])
|
|
else:
|
|
# the decorator was invoked with arguments
|
|
def set_handler(handler):
|
|
return self.on(handler.__name__, *args, **kwargs)(handler)
|
|
|
|
return set_handler
|
|
|
|
def on_namespace(self, namespace_handler):
|
|
if not isinstance(namespace_handler, Namespace):
|
|
raise ValueError('Not a namespace instance.')
|
|
namespace_handler._set_socketio(self)
|
|
if self.server:
|
|
self.server.register_namespace(namespace_handler)
|
|
else:
|
|
self.namespace_handlers.append(namespace_handler)
|
|
|
|
def emit(self, event, *args, **kwargs):
|
|
"""Emit a server generated SocketIO event.
|
|
|
|
This function emits a SocketIO event to one or more connected clients.
|
|
A JSON blob can be attached to the event as payload. This function can
|
|
be used outside of a SocketIO event context, so it is appropriate to
|
|
use when the server is the originator of an event, outside of any
|
|
client context, such as in a regular HTTP request handler or a
|
|
background task. Example::
|
|
|
|
@app.route('/ping')
|
|
def ping():
|
|
socketio.emit('ping event', {'data': 42}, namespace='/chat')
|
|
|
|
:param event: The name of the user event to emit.
|
|
:param args: A dictionary with the JSON data to send as payload.
|
|
:param namespace: The namespace under which the message is to be sent.
|
|
Defaults to the global namespace.
|
|
:param to: Send the message to all the users in the given room. If
|
|
this parameter is not included, the event is sent to all
|
|
connected users.
|
|
:param include_self: ``True`` to include the sender when broadcasting
|
|
or addressing a room, or ``False`` to send to
|
|
everyone but the sender.
|
|
:param skip_sid: The session id of a client to ignore when broadcasting
|
|
or addressing a room. This is typically set to the
|
|
originator of the message, so that everyone except
|
|
that client receive the message. To skip multiple sids
|
|
pass a list.
|
|
:param callback: If given, this function will be called to acknowledge
|
|
that the client has received the message. The
|
|
arguments that will be passed to the function are
|
|
those provided by the client. Callback functions can
|
|
only be used when addressing an individual client.
|
|
"""
|
|
namespace = kwargs.pop('namespace', '/')
|
|
to = kwargs.pop('to', kwargs.pop('room', None))
|
|
include_self = kwargs.pop('include_self', True)
|
|
skip_sid = kwargs.pop('skip_sid', None)
|
|
if not include_self and not skip_sid:
|
|
skip_sid = flask.request.sid
|
|
callback = kwargs.pop('callback', None)
|
|
if callback:
|
|
# wrap the callback so that it sets app app and request contexts
|
|
sid = None
|
|
if has_request_context():
|
|
sid = getattr(flask.request, 'sid', None)
|
|
original_callback = callback
|
|
|
|
def _callback_wrapper(*args):
|
|
return self._handle_event(original_callback, None, namespace,
|
|
sid, *args)
|
|
|
|
if sid:
|
|
# the callback wrapper above will install a request context
|
|
# before invoking the original callback
|
|
# we only use it if the emit was issued from a Socket.IO
|
|
# populated request context (i.e. request.sid is defined)
|
|
callback = _callback_wrapper
|
|
self.server.emit(event, *args, namespace=namespace, to=to,
|
|
skip_sid=skip_sid, callback=callback, **kwargs)
|
|
|
|
def send(self, data, json=False, namespace=None, to=None,
|
|
callback=None, include_self=True, skip_sid=None, **kwargs):
|
|
"""Send a server-generated SocketIO message.
|
|
|
|
This function sends a simple SocketIO message to one or more connected
|
|
clients. The message can be a string or a JSON blob. This is a simpler
|
|
version of ``emit()``, which should be preferred. This function can be
|
|
used outside of a SocketIO event context, so it is appropriate to use
|
|
when the server is the originator of an event.
|
|
|
|
:param data: The message to send, either a string or a JSON blob.
|
|
:param json: ``True`` if ``message`` is a JSON blob, ``False``
|
|
otherwise.
|
|
:param namespace: The namespace under which the message is to be sent.
|
|
Defaults to the global namespace.
|
|
:param to: Send the message only to the users in the given room. If
|
|
this parameter is not included, the message is sent to all
|
|
connected users.
|
|
:param include_self: ``True`` to include the sender when broadcasting
|
|
or addressing a room, or ``False`` to send to
|
|
everyone but the sender.
|
|
:param skip_sid: The session id of a client to ignore when broadcasting
|
|
or addressing a room. This is typically set to the
|
|
originator of the message, so that everyone except
|
|
that client receive the message. To skip multiple sids
|
|
pass a list.
|
|
:param callback: If given, this function will be called to acknowledge
|
|
that the client has received the message. The
|
|
arguments that will be passed to the function are
|
|
those provided by the client. Callback functions can
|
|
only be used when addressing an individual client.
|
|
"""
|
|
skip_sid = flask.request.sid if not include_self else skip_sid
|
|
if json:
|
|
self.emit('json', data, namespace=namespace, to=to,
|
|
skip_sid=skip_sid, callback=callback, **kwargs)
|
|
else:
|
|
self.emit('message', data, namespace=namespace, to=to,
|
|
skip_sid=skip_sid, callback=callback, **kwargs)
|
|
|
|
def close_room(self, room, namespace=None):
|
|
"""Close a room.
|
|
|
|
This function removes any users that are in the given room and then
|
|
deletes the room from the server. This function can be used outside
|
|
of a SocketIO event context.
|
|
|
|
:param room: The name of the room to close.
|
|
:param namespace: The namespace under which the room exists. Defaults
|
|
to the global namespace.
|
|
"""
|
|
self.server.close_room(room, namespace)
|
|
|
|
def run(self, app, host=None, port=None, **kwargs): # pragma: no cover
|
|
"""Run the SocketIO web server.
|
|
|
|
:param app: The Flask application instance.
|
|
:param host: The hostname or IP address for the server to listen on.
|
|
Defaults to 127.0.0.1.
|
|
:param port: The port number for the server to listen on. Defaults to
|
|
5000.
|
|
:param debug: ``True`` to start the server in debug mode, ``False`` to
|
|
start in normal mode.
|
|
:param use_reloader: ``True`` to enable the Flask reloader, ``False``
|
|
to disable it.
|
|
:param reloader_options: A dictionary with options that are passed to
|
|
the Flask reloader, such as ``extra_files``,
|
|
``reloader_type``, etc.
|
|
:param extra_files: A list of additional files that the Flask
|
|
reloader should watch. Defaults to ``None``.
|
|
Deprecated, use ``reloader_options`` instead.
|
|
:param log_output: If ``True``, the server logs all incoming
|
|
connections. If ``False`` logging is disabled.
|
|
Defaults to ``True`` in debug mode, ``False``
|
|
in normal mode. Unused when the threading async
|
|
mode is used.
|
|
:param kwargs: Additional web server options. The web server options
|
|
are specific to the server used in each of the supported
|
|
async modes. Note that options provided here will
|
|
not be seen when using an external web server such
|
|
as gunicorn, since this method is not called in that
|
|
case.
|
|
"""
|
|
if host is None:
|
|
host = '127.0.0.1'
|
|
if port is None:
|
|
server_name = app.config['SERVER_NAME']
|
|
if server_name and ':' in server_name:
|
|
port = int(server_name.rsplit(':', 1)[1])
|
|
else:
|
|
port = 5000
|
|
|
|
debug = kwargs.pop('debug', app.debug)
|
|
log_output = kwargs.pop('log_output', debug)
|
|
use_reloader = kwargs.pop('use_reloader', debug)
|
|
extra_files = kwargs.pop('extra_files', None)
|
|
reloader_options = kwargs.pop('reloader_options', {})
|
|
if extra_files:
|
|
reloader_options['extra_files'] = extra_files
|
|
|
|
app.debug = debug
|
|
if app.debug and self.server.eio.async_mode != 'threading':
|
|
# put the debug middleware between the SocketIO middleware
|
|
# and the Flask application instance
|
|
#
|
|
# mw1 mw2 mw3 Flask app
|
|
# o ---- o ---- o ---- o
|
|
# /
|
|
# o Flask-SocketIO
|
|
# \ middleware
|
|
# o
|
|
# Flask-SocketIO WebSocket handler
|
|
#
|
|
# BECOMES
|
|
#
|
|
# dbg-mw mw1 mw2 mw3 Flask app
|
|
# o ---- o ---- o ---- o ---- o
|
|
# /
|
|
# o Flask-SocketIO
|
|
# \ middleware
|
|
# o
|
|
# Flask-SocketIO WebSocket handler
|
|
#
|
|
self.sockio_mw.wsgi_app = DebuggedApplication(
|
|
self.sockio_mw.wsgi_app, evalex=True)
|
|
|
|
if self.server.eio.async_mode == 'threading':
|
|
try:
|
|
import simple_websocket # noqa: F401
|
|
except ImportError:
|
|
from werkzeug._internal import _log
|
|
_log('warning', 'WebSocket transport not available. Install '
|
|
'simple-websocket for improved performance.')
|
|
app.run(host=host, port=port, threaded=True,
|
|
use_reloader=use_reloader, **reloader_options, **kwargs)
|
|
elif self.server.eio.async_mode == 'eventlet':
|
|
def run_server():
|
|
import eventlet
|
|
import eventlet.wsgi
|
|
import eventlet.green
|
|
addresses = eventlet.green.socket.getaddrinfo(host, port)
|
|
if not addresses:
|
|
raise RuntimeError(
|
|
'Could not resolve host to a valid address')
|
|
eventlet_socket = eventlet.listen(addresses[0][4],
|
|
addresses[0][0])
|
|
|
|
# If provided an SSL argument, use an SSL socket
|
|
ssl_args = ['keyfile', 'certfile', 'server_side', 'cert_reqs',
|
|
'ssl_version', 'ca_certs',
|
|
'do_handshake_on_connect', 'suppress_ragged_eofs',
|
|
'ciphers']
|
|
ssl_params = {k: kwargs[k] for k in kwargs
|
|
if k in ssl_args and kwargs[k] is not None}
|
|
for k in ssl_args:
|
|
kwargs.pop(k, None)
|
|
if len(ssl_params) > 0:
|
|
ssl_params['server_side'] = True # Listening requires true
|
|
eventlet_socket = eventlet.wrap_ssl(eventlet_socket,
|
|
**ssl_params)
|
|
|
|
eventlet.wsgi.server(eventlet_socket, app,
|
|
log_output=log_output, **kwargs)
|
|
|
|
if use_reloader:
|
|
run_with_reloader(run_server, **reloader_options)
|
|
else:
|
|
run_server()
|
|
elif self.server.eio.async_mode == 'gevent':
|
|
from gevent import pywsgi
|
|
try:
|
|
from geventwebsocket.handler import WebSocketHandler
|
|
websocket = True
|
|
except ImportError:
|
|
app.logger.warning(
|
|
'WebSocket transport not available. Install '
|
|
'gevent-websocket for improved performance.')
|
|
websocket = False
|
|
|
|
log = 'default'
|
|
if not log_output:
|
|
log = None
|
|
if websocket:
|
|
self.wsgi_server = pywsgi.WSGIServer(
|
|
(host, port), app, handler_class=WebSocketHandler,
|
|
log=log, **kwargs)
|
|
else:
|
|
self.wsgi_server = pywsgi.WSGIServer((host, port), app,
|
|
log=log, **kwargs)
|
|
|
|
if use_reloader:
|
|
# monkey patching is required by the reloader
|
|
from gevent import monkey
|
|
monkey.patch_thread()
|
|
monkey.patch_time()
|
|
|
|
def run_server():
|
|
self.wsgi_server.serve_forever()
|
|
|
|
run_with_reloader(run_server, **reloader_options)
|
|
else:
|
|
self.wsgi_server.serve_forever()
|
|
|
|
def stop(self):
|
|
"""Stop a running SocketIO web server.
|
|
|
|
This method must be called from a HTTP or SocketIO handler function.
|
|
"""
|
|
if self.server.eio.async_mode == 'threading':
|
|
func = flask.request.environ.get('werkzeug.server.shutdown')
|
|
if func:
|
|
func()
|
|
else:
|
|
raise RuntimeError('Cannot stop unknown web server')
|
|
elif self.server.eio.async_mode == 'eventlet':
|
|
raise SystemExit
|
|
elif self.server.eio.async_mode == 'gevent':
|
|
self.wsgi_server.stop()
|
|
|
|
def start_background_task(self, target, *args, **kwargs):
|
|
"""Start a background task using the appropriate async model.
|
|
|
|
This is a utility function that applications can use to start a
|
|
background task using the method that is compatible with the
|
|
selected async mode.
|
|
|
|
:param target: the target function to execute.
|
|
:param args: arguments to pass to the function.
|
|
:param kwargs: keyword arguments to pass to the function.
|
|
|
|
This function returns an object compatible with the `Thread` class in
|
|
the Python standard library. The `start()` method on this object is
|
|
already called by this function.
|
|
"""
|
|
return self.server.start_background_task(target, *args, **kwargs)
|
|
|
|
def sleep(self, seconds=0):
|
|
"""Sleep for the requested amount of time using the appropriate async
|
|
model.
|
|
|
|
This is a utility function that applications can use to put a task to
|
|
sleep without having to worry about using the correct call for the
|
|
selected async mode.
|
|
"""
|
|
return self.server.sleep(seconds)
|
|
|
|
def test_client(self, app, namespace=None, query_string=None,
|
|
headers=None, auth=None, flask_test_client=None):
|
|
"""The Socket.IO test client is useful for testing a Flask-SocketIO
|
|
server. It works in a similar way to the Flask Test Client, but
|
|
adapted to the Socket.IO server.
|
|
|
|
:param app: The Flask application instance.
|
|
:param namespace: The namespace for the client. If not provided, the
|
|
client connects to the server on the global
|
|
namespace.
|
|
:param query_string: A string with custom query string arguments.
|
|
:param headers: A dictionary with custom HTTP headers.
|
|
:param auth: Optional authentication data, given as a dictionary.
|
|
:param flask_test_client: The instance of the Flask test client
|
|
currently in use. Passing the Flask test
|
|
client is optional, but is necessary if you
|
|
want the Flask user session and any other
|
|
cookies set in HTTP routes accessible from
|
|
Socket.IO events.
|
|
"""
|
|
return SocketIOTestClient(app, self, namespace=namespace,
|
|
query_string=query_string, headers=headers,
|
|
auth=auth,
|
|
flask_test_client=flask_test_client)
|
|
|
|
def _handle_event(self, handler, message, namespace, sid, *args):
|
|
environ = self.server.get_environ(sid, namespace=namespace)
|
|
if not environ:
|
|
# we don't have record of this client, ignore this event
|
|
return '', 400
|
|
app = environ['flask.app']
|
|
with app.request_context(environ):
|
|
if self.manage_session:
|
|
# manage a separate session for this client's Socket.IO events
|
|
# created as a copy of the regular user session
|
|
if 'saved_session' not in environ:
|
|
environ['saved_session'] = _ManagedSession(flask.session)
|
|
session_obj = environ['saved_session']
|
|
else:
|
|
# let Flask handle the user session
|
|
# for cookie based sessions, this effectively freezes the
|
|
# session to its state at connection time
|
|
# for server-side sessions, this allows HTTP and Socket.IO to
|
|
# share the session, with both having read/write access to it
|
|
session_obj = flask.session._get_current_object()
|
|
_request_ctx_stack.top.session = session_obj
|
|
flask.request.sid = sid
|
|
flask.request.namespace = namespace
|
|
flask.request.event = {'message': message, 'args': args}
|
|
try:
|
|
if message == 'connect':
|
|
auth = args[1] if len(args) > 1 else None
|
|
try:
|
|
ret = handler(auth)
|
|
except TypeError:
|
|
ret = handler()
|
|
else:
|
|
ret = handler(*args)
|
|
except:
|
|
err_handler = self.exception_handlers.get(
|
|
namespace, self.default_exception_handler)
|
|
if err_handler is None:
|
|
raise
|
|
type, value, traceback = sys.exc_info()
|
|
return err_handler(value)
|
|
if not self.manage_session:
|
|
# when Flask is managing the user session, it needs to save it
|
|
if not hasattr(session_obj, 'modified') or \
|
|
session_obj.modified:
|
|
resp = app.response_class()
|
|
app.session_interface.save_session(app, session_obj, resp)
|
|
return ret
|
|
|
|
|
|
def emit(event, *args, **kwargs):
|
|
"""Emit a SocketIO event.
|
|
|
|
This function emits a SocketIO event to one or more connected clients. A
|
|
JSON blob can be attached to the event as payload. This is a function that
|
|
can only be called from a SocketIO event handler, as in obtains some
|
|
information from the current client context. Example::
|
|
|
|
@socketio.on('my event')
|
|
def handle_my_custom_event(json):
|
|
emit('my response', {'data': 42})
|
|
|
|
:param event: The name of the user event to emit.
|
|
:param args: A dictionary with the JSON data to send as payload.
|
|
:param namespace: The namespace under which the message is to be sent.
|
|
Defaults to the namespace used by the originating event.
|
|
A ``'/'`` can be used to explicitly specify the global
|
|
namespace.
|
|
:param callback: Callback function to invoke with the client's
|
|
acknowledgement.
|
|
:param broadcast: ``True`` to send the message to all clients, or ``False``
|
|
to only reply to the sender of the originating event.
|
|
:param to: Send the message to all the users in the given room. If this
|
|
argument is not set and ``broadcast`` is ``False``, then the
|
|
message is sent only to the originating user.
|
|
:param include_self: ``True`` to include the sender when broadcasting or
|
|
addressing a room, or ``False`` to send to everyone
|
|
but the sender.
|
|
:param skip_sid: The session id of a client to ignore when broadcasting
|
|
or addressing a room. This is typically set to the
|
|
originator of the message, so that everyone except
|
|
that client receive the message. To skip multiple sids
|
|
pass a list.
|
|
:param ignore_queue: Only used when a message queue is configured. If
|
|
set to ``True``, the event is emitted to the
|
|
clients directly, without going through the queue.
|
|
This is more efficient, but only works when a
|
|
single server process is used, or when there is a
|
|
single addressee. It is recommended to always leave
|
|
this parameter with its default value of ``False``.
|
|
"""
|
|
if 'namespace' in kwargs:
|
|
namespace = kwargs['namespace']
|
|
else:
|
|
namespace = flask.request.namespace
|
|
callback = kwargs.get('callback')
|
|
broadcast = kwargs.get('broadcast')
|
|
to = kwargs.pop('to', kwargs.pop('room', None))
|
|
if to is None and not broadcast:
|
|
to = flask.request.sid
|
|
include_self = kwargs.get('include_self', True)
|
|
skip_sid = kwargs.get('skip_sid')
|
|
ignore_queue = kwargs.get('ignore_queue', False)
|
|
|
|
socketio = flask.current_app.extensions['socketio']
|
|
return socketio.emit(event, *args, namespace=namespace, to=to,
|
|
include_self=include_self, skip_sid=skip_sid,
|
|
callback=callback, ignore_queue=ignore_queue)
|
|
|
|
|
|
def send(message, **kwargs):
|
|
"""Send a SocketIO message.
|
|
|
|
This function sends a simple SocketIO message to one or more connected
|
|
clients. The message can be a string or a JSON blob. This is a simpler
|
|
version of ``emit()``, which should be preferred. This is a function that
|
|
can only be called from a SocketIO event handler.
|
|
|
|
:param message: The message to send, either a string or a JSON blob.
|
|
:param json: ``True`` if ``message`` is a JSON blob, ``False``
|
|
otherwise.
|
|
:param namespace: The namespace under which the message is to be sent.
|
|
Defaults to the namespace used by the originating event.
|
|
An empty string can be used to use the global namespace.
|
|
:param callback: Callback function to invoke with the client's
|
|
acknowledgement.
|
|
:param broadcast: ``True`` to send the message to all connected clients, or
|
|
``False`` to only reply to the sender of the originating
|
|
event.
|
|
:param to: Send the message to all the users in the given room. If this
|
|
argument is not set and ``broadcast`` is ``False``, then the
|
|
message is sent only to the originating user.
|
|
:param include_self: ``True`` to include the sender when broadcasting or
|
|
addressing a room, or ``False`` to send to everyone
|
|
but the sender.
|
|
:param skip_sid: The session id of a client to ignore when broadcasting
|
|
or addressing a room. This is typically set to the
|
|
originator of the message, so that everyone except
|
|
that client receive the message. To skip multiple sids
|
|
pass a list.
|
|
:param ignore_queue: Only used when a message queue is configured. If
|
|
set to ``True``, the event is emitted to the
|
|
clients directly, without going through the queue.
|
|
This is more efficient, but only works when a
|
|
single server process is used, or when there is a
|
|
single addressee. It is recommended to always leave
|
|
this parameter with its default value of ``False``.
|
|
"""
|
|
json = kwargs.get('json', False)
|
|
if 'namespace' in kwargs:
|
|
namespace = kwargs['namespace']
|
|
else:
|
|
namespace = flask.request.namespace
|
|
callback = kwargs.get('callback')
|
|
broadcast = kwargs.get('broadcast')
|
|
to = kwargs.pop('to', kwargs.pop('room', None))
|
|
if to is None and not broadcast:
|
|
to = flask.request.sid
|
|
include_self = kwargs.get('include_self', True)
|
|
skip_sid = kwargs.get('skip_sid')
|
|
ignore_queue = kwargs.get('ignore_queue', False)
|
|
|
|
socketio = flask.current_app.extensions['socketio']
|
|
return socketio.send(message, json=json, namespace=namespace, to=to,
|
|
include_self=include_self, skip_sid=skip_sid,
|
|
callback=callback, ignore_queue=ignore_queue)
|
|
|
|
|
|
def join_room(room, sid=None, namespace=None):
|
|
"""Join a room.
|
|
|
|
This function puts the user in a room, under the current namespace. The
|
|
user and the namespace are obtained from the event context. This is a
|
|
function that can only be called from a SocketIO event handler. Example::
|
|
|
|
@socketio.on('join')
|
|
def on_join(data):
|
|
username = session['username']
|
|
room = data['room']
|
|
join_room(room)
|
|
send(username + ' has entered the room.', room=room)
|
|
|
|
:param room: The name of the room to join.
|
|
:param sid: The session id of the client. If not provided, the client is
|
|
obtained from the request context.
|
|
:param namespace: The namespace for the room. If not provided, the
|
|
namespace is obtained from the request context.
|
|
"""
|
|
socketio = flask.current_app.extensions['socketio']
|
|
sid = sid or flask.request.sid
|
|
namespace = namespace or flask.request.namespace
|
|
socketio.server.enter_room(sid, room, namespace=namespace)
|
|
|
|
|
|
def leave_room(room, sid=None, namespace=None):
|
|
"""Leave a room.
|
|
|
|
This function removes the user from a room, under the current namespace.
|
|
The user and the namespace are obtained from the event context. Example::
|
|
|
|
@socketio.on('leave')
|
|
def on_leave(data):
|
|
username = session['username']
|
|
room = data['room']
|
|
leave_room(room)
|
|
send(username + ' has left the room.', room=room)
|
|
|
|
:param room: The name of the room to leave.
|
|
:param sid: The session id of the client. If not provided, the client is
|
|
obtained from the request context.
|
|
:param namespace: The namespace for the room. If not provided, the
|
|
namespace is obtained from the request context.
|
|
"""
|
|
socketio = flask.current_app.extensions['socketio']
|
|
sid = sid or flask.request.sid
|
|
namespace = namespace or flask.request.namespace
|
|
socketio.server.leave_room(sid, room, namespace=namespace)
|
|
|
|
|
|
def close_room(room, namespace=None):
|
|
"""Close a room.
|
|
|
|
This function removes any users that are in the given room and then deletes
|
|
the room from the server.
|
|
|
|
:param room: The name of the room to close.
|
|
:param namespace: The namespace for the room. If not provided, the
|
|
namespace is obtained from the request context.
|
|
"""
|
|
socketio = flask.current_app.extensions['socketio']
|
|
namespace = namespace or flask.request.namespace
|
|
socketio.server.close_room(room, namespace=namespace)
|
|
|
|
|
|
def rooms(sid=None, namespace=None):
|
|
"""Return a list of the rooms the client is in.
|
|
|
|
This function returns all the rooms the client has entered, including its
|
|
own room, assigned by the Socket.IO server.
|
|
|
|
:param sid: The session id of the client. If not provided, the client is
|
|
obtained from the request context.
|
|
:param namespace: The namespace for the room. If not provided, the
|
|
namespace is obtained from the request context.
|
|
"""
|
|
socketio = flask.current_app.extensions['socketio']
|
|
sid = sid or flask.request.sid
|
|
namespace = namespace or flask.request.namespace
|
|
return socketio.server.rooms(sid, namespace=namespace)
|
|
|
|
|
|
def disconnect(sid=None, namespace=None, silent=False):
|
|
"""Disconnect the client.
|
|
|
|
This function terminates the connection with the client. As a result of
|
|
this call the client will receive a disconnect event. Example::
|
|
|
|
@socketio.on('message')
|
|
def receive_message(msg):
|
|
if is_banned(session['username']):
|
|
disconnect()
|
|
else:
|
|
# ...
|
|
|
|
:param sid: The session id of the client. If not provided, the client is
|
|
obtained from the request context.
|
|
:param namespace: The namespace for the room. If not provided, the
|
|
namespace is obtained from the request context.
|
|
:param silent: this option is deprecated.
|
|
"""
|
|
socketio = flask.current_app.extensions['socketio']
|
|
sid = sid or flask.request.sid
|
|
namespace = namespace or flask.request.namespace
|
|
return socketio.server.disconnect(sid, namespace=namespace)
|