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. --> other music plugin enabled. -->
<Music class="col-3" /> <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" /> <DateTimeWeather class="col-3" />
</Row> </Row>
<!-- Display the following widgets on a second row --> <!-- Display the following widgets on a second row -->
<Row> <Row>
<!-- Show a carousel of images from a local folder. For security reasons, the folder must be <!-- 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" /> <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 <!-- Show the news headlines parsed from a list of RSS feed and stored locally through the
@ -151,11 +153,7 @@ class HttpBackend(Backend):
Requires: Requires:
* **flask** (``pip install flask``) * **gunicorn** (``pip install gunicorn``) - optional, to run the Platypush webapp over uWSGI.
* **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.
By default the Platypush web server will run in a By default the Platypush web server will run in a
process spawned on the fly by the HTTP backend. However, being 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_HTTP_PORT = 8008
_DEFAULT_WEBSOCKET_PORT = 8009 _DEFAULT_WEBSOCKET_PORT = 8009
def __init__(self, port=_DEFAULT_HTTP_PORT, def __init__(
websocket_port=_DEFAULT_WEBSOCKET_PORT, self,
bind_address='0.0.0.0', port=_DEFAULT_HTTP_PORT,
disable_websocket=False, resource_dirs=None, websocket_port=_DEFAULT_WEBSOCKET_PORT,
ssl_cert=None, ssl_key=None, ssl_cafile=None, ssl_capath=None, bind_address='0.0.0.0',
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) :param port: Listen port for the web server (default: 8008)
:type port: int :type port: int
@ -246,26 +254,37 @@ class HttpBackend(Backend):
self.bind_address = bind_address self.bind_address = bind_address
if resource_dirs: if resource_dirs:
self.resource_dirs = {name: os.path.abspath( self.resource_dirs = {
os.path.expanduser(d)) for name, d in resource_dirs.items()} name: os.path.abspath(os.path.expanduser(d))
for name, d in resource_dirs.items()
}
else: else:
self.resource_dirs = {} self.resource_dirs = {}
self.active_websockets = set() self.active_websockets = set()
self.run_externally = run_externally self.run_externally = run_externally
self.uwsgi_args = uwsgi_args or [] self.uwsgi_args = uwsgi_args or []
self.ssl_context = get_ssl_server_context(ssl_cert=ssl_cert, self.ssl_context = (
ssl_key=ssl_key, get_ssl_server_context(
ssl_cafile=ssl_cafile, ssl_cert=ssl_cert,
ssl_capath=ssl_capath) \ ssl_key=ssl_key,
if ssl_cert else None ssl_cafile=ssl_cafile,
ssl_capath=ssl_capath,
)
if ssl_cert
else None
)
if self.uwsgi_args: if self.uwsgi_args:
self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + \ self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + [
['--module', 'platypush.backend.http.uwsgi', '--enable-threads'] '--module',
'platypush.backend.http.uwsgi',
'--enable-threads',
]
self.local_base_url = '{proto}://localhost:{port}'.\ self.local_base_url = '{proto}://localhost:{port}'.format(
format(proto=('https' if ssl_cert else 'http'), port=self.port) proto=('https' if ssl_cert else 'http'), port=self.port
)
self._websocket_lock_timeout = 10 self._websocket_lock_timeout = 10
self._websocket_lock = threading.RLock() self._websocket_lock = threading.RLock()
@ -275,7 +294,7 @@ class HttpBackend(Backend):
self.logger.warning('Use cURL or any HTTP client to query the HTTP backend') self.logger.warning('Use cURL or any HTTP client to query the HTTP backend')
def on_stop(self): def on_stop(self):
""" On backend stop """ """On backend stop"""
super().on_stop() super().on_stop()
self.logger.info('Received STOP event on HttpBackend') self.logger.info('Received STOP event on HttpBackend')
@ -284,7 +303,9 @@ class HttpBackend(Backend):
self.server_proc.kill() self.server_proc.kill()
self.server_proc.wait(timeout=10) self.server_proc.wait(timeout=10)
if self.server_proc.poll() is not None: 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: else:
self.logger.info('HTTP server process terminated') self.logger.info('HTTP server process terminated')
else: else:
@ -293,17 +314,25 @@ class HttpBackend(Backend):
if self.server_proc.is_alive(): if self.server_proc.is_alive():
self.server_proc.kill() self.server_proc.kill()
if self.server_proc.is_alive(): 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: else:
self.logger.info('HTTP server process terminated') 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._websocket_loop.stop()
self.logger.info('HTTP websocket service terminated') self.logger.info('HTTP websocket service terminated')
def _acquire_websocket_lock(self, ws): def _acquire_websocket_lock(self, ws):
try: 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: if not acquire_ok:
raise TimeoutError('Websocket lock acquire timeout') raise TimeoutError('Websocket lock acquire timeout')
@ -313,13 +342,19 @@ class HttpBackend(Backend):
finally: finally:
self._websocket_lock.release() 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: 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): def _release_websocket_lock(self, ws):
try: 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: if not acquire_ok:
raise TimeoutError('Websocket lock acquire timeout') raise TimeoutError('Websocket lock acquire timeout')
@ -327,12 +362,15 @@ class HttpBackend(Backend):
if addr in self._websocket_locks: if addr in self._websocket_locks:
self._websocket_locks[addr].release() self._websocket_locks[addr].release()
except Exception as e: 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: finally:
self._websocket_lock.release() self._websocket_lock.release()
def notify_web_clients(self, event): def notify_web_clients(self, event):
""" Notify all the connected web clients (over websocket) of a new event """ """Notify all the connected web clients (over websocket) of a new event"""
async def send_event(ws): async def send_event(ws):
try: try:
self._acquire_websocket_lock(ws) self._acquire_websocket_lock(ws)
@ -349,26 +387,35 @@ class HttpBackend(Backend):
try: try:
loop.run_until_complete(send_event(_ws)) loop.run_until_complete(send_event(_ws))
except ConnectionClosed: 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) self.active_websockets.remove(_ws)
if _ws.remote_address in self._websocket_locks: if _ws.remote_address in self._websocket_locks:
del self._websocket_locks[_ws.remote_address] del self._websocket_locks[_ws.remote_address]
def websocket(self): def websocket(self):
""" Websocket main server """ """Websocket main server"""
set_thread_name('WebsocketServer') set_thread_name('WebsocketServer')
async def register_websocket(websocket, path): 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>' 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) self.active_websockets.add(websocket)
try: try:
await websocket.recv() await websocket.recv()
except ConnectionClosed: 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) self.active_websockets.remove(websocket)
if address in self._websocket_locks: if address in self._websocket_locks:
del self._websocket_locks[address] del self._websocket_locks[address]
@ -379,8 +426,13 @@ class HttpBackend(Backend):
self._websocket_loop = get_or_create_event_loop() self._websocket_loop = get_or_create_event_loop()
self._websocket_loop.run_until_complete( self._websocket_loop.run_until_complete(
websocket_serve(register_websocket, self.bind_address, self.websocket_port, websocket_serve(
**websocket_args)) register_websocket,
self.bind_address,
self.websocket_port,
**websocket_args
)
)
self._websocket_loop.run_forever() self._websocket_loop.run_forever()
def _start_web_server(self): def _start_web_server(self):
@ -415,8 +467,9 @@ class HttpBackend(Backend):
self.websocket_thread.start() self.websocket_thread.start()
if not self.run_externally: if not self.run_externally:
self.server_proc = Process(target=self._start_web_server(), self.server_proc = Process(
name='WebServer') target=self._start_web_server(), name='WebServer'
)
self.server_proc.start() self.server_proc.start()
self.server_proc.join() self.server_proc.join()
elif self.uwsgi_args: elif self.uwsgi_args:
@ -424,9 +477,11 @@ class HttpBackend(Backend):
self.logger.info('Starting uWSGI with arguments {}'.format(uwsgi_cmd)) self.logger.info('Starting uWSGI with arguments {}'.format(uwsgi_cmd))
self.server_proc = subprocess.Popen(uwsgi_cmd) self.server_proc = subprocess.Popen(uwsgi_cmd)
else: else:
self.logger.info('The web server is configured to be launched externally but ' + self.logger.info(
'no uwsgi_args were provided. Make sure that you run another external service' + 'The web server is configured to be launched externally but '
'for the webserver (e.g. nginx)') + '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: # vim:sw=4:ts=4:et:

View file

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

View file

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

View file

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