Files
SimpleRemoter/server/2015Remote/McpMachine.cpp
T
yuanyuanxianganddeepseek-flash 38cd207f5d Feature: Add host management and connection management MCP tools
Expose the online-list TLS column through list_online_hosts, search_hosts
and get_host_detail. The value comes from the flag the client reports at
login, so old clients always read false.

Add list_network_connections and kill_network_connection on top of the
connection manager sub-connection protocol. The kill path matches the
original 110-byte record instead of rebuilding addresses, supports a dry
run, refuses UDP/IPv6 targets, and verifies that the quadruple really
disappeared instead of assuming success.

Add a host_* family backed by the machine manager protocol: ten read
tools covering every tab of the host management dialog, and four
recoverable write tools for process freeze/thaw, window restore/hide/
maximize/minimize, service start/stop/pause/continue/restart, and
scheduled task run/stop. Every write re-reads the snapshot before sending
and re-checks afterwards, reporting -32009 for stale targets rather than
acting blindly. The family is gated by McpMachine, the risky subset by
McpMachineDangerous, and writes additionally require read-only mode to
be off.

Route managed sub-connections from MessageHandle and OfflineProc, keep
the single-in-flight rule by sharing the pending table with a request
kind, and move the shared JSON/lock/encoding helpers into McpInternal so
both tool families use one implementation. Settings dialog, design docs
and en_US/zh_TW/de_DE translations are updated accordingly.

Co-Authored-By: deepseek-flash
2026-10-21 20:01:41 +02:00

2885 lines
120 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// McpMachine.cpp : MCP「连接管理」与「主机管理」工具族
//
// 连接管理:主连接 COMMAND_CONNECTION(253) → 客户端 CConnectionsManager 子连接回
// TOKEN_CONNECTION_LIST(255)。包格式 [255][u16 LE count][ConnectionInfo × count],
// ConnectionInfo 为 110 字节 packed 结构。强制断开 = 回发 [254][原始 110 字节]。
// 主机管理:主连接 COMMAND_MACHINE_MANAGE(162) → 客户端 CMachineManager 子连接回
// TOKEN_SYSINFOLIST(61) → 之后在同一子连接上发 1 字节 tab(0..9),回
// TOKEN_MACHINE_*(31..42)。动作 10..30 由客户端处理,多数动作会自动回一份
// 刷新后的列表(服务 = 40,任务 = 41),进程冻结/解冻与窗口操作不回包。
//
// 会话状态(m_Pending 中的 Machine / ConnMgr 条目)全部由 CMcpServer 持有;
// 本文件只负责工具 schema、参数校验、组包,以及响应解析。协议与设计见
// docs/Mcp_Machine_Design.md。
#include "stdafx.h"
#include "McpMachine.h"
#include "McpServer.h"
#include "McpInternal.h"
#include "context.h"
#include "2015RemoteDlg.h"
#include "LangManager.h"
#include <ws2tcpip.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string.h>
#include <functional>
#include <string>
#include <vector>
using namespace McpInternal;
namespace {
// ========== 错误码(与 McpServer.cpp 保持一致,另加 -32009) ==========
const int kErrGeneric = -32000;
const int kErrTimeout = -32001;
const int kErrOffline = -32002;
const int kErrBusy = -32003;
const int kErrSend = -32004;
const int kErrUnsupported = -32005;
const int kErrDisabled = -32006;
const int kErrNotFound = -32009; // 目标在最新快照中不存在(PID/HWND/服务/任务/连接)
// ========== 超时(ms) ==========
const int kReadyTimeoutMs = 5000; // 主连接发命令 → 子连接回就绪包
const int kListTimeoutMs = 12000; // tab 请求 → 列表响应(服务/软件枚举较慢)
const int kActionTimeoutMs = 12000; // 动作 → 客户端自动回的刷新列表
// 整个会话(含多步动作)的截止时间。restart 需要 prime + stop + start 三步,
// 每步最多 12s,故留 60s 余量;单步等待再按剩余时间收敛。
const int kSessionDeadlineMs = 60000;
// 单个包的上限:正常列表远小于此;超过即视为异常,置 clientError 并断开子连接。
const size_t kMaxPacketBytes = 8u * 1024u * 1024u;
const size_t kConnRecordSize = sizeof(ConnectionInfo);
static_assert(sizeof(ConnectionInfo) == 110, "ConnectionInfo layout changed; update the parser");
static_assert(sizeof(WINDOWSINFO) == 1044, "WINDOWSINFO layout changed; update the parser");
// ========== 小工具 ==========
std::string Trim(const std::string& s) {
size_t b = 0, e = s.size();
while (b < e && (s[b] == ' ' || s[b] == '\t')) ++b;
while (e > b && (s[e - 1] == ' ' || s[e - 1] == '\t')) --e;
return s.substr(b, e - b);
}
bool MatchCI(const std::string& value, const std::string& needle) {
return needle.empty() || ContainsCI(value, needle);
}
bool EqualsCI(const std::string& a, const std::string& b) {
return a.size() == b.size() && _stricmp(a.c_str(), b.c_str()) == 0;
}
bool GetBoolArg(const Json::Value& args, const char* key, bool def) {
if (!args.isMember(key)) return def;
const Json::Value& v = args[key];
if (v.isBool()) return v.asBool();
if (v.isNumeric()) return v.asInt() != 0;
if (v.isString()) {
std::string s = ToLowerAscii(v.asString());
return s == "1" || s == "true" || s == "yes";
}
return def;
}
// 可选整数:缺省不算错;类型/范围错写 errJson。
bool GetOptIntArg(const Json::Value& id, const Json::Value& args, const char* key,
int minV, int maxV, int& out, std::string& errJson) {
if (!args.isMember(key)) return true;
int v = 0;
if (!GetIntArg(args, key, v)) {
errJson = BuildError(id, -32602, std::string("Invalid parameter '") + key + "': expected an integer");
return false;
}
if (v < minV || v > maxV) {
errJson = BuildError(id, -32602, std::string("Parameter '") + key + "' out of range [" +
std::to_string(minV) + ", " + std::to_string(maxV) + "]");
return false;
}
out = v;
return true;
}
std::string OptStr(const Json::Value& args, const char* key) {
return Trim(GetStringArg(args, key));
}
// 严格 UTF-8 → 目标代码页:任一字符无法表示即失败(不用 '?' 顶替)。
bool Utf8ToCpExact(const std::string& utf8, UINT cp, std::string& out) {
out.clear();
if (utf8.empty()) return true;
int wlen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8.c_str(), (int)utf8.size(), NULL, 0);
if (wlen <= 0) return false;
std::wstring w(wlen, L'\0');
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8.c_str(), (int)utf8.size(),
&w[0], wlen) != wlen)
return false;
BOOL usedDefault = FALSE;
int alen = WideCharToMultiByte(cp, WC_NO_BEST_FIT_CHARS, w.c_str(), wlen, NULL, 0, NULL, &usedDefault);
if (alen <= 0 || usedDefault) return false;
out.assign(alen, '\0');
usedDefault = FALSE;
if (WideCharToMultiByte(cp, WC_NO_BEST_FIT_CHARS, w.c_str(), wlen, &out[0], alen,
NULL, &usedDefault) != alen || usedDefault) {
out.clear();
return false;
}
return true;
}
bool IsValidUtf8(const char* s, size_t n) {
if (!s || n == 0) return true;
int wlen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s, (int)n, NULL, 0);
return wlen > 0;
}
bool IsWindowsClient(context* ctx) {
if (!ctx) return false;
CString t = ctx->GetAdditionalData(RES_CLIENT_TYPE);
return !(t == "LNX" || t == "MAC" || t == "APK");
}
std::string EndpointAddr(const std::string& ep) {
size_t c = ep.rfind(':');
return c == std::string::npos ? ep : ep.substr(0, c);
}
int EndpointPort(const std::string& ep) {
size_t c = ep.rfind(':');
if (c == std::string::npos) return 0;
std::string p = ep.substr(c + 1);
return IsDigits(p) ? atoi(p.c_str()) : 0;
}
const char* TcpStateName(DWORD state) {
switch (state) {
case 1: return "CLOSED";
case 2: return "LISTEN";
case 3: return "SYN_SENT";
case 4: return "SYN_RCVD";
case 5: return "ESTABLISHED";
case 6: return "FIN_WAIT1";
case 7: return "FIN_WAIT2";
case 8: return "CLOSE_WAIT";
case 9: return "CLOSING";
case 10: return "LAST_ACK";
case 11: return "TIME_WAIT";
case 12: return "DELETE_TCB";
default: return "";
}
}
const char* PriorityName(DWORD pri) {
switch (pri) {
case 0x00000040: return "Idle";
case 0x00004000: return "BelowNormal";
case 0x00000020: return "Normal";
case 0x00008000: return "AboveNormal";
case 0x00000080: return "High";
case 0x00000100: return "Realtime";
default: return "";
}
}
// OLE Automation DATE → "YYYY-MM-DD hh:mm:ss"(与 sys/MachineDlg.cpp:oleTime2Str 完全一致)
std::string OleDateToStr(double t) {
if (t == 0.0) return "";
time_t tt = (time_t)(t * 24.0 * 3600.0 - 2209190400.0);
struct tm tm1;
if (localtime_s(&tm1, &tt) != 0) return "";
char buf[32] = { 0 };
sprintf_s(buf, "%04d-%02d-%02d %02d:%02d:%02d",
tm1.tm_year + 1900, tm1.tm_mon + 1, tm1.tm_mday,
tm1.tm_hour, tm1.tm_min, tm1.tm_sec);
return buf;
}
// ========== schema 辅助 ==========
Json::Value PropStr(const char* desc = nullptr) {
Json::Value p(Json::objectValue);
p["type"] = "string";
if (desc) p["description"] = desc;
return p;
}
Json::Value PropInt(const char* desc = nullptr) {
Json::Value p(Json::objectValue);
p["type"] = "integer";
if (desc) p["description"] = desc;
return p;
}
Json::Value PropBool(const char* desc = nullptr) {
Json::Value p(Json::objectValue);
p["type"] = "boolean";
if (desc) p["description"] = desc;
return p;
}
Json::Value PropStrEnum(const char* desc, std::initializer_list<const char*> values) {
Json::Value p = PropStr(desc);
Json::Value enums(Json::arrayValue);
for (const char* v : values) enums.append(v);
p["enum"] = enums;
return p;
}
Json::Value MakeSchema(const Json::Value& props, std::initializer_list<const char*> required) {
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
if (required.size() > 0) {
Json::Value req(Json::arrayValue);
for (const char* r : required) req.append(r);
schema["required"] = req;
}
return schema;
}
// 列表类 outputSchema:{ <arrayKey>: [...], total, totalAll, offset, limit, hasMore }
Json::Value MakeListOutputSchema(const char* arrayKey, const Json::Value& itemProps) {
Json::Value props(Json::objectValue);
Json::Value arr(Json::objectValue);
arr["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
items["properties"] = itemProps;
arr["items"] = items;
props[arrayKey] = arr;
props["total"] = PropInt(u8"过滤后的条目数");
props["totalAll"] = PropInt(u8"未过滤的条目总数");
props["offset"] = PropInt();
props["limit"] = PropInt();
props["hasMore"] = PropBool();
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value req(Json::arrayValue);
req.append(arrayKey);
schema["required"] = req;
return schema;
}
Json::Value HostIdProp() {
return PropStr(u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段");
}
// 所有读工具的公共分页参数
void AddPagingProps(Json::Value& props) {
props["offset"] = PropInt(u8"分页起点,默认 0");
props["limit"] = PropInt(u8"本次最多返回条数,默认 100,上限 1000");
}
Json::Value ActionOutputSchema() {
Json::Value props(Json::objectValue);
props["ok"] = PropBool(u8"动作是否达到预期(false 表示未生效/无法确认,不会伪报成功)");
props["action"] = PropStr();
props["target"] = PropStr();
props["verified"] = PropBool(u8"是否已用动作后的最新快照复核");
props["changed"] = PropBool(u8"是否真的改变了状态(如服务本来就是目标状态时为 false)");
props["state"] = PropStr(u8"复核时观测到的状态");
props["reason"] = PropStr(u8"ok=false 时的原因");
props["message"] = PropStr(u8"人类可读的说明");
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value req(Json::arrayValue);
req.append("ok");
schema["required"] = req;
return schema;
}
std::string BuildStructuredResult(const Json::Value& id, const Json::Value& sc,
const std::string& text) {
Json::Value result(Json::objectValue);
result["structuredContent"] = sc;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = text;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
std::string BuildListResult(const Json::Value& id, const char* arrayKey,
const Json::Value& page, int total, int totalAll,
int offset, int limit, bool hasMore,
const std::string& text) {
Json::Value sc(Json::objectValue);
sc[arrayKey] = page;
sc["total"] = total;
sc["totalAll"] = totalAll;
sc["offset"] = offset;
sc["limit"] = limit;
sc["hasMore"] = hasMore;
return BuildStructuredResult(id, sc, text);
}
std::string BuildActionResult(const Json::Value& id, bool ok, bool verified, bool changed,
const std::string& action, const std::string& target,
const std::string& state, const std::string& reason,
const std::string& message) {
Json::Value sc(Json::objectValue);
sc["ok"] = ok;
sc["action"] = action;
sc["target"] = target;
sc["verified"] = verified;
sc["changed"] = changed;
sc["state"] = state;
if (!reason.empty()) sc["reason"] = reason;
sc["message"] = message;
std::string text = std::string(ok ? "OK: " : "NOT OK: ") + action;
if (!target.empty()) text += " " + target;
if (!state.empty()) text += " (state=" + state + ")";
if (!reason.empty()) text += " reason=" + reason;
return BuildStructuredResult(id, sc, text);
}
// ========== 分页与过滤 ==========
struct Paging {
int offset = 0;
int limit = 100;
};
bool ParsePaging(const Json::Value& id, const Json::Value& args, Paging& p, std::string& errJson) {
if (!GetOptIntArg(id, args, "offset", 0, 100000000, p.offset, errJson)) return false;
if (!GetOptIntArg(id, args, "limit", 1, 1000, p.limit, errJson)) return false;
return true;
}
template <typename Pred>
Json::Value FilterAndPage(const Json::Value& all, Pred pred, const Paging& p,
int& totalFiltered, bool& hasMore) {
std::vector<Json::Value> keep;
keep.reserve(all.size());
for (Json::ArrayIndex i = 0; i < all.size(); ++i) {
if (pred(all[i])) keep.push_back(all[i]);
}
totalFiltered = (int)keep.size();
int begin = p.offset < totalFiltered ? p.offset : totalFiltered;
int end = begin + p.limit;
if (end > totalFiltered) end = totalFiltered;
Json::Value page(Json::arrayValue);
for (int i = begin; i < end; ++i) page.append(keep[i]);
hasMore = end < totalFiltered;
return page;
}
// ========== 报文读取器(显式偏移 + 有界,禁止未对齐指针强转) ==========
class Reader {
public:
explicit Reader(const std::vector<BYTE>& v) : m_p(v.data()), m_len(v.size()) {}
size_t Offset() const { return m_off; }
size_t Remaining() const { return m_off <= m_len ? m_len - m_off : 0; }
bool Need(size_t n) const { return Remaining() >= n; }
bool U8(BYTE& v) {
if (!Need(1)) return false;
v = m_p[m_off++];
return true;
}
bool U32(DWORD& v) {
if (!Need(4)) return false;
memcpy(&v, m_p + m_off, 4);
m_off += 4;
return true;
}
bool Dates(double& a, double& b) {
if (!Need(16)) return false;
memcpy(&a, m_p + m_off, 8);
memcpy(&b, m_p + m_off + 8, 8);
m_off += 16;
return true;
}
bool Skip(size_t n) {
if (!Need(n)) return false;
m_off += n;
return true;
}
// 剩余字节是否全为 0。客户端用 LocalSize() 决定发送长度,分配粒度可能带出少量
// 尾随 0 填充;记录解析循环据此提前结束,而不是把填充误判成截断。
bool AllZero() const {
for (size_t i = m_off; i < m_len; ++i) {
if (m_p[i] != 0) return false;
}
return true;
}
// 读取 NUL 结尾字符串(最多 maxBytes),推进 n + 1 + skipExtra 字节。
// 找不到 NUL 视为截断。
bool Str(UINT cp, size_t maxBytes, size_t skipExtra, std::string& out) {
size_t avail = Remaining();
if (avail > maxBytes) avail = maxBytes;
if (avail == 0) return false;
size_t n = BoundedStrlen((const char*)m_p + m_off, avail);
if (n == avail) return false; // 没有 NUL → 截断
if (!Need(n + 1 + skipExtra)) return false; // 尾随的 pad 字节缺失
out = ToUtf8((const char*)m_p + m_off, cp);
m_off += n + 1 + skipExtra;
return true;
}
// 定长字符串字段(如 char title[260]),字段内以 NUL 结尾或被 pad 填满。
bool FixedStr(UINT cp, size_t fixed, std::string& out) {
if (!Need(fixed)) return false;
char buf[1025];
size_t n = fixed < sizeof(buf) - 1 ? fixed : sizeof(buf) - 1;
memcpy(buf, m_p + m_off, n);
buf[n] = 0;
out = ToUtf8(buf, cp);
m_off += fixed;
return true;
}
private:
const BYTE* m_p;
size_t m_len;
size_t m_off = 0;
};
// ========== 解析:主机管理各 tab ==========
bool ParseProcesses(const std::vector<BYTE>& data, UINT cp, Json::Value& out, std::string& err) {
out = Json::Value(Json::arrayValue);
if (data.size() <= 1) return true; // 只有 token = 空列表
Reader r(data);
if (!r.Skip(1)) { err = "empty process packet"; return false; }
while (r.Remaining() > 0) {
if (r.AllZero()) break; // 末尾 0 填充
size_t start = r.Offset();
DWORD pid = 0;
BYTE is64 = 0;
std::string title, exe, path, user;
DWORD pri = 0, threads = 0, workingSetKb = 0, fileSizeKb = 0;
if (!r.U32(pid) || !r.U8(is64) ||
!r.FixedStr(cp, MAX_PATH, title) ||
!r.Str(cp, r.Remaining(), 1, exe) ||
!r.Str(cp, r.Remaining(), 1, path) ||
!r.U32(pri) || !r.U32(threads) ||
!r.Str(cp, r.Remaining(), 1, user) ||
!r.U32(workingSetKb) || !r.U32(fileSizeKb)) {
err = "truncated process record at offset " + std::to_string(start);
return false;
}
Json::Value item(Json::objectValue);
item["pid"] = (Json::UInt)pid;
item["arch"] = is64 ? "x64" : "x86";
item["title"] = title;
item["name"] = exe;
item["path"] = path;
item["priorityClass"] = (Json::UInt)pri;
item["priority"] = PriorityName(pri);
item["threads"] = (Json::UInt)threads;
item["user"] = user;
item["workingSetKb"] = (Json::UInt)workingSetKb;
item["fileSizeKb"] = (Json::UInt)fileSizeKb;
out.append(item);
}
return true;
}
bool ParseWindows(const std::vector<BYTE>& data, UINT cp, Json::Value& out, std::string& err) {
out = Json::Value(Json::arrayValue);
if (data.size() <= 1) return true;
size_t payload = data.size() - 1;
// 记录是定长的;若末尾有不足一条记录的多余字节(分配粒度带来的 0 填充),忽略即可。
size_t count = payload / sizeof(WINDOWSINFO);
for (size_t i = 0; i < count; ++i) {
WINDOWSINFO info;
memcpy(&info, data.data() + 1 + i * sizeof(WINDOWSINFO), sizeof(info));
char title[sizeof(info.strTitle) + 1] = { 0 };
memcpy(title, info.strTitle, sizeof(info.strTitle));
Json::Value item(Json::objectValue);
item["hwnd"] = (Json::UInt)info.m_hwnd;
item["title"] = ToUtf8(title, cp);
item["pid"] = (Json::UInt)info.m_poceessid;
item["width"] = info.w;
item["height"] = info.h;
item["canSee"] = info.canlook ? true : false;
out.append(item);
}
return true;
}
bool ParseNetState(const std::vector<BYTE>& data, UINT cp, Json::Value& out, std::string& err) {
out = Json::Value(Json::arrayValue);
if (data.size() <= 1) return true;
Reader r(data);
if (!r.Skip(1)) { err = "empty netstate packet"; return false; }
while (r.Remaining() > 0) {
if (r.AllZero()) break; // 末尾 0 填充
size_t start = r.Offset();
std::string proc, proto, local, remote, state;
DWORD pid = 0;
if (!r.Str(cp, r.Remaining(), 1, proc) ||
!r.U32(pid) || !r.Skip(2) ||
!r.Str(cp, r.Remaining(), 1, proto) ||
!r.Str(cp, r.Remaining(), 1, local) ||
!r.Str(cp, r.Remaining(), 1, remote) ||
!r.Str(cp, r.Remaining(), 1, state)) {
err = "truncated netstate record at offset " + std::to_string(start);
return false;
}
std::string protocol = (proto == "[UDP]") ? "udp" : "tcp";
Json::Value item(Json::objectValue);
item["process"] = proc;
item["pid"] = (Json::UInt)pid;
item["protocol"] = protocol;
item["localEndpoint"] = local;
item["localAddress"] = EndpointAddr(local);
item["localPort"] = EndpointPort(local);
item["remoteEndpoint"] = remote;
item["remoteAddress"] = EndpointAddr(remote);
item["remotePort"] = EndpointPort(remote);
item["state"] = Trim(state);
out.append(item);
}
return true;
}
bool ParseSoftware(const std::vector<BYTE>& data, UINT cp, Json::Value& out, std::string& err) {
out = Json::Value(Json::arrayValue);
if (data.size() <= 1) return true;
Reader r(data);
if (!r.Skip(1)) { err = "empty software packet"; return false; }
while (r.Remaining() > 0) {
if (r.AllZero()) break; // 末尾 0 填充
size_t start = r.Offset();
std::string name, publisher, version, installDate, uninstall;
if (!r.Str(cp, r.Remaining(), 1, name) ||
!r.Str(cp, r.Remaining(), 1, publisher) ||
!r.Str(cp, r.Remaining(), 1, version) ||
!r.Str(cp, r.Remaining(), 1, installDate) ||
!r.Str(cp, r.Remaining(), 1, uninstall)) {
err = "truncated software record at offset " + std::to_string(start);
return false;
}
Json::Value item(Json::objectValue);
item["name"] = name;
item["publisher"] = publisher;
item["version"] = version;
item["installDate"] = installDate;
item["uninstallString"] = uninstall;
out.append(item);
}
return true;
}
bool ParseBrowserHistory(const std::vector<BYTE>& data, UINT cp, Json::Value& out, std::string& err) {
out = Json::Value(Json::arrayValue);
if (data.size() <= 1) return true;
size_t payload = data.size() - 1;
const size_t kRec = 100 + 1024 + 1024; // Browsinghistory
size_t count = payload / kRec; // 末尾不足一条记录的字节忽略
for (size_t i = 0; i < count; ++i) {
const BYTE* p = data.data() + 1 + i * kRec;
char t[101] = { 0 }, title[1025] = { 0 }, url[1025] = { 0 };
memcpy(t, p, 100);
memcpy(title, p + 100, 1024);
memcpy(url, p + 100 + 1024, 1024);
Json::Value item(Json::objectValue);
item["time"] = t; // 客户端已格式化为 ASCII 时间
item["title"] = ToUtf8(title, cp);
item["url"] = ToUtf8(url, cp);
out.append(item);
}
return true;
}
bool ParseFavorites(const std::vector<BYTE>& data, UINT cp, Json::Value& out, std::string& err) {
out = Json::Value(Json::arrayValue);
if (data.size() <= 1) return true;
Reader r(data);
if (!r.Skip(1)) { err = "empty favorites packet"; return false; }
while (r.Remaining() > 0) {
if (r.AllZero()) break; // 末尾 0 填充
size_t start = r.Offset();
std::string name, url;
if (!r.Str(cp, r.Remaining(), 1, name) ||
!r.Str(cp, r.Remaining(), 1, url)) {
err = "truncated favorites record at offset " + std::to_string(start);
return false;
}
Json::Value item(Json::objectValue);
item["name"] = name;
item["url"] = url;
out.append(item);
}
return true;
}
bool ParseServices(const std::vector<BYTE>& data, UINT cp, Json::Value& out, std::string& err) {
out = Json::Value(Json::arrayValue);
if (data.size() <= 1) return true;
Reader r(data);
if (!r.Skip(1)) { err = "empty service packet"; return false; }
while (r.Remaining() > 0) {
if (r.AllZero()) break; // 末尾 0 填充
size_t start = r.Offset();
std::string displayName, description, state, startType, login, interactive, name, binaryPath;
if (!r.Str(cp, r.Remaining(), 0, displayName) ||
!r.Str(cp, r.Remaining(), 0, description) ||
!r.Str(cp, r.Remaining(), 0, state) ||
!r.Str(cp, r.Remaining(), 0, startType) ||
!r.Str(cp, r.Remaining(), 0, login) ||
!r.Str(cp, r.Remaining(), 0, interactive) ||
!r.Str(cp, r.Remaining(), 0, name) ||
!r.Str(cp, r.Remaining(), 0, binaryPath)) {
err = "truncated service record at offset " + std::to_string(start);
return false;
}
Json::Value item(Json::objectValue);
item["displayName"] = displayName;
item["description"] = description;
item["state"] = state;
item["startType"] = startType;
item["login"] = login;
item["interactive"] = (interactive == u8"是");
item["name"] = name;
item["binaryPath"] = binaryPath;
out.append(item);
}
return true;
}
// 任务记录:name/path/exe 为 UTF-8,status 为 ANSI(GBK),各以 NUL 结尾;
// 之后两个 DATE(double)。列表以两个 0 字节结尾。
bool ParseTasks(const std::vector<BYTE>& data, Json::Value& out, std::string& err) {
out = Json::Value(Json::arrayValue);
if (data.size() <= 2) return true; // 只有 token(可能带 1 个结束 0)
Reader r(data);
if (!r.Skip(1)) { err = "empty task packet"; return false; }
while (r.Remaining() > 2) {
if (data[r.Offset()] == 0) break; // 结束标记(两个 0 字节)
size_t start = r.Offset();
std::string name, path, exe, status;
double last = 0, next = 0;
if (!r.Str(CP_UTF8, r.Remaining(), 0, name) ||
!r.Str(CP_UTF8, r.Remaining(), 0, path) ||
!r.Str(CP_UTF8, r.Remaining(), 0, exe) ||
!r.Str(936, r.Remaining(), 0, status) ||
!r.Dates(last, next)) {
err = "truncated task record at offset " + std::to_string(start);
return false;
}
if (name.empty()) break; // 防御:空名视为结束
// folder = path 去掉末尾 "\<name>";不匹配时退回最后一个反斜杠之前。
std::string folder;
std::string suffix = "\\" + name;
if (path.size() >= suffix.size() &&
_stricmp(path.c_str() + path.size() - suffix.size(), suffix.c_str()) == 0) {
folder = path.substr(0, path.size() - suffix.size());
} else {
size_t pos = path.rfind('\\');
folder = (pos == std::string::npos) ? "\\" : path.substr(0, pos);
}
if (folder.empty()) folder = "\\";
Json::Value item(Json::objectValue);
item["name"] = name;
item["path"] = path;
item["folder"] = folder;
item["exe"] = exe;
item["status"] = status;
item["lastRun"] = OleDateToStr(last);
item["nextRun"] = OleDateToStr(next);
out.append(item);
}
return true;
}
// ========== 解析:连接管理 ==========
std::string FormatAddr(uint8_t af, const uint8_t* addr) {
char buf[INET6_ADDRSTRLEN] = { 0 };
if (af == AF_INET) {
inet_ntop(AF_INET, addr, buf, INET_ADDRSTRLEN);
} else if (af == AF_INET6) {
inet_ntop(AF_INET6, addr, buf, INET6_ADDRSTRLEN);
}
return buf;
}
struct ConnRecord {
const BYTE* raw = nullptr; // 指向 data 中的原始 110 字节(强制断开原样回传)
int proto = 0;
int af = 0;
uint32_t pid = 0;
uint8_t localAddr[16] = { 0 };
uint16_t localPort = 0;
uint8_t remoteAddr[16] = { 0 };
uint16_t remotePort = 0;
uint32_t state = 0;
std::string procName;
};
// 解析连接列表。data 首字节必须是 TOKEN_CONNECTION_LIST。
bool ParseConnections(const std::vector<BYTE>& data,
std::vector<ConnRecord>& records, std::string& err) {
records.clear();
if (data.empty()) { err = "empty connection packet"; return false; }
if (data[0] != TOKEN_CONNECTION_LIST) {
err = "unexpected token " + std::to_string((int)data[0]) + " for connection list";
return false;
}
if (data.size() < 3) { err = "connection packet too short"; return false; }
uint16_t count = 0;
memcpy(&count, data.data() + 1, 2);
size_t need = 3 + (size_t)count * kConnRecordSize;
if (data.size() < need) {
err = "connection packet truncated: declared " + std::to_string(count) +
" records but only " + std::to_string(data.size() - 3) + " bytes";
return false;
}
for (uint16_t i = 0; i < count; ++i) {
const BYTE* p = data.data() + 3 + (size_t)i * kConnRecordSize;
ConnRecord rec;
rec.raw = p;
rec.proto = p[0];
rec.af = p[1];
memcpy(&rec.pid, p + 2, 4);
memcpy(rec.localAddr, p + 6, 16);
memcpy(&rec.localPort, p + 22, 2);
memcpy(rec.remoteAddr, p + 24, 16);
memcpy(&rec.remotePort, p + 40, 2);
memcpy(&rec.state, p + 42, 4);
char name[65] = { 0 };
memcpy(name, p + 46, 64);
rec.procName = name; // 客户端写的是 UTF-8
records.push_back(rec);
}
return true;
}
Json::Value ConnRecordToJson(const ConnRecord& rec) {
Json::Value item(Json::objectValue);
item["protocol"] = (rec.proto == 1) ? "tcp" : (rec.proto == 2 ? "udp" : "unknown");
item["family"] = (rec.af == AF_INET) ? "ipv4" : (rec.af == AF_INET6 ? "ipv6" : "unknown");
item["pid"] = (Json::UInt)rec.pid;
item["process"] = rec.procName;
item["localAddress"] = FormatAddr((uint8_t)rec.af, rec.localAddr);
item["localPort"] = (Json::UInt)rec.localPort;
item["remoteAddress"] = FormatAddr((uint8_t)rec.af, rec.remoteAddr);
item["remotePort"] = (Json::UInt)rec.remotePort;
item["state"] = (Json::UInt)rec.state;
item["stateName"] = (rec.proto == 1) ? TcpStateName(rec.state) : "";
item["killable"] = (rec.proto == 1 && rec.af == AF_INET);
return item;
}
// ========== 主机解析 / 会话封装 ==========
struct HostTool {
uint64_t dev = 0;
context* mainCtx = nullptr;
};
// requireWindows_ = true 时对 LNX/MAC/APK 直接返回 -32005,避免白等超时。
bool ResolveHost(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent,
HostTool& ht, bool requireWindows_, std::string& errJson) {
uint64_t dev = 0;
std::string err;
if (!ParseHostIdArg(args, dev, err)) {
errJson = BuildError(id, -32602, err);
return false;
}
context* ctx = FindMainContext(parent, dev);
if (!ctx) {
errJson = BuildError(id, kErrOffline, "Host not found or offline: " + std::to_string(dev));
return false;
}
if (requireWindows_ && !IsWindowsClient(ctx)) {
errJson = BuildError(id, kErrUnsupported, "This tool is only supported on Windows hosts");
return false;
}
ht.dev = dev;
ht.mainCtx = ctx;
return true;
}
std::string WaitFailJson(const Json::Value& id, CMcpServer::ManagedWaitResult r,
const std::string& clientErr, const char* what, bool readyPhase) {
switch (r) {
case CMcpServer::ManagedWaitResult::ClientError:
return BuildError(id, kErrGeneric, std::string("Client reported an error: ") +
(clientErr.empty() ? std::string("(no detail)") : clientErr));
case CMcpServer::ManagedWaitResult::Closed:
return BuildError(id, kErrTimeout, std::string("Sub-connection closed while waiting for ") + what);
case CMcpServer::ManagedWaitResult::Timeout:
default: {
std::string msg = std::string("Timeout waiting for ") + what;
if (readyPhase) {
msg += " (host may be offline, the feature may be disabled on the client, "
"or the client is too old to support it)";
}
return BuildError(id, kErrTimeout, msg);
}
}
}
// 一次主机管理 / 连接管理会话。析构时若未正常收尾,按失败路径打墓碑并关子连接。
class ManagedSession {
public:
ManagedSession() {}
~ManagedSession() { Abort(); }
ManagedSession(const ManagedSession&) = delete;
ManagedSession& operator=(const ManagedSession&) = delete;
bool Open(const Json::Value& id, const std::string& tool, const HostTool& ht,
bool connMgr, std::vector<BYTE>* firstPacket, std::string& errJson) {
m_dev = ht.dev;
CMcpServer& mcp = McpServer();
bool began = connMgr ? mcp.BeginConnMgrSession(m_dev, tool, ht.mainCtx)
: mcp.BeginMachineSession(m_dev, tool, ht.mainCtx);
if (!began) {
errJson = BuildError(id, kErrBusy,
"Device busy: another MCP request is pending for this host");
return false;
}
m_open = true;
BYTE cmd = connMgr ? (BYTE)COMMAND_CONNECTION : (BYTE)COMMAND_MACHINE_MANAGE;
if (!ht.mainCtx->Send2Client(&cmd, 1)) {
errJson = BuildError(id, kErrSend, "Failed to send command to host");
return false; // 析构 → Abort(打墓碑)
}
BYTE readyToken = connMgr ? (BYTE)TOKEN_CONNECTION_LIST : (BYTE)TOKEN_SYSINFOLIST;
std::vector<BYTE> ready;
std::string clientErr;
CMcpServer::ManagedWaitResult r =
mcp.WaitManagedPacket(m_dev, readyToken, ready, m_sub, kReadyTimeoutMs, clientErr);
if (r != CMcpServer::ManagedWaitResult::Ok) {
errJson = WaitFailJson(id, r, clientErr,
connMgr ? "connection list" : "machine manager readiness",
true);
return false;
}
if (firstPacket) *firstPacket = ready;
return true;
}
// 发 1 字节 tab 请求并等待指定响应 token。
bool Request(const Json::Value& id, BYTE req, BYTE respToken, std::vector<BYTE>& out,
std::string& errJson) {
CMcpServer& mcp = McpServer();
mcp.ArmManagedResponse(m_dev, respToken);
if (!m_sub || !m_sub->Send2Client(&req, 1)) {
errJson = BuildError(id, kErrSend, "Failed to send request to host");
return false;
}
std::string clientErr;
CMcpServer::ManagedWaitResult r =
mcp.WaitManagedPacket(m_dev, respToken, out, m_sub, kListTimeoutMs, clientErr);
if (r != CMcpServer::ManagedWaitResult::Ok) {
errJson = WaitFailJson(id, r, clientErr, "host management response", false);
return false;
}
return true;
}
// 发一个动作包并等待指定的刷新响应(客户端自动回)。
bool Action(const Json::Value& id, std::vector<BYTE>& pkt, BYTE respToken,
std::vector<BYTE>& out, std::string& errJson) {
CMcpServer& mcp = McpServer();
mcp.ArmManagedResponse(m_dev, respToken);
if (!m_sub || !m_sub->Send2Client(pkt.data(), (ULONG)pkt.size())) {
errJson = BuildError(id, kErrSend, "Failed to send action to host");
return false;
}
m_sub->WaitSendIdle(2000);
std::string clientErr;
CMcpServer::ManagedWaitResult r =
mcp.WaitManagedPacket(m_dev, respToken, out, m_sub, kActionTimeoutMs, clientErr);
if (r != CMcpServer::ManagedWaitResult::Ok) {
errJson = WaitFailJson(id, r, clientErr, "action confirmation", false);
return false;
}
return true;
}
// 发一个无响应的动作包(进程冻结/解冻、窗口操作)。
bool SendNoReply(const Json::Value& id, std::vector<BYTE>& pkt, std::string& errJson) {
if (!m_sub || !m_sub->Send2Client(pkt.data(), (ULONG)pkt.size())) {
errJson = BuildError(id, kErrSend, "Failed to send action to host");
return false;
}
m_sub->WaitSendIdle(2000);
return true;
}
void Finish() {
if (!m_open) return;
McpServer().EndManagedSession(m_dev, true, false);
m_open = false;
}
void Abort() {
if (!m_open) return;
McpServer().EndManagedSession(m_dev, true, true);
m_open = false;
}
uint64_t Dev() const { return m_dev; }
context* Sub() const { return m_sub; }
private:
bool m_open = false;
uint64_t m_dev = 0;
context* m_sub = nullptr;
};
// 目标过期:-32009,提示调用方刷新快照后重试。
std::string NotFoundJson(const Json::Value& id, const std::string& what) {
return BuildError(id, kErrNotFound,
what + " does not exist in the latest snapshot; refresh the list and retry");
}
} // namespace
// ============================================================================
// CMcpServer:P8 会话状态机(在 UI 线程 / 工具线程之间共享)
// ============================================================================
bool CMcpServer::KindAcceptsToken(PendingKind kind, BYTE token) {
if (kind == PendingKind::Machine) return token == (BYTE)TOKEN_SYSINFOLIST;
if (kind == PendingKind::ConnMgr) return token == (BYTE)TOKEN_CONNECTION_LIST;
return false;
}
void CMcpServer::RefreshManagedRoutesLocked() {
m_ManagedRoutes.store((int)(m_SubCtxToDevice.size() + m_AbsorbedSubCtx.size()),
std::memory_order_relaxed);
}
void CMcpServer::PruneOrphanMarksLocked(std::chrono::steady_clock::time_point now) {
for (auto it = m_OrphanByDevice.begin(); it != m_OrphanByDevice.end();) {
if (it->second.expiry <= now) it = m_OrphanByDevice.erase(it);
else ++it;
}
for (auto it = m_OrphanByPeer.begin(); it != m_OrphanByPeer.end();) {
if (it->second.expiry <= now) it = m_OrphanByPeer.erase(it);
else ++it;
}
}
bool CMcpServer::BeginManagedSession(uint64_t device_id, const std::string& tool,
PendingKind kind, context* mainCtx) {
if (IsFileTransferPending(device_id)) return false; // 文件传输在飞:单设备单飞行
std::lock_guard<std::mutex> lk(m_PendingMutex);
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
PendingRequest r;
r.tool = tool;
r.kind = kind;
r.mainCtx = mainCtx;
r.peer = mainCtx ? mainCtx->GetPeerName() : "";
r.deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(kSessionDeadlineMs);
m_Pending[device_id] = std::move(r);
return true;
}
bool CMcpServer::BeginMachineSession(uint64_t device_id, const std::string& tool, context* mainCtx) {
return BeginManagedSession(device_id, tool, PendingKind::Machine, mainCtx);
}
bool CMcpServer::BeginConnMgrSession(uint64_t device_id, const std::string& tool, context* mainCtx) {
return BeginManagedSession(device_id, tool, PendingKind::ConnMgr, mainCtx);
}
void CMcpServer::ArmManagedResponse(uint64_t device_id, BYTE expectedToken) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return;
if (it->second.kind != PendingKind::Machine && it->second.kind != PendingKind::ConnMgr) return;
it->second.expectedToken = expectedToken;
it->second.hasResponse = false;
it->second.response.clear();
it->second.clientError.clear();
}
CMcpServer::ManagedWaitResult CMcpServer::WaitManagedPacket(uint64_t device_id, BYTE token,
std::vector<BYTE>& out,
context*& subCtxOut,
int timeoutMs, std::string& clientError) {
std::unique_lock<std::mutex> lk(m_PendingMutex);
auto waitUntil = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
// 循环等待:迟到的/无关的包被丢弃后继续等,直到本次步骤超时或会话截止时间。
for (;;) {
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return ManagedWaitResult::Closed;
PendingRequest& r = it->second;
if (r.kind != PendingKind::Machine && r.kind != PendingKind::ConnMgr)
return ManagedWaitResult::Closed;
subCtxOut = r.subCtx; // 绑定可能发生在等待期间
if (!r.clientError.empty()) {
clientError = r.clientError;
r.clientError.clear();
return ManagedWaitResult::ClientError;
}
if (r.hasResponse) {
bool match = !r.response.empty() && r.response[0] == token;
if (match) {
out = std::move(r.response);
r.hasResponse = false;
return ManagedWaitResult::Ok;
}
// 期望 token 之外的响应(例如客户端主动发来的提示):丢弃后继续等。
r.hasResponse = false;
r.response.clear();
}
if (r.closed) return ManagedWaitResult::Closed;
if (waitUntil > r.deadline) waitUntil = r.deadline;
auto now = std::chrono::steady_clock::now();
if (now >= waitUntil) return ManagedWaitResult::Timeout;
m_PendingCv.wait_until(lk, waitUntil);
}
}
void CMcpServer::EndManagedSession(uint64_t device_id, bool cancelIo, bool markOrphan) {
context* sub = nullptr;
PendingKind kind = PendingKind::Generic;
std::string peer;
{
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto now = std::chrono::steady_clock::now();
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) {
PruneOrphanMarksLocked(now);
return;
}
PendingRequest& r = it->second;
if (r.kind != PendingKind::Machine && r.kind != PendingKind::ConnMgr) return;
sub = r.subCtx;
kind = r.kind;
peer = r.peer;
if (sub) {
m_SubCtxToDevice.erase(sub);
// MCP 关掉的子连接上仍可能有在途包:登记为吸收,直到 OfflineProc 回收。
if (cancelIo) m_AbsorbedSubCtx.insert(sub);
}
m_Pending.erase(it);
if (markOrphan) {
OrphanMark mk;
mk.expiry = now + std::chrono::seconds(5);
mk.kind = kind;
m_OrphanByDevice[device_id] = mk;
if (!peer.empty()) m_OrphanByPeer[peer] = mk;
}
PruneOrphanMarksLocked(now);
RefreshManagedRoutesLocked();
}
Mprintf("[MCP-P8] EndManagedSession dev=%llu kind=%d sub=%p cancelIo=%d orphan=%d\n",
device_id, (int)kind, (void*)sub, cancelIo ? 1 : 0, markOrphan ? 1 : 0);
if (sub && cancelIo) sub->CancelIO(); // 锁外关子连接
}
bool CMcpServer::OnManagedSubPacket(context* subCtx, const BYTE* data, ULONG len) {
if (!subCtx || !data || len == 0) return false;
BYTE token = data[0];
bool readyToken = (token == (BYTE)TOKEN_SYSINFOLIST) ||
(token == (BYTE)TOKEN_CONNECTION_LIST);
// 快速路径:没有已绑定/已吸收的子连接,且不是就绪包 → 与 P8 无关。
// MessageHandle 每个包都会调用本函数(含音频/屏幕等高频子连接),不能逐包抢锁。
if (!readyToken && m_ManagedRoutes.load(std::memory_order_relaxed) == 0) return false;
// 主连接自身不会发就绪包;若 clientID 指向的“主连接”就是本 ctx,回落 MFC。
// FindMainContext 要取 parent->m_cs,必须在持 m_PendingMutex 之前完成,
// 维持锁序 m_cs → m_PendingMutex(与 P2/P7 的调用顺序一致),避免反向等待。
uint64_t cid = subCtx->GetClientID();
bool mainIsSelf = readyToken && cid != 0 && FindMainContext(m_parent, cid) == subCtx;
std::unique_lock<std::mutex> lk(m_PendingMutex);
// ---- 情形 1:已绑定的子连接 → 该连接上的包全部由 MCP 消费 ----
auto bindIt = m_SubCtxToDevice.find(subCtx);
if (bindIt != m_SubCtxToDevice.end()) {
uint64_t dev = bindIt->second;
auto it = m_Pending.find(dev);
if (it == m_Pending.end() ||
(it->second.kind != PendingKind::Machine && it->second.kind != PendingKind::ConnMgr)) {
// 会话已结束但包还在路上:吸收并断开
m_SubCtxToDevice.erase(bindIt);
m_AbsorbedSubCtx.insert(subCtx);
RefreshManagedRoutesLocked();
lk.unlock();
Mprintf("[MCP-P8] bound sub=%p without session: absorb+close\n", (void*)subCtx);
subCtx->CancelIO();
return true;
}
PendingRequest& r = it->second;
if (len > kMaxPacketBytes) {
r.clientError = "response packet too large (" + std::to_string(len) + " bytes)";
m_PendingCv.notify_all();
lk.unlock();
subCtx->CancelIO();
return true;
}
if (token == (BYTE)TOKEN_MACHINE_MSG) {
// 客户端主动报错(CP936 文本)
r.clientError = ToUtf8((const char*)data + 1, 936);
m_PendingCv.notify_all();
return true;
}
if (r.expectedToken != 0 && token == r.expectedToken) {
r.response.assign(data, data + len);
r.hasResponse = true;
m_PendingCv.notify_all();
}
// 其它包(迟到/无关)丢弃
return true;
}
// ---- 情形 2:已吸收(CancelIO 后仍到达的尾包)→ 丢弃 ----
if (m_AbsorbedSubCtx.find(subCtx) != m_AbsorbedSubCtx.end()) {
lk.unlock();
subCtx->CancelIO();
return true;
}
// ---- 情形 3:未绑定且不是就绪包 → 交回 MFC ----
if (!readyToken) return false;
if (mainIsSelf) return false;
PendingKind wantKind = (token == (BYTE)TOKEN_SYSINFOLIST) ? PendingKind::Machine
: PendingKind::ConnMgr;
// 3a. 按 clientID 精确匹配(新客户端经 TOKEN_CONN_AUTH 已钉 ID)
uint64_t boundDev = 0;
if (cid != 0) {
auto it = m_Pending.find(cid);
if (it != m_Pending.end() && it->second.kind == wantKind && it->second.subCtx == nullptr)
boundDev = cid;
}
std::string peer = subCtx->GetPeerName();
// 3b. 老客户端子连接 clientID 可能为 0:按 peer 兜底,仅在同 peer 唯一候选时绑定
if (boundDev == 0 && cid == 0 && !peer.empty()) {
uint64_t candidate = 0;
int matches = 0;
for (auto& kv : m_Pending) {
if (kv.second.kind != wantKind) continue;
if (kv.second.subCtx != nullptr) continue;
if (kv.second.peer == peer) {
candidate = kv.first;
++matches;
}
}
if (matches == 1) boundDev = candidate;
}
if (boundDev == 0) {
// 3c. 没有等待中的会话:若刚有会话异常结束(5s 墓碑),吸收这个迟到的就绪包,
// 避免弹出 MFC 对话框。
auto now = std::chrono::steady_clock::now();
PruneOrphanMarksLocked(now);
bool absorb = false;
if (cid != 0) {
auto o = m_OrphanByDevice.find(cid);
if (o != m_OrphanByDevice.end() && o->second.expiry > now &&
KindAcceptsToken(o->second.kind, token))
absorb = true;
} else if (!peer.empty()) {
auto o = m_OrphanByPeer.find(peer);
if (o != m_OrphanByPeer.end() && o->second.expiry > now &&
KindAcceptsToken(o->second.kind, token))
absorb = true;
}
if (absorb) {
m_AbsorbedSubCtx.insert(subCtx);
RefreshManagedRoutesLocked();
lk.unlock();
Mprintf("[MCP-P8] late ready token=%d peer=%s cid=%llu: absorb+close\n",
(int)token, peer.c_str(), (unsigned long long)cid);
subCtx->CancelIO();
return true;
}
return false; // 用户手动打开对话框
}
// ---- 绑定 ----
PendingRequest& r = m_Pending[boundDev];
r.subCtx = subCtx;
m_SubCtxToDevice[subCtx] = boundDev;
RefreshManagedRoutesLocked();
if (len > kMaxPacketBytes) {
r.clientError = "readiness packet too large (" + std::to_string(len) + " bytes)";
m_PendingCv.notify_all();
lk.unlock();
subCtx->CancelIO();
return true;
}
r.response.assign(data, data + len);
r.hasResponse = true;
m_PendingCv.notify_all();
Mprintf("[MCP-P8] bind dev=%llu tool=%s sub=%p token=%d len=%d\n",
boundDev, r.tool.c_str(), (void*)subCtx, (int)token, (int)len);
return true;
}
void CMcpServer::OnManagedContextClosed(context* subCtx) {
if (!subCtx) return;
std::lock_guard<std::mutex> lk(m_PendingMutex);
m_AbsorbedSubCtx.erase(subCtx);
m_SubCtxToDevice.erase(subCtx);
for (auto& kv : m_Pending) {
PendingRequest& r = kv.second;
if (r.kind != PendingKind::Machine && r.kind != PendingKind::ConnMgr) continue;
if (r.subCtx == subCtx || r.mainCtx == subCtx) {
Mprintf("[MCP-P8] context closed dev=%llu tool=%s: wake waiter\n",
kv.first, r.tool.c_str());
if (r.subCtx == subCtx) {
// 子连接已死:不要再让超时路径对它 CancelIO
r.subCtx = nullptr;
}
r.closed = true;
m_PendingCv.notify_all();
}
}
RefreshManagedRoutesLocked();
}
// ============================================================================
// 连接管理
// ============================================================================
namespace {
bool ListConnections(const Json::Value& id, const HostTool& ht, const std::string& toolName,
std::vector<ConnRecord>& records, std::string& errJson) {
ManagedSession session;
std::vector<BYTE> first;
if (!session.Open(id, toolName, ht, /*connMgr=*/true, &first, errJson))
return false;
if (!ParseConnections(first, records, errJson)) {
errJson = BuildError(id, kErrGeneric, "Malformed connection list: " + errJson);
return false;
}
session.Finish();
return true;
}
std::string BuildListNetworkConnections(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
HostTool ht;
std::string errJson;
if (!ResolveHost(id, args, parent, ht, /*requireWindows=*/true, errJson)) return errJson;
Paging paging;
if (!ParsePaging(id, args, paging, errJson)) return errJson;
std::string protocol = ToLowerAscii(OptStr(args, "protocol"));
if (protocol.empty() || protocol == "all") protocol = "all";
else if (protocol != "tcp" && protocol != "udp") {
return BuildError(id, -32602, "Invalid protocol: expected tcp, udp or all");
}
std::string family = ToLowerAscii(OptStr(args, "family"));
if (family.empty() || family == "all") family = "all";
else if (family != "ipv4" && family != "ipv6") {
return BuildError(id, -32602, "Invalid family: expected ipv4, ipv6 or all");
}
int pid = -1;
if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pid, errJson)) return errJson;
int localPort = -1, remotePort = -1;
if (!GetOptIntArg(id, args, "local_port", 0, 65535, localPort, errJson)) return errJson;
if (!GetOptIntArg(id, args, "remote_port", 0, 65535, remotePort, errJson)) return errJson;
std::string process = OptStr(args, "process");
std::string remoteAddr = OptStr(args, "remote_address");
std::string state = OptStr(args, "state");
std::vector<ConnRecord> records;
if (!ListConnections(id, ht, "list_network_connections", records, errJson)) return errJson;
Json::Value all(Json::arrayValue);
for (const ConnRecord& rec : records) all.append(ConnRecordToJson(rec));
auto pred = [&](const Json::Value& item) {
if (protocol != "all" && item["protocol"].asString() != protocol) return false;
if (family != "all" && item["family"].asString() != family) return false;
if (pid >= 0 && item["pid"].asUInt() != (Json::UInt)pid) return false;
if (localPort >= 0 && item["localPort"].asUInt() != (Json::UInt)localPort) return false;
if (remotePort >= 0 && item["remotePort"].asUInt() != (Json::UInt)remotePort) return false;
if (!MatchCI(item["process"].asString(), process)) return false;
if (!remoteAddr.empty() && !ContainsCI(item["remoteAddress"].asString(), remoteAddr)) return false;
if (!state.empty()) {
if (IsDigits(state)) {
if (item["state"].asUInt() != (Json::UInt)atoi(state.c_str())) return false;
} else if (!ContainsCI(item["stateName"].asString(), state)) {
return false;
}
}
return true;
};
int totalAll = (int)all.size();
int totalFiltered = 0;
bool hasMore = false;
Json::Value page = FilterAndPage(all, pred, paging, totalFiltered, hasMore);
std::string text = std::string(u8"共 ") + std::to_string(totalFiltered) + u8" 条连接(库中 " +
std::to_string(totalAll) + u8" 条,本次返回 " + std::to_string(page.size()) + u8" 条)";
return BuildListResult(id, "connections", page, totalFiltered, totalAll,
paging.offset, paging.limit, hasMore, text);
}
// 解析 IPv4 点分十进制 → 4 字节;仅接受严格 IPv4。
bool ParseIPv4(const std::string& s, uint8_t out[4]) {
return inet_pton(AF_INET, s.c_str(), out) == 1;
}
std::string BuildKillNetworkConnection(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
CMcpServer& mcp = McpServer();
if (!mcp.IsManageEnabled() || mcp.IsReadonly()) {
return BuildError(id, kErrDisabled,
"kill_network_connection is disabled (requires McpManage=1 and McpReadonly=0)");
}
HostTool ht;
std::string errJson;
if (!ResolveHost(id, args, parent, ht, /*requireWindows=*/true, errJson)) return errJson;
std::string protocol = ToLowerAscii(OptStr(args, "protocol"));
if (protocol.empty()) protocol = "tcp";
if (protocol != "tcp" && protocol != "udp")
return BuildError(id, -32602, "Invalid protocol: expected tcp or udp");
std::string localAddrStr = OptStr(args, "local_address");
std::string remoteAddrStr = OptStr(args, "remote_address");
if (localAddrStr.empty() || remoteAddrStr.empty())
return BuildError(id, -32602, "local_address and remote_address are required");
int localPort = 0, remotePort = 0;
if (!GetOptIntArg(id, args, "local_port", 0, 65535, localPort, errJson)) return errJson;
if (!GetOptIntArg(id, args, "remote_port", 0, 65535, remotePort, errJson)) return errJson;
if (!args.isMember("local_port") || !args.isMember("remote_port"))
return BuildError(id, -32602, "local_port and remote_port are required");
int pidFilter = -1;
if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson;
bool dryRun = GetBoolArg(args, "dry_run", false);
uint8_t localBytes[4] = { 0 }, remoteBytes[4] = { 0 };
if (!ParseIPv4(localAddrStr, localBytes) || !ParseIPv4(remoteAddrStr, remoteBytes))
return BuildError(id, kErrUnsupported,
"Only IPv4 addresses are supported for forced disconnect "
"(the client can only drop IPv4 TCP connections)");
std::string toolName = "kill_network_connection";
std::vector<ConnRecord> records;
if (!ListConnections(id, ht, toolName, records, errJson)) return errJson;
// 精确匹配:协议 + 地址字节 + 端口(+ 可选 pid)。用客户端上报的原始记录,不重拼地址。
std::string target = protocol + " " + localAddrStr + ":" + std::to_string(localPort) +
" -> " + remoteAddrStr + ":" + std::to_string(remotePort);
const ConnRecord* found = nullptr;
int matches = 0;
for (const ConnRecord& rec : records) {
int wantProto = (protocol == "tcp") ? 1 : 2;
if (rec.proto != wantProto) continue;
if (rec.af != AF_INET) continue;
if (memcmp(rec.localAddr, localBytes, 4) != 0) continue;
if (rec.localPort != (uint16_t)localPort) continue;
if (memcmp(rec.remoteAddr, remoteBytes, 4) != 0) continue;
if (rec.remotePort != (uint16_t)remotePort) continue;
if (pidFilter >= 0 && rec.pid != (uint32_t)pidFilter) continue;
found = &rec;
++matches;
}
if (matches == 0) {
AuditManageAction(parent, ht.dev, toolName, target + " result=not_found");
return NotFoundJson(id, "connection " + target);
}
if (matches > 1) {
return BuildError(id, -32602,
"Ambiguous target: " + std::to_string(matches) +
" connections match; specify pid to disambiguate");
}
Json::Value targetJson = ConnRecordToJson(*found);
if (found->proto != 1 || found->af != AF_INET) {
return BuildError(id, kErrUnsupported,
"Only IPv4 TCP connections can be forcibly disconnected");
}
if (pidFilter < 0) pidFilter = (int)found->pid;
if (dryRun) {
Json::Value sc(Json::objectValue);
sc["ok"] = true;
sc["dryRun"] = true;
sc["target"] = target;
sc["connection"] = targetJson;
AuditManageAction(parent, ht.dev, toolName, target + " pid=" + std::to_string(pidFilter) +
" result=dry_run");
return BuildStructuredResult(id, sc, "Dry run: target found, nothing was sent");
}
// 会话保持:取列表用的是第一条 255,断开后客户端会自动再回一条 255。
ManagedSession session;
std::vector<BYTE> first;
if (!session.Open(id, toolName, ht, /*connMgr=*/true, &first, errJson)) return errJson;
std::vector<ConnRecord> pre;
if (!ParseConnections(first, pre, errJson)) {
errJson = BuildError(id, kErrGeneric, "Malformed connection list: " + errJson);
return errJson;
}
// 重新在最新列表里定位(模板绑定期间列表可能变化;原始记录也必须来自最新包)
const ConnRecord* latest = nullptr;
int latestMatches = 0;
for (const ConnRecord& rec : pre) {
if (rec.proto != 1 || rec.af != AF_INET) continue;
if (memcmp(rec.localAddr, localBytes, 4) != 0) continue;
if (rec.localPort != (uint16_t)localPort) continue;
if (memcmp(rec.remoteAddr, remoteBytes, 4) != 0) continue;
if (rec.remotePort != (uint16_t)remotePort) continue;
if (pidFilter >= 0 && rec.pid != (uint32_t)pidFilter) continue;
latest = &rec;
++latestMatches;
}
if (latestMatches != 1) {
AuditManageAction(parent, ht.dev, toolName,
target + " result=not_found(stale)");
return latestMatches == 0 ? NotFoundJson(id, "connection " + target)
: BuildError(id, -32602,
"Ambiguous target in the latest snapshot; specify pid");
}
std::vector<BYTE> pkt;
pkt.reserve(1 + kConnRecordSize);
pkt.push_back((BYTE)COMMAND_KILL_CONNECTION);
pkt.insert(pkt.end(), latest->raw, latest->raw + kConnRecordSize);
std::vector<BYTE> after;
if (!session.Action(id, pkt, (BYTE)TOKEN_CONNECTION_LIST, after, errJson)) {
AuditManageAction(parent, ht.dev, toolName,
target + " pid=" + std::to_string(pidFilter) + " result=send_or_timeout");
return errJson;
}
std::vector<ConnRecord> post;
std::string perr;
if (!ParseConnections(after, post, perr)) {
AuditManageAction(parent, ht.dev, toolName,
target + " pid=" + std::to_string(pidFilter) + " result=bad_refresh");
return BuildError(id, kErrGeneric,
"The action was sent but the refreshed list could not be parsed: " + perr);
}
bool stillPresent = false;
for (const ConnRecord& rec : post) {
if (rec.proto != 1 || rec.af != AF_INET) continue;
if (memcmp(rec.localAddr, localBytes, 4) != 0) continue;
if (rec.localPort != (uint16_t)localPort) continue;
if (memcmp(rec.remoteAddr, remoteBytes, 4) != 0) continue;
if (rec.remotePort != (uint16_t)remotePort) continue;
if (pidFilter >= 0 && rec.pid != (uint32_t)pidFilter) continue;
stillPresent = true;
break;
}
session.Finish();
AuditManageAction(parent, ht.dev, toolName,
target + " pid=" + std::to_string(pidFilter) +
(stillPresent ? " result=still_present" : " result=disconnected"));
if (stillPresent) {
return BuildActionResult(id, false, true, false, "kill", target, "present",
"still_present",
u8"连接仍在:客户端可能没有管理员权限(SetTcpEntry 失败),"
u8"或该连接已被系统重建。");
}
return BuildActionResult(id, true, true, true, "kill", target, "removed", "",
u8"连接已断开(刷新后的连接列表中四元组已消失)。");
}
} // namespace
// ============================================================================
// 主机管理:读工具
// ============================================================================
namespace {
// 打开主机管理会话 → 发 1 字节 tab → 取响应 → 结束会话。
bool FetchMachineTab(const Json::Value& id, const HostTool& ht, const std::string& toolName,
BYTE tab, BYTE respToken, std::vector<BYTE>& out, std::string& errJson) {
ManagedSession session;
if (!session.Open(id, toolName, ht, /*connMgr=*/false, nullptr, errJson)) return false;
if (!session.Request(id, tab, respToken, out, errJson)) return false;
session.Finish();
return true;
}
std::string ParseErrorToJson(const Json::Value& id, const char* what, const std::string& detail) {
return BuildError(id, kErrGeneric,
std::string("Malformed ") + what + " response: " + detail);
}
} // namespace
// ---- host_list_processes ----
static std::string BuildHostListProcesses(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
HostTool ht;
std::string errJson;
if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson;
Paging paging;
if (!ParsePaging(id, args, paging, errJson)) return errJson;
int pidFilter = -1;
if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson;
std::string name = OptStr(args, "name");
std::string arch = ToLowerAscii(OptStr(args, "arch"));
if (!arch.empty() && arch != "x64" && arch != "x86")
return BuildError(id, -32602, "Invalid arch: expected x64 or x86");
std::vector<BYTE> data;
if (!FetchMachineTab(id, ht, "host_list_processes", (BYTE)COMMAND_MACHINE_PROCESS,
(BYTE)TOKEN_MACHINE_PROCESS, data, errJson))
return errJson;
Json::Value all;
std::string perr;
if (!ParseProcesses(data, 936, all, perr)) return ParseErrorToJson(id, "process list", perr);
auto pred = [&](const Json::Value& item) {
if (pidFilter >= 0 && item["pid"].asUInt() != (Json::UInt)pidFilter) return false;
if (!MatchCI(item["name"].asString(), name)) return false;
if (!arch.empty() && item["arch"].asString() != arch) return false;
return true;
};
int totalAll = (int)all.size(), total = 0;
bool hasMore = false;
Json::Value page = FilterAndPage(all, pred, paging, total, hasMore);
std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个进程(全部 " +
std::to_string(totalAll) + u8" 个)";
return BuildListResult(id, "processes", page, total, totalAll,
paging.offset, paging.limit, hasMore, text);
}
// ---- host_list_windows ----
static std::string BuildHostListWindows(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
HostTool ht;
std::string errJson;
if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson;
Paging paging;
if (!ParsePaging(id, args, paging, errJson)) return errJson;
int pidFilter = -1;
if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson;
std::string title = OptStr(args, "title");
std::vector<BYTE> data;
if (!FetchMachineTab(id, ht, "host_list_windows", (BYTE)COMMAND_MACHINE_WINDOWS,
(BYTE)TOKEN_MACHINE_WINDOWS, data, errJson))
return errJson;
Json::Value all;
std::string perr;
if (!ParseWindows(data, 936, all, perr)) return ParseErrorToJson(id, "window list", perr);
auto pred = [&](const Json::Value& item) {
if (pidFilter >= 0 && item["pid"].asUInt() != (Json::UInt)pidFilter) return false;
if (!MatchCI(item["title"].asString(), title)) return false;
return true;
};
int totalAll = (int)all.size(), total = 0;
bool hasMore = false;
Json::Value page = FilterAndPage(all, pred, paging, total, hasMore);
std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个可见窗口(全部 " +
std::to_string(totalAll) + u8" 个)";
return BuildListResult(id, "windows", page, total, totalAll,
paging.offset, paging.limit, hasMore, text);
}
// ---- host_list_network_state ----
static std::string BuildHostListNetworkState(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
HostTool ht;
std::string errJson;
if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson;
Paging paging;
if (!ParsePaging(id, args, paging, errJson)) return errJson;
int pidFilter = -1, localPort = -1, remotePort = -1;
if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson;
if (!GetOptIntArg(id, args, "local_port", 0, 65535, localPort, errJson)) return errJson;
if (!GetOptIntArg(id, args, "remote_port", 0, 65535, remotePort, errJson)) return errJson;
std::string process = OptStr(args, "process");
std::string protocol = ToLowerAscii(OptStr(args, "protocol"));
std::string state = OptStr(args, "state");
std::vector<BYTE> data;
if (!FetchMachineTab(id, ht, "host_list_network_state", (BYTE)COMMAND_MACHINE_NETSTATE,
(BYTE)TOKEN_MACHINE_NETSTATE, data, errJson))
return errJson;
Json::Value all;
std::string perr;
if (!ParseNetState(data, 936, all, perr)) return ParseErrorToJson(id, "network state", perr);
auto pred = [&](const Json::Value& item) {
if (pidFilter >= 0 && item["pid"].asUInt() != (Json::UInt)pidFilter) return false;
if (localPort >= 0 && item["localPort"].asInt() != localPort) return false;
if (remotePort >= 0 && item["remotePort"].asInt() != remotePort) return false;
if (!process.empty() && !MatchCI(item["process"].asString(), process)) return false;
if (!protocol.empty() && protocol != "all" && item["protocol"].asString() != protocol)
return false;
if (!state.empty() && !MatchCI(item["state"].asString(), state)) return false;
return true;
};
int totalAll = (int)all.size(), total = 0;
bool hasMore = false;
Json::Value page = FilterAndPage(all, pred, paging, total, hasMore);
std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 条网络连接(全部 " +
std::to_string(totalAll) + u8" 条)";
return BuildListResult(id, "connections", page, total, totalAll,
paging.offset, paging.limit, hasMore, text);
}
// ---- host_list_software ----
static std::string BuildHostListSoftware(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
HostTool ht;
std::string errJson;
if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson;
Paging paging;
if (!ParsePaging(id, args, paging, errJson)) return errJson;
std::string name = OptStr(args, "name");
std::string publisher = OptStr(args, "publisher");
std::vector<BYTE> data;
if (!FetchMachineTab(id, ht, "host_list_software", (BYTE)COMMAND_MACHINE_SOFTWARE,
(BYTE)TOKEN_MACHINE_SOFTWARE, data, errJson))
return errJson;
Json::Value all;
std::string perr;
if (!ParseSoftware(data, 936, all, perr)) return ParseErrorToJson(id, "software list", perr);
auto pred = [&](const Json::Value& item) {
if (!MatchCI(item["name"].asString(), name)) return false;
if (!MatchCI(item["publisher"].asString(), publisher)) return false;
return true;
};
int totalAll = (int)all.size(), total = 0;
bool hasMore = false;
Json::Value page = FilterAndPage(all, pred, paging, total, hasMore);
std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个已安装软件(全部 " +
std::to_string(totalAll) + u8" 个)";
return BuildListResult(id, "software", page, total, totalAll,
paging.offset, paging.limit, hasMore, text);
}
// ---- host_list_browser_history ----
static std::string BuildHostListBrowserHistory(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
HostTool ht;
std::string errJson;
if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson;
Paging paging;
if (!ParsePaging(id, args, paging, errJson)) return errJson;
std::string title = OptStr(args, "title");
std::string url = OptStr(args, "url");
std::vector<BYTE> data;
if (!FetchMachineTab(id, ht, "host_list_browser_history", (BYTE)COMMAND_MACHINE_HTML,
(BYTE)TOKEN_MACHINE_HTML, data, errJson))
return errJson;
Json::Value all;
std::string perr;
if (!ParseBrowserHistory(data, 936, all, perr))
return ParseErrorToJson(id, "browser history", perr);
auto pred = [&](const Json::Value& item) {
if (!MatchCI(item["title"].asString(), title)) return false;
if (!MatchCI(item["url"].asString(), url)) return false;
return true;
};
int totalAll = (int)all.size(), total = 0;
bool hasMore = false;
Json::Value page = FilterAndPage(all, pred, paging, total, hasMore);
std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 条浏览记录(全部 " +
std::to_string(totalAll) + u8" 条)";
return BuildListResult(id, "history", page, total, totalAll,
paging.offset, paging.limit, hasMore, text);
}
// ---- host_list_favorites ----
static std::string BuildHostListFavorites(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
HostTool ht;
std::string errJson;
if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson;
Paging paging;
if (!ParsePaging(id, args, paging, errJson)) return errJson;
std::string name = OptStr(args, "name");
std::string url = OptStr(args, "url");
std::vector<BYTE> data;
if (!FetchMachineTab(id, ht, "host_list_favorites", (BYTE)COMMAND_MACHINE_FAVORITES,
(BYTE)TOKEN_MACHINE_FAVORITES, data, errJson))
return errJson;
Json::Value all;
std::string perr;
if (!ParseFavorites(data, 936, all, perr)) return ParseErrorToJson(id, "favorites", perr);
auto pred = [&](const Json::Value& item) {
if (!MatchCI(item["name"].asString(), name)) return false;
if (!MatchCI(item["url"].asString(), url)) return false;
return true;
};
int totalAll = (int)all.size(), total = 0;
bool hasMore = false;
Json::Value page = FilterAndPage(all, pred, paging, total, hasMore);
std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个收藏(全部 " +
std::to_string(totalAll) + u8" 个)";
return BuildListResult(id, "favorites", page, total, totalAll,
paging.offset, paging.limit, hasMore, text);
}
// ---- host_list_win32_services / host_list_driver_services ----
static std::string BuildHostListServices(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent, bool drivers) {
HostTool ht;
std::string errJson;
const char* toolName = drivers ? "host_list_driver_services" : "host_list_win32_services";
BYTE tab = drivers ? (BYTE)COMMAND_MACHINE_DRIVERSERVICE : (BYTE)COMMAND_MACHINE_WIN32SERVICE;
if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson;
Paging paging;
if (!ParsePaging(id, args, paging, errJson)) return errJson;
std::string name = OptStr(args, "name");
std::string status = OptStr(args, "status");
std::string startType = OptStr(args, "start_type");
std::vector<BYTE> data;
if (!FetchMachineTab(id, ht, toolName, tab, (BYTE)TOKEN_MACHINE_SERVICE_LIST, data, errJson))
return errJson;
Json::Value all;
std::string perr;
if (!ParseServices(data, 936, all, perr)) return ParseErrorToJson(id, "service list", perr);
auto pred = [&](const Json::Value& item) {
if (!name.empty() &&
!(MatchCI(item["name"].asString(), name) || MatchCI(item["displayName"].asString(), name)))
return false;
if (!MatchCI(item["state"].asString(), status)) return false;
if (!MatchCI(item["startType"].asString(), startType)) return false;
return true;
};
int totalAll = (int)all.size(), total = 0;
bool hasMore = false;
Json::Value page = FilterAndPage(all, pred, paging, total, hasMore);
std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个服务(全部 " +
std::to_string(totalAll) + u8" 个)";
return BuildListResult(id, "services", page, total, totalAll,
paging.offset, paging.limit, hasMore, text);
}
// ---- host_list_scheduled_tasks ----
static std::string BuildHostListScheduledTasks(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
HostTool ht;
std::string errJson;
if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson;
Paging paging;
if (!ParsePaging(id, args, paging, errJson)) return errJson;
std::string name = OptStr(args, "name");
std::string folder = OptStr(args, "folder");
std::string status = OptStr(args, "status");
std::vector<BYTE> data;
if (!FetchMachineTab(id, ht, "host_list_scheduled_tasks", (BYTE)COMMAND_MACHINE_TASK,
(BYTE)TOKEN_MACHINE_TASKLIST, data, errJson))
return errJson;
Json::Value all;
std::string perr;
if (!ParseTasks(data, all, perr)) return ParseErrorToJson(id, "scheduled task list", perr);
auto pred = [&](const Json::Value& item) {
if (!MatchCI(item["name"].asString(), name)) return false;
if (!folder.empty() && !MatchCI(item["folder"].asString(), folder)) return false;
if (!MatchCI(item["status"].asString(), status)) return false;
return true;
};
int totalAll = (int)all.size(), total = 0;
bool hasMore = false;
Json::Value page = FilterAndPage(all, pred, paging, total, hasMore);
std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个计划任务(全部 " +
std::to_string(totalAll) + u8" 个)";
return BuildListResult(id, "tasks", page, total, totalAll,
paging.offset, paging.limit, hasMore, text);
}
// ---- host_get_hosts_file ----
static std::string BuildHostGetHostsFile(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
HostTool ht;
std::string errJson;
if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson;
int maxBytes = 256 * 1024;
if (!GetOptIntArg(id, args, "max_bytes", 1, 1024 * 1024, maxBytes, errJson)) return errJson;
bool includeContent = GetBoolArg(args, "include_content", true);
std::vector<BYTE> data;
if (!FetchMachineTab(id, ht, "host_get_hosts_file", (BYTE)COMMAND_MACHINE_HOSTS,
(BYTE)TOKEN_MACHINE_HOSTS, data, errJson))
return errJson;
// [39][文件原文][2 个 0 字节]:取到第一个 NUL 为止。
if (data.size() < 1) return BuildError(id, kErrGeneric, "Empty hosts response");
size_t n = data.size() - 1;
size_t strLen = BoundedStrlen((const char*)data.data() + 1, n);
const BYTE* raw = data.data() + 1;
UINT cp = 936;
size_t skip = 0;
if (strLen >= 3 && raw[0] == 0xEF && raw[1] == 0xBB && raw[2] == 0xBF) {
cp = CP_UTF8;
skip = 3;
} else if (IsValidUtf8((const char*)raw, strLen)) {
cp = CP_UTF8;
}
size_t contentLen = strLen - skip;
bool truncated = contentLen > (size_t)maxBytes;
size_t take = truncated ? (size_t)maxBytes : contentLen;
std::string text = ToUtf8(std::string((const char*)raw + skip, take).c_str(), cp);
Json::Value sc(Json::objectValue);
sc["path"] = "%SystemRoot%\\System32\\drivers\\etc\\hosts";
sc["encoding"] = (cp == CP_UTF8) ? "utf-8" : "gbk";
sc["bytes"] = (Json::UInt)contentLen;
sc["returnedBytes"] = (Json::UInt)take;
sc["truncated"] = truncated;
if (includeContent) {
// 按行切分(去掉 \r,跳过末尾空行);截断时丢弃最后一行不完整的内容。
Json::Value lines(Json::arrayValue);
size_t pos = 0;
bool droppedPartial = false;
while (pos < text.size()) {
size_t nl = text.find('\n', pos);
bool last = (nl == std::string::npos);
std::string line = last ? text.substr(pos) : text.substr(pos, nl - pos);
if (!line.empty() && line[line.size() - 1] == '\r') line.erase(line.size() - 1);
if (truncated && last) { droppedPartial = true; break; }
lines.append(line);
if (last) break;
pos = nl + 1;
}
if (droppedPartial) sc["truncatedAtLineBoundary"] = true;
sc["lines"] = lines;
sc["lineCount"] = (Json::UInt)lines.size();
if (!truncated) sc["content"] = text;
}
std::string summary = std::string(u8"hosts 文件 ") + std::to_string(contentLen) + u8" 字节" +
(truncated ? u8"(已截断)" : "");
return BuildStructuredResult(id, sc, summary);
}
// ============================================================================
// 主机管理:写工具(只读 + 可恢复操作)
// ============================================================================
namespace {
bool RequireWritable(const Json::Value& id, std::string& errJson) {
if (McpServer().IsReadonly()) {
errJson = BuildError(id, kErrDisabled,
"Host management write operations are disabled: MCP is in read-only mode "
"(set McpReadonly=0 to enable)");
return false;
}
return true;
}
bool RequireDangerous(const Json::Value& id, std::string& errJson) {
if (!McpServer().IsMachineDangerousEnabled()) {
errJson = BuildError(id, kErrDisabled,
"This action is disabled: set McpMachineDangerous=1 to enable it");
return false;
}
return true;
}
// 从 JSON 读 hwnd:接受整数、十进制字符串或 0x 前缀十六进制字符串。
bool ReadHwndArg(const Json::Value& id, const Json::Value& args, uint32_t& out, std::string& errJson) {
if (!args.isMember("hwnd")) {
errJson = BuildError(id, -32602, "Missing required parameter: hwnd");
return false;
}
const Json::Value& v = args["hwnd"];
if (v.isNumeric()) {
out = (uint32_t)v.asUInt64();
return true;
}
if (v.isString()) {
std::string s = Trim(v.asString());
if (s.size() > 2 && (s[0] == '0') && (s[1] == 'x' || s[1] == 'X')) {
out = (uint32_t)strtoul(s.c_str() + 2, nullptr, 16);
return true;
}
if (IsDigits(s)) {
out = (uint32_t)strtoul(s.c_str(), nullptr, 10);
return true;
}
}
errJson = BuildError(id, -32602, "Invalid hwnd: expected an integer, decimal string or 0x-hex string");
return false;
}
const Json::Value* FindProcess(const Json::Value& list, DWORD pid) {
for (Json::ArrayIndex i = 0; i < list.size(); ++i) {
if (list[i]["pid"].asUInt() == (Json::UInt)pid) return &list[i];
}
return nullptr;
}
const Json::Value* FindWindow(const Json::Value& list, DWORD hwnd) {
for (Json::ArrayIndex i = 0; i < list.size(); ++i) {
if (list[i]["hwnd"].asUInt() == (Json::UInt)hwnd) return &list[i];
}
return nullptr;
}
const Json::Value* FindService(const Json::Value& list, const std::string& name) {
for (Json::ArrayIndex i = 0; i < list.size(); ++i) {
if (EqualsCI(list[i]["name"].asString(), name)) return &list[i];
}
return nullptr;
}
bool ServiceStateIn(const std::string& state, std::initializer_list<const char*> accepted) {
for (const char* s : accepted) {
if (EqualsCI(state, s)) return true;
}
return false;
}
// 服务 tab 先取列表(同时把客户端 m_dwServiceType 钉到正确类型)。
bool FetchServicesList(const Json::Value& id, ManagedSession& session, BYTE tab,
Json::Value& all, std::string& errJson) {
std::vector<BYTE> data;
if (!session.Request(id, tab, (BYTE)TOKEN_MACHINE_SERVICE_LIST, data, errJson)) return false;
std::string perr;
if (!ParseServices(data, 936, all, perr)) {
errJson = ParseErrorToJson(id, "service list", perr);
return false;
}
return true;
}
} // namespace
// ---- host_control_process(freeze / thaw) ----
static std::string BuildHostControlProcess(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
std::string errJson;
if (!RequireWritable(id, errJson)) return errJson;
std::string action = ToLowerAscii(OptStr(args, "action"));
if (action != "freeze" && action != "thaw")
return BuildError(id, -32602, "Invalid action: expected freeze or thaw");
if (!RequireDangerous(id, errJson)) return errJson;
int pidFilter = -1;
if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson;
if (pidFilter < 0) return BuildError(id, -32602, "Missing required parameter: pid");
std::string expectedName = OptStr(args, "name");
HostTool ht;
if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson;
ManagedSession session;
if (!session.Open(id, "host_control_process", ht, false, nullptr, errJson)) return errJson;
std::vector<BYTE> data;
if (!session.Request(id, (BYTE)COMMAND_MACHINE_PROCESS, (BYTE)TOKEN_MACHINE_PROCESS,
data, errJson))
return errJson;
Json::Value pre;
std::string perr;
if (!ParseProcesses(data, 936, pre, perr)) return ParseErrorToJson(id, "process list", perr);
const Json::Value* proc = FindProcess(pre, (DWORD)pidFilter);
if (!proc) {
AuditManageAction(parent, ht.dev, "host_control_process",
action + " pid=" + std::to_string(pidFilter) + " result=not_found");
return NotFoundJson(id, "process pid=" + std::to_string(pidFilter));
}
if (!expectedName.empty() && !EqualsCI(proc->get("name", Json::Value("")).asString(), expectedName)) {
// PID 可能已被系统复用:名字对不上时拒绝,避免误冻别的进程
return NotFoundJson(id, "process pid=" + std::to_string(pidFilter) +
" named '" + expectedName + "' (current name: " +
proc->get("name", Json::Value("")).asString() + ")");
}
std::vector<BYTE> pkt(5);
pkt[0] = (action == "freeze") ? (BYTE)COMMAND_PROCESS_FREEZING
: (BYTE)COMMAND_PROCESS_THAW;
DWORD pid32 = (DWORD)pidFilter;
memcpy(&pkt[1], &pid32, 4);
if (!session.SendNoReply(id, pkt, errJson)) {
AuditManageAction(parent, ht.dev, "host_control_process",
action + " pid=" + std::to_string(pidFilter) + " result=send_failed");
return errJson;
}
// 动作不回包:再取一次进程列表复核目标仍在。
std::vector<BYTE> after;
if (!session.Request(id, (BYTE)COMMAND_MACHINE_PROCESS, (BYTE)TOKEN_MACHINE_PROCESS,
after, errJson))
return errJson;
Json::Value post;
if (!ParseProcesses(after, 936, post, perr)) return ParseErrorToJson(id, "process list", perr);
session.Finish();
const Json::Value* now = FindProcess(post, (DWORD)pidFilter);
bool present = now != nullptr;
AuditManageAction(parent, ht.dev, "host_control_process",
action + " pid=" + std::to_string(pidFilter) +
" result=" + (present ? "present" : "missing"));
if (!present) {
return BuildActionResult(id, false, true, false, action,
"pid=" + std::to_string(pidFilter), "missing", "process_gone",
u8"动作已下发,但复核时进程已不存在(可能已自行退出)。");
}
// 冻结不改变「进程是否存在」,客户端也不回报结果:只能确认动作已发送且目标仍在。
return BuildActionResult(id, true, true, true, action,
"pid=" + std::to_string(pidFilter), "present", "",
u8"动作已下发,复核时进程仍在。注意:冻结/解冻需要客户端管理员权限,"
u8"若未生效只能通过目标进程的行为确认。");
}
// ---- host_control_window(restore / hide / maximize / minimize) ----
static std::string BuildHostControlWindow(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
std::string errJson;
if (!RequireWritable(id, errJson)) return errJson;
std::string action = ToLowerAscii(OptStr(args, "action"));
DWORD sw = 0;
if (action == "restore") sw = SW_RESTORE;
else if (action == "hide") sw = SW_HIDE;
else if (action == "maximize") sw = SW_SHOWMAXIMIZED;
else if (action == "minimize") sw = SW_SHOWMINIMIZED;
else return BuildError(id, -32602, "Invalid action: expected restore, hide, maximize or minimize");
uint32_t hwnd = 0;
if (!ReadHwndArg(id, args, hwnd, errJson)) return errJson;
HostTool ht;
if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson;
ManagedSession session;
if (!session.Open(id, "host_control_window", ht, false, nullptr, errJson)) return errJson;
std::vector<BYTE> data;
if (!session.Request(id, (BYTE)COMMAND_MACHINE_WINDOWS, (BYTE)TOKEN_MACHINE_WINDOWS,
data, errJson))
return errJson;
Json::Value pre;
std::string perr;
if (!ParseWindows(data, 936, pre, perr)) return ParseErrorToJson(id, "window list", perr);
if (!FindWindow(pre, hwnd)) {
AuditManageAction(parent, ht.dev, "host_control_window",
action + " hwnd=" + std::to_string(hwnd) + " result=not_found");
return NotFoundJson(id, "window hwnd=" + std::to_string(hwnd));
}
// [11][hwnd][SW_*] + 零填充到 20 字节(与 MFC 下发格式一致;客户端只读前 9 字节)
std::vector<BYTE> pkt(20, 0);
pkt[0] = (BYTE)COMMAND_WINDOW_OPERATE;
DWORD hwnd32 = (DWORD)hwnd;
memcpy(&pkt[1], &hwnd32, 4);
memcpy(&pkt[5], &sw, 4);
if (!session.SendNoReply(id, pkt, errJson)) {
AuditManageAction(parent, ht.dev, "host_control_window",
action + " hwnd=" + std::to_string(hwnd) + " result=send_failed");
return errJson;
}
std::vector<BYTE> after;
if (!session.Request(id, (BYTE)COMMAND_MACHINE_WINDOWS, (BYTE)TOKEN_MACHINE_WINDOWS,
after, errJson))
return errJson;
Json::Value post;
if (!ParseWindows(after, 936, post, perr)) return ParseErrorToJson(id, "window list", perr);
session.Finish();
bool present = FindWindow(post, hwnd) != nullptr;
// 列表只含可见窗口:hide 后应消失;其余动作应仍然可见(最小化不改变可见性)。
bool expectedPresent = (action != "hide");
std::string state = present ? "visible" : "hidden";
AuditManageAction(parent, ht.dev, "host_control_window",
action + " hwnd=" + std::to_string(hwnd) + " result=" + state);
if (present != expectedPresent) {
return BuildActionResult(id, false, true, false, action,
"hwnd=" + std::to_string(hwnd), state, "unexpected_state",
u8"动作已下发,但复核结果与预期不符(窗口可能已关闭,"
u8"或该操作需要更高权限)。");
}
return BuildActionResult(id, true, true, true, action,
"hwnd=" + std::to_string(hwnd), state, "",
action == "hide" ? u8"窗口已隐藏(可见窗口列表中已消失)。"
: u8"动作已生效,窗口仍在可见窗口列表中。");
}
// ---- host_control_service(start / pause / continue / stop / restart) ----
static std::string BuildHostControlService(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
std::string errJson;
if (!RequireWritable(id, errJson)) return errJson;
std::string action = ToLowerAscii(OptStr(args, "action"));
bool dangerous = (action == "stop" || action == "restart");
if (action != "start" && action != "stop" && action != "pause" &&
action != "continue" && action != "restart")
return BuildError(id, -32602,
"Invalid action: expected start, stop, pause, continue or restart");
if (dangerous && !RequireDangerous(id, errJson)) return errJson;
std::string serviceName = Trim(GetStringArg(args, "name"));
if (serviceName.empty())
return BuildError(id, -32602, "Missing required parameter: name (service name)");
HostTool ht;
if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson;
std::string ansiName;
if (!Utf8ToCpExact(serviceName, 936, ansiName)) {
return BuildError(id, -32602,
"Service name contains characters that cannot be represented in the "
"client's ANSI code page (CP936)");
}
ManagedSession session;
if (!session.Open(id, "host_control_service", ht, false, nullptr, errJson)) return errJson;
// prime:优先 Win32 服务,找不到再查驱动服务(同时钉住客户端 m_dwServiceType)。
Json::Value list;
BYTE serviceTab = (BYTE)COMMAND_MACHINE_WIN32SERVICE;
if (!FetchServicesList(id, session, serviceTab, list, errJson)) return errJson;
const Json::Value* svc = FindService(list, serviceName);
if (!svc) {
serviceTab = (BYTE)COMMAND_MACHINE_DRIVERSERVICE;
if (!FetchServicesList(id, session, serviceTab, list, errJson)) return errJson;
svc = FindService(list, serviceName);
}
if (!svc) {
AuditManageAction(parent, ht.dev, "host_control_service",
action + " " + serviceName + " result=not_found");
return NotFoundJson(id, "service '" + serviceName + "'");
}
std::string initial = svc->get("state", Json::Value("")).asString();
// 客户端只在状态满足时才真正动作:先按同样的规则判断,避免把 no-op 报成成功。
auto stateIs = [](const std::string& s, const char* want) { return EqualsCI(s, want); };
if (action == "start" && stateIs(initial, "Running")) {
session.Finish();
return BuildActionResult(id, true, true, false, action, serviceName, initial, "already_running",
u8"服务已是 Running,未下发动作。");
}
if (action == "stop" && stateIs(initial, "Stopped")) {
session.Finish();
return BuildActionResult(id, true, true, false, action, serviceName, initial, "already_stopped",
u8"服务已是 Stopped,未下发动作。");
}
if (action == "pause" && stateIs(initial, "Paused")) {
session.Finish();
return BuildActionResult(id, true, true, false, action, serviceName, initial, "already_paused",
u8"服务已是 Paused,未下发动作。");
}
auto sendServiceAction = [&](BYTE cmd, const char* label,
std::vector<BYTE>& resp, std::string& err) -> bool {
std::vector<BYTE> pkt;
pkt.reserve(1 + ansiName.size() + 1);
pkt.push_back(cmd);
pkt.insert(pkt.end(), ansiName.begin(), ansiName.end());
pkt.push_back(0);
return session.Action(id, pkt, (BYTE)TOKEN_MACHINE_SERVICE_LIST, resp, err);
};
auto findState = [&](const std::vector<BYTE>& resp, std::string& state,
std::string& err) -> bool {
Json::Value after;
if (!ParseServices(resp, 936, after, err)) return false;
const Json::Value* now = FindService(after, serviceName);
if (!now) return false;
state = now->get("state", Json::Value("")).asString();
return true;
};
std::vector<BYTE> resp;
std::string state;
if (action != "restart") {
BYTE cmd = (action == "start") ? (BYTE)COMMAND_STARTSERVERICE
: (action == "stop") ? (BYTE)COMMAND_STOPSERVERICE
: (action == "pause") ? (BYTE)COMMAND_PAUSESERVERICE
: (BYTE)COMMAND_CONTINUESERVERICE;
if (!sendServiceAction(cmd, action.c_str(), resp, errJson)) {
AuditManageAction(parent, ht.dev, "host_control_service",
action + " " + serviceName + " result=send_or_timeout");
return errJson;
}
} else {
// restart = stop → 等刷新 → start → 等刷新
if (!sendServiceAction((BYTE)COMMAND_STOPSERVERICE, "stop", resp, errJson)) {
AuditManageAction(parent, ht.dev, "host_control_service",
"restart " + serviceName + " result=stop_send_or_timeout");
return errJson;
}
std::string sErr;
if (!findState(resp, state, sErr)) {
return BuildError(id, kErrGeneric,
"Service list after stop could not be parsed: " + sErr);
}
if (!sendServiceAction((BYTE)COMMAND_STARTSERVERICE, "start", resp, errJson)) {
AuditManageAction(parent, ht.dev, "host_control_service",
"restart " + serviceName + " (stop state=" + state +
") result=start_send_or_timeout");
return errJson;
}
}
std::string sErr;
bool listed = findState(resp, state, sErr);
session.Finish();
if (!listed) {
AuditManageAction(parent, ht.dev, "host_control_service",
action + " " + serviceName + " result=missing_or_bad_list");
return BuildActionResult(id, false, true, true, action, serviceName, "missing",
"service_missing_or_bad_refresh",
u8"动作已下发,但刷新后的服务列表里找不到该服务或无法解析。");
}
bool ok = false;
if (action == "start" || action == "continue" || action == "restart")
ok = ServiceStateIn(state, { "Running", "Starting", "Continue" });
else if (action == "stop")
ok = ServiceStateIn(state, { "Stopped", "Stopping" });
else if (action == "pause")
ok = ServiceStateIn(state, { "Paused", "Pausing" });
AuditManageAction(parent, ht.dev, "host_control_service",
action + " " + serviceName + " initial=" + initial +
" result=" + state + (ok ? " (ok)" : " (unexpected)"));
if (!ok) {
return BuildActionResult(id, false, true, true, action, serviceName, state,
"unexpected_state",
u8"动作已下发,但复核时服务状态不在预期集合内"
u8"(可能需要管理员权限,或服务拒绝该控制请求)。");
}
return BuildActionResult(id, true, true, true, action, serviceName, state, "",
action == "restart" ? u8"已先停止再启动,复核状态符合预期。"
: u8"复核时服务状态符合预期。");
}
// ---- host_control_task(run / stop) ----
static std::string BuildHostControlTask(const Json::Value& id, const Json::Value& args,
CMy2015RemoteDlg* parent) {
std::string errJson;
if (!RequireWritable(id, errJson)) return errJson;
std::string action = ToLowerAscii(OptStr(args, "action"));
if (action != "run" && action != "stop")
return BuildError(id, -32602, "Invalid action: expected run or stop");
std::string taskName = Trim(GetStringArg(args, "name"));
if (taskName.empty()) return BuildError(id, -32602, "Missing required parameter: name");
std::string wantedFolder = Trim(GetStringArg(args, "folder"));
HostTool ht;
if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson;
ManagedSession session;
if (!session.Open(id, "host_control_task", ht, false, nullptr, errJson)) return errJson;
std::vector<BYTE> data;
if (!session.Request(id, (BYTE)COMMAND_MACHINE_TASK, (BYTE)TOKEN_MACHINE_TASKLIST,
data, errJson))
return errJson;
Json::Value pre;
std::string perr;
if (!ParseTasks(data, pre, perr)) return ParseErrorToJson(id, "scheduled task list", perr);
std::vector<const Json::Value*> hits;
for (Json::ArrayIndex i = 0; i < pre.size(); ++i) {
if (!EqualsCI(pre[i]["name"].asString(), taskName)) continue;
if (!wantedFolder.empty() && !EqualsCI(pre[i]["folder"].asString(), wantedFolder)) continue;
hits.push_back(&pre[i]);
}
if (hits.empty()) {
AuditManageAction(parent, ht.dev, "host_control_task",
action + " " + taskName + " result=not_found");
return NotFoundJson(id, "scheduled task '" + taskName + "'");
}
if (hits.size() > 1) {
return BuildError(id, -32602,
"Ambiguous task name: " +
std::to_string(hits.size()) +
" tasks share this name; specify folder");
}
std::string folder = hits[0]->get("folder", Json::Value("\\")).asString();
if (folder.empty()) folder = "\\";
std::string ansiFolder, ansiName;
if (!Utf8ToCpExact(folder, 936, ansiFolder) || !Utf8ToCpExact(taskName, 936, ansiName)) {
return BuildError(id, -32602,
"Task folder/name contains characters that cannot be represented in the "
"client's ANSI code page (CP936)");
}
std::vector<BYTE> pkt;
pkt.push_back(action == "run" ? (BYTE)COMMAND_TASKSTART : (BYTE)COMMAND_TASKSTOP);
pkt.insert(pkt.end(), ansiFolder.begin(), ansiFolder.end());
pkt.push_back(0);
pkt.insert(pkt.end(), ansiName.begin(), ansiName.end());
pkt.push_back(0);
std::vector<BYTE> resp;
if (!session.Action(id, pkt, (BYTE)TOKEN_MACHINE_TASKLIST, resp, errJson)) {
AuditManageAction(parent, ht.dev, "host_control_task",
action + " " + taskName + " result=send_or_timeout");
return errJson;
}
Json::Value post;
if (!ParseTasks(resp, post, perr)) {
return BuildError(id, kErrGeneric, "Task list after action could not be parsed: " + perr);
}
session.Finish();
const Json::Value* after = nullptr;
for (Json::ArrayIndex i = 0; i < post.size(); ++i) {
if (EqualsCI(post[i]["name"].asString(), taskName) &&
EqualsCI(post[i]["folder"].asString(), folder)) {
after = &post[i];
break;
}
}
if (!after) {
AuditManageAction(parent, ht.dev, "host_control_task",
action + " " + taskName + " result=missing");
return BuildActionResult(id, false, true, true, action, taskName, "missing", "task_missing",
u8"动作已下发,但复核时任务已不在列表中。");
}
std::string state = after->get("status", Json::Value("")).asString();
AuditManageAction(parent, ht.dev, "host_control_task",
action + " " + taskName + " result=" + state);
// 任务动作没有回执:只能确认任务仍存在并报告当前状态,不推断是否真的运行成功。
return BuildActionResult(id, true, true, true, action, taskName, state, "",
action == "run"
? u8"动作已下发;任务仍存在,当前状态见 state。"
u8"(客户端无回执,若任务被禁用或凭据无效,Run 会失败且状态仍为原值。)"
: u8"动作已下发;任务仍存在,当前状态见 state。");
}
// ============================================================================
// 工具注册与分发
// ============================================================================
namespace McpMachine {
namespace {
void AddTool(Json::Value& tools, const char* name, const char* desc,
const Json::Value& inputSchema, const Json::Value& outputSchema) {
Json::Value tool(Json::objectValue);
tool["name"] = name;
tool["description"] = desc; // 调用方传 u8 字面量,保证 JSON 里是 UTF-8
tool["inputSchema"] = inputSchema;
tool["outputSchema"] = outputSchema;
tools.append(tool);
}
// 主机管理各 tab 的条目 schema
Json::Value ProcessItemSchema() {
Json::Value p(Json::objectValue);
p["pid"] = PropInt();
p["arch"] = PropStr(u8"x86 / x64");
p["title"] = PropStr(u8"进程主窗口标题");
p["name"] = PropStr(u8"可执行文件名");
p["path"] = PropStr();
p["priorityClass"] = PropInt(u8"Windows 优先级类原始值");
p["priority"] = PropStr(u8"优先级名称");
p["threads"] = PropInt();
p["user"] = PropStr();
p["workingSetKb"] = PropInt();
p["fileSizeKb"] = PropInt();
return p;
}
Json::Value WindowItemSchema() {
Json::Value p(Json::objectValue);
p["hwnd"] = PropInt(u8"窗口句柄(十进制)");
p["title"] = PropStr();
p["pid"] = PropInt();
p["width"] = PropInt();
p["height"] = PropInt();
p["canSee"] = PropBool(u8"客户端是否可抓取该窗口画面");
return p;
}
Json::Value NetStateItemSchema() {
Json::Value p(Json::objectValue);
p["process"] = PropStr();
p["pid"] = PropInt();
p["protocol"] = PropStr(u8"tcp / udp");
p["localEndpoint"] = PropStr(u8"地址:端口");
p["localAddress"] = PropStr();
p["localPort"] = PropInt();
p["remoteEndpoint"] = PropStr(u8"地址:端口;UDP 无对端时为 *.*.*.*:*");
p["remoteAddress"] = PropStr();
p["remotePort"] = PropInt();
p["state"] = PropStr(u8"TCP 状态;UDP 为空");
return p;
}
Json::Value ConnectionItemSchema() {
Json::Value p(Json::objectValue);
p["protocol"] = PropStr(u8"tcp / udp");
p["family"] = PropStr(u8"ipv4 / ipv6");
p["pid"] = PropInt();
p["process"] = PropStr();
p["localAddress"] = PropStr();
p["localPort"] = PropInt();
p["remoteAddress"] = PropStr();
p["remotePort"] = PropInt();
p["state"] = PropInt(u8"MIB_TCP_STATE_* 原始值");
p["stateName"] = PropStr(u8"TCP 状态名;UDP 为空");
p["killable"] = PropBool(u8"是否可被 kill_network_connection 强制断开(仅 IPv4 TCP)");
return p;
}
Json::Value SoftwareItemSchema() {
Json::Value p(Json::objectValue);
p["name"] = PropStr();
p["publisher"] = PropStr();
p["version"] = PropStr();
p["installDate"] = PropStr();
p["uninstallString"] = PropStr(u8"卸载命令原文(本工具不执行)");
return p;
}
Json::Value HistoryItemSchema() {
Json::Value p(Json::objectValue);
p["time"] = PropStr();
p["title"] = PropStr();
p["url"] = PropStr();
return p;
}
Json::Value FavoriteItemSchema() {
Json::Value p(Json::objectValue);
p["name"] = PropStr();
p["url"] = PropStr();
return p;
}
Json::Value ServiceItemSchema() {
Json::Value p(Json::objectValue);
p["displayName"] = PropStr();
p["description"] = PropStr();
p["state"] = PropStr(u8"Running / Stopped / Paused / ...");
p["startType"] = PropStr();
p["login"] = PropStr();
p["interactive"] = PropBool();
p["name"] = PropStr(u8"服务名(host_control_service 的 name 参数)");
p["binaryPath"] = PropStr();
return p;
}
Json::Value TaskItemSchema() {
Json::Value p(Json::objectValue);
p["name"] = PropStr();
p["path"] = PropStr();
p["folder"] = PropStr(u8"任务所在文件夹(host_control_task 的 folder 参数)");
p["exe"] = PropStr();
p["status"] = PropStr(u8"Ready / Running / Disabled / ...");
p["lastRun"] = PropStr(u8"上次运行时间,未知为空");
p["nextRun"] = PropStr(u8"下次运行时间,未知为空");
return p;
}
// 读工具公共 input:id + 分页
Json::Value ReadInputSchema(Json::Value props) {
props["id"] = HostIdProp();
AddPagingProps(props);
return MakeSchema(props, { "id" });
}
} // namespace
bool Handles(const std::string& toolName) {
static const char* kTools[] = {
"list_network_connections", "kill_network_connection",
"host_list_processes", "host_list_windows", "host_list_network_state",
"host_list_software", "host_list_browser_history", "host_list_favorites",
"host_list_win32_services", "host_list_driver_services",
"host_list_scheduled_tasks", "host_get_hosts_file",
"host_control_process", "host_control_window",
"host_control_service", "host_control_task",
};
for (const char* t : kTools) {
if (toolName == t) return true;
}
return false;
}
void AppendTools(Json::Value& tools) {
CMcpServer& mcp = McpServer();
// 总开关关闭:本族工具全部不可见(直接调用返回 -32006)。
if (!mcp.IsMachineEnabled()) return;
const bool writable = !mcp.IsReadonly();
// ---- 连接管理 ----
{
Json::Value props(Json::objectValue);
props["protocol"] = PropStrEnum(u8"协议过滤,默认 all", { "tcp", "udp", "all" });
props["family"] = PropStrEnum(u8"地址族过滤,默认 all", { "ipv4", "ipv6", "all" });
props["pid"] = PropInt(u8"按进程 PID 过滤");
props["process"] = PropStr(u8"按进程名子串过滤(不区分大小写)");
props["local_port"] = PropInt();
props["remote_port"] = PropInt();
props["remote_address"] = PropStr(u8"按远端地址子串过滤");
props["state"] = PropStr(u8"按状态过滤:数字(MIB_TCP_STATE_*)或状态名子串,如 ESTABLISHED");
AddTool(tools, "list_network_connections",
u8"列出指定在线 Windows 主机的 TCP/UDP 连接(相当于「连接管理」对话框)。返回四元组、所属进程与 TCP 状态;killable=true 表示该连接可被 kill_network_connection 断开。",
ReadInputSchema(props), MakeListOutputSchema("connections", ConnectionItemSchema()));
}
if (writable && mcp.IsManageEnabled()) {
Json::Value props(Json::objectValue);
props["id"] = HostIdProp();
props["protocol"] = PropStrEnum(u8"协议,默认 tcp(仅 IPv4 TCP 可断开)", { "tcp", "udp" });
props["local_address"] = PropStr(u8"本地 IPv4 地址,如 192.168.0.55");
props["local_port"] = PropInt(u8"本地端口(主机序)");
props["remote_address"] = PropStr(u8"远端 IPv4 地址");
props["remote_port"] = PropInt(u8"远端端口(主机序)");
props["pid"] = PropInt(u8"可选:同时校验所属进程 PID(多行匹配时用于消歧)");
props["dry_run"] = PropBool(u8"true 时只校验目标存在,不发断开命令,默认 false");
AddTool(tools, "kill_network_connection",
u8"强制断开指定在线 Windows 主机上的一条 IPv4 TCP 连接(客户端调 SetTcpEntry DELETE_TCB)。必须提供完整四元组;目标不存在返回 -32009(请先刷新 list_network_connections)。断开后会用客户端自动刷新回来的列表复核,若连接仍在则 ok=false、reason=still_present(常见原因:客户端无管理员权限)。",
MakeSchema(props, { "id", "local_address", "local_port",
"remote_address", "remote_port" }),
ActionOutputSchema());
}
// ---- 主机管理:10 个只读标签页 ----
{
Json::Value props(Json::objectValue);
props["pid"] = PropInt(u8"按 PID 过滤");
props["name"] = PropStr(u8"按进程名子串过滤");
props["arch"] = PropStrEnum(u8"按架构过滤", { "x86", "x64" });
AddTool(tools, "host_list_processes",
u8"列出在线 Windows 主机的进程(主机管理「进程」标签页)。返回 PID、架构、窗口标题、路径、优先级、线程数、用户、工作集与文件大小。",
ReadInputSchema(props), MakeListOutputSchema("processes", ProcessItemSchema()));
}
{
Json::Value props(Json::objectValue);
props["pid"] = PropInt(u8"按所属进程 PID 过滤");
props["title"] = PropStr(u8"按窗口标题子串过滤");
AddTool(tools, "host_list_windows",
u8"列出在线 Windows 主机上当前可见的顶层窗口(主机管理「窗口」标签页)。只包含可见窗口,故隐藏后窗口会从结果中消失。hwnd 可用于 host_control_window。",
ReadInputSchema(props), MakeListOutputSchema("windows", WindowItemSchema()));
}
{
Json::Value props(Json::objectValue);
props["pid"] = PropInt();
props["process"] = PropStr(u8"按进程名子串过滤");
props["protocol"] = PropStrEnum(u8"协议过滤", { "tcp", "udp" });
props["local_port"] = PropInt();
props["remote_port"] = PropInt();
props["state"] = PropStr(u8"按状态子串过滤");
AddTool(tools, "host_list_network_state",
u8"列出在线 Windows 主机的网络连接及其所属进程(主机管理「网络状态」标签页)。数据来源与「连接管理」不同(这里是进程视角、含 UDP 的无对端标记);仅支持 IPv4 展示。",
ReadInputSchema(props), MakeListOutputSchema("connections", NetStateItemSchema()));
}
{
Json::Value props(Json::objectValue);
props["name"] = PropStr(u8"按软件名子串过滤");
props["publisher"] = PropStr(u8"按发布者子串过滤");
AddTool(tools, "host_list_software",
u8"列出在线 Windows 主机已安装的软件(主机管理「软件」标签页,来自注册表 Uninstall 项)。只读;不提供卸载能力。",
ReadInputSchema(props), MakeListOutputSchema("software", SoftwareItemSchema()));
}
{
Json::Value props(Json::objectValue);
props["title"] = PropStr(u8"按标题子串过滤");
props["url"] = PropStr(u8"按 URL 子串过滤");
AddTool(tools, "host_list_browser_history",
u8"列出在线 Windows 主机的 IE 浏览历史(主机管理「浏览记录」标签页)。隐私数据,仅在显式开启主机管理时可见。",
ReadInputSchema(props), MakeListOutputSchema("history", HistoryItemSchema()));
}
{
Json::Value props(Json::objectValue);
props["name"] = PropStr(u8"按收藏名子串过滤");
props["url"] = PropStr(u8"按 URL 子串过滤");
AddTool(tools, "host_list_favorites",
u8"列出在线 Windows 主机的 IE 收藏夹(主机管理「收藏夹」标签页)。隐私数据,仅在显式开启主机管理时可见。",
ReadInputSchema(props), MakeListOutputSchema("favorites", FavoriteItemSchema()));
}
{
Json::Value props(Json::objectValue);
props["name"] = PropStr(u8"按服务名或显示名子串过滤");
props["status"] = PropStr(u8"按状态子串过滤,如 Running");
props["start_type"] = PropStr(u8"按启动类型子串过滤,如 Auto");
AddTool(tools, "host_list_win32_services",
u8"列出在线 Windows 主机的 Win32 服务(主机管理「服务」标签页之一)。返回显示名、描述、状态、启动类型、登录身份、服务名与可执行路径;服务名可用于 host_control_service。",
ReadInputSchema(props), MakeListOutputSchema("services", ServiceItemSchema()));
}
{
Json::Value props(Json::objectValue);
props["name"] = PropStr(u8"按服务名或显示名子串过滤");
props["status"] = PropStr(u8"按状态子串过滤");
props["start_type"] = PropStr(u8"按启动类型子串过滤");
AddTool(tools, "host_list_driver_services",
u8"列出在线 Windows 主机的驱动服务(主机管理「服务」标签页之一)。字段同 host_list_win32_services。",
ReadInputSchema(props), MakeListOutputSchema("services", ServiceItemSchema()));
}
{
Json::Value props(Json::objectValue);
props["name"] = PropStr(u8"按任务名子串过滤");
props["folder"] = PropStr(u8"按任务文件夹子串过滤,如 \\Microsoft");
props["status"] = PropStr(u8"按状态子串过滤,如 Ready / Disabled");
AddTool(tools, "host_list_scheduled_tasks",
u8"列出在线 Windows 主机的计划任务(主机管理「计划任务」标签页)。返回任务名、完整路径、所在文件夹、可执行文件、状态与上次/下次运行时间。任务名 + folder 可用于 host_control_task。",
ReadInputSchema(props), MakeListOutputSchema("tasks", TaskItemSchema()));
}
{
Json::Value props(Json::objectValue);
props["id"] = HostIdProp();
props["include_content"] = PropBool(u8"是否返回按行切分的内容,默认 true");
props["max_bytes"] = PropInt(u8"最多返回的原始字节数,默认 262144,上限 1048576");
Json::Value outProps(Json::objectValue);
outProps["path"] = PropStr();
outProps["encoding"] = PropStr(u8"utf-8 / gbk(自动探测)");
outProps["bytes"] = PropInt(u8"文件原始字节数");
outProps["returnedBytes"] = PropInt();
outProps["truncated"] = PropBool();
outProps["lineCount"] = PropInt();
Json::Value lines(Json::objectValue);
lines["type"] = "array";
lines["items"] = PropStr();
outProps["lines"] = lines;
outProps["content"] = PropStr(u8"未截断时的完整文本");
Json::Value out(Json::objectValue);
out["type"] = "object";
out["properties"] = outProps;
AddTool(tools, "host_get_hosts_file",
u8"读取在线 Windows 主机的 hosts 文件(主机管理「HOSTS」标签页),按行返回并自动识别 UTF-8/GBK。只读;不提供写入能力。",
MakeSchema(props, { "id" }), out);
}
// ---- 主机管理:4 个写工具(只读 + 可恢复操作) ----
if (writable) {
{
Json::Value props(Json::objectValue);
props["id"] = HostIdProp();
props["action"] = PropStrEnum(u8"freeze=挂起进程,thaw=恢复进程", { "freeze", "thaw" });
props["pid"] = PropInt(u8"目标进程 PID");
props["name"] = PropStr(u8"可选:校验进程名,防止 PID 被系统复用后误操作");
AddTool(tools, "host_control_process",
u8"挂起或恢复在线 Windows 主机上的一个进程(对应主机管理「进程」右键的冻结/解冻)。可能导致目标进程超时或崩溃,故归入危险操作:需要同时开启 McpMachineDangerous。动作没有回执,客户端也不回报结果;工具会在动作后重新拉取进程列表确认目标仍存在。",
MakeSchema(props, { "id", "action", "pid" }),
ActionOutputSchema());
}
{
Json::Value props(Json::objectValue);
props["id"] = HostIdProp();
props["action"] = PropStrEnum(u8"restore/hide/maximize/minimize",
{ "restore", "hide", "maximize", "minimize" });
Json::Value hwnd(Json::objectValue);
Json::Value types(Json::arrayValue);
types.append("integer");
types.append("string");
hwnd["type"] = types;
hwnd["description"] = u8"窗口句柄,取 host_list_windows 的 hwnd;整数或 0x 前缀字符串";
props["hwnd"] = hwnd;
AddTool(tools, "host_control_window",
u8"对在线 Windows 主机的窗口执行可恢复操作:restore(还原)/hide(隐藏)/maximize(最大化)/minimize(最小化)。动作后重新枚举可见窗口复核:hide 后 hwnd 应消失,其余动作后仍应存在。不提供关闭窗口。",
MakeSchema(props, { "id", "action", "hwnd" }),
ActionOutputSchema());
}
{
Json::Value props(Json::objectValue);
props["id"] = HostIdProp();
props["action"] = PropStrEnum(u8"start/stop/pause/continue/restart",
{ "start", "stop", "pause", "continue", "restart" });
props["name"] = PropStr(u8"服务名(ServiceName,不是显示名),取 host_list_*_services 的 name");
AddTool(tools, "host_control_service",
u8"控制在线 Windows 主机的服务:start/stop/pause/continue/restart。客户端对每次控制都会自动回一份刷新后的服务列表,工具据此复核状态并如实返回 ok;stop 与 restart 归入危险操作,需要同时开启 McpMachineDangerous。restart 先 stop 再 start。不提供删除服务。",
MakeSchema(props, { "id", "action", "name" }),
ActionOutputSchema());
}
{
Json::Value props(Json::objectValue);
props["id"] = HostIdProp();
props["action"] = PropStrEnum(u8"run=立即运行,stop=停止正在运行的任务", { "run", "stop" });
props["name"] = PropStr(u8"任务名,取 host_list_scheduled_tasks 的 name");
props["folder"] = PropStr(u8"可选:任务所在文件夹;同名任务存在多个时必填");
AddTool(tools, "host_control_task",
u8"运行或停止在线 Windows 主机的计划任务(主机管理「计划任务」右键)。客户端无回执,工具只复核任务仍存在并报告当前状态,不声称任务一定执行成功。不提供创建/删除任务。",
MakeSchema(props, { "id", "action", "name" }),
ActionOutputSchema());
}
}
}
std::string Call(const Json::Value& id, const std::string& toolName,
const Json::Value& args, CMy2015RemoteDlg* parent) {
CMcpServer& mcp = McpServer();
if (!mcp.IsMachineEnabled()) {
return BuildError(id, kErrDisabled,
"Host management tools are disabled (set McpMachine=1 to enable)");
}
if (!args.isObject()) {
return BuildError(id, -32602, "Invalid params: arguments must be an object");
}
// ---- 连接管理 ----
if (toolName == "list_network_connections")
return BuildListNetworkConnections(id, args, parent);
if (toolName == "kill_network_connection")
return BuildKillNetworkConnection(id, args, parent);
// ---- 主机管理:只读 ----
if (toolName == "host_list_processes") return BuildHostListProcesses(id, args, parent);
if (toolName == "host_list_windows") return BuildHostListWindows(id, args, parent);
if (toolName == "host_list_network_state") return BuildHostListNetworkState(id, args, parent);
if (toolName == "host_list_software") return BuildHostListSoftware(id, args, parent);
if (toolName == "host_list_browser_history") return BuildHostListBrowserHistory(id, args, parent);
if (toolName == "host_list_favorites") return BuildHostListFavorites(id, args, parent);
if (toolName == "host_list_win32_services")
return BuildHostListServices(id, args, parent, /*drivers=*/false);
if (toolName == "host_list_driver_services")
return BuildHostListServices(id, args, parent, /*drivers=*/true);
if (toolName == "host_list_scheduled_tasks") return BuildHostListScheduledTasks(id, args, parent);
if (toolName == "host_get_hosts_file") return BuildHostGetHostsFile(id, args, parent);
// ---- 主机管理:写(每个 builder 内部再校验只读/危险开关) ----
if (toolName == "host_control_process") return BuildHostControlProcess(id, args, parent);
if (toolName == "host_control_window") return BuildHostControlWindow(id, args, parent);
if (toolName == "host_control_service") return BuildHostControlService(id, args, parent);
if (toolName == "host_control_task") return BuildHostControlTask(id, args, parent);
return BuildError(id, -32601, "Unknown tool: " + toolName);
}
} // namespace McpMachine