import functools import importlib import os import logging import json import socket import subprocess import sys import time import websocket import yaml from queue import Queue from threading import Thread from getopt import getopt __author__ = 'Fabio Manganiello ' #-----------# 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 _exec_func(args, retry=True): action = args.pop('action') tokens = 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: ret = plugin.run(method=method_name, **args) out = None err = None if isinstance(ret, list): out = ret[0] err = ret[1] if len(ret) > 1 else None elif ret is not None: out = ret if out: logging.info('Command output: {}'.format(out)) if err: logging.warn('Command error: {}'.format(err)) except Exception as e: logging.exception(e) if retry: # Put the action back where it was before retrying args['action'] = action logging.info('Reloading plugin {} and retrying'.format(module_name)) _init_plugin(module_name, reload=True) _exec_func(args, retry=False) def on_msg(msg): Thread(target=_exec_func, args=(msg,)).start() 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/runbullet/config.yaml os.path.join(os.environ['HOME'], '.config', 'runbullet', 'config.yaml'), # /etc/runbullet/config.yaml os.path.join(os.sep, 'etc', 'runbullet', 'config.yaml'), ] for loc in locations: try: with open(loc,'r') as f: config = yaml.load(f) except FileNotFoundError as e: pass return config def get_backends(config): backends = [] for k in config.keys(): if k.startswith('backend.') and ( 'disabled' not in config[k] or not config[k]['disabled']): 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' try: b = getattr(module, cls_name)(config[k]) backends.append(b) except AttributeError as e: logging.warn('No such class in {}: {}'.format( module.__name__, cls_name)) raise RuntimeError(e) return backends def get_device_id(): global config return config['device_id'] def main(): 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 ] -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) logging.info('Configuration dump: {}'.format(config)) if 'device_id' not in config: config['device_id'] = socket.gethostname() if 'debug' in config: DEBUG = config['debug'] if DEBUG: logging.basicConfig(level=logging.DEBUG) websocket.enableTrace(True) else: logging.basicConfig(level=logging.INFO) mq = Queue() backends = get_backends(config) for backend in backends: backend.mq = mq backend.start() while True: try: on_msg(mq.get()) except KeyboardInterrupt: return if __name__ == '__main__': main() # vim:sw=4:ts=4:et: