mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-06 07:09:09 +08:00
refactor(plugin): share runtime dependency owners
This commit is contained in:
65
app/adapters/external/market.py
vendored
65
app/adapters/external/market.py
vendored
@@ -47,7 +47,46 @@ _installed_plugins_provider: InstalledPluginsProvider = _empty_installed_plugins
|
||||
_plugin_install_gateway: PluginInstallGateway = _unconfigured_plugin_install_gateway
|
||||
_async_plugin_install_gateway: AsyncPluginInstallGateway = _unconfigured_async_plugin_install_gateway
|
||||
_plugin_market_transport = PluginMarketTransport()
|
||||
_plugin_runtime_health = PluginRuntimeHealth()
|
||||
_plugin_runtime_health: Optional[PluginRuntimeHealth] = None
|
||||
_plugin_dependency_installer: Optional[PluginDependencyInstaller] = None
|
||||
|
||||
|
||||
def configure_plugin_runtime_owners(
|
||||
*,
|
||||
health: PluginRuntimeHealth,
|
||||
dependency: PluginDependencyInstaller,
|
||||
) -> None:
|
||||
"""发布组合根构造的插件运行健康与依赖安装唯一 owner。"""
|
||||
global _plugin_runtime_health, _plugin_dependency_installer
|
||||
_plugin_runtime_health = health
|
||||
_plugin_dependency_installer = dependency
|
||||
|
||||
|
||||
def reset_plugin_runtime_owners() -> None:
|
||||
"""撤销当前 lifespan 发布的插件运行健康与依赖安装 owner。"""
|
||||
global _plugin_runtime_health, _plugin_dependency_installer
|
||||
_plugin_runtime_health = None
|
||||
_plugin_dependency_installer = None
|
||||
|
||||
|
||||
def _plugin_runtime_health_owner() -> PluginRuntimeHealth:
|
||||
"""返回已发布 owner;旧入口提前调用时惰性保留兼容行为。"""
|
||||
global _plugin_runtime_health
|
||||
if _plugin_runtime_health is None:
|
||||
_plugin_runtime_health = PluginRuntimeHealth()
|
||||
return _plugin_runtime_health
|
||||
|
||||
|
||||
def _plugin_dependency_owner() -> PluginDependencyInstaller:
|
||||
"""返回已发布 owner;旧入口提前调用时只构造一份惰性实例。"""
|
||||
global _plugin_dependency_installer
|
||||
if _plugin_dependency_installer is None:
|
||||
_plugin_dependency_installer = PluginDependencyInstaller(
|
||||
_plugin_runtime_health_owner(),
|
||||
installed_plugins_provider=lambda: _installed_plugins_provider(),
|
||||
plugin_dir=PLUGIN_DIR,
|
||||
)
|
||||
return _plugin_dependency_installer
|
||||
|
||||
|
||||
def configure_installed_plugins_provider(provider: InstalledPluginsProvider) -> None:
|
||||
@@ -144,23 +183,17 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
find_links_dirs: Optional[List[Path]] = None,
|
||||
) -> Tuple[bool, str]:
|
||||
"""把兼容入口委托给系统运行环境 owner。"""
|
||||
return _plugin_runtime_health.install_packages_with_fallback(
|
||||
return _plugin_runtime_health_owner().install_packages_with_fallback(
|
||||
dependency_files, find_links_dirs
|
||||
)
|
||||
|
||||
def find_missing_dependencies(self) -> List[str]:
|
||||
"""把兼容入口委托给依赖聚合 owner。"""
|
||||
return PluginDependencyInstaller(
|
||||
_plugin_runtime_health, installed_plugins_provider=_installed_plugins_provider,
|
||||
plugin_dir=PLUGIN_DIR,
|
||||
).find_missing()
|
||||
return _plugin_dependency_owner().find_missing()
|
||||
|
||||
def install_dependencies(self, dependencies: List[str]) -> Tuple[bool, str]:
|
||||
"""把兼容入口委托给依赖聚合 owner。"""
|
||||
return PluginDependencyInstaller(
|
||||
_plugin_runtime_health, installed_plugins_provider=_installed_plugins_provider,
|
||||
plugin_dir=PLUGIN_DIR,
|
||||
).install(dependencies)
|
||||
return _plugin_dependency_owner().install(dependencies)
|
||||
|
||||
@classmethod
|
||||
async def async_install_packages_with_fallback(
|
||||
@@ -168,7 +201,7 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
find_links_dirs: Optional[List[Path]] = None,
|
||||
) -> Tuple[bool, str]:
|
||||
"""把异步兼容入口委托给系统运行环境 owner。"""
|
||||
return await _plugin_runtime_health.async_install_packages_with_fallback(
|
||||
return await _plugin_runtime_health_owner().async_install_packages_with_fallback(
|
||||
dependency_files, find_links_dirs
|
||||
)
|
||||
|
||||
@@ -176,17 +209,11 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
self, dependencies: List[str],
|
||||
) -> Tuple[bool, str]:
|
||||
"""把异步兼容入口委托给依赖聚合 owner。"""
|
||||
return await PluginDependencyInstaller(
|
||||
_plugin_runtime_health, installed_plugins_provider=_installed_plugins_provider,
|
||||
plugin_dir=PLUGIN_DIR,
|
||||
).async_install(dependencies)
|
||||
return await _plugin_dependency_owner().async_install(dependencies)
|
||||
|
||||
async def async_find_missing_dependencies(self) -> List[str]:
|
||||
"""把异步兼容入口委托给依赖聚合 owner。"""
|
||||
return await PluginDependencyInstaller(
|
||||
_plugin_runtime_health, installed_plugins_provider=_installed_plugins_provider,
|
||||
plugin_dir=PLUGIN_DIR,
|
||||
).async_find_missing()
|
||||
return await _plugin_dependency_owner().async_find_missing()
|
||||
|
||||
async def async_install(
|
||||
self, pid: str, repo_url: str, package_version: Optional[str] = None,
|
||||
|
||||
@@ -24,6 +24,7 @@ class PluginMarketComposition:
|
||||
transport: PluginMarketTransport
|
||||
client: PluginMarketClient
|
||||
package: PluginPackageManager
|
||||
health: PluginRuntimeHealth
|
||||
dependency: PluginDependencyInstaller
|
||||
|
||||
|
||||
@@ -40,6 +41,7 @@ def compose_plugin_market(
|
||||
plugin_root = root_path / "app" / "plugins"
|
||||
transport = PluginMarketTransport.get_existing_instance() or PluginMarketTransport()
|
||||
client = PluginMarketClient(transport)
|
||||
health = PluginRuntimeHealth()
|
||||
composition = PluginMarketComposition(
|
||||
transport=transport,
|
||||
client=client,
|
||||
@@ -48,10 +50,11 @@ def compose_plugin_market(
|
||||
plugin_root=plugin_root,
|
||||
),
|
||||
dependency=PluginDependencyInstaller(
|
||||
PluginRuntimeHealth(),
|
||||
health,
|
||||
installed_plugins_provider=installed_plugins_provider,
|
||||
plugin_dir=plugin_root,
|
||||
),
|
||||
health=health,
|
||||
)
|
||||
_market_client = client
|
||||
return composition
|
||||
|
||||
@@ -9,6 +9,8 @@ from app.adapters.external.market import (
|
||||
LOCAL_REPO_PREFIX,
|
||||
configure_installed_plugins_provider,
|
||||
configure_plugin_install_gateway,
|
||||
configure_plugin_runtime_owners,
|
||||
reset_plugin_runtime_owners,
|
||||
)
|
||||
from app.adapters.external.plugin.client import (
|
||||
VERSION_BACKWARD_COMPATIBLE_FLAGS,
|
||||
@@ -190,6 +192,10 @@ def configure_plugin_services() -> None:
|
||||
market_transport = market_composition.transport
|
||||
market_client = market_composition.client
|
||||
package_manager = market_composition.package
|
||||
configure_plugin_runtime_owners(
|
||||
health=market_composition.health,
|
||||
dependency=market_composition.dependency,
|
||||
)
|
||||
plugin_manager = get_plugin_manager()
|
||||
inventory_reader = PluginCandidateInventoryReader(
|
||||
market_loader=market_client.get_plugin_index_result,
|
||||
@@ -775,4 +781,5 @@ def stop_plugins() -> bool:
|
||||
reset_plugin_catalog_query()
|
||||
reset_plugin_release_service()
|
||||
reset_plugin_rating_service()
|
||||
reset_plugin_runtime_owners()
|
||||
reset_plugin_market_composition()
|
||||
|
||||
@@ -227,13 +227,17 @@ re-exported from the package root, SDK or Compat.
|
||||
|
||||
Complexity and concurrency governance covers the complete canonical execution
|
||||
surface rather than only public methods. `scripts/architecture/complexity.py --v2`
|
||||
ratchets private and nested methods, class/file hotspots, and the
|
||||
`app/scheduler/` package; its generated baseline is an evidence ledger, not a
|
||||
permission to add another oversized owner. `scripts/architecture/concurrency.py`
|
||||
enumerates native Thread, Timer, thread/process executors, Process,
|
||||
asyncio task/thread helpers, and executor hand-off calls across host roots. Each
|
||||
fact must have a stable lexical owner and remain in the baseline with no new
|
||||
call site or owner drift. `app/plugins/**`, `app/sdk/**`, and
|
||||
uses complete AST child traversal to ratchet private/dunder/nested methods,
|
||||
class/file hotspots, and the `app/scheduler/` package, including owners nested
|
||||
under `Match`, `TryStar`, and other control-flow nodes. Its generated baseline
|
||||
is an evidence ledger, not permission to add another oversized owner.
|
||||
`scripts/architecture/concurrency.py` resolves canonical imports and aliases for
|
||||
native Thread, Timer, thread/process executors, Process, TaskGroup, event-loop
|
||||
task submission, asyncio task/thread helpers, and executor hand-off calls. It
|
||||
scans the complete `app/**` host tree and aggregates each canonical target by
|
||||
stable lexical owner and call count, so physical line movement is not semantic
|
||||
drift and multiple same-line calls cannot collapse. New owners/count growth and
|
||||
stale baselines after count reduction all block CI. `app/plugins/**`, `app/sdk/**`, and
|
||||
`app/runtime/compat/**` are excluded because their compatibility/runtime
|
||||
contracts are governed by their own exact manifests.
|
||||
|
||||
@@ -1030,6 +1034,7 @@ driven workflow registration.
|
||||
| `app/adapters/system/plugin/package.py` | `PluginPackageManager`, the unique install, checkpoint, backup, restore and physical-delete owner |
|
||||
| `app/adapters/system/plugin/dependency.py` | `PluginDependencyInstaller`, the dependency manifest, missing-package inspection and installation owner |
|
||||
| `app/adapters/system/plugin/health.py` | `PluginRuntimeHealth`, the runtime-environment guard, pre/post-install check and recovery owner |
|
||||
| `app/startup/composition/plugin.py` | Constructs one market transport/client, package manager, runtime-health owner and dependency installer per lifespan; `PluginHelper` compatibility calls consume those same published owners |
|
||||
| `app/runtime/extensions/resource.py` | Data-only managed-resource registry and sync/async lifecycle adapters |
|
||||
| `app/runtime/resources.py` | Lightweight acquisition, state observation and shutdown facade |
|
||||
| `app/foundation/reflection.py` | Generic reflection and Python module discovery |
|
||||
|
||||
@@ -2392,6 +2392,7 @@ def test_plugin_market_construction_is_owned_by_startup_composition():
|
||||
"PluginMarketTransport",
|
||||
"PluginPackageSourceClient",
|
||||
"PluginPackageManager",
|
||||
"PluginRuntimeHealth",
|
||||
"PluginDependencyInstaller",
|
||||
}
|
||||
assert forbidden_names.isdisjoint(initializer_imports)
|
||||
@@ -2417,6 +2418,7 @@ def test_plugin_market_construction_is_owned_by_startup_composition():
|
||||
assert composition_calls.count("PluginMarketTransport") == 1
|
||||
assert composition_calls.count("PluginPackageSourceClient") == 1
|
||||
assert composition_calls.count("PluginPackageManager") == 1
|
||||
assert composition_calls.count("PluginRuntimeHealth") == 1
|
||||
assert composition_calls.count("PluginDependencyInstaller") == 1
|
||||
|
||||
|
||||
@@ -2428,6 +2430,28 @@ def test_plugin_market_transport_is_process_shared_across_compat_and_composition
|
||||
assert PluginMarketTransport.get_existing_instance() is _plugin_market_transport
|
||||
|
||||
|
||||
def test_plugin_runtime_owners_are_shared_with_compat_facade():
|
||||
"""兼容门面必须消费组合根发布的健康检查与依赖安装 owner。"""
|
||||
from app.adapters.external.market import (
|
||||
_plugin_dependency_owner,
|
||||
_plugin_runtime_health_owner,
|
||||
configure_plugin_runtime_owners,
|
||||
reset_plugin_runtime_owners,
|
||||
)
|
||||
|
||||
health = object()
|
||||
dependency = object()
|
||||
configure_plugin_runtime_owners(
|
||||
health=health, # type: ignore[arg-type]
|
||||
dependency=dependency, # type: ignore[arg-type]
|
||||
)
|
||||
try:
|
||||
assert _plugin_runtime_health_owner() is health
|
||||
assert _plugin_dependency_owner() is dependency
|
||||
finally:
|
||||
reset_plugin_runtime_owners()
|
||||
|
||||
|
||||
def test_domain_initializer_delegates_construction_to_composition():
|
||||
"""领域 initializer 只负责生命周期调用,不得直接构造 Adapter 或领域服务。"""
|
||||
initializer_path = APP_ROOT / "startup" / "initializers" / "domain.py"
|
||||
|
||||
@@ -712,36 +712,25 @@ async def test_agent_install_uses_application_gateway(
|
||||
def test_startup_composition_configures_external_helper_gateway(monkeypatch) -> None:
|
||||
"""启动组合根必须装配独立 owner,并向公开 Helper 发布同一 Gateway。"""
|
||||
transport = Mock()
|
||||
source = Mock()
|
||||
market_client = Mock()
|
||||
package_manager = Mock()
|
||||
health = Mock()
|
||||
dependency = Mock()
|
||||
gateway_calls = []
|
||||
application_calls = []
|
||||
transport_consumers = []
|
||||
gateway = Mock()
|
||||
sync_runner = Mock(return_value=(True, "installed"))
|
||||
async_runner = AsyncMock(return_value=(True, "installed"))
|
||||
|
||||
monkeypatch.setattr(
|
||||
plugins_initializer,
|
||||
"PluginMarketTransport",
|
||||
lambda: transport,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
plugins_initializer,
|
||||
"PluginMarketClient",
|
||||
lambda injected: transport_consumers.append(("market", injected)) or Mock(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
plugins_initializer,
|
||||
"PluginPackageSourceClient",
|
||||
lambda injected: transport_consumers.append(("package", injected)) or source,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
plugins_initializer,
|
||||
"PluginPackageManager",
|
||||
lambda **kwargs: (
|
||||
transport_consumers.append(("package-owner", kwargs["source"]))
|
||||
or package_manager
|
||||
"compose_plugin_market",
|
||||
lambda **_kwargs: SimpleNamespace(
|
||||
transport=transport,
|
||||
client=market_client,
|
||||
package=package_manager,
|
||||
health=health,
|
||||
dependency=dependency,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -796,11 +785,6 @@ def test_startup_composition_configures_external_helper_gateway(monkeypatch) ->
|
||||
"_run_plugin_install_async",
|
||||
async_runner,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
plugins_initializer,
|
||||
"PluginDependencyInstaller",
|
||||
lambda *_args, **_kwargs: Mock(),
|
||||
)
|
||||
for name in (
|
||||
"configure_plugin_legacy_import_services",
|
||||
"configure_plugin_resource_import_preparer",
|
||||
@@ -809,6 +793,7 @@ def test_startup_composition_configures_external_helper_gateway(monkeypatch) ->
|
||||
"configure_plugin_catalog_factory",
|
||||
"configure_plugin_runtime_factory",
|
||||
"configure_plugin_route_refresher",
|
||||
"configure_plugin_runtime_owners",
|
||||
"configure_plugin_system",
|
||||
"configure_plugin_storage",
|
||||
):
|
||||
@@ -827,11 +812,6 @@ def test_startup_composition_configures_external_helper_gateway(monkeypatch) ->
|
||||
|
||||
plugins_initializer.configure_plugin_services()
|
||||
|
||||
assert transport_consumers == [
|
||||
("market", transport),
|
||||
("package", transport),
|
||||
("package-owner", source),
|
||||
]
|
||||
assert application_calls == [gateway]
|
||||
assert len(gateway_calls) == 1
|
||||
assert callable(gateway_calls[0]["install"])
|
||||
|
||||
Reference in New Issue
Block a user