Files
SimpleRemoter/docs/Mcp_Terminal_Design.md
yuanyuanxiang 70dbb6b255 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
2026-08-24 19:11:16 +02:00

26 KiB
Raw Blame History

MCP 远程命令与持久终端 — 设计与实现

本文档覆盖 MCP 侧两类命令执行能力:

  1. 一次性命令 exec_commandP3a—— 每次新建一个 shell、跑一条命令、读完即关。
  2. 持久终端 terminal_open / terminal_exec / terminal_closeP4—— 同一主机一个 shell 会话,跨命令保持 cwd 与环境变量。

阅读顺序建议:先读 docs/Mcp_Design.mdPhase 1HTTP 协议、token 校验、配置、list_online_hosts)与 docs/Mcp_Phase2_Design.md(模式 A/A/B、工具路线图、P2aP3a 概要),再读本文档的实现级细节。本文档是后续人员 / 大模型接手与排查问题的主参考。


1. 术语与总览

术语 含义
一次性命令 exec_command:无状态,每次独立 shell命令结束即 CancelIO 关闭子链接。
持久终端 terminal_openterminal_exec×N→terminal_close有状态cwd/env 跨命令保持。
哨兵sentinel 包装在命令尾部的一段随机标记,用于在输出流里定位「命令真实退出码」并截断输出。
子链接sub-connection / context* 客户端为 shell 建立的独立网络连接,context 是其服务端句柄。
主连接(FindMainContext 客户端与控制端的常驻长连接,用于下发 COMMAND_*
isPty true=ConPTYUTF-8cp=CP_UTF8false=老 cmd 管道GBKcp=936)。

两类能力共享同一套底层机制(终端链路、编码、哨兵、状态机 map、审计区别只在生命周期(一次性 vs 保持)与安全门(白名单 vs 无白名单 + 独立开关)。这也是为什么它们会互相排他(见 §6.3)。


2. 共享基础设施

2.1 客户端终端链路(两种能力通用)

无论一次性还是持久shell 的建立过程完全相同:

服务端                    客户端
  │  COMMAND_SHELL主连接 ──►  客户端创建 shell 子连接
  │  ◄── TOKEN_SHELL_START 或 TOKEN_TERMINAL_START子连接
  │         ↓ MessageHandle 顶部判定 IsTermPending → 交给 RegisterTerminalContext 接管
  │  COMMAND_NEXT+ 若 PTY 先发 CMD_TERMINAL_RESIZE 80x24──►  客户端启动 shell 输出回流
  │  ◄── 持续 shell 输出TOKEN_TERMINAL_DATA 之类,经 IsTerminalContext 路由到 OnTerminalData
  │  ◄── TOKEN_TERMINAL_CLOSEshell 进程退出)→ OnTerminalClosed

关键点:

  • COMMAND_NEXT 不能漏发——客户端读线程靠它才启动输出回流,漏发会导致 shell 在跑但输出永不送回(RegisterTerminalContext 内的注释明确强调)。
  • PTY 需先告知初始尺寸 80×24,否则 TUI 尺寸错乱。
  • TOKEN_SHELL_STARTTOKEN_TERMINAL_START 二选一:老 ShellManagercmd 管道回前者ConPTY 回后者;服务端在 MessageHandle 里据此判定 isPty

2.2 编码

isPty 底层 服务端 cp
true ConPTY CP_UTF8
false 老 cmd 管道 936GBK
  • 发送方向:命令/哨兵行先 UTF-8 组好,再 ToAnsi(line, cp) 转成客户端编码,追加 "\r\n"
  • 接收方向:原始字节 ToUtf8(raw, cp) 转回 UTF-8。
  • 哨兵是纯 ASCII两种编码下字节一致编码无关。

2.3 哨兵机制

命令被包装成一行复合命令:

[@echo off & ] <cmd> 2>&1 && echo __MCP_DONE_<nonce>__0 || echo __MCP_DONE_<nonce>__1
  • @echo off & 仅 ConPTY 需要(抑制 ConPTY 把输入整行回显)。老 ShellManager 已自行跳过回显,加 @echo off 反而破坏它的回显跳过逻辑。
  • && / || 控制操作符取命令真实退出码%errorlevel% 在复合句中解析期展开、已过期,故不能用 %errorlevel%)。
  • <nonce> 为 8 位随机 hexGenerateRandomToken().substr(0,8)),保证命令真实输出不可能恰好包含完整哨兵串。
  • 2>&1 让 stderr 也走哨兵链stdout/stderr 都能被捕获且退出码正确。

FindSentinel(检测)rfind 从后往前找,且要求哨兵在行首p==0 或前一个字节是 \n),后跟 01

size_t from = npos;
while (true) {
    size_t p = s.rfind(marker, from);
    bool lineStart = (p == 0) || (s[p-1] == '\n');
    if (lineStart && s[p+marker.size()]  '0'  '1') { exitCode=...; pos=p; return true; }
    from = p - 1;
}

为什么要「从后往前 + 行首」ConPTY 会把整条命令行回显进输出流,回显里的哨兵字样嵌在 && echo ... || echo ... 中间、前面是空格(非行首)。若用 rfind 直取,回显包先于真实输出到达时会命中回显里的 __1 提前结束、截断真实输出。行首校验能排除回显行。老 ShellManager 无回显,输出里只有真实哨兵(仍在行首),逻辑一致。

2.4 输出清洗管线

从原始字节到返回 stdout 字符串,固定五步(CleanTerminalOutput,一次性命令为内联等价逻辑):

raw 字节
  → ToUtf8(raw, cp)                      按 2.2 编码解码
  → StripAnsi(...)                       剥 CSI/OSC/单字节 ESC去彩色码
  → CRLF → LF                            "\r\n" 归一为 "\n"
  → StripEchoedCommand(stdout, sentLine) 剔 ConPTY 输入行回显(见下)
  → TrimRight(...)                       去尾部空白

StripEchoedCommand(回显剔除)ConPTY 的 ENABLE_ECHO_INPUT 会在 @echo off 生效之前把整条输入命令行回显进输出流。修复方法是在服务端把「刚发送的、含唯一 nonce 的完整命令行」(SendTerminalCommandLine 的返回值)从输出里整行 find + 剔除:

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 == 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);
}
  • echoedLine 含随机 nonce命令真实输出不可能恰好相同 → 不会误删合法输出。
  • ShellManager 无回显,find 不到该行 → 无操作,安全。

2.5 状态机与互斥(两类能力共用)

struct TermSession {
    bool      started;      // false=已发 COMMAND_SHELL 待 STARTtrue=已接管
    context*  subCtx;       // shell 子连接上下文
    bool      isPty;        // ConPTY(UTF-8) / cmd 管道(GBK)
    std::string command;    // 原始 UTF-8 命令(审计用)
    std::string nonce;      // 哨兵随机串
    std::vector<BYTE> data; // 收集的原始 shell 输出
    size_t    sentPos;      // 哨兵在 data 中的位置
    int       exitCode;     // 0/1/-1
    bool      done;         // 哨兵命中
    bool      closed;       // TOKEN_TERMINAL_CLOSE进程退出
    // ── 以下为持久终端新增 ──
    std::string sessionId;  // 持久会话 token一次性 exec 为空)
    bool      persistent;   // 是否持久会话
    bool      busy;         // 一条命令在飞terminal_exec 复位 → WaitTermCommand 返回)
    time_t    lastActiveAt; // idle 回收用(秒)
};

std::mutex                    m_TermMutex;             // 保护下面两个 map
std::condition_variable       m_TermCv;
std::map<uint64_t, TermSession>  m_TermSessions;       // device_id → 会话
std::map<context*, uint64_t>     m_TermContextToDevice; // subCtx → device_id顶部路由
  • m_TermContextToDevice反向索引MessageHandle 顶部用 IsTerminalContext(subCtx) 判定「这个子链接的输出是否该路由到 MCP 的 OnTerminalData,而不是打开 MFC 终端对话框」。
  • httplib 默认多线程处理请求 → 多个 tools/call 会并发。所有会话状态都靠 m_TermMutex + busy 串行化,不要假定请求串行

3. 一次性命令 exec_command

3.1 安全门(顺序)

ParseHostIdArg           → -32602 参数非法
FindMainContext          → -32002 主机不存在/离线
clientType==LNX/MAC      → -32005 仅 Windows
command 为空              → -32602
mcp.IsReadonly()         → -32006 只读模式默认开,需 McpReadonly=0
含 &|<>^ 五元字符         → -32008 防注入绕过白名单
含控制字符(\r\n 等 <0x20 → -32008 防换行拆分绕过白名单
白名单前缀校验            → -32007 不在白名单
  • 白名单IsCommandAllowed 做「trim + ASCII 小写后前缀匹配」,且前缀后须为空/空格/制表符(防 dirx 误配 dir)。配置为空时回落到内置 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
    
  • 元字符门 &|<>^安全门(拒绝 shell 注入/命令拼接),比持久终端的 &| 更严。
  • 控制字符门:同时拒绝所有 <0x20 的控制字符(\r/\n 等)。否则 dir \n rd /s /q ... 这种带内嵌换行的命令会通过「前缀 dir + 空格」的白名单校验,却在 cmd.exe 里被换行拆成两条独立命令、执行任意后续行——彻底绕过只读白名单。这是 2026-08-24 独立审查发现并修复的安全漏洞

3.2 流程

BeginTermPending(devId, command, nonce)   → false 则 -32003该 host 已有终端会话)
ctx->Send2Client(COMMAND_SHELL)            → 失败 ClearTermPending + -32004
WaitTerminalReady(devId, subCtx, isPty, timeoutMs)
                                           → 超时 -32001内部已自清理
组哨兵行 Send2Client                        (见 §2.3
WaitTerminalDone(devId, raw, exitCode, closed, timeoutMs)
                                           → 超时 -32001内部已自清理 + 调用方 CancelIO
subCtx->CancelIO()                         ← 一次性:读完即关子链接,结束 shell
清洗输出§2.4
审计PostMessageA(WM_SHOWERRORMSG, ..., "MCP命令执行")
返回 { stdout, exit_code }

3.3 WaitTerminalDone 三态(一次性专用)

  • 哨兵命中doneout = data[0..sentPos)(截断到哨兵前),exitCode=0/1擦除会话+路由,返回 true。
  • 进程退出closedout = 全部 data(无哨兵),exitCode=-1,擦除会话+路由,返回 true。
  • 超时:擦除会话+路由,返回 false调用方 CancelIO)。

4. 持久终端 terminal_open / terminal_exec / terminal_close

4.1 工具契约

工具 入参required 出参
terminal_open id+timeout_ms session_id(32hex)、is_pty(bool)
terminal_exec idsession_idcommand+timeout_ms stdoutexit_code
terminal_close idsession_id closed(bool恒 true)

session_id 由服务端 GenerateRandomToken() 生成32 hex / 128 bit

4.2 共享前置 ResolveTerminalSessionHost

terminal_open / terminal_exec 复用(terminal_close 为幂等内联版,见 §4.5

IsTerminalEnabled() && !IsReadonly()  → -32006需 McpTerminal=1 且 McpReadonly=0
ParseHostIdArg                          → -32602
FindMainContext                         → -32002 主机不存在/离线
clientType==LNX/MAC                     → -32005 仅 Windows

4.3 terminal_open

SweepIdleTerminals(300)                 ← 先回收闲置会话
ResolveTerminalSessionHost
sessionId = GenerateRandomToken()
BeginTermOpen(devId, sessionId)         → false 则 -32003单设备单终端
ctx->Send2Client(COMMAND_SHELL)          → 失败 ClearTermPending + -32004
WaitTerminalReady(...)                   → 超时 -32001自清理
审计 "term-open"
返回 { session_id, is_pty }

关键WaitTerminalReady 成功后不擦除会话(与一次性相反)——会话要留给后续 terminal_exec。此时 started=truebusy=falseshell 空闲运行。

4.4 terminal_exec

SweepIdleTerminals(300)
ResolveTerminalSessionHost
session_id 为空            → -32602
command 为空               → -32602
command 含 & 或 |          → -32008正确性门见 §4.6
nonce = 随机 8 hex
BeginTermCommand(devId, sessionId, command, nonce, subCtx, isPty)
                           → false 则 -32002不存在/不匹配/未就绪/忙碌/已关)
sentLine = SendTerminalCommandLine(subCtx, isPty, command, nonce)
WaitTermCommand(devId, raw, exitCode, closed, timeoutMs)
                           → 超时subCtx->CancelIO() + -32001
                           → closedsubCtx->CancelIO()(会话已被清理,仅对称无害)
stdoutStr = CleanTerminalOutput(raw, cp, sentLine)
审计 "term-exec [sessionId]: command"
返回 { stdout, exit_code }

WaitTermCommand 三态(持久专用,与一次性不同)

  • 哨兵命中donebusy=false、刷新 lastActiveAtout 截到 sentPos保留会话,返回 true。← 这是「持久」的核心:命令完成后 shell 不关。
  • 进程退出closedout=全部 dataexitCode=-1擦除会话+路由,返回 true。
  • 超时:擦除会话+路由,返回 false调用方 CancelIO)。

4.5 terminal_close

SweepIdleTerminals(300)
IsTerminalEnabled() && !IsReadonly()  → -32006
ParseHostIdArg                          → -32602
session_id 为空                          → -32602
CloseTermSession(devId, sessionId)
    r==2sessionId 不匹配)             → -32002防拼错静默泄漏真会话
审计 "term-close"
返回 { closed: true }

CloseTermSession 返回值语义:0=已关闭;1=会话不存在(幂等,仍返回 {closed:true}2=sessionId 不匹配(报错)。

4.6 正确性门 &|(非安全门)

terminal_exec 拒绝含 &| 的命令find_first_of("&|"),含 &&/||/|),报 -32008。原因:哨兵包装 cmd 2>&1 && echo ... || echo ... 假定命令是「扁平」的,命令内含 &/| 会与包装的控制操作符合流、导致退出码失真/输出归属不清。

  • 这是正确性限制,不是白名单——持久终端本就不设白名单。
  • 同样拒绝 <0x20 的控制字符(\r/\n):换行会把命令拆成多行、只有末行被哨兵包装,导致退出码与输出归属失真。
  • > < ^(重定向 / 转义)允许不破坏哨兵链。AI 把链式/管道命令拆成多条 terminal_exec 调用即可cwd/env 保持,等价)。

对比:一次性 exec_command 因有白名单,其元字符门更严(&|<>^ 五元字符全拒§3.1)。


5. 状态机方法明细

5.1 一次性命令专用

方法 作用
BeginTermPending(devId, cmd, nonce) 锁内登记会话;已有会话则 false单设备单终端
WaitTerminalReady(devId, subCtx, isPty, timeout) started;超时擦除并返回 false。成功输出 subCtx/isPty不擦
WaitTerminalDone(...) 等 `done
ClearTermPending(devId) 发送失败等提前退出路径的清理。
IsTermPending(devId) MessageHandle 判定是否有待接管的终端会话。
RegisterTerminalContext(devId, subCtx, isPty) 子连接就绪接管:置 started/subCtx/isPty、登记反向索引、发 COMMAND_NEXT(+PTY resize)、notify_all
IsTerminalContext(subCtx) MessageHandle 顶部路由判定。
OnTerminalData(subCtx, data, len) 追加输出、FindSentinel 命中置 done + notify`done
OnTerminalClosed(subCtx) closed;空闲持久会话直接擦;notify_all 唤醒 busy 等待线程。

5.2 持久终端专用

方法 作用
BeginTermOpen(devId, sessionId) 锁内建持久会话(persistent=truelastActiveAt=now);已有会话 false。
BeginTermCommand(devId, sid, cmd, nonce, subCtx, isPty) 锁内校验「persistent && sid 匹配 && started && !busy && !closed」→ 置 busy=true原子复位 data/sentPos/exitCode/done/closed、更新 command/nonce/lastActiveAt
WaitTermCommand(...) 等 `done
CloseTermSession(devId, sid) 不存在→1sid 不匹配→2busy 时仅置 closed+notify交等待线程清理防双重擦除非 busy 直接擦+锁外 CancelIO
SweepIdleTerminals(idleTimeoutSec) 回收 persistent && !busy && difftime(now,lastActiveAt)>idle 的会话;收集后锁外 CancelIO

5.3 OnTerminalClosed 的泄漏修复

OnTerminalClosed两个入口

  1. MessageHandleTOKEN_TERMINAL_CLOSE 分支(客户端 shell 优雅退出时发 token
  2. OfflineProc2015RemoteDlg.cpp)——连接骤然断开(网络断/客户端崩溃,无 tokenWebService().OnTerminalClosed 并列调用 McpServer().OnTerminalClosed。此入口是 2026-08-24 独立审查发现缺失并补齐的:否则骤断的持久终端会话会在 m_TermSessions/m_TermContextToDevice 里留悬空 subCtx,连接池复用地地址后会误路由新连接、甚至 CancelIO 掉无关连接。

shell 进程退出时,若是一个空闲持久会话persistent && !busy && started),没有等待线程会清理它,会泄漏死 subCtx。故:

s.closed = true;
if (s.persistent && !s.busy && s.started) {
    m_TermContextToDevice.erase(subCtx);   // 空闲持久会话:直接擦(无需 CancelIO已死
    m_TermSessions.erase(sit);
}
m_TermCv.notify_all();   // busy 场景唤醒等待线程由其清理;一次性 exec 路径不变

6. 并发模型与不变量

6.1 关键不变量

  • CancelIO 一律在 m_TermMutex 之外(所有路径:BuildExecCommandBuildTerminalExecCloseTermSessionSweepIdleTerminals)。这是硬约束——CancelIO 可能阻塞/回调,锁内调用会死锁。
  • wait_for 期间不持有会被他人擦除的迭代器:等待线程(WaitTermCommand)持 it,但只有它自己会在超时/closed 路径擦除该会话;terminal_close/sweepbusy 标志而不会擦除「正在等待的命令」的会话。这就是 busy 标志的核心作用。
  • 单设备单终端terminal_*exec_command共享 m_TermSessions 而互斥(双向 -32003)。BeginTermPendingBeginTermOpen 都做「已有会话则 false」。

6.2 busy 标志生命周期

terminal_exec:  BeginTermCommand 置 busy=true
                Send2Client
                WaitTermCommand 返回时:
                  done 路径   → busy=false保留会话
                  closed 路径 → 会话被擦除
                  超时路径    → 会话被擦除
terminal_close会话忙时→ 仅置 closed=true + notify不擦交等待线程清理

6.3 互斥矩阵

exec_command terminal_* list_files/get_screenshot
exec_command 互斥(-32003 互斥(-32003 无关(各用 m_Pending
terminal_* 互斥 单设备单终端 无关

Web 终端与 MCP 终端各用独立 map理论上可同设备并存既有现象非本次范围

6.4 idle 回收语义

SweepIdleTerminals(300)lastActiveAtBeginTermOpen / BeginTermCommand **(命令开始时)**更新,命令完成后不刷新。因此「两次命令间隔 < 300s」的活跃会话安全「一次长命令 + 长时间无新命令」的会话会在最后一次命令开始 300s 后被回收。回收是非惰性的——每次 terminal_* 入口先 sweep所以空闲会话不会在无请求时被后台主动关闭但也不占后台定时器资源。用 difftime 比较防时钟回拨下溢。


7. 安全模型

维度 exec_command 持久终端
开关 McpReadonly=0 McpTerminal=1 McpReadonly=0(默认 McpTerminal=0
白名单 有(内置/自定义前缀白名单) (完整 shell可写命令、重定向、改环境
元字符门 `& <>^` + 控制字符全拒(安全门)
审计 WM_SHOWERRORMSG 标题「MCP命令执行」 标题「MCP持久终端」open/exec/close 各一条)
生命周期 命令结束即关 会话保持,闲置 300s 回收,terminal_close 显式关
会话隔离 单设备单终端 单设备单终端 + session_id 校验防串用

审计实现:PostMessageA(WM_SHOWERRORMSG, new CString(...), new CString(_TR("...标题..."))) → 进入服务端 m_MessageLogget_audit_log 可查。审计不可关闭。


8. 配置项

默认 说明
McpEnabled 0 MCP 总开关
McpPort 6544 监听端口
McpBind 127.0.0.1 绑定地址(默认仅回环)
McpToken (空→运行时兜底) Bearer token
McpReadonly 1 只读模式(禁 exec_command 与持久终端)
McpCmdWhitelist (空→内置白名单) exec_command 白名单,逗号分隔
McpTerminal 0 持久终端开关(要求 McpReadonly=0

存储位置Release 版存注册表 HKCU\Software\YAMA\settings不是 settings.inisettings.ini 仅 Debug 版读取)。改动需重启程序生效。

启动读取2015RemoteDlg.cpp

McpServer().SetReadonly(THIS_CFG.GetInt("settings", "McpReadonly", 1) != 0);
McpServer().SetCmdWhitelist(THIS_CFG.GetStr("settings", "McpCmdWhitelist", ""));
McpServer().SetTerminalEnabled(THIS_CFG.GetInt("settings", "McpTerminal", 0) != 0);

工具可见性terminal_* 三个工具在 tools/list仅在 IsTerminalEnabled() && !IsReadonly() 时出现(门控在 BuildToolsListResult)。故默认配置下 tools/list 看不到它们。


9. 错误码

含义 触发点
-32001 超时 WaitTerminalReady/WaitTermCommand/WaitPending 等超时
-32002 主机/会话不存在 FindMainContext 失败、session_id 不匹配/未就绪/已关
-32003 设备忙 单设备单终端冲突、同 host 已有挂起请求
-32004 发送失败 Send2Client 失败
-32005 非 Windows LNX/MAC 客户端
-32006 已禁用/只读 IsReadonly()!IsTerminalEnabled()
-32007 白名单拒绝 IsCommandAllowed false
-32008 含禁用字符 exec_command 的 `&
-32602 参数非法 ParseHostIdArg 失败、缺 required 参数
-32700 JSON 解析错误 请求体非合法 JSON-RPC

10. 排查指南(供后续人员/大模型)

10.1 exec_command 返回 -32001 超时

  • 先看是不是目标机环境问题而非代码 bugnslookup/where 在部分机器会因 DNS/文件系统异常稳定超时(见项目记忆)。裸 nslookup <host> 卡默认重试 20~40s > MCP 超时;where cmd.exe 在特定机器卡 PATH 解析。
  • 服务端行为正确:超时返回 -32001 并清理终端会话,不挂起/崩溃。

10.2 terminal_*tools/list 里看不到

  • 检查 McpTerminal=1 McpReadonly=0。任一不满足,工具不出现且调用返回 -32006
  • 改配置后需重启程序(注册表配置启动时读)。

10.3 terminal_exec 返回 -32002

  • 会话已被回收(闲置 >300s、shell 已退出、或 session_id 拼错。重新 terminal_open
  • terminal_close-32002 特指 session_id 不匹配(防串用)。

10.4 -32003 设备忙

  • 单设备单终端:同 host 已有终端会话(持久或一次性)在飞。等它结束或 terminal_close
  • 紧接 terminal_close(会话 busy 时)后立刻 terminal_open 可能偶发 -32003busy 会话的关闭是「标记 closed + 交等待线程清理」,清理完成前有极短窗口。重试一次即可,非死锁。

10.5 stdout 里出现命令本身 / 哨兵

  • 命令本身回显 = ConPTY 输入回显,已由 StripEchoedCommand 服务端剔除§2.4)。若仍出现,检查是否该行因编码(非 PTY 老管道的 GBKechoedLine 字节不一致而 find 不到——老管道本应无回显。
  • 哨兵 __MCP_DONE_... 出现 = 哨兵截断/清洗异常,通常意味着命令含 &| 破坏了控制操作符链(持久终端会提前 -32008 拒绝,一次性会 -32008 拒绝五元字符)。

10.6 中文/乱码

  • 输出乱码:检查 isPtycp 是否匹配PTY=UTF-8老管道=GBK
  • get_audit_log 中文乱码已知问题GBK 写入 UTF-8 JSON影响所有审计条目,非终端特有,暂未修复。

10.7 会话泄漏 / 子链接不关

  • 三个擦除点:WaitTermCommandclosed/超时)、CloseTermSessionSweepIdleTerminals;以及 OnTerminalClosed空闲持久会话的直接擦除§5.3)。排查时确认 shell 退出路径 OnTerminalClosed 是否命中、COMMAND_NEXT 是否漏发导致输出永不回流。

11. 关键源文件索引

文件 内容
server/2015Remote/McpServer.h TermSession 结构、BeginTermOpen/Command/WaitTermCommand/CloseTermSession/SweepIdleTerminals 声明、m_terminalEnabled
server/2015Remote/McpServer.cpp BuildExecCommand~1946BuildTerminalOpen/Exec/Close~2345、状态机方法~2660FindSentinel~2569StripEchoedCommand~1935SendTerminalCommandLine~2192CleanTerminalOutput~2206ResolveTerminalSessionHost~2160
server/2015Remote/McpSettingsDlg.h/.cpp IDC_MCP_TERMINAL 复选框、回填/落盘 McpTerminal
server/2015Remote/2015RemoteDlg.cpp 启动读 McpTerminalSetTerminalEnabled

构建由 VS2019MSBuild v143完成项目工具集 v142无法在外部环境构建。