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`.
This commit is contained in:
Fabio Manganiello 2023-05-05 00:07:13 +02:00
parent 2c254e8eb9
commit 285f3941d9
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
5 changed files with 40 additions and 109 deletions

View file

@ -301,6 +301,7 @@ autodoc_mock_imports = [
'bleak', 'bleak',
'bluetooth_numbers', 'bluetooth_numbers',
'TheengsDecoder', 'TheengsDecoder',
'waitress',
] ]
sys.path.insert(0, os.path.abspath('../..')) sys.path.insert(0, os.path.abspath('../..'))

View file

@ -1,15 +1,16 @@
import os import os
import subprocess
import threading import threading
from multiprocessing import Process from multiprocessing import Process
try: try:
from websockets.exceptions import ConnectionClosed from websockets.exceptions import ConnectionClosed # type: ignore
from websockets import serve as websocket_serve # type: ignore from websockets import serve as websocket_serve # type: ignore
except ImportError: except ImportError:
from websockets import ConnectionClosed, serve as websocket_serve # type: ignore from websockets import ConnectionClosed, serve as websocket_serve # type: ignore
import waitress
from platypush.backend import Backend from platypush.backend import Backend
from platypush.backend.http.app import application from platypush.backend.http.app import application
from platypush.bus.redis import RedisBus 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 * **Session token**, generated upon login, it can be used to authenticate requests through the ``Cookie`` header
(cookie name: ``session_token``). (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 <n_workers> -b <bind_address>:8008 platypush.backend.http.uwsgi
""" """
_DEFAULT_HTTP_PORT = 8008 _DEFAULT_HTTP_PORT = 8008
@ -185,8 +170,7 @@ class HttpBackend(Backend):
ssl_cafile=None, ssl_cafile=None,
ssl_capath=None, ssl_capath=None,
maps=None, maps=None,
run_externally=False, flask_args=None,
uwsgi_args=None,
**kwargs, **kwargs,
): ):
""" """
@ -222,25 +206,8 @@ class HttpBackend(Backend):
the value is the absolute path to expose. the value is the absolute path to expose.
:type resource_dirs: dict[str, str] :type resource_dirs: dict[str, str]
:param run_externally: If set, then the HTTP backend will not directly :param flask_args: Extra key-value arguments that should be passed to the Flask service.
spawn the web server. Set this option if you plan to run the webapp :type flask_args: dict[str, str]
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]
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
@ -264,8 +231,7 @@ class HttpBackend(Backend):
self.resource_dirs = {} self.resource_dirs = {}
self.active_websockets = set() self.active_websockets = set()
self.run_externally = run_externally self.flask_args = flask_args or {}
self.uwsgi_args = uwsgi_args or []
self.ssl_context = ( self.ssl_context = (
get_ssl_server_context( get_ssl_server_context(
ssl_cert=ssl_cert, ssl_cert=ssl_cert,
@ -277,13 +243,6 @@ class HttpBackend(Backend):
else None 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' protocol = 'https' if ssl_cert else 'http'
self.local_base_url = f'{protocol}://localhost:{self.port}' self.local_base_url = f'{protocol}://localhost:{self.port}'
self._websocket_lock_timeout = 10 self._websocket_lock_timeout = 10
@ -299,16 +258,6 @@ class HttpBackend(Backend):
self.logger.info('Received STOP event on HttpBackend') self.logger.info('Received STOP event on HttpBackend')
if self.server_proc: if self.server_proc:
if isinstance(self.server_proc, subprocess.Popen):
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')
else:
self.server_proc.terminate() self.server_proc.terminate()
self.server_proc.join(timeout=10) self.server_proc.join(timeout=10)
if self.server_proc.is_alive(): if self.server_proc.is_alive():
@ -440,8 +389,6 @@ class HttpBackend(Backend):
kwargs = { kwargs = {
'host': self.bind_address, 'host': self.bind_address,
'port': self.port, 'port': self.port,
'use_reloader': False,
'debug': False,
} }
assert isinstance( assert isinstance(
@ -451,8 +398,10 @@ class HttpBackend(Backend):
application.config['redis_queue'] = self.bus.redis_queue application.config['redis_queue'] = self.bus.redis_queue
if self.ssl_context: if self.ssl_context:
kwargs['ssl_context'] = 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 return proc
@ -480,20 +429,9 @@ class HttpBackend(Backend):
self._service_registry_thread.start() self._service_registry_thread.start()
def _run_web_server(self): def _run_web_server(self):
if not self.run_externally:
self.server_proc = Process(target=self._web_server_proc(), name='WebServer') self.server_proc = Process(target=self._web_server_proc(), name='WebServer')
self.server_proc.start() self.server_proc.start()
self.server_proc.join() 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)'
)
def run(self): def run(self):
super().run() super().run()

View file

@ -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:

View file

@ -22,3 +22,4 @@ paho-mqtt
websocket-client websocket-client
croniter croniter
python-magic python-magic
waitress

View file

@ -60,25 +60,26 @@ setup(
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
], ],
install_requires=[ install_requires=[
'alembic',
'bcrypt',
'croniter',
'flask',
'frozendict',
'marshmallow',
'marshmallow_dataclass',
'python-dateutil',
'python-magic',
'pyyaml', 'pyyaml',
'redis', 'redis',
'requests', 'requests',
'croniter', 'rsa',
'sqlalchemy', 'sqlalchemy',
'alembic', 'tz',
'websockets', 'waitress',
'websocket-client', 'websocket-client',
'websockets',
'wheel', 'wheel',
'zeroconf>=0.27.0', 'zeroconf>=0.27.0',
'tz',
'python-dateutil',
'rsa',
'marshmallow',
'marshmallow_dataclass',
'frozendict',
'flask',
'bcrypt',
'python-magic',
], ],
extras_require={ extras_require={
# Support for thread custom name # Support for thread custom name
@ -89,8 +90,9 @@ setup(
'pushbullet': [ 'pushbullet': [
'pushbullet.py @ https://github.com/rbrcsk/pushbullet.py/tarball/master' 'pushbullet.py @ https://github.com/rbrcsk/pushbullet.py/tarball/master'
], ],
# Support for HTTP backend over uWSGI # This is only kept for back-compatibility purposes, as all the
'http': ['gunicorn'], # dependencies of the HTTP webserver are now core dependencies.
'http': [],
# Support for MQTT backends # Support for MQTT backends
'mqtt': ['paho-mqtt'], 'mqtt': ['paho-mqtt'],
# Support for RSS feeds parser # Support for RSS feeds parser
@ -230,7 +232,7 @@ setup(
# Support for zigbee2mqtt # Support for zigbee2mqtt
'zigbee': ['paho-mqtt'], 'zigbee': ['paho-mqtt'],
# Support for Z-Wave # Support for Z-Wave
'zwave': ['python-openzwave'], 'zwave': ['paho-mqtt'],
# Support for Mozilla DeepSpeech speech-to-text engine # Support for Mozilla DeepSpeech speech-to-text engine
'deepspeech': ['deepspeech', 'numpy', 'sounddevice'], 'deepspeech': ['deepspeech', 'numpy', 'sounddevice'],
# Support for PicoVoice hotword detection engine # Support for PicoVoice hotword detection engine