Feature: Add MCP remote_open/remote_close remote control sessions
Add M1 of MCP remote control (docs/Mcp_RemoteControl_Design.md): the
remote_open / remote_close tools plus the ScreenCtrlSession state machine.
remote_open establishes a hidden screen sub-connection by reusing
WebService::StartRemoteDesktop (COMMAND_SCREEN_SPY -> CScreenSpyDlg ->
RegisterScreenContext), polls for the sub-connection plus its physical
resolution (TOKEN_BITMAPINFO -> NotifyResolutionChange -> GetScreenSize),
then records the session (single device, single session, reverse-mapped
subCtx for OfflineProc cleanup) and returns {session_id, screen_w,
screen_h}. remote_close validates session_id and tears down the
sub-connection idempotently. A McpRemoteControl settings checkbox (default
off, requires McpReadonly=0) gates the tools; every open/close is audited
via WM_SHOWERRORMSG.
Gating: multi-monitor hosts are rejected with -32008 (phase 1 supports only
single monitor, where Observe=main screen and Act=virtual desktop coincide);
the monitor count comes from the client heartbeat RES_RESOLUTION ("N:W*H").
Known limitations (deferred to the injection milestones): mutual exclusion
with human remote-desktop viewing is one-directional in M1 (a human who
joins during an MCP session can tear it down on disconnect), and subCtx is
not yet dereferenced so no liveness re-check is needed until
remote_mouse/remote_keyboard.
Co-Authored-By: deepseek-v4-pro
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
#include "HostJson.h" // BuildHostJson(单台主机序列化公共函数)
|
||||
#include "context.h" // context 接口
|
||||
#include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员(m_HostList/m_cs/m_ClientMap)+ VERSION_STR
|
||||
#include "WebService.h" // 远程控制复用屏幕子连接(StartRemoteDesktop / GetScreenContext / GetScreenSize)
|
||||
#include "Server.h" // CONTEXT_OBJECT 定义(GetScreenContext 返回 CONTEXT_OBJECT* → context* 上转型)
|
||||
#include "LangManager.h" // _TR(审计日志标题语言映射)
|
||||
|
||||
#include <sstream>
|
||||
@@ -1125,6 +1127,14 @@ std::string BuildTerminalOpen(const Json::Value& id, const Json::Value& args, CM
|
||||
std::string BuildTerminalExec(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
std::string BuildTerminalClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
|
||||
// ===== P5 前置声明(定义见下方「tools/call 分派」前)=====
|
||||
Json::Value BuildRemoteOpenInputSchema();
|
||||
Json::Value BuildRemoteOpenOutputSchema();
|
||||
Json::Value BuildRemoteCloseInputSchema();
|
||||
Json::Value BuildRemoteCloseOutputSchema();
|
||||
std::string BuildRemoteOpen(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
std::string BuildRemoteClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
|
||||
// tools/list
|
||||
std::string BuildToolsListResult(const Json::Value& id) {
|
||||
Json::Value result(Json::objectValue);
|
||||
@@ -1328,6 +1338,27 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
}
|
||||
}
|
||||
|
||||
// 15) remote_open / remote_close(P5:MCP 远程控制,仅 Windows,
|
||||
// 安全门:McpRemoteControl=1 且 McpReadonly=0;全程审计 + idle 回收)
|
||||
if (CMcpServer::Instance().IsRemoteControlEnabled() && !CMcpServer::Instance().IsReadonly()) {
|
||||
{
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "remote_open";
|
||||
tool["description"] = u8"在指定在线 Windows 主机上建立远程控制会话(隐藏屏幕子连接)并返回 session_id 与物理屏幕分辨率(screen_w/screen_h)。后续 remote_mouse/remote_keyboard 用归一化坐标(0..1)注入输入,用毕须 remote_close。";
|
||||
tool["inputSchema"] = BuildRemoteOpenInputSchema();
|
||||
tool["outputSchema"] = BuildRemoteOpenOutputSchema();
|
||||
tools.append(tool);
|
||||
}
|
||||
{
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "remote_close";
|
||||
tool["description"] = u8"关闭并释放指定远程控制会话(幂等:会话已不存在也返回成功)。";
|
||||
tool["inputSchema"] = BuildRemoteCloseInputSchema();
|
||||
tool["outputSchema"] = BuildRemoteCloseOutputSchema();
|
||||
tools.append(tool);
|
||||
}
|
||||
}
|
||||
|
||||
result["tools"] = tools;
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
@@ -2533,6 +2564,287 @@ std::string BuildTerminalClose(const Json::Value& id, const Json::Value& args, C
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// ===== P5:MCP 远程控制(remote_open / remote_close)=====
|
||||
|
||||
static const int kScreenCtrlIdleTimeoutSec = 300; // 远程控制 idle 回收超时(秒)
|
||||
|
||||
// 共享前置:校验 McpRemoteControl && !readonly → 解析 id → FindMainContext → 拒绝 LNX/MAC。
|
||||
// 成功返回 true 并输出 devId/ctx;失败时 errJson 已写入对应 JSON 错误串。
|
||||
static bool ResolveScreenCtrlSessionHost(const Json::Value& id, const Json::Value& args,
|
||||
CMy2015RemoteDlg* parent,
|
||||
uint64_t& devId, context*& ctx, std::string& errJson) {
|
||||
CMcpServer& mcp = CMcpServer::Instance();
|
||||
if (!mcp.IsRemoteControlEnabled() || mcp.IsReadonly()) {
|
||||
errJson = BuildError(id, -32006,
|
||||
"Remote control is disabled: requires McpRemoteControl=1 and McpReadonly=0");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string err;
|
||||
if (!ParseHostIdArg(args, devId, err)) {
|
||||
errJson = BuildError(id, -32602, err);
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx = FindMainContext(parent, devId);
|
||||
if (!ctx) {
|
||||
errJson = BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||||
return false;
|
||||
}
|
||||
|
||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
if (clientType == "LNX" || clientType == "MAC") {
|
||||
errJson = BuildError(id, -32005, "Remote control is only supported on Windows hosts");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// remote_open 的 inputSchema(id 必填、timeout_ms 可选)
|
||||
Json::Value BuildRemoteOpenInputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
Json::Value idProp(Json::objectValue);
|
||||
idProp["type"] = "string";
|
||||
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
|
||||
props["id"] = idProp;
|
||||
Json::Value timeoutProp(Json::objectValue);
|
||||
timeoutProp["type"] = "integer";
|
||||
timeoutProp["description"] = u8"等待屏幕子连接建立 + 分辨率到达的超时毫秒数(可选,默认 20000,上限 600000)";
|
||||
props["timeout_ms"] = timeoutProp;
|
||||
Json::Value schema(Json::objectValue);
|
||||
schema["type"] = "object";
|
||||
schema["properties"] = props;
|
||||
Json::Value required(Json::arrayValue);
|
||||
required.append("id");
|
||||
schema["required"] = required;
|
||||
return schema;
|
||||
}
|
||||
|
||||
Json::Value BuildRemoteOpenOutputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
Json::Value sid(Json::objectValue);
|
||||
sid["type"] = "string";
|
||||
sid["description"] = u8"控制会话 token,后续 remote_mouse / remote_keyboard / remote_close 用";
|
||||
props["session_id"] = sid;
|
||||
Json::Value sw(Json::objectValue);
|
||||
sw["type"] = "integer";
|
||||
sw["description"] = u8"物理屏幕宽度(虚拟桌面像素),用于把归一化坐标 0..1 映射为像素";
|
||||
props["screen_w"] = sw;
|
||||
Json::Value sh(Json::objectValue);
|
||||
sh["type"] = "integer";
|
||||
sh["description"] = u8"物理屏幕高度(虚拟桌面像素)";
|
||||
props["screen_h"] = sh;
|
||||
Json::Value schema(Json::objectValue);
|
||||
schema["type"] = "object";
|
||||
schema["properties"] = props;
|
||||
Json::Value required(Json::arrayValue);
|
||||
required.append("session_id");
|
||||
required.append("screen_w");
|
||||
required.append("screen_h");
|
||||
schema["required"] = required;
|
||||
return schema;
|
||||
}
|
||||
|
||||
Json::Value BuildRemoteCloseInputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
Json::Value idProp(Json::objectValue);
|
||||
idProp["type"] = "string";
|
||||
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
|
||||
props["id"] = idProp;
|
||||
Json::Value sid(Json::objectValue);
|
||||
sid["type"] = "string";
|
||||
sid["description"] = u8"remote_open 返回的 session_id";
|
||||
props["session_id"] = sid;
|
||||
Json::Value schema(Json::objectValue);
|
||||
schema["type"] = "object";
|
||||
schema["properties"] = props;
|
||||
Json::Value required(Json::arrayValue);
|
||||
required.append("id");
|
||||
required.append("session_id");
|
||||
schema["required"] = required;
|
||||
return schema;
|
||||
}
|
||||
|
||||
Json::Value BuildRemoteCloseOutputSchema() {
|
||||
Json::Value props(Json::objectValue);
|
||||
Json::Value closed(Json::objectValue);
|
||||
closed["type"] = "boolean";
|
||||
closed["description"] = u8"恒为 true(幂等)";
|
||||
props["closed"] = closed;
|
||||
Json::Value schema(Json::objectValue);
|
||||
schema["type"] = "object";
|
||||
schema["properties"] = props;
|
||||
Json::Value required(Json::arrayValue);
|
||||
required.append("closed");
|
||||
schema["required"] = required;
|
||||
return schema;
|
||||
}
|
||||
|
||||
// tools/call:remote_open(建立隐藏屏幕子连接 + 取物理分辨率)
|
||||
std::string BuildRemoteOpen(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||||
CMcpServer& mcp = CMcpServer::Instance();
|
||||
mcp.SweepIdleScreenCtrl(kScreenCtrlIdleTimeoutSec);
|
||||
|
||||
uint64_t devId = 0;
|
||||
context* ctx = nullptr;
|
||||
std::string errJson;
|
||||
if (!ResolveScreenCtrlSessionHost(id, args, parent, devId, ctx, errJson))
|
||||
return errJson;
|
||||
|
||||
// 多显示器门(§7.2 / §12.1):Observe 抓主屏,Act 注入在虚拟桌面空间,两者仅单显示器时
|
||||
// 重合。客户端心跳 RES_RESOLUTION = "N:W*H"(N=显示器数);N>1 时归一化坐标会系统性
|
||||
// 偏移,阶段一直接拒绝(-32008)。无冒号(老客户端格式)则无法判定,放行不拦。
|
||||
{
|
||||
CString res = ctx->GetAdditionalData(RES_RESOLUTION);
|
||||
int colon = res.Find(':');
|
||||
if (colon > 0) {
|
||||
CString countStr = res.Left(colon);
|
||||
int monitorCount = atoi(countStr);
|
||||
if (monitorCount > 1)
|
||||
return BuildError(id, -32008,
|
||||
"Multi-monitor hosts are not supported yet (phase 1): monitor count " +
|
||||
std::to_string(monitorCount));
|
||||
}
|
||||
}
|
||||
|
||||
// 远程控制复用 Web 的隐藏屏幕子连接(COMMAND_SCREEN_SPY → CScreenSpyDlg → RegisterScreenContext)。
|
||||
// 该路径依赖 Web 远程服务已启动(WebSvrPort>0,默认开启);未启动时子连接无法建立/注册。
|
||||
if (!WebService().IsRunning())
|
||||
return BuildError(id, -32005,
|
||||
"Remote control requires the Web remote service (WebSvrPort) to be enabled");
|
||||
|
||||
// 与人类远程桌面观看互斥:已有屏幕子连接(web 观看)→ 拒绝,避免向人类正在看的画面注入。
|
||||
if (WebService().HasActiveSession(devId))
|
||||
return BuildError(id, -32003, "Device busy: a remote desktop session is already active for this host");
|
||||
|
||||
int timeoutMs = kMcpToolTimeoutMs;
|
||||
std::string t = GetStringArg(args, "timeout_ms");
|
||||
if (!t.empty() && IsDigits(t)) {
|
||||
int v = atoi(t.c_str());
|
||||
if (v > 0 && v <= 600000) timeoutMs = v;
|
||||
}
|
||||
|
||||
std::string sessionId = GenerateRandomToken();
|
||||
if (!mcp.BeginScreenCtrlOpen(devId, sessionId))
|
||||
return BuildError(id, -32003, "Device busy: a remote control session is already active for this host");
|
||||
|
||||
// 建立隐藏屏幕子连接(发 COMMAND_SCREEN_SPY、标记 web-triggered)。
|
||||
if (!WebService().StartRemoteDesktop(devId)) {
|
||||
mcp.CloseScreenCtrlSession(devId, sessionId);
|
||||
// StartRemoteDesktop 先置 web-triggered 再 Send2Client,发送失败需一并清理,
|
||||
// 否则该设备残留 web-triggered 标记、后续人类 web 观看会误走隐藏会话路径。
|
||||
WebService().ClearWebTriggered(devId);
|
||||
return BuildError(id, -32004, "Failed to start remote desktop session");
|
||||
}
|
||||
|
||||
// 轮询等待子连接就绪(HasActiveSession)+ 分辨率到达(TOKEN_BITMAPINFO → NotifyResolutionChange)。
|
||||
// 屏幕子连接的注册/分辨率由 CScreenSpyDlg 完成,无 McpServer 回调,故用轮询(open 低频)。
|
||||
int screenW = 0, screenH = 0;
|
||||
context* subCtx = nullptr;
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
|
||||
bool ready = false;
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
if (WebService().HasActiveSession(devId) && WebService().GetScreenSize(devId, screenW, screenH)) {
|
||||
subCtx = WebService().GetScreenContext(devId);
|
||||
ready = (subCtx != nullptr);
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
}
|
||||
|
||||
if (!ready) {
|
||||
mcp.CloseScreenCtrlSession(devId, sessionId);
|
||||
if (parent) parent->CloseWebRemoteDesktopByClientID(devId);
|
||||
WebService().ClearWebTriggered(devId);
|
||||
return BuildError(id, -32001, "Timeout waiting for screen sub-connection / resolution");
|
||||
}
|
||||
|
||||
if (!mcp.MarkScreenCtrlReady(devId, sessionId, subCtx, screenW, screenH)) {
|
||||
// 并发 remote_close 已关掉会话:放弃本次 open(子连接已由 close 关闭)。
|
||||
return BuildError(id, -32002, "Remote control session was closed concurrently: " + sessionId);
|
||||
}
|
||||
|
||||
// 审计:打开会话(不可关闭)。
|
||||
if (parent) {
|
||||
std::string text = "host " + std::to_string(devId) + " remote-open: session " + sessionId
|
||||
+ " (" + std::to_string(screenW) + "x" + std::to_string(screenH) + ")";
|
||||
parent->PostMessageA(WM_SHOWERRORMSG,
|
||||
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
|
||||
(LPARAM)new CString(_TR("MCP远程控制")));
|
||||
}
|
||||
|
||||
Json::Value result(Json::objectValue);
|
||||
Json::Value structuredContent(Json::objectValue);
|
||||
structuredContent["session_id"] = sessionId;
|
||||
structuredContent["screen_w"] = screenW;
|
||||
structuredContent["screen_h"] = screenH;
|
||||
result["structuredContent"] = structuredContent;
|
||||
|
||||
Json::Value content(Json::arrayValue);
|
||||
Json::Value item(Json::objectValue);
|
||||
item["type"] = "text";
|
||||
item["text"] = sessionId;
|
||||
content.append(item);
|
||||
result["content"] = content;
|
||||
result["isError"] = false;
|
||||
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// tools/call:remote_close(关闭控制会话;幂等,session_id 不匹配报错)
|
||||
std::string BuildRemoteClose(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||||
CMcpServer& mcp = CMcpServer::Instance();
|
||||
mcp.SweepIdleScreenCtrl(kScreenCtrlIdleTimeoutSec);
|
||||
|
||||
if (!mcp.IsRemoteControlEnabled() || mcp.IsReadonly())
|
||||
return BuildError(id, -32006, "Remote control is disabled: requires McpRemoteControl=1 and McpReadonly=0");
|
||||
|
||||
uint64_t devId = 0;
|
||||
std::string err;
|
||||
if (!ParseHostIdArg(args, devId, err))
|
||||
return BuildError(id, -32602, err);
|
||||
|
||||
std::string sessionId = GetStringArg(args, "session_id");
|
||||
if (sessionId.empty())
|
||||
return BuildError(id, -32602, "Missing required parameter: session_id");
|
||||
|
||||
int r = mcp.CloseScreenCtrlSession(devId, sessionId);
|
||||
if (r == 2)
|
||||
return BuildError(id, -32002, "Remote control session_id mismatch: " + sessionId);
|
||||
|
||||
// 仅当确实关闭了 MCP 会话(r==0)时才关闭隐藏屏幕子连接;r==1(幂等,会话已不存在,
|
||||
// 如已被 idle 回收)时不得误关人类正在观看的 web 会话(CloseWebRemoteDesktopByClientID
|
||||
// 会关掉该设备任一 IsWebSession 的对话框)。
|
||||
if (r == 0) {
|
||||
// 关闭隐藏屏幕子连接(幂等):WM_CLOSE → CScreenSpyDlg 析构 → UnregisterScreenContext。
|
||||
if (parent) parent->CloseWebRemoteDesktopByClientID(devId);
|
||||
WebService().ClearWebTriggered(devId);
|
||||
}
|
||||
|
||||
// 审计:关闭会话(不可关闭)。
|
||||
if (parent) {
|
||||
std::string text = "host " + std::to_string(devId) + " remote-close: session " + sessionId;
|
||||
parent->PostMessageA(WM_SHOWERRORMSG,
|
||||
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
|
||||
(LPARAM)new CString(_TR("MCP远程控制")));
|
||||
}
|
||||
|
||||
Json::Value result(Json::objectValue);
|
||||
Json::Value structuredContent(Json::objectValue);
|
||||
structuredContent["closed"] = true;
|
||||
result["structuredContent"] = structuredContent;
|
||||
|
||||
Json::Value content(Json::arrayValue);
|
||||
Json::Value item(Json::objectValue);
|
||||
item["type"] = "text";
|
||||
item["text"] = std::string(u8"closed");
|
||||
content.append(item);
|
||||
result["content"] = content;
|
||||
result["isError"] = false;
|
||||
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
|
||||
// tools/call 分派
|
||||
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||
const Json::Value& id = root["id"];
|
||||
@@ -2561,6 +2873,8 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||
if (toolName == "terminal_open") return BuildTerminalOpen(id, args, parent);
|
||||
if (toolName == "terminal_exec") return BuildTerminalExec(id, args, parent);
|
||||
if (toolName == "terminal_close") return BuildTerminalClose(id, args, parent);
|
||||
if (toolName == "remote_open") return BuildRemoteOpen(id, args, parent);
|
||||
if (toolName == "remote_close") return BuildRemoteClose(id, args, parent);
|
||||
|
||||
return BuildError(id, -32602,
|
||||
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
|
||||
@@ -2842,6 +3156,80 @@ int CMcpServer::SweepIdleTerminals(time_t idleTimeoutSec) {
|
||||
return (int)toCancel.size();
|
||||
}
|
||||
|
||||
// ===== P5:MCP 远程控制(状态机方法)=====
|
||||
|
||||
bool CMcpServer::IsScreenCtrlContext(context* subCtx) {
|
||||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||||
return m_ScreenCtrlContextToDevice.find(subCtx) != m_ScreenCtrlContextToDevice.end();
|
||||
}
|
||||
|
||||
bool CMcpServer::BeginScreenCtrlOpen(uint64_t device_id, const std::string& sessionId) {
|
||||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||||
if (m_ScreenCtrlSessions.find(device_id) != m_ScreenCtrlSessions.end()) return false; // 单设备单会话
|
||||
ScreenCtrlSession s;
|
||||
s.sessionId = sessionId;
|
||||
s.lastActiveAt = time(nullptr);
|
||||
m_ScreenCtrlSessions[device_id] = std::move(s);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CMcpServer::MarkScreenCtrlReady(uint64_t device_id, const std::string& sessionId,
|
||||
context* subCtx, int screenW, int screenH) {
|
||||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||||
auto it = m_ScreenCtrlSessions.find(device_id);
|
||||
if (it == m_ScreenCtrlSessions.end() || it->second.sessionId != sessionId) return false;
|
||||
ScreenCtrlSession& s = it->second;
|
||||
s.subCtx = subCtx;
|
||||
s.started = true;
|
||||
s.screenW = screenW;
|
||||
s.screenH = screenH;
|
||||
s.lastActiveAt = time(nullptr);
|
||||
m_ScreenCtrlContextToDevice[subCtx] = device_id;
|
||||
return true;
|
||||
}
|
||||
|
||||
int CMcpServer::CloseScreenCtrlSession(uint64_t device_id, const std::string& sessionId) {
|
||||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||||
auto it = m_ScreenCtrlSessions.find(device_id);
|
||||
if (it == m_ScreenCtrlSessions.end()) return 1; // 不存在(幂等)
|
||||
if (it->second.sessionId != sessionId) return 2; // token 不匹配
|
||||
if (it->second.subCtx) m_ScreenCtrlContextToDevice.erase(it->second.subCtx);
|
||||
m_ScreenCtrlSessions.erase(it);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int CMcpServer::SweepIdleScreenCtrl(time_t idleTimeoutSec) {
|
||||
time_t now = time(nullptr);
|
||||
std::vector<uint64_t> toClose;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||||
for (auto it = m_ScreenCtrlSessions.begin(); it != m_ScreenCtrlSessions.end(); ) {
|
||||
ScreenCtrlSession& s = it->second;
|
||||
if (difftime(now, s.lastActiveAt) > (double)idleTimeoutSec) {
|
||||
if (s.subCtx) m_ScreenCtrlContextToDevice.erase(s.subCtx);
|
||||
toClose.push_back(it->first);
|
||||
it = m_ScreenCtrlSessions.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 锁外关闭隐藏对话框(WM_CLOSE → CScreenSpyDlg 析构 → UnregisterScreenContext)。
|
||||
for (uint64_t devId : toClose) {
|
||||
if (m_parent) m_parent->CloseWebRemoteDesktopByClientID(devId);
|
||||
WebService().ClearWebTriggered(devId);
|
||||
}
|
||||
return (int)toClose.size();
|
||||
}
|
||||
|
||||
void CMcpServer::OnScreenControlClosed(context* subCtx) {
|
||||
std::lock_guard<std::mutex> lk(m_ScreenCtrlMutex);
|
||||
auto it = m_ScreenCtrlContextToDevice.find(subCtx);
|
||||
if (it == m_ScreenCtrlContextToDevice.end()) return;
|
||||
m_ScreenCtrlSessions.erase(it->second);
|
||||
m_ScreenCtrlContextToDevice.erase(it);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// CMcpServer Implementation
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Reference in New Issue
Block a user