From 2bf1c07041b4a691ac506526666d7c22354ccf98 Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Mon, 29 Jul 2024 03:09:02 +0300 Subject: [PATCH] [+] plugins command --- src/modules/EventsSystem/__init__.py | 25 ++- src/modules/PluginsLoader/__init__.py | 238 ++++++++++++++++---------- 2 files changed, 168 insertions(+), 95 deletions(-) diff --git a/src/modules/EventsSystem/__init__.py b/src/modules/EventsSystem/__init__.py index 9fb69b1..7680662 100644 --- a/src/modules/EventsSystem/__init__.py +++ b/src/modules/EventsSystem/__init__.py @@ -96,6 +96,21 @@ class EventsSystem: self.log.debug("used builtins_hook") builtins.ev = self + def unregister(self, func): + self.log.debug(f"unregister {func}") + s = a = 0 + for k, funcs in self.__events.items(): + for f in funcs: + if f is func: + s += 1 + self.__events[k].remove(func) + for k, funcs in self.__async_events.items(): + for f in funcs: + if f is func: + a += 1 + self.__async_events[k].remove(func) + self.log.debug(f"unregister in {s+a} events; S:{s}; A:{a};") + def is_event(self, event_name): return (event_name in self.__async_events.keys() or event_name in self.__events.keys() or @@ -117,15 +132,13 @@ class EventsSystem: return if async_event or inspect.iscoroutinefunction(event_func): if event_name not in self.__async_events: - self.__async_events.update({str(event_name): [event_func]}) - else: - self.__async_events[event_name].append(event_func) + self.__async_events[event_name] = [] + self.__async_events[event_name].append(event_func) self.log.debug("Register ok") else: if event_name not in self.__events: - self.__events.update({str(event_name): [event_func]}) - else: - self.__events[event_name].append(event_func) + self.__events[event_name] = [] + self.__events[event_name].append(event_func) self.log.debug("Register ok") async def call_async_event(self, event_name, *args, **kwargs): diff --git a/src/modules/PluginsLoader/__init__.py b/src/modules/PluginsLoader/__init__.py index f3670bc..df304cc 100644 --- a/src/modules/PluginsLoader/__init__.py +++ b/src/modules/PluginsLoader/__init__.py @@ -11,6 +11,7 @@ import inspect import os import subprocess import sys +import time import types from contextlib import contextmanager from pathlib import Path @@ -21,6 +22,7 @@ from core import get_logger class KuiToi: _plugins_dir = "" + _file = "" def __init__(self, name): if not name: @@ -29,6 +31,8 @@ class KuiToi: self.__name = name self.__dir = Path(self._plugins_dir) / self.__name os.makedirs(self.__dir, exist_ok=True) + self.__events_funcs = [] + self.register_event = self.register @property def log(self): @@ -63,8 +67,13 @@ class KuiToi: def register(self, event_name, event_func): self.log.debug(f"Registering event {event_name}") + self.__events_funcs.append(event_func) ev.register(event_name, event_func) + def _unload(self): + for f in self.__events_funcs: + ev.unregister(f) + def call_event(self, event_name, *args, **kwargs): self.log.debug(f"Called event {event_name}") return ev.call_event(event_name, *args, **kwargs) @@ -109,16 +118,45 @@ class PluginsLoader: self.plugins_tasks = [] self.plugins_dir = plugins_dir self.log = get_logger("PluginsLoader") - self.loaded_str = "Plugins: " + self.loaded = [] ev.register("_plugins_start", self.start) ev.register("_plugins_unload", self.unload) - ev.register("_plugins_get", lambda x: list(self.plugins.keys())) - console.add_command("plugins", lambda x: self.loaded_str[:-2]) - console.add_command("pl", lambda x: self.loaded_str[:-2]) + ev.register("_plugins_get", lambda _: "Plugins: " + ", ".join(f"{i[0]}:{'on' if i[1] else 'off'}" for i in self.loaded)) + console.add_command("plugins", self._parse_console, None, "Plugins manipulations", {"plugins": {"reload", "load", "unload", "list"}}) + console.add_command("pl", lambda _: ev.call_event("_plugins_get")[0]) sys.path.append(self._pip_dir) os.makedirs(self._pip_dir, exist_ok=True) console.add_command("install", self._pip_install) + async def _parse_console(self, x): + usage = 'Usage: plugin [reload | load | unload | list]' + if not x: + return usage + match x[0]: + case 'reload': + if len(x) == 2: + t1 = time.monotonic() + ok, _, file, _ = await self._unload_by_name(x[1], True) + if ok: + if await self._load_by_file(file): + return f"Plugin reloaded ({time.monotonic() - t1:.1f}sec)" + return "Plugin not found" + return usage + case 'load': + if len(x) == 2: + if await self._load_by_file(x[1]): + return "Plugin loaded" + return usage + case 'unload': + if len(x) == 2: + ok, _, _, _ = await self._unload_by_name(x[1], True) + if ok: + return "Plugin unloaded" + return usage + case 'list': + return ev.call_event("_plugins_get")[0] + return usage + def _pip_install(self, x): self.log.debug(f"_pip_install {x}") if len(x) > 0: @@ -131,83 +169,111 @@ class PluginsLoader: else: return "Invalid syntax" + async def _load_by_file(self, file): + file_path = os.path.join(self.plugins_dir, file) + if os.path.isfile(file_path) and file.endswith(".py"): + try: + self.log.debug(f"Loading plugin: {file[:-3]}") + plugin = types.ModuleType(file[:-3]) + plugin.KuiToi = KuiToi + plugin.KuiToi._plugins_dir = self.plugins_dir + plugin.KuiToi._file = file + plugin.print = print + plugin.__file__ = file_path + with open(f'{file_path}', 'r', encoding=config.enc) as f: + code = f.read() + exec(code, plugin.__dict__) + + ok = True + try: + is_func = inspect.isfunction + if not is_func(plugin.load): + self.log.error(i18n.plugins_not_found_load) + ok = False + if not is_func(plugin.start): + self.log.error(i18n.plugins_not_found_start) + ok = False + if not is_func(plugin.unload): + self.log.error(i18n.plugins_not_found_unload) + ok = False + if type(plugin.kt) != KuiToi: + self.log.error(i18n.plugins_kt_invalid) + ok = False + except AttributeError: + ok = False + if not ok: + self.log.error(i18n.plugins_invalid.format(file_path)) + return + + pl_name = plugin.kt.name + if self.plugins.get(pl_name) is not None: + raise NameError(f'Having plugins with identical names is not allowed; ' + f'Plugin name: "{pl_name}"; Plugin file "{file_path}"') + + plugin.open = plugin.kt.open + is_coro_func = inspect.iscoroutinefunction + self.plugins.update( + { + pl_name: { + "plugin": plugin, + "load": { + "func": plugin.load, + "async": is_coro_func(plugin.load) + }, + "start": { + "func": plugin.start, + "async": is_coro_func(plugin.start) + }, + "unload": { + "func": plugin.unload, + "async": is_coro_func(plugin.unload) + } + } + } + ) + if self.plugins[pl_name]["load"]['async']: + plugin.log.debug(f"I'm async") + await plugin.load() + else: + plugin.log.debug(f"I'm sync") + th = Thread(target=plugin.load, name=f"{pl_name}.load()") + th.start() + th.join() + self.loaded.append((pl_name, True)) + self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}") + return True + except Exception as e: + self.loaded.append((file, False)) + self.log.error(i18n.plugins_error_loading.format(file, f"{e}")) + self.log.exception(e) + return False + async def load(self): self.log.debug("Loading plugins...") for file in os.listdir(self.plugins_dir): - file_path = os.path.join(self.plugins_dir, file) - if os.path.isfile(file_path) and file.endswith(".py"): - try: - self.log.debug(f"Loading plugin: {file[:-3]}") - plugin = types.ModuleType(file[:-3]) - plugin.KuiToi = KuiToi - plugin.KuiToi._plugins_dir = self.plugins_dir - plugin.print = print - plugin.__file__ = file_path - with open(f'{file_path}', 'r', encoding=config.enc) as f: - code = f.read() - exec(code, plugin.__dict__) + await self._load_by_file(file) - ok = True - try: - is_func = inspect.isfunction - if not is_func(plugin.load): - self.log.error(i18n.plugins_not_found_load) - ok = False - if not is_func(plugin.start): - self.log.error(i18n.plugins_not_found_start) - ok = False - if not is_func(plugin.unload): - self.log.error(i18n.plugins_not_found_unload) - ok = False - if type(plugin.kt) != KuiToi: - self.log.error(i18n.plugins_kt_invalid) - ok = False - except AttributeError: - ok = False - if not ok: - self.log.error(i18n.plugins_invalid.format(file_path)) - return - - pl_name = plugin.kt.name - if self.plugins.get(pl_name) is not None: - raise NameError(f'Having plugins with identical names is not allowed; ' - f'Plugin name: "{pl_name}"; Plugin file "{file_path}"') - - plugin.open = plugin.kt.open - is_coro_func = inspect.iscoroutinefunction - self.plugins.update( - { - pl_name: { - "plugin": plugin, - "load": { - "func": plugin.load, - "async": is_coro_func(plugin.load) - }, - "start": { - "func": plugin.start, - "async": is_coro_func(plugin.start) - }, - "unload": { - "func": plugin.unload, - "async": is_coro_func(plugin.unload) - } - } - } - ) - if self.plugins[pl_name]["load"]['async']: - plugin.log.debug(f"I'm async") - await plugin.load() - else: - plugin.log.debug(f"I'm sync") - th = Thread(target=plugin.load, name=f"{pl_name}.load()") - th.start() - th.join() - self.loaded_str += f"{pl_name}:ok, " - self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}") - except Exception as e: - self.loaded_str += f"{file}:no, " - self.log.error(i18n.plugins_error_loading.format(file, f"{e}")) - self.log.exception(e) + async def _unload_by_name(self, name, reload=False): + t1 = time.monotonic() + data = self.plugins.get(name) + if not data: + return False, name, None, None + try: + if reload: + data['plugin'].kt._unload() + self.loaded.remove((name, True)) + self.plugins.pop(name) + if data['unload']['async']: + self.log.debug(f"Unload async plugin: {name}") + await data['unload']['func']() + else: + self.log.debug(f"Unload sync plugin: {name}") + th = Thread(target=data['unload']['func'], name=f"Thread {name}") + th.start() + th.join() + except Exception as e: + self.log.exception(e) + return True, name, data['plugin'].kt._file, time.monotonic() - t1 async def start(self, _): for pl_name, pl_data in self.plugins.items(): @@ -225,15 +291,9 @@ class PluginsLoader: self.log.exception(e) async def unload(self, _): - for pl_name, pl_data in self.plugins.items(): - try: - if pl_data['unload']['async']: - self.log.debug(f"Unload async plugin: {pl_name}") - await pl_data['unload']['func']() - else: - self.log.debug(f"Unload sync plugin: {pl_name}") - th = Thread(target=pl_data['unload']['func'], name=f"Thread {pl_name}") - th.start() - th.join() - except Exception as e: - self.log.exception(e) + t = [] + for n in self.plugins.keys(): + await asyncio.sleep(0.01) + t.append(self._unload_by_name(n)) + self.log.debug(await asyncio.gather(*t)) + self.log.debug("Plugins unloaded")