2015-07-12 20:36:46 +02:00
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import sys
|
2018-06-25 14:29:42 +02:00
|
|
|
import shutil
|
|
|
|
import time
|
2016-08-10 12:43:35 +02:00
|
|
|
from collections import defaultdict
|
2015-07-12 20:36:46 +02:00
|
|
|
|
2019-08-02 16:18:37 +02:00
|
|
|
import importlib
|
|
|
|
import json
|
|
|
|
|
version 0.2.7, plugin system, multiuser plugin for zeroproxies, reworked imports, cookie parse, stats moved to plugin, usermanager class, dont generate site auth on listing, multiline notifications, allow server side prompt from user, update script keep plugins disabled status
2015-03-24 01:33:09 +01:00
|
|
|
from Debug import Debug
|
|
|
|
from Config import config
|
2019-04-08 00:50:23 +02:00
|
|
|
import plugins
|
|
|
|
|
version 0.2.7, plugin system, multiuser plugin for zeroproxies, reworked imports, cookie parse, stats moved to plugin, usermanager class, dont generate site auth on listing, multiline notifications, allow server side prompt from user, update script keep plugins disabled status
2015-03-24 01:33:09 +01:00
|
|
|
|
2015-07-12 20:36:46 +02:00
|
|
|
class PluginManager:
|
|
|
|
def __init__(self):
|
|
|
|
self.log = logging.getLogger("PluginManager")
|
2019-08-02 16:18:37 +02:00
|
|
|
self.path_plugins = os.path.abspath(os.path.dirname(plugins.__file__))
|
|
|
|
self.path_installed_plugins = config.data_dir + "/__plugins__"
|
2016-08-10 12:43:35 +02:00
|
|
|
self.plugins = defaultdict(list) # Registered plugins (key: class name, value: list of plugins for class)
|
2016-11-07 22:35:28 +01:00
|
|
|
self.subclass_order = {} # Record the load order of the plugins, to keep it after reload
|
2016-08-10 12:43:35 +02:00
|
|
|
self.pluggable = {}
|
2015-07-12 20:36:46 +02:00
|
|
|
self.plugin_names = [] # Loaded plugin names
|
2019-08-02 16:18:37 +02:00
|
|
|
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
|
2019-04-18 12:21:50 +02:00
|
|
|
self.reloading = False
|
2019-08-02 16:18:37 +02:00
|
|
|
self.config_path = config.data_dir + "/plugins.json"
|
|
|
|
self.loadConfig()
|
|
|
|
|
|
|
|
self.config.setdefault("builtin", {})
|
2015-07-12 20:36:46 +02:00
|
|
|
|
2019-08-02 16:18:37 +02:00
|
|
|
sys.path.append(os.path.join(os.getcwd(), self.path_plugins))
|
2018-06-25 14:29:42 +02:00
|
|
|
self.migratePlugins()
|
2015-07-12 20:36:46 +02:00
|
|
|
|
|
|
|
if config.debug: # Auto reload Plugins on file change
|
|
|
|
from Debug import DebugReloader
|
2019-03-16 02:42:43 +01:00
|
|
|
DebugReloader.watcher.addCallback(self.reloadPlugins)
|
2015-07-12 20:36:46 +02:00
|
|
|
|
2019-08-02 16:18:37 +02:00
|
|
|
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)
|
|
|
|
|
2018-06-25 14:29:42 +02:00
|
|
|
def migratePlugins(self):
|
2019-08-02 16:18:37 +02:00
|
|
|
for dir_name in os.listdir(self.path_plugins):
|
2018-06-25 14:29:42 +02:00
|
|
|
if dir_name == "Mute":
|
|
|
|
self.log.info("Deleting deprecated/renamed plugin: %s" % dir_name)
|
2019-08-02 16:18:37 +02:00
|
|
|
shutil.rmtree("%s/%s" % (self.path_plugins, dir_name))
|
2018-06-25 14:29:42 +02:00
|
|
|
|
2015-07-12 20:36:46 +02:00
|
|
|
# -- Load / Unload --
|
|
|
|
|
2019-08-02 16:18:37 +02:00
|
|
|
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 = []
|
|
|
|
|
2019-08-08 14:37:42 +02:00
|
|
|
for address, site_plugins in sorted(self.config.items()):
|
2019-08-02 16:18:37 +02:00
|
|
|
if address == "builtin":
|
|
|
|
continue
|
2019-08-08 14:37:42 +02:00
|
|
|
for plugin_inner_path, plugin_config in sorted(site_plugins.items()):
|
2019-08-02 16:18:37 +02:00
|
|
|
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
|
|
|
|
|
2015-07-12 20:36:46 +02:00
|
|
|
# Load all plugin
|
|
|
|
def loadPlugins(self):
|
2019-04-15 22:18:18 +02:00
|
|
|
all_loaded = True
|
2018-06-25 14:29:51 +02:00
|
|
|
s = time.time()
|
2019-08-02 16:18:37 +02:00
|
|
|
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)
|
2015-07-12 20:36:46 +02:00
|
|
|
try:
|
2019-08-02 16:18:37 +02:00
|
|
|
sys.modules[plugin["name"]] = __import__(plugin["dir_name"])
|
2019-03-15 21:06:59 +01:00
|
|
|
except Exception as err:
|
2019-08-02 16:18:37 +02:00
|
|
|
self.log.error("Plugin %s load error: %s" % (plugin["name"], Debug.formatException(err)))
|
2019-04-15 22:18:18 +02:00
|
|
|
all_loaded = False
|
2019-08-02 16:18:37 +02:00
|
|
|
if plugin["name"] not in self.plugin_names:
|
|
|
|
self.plugin_names.append(plugin["name"])
|
2015-07-12 20:36:46 +02:00
|
|
|
|
2018-06-25 14:29:51 +02:00
|
|
|
self.log.debug("Plugins loaded in %.3fs" % (time.time() - s))
|
2017-10-03 14:57:44 +02:00
|
|
|
for func in self.after_load:
|
|
|
|
func()
|
2019-04-15 22:18:18 +02:00
|
|
|
return all_loaded
|
2017-10-03 14:57:44 +02:00
|
|
|
|
2015-07-12 20:36:46 +02:00
|
|
|
# Reload all plugins
|
|
|
|
def reloadPlugins(self):
|
2019-04-18 12:21:50 +02:00
|
|
|
self.reloading = True
|
2018-06-25 14:30:04 +02:00
|
|
|
self.after_load = []
|
2016-08-10 12:43:35 +02:00
|
|
|
self.plugins_before = self.plugins
|
|
|
|
self.plugins = defaultdict(list) # Reset registered plugins
|
2019-03-15 21:06:59 +01:00
|
|
|
for module_name, module in list(sys.modules.items()):
|
2019-08-02 16:18:37 +02:00
|
|
|
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)))
|
2015-07-12 20:36:46 +02:00
|
|
|
|
|
|
|
self.loadPlugins() # Load new plugins
|
|
|
|
|
2016-08-10 12:43:35 +02:00
|
|
|
# Change current classes in memory
|
|
|
|
import gc
|
|
|
|
patched = {}
|
2019-03-15 21:06:59 +01:00
|
|
|
for class_name, classes in self.plugins.items():
|
2016-08-10 12:43:35 +02:00
|
|
|
classes = classes[:] # Copy the current plugins
|
|
|
|
classes.reverse()
|
|
|
|
base_class = self.pluggable[class_name] # Original class
|
|
|
|
classes.append(base_class) # Add the class itself to end of inherience line
|
|
|
|
plugined_class = type(class_name, tuple(classes), dict()) # Create the plugined class
|
|
|
|
for obj in gc.get_objects():
|
|
|
|
if type(obj).__name__ == class_name:
|
|
|
|
obj.__class__ = plugined_class
|
|
|
|
patched[class_name] = patched.get(class_name, 0) + 1
|
|
|
|
self.log.debug("Patched objects: %s" % patched)
|
|
|
|
|
|
|
|
# Change classes in modules
|
|
|
|
patched = {}
|
2019-03-15 21:06:59 +01:00
|
|
|
for class_name, classes in self.plugins.items():
|
|
|
|
for module_name, module in list(sys.modules.items()):
|
2016-08-10 12:43:35 +02:00
|
|
|
if class_name in dir(module):
|
|
|
|
if "__class__" not in dir(getattr(module, class_name)): # Not a class
|
|
|
|
continue
|
|
|
|
base_class = self.pluggable[class_name]
|
|
|
|
classes = self.plugins[class_name][:]
|
|
|
|
classes.reverse()
|
|
|
|
classes.append(base_class)
|
|
|
|
plugined_class = type(class_name, tuple(classes), dict())
|
|
|
|
setattr(module, class_name, plugined_class)
|
|
|
|
patched[class_name] = patched.get(class_name, 0) + 1
|
|
|
|
|
|
|
|
self.log.debug("Patched modules: %s" % patched)
|
2019-04-18 12:21:50 +02:00
|
|
|
self.reloading = False
|
2016-08-10 12:43:35 +02:00
|
|
|
|
2015-07-12 20:36:46 +02:00
|
|
|
|
|
|
|
plugin_manager = PluginManager() # Singletone
|
version 0.2.7, plugin system, multiuser plugin for zeroproxies, reworked imports, cookie parse, stats moved to plugin, usermanager class, dont generate site auth on listing, multiline notifications, allow server side prompt from user, update script keep plugins disabled status
2015-03-24 01:33:09 +01:00
|
|
|
|
|
|
|
# -- Decorators --
|
|
|
|
|
|
|
|
# Accept plugin to class decorator
|
2015-07-12 20:36:46 +02:00
|
|
|
|
|
|
|
|
version 0.2.7, plugin system, multiuser plugin for zeroproxies, reworked imports, cookie parse, stats moved to plugin, usermanager class, dont generate site auth on listing, multiline notifications, allow server side prompt from user, update script keep plugins disabled status
2015-03-24 01:33:09 +01:00
|
|
|
def acceptPlugins(base_class):
|
2015-07-12 20:36:46 +02:00
|
|
|
class_name = base_class.__name__
|
2016-08-10 12:43:35 +02:00
|
|
|
plugin_manager.pluggable[class_name] = base_class
|
2015-07-12 20:36:46 +02:00
|
|
|
if class_name in plugin_manager.plugins: # Has plugins
|
|
|
|
classes = plugin_manager.plugins[class_name][:] # Copy the current plugins
|
2016-11-07 22:35:28 +01:00
|
|
|
|
|
|
|
# Restore the subclass order after reload
|
|
|
|
if class_name in plugin_manager.subclass_order:
|
|
|
|
classes = sorted(
|
|
|
|
classes,
|
|
|
|
key=lambda key:
|
|
|
|
plugin_manager.subclass_order[class_name].index(str(key))
|
|
|
|
if str(key) in plugin_manager.subclass_order[class_name]
|
|
|
|
else 9999
|
|
|
|
)
|
2019-03-15 21:06:59 +01:00
|
|
|
plugin_manager.subclass_order[class_name] = list(map(str, classes))
|
2016-11-07 22:35:28 +01:00
|
|
|
|
2015-07-12 20:36:46 +02:00
|
|
|
classes.reverse()
|
|
|
|
classes.append(base_class) # Add the class itself to end of inherience line
|
|
|
|
plugined_class = type(class_name, tuple(classes), dict()) # Create the plugined class
|
|
|
|
plugin_manager.log.debug("New class accepts plugins: %s (Loaded plugins: %s)" % (class_name, classes))
|
|
|
|
else: # No plugins just use the original
|
|
|
|
plugined_class = base_class
|
|
|
|
return plugined_class
|
version 0.2.7, plugin system, multiuser plugin for zeroproxies, reworked imports, cookie parse, stats moved to plugin, usermanager class, dont generate site auth on listing, multiline notifications, allow server side prompt from user, update script keep plugins disabled status
2015-03-24 01:33:09 +01:00
|
|
|
|
|
|
|
|
|
|
|
# Register plugin to class name decorator
|
|
|
|
def registerTo(class_name):
|
2019-04-18 12:21:50 +02:00
|
|
|
if config.debug and not plugin_manager.reloading:
|
2019-04-15 22:18:18 +02:00
|
|
|
import gc
|
|
|
|
for obj in gc.get_objects():
|
|
|
|
if type(obj).__name__ == class_name:
|
|
|
|
raise Exception("Class %s instances already present in memory" % class_name)
|
|
|
|
break
|
|
|
|
|
2015-07-12 20:36:46 +02:00
|
|
|
plugin_manager.log.debug("New plugin registered to: %s" % class_name)
|
|
|
|
if class_name not in plugin_manager.plugins:
|
|
|
|
plugin_manager.plugins[class_name] = []
|
version 0.2.7, plugin system, multiuser plugin for zeroproxies, reworked imports, cookie parse, stats moved to plugin, usermanager class, dont generate site auth on listing, multiline notifications, allow server side prompt from user, update script keep plugins disabled status
2015-03-24 01:33:09 +01:00
|
|
|
|
2015-07-12 20:36:46 +02:00
|
|
|
def classDecorator(self):
|
|
|
|
plugin_manager.plugins[class_name].append(self)
|
|
|
|
return self
|
|
|
|
return classDecorator
|
version 0.2.7, plugin system, multiuser plugin for zeroproxies, reworked imports, cookie parse, stats moved to plugin, usermanager class, dont generate site auth on listing, multiline notifications, allow server side prompt from user, update script keep plugins disabled status
2015-03-24 01:33:09 +01:00
|
|
|
|
|
|
|
|
2017-10-03 14:57:44 +02:00
|
|
|
def afterLoad(func):
|
|
|
|
plugin_manager.after_load.append(func)
|
|
|
|
return func
|
|
|
|
|
|
|
|
|
version 0.2.7, plugin system, multiuser plugin for zeroproxies, reworked imports, cookie parse, stats moved to plugin, usermanager class, dont generate site auth on listing, multiline notifications, allow server side prompt from user, update script keep plugins disabled status
2015-03-24 01:33:09 +01:00
|
|
|
# - Example usage -
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2015-07-12 20:36:46 +02:00
|
|
|
@registerTo("Request")
|
|
|
|
class RequestPlugin(object):
|
|
|
|
|
|
|
|
def actionMainPage(self, path):
|
|
|
|
return "Hello MainPage!"
|
|
|
|
|
|
|
|
@acceptPlugins
|
|
|
|
class Request(object):
|
|
|
|
|
|
|
|
def route(self, path):
|
|
|
|
func = getattr(self, "action" + path, None)
|
|
|
|
if func:
|
|
|
|
return func(path)
|
|
|
|
else:
|
|
|
|
return "Can't route to", path
|
|
|
|
|
2019-03-15 21:06:59 +01:00
|
|
|
print(Request().route("MainPage"))
|