From 285f3941d9b51ad6f9aa2ac5b36b1ea18555f437 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Fri, 5 May 2023 00:07:13 +0200 Subject: [PATCH] Always use an external uWSGI server to run the web service. Added `waitress` dependency. For performance and security reasons, it's better to always run the Flask application inside of a uWSGI server. `waitress` also makes things easier by avoiding to ask the user to manually provide the external executable arguments, as it was the case with `uwsgi` and `gunicorn`. --- docs/source/conf.py | 1 + platypush/backend/http/__init__.py | 104 ++++++----------------------- platypush/backend/http/uwsgi.py | 11 --- requirements.txt | 1 + setup.py | 32 ++++----- 5 files changed, 40 insertions(+), 109 deletions(-) delete mode 100644 platypush/backend/http/uwsgi.py diff --git a/docs/source/conf.py b/docs/source/conf.py index a2d44a734d..f875a8881e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -301,6 +301,7 @@ autodoc_mock_imports = [ 'bleak', 'bluetooth_numbers', 'TheengsDecoder', + 'waitress', ] sys.path.insert(0, os.path.abspath('../..')) diff --git a/platypush/backend/http/__init__.py b/platypush/backend/http/__init__.py index 6e160f7be3..f70c077e29 100644 --- a/platypush/backend/http/__init__.py +++ b/platypush/backend/http/__init__.py @@ -1,15 +1,16 @@ import os -import subprocess import threading from multiprocessing import Process try: - from websockets.exceptions import ConnectionClosed + from websockets.exceptions import ConnectionClosed # type: ignore from websockets import serve as websocket_serve # type: ignore except ImportError: from websockets import ConnectionClosed, serve as websocket_serve # type: ignore +import waitress + from platypush.backend import Backend from platypush.backend.http.app import application from platypush.bus.redis import RedisBus @@ -152,22 +153,6 @@ class HttpBackend(Backend): * **Session token**, generated upon login, it can be used to authenticate requests through the ``Cookie`` header (cookie name: ``session_token``). - Requires: - - * **gunicorn** (``pip install gunicorn``) - optional, to run the Platypush webapp over uWSGI. - - By default the Platypush web server will run in a - process spawned on the fly by the HTTP backend. However, being a - Flask app, it will serve clients in a single thread and it won't - support many features of a full-blown web server. gunicorn allows - you to easily spawn the web server in a uWSGI wrapper, separate - from the main Platypush daemon, and the uWSGI layer can be easily - exposed over an nginx/lighttpd web server. - - Command to run the web server over a gunicorn uWSGI wrapper:: - - gunicorn -w -b :8008 platypush.backend.http.uwsgi - """ _DEFAULT_HTTP_PORT = 8008 @@ -185,8 +170,7 @@ class HttpBackend(Backend): ssl_cafile=None, ssl_capath=None, maps=None, - run_externally=False, - uwsgi_args=None, + flask_args=None, **kwargs, ): """ @@ -222,25 +206,8 @@ class HttpBackend(Backend): the value is the absolute path to expose. :type resource_dirs: dict[str, str] - :param run_externally: If set, then the HTTP backend will not directly - spawn the web server. Set this option if you plan to run the webapp - in a separate web server (recommended), like uwsgi or uwsgi+nginx. - :type run_externally: bool - - :param uwsgi_args: If ``run_externally`` is set and you would like the - HTTP backend to directly spawn and control the uWSGI application - server instance, then pass the list of uWSGI arguments through - this parameter. Some examples include:: - - # Start uWSGI instance listening on HTTP port 8008 with 4 - # processes - ['--plugin', 'python', '--http-socket', ':8008', '--master', '--processes', '4'] - - # Start uWSGI instance listening on uWSGI socket on port 3031. - # You can then use another full-blown web server, like nginx - # or Apache, to communicate with the uWSGI instance - ['--plugin', 'python', '--socket', '127.0.0.1:3031', '--master', '--processes', '4'] - :type uwsgi_args: list[str] + :param flask_args: Extra key-value arguments that should be passed to the Flask service. + :type flask_args: dict[str, str] """ super().__init__(**kwargs) @@ -264,8 +231,7 @@ class HttpBackend(Backend): self.resource_dirs = {} self.active_websockets = set() - self.run_externally = run_externally - self.uwsgi_args = uwsgi_args or [] + self.flask_args = flask_args or {} self.ssl_context = ( get_ssl_server_context( ssl_cert=ssl_cert, @@ -277,13 +243,6 @@ class HttpBackend(Backend): else None ) - if self.uwsgi_args: - self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + [ - '--module', - 'platypush.backend.http.uwsgi', - '--enable-threads', - ] - protocol = 'https' if ssl_cert else 'http' self.local_base_url = f'{protocol}://localhost:{self.port}' self._websocket_lock_timeout = 10 @@ -299,26 +258,16 @@ class HttpBackend(Backend): self.logger.info('Received STOP event on HttpBackend') if self.server_proc: - if isinstance(self.server_proc, subprocess.Popen): + self.server_proc.terminate() + self.server_proc.join(timeout=10) + if self.server_proc.is_alive(): self.server_proc.kill() - self.server_proc.wait(timeout=10) - if self.server_proc.poll() is not None: - self.logger.info( - 'HTTP server process may be still alive at termination' - ) - else: - self.logger.info('HTTP server process terminated') + if self.server_proc.is_alive(): + self.logger.info( + 'HTTP server process may be still alive at termination' + ) else: - self.server_proc.terminate() - self.server_proc.join(timeout=10) - if self.server_proc.is_alive(): - self.server_proc.kill() - if self.server_proc.is_alive(): - self.logger.info( - 'HTTP server process may be still alive at termination' - ) - else: - self.logger.info('HTTP server process terminated') + self.logger.info('HTTP server process terminated') if ( self.websocket_thread @@ -440,8 +389,6 @@ class HttpBackend(Backend): kwargs = { 'host': self.bind_address, 'port': self.port, - 'use_reloader': False, - 'debug': False, } assert isinstance( @@ -451,8 +398,10 @@ class HttpBackend(Backend): application.config['redis_queue'] = self.bus.redis_queue if self.ssl_context: kwargs['ssl_context'] = self.ssl_context + if self.flask_args: + kwargs.update(self.flask_args) - application.run(**kwargs) + waitress.serve(application, **kwargs) return proc @@ -480,20 +429,9 @@ class HttpBackend(Backend): self._service_registry_thread.start() def _run_web_server(self): - if not self.run_externally: - self.server_proc = Process(target=self._web_server_proc(), name='WebServer') - self.server_proc.start() - self.server_proc.join() - elif self.uwsgi_args: - uwsgi_cmd = ['uwsgi'] + self.uwsgi_args - self.logger.info('Starting uWSGI with arguments %s', uwsgi_cmd) - self.server_proc = subprocess.Popen(uwsgi_cmd) - else: - raise RuntimeError( - 'The web server is configured to be launched externally but ' - 'no uwsgi_args were provided. Make sure that you run another external service' - 'for the web server (e.g. nginx)' - ) + self.server_proc = Process(target=self._web_server_proc(), name='WebServer') + self.server_proc.start() + self.server_proc.join() def run(self): super().run() diff --git a/platypush/backend/http/uwsgi.py b/platypush/backend/http/uwsgi.py deleted file mode 100644 index 6312c2e92f..0000000000 --- a/platypush/backend/http/uwsgi.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -uWSGI webapp entry point -""" - -from platypush.backend.http.app import application - -if __name__ == '__main__': - application.run() - - -# vim:sw=4:ts=4:et: diff --git a/requirements.txt b/requirements.txt index e671859392..b00880996e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,3 +22,4 @@ paho-mqtt websocket-client croniter python-magic +waitress diff --git a/setup.py b/setup.py index 85dc5187e3..f795225473 100755 --- a/setup.py +++ b/setup.py @@ -60,25 +60,26 @@ setup( "Development Status :: 4 - Beta", ], install_requires=[ + 'alembic', + 'bcrypt', + 'croniter', + 'flask', + 'frozendict', + 'marshmallow', + 'marshmallow_dataclass', + 'python-dateutil', + 'python-magic', 'pyyaml', 'redis', 'requests', - 'croniter', + 'rsa', 'sqlalchemy', - 'alembic', - 'websockets', + 'tz', + 'waitress', 'websocket-client', + 'websockets', 'wheel', 'zeroconf>=0.27.0', - 'tz', - 'python-dateutil', - 'rsa', - 'marshmallow', - 'marshmallow_dataclass', - 'frozendict', - 'flask', - 'bcrypt', - 'python-magic', ], extras_require={ # Support for thread custom name @@ -89,8 +90,9 @@ setup( 'pushbullet': [ 'pushbullet.py @ https://github.com/rbrcsk/pushbullet.py/tarball/master' ], - # Support for HTTP backend over uWSGI - 'http': ['gunicorn'], + # This is only kept for back-compatibility purposes, as all the + # dependencies of the HTTP webserver are now core dependencies. + 'http': [], # Support for MQTT backends 'mqtt': ['paho-mqtt'], # Support for RSS feeds parser @@ -230,7 +232,7 @@ setup( # Support for zigbee2mqtt 'zigbee': ['paho-mqtt'], # Support for Z-Wave - 'zwave': ['python-openzwave'], + 'zwave': ['paho-mqtt'], # Support for Mozilla DeepSpeech speech-to-text engine 'deepspeech': ['deepspeech', 'numpy', 'sounddevice'], # Support for PicoVoice hotword detection engine