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.

344 line
14KB

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