diff --git a/arclet/entari/plugin/model.py b/arclet/entari/plugin/model.py index 2900dfe..8e076ac 100644 --- a/arclet/entari/plugin/model.py +++ b/arclet/entari/plugin/model.py @@ -17,7 +17,7 @@ from loguru import logger from satori.client import Account -from .service import plugin_service +from .service import plugin_service, PluginLifecycleService if TYPE_CHECKING: from ..event import Event @@ -121,15 +121,21 @@ class Plugin: _preparing: list[_Lifespan] = field(init=False, default_factory=list) _cleanup: list[_Lifespan] = field(init=False, default_factory=list) + _running: list[_Lifespan] = field(init=False, default_factory=list) _connected: list[_AccountUpdate] = field(init=False, default_factory=list) _disconnected: list[_AccountUpdate] = field(init=False, default_factory=list) + _lifecycle: PluginLifecycleService | None = field(init=False, default=None) _services: dict[str, Service] = field(init=False, default_factory=dict) def on_prepare(self, func: _Lifespan): self._preparing.append(func) return func + def on_running(self, func: _Lifespan): + self._running.append(func) + return func + def on_cleanup(self, func: _Lifespan): self._cleanup.append(func) return func @@ -159,6 +165,10 @@ def __post_init__(self): plugin_service._keep_values[self.id] = {} if self.id not in plugin_service._referents: plugin_service._referents[self.id] = set() + if self.id not in plugin_service._subplugined: + self._lifecycle = PluginLifecycleService(self.id) + if plugin_service.status.blocking and (self._preparing or self._running or self._cleanup): + it(Launart).add_component(self._lifecycle) finalize(self, self.dispose) def dispose(self): @@ -166,6 +176,8 @@ def dispose(self): if self._is_disposed: return self._is_disposed = True + if self._lifecycle and self._lifecycle.status.prepared: + it(Launart).remove_component(self._lifecycle) for serv in self._services.values(): try: it(Launart).remove_component(serv) @@ -176,16 +188,19 @@ def dispose(self): Path(self.module.__spec__.cached).unlink(missing_ok=True) sys.modules.pop(self.module.__name__, None) delattr(self.module, "__plugin__") - for subplug in self.subplugins: - if subplug not in plugin_service.plugins: - continue - logger.debug(f"disposing sub-plugin {subplug} of {self.id}") - try: - plugin_service.plugins[subplug].dispose() - except Exception as e: - logger.error(f"failed to dispose sub-plugin {subplug} caused by {e!r}") - plugin_service.plugins.pop(subplug, None) - self.subplugins.clear() + if self.subplugins: + subplugs = list(i.removeprefix(self.id)[1:] for i in self.subplugins) + subplugs = (subplugs[:3] + ["..."]) if len(subplugs) > 3 else subplugs + logger.debug(f"disposing sub-plugin {', '.join(subplugs)} of {self.id}") + for subplug in self.subplugins: + if subplug not in plugin_service.plugins: + continue + try: + plugin_service.plugins[subplug].dispose() + except Exception as e: + logger.error(f"failed to dispose sub-plugin {subplug} caused by {e!r}") + plugin_service.plugins.pop(subplug, None) + self.subplugins.clear() for disp in self.dispatchers.values(): disp.dispose() self.dispatchers.clear() diff --git a/arclet/entari/plugin/service.py b/arclet/entari/plugin/service.py index 6e39700..c1b7da7 100644 --- a/arclet/entari/plugin/service.py +++ b/arclet/entari/plugin/service.py @@ -1,7 +1,7 @@ import asyncio from typing import TYPE_CHECKING -from launart import Launart, Service +from launart import Launart, Service, any_completed from launart.status import Phase from loguru import logger @@ -9,8 +9,72 @@ from .model import KeepingVariable, Plugin -class PluginService(Service): - id = "arclet.entari.plugin_service" +class PluginLifecycleService(Service): + @property + def id(self) -> str: + return f"{self.plugin_id}.lifecycle" + + @property + def required(self) -> set[str]: + return {"arclet.entari.plugin.manager"} + + @property + def stages(self) -> set[Phase]: + return {"preparing", "cleanup", "blocking"} + + def __init__(self, plugin_id: str): + super().__init__() + self.plugin_id = plugin_id + + @property + def available(self) -> bool: + return bool(plug := plugin_service.plugins.get(self.plugin_id)) and bool(plug._preparing or plug._running or plug._cleanup) + + @staticmethod + def iter_preparing(plug: "Plugin"): + for func in plug._preparing: + yield func + for subplug in plug.subplugins: + yield from PluginLifecycleService.iter_preparing(plugin_service.plugins[subplug]) + + @staticmethod + def iter_cleanup(plug: "Plugin"): + for func in plug._cleanup: + yield func + for subplug in plug.subplugins: + yield from PluginLifecycleService.iter_cleanup(plugin_service.plugins[subplug]) + + @staticmethod + def iter_running(plug: "Plugin"): + for func in plug._running: + yield func + for subplug in plug.subplugins: + yield from PluginLifecycleService.iter_running(plugin_service.plugins[subplug]) + + async def launch(self, manager: Launart): + plug = plugin_service.plugins[self.plugin_id] + + async with self.stage("preparing"): + await asyncio.gather(*[func() for func in PluginLifecycleService.iter_preparing(plug)], return_exceptions=True) + async with self.stage("blocking"): + sigexit_task = asyncio.create_task(manager.status.wait_for_sigexit()) + running_tasks = [asyncio.create_task(func()) for func in PluginLifecycleService.iter_running(plug)] # type: ignore + done, pending = await any_completed( + sigexit_task, + *running_tasks, + ) + if sigexit_task in done: + for task in pending: + task.cancel() + await task + async with self.stage("cleanup"): + await asyncio.gather(*[func() for func in PluginLifecycleService.iter_cleanup(plug)], return_exceptions=True) + + del plug + + +class PluginManagerService(Service): + id = "arclet.entari.plugin.manager" plugins: dict[str, "Plugin"] _keep_values: dict[str, dict[str, "KeepingVariable"]] @@ -35,37 +99,32 @@ def stages(self) -> set[Phase]: return {"preparing", "cleanup", "blocking"} async def launch(self, manager: Launart): - _preparing = [] - _cleanup = [] for plug in self.plugins.values(): + if plug._lifecycle and plug._lifecycle.available: + manager.add_component(plug._lifecycle) for serv in plug._services.values(): manager.add_component(serv) async with self.stage("preparing"): - for plug in self.plugins.values(): - _preparing.extend([func() for func in plug._preparing]) - await asyncio.gather(*_preparing, return_exceptions=True) + pass async with self.stage("blocking"): await manager.status.wait_for_sigexit() async with self.stage("cleanup"): - for plug in self.plugins.values(): - _cleanup.extend([func() for func in plug._cleanup]) - await asyncio.gather(*_cleanup, return_exceptions=True) - ids = [k for k in self.plugins.keys() if k not in self._subplugined] - for plug_id in ids: - plug = self.plugins[plug_id] - logger.debug(f"disposing plugin {plug.id}") - try: - plug.dispose() - except Exception as e: - logger.error(f"failed to dispose plugin {plug.id} caused by {e!r}") - self.plugins.pop(plug_id, None) - for values in self._keep_values.values(): - for value in values.values(): - value.dispose() - values.clear() - self._keep_values.clear() - - -plugin_service = PluginService() + ids = [k for k in self.plugins.keys() if k not in self._subplugined] + for plug_id in ids: + plug = self.plugins[plug_id] + logger.debug(f"disposing plugin {plug.id}") + try: + plug.dispose() + except Exception as e: + logger.error(f"failed to dispose plugin {plug.id} caused by {e!r}") + self.plugins.pop(plug_id, None) + for values in self._keep_values.values(): + for value in values.values(): + value.dispose() + values.clear() + self._keep_values.clear() + + +plugin_service = PluginManagerService()