You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
преди 1 година
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. # encoding:utf-8
  2. import importlib
  3. import importlib.util
  4. import json
  5. import os
  6. import sys
  7. from common.singleton import singleton
  8. from common.sorted_dict import SortedDict
  9. from .event import *
  10. from common.log import logger
  11. from config import conf
  12. @singleton
  13. class PluginManager:
  14. def __init__(self):
  15. self.plugins = SortedDict(lambda k,v: v.priority,reverse=True)
  16. self.listening_plugins = {}
  17. self.instances = {}
  18. self.pconf = {}
  19. self.current_plugin_path = None
  20. self.loaded = {}
  21. def register(self, name: str, desire_priority: int = 0, **kwargs):
  22. def wrapper(plugincls):
  23. plugincls.name = name
  24. plugincls.priority = desire_priority
  25. plugincls.desc = kwargs.get('desc')
  26. plugincls.author = kwargs.get('author')
  27. plugincls.path = self.current_plugin_path
  28. plugincls.version = kwargs.get('version') if kwargs.get('version') != None else "1.0"
  29. plugincls.namecn = kwargs.get('namecn') if kwargs.get('namecn') != None else name
  30. plugincls.hidden = kwargs.get('hidden') if kwargs.get('hidden') != None else False
  31. plugincls.enabled = True
  32. if self.current_plugin_path == None:
  33. raise Exception("Plugin path not set")
  34. self.plugins[name.upper()] = plugincls
  35. logger.info("Plugin %s_v%s registered, path=%s" % (name, plugincls.version, plugincls.path))
  36. return wrapper
  37. def save_config(self):
  38. with open("./plugins/plugins.json", "w", encoding="utf-8") as f:
  39. json.dump(self.pconf, f, indent=4, ensure_ascii=False)
  40. def load_config(self):
  41. logger.info("Loading plugins config...")
  42. modified = False
  43. if os.path.exists("./plugins/plugins.json"):
  44. with open("./plugins/plugins.json", "r", encoding="utf-8") as f:
  45. pconf = json.load(f)
  46. pconf['plugins'] = SortedDict(lambda k,v: v["priority"],pconf['plugins'],reverse=True)
  47. else:
  48. modified = True
  49. pconf = {"plugins": SortedDict(lambda k,v: v["priority"],reverse=True)}
  50. self.pconf = pconf
  51. if modified:
  52. self.save_config()
  53. return pconf
  54. def scan_plugins(self):
  55. logger.info("Scaning plugins ...")
  56. plugins_dir = "./plugins"
  57. raws = [self.plugins[name] for name in self.plugins]
  58. for plugin_name in os.listdir(plugins_dir):
  59. plugin_path = os.path.join(plugins_dir, plugin_name)
  60. if os.path.isdir(plugin_path):
  61. # 判断插件是否包含同名__init__.py文件
  62. main_module_path = os.path.join(plugin_path,"__init__.py")
  63. if os.path.isfile(main_module_path):
  64. # 导入插件
  65. import_path = "plugins.{}".format(plugin_name)
  66. try:
  67. self.current_plugin_path = plugin_path
  68. if plugin_path in self.loaded:
  69. if self.loaded[plugin_path] == None:
  70. logger.info("reload module %s" % plugin_name)
  71. self.loaded[plugin_path] = importlib.reload(sys.modules[import_path])
  72. dependent_module_names = [name for name in sys.modules.keys() if name.startswith( import_path+ '.')]
  73. for name in dependent_module_names:
  74. logger.info("reload module %s" % name)
  75. importlib.reload(sys.modules[name])
  76. else:
  77. self.loaded[plugin_path] = importlib.import_module(import_path)
  78. self.current_plugin_path = None
  79. except Exception as e:
  80. logger.exception("Failed to import plugin %s: %s" % (plugin_name, e))
  81. continue
  82. pconf = self.pconf
  83. news = [self.plugins[name] for name in self.plugins]
  84. new_plugins = list(set(news) - set(raws))
  85. modified = False
  86. for name, plugincls in self.plugins.items():
  87. rawname = plugincls.name
  88. if rawname not in pconf["plugins"]:
  89. modified = True
  90. logger.info("Plugin %s not found in pconfig, adding to pconfig..." % name)
  91. pconf["plugins"][rawname] = {"enabled": plugincls.enabled, "priority": plugincls.priority}
  92. else:
  93. self.plugins[name].enabled = pconf["plugins"][rawname]["enabled"]
  94. self.plugins[name].priority = pconf["plugins"][rawname]["priority"]
  95. self.plugins._update_heap(name) # 更新下plugins中的顺序
  96. if modified:
  97. self.save_config()
  98. return new_plugins
  99. def refresh_order(self):
  100. for event in self.listening_plugins.keys():
  101. self.listening_plugins[event].sort(key=lambda name: self.plugins[name].priority, reverse=True)
  102. def activate_plugins(self): # 生成新开启的插件实例
  103. failed_plugins = []
  104. for name, plugincls in self.plugins.items():
  105. if plugincls.enabled:
  106. if name not in self.instances:
  107. try:
  108. instance = plugincls()
  109. except Exception as e:
  110. logger.warn("Failed to init %s, diabled. %s" % (name, e))
  111. self.disable_plugin(name)
  112. failed_plugins.append(name)
  113. continue
  114. self.instances[name] = instance
  115. for event in instance.handlers:
  116. if event not in self.listening_plugins:
  117. self.listening_plugins[event] = []
  118. self.listening_plugins[event].append(name)
  119. self.refresh_order()
  120. return failed_plugins
  121. def reload_plugin(self, name:str):
  122. name = name.upper()
  123. if name in self.instances:
  124. for event in self.listening_plugins:
  125. if name in self.listening_plugins[event]:
  126. self.listening_plugins[event].remove(name)
  127. del self.instances[name]
  128. self.activate_plugins()
  129. return True
  130. return False
  131. def load_plugins(self):
  132. self.load_config()
  133. self.scan_plugins()
  134. pconf = self.pconf
  135. logger.debug("plugins.json config={}".format(pconf))
  136. for name,plugin in pconf["plugins"].items():
  137. if name.upper() not in self.plugins:
  138. logger.error("Plugin %s not found, but found in plugins.json" % name)
  139. self.activate_plugins()
  140. def emit_event(self, e_context: EventContext, *args, **kwargs):
  141. if e_context.event in self.listening_plugins:
  142. for name in self.listening_plugins[e_context.event]:
  143. if self.plugins[name].enabled and e_context.action == EventAction.CONTINUE:
  144. logger.debug("Plugin %s triggered by event %s" % (name,e_context.event))
  145. instance = self.instances[name]
  146. instance.handlers[e_context.event](e_context, *args, **kwargs)
  147. return e_context
  148. def set_plugin_priority(self, name:str, priority:int):
  149. name = name.upper()
  150. if name not in self.plugins:
  151. return False
  152. if self.plugins[name].priority == priority:
  153. return True
  154. self.plugins[name].priority = priority
  155. self.plugins._update_heap(name)
  156. rawname = self.plugins[name].name
  157. self.pconf["plugins"][rawname]["priority"] = priority
  158. self.pconf["plugins"]._update_heap(rawname)
  159. self.save_config()
  160. self.refresh_order()
  161. return True
  162. def enable_plugin(self, name:str):
  163. name = name.upper()
  164. if name not in self.plugins:
  165. return False, "插件不存在"
  166. if not self.plugins[name].enabled :
  167. self.plugins[name].enabled = True
  168. rawname = self.plugins[name].name
  169. self.pconf["plugins"][rawname]["enabled"] = True
  170. self.save_config()
  171. failed_plugins = self.activate_plugins()
  172. if name in failed_plugins:
  173. return False, "插件开启失败"
  174. return True, "插件已开启"
  175. return True, "插件已开启"
  176. def disable_plugin(self, name:str):
  177. name = name.upper()
  178. if name not in self.plugins:
  179. return False
  180. if self.plugins[name].enabled :
  181. self.plugins[name].enabled = False
  182. rawname = self.plugins[name].name
  183. self.pconf["plugins"][rawname]["enabled"] = False
  184. self.save_config()
  185. return True
  186. return True
  187. def list_plugins(self):
  188. return self.plugins
  189. def install_plugin(self, repo:str):
  190. try:
  191. import common.package_manager as pkgmgr
  192. pkgmgr.check_dulwich()
  193. except Exception as e:
  194. logger.error("Failed to install plugin, {}".format(e))
  195. return False, "无法导入dulwich,安装插件失败"
  196. import re
  197. from dulwich import porcelain
  198. logger.info("clone git repo: {}".format(repo))
  199. match = re.match(r"^(https?:\/\/|git@)([^\/:]+)[\/:]([^\/:]+)\/(.+).git$", repo)
  200. if not match:
  201. try:
  202. with open("./plugins/source.json","r", encoding="utf-8") as f:
  203. source = json.load(f)
  204. if repo in source["repo"]:
  205. repo = source["repo"][repo]["url"]
  206. match = re.match(r"^(https?:\/\/|git@)([^\/:]+)[\/:]([^\/:]+)\/(.+).git$", repo)
  207. if not match:
  208. return False, "安装插件失败,source中的仓库地址不合法"
  209. else:
  210. return False, "安装插件失败,仓库地址不合法"
  211. except Exception as e:
  212. logger.error("Failed to install plugin, {}".format(e))
  213. return False, "安装插件失败,请检查仓库地址是否正确"
  214. dirname = os.path.join("./plugins",match.group(4))
  215. try:
  216. repo = porcelain.clone(repo, dirname, checkout=True)
  217. if os.path.exists(os.path.join(dirname,"requirements.txt")):
  218. logger.info("detect requirements.txt,installing...")
  219. pkgmgr.install_requirements(os.path.join(dirname,"requirements.txt"))
  220. return True, "安装插件成功,请使用 #scanp 命令扫描插件或重启程序"
  221. except Exception as e:
  222. logger.error("Failed to install plugin, {}".format(e))
  223. return False, "安装插件失败,"+str(e)
  224. def uninstall_plugin(self, name:str):
  225. name = name.upper()
  226. if name not in self.plugins:
  227. return False, "插件不存在"
  228. if name in self.instances:
  229. self.disable_plugin(name)
  230. dirname = self.plugins[name].path
  231. try:
  232. import shutil
  233. shutil.rmtree(dirname)
  234. rawname = self.plugins[name].name
  235. for event in self.listening_plugins:
  236. if name in self.listening_plugins[event]:
  237. self.listening_plugins[event].remove(name)
  238. del self.plugins[name]
  239. del self.pconf["plugins"][rawname]
  240. self.loaded[dirname] = None
  241. self.save_config()
  242. return True, "卸载插件成功"
  243. except Exception as e:
  244. logger.error("Failed to uninstall plugin, {}".format(e))
  245. return False, "卸载插件失败,请手动删除文件夹完成卸载,"+str(e)