refactor(runtime): activate managed resources on demand (#6334)

This commit is contained in:
InfinityPacer
2026-08-16 16:44:45 +08:00
committed by GitHub
parent 7e851dbfa7
commit b8b59ae20a
27 changed files with 3068 additions and 116 deletions
+196
View File
@@ -0,0 +1,196 @@
"""从旧插件源码导入中识别必须提前就绪的宿主资源。"""
from __future__ import annotations
import ast
import threading
import tokenize
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, FrozenSet, Iterable, Set, Tuple
@dataclass(frozen=True, slots=True)
class ResourceImportRule:
"""描述第三方模块导入与宿主资源能力之间的静态映射。"""
capability_id: str # 导入前必须准备的宿主能力标识
module_prefixes: tuple[str, ...] # 按完整包边界匹配的第三方模块前缀
headed_entrypoints: tuple[str, ...] # 已确认允许 headed 模式的公开入口
# 旧插件可能绕过宿主浏览器门面直接调用 CloakBrowser。其六个 launch
# 入口均允许 headed 模式,因此导入该包或任意子模块时保守准备虚拟显示。
RESOURCE_IMPORT_RULES: tuple[ResourceImportRule, ...] = (
ResourceImportRule(
capability_id="host.display",
module_prefixes=("cloakbrowser",),
headed_entrypoints=(
"launch",
"launch_async",
"launch_context",
"launch_context_async",
"launch_persistent_context",
"launch_persistent_context_async",
),
),
)
_scan_cache_lock = threading.RLock()
_scan_cache: Dict[Path, Tuple[int, int, int, int, int, FrozenSet[str]]] = {}
class PluginResourceImportScanError(RuntimeError):
"""表示单个插件源码无法生成可靠的精确资源集合。"""
def _all_resource_capabilities() -> FrozenSet[str]:
"""扫描不完整时返回全部已登记资源,避免漏失导入前置条件。"""
return frozenset(rule.capability_id for rule in RESOURCE_IMPORT_RULES)
def _matches_module(module_name: str, module_prefixes: Iterable[str]) -> bool:
"""按完整包边界匹配模块,避免相似名称产生误报。"""
return any(
module_name == prefix or module_name.startswith(f"{prefix}.")
for prefix in module_prefixes
)
def _dynamic_import_aliases(tree: ast.AST) -> tuple[Set[str], Set[str]]:
"""收集 importlib 模块及 import_module 函数的本地别名。"""
module_aliases = {"importlib"}
function_aliases: Set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for imported in node.names:
if imported.name == "importlib":
module_aliases.add(imported.asname or imported.name)
elif isinstance(node, ast.ImportFrom) and node.module == "importlib":
for imported in node.names:
if imported.name == "import_module":
function_aliases.add(imported.asname or imported.name)
return module_aliases, function_aliases
def _constant_dynamic_import(
node: ast.Call,
*,
importlib_aliases: Set[str],
import_module_aliases: Set[str],
) -> str | None:
"""提取受支持动态导入调用中的常量模块名。"""
if not node.args:
return None
is_import_call = isinstance(node.func, ast.Name) and (
node.func.id == "__import__" or node.func.id in import_module_aliases
)
if (
isinstance(node.func, ast.Attribute)
and isinstance(node.func.value, ast.Name)
and node.func.value.id in importlib_aliases
and node.func.attr == "import_module"
):
is_import_call = True
if not is_import_call:
return None
argument = node.args[0]
if isinstance(argument, ast.Constant) and isinstance(argument.value, str):
return argument.value
return None
def _imported_modules(tree: ast.AST) -> FrozenSet[str]:
"""提取静态导入以及可确定目标的动态导入模块名。"""
modules: Set[str] = set()
importlib_aliases, import_module_aliases = _dynamic_import_aliases(tree)
for node in ast.walk(tree):
if isinstance(node, ast.Import):
modules.update(imported.name for imported in node.names)
elif isinstance(node, ast.ImportFrom) and node.module:
modules.add(node.module)
elif isinstance(node, ast.Call):
module_name = _constant_dynamic_import(
node,
importlib_aliases=importlib_aliases,
import_module_aliases=import_module_aliases,
)
if module_name:
modules.add(module_name)
return frozenset(modules)
def _scan_source(plugin_id: str, path: Path) -> FrozenSet[str]:
"""读取并解析单个源码文件;不完整结果不能进入插件导入阶段。"""
try:
before_stat = path.stat()
# 热加载工具可能保留 mtime,等长替换也不会改变 sizectime 与 inode/device
# 一并参与身份判断,避免把已替换源码误认为旧缓存。
cache_key = (
before_stat.st_mtime_ns,
before_stat.st_ctime_ns,
before_stat.st_size,
before_stat.st_dev,
before_stat.st_ino,
)
with _scan_cache_lock:
cached = _scan_cache.get(path)
if cached and cached[:5] == cache_key:
return cached[5]
with tokenize.open(path) as source_file:
source = source_file.read()
tree = ast.parse(source, filename=str(path))
after_stat = path.stat()
except (OSError, SyntaxError, UnicodeError) as error:
raise PluginResourceImportScanError(
f"无法扫描插件 {plugin_id} 源码 {path.name}{error}"
) from error
after_key = (
after_stat.st_mtime_ns,
after_stat.st_ctime_ns,
after_stat.st_size,
after_stat.st_dev,
after_stat.st_ino,
)
if cache_key != after_key:
raise PluginResourceImportScanError(
f"扫描插件 {plugin_id} 时源码 {path.name} 发生变化"
)
capabilities: Set[str] = set()
for module_name in _imported_modules(tree):
for rule in RESOURCE_IMPORT_RULES:
if _matches_module(module_name, rule.module_prefixes):
capabilities.add(rule.capability_id)
result = frozenset(capabilities)
with _scan_cache_lock:
_scan_cache[path] = (*cache_key, result)
return result
def scan_plugin_resource_imports(
plugin_id: str,
plugin_dir: Path,
) -> tuple[str, ...]:
"""递归扫描插件源码并返回导入前必须准备的 capability ID。"""
if not plugin_dir.is_dir():
raise PluginResourceImportScanError(
f"插件 {plugin_id} 源码目录不存在:{plugin_dir}"
)
capabilities: Set[str] = set()
try:
source_files = sorted(plugin_dir.rglob("*.py"))
except OSError:
return tuple(sorted(_all_resource_capabilities()))
for path in source_files:
if "__pycache__" in path.parts:
continue
try:
capabilities.update(_scan_source(plugin_id, path))
except PluginResourceImportScanError:
# Python 最终只会导入真实依赖链;无法解析的残留或平台专用文件不应
# 阻断整个插件,但必须按最保守资源集合准备后再交给 loader 判断。
capabilities.update(_all_resource_capabilities())
return tuple(sorted(capabilities))
@@ -0,0 +1,194 @@
"""Managed Resource 的声明发现与 Capability Runtime 适配器。"""
from __future__ import annotations
import asyncio
import importlib
import inspect
from pathlib import Path
from typing import Any, Iterable
from app.runtime.capabilities.errors import CapabilityAdapterContractError
from app.runtime.capabilities.model import (
ActivationPolicy,
AdapterExecutionMode,
CapabilitySpec,
)
from app.runtime.capabilities.registry import CapabilityRegistry
from app.runtime.managed_resources import (
MANAGED_RESOURCE_ASYNC_KIND,
MANAGED_RESOURCE_SYNC_KIND,
)
_DEFAULT_RESOURCE_ROOT = Path(__file__).resolve().parents[2] / "adapters"
_RESOURCE_KINDS = {MANAGED_RESOURCE_SYNC_KIND, MANAGED_RESOURCE_ASYNC_KIND}
def _load_entrypoint(spec: CapabilitySpec) -> Any:
"""解析声明中的 canonical 实现对象,不创建资源实例。"""
module_name, symbol_name = spec.entrypoint.split(":", maxsplit=1)
module = importlib.import_module(module_name)
try:
return getattr(module, symbol_name)
except AttributeError as error:
raise CapabilityAdapterContractError(
f"{spec.entrypoint} 未公开 Managed Resource 实现"
) from error
def _create_candidate(spec: CapabilitySpec, implementation: Any) -> Any:
"""通过零参数工厂创建资源候选,实例在 start 成功前不可见。"""
if not callable(implementation):
raise CapabilityAdapterContractError(
f"{spec.entrypoint} 不是可调用的 Managed Resource 工厂"
)
candidate = implementation()
if candidate is None or inspect.isawaitable(candidate):
close = getattr(candidate, "close", None)
if callable(close):
close()
raise CapabilityAdapterContractError(
f"{spec.entrypoint} 必须同步返回资源候选"
)
return candidate
def _resource_method(spec: CapabilitySpec, candidate: Any, name: str) -> Any:
"""读取必需生命周期方法并生成稳定合同错误。"""
callback = getattr(candidate, name, None)
if not callable(callback):
raise CapabilityAdapterContractError(
f"{spec.entrypoint} 的资源候选缺少 {name}()"
)
return callback
class SyncManagedResourceAdapter:
"""把同步 start/stop 资源接入 Capability Runtime。"""
execution_mode = AdapterExecutionMode.SYNC
@staticmethod
def materialize(spec: CapabilitySpec) -> Any:
"""解析资源工厂。"""
return _load_entrypoint(spec)
@staticmethod
def create(
spec: CapabilitySpec,
implementation: Any,
_generation: int,
_previous: Any = None,
) -> Any:
"""创建尚未发布的同步资源候选。"""
return _create_candidate(spec, implementation)
@staticmethod
def start(spec: CapabilitySpec, candidate: Any, _generation: int) -> None:
"""启动同步候选;同步 kind 不接受 awaitable 返回值。"""
result = _resource_method(spec, candidate, "start")()
if inspect.isawaitable(result):
close = getattr(result, "close", None)
if callable(close):
close()
raise CapabilityAdapterContractError(
f"{spec.entrypoint}.start() 返回 awaitable,与同步 kind 不匹配"
)
@staticmethod
def stop(spec: CapabilitySpec, instance: Any, _generation: int) -> None:
"""停止同步资源,异常交由 Runtime 保留资源所有权并支持重试。"""
result = _resource_method(spec, instance, "stop")()
if inspect.isawaitable(result):
close = getattr(result, "close", None)
if callable(close):
close()
raise CapabilityAdapterContractError(
f"{spec.entrypoint}.stop() 返回 awaitable,与同步 kind 不匹配"
)
@staticmethod
def cleanup(
spec: CapabilitySpec,
candidate: Any,
generation: int,
_error: BaseException,
) -> None:
"""启动失败时按同一 stop 合同清理尚未发布的候选。"""
SyncManagedResourceAdapter.stop(spec, candidate, generation)
class AsyncManagedResourceAdapter:
"""把异步 start/stop 资源接入 Capability Runtime。"""
execution_mode = AdapterExecutionMode.ASYNC
@staticmethod
async def materialize(spec: CapabilitySpec) -> Any:
"""在线程中解析资源工厂,避免第三方导入阻塞事件循环。"""
return await asyncio.to_thread(_load_entrypoint, spec)
@staticmethod
async def create(
spec: CapabilitySpec,
implementation: Any,
_generation: int,
_previous: Any = None,
) -> Any:
"""创建尚未发布的异步资源候选。"""
return _create_candidate(spec, implementation)
@staticmethod
async def start(spec: CapabilitySpec, candidate: Any, _generation: int) -> None:
"""等待异步候选完成启动。"""
result = _resource_method(spec, candidate, "start")()
if not inspect.isawaitable(result):
raise CapabilityAdapterContractError(
f"{spec.entrypoint}.start() 必须返回 awaitable"
)
await result
@staticmethod
async def stop(spec: CapabilitySpec, instance: Any, _generation: int) -> None:
"""等待异步资源完成停止。"""
result = _resource_method(spec, instance, "stop")()
if not inspect.isawaitable(result):
raise CapabilityAdapterContractError(
f"{spec.entrypoint}.stop() 必须返回 awaitable"
)
await result
@staticmethod
async def cleanup(
spec: CapabilitySpec,
candidate: Any,
generation: int,
_error: BaseException,
) -> None:
"""启动失败时等待同一 stop 合同清理候选。"""
await AsyncManagedResourceAdapter.stop(spec, candidate, generation)
def _validate_registry(registry: CapabilityRegistry) -> None:
"""固定类别级声明合同,资源只能由显式首用触发。"""
for spec in registry.list_specs():
if set(spec.metadata) != {"name"}:
raise ValueError(f"{spec.source}: Managed Resource metadata 只能包含 name")
if spec.activation is not ActivationPolicy.ON_FIRST_USE:
raise ValueError(f"{spec.source}: Managed Resource 必须使用 on_first_use")
if spec.selector is not None or spec.watch:
raise ValueError(f"{spec.source}: Managed Resource 不接受配置 selector 或 watch")
def build_managed_resource_registry(
roots: Iterable[Path | str] | None = None,
) -> CapabilityRegistry:
"""从 data-only manifest 构建不导入资源实现的注册表。"""
registry = CapabilityRegistry.discover(
tuple(roots) if roots is not None else (_DEFAULT_RESOURCE_ROOT,),
kinds=_RESOURCE_KINDS,
selector_schemas={},
)
_validate_registry(registry)
return registry
+23
View File
@@ -37,6 +37,7 @@ from app.schemas.types import EventType, SystemConfigKey
LegacyDiagnosticsConfigurator = Callable[..., None]
LegacyImportScanner = Callable[..., None]
LegacyPluginImportPreparer = Callable[..., None]
PluginInstallReporter = Callable[..., None]
SiteAuthLevelProvider = Callable[[], int]
@@ -45,6 +46,10 @@ def _ignore_legacy_diagnostics(**_kwargs) -> None:
"""在启动组合根尚未注入兼容服务时保持插件加载可用。"""
def _ignore_plugin_resource_imports(**_kwargs) -> None:
"""未进入应用启动组合时不主动创建进程级宿主资源。"""
def _unavailable_site_auth_level() -> int:
"""站点能力尚未装配时返回未认证等级。"""
return 0
@@ -54,6 +59,9 @@ _legacy_diagnostics_configurator: LegacyDiagnosticsConfigurator = (
_ignore_legacy_diagnostics
)
_legacy_import_scanner: LegacyImportScanner = _ignore_legacy_diagnostics
_legacy_plugin_import_preparer: LegacyPluginImportPreparer = (
_ignore_plugin_resource_imports
)
_plugin_install_reporter: PluginInstallReporter = _ignore_legacy_diagnostics
_site_auth_level_provider: SiteAuthLevelProvider = _unavailable_site_auth_level
@@ -69,6 +77,14 @@ def configure_plugin_legacy_import_services(
_legacy_import_scanner = import_scanner
def configure_plugin_resource_import_preparer(
preparer: LegacyPluginImportPreparer,
) -> None:
"""注入旧插件导入前的宿主资源准备器。"""
global _legacy_plugin_import_preparer
_legacy_plugin_import_preparer = preparer
def configure_plugin_install_reporter(reporter: PluginInstallReporter) -> None:
"""由启动组合根注入插件安装上报器,避免扩展层依赖远程服务。"""
global _plugin_install_reporter
@@ -318,6 +334,13 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
module_name = f"app.plugins.{plugin_dir.name}"
logger.debug(f"正在导入插件模块:{module_name}")
# 旧插件可能直接导入带宿主资源前置条件的第三方包。资源必须在
# Python 执行插件模块顶层代码前就绪,否则导入副作用无法安全回滚。
_legacy_plugin_import_preparer(
plugin_id=plugin_dir.name,
plugin_dir=plugin_dir,
)
_legacy_import_scanner(
plugin_id=plugin_dir.name,
plugin_dir=plugin_dir,
+182
View File
@@ -0,0 +1,182 @@
"""进程级托管资源的轻量调用门面。"""
from __future__ import annotations
import asyncio
import threading
from typing import Any, Optional, Protocol
MANAGED_RESOURCE_SYNC_KIND = "managed_resource.sync"
MANAGED_RESOURCE_ASYNC_KIND = "managed_resource.async"
class ManagedResourceRuntime(Protocol):
"""Managed Resource 门面依赖的最小 Capability Runtime 合同。"""
@property
def is_shutdown(self) -> bool:
"""返回 Runtime 是否已进入不可逆关闭态。"""
def get_spec(self, capability_id: str) -> Any:
"""返回资源声明。"""
def get_running(self, capability_id: str) -> Any:
"""只查询已发布实例。"""
def snapshot(self, capability_id: str) -> Any:
"""返回资源状态快照。"""
def observations(self, capability_id: Optional[str] = None) -> tuple[Any, ...]:
"""返回资源转换观测。"""
def activate(self, capability_id: str, *, reason: str, retry: bool = False) -> Any:
"""通过同步 adapter 激活资源。"""
async def activate_async(
self,
capability_id: str,
*,
reason: str,
retry: bool = False,
) -> Any:
"""通过异步 adapter 激活资源。"""
def stop(self, capability_id: str, *, reason: str) -> None:
"""通过同步 adapter 停止资源。"""
async def stop_async(self, capability_id: str, *, reason: str) -> None:
"""通过异步 adapter 停止资源。"""
async def shutdown_async(self, *, reason: str) -> None:
"""关闭混合同步和异步 adapter 的 Runtime。"""
_runtime_lock = threading.RLock()
_managed_resource_runtime: Optional[ManagedResourceRuntime] = None
def configure_managed_resource_runtime(runtime: ManagedResourceRuntime) -> None:
"""由启动组合层注入唯一的 Managed Resource Runtime。"""
if runtime is None:
raise ValueError("Managed Resource Runtime 不能为空")
global _managed_resource_runtime
with _runtime_lock:
_managed_resource_runtime = runtime
def _runtime(*, required: bool) -> Optional[ManagedResourceRuntime]:
"""读取当前 Runtime;资源使用路径要求启动组合已经完成装配。"""
with _runtime_lock:
runtime = _managed_resource_runtime
if runtime is None and required:
raise RuntimeError("Managed Resource Runtime 尚未初始化")
return runtime
def _resource_kind(runtime: ManagedResourceRuntime, capability_id: str) -> str:
"""返回声明的执行模式;未知资源继续沿用 Runtime 的领域错误。"""
spec = runtime.get_spec(capability_id)
if spec is None:
runtime.get_running(capability_id)
raise RuntimeError(f"未知 Managed Resource{capability_id}")
return str(spec.kind)
def acquire_managed_resource(
capability_id: str,
*,
reason: str,
retry: bool = True,
) -> Any:
"""同步激活一个声明为同步模式的托管资源。"""
runtime = _runtime(required=True)
kind = _resource_kind(runtime, capability_id)
if kind != MANAGED_RESOURCE_SYNC_KIND:
raise RuntimeError(f"异步 Managed Resource 不能通过同步入口激活:{capability_id}")
return runtime.activate(capability_id, reason=reason, retry=retry)
async def acquire_managed_resource_async(
capability_id: str,
*,
reason: str,
retry: bool = True,
) -> Any:
"""异步激活资源;同步资源移交工作线程,避免阻塞事件循环。"""
runtime = _runtime(required=True)
kind = _resource_kind(runtime, capability_id)
if kind == MANAGED_RESOURCE_ASYNC_KIND:
return await runtime.activate_async(
capability_id,
reason=reason,
retry=retry,
)
if kind == MANAGED_RESOURCE_SYNC_KIND:
return await asyncio.to_thread(
runtime.activate,
capability_id,
reason=reason,
retry=retry,
)
raise RuntimeError(f"未知 Managed Resource kind{kind}")
def get_running_managed_resource(capability_id: str) -> Any:
"""只查询已发布资源;Runtime 未配置时返回 None,不触发初始化。"""
runtime = _runtime(required=False)
if runtime is None:
return None
return runtime.get_running(capability_id)
def managed_resource_snapshot(capability_id: str) -> Any:
"""返回资源状态快照;Runtime 未配置时返回 None。"""
runtime = _runtime(required=False)
if runtime is None:
return None
return runtime.snapshot(capability_id)
def managed_resource_observations(
capability_id: Optional[str] = None,
) -> tuple[Any, ...]:
"""返回资源转换观测;Runtime 未配置时返回空快照。"""
runtime = _runtime(required=False)
if runtime is None:
return ()
return runtime.observations(capability_id)
def stop_managed_resource(capability_id: str, *, reason: str) -> None:
"""同步停止资源;Runtime 未配置时保持幂等且不反向初始化。"""
runtime = _runtime(required=False)
if runtime is None:
return
kind = _resource_kind(runtime, capability_id)
if kind != MANAGED_RESOURCE_SYNC_KIND:
raise RuntimeError(f"异步 Managed Resource 不能通过同步入口停止:{capability_id}")
runtime.stop(capability_id, reason=reason)
async def stop_managed_resource_async(capability_id: str, *, reason: str) -> None:
"""异步停止资源;同步资源移交工作线程。"""
runtime = _runtime(required=False)
if runtime is None:
return
kind = _resource_kind(runtime, capability_id)
if kind == MANAGED_RESOURCE_ASYNC_KIND:
await runtime.stop_async(capability_id, reason=reason)
return
if kind == MANAGED_RESOURCE_SYNC_KIND:
await asyncio.to_thread(runtime.stop, capability_id, reason=reason)
return
raise RuntimeError(f"未知 Managed Resource kind{kind}")
async def shutdown_managed_resource_runtime(*, reason: str) -> None:
"""关闭已配置 Runtime;未配置时直接返回,绝不因关闭而创建资源。"""
runtime = _runtime(required=False)
if runtime is None:
return
await runtime.shutdown_async(reason=reason)