Forking what is left of ZeroNet and hopefully adding an AT Proto Frontend/Proxy
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"))