Feature: Add exec_command MCP tool
Add a one-shot exec_command MCP tool that runs a command on a remote Windows host and returns stdout plus exit code, reusing the Web terminal link (main-connection COMMAND_SHELL, a shell sub-connection, and a sentinel command line located via rfind to tolerate ConPTY echo). The sentinel marker is embedded in the command line ConPTY echoes back, so it is only treated as hit when it starts a line (preceded by a newline or the buffer start); this keeps the echoed marker from being mistaken for the real sentinel when the echo packet arrives before the output. Execution is gated at Web remote-desktop sensitivity: a read-only mode (McpReadonly, default on, hides the tool) and a command whitelist (McpCmdWhitelist) with built-in read-only prefixes. Shell metacharacters (& | < > ^) are rejected before whitelist matching, and each execution is recorded in the server audit log. Extend the MCP settings dialog with the read-only checkbox and a multi-line whitelist box (commas and newlines both accepted, normalized to a comma-separated list on save), and add English and Traditional Chinese mappings for the new UI and audit-log strings. Co-Authored-By: deepseek-v4-pro
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#include "HostJson.h" // BuildHostJson(单台主机序列化公共函数)
|
||||
#include "context.h" // context 接口
|
||||
#include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员(m_HostList/m_cs/m_ClientMap)+ VERSION_STR
|
||||
#include "LangManager.h" // _TR(审计日志标题语言映射)
|
||||
|
||||
#include <sstream>
|
||||
|
||||
@@ -889,6 +890,59 @@ Json::Value BuildListRegistryOutputSchema() {
|
||||
return schema;
|
||||
}
|
||||
|
||||
// exec_command 的 inputSchema(id 必填、command 必填、timeout_ms 可选)
|
||||
Json::Value BuildExecCommandInputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
|
||||
Json::Value idProp(Json::objectValue);
|
||||
idProp["type"] = "string";
|
||||
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
|
||||
props["id"] = idProp;
|
||||
|
||||
Json::Value cmdProp(Json::objectValue);
|
||||
cmdProp["type"] = "string";
|
||||
cmdProp["description"] = u8"要执行的命令(受只读模式与命令白名单约束,仅允许只读命令前缀)";
|
||||
props["command"] = cmdProp;
|
||||
|
||||
Json::Value timeoutProp(Json::objectValue);
|
||||
timeoutProp["type"] = "integer";
|
||||
timeoutProp["description"] = u8"等待输出完成的超时毫秒数(可选,默认 20000,上限 600000)";
|
||||
props["timeout_ms"] = timeoutProp;
|
||||
|
||||
Json::Value schema(Json::objectValue);
|
||||
schema["type"] = "object";
|
||||
schema["properties"] = props;
|
||||
Json::Value required(Json::arrayValue);
|
||||
required.append("id");
|
||||
required.append("command");
|
||||
schema["required"] = required;
|
||||
return schema;
|
||||
}
|
||||
|
||||
// exec_command 的 outputSchema(stdout 文本 / exit_code)
|
||||
Json::Value BuildExecCommandOutputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
|
||||
Json::Value stdoutProp(Json::objectValue);
|
||||
stdoutProp["type"] = "string";
|
||||
stdoutProp["description"] = u8"命令输出(已剥哨兵与 ANSI 转义)";
|
||||
props["stdout"] = stdoutProp;
|
||||
|
||||
Json::Value exitProp(Json::objectValue);
|
||||
exitProp["type"] = "integer";
|
||||
exitProp["description"] = u8"退出码:0=成功、1=非零退出、-1=未知(进程异常退出)";
|
||||
props["exit_code"] = exitProp;
|
||||
|
||||
Json::Value schema(Json::objectValue);
|
||||
schema["type"] = "object";
|
||||
schema["properties"] = props;
|
||||
Json::Value required(Json::arrayValue);
|
||||
required.append("stdout");
|
||||
required.append("exit_code");
|
||||
schema["required"] = required;
|
||||
return schema;
|
||||
}
|
||||
|
||||
// get_client_log 的 outputSchema(原始日志文本)
|
||||
Json::Value BuildClientLogOutputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
@@ -1221,6 +1275,19 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
tools.append(tool);
|
||||
}
|
||||
|
||||
// 13) exec_command(P3:一次性远程命令,仅 Windows,安全门:只读默认 + 白名单 + 审计)
|
||||
// 只读模式下 tools/list 直接隐藏该工具;即便被绕过,BuildExecCommand 也会再次校验只读。
|
||||
if (!CMcpServer::Instance().IsReadonly()) {
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "exec_command";
|
||||
tool["description"] = u8"在指定在线 Windows 主机上执行一条命令并返回 stdout 与退出码(受只读模式与命令白名单约束,默认仅允许只读命令前缀,见 McpReadonly / McpCmdWhitelist)。";
|
||||
|
||||
tool["inputSchema"] = BuildExecCommandInputSchema();
|
||||
tool["outputSchema"] = BuildExecCommandOutputSchema();
|
||||
|
||||
tools.append(tool);
|
||||
}
|
||||
|
||||
result["tools"] = tools;
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
@@ -1759,6 +1826,187 @@ std::string BuildListRegistry(const Json::Value& id, const Json::Value& args, CM
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// ========== P3:exec_command ==========
|
||||
|
||||
// exec_command 默认白名单:只读命令前缀(逗号分隔,前缀匹配,大小写不敏感)。
|
||||
// 只放查询/只读类命令;写命令(del/copy/move/reg add/sc stop/...)一律不在列。
|
||||
static const char* kDefaultCmdWhitelist =
|
||||
"dir,type,cd,chdir,ver,hostname,whoami,where,tasklist,systeminfo,ipconfig,netstat,"
|
||||
"path,set,find,findstr,reg query,sc query,"
|
||||
"ping,tracert,nslookup,getmac,driverquery,query,gpresult,"
|
||||
"arp -a,route print,schtasks /query";
|
||||
|
||||
// 去首尾空白(空格/制表符/回车/换行)。
|
||||
static std::string Trim(const std::string& s) {
|
||||
size_t b = s.find_first_not_of(" \t\r\n");
|
||||
if (b == std::string::npos) return "";
|
||||
return s.substr(b, s.find_last_not_of(" \t\r\n") - b + 1);
|
||||
}
|
||||
|
||||
// 去尾部空白(含尾部 CR/LF)。
|
||||
static std::string TrimRight(const std::string& s) {
|
||||
size_t e = s.find_last_not_of(" \t\r\n");
|
||||
return (e == std::string::npos) ? "" : s.substr(0, e + 1);
|
||||
}
|
||||
|
||||
// 命令白名单校验:命令(trim + ASCII 小写)以某白名单项为前缀,且其后字符为空/空格/制表符,
|
||||
// 防止 "dirx" 前缀误配 "dir"。多词前缀(sc query / reg query)也按完整前缀匹配。
|
||||
static bool IsCommandAllowed(const std::string& command, const std::string& whitelist) {
|
||||
std::string c = ToLowerAscii(Trim(command));
|
||||
if (c.empty()) return false;
|
||||
size_t start = 0;
|
||||
while (start < whitelist.size()) {
|
||||
size_t comma = whitelist.find(',', start);
|
||||
std::string item = Trim(whitelist.substr(
|
||||
start, comma == std::string::npos ? std::string::npos : comma - start));
|
||||
if (!item.empty()) {
|
||||
std::string p = ToLowerAscii(item);
|
||||
if (c.size() >= p.size() && c.compare(0, p.size(), p) == 0 &&
|
||||
(c.size() == p.size() || c[p.size()] == ' ' || c[p.size()] == '\t')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (comma == std::string::npos) break;
|
||||
start = comma + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 剥 ANSI 转义序列(CSI/OSC/单字节 ESC),避免终端彩色码污染返回文本。
|
||||
static std::string StripAnsi(const std::string& s) {
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (size_t i = 0; i < s.size(); ++i) {
|
||||
if (s[i] != '\x1b') { out += s[i]; continue; }
|
||||
++i; // 吞 ESC
|
||||
if (i < s.size() && s[i] == '[') { // CSI ... 结尾 @–~
|
||||
while (++i < s.size() && !(s[i] >= '@' && s[i] <= '~')) {}
|
||||
} else if (i < s.size() && s[i] == ']') { // OSC ... 结尾 BEL/ESC
|
||||
while (++i < s.size() && s[i] != '\x07' && s[i] != '\x1b') {}
|
||||
}
|
||||
// 其它单字节 ESC 序列吞掉当前字节即可(循环 ++i 会跳过它)
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// tools/call:exec_command(Windows 一次性远程命令:主连接 COMMAND_SHELL → 子连接终端 →
|
||||
// 哨兵命令行 → 收集 stdout + exit_code → 关子链接)。安全门:只读默认 + 白名单 + 审计。
|
||||
std::string BuildExecCommand(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||||
uint64_t devId = 0;
|
||||
std::string err;
|
||||
if (!ParseHostIdArg(args, devId, err))
|
||||
return BuildError(id, -32602, err);
|
||||
|
||||
context* ctx = FindMainContext(parent, devId);
|
||||
if (!ctx)
|
||||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||||
|
||||
// 终端仅 Windows 客户端实现(ConPTY / 老 cmd 管道);LNX/MAC 远期再考虑。
|
||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
if (clientType == "LNX" || clientType == "MAC")
|
||||
return BuildError(id, -32005, "exec_command is only supported on Windows hosts");
|
||||
|
||||
std::string command = Trim(GetStringArg(args, "command"));
|
||||
if (command.empty())
|
||||
return BuildError(id, -32602, "Missing required parameter: command");
|
||||
|
||||
CMcpServer& mcp = CMcpServer::Instance();
|
||||
|
||||
// 安全门 1:只读模式直接拒绝(默认只读,exec_command 需显式关只读才可用)。
|
||||
if (mcp.IsReadonly())
|
||||
return BuildError(id, -32006, "exec_command is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
|
||||
|
||||
// 安全门 2:拒绝 shell 元字符,防止 "dir && del ..." 之类绕过白名单的注入。
|
||||
if (command.find_first_of("&|<>^") != std::string::npos)
|
||||
return BuildError(id, -32008, "Command contains forbidden shell characters: " + command);
|
||||
|
||||
// 安全门 3:命令白名单前缀校验。
|
||||
std::string whitelist = mcp.GetCmdWhitelist();
|
||||
if (whitelist.empty()) whitelist = kDefaultCmdWhitelist;
|
||||
if (!IsCommandAllowed(command, whitelist))
|
||||
return BuildError(id, -32007, "Command not allowed by whitelist: " + command);
|
||||
|
||||
int timeoutMs = kMcpToolTimeoutMs;
|
||||
std::string t = GetStringArg(args, "timeout_ms");
|
||||
if (!t.empty() && IsDigits(t)) {
|
||||
int v = atoi(t.c_str());
|
||||
if (v > 0 && v <= 600000) timeoutMs = v;
|
||||
}
|
||||
|
||||
std::string nonce = GenerateRandomToken().substr(0, 8);
|
||||
|
||||
if (!mcp.BeginTermPending(devId, command, nonce))
|
||||
return BuildError(id, -32003, "Device busy: a terminal session is already active for this host");
|
||||
|
||||
// 主连接下发 COMMAND_SHELL,客户端建立 shell 子连接并回 TOKEN_*_START。
|
||||
BYTE cmd = COMMAND_SHELL;
|
||||
if (!ctx->Send2Client(&cmd, 1)) {
|
||||
mcp.ClearTermPending(devId);
|
||||
return BuildError(id, -32004, "Failed to send command to host");
|
||||
}
|
||||
|
||||
context* subCtx = nullptr;
|
||||
bool isPty = false;
|
||||
if (!mcp.WaitTerminalReady(devId, subCtx, isPty, timeoutMs))
|
||||
return BuildError(id, -32001, "Timeout waiting for shell to start");
|
||||
|
||||
// 编码:ConPTY(UTF-8) / 老 cmd 管道(GBK)。哨兵为纯 ASCII,两种编码下字节一致。
|
||||
UINT cp = isPty ? CP_UTF8 : 936;
|
||||
// 哨兵命令行:@echo off 仅 ConPTY 需要(抑制回显噪声);老 ShellManager 已自行跳过回显,
|
||||
// 加 @echo off 反而破坏其回显跳过逻辑。&&/|| 取命令真实退出码(%errorlevel% 在复合句中
|
||||
// 是解析期展开、已过期,故用控制操作符)。哨兵为随机串,命令输出不可能恰好包含。
|
||||
std::string line;
|
||||
if (isPty) line += "@echo off & ";
|
||||
line += command;
|
||||
line += " 2>&1 && echo __MCP_DONE_" + nonce + "__0 || echo __MCP_DONE_" + nonce + "__1";
|
||||
std::string wireLine = ToAnsi(line, cp) + "\r\n";
|
||||
subCtx->Send2Client((BYTE*)wireLine.data(), (ULONG)wireLine.size());
|
||||
|
||||
std::vector<BYTE> raw;
|
||||
int exitCode = -1;
|
||||
bool closed = false;
|
||||
if (!mcp.WaitTerminalDone(devId, raw, exitCode, closed, timeoutMs)) {
|
||||
subCtx->CancelIO();
|
||||
return BuildError(id, -32001, "Timeout waiting for command output");
|
||||
}
|
||||
subCtx->CancelIO(); // 关子链接,结束 shell 进程
|
||||
|
||||
// 清洗:raw → UTF-8 → 剥 ANSI → CRLF 归一。raw 已截断到哨兵前。
|
||||
std::string stdoutStr;
|
||||
if (!raw.empty()) {
|
||||
std::string rawStr((const char*)raw.data(), raw.size());
|
||||
stdoutStr = StripAnsi(ToUtf8(rawStr.c_str(), cp));
|
||||
size_t p = 0;
|
||||
while ((p = stdoutStr.find("\r\n", p)) != std::string::npos)
|
||||
stdoutStr.erase(p, 1);
|
||||
stdoutStr = TrimRight(stdoutStr);
|
||||
}
|
||||
|
||||
// 审计:命令执行落服务端消息/审计日志(不可关闭)。
|
||||
if (parent) {
|
||||
std::string text = "host " + std::to_string(devId) + " exec: " + command;
|
||||
parent->PostMessageA(WM_SHOWERRORMSG,
|
||||
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
|
||||
(LPARAM)new CString(_TR("MCP命令执行"))); // 标题走语言映射(GBK,随语言切换)
|
||||
}
|
||||
|
||||
Json::Value result(Json::objectValue);
|
||||
Json::Value structuredContent(Json::objectValue);
|
||||
structuredContent["stdout"] = stdoutStr;
|
||||
structuredContent["exit_code"] = exitCode;
|
||||
result["structuredContent"] = structuredContent;
|
||||
|
||||
Json::Value content(Json::arrayValue);
|
||||
Json::Value item(Json::objectValue);
|
||||
item["type"] = "text";
|
||||
item["text"] = stdoutStr.empty() ? std::string(u8"(无输出)") : stdoutStr;
|
||||
content.append(item);
|
||||
result["content"] = content;
|
||||
result["isError"] = false;
|
||||
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// tools/call:get_client_log(主连接下发 COMMAND_QUERY_LOG,子连接回传 TOKEN_REPORT_LOG)
|
||||
std::string BuildGetClientLog(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||||
uint64_t devId = 0;
|
||||
@@ -1873,6 +2121,7 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||
if (toolName == "get_client_log") return BuildGetClientLog(id, args, parent);
|
||||
if (toolName == "get_audit_log") return BuildGetAuditLog(id, parent);
|
||||
if (toolName == "list_registry") return BuildListRegistry(id, args, parent);
|
||||
if (toolName == "exec_command") return BuildExecCommand(id, args, parent);
|
||||
|
||||
return BuildError(id, -32602,
|
||||
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
|
||||
@@ -1880,6 +2129,158 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||
|
||||
} // namespace
|
||||
|
||||
// ===== P3:exec_command 哨兵检测与终端会话 =====
|
||||
|
||||
// 在原始 shell 输出字节流中定位哨兵(纯 ASCII,编码无关)。真实哨兵由 echo 单独输出、独占
|
||||
// 一行(前面是 \n 或缓冲区开头);而 ConPTY 会把整条命令行回显进输出流,哨兵字样嵌在
|
||||
// "&& echo ... || echo ..." 里、前面是空格。据此从后往前找「行首命中」,排除回显行误判:
|
||||
// 否则回显包先于命令输出单独到达时,rfind 会命中回显里的 __1 提前结束、截断真实输出。
|
||||
// 老 ShellManager 已自行跳过回显,输出里只有真实哨兵(仍为行首)。命中 __0 → exit 0;__1 → exit 1。
|
||||
static bool FindSentinel(const std::vector<BYTE>& buf, const std::string& nonce,
|
||||
size_t& pos, int& exitCode) {
|
||||
std::string marker = "__MCP_DONE_" + nonce + "__";
|
||||
if (marker.size() + 1 > buf.size()) return false;
|
||||
std::string s((const char*)buf.data(), buf.size());
|
||||
size_t from = std::string::npos;
|
||||
while (true) {
|
||||
size_t p = s.rfind(marker, from);
|
||||
if (p == std::string::npos) return false;
|
||||
bool lineStart = (p == 0) || (s[p - 1] == '\n');
|
||||
size_t digitPos = p + marker.size();
|
||||
if (lineStart && digitPos < s.size() &&
|
||||
(s[digitPos] == '0' || s[digitPos] == '1')) {
|
||||
exitCode = (s[digitPos] == '0') ? 0 : 1;
|
||||
pos = p;
|
||||
return true;
|
||||
}
|
||||
if (p == 0) return false;
|
||||
from = p - 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool CMcpServer::IsTermPending(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||
auto it = m_TermSessions.find(device_id);
|
||||
return it != m_TermSessions.end() && !it->second.started;
|
||||
}
|
||||
|
||||
void CMcpServer::RegisterTerminalContext(uint64_t device_id, context* subCtx, bool isPty) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||
auto it = m_TermSessions.find(device_id);
|
||||
if (it == m_TermSessions.end() || it->second.started) return; // 没有等它的会话
|
||||
TermSession& s = it->second;
|
||||
s.started = true;
|
||||
s.subCtx = subCtx;
|
||||
s.isPty = isPty;
|
||||
m_TermContextToDevice[subCtx] = device_id;
|
||||
}
|
||||
|
||||
// 关键步骤:告知客户端「启动 shell 输出回流」。客户端读线程靠 COMMAND_NEXT 才启动,
|
||||
// 漏发会导致 shell 在跑但输出永不送回。PTY 还要先告知初始 80x24,否则 TUI 尺寸错乱。
|
||||
if (isPty) {
|
||||
BYTE resizeBuf[5];
|
||||
resizeBuf[0] = CMD_TERMINAL_RESIZE;
|
||||
*(short*)(resizeBuf + 1) = (short)80;
|
||||
*(short*)(resizeBuf + 3) = (short)24;
|
||||
subCtx->Send2Client(resizeBuf, 5);
|
||||
}
|
||||
BYTE startCmd = COMMAND_NEXT;
|
||||
subCtx->Send2Client(&startCmd, 1);
|
||||
|
||||
m_TermCv.notify_all();
|
||||
}
|
||||
|
||||
bool CMcpServer::IsTerminalContext(context* subCtx) {
|
||||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||
return m_TermContextToDevice.find(subCtx) != m_TermContextToDevice.end();
|
||||
}
|
||||
|
||||
void CMcpServer::OnTerminalData(context* subCtx, const BYTE* data, ULONG len) {
|
||||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||
auto it = m_TermContextToDevice.find(subCtx);
|
||||
if (it == m_TermContextToDevice.end()) return;
|
||||
auto sit = m_TermSessions.find(it->second);
|
||||
if (sit == m_TermSessions.end()) return;
|
||||
TermSession& s = sit->second;
|
||||
if (s.done || s.closed) return; // 已结束,忽略迟到数据
|
||||
s.data.insert(s.data.end(), data, data + len);
|
||||
if (FindSentinel(s.data, s.nonce, s.sentPos, s.exitCode)) {
|
||||
s.done = true;
|
||||
m_TermCv.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
void CMcpServer::OnTerminalClosed(context* subCtx) {
|
||||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||
auto it = m_TermContextToDevice.find(subCtx);
|
||||
if (it == m_TermContextToDevice.end()) return;
|
||||
auto sit = m_TermSessions.find(it->second);
|
||||
if (sit == m_TermSessions.end()) return;
|
||||
sit->second.closed = true;
|
||||
m_TermCv.notify_all();
|
||||
}
|
||||
|
||||
bool CMcpServer::BeginTermPending(uint64_t device_id, const std::string& command, const std::string& nonce) {
|
||||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||
if (m_TermSessions.find(device_id) != m_TermSessions.end()) return false; // 已有会话
|
||||
TermSession s;
|
||||
s.command = command;
|
||||
s.nonce = nonce;
|
||||
m_TermSessions[device_id] = std::move(s);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CMcpServer::WaitTerminalReady(uint64_t device_id, context*& subCtx, bool& isPty, int timeoutMs) {
|
||||
std::unique_lock<std::mutex> lk(m_TermMutex);
|
||||
auto it = m_TermSessions.find(device_id);
|
||||
if (it == m_TermSessions.end()) return false;
|
||||
|
||||
bool signaled = m_TermCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
|
||||
[&] { return it->second.started; });
|
||||
if (!signaled) {
|
||||
m_TermSessions.erase(it); // 超时 → 清理
|
||||
return false;
|
||||
}
|
||||
subCtx = it->second.subCtx;
|
||||
isPty = it->second.isPty;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CMcpServer::WaitTerminalDone(uint64_t device_id, std::vector<BYTE>& out,
|
||||
int& exitCode, bool& closed, int timeoutMs) {
|
||||
std::unique_lock<std::mutex> lk(m_TermMutex);
|
||||
auto it = m_TermSessions.find(device_id);
|
||||
if (it == m_TermSessions.end()) return false;
|
||||
|
||||
bool signaled = m_TermCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
|
||||
[&] { return it->second.done || it->second.closed; });
|
||||
|
||||
if (!signaled) { // 超时 → 清理(含路由表)
|
||||
m_TermContextToDevice.erase(it->second.subCtx);
|
||||
m_TermSessions.erase(it);
|
||||
return false;
|
||||
}
|
||||
|
||||
TermSession s = it->second; // 拷贝出,避免擦除后悬空
|
||||
m_TermContextToDevice.erase(s.subCtx);
|
||||
m_TermSessions.erase(it);
|
||||
|
||||
closed = s.closed;
|
||||
exitCode = s.exitCode;
|
||||
if (s.done && s.sentPos <= s.data.size())
|
||||
out.assign(s.data.begin(), s.data.begin() + s.sentPos); // 截断到哨兵前
|
||||
else
|
||||
out = s.data; // 进程退出无哨兵:返回已收集的原始输出
|
||||
return true;
|
||||
}
|
||||
|
||||
void CMcpServer::ClearTermPending(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||||
auto it = m_TermSessions.find(device_id);
|
||||
if (it != m_TermSessions.end()) m_TermSessions.erase(it);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// CMcpServer Implementation
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Reference in New Issue
Block a user