Feature: Add MCP device management-control tools (kill/window/service/power)
Add P7 management-control tools behind a single McpManage flag (plus
McpReadonly=0): kill_process, operate_window, control_service and
power_action. The write tools run a ready->confirm lifecycle on their
sub-connection, using a new followUp/followUpSent field to tell the two
arrivals of the same TOKEN_*LIST apart; the write command and the follow-up
refresh command go out as two separate Send2Client calls, with a new
WaitSendIdle() (context.h / Server.h) draining the outbound buffer in between
so the same buffer is never WSASend'd concurrently.
McpSettingsDlg gains a "management operations" checkbox (IDC_MCP_MANAGE), the
main toolbar gains an MCP entry button, and en/de/zh-TW strings are synced.
power_action is fire-and-forget over the main connection (no confirmation).
Verified end-to-end against a real Windows 10/VMware client: kill_process
returns remaining:[] and operate_window/control_service return ok:true with
their confirm packet arriving after an HTTP split. Verification surfaced a
prerequisite parser bug, fixed separately in a72e6eb, where HeaderParser
latched m_bShouldUnmask FALSE on HTTP continuation chunks and dropped the
confirm packet.
Co-Authored-By: deepseek-v4-pro
This commit is contained in:
1 parent
a72e6eb391
commit
9eed43bc8d
13 files changed
+932
-10
No files matched your search
Binary file not shown.
@@ -1415,6 +1415,7 @@ VOID CMy2015RemoteDlg::CreateToolBar()
|
||||
m_ToolBar.SetButtonText(12, _TR("搜索"));
|
||||
m_ToolBar.SetButtonText(13, _TR("帮助"));
|
||||
m_ToolBar.SetButtonText(14, _T("Lang/语言"));
|
||||
m_ToolBar.SetButtonText(15, _T("MCP"));
|
||||
|
||||
// ============================================================
|
||||
// UIBranding: 根据编译时配置或运行时标志隐藏工具栏按钮
|
||||
@@ -1465,7 +1466,7 @@ VOID CMy2015RemoteDlg::CreateToolBar()
|
||||
}
|
||||
|
||||
// 如果所有按钮都被隐藏,则隐藏整个工具栏
|
||||
if (hiddenCount >= 15) {
|
||||
if (hiddenCount >= 16) {
|
||||
m_ToolBar.ShowWindow(SW_HIDE);
|
||||
}
|
||||
|
||||
@@ -2209,6 +2210,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
||||
McpServer().SetRemoteControlEnabled(THIS_CFG.GetInt("settings", "McpRemoteControl", 0) != 0);
|
||||
// 文件传输开关:默认关;仅 download_file 门控(不要求只读关)。
|
||||
McpServer().SetFileTransferEnabled(THIS_CFG.GetInt("settings", "McpFileTransfer", 0) != 0);
|
||||
// P7 管理控制开关:默认关;要求只读关(McpReadonly=0)才生效(工具列表/分派双重门控)。
|
||||
McpServer().SetManageEnabled(THIS_CFG.GetInt("settings", "McpManage", 0) != 0);
|
||||
if (!McpServer().Start(mcpBind, mcpPort)) {
|
||||
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
|
||||
} else {
|
||||
@@ -5202,6 +5205,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject)
|
||||
McpServer().OnScreenControlClosed(ContextObject);
|
||||
}
|
||||
|
||||
// MCP P7 管理写操作(kill/窗口/服务)接管的子连接断开:清挂起,避免超时路径对
|
||||
// 已回收(连接池复用)的 context 误 CancelIO(与终端/屏幕子连接的清理同理)。
|
||||
if (McpServer().IsRunning()) {
|
||||
McpServer().OnManageSubClosed(ContextObject);
|
||||
}
|
||||
|
||||
SOCKET nSocket = ContextObject->sClientSocket;
|
||||
|
||||
CDialogBase* p = (CDialogBase*)ContextObject->hDlg;
|
||||
@@ -6541,8 +6550,13 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
case TOKEN_PSLIST: { // 进程管理【x】
|
||||
// P2b:MCP 挂起时接管。进程/窗口列表无请求 id,按 device_id 匹配并同步拷贝
|
||||
// (此时 IO 线程阻塞在 NotifyProc,缓冲尚未被覆盖,拷贝安全)。
|
||||
// P7:写工具(kill_process/operate_window)的「就绪包」与「确认包」是同一 token,
|
||||
// OnSystemReady 命中就绪包时接管子连接并下发写命令(返回 true、不 CancelIO),
|
||||
// 命中确认包或只读工具时返回 false,走 TakeMainResponse + CancelIO 收尾。
|
||||
uint64_t devId = ContextObject->GetClientID();
|
||||
if (McpServer().IsPending(devId)) {
|
||||
if (McpServer().OnSystemReady(devId, ContextObject))
|
||||
break;
|
||||
McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength());
|
||||
ContextObject->CancelIO(); // 用完即关,避免子链接泄漏(仿 WebService 终端关闭)
|
||||
break;
|
||||
@@ -6585,8 +6599,12 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
case TOKEN_SERVERLIST: { // 服务管理【x】
|
||||
// P3:MCP 挂起时接管。服务列表为一次性子链接回传(CServicesManager 构造即发),
|
||||
// 取走即 CancelIO 关子链接(无 MFC 对话框续用该子链接)。
|
||||
// P7:control_service 的就绪/确认是同一 token,OnServiceReady 命中就绪包时接管并下发
|
||||
// COMMAND_SERVICECONFIG(返回 true、不 CancelIO),确认包走 TakeMainResponse 收尾。
|
||||
uint64_t devId = ContextObject->GetClientID();
|
||||
if (McpServer().IsPending(devId)) {
|
||||
if (McpServer().OnServiceReady(devId, ContextObject))
|
||||
break;
|
||||
McpServer().TakeMainResponse(devId, szBuffer, len);
|
||||
ContextObject->CancelIO();
|
||||
break;
|
||||
|
||||
@@ -840,6 +840,129 @@ Json::Value BuildServiceListOutputSchema() {
|
||||
return schema;
|
||||
}
|
||||
|
||||
// ========== P7 输入/输出 schema ==========
|
||||
|
||||
// 公共:为 props 追加必填的 id 字符串属性。
|
||||
static void AddIdRequiredProp(Json::Value& props) {
|
||||
Json::Value idProp(Json::objectValue);
|
||||
idProp["type"] = "string";
|
||||
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
|
||||
props["id"] = idProp;
|
||||
}
|
||||
|
||||
// 公共:组装 {id, ...额外必填} 的 object schema。
|
||||
static Json::Value FinalizeIdSchema(Json::Value props, std::initializer_list<const char*> extraRequired) {
|
||||
Json::Value schema(Json::objectValue);
|
||||
schema["type"] = "object";
|
||||
schema["properties"] = props;
|
||||
Json::Value required(Json::arrayValue);
|
||||
required.append("id");
|
||||
for (const char* r : extraRequired) required.append(r);
|
||||
schema["required"] = required;
|
||||
return schema;
|
||||
}
|
||||
|
||||
// 公共:{ ok: boolean } 输出 schema(operate_window / control_service / power_action 共用)。
|
||||
static Json::Value BuildOkOutputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
Json::Value ok(Json::objectValue);
|
||||
ok["type"] = "boolean";
|
||||
props["ok"] = ok;
|
||||
Json::Value schema(Json::objectValue);
|
||||
schema["type"] = "object";
|
||||
schema["properties"] = props;
|
||||
return schema;
|
||||
}
|
||||
|
||||
Json::Value BuildKillProcessInputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
AddIdRequiredProp(props);
|
||||
|
||||
Json::Value pidsProp(Json::objectValue);
|
||||
pidsProp["type"] = "array";
|
||||
Json::Value pidItems(Json::objectValue);
|
||||
pidItems["type"] = "integer";
|
||||
pidsProp["items"] = pidItems;
|
||||
pidsProp["description"] = u8"要终止的进程 PID 列表(至少 1 个)";
|
||||
props["pids"] = pidsProp;
|
||||
|
||||
return FinalizeIdSchema(props, { "pids" });
|
||||
}
|
||||
|
||||
Json::Value BuildKillProcessOutputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
Json::Value remProp(Json::objectValue);
|
||||
remProp["type"] = "array";
|
||||
Json::Value remItems(Json::objectValue);
|
||||
remItems["type"] = "integer";
|
||||
remProp["items"] = remItems;
|
||||
remProp["description"] = u8"杀完后仍在运行的 PID(含权限不足未能终止的)";
|
||||
props["remaining"] = remProp;
|
||||
|
||||
Json::Value schema(Json::objectValue);
|
||||
schema["type"] = "object";
|
||||
schema["properties"] = props;
|
||||
return schema;
|
||||
}
|
||||
|
||||
Json::Value BuildOperateWindowInputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
AddIdRequiredProp(props);
|
||||
|
||||
Json::Value hwndProp(Json::objectValue);
|
||||
hwndProp["type"] = "integer";
|
||||
hwndProp["description"] = u8"窗口句柄,取 list_windows 返回的 hwnd 字段";
|
||||
props["hwnd"] = hwndProp;
|
||||
|
||||
Json::Value actionProp(Json::objectValue);
|
||||
actionProp["type"] = "string";
|
||||
actionProp["description"] = u8"操作:close(关闭) | show(显示) | hide(隐藏) | minimize(最小化) | maximize(最大化) | restore(还原)";
|
||||
props["action"] = actionProp;
|
||||
|
||||
return FinalizeIdSchema(props, { "hwnd", "action" });
|
||||
}
|
||||
|
||||
Json::Value BuildOperateWindowOutputSchema() {
|
||||
return BuildOkOutputSchema();
|
||||
}
|
||||
|
||||
Json::Value BuildControlServiceInputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
AddIdRequiredProp(props);
|
||||
|
||||
Json::Value nameProp(Json::objectValue);
|
||||
nameProp["type"] = "string";
|
||||
nameProp["description"] = u8"服务名,取 list_services 返回的 service_name 字段";
|
||||
props["service_name"] = nameProp;
|
||||
|
||||
Json::Value actionProp(Json::objectValue);
|
||||
actionProp["type"] = "string";
|
||||
actionProp["description"] = u8"操作:start(启动) | stop(停止) | set_auto(设为自动) | set_manual(设为手动)";
|
||||
props["action"] = actionProp;
|
||||
|
||||
return FinalizeIdSchema(props, { "service_name", "action" });
|
||||
}
|
||||
|
||||
Json::Value BuildControlServiceOutputSchema() {
|
||||
return BuildOkOutputSchema();
|
||||
}
|
||||
|
||||
Json::Value BuildPowerActionInputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
AddIdRequiredProp(props);
|
||||
|
||||
Json::Value actionProp(Json::objectValue);
|
||||
actionProp["type"] = "string";
|
||||
actionProp["description"] = u8"操作:logout(注销) | shutdown(关机) | reboot(重启)";
|
||||
props["action"] = actionProp;
|
||||
|
||||
return FinalizeIdSchema(props, { "action" });
|
||||
}
|
||||
|
||||
Json::Value BuildPowerActionOutputSchema() {
|
||||
return BuildOkOutputSchema();
|
||||
}
|
||||
|
||||
// list_registry 的 inputSchema(id 必填,path 可选;path 缺省/空 = 列根键)
|
||||
Json::Value BuildListRegistryInputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
@@ -1163,6 +1286,20 @@ Json::Value BuildUploadFileOutputSchema();
|
||||
std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
std::string BuildRemoteClipboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
|
||||
// ===== P7 前置声明(定义见下方「tools/call 分派」前)=====
|
||||
Json::Value BuildKillProcessInputSchema();
|
||||
Json::Value BuildKillProcessOutputSchema();
|
||||
Json::Value BuildOperateWindowInputSchema();
|
||||
Json::Value BuildOperateWindowOutputSchema();
|
||||
Json::Value BuildControlServiceInputSchema();
|
||||
Json::Value BuildControlServiceOutputSchema();
|
||||
Json::Value BuildPowerActionInputSchema();
|
||||
Json::Value BuildPowerActionOutputSchema();
|
||||
std::string BuildKillProcess(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
std::string BuildOperateWindow(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
std::string BuildControlService(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
std::string BuildPowerAction(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
|
||||
// tools/list
|
||||
std::string BuildToolsListResult(const Json::Value& id) {
|
||||
Json::Value result(Json::objectValue);
|
||||
@@ -1433,6 +1570,43 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
tools.append(tool);
|
||||
}
|
||||
|
||||
// 18) kill_process / operate_window / control_service / power_action(P7:设备管理控制,
|
||||
// 仅 Windows,安全门:McpManage=1 且 McpReadonly=0)
|
||||
if (CMcpServer::Instance().IsManageEnabled() && !CMcpServer::Instance().IsReadonly()) {
|
||||
{
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "kill_process";
|
||||
tool["description"] = u8"终止指定在线 Windows 主机上的一个或多个进程(TerminateProcess 强制结束,不保存进程状态,可能造成数据丢失)。返回 remaining(杀完后仍在运行的 PID)。不可撤销,请谨慎确认 PID。";
|
||||
tool["inputSchema"] = BuildKillProcessInputSchema();
|
||||
tool["outputSchema"] = BuildKillProcessOutputSchema();
|
||||
tools.append(tool);
|
||||
}
|
||||
{
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "operate_window";
|
||||
tool["description"] = u8"对指定在线 Windows 主机的窗口执行操作:close(关闭)/show(显示)/hide(隐藏)/minimize(最小化)/maximize(最大化)/restore(还原)。hwnd 取 list_windows 返回的 hwnd。ok 仅表示已下发(窗口句柄可能已失效)。";
|
||||
tool["inputSchema"] = BuildOperateWindowInputSchema();
|
||||
tool["outputSchema"] = BuildOperateWindowOutputSchema();
|
||||
tools.append(tool);
|
||||
}
|
||||
{
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "control_service";
|
||||
tool["description"] = u8"控制指定在线 Windows 主机的服务:start(启动)/stop(停止)/set_auto(设为自动)/set_manual(设为手动)。service_name 取 list_services 返回的 service_name。ok 仅表示已下发(成功与否需再查 list_services 核对状态)。";
|
||||
tool["inputSchema"] = BuildControlServiceInputSchema();
|
||||
tool["outputSchema"] = BuildControlServiceOutputSchema();
|
||||
tools.append(tool);
|
||||
}
|
||||
{
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "power_action";
|
||||
tool["description"] = u8"对指定在线 Windows 主机执行电源操作:logout(注销)/shutdown(关机)/reboot(重启)。不可撤销,主机将立即下线;需客户端管理员权限。";
|
||||
tool["inputSchema"] = BuildPowerActionInputSchema();
|
||||
tool["outputSchema"] = BuildPowerActionOutputSchema();
|
||||
tools.append(tool);
|
||||
}
|
||||
}
|
||||
|
||||
result["tools"] = tools;
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
@@ -4175,6 +4349,332 @@ std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2
|
||||
return BuildError(id, -32001, msg);
|
||||
}
|
||||
|
||||
// ========== P7:设备管理控制 ==========
|
||||
|
||||
// 读取 JSON number 或纯数字字符串为 uint64。
|
||||
static bool ReadUInt64(const Json::Value& v, uint64_t& out) {
|
||||
if (v.isNumeric()) { out = v.asUInt64(); return true; }
|
||||
if (v.isString()) {
|
||||
std::string s = v.asString();
|
||||
if (IsDigits(s)) { out = strtoull(s.c_str(), nullptr, 10); return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out) {
|
||||
if (!args.isMember(key)) return false;
|
||||
return ReadUInt64(args[key], out);
|
||||
}
|
||||
|
||||
// 写审计(杀进程/窗口/服务/电源共用):PostMessageA 落到服务端消息/审计日志(不可关闭)。
|
||||
static void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
|
||||
const std::string& tool, const std::string& detail) {
|
||||
if (!parent) return;
|
||||
std::string text = "host " + std::to_string(devId) + " " + tool + ": " + detail;
|
||||
parent->PostMessageA(WM_SHOWERRORMSG,
|
||||
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
|
||||
(LPARAM)new CString(_TR("MCP管理操作")));
|
||||
}
|
||||
|
||||
// kill_process:主连接 COMMAND_SYSTEM → 系统子连接 TOKEN_PSLIST(就绪)→ COMMAND_KILLPROCESS
|
||||
// → 补发 COMMAND_PSLIST → 第二个 TOKEN_PSLIST(确认)。payload 与 SystemDlg::OnPlistKill 一致。
|
||||
std::string BuildKillProcess(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||||
uint64_t devId = 0;
|
||||
std::string err;
|
||||
if (!ParseHostIdArg(args, devId, err))
|
||||
return BuildError(id, -32602, err);
|
||||
|
||||
context* ctx = FindMainContext(parent, devId);
|
||||
if (!ctx)
|
||||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||||
|
||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
|
||||
return BuildError(id, -32005, "kill_process is only supported on Windows hosts");
|
||||
|
||||
CMcpServer& mcp = CMcpServer::Instance();
|
||||
if (mcp.IsReadonly())
|
||||
return BuildError(id, -32006, "kill_process is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
|
||||
if (!mcp.IsManageEnabled())
|
||||
return BuildError(id, -32006, "kill_process is disabled: management operations are off (set McpManage=1 to enable)");
|
||||
|
||||
const Json::Value& pids = args["pids"];
|
||||
if (!pids.isArray() || pids.size() == 0)
|
||||
return BuildError(id, -32602, "Missing or invalid parameter: pids (non-empty integer array)");
|
||||
|
||||
std::vector<uint64_t> pidList;
|
||||
std::vector<BYTE> followUp;
|
||||
followUp.push_back((BYTE)COMMAND_KILLPROCESS);
|
||||
for (unsigned int i = 0; i < pids.size(); ++i) {
|
||||
uint64_t pid = 0;
|
||||
if (!ReadUInt64(pids[i], pid) || pid == 0)
|
||||
return BuildError(id, -32602, "pids must be non-zero integers");
|
||||
pidList.push_back(pid);
|
||||
DWORD d = (DWORD)pid;
|
||||
followUp.push_back((BYTE)(d & 0xFF));
|
||||
followUp.push_back((BYTE)((d >> 8) & 0xFF));
|
||||
followUp.push_back((BYTE)((d >> 16) & 0xFF));
|
||||
followUp.push_back((BYTE)((d >> 24) & 0xFF));
|
||||
}
|
||||
|
||||
if (!mcp.BeginPendingFollowUp(devId, "kill_process", followUp))
|
||||
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||||
|
||||
BYTE cmd = COMMAND_SYSTEM;
|
||||
if (!ctx->Send2Client(&cmd, 1)) {
|
||||
mcp.ClearPending(devId);
|
||||
return BuildError(id, -32004, "Failed to send command to host");
|
||||
}
|
||||
|
||||
std::vector<BYTE> data;
|
||||
if (!mcp.WaitPendingWrite(devId, data, kMcpToolTimeoutMs))
|
||||
return BuildError(id, -32001, "Timeout waiting for process list after kill");
|
||||
|
||||
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
|
||||
Json::Value procs = ParseProcessList(data, cp);
|
||||
|
||||
Json::Value remaining(Json::arrayValue);
|
||||
std::string detail = "pids=";
|
||||
for (size_t i = 0; i < pidList.size(); ++i) {
|
||||
if (i) detail += ",";
|
||||
detail += std::to_string(pidList[i]);
|
||||
bool stillAlive = false;
|
||||
for (unsigned int j = 0; j < procs.size(); ++j) {
|
||||
if (procs[j]["pid"].asUInt64() == pidList[i]) { stillAlive = true; break; }
|
||||
}
|
||||
if (stillAlive) remaining.append((Json::UInt64)pidList[i]);
|
||||
}
|
||||
|
||||
AuditManageAction(parent, devId, "kill_process", detail);
|
||||
|
||||
Json::Value result(Json::objectValue);
|
||||
Json::Value structuredContent(Json::objectValue);
|
||||
structuredContent["remaining"] = remaining;
|
||||
result["structuredContent"] = structuredContent;
|
||||
|
||||
Json::Value content(Json::arrayValue);
|
||||
Json::Value item(Json::objectValue);
|
||||
item["type"] = "text";
|
||||
item["text"] = remaining.empty()
|
||||
? std::string(u8"目标进程已全部终止。")
|
||||
: std::string(u8"仍有 ") + std::to_string((int)remaining.size()) + std::string(u8" 个进程存活(见 remaining)。");
|
||||
content.append(item);
|
||||
result["content"] = content;
|
||||
result["isError"] = false;
|
||||
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// operate_window:主连接 COMMAND_WSLIST → 窗口子连接 TOKEN_WSLIST(就绪)→ CMD_WINDOW_CLOSE
|
||||
// (close 客户端自动回 TOKEN_WSLIST)或 CMD_WINDOW_TEST(test 补发 COMMAND_WSLIST 索取)。
|
||||
// 20 字节零填充 buffer 复刻 SystemDlg::OnWlistClose/OnWlistHide/Recover/Max/Min(hwnd 为 DWORD)。
|
||||
std::string BuildOperateWindow(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||||
uint64_t devId = 0;
|
||||
std::string err;
|
||||
if (!ParseHostIdArg(args, devId, err))
|
||||
return BuildError(id, -32602, err);
|
||||
|
||||
context* ctx = FindMainContext(parent, devId);
|
||||
if (!ctx)
|
||||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||||
|
||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
|
||||
return BuildError(id, -32005, "operate_window is only supported on Windows hosts");
|
||||
|
||||
CMcpServer& mcp = CMcpServer::Instance();
|
||||
if (mcp.IsReadonly())
|
||||
return BuildError(id, -32006, "operate_window is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
|
||||
if (!mcp.IsManageEnabled())
|
||||
return BuildError(id, -32006, "operate_window is disabled: management operations are off (set McpManage=1 to enable)");
|
||||
|
||||
uint64_t hwnd = 0;
|
||||
if (!ReadUInt64Arg(args, "hwnd", hwnd) || hwnd == 0)
|
||||
return BuildError(id, -32602, "Missing or invalid parameter: hwnd (non-zero window handle)");
|
||||
|
||||
std::string action = Trim(GetStringArg(args, "action"));
|
||||
|
||||
BYTE lpMsgBuf[20] = { 0 };
|
||||
DWORD h = (DWORD)hwnd;
|
||||
if (action == "close") {
|
||||
lpMsgBuf[0] = CMD_WINDOW_CLOSE;
|
||||
memcpy(lpMsgBuf + 1, &h, sizeof(DWORD));
|
||||
} else {
|
||||
DWORD dHow;
|
||||
if (action == "hide") dHow = SW_HIDE; // 0
|
||||
else if (action == "show") dHow = SW_NORMAL; // 1
|
||||
else if (action == "restore") dHow = SW_NORMAL; // 1
|
||||
else if (action == "maximize") dHow = SW_MAXIMIZE; // 3
|
||||
else if (action == "minimize") dHow = SW_MINIMIZE; // 6
|
||||
else return BuildError(id, -32602,
|
||||
"Invalid action: " + action + " (expect close|show|hide|minimize|maximize|restore)");
|
||||
lpMsgBuf[0] = CMD_WINDOW_TEST;
|
||||
memcpy(lpMsgBuf + 1, &h, sizeof(DWORD));
|
||||
memcpy(lpMsgBuf + 1 + sizeof(DWORD), &dHow, sizeof(DWORD));
|
||||
}
|
||||
|
||||
std::vector<BYTE> followUp(lpMsgBuf, lpMsgBuf + sizeof(lpMsgBuf));
|
||||
if (!mcp.BeginPendingFollowUp(devId, "operate_window", followUp))
|
||||
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||||
|
||||
BYTE cmd = COMMAND_WSLIST;
|
||||
if (!ctx->Send2Client(&cmd, 1)) {
|
||||
mcp.ClearPending(devId);
|
||||
return BuildError(id, -32004, "Failed to send command to host");
|
||||
}
|
||||
|
||||
std::vector<BYTE> data;
|
||||
if (!mcp.WaitPendingWrite(devId, data, kMcpToolTimeoutMs))
|
||||
return BuildError(id, -32001, "Timeout waiting for window operation");
|
||||
|
||||
AuditManageAction(parent, devId, "operate_window",
|
||||
"action=" + action + " hwnd=" + std::to_string(hwnd));
|
||||
|
||||
Json::Value result(Json::objectValue);
|
||||
Json::Value structuredContent(Json::objectValue);
|
||||
structuredContent["ok"] = true;
|
||||
result["structuredContent"] = structuredContent;
|
||||
|
||||
Json::Value content(Json::arrayValue);
|
||||
Json::Value item(Json::objectValue);
|
||||
item["type"] = "text";
|
||||
item["text"] = std::string(u8"已下发窗口操作:") + action;
|
||||
content.append(item);
|
||||
result["content"] = content;
|
||||
result["isError"] = false;
|
||||
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// control_service:主连接 COMMAND_SERVICES → 服务子连接 TOKEN_SERVERLIST(就绪)→
|
||||
// COMMAND_SERVICECONFIG → 客户端写后自动 SendServicesList(确认,无需补发)。
|
||||
std::string BuildControlService(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||||
uint64_t devId = 0;
|
||||
std::string err;
|
||||
if (!ParseHostIdArg(args, devId, err))
|
||||
return BuildError(id, -32602, err);
|
||||
|
||||
context* ctx = FindMainContext(parent, devId);
|
||||
if (!ctx)
|
||||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||||
|
||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
|
||||
return BuildError(id, -32005, "control_service is only supported on Windows hosts");
|
||||
|
||||
CMcpServer& mcp = CMcpServer::Instance();
|
||||
if (mcp.IsReadonly())
|
||||
return BuildError(id, -32006, "control_service is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
|
||||
if (!mcp.IsManageEnabled())
|
||||
return BuildError(id, -32006, "control_service is disabled: management operations are off (set McpManage=1 to enable)");
|
||||
|
||||
std::string serviceName = Trim(GetStringArg(args, "service_name"));
|
||||
if (serviceName.empty())
|
||||
return BuildError(id, -32602, "Missing required parameter: service_name");
|
||||
|
||||
std::string action = Trim(GetStringArg(args, "action"));
|
||||
BYTE bCmd;
|
||||
if (action == "start") bCmd = 1;
|
||||
else if (action == "stop") bCmd = 2;
|
||||
else if (action == "set_auto") bCmd = 3;
|
||||
else if (action == "set_manual") bCmd = 4;
|
||||
else return BuildError(id, -32602,
|
||||
"Invalid action: " + action + " (expect start|stop|set_auto|set_manual)");
|
||||
|
||||
// 服务名来自 Windows A 接口(GBK/936),下发前转 ANSI。payload 与 MachineDlg 一致:
|
||||
// [COMMAND_SERVICECONFIG:1][bCmd:1][serviceName\0]。
|
||||
std::string ansiName = ToAnsi(serviceName, 936);
|
||||
std::vector<BYTE> followUp;
|
||||
followUp.reserve(2 + ansiName.size() + 1);
|
||||
followUp.push_back((BYTE)COMMAND_SERVICECONFIG);
|
||||
followUp.push_back(bCmd);
|
||||
followUp.insert(followUp.end(), ansiName.begin(), ansiName.end());
|
||||
followUp.push_back(0);
|
||||
|
||||
if (!mcp.BeginPendingFollowUp(devId, "control_service", followUp))
|
||||
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||||
|
||||
BYTE cmd = COMMAND_SERVICES;
|
||||
if (!ctx->Send2Client(&cmd, 1)) {
|
||||
mcp.ClearPending(devId);
|
||||
return BuildError(id, -32004, "Failed to send command to host");
|
||||
}
|
||||
|
||||
std::vector<BYTE> data;
|
||||
if (!mcp.WaitPendingWrite(devId, data, kMcpToolTimeoutMs))
|
||||
return BuildError(id, -32001, "Timeout waiting for service operation");
|
||||
|
||||
AuditManageAction(parent, devId, "control_service",
|
||||
"service=" + serviceName + " action=" + action);
|
||||
|
||||
Json::Value result(Json::objectValue);
|
||||
Json::Value structuredContent(Json::objectValue);
|
||||
structuredContent["ok"] = true;
|
||||
result["structuredContent"] = structuredContent;
|
||||
|
||||
Json::Value content(Json::arrayValue);
|
||||
Json::Value item(Json::objectValue);
|
||||
item["type"] = "text";
|
||||
item["text"] = std::string(u8"已下发服务操作:") + action + std::string(u8" ") + serviceName;
|
||||
content.append(item);
|
||||
result["content"] = content;
|
||||
result["isError"] = false;
|
||||
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// power_action:主连接 TOKEN_MACHINE_MANAGE,发完即走(无响应);主机立即离线。
|
||||
std::string BuildPowerAction(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||||
uint64_t devId = 0;
|
||||
std::string err;
|
||||
if (!ParseHostIdArg(args, devId, err))
|
||||
return BuildError(id, -32602, err);
|
||||
|
||||
context* ctx = FindMainContext(parent, devId);
|
||||
if (!ctx)
|
||||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||||
|
||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
|
||||
return BuildError(id, -32005, "power_action is only supported on Windows hosts");
|
||||
|
||||
CMcpServer& mcp = CMcpServer::Instance();
|
||||
if (mcp.IsReadonly())
|
||||
return BuildError(id, -32006, "power_action is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
|
||||
if (!mcp.IsManageEnabled())
|
||||
return BuildError(id, -32006, "power_action is disabled: management operations are off (set McpManage=1 to enable)");
|
||||
|
||||
std::string action = Trim(GetStringArg(args, "action"));
|
||||
BYTE type;
|
||||
if (action == "logout") type = MACHINE_LOGOUT;
|
||||
else if (action == "shutdown") type = MACHINE_SHUTDOWN;
|
||||
else if (action == "reboot") type = MACHINE_REBOOT;
|
||||
else return BuildError(id, -32602,
|
||||
"Invalid action: " + action + " (expect logout|shutdown|reboot)");
|
||||
|
||||
// 主连接、发完即走、无响应(客户端 KernelManager 只校验 ulLength>1 并读 szBuffer[1])。
|
||||
BYTE token[2] = { (BYTE)TOKEN_MACHINE_MANAGE, type };
|
||||
if (!ctx->Send2Client(token, 2))
|
||||
return BuildError(id, -32004, "Failed to send command to host");
|
||||
|
||||
AuditManageAction(parent, devId, "power_action", "action=" + action);
|
||||
|
||||
Json::Value result(Json::objectValue);
|
||||
Json::Value structuredContent(Json::objectValue);
|
||||
structuredContent["ok"] = true;
|
||||
result["structuredContent"] = structuredContent;
|
||||
|
||||
Json::Value content(Json::arrayValue);
|
||||
Json::Value item(Json::objectValue);
|
||||
item["type"] = "text";
|
||||
item["text"] = std::string(u8"已下发电源操作:") + action;
|
||||
content.append(item);
|
||||
result["content"] = content;
|
||||
result["isError"] = false;
|
||||
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// tools/call 分派
|
||||
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||
const Json::Value& id = root["id"];
|
||||
@@ -4210,6 +4710,10 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||
if (toolName == "remote_clipboard") return BuildRemoteClipboard(id, args, parent);
|
||||
if (toolName == "download_file") return BuildDownloadFile(id, args, parent);
|
||||
if (toolName == "upload_file") return BuildUploadFile(id, args, parent);
|
||||
if (toolName == "kill_process") return BuildKillProcess(id, args, parent);
|
||||
if (toolName == "operate_window") return BuildOperateWindow(id, args, parent);
|
||||
if (toolName == "control_service") return BuildControlService(id, args, parent);
|
||||
if (toolName == "power_action") return BuildPowerAction(id, args, parent);
|
||||
|
||||
return BuildError(id, -32602,
|
||||
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
|
||||
@@ -4727,7 +5231,8 @@ bool CMcpServer::IsPending(uint64_t device_id) {
|
||||
void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) {
|
||||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||
auto it = m_Pending.find(device_id);
|
||||
if (it == m_Pending.end()) return; // 已超时清理 → 迟到数据,丢弃
|
||||
if (it == m_Pending.end()) { Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d: no pending (late/dropped)\n", device_id, (int)len); return; } // 已超时清理 → 迟到数据,丢弃
|
||||
Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d done\n", device_id, (int)len);
|
||||
it->second.data.assign(data, data + len);
|
||||
it->second.done = true;
|
||||
m_PendingCv.notify_one();
|
||||
@@ -4902,6 +5407,110 @@ bool CMcpServer::WaitPendingRegistry(uint64_t device_id, std::vector<BYTE>& subk
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== P7:设备管理控制(写工具接管)=====
|
||||
|
||||
bool CMcpServer::BeginPendingFollowUp(uint64_t device_id, const std::string& tool,
|
||||
const std::vector<BYTE>& followUp) {
|
||||
if (IsFileTransferPending(device_id)) return false; // 该 host 有文件传输会话在飞(互斥,见 F2)
|
||||
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.followUp = followUp;
|
||||
m_Pending[device_id] = std::move(r);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CMcpServer::OnSystemReady(uint64_t device_id, context* subCtx) {
|
||||
std::string tool;
|
||||
std::vector<BYTE> followUp;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||
auto it = m_Pending.find(device_id);
|
||||
if (it == m_Pending.end()) { Mprintf("[MCP-P7] OnSystemReady dev=%llu: no pending\n", device_id); return false; } // 无挂起 → 回落 MFC
|
||||
tool = it->second.tool;
|
||||
if (tool != "kill_process" && tool != "operate_window") { Mprintf("[MCP-P7] OnSystemReady dev=%llu tool=%s: not write tool\n", device_id, tool.c_str()); return false; }
|
||||
if (it->second.followUpSent) { Mprintf("[MCP-P7] OnSystemReady dev=%llu tool=%s: CONFIRM token -> TakeMainResponse\n", device_id, tool.c_str()); return false; } // 确认包 → TakeMainResponse
|
||||
followUp = it->second.followUp;
|
||||
it->second.subCtx = subCtx;
|
||||
it->second.followUpSent = true;
|
||||
}
|
||||
|
||||
// 锁外下发写命令;kill/test 需补发刷新命令索取新列表(close/control_service 客户端自动回)。
|
||||
BOOL b1 = subCtx->Send2Client(followUp.data(), (ULONG)followUp.size());
|
||||
BYTE refresh = 0;
|
||||
if (tool == "kill_process") {
|
||||
refresh = (BYTE)COMMAND_PSLIST;
|
||||
} else if (!followUp.empty() && followUp[0] == CMD_WINDOW_TEST) {
|
||||
refresh = (BYTE)COMMAND_WSLIST;
|
||||
}
|
||||
if (refresh) {
|
||||
// followUp 与 refresh 是两次独立 Send2Client,各自 PostQueuedCompletionStatus(IOWrite),
|
||||
// worker 会对同一 OutCompressedBuffer 并发发起 WSASend,造成竞态/丢包。用 WaitSendIdle
|
||||
// 确定性等首个包真正发完再发刷新包(比 MFC 的 Sleep(100) 盲等更稳)。
|
||||
subCtx->WaitSendIdle(2000);
|
||||
BOOL b2 = subCtx->Send2Client(&refresh, 1);
|
||||
Mprintf("[MCP-P7] OnSystemReady dev=%llu tool=%s READY: followUpSend=%d refreshSend=%d refresh=%d\n",
|
||||
device_id, tool.c_str(), b1 ? 1 : 0, b2 ? 1 : 0, (int)refresh);
|
||||
}
|
||||
return true; // 已接管,不 CancelIO
|
||||
}
|
||||
|
||||
bool CMcpServer::OnServiceReady(uint64_t device_id, context* subCtx) {
|
||||
std::vector<BYTE> followUp;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||
auto it = m_Pending.find(device_id);
|
||||
if (it == m_Pending.end()) return false;
|
||||
if (it->second.tool != "control_service") return false;
|
||||
if (it->second.followUpSent) return false; // 确认包 → 调用方 TakeMainResponse
|
||||
followUp = it->second.followUp;
|
||||
it->second.subCtx = subCtx;
|
||||
it->second.followUpSent = true;
|
||||
}
|
||||
subCtx->Send2Client(followUp.data(), (ULONG)followUp.size());
|
||||
return true; // 客户端写后自动回,无需补发
|
||||
}
|
||||
|
||||
bool CMcpServer::WaitPendingWrite(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs) {
|
||||
context* subCtx = nullptr;
|
||||
std::unique_lock<std::mutex> lk(m_PendingMutex);
|
||||
auto it = m_Pending.find(device_id);
|
||||
if (it == m_Pending.end()) return false;
|
||||
|
||||
bool signaled = m_PendingCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
|
||||
[&] { return it->second.done; });
|
||||
if (!signaled || it->second.data.empty()) {
|
||||
subCtx = it->second.subCtx;
|
||||
Mprintf("[MCP-P7] WaitPendingWrite dev=%llu TIMEOUT: signaled=%d dataEmpty=%d subCtx=%p\n",
|
||||
device_id, signaled ? 1 : 0, it->second.data.empty() ? 1 : 0, (void*)subCtx);
|
||||
m_Pending.erase(it); // 超时/空数据 → 清理
|
||||
lk.unlock();
|
||||
if (subCtx) subCtx->CancelIO(); // 锁外关子连接,防孤儿子连接
|
||||
return false;
|
||||
}
|
||||
out = std::move(it->second.data);
|
||||
Mprintf("[MCP-P7] WaitPendingWrite dev=%llu OK len=%d\n", device_id, (int)out.size());
|
||||
m_Pending.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
void CMcpServer::OnManageSubClosed(context* subCtx) {
|
||||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||
// 只匹配「已接管、等确认包」的写工具子连接(read 工具从不存 subCtx,天然不命中)。
|
||||
// 扫描而非按 device_id 查找,避免依赖断线后 GetClientID 是否仍有效。
|
||||
for (auto& kv : m_Pending) {
|
||||
if (kv.second.subCtx == subCtx) {
|
||||
Mprintf("[MCP-P7] OnManageSubClosed subCtx=%p tool=%s: sub-conn closed before confirm\n",
|
||||
(void*)subCtx, kv.second.tool.c_str());
|
||||
kv.second.subCtx = nullptr; // 断线后子连接已死/将回收,超时路径不得再 CancelIO
|
||||
kv.second.done = true; // 唤醒等待线程(WaitPendingWrite 见 data 空 → 返回失败)
|
||||
m_PendingCv.notify_all();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rand_s:Windows CRT 加密安全随机源(基于系统 CSPRNG)。其声明需在 <stdlib.h> 前
|
||||
// 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。
|
||||
extern "C" int __cdecl rand_s(unsigned int* randomValue);
|
||||
|
||||
@@ -242,6 +242,34 @@ public:
|
||||
// 收尾:擦会话 + 锁外 CancelIO 文件管理器/流式两条子链接(幂等)。
|
||||
void ClearFileTransfer(uint64_t device_id);
|
||||
|
||||
// ===== P7:设备管理控制(kill_process / operate_window / control_service / power_action)=====
|
||||
// 写工具复用 m_Pending 单飞行;followUp/followUpSent/subCtx 标记接管进度。
|
||||
// 「就绪 token」与「确认 token」是同一 token(客户端子连接构造即回一包,写后再回一包),
|
||||
// 故必须用 followUpSent 区分两次到达,不能靠 token 区分(与 list_registry 的 PATH/KEY 双 token 不同)。
|
||||
void SetManageEnabled(bool enabled) { m_manageEnabled = enabled; }
|
||||
bool IsManageEnabled() const { return m_manageEnabled; }
|
||||
|
||||
// 登记写挂起:与 BeginPending 同单飞行,另预填 followUp(写命令 payload)。
|
||||
bool BeginPendingFollowUp(uint64_t device_id, const std::string& tool,
|
||||
const std::vector<BYTE>& followUp);
|
||||
|
||||
// MessageHandle(TOKEN_PSLIST/TOKEN_WSLIST) 调用:命中且为写工具(kill_process/operate_window)
|
||||
// 则接管子连接、下发 followUp(返回 true=已接管,调用方不 CancelIO);否则 false(回落 MFC
|
||||
// 或按确认包走 TakeMainResponse)。
|
||||
bool OnSystemReady(uint64_t device_id, context* subCtx);
|
||||
|
||||
// MessageHandle(TOKEN_SERVERLIST) 调用:命中且为 control_service 则接管、下发
|
||||
// COMMAND_SERVICECONFIG(返回 true=已接管);否则 false。
|
||||
bool OnServiceReady(uint64_t device_id, context* subCtx);
|
||||
|
||||
// 工具线程:等待写确认包(第二个同 token 包);超时/空数据清挂起并 CancelIO 子连接。
|
||||
bool WaitPendingWrite(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs);
|
||||
|
||||
// OfflineProc 调用:接管的子连接断开时清挂起(置 done 并清 subCtx,唤醒等待线程)。
|
||||
// 不擦表(由 WaitPendingWrite 持有迭代器时自行擦除,防迭代器失效);清 subCtx 使
|
||||
// 超时路径不会对已回收(连接池复用)的 context 误 CancelIO。
|
||||
void OnManageSubClosed(context* subCtx);
|
||||
|
||||
// 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。
|
||||
void SetReadonly(bool readonly) { m_readonly = readonly; }
|
||||
void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; }
|
||||
@@ -274,6 +302,10 @@ private:
|
||||
bool pathDone = false; // list_registry:已收到 TOKEN_REG_PATH
|
||||
bool keyDone = false; // list_registry:已收到 TOKEN_REG_KEY
|
||||
bool done = false;
|
||||
// ===== P7:写工具(kill_process / operate_window / control_service)接管进度 =====
|
||||
std::vector<BYTE> followUp; // 接管后下发的写命令 payload(首字节 = 写命令)
|
||||
bool followUpSent = false; // false=等子连接就绪(首包);true=已发写命令、等确认包
|
||||
context* subCtx = nullptr; // 接管的子连接(超时/失败 CancelIO 用,防孤儿子连接)
|
||||
};
|
||||
std::mutex m_PendingMutex;
|
||||
std::condition_variable m_PendingCv;
|
||||
@@ -348,6 +380,7 @@ private:
|
||||
bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false)
|
||||
bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false)
|
||||
bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file)
|
||||
bool m_manageEnabled = false; // P7 管理控制开关(默认关;要求 m_readonly=false)
|
||||
};
|
||||
|
||||
// 全局访问器(仿 WebService(),见 WebService.h 末尾)
|
||||
|
||||
@@ -145,6 +145,9 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
m_btnFileTransfer.Create(_TR("启用文件传输(download_file)"),
|
||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||||
r0, this, IDC_MCP_FILETRANSFER);
|
||||
m_btnManage.Create(_TR("启用管理操作(进程/窗口/服务/电源)"),
|
||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||||
r0, this, IDC_MCP_MANAGE);
|
||||
m_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
|
||||
m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP |
|
||||
ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL,
|
||||
@@ -168,6 +171,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
m_btnTerminal.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnRemoteControl.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnFileTransfer.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnManage.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_lblWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_editWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
@@ -184,6 +188,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
int terminal = THIS_CFG.GetInt("settings", "McpTerminal", 0);
|
||||
int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0);
|
||||
int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0);
|
||||
int manage = THIS_CFG.GetInt("settings", "McpManage", 0);
|
||||
std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", "");
|
||||
|
||||
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
|
||||
@@ -194,6 +199,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED);
|
||||
m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED);
|
||||
m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED);
|
||||
m_btnManage.SetCheck(manage ? BST_CHECKED : BST_UNCHECKED);
|
||||
// 白名单存储为逗号分隔,展示为每行一条。
|
||||
m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str()));
|
||||
|
||||
@@ -213,6 +219,7 @@ void CMcpSettingsDlg::OnOK()
|
||||
bool terminal = (m_btnTerminal.GetCheck() == BST_CHECKED);
|
||||
bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED);
|
||||
bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED);
|
||||
bool manage = (m_btnManage.GetCheck() == BST_CHECKED);
|
||||
|
||||
// 端口校验:1-65535
|
||||
int port = atoi(CT2A(sPort));
|
||||
@@ -239,14 +246,15 @@ void CMcpSettingsDlg::OnOK()
|
||||
THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 1 : 0);
|
||||
THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0);
|
||||
THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0);
|
||||
THIS_CFG.SetInt("settings", "McpManage", manage ? 1 : 0);
|
||||
std::string whitelist = CT2A(sWhitelist);
|
||||
whitelist = NormalizeWhitelist(whitelist);
|
||||
THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist);
|
||||
|
||||
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
|
||||
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
|
||||
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。") + _T("\r\n") +
|
||||
_TR("持久终端与远程控制仅在只读模式关闭时生效。"),
|
||||
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。") + _T("\r\n") +
|
||||
_TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。"),
|
||||
_TR("提示"), MB_ICONINFORMATION);
|
||||
|
||||
CDialog::OnOK();
|
||||
@@ -289,6 +297,9 @@ void CMcpSettingsDlg::LayoutControls(int cx, int cy)
|
||||
m_btnFileTransfer.MoveWindow(margin, y, cx - margin * 2, 22);
|
||||
y += 30;
|
||||
|
||||
m_btnManage.MoveWindow(margin, y, cx - margin * 2, 22);
|
||||
y += 30;
|
||||
|
||||
const int whitelistH = 90;
|
||||
m_lblWhitelist.MoveWindow(margin, y, labelW, rowH);
|
||||
m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH);
|
||||
|
||||
@@ -31,6 +31,7 @@ private:
|
||||
IDC_MCP_TERMINAL = 1007, // 「启用持久终端」复选框(全命令,无白名单,要求只读关)
|
||||
IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关)
|
||||
IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关)
|
||||
IDC_MCP_MANAGE = 1010, // 「启用管理操作」复选框(kill/窗口/服务/电源,要求只读关)
|
||||
};
|
||||
|
||||
CButton m_btnEnable;
|
||||
@@ -40,6 +41,7 @@ private:
|
||||
CButton m_btnTerminal;
|
||||
CButton m_btnRemoteControl;
|
||||
CButton m_btnFileTransfer;
|
||||
CButton m_btnManage;
|
||||
CStatic m_lblWhitelist;
|
||||
CEdit m_editWhitelist;
|
||||
CButton m_btnOK, m_btnCancel;
|
||||
|
||||
@@ -573,6 +573,20 @@ public:
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
// 等待 OutCompressedBuffer 排空。OnClientPostSending 在最后一个 WSASend 完成后
|
||||
// ClearBuffer 使有效长度归零,此处轮询该信号,避免背靠背 Send2Client 对同一缓冲
|
||||
// 并发 WSASend(MCP OnSystemReady 发写命令后补发刷新命令时调用)。
|
||||
bool WaitSendIdle(int timeoutMs) override
|
||||
{
|
||||
ULONGLONG deadline = (timeoutMs > 0) ? (GetTickCount64() + (ULONGLONG)timeoutMs) : 0;
|
||||
while (OutCompressedBuffer.GetBufferLength() != 0) {
|
||||
if (timeoutMs > 0 && GetTickCount64() >= deadline) {
|
||||
return false;
|
||||
}
|
||||
Sleep(1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
VOID SetClientInfo(const CString(&s)[ONLINELIST_MAX], const std::vector<std::string>& a = {})
|
||||
{
|
||||
for (int i = 0; i < ONLINELIST_MAX; i++) {
|
||||
|
||||
@@ -55,6 +55,16 @@ public:
|
||||
}
|
||||
virtual void SetGroupName(const std::string& group) {}
|
||||
|
||||
// 等待本连接待发送缓冲排空(用于「发写命令 → 补发刷新命令」的背靠背发送:
|
||||
// 两次 Send2Client 会对同一发送缓冲并发 WSASend,造成竞态/丢包)。非 IOCP
|
||||
// 服务端无异步发送缓冲,默认立即返回 true;CONTEXT_OBJECT 覆写为轮询发送缓冲
|
||||
// 归零直到排空或超时。
|
||||
virtual bool WaitSendIdle(int timeoutMs)
|
||||
{
|
||||
(void)timeoutMs;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查客户端是否支持 V2 文件传输
|
||||
bool SupportsFileV2() const {
|
||||
CString caps = GetClientData(ONLINELIST_CAPABILITIES);
|
||||
|
||||
@@ -1989,18 +1989,20 @@ MCP
|
||||
端口需为 1-65535 的数字=Port muss eine Zahl zwischen 1 und 65535 sein
|
||||
Token 不能为空=Token darf nicht leer sein
|
||||
MCP 设置已保存。=MCP-Einstellungen gespeichert.
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。=Aenderungen an Aktivierung/Port/Bindeadresse/Token/Nur-Lesen/Whitelist/persistentem Terminal/Fernsteuerung/Dateiuebertragung werden nach Neustart wirksam.
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=Aenderungen an Aktivierung/Port/Bindeadresse/Token/Nur-Lesen/Whitelist/persistentem Terminal/Fernsteuerung/Dateiuebertragung/Verwaltungsoperationen werden nach Neustart wirksam.
|
||||
只读模式(禁命令执行)=Nur-Lesen-Modus (Befehlsausfuehrung deaktiviert)
|
||||
命令白名单=Befehls-Whitelist
|
||||
MCP命令执行=MCP-Befehlsausfuehrung
|
||||
启用持久终端(全命令,无白名单)=Persistentes Terminal aktivieren (volle Befehle, keine Whitelist)
|
||||
持久终端与远程控制仅在只读模式关闭时生效。=Persistentes Terminal und Fernsteuerung werden nur wirksam, wenn der Nur-Lesen-Modus aus ist.
|
||||
持久终端、远程控制与管理操作仅在只读模式关闭时生效。=Persistentes Terminal, Fernsteuerung und Verwaltungsoperationen werden nur wirksam, wenn der Nur-Lesen-Modus aus ist.
|
||||
MCP持久终端=MCP-persistentes Terminal
|
||||
MCP远程控制=MCP-Fernsteuerung
|
||||
MCP文件传输=MCP-Dateiuebertragung
|
||||
MCP管理操作=MCP-Verwaltungsoperation
|
||||
Token=Token
|
||||
启用远程控制(AI 操控桌面)=Fernsteuerung aktivieren (KI steuert den Desktop)
|
||||
启用文件传输(download_file)=Dateiuebertragung aktivieren (download_file)
|
||||
启用管理操作(进程/窗口/服务/电源)=Verwaltungsoperationen aktivieren (Prozess/Fenster/Dienst/Energie)
|
||||
第 %d 个钱包地址不合法!=Wallet-Adresse #%d ist ungueltig!
|
||||
%d 新连接\r\n=%d Neue Verbindung\r\n
|
||||
%d 本地连接断开\r\n=%d Lokale Verbindung geschlossen\r\n
|
||||
|
||||
@@ -1990,18 +1990,20 @@ MCP
|
||||
端口需为 1-65535 的数字=Port must be a number between 1 and 65535
|
||||
Token 不能为空=Token cannot be empty
|
||||
MCP 设置已保存。=MCP settings saved.
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer take effect after restart.
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer/management operations take effect after restart.
|
||||
只读模式(禁命令执行)=Read-only mode (disable command execution)
|
||||
命令白名单=Command whitelist
|
||||
MCP命令执行=MCP command execution
|
||||
启用持久终端(全命令,无白名单)=Enable persistent terminal (full commands, no whitelist)
|
||||
持久终端与远程控制仅在只读模式关闭时生效。=Persistent terminal and remote control take effect only when read-only mode is off.
|
||||
持久终端、远程控制与管理操作仅在只读模式关闭时生效。=Persistent terminal, remote control and management operations take effect only when read-only mode is off.
|
||||
MCP持久终端=MCP persistent terminal
|
||||
MCP远程控制=MCP remote control
|
||||
MCP文件传输=MCP file transfer
|
||||
MCP管理操作=MCP management operation
|
||||
Token=Token
|
||||
启用远程控制(AI 操控桌面)=Enable remote control (AI controls the desktop)
|
||||
启用文件传输(download_file)=Enable file transfer (download_file)
|
||||
启用管理操作(进程/窗口/服务/电源)=Enable management operations (process/window/service/power)
|
||||
第 %d 个钱包地址不合法!=Wallet address #%d is invalid!
|
||||
%d 新连接\r\n=%d New connection\r\n
|
||||
%d 本地连接断开\r\n=%d Local connection closed\r\n
|
||||
|
||||
@@ -1981,18 +1981,20 @@ MCP
|
||||
端口需为 1-65535 的数字=連接埠需為 1-65535 的數字
|
||||
Token 不能为空=Token 不能為空
|
||||
MCP 设置已保存。=MCP 設定已儲存。
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸的變更需重新啟動程式後生效。
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸/管理操作的變更需重新啟動程式後生效。
|
||||
只读模式(禁命令执行)=唯讀模式(禁命令執行)
|
||||
命令白名单=命令白名單
|
||||
MCP命令执行=MCP命令執行
|
||||
启用持久终端(全命令,无白名单)=啟用持久終端(完整命令,無白名單)
|
||||
持久终端与远程控制仅在只读模式关闭时生效。=持久終端與遠端控制僅在唯讀模式關閉時生效。
|
||||
持久终端、远程控制与管理操作仅在只读模式关闭时生效。=持久終端、遠端控制與管理操作僅在唯讀模式關閉時生效。
|
||||
MCP持久终端=MCP持久終端
|
||||
MCP远程控制=MCP遠端控制
|
||||
MCP文件传输=MCP檔案傳輸
|
||||
MCP管理操作=MCP管理操作
|
||||
Token=Token
|
||||
启用远程控制(AI 操控桌面)=啟用遠端控制(AI 操控桌面)
|
||||
启用文件传输(download_file)=啟用檔案傳輸(download_file)
|
||||
启用管理操作(进程/窗口/服务/电源)=啟用管理操作(程序/視窗/服務/電源)
|
||||
第 %d 个钱包地址不合法!=第 %d 個錢包位址不合法!
|
||||
%d 新连接\r\n=%d 新連線\r\n
|
||||
%d 本地连接断开\r\n=%d 本機連線斷開\r\n
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 108 KiB |
Reference in new issue
Block a user