mirror of
https://github.com/kuitoi/kuitoi-Server.git
synced 2026-04-24 00:56:36 +00:00
300 lines
11 KiB
Python
300 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Developed by KuiToi Dev
|
|
# File modules.PluginsLoader
|
|
# Written by: SantaSpeen
|
|
# Version 1.0
|
|
# Licence: FPA
|
|
# (c) kuitoi.su 2023
|
|
import asyncio
|
|
import inspect
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import types
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from threading import Thread
|
|
|
|
from core import get_logger
|
|
|
|
|
|
class KuiToi:
|
|
_plugins_dir = ""
|
|
_file = ""
|
|
|
|
def __init__(self, name):
|
|
if not name:
|
|
raise AttributeError("KuiToi: Name is required")
|
|
self.__log = get_logger(f"Plugin | {name}")
|
|
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):
|
|
return self.__log
|
|
|
|
@property
|
|
def name(self):
|
|
return self.__name
|
|
|
|
@property
|
|
def dir(self):
|
|
return self.__dir
|
|
|
|
@contextmanager
|
|
def open(self, file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
|
|
path = self.__dir / file
|
|
if str(self.__dir) in str(file):
|
|
path = file
|
|
self.log.debug(f'Trying to open "{path}" with mode "{mode}"')
|
|
# Really need?
|
|
# if not os.path.exists(path):
|
|
# with open(path, 'x'): ...
|
|
f = None
|
|
try:
|
|
f = open(path, mode, buffering, encoding, errors, newline, closefd, opener)
|
|
yield f
|
|
except Exception as e:
|
|
raise e
|
|
finally:
|
|
if f is not None:
|
|
f.close()
|
|
|
|
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)
|
|
|
|
async def call_async_event(self, event_name, *args, **kwargs):
|
|
self.log.debug(f"Called async event {event_name}")
|
|
return await ev.call_async_event(event_name, *args, **kwargs)
|
|
|
|
def call_lua_event(self, event_name, *args):
|
|
self.log.debug(f"Called lua event {event_name}")
|
|
return ev.call_lua_event(event_name, *args)
|
|
|
|
def get_player(self, pid=None, nick=None, cid=None):
|
|
self.log.debug("Requests get_player")
|
|
return ev.call_event("_get_player", cid=cid or pid, nick=nick)[0]
|
|
|
|
def get_players(self):
|
|
self.log.debug("Requests get_players")
|
|
return self.get_player(-1)
|
|
|
|
def players_counter(self):
|
|
self.log.debug("Requests players_counter")
|
|
return len(self.get_players())
|
|
|
|
def is_player_connected(self, pid=None, nick=None):
|
|
self.log.debug("Requests is_player_connected")
|
|
if pid < 0:
|
|
return False
|
|
return bool(self.get_player(cid=pid, nick=nick))
|
|
|
|
def add_command(self, key, func, man, desc, custom_completer) -> dict:
|
|
self.log.debug("Requests add_command")
|
|
return console.add_command(key, func, man, desc, custom_completer)
|
|
|
|
|
|
class PluginsLoader:
|
|
_pip_dir = str(Path("pip-packets").resolve())
|
|
|
|
def __init__(self, plugins_dir):
|
|
self.loop = asyncio.get_event_loop()
|
|
self.plugins = {}
|
|
self.plugins_tasks = []
|
|
self.plugins_dir = plugins_dir
|
|
self.log = get_logger("PluginsLoader")
|
|
self.loaded = []
|
|
ev.register("_plugins_start", self.start)
|
|
ev.register("_plugins_unload", self.unload)
|
|
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 <name> | load <file.py> | unload <name> | 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:
|
|
try:
|
|
subprocess.check_call(['pip', 'install', *x, '--target', self._pip_dir])
|
|
return "Success"
|
|
except subprocess.CalledProcessError as e:
|
|
self.log.debug(f"error: {e}")
|
|
return f"Failed to install packages"
|
|
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):
|
|
await self._load_by_file(file)
|
|
|
|
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():
|
|
try:
|
|
if pl_data['start']['async']:
|
|
self.log.debug(f"Start async plugin: {pl_name}")
|
|
t = self.loop.create_task(pl_data['start']['func']())
|
|
self.plugins_tasks.append(t)
|
|
else:
|
|
self.log.debug(f"Start sync plugin: {pl_name}")
|
|
th = Thread(target=pl_data['start']['func'], name=f"Thread {pl_name}")
|
|
th.start()
|
|
self.plugins_tasks.append(th)
|
|
except Exception as e:
|
|
self.log.exception(e)
|
|
|
|
async def unload(self, _):
|
|
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")
|