forked from platypush/platypush
339e7b73a5
Solves, among the others, #2, #18 and #22
230 lines
6.2 KiB
Python
230 lines
6.2 KiB
Python
import functools
|
|
import importlib
|
|
import os
|
|
import logging
|
|
import json
|
|
import socket
|
|
import sys
|
|
import traceback
|
|
import websocket
|
|
import yaml
|
|
|
|
from queue import Queue
|
|
from threading import Thread
|
|
from getopt import getopt
|
|
|
|
from .message.request import Request
|
|
from .message.response import Response
|
|
|
|
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
|
|
__version__ = '0.3.3'
|
|
|
|
#-----------#
|
|
|
|
config = {}
|
|
modules = {}
|
|
wrkdir = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
|
|
def _init_plugin(plugin_name, reload=False):
|
|
global modules
|
|
global config
|
|
|
|
if plugin_name in modules and not reload:
|
|
return modules[plugin_name]
|
|
|
|
try:
|
|
module = importlib.import_module(__package__ + '.plugins.' + plugin_name)
|
|
except ModuleNotFoundError as e:
|
|
logging.warn('No such plugin: {}'.format(plugin_name))
|
|
raise RuntimeError(e)
|
|
|
|
# e.g. plugins.music.mpd main class: MusicMpdPlugin
|
|
cls_name = functools.reduce(
|
|
lambda a,b: a.title() + b.title(),
|
|
(plugin_name.title().split('.'))
|
|
) + 'Plugin'
|
|
|
|
plugin_conf = config[plugin_name] if plugin_name in config else {}
|
|
|
|
try:
|
|
plugin = getattr(module, cls_name)(plugin_conf)
|
|
modules[plugin_name] = plugin
|
|
except AttributeError as e:
|
|
logging.warn('No such class in {}: {}'.format(
|
|
plugin_name, cls_name))
|
|
raise RuntimeError(e)
|
|
|
|
return plugin
|
|
|
|
|
|
def _execute_request(request, retry=True):
|
|
tokens = request.action.split('.')
|
|
module_name = str.join('.', tokens[:-1])
|
|
method_name = tokens[-1:][0]
|
|
|
|
try:
|
|
plugin = _init_plugin(module_name)
|
|
except RuntimeError as e: # Module/class not found
|
|
logging.exception(e)
|
|
return
|
|
|
|
try:
|
|
response = plugin.run(method=method_name, **request.args)
|
|
if response and response.is_error():
|
|
logging.warn('Response processed with errors: {}'.format(response))
|
|
else:
|
|
logging.info('Processed response: {}'.format(response))
|
|
except Exception as e:
|
|
response = Response(output=None, errors=[str(e), traceback.format_exc()])
|
|
logging.exception(e)
|
|
if retry:
|
|
logging.info('Reloading plugin {} and retrying'.format(module_name))
|
|
_init_plugin(module_name, reload=True)
|
|
_execute_request(request, retry=False)
|
|
finally:
|
|
# Send the response on the backend that received the request
|
|
if request.backend and request.origin:
|
|
response.target = request.origin
|
|
request.backend.send_response(response)
|
|
|
|
|
|
def on_msg(msg):
|
|
if isinstance(msg, Request):
|
|
logging.info('Processing request: {}'.format(msg))
|
|
Thread(target=_execute_request, args=(msg,)).start()
|
|
elif isinstance(msg, Response):
|
|
logging.info('Received response: {}'.format(msg))
|
|
|
|
|
|
def parse_config_file(config_file=None):
|
|
global config
|
|
|
|
if config_file:
|
|
locations = [config_file]
|
|
else:
|
|
locations = [
|
|
# ./config.yaml
|
|
os.path.join(wrkdir, 'config.yaml'),
|
|
# ~/.config/platypush/config.yaml
|
|
os.path.join(os.environ['HOME'], '.config', 'platypush', 'config.yaml'),
|
|
# /etc/platypush/config.yaml
|
|
os.path.join(os.sep, 'etc', 'platypush', 'config.yaml'),
|
|
]
|
|
|
|
for loc in locations:
|
|
try:
|
|
with open(loc,'r') as f:
|
|
config = yaml.load(f)
|
|
except FileNotFoundError as e:
|
|
pass
|
|
|
|
for section in config:
|
|
if 'disabled' in config[section] and config[section]['disabled']:
|
|
del config[section]
|
|
|
|
if 'logging' not in config:
|
|
config['logging'] = logging.INFO
|
|
else:
|
|
config['logging'] = getattr(logging, config['logging'].upper())
|
|
|
|
if 'device_id' not in config:
|
|
config['device_id'] = socket.gethostname()
|
|
|
|
return config
|
|
|
|
|
|
def init_backends(config, bus=None):
|
|
backends = {}
|
|
|
|
for k in config.keys():
|
|
if not k.startswith('backend.'): continue
|
|
|
|
module = importlib.import_module(__package__ + '.' + k)
|
|
|
|
# e.g. backend.pushbullet main class: PushbulletBackend
|
|
cls_name = functools.reduce(
|
|
lambda a,b: a.title() + b.title(),
|
|
(module.__name__.title().split('.')[2:])
|
|
) + 'Backend'
|
|
|
|
# Ignore the pusher attribute here
|
|
if 'pusher' in config[k]: del config[k]['pusher']
|
|
|
|
try:
|
|
b = getattr(module, cls_name)(bus=bus, **config[k])
|
|
name = '.'.join((k.split('.'))[1:])
|
|
backends[name] = b
|
|
except AttributeError as e:
|
|
logging.warn('No such class in {}: {}'.format(
|
|
module.__name__, cls_name))
|
|
raise RuntimeError(e)
|
|
|
|
return backends
|
|
|
|
|
|
def get_default_pusher_backend(config):
|
|
backends = ['.'.join((k.split('.'))[1:])
|
|
for k in config.keys() if k.startswith('backend.')
|
|
and 'pusher' in config[k] and config[k]['pusher'] is True]
|
|
|
|
return backends[0] if backends else None
|
|
|
|
|
|
def get_logging_level():
|
|
global config
|
|
return config['logging']
|
|
|
|
|
|
def get_device_id():
|
|
global config
|
|
return config['device_id'] if 'device_id' in config else None
|
|
|
|
|
|
def main():
|
|
print('Starting platypush v.{}'.format(__version__))
|
|
|
|
debug = False
|
|
config_file = None
|
|
|
|
plugins_dir = os.path.join(wrkdir, 'plugins')
|
|
sys.path.insert(0, plugins_dir)
|
|
|
|
optlist, args = getopt(sys.argv[1:], 'vh')
|
|
for opt, arg in optlist:
|
|
if opt == '-c':
|
|
config_file = arg
|
|
if opt == '-v':
|
|
debug = True
|
|
elif opt == '-h':
|
|
print('''
|
|
Usage: {} [-v] [-h] [-c <config_file>]
|
|
-v Enable debug mode
|
|
-h Show this help
|
|
-c Path to the configuration file (default: ./config.yaml)
|
|
'''.format(sys.argv[0]))
|
|
return
|
|
|
|
config = parse_config_file(config_file)
|
|
if debug: config['logging'] = logging.DEBUG
|
|
|
|
logging.basicConfig(level=get_logging_level(), stream=sys.stdout)
|
|
logging.debug('Configuration dump: {}'.format(config))
|
|
|
|
bus = Queue()
|
|
backends = init_backends(config, bus)
|
|
|
|
for backend in backends.values():
|
|
backend.start()
|
|
|
|
while True:
|
|
try:
|
|
on_msg(bus.get())
|
|
except KeyboardInterrupt:
|
|
return
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|
|
# vim:sw=4:ts=4:et:
|
|
|