Removed dependency from prctl.

Also, black'd and LINT-fixed some files that hadn't been touched in a
while.
This commit is contained in:
Fabio Manganiello 2023-07-23 19:04:01 +02:00
parent 04b759e4d5
commit 0dc380fa94
Signed by: blacklight
GPG key ID: D90FBA7F76362774
14 changed files with 270 additions and 199 deletions

View file

@ -23,7 +23,7 @@ from .message.event import Event
from .message.event.application import ApplicationStartedEvent from .message.event.application import ApplicationStartedEvent
from .message.request import Request from .message.request import Request
from .message.response import Response from .message.response import Response
from .utils import set_thread_name, get_enabled_plugins from .utils import get_enabled_plugins
__author__ = 'Fabio Manganiello <fabio@manganiello.tech>' __author__ = 'Fabio Manganiello <fabio@manganiello.tech>'
__version__ = '0.50.3' __version__ = '0.50.3'
@ -252,7 +252,6 @@ class Daemon:
if not self.no_capture_stderr: if not self.no_capture_stderr:
sys.stderr = Logger(log.warning) sys.stderr = Logger(log.warning)
set_thread_name('platypush')
log.info('---- Starting platypush v.%s', __version__) log.info('---- Starting platypush v.%s', __version__)
# Initialize the backends and link them to the bus # Initialize the backends and link them to the bus

View file

@ -18,7 +18,6 @@ from platypush.utils import (
set_timeout, set_timeout,
clear_timeout, clear_timeout,
get_redis_queue_name_by_message, get_redis_queue_name_by_message,
set_thread_name,
get_backend_name_by_class, get_backend_name_by_class,
) )
@ -81,7 +80,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
self._request_context = kwargs['_req_ctx'] if '_req_ctx' in kwargs else None self._request_context = kwargs['_req_ctx'] if '_req_ctx' in kwargs else None
if 'logging' in kwargs: if 'logging' in kwargs:
self.logger.setLevel(getattr(logging, kwargs.get('logging').upper())) self.logger.setLevel(getattr(logging, kwargs['logging'].upper()))
def on_message(self, msg): def on_message(self, msg):
""" """
@ -95,21 +94,23 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
""" """
msg = Message.build(msg) msg = Message.build(msg)
if msg is None:
return
if not getattr(msg, 'target', None) or msg.target != self.device_id: if not getattr(msg, 'target', None) or msg.target != self.device_id:
return # Not for me return # Not for me
self.logger.debug( self.logger.debug(
'Message received on the {} backend: {}'.format( 'Message received on the %s backend: %s', self.__class__.__name__, msg
self.__class__.__name__, msg
)
) )
if self._is_expected_response(msg): if self._is_expected_response(msg):
# Expected response, trigger the response handler # Expected response, trigger the response handler
clear_timeout() clear_timeout()
# pylint: disable=unsubscriptable-object if self._request_context:
self._request_context['on_response'](msg) # pylint: disable=unsubscriptable-object
self._request_context['on_response'](msg)
self.stop() self.stop()
return return
@ -117,8 +118,10 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
self.bus.post(msg) self.bus.post(msg)
def _is_expected_response(self, msg): def _is_expected_response(self, msg):
"""Internal only - returns true if we are expecting for a response """
and msg is that response""" Internal only - returns true if we are expecting for a response and msg
is that response.
"""
# pylint: disable=unsubscriptable-object # pylint: disable=unsubscriptable-object
return ( return (
@ -131,12 +134,12 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
config_name = ( config_name = (
'backend.' + self.__class__.__name__.split('Backend', maxsplit=1)[0].lower() 'backend.' + self.__class__.__name__.split('Backend', maxsplit=1)[0].lower()
) )
return Config.get(config_name) return Config.get(config_name) or {}
def _setup_response_handler(self, request, on_response, response_timeout): def _setup_response_handler(self, request, on_response, response_timeout):
def _timeout_hndl(): def _timeout_hndl():
raise RuntimeError( raise RuntimeError(
'Timed out while waiting for a response from {}'.format(request.target) f'Timed out while waiting for a response from {request.target}'
) )
req_ctx = { req_ctx = {
@ -177,7 +180,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
request, request,
on_response=None, on_response=None,
response_timeout=_default_response_timeout, response_timeout=_default_response_timeout,
**kwargs **kwargs,
): ):
""" """
Send a request message on the backend. Send a request message on the backend.
@ -237,9 +240,10 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
except KeyError: except KeyError:
self.logger.warning( self.logger.warning(
( (
"Backend {} does not implement send_message " "Backend %s does not implement send_message "
"and the fallback Redis backend isn't configured" "and the fallback Redis backend isn't configured"
).format(self.__class__.__name__) ),
self.__class__.__name__,
) )
return return
@ -248,7 +252,6 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
def run(self): def run(self):
"""Starts the backend thread. To be implemented in the derived classes if the loop method isn't defined.""" """Starts the backend thread. To be implemented in the derived classes if the loop method isn't defined."""
self.thread_id = get_ident() self.thread_id = get_ident()
set_thread_name(self._thread_name)
if not callable(self.loop): if not callable(self.loop):
return return
@ -272,21 +275,19 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
time.sleep(5) time.sleep(5)
except Exception as e: except Exception as e:
self.logger.error( self.logger.error(
'{} initialization error: {}'.format( '%s initialization error: %s', self.__class__.__name__, e
self.__class__.__name__, str(e)
)
) )
self.logger.exception(e) self.logger.exception(e)
time.sleep(self.poll_seconds or 5) time.sleep(self.poll_seconds or 5)
def __enter__(self): def __enter__(self):
"""Invoked when the backend is initialized, if the main logic is within a ``loop()`` function""" """Invoked when the backend is initialized, if the main logic is within a ``loop()`` function"""
self.logger.info('Initialized backend {}'.format(self.__class__.__name__)) self.logger.info('Initialized backend %s', self.__class__.__name__)
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, *_, **__):
"""Invoked when the backend is terminated, if the main logic is within a ``loop()`` function""" """Invoked when the backend is terminated, if the main logic is within a ``loop()`` function"""
self.on_stop() self.on_stop()
self.logger.info('Terminated backend {}'.format(self.__class__.__name__)) self.logger.info('Terminated backend %s', self.__class__.__name__)
def on_stop(self): def on_stop(self):
"""Callback invoked when the process stops""" """Callback invoked when the process stops"""
@ -324,9 +325,14 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
return redis return redis
def get_message_response(self, msg): def get_message_response(self, msg):
queue = get_redis_queue_name_by_message(msg)
if not queue:
self.logger.warning('No response queue configured for the message')
return None
try: try:
redis = self._get_redis() redis = self._get_redis()
response = redis.blpop(get_redis_queue_name_by_message(msg), timeout=60) response = redis.blpop(queue, timeout=60)
if response and len(response) > 1: if response and len(response) > 1:
response = Message.build(response[1]) response = Message.build(response[1])
else: else:
@ -334,9 +340,9 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
return response return response
except Exception as e: except Exception as e:
self.logger.error( self.logger.error('Error while processing response to %s: %s', msg, e)
'Error while processing response to {}: {}'.format(msg, str(e))
) return None
@staticmethod @staticmethod
def _get_ip() -> str: def _get_ip() -> str:
@ -395,18 +401,9 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
} }
name = name or re.sub(r'Backend$', '', self.__class__.__name__).lower() name = name or re.sub(r'Backend$', '', self.__class__.__name__).lower()
srv_type = srv_type or '_platypush-{name}._{proto}.local.'.format( srv_type = srv_type or f'_platypush-{name}._{"udp" if udp else "tcp"}.local.'
name=name, proto='udp' if udp else 'tcp' srv_name = srv_name or f'{self.device_id}.{srv_type}'
) srv_port = port if port else getattr(self, 'port', None)
srv_name = srv_name or '{host}.{type}'.format(
host=self.device_id, type=srv_type
)
if port:
srv_port = port
else:
srv_port = self.port if hasattr(self, 'port') else None
self.zeroconf_info = ServiceInfo( self.zeroconf_info = ServiceInfo(
srv_type, srv_type,
srv_name, srv_name,
@ -439,9 +436,10 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
self.zeroconf.unregister_service(self.zeroconf_info) self.zeroconf.unregister_service(self.zeroconf_info)
except Exception as e: except Exception as e:
self.logger.warning( self.logger.warning(
'Could not register Zeroconf service {}: {}: {}'.format( 'Could not register Zeroconf service %s: %s: %s',
self.zeroconf_info.name, type(e).__name__, str(e) self.zeroconf_info.name,
) type(e).__name__,
e,
) )
if self.zeroconf: if self.zeroconf:

View file

@ -5,7 +5,6 @@ from typing import Optional, List
from platypush.backend import Backend from platypush.backend import Backend
from platypush.context import get_plugin from platypush.context import get_plugin
from platypush.message.event.google.pubsub import GooglePubsubMessageEvent from platypush.message.event.google.pubsub import GooglePubsubMessageEvent
from platypush.utils import set_thread_name
class GooglePubsubBackend(Backend): class GooglePubsubBackend(Backend):
@ -25,7 +24,9 @@ class GooglePubsubBackend(Backend):
""" """
def __init__(self, topics: List[str], credentials_file: Optional[str] = None, *args, **kwargs): def __init__(
self, topics: List[str], credentials_file: Optional[str] = None, *args, **kwargs
):
""" """
:param topics: List of topics to subscribe. You can either specify the full topic name in the format :param topics: List of topics to subscribe. You can either specify the full topic name in the format
``projects/<project_id>/topics/<topic_name>``, where ``<project_id>`` must be the ID of your ``projects/<project_id>/topics/<topic_name>``, where ``<project_id>`` must be the ID of your
@ -35,7 +36,7 @@ class GooglePubsubBackend(Backend):
``google.pubsub`` plugin or ``~/.credentials/platypush/google/pubsub.json``). ``google.pubsub`` plugin or ``~/.credentials/platypush/google/pubsub.json``).
""" """
super().__init__(*args, **kwargs) super().__init__(*args, name='GooglePubSub', **kwargs)
self.topics = topics self.topics = topics
if credentials_file: if credentials_file:
@ -46,7 +47,9 @@ class GooglePubsubBackend(Backend):
@staticmethod @staticmethod
def _get_plugin(): def _get_plugin():
return get_plugin('google.pubsub') plugin = get_plugin('google.pubsub')
assert plugin, 'google.pubsub plugin not enabled'
return plugin
def _message_callback(self, topic): def _message_callback(self, topic):
def callback(msg): def callback(msg):
@ -54,7 +57,7 @@ class GooglePubsubBackend(Backend):
try: try:
data = json.loads(data) data = json.loads(data)
except Exception as e: except Exception as e:
self.logger.debug('Not a valid JSON: {}: {}'.format(data, str(e))) self.logger.debug('Not a valid JSON: %s: %s', data, e)
msg.ack() msg.ack()
self.bus.post(GooglePubsubMessageEvent(topic=topic, msg=data)) self.bus.post(GooglePubsubMessageEvent(topic=topic, msg=data))
@ -64,20 +67,23 @@ class GooglePubsubBackend(Backend):
def run(self): def run(self):
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
from google.cloud import pubsub_v1 from google.cloud import pubsub_v1
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
from google.api_core.exceptions import AlreadyExists from google.api_core.exceptions import AlreadyExists
super().run() super().run()
set_thread_name('GooglePubSub')
plugin = self._get_plugin() plugin = self._get_plugin()
project_id = plugin.get_project_id() project_id = plugin.get_project_id()
credentials = plugin.get_credentials(plugin.subscriber_audience) credentials = plugin.get_credentials(plugin.subscriber_audience)
subscriber = pubsub_v1.SubscriberClient(credentials=credentials) subscriber = pubsub_v1.SubscriberClient(credentials=credentials)
for topic in self.topics: for topic in self.topics:
if not topic.startswith('projects/{}/topics/'.format(project_id)): prefix = f'projects/{project_id}/topics/'
topic = 'projects/{}/topics/{}'.format(project_id, topic) if not topic.startswith(prefix):
subscription_name = '/'.join([*topic.split('/')[:2], 'subscriptions', topic.split('/')[-1]]) topic = f'{prefix}{topic}'
subscription_name = '/'.join(
[*topic.split('/')[:2], 'subscriptions', topic.split('/')[-1]]
)
try: try:
subscriber.create_subscription(name=subscription_name, topic=topic) subscriber.create_subscription(name=subscription_name, topic=topic)

View file

@ -1,28 +1,36 @@
import logging import logging
import re import re
import requests from threading import Thread
import time import time
import requests
from frozendict import frozendict from frozendict import frozendict
from threading import Thread
from platypush.message.event.http import HttpEvent from platypush.message.event.http import HttpEvent
from platypush.utils import set_thread_name
class HttpRequest(object): class HttpRequest:
"""
Backend used for polling HTTP resources.
"""
poll_seconds = 60 poll_seconds = 60
timeout = 5 timeout = 5
class HttpRequestArguments(object): class HttpRequestArguments:
def __init__(self, url, method='get', *args, **kwargs): """
Models the properties of an HTTP request.
"""
def __init__(self, url, *args, method='get', **kwargs):
self.method = method.lower() self.method = method.lower()
self.url = url self.url = url
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
def __init__(self, args, bus=None, poll_seconds=None, timeout=None, def __init__(
skip_first_call=True, **kwargs): self, args, bus=None, poll_seconds=None, timeout=None, skip_first_call=True, **_
):
super().__init__() super().__init__()
self.poll_seconds = poll_seconds or self.poll_seconds self.poll_seconds = poll_seconds or self.poll_seconds
@ -43,12 +51,13 @@ class HttpRequest(object):
self.args.kwargs['timeout'] = self.timeout self.args.kwargs['timeout'] = self.timeout
self.request_args = { self.request_args = {
'method': self.args.method, 'url': self.args.url, **self.args.kwargs 'method': self.args.method,
'url': self.args.url,
**self.args.kwargs,
} }
def execute(self): def execute(self):
def _thread_func(): def _thread_func():
set_thread_name('HttpPoll')
is_first_call = self.last_request_timestamp == 0 is_first_call = self.last_request_timestamp == 0
self.last_request_timestamp = time.time() self.last_request_timestamp = time.time()
@ -63,30 +72,45 @@ class HttpRequest(object):
else: else:
event = HttpEvent(dict(self), new_items) event = HttpEvent(dict(self), new_items)
if new_items and self.bus: if (
if not self.skip_first_call or ( new_items
self.skip_first_call and not is_first_call): and self.bus
self.bus.post(event) and (
not self.skip_first_call
or (self.skip_first_call and not is_first_call)
)
):
self.bus.post(event)
response.raise_for_status() response.raise_for_status()
except Exception as e: except Exception as e:
self.logger.exception(e) self.logger.exception(e)
self.logger.warning('Encountered an error while retrieving {}: {}'. self.logger.warning(
format(self.args.url, str(e))) 'Encountered an error while retrieving %s: %s', self.args.url, e
)
Thread(target=_thread_func, name='HttpPoll').start() Thread(target=_thread_func, name='HttpPoll').start()
def get_new_items(self, response): def get_new_items(self, response):
""" Gets new items out of a response """ """Gets new items out of a response"""
raise NotImplementedError("get_new_items must be implemented in a derived class") raise NotImplementedError(
"get_new_items must be implemented in a derived class"
)
def __iter__(self): def __iter__(self):
for (key, value) in self.request_args.items(): """
:return: The ``request_args`` as key-value pairs.
"""
for key, value in self.request_args.items():
yield key, value yield key, value
class JsonHttpRequest(HttpRequest): class JsonHttpRequest(HttpRequest):
def __init__(self, path=None, *args, **kwargs): """
Specialization of the HttpRequest class for JSON requests.
"""
def __init__(self, *args, path=None, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.path = path self.path = path
self.seen_entries = set() self.seen_entries = set()
@ -97,7 +121,8 @@ class JsonHttpRequest(HttpRequest):
if self.path: if self.path:
m = re.match(r'\${\s*(.*)\s*}', self.path) m = re.match(r'\${\s*(.*)\s*}', self.path)
response = eval(m.group(1)) if m:
response = eval(m.group(1)) # pylint: disable=eval-used
for entry in response: for entry in response:
flattened_entry = deep_freeze(entry) flattened_entry = deep_freeze(entry)
@ -109,6 +134,11 @@ class JsonHttpRequest(HttpRequest):
def deep_freeze(x): def deep_freeze(x):
"""
Deep freezes a Python object - works for strings, dictionaries, sets and
iterables.
"""
if isinstance(x, str) or not hasattr(x, "__len__"): if isinstance(x, str) or not hasattr(x, "__len__"):
return x return x
if hasattr(x, "keys") and hasattr(x, "values"): if hasattr(x, "keys") and hasattr(x, "values"):
@ -118,4 +148,5 @@ def deep_freeze(x):
return frozenset(map(deep_freeze, x)) return frozenset(map(deep_freeze, x))
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -5,11 +5,17 @@ import threading
import time import time
from platypush.backend import Backend from platypush.backend import Backend
from platypush.utils import set_thread_name from platypush.message.event.music.snapcast import (
from platypush.message.event.music.snapcast import ClientVolumeChangeEvent, \ ClientVolumeChangeEvent,
GroupMuteChangeEvent, ClientConnectedEvent, ClientDisconnectedEvent, \ GroupMuteChangeEvent,
ClientLatencyChangeEvent, ClientNameChangeEvent, GroupStreamChangeEvent, \ ClientConnectedEvent,
StreamUpdateEvent, ServerUpdateEvent ClientDisconnectedEvent,
ClientLatencyChangeEvent,
ClientNameChangeEvent,
GroupStreamChangeEvent,
StreamUpdateEvent,
ServerUpdateEvent,
)
class MusicSnapcastBackend(Backend): class MusicSnapcastBackend(Backend):
@ -31,11 +37,17 @@ class MusicSnapcastBackend(Backend):
""" """
_DEFAULT_SNAPCAST_PORT = 1705 _DEFAULT_SNAPCAST_PORT = 1705
_DEFAULT_POLL_SECONDS = 10 # Poll servers each 10 seconds _DEFAULT_POLL_SECONDS = 10 # Poll servers each 10 seconds
_SOCKET_EOL = '\r\n'.encode() _SOCKET_EOL = '\r\n'.encode()
def __init__(self, hosts=None, ports=None, def __init__(
poll_seconds=_DEFAULT_POLL_SECONDS, *args, **kwargs): self,
hosts=None,
ports=None,
poll_seconds=_DEFAULT_POLL_SECONDS,
*args,
**kwargs,
):
""" """
:param hosts: List of Snapcast server names or IPs to monitor (default: :param hosts: List of Snapcast server names or IPs to monitor (default:
`['localhost']` `['localhost']`
@ -72,24 +84,25 @@ class MusicSnapcastBackend(Backend):
if self._socks.get(host): if self._socks.get(host):
return self._socks[host] return self._socks[host]
self.logger.debug('Connecting to {host}:{port}'.format(host=host, port=port)) self.logger.debug('Connecting to %s:%d', host, port)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port)) sock.connect((host, port))
self._socks[host] = sock self._socks[host] = sock
self.logger.info('Connected to {host}:{port}'.format(host=host, port=port)) self.logger.info('Connected to %s:%d', host, port)
return sock return sock
def _disconnect(self, host, port): def _disconnect(self, host, port):
sock = self._socks.get(host) sock = self._socks.get(host)
if not sock: if not sock:
self.logger.debug('Not connected to {}:{}'.format(host, port)) self.logger.debug('Not connected to %s:%d', host, port)
return return
try: try:
sock.close() sock.close()
except Exception as e: except Exception as e:
self.logger.warning(('Exception while disconnecting from {host}:{port}: {error}'. self.logger.warning(
format(host=host, port=port, error=str(e)))) 'Exception while disconnecting from %s:%d: %s', host, port, e
)
finally: finally:
self._socks[host] = None self._socks[host] = None
@ -103,7 +116,7 @@ class MusicSnapcastBackend(Backend):
if ready[0]: if ready[0]:
buf += sock.recv(1) buf += sock.recv(1)
else: else:
return return None
return json.loads(buf.decode().strip()) return json.loads(buf.decode().strip())
@ -115,8 +128,9 @@ class MusicSnapcastBackend(Backend):
client_id = msg.get('params', {}).get('id') client_id = msg.get('params', {}).get('id')
volume = msg.get('params', {}).get('volume', {}).get('percent') volume = msg.get('params', {}).get('volume', {}).get('percent')
muted = msg.get('params', {}).get('volume', {}).get('muted') muted = msg.get('params', {}).get('volume', {}).get('muted')
evt = ClientVolumeChangeEvent(host=host, client=client_id, evt = ClientVolumeChangeEvent(
volume=volume, muted=muted) host=host, client=client_id, volume=volume, muted=muted
)
elif msg.get('method') == 'Group.OnMute': elif msg.get('method') == 'Group.OnMute':
group_id = msg.get('params', {}).get('id') group_id = msg.get('params', {}).get('id')
muted = msg.get('params', {}).get('mute') muted = msg.get('params', {}).get('mute')
@ -149,10 +163,8 @@ class MusicSnapcastBackend(Backend):
return evt return evt
def _client(self, host, port, thread_name): def _client(self, host, port):
def _thread(): def _thread():
set_thread_name(thread_name)
while not self.should_stop(): while not self.should_stop():
try: try:
sock = self._connect(host, port) sock = self._connect(host, port)
@ -164,16 +176,20 @@ class MusicSnapcastBackend(Backend):
msgs = [msgs] msgs = [msgs]
for msg in msgs: for msg in msgs:
self.logger.debug('Received message on {host}:{port}: {msg}'. self.logger.debug(
format(host=host, port=port, msg=msg)) 'Received message on {host}:{port}: {msg}'.format(
host=host, port=port, msg=msg
)
)
# noinspection PyTypeChecker
evt = self._parse_msg(host=host, msg=msg) evt = self._parse_msg(host=host, msg=msg)
if evt: if evt:
self.bus.post(evt) self.bus.post(evt)
except Exception as e: except Exception as e:
self.logger.warning('Exception while getting the status ' + 'of the Snapcast server {}:{}: {}'. self.logger.warning(
format(host, port, str(e))) 'Exception while getting the status '
+ 'of the Snapcast server {}:{}: {}'.format(host, port, str(e))
)
self._disconnect(host, port) self._disconnect(host, port)
finally: finally:
@ -184,17 +200,19 @@ class MusicSnapcastBackend(Backend):
def run(self): def run(self):
super().run() super().run()
self.logger.info('Initialized Snapcast backend - hosts: {} ports: {}'. self.logger.info(
format(self.hosts, self.ports)) 'Initialized Snapcast backend - hosts: {} ports: {}'.format(
self.hosts, self.ports
)
)
while not self.should_stop(): while not self.should_stop():
for i, host in enumerate(self.hosts): for i, host in enumerate(self.hosts):
port = self.ports[i] port = self.ports[i]
thread_name = 'Snapcast-{host}-{port}'.format(host=host, port=port) thread_name = f'Snapcast-{host}-{port}'
self._threads[host] = threading.Thread( self._threads[host] = threading.Thread(
target=self._client(host, port, thread_name), target=self._client(host, port), name=thread_name
name=thread_name
) )
self._threads[host].start() self._threads[host].start()
@ -211,8 +229,11 @@ class MusicSnapcastBackend(Backend):
try: try:
sock.close() sock.close()
except Exception as e: except Exception as e:
self.logger.warning('Could not close Snapcast connection to {}: {}: {}'.format( self.logger.warning(
host, type(e), str(e))) 'Could not close Snapcast connection to {}: {}: {}'.format(
host, type(e), str(e)
)
)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -6,7 +6,6 @@ from typing import Optional
from platypush.backend import Backend from platypush.backend import Backend
from platypush.message import Message from platypush.message import Message
from platypush.utils import set_thread_name
class TcpBackend(Backend): class TcpBackend(Backend):
@ -17,19 +16,20 @@ class TcpBackend(Backend):
# Maximum length of a request to be processed # Maximum length of a request to be processed
_MAX_REQ_SIZE = 2048 _MAX_REQ_SIZE = 2048
def __init__(self, port, bind_address=None, listen_queue=5, *args, **kwargs): def __init__(self, port, bind_address=None, listen_queue=5, **kwargs):
""" """
:param port: TCP port number :param port: TCP port number
:type port: int :type port: int
:param bind_address: Specify a bind address if you want to hook the service to a specific interface (default: listen for any connections) :param bind_address: Specify a bind address if you want to hook the
service to a specific interface (default: listen for any connections).
:type bind_address: str :type bind_address: str
:param listen_queue: Maximum number of queued connections (default: 5) :param listen_queue: Maximum number of queued connections (default: 5)
:type listen_queue: int :type listen_queue: int
""" """
super().__init__(*args, **kwargs) super().__init__(name=self.__class__.__name__, **kwargs)
self.port = port self.port = port
self.bind_address = bind_address or '0.0.0.0' self.bind_address = bind_address or '0.0.0.0'
@ -46,8 +46,11 @@ class TcpBackend(Backend):
while not self.should_stop(): while not self.should_stop():
if processed_bytes > self._MAX_REQ_SIZE: if processed_bytes > self._MAX_REQ_SIZE:
self.logger.warning('Ignoring message longer than {} bytes from {}' self.logger.warning(
.format(self._MAX_REQ_SIZE, address[0])) 'Ignoring message longer than {} bytes from {}'.format(
self._MAX_REQ_SIZE, address[0]
)
)
return return
ch = sock.recv(1) ch = sock.recv(1)
@ -76,17 +79,16 @@ class TcpBackend(Backend):
return return
msg = Message.build(msg) msg = Message.build(msg)
self.logger.info('Received request from {}: {}'.format(msg, address[0])) self.logger.info('Received request from %s: %s', msg, address[0])
self.on_message(msg) self.on_message(msg)
response = self.get_message_response(msg) response = self.get_message_response(msg)
self.logger.info('Processing response on the TCP backend: {}'.format(response)) self.logger.info('Processing response on the TCP backend: %s', response)
if response: if response:
sock.send(str(response).encode()) sock.send(str(response).encode())
def _f_wrapper(): def _f_wrapper():
set_thread_name('TCPListener')
try: try:
_f() _f()
finally: finally:
@ -111,11 +113,16 @@ class TcpBackend(Backend):
serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serv_sock.settimeout(0.5) serv_sock.settimeout(0.5)
self.logger.info('Initialized TCP backend on port {} with bind address {}'. self.logger.info(
format(self.port, self.bind_address)) 'Initialized TCP backend on port %s with bind address %s',
self.port,
self.bind_address,
)
serv_sock.listen(self.listen_queue) serv_sock.listen(self.listen_queue)
self._srv = multiprocessing.Process(target=self._accept_process, args=(serv_sock,)) self._srv = multiprocessing.Process(
target=self._accept_process, args=(serv_sock,)
)
self._srv.start() self._srv.start()
while not self.should_stop(): while not self.should_stop():
@ -124,7 +131,7 @@ class TcpBackend(Backend):
except (socket.timeout, queue.Empty): except (socket.timeout, queue.Empty):
continue continue
self.logger.info('Accepted connection from client {}'.format(address[0])) self.logger.info('Accepted connection from client %s', address[0])
self._process_client(sock, address) self._process_client(sock, address)
if self._srv: if self._srv:

View file

@ -8,7 +8,7 @@ import croniter
from dateutil.tz import gettz from dateutil.tz import gettz
from platypush.procedure import Procedure from platypush.procedure import Procedure
from platypush.utils import is_functional_cron, set_thread_name from platypush.utils import is_functional_cron
logger = logging.getLogger('platypush:cron') logger = logging.getLogger('platypush:cron')
@ -52,7 +52,7 @@ class Cronjob(threading.Thread):
""" """
def __init__(self, name, cron_expression, actions): def __init__(self, name, cron_expression, actions):
super().__init__() super().__init__(name=f'cron:{name}')
self.cron_expression = cron_expression self.cron_expression = cron_expression
self.name = name self.name = name
self.state = CronjobState.IDLE self.state = CronjobState.IDLE
@ -79,7 +79,6 @@ class Cronjob(threading.Thread):
""" """
Inner logic of the cronjob thread. Inner logic of the cronjob thread.
""" """
set_thread_name(f'cron:{self.name}')
# Wait until an event is received or the next execution slot is reached # Wait until an event is received or the next execution slot is reached
self.wait() self.wait()
@ -203,7 +202,7 @@ class CronScheduler(threading.Thread):
logger.info('Running cron scheduler') logger.info('Running cron scheduler')
while not self.should_stop(): while not self.should_stop():
for (job_name, job_config) in self.jobs_config.items(): for job_name, job_config in self.jobs_config.items():
job = self._get_job(name=job_name, config=job_config) job = self._get_job(name=job_name, config=job_config)
if job.state == CronjobState.IDLE: if job.state == CronjobState.IDLE:
try: try:

View file

@ -5,7 +5,6 @@ from typing import Dict, Optional
from platypush.context import get_bus from platypush.context import get_bus
from platypush.entities import Entity from platypush.entities import Entity
from platypush.message.event.entities import EntityUpdateEvent from platypush.message.event.entities import EntityUpdateEvent
from platypush.utils import set_thread_name
from platypush.entities._base import EntityKey, EntitySavedCallback from platypush.entities._base import EntityKey, EntitySavedCallback
from platypush.entities._engine.queue import EntitiesQueue from platypush.entities._engine.queue import EntitiesQueue
@ -99,7 +98,6 @@ class EntitiesEngine(Thread):
def run(self): def run(self):
super().run() super().run()
set_thread_name('entities')
self.logger.info('Started entities engine') self.logger.info('Started entities engine')
self._running.set() self._running.set()

View file

@ -3,20 +3,23 @@ import json
import logging import logging
import threading import threading
from functools import wraps from functools import wraps
from typing import Optional, Type
from platypush.common import exec_wrapper from platypush.common import exec_wrapper
from platypush.config import Config from platypush.config import Config
from platypush.message.event import Event from platypush.message.event import Event
from platypush.message.request import Request from platypush.message.request import Request
from platypush.procedure import Procedure from platypush.procedure import Procedure
from platypush.utils import get_event_class_by_type, set_thread_name, is_functional_hook from platypush.utils import get_event_class_by_type, is_functional_hook
logger = logging.getLogger('platypush') logger = logging.getLogger('platypush')
def parse(msg): def parse(msg):
"""Builds a dict given another dictionary or """
a JSON UTF-8 encoded string/bytearray""" Builds a dict given another dictionary or a JSON UTF-8 encoded
string/bytearray.
"""
if isinstance(msg, (bytes, bytearray)): if isinstance(msg, (bytes, bytearray)):
msg = msg.decode('utf-8') msg = msg.decode('utf-8')
@ -24,16 +27,18 @@ def parse(msg):
try: try:
msg = json.loads(msg.strip()) msg = json.loads(msg.strip())
except json.JSONDecodeError: except json.JSONDecodeError:
logger.warning('Invalid JSON message: {}'.format(msg)) logger.warning('Invalid JSON message: %s', msg)
return None return None
return msg return msg
# pylint: disable=too-few-public-methods
class EventCondition: class EventCondition:
"""Event hook condition class""" """Event hook condition class"""
def __init__(self, type=Event.__class__, priority=None, **kwargs): # pylint: disable=redefined-builtin
def __init__(self, type: Optional[Type[Event]] = None, priority=None, **kwargs):
""" """
Rule constructor. Rule constructor.
Params: Params:
@ -42,40 +47,40 @@ class EventCondition:
or recognized_phrase='Your phrase') or recognized_phrase='Your phrase')
""" """
self.type = type self.type = type or Event.__class__ # type: ignore
self.args = {} self.args = {}
self.parsed_args = {} self.parsed_args = {}
self.priority = priority self.priority = priority
for (key, value) in kwargs.items(): for key, value in kwargs.items():
# TODO So far we only allow simple value match. If value is a dict
# instead, we should allow more a sophisticated attribute matching,
# e.g. or conditions, in, and other operators.
self.args[key] = value self.args[key] = value
@classmethod @classmethod
def build(cls, rule): def build(cls, rule):
"""Builds a rule given either another EventRule, a dictionary or """
a JSON UTF-8 encoded string/bytearray""" Builds a rule given either another EventRule, a dictionary or a JSON
UTF-8 encoded string/bytearray.
"""
if isinstance(rule, cls): if isinstance(rule, cls):
return rule return rule
else:
rule = parse(rule)
rule = parse(rule)
assert isinstance(rule, dict), f'Not a valid rule: {rule}' assert isinstance(rule, dict), f'Not a valid rule: {rule}'
type = get_event_class_by_type(rule.pop('type') if 'type' in rule else 'Event') type = get_event_class_by_type(rule.pop('type') if 'type' in rule else 'Event')
args = {} args = {}
for (key, value) in rule.items(): for key, value in rule.items():
args[key] = value args[key] = value
return cls(type=type, **args) return cls(type=type, **args)
class EventAction(Request): class EventAction(Request):
"""Event hook action class. It is a special type of runnable request """
whose fields can be configured later depending on the event context""" Event hook action class. It is a special type of runnable request whose
fields can be configured later depending on the event context.
"""
def __init__(self, target=None, action=None, **args): def __init__(self, target=None, action=None, **args):
if target is None: if target is None:
@ -84,30 +89,34 @@ class EventAction(Request):
super().__init__(target=target, action=action, **args_copy) super().__init__(target=target, action=action, **args_copy)
@classmethod @classmethod
def build(cls, action): def build(cls, msg):
action = super().parse(action) msg = super().parse(msg)
action['origin'] = Config.get('device_id') msg['origin'] = Config.get('device_id')
if 'target' not in action: if 'target' not in msg:
action['target'] = action['origin'] msg['target'] = msg['origin']
token = Config.get('token') token = Config.get('token')
if token: if token:
action['token'] = token msg['token'] = token
return super().build(action) return super().build(msg)
class EventHook: class EventHook:
"""Event hook class. It consists of one conditions and """
one or multiple actions to be executed""" Event hook class. It consists of one conditions and one or multiple actions
to be executed.
"""
def __init__(self, name, priority=None, condition=None, actions=None): def __init__(self, name, priority=None, condition=None, actions=None):
"""Constructor. Takes a name, a EventCondition object and an event action """
procedure as input. It may also have a priority attached Takes a name, a EventCondition object and an event action procedure as
as a positive number. If multiple hooks match against an event, input. It may also have a priority attached as a positive number. If
only the ones that have either the maximum match score or the multiple hooks match against an event, only the ones that have either
maximum pre-configured priority will be run.""" the maximum match score or the maximum pre-configured priority will be
run.
"""
self.name = name self.name = name
self.condition = EventCondition.build(condition or {}) self.condition = EventCondition.build(condition or {})
@ -116,24 +125,28 @@ class EventHook:
self.condition.priority = self.priority self.condition.priority = self.priority
@classmethod @classmethod
def build(cls, name, hook): def build(cls, name, hook): # pylint: disable=redefined-outer-name
"""Builds a rule given either another EventRule, a dictionary or """
a JSON UTF-8 encoded string/bytearray""" Builds a rule given either another EventRule, a dictionary or a JSON
UTF-8 encoded string/bytearray.
"""
if isinstance(hook, cls): if isinstance(hook, cls):
return hook return hook
else:
hook = parse(hook)
hook = parse(hook)
if is_functional_hook(hook): if is_functional_hook(hook):
actions = Procedure(name=name, requests=[hook], _async=False) actions = Procedure(name=name, requests=[hook], _async=False)
return cls(name=name, condition=hook.condition, actions=actions) return cls(
name=name, condition=getattr(hook, 'condition', None), actions=actions
)
assert isinstance(hook, dict) assert isinstance(hook, dict)
condition = EventCondition.build(hook['if']) if 'if' in hook else None condition = EventCondition.build(hook['if']) if 'if' in hook else None
actions = [] actions = []
priority = hook['priority'] if 'priority' in hook else None priority = hook['priority'] if 'priority' in hook else None
condition.priority = priority if condition:
condition.priority = priority
if 'then' in hook: if 'then' in hook:
if isinstance(hook['then'], list): if isinstance(hook['then'], list):
@ -145,29 +158,38 @@ class EventHook:
return cls(name=name, condition=condition, actions=actions, priority=priority) return cls(name=name, condition=condition, actions=actions, priority=priority)
def matches_event(self, event): def matches_event(self, event):
"""Returns an EventMatchResult object containing the information """
about the match between the event and this hook""" Returns an EventMatchResult object containing the information about the
match between the event and this hook.
"""
return event.matches_condition(self.condition) return event.matches_condition(self.condition)
def run(self, event): def run(self, event):
"""Checks the condition of the hook against a particular event and """
runs the hook actions if the condition is met""" Checks the condition of the hook against a particular event and runs
the hook actions if the condition is met.
"""
def _thread_func(result): def _thread_func(result):
set_thread_name('Event-' + self.name) executor = getattr(self.actions, 'execute', None)
self.actions.execute(event=event, **result.parsed_args) if executor and callable(executor):
executor(event=event, **result.parsed_args)
result = self.matches_event(event) result = self.matches_event(event)
if result.is_match: if result.is_match:
logger.info('Running hook {} triggered by an event'.format(self.name)) logger.info('Running hook %s triggered by an event', self.name)
threading.Thread( threading.Thread(
target=_thread_func, name='Event-' + self.name, args=(result,) target=_thread_func, name='Event-' + self.name, args=(result,)
).start() ).start()
def hook(event_type=Event, **condition): def hook(event_type=Event, **condition):
"""
Decorator used for event hook functions.
"""
def wrapper(f): def wrapper(f):
f.hook = True f.hook = True
f.condition = EventCondition(type=event_type, **condition) f.condition = EventCondition(type=event_type, **condition)

View file

@ -30,7 +30,7 @@ class Request(Message):
target, target,
action, action,
origin=None, origin=None,
id=None, id=None, # pylint: disable=redefined-builtin
backend=None, backend=None,
args=None, args=None,
token=None, token=None,
@ -109,8 +109,6 @@ class Request(Message):
return proc.execute(*args, **kwargs) return proc.execute(*args, **kwargs)
def _expand_context(self, event_args=None, **context): def _expand_context(self, event_args=None, **context):
from platypush.config import Config
if event_args is None: if event_args is None:
event_args = copy.deepcopy(self.args) event_args = copy.deepcopy(self.args)
@ -138,16 +136,19 @@ class Request(Message):
return event_args return event_args
@classmethod @classmethod
# pylint: disable=too-many-branches
def expand_value_from_context(cls, _value, **context): def expand_value_from_context(cls, _value, **context):
for k, v in context.items(): for k, v in context.items():
if isinstance(v, Message): if isinstance(v, Message):
v = json.loads(str(v)) v = json.loads(str(v))
try: try:
exec('{}={}'.format(k, v)) exec('{}={}'.format(k, v)) # pylint: disable=exec-used
except Exception: except Exception:
if isinstance(v, str): if isinstance(v, str):
try: try:
exec('{}="{}"'.format(k, re.sub(r'(^|[^\\])"', '\1\\"', v))) exec( # pylint: disable=exec-used
'{}="{}"'.format(k, re.sub(r'(^|[^\\])"', '\1\\"', v))
)
except Exception as e2: except Exception as e2:
logger.debug( logger.debug(
'Could not set context variable %s=%s: %s', k, v, e2 'Could not set context variable %s=%s: %s', k, v, e2
@ -167,7 +168,7 @@ class Request(Message):
_value = m.group(4) _value = m.group(4)
try: try:
context_value = eval(inner_expr) context_value = eval(inner_expr) # pylint: disable=eval-used
if callable(context_value): if callable(context_value):
context_value = context_value() context_value = context_value()
@ -209,6 +210,7 @@ class Request(Message):
redis.send_message(queue_name, response) redis.send_message(queue_name, response)
redis.expire(queue_name, 60) redis.expire(queue_name, 60)
# pylint: disable=too-many-statements
def execute(self, n_tries=1, _async=True, **context): def execute(self, n_tries=1, _async=True, **context):
""" """
Execute this request and returns a Response object Execute this request and returns a Response object
@ -224,6 +226,7 @@ class Request(Message):
- group: ${group_name} # will be expanded as "Kitchen lights") - group: ${group_name} # will be expanded as "Kitchen lights")
""" """
# pylint: disable=too-many-branches
def _thread_func(_n_tries, errors=None): def _thread_func(_n_tries, errors=None):
from platypush.context import get_bus from platypush.context import get_bus
from platypush.plugins import RunnablePlugin from platypush.plugins import RunnablePlugin

View file

@ -24,7 +24,6 @@ from platypush.message.event.light import (
LightStatusChangeEvent, LightStatusChangeEvent,
) )
from platypush.plugins import RunnablePlugin, action from platypush.plugins import RunnablePlugin, action
from platypush.utils import set_thread_name
class LightHuePlugin(RunnablePlugin, LightEntityManager): class LightHuePlugin(RunnablePlugin, LightEntityManager):
@ -1054,7 +1053,6 @@ class LightHuePlugin(RunnablePlugin, LightEntityManager):
return self._animation_stop.is_set() return self._animation_stop.is_set()
def _animate_thread(lights): def _animate_thread(lights):
set_thread_name('HueAnimate')
get_bus().post( get_bus().post(
LightAnimationStartedEvent( LightAnimationStartedEvent(
lights=lights, lights=lights,
@ -1209,7 +1207,7 @@ class LightHuePlugin(RunnablePlugin, LightEntityManager):
def status(self, *_, **__) -> Iterable[LightEntity]: def status(self, *_, **__) -> Iterable[LightEntity]:
lights = self.transform_entities(self._get_lights(publish_entities=True)) lights = self.transform_entities(self._get_lights(publish_entities=True))
for light in lights: for light in lights:
light.id = light.external_id light.id = light.external_id # type: ignore
for attr, value in (light.data or {}).items(): for attr, value in (light.data or {}).items():
setattr(light, attr, value) setattr(light, attr, value)

View file

@ -89,7 +89,7 @@ def get_plugin_class_by_name(plugin_name):
module = get_plugin_module_by_name(plugin_name) module = get_plugin_module_by_name(plugin_name)
if not module: if not module:
return return None
class_name = getattr( class_name = getattr(
module, ''.join([_.capitalize() for _ in plugin_name.split('.')]) + 'Plugin' module, ''.join([_.capitalize() for _ in plugin_name.split('.')]) + 'Plugin'
@ -126,7 +126,7 @@ def get_backend_class_by_name(backend_name: str):
module = get_backend_module_by_name(backend_name) module = get_backend_module_by_name(backend_name)
if not module: if not module:
return return None
class_name = getattr( class_name = getattr(
module, module,
@ -149,7 +149,7 @@ def get_backend_class_by_name(backend_name: str):
return None return None
def get_backend_name_by_class(backend) -> Optional[str]: def get_backend_name_by_class(backend) -> str:
"""Gets the common name of a backend (e.g. "http" or "mqtt") given its class.""" """Gets the common name of a backend (e.g. "http" or "mqtt") given its class."""
from platypush.backend import Backend from platypush.backend import Backend
@ -206,12 +206,14 @@ def get_decorators(cls, climb_class_hierarchy=False):
decorators = {} decorators = {}
# noinspection PyPep8Naming
def visit_FunctionDef(node): def visit_FunctionDef(node):
for n in node.decorator_list: for n in node.decorator_list:
if isinstance(n, ast.Call): if isinstance(n, ast.Call):
# noinspection PyUnresolvedReferences name = (
name = n.func.attr if isinstance(n.func, ast.Attribute) else n.func.id n.func.attr
if isinstance(n.func, ast.Attribute)
else n.func.id # type: ignore
)
else: else:
name = n.attr if isinstance(n, ast.Attribute) else n.id name = n.attr if isinstance(n, ast.Attribute) else n.id
@ -257,6 +259,7 @@ def _get_ssl_context(
else: else:
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
assert ssl_cert, 'No certificate specified'
if ssl_cafile or ssl_capath: if ssl_cafile or ssl_capath:
ssl_context.load_verify_locations(cafile=ssl_cafile, capath=ssl_capath) ssl_context.load_verify_locations(cafile=ssl_cafile, capath=ssl_capath)
@ -311,18 +314,6 @@ def get_ssl_client_context(
) )
def set_thread_name(name: str):
"""
Set the name of the current thread.
"""
try:
import prctl
prctl.set_name(name) # pylint: disable=no-member
except ImportError:
logger.debug('Unable to set thread name: prctl module is missing')
def find_bins_in_path(bin_name): def find_bins_in_path(bin_name):
""" """
Search for a binary in the PATH variable. Search for a binary in the PATH variable.
@ -399,7 +390,6 @@ def get_mime_type(resource: str) -> Optional[str]:
offset = len('file://') offset = len('file://')
resource = resource[offset:] resource = resource[offset:]
# noinspection HttpUrlsUsage
if resource.startswith('http://') or resource.startswith('https://'): if resource.startswith('http://') or resource.startswith('https://'):
with urllib.request.urlopen(resource) as response: with urllib.request.urlopen(resource) as response:
return response.info().get_content_type() return response.info().get_content_type()
@ -442,6 +432,8 @@ def grouper(n, iterable, fillvalue=None):
for chunk in zip_longest(*args): for chunk in zip_longest(*args):
yield filter(None, chunk) yield filter(None, chunk)
return
def is_functional_procedure(obj) -> bool: def is_functional_procedure(obj) -> bool:
""" """
@ -529,7 +521,7 @@ def get_or_generate_jwt_rsa_key_pair():
""" """
from platypush.config import Config from platypush.config import Config
key_dir = os.path.join(Config.get('workdir'), 'jwt') key_dir = os.path.join(Config.workdir, 'jwt')
priv_key_file = os.path.join(key_dir, 'id_rsa') priv_key_file = os.path.join(key_dir, 'id_rsa')
pub_key_file = priv_key_file + '.pub' pub_key_file = priv_key_file + '.pub'

View file

@ -13,7 +13,6 @@ marshmallow_dataclass
paho-mqtt paho-mqtt
python-dateutil python-dateutil
python-magic python-magic
python-prctl
pyyaml pyyaml
redis redis
requests requests

View file

@ -83,8 +83,6 @@ setup(
'zeroconf>=0.27.0', 'zeroconf>=0.27.0',
], ],
extras_require={ extras_require={
# Support for thread custom name
'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