forked from platypush/platypush
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:
parent
2c254e8eb9
commit
285f3941d9
5 changed files with 40 additions and 109 deletions
|
@ -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('../..'))
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
|
|
@ -22,3 +22,4 @@ paho-mqtt
|
||||||
websocket-client
|
websocket-client
|
||||||
croniter
|
croniter
|
||||||
python-magic
|
python-magic
|
||||||
|
waitress
|
||||||
|
|
32
setup.py
32
setup.py
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue