Feature: Add persistent remote terminal MCP tools

Add terminal_open / terminal_exec / terminal_close so an AI can hold one
shell session per Windows host and run a sequence of commands with cwd and
environment preserved, instead of the one-shot exec_command.

The persistent terminal is a separate full-command write capability gated by
McpTerminal (default off) plus McpReadonly=0, with no whitelist and full
audit. One device maps to one terminal session via the shared m_TermSessions
map; idle sessions are swept after 300s. terminal_exec rejects commands that
contain & or | (they corrupt the sentinel control-operator chain) as well as
control characters.

Also harden exec_command and terminal_exec against newline/CR injection, fix a
dangling subCtx after an abrupt shell disconnect, and refresh lastActiveAt on
command completion. Add en/zh-TW translations for the new UI strings and a
design document covering both exec_command and the persistent terminal.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxiang
2026-08-24 19:11:16 +02:00
parent 0ddbc1aced
commit 70dbb6b255
8 changed files with 1099 additions and 9 deletions

View File

@@ -1114,6 +1114,17 @@ Json::Value BuildListFilesOutputSchema() {
return schema;
}
// ===== P4 前置声明定义见下方「tools/call 分派」前)=====
Json::Value BuildTerminalOpenInputSchema();
Json::Value BuildTerminalOpenOutputSchema();
Json::Value BuildTerminalExecInputSchema();
Json::Value BuildTerminalExecOutputSchema();
Json::Value BuildTerminalCloseInputSchema();
Json::Value BuildTerminalCloseOutputSchema();
std::string BuildTerminalOpen(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildTerminalExec(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildTerminalClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// tools/list
std::string BuildToolsListResult(const Json::Value& id) {
Json::Value result(Json::objectValue);
@@ -1288,6 +1299,35 @@ std::string BuildToolsListResult(const Json::Value& id) {
tools.append(tool);
}
// 14) terminal_open / terminal_exec / terminal_closeP4持久远程终端仅 Windows
// 安全门McpTerminal=1 且 McpReadonly=0无白名单全命令审计 + idle 回收)
if (CMcpServer::Instance().IsTerminalEnabled() && !CMcpServer::Instance().IsReadonly()) {
{
Json::Value tool(Json::objectValue);
tool["name"] = "terminal_open";
tool["description"] = u8"在指定在线 Windows 主机上打开一个持久 shell 会话并返回 session_id 与终端模式。后续 terminal_exec 复用该会话cwd/环境变量跨命令保持),用毕须 terminal_close。";
tool["inputSchema"] = BuildTerminalOpenInputSchema();
tool["outputSchema"] = BuildTerminalOpenOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "terminal_exec";
tool["description"] = u8"在已打开的持久会话中执行一条命令并返回 stdout 与退出码。命令不受白名单约束,但不能含 & 或 |(会破坏输出哨兵捕获),请拆成多条调用;重定向 > < 与转义 ^ 允许。";
tool["inputSchema"] = BuildTerminalExecInputSchema();
tool["outputSchema"] = BuildTerminalExecOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "terminal_close";
tool["description"] = u8"关闭并释放指定持久会话(幂等:会话已不存在也返回成功)。";
tool["inputSchema"] = BuildTerminalCloseInputSchema();
tool["outputSchema"] = BuildTerminalCloseOutputSchema();
tools.append(tool);
}
}
result["tools"] = tools;
return BuildResult(id, result);
}
@@ -1889,6 +1929,26 @@ static std::string StripAnsi(const std::string& s) {
return out;
}
// 去掉 ConPTY 把整行输入回显造成的噪声echoedLine 是发送的哨兵命令行(含唯一 nonce
// 输出里若出现该行(连同其后换行)则整行剔除。非 PTY 老 ShellManager 已自行跳回显,
// 找不到该行即无操作。echoedLine 含随机 nonce命令真实输出不可能恰好相同。
static std::string StripEchoedCommand(const std::string& s, const std::string& echoedLine) {
if (echoedLine.empty() || s.empty()) return s;
size_t p = s.find(echoedLine);
if (p == std::string::npos) return s;
size_t q = p + echoedLine.size();
if (q < s.size() && s[q] == '\n') ++q; // CRLF 已归一为 \n
return s.substr(0, p) + s.substr(q);
}
// 命令是否含控制字符(<0x20含 \r\n。\r\n 会把命令拆成多行:一次性 exec 里会绕过
// 白名单执行任意后续行(安全);持久终端里只有末行被哨兵包装、退出码失真(正确性)。二者都拒绝。
static bool ContainsControlChar(const std::string& s) {
for (char ch : s)
if ((unsigned char)ch < 0x20) return true;
return false;
}
// tools/callexec_commandWindows 一次性远程命令:主连接 COMMAND_SHELL → 子连接终端 →
// 哨兵命令行 → 收集 stdout + exit_code → 关子链接)。安全门:只读默认 + 白名单 + 审计。
std::string BuildExecCommand(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
@@ -1916,8 +1976,8 @@ std::string BuildExecCommand(const Json::Value& id, const Json::Value& args, CMy
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)
// 安全门 2拒绝 shell 元字符与控制字符(\r\n 会把命令拆成多行绕过白名单),防注入。
if (command.find_first_of("&|<>^") != std::string::npos || ContainsControlChar(command))
return BuildError(id, -32008, "Command contains forbidden shell characters: " + command);
// 安全门 3命令白名单前缀校验。
@@ -1971,7 +2031,8 @@ std::string BuildExecCommand(const Json::Value& id, const Json::Value& args, CMy
}
subCtx->CancelIO(); // 关子链接,结束 shell 进程
// 清洗raw → UTF-8 → 剥 ANSI → CRLF 归一。raw 已截断到哨兵前
// 清洗raw → UTF-8 → 剥 ANSI → CRLF 归一 → 去 ConPTY 回显命令行 → TrimRight
// raw 已截断到哨兵前。
std::string stdoutStr;
if (!raw.empty()) {
std::string rawStr((const char*)raw.data(), raw.size());
@@ -1979,6 +2040,7 @@ std::string BuildExecCommand(const Json::Value& id, const Json::Value& args, CMy
size_t p = 0;
while ((p = stdoutStr.find("\r\n", p)) != std::string::npos)
stdoutStr.erase(p, 1);
stdoutStr = StripEchoedCommand(stdoutStr, line);
stdoutStr = TrimRight(stdoutStr);
}
@@ -2097,6 +2159,380 @@ std::string BuildGetAuditLog(const Json::Value& id, CMy2015RemoteDlg* parent) {
return BuildResult(id, result);
}
// ===== P4持久远程终端terminal_open / terminal_exec / terminal_close=====
static const int kTermIdleTimeoutSec = 300; // 持久终端 idle 回收超时(秒)
// 共享前置:校验 McpTerminal && !readonly → 解析 id → FindMainContext → 拒绝 LNX/MAC。
// 成功返回 true 并输出 devId/ctx失败时 errJson 已写入对应 JSON 错误串。
static bool ResolveTerminalSessionHost(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent,
uint64_t& devId, context*& ctx, std::string& errJson) {
CMcpServer& mcp = CMcpServer::Instance();
if (!mcp.IsTerminalEnabled() || mcp.IsReadonly()) {
errJson = BuildError(id, -32006,
"Persistent terminal is disabled: requires McpTerminal=1 and McpReadonly=0");
return false;
}
std::string err;
if (!ParseHostIdArg(args, devId, err)) {
errJson = BuildError(id, -32602, err);
return false;
}
ctx = FindMainContext(parent, devId);
if (!ctx) {
errJson = BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
return false;
}
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC") {
errJson = BuildError(id, -32005, "Persistent terminal is only supported on Windows hosts");
return false;
}
return true;
}
// 发送哨兵命令行(复用 exec_command 的编码/哨兵逻辑);返回所发送的完整命令行,
// 供调用方剔除 ConPTY 把输入整行回显造成的噪声。cp 由 isPty 决定。
static std::string SendTerminalCommandLine(context* subCtx, bool isPty, const std::string& command,
const std::string& nonce) {
UINT cp = isPty ? CP_UTF8 : 936;
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());
return line;
}
// 输出清洗(复用 exec_command 尾部逻辑raw → UTF-8 → 剥 ANSI → CRLF 归一 →
// 去 ConPTY 回显命令行 → TrimRight。
static std::string CleanTerminalOutput(const std::vector<BYTE>& raw, UINT cp,
const std::string& echoedLine) {
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 = StripEchoedCommand(stdoutStr, echoedLine);
stdoutStr = TrimRight(stdoutStr);
}
return stdoutStr;
}
// terminal_open 的 inputSchemaid 必填、timeout_ms 可选)
Json::Value BuildTerminalOpenInputSchema() {
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 timeoutProp(Json::objectValue);
timeoutProp["type"] = "integer";
timeoutProp["description"] = u8"等待 shell 启动的超时毫秒数(可选,默认 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");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalOpenOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value sid(Json::objectValue);
sid["type"] = "string";
sid["description"] = u8"持久会话 token后续 terminal_exec / terminal_close 用";
props["session_id"] = sid;
Json::Value pty(Json::objectValue);
pty["type"] = "boolean";
pty["description"] = u8"true=ConPTY(UTF-8)false=老 cmd 管道(GBK)";
props["is_pty"] = pty;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("session_id");
required.append("is_pty");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalExecInputSchema() {
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 sid(Json::objectValue);
sid["type"] = "string";
sid["description"] = u8"terminal_open 返回的 session_id";
props["session_id"] = sid;
Json::Value cmdProp(Json::objectValue);
cmdProp["type"] = "string";
cmdProp["description"] = u8"要执行的命令(完整 shell不受白名单约束但不能含 & 或 |,请拆成多条调用)";
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("session_id");
required.append("command");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalExecOutputSchema() {
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;
}
Json::Value BuildTerminalCloseInputSchema() {
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 sid(Json::objectValue);
sid["type"] = "string";
sid["description"] = u8"terminal_open 返回的 session_id";
props["session_id"] = sid;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
required.append("session_id");
schema["required"] = required;
return schema;
}
Json::Value BuildTerminalCloseOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value closed(Json::objectValue);
closed["type"] = "boolean";
closed["description"] = u8"恒为 true关闭幂等";
props["closed"] = closed;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("closed");
schema["required"] = required;
return schema;
}
// tools/callterminal_open打开持久 shell 会话,返回 session_id用毕须 terminal_close
std::string BuildTerminalOpen(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
CMcpServer& mcp = CMcpServer::Instance();
mcp.SweepIdleTerminals(kTermIdleTimeoutSec);
uint64_t devId = 0;
context* ctx = nullptr;
std::string errJson;
if (!ResolveTerminalSessionHost(id, args, parent, devId, ctx, errJson))
return errJson;
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 sessionId = GenerateRandomToken();
if (!mcp.BeginTermOpen(devId, sessionId))
return BuildError(id, -32003, "Device busy: a terminal session is already active for this host");
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");
// 审计:打开会话(不可关闭)。
if (parent) {
std::string text = "host " + std::to_string(devId) + " term-open: session " + sessionId;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP持久终端")));
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["session_id"] = sessionId;
structuredContent["is_pty"] = isPty;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = sessionId;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/callterminal_exec在已打开的持久会话中执行一条命令无白名单但禁 & |
std::string BuildTerminalExec(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
CMcpServer& mcp = CMcpServer::Instance();
mcp.SweepIdleTerminals(kTermIdleTimeoutSec);
uint64_t devId = 0;
context* ctx = nullptr;
std::string errJson;
if (!ResolveTerminalSessionHost(id, args, parent, devId, ctx, errJson))
return errJson;
std::string sessionId = GetStringArg(args, "session_id");
if (sessionId.empty())
return BuildError(id, -32602, "Missing required parameter: session_id");
std::string command = Trim(GetStringArg(args, "command"));
if (command.empty())
return BuildError(id, -32602, "Missing required parameter: command");
// 正确性门(非安全门):& 与 | 会破坏哨兵控制操作符链、导致退出码失真;拆成多条调用即可。
// \r\n 会把命令拆成多行、只有末行被哨兵包装,同样破坏退出码捕获,一并拒绝。
if (command.find_first_of("&|") != std::string::npos || ContainsControlChar(command))
return BuildError(id, -32008,
"Command contains & or | or a newline (breaks output capture); run chained/piped commands as separate terminal_exec calls");
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);
context* subCtx = nullptr;
bool isPty = false;
if (!mcp.BeginTermCommand(devId, sessionId, command, nonce, subCtx, isPty))
return BuildError(id, -32002, "Terminal session not found, not ready, or busy: " + sessionId);
std::string sentLine = SendTerminalCommandLine(subCtx, isPty, command, nonce);
std::vector<BYTE> raw;
int exitCode = -1;
bool closed = false;
if (!mcp.WaitTermCommand(devId, raw, exitCode, closed, timeoutMs)) {
subCtx->CancelIO();
return BuildError(id, -32001, "Timeout waiting for command output");
}
if (closed) {
// shell 进程退出:会话已被 WaitTermCommand 清理子链接已死CancelIO 仅对称(无害)。
subCtx->CancelIO();
}
std::string stdoutStr = CleanTerminalOutput(raw, isPty ? CP_UTF8 : 936, sentLine);
// 审计:命令执行(不可关闭)。
if (parent) {
std::string text = "host " + std::to_string(devId) + " term-exec [" + sessionId + "]: " + command;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP持久终端")));
}
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/callterminal_close关闭持久会话幂等session_id 不匹配报错)
std::string BuildTerminalClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
CMcpServer& mcp = CMcpServer::Instance();
mcp.SweepIdleTerminals(kTermIdleTimeoutSec);
if (!mcp.IsTerminalEnabled() || mcp.IsReadonly())
return BuildError(id, -32006, "Persistent terminal is disabled: requires McpTerminal=1 and McpReadonly=0");
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
std::string sessionId = GetStringArg(args, "session_id");
if (sessionId.empty())
return BuildError(id, -32602, "Missing required parameter: session_id");
int r = mcp.CloseTermSession(devId, sessionId);
if (r == 2)
return BuildError(id, -32002, "Terminal session_id mismatch: " + sessionId);
// 审计:关闭会话(不可关闭)。
if (parent) {
std::string text = "host " + std::to_string(devId) + " term-close: session " + sessionId;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP持久终端")));
}
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["closed"] = true;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"closed");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/call 分派
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
const Json::Value& id = root["id"];
@@ -2122,6 +2558,9 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* 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);
if (toolName == "terminal_open") return BuildTerminalOpen(id, args, parent);
if (toolName == "terminal_exec") return BuildTerminalExec(id, args, parent);
if (toolName == "terminal_close") return BuildTerminalClose(id, args, parent);
return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
@@ -2217,8 +2656,14 @@ void CMcpServer::OnTerminalClosed(context* 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();
TermSession& s = sit->second;
s.closed = true;
if (s.persistent && !s.busy && s.started) {
// 空闲持久会话shell 已退、无等待线程会清理,直接擦路由+会话(无需 CancelIO已死
m_TermContextToDevice.erase(subCtx);
m_TermSessions.erase(sit);
}
m_TermCv.notify_all(); // busy 场景唤醒等待线程由其清理;一次性 exec 路径不变
}
bool CMcpServer::BeginTermPending(uint64_t device_id, const std::string& command, const std::string& nonce) {
@@ -2281,6 +2726,122 @@ void CMcpServer::ClearTermPending(uint64_t device_id) {
if (it != m_TermSessions.end()) m_TermSessions.erase(it);
}
// ===== P4持久远程终端状态机方法=====
bool CMcpServer::BeginTermOpen(uint64_t device_id, const std::string& sessionId) {
std::lock_guard<std::mutex> lk(m_TermMutex);
if (m_TermSessions.find(device_id) != m_TermSessions.end()) return false; // 单设备单终端
TermSession s;
s.sessionId = sessionId;
s.persistent = true;
s.lastActiveAt = time(nullptr);
m_TermSessions[device_id] = std::move(s);
return true;
}
bool CMcpServer::BeginTermCommand(uint64_t device_id, const std::string& sessionId,
const std::string& command, const std::string& nonce,
context*& subCtx, bool& isPty) {
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
if (it == m_TermSessions.end()) return false;
TermSession& s = it->second;
if (!s.persistent || s.sessionId != sessionId) return false; // 非持久或 token 不匹配
if (!s.started) return false; // 未就绪
if (s.busy) return false; // 已有命令在飞
if (s.closed) return false; // 已关闭
s.busy = true;
s.command = command;
s.nonce = nonce;
s.data.clear();
s.sentPos = 0;
s.exitCode = -1;
s.done = false;
s.closed = false;
s.lastActiveAt = time(nullptr);
subCtx = s.subCtx;
isPty = s.isPty;
return true;
}
bool CMcpServer::WaitTermCommand(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) { // 超时 → 清理会话+路由(调用方负责 CancelIO
context* subCtx = it->second.subCtx;
m_TermContextToDevice.erase(subCtx);
m_TermSessions.erase(it);
return false;
}
if (it->second.closed) { // 进程退出 → 清理,返回已收集输出(无哨兵)
context* subCtx = it->second.subCtx;
out = it->second.data;
exitCode = -1;
closed = true;
m_TermContextToDevice.erase(subCtx);
m_TermSessions.erase(it);
return true;
}
// 哨兵命中 → 保持会话,复位 busy刷新 lastActiveAt避免「长命令刚完成即被 idle 回收」
// idle 语义应为「距上次活动」,而非「距上次命令开始」)。
it->second.busy = false;
it->second.lastActiveAt = time(nullptr);
exitCode = it->second.exitCode;
closed = false;
if (it->second.sentPos <= it->second.data.size())
out.assign(it->second.data.begin(), it->second.data.begin() + it->second.sentPos);
return true;
}
int CMcpServer::CloseTermSession(uint64_t device_id, const std::string& sessionId) {
context* subCtx = nullptr;
{
std::lock_guard<std::mutex> lk(m_TermMutex);
auto it = m_TermSessions.find(device_id);
if (it == m_TermSessions.end()) return 1; // 不存在(幂等)
TermSession& s = it->second;
if (!s.persistent || s.sessionId != sessionId) return 2; // token 不匹配
if (s.busy) {
// 有命令在飞:置 closed 唤醒等待线程,由其清理(避免双重擦除)。
s.closed = true;
m_TermCv.notify_all();
return 0;
}
subCtx = s.subCtx;
m_TermContextToDevice.erase(s.subCtx);
m_TermSessions.erase(it);
}
if (subCtx) subCtx->CancelIO(); // 锁外取消 IO触发客户端 shell 退出
return 0;
}
int CMcpServer::SweepIdleTerminals(time_t idleTimeoutSec) {
time_t now = time(nullptr);
std::vector<context*> toCancel;
{
std::lock_guard<std::mutex> lk(m_TermMutex);
for (auto it = m_TermSessions.begin(); it != m_TermSessions.end(); ) {
TermSession& s = it->second;
if (s.persistent && !s.busy && difftime(now, s.lastActiveAt) > (double)idleTimeoutSec) {
if (s.subCtx) { m_TermContextToDevice.erase(s.subCtx); toCancel.push_back(s.subCtx); }
it = m_TermSessions.erase(it);
} else {
++it;
}
}
}
for (context* c : toCancel) if (c) c->CancelIO(); // 锁外 CancelIO
return (int)toCancel.size();
}
//////////////////////////////////////////////////////////////////////////
// CMcpServer Implementation
//////////////////////////////////////////////////////////////////////////