fix(plugin): close cancellable install lifecycle gaps

This commit is contained in:
InfinityPacer
2026-08-23 14:26:42 +08:00
parent 0378eabb8f
commit 6c2817e383
13 changed files with 240 additions and 47 deletions
+25
View File
@@ -188,9 +188,11 @@ class SystemUtils:
grace_seconds: float = 5,
) -> None:
"""终止安装子进程及其同组子进程,并确保句柄已回收。"""
process_tree = SystemUtils._process_tree(process.pid)
if process.returncode is None:
try:
if os.name == "nt":
SystemUtils._signal_process_tree(process_tree)
process.terminate()
else:
os.killpg(process.pid, signal.SIGTERM)
@@ -205,9 +207,11 @@ class SystemUtils:
except (asyncio.TimeoutError, asyncio.CancelledError):
try:
if os.name == "nt":
SystemUtils._signal_process_tree(process_tree, force=True)
process.kill()
else:
os.killpg(process.pid, signal.SIGKILL)
SystemUtils._signal_process_tree(process_tree, force=True)
except (ProcessLookupError, OSError):
pass
try:
@@ -224,6 +228,27 @@ class SystemUtils:
except (ProcessLookupError, OSError):
pass
@staticmethod
def _process_tree(pid: int) -> list[psutil.Process]:
"""收集安装进程及其后代,避免构建进程继续写运行环境。"""
try:
parent = psutil.Process(pid)
return [parent, *parent.children(recursive=True)]
except (psutil.Error, OSError):
return []
@staticmethod
def _signal_process_tree(
processes: list[psutil.Process],
force: bool = False,
) -> None:
"""向进程树发送终止或强制结束信号。"""
for child in reversed(processes):
try:
(child.kill if force else child.terminate)()
except (psutil.Error, OSError):
continue
@staticmethod
async def execute_with_subprocess_async(
command: list,
+5 -3
View File
@@ -101,16 +101,18 @@ class PluginPackageManager:
@staticmethod
def rollback(checkpoint: PluginPackageCheckpoint) -> None:
"""删除当前包并把变更前文件快照恢复到运行目录。"""
if checkpoint.plugin_dir.exists():
shutil.rmtree(checkpoint.plugin_dir)
snapshot_dir = checkpoint.transaction_dir / "package"
if checkpoint.existed:
if not snapshot_dir.is_dir():
raise FileNotFoundError(
f"插件 {checkpoint.plugin_id} 的补偿快照不存在:{snapshot_dir}"
)
if checkpoint.plugin_dir.exists():
shutil.rmtree(checkpoint.plugin_dir)
if checkpoint.existed:
shutil.copytree(snapshot_dir, checkpoint.plugin_dir)
shutil.rmtree(checkpoint.transaction_dir, ignore_errors=False)
if checkpoint.transaction_dir.exists():
shutil.rmtree(checkpoint.transaction_dir, ignore_errors=False)
async def async_rollback(self, checkpoint: PluginPackageCheckpoint) -> None:
"""在线程池中恢复插件包文件快照。"""