From 713ff17e91ce5826018baa4f34905f602834d95b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 16:18:37 +0200 Subject: [PATCH] Allow load installed third-party plugins and enable/disable plugins in config file data/plugins.json --- src/Plugin/PluginManager.py | 157 +++++++++++++++++++++++++++--------- 1 file changed, 121 insertions(+), 36 deletions(-) diff --git a/src/Plugin/PluginManager.py b/src/Plugin/PluginManager.py index fbf37d3c..dae4f2ec 100644 --- a/src/Plugin/PluginManager.py +++ b/src/Plugin/PluginManager.py @@ -5,62 +5,143 @@ import shutil import time from collections import defaultdict +import importlib +import json + from Debug import Debug from Config import config - import plugins -import importlib - class PluginManager: def __init__(self): self.log = logging.getLogger("PluginManager") - self.plugin_path = os.path.abspath(os.path.dirname(plugins.__file__)) + self.path_plugins = os.path.abspath(os.path.dirname(plugins.__file__)) + self.path_installed_plugins = config.data_dir + "/__plugins__" self.plugins = defaultdict(list) # Registered plugins (key: class name, value: list of plugins for class) self.subclass_order = {} # Record the load order of the plugins, to keep it after reload self.pluggable = {} self.plugin_names = [] # Loaded plugin names - self.after_load = [] # Execute functions after loaded plugins + self.plugins_updated = {} # List of updated plugins since restart + self.plugins_rev = {} # Installed plugins revision numbers + self.after_load = [] # Execute functions after loaded plugins + self.function_flags = {} # Flag function for permissions self.reloading = False + self.config_path = config.data_dir + "/plugins.json" + self.loadConfig() - sys.path.append(os.path.join(os.getcwd(), self.plugin_path)) + self.config.setdefault("builtin", {}) + + sys.path.append(os.path.join(os.getcwd(), self.path_plugins)) self.migratePlugins() if config.debug: # Auto reload Plugins on file change from Debug import DebugReloader DebugReloader.watcher.addCallback(self.reloadPlugins) + def loadConfig(self): + if os.path.isfile(self.config_path): + try: + self.config = json.load(open(self.config_path, encoding="utf8")) + except Exception as err: + self.log.error("Error loading %s: %s" % (self.config_path, err)) + self.config = {} + else: + self.config = {} + + def saveConfig(self): + f = open(self.config_path, "w", encoding="utf8") + json.dump(self.config, f, ensure_ascii=False, sort_keys=True, indent=2) + def migratePlugins(self): - for dir_name in os.listdir(self.plugin_path): + for dir_name in os.listdir(self.path_plugins): if dir_name == "Mute": self.log.info("Deleting deprecated/renamed plugin: %s" % dir_name) - shutil.rmtree("%s/%s" % (self.plugin_path, dir_name)) + shutil.rmtree("%s/%s" % (self.path_plugins, dir_name)) # -- Load / Unload -- + def listPlugins(self, list_disabled=False): + plugins = [] + for dir_name in sorted(os.listdir(self.path_plugins)): + dir_path = os.path.join(self.path_plugins, dir_name) + plugin_name = dir_name.replace("disabled-", "") + if dir_name.startswith("disabled"): + is_enabled = False + else: + is_enabled = True + + plugin_config = self.config["builtin"].get(plugin_name, {}) + if "enabled" in plugin_config: + is_enabled = plugin_config["enabled"] + + if dir_name == "__pycache__" or not os.path.isdir(dir_path): + continue # skip + if dir_name.startswith("Debug") and not config.debug: + continue # Only load in debug mode if module name starts with Debug + if not is_enabled and not list_disabled: + continue # Dont load if disabled + + plugin = {} + plugin["source"] = "builtin" + plugin["name"] = plugin_name + plugin["dir_name"] = dir_name + plugin["dir_path"] = dir_path + plugin["inner_path"] = plugin_name + plugin["enabled"] = is_enabled + plugin["rev"] = config.rev + plugin["loaded"] = plugin_name in self.plugin_names + plugins.append(plugin) + + plugins += self.listInstalledPlugins(list_disabled) + return plugins + + def listInstalledPlugins(self, list_disabled=False): + plugins = [] + + for address, site_plugins in self.config.items(): + if address == "builtin": + continue + for plugin_inner_path, plugin_config in site_plugins.items(): + is_enabled = plugin_config.get("enabled", False) + if not is_enabled and not list_disabled: + continue + plugin_name = os.path.basename(plugin_inner_path) + + dir_path = "%s/%s/%s" % (self.path_installed_plugins, address, plugin_inner_path) + + plugin = {} + plugin["source"] = address + plugin["name"] = plugin_name + plugin["dir_name"] = plugin_name + plugin["dir_path"] = dir_path + plugin["inner_path"] = plugin_inner_path + plugin["enabled"] = is_enabled + plugin["rev"] = plugin_config.get("rev", 0) + plugin["loaded"] = plugin_name in self.plugin_names + plugins.append(plugin) + + return plugins + # Load all plugin def loadPlugins(self): all_loaded = True s = time.time() - for dir_name in sorted(os.listdir(self.plugin_path)): - dir_path = os.path.join(self.plugin_path, dir_name) - if dir_name == "__pycache__": - continue # skip - if dir_name.startswith("disabled"): - continue # Dont load if disabled - if not os.path.isdir(dir_path): - continue # Dont load if not dir - if dir_name.startswith("Debug") and not config.debug: - continue # Only load in debug mode if module name starts with Debug - self.log.debug("Loading plugin: %s" % dir_name) + print(sys.path) + for plugin in self.listPlugins(): + self.log.debug("Loading plugin: %s (%s)" % (plugin["name"], plugin["source"])) + if plugin["source"] != "builtin": + self.plugins_rev[plugin["name"]] = plugin["rev"] + site_plugin_dir = os.path.dirname(plugin["dir_path"]) + if site_plugin_dir not in sys.path: + sys.path.append(site_plugin_dir) try: - __import__(dir_name) + sys.modules[plugin["name"]] = __import__(plugin["dir_name"]) except Exception as err: - self.log.error("Plugin %s load error: %s" % (dir_name, Debug.formatException(err))) + self.log.error("Plugin %s load error: %s" % (plugin["name"], Debug.formatException(err))) all_loaded = False - if dir_name not in self.plugin_names: - self.plugin_names.append(dir_name) + if plugin["name"] not in self.plugin_names: + self.plugin_names.append(plugin["name"]) self.log.debug("Plugins loaded in %.3fs" % (time.time() - s)) for func in self.after_load: @@ -74,19 +155,23 @@ class PluginManager: self.plugins_before = self.plugins self.plugins = defaultdict(list) # Reset registered plugins for module_name, module in list(sys.modules.items()): - if module and getattr(module, "__file__", None) and self.plugin_path in module.__file__: # Module file in plugin_path - if "allow_reload" in dir(module) and not module.allow_reload: # Reload disabled - # Re-add non-reloadable plugins - for class_name, classes in self.plugins_before.items(): - for c in classes: - if c.__module__ != module.__name__: - continue - self.plugins[class_name].append(c) - else: - try: - importlib.reload(module) - except Exception as err: - self.log.error("Plugin %s reload error: %s" % (module_name, Debug.formatException(err))) + if not module or not getattr(module, "__file__", None): + continue + if self.path_plugins not in module.__file__ and self.path_installed_plugins not in module.__file__: + continue + + if "allow_reload" in dir(module) and not module.allow_reload: # Reload disabled + # Re-add non-reloadable plugins + for class_name, classes in self.plugins_before.items(): + for c in classes: + if c.__module__ != module.__name__: + continue + self.plugins[class_name].append(c) + else: + try: + importlib.reload(module) + except Exception as err: + self.log.error("Plugin %s reload error: %s" % (module_name, Debug.formatException(err))) self.loadPlugins() # Load new plugins