Skip to content

Commit

Permalink
✨ plugin.lifecycle service
Browse files Browse the repository at this point in the history
  • Loading branch information
RF-Tar-Railt committed Oct 28, 2024
1 parent 28fe12e commit bddc086
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 39 deletions.
37 changes: 26 additions & 11 deletions arclet/entari/plugin/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -159,13 +165,19 @@ 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):
plugin_service._unloaded.add(self.id)
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)
Expand All @@ -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()
Expand Down
115 changes: 87 additions & 28 deletions arclet/entari/plugin/service.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,80 @@
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

if TYPE_CHECKING:
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"]]
Expand All @@ -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()

0 comments on commit bddc086

Please sign in to comment.