fix(frame): 工具执行加异常保护,畸形参数不再崩溃 CAD / 泄漏写锁

cad_mcp_frame 的工具执行发生在 AutoCAD 主线程的窗口消息处理链上
(TcpServerLoop -> PostMessage -> WndProc -> ExecuteTaskInMainThread -> mcp_Tools::Call)。
插件 Execute 在参数类型不符时会抛 nlohmann::type_error(如 args.value("bulge", 0.5)
收到字符串、parsePoint 对 ["a","b"] 取 get<double>())。原实现全程无 try/catch,
异常逃逸出 WndProc 属未定义行为(实测表现为 CAD 崩溃),且会让 DLL 代理的文档写锁
无法释放。LLM 驱动的审图流程会生成这类错误参数,属日常会踩到的路径。

- mcp_ConfigParser.cpp: mcp_Tools::Call 用 try/catch 包住 Execute,
  std::exception -> make_error(-32603)(上层映射为 result.isError=true),catch(...) 兜底。
- mcp_server.cpp: HandleToolsCall 入口校验 params/arguments 必须是对象、name 必须是
  字符串,否则回 -32602(协议错误),从源头避免 value()/get<>() 抛异常;
  ExecuteTaskInMainThread 增加兜底 try/catch,并补 !req.is_object() -> -32600。
- mcp_tool.cpp: mcp_Tool_DllProxy::Execute 的文档写锁改为 RAII 守卫
  (ScopedDocumentWriteLock),异常展开时同样解锁,避免写锁泄漏。

验证(关 CAD -> 重建 -> 重启后实测):
- 新增 verify_mcp_fault_injection.py 22/22 PASS:
  A 结构性错误(params/arguments/name 类型错、非对象请求、非法 JSON)分别回
  -32602/-32600/-32700;B 工具内部类型异常被兜为 result.isError=true;
  C 反复注入 10 次后 CAD 存活、写锁未泄漏(有效写操作仍成功)。
- 回归:verify_mcp_frame 4/4、verify_stdio_bridge 24/24、verify_annotate 9/9、
  dwg_review --check 正常。
This commit is contained in:
gjm committed 2026-10-14 15:40:20 +08:00
1 parent 5d83d3fa76
commit 48229c28b0
4 files changed
+317 -32

No files matched your search

+18 -1
View File
@@ -10,6 +10,7 @@
#include <map>
#include <algorithm>
#include <cwctype>
#include <exception>
namespace
{
@@ -252,7 +253,23 @@ nlohmann::json mcp_Tools::Call(const std::string &name, const nlohmann::json &ar
auto it = m_tools.find(name);
if (it != m_tools.end())
{
return it->second->Execute(args);
// 工具由插件 DLL 实现,可能因参数类型不符等原因抛异常(如 nlohmann::type_error)。
// 本函数运行在 AutoCAD 主线程的窗口消息处理链上,异常若逃逸出 WndProc 属未定义行为
// (实际表现为 CAD 崩溃),且会让 DLL 代理的文档写锁无法释放。故在此统一兜住,
// 转换为「工具执行错误」(-32603),由上层映射成 result.isError = true。
try
{
return it->second->Execute(args);
}
catch (const std::exception &e)
{
return mcp_Tool_Utility::make_error(-32603,
L"Tool execution failed: " + mcp_utils::utf8ToWString(std::string(e.what())));
}
catch (...)
{
return mcp_Tool_Utility::make_error(-32603, L"Tool execution failed: unknown exception");
}
}
// 找不到工具时,返回标准的 JSON-RPC 协议错误。
+59 -11
View File
@@ -4,6 +4,7 @@
#include <ws2tcpip.h>
#include <thread>
#include <atomic>
#include <exception>
#include <nlohmann/json.hpp>
#include "mcp_ConfigParser.h"
#include "mcp_utils.h"
@@ -62,9 +63,25 @@ namespace
resp["jsonrpc"] = "2.0";
resp["id"] = msgId;
// 参数结构校验:本函数运行在 AutoCAD 主线程的 WndProc 调用链上,nlohmann 的
// value()/get<>() 在类型不符时会抛异常,异常逃逸出 WndProc 会导致 CAD 崩溃。
// 故在入口处把结构性问题统一挡下,按 MCP 规范回 -32602(协议错误)。
auto invalidParams = [&resp](const char *message) {
resp["error"] = { { "code", -32602 }, { "message", message } };
return resp.dump();
};
if (!params.is_object())
return invalidParams("Invalid params: params must be an object");
if (params.contains("name") && !params["name"].is_string())
return invalidParams("Invalid params: name must be a string");
std::string toolName = params.value("name", "");
nlohmann::json args = params.value("arguments", nlohmann::json::object());
if (!args.is_object())
return invalidParams("Invalid params: arguments must be an object");
// 让工具集去执行,返回 payload 可能是 {"result": ...} 也可能是 {"error": ...}
nlohmann::json payload = g_mcpTools.Call(toolName, args);
@@ -115,29 +132,60 @@ namespace
// 非法 JSON 报文
responseStr = R"({"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null})";
}
else if (!req.is_object())
{
// 合法 JSON 但不是对象(数组/标量):非法请求
nlohmann::json err;
err["jsonrpc"] = "2.0";
err["id"] = nullptr;
err["error"] = { {"code", -32600}, {"message", "Invalid Request"} };
responseStr = err.dump();
}
else
{
std::string method = req.value("method", "");
// id 允许 string 或 number;缺失 id 表示这是一条"通知",按规范不应产生任何响应
bool isNotification = !req.contains("id");
nlohmann::json idVal = isNotification ? nlohmann::json(nullptr) : req["id"];
if (method == "tools/list")
// 兜底:本函数由 WndProc 直接调用,任何异常都不允许逃逸(逃逸属未定义行为,
// 实际表现为 CAD 崩溃)。工具自身的异常已在 mcp_Tools::Call 内转为 isError,
// 这里再兜住参数解析等更外层的问题。
try
{
responseStr = HandleToolsList(idVal);
std::string method = req.value("method", "");
if (method == "tools/list")
{
responseStr = HandleToolsList(idVal);
}
else if (method == "tools/call")
{
responseStr = HandleToolsCall(idVal, req.value("params", nlohmann::json::object()));
}
else
{
// 找不到方法
nlohmann::json err;
err["jsonrpc"] = "2.0";
err["id"] = idVal;
err["error"] = { {"code", -32601}, {"message", "Method not found"} };
responseStr = err.dump();
}
}
else if (method == "tools/call")
catch (const std::exception &e)
{
responseStr = HandleToolsCall(idVal, req.value("params", nlohmann::json::object()));
}
else
{
// 找不到方法
nlohmann::json err;
err["jsonrpc"] = "2.0";
err["id"] = idVal;
err["error"] = { {"code", -32601}, {"message", "Method not found"} };
err["error"] = { {"code", -32603}, {"message", std::string("Internal error: ") + e.what()} };
responseStr = err.dump();
}
catch (...)
{
nlohmann::json err;
err["jsonrpc"] = "2.0";
err["id"] = idVal;
err["error"] = { {"code", -32603}, {"message", "Internal error: unknown exception"} };
responseStr = err.dump();
}
+37 -20
View File
@@ -37,6 +37,32 @@ namespace
outScript = mcp_utils::utf8ToWString(tpl);
return true;
}
// 文档写锁的 RAII 守卫:插件 Execute 可能抛异常,异常展开时也必须解锁,
// 否则文档会停留在 kWrite 锁定状态,导致后续所有写操作失败。
class ScopedDocumentWriteLock
{
public:
ScopedDocumentWriteLock()
{
m_pDoc = (acDocManager != nullptr) ? acDocManager->curDocument() : nullptr;
if (m_pDoc != nullptr && acDocManager->lockDocument(m_pDoc, AcAp::kWrite) == Acad::eOk)
m_locked = true;
}
~ScopedDocumentWriteLock()
{
if (m_locked && m_pDoc != nullptr)
acDocManager->unlockDocument(m_pDoc);
}
ScopedDocumentWriteLock(const ScopedDocumentWriteLock &) = delete;
ScopedDocumentWriteLock &operator=(const ScopedDocumentWriteLock &) = delete;
private:
AcApDocument *m_pDoc = nullptr;
bool m_locked = false;
};
}
#pragma region LispInline
@@ -101,28 +127,19 @@ mcp_Tool_DllProxy::~mcp_Tool_DllProxy()
nlohmann::json mcp_Tool_DllProxy::Execute(const nlohmann::json &args)
{
if (m_pRealTool)
{
// DLL 工具通常会写数据库(创建/修改实体、图层等)。基座运行在 AutoCAD 主线程的
// 窗口消息处理中,此时处于「应用上下文」,直接对数据库做 kForWrite 打开会返回
// eLockViolation(表现为 openWriteSpace 拿不到模型空间)。因此统一在此加文档写锁,
// 工具执行完立即解锁。只读工具被锁也无副作用。
AcApDocument* pDoc = acDocManager ? acDocManager->curDocument() : nullptr;
const bool locked = (pDoc != nullptr &&
acDocManager->lockDocument(pDoc, AcAp::kWrite) == Acad::eOk);
if (!m_pRealTool)
return mcp_Tool_Utility::make_error(-32603, L"Underlying DLL tool instance is invalid");
std::string argStr = args.dump();
//return m_pRealTool->Execute(args); // 完美转发
const char* retStr = m_pRealTool->ExecuteAbiSafe(argStr.c_str());
nlohmann::json result = nlohmann::json::parse(retStr);
// DLL 工具通常会写数据库(创建/修改实体、图层等)。基座运行在 AutoCAD 主线程的
// 窗口消息处理中,此时处于「应用上下文」,直接对数据库做 kForWrite 打开会返回
// eLockViolation(表现为 openWriteSpace 拿不到模型空间)。因此统一在此加文档写锁,
// 工具执行完立即解锁。只读工具被锁也无副作用。
// 用 RAII 守卫:插件 Execute 可能抛异常,异常展开时同样必须解锁,否则写锁会泄漏。
ScopedDocumentWriteLock docWriteLock;
if (locked)
{
acDocManager->unlockDocument(pDoc);
}
return result;
}
return mcp_Tool_Utility::make_error(-32603, L"Underlying DLL tool instance is invalid");
std::string argStr = args.dump();
const char* retStr = m_pRealTool->ExecuteAbiSafe(argStr.c_str());
return nlohmann::json::parse(retStr);
}
#pragma endregion DllProxy