plugin_manager.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. # encoding:utf-8
  2. import importlib
  3. import importlib.util
  4. import json
  5. import os
  6. import sys
  7. from common.log import logger
  8. from common.singleton import singleton
  9. from common.sorted_dict import SortedDict
  10. from config import conf, write_plugin_config
  11. from .event import *
  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 = kwargs.get("enabled") if kwargs.get("enabled") != None else 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. @staticmethod
  55. def _load_all_config():
  56. """
  57. 背景: 目前插件配置存放于每个插件目录的config.json下,docker运行时不方便进行映射,故增加统一管理的入口,优先
  58. 加载 plugins/config.json,原插件目录下的config.json 不受影响
  59. 从 plugins/config.json 中加载所有插件的配置并写入 config.py 的全局配置中,供插件中使用
  60. 插件实例中通过 config.pconf(plugin_name) 即可获取该插件的配置
  61. """
  62. all_config_path = "./plugins/config.json"
  63. try:
  64. if os.path.exists(all_config_path):
  65. # read from all plugins config
  66. with open(all_config_path, "r", encoding="utf-8") as f:
  67. all_conf = json.load(f)
  68. logger.info(f"load all config from plugins/config.json: {all_conf}")
  69. # write to global config
  70. write_plugin_config(all_conf)
  71. except Exception as e:
  72. logger.error(e)
  73. def scan_plugins(self):
  74. logger.info("Scaning plugins ...")
  75. plugins_dir = "./plugins"
  76. raws = [self.plugins[name] for name in self.plugins]
  77. for plugin_name in os.listdir(plugins_dir):
  78. plugin_path = os.path.join(plugins_dir, plugin_name)
  79. if os.path.isdir(plugin_path):
  80. # 判断插件是否包含同名__init__.py文件
  81. main_module_path = os.path.join(plugin_path, "__init__.py")
  82. if os.path.isfile(main_module_path):
  83. # 导入插件
  84. import_path = "plugins.{}".format(plugin_name)
  85. try:
  86. self.current_plugin_path = plugin_path
  87. if plugin_path in self.loaded:
  88. if plugin_name.upper() != 'GODCMD':
  89. logger.info("reload module %s" % plugin_name)
  90. self.loaded[plugin_path] = importlib.reload(sys.modules[import_path])
  91. dependent_module_names = [name for name in sys.modules.keys() if name.startswith(import_path + ".")]
  92. for name in dependent_module_names:
  93. logger.info("reload module %s" % name)
  94. importlib.reload(sys.modules[name])
  95. else:
  96. self.loaded[plugin_path] = importlib.import_module(import_path)
  97. self.current_plugin_path = None
  98. except Exception as e:
  99. logger.warn("Failed to import plugin %s: %s" % (plugin_name, e))
  100. continue
  101. pconf = self.pconf
  102. news = [self.plugins[name] for name in self.plugins]
  103. new_plugins = list(set(news) - set(raws))
  104. modified = False
  105. for name, plugincls in self.plugins.items():
  106. rawname = plugincls.name
  107. if rawname not in pconf["plugins"]:
  108. modified = True
  109. logger.info("Plugin %s not found in pconfig, adding to pconfig..." % name)
  110. pconf["plugins"][rawname] = {
  111. "enabled": plugincls.enabled,
  112. "priority": plugincls.priority,
  113. }
  114. else:
  115. self.plugins[name].enabled = pconf["plugins"][rawname]["enabled"]
  116. self.plugins[name].priority = pconf["plugins"][rawname]["priority"]
  117. self.plugins._update_heap(name) # 更新下plugins中的顺序
  118. if modified:
  119. self.save_config()
  120. return new_plugins
  121. def refresh_order(self):
  122. for event in self.listening_plugins.keys():
  123. self.listening_plugins[event].sort(key=lambda name: self.plugins[name].priority, reverse=True)
  124. def activate_plugins(self): # 生成新开启的插件实例
  125. failed_plugins = []
  126. self._load_all_config() # 重新读取全局插件配置,支持使用#reloadp命令对插件配置热更新
  127. for name, plugincls in self.plugins.items():
  128. if plugincls.enabled:
  129. if 'GODCMD' in self.instances and name == 'GODCMD':
  130. continue
  131. # if name not in self.instances:
  132. try:
  133. instance = plugincls()
  134. except Exception as e:
  135. logger.warn("Failed to init %s, diabled. %s" % (name, e))
  136. self.disable_plugin(name)
  137. failed_plugins.append(name)
  138. continue
  139. self.instances[name] = instance
  140. for event in instance.handlers:
  141. if event not in self.listening_plugins:
  142. self.listening_plugins[event] = []
  143. self.listening_plugins[event].append(name)
  144. self.refresh_order()
  145. return failed_plugins
  146. def reload_plugin(self, name: str):
  147. name = name.upper()
  148. if name in self.instances:
  149. for event in self.listening_plugins:
  150. if name in self.listening_plugins[event]:
  151. self.listening_plugins[event].remove(name)
  152. del self.instances[name]
  153. self.activate_plugins()
  154. return True
  155. return False
  156. def load_plugins(self):
  157. self.load_config()
  158. self.scan_plugins()
  159. # 加载全量插件配置
  160. self._load_all_config()
  161. pconf = self.pconf
  162. logger.debug("plugins.json config={}".format(pconf))
  163. for name, plugin in pconf["plugins"].items():
  164. if name.upper() not in self.plugins:
  165. logger.error("Plugin %s not found, but found in plugins.json" % name)
  166. self.activate_plugins()
  167. def emit_event(self, e_context: EventContext, *args, **kwargs):
  168. if e_context.event in self.listening_plugins:
  169. for name in self.listening_plugins[e_context.event]:
  170. if self.plugins[name].enabled and e_context.action == EventAction.CONTINUE:
  171. logger.debug("Plugin %s triggered by event %s" % (name, e_context.event))
  172. instance = self.instances[name]
  173. instance.handlers[e_context.event](e_context, *args, **kwargs)
  174. if e_context.is_break():
  175. e_context["breaked_by"] = name
  176. logger.debug("Plugin %s breaked event %s" % (name, e_context.event))
  177. return e_context
  178. def set_plugin_priority(self, name: str, priority: int):
  179. name = name.upper()
  180. if name not in self.plugins:
  181. return False
  182. if self.plugins[name].priority == priority:
  183. return True
  184. self.plugins[name].priority = priority
  185. self.plugins._update_heap(name)
  186. rawname = self.plugins[name].name
  187. self.pconf["plugins"][rawname]["priority"] = priority
  188. self.pconf["plugins"]._update_heap(rawname)
  189. self.save_config()
  190. self.refresh_order()
  191. return True
  192. def enable_plugin(self, name: str):
  193. name = name.upper()
  194. if name not in self.plugins:
  195. return False, "插件不存在"
  196. if not self.plugins[name].enabled:
  197. self.plugins[name].enabled = True
  198. rawname = self.plugins[name].name
  199. self.pconf["plugins"][rawname]["enabled"] = True
  200. self.save_config()
  201. failed_plugins = self.activate_plugins()
  202. if name in failed_plugins:
  203. return False, "插件开启失败"
  204. return True, "插件已开启"
  205. return True, "插件已开启"
  206. def disable_plugin(self, name: str):
  207. name = name.upper()
  208. if name not in self.plugins:
  209. return False
  210. if self.plugins[name].enabled:
  211. self.plugins[name].enabled = False
  212. rawname = self.plugins[name].name
  213. self.pconf["plugins"][rawname]["enabled"] = False
  214. self.save_config()
  215. return True
  216. return True
  217. def list_plugins(self):
  218. return self.plugins
  219. def install_plugin(self, repo: str):
  220. try:
  221. import common.package_manager as pkgmgr
  222. pkgmgr.check_dulwich()
  223. except Exception as e:
  224. logger.error("Failed to install plugin, {}".format(e))
  225. return False, "无法导入dulwich,安装插件失败"
  226. import re
  227. from dulwich import porcelain
  228. logger.info("clone git repo: {}".format(repo))
  229. match = re.match(r"^(https?:\/\/|git@)([^\/:]+)[\/:]([^\/:]+)\/(.+).git$", repo)
  230. if not match:
  231. try:
  232. with open("./plugins/source.json", "r", encoding="utf-8") as f:
  233. source = json.load(f)
  234. if repo in source["repo"]:
  235. repo = source["repo"][repo]["url"]
  236. match = re.match(r"^(https?:\/\/|git@)([^\/:]+)[\/:]([^\/:]+)\/(.+).git$", repo)
  237. if not match:
  238. return False, "安装插件失败,source中的仓库地址不合法"
  239. else:
  240. return False, "安装插件失败,仓库地址不合法"
  241. except Exception as e:
  242. logger.error("Failed to install plugin, {}".format(e))
  243. return False, "安装插件失败,请检查仓库地址是否正确"
  244. dirname = os.path.join("./plugins", match.group(4))
  245. try:
  246. repo = porcelain.clone(repo, dirname, checkout=True)
  247. if os.path.exists(os.path.join(dirname, "requirements.txt")):
  248. logger.info("detect requirements.txt,installing...")
  249. pkgmgr.install_requirements(os.path.join(dirname, "requirements.txt"))
  250. return True, "安装插件成功,请使用 #scanp 命令扫描插件或重启程序,开启前请检查插件是否需要配置"
  251. except Exception as e:
  252. logger.error("Failed to install plugin, {}".format(e))
  253. return False, "安装插件失败," + str(e)
  254. def update_plugin(self, name: str):
  255. try:
  256. import common.package_manager as pkgmgr
  257. pkgmgr.check_dulwich()
  258. except Exception as e:
  259. logger.error("Failed to install plugin, {}".format(e))
  260. return False, "无法导入dulwich,更新插件失败"
  261. from dulwich import porcelain
  262. name = name.upper()
  263. if name not in self.plugins:
  264. return False, "插件不存在"
  265. if name in [
  266. "HELLO",
  267. "GODCMD",
  268. "ROLE",
  269. "TOOL",
  270. "BDUNIT",
  271. "BANWORDS",
  272. "FINISH",
  273. "DUNGEON",
  274. ]:
  275. return False, "预置插件无法更新,请更新主程序仓库"
  276. dirname = self.plugins[name].path
  277. try:
  278. porcelain.pull(dirname, "origin")
  279. if os.path.exists(os.path.join(dirname, "requirements.txt")):
  280. logger.info("detect requirements.txt,installing...")
  281. pkgmgr.install_requirements(os.path.join(dirname, "requirements.txt"))
  282. return True, "更新插件成功,请重新运行程序"
  283. except Exception as e:
  284. logger.error("Failed to update plugin, {}".format(e))
  285. return False, "更新插件失败," + str(e)
  286. def uninstall_plugin(self, name: str):
  287. name = name.upper()
  288. if name not in self.plugins:
  289. return False, "插件不存在"
  290. if name in self.instances:
  291. self.disable_plugin(name)
  292. dirname = self.plugins[name].path
  293. try:
  294. import shutil
  295. shutil.rmtree(dirname)
  296. rawname = self.plugins[name].name
  297. for event in self.listening_plugins:
  298. if name in self.listening_plugins[event]:
  299. self.listening_plugins[event].remove(name)
  300. del self.plugins[name]
  301. del self.pconf["plugins"][rawname]
  302. self.loaded[dirname] = None
  303. self.save_config()
  304. return True, "卸载插件成功"
  305. except Exception as e:
  306. logger.error("Failed to uninstall plugin, {}".format(e))
  307. return False, "卸载插件失败,请手动删除文件夹完成卸载," + str(e)