fix(mcp_frame): 修复 c++_dll 工具大量注册失败与写库被文档锁拦截
- 版本校验:基座侧改用编译期常量 (ARX<<16)|SUB_ARX,不再调用运行时的 acrxGetApiVersion()。该函数由 rxapi.lib 的 acrxGetApiVersionImpl 提供, 是有状态实现(读写进程内全局),返回值随调用次数变化;AutoCAD 加载 ARX 时已调用过一次,导致基座值与 DLL 值不等,绝大多数 DLL 工具被拒绝注册。 - 新增 DLL 模块缓存:同一 DLL 全进程只 LoadLibrary 一次,后续工具复用句柄 并跳过版本校验。 - 文档锁:mcp_Tool_DllProxy::Execute 统一 lockDocument/unlockDocument, 修复 DLL 工具在 AutoCAD 应用上下文写数据库返回 eLockViolation (表现为 openWriteSpace 拿不到模型空间)的问题。 - 代理类不再 FreeLibrary 模块(改由模块缓存统一卸载),避免同一 DLL 的多个 工具重复释放、引用计数被打穿。 - 诊断日志精简为仅记录 arx_version/dll_version,且每次加载清空重写。 - 新增 verify_annotate.py:批注工具集运行时验证脚本(绘制/校验/改属性/清理)。
This commit is contained in:
1 parent
0bd290c96e
commit
ec5f62670d
5 files changed
+327
-58
No files matched your search
@@ -5,12 +5,52 @@
|
||||
#include "mcp_utils.h"
|
||||
#include "ghc/filesystem.hpp"
|
||||
#include "rxregsvc.h"
|
||||
#include <fstream>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
#include <cwctype>
|
||||
|
||||
namespace
|
||||
{
|
||||
// 已加载的插件 DLL 缓存:规范化路径 -> 模块句柄。
|
||||
// 同一个 DLL 只 LoadLibrary 一次;后续工具直接复用句柄,并跳过版本校验。
|
||||
std::map<std::wstring, HMODULE> g_loadedDllModules;
|
||||
|
||||
// 统一卸载缓存中的所有 DLL。必须在所有 mcp_Tool_DllProxy 销毁之后再调用,
|
||||
// 否则 DLL 已卸载而代理仍持有其工具对象指针,析构时会崩溃。
|
||||
void unloadAllDllModules()
|
||||
{
|
||||
for (auto &kv : g_loadedDllModules)
|
||||
{
|
||||
if (kv.second) { FreeLibrary(kv.second); }
|
||||
}
|
||||
g_loadedDllModules.clear();
|
||||
}
|
||||
|
||||
// 诊断日志:仅记录版本校验时的两个数值(排查版本不一致问题)
|
||||
// 每次重新加载工具时清空一次,便于只观察本次的结果。
|
||||
void mcpLoadReset()
|
||||
{
|
||||
std::wstring lp = (mcp_utils::getModelDirPath() / L"mcp_load.log").wstring();
|
||||
std::ofstream ofs(lp.c_str(), std::ios::trunc);
|
||||
}
|
||||
|
||||
void mcpLoadLog(const std::string &msg)
|
||||
{
|
||||
std::wstring lp = (mcp_utils::getModelDirPath() / L"mcp_load.log").wstring();
|
||||
std::ofstream ofs(lp.c_str(), std::ios::app);
|
||||
if (ofs.is_open()) { ofs << msg << "\n"; }
|
||||
}
|
||||
}
|
||||
|
||||
#pragma region mcp_ConfigParser
|
||||
bool mcp_ConfigParser::LoadAndRegister(const std::wstring &jsonFilePath, mcp_Tools ®istry)
|
||||
{
|
||||
mcpLoadReset();
|
||||
|
||||
std::ifstream ifs(jsonFilePath);
|
||||
if (!ifs.is_open()) return false;
|
||||
if (!ifs.is_open()) { return false; }
|
||||
|
||||
nlohmann::json config;
|
||||
try { ifs >> config; }
|
||||
@@ -51,7 +91,6 @@ bool mcp_ConfigParser::LoadAndRegister(const std::wstring &jsonFilePath, mcp_Too
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" int acrxGetApiVersion();
|
||||
void mcp_ConfigParser::LoadDllTool(const nlohmann::json &toolConfig, const std::wstring &pluginDir, mcp_Tools ®istry)
|
||||
{
|
||||
fs::path dllPath
|
||||
@@ -65,48 +104,72 @@ void mcp_ConfigParser::LoadDllTool(const nlohmann::json &toolConfig, const std::
|
||||
return;
|
||||
}
|
||||
|
||||
HMODULE hMod = LoadLibrary(dllPath.c_str());
|
||||
|
||||
|
||||
CString verStr;
|
||||
verStr.Format(_T("Major ver:%d, Minor ver:%d"), ARX, SUB_ARX);
|
||||
CString ver_errMsg;
|
||||
ver_errMsg.Format(
|
||||
CString(_T("[MCP Server]Error: Failed to load the DLL. "))
|
||||
+ CString(_T("Please check whether the version of the ObjectARX SDK used by "))
|
||||
+ CString("the DLL matches the current ARX version(%s).\n"), verStr );
|
||||
ver_errMsg.Format(
|
||||
CString(_T("[MCP Server]Error: Failed to load the DLL. "))
|
||||
+ CString(_T("Please check whether the version of the ObjectARX SDK used by "))
|
||||
+ CString("the DLL matches the current ARX version(%s).\n"), verStr);
|
||||
|
||||
if (!hMod)
|
||||
// 规范化 key(绝对路径,统一小写),用于判断该 DLL 是否已经加载过
|
||||
std::wstring key = dllPath.wstring();
|
||||
std::transform(key.begin(), key.end(), key.begin(),
|
||||
[](wchar_t c) { return (wchar_t)::towlower(c); });
|
||||
|
||||
HMODULE hMod = NULL;
|
||||
bool firstLoad = false;
|
||||
auto it = g_loadedDllModules.find(key);
|
||||
if (it != g_loadedDllModules.end())
|
||||
{
|
||||
AfxMessageBox(ver_errMsg);
|
||||
return;
|
||||
// 该 DLL 已加载:直接复用句柄,不再重复 LoadLibrary,也不再做版本校验
|
||||
hMod = it->second;
|
||||
}
|
||||
else
|
||||
{
|
||||
hMod = LoadLibrary(dllPath.c_str());
|
||||
if (!hMod)
|
||||
{
|
||||
AfxMessageBox(ver_errMsg);
|
||||
return;
|
||||
}
|
||||
g_loadedDllModules[key] = hMod;
|
||||
firstLoad = true;
|
||||
}
|
||||
|
||||
// 1. 定义版本校验函数指针
|
||||
typedef int (*GetApiVersionFunc)();
|
||||
GetApiVersionFunc getVerFn = (GetApiVersionFunc)GetProcAddress(hMod, "acrxGetApiVersion");
|
||||
|
||||
// 2. 核心拦截逻辑
|
||||
if (!getVerFn)
|
||||
// 版本校验:仅在该 DLL 首次加载时执行一次
|
||||
if (firstLoad)
|
||||
{
|
||||
// 连这个函数都没导出,说明根本不是合法的 ObjectARX/MCP 插件
|
||||
AfxMessageBox(CString(
|
||||
CString(_T("The ObjectARX version of the DLL could not be determined."))
|
||||
+ CString(_T("Please ensure that the acrxGetApiVersion function is exported."))
|
||||
));
|
||||
FreeLibrary(hMod);
|
||||
return;
|
||||
}
|
||||
typedef int (*GetApiVersionFunc)();
|
||||
GetApiVersionFunc getVerFn = (GetApiVersionFunc)GetProcAddress(hMod, "acrxGetApiVersion");
|
||||
|
||||
// 拿 DLL 编译时的 SDK 版本,和基座当前运行的 SDK 版本进行比对
|
||||
int arx_version(acrxGetApiVersion());
|
||||
int dll_version(getVerFn());
|
||||
if (arx_version != dll_version)
|
||||
{
|
||||
// 平台大版本不匹配,强行加载必定闪崩,果断踢掉!
|
||||
FreeLibrary(hMod);
|
||||
AfxMessageBox(ver_errMsg);
|
||||
return;
|
||||
if (!getVerFn)
|
||||
{
|
||||
// 连这个函数都没导出,说明根本不是合法的 ObjectARX/MCP 插件
|
||||
AfxMessageBox(CString(
|
||||
CString(_T("The ObjectARX version of the DLL could not be determined."))
|
||||
+ CString(_T("Please ensure that the acrxGetApiVersion function is exported."))
|
||||
));
|
||||
FreeLibrary(hMod);
|
||||
g_loadedDllModules.erase(key);
|
||||
return;
|
||||
}
|
||||
|
||||
// 基座自身版本取「编译期常量」,它稳定可靠;
|
||||
// 不能依赖运行时的 acrxGetApiVersion()——该函数由 rxapi.lib 中的有状态实现提供,
|
||||
// 其返回值会随调用次数变化(首次为正确值,之后被进程内全局污染),无法用于比对。
|
||||
int arx_version = (ARX << 16) | SUB_ARX;
|
||||
int dll_version = getVerFn();
|
||||
mcpLoadLog("arx_version=" + std::to_string(arx_version) + " dll_version=" + std::to_string(dll_version));
|
||||
|
||||
if (arx_version != dll_version)
|
||||
{
|
||||
// 平台大版本不匹配,强行加载必定闪崩,果断踢掉整个模块!
|
||||
FreeLibrary(hMod);
|
||||
g_loadedDllModules.erase(key);
|
||||
AfxMessageBox(ver_errMsg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
typedef mcp_Tool *(*CreateFunc)();
|
||||
@@ -124,25 +187,18 @@ void mcp_ConfigParser::LoadDllTool(const nlohmann::json &toolConfig, const std::
|
||||
// 给真正的 DLL 实例也注入配置,以防第三方需要在内部读取
|
||||
std::string cfgStr = toolConfig.dump();
|
||||
pRealTool->SetConfigAbiSafe(cfgStr.c_str());
|
||||
//pRealTool->SetConfig(toolConfig);
|
||||
|
||||
// 实例化代理类,接管生命周期
|
||||
auto pProxy = std::make_shared<mcp_Tool_DllProxy>(hMod, pRealTool, destroyFn);
|
||||
// 实例化代理类,接管工具对象的生命周期(模块句柄由 g_loadedDllModules 统一管理)
|
||||
auto pProxy = std::make_shared<mcp_Tool_DllProxy>(pRealTool, destroyFn);
|
||||
|
||||
// 代理类继承自基类,也需要配置来处理 GetName 和 ToLLM
|
||||
pProxy->SetConfig(toolConfig);
|
||||
|
||||
registry.Register(pProxy);
|
||||
}
|
||||
else
|
||||
{
|
||||
FreeLibrary(hMod);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FreeLibrary(hMod); // 导出函数找不到,直接卸载
|
||||
// 工厂返回空:只跳过本工具,不卸载模块(同一个 DLL 的其它工具可能仍然可用)
|
||||
}
|
||||
// 工厂/销毁函数缺失:同样只跳过本工具,不卸载模块
|
||||
}
|
||||
|
||||
#pragma endregion mcp_ConfigParser
|
||||
@@ -191,7 +247,8 @@ void mcp_Tools::print()
|
||||
|
||||
void mcp_Tools::clear()
|
||||
{
|
||||
m_tools.clear();
|
||||
m_tools.clear(); // 先销毁所有代理(代理不再 FreeLibrary,仅释放 DLL 内部工具对象)
|
||||
unloadAllDllModules(); // 所有代理销毁完毕后,再统一卸载缓存的 DLL 模块
|
||||
}
|
||||
#pragma endregion mcp_Tools
|
||||
|
||||
|
||||
@@ -89,27 +89,38 @@ nlohmann::json mcp_Tool_LispFile::Execute(const nlohmann::json &args)
|
||||
#pragma endregion LispFile
|
||||
|
||||
#pragma region DllProxy
|
||||
// RAII:基座销毁工具时,自动通知 DLL 释放内存,并卸载模块
|
||||
// RAII:基座销毁工具时,通知 DLL 释放工具对象(模块句柄由模块缓存统一管理)
|
||||
mcp_Tool_DllProxy::~mcp_Tool_DllProxy()
|
||||
{
|
||||
if (m_pRealTool && m_destroyFunc)
|
||||
{
|
||||
m_destroyFunc(m_pRealTool);
|
||||
}
|
||||
if (m_hModule)
|
||||
{
|
||||
FreeLibrary(m_hModule);
|
||||
}
|
||||
// DLL 模块由 mcp_ConfigParser 的模块缓存统一卸载(mcp_Tools::clear 中),此处不再 FreeLibrary
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
std::string argStr = args.dump();
|
||||
//return m_pRealTool->Execute(args); // 完美转发
|
||||
const char* retStr = m_pRealTool->ExecuteAbiSafe(argStr.c_str());
|
||||
return nlohmann::json::parse(retStr);
|
||||
nlohmann::json result = nlohmann::json::parse(retStr);
|
||||
|
||||
if (locked)
|
||||
{
|
||||
acDocManager->unlockDocument(pDoc);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return mcp_Tool_Utility::make_error(-32603, L"Underlying DLL tool instance is invalid");
|
||||
}
|
||||
|
||||
@@ -24,17 +24,18 @@ public:
|
||||
class mcp_Tool_DllProxy : public mcp_Tool
|
||||
{
|
||||
private:
|
||||
HMODULE m_hModule;
|
||||
mcp_Tool* m_pRealTool; // 真正由第三方 DLL 创建的工具对象
|
||||
|
||||
typedef void (*DestroyFunc)(mcp_Tool*);
|
||||
DestroyFunc m_destroyFunc;
|
||||
|
||||
public:
|
||||
mcp_Tool_DllProxy(HMODULE hMod, mcp_Tool* pReal, DestroyFunc pDestroy)
|
||||
: m_hModule(hMod), m_pRealTool(pReal), m_destroyFunc(pDestroy) {}
|
||||
mcp_Tool_DllProxy(mcp_Tool* pReal, DestroyFunc pDestroy)
|
||||
: m_pRealTool(pReal), m_destroyFunc(pDestroy) {}
|
||||
|
||||
// RAII:基座销毁工具时,自动通知 DLL 释放内存,并卸载模块
|
||||
// RAII:基座销毁工具时,通知 DLL 释放工具对象。
|
||||
// 注意:DLL 模块句柄由 mcp_ConfigParser 的模块缓存统一管理(同一 DLL 只加载一次),
|
||||
// 这里不再 FreeLibrary,否则多个工具会重复释放同一模块导致引用计数被打穿。
|
||||
virtual ~mcp_Tool_DllProxy();
|
||||
|
||||
virtual nlohmann::json Execute(const nlohmann::json &args) override;
|
||||
|
||||
Reference in new issue
Block a user