HTTP backend dependencies moved from optional to required

If Platypush is supposed to work also without a manually created
`config.yaml`, and the HTTP backend is enabled by default in that
configuration, then Flask and companions should be among the required
dependencies.
This commit is contained in:
Fabio Manganiello 2022-04-27 14:52:41 +02:00
parent 371fd7e46b
commit fee5fc4ae0
Signed by: blacklight
GPG key ID: D90FBA7F76362774
4 changed files with 132 additions and 61 deletions

View file

@ -91,14 +91,16 @@ class HttpBackend(Backend):
other music plugin enabled. -->
<Music class="col-3" />
<!-- Show current date, time and weather. It requires a `weather` plugin or backend enabled -->
<!-- Show current date, time and weather.
It requires a `weather` plugin or backend enabled -->
<DateTimeWeather class="col-3" />
</Row>
<!-- Display the following widgets on a second row -->
<Row>
<!-- Show a carousel of images from a local folder. For security reasons, the folder must be
explicitly exposed as an HTTP resource through the backend `resource_dirs` attribute. -->
explicitly exposed as an HTTP resource through the backend
`resource_dirs` attribute. -->
<ImageCarousel class="col-6" img-dir="/mnt/hd/photos/carousel" />
<!-- Show the news headlines parsed from a list of RSS feed and stored locally through the
@ -151,11 +153,7 @@ class HttpBackend(Backend):
Requires:
* **flask** (``pip install flask``)
* **bcrypt** (``pip install bcrypt``)
* **magic** (``pip install python-magic``), optional, for MIME type
support if you want to enable media streaming
* **gunicorn** (``pip install gunicorn``) - optional but recommended.
* **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
@ -174,12 +172,22 @@ class HttpBackend(Backend):
_DEFAULT_HTTP_PORT = 8008
_DEFAULT_WEBSOCKET_PORT = 8009
def __init__(self, port=_DEFAULT_HTTP_PORT,
def __init__(
self,
port=_DEFAULT_HTTP_PORT,
websocket_port=_DEFAULT_WEBSOCKET_PORT,
bind_address='0.0.0.0',
disable_websocket=False, resource_dirs=None,
ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None,
maps=None, run_externally=False, uwsgi_args=None, **kwargs):
disable_websocket=False,
resource_dirs=None,
ssl_cert=None,
ssl_key=None,
ssl_cafile=None,
ssl_capath=None,
maps=None,
run_externally=False,
uwsgi_args=None,
**kwargs
):
"""
:param port: Listen port for the web server (default: 8008)
:type port: int
@ -246,26 +254,37 @@ class HttpBackend(Backend):
self.bind_address = bind_address
if resource_dirs:
self.resource_dirs = {name: os.path.abspath(
os.path.expanduser(d)) for name, d in resource_dirs.items()}
self.resource_dirs = {
name: os.path.abspath(os.path.expanduser(d))
for name, d in resource_dirs.items()
}
else:
self.resource_dirs = {}
self.active_websockets = set()
self.run_externally = run_externally
self.uwsgi_args = uwsgi_args or []
self.ssl_context = get_ssl_server_context(ssl_cert=ssl_cert,
self.ssl_context = (
get_ssl_server_context(
ssl_cert=ssl_cert,
ssl_key=ssl_key,
ssl_cafile=ssl_cafile,
ssl_capath=ssl_capath) \
if ssl_cert else None
ssl_capath=ssl_capath,
)
if ssl_cert
else None
)
if self.uwsgi_args:
self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + \
['--module', 'platypush.backend.http.uwsgi', '--enable-threads']
self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + [
'--module',
'platypush.backend.http.uwsgi',
'--enable-threads',
]
self.local_base_url = '{proto}://localhost:{port}'.\
format(proto=('https' if ssl_cert else 'http'), port=self.port)
self.local_base_url = '{proto}://localhost:{port}'.format(
proto=('https' if ssl_cert else 'http'), port=self.port
)
self._websocket_lock_timeout = 10
self._websocket_lock = threading.RLock()
@ -284,7 +303,9 @@ class HttpBackend(Backend):
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')
self.logger.info(
'HTTP server process may be still alive at termination'
)
else:
self.logger.info('HTTP server process terminated')
else:
@ -293,17 +314,25 @@ class HttpBackend(Backend):
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')
self.logger.info(
'HTTP server process may be still alive at termination'
)
else:
self.logger.info('HTTP server process terminated')
if self.websocket_thread and self.websocket_thread.is_alive() and self._websocket_loop:
if (
self.websocket_thread
and self.websocket_thread.is_alive()
and self._websocket_loop
):
self._websocket_loop.stop()
self.logger.info('HTTP websocket service terminated')
def _acquire_websocket_lock(self, ws):
try:
acquire_ok = self._websocket_lock.acquire(timeout=self._websocket_lock_timeout)
acquire_ok = self._websocket_lock.acquire(
timeout=self._websocket_lock_timeout
)
if not acquire_ok:
raise TimeoutError('Websocket lock acquire timeout')
@ -313,13 +342,19 @@ class HttpBackend(Backend):
finally:
self._websocket_lock.release()
acquire_ok = self._websocket_locks[addr].acquire(timeout=self._websocket_lock_timeout)
acquire_ok = self._websocket_locks[addr].acquire(
timeout=self._websocket_lock_timeout
)
if not acquire_ok:
raise TimeoutError('Websocket on address {} not ready to receive data'.format(addr))
raise TimeoutError(
'Websocket on address {} not ready to receive data'.format(addr)
)
def _release_websocket_lock(self, ws):
try:
acquire_ok = self._websocket_lock.acquire(timeout=self._websocket_lock_timeout)
acquire_ok = self._websocket_lock.acquire(
timeout=self._websocket_lock_timeout
)
if not acquire_ok:
raise TimeoutError('Websocket lock acquire timeout')
@ -327,12 +362,15 @@ class HttpBackend(Backend):
if addr in self._websocket_locks:
self._websocket_locks[addr].release()
except Exception as e:
self.logger.warning('Unhandled exception while releasing websocket lock: {}'.format(str(e)))
self.logger.warning(
'Unhandled exception while releasing websocket lock: {}'.format(str(e))
)
finally:
self._websocket_lock.release()
def notify_web_clients(self, event):
"""Notify all the connected web clients (over websocket) of a new event"""
async def send_event(ws):
try:
self._acquire_websocket_lock(ws)
@ -349,7 +387,9 @@ class HttpBackend(Backend):
try:
loop.run_until_complete(send_event(_ws))
except ConnectionClosed:
self.logger.warning('Websocket client {} connection lost'.format(_ws.remote_address))
self.logger.warning(
'Websocket client {} connection lost'.format(_ws.remote_address)
)
self.active_websockets.remove(_ws)
if _ws.remote_address in self._websocket_locks:
del self._websocket_locks[_ws.remote_address]
@ -359,16 +399,23 @@ class HttpBackend(Backend):
set_thread_name('WebsocketServer')
async def register_websocket(websocket, path):
address = websocket.remote_address if websocket.remote_address \
address = (
websocket.remote_address
if websocket.remote_address
else '<unknown client>'
)
self.logger.info('New websocket connection from {} on path {}'.format(address, path))
self.logger.info(
'New websocket connection from {} on path {}'.format(address, path)
)
self.active_websockets.add(websocket)
try:
await websocket.recv()
except ConnectionClosed:
self.logger.info('Websocket client {} closed connection'.format(address))
self.logger.info(
'Websocket client {} closed connection'.format(address)
)
self.active_websockets.remove(websocket)
if address in self._websocket_locks:
del self._websocket_locks[address]
@ -379,8 +426,13 @@ class HttpBackend(Backend):
self._websocket_loop = get_or_create_event_loop()
self._websocket_loop.run_until_complete(
websocket_serve(register_websocket, self.bind_address, self.websocket_port,
**websocket_args))
websocket_serve(
register_websocket,
self.bind_address,
self.websocket_port,
**websocket_args
)
)
self._websocket_loop.run_forever()
def _start_web_server(self):
@ -415,8 +467,9 @@ class HttpBackend(Backend):
self.websocket_thread.start()
if not self.run_externally:
self.server_proc = Process(target=self._start_web_server(),
name='WebServer')
self.server_proc = Process(
target=self._start_web_server(), name='WebServer'
)
self.server_proc.start()
self.server_proc.join()
elif self.uwsgi_args:
@ -424,9 +477,11 @@ class HttpBackend(Backend):
self.logger.info('Starting uWSGI with arguments {}'.format(uwsgi_cmd))
self.server_proc = subprocess.Popen(uwsgi_cmd)
else:
self.logger.info('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 webserver (e.g. nginx)')
self.logger.info(
'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 webserver (e.g. nginx)'
)
# vim:sw=4:ts=4:et:

View file

@ -2,9 +2,6 @@ manifest:
events: {}
install:
pip:
- flask
- bcrypt
- python-magic
- gunicorn
package: platypush.backend.http
type: backend

View file

@ -20,3 +20,4 @@ zeroconf
paho-mqtt
websocket-client
croniter
python-magic

View file

@ -17,7 +17,7 @@ def readfile(fname):
def pkg_files(dir):
paths = []
# noinspection PyShadowingNames
for (path, dirs, files) in os.walk(dir):
for (path, _, files) in os.walk(dir):
for file in files:
paths.append(os.path.join('..', path, file))
return paths
@ -68,17 +68,21 @@ setup(
'pyjwt',
'marshmallow',
'frozendict',
'flask',
'bcrypt',
'python-magic',
],
extras_require={
# Support for thread custom name
'threadname': ['python-prctl'],
# Support for Kafka backend and plugin
'kafka': ['kafka-python'],
# Support for Pushbullet backend and plugin
'pushbullet': ['pushbullet.py @ https://github.com/rbrcsk/pushbullet.py/tarball/master'],
# Support for HTTP backend
'http': ['flask', 'bcrypt', 'python-magic', 'gunicorn'],
'pushbullet': [
'pushbullet.py @ https://github.com/rbrcsk/pushbullet.py/tarball/master'
],
# Support for HTTP backend over uWSGI
'http': ['gunicorn'],
# Support for MQTT backends
'mqtt': ['paho-mqtt'],
# Support for RSS feeds parser
@ -90,7 +94,11 @@ setup(
# Support for MPD/Mopidy music server plugin and backend
'mpd': ['python-mpd2'],
# Support for Google text2speech plugin
'google-tts': ['oauth2client', 'google-api-python-client', 'google-cloud-texttospeech'],
'google-tts': [
'oauth2client',
'google-api-python-client',
'google-cloud-texttospeech',
],
# Support for OMXPlayer plugin
'omxplayer': ['omxplayer-wrapper'],
# Support for YouTube
@ -138,7 +146,8 @@ setup(
# Support for web media subtitles
'subtitles': [
'webvtt-py',
'python-opensubtitles @ https://github.com/agonzalezro/python-opensubtitles/tarball/master'],
'python-opensubtitles @ https://github.com/agonzalezro/python-opensubtitles/tarball/master',
],
# Support for mpv player plugin
'mpv': ['python-mpv'],
# Support for NFC tags
@ -156,14 +165,21 @@ setup(
# Support for Dropbox integration
'dropbox': ['dropbox'],
# Support for Leap Motion backend
'leap': ['leap-sdk @ https://github.com/BlackLight/leap-sdk-python3/tarball/master'],
'leap': [
'leap-sdk @ https://github.com/BlackLight/leap-sdk-python3/tarball/master'
],
# Support for Flic buttons
'flic': ['flic @ https://github.com/50ButtonsEach/fliclib-linux-hci/tarball/master'],
'flic': [
'flic @ https://github.com/50ButtonsEach/fliclib-linux-hci/tarball/master'
],
# Support for Alexa/Echo plugin
'alexa': ['avs @ https://github.com/BlackLight/avs/tarball/master'],
# Support for bluetooth devices
'bluetooth': ['pybluez', 'gattlib',
'pyobex @ https://github.com/BlackLight/PyOBEX/tarball/master'],
'bluetooth': [
'pybluez',
'gattlib',
'pyobex @ https://github.com/BlackLight/PyOBEX/tarball/master',
],
# Support for TP-Link devices
'tplink': ['pyHS100'],
# Support for PMW3901 2-Dimensional Optical Flow Sensor
@ -231,7 +247,9 @@ setup(
# Support for Twilio integration
'twilio': ['twilio'],
# Support for DHT11/DHT22/AM2302 temperature/humidity sensors
'dht': ['Adafruit_Python_DHT @ git+https://github.com/adafruit/Adafruit_Python_DHT'],
'dht': [
'Adafruit_Python_DHT @ git+https://github.com/adafruit/Adafruit_Python_DHT'
],
# Support for LCD display integration
'lcd': ['RPi.GPIO', 'RPLCD'],
# Support for IMAP mail integration