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:
1 parent
5d83d3fa76
commit
48229c28b0
4 files changed
+317
-32
No files matched your search
@@ -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 协议错误。
|
||||
|
||||
@@ -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,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
|
||||
|
||||
Reference in new issue
Block a user