实现了渠道插件的全生命周期管理能力,包括: 1. 插件清单的加载、解析与存储 2. 依赖校验与拓扑排序安装/卸载 3. 插件发现与安全沙箱执行环境 4. 插件注册中心与目录管理能力
132 lines
4.6 KiB
Python
132 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections import defaultdict, deque
|
|
from typing import Any
|
|
|
|
from .manifest import ChannelPluginManifest
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DependencyGraph:
|
|
"""渠道插件依赖图 — 检测循环依赖并计算拓扑排序安装顺序。"""
|
|
|
|
def __init__(self):
|
|
self._nodes: dict[str, ChannelPluginManifest] = {}
|
|
self._edges: dict[str, set[str]] = defaultdict(set)
|
|
self._reverse_edges: dict[str, set[str]] = defaultdict(set)
|
|
|
|
def add(self, manifest: ChannelPluginManifest) -> None:
|
|
self._nodes[manifest.id] = manifest
|
|
for dep in manifest.dependencies:
|
|
dep_id = dep if isinstance(dep, str) else dep.get("id", "")
|
|
if dep_id:
|
|
self._edges[manifest.id].add(dep_id)
|
|
self._reverse_edges[dep_id].add(manifest.id)
|
|
|
|
def remove(self, plugin_id: str) -> None:
|
|
if plugin_id not in self._nodes:
|
|
return
|
|
del self._nodes[plugin_id]
|
|
for dep_id in list(self._edges.get(plugin_id, [])):
|
|
self._reverse_edges[dep_id].discard(plugin_id)
|
|
self._edges.pop(plugin_id, None)
|
|
for other_id, deps in list(self._edges.items()):
|
|
if plugin_id in deps:
|
|
deps.discard(plugin_id)
|
|
self._reverse_edges[plugin_id].discard(other_id)
|
|
|
|
def get_dependencies(self, plugin_id: str) -> set[str]:
|
|
return set(self._edges.get(plugin_id, []))
|
|
|
|
def get_dependents(self, plugin_id: str) -> set[str]:
|
|
return set(self._reverse_edges.get(plugin_id, []))
|
|
|
|
def has_cycle(self) -> tuple[bool, list[str]]:
|
|
"""检测是否存在循环依赖,返回 (是否有环, 环中的节点列表)。"""
|
|
visited: set[str] = set()
|
|
rec_stack: set[str] = set()
|
|
cycle: list[str] = []
|
|
|
|
def dfs(node: str, path: list[str]) -> bool:
|
|
visited.add(node)
|
|
rec_stack.add(node)
|
|
path.append(node)
|
|
for neighbor in self._edges.get(node, set()):
|
|
if neighbor not in visited:
|
|
if dfs(neighbor, path):
|
|
return True
|
|
elif neighbor in rec_stack:
|
|
cycle_start = path.index(neighbor)
|
|
cycle.extend(path[cycle_start:])
|
|
return True
|
|
path.pop()
|
|
rec_stack.remove(node)
|
|
return False
|
|
|
|
for node in self._nodes:
|
|
if node not in visited:
|
|
if dfs(node, []):
|
|
return True, cycle
|
|
return False, []
|
|
|
|
def topological_sort(self) -> list[str]:
|
|
"""返回拓扑排序后的插件 ID 列表。如果存在循环依赖,抛出 ValueError。"""
|
|
has_cycle, cycle = self.has_cycle()
|
|
if has_cycle:
|
|
raise ValueError(f"Circular dependency detected: {' -> '.join(cycle)}")
|
|
|
|
in_degree = {node: 0 for node in self._nodes}
|
|
for node, deps in self._edges.items():
|
|
for dep in deps:
|
|
if dep in in_degree:
|
|
in_degree[node] += 1
|
|
|
|
queue = deque(node for node, degree in in_degree.items() if degree == 0)
|
|
result = []
|
|
|
|
while queue:
|
|
node = queue.popleft()
|
|
result.append(node)
|
|
for dependent in self._reverse_edges.get(node, set()):
|
|
if dependent in in_degree:
|
|
in_degree[dependent] -= 1
|
|
if in_degree[dependent] == 0:
|
|
queue.append(dependent)
|
|
|
|
if len(result) != len(self._nodes):
|
|
raise ValueError("Dependency graph has unresolved dependencies")
|
|
|
|
return result
|
|
|
|
def install_order(self) -> list[str]:
|
|
"""获取安装顺序(依赖优先)。"""
|
|
return self.topological_sort()
|
|
|
|
def uninstall_order(self) -> list[str]:
|
|
"""获取卸载顺序(反向拓扑排序,被依赖者优先)。"""
|
|
return list(reversed(self.topological_sort()))
|
|
|
|
def get_subgraph(self, plugin_id: str) -> "DependencyGraph":
|
|
"""获取包含指定插件及其所有依赖的子图。"""
|
|
subgraph = DependencyGraph()
|
|
visited: set[str] = set()
|
|
|
|
def visit(node: str) -> None:
|
|
if node in visited or node not in self._nodes:
|
|
return
|
|
visited.add(node)
|
|
subgraph.add(self._nodes[node])
|
|
for dep in self._edges.get(node, set()):
|
|
visit(dep)
|
|
|
|
visit(plugin_id)
|
|
return subgraph
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"nodes": list(self._nodes.keys()),
|
|
"edges": {k: list(v) for k, v in self._edges.items()},
|
|
}
|