Allow load installed third-party plugins and enable/disable plugins in config file data/plugins.json
This commit is contained in:
parent
0c659a477d
commit
713ff17e91
1 changed files with 121 additions and 36 deletions
|
@ -5,62 +5,143 @@ import shutil
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
|
||||||
from Debug import Debug
|
from Debug import Debug
|
||||||
from Config import config
|
from Config import config
|
||||||
|
|
||||||
import plugins
|
import plugins
|
||||||
|
|
||||||
import importlib
|
|
||||||
|
|
||||||
|
|
||||||
class PluginManager:
|
class PluginManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.log = logging.getLogger("PluginManager")
|
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.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.subclass_order = {} # Record the load order of the plugins, to keep it after reload
|
||||||
self.pluggable = {}
|
self.pluggable = {}
|
||||||
self.plugin_names = [] # Loaded plugin names
|
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.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()
|
self.migratePlugins()
|
||||||
|
|
||||||
if config.debug: # Auto reload Plugins on file change
|
if config.debug: # Auto reload Plugins on file change
|
||||||
from Debug import DebugReloader
|
from Debug import DebugReloader
|
||||||
DebugReloader.watcher.addCallback(self.reloadPlugins)
|
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):
|
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":
|
if dir_name == "Mute":
|
||||||
self.log.info("Deleting deprecated/renamed plugin: %s" % dir_name)
|
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 --
|
# -- 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
|
# Load all plugin
|
||||||
def loadPlugins(self):
|
def loadPlugins(self):
|
||||||
all_loaded = True
|
all_loaded = True
|
||||||
s = time.time()
|
s = time.time()
|
||||||
for dir_name in sorted(os.listdir(self.plugin_path)):
|
print(sys.path)
|
||||||
dir_path = os.path.join(self.plugin_path, dir_name)
|
for plugin in self.listPlugins():
|
||||||
if dir_name == "__pycache__":
|
self.log.debug("Loading plugin: %s (%s)" % (plugin["name"], plugin["source"]))
|
||||||
continue # skip
|
if plugin["source"] != "builtin":
|
||||||
if dir_name.startswith("disabled"):
|
self.plugins_rev[plugin["name"]] = plugin["rev"]
|
||||||
continue # Dont load if disabled
|
site_plugin_dir = os.path.dirname(plugin["dir_path"])
|
||||||
if not os.path.isdir(dir_path):
|
if site_plugin_dir not in sys.path:
|
||||||
continue # Dont load if not dir
|
sys.path.append(site_plugin_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)
|
|
||||||
try:
|
try:
|
||||||
__import__(dir_name)
|
sys.modules[plugin["name"]] = __import__(plugin["dir_name"])
|
||||||
except Exception as err:
|
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
|
all_loaded = False
|
||||||
if dir_name not in self.plugin_names:
|
if plugin["name"] not in self.plugin_names:
|
||||||
self.plugin_names.append(dir_name)
|
self.plugin_names.append(plugin["name"])
|
||||||
|
|
||||||
self.log.debug("Plugins loaded in %.3fs" % (time.time() - s))
|
self.log.debug("Plugins loaded in %.3fs" % (time.time() - s))
|
||||||
for func in self.after_load:
|
for func in self.after_load:
|
||||||
|
@ -74,19 +155,23 @@ class PluginManager:
|
||||||
self.plugins_before = self.plugins
|
self.plugins_before = self.plugins
|
||||||
self.plugins = defaultdict(list) # Reset registered plugins
|
self.plugins = defaultdict(list) # Reset registered plugins
|
||||||
for module_name, module in list(sys.modules.items()):
|
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 not module or not getattr(module, "__file__", None):
|
||||||
if "allow_reload" in dir(module) and not module.allow_reload: # Reload disabled
|
continue
|
||||||
# Re-add non-reloadable plugins
|
if self.path_plugins not in module.__file__ and self.path_installed_plugins not in module.__file__:
|
||||||
for class_name, classes in self.plugins_before.items():
|
continue
|
||||||
for c in classes:
|
|
||||||
if c.__module__ != module.__name__:
|
if "allow_reload" in dir(module) and not module.allow_reload: # Reload disabled
|
||||||
continue
|
# Re-add non-reloadable plugins
|
||||||
self.plugins[class_name].append(c)
|
for class_name, classes in self.plugins_before.items():
|
||||||
else:
|
for c in classes:
|
||||||
try:
|
if c.__module__ != module.__name__:
|
||||||
importlib.reload(module)
|
continue
|
||||||
except Exception as err:
|
self.plugins[class_name].append(c)
|
||||||
self.log.error("Plugin %s reload error: %s" % (module_name, Debug.formatException(err)))
|
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
|
self.loadPlugins() # Load new plugins
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue