mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-05 14:48:40 +08:00
fix: isolate agent sessions and retain runtime resource ownership (#7149)
* fix: arbitrate subscription identity and restore workflow state * fix: isolate agent sessions and retain runtime resource ownership * test: preserve legacy empty-owner subscription transactions * test: handle reaped terminal descendants during shutdown polling * fix: align shared atomic configuration method with lifecycle prerequisite * test: cover subagent cancellation and completion lifecycle boundaries --------- Co-authored-by: jxxghp <jxxghp@gmail.com>
This commit is contained in:
@@ -5,7 +5,25 @@ Agent 文件写入工具的共享辅助函数。
|
||||
import hashlib
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
_write_locks: WeakValueDictionary[str, threading.Lock] = WeakValueDictionary()
|
||||
_write_locks_guard = threading.Lock()
|
||||
|
||||
|
||||
def _write_lock(path: Path) -> threading.Lock:
|
||||
"""
|
||||
为同一规范目标保留进程内互斥,闲置后自动回收路径锁。
|
||||
"""
|
||||
key = os.path.normcase(str(path.resolve()))
|
||||
with _write_locks_guard:
|
||||
lock = _write_locks.get(key)
|
||||
if lock is None:
|
||||
lock = threading.Lock()
|
||||
_write_locks[key] = lock
|
||||
return lock
|
||||
|
||||
|
||||
class FileVersionConflictError(RuntimeError):
|
||||
@@ -29,9 +47,26 @@ def atomic_write_text(
|
||||
path: Path,
|
||||
content: str,
|
||||
expected_sha256: str | None = None,
|
||||
) -> None:
|
||||
*,
|
||||
overwrite: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
校验目标版本后,在同目录写入临时文件并原子替换文本。
|
||||
在同目标写锁内校验版本并原子替换,返回本次提交内容的SHA256。
|
||||
|
||||
:param path: 已通过权限检查的目标路径
|
||||
:param content: 本次完整文本内容
|
||||
:param expected_sha256: 当前版本摘要;None表示未提供版本前提
|
||||
:param overwrite: False使用原子排他创建,已有目标不得替换
|
||||
:return: 本次实际提交文本的SHA256,不重新读取可能被后续调用修改的文件
|
||||
:raises FileVersionConflictError: 版本变化或排他创建时目标已存在
|
||||
"""
|
||||
with _write_lock(path):
|
||||
return _write_text_locked(path, content, expected_sha256, overwrite=overwrite)
|
||||
|
||||
|
||||
def _write_text_locked(path: Path, content: str, expected_sha256: str | None, *, overwrite: bool) -> str:
|
||||
"""
|
||||
在已持有目标锁时写入临时文件,禁止覆盖使用原子link拒绝已存在目标。
|
||||
"""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
descriptor, temp_name = tempfile.mkstemp(
|
||||
@@ -55,7 +90,14 @@ def atomic_write_text(
|
||||
raise FileVersionConflictError(str(path))
|
||||
if path.exists():
|
||||
os.chmod(temp_path, path.stat().st_mode)
|
||||
os.replace(temp_path, path)
|
||||
if overwrite:
|
||||
os.replace(temp_path, path)
|
||||
else:
|
||||
try:
|
||||
os.link(temp_path, path)
|
||||
except FileExistsError as error:
|
||||
raise FileVersionConflictError(str(path)) from error
|
||||
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
||||
finally:
|
||||
if temp_path.exists():
|
||||
temp_path.unlink()
|
||||
|
||||
@@ -348,11 +348,9 @@ class ApplyPatchTool(MoviePilotTool):
|
||||
f"错误:文件 {resolved_path} 在应用补丁期间被创建,拒绝覆盖。"
|
||||
"请确认文件状态后重新应用补丁。"
|
||||
)
|
||||
await self.run_blocking(
|
||||
"default", atomic_write_text, local_path, new_content, sha256
|
||||
)
|
||||
new_sha256 = await self.run_blocking(
|
||||
"default", calculate_file_sha256, local_path
|
||||
"default", atomic_write_text, local_path, new_content, sha256,
|
||||
overwrite=file_patch.operation != "add",
|
||||
)
|
||||
verb = "新增" if file_patch.operation == "add" else "更新"
|
||||
results.append(f"{verb} {resolved_path}(sha256={new_sha256})")
|
||||
|
||||
@@ -151,16 +151,13 @@ class EditFileTool(MoviePilotTool):
|
||||
new_text,
|
||||
-1 if replace_all else 1,
|
||||
)
|
||||
await self.run_blocking(
|
||||
new_sha256 = await self.run_blocking(
|
||||
"default",
|
||||
atomic_write_text,
|
||||
local_path,
|
||||
new_content,
|
||||
current_sha256,
|
||||
)
|
||||
new_sha256 = await self.run_blocking(
|
||||
"default", calculate_file_sha256, local_path
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"成功编辑文件 {resolved_path},替换了 {replacement_count} 处内容"
|
||||
|
||||
@@ -224,7 +224,7 @@ class ViewImageTool(MoviePilotTool):
|
||||
if has_file_path:
|
||||
resolved_path, access_error = await self._check_local_file_access(
|
||||
clean_file_path,
|
||||
operation="读取图片",
|
||||
operation="读取",
|
||||
)
|
||||
if access_error:
|
||||
return self._failure("local_path_access_denied", access_error, source_type)
|
||||
|
||||
@@ -129,15 +129,13 @@ class WriteFileTool(MoviePilotTool):
|
||||
"请重新读取文件并基于最新内容写入。"
|
||||
)
|
||||
|
||||
await self.run_blocking(
|
||||
new_sha256 = await self.run_blocking(
|
||||
"default",
|
||||
atomic_write_text,
|
||||
local_path,
|
||||
content,
|
||||
current_sha256,
|
||||
)
|
||||
new_sha256 = await self.run_blocking(
|
||||
"default", calculate_file_sha256, local_path
|
||||
overwrite=overwrite,
|
||||
)
|
||||
|
||||
logger.info(f"成功写入文件 {resolved_path}")
|
||||
|
||||
Reference in New Issue
Block a user