mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
fix(agent): await stdio mcp reader shutdown
This commit is contained in:
+5
-2
@@ -217,8 +217,11 @@ class _StdioMcpSession:
|
|||||||
|
|
||||||
async def __aexit__(self, exc_type, exc, tb) -> None:
|
async def __aexit__(self, exc_type, exc, tb) -> None:
|
||||||
"""结束 stdio MCP 子进程。"""
|
"""结束 stdio MCP 子进程。"""
|
||||||
if self.stderr_task:
|
stderr_task = self.stderr_task
|
||||||
self.stderr_task.cancel()
|
self.stderr_task = None
|
||||||
|
if stderr_task:
|
||||||
|
stderr_task.cancel()
|
||||||
|
await asyncio.gather(stderr_task, return_exceptions=True)
|
||||||
if not self.process:
|
if not self.process:
|
||||||
return
|
return
|
||||||
if self.process.returncode is None:
|
if self.process.returncode is None:
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ Event Contract Registry 是 53 个事件的逐项机器清单。下表按相同
|
|||||||
- 已登记的周期 Agent task:E1,重启时通过任务定义重建;单次执行要有 execution 记录。
|
- 已登记的周期 Agent task:E1,重启时通过任务定义重建;单次执行要有 execution 记录。
|
||||||
- Agent 创建/修改订阅、删除数据等工具:业务事务按 E2/E3;聊天输出不能替代业务完成证据。
|
- Agent 创建/修改订阅、删除数据等工具:业务事务按 E2/E3;聊天输出不能替代业务完成证据。
|
||||||
- 会话 stop/cancel:E0 控制信号;被取消工具的底层阻塞 I/O 可能继续,资源所有者必须最终回收。
|
- 会话 stop/cancel:E0 控制信号;被取消工具的底层阻塞 I/O 可能继续,资源所有者必须最终回收。
|
||||||
|
- OpenAI/Anthropic 协议流的请求级 Agent worker 由 `api.openai.stream` /
|
||||||
|
`api.anthropic.stream` 登记并在 lifespan shutdown 时取消;它们仍是 E0 请求交付,不提供跨重启恢复。
|
||||||
|
- stdio MCP 的 stderr reader 属于会话资源内部任务;会话退出时先取消并等待 reader 收口,再终止子进程,避免
|
||||||
|
资源已释放而 reader 仍悬挂。
|
||||||
|
|
||||||
### Transfer pending / 文件整理
|
### Transfer pending / 文件整理
|
||||||
|
|
||||||
|
|||||||
+29
-1
@@ -1,10 +1,11 @@
|
|||||||
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.agent.mcp import AgentMcpManager, AgentMcpToolSpec
|
from app.agent.mcp import AgentMcpManager, AgentMcpToolSpec, _StdioMcpSession
|
||||||
from app.agent.tools.catalog import ToolCatalogSnapshot
|
from app.agent.tools.catalog import ToolCatalogSnapshot
|
||||||
from app.agent.tools.impl.mcp import (
|
from app.agent.tools.impl.mcp import (
|
||||||
McpExternalTool,
|
McpExternalTool,
|
||||||
@@ -86,6 +87,33 @@ async def test_stdio_mcp_server_lists_tools(tmp_path):
|
|||||||
assert tools[0].input_schema["properties"]["text"]["type"] == "string"
|
assert tools[0].input_schema["properties"]["text"]["type"] == "string"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_stdio_mcp_session_waits_for_stderr_reader_shutdown() -> None:
|
||||||
|
"""stdio MCP 会话退出时必须等待 stderr reader 真正完成。"""
|
||||||
|
session = _StdioMcpSession(
|
||||||
|
AgentMcpServerConfig(id="fake", name="Fake MCP", transport="stdio")
|
||||||
|
)
|
||||||
|
started = asyncio.Event()
|
||||||
|
cancelled = asyncio.Event()
|
||||||
|
|
||||||
|
async def reader() -> None:
|
||||||
|
"""模拟持续读取 stderr 的会话子任务。"""
|
||||||
|
started.set()
|
||||||
|
try:
|
||||||
|
await asyncio.Event().wait()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
cancelled.set()
|
||||||
|
raise
|
||||||
|
|
||||||
|
session.stderr_task = asyncio.create_task(reader())
|
||||||
|
await started.wait()
|
||||||
|
|
||||||
|
await session.__aexit__(None, None, None)
|
||||||
|
|
||||||
|
assert session.stderr_task is None
|
||||||
|
assert cancelled.is_set()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_enabled_specs_preserve_cross_server_name_collisions() -> None:
|
async def test_enabled_specs_preserve_cross_server_name_collisions() -> None:
|
||||||
"""跨 MCP 服务器生成相同 Agent 名时必须把全部身份交给目录判断。"""
|
"""跨 MCP 服务器生成相同 Agent 名时必须把全部身份交给目录判断。"""
|
||||||
|
|||||||
Reference in New Issue
Block a user