PluginManager.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import logging
  2. import os
  3. import sys
  4. import shutil
  5. import time
  6. from collections import defaultdict
  7. from Debug import Debug
  8. from Config import config
  9. class PluginManager:
  10. def __init__(self):
  11. self.log = logging.getLogger("PluginManager")
  12. self.plugin_path = "plugins" # Plugin directory
  13. self.plugins = defaultdict(list) # Registered plugins (key: class name, value: list of plugins for class)
  14. self.subclass_order = {} # Record the load order of the plugins, to keep it after reload
  15. self.pluggable = {}
  16. self.plugin_names = [] # Loaded plugin names
  17. self.after_load = [] # Execute functions after loaded plugins
  18. sys.path.append(os.path.join(os.getcwd(), self.plugin_path))
  19. self.migratePlugins()
  20. if config.debug: # Auto reload Plugins on file change
  21. from Debug import DebugReloader
  22. DebugReloader(self.reloadPlugins)
  23. def migratePlugins(self):
  24. for dir_name in os.listdir(self.plugin_path):
  25. if dir_name == "Mute":
  26. self.log.info("Deleting deprecated/renamed plugin: %s" % dir_name)
  27. shutil.rmtree("%s/%s" % (self.plugin_path, dir_name))
  28. # -- Load / Unload --
  29. # Load all plugin
  30. def loadPlugins(self):
  31. s = time.time()
  32. for dir_name in sorted(os.listdir(self.plugin_path)):
  33. dir_path = os.path.join(self.plugin_path, dir_name)
  34. if dir_name.startswith("disabled"):
  35. continue # Dont load if disabled
  36. if not os.path.isdir(dir_path):
  37. continue # Dont load if not dir
  38. if dir_name.startswith("Debug") and not config.debug:
  39. continue # Only load in debug mode if module name starts with Debug
  40. self.log.debug("Loading plugin: %s" % dir_name)
  41. try:
  42. __import__(dir_name)
  43. except Exception, err:
  44. self.log.error("Plugin %s load error: %s" % (dir_name, Debug.formatException(err)))
  45. if dir_name not in self.plugin_names:
  46. self.plugin_names.append(dir_name)
  47. self.log.debug("Plugins loaded in %.3fs" % (time.time() - s))
  48. for func in self.after_load:
  49. func()
  50. # Reload all plugins
  51. def reloadPlugins(self):
  52. self.after_load = []
  53. self.plugins_before = self.plugins
  54. self.plugins = defaultdict(list) # Reset registered plugins
  55. for module_name, module in sys.modules.items():
  56. if module and "__file__" in dir(module) and self.plugin_path in module.__file__: # Module file within plugin_path
  57. if "allow_reload" in dir(module) and not module.allow_reload: # Reload disabled
  58. # Re-add non-reloadable plugins
  59. for class_name, classes in self.plugins_before.iteritems():
  60. for c in classes:
  61. if c.__module__ != module.__name__:
  62. continue
  63. self.plugins[class_name].append(c)
  64. else:
  65. try:
  66. reload(module)
  67. except Exception, err:
  68. self.log.error("Plugin %s reload error: %s" % (module_name, Debug.formatException(err)))
  69. self.loadPlugins() # Load new plugins
  70. # Change current classes in memory
  71. import gc
  72. patched = {}
  73. for class_name, classes in self.plugins.iteritems():
  74. classes = classes[:] # Copy the current plugins
  75. classes.reverse()
  76. base_class = self.pluggable[class_name] # Original class
  77. classes.append(base_class) # Add the class itself to end of inherience line
  78. plugined_class = type(class_name, tuple(classes), dict()) # Create the plugined class
  79. for obj in gc.get_objects():
  80. if type(obj).__name__ == class_name:
  81. obj.__class__ = plugined_class
  82. patched[class_name] = patched.get(class_name, 0) + 1
  83. self.log.debug("Patched objects: %s" % patched)
  84. # Change classes in modules
  85. patched = {}
  86. for class_name, classes in self.plugins.iteritems():
  87. for module_name, module in sys.modules.iteritems():
  88. if class_name in dir(module):
  89. if "__class__" not in dir(getattr(module, class_name)): # Not a class
  90. continue
  91. base_class = self.pluggable[class_name]
  92. classes = self.plugins[class_name][:]
  93. classes.reverse()
  94. classes.append(base_class)
  95. plugined_class = type(class_name, tuple(classes), dict())
  96. setattr(module, class_name, plugined_class)
  97. patched[class_name] = patched.get(class_name, 0) + 1
  98. self.log.debug("Patched modules: %s" % patched)
  99. plugin_manager = PluginManager() # Singletone
  100. # -- Decorators --
  101. # Accept plugin to class decorator
  102. def acceptPlugins(base_class):
  103. class_name = base_class.__name__
  104. plugin_manager.pluggable[class_name] = base_class
  105. if class_name in plugin_manager.plugins: # Has plugins
  106. classes = plugin_manager.plugins[class_name][:] # Copy the current plugins
  107. # Restore the subclass order after reload
  108. if class_name in plugin_manager.subclass_order:
  109. classes = sorted(
  110. classes,
  111. key=lambda key:
  112. plugin_manager.subclass_order[class_name].index(str(key))
  113. if str(key) in plugin_manager.subclass_order[class_name]
  114. else 9999
  115. )
  116. plugin_manager.subclass_order[class_name] = map(str, classes)
  117. classes.reverse()
  118. classes.append(base_class) # Add the class itself to end of inherience line
  119. plugined_class = type(class_name, tuple(classes), dict()) # Create the plugined class
  120. plugin_manager.log.debug("New class accepts plugins: %s (Loaded plugins: %s)" % (class_name, classes))
  121. else: # No plugins just use the original
  122. plugined_class = base_class
  123. return plugined_class
  124. # Register plugin to class name decorator
  125. def registerTo(class_name):
  126. plugin_manager.log.debug("New plugin registered to: %s" % class_name)
  127. if class_name not in plugin_manager.plugins:
  128. plugin_manager.plugins[class_name] = []
  129. def classDecorator(self):
  130. plugin_manager.plugins[class_name].append(self)
  131. return self
  132. return classDecorator
  133. def afterLoad(func):
  134. plugin_manager.after_load.append(func)
  135. return func
  136. # - Example usage -
  137. if __name__ == "__main__":
  138. @registerTo("Request")
  139. class RequestPlugin(object):
  140. def actionMainPage(self, path):
  141. return "Hello MainPage!"
  142. @acceptPlugins
  143. class Request(object):
  144. def route(self, path):
  145. func = getattr(self, "action" + path, None)
  146. if func:
  147. return func(path)
  148. else:
  149. return "Can't route to", path
  150. print Request().route("MainPage")