1import logging 2import os 3import sys 4import shutil 5import time 6from collections import defaultdict 7 8import importlib 9import json 10 11from Debug import Debug 12from Config import config 13import plugins 14 15 16class PluginManager: 17 def __init__(self): 18 self.log = logging.getLogger("PluginManager") 19 self.path_plugins = os.path.abspath(os.path.dirname(plugins.__file__)) 20 self.path_installed_plugins = config.data_dir + "/__plugins__" 21 self.plugins = defaultdict(list) # Registered plugins (key: class name, value: list of plugins for class) 22 self.subclass_order = {} # Record the load order of the plugins, to keep it after reload 23 self.pluggable = {} 24 self.plugin_names = [] # Loaded plugin names 25 self.plugins_updated = {} # List of updated plugins since restart 26 self.plugins_rev = {} # Installed plugins revision numbers 27 self.after_load = [] # Execute functions after loaded plugins 28 self.function_flags = {} # Flag function for permissions 29 self.reloading = False 30 self.config_path = config.data_dir + "/plugins.json" 31 self.loadConfig() 32 33 self.config.setdefault("builtin", {}) 34 35 sys.path.append(os.path.join(os.getcwd(), self.path_plugins)) 36 self.migratePlugins() 37 38 if config.debug: # Auto reload Plugins on file change 39 from Debug import DebugReloader 40 DebugReloader.watcher.addCallback(self.reloadPlugins) 41 42 def loadConfig(self): 43 if os.path.isfile(self.config_path): 44 try: 45 self.config = json.load(open(self.config_path, encoding="utf8")) 46 except Exception as err: 47 self.log.error("Error loading %s: %s" % (self.config_path, err)) 48 self.config = {} 49 else: 50 self.config = {} 51 52 def saveConfig(self): 53 f = open(self.config_path, "w", encoding="utf8") 54 json.dump(self.config, f, ensure_ascii=False, sort_keys=True, indent=2) 55 56 def migratePlugins(self): 57 for dir_name in os.listdir(self.path_plugins): 58 if dir_name == "Mute": 59 self.log.info("Deleting deprecated/renamed plugin: %s" % dir_name) 60 shutil.rmtree("%s/%s" % (self.path_plugins, dir_name)) 61 62 # -- Load / Unload -- 63 64 def listPlugins(self, list_disabled=False): 65 plugins = [] 66 for dir_name in sorted(os.listdir(self.path_plugins)): 67 dir_path = os.path.join(self.path_plugins, dir_name) 68 plugin_name = dir_name.replace("disabled-", "") 69 if dir_name.startswith("disabled"): 70 is_enabled = False 71 else: 72 is_enabled = True 73 74 plugin_config = self.config["builtin"].get(plugin_name, {}) 75 if "enabled" in plugin_config: 76 is_enabled = plugin_config["enabled"] 77 78 if dir_name == "__pycache__" or not os.path.isdir(dir_path): 79 continue # skip 80 if dir_name.startswith("Debug") and not config.debug: 81 continue # Only load in debug mode if module name starts with Debug 82 if not is_enabled and not list_disabled: 83 continue # Dont load if disabled 84 85 plugin = {} 86 plugin["source"] = "builtin" 87 plugin["name"] = plugin_name 88 plugin["dir_name"] = dir_name 89 plugin["dir_path"] = dir_path 90 plugin["inner_path"] = plugin_name 91 plugin["enabled"] = is_enabled 92 plugin["rev"] = config.rev 93 plugin["loaded"] = plugin_name in self.plugin_names 94 plugins.append(plugin) 95 96 plugins += self.listInstalledPlugins(list_disabled) 97 return plugins 98 99 def listInstalledPlugins(self, list_disabled=False): 100 plugins = [] 101 102 for address, site_plugins in sorted(self.config.items()): 103 if address == "builtin": 104 continue 105 for plugin_inner_path, plugin_config in sorted(site_plugins.items()): 106 is_enabled = plugin_config.get("enabled", False) 107 if not is_enabled and not list_disabled: 108 continue 109 plugin_name = os.path.basename(plugin_inner_path) 110 111 dir_path = "%s/%s/%s" % (self.path_installed_plugins, address, plugin_inner_path) 112 113 plugin = {} 114 plugin["source"] = address 115 plugin["name"] = plugin_name 116 plugin["dir_name"] = plugin_name 117 plugin["dir_path"] = dir_path 118 plugin["inner_path"] = plugin_inner_path 119 plugin["enabled"] = is_enabled 120 plugin["rev"] = plugin_config.get("rev", 0) 121 plugin["loaded"] = plugin_name in self.plugin_names 122 plugins.append(plugin) 123 124 return plugins 125 126 # Load all plugin 127 def loadPlugins(self): 128 all_loaded = True 129 s = time.time() 130 for plugin in self.listPlugins(): 131 self.log.debug("Loading plugin: %s (%s)" % (plugin["name"], plugin["source"])) 132 if plugin["source"] != "builtin": 133 self.plugins_rev[plugin["name"]] = plugin["rev"] 134 site_plugin_dir = os.path.dirname(plugin["dir_path"]) 135 if site_plugin_dir not in sys.path: 136 sys.path.append(site_plugin_dir) 137 try: 138 sys.modules[plugin["name"]] = __import__(plugin["dir_name"]) 139 except Exception as err: 140 self.log.error("Plugin %s load error: %s" % (plugin["name"], Debug.formatException(err))) 141 all_loaded = False 142 if plugin["name"] not in self.plugin_names: 143 self.plugin_names.append(plugin["name"]) 144 145 self.log.debug("Plugins loaded in %.3fs" % (time.time() - s)) 146 for func in self.after_load: 147 func() 148 return all_loaded 149 150 # Reload all plugins 151 def reloadPlugins(self): 152 self.reloading = True 153 self.after_load = [] 154 self.plugins_before = self.plugins 155 self.plugins = defaultdict(list) # Reset registered plugins 156 for module_name, module in list(sys.modules.items()): 157 if not module or not getattr(module, "__file__", None): 158 continue 159 if self.path_plugins not in module.__file__ and self.path_installed_plugins not in module.__file__: 160 continue 161 162 if "allow_reload" in dir(module) and not module.allow_reload: # Reload disabled 163 # Re-add non-reloadable plugins 164 for class_name, classes in self.plugins_before.items(): 165 for c in classes: 166 if c.__module__ != module.__name__: 167 continue 168 self.plugins[class_name].append(c) 169 else: 170 try: 171 importlib.reload(module) 172 except Exception as err: 173 self.log.error("Plugin %s reload error: %s" % (module_name, Debug.formatException(err))) 174 175 self.loadPlugins() # Load new plugins 176 177 # Change current classes in memory 178 import gc 179 patched = {} 180 for class_name, classes in self.plugins.items(): 181 classes = classes[:] # Copy the current plugins 182 classes.reverse() 183 base_class = self.pluggable[class_name] # Original class 184 classes.append(base_class) # Add the class itself to end of inherience line 185 plugined_class = type(class_name, tuple(classes), dict()) # Create the plugined class 186 for obj in gc.get_objects(): 187 if type(obj).__name__ == class_name: 188 obj.__class__ = plugined_class 189 patched[class_name] = patched.get(class_name, 0) + 1 190 self.log.debug("Patched objects: %s" % patched) 191 192 # Change classes in modules 193 patched = {} 194 for class_name, classes in self.plugins.items(): 195 for module_name, module in list(sys.modules.items()): 196 if class_name in dir(module): 197 if "__class__" not in dir(getattr(module, class_name)): # Not a class 198 continue 199 base_class = self.pluggable[class_name] 200 classes = self.plugins[class_name][:] 201 classes.reverse() 202 classes.append(base_class) 203 plugined_class = type(class_name, tuple(classes), dict()) 204 setattr(module, class_name, plugined_class) 205 patched[class_name] = patched.get(class_name, 0) + 1 206 207 self.log.debug("Patched modules: %s" % patched) 208 self.reloading = False 209 210 211plugin_manager = PluginManager() # Singletone 212 213# -- Decorators -- 214 215# Accept plugin to class decorator 216 217 218def acceptPlugins(base_class): 219 class_name = base_class.__name__ 220 plugin_manager.pluggable[class_name] = base_class 221 if class_name in plugin_manager.plugins: # Has plugins 222 classes = plugin_manager.plugins[class_name][:] # Copy the current plugins 223 224 # Restore the subclass order after reload 225 if class_name in plugin_manager.subclass_order: 226 classes = sorted( 227 classes, 228 key=lambda key: 229 plugin_manager.subclass_order[class_name].index(str(key)) 230 if str(key) in plugin_manager.subclass_order[class_name] 231 else 9999 232 ) 233 plugin_manager.subclass_order[class_name] = list(map(str, classes)) 234 235 classes.reverse() 236 classes.append(base_class) # Add the class itself to end of inherience line 237 plugined_class = type(class_name, tuple(classes), dict()) # Create the plugined class 238 plugin_manager.log.debug("New class accepts plugins: %s (Loaded plugins: %s)" % (class_name, classes)) 239 else: # No plugins just use the original 240 plugined_class = base_class 241 return plugined_class 242 243 244# Register plugin to class name decorator 245def registerTo(class_name): 246 if config.debug and not plugin_manager.reloading: 247 import gc 248 for obj in gc.get_objects(): 249 if type(obj).__name__ == class_name: 250 raise Exception("Class %s instances already present in memory" % class_name) 251 break 252 253 plugin_manager.log.debug("New plugin registered to: %s" % class_name) 254 if class_name not in plugin_manager.plugins: 255 plugin_manager.plugins[class_name] = [] 256 257 def classDecorator(self): 258 plugin_manager.plugins[class_name].append(self) 259 return self 260 return classDecorator 261 262 263def afterLoad(func): 264 plugin_manager.after_load.append(func) 265 return func 266 267 268# - Example usage - 269 270if __name__ == "__main__": 271 @registerTo("Request") 272 class RequestPlugin(object): 273 274 def actionMainPage(self, path): 275 return "Hello MainPage!" 276 277 @acceptPlugins 278 class Request(object): 279 280 def route(self, path): 281 func = getattr(self, "action" + path, None) 282 if func: 283 return func(path) 284 else: 285 return "Can't route to", path 286 287 print(Request().route("MainPage")) 288