Compare commits
10
Commits
v1.3.9
..
9eed43bc8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eed43bc8d | ||
|
|
a72e6eb391 | ||
|
|
e9d60e6e23 | ||
|
|
42df128f62 | ||
|
|
32cc7af371 | ||
|
|
033ff077ef | ||
|
|
0416dadbba | ||
|
|
91238da50d | ||
|
|
221617fe02 | ||
|
|
dd845823b6 |
No files matched your search
@@ -66,13 +66,15 @@ BOOL IsClientAppRunning(void* thisApp)
|
|||||||
return S_CLIENT_NORMAL == This->g_bExit;
|
return S_CLIENT_NORMAL == This->g_bExit;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared)
|
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared, int headerEnc)
|
||||||
{
|
{
|
||||||
auto v = StringToVector(remoteAddr, ':', 2);
|
auto v = StringToVector(remoteAddr, ':', 2);
|
||||||
if (v[0].empty() || v[1].empty())
|
if (v[0].empty() || v[1].empty())
|
||||||
return nullptr;
|
return nullptr;
|
||||||
auto a = new ClientApp(g_MyApp.g_Connection, run, shared);
|
auto a = new ClientApp(g_MyApp.g_Connection, run, shared);
|
||||||
a->g_Connection->SetServer(v[0].c_str(), atoi(v[1].c_str()));
|
a->g_Connection->SetServer(v[0].c_str(), atoi(v[1].c_str()));
|
||||||
|
if (headerEnc == PROTOCOL_SHINE || headerEnc == PROTOCOL_HELL)
|
||||||
|
a->g_Connection->iHeaderEnc = headerEnc;
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,6 +599,9 @@ DWORD WINAPI StartClient(LPVOID lParam)
|
|||||||
auto saved_ip = cfg.GetStr("settings", "master");
|
auto saved_ip = cfg.GetStr("settings", "master");
|
||||||
auto saved_port = cfg.GetInt("settings", "port");
|
auto saved_port = cfg.GetInt("settings", "port");
|
||||||
settings.SetServer(saved_ip.c_str(), saved_port);
|
settings.SetServer(saved_ip.c_str(), saved_port);
|
||||||
|
auto saved_proto = cfg.GetStr("settings", "master_proto");
|
||||||
|
if (saved_proto == "0" || saved_proto == "1")
|
||||||
|
settings.iHeaderEnc = atoi(saved_proto.c_str());
|
||||||
Mprintf("[StartClient] Client is assigned to %s:%d- %ds left.\n", saved_ip.c_str(), saved_port,
|
Mprintf("[StartClient] Client is assigned to %s:%d- %ds left.\n", saved_ip.c_str(), saved_port,
|
||||||
int(valid_to-now));
|
int(valid_to-now));
|
||||||
}
|
}
|
||||||
@@ -617,7 +622,9 @@ DWORD WINAPI StartClient(LPVOID lParam)
|
|||||||
auto a = cfg.GetStr("settings", "share_list");
|
auto a = cfg.GetStr("settings", "share_list");
|
||||||
auto shareList = a.empty() ? std::vector<std::string>{} : StringToVector(a, '|');
|
auto shareList = a.empty() ? std::vector<std::string>{} : StringToVector(a, '|');
|
||||||
for (int i = 0; i < shareList.size(); ++i) {
|
for (int i = 0; i < shareList.size(); ++i) {
|
||||||
auto a = NewClientStartArg(shareList[i].c_str(), IsSharedRunning, TRUE);
|
auto v = StringToVector(shareList[i], ':', 3);
|
||||||
|
int proto = (v.size() >= 3 && (v[2] == "0" || v[2] == "1")) ? atoi(v[2].c_str()) : -1;
|
||||||
|
auto a = NewClientStartArg(shareList[i].c_str(), IsSharedRunning, TRUE, proto);
|
||||||
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
|
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
|
||||||
Mprintf("[StartClient] Client is shared to %s.\n", shareList[i].c_str());
|
Mprintf("[StartClient] Client is shared to %s.\n", shareList[i].c_str());
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -130,7 +130,7 @@ public:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run = IsClientAppRunning, BOOL shared=FALSE);
|
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run = IsClientAppRunning, BOOL shared=FALSE, int headerEnc = -1);
|
||||||
|
|
||||||
// 启动核心线程,参数为:ClientApp
|
// 启动核心线程,参数为:ClientApp
|
||||||
DWORD WINAPI StartClient(LPVOID lParam);
|
DWORD WINAPI StartClient(LPVOID lParam);
|
||||||
|
|||||||
@@ -1019,6 +1019,15 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
case COMMAND_SHARE:
|
case COMMAND_SHARE:
|
||||||
case COMMAND_ASSIGN_MASTER:
|
case COMMAND_ASSIGN_MASTER:
|
||||||
if (ulLength > 2) {
|
if (ulLength > 2) {
|
||||||
|
// 解析末尾协议段(Shine=0 / HELL=1,旧命令无此段则 -1 沿用烘焙值)
|
||||||
|
int shareProto = -1;
|
||||||
|
{
|
||||||
|
auto pv = StringToVector((char*)szBuffer + 2, ':', 5);
|
||||||
|
if (szBuffer[1] == SHARE_TYPE_YAMA && pv.size() >= 3 && (pv[2] == "0" || pv[2] == "1"))
|
||||||
|
shareProto = atoi(pv[2].c_str());
|
||||||
|
else if (szBuffer[1] == SHARE_TYPE_YAMA_FOREVER && pv.size() >= 4 && (pv[3] == "0" || pv[3] == "1"))
|
||||||
|
shareProto = atoi(pv[3].c_str());
|
||||||
|
}
|
||||||
switch (szBuffer[1]) {
|
switch (szBuffer[1]) {
|
||||||
case SHARE_TYPE_YAMA_FOREVER: {
|
case SHARE_TYPE_YAMA_FOREVER: {
|
||||||
auto v = StringToVector((char*)szBuffer + 2, ':', 3);
|
auto v = StringToVector((char*)szBuffer + 2, ':', 3);
|
||||||
@@ -1029,6 +1038,8 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
if (now <= valid_to) break; // Avoid assign again
|
if (now <= valid_to) break; // Avoid assign again
|
||||||
m_cfg->SetStr("settings", "master", v[0]);
|
m_cfg->SetStr("settings", "master", v[0]);
|
||||||
m_cfg->SetStr("settings", "port", v[1]);
|
m_cfg->SetStr("settings", "port", v[1]);
|
||||||
|
if (shareProto == PROTOCOL_SHINE || shareProto == PROTOCOL_HELL)
|
||||||
|
m_cfg->SetStr("settings", "master_proto", std::to_string(shareProto));
|
||||||
float days = atof(v[2].c_str());
|
float days = atof(v[2].c_str());
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
auto valid_to = time(0) + days*86400;
|
auto valid_to = time(0) + days*86400;
|
||||||
@@ -1047,6 +1058,8 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
if (v[0].empty() || v[1].empty())
|
if (v[0].empty() || v[1].empty())
|
||||||
break;
|
break;
|
||||||
auto share = v[0] + ":" + v[1];
|
auto share = v[0] + ":" + v[1];
|
||||||
|
if (shareProto == PROTOCOL_SHINE || shareProto == PROTOCOL_HELL)
|
||||||
|
share += ":" + std::to_string(shareProto);
|
||||||
auto list = m_cfg->GetStr("settings", "share_list");
|
auto list = m_cfg->GetStr("settings", "share_list");
|
||||||
auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|');
|
auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|');
|
||||||
if (VectorContains(shareList, share)) break;
|
if (VectorContains(shareList, share)) break;
|
||||||
@@ -1054,7 +1067,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
|||||||
m_cfg->SetStr("settings", "share_list", VectorJoin(shareList, '|'));
|
m_cfg->SetStr("settings", "share_list", VectorJoin(shareList, '|'));
|
||||||
Mprintf("Share client to new master: %s\n", share.c_str());
|
Mprintf("Share client to new master: %s\n", share.c_str());
|
||||||
}
|
}
|
||||||
auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE);
|
auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE, shareProto);
|
||||||
if (nullptr!=a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
|
if (nullptr!=a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
# MCP P7 设计:设备管理控制(kill / window / service / power)
|
||||||
|
|
||||||
|
> 延续 `Mcp_Design.md` → `Mcp_Phase2_Design.md`(P2)→ P3/P4/P5/P6 的阶段演进。
|
||||||
|
> 本文定义 P7:把 YAMA 服务端高频「控制类」操作 MCP 化,补上从「观测」到「管控」的短板,
|
||||||
|
> 服务「自然语言即可操作服务端、对受管设备全方位管控」的最终目标。
|
||||||
|
>
|
||||||
|
> 安全门结论(已确认):新增**单一 `McpManage` 开关**统一门控本阶段全部控制类工具,
|
||||||
|
> 并要求 `McpReadonly=0`;不再为每个工具单设开关。
|
||||||
|
|
||||||
|
## 1. 目标与范围
|
||||||
|
|
||||||
|
P7 交付 4 个控制类工具,覆盖服务端日常最常用的写操作:
|
||||||
|
|
||||||
|
| 工具 | 动作 | 命令 | 连接 | 确认方式 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `kill_process` | 结束进程(多 pid) | `COMMAND_KILLPROCESS` | 系统子连接 | 补发 `COMMAND_PSLIST` 收新列表 |
|
||||||
|
| `operate_window` | 关闭/显示/隐藏/最小化/最大化/还原 | `CMD_WINDOW_CLOSE` / `CMD_WINDOW_TEST` | 窗口子连接 | close 自动回 `TOKEN_WSLIST`;test 补发 `COMMAND_WSLIST` |
|
||||||
|
| `control_service` | 启动/停止/设为自动/设为手动 | `COMMAND_SERVICECONFIG` | 服务子连接 | 客户端每项 Sleep(500) 后自动 `SendServicesList` |
|
||||||
|
| `power_action` | 注销/关机/重启 | `TOKEN_MACHINE_MANAGE` | 主连接 | 发完即走(无响应) |
|
||||||
|
|
||||||
|
仅 Windows 客户端支持(`clientType` 为 LNX/MAC/**APK** 提前返回 `-32005`;注意 `list_services`
|
||||||
|
只排除了 LNX/MAC、漏掉 APK,P7 需把 APK 一并排除)。
|
||||||
|
|
||||||
|
## 2. 复用现状
|
||||||
|
|
||||||
|
P7 **不改客户端**。客户端已全部实现下列处理(MFC 界面在跑),MCP 侧只需补齐
|
||||||
|
「发命令 → 接管子连接 → 发写命令 → 收确认 → 关闭」这条链:
|
||||||
|
|
||||||
|
- 系统子连接:`COMMAND_SYSTEM`(进程)/ `COMMAND_WSLIST`(窗口)→ 客户端开 `CSystemManager`
|
||||||
|
子连接(`client/KernelManager.cpp` 1169/1182,`client/SystemManager.cpp`)。
|
||||||
|
- `COMMAND_KILLPROCESS`:`CSystemManager::KillProcess`(`TerminateProcess`,无回包)。
|
||||||
|
- `CMD_WINDOW_CLOSE`:`PostMessage(hWnd, WM_CLOSE)` 后自动 `SendWindowsList`。
|
||||||
|
- `CMD_WINDOW_TEST`:`ShowWindow(hwnd, dHow)`(不自动回包)。
|
||||||
|
- `COMMAND_SERVICECONFIG`:`CServicesManager::ServicesConfig`(`client/ServicesManager.cpp` 213),
|
||||||
|
bCmd=1 启动 / 2 停止 / 3 自动 / 4 手动,每项后 `Sleep(500)` + `SendServicesList`。
|
||||||
|
- `TOKEN_MACHINE_MANAGE`:`KernelManager.cpp` 872,`ExitWindowsEx`(注销/关机/重启)。
|
||||||
|
|
||||||
|
复用现有 MCP 基建(`server/2015Remote/McpServer.{h,cpp}`):
|
||||||
|
|
||||||
|
- `ParseHostIdArg` / `FindMainContext`:入参解析 + 主连接定位。
|
||||||
|
- `BeginPending` / `WaitPending` / `ClearPending` / `TakeMainResponse`:单飞行挂起 + 响应回收。
|
||||||
|
- `OnRegeditReady`:已示范「接管子连接 → 发后续命令」的范式(P3 list_registry 三阶段),P7 直接泛化。
|
||||||
|
|
||||||
|
## 3. 子连接生命周期(P7 核心新增)
|
||||||
|
|
||||||
|
写工具与只读工具的关键差异:只读工具「子连接回一包即完成」,写工具需要
|
||||||
|
「**接管子连接 → 发写命令 → 收确认 → 关闭**」。在现有 `PendingRequest` 上增加
|
||||||
|
`followUp`(接管后要发的写命令 payload,二进制)+ `followUpSent`(是否已发写命令)两个字段:
|
||||||
|
|
||||||
|
```
|
||||||
|
followUpSent == false → 等子连接就绪(首包 TOKEN_*LIST 到达 = 就绪)
|
||||||
|
followUpSent == true → 已发写命令,等确认包
|
||||||
|
```
|
||||||
|
|
||||||
|
> 关键事实(已核实):三个工具各自的「就绪 token」与「确认 token」**是同一个 token**——
|
||||||
|
> 客户端子连接构造即回一包(就绪),写操作后再回一包(确认)。因此必须用 `followUpSent`
|
||||||
|
> 区分这两次到达,而不能靠 token 区分。这也是它与 `list_registry`(PATH/KEY 两个不同 token)
|
||||||
|
> 的差别。
|
||||||
|
|
||||||
|
流程(以 `kill_process` 为例):
|
||||||
|
|
||||||
|
1. 工具线程 `BeginPending(devId, "kill_process")` 登记单飞行(false = 设备忙),并预填
|
||||||
|
`followUp`(`COMMAND_KILLPROCESS` 多 pid 包)。
|
||||||
|
2. 主连接发 `COMMAND_SYSTEM`。
|
||||||
|
3. 客户端开系统子连接,首包 `TOKEN_PSLIST` 到达 → `MessageHandle` 调新增的
|
||||||
|
`OnSystemReady(devId, subCtx)`(类比 `OnRegeditReady`):
|
||||||
|
- 命中挂起且 `followUpSent==false` → 保存 `subCtx`、置 `followUpSent=true`、向子连接发
|
||||||
|
`followUp`,随后补发 `COMMAND_PSLIST` 索取新列表。
|
||||||
|
4. 客户端杀进程后回第二个 `TOKEN_PSLIST` → `TakeMainResponse` 取走(`followUpSent==true`
|
||||||
|
命中)→ 关闭子连接(`CancelIO`)→ 唤醒工具线程。
|
||||||
|
5. 工具线程解析新进程列表返回;超时/失败走 `ClearPending`(**含 `subCtx->CancelIO()`**,防孤儿子连接)。
|
||||||
|
|
||||||
|
> 注意:`followUp`(写命令)与「补发的 `COMMAND_PSLIST`/`COMMAND_WSLIST`」必须是**两次独立的
|
||||||
|
> `Send2Client` 调用**,不能拼成一个 buffer——客户端 `OnReceive` 按 `szBuffer[0]` 单命令分发,
|
||||||
|
> 拼包会把后续字节当成 pid/hwnd 解析。
|
||||||
|
|
||||||
|
接管函数(进程/窗口共用同一个 `OnSystemReady`——两者都在同一个 `CSystemManager` 子连接上,
|
||||||
|
客户端 `OnReceive` 统一处理 `COMMAND_PSLIST`/`COMMAND_KILLPROCESS`/`COMMAND_WSLIST`/
|
||||||
|
`CMD_WINDOW_CLOSE`/`CMD_WINDOW_TEST`):
|
||||||
|
|
||||||
|
| 工具 | 开子连接命令 | 就绪包(首包) | 接管后写命令 | 确认包(第二包) |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `kill_process` | `COMMAND_SYSTEM` | `TOKEN_PSLIST` | `COMMAND_KILLPROCESS` + 补 `COMMAND_PSLIST` | `TOKEN_PSLIST` |
|
||||||
|
| `operate_window` | `COMMAND_WSLIST` | `TOKEN_WSLIST` | `CMD_WINDOW_CLOSE`(close 自动回);`CMD_WINDOW_TEST`(test 补 `COMMAND_WSLIST`) | `TOKEN_WSLIST` |
|
||||||
|
| `control_service` | `COMMAND_SERVICES` | `TOKEN_SERVERLIST` | `COMMAND_SERVICECONFIG`(客户端写后自动 `SendServicesList`) | `TOKEN_SERVERLIST` |
|
||||||
|
|
||||||
|
`MessageHandle` 现有分派点(均已核实):
|
||||||
|
|
||||||
|
- `TOKEN_PSLIST`/`TOKEN_WSLIST` 分支:`2015RemoteDlg.cpp:6540`(当前 `TakeMainResponse`+`CancelIO`)
|
||||||
|
- `TOKEN_SERVERLIST` 分支:`2015RemoteDlg.cpp:6585`(同上)
|
||||||
|
|
||||||
|
接入方式:在这两个分支的 `IsPending(devId)` 命中后,**先按 `pending.tool` 判断是否为
|
||||||
|
写工具(kill/operate/control_service)**;是则调 `OnSystemReady`/`OnServiceReady`(返回 true
|
||||||
|
= 已接管、不 `CancelIO`),否则沿用现有 `TakeMainResponse`+`CancelIO`。与 `TOKEN_REGEDIT`
|
||||||
|
分支调用 `OnRegeditReady`(`2015RemoteDlg.cpp:6561`)的写法一致,避免与人工界面抢子连接。
|
||||||
|
|
||||||
|
## 4. 工具 schema
|
||||||
|
|
||||||
|
### 4.1 `kill_process`
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// inputSchema
|
||||||
|
{ "id": "string(必填,主机 id)",
|
||||||
|
"pids": ["integer 数组(必填,至少 1 个进程 PID)"] }
|
||||||
|
// outputSchema
|
||||||
|
{ "remaining": ["integer 数组(杀完后仍在运行的 PID)"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
- payload:`[COMMAND_KILLPROCESS:1][pid:4]*N`(DWORD 小端,与 `SystemDlg::OnPlistKill` 一致)。
|
||||||
|
- 成功判定:返回 `remaining`(发 `COMMAND_PSLIST` 后进程列表里已不含目标 pid 即视为成功)。
|
||||||
|
- 描述需写明:`TerminateProcess` 强制结束、不保存进程状态,可能造成数据丢失。
|
||||||
|
|
||||||
|
### 4.2 `operate_window`
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// inputSchema
|
||||||
|
{ "id": "string(必填)",
|
||||||
|
"hwnd": "integer(必填,窗口句柄,取 list_windows 返回的 hwnd)",
|
||||||
|
"action": "string(必填)close|show|hide|minimize|maximize|restore" }
|
||||||
|
// outputSchema
|
||||||
|
{ "ok": "boolean" }
|
||||||
|
```
|
||||||
|
|
||||||
|
- payload(与 `SystemDlg::OnWlistClose/OnWlistHide/OnWlistRecover/OnWlistMax/OnWlistMin` 一致,
|
||||||
|
**固定 20 字节零填充 `BYTE lpMsgBuf[20]`,hwnd 一律按 `DWORD`(4 字节)小端写入 offset 1**):
|
||||||
|
- `close` → `[CMD_WINDOW_CLOSE:1][hwnd:4]`(客户端 `PostMessage WM_CLOSE`)。
|
||||||
|
- 其余 → `[CMD_WINDOW_TEST:1][hwnd:4][dHow:4]`,dHow 映射(MFC 实测值):
|
||||||
|
`hide=SW_HIDE(0)` / `show=SW_NORMAL(1)` / `restore=SW_NORMAL(1)` /
|
||||||
|
`maximize=SW_MAXIMIZE(3)` / `minimize=SW_MINIMIZE(6)`。
|
||||||
|
- 缓冲器必须补零到 20 字节:客户端 `close` 分支读 `*((HWND*)(szBuffer+1))`,在 x64 是 8 字节,
|
||||||
|
而服务端只写了 4 字节 hwnd——正是 20 字节零填充补齐了高 4 字节,读出的 HWND 才是正确值;
|
||||||
|
若 MCP 发最小 5 字节包,会越界读。`test` 分支(`TestWindow`)读 `[hwnd:4][dHow:4]` 共 9 字节,
|
||||||
|
20 字节缓冲也足够。MFC 固定发 `lpMsgBuf[20]` 正是为此,MCP 照搬。
|
||||||
|
|
||||||
|
### 4.3 `control_service`
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// inputSchema
|
||||||
|
{ "id": "string(必填)",
|
||||||
|
"service_name": "string(必填,服务名,取 list_services 返回的 service_name)",
|
||||||
|
"action": "string(必填)start|stop|set_auto|set_manual" }
|
||||||
|
// outputSchema
|
||||||
|
{ "ok": "boolean" }
|
||||||
|
```
|
||||||
|
|
||||||
|
- payload:`COMMAND_SERVICECONFIG [cmd:1][bCmd:1][serviceName\0]`,
|
||||||
|
bCmd:`start=1` / `stop=2` / `set_auto=3` / `set_manual=4`。
|
||||||
|
- 边界:客户端 `ServicesManager` 仅实现这 4 项;**无** pause / continue / delete
|
||||||
|
(它们属于 `MachineDlg` 机器管理的另一条链路,留待 P8 评估)。
|
||||||
|
|
||||||
|
### 4.4 `power_action`
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// inputSchema
|
||||||
|
{ "id": "string(必填)",
|
||||||
|
"action": "string(必填)logout|shutdown|reboot" }
|
||||||
|
// outputSchema
|
||||||
|
{ "ok": "boolean" }
|
||||||
|
```
|
||||||
|
|
||||||
|
- payload:`TOKEN_MACHINE_MANAGE [token:1][type:1]`,type:`logout=0` / `shutdown=1` / `reboot=2`
|
||||||
|
(`enum MachineCommand` 顺序)。MFC 发的是 32 字节缓冲(`2015RemoteDlg::MachineManage`),
|
||||||
|
但客户端只校验 `ulLength>1` 并读 `szBuffer[1]`,故 2 字节即可。
|
||||||
|
- 走**主连接**、发完即走、无响应;主机将立即离线。描述需明确「不可撤销、主机下线」。
|
||||||
|
- 需客户端管理员权限(`EnableShutdownPrivilege`),无权限时客户端静默忽略(同 MFC 现状)。
|
||||||
|
- **仅 Windows 客户端**:Linux 侧文档写的是 `COMMAND_SESSION`(`linux/README.md`),但代码中
|
||||||
|
未发现对应 handler,即 Linux 实际未实现电源操作——`power_action` 对 LNX/MAC/APK 一律返回
|
||||||
|
`-32005`,不尝试发送。
|
||||||
|
|
||||||
|
## 5. 安全门(单一 `McpManage` 开关)
|
||||||
|
|
||||||
|
- 新增配置键 `settings/McpManage`(默认 `0`,关闭)。
|
||||||
|
- 生效条件:`McpManage=1` 且 `McpReadonly=0`;二者任一不满足即隐藏 4 个工具。
|
||||||
|
- 双保险:`tools/list` 隐藏 + 每个 `BuildXxx` 入口二次校验(与 `exec_command` 的
|
||||||
|
`IsReadonly()` 双保险一致)。
|
||||||
|
- `McpSettingsDlg` 增加「管理操作(进程/窗口/服务/电源)」复选框,写入 `THIS_CFG`。
|
||||||
|
- 审计:每次写操作写入服务端审计日志(`get_audit_log` 可见),记录 tool + host id + 参数摘要 + 结果。
|
||||||
|
|
||||||
|
## 6. 单飞行与并发约束
|
||||||
|
|
||||||
|
- 沿用现有约束:同 host 同一时刻只允许一个 MCP 挂起请求(`BeginPending` 保证)。
|
||||||
|
- 写工具与只读工具共用 `m_Pending`,天然互斥;与 MFC 对话框并存(各用独立子连接,互不干扰)。
|
||||||
|
- 超时沿用 `kMcpToolTimeoutMs = 20000`;`power_action` 因无响应、发完即返回成功。
|
||||||
|
|
||||||
|
## 7. 边界与风险
|
||||||
|
|
||||||
|
1. **杀进程/关机/重启不可逆**:描述与审计都需显著标注;不提供「确认对话框」,
|
||||||
|
确认责任在调用方(MCP 客户端/LLM agent)。
|
||||||
|
2. **kill 无客户端回执**:成功与否靠「新进程列表不含目标 pid」推断,`TerminateProcess`
|
||||||
|
失败(权限不足等)时进程仍在 `remaining`,由调用方判断。
|
||||||
|
3. **服务操作仅 4 项**:pause/continue/delete 缺省,如需需评估 `MachineDlg` 链路。
|
||||||
|
4. **窗口句柄时效**:hwnd 可能已失效(窗口已关闭),客户端 `PostMessage/ShowWindow`
|
||||||
|
对失效句柄静默无操作,`operate_window` 的 `ok=true` 只表示「已下发」。
|
||||||
|
5. **子连接竞态**:写工具接管子连接后若客户端断线,`OfflineProc` 调 `OnManageSubClosed`
|
||||||
|
清理挂起(置 done 并清 `subCtx`、唤醒等待线程;**不擦表**,由 `WaitPendingWrite` 自行擦除,
|
||||||
|
防迭代器失效)。清 `subCtx` 是关键——context 对象被连接池回收复用,若超时路径仍对旧指针
|
||||||
|
`CancelIO` 会误伤新连接(与 `OnTerminalClosed`/`OnScreenControlClosed` 同理)。
|
||||||
|
|
||||||
|
## 8. P8 / P9 路线图(后续)
|
||||||
|
|
||||||
|
- **P8 文件与信息面**:`delete_file/delete_directory`、`create_folder`、`rename_file`、
|
||||||
|
`open_file`;`list_network_connections`、`list_software`、`get/set_hosts`、
|
||||||
|
`list_scheduled_tasks`(+增删启停)、`get_keylog`。
|
||||||
|
- **P9 进阶与 NL 增强**:`set/delete_registry`、`screen_blank`/`lock_input`/`get_clipboard`、
|
||||||
|
`open_url`/`send_message`;**批量操作**(`ids[]`/`group` 维度);离线主机枚举
|
||||||
|
`list_all_hosts`。
|
||||||
|
|
||||||
|
## 9. 实现与验证记录(2026-10-01)
|
||||||
|
|
||||||
|
已实现并端到端验证(真实客户端 DESKTOP-A55C0OI / Windows 10 / VMware,id=3441387693643366974):
|
||||||
|
|
||||||
|
- `kill_process` → `remaining:[]`;`operate_window`(restore) → `ok:true`;
|
||||||
|
`control_service`(set_manual AJRouter) → `ok:true`;`power_action`(reboot) → `ok:true`(发完即走)。
|
||||||
|
三个写工具的确认包(第二个 `TOKEN_*LIST`)均正常到达,列表均 >1460 字节触发 HTTP 分片。
|
||||||
|
- 落地:`McpManage` 开关 + `McpReadonly=0` 双保险;`McpSettingsDlg` 增「管理操作」复选框;
|
||||||
|
主工具栏增 `MCP` 入口按钮(`ID_MCP_SETTINGS`);en/de/zh-TW 三语文案同步。
|
||||||
|
- 前置解析 bug(验证中暴露,已单独提交 `a72e6eb`):`HeaderParser::Parse` 的
|
||||||
|
`m_bShouldUnmask` 在 HTTP 分片续包上被误置 `FALSE`,导致写工具的确认包被
|
||||||
|
`PARSER_FAILED` 丢弃(「首包能到、后续包到不了」);修复为 `else if (m_bShouldUnmask != TRUE)`。
|
||||||
Binary file not shown.
@@ -27,6 +27,7 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include "KeyBoardDlg.h"
|
#include "KeyBoardDlg.h"
|
||||||
#include "InputDlg.h"
|
#include "InputDlg.h"
|
||||||
|
#include "ShareHostDlg.h"
|
||||||
#include "CPasswordDlg.h"
|
#include "CPasswordDlg.h"
|
||||||
#include "FeatureFlags.h"
|
#include "FeatureFlags.h"
|
||||||
#include "FeatureLimitsDlg.h"
|
#include "FeatureLimitsDlg.h"
|
||||||
@@ -43,6 +44,8 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include "common/skCrypter.h"
|
#include "common/skCrypter.h"
|
||||||
#include "common/commands.h"
|
#include "common/commands.h"
|
||||||
|
#include "QualityLevelsDlg.h"
|
||||||
|
#include "CPreviewLevelsDlg.h"
|
||||||
#include "McpServer.h"
|
#include "McpServer.h"
|
||||||
#include "McpSettingsDlg.h"
|
#include "McpSettingsDlg.h"
|
||||||
#include "common/md5.h"
|
#include "common/md5.h"
|
||||||
@@ -1013,6 +1016,8 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
|||||||
ON_COMMAND(ID_TOOL_IMPORT_LICENSE, &CMy2015RemoteDlg::OnToolImportLicense)
|
ON_COMMAND(ID_TOOL_IMPORT_LICENSE, &CMy2015RemoteDlg::OnToolImportLicense)
|
||||||
ON_COMMAND(ID_TOOL_V2_PRIVATEKEY, &CMy2015RemoteDlg::OnToolV2PrivateKey)
|
ON_COMMAND(ID_TOOL_V2_PRIVATEKEY, &CMy2015RemoteDlg::OnToolV2PrivateKey)
|
||||||
ON_COMMAND(ID_MENU_NOTIFY_SETTINGS, &CMy2015RemoteDlg::OnMenuNotifySettings)
|
ON_COMMAND(ID_MENU_NOTIFY_SETTINGS, &CMy2015RemoteDlg::OnMenuNotifySettings)
|
||||||
|
ON_COMMAND(ID_TOOL_QUALITY_LEVELS, &CMy2015RemoteDlg::OnQualityLevels)
|
||||||
|
ON_COMMAND(ID_TOOL_PREVIEW_LEVELS, &CMy2015RemoteDlg::OnPreviewLevels)
|
||||||
ON_COMMAND(ID_EXECUTE_TESTRUN, &CMy2015RemoteDlg::OnExecuteTestrun)
|
ON_COMMAND(ID_EXECUTE_TESTRUN, &CMy2015RemoteDlg::OnExecuteTestrun)
|
||||||
ON_COMMAND(ID_EXECUTE_GHOST, &CMy2015RemoteDlg::OnExecuteGhost)
|
ON_COMMAND(ID_EXECUTE_GHOST, &CMy2015RemoteDlg::OnExecuteGhost)
|
||||||
ON_COMMAND(ID_MASTER_TRAIL, &CMy2015RemoteDlg::OnMasterTrail)
|
ON_COMMAND(ID_MASTER_TRAIL, &CMy2015RemoteDlg::OnMasterTrail)
|
||||||
@@ -1409,6 +1414,8 @@ VOID CMy2015RemoteDlg::CreateToolBar()
|
|||||||
m_ToolBar.SetButtonText(11, _TR("生成服务端"));
|
m_ToolBar.SetButtonText(11, _TR("生成服务端"));
|
||||||
m_ToolBar.SetButtonText(12, _TR("搜索"));
|
m_ToolBar.SetButtonText(12, _TR("搜索"));
|
||||||
m_ToolBar.SetButtonText(13, _TR("帮助"));
|
m_ToolBar.SetButtonText(13, _TR("帮助"));
|
||||||
|
m_ToolBar.SetButtonText(14, _T("Lang/语言"));
|
||||||
|
m_ToolBar.SetButtonText(15, _T("MCP"));
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// UIBranding: 根据编译时配置或运行时标志隐藏工具栏按钮
|
// UIBranding: 根据编译时配置或运行时标志隐藏工具栏按钮
|
||||||
@@ -1459,7 +1466,7 @@ VOID CMy2015RemoteDlg::CreateToolBar()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果所有按钮都被隐藏,则隐藏整个工具栏
|
// 如果所有按钮都被隐藏,则隐藏整个工具栏
|
||||||
if (hiddenCount >= 14) {
|
if (hiddenCount >= 16) {
|
||||||
m_ToolBar.ShowWindow(SW_HIDE);
|
m_ToolBar.ShowWindow(SW_HIDE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2203,6 +2210,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
|||||||
McpServer().SetRemoteControlEnabled(THIS_CFG.GetInt("settings", "McpRemoteControl", 0) != 0);
|
McpServer().SetRemoteControlEnabled(THIS_CFG.GetInt("settings", "McpRemoteControl", 0) != 0);
|
||||||
// 文件传输开关:默认关;仅 download_file 门控(不要求只读关)。
|
// 文件传输开关:默认关;仅 download_file 门控(不要求只读关)。
|
||||||
McpServer().SetFileTransferEnabled(THIS_CFG.GetInt("settings", "McpFileTransfer", 0) != 0);
|
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)) {
|
if (!McpServer().Start(mcpBind, mcpPort)) {
|
||||||
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
|
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
|
||||||
} else {
|
} else {
|
||||||
@@ -2405,6 +2414,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
|||||||
// 这里读完 INI 后再 Apply 一次纠正过来。
|
// 这里读完 INI 后再 Apply 一次纠正过来。
|
||||||
LoadThumbnailSettingsFromCfg();
|
LoadThumbnailSettingsFromCfg();
|
||||||
ApplyThumbnailSettings();
|
ApplyThumbnailSettings();
|
||||||
|
LoadQualityProfilesFromCfg();
|
||||||
|
LoadPreviewProfilesFromCfg();
|
||||||
m_runNormal = THIS_CFG.GetInt("settings", "RunNormal", 0);
|
m_runNormal = THIS_CFG.GetInt("settings", "RunNormal", 0);
|
||||||
|
|
||||||
CMenu* SubMenu = m_MainMenu.GetSubMenu(2);
|
CMenu* SubMenu = m_MainMenu.GetSubMenu(2);
|
||||||
@@ -5194,6 +5205,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject)
|
|||||||
McpServer().OnScreenControlClosed(ContextObject);
|
McpServer().OnScreenControlClosed(ContextObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MCP P7 管理写操作(kill/窗口/服务)接管的子连接断开:清挂起,避免超时路径对
|
||||||
|
// 已回收(连接池复用)的 context 误 CancelIO(与终端/屏幕子连接的清理同理)。
|
||||||
|
if (McpServer().IsRunning()) {
|
||||||
|
McpServer().OnManageSubClosed(ContextObject);
|
||||||
|
}
|
||||||
|
|
||||||
SOCKET nSocket = ContextObject->sClientSocket;
|
SOCKET nSocket = ContextObject->sClientSocket;
|
||||||
|
|
||||||
CDialogBase* p = (CDialogBase*)ContextObject->hDlg;
|
CDialogBase* p = (CDialogBase*)ContextObject->hDlg;
|
||||||
@@ -6533,8 +6550,13 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
case TOKEN_PSLIST: { // 进程管理【x】
|
case TOKEN_PSLIST: { // 进程管理【x】
|
||||||
// P2b:MCP 挂起时接管。进程/窗口列表无请求 id,按 device_id 匹配并同步拷贝
|
// P2b:MCP 挂起时接管。进程/窗口列表无请求 id,按 device_id 匹配并同步拷贝
|
||||||
// (此时 IO 线程阻塞在 NotifyProc,缓冲尚未被覆盖,拷贝安全)。
|
// (此时 IO 线程阻塞在 NotifyProc,缓冲尚未被覆盖,拷贝安全)。
|
||||||
|
// P7:写工具(kill_process/operate_window)的「就绪包」与「确认包」是同一 token,
|
||||||
|
// OnSystemReady 命中就绪包时接管子连接并下发写命令(返回 true、不 CancelIO),
|
||||||
|
// 命中确认包或只读工具时返回 false,走 TakeMainResponse + CancelIO 收尾。
|
||||||
uint64_t devId = ContextObject->GetClientID();
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
if (McpServer().IsPending(devId)) {
|
if (McpServer().IsPending(devId)) {
|
||||||
|
if (McpServer().OnSystemReady(devId, ContextObject))
|
||||||
|
break;
|
||||||
McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength());
|
McpServer().TakeMainResponse(devId, ContextObject->GetBuffer(0), ContextObject->GetBufferLength());
|
||||||
ContextObject->CancelIO(); // 用完即关,避免子链接泄漏(仿 WebService 终端关闭)
|
ContextObject->CancelIO(); // 用完即关,避免子链接泄漏(仿 WebService 终端关闭)
|
||||||
break;
|
break;
|
||||||
@@ -6577,8 +6599,12 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
case TOKEN_SERVERLIST: { // 服务管理【x】
|
case TOKEN_SERVERLIST: { // 服务管理【x】
|
||||||
// P3:MCP 挂起时接管。服务列表为一次性子链接回传(CServicesManager 构造即发),
|
// P3:MCP 挂起时接管。服务列表为一次性子链接回传(CServicesManager 构造即发),
|
||||||
// 取走即 CancelIO 关子链接(无 MFC 对话框续用该子链接)。
|
// 取走即 CancelIO 关子链接(无 MFC 对话框续用该子链接)。
|
||||||
|
// P7:control_service 的就绪/确认是同一 token,OnServiceReady 命中就绪包时接管并下发
|
||||||
|
// COMMAND_SERVICECONFIG(返回 true、不 CancelIO),确认包走 TakeMainResponse 收尾。
|
||||||
uint64_t devId = ContextObject->GetClientID();
|
uint64_t devId = ContextObject->GetClientID();
|
||||||
if (McpServer().IsPending(devId)) {
|
if (McpServer().IsPending(devId)) {
|
||||||
|
if (McpServer().OnServiceReady(devId, ContextObject))
|
||||||
|
break;
|
||||||
McpServer().TakeMainResponse(devId, szBuffer, len);
|
McpServer().TakeMainResponse(devId, szBuffer, len);
|
||||||
ContextObject->CancelIO();
|
ContextObject->CancelIO();
|
||||||
break;
|
break;
|
||||||
@@ -7896,7 +7922,7 @@ LRESULT CMy2015RemoteDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
|
|
||||||
void CMy2015RemoteDlg::OnOnlineShare()
|
void CMy2015RemoteDlg::OnOnlineShare()
|
||||||
{
|
{
|
||||||
CInputDialog dlg(this);
|
CShareHostDlg dlg(this, FALSE);
|
||||||
dlg.Init(_TR("分享主机"), _TR("输入<IP:PORT>地址:"));
|
dlg.Init(_TR("分享主机"), _TR("输入<IP:PORT>地址:"));
|
||||||
dlg.SetHistoryKey("ShareAddress");
|
dlg.SetHistoryKey("ShareAddress");
|
||||||
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty())
|
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty())
|
||||||
@@ -7905,9 +7931,10 @@ void CMy2015RemoteDlg::OnOnlineShare()
|
|||||||
MessageBoxL("字符串长度超出[0, 250]范围限制!", "提示", MB_ICONINFORMATION);
|
MessageBoxL("字符串长度超出[0, 250]范围限制!", "提示", MB_ICONINFORMATION);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
CharMsg* buf = new CharMsg(dlg.m_str.GetLength()+1);
|
CString payload = dlg.m_str + _T(":") + (dlg.m_bShine ? _T("0") : _T("1"));
|
||||||
memcpy(buf->data, dlg.m_str, dlg.m_str.GetLength());
|
CharMsg* buf = new CharMsg(payload.GetLength()+1);
|
||||||
buf->data[dlg.m_str.GetLength()] = 0;
|
memcpy(buf->data, payload, payload.GetLength());
|
||||||
|
buf->data[payload.GetLength()] = 0;
|
||||||
PostMessageA(WM_SHARE_CLIENT, (WPARAM)buf, NULL);
|
PostMessageA(WM_SHARE_CLIENT, (WPARAM)buf, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8793,7 +8820,7 @@ void CMy2015RemoteDlg::OnListSingleClick(NMHDR* pNMHDR, LRESULT* pResult)
|
|||||||
OpenLoopTip(ctx, anchor);
|
OpenLoopTip(ctx, anchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 屏幕预览参数挑选:复用既有 GetTargetQualityLevel + GetScreenPreviewProfile
|
// 屏幕预览参数挑选:复用 GetTargetQualityLevel + GetPreviewProfileCfg(可配置的 6 档预览参数表)
|
||||||
// RTT 阈值表与屏幕共享共用(FRP 模式阈值更宽松);超清档(Ultra/High)+ 4K/超宽屏
|
// RTT 阈值表与屏幕共享共用(FRP 模式阈值更宽松);超清档(Ultra/High)+ 4K/超宽屏
|
||||||
// 时按 min(screenWidth/4, 1280) 自适应放大,避免高分屏被压成无信息缩略图
|
// 时按 min(screenWidth/4, 1280) 自适应放大,避免高分屏被压成无信息缩略图
|
||||||
void CMy2015RemoteDlg::ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const
|
void CMy2015RemoteDlg::ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const
|
||||||
@@ -8807,7 +8834,7 @@ void CMy2015RemoteDlg::ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, B
|
|||||||
int rttForLevel = ping > 0 ? ping : 250;
|
int rttForLevel = ping > 0 ? ping : 250;
|
||||||
|
|
||||||
int level = GetTargetQualityLevel(rttForLevel, m_settings.UsingFRPProxy ? 1 : 0);
|
int level = GetTargetQualityLevel(rttForLevel, m_settings.UsingFRPProxy ? 1 : 0);
|
||||||
const PreviewProfile& prof = GetScreenPreviewProfile(level);
|
const PreviewProfile& prof = GetPreviewProfileCfg(level);
|
||||||
int base = prof.maxWidth;
|
int base = prof.maxWidth;
|
||||||
BYTE q = (BYTE)prof.jpegQuality;
|
BYTE q = (BYTE)prof.jpegQuality;
|
||||||
|
|
||||||
@@ -9141,6 +9168,118 @@ void CMy2015RemoteDlg::SaveThumbnailSettingsToCfg() const
|
|||||||
THIS_CFG.SetInt("thumbnail", "JpegQuality", m_ThumbnailCfg.JpegQuality);
|
THIS_CFG.SetInt("thumbnail", "JpegQuality", m_ThumbnailCfg.JpegQuality);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 远程桌面质量等级配置([quality] 节) =====
|
||||||
|
// 6 档 QualityProfile 的读写与下发入口。缺省值回落到 common/commands.h 的
|
||||||
|
// GetQualityProfile(),与客户端默认表一致;用户改过的档位单独落盘到 INI。
|
||||||
|
|
||||||
|
void CMy2015RemoteDlg::LoadQualityProfilesFromCfg()
|
||||||
|
{
|
||||||
|
// 先整体回落到内置默认,再逐字段覆盖,避免 INI 缺项时读到 0
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
m_QualityProfiles[i] = GetQualityProfile(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
std::string sec = "profile" + std::to_string(i);
|
||||||
|
QualityProfile& p = m_QualityProfiles[i];
|
||||||
|
|
||||||
|
p.maxFPS = THIS_CFG.GetInt("quality", sec + "_maxFPS", p.maxFPS);
|
||||||
|
p.maxWidth = THIS_CFG.GetInt("quality", sec + "_maxWidth", p.maxWidth);
|
||||||
|
p.algorithm = THIS_CFG.GetInt("quality", sec + "_algorithm", p.algorithm);
|
||||||
|
p.bitRate = THIS_CFG.GetInt("quality", sec + "_bitRate", p.bitRate);
|
||||||
|
|
||||||
|
// Clamp 到安全范围(防 INI 被手工编错)
|
||||||
|
if (p.maxFPS < 1) p.maxFPS = 1;
|
||||||
|
if (p.maxFPS > 60) p.maxFPS = 60;
|
||||||
|
if (p.maxWidth < 0) p.maxWidth = 0;
|
||||||
|
if (p.maxWidth > 8192) p.maxWidth = 8192;
|
||||||
|
if (p.algorithm < ALGORITHM_GRAY || p.algorithm > ALGORITHM_RGB565)
|
||||||
|
p.algorithm = ALGORITHM_H264;
|
||||||
|
if (p.bitRate < 0) p.bitRate = 0;
|
||||||
|
if (p.bitRate > 50000) p.bitRate = 50000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMy2015RemoteDlg::SaveQualityProfilesToCfg() const
|
||||||
|
{
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
std::string sec = "profile" + std::to_string(i);
|
||||||
|
const QualityProfile& p = m_QualityProfiles[i];
|
||||||
|
THIS_CFG.SetInt("quality", sec + "_maxFPS", p.maxFPS);
|
||||||
|
THIS_CFG.SetInt("quality", sec + "_maxWidth", p.maxWidth);
|
||||||
|
THIS_CFG.SetInt("quality", sec + "_algorithm", p.algorithm);
|
||||||
|
THIS_CFG.SetInt("quality", sec + "_bitRate", p.bitRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QualityProfile& CMy2015RemoteDlg::GetQualityProfileCfg(int level) const
|
||||||
|
{
|
||||||
|
if (level < 0 || level >= QUALITY_COUNT)
|
||||||
|
return GetQualityProfile(level); // 越界:与 common 默认表同源兜底
|
||||||
|
return m_QualityProfiles[level];
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMy2015RemoteDlg::OnQualityLevels()
|
||||||
|
{
|
||||||
|
CQualityLevelsDlg dlg(m_QualityProfiles, this);
|
||||||
|
if (dlg.DoModal() == IDOK) {
|
||||||
|
SaveQualityProfilesToCfg();
|
||||||
|
MessageBoxL("质量等级已保存,下次打开远程桌面时生效。", "提示", MB_ICONINFORMATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 屏幕预览质量等级配置([preview] 节) =====
|
||||||
|
// 6 档 PreviewProfile 的读写入口。缺省值回落到 common/commands.h 的
|
||||||
|
// GetScreenPreviewProfile(),与内置预览参数表一致;用户改过的档位单独落盘到 INI。
|
||||||
|
|
||||||
|
void CMy2015RemoteDlg::LoadPreviewProfilesFromCfg()
|
||||||
|
{
|
||||||
|
// 先整体回落到内置默认,再逐字段覆盖,避免 INI 缺项时读到 0
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
m_PreviewProfiles[i] = GetScreenPreviewProfile(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
std::string sec = "preview" + std::to_string(i);
|
||||||
|
PreviewProfile& p = m_PreviewProfiles[i];
|
||||||
|
|
||||||
|
p.maxWidth = THIS_CFG.GetInt("preview", sec + "_maxWidth", p.maxWidth);
|
||||||
|
p.jpegQuality = THIS_CFG.GetInt("preview", sec + "_jpegQuality", p.jpegQuality);
|
||||||
|
|
||||||
|
// Clamp 到安全范围(防 INI 被手工编错)
|
||||||
|
if (p.maxWidth < 64) p.maxWidth = 64;
|
||||||
|
if (p.maxWidth > 2560) p.maxWidth = 2560;
|
||||||
|
if (p.jpegQuality < 1) p.jpegQuality = 1;
|
||||||
|
if (p.jpegQuality > 100) p.jpegQuality = 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMy2015RemoteDlg::SavePreviewProfilesToCfg() const
|
||||||
|
{
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
std::string sec = "preview" + std::to_string(i);
|
||||||
|
const PreviewProfile& p = m_PreviewProfiles[i];
|
||||||
|
THIS_CFG.SetInt("preview", sec + "_maxWidth", p.maxWidth);
|
||||||
|
THIS_CFG.SetInt("preview", sec + "_jpegQuality", p.jpegQuality);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PreviewProfile& CMy2015RemoteDlg::GetPreviewProfileCfg(int level) const
|
||||||
|
{
|
||||||
|
if (level < 0 || level >= QUALITY_COUNT)
|
||||||
|
return GetScreenPreviewProfile(level); // 越界:与 common 默认表同源兜底
|
||||||
|
return m_PreviewProfiles[level];
|
||||||
|
}
|
||||||
|
|
||||||
|
void CMy2015RemoteDlg::OnPreviewLevels()
|
||||||
|
{
|
||||||
|
CPreviewLevelsDlg dlg(m_PreviewProfiles, this);
|
||||||
|
if (dlg.DoModal() == IDOK) {
|
||||||
|
SavePreviewProfilesToCfg();
|
||||||
|
MessageBoxL("预览质量等级已保存,下次发送预览时生效。", "提示", MB_ICONINFORMATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CMy2015RemoteDlg::ApplyThumbnailSettings()
|
void CMy2015RemoteDlg::ApplyThumbnailSettings()
|
||||||
{
|
{
|
||||||
// 任何已缓存的 HBITMAP 都是按"旧 ThumbWidth"渲染的,尺寸不再可信 —— 一律清空。
|
// 任何已缓存的 HBITMAP 都是按"旧 ThumbWidth"渲染的,尺寸不再可信 —— 一律清空。
|
||||||
@@ -9660,26 +9799,27 @@ void CMy2015RemoteDlg::OnShellcodeTestAesBin()
|
|||||||
|
|
||||||
void CMy2015RemoteDlg::OnOnlineAssignTo()
|
void CMy2015RemoteDlg::OnOnlineAssignTo()
|
||||||
{
|
{
|
||||||
CInputDialog dlg(this);
|
CShareHostDlg dlg(this, TRUE);
|
||||||
dlg.Init(_TR("转移主机(到期自动复原)"), _TR("输入<IP:PORT>地址:"));
|
dlg.Init(_TR("转移主机(到期自动复原)"), _TR("输入<IP:PORT>地址:"));
|
||||||
dlg.SetHistoryKey("ShareAddress");
|
dlg.SetHistoryKey("ShareAddress");
|
||||||
dlg.Init2(_TR("天数(支持浮点数):"), "30");
|
dlg.InitDays(_TR("天数(支持浮点数):"), "30");
|
||||||
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty() || atof(dlg.m_sSecondInput.GetString())<=0)
|
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty() || atof(dlg.m_sDays.GetString())<=0)
|
||||||
return;
|
return;
|
||||||
if (dlg.m_str.GetLength() >= 250) {
|
if (dlg.m_str.GetLength() >= 250) {
|
||||||
MessageBoxL("字符串长度超出[0, 250]范围限制!", "提示", MB_ICONINFORMATION);
|
MessageBoxL("字符串长度超出[0, 250]范围限制!", "提示", MB_ICONINFORMATION);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (dlg.m_sSecondInput.GetLength() >= 6) {
|
if (dlg.m_sDays.GetLength() >= 6) {
|
||||||
MessageBoxL("超出使用时间可输入的字符数限制!", "提示", MB_ICONINFORMATION);
|
MessageBoxL("超出使用时间可输入的字符数限制!", "提示", MB_ICONINFORMATION);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
CString daysProto = dlg.m_sDays + _T(":") + (dlg.m_bShine ? _T("0") : _T("1"));
|
||||||
CharMsg* buf1 = new CharMsg(dlg.m_str.GetLength() + 1);
|
CharMsg* buf1 = new CharMsg(dlg.m_str.GetLength() + 1);
|
||||||
CharMsg* buf2 = new CharMsg(dlg.m_sSecondInput.GetLength() + 1);
|
CharMsg* buf2 = new CharMsg(daysProto.GetLength() + 1);
|
||||||
memcpy(buf1->data, dlg.m_str, dlg.m_str.GetLength());
|
memcpy(buf1->data, dlg.m_str, dlg.m_str.GetLength());
|
||||||
memcpy(buf2->data, dlg.m_sSecondInput, dlg.m_sSecondInput.GetLength());
|
memcpy(buf2->data, daysProto, daysProto.GetLength());
|
||||||
buf1->data[dlg.m_str.GetLength()] = 0;
|
buf1->data[dlg.m_str.GetLength()] = 0;
|
||||||
buf2->data[dlg.m_sSecondInput.GetLength()] = 0;
|
buf2->data[daysProto.GetLength()] = 0;
|
||||||
PostMessageA(WM_ASSIGN_CLIENT, (WPARAM)buf1, (LPARAM)buf2);
|
PostMessageA(WM_ASSIGN_CLIENT, (WPARAM)buf1, (LPARAM)buf2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "TrueColorToolBar.h"
|
#include "TrueColorToolBar.h"
|
||||||
#include "IOCPServer.h"
|
#include "IOCPServer.h"
|
||||||
#include <common/location.h>
|
#include <common/location.h>
|
||||||
|
#include "common/commands.h"
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -328,6 +329,24 @@ public:
|
|||||||
void LoadThumbnailSettingsFromCfg(); // 从 THIS_CFG 读到 m_ThumbnailCfg
|
void LoadThumbnailSettingsFromCfg(); // 从 THIS_CFG 读到 m_ThumbnailCfg
|
||||||
void SaveThumbnailSettingsToCfg() const; // m_ThumbnailCfg 落盘到 THIS_CFG
|
void SaveThumbnailSettingsToCfg() const; // m_ThumbnailCfg 落盘到 THIS_CFG
|
||||||
void TickThumbnailRefresh(); // TIMER_THUMBNAIL_REFRESH 主循环
|
void TickThumbnailRefresh(); // TIMER_THUMBNAIL_REFRESH 主循环
|
||||||
|
|
||||||
|
// ===== 远程桌面质量等级配置(6 档 QualityProfile) =====
|
||||||
|
// 主控端 UI 偏好,持久化走 THIS_CFG [quality] 节。服务端权威,经 CMD_QUALITY_PROFILES
|
||||||
|
// 下发给客户端。仅 UI 线程访问;修改后需重开远程桌面会话才对新连接生效。
|
||||||
|
QualityProfile m_QualityProfiles[QUALITY_COUNT];
|
||||||
|
void LoadQualityProfilesFromCfg(); // 从 THIS_CFG 读到 m_QualityProfiles(缺省回落 GetQualityProfile)
|
||||||
|
void SaveQualityProfilesToCfg() const; // m_QualityProfiles 落盘到 THIS_CFG
|
||||||
|
const QualityProfile& GetQualityProfileCfg(int level) const; // 供 ScreenSpyDlg 下发用
|
||||||
|
void OnQualityLevels(); // 高级→远程桌面质量等级
|
||||||
|
|
||||||
|
// ===== 屏幕预览质量等级配置(6 档 PreviewProfile) =====
|
||||||
|
// 双击主机单发预览 / 循环快照浮窗所用。持久化走 THIS_CFG [preview] 节。
|
||||||
|
// 仅 UI 线程访问;修改后下次发预览请求即生效。
|
||||||
|
PreviewProfile m_PreviewProfiles[QUALITY_COUNT];
|
||||||
|
void LoadPreviewProfilesFromCfg(); // 从 THIS_CFG 读到 m_PreviewProfiles(缺省回落 GetScreenPreviewProfile)
|
||||||
|
void SavePreviewProfilesToCfg() const; // m_PreviewProfiles 落盘到 THIS_CFG
|
||||||
|
const PreviewProfile& GetPreviewProfileCfg(int level) const; // 供 ChooseScreenPreviewParams 用
|
||||||
|
void OnPreviewLevels(); // 高级→屏幕预览质量等级
|
||||||
void SendThumbnailRequest(class context* ctx);// 发一次缩略图请求
|
void SendThumbnailRequest(class context* ctx);// 发一次缩略图请求
|
||||||
void CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes);
|
void CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes);
|
||||||
void ClearThumbnailCacheEntry(uint64_t clientID);
|
void ClearThumbnailCacheEntry(uint64_t clientID);
|
||||||
|
|||||||
@@ -334,6 +334,7 @@
|
|||||||
<ClInclude Include="NotifySettingsDlg.h" />
|
<ClInclude Include="NotifySettingsDlg.h" />
|
||||||
<ClInclude Include="FeatureLimitsDlg.h" />
|
<ClInclude Include="FeatureLimitsDlg.h" />
|
||||||
<ClInclude Include="FrpsForSubDlg.h" />
|
<ClInclude Include="FrpsForSubDlg.h" />
|
||||||
|
<ClInclude Include="ShareHostDlg.h" />
|
||||||
<ClInclude Include="PluginSettingsDlg.h" />
|
<ClInclude Include="PluginSettingsDlg.h" />
|
||||||
<ClInclude Include="PreviewTipWnd.h" />
|
<ClInclude Include="PreviewTipWnd.h" />
|
||||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||||
@@ -462,6 +463,7 @@
|
|||||||
<ClCompile Include="NotifySettingsDlg.cpp" />
|
<ClCompile Include="NotifySettingsDlg.cpp" />
|
||||||
<ClCompile Include="FeatureLimitsDlg.cpp" />
|
<ClCompile Include="FeatureLimitsDlg.cpp" />
|
||||||
<ClCompile Include="FrpsForSubDlg.cpp" />
|
<ClCompile Include="FrpsForSubDlg.cpp" />
|
||||||
|
<ClCompile Include="ShareHostDlg.cpp" />
|
||||||
<ClCompile Include="Loader.c">
|
<ClCompile Include="Loader.c">
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
<ClCompile Include="NotifyManager.cpp" />
|
<ClCompile Include="NotifyManager.cpp" />
|
||||||
<ClCompile Include="NotifySettingsDlg.cpp" />
|
<ClCompile Include="NotifySettingsDlg.cpp" />
|
||||||
<ClCompile Include="FrpsForSubDlg.cpp" />
|
<ClCompile Include="FrpsForSubDlg.cpp" />
|
||||||
|
<ClCompile Include="ShareHostDlg.cpp" />
|
||||||
<ClCompile Include="NetworkDlg.cpp" />
|
<ClCompile Include="NetworkDlg.cpp" />
|
||||||
<ClCompile Include="IPHistoryDlg.cpp" />
|
<ClCompile Include="IPHistoryDlg.cpp" />
|
||||||
<ClCompile Include="FeatureLimitsDlg.cpp" />
|
<ClCompile Include="FeatureLimitsDlg.cpp" />
|
||||||
@@ -181,6 +182,7 @@
|
|||||||
<ClInclude Include="NotifyManager.h" />
|
<ClInclude Include="NotifyManager.h" />
|
||||||
<ClInclude Include="NotifySettingsDlg.h" />
|
<ClInclude Include="NotifySettingsDlg.h" />
|
||||||
<ClInclude Include="FrpsForSubDlg.h" />
|
<ClInclude Include="FrpsForSubDlg.h" />
|
||||||
|
<ClInclude Include="ShareHostDlg.h" />
|
||||||
<ClInclude Include="TerminalModuleLoader.h" />
|
<ClInclude Include="TerminalModuleLoader.h" />
|
||||||
<ClInclude Include="..\..\common\SafeString.h" />
|
<ClInclude Include="..\..\common\SafeString.h" />
|
||||||
<ClInclude Include="NetworkDlg.h" />
|
<ClInclude Include="NetworkDlg.h" />
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ CBuildDlg::CBuildDlg(CWnd* pParent)
|
|||||||
, m_strPort(_T(""))
|
, m_strPort(_T(""))
|
||||||
, m_strFindden(FLAG_FINDEN)
|
, m_strFindden(FLAG_FINDEN)
|
||||||
, m_sGroupName(_T("default"))
|
, m_sGroupName(_T("default"))
|
||||||
, m_strEncryptIP(_T("是"))
|
, m_strEncryptIP(_L(_T("是")))
|
||||||
, m_sInstallDir(_T(""))
|
, m_sInstallDir(_T(""))
|
||||||
, m_sInstallName(_T(""))
|
, m_sInstallName(_T(""))
|
||||||
, m_sDownloadUrl(_T(""))
|
, m_sDownloadUrl(_T(""))
|
||||||
|
|||||||
@@ -551,6 +551,18 @@ void CIconButton::DrawIconLetterZ(CDC* pDC, const CRect& rc)
|
|||||||
DrawLetterIcon(pDC, rc, _T("Z"));
|
DrawLetterIcon(pDC, rc, _T("Z"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Letter C icon (获取剪贴板)
|
||||||
|
void CIconButton::DrawIconLetterC(CDC* pDC, const CRect& rc)
|
||||||
|
{
|
||||||
|
DrawLetterIcon(pDC, rc, _T("C"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Letter P icon (设置剪贴板)
|
||||||
|
void CIconButton::DrawIconLetterP(CDC* pDC, const CRect& rc)
|
||||||
|
{
|
||||||
|
DrawLetterIcon(pDC, rc, _T("P"));
|
||||||
|
}
|
||||||
|
|
||||||
// Audio On: speaker with sound waves
|
// Audio On: speaker with sound waves
|
||||||
void CIconButton::DrawIconAudioOn(CDC* pDC, const CRect& rc)
|
void CIconButton::DrawIconAudioOn(CDC* pDC, const CRect& rc)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ public:
|
|||||||
static void DrawIconLetterX(CDC* pDC, const CRect& rc); // 字母 X 图标
|
static void DrawIconLetterX(CDC* pDC, const CRect& rc); // 字母 X 图标
|
||||||
static void DrawIconLetterY(CDC* pDC, const CRect& rc); // 字母 Y 图标
|
static void DrawIconLetterY(CDC* pDC, const CRect& rc); // 字母 Y 图标
|
||||||
static void DrawIconLetterZ(CDC* pDC, const CRect& rc); // 字母 Z 图标
|
static void DrawIconLetterZ(CDC* pDC, const CRect& rc); // 字母 Z 图标
|
||||||
|
static void DrawIconLetterC(CDC* pDC, const CRect& rc); // 字母 C 图标 (获取剪贴板)
|
||||||
|
static void DrawIconLetterP(CDC* pDC, const CRect& rc); // 字母 P 图标 (设置剪贴板)
|
||||||
static void DrawIconAudioOn(CDC* pDC, const CRect& rc); // 音频开启图标 (喇叭+声波)
|
static void DrawIconAudioOn(CDC* pDC, const CRect& rc); // 音频开启图标 (喇叭+声波)
|
||||||
static void DrawIconAudioOff(CDC* pDC, const CRect& rc); // 音频关闭图标 (喇叭+斜线)
|
static void DrawIconAudioOff(CDC* pDC, const CRect& rc); // 音频关闭图标 (喇叭+斜线)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// CPreviewLevelsDlg.h
|
||||||
|
// 屏幕预览质量等级配置对话框:编辑 6 档 PreviewProfile(编码宽度 / JPEG 质量)。
|
||||||
|
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::SavePreviewProfilesToCfg() 负责。
|
||||||
|
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现,勿多处包含)。
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LangManager.h"
|
||||||
|
|
||||||
|
class CPreviewLevelsDlg : public CDialog
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum { IDD = IDD_PREVIEW_LEVELS };
|
||||||
|
|
||||||
|
// profiles: 指向 6 元素 PreviewProfile 数组,确认后写回
|
||||||
|
CPreviewLevelsDlg(PreviewProfile* profiles, CWnd* pParent = NULL)
|
||||||
|
: CDialog(CPreviewLevelsDlg::IDD, pParent), m_profiles(profiles)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
PreviewProfile* m_profiles;
|
||||||
|
PreviewProfile m_working[QUALITY_COUNT];
|
||||||
|
|
||||||
|
static const char* LevelName(int i)
|
||||||
|
{
|
||||||
|
static const char* names[QUALITY_COUNT] = {
|
||||||
|
"Ultra", "High", "Good", "Medium", "Low", "Minimal"
|
||||||
|
};
|
||||||
|
return (i >= 0 && i < QUALITY_COUNT) ? names[i] : "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadToControls()
|
||||||
|
{
|
||||||
|
CString s;
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
s.Format(_T("%d"), m_working[i].maxWidth);
|
||||||
|
SetDlgItemText(IDC_PLEVEL_WIDTH + i, s);
|
||||||
|
s.Format(_T("%d"), m_working[i].jpegQuality);
|
||||||
|
SetDlgItemText(IDC_PLEVEL_QUALITY + i, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadInt(int id, int& out)
|
||||||
|
{
|
||||||
|
CString s;
|
||||||
|
GetDlgItemText(id, s);
|
||||||
|
s.Trim();
|
||||||
|
if (s.IsEmpty()) return false;
|
||||||
|
|
||||||
|
int v = 0;
|
||||||
|
for (int k = 0; k < s.GetLength(); k++) {
|
||||||
|
TCHAR ch = s[k];
|
||||||
|
if (ch < _T('0') || ch > _T('9')) return false;
|
||||||
|
v = v * 10 + (ch - _T('0'));
|
||||||
|
}
|
||||||
|
out = v;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadFromControls(CString& err)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
int w, q;
|
||||||
|
if (!ReadInt(IDC_PLEVEL_WIDTH + i, w))
|
||||||
|
{ err.Format(_T("等级 %s 的最大宽度不是有效数字"), LevelName(i)); return false; }
|
||||||
|
if (!ReadInt(IDC_PLEVEL_QUALITY + i, q))
|
||||||
|
{ err.Format(_T("等级 %s 的 JPEG 质量不是有效数字"), LevelName(i)); return false; }
|
||||||
|
|
||||||
|
if (w < 64 || w > 2560)
|
||||||
|
{ err.Format(_T("等级 %s 的最大宽度需在 64~2560 之间"), LevelName(i)); return false; }
|
||||||
|
if (q < 1 || q > 100)
|
||||||
|
{ err.Format(_T("等级 %s 的 JPEG 质量需在 1~100 之间"), LevelName(i)); return false; }
|
||||||
|
|
||||||
|
m_working[i].maxWidth = w;
|
||||||
|
m_working[i].jpegQuality = q;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetToDefaults()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
m_working[i] = GetScreenPreviewProfile(i);
|
||||||
|
}
|
||||||
|
LoadToControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual BOOL OnInitDialog() override
|
||||||
|
{
|
||||||
|
CDialog::OnInitDialog();
|
||||||
|
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
m_working[i] = m_profiles[i];
|
||||||
|
}
|
||||||
|
LoadToControls();
|
||||||
|
TranslateDialog(this);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void OnOK() override
|
||||||
|
{
|
||||||
|
CString err;
|
||||||
|
if (!ReadFromControls(err)) {
|
||||||
|
MessageBox(err, _TR("提示"), MB_ICONWARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
m_profiles[i] = m_working[i];
|
||||||
|
}
|
||||||
|
CDialog::OnOK();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
|
||||||
|
{
|
||||||
|
if (LOWORD(wParam) == IDC_PLEVEL_RESET && HIWORD(wParam) == BN_CLICKED) {
|
||||||
|
ResetToDefaults();
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
return CDialog::OnCommand(wParam, lParam);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -107,7 +107,7 @@ void CWalletDlg::OnOK()
|
|||||||
if (a[i].IsEmpty()) continue;
|
if (a[i].IsEmpty()) continue;
|
||||||
if (WALLET_UNKNOWN == detectWalletType(a[i].GetString())) {
|
if (WALLET_UNKNOWN == detectWalletType(a[i].GetString())) {
|
||||||
char tip[100];
|
char tip[100];
|
||||||
sprintf(tip, "第 %d 个钱包地址不合法!", i + 1);
|
sprintf(tip, _TRF("第 %d 个钱包地址不合法!"), i + 1);
|
||||||
MessageBoxL(CString(tip), "提示", MB_ICONINFORMATION);
|
MessageBoxL(CString(tip), "提示", MB_ICONINFORMATION);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ BOOL CHideScreenSpyDlg::OnInitDialog()
|
|||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
// ShowWindow(SW_MINIMIZE);
|
// ShowWindow(SW_MINIMIZE);
|
||||||
#endif
|
#endif
|
||||||
m_strTip = CString("请等待......");
|
m_strTip = _TR("请等待......");
|
||||||
return TRUE; // return TRUE unless you set the focus to a control
|
return TRUE; // return TRUE unless you set the focus to a control
|
||||||
// EXCEPTION: OCX Property Pages should return FALSE
|
// EXCEPTION: OCX Property Pages should return FALSE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -703,12 +703,13 @@ protected:
|
|||||||
if (langCode == _T("zh_CN")) return _T("Simplified Chinese");
|
if (langCode == _T("zh_CN")) return _T("Simplified Chinese");
|
||||||
if (langCode == _T("zh_TW")) return _T("Traditional Chinese");
|
if (langCode == _T("zh_TW")) return _T("Traditional Chinese");
|
||||||
if (langCode == _T("en_US")) return _T("English");
|
if (langCode == _T("en_US")) return _T("English");
|
||||||
|
if (langCode == _T("de_DE")) return _T("Deutsch");
|
||||||
return langCode;
|
return langCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
LPCDLGTEMPLATE CreateDialogTemplate()
|
LPCDLGTEMPLATE CreateDialogTemplate()
|
||||||
{
|
{
|
||||||
const WORD DLG_WIDTH = 200;
|
const WORD DLG_WIDTH = 260;
|
||||||
const WORD DLG_HEIGHT = 75;
|
const WORD DLG_HEIGHT = 75;
|
||||||
|
|
||||||
m_templateBuffer.clear();
|
m_templateBuffer.clear();
|
||||||
@@ -722,24 +723,24 @@ protected:
|
|||||||
AppendData(&dlgTemplate, sizeof(DLGTEMPLATE));
|
AppendData(&dlgTemplate, sizeof(DLGTEMPLATE));
|
||||||
AppendWord(0); // 菜单
|
AppendWord(0); // 菜单
|
||||||
AppendWord(0); // 窗口类
|
AppendWord(0); // 窗口类
|
||||||
AppendString(_T("Select Language"));
|
AppendString(_T("切换语言 / Switch Language"));
|
||||||
AlignToDword();
|
AlignToDword();
|
||||||
|
|
||||||
// 静态文本
|
// 静态文本
|
||||||
AddControl(0x0082, 15, 15, 50, 12, (WORD)-1,
|
AddControl(0x0082, 15, 15, 50, 12, (WORD)-1,
|
||||||
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("Language:"));
|
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("语言:"));
|
||||||
|
|
||||||
// ComboBox
|
// ComboBox
|
||||||
AddControl(0x0085, 65, 13, 120, 150, 1001,
|
AddControl(0x0085, 65, 13, 180, 150, 1001,
|
||||||
CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T(""));
|
CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T(""));
|
||||||
|
|
||||||
// OK 按钮
|
// OK 按钮
|
||||||
AddControl(0x0080, 45, 50, 50, 14, IDOK,
|
AddControl(0x0080, 75, 50, 50, 14, IDOK,
|
||||||
BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("OK"));
|
BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("确定"));
|
||||||
|
|
||||||
// Cancel 按钮
|
// Cancel 按钮
|
||||||
AddControl(0x0080, 105, 50, 50, 14, IDCANCEL,
|
AddControl(0x0080, 135, 50, 50, 14, IDCANCEL,
|
||||||
BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("Cancel"));
|
BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("取消"));
|
||||||
|
|
||||||
return (LPCDLGTEMPLATE)m_templateBuffer.data();
|
return (LPCDLGTEMPLATE)m_templateBuffer.data();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -840,6 +840,129 @@ Json::Value BuildServiceListOutputSchema() {
|
|||||||
return schema;
|
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 缺省/空 = 列根键)
|
// list_registry 的 inputSchema(id 必填,path 可选;path 缺省/空 = 列根键)
|
||||||
Json::Value BuildListRegistryInputSchema() {
|
Json::Value BuildListRegistryInputSchema() {
|
||||||
Json::Value props(Json::objectValue);
|
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 BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||||
std::string BuildRemoteClipboard(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
|
// tools/list
|
||||||
std::string BuildToolsListResult(const Json::Value& id) {
|
std::string BuildToolsListResult(const Json::Value& id) {
|
||||||
Json::Value result(Json::objectValue);
|
Json::Value result(Json::objectValue);
|
||||||
@@ -1433,6 +1570,43 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
|||||||
tools.append(tool);
|
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;
|
result["tools"] = tools;
|
||||||
return BuildResult(id, result);
|
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);
|
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 分派
|
// tools/call 分派
|
||||||
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||||
const Json::Value& id = root["id"];
|
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 == "remote_clipboard") return BuildRemoteClipboard(id, args, parent);
|
||||||
if (toolName == "download_file") return BuildDownloadFile(id, args, parent);
|
if (toolName == "download_file") return BuildDownloadFile(id, args, parent);
|
||||||
if (toolName == "upload_file") return BuildUploadFile(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,
|
return BuildError(id, -32602,
|
||||||
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
|
"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) {
|
void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) {
|
||||||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||||
auto it = m_Pending.find(device_id);
|
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.data.assign(data, data + len);
|
||||||
it->second.done = true;
|
it->second.done = true;
|
||||||
m_PendingCv.notify_one();
|
m_PendingCv.notify_one();
|
||||||
@@ -4902,6 +5407,110 @@ bool CMcpServer::WaitPendingRegistry(uint64_t device_id, std::vector<BYTE>& subk
|
|||||||
return true;
|
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> 前
|
// rand_s:Windows CRT 加密安全随机源(基于系统 CSPRNG)。其声明需在 <stdlib.h> 前
|
||||||
// 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。
|
// 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。
|
||||||
extern "C" int __cdecl rand_s(unsigned int* randomValue);
|
extern "C" int __cdecl rand_s(unsigned int* randomValue);
|
||||||
|
|||||||
@@ -242,6 +242,34 @@ public:
|
|||||||
// 收尾:擦会话 + 锁外 CancelIO 文件管理器/流式两条子链接(幂等)。
|
// 收尾:擦会话 + 锁外 CancelIO 文件管理器/流式两条子链接(幂等)。
|
||||||
void ClearFileTransfer(uint64_t device_id);
|
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 后设置)。
|
// 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。
|
||||||
void SetReadonly(bool readonly) { m_readonly = readonly; }
|
void SetReadonly(bool readonly) { m_readonly = readonly; }
|
||||||
void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; }
|
void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; }
|
||||||
@@ -274,6 +302,10 @@ private:
|
|||||||
bool pathDone = false; // list_registry:已收到 TOKEN_REG_PATH
|
bool pathDone = false; // list_registry:已收到 TOKEN_REG_PATH
|
||||||
bool keyDone = false; // list_registry:已收到 TOKEN_REG_KEY
|
bool keyDone = false; // list_registry:已收到 TOKEN_REG_KEY
|
||||||
bool done = false;
|
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::mutex m_PendingMutex;
|
||||||
std::condition_variable m_PendingCv;
|
std::condition_variable m_PendingCv;
|
||||||
@@ -348,6 +380,7 @@ private:
|
|||||||
bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false)
|
bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false)
|
||||||
bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false)
|
bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false)
|
||||||
bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file)
|
bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file)
|
||||||
|
bool m_manageEnabled = false; // P7 管理控制开关(默认关;要求 m_readonly=false)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 全局访问器(仿 WebService(),见 WebService.h 末尾)
|
// 全局访问器(仿 WebService(),见 WebService.h 末尾)
|
||||||
|
|||||||
@@ -145,6 +145,9 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
|||||||
m_btnFileTransfer.Create(_TR("启用文件传输(download_file)"),
|
m_btnFileTransfer.Create(_TR("启用文件传输(download_file)"),
|
||||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||||||
r0, this, IDC_MCP_FILETRANSFER);
|
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_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
|
||||||
m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP |
|
m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP |
|
||||||
ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL,
|
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_btnTerminal.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||||
m_btnRemoteControl.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_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_lblWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||||
m_editWhitelist.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));
|
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 terminal = THIS_CFG.GetInt("settings", "McpTerminal", 0);
|
||||||
int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0);
|
int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0);
|
||||||
int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 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", "");
|
std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", "");
|
||||||
|
|
||||||
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
|
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
|
||||||
@@ -194,6 +199,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
|||||||
m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED);
|
m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED);
|
||||||
m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED);
|
m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED);
|
||||||
m_btnFileTransfer.SetCheck(fileTransfer ? 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()));
|
m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str()));
|
||||||
|
|
||||||
@@ -213,6 +219,7 @@ void CMcpSettingsDlg::OnOK()
|
|||||||
bool terminal = (m_btnTerminal.GetCheck() == BST_CHECKED);
|
bool terminal = (m_btnTerminal.GetCheck() == BST_CHECKED);
|
||||||
bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED);
|
bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED);
|
||||||
bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED);
|
bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED);
|
||||||
|
bool manage = (m_btnManage.GetCheck() == BST_CHECKED);
|
||||||
|
|
||||||
// 端口校验:1-65535
|
// 端口校验:1-65535
|
||||||
int port = atoi(CT2A(sPort));
|
int port = atoi(CT2A(sPort));
|
||||||
@@ -239,14 +246,15 @@ void CMcpSettingsDlg::OnOK()
|
|||||||
THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 1 : 0);
|
THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 1 : 0);
|
||||||
THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0);
|
THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0);
|
||||||
THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0);
|
THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0);
|
||||||
|
THIS_CFG.SetInt("settings", "McpManage", manage ? 1 : 0);
|
||||||
std::string whitelist = CT2A(sWhitelist);
|
std::string whitelist = CT2A(sWhitelist);
|
||||||
whitelist = NormalizeWhitelist(whitelist);
|
whitelist = NormalizeWhitelist(whitelist);
|
||||||
THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist);
|
THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist);
|
||||||
|
|
||||||
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
|
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
|
||||||
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
|
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
|
||||||
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。") + _T("\r\n") +
|
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。") + _T("\r\n") +
|
||||||
_TR("持久终端与远程控制仅在只读模式关闭时生效。"),
|
_TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。"),
|
||||||
_TR("提示"), MB_ICONINFORMATION);
|
_TR("提示"), MB_ICONINFORMATION);
|
||||||
|
|
||||||
CDialog::OnOK();
|
CDialog::OnOK();
|
||||||
@@ -289,6 +297,9 @@ void CMcpSettingsDlg::LayoutControls(int cx, int cy)
|
|||||||
m_btnFileTransfer.MoveWindow(margin, y, cx - margin * 2, 22);
|
m_btnFileTransfer.MoveWindow(margin, y, cx - margin * 2, 22);
|
||||||
y += 30;
|
y += 30;
|
||||||
|
|
||||||
|
m_btnManage.MoveWindow(margin, y, cx - margin * 2, 22);
|
||||||
|
y += 30;
|
||||||
|
|
||||||
const int whitelistH = 90;
|
const int whitelistH = 90;
|
||||||
m_lblWhitelist.MoveWindow(margin, y, labelW, rowH);
|
m_lblWhitelist.MoveWindow(margin, y, labelW, rowH);
|
||||||
m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH);
|
m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ private:
|
|||||||
IDC_MCP_TERMINAL = 1007, // 「启用持久终端」复选框(全命令,无白名单,要求只读关)
|
IDC_MCP_TERMINAL = 1007, // 「启用持久终端」复选框(全命令,无白名单,要求只读关)
|
||||||
IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关)
|
IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关)
|
||||||
IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关)
|
IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关)
|
||||||
|
IDC_MCP_MANAGE = 1010, // 「启用管理操作」复选框(kill/窗口/服务/电源,要求只读关)
|
||||||
};
|
};
|
||||||
|
|
||||||
CButton m_btnEnable;
|
CButton m_btnEnable;
|
||||||
@@ -40,6 +41,7 @@ private:
|
|||||||
CButton m_btnTerminal;
|
CButton m_btnTerminal;
|
||||||
CButton m_btnRemoteControl;
|
CButton m_btnRemoteControl;
|
||||||
CButton m_btnFileTransfer;
|
CButton m_btnFileTransfer;
|
||||||
|
CButton m_btnManage;
|
||||||
CStatic m_lblWhitelist;
|
CStatic m_lblWhitelist;
|
||||||
CEdit m_editWhitelist;
|
CEdit m_editWhitelist;
|
||||||
CButton m_btnOK, m_btnCancel;
|
CButton m_btnOK, m_btnCancel;
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
// QualityLevelsDlg.h
|
||||||
|
// 远程桌面质量等级配置对话框:编辑 6 档 QualityProfile(帧率/宽度/算法/码率)。
|
||||||
|
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::SaveQualityProfilesToCfg() 负责。
|
||||||
|
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现,勿多处包含)。
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LangManager.h"
|
||||||
|
|
||||||
|
class CQualityLevelsDlg : public CDialog
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum { IDD = IDD_QUALITY_LEVELS };
|
||||||
|
|
||||||
|
// profiles: 指向 6 元素 QualityProfile 数组,确认后写回
|
||||||
|
CQualityLevelsDlg(QualityProfile* profiles, CWnd* pParent = NULL)
|
||||||
|
: CDialog(CQualityLevelsDlg::IDD, pParent), m_profiles(profiles)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QualityProfile* m_profiles;
|
||||||
|
QualityProfile m_working[QUALITY_COUNT];
|
||||||
|
|
||||||
|
static const char* AlgorithmName(int algo)
|
||||||
|
{
|
||||||
|
switch (algo) {
|
||||||
|
case ALGORITHM_GRAY: return "GRAY";
|
||||||
|
case ALGORITHM_DIFF: return "DIFF";
|
||||||
|
case ALGORITHM_H264: return "H264";
|
||||||
|
case ALGORITHM_RGB565: return "RGB565";
|
||||||
|
default: return "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* LevelName(int i)
|
||||||
|
{
|
||||||
|
static const char* names[QUALITY_COUNT] = {
|
||||||
|
"Ultra", "High", "Good", "Medium", "Low", "Minimal"
|
||||||
|
};
|
||||||
|
return (i >= 0 && i < QUALITY_COUNT) ? names[i] : "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
void FillAlgoCombo(int i)
|
||||||
|
{
|
||||||
|
CComboBox* cb = (CComboBox*)GetDlgItem(IDC_QLEVEL_ALGO + i);
|
||||||
|
if (!cb) return;
|
||||||
|
for (int a = 0; a < 4; a++) {
|
||||||
|
cb->AddString(CString(AlgorithmName(a)));
|
||||||
|
}
|
||||||
|
cb->SetCurSel(m_working[i].algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadToControls()
|
||||||
|
{
|
||||||
|
CString s;
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
s.Format(_T("%d"), m_working[i].maxFPS);
|
||||||
|
SetDlgItemText(IDC_QLEVEL_FPS + i, s);
|
||||||
|
s.Format(_T("%d"), m_working[i].maxWidth);
|
||||||
|
SetDlgItemText(IDC_QLEVEL_WIDTH + i, s);
|
||||||
|
s.Format(_T("%d"), m_working[i].bitRate);
|
||||||
|
SetDlgItemText(IDC_QLEVEL_BITRATE + i, s);
|
||||||
|
FillAlgoCombo(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadInt(int id, int& out)
|
||||||
|
{
|
||||||
|
CString s;
|
||||||
|
GetDlgItemText(id, s);
|
||||||
|
s.Trim();
|
||||||
|
if (s.IsEmpty()) return false;
|
||||||
|
|
||||||
|
int v = 0;
|
||||||
|
for (int k = 0; k < s.GetLength(); k++) {
|
||||||
|
TCHAR ch = s[k];
|
||||||
|
if (ch < _T('0') || ch > _T('9')) return false;
|
||||||
|
v = v * 10 + (ch - _T('0'));
|
||||||
|
}
|
||||||
|
out = v;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadFromControls(CString& err)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
int fps, w, br;
|
||||||
|
if (!ReadInt(IDC_QLEVEL_FPS + i, fps))
|
||||||
|
{ err.Format(_T("等级 %s 的帧率不是有效数字"), LevelName(i)); return false; }
|
||||||
|
if (!ReadInt(IDC_QLEVEL_WIDTH + i, w))
|
||||||
|
{ err.Format(_T("等级 %s 的最大宽度不是有效数字"), LevelName(i)); return false; }
|
||||||
|
if (!ReadInt(IDC_QLEVEL_BITRATE + i, br))
|
||||||
|
{ err.Format(_T("等级 %s 的码率不是有效数字"), LevelName(i)); return false; }
|
||||||
|
|
||||||
|
if (fps < 1 || fps > 60)
|
||||||
|
{ err.Format(_T("等级 %s 的帧率需在 1~60 之间"), LevelName(i)); return false; }
|
||||||
|
if (w < 0 || w > 8192)
|
||||||
|
{ err.Format(_T("等级 %s 的最大宽度需在 0~8192 之间(0=原始分辨率)"), LevelName(i)); return false; }
|
||||||
|
if (br < 0 || br > 50000)
|
||||||
|
{ err.Format(_T("等级 %s 的码率需在 0~50000 之间"), LevelName(i)); return false; }
|
||||||
|
|
||||||
|
CComboBox* cb = (CComboBox*)GetDlgItem(IDC_QLEVEL_ALGO + i);
|
||||||
|
int algo = cb ? cb->GetCurSel() : -1;
|
||||||
|
if (algo < 0 || algo > 3)
|
||||||
|
{ err.Format(_T("等级 %s 的编码算法未选择"), LevelName(i)); return false; }
|
||||||
|
|
||||||
|
m_working[i].maxFPS = fps;
|
||||||
|
m_working[i].maxWidth = w;
|
||||||
|
m_working[i].bitRate = br;
|
||||||
|
m_working[i].algorithm = algo;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetToDefaults()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
m_working[i] = GetQualityProfile(i);
|
||||||
|
}
|
||||||
|
LoadToControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual BOOL OnInitDialog() override
|
||||||
|
{
|
||||||
|
CDialog::OnInitDialog();
|
||||||
|
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
m_working[i] = m_profiles[i];
|
||||||
|
}
|
||||||
|
LoadToControls();
|
||||||
|
TranslateDialog(this);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void OnOK() override
|
||||||
|
{
|
||||||
|
CString err;
|
||||||
|
if (!ReadFromControls(err)) {
|
||||||
|
MessageBox(err, _TR("提示"), MB_ICONWARNING);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
m_profiles[i] = m_working[i];
|
||||||
|
}
|
||||||
|
CDialog::OnOK();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
|
||||||
|
{
|
||||||
|
if (LOWORD(wParam) == IDC_QLEVEL_RESET && HIWORD(wParam) == BN_CLICKED) {
|
||||||
|
ResetToDefaults();
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
return CDialog::OnCommand(wParam, lParam);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -889,13 +889,17 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
|||||||
// 启动传输速率更新定时器 (1秒)
|
// 启动传输速率更新定时器 (1秒)
|
||||||
SetTimer(4, 1000, NULL);
|
SetTimer(4, 1000, NULL);
|
||||||
|
|
||||||
// 下发质量配置表(让客户端可以持久化保存)
|
// 下发质量配置表(让客户端可以持久化保存)。
|
||||||
|
// 优先使用主控端在「高级 → 远程桌面质量等级」里自定义的档位;无主控端时回落内置默认表。
|
||||||
{
|
{
|
||||||
BYTE profileCmd[1 + sizeof(QualityProfile) * QUALITY_COUNT];
|
BYTE profileCmd[1 + sizeof(QualityProfile) * QUALITY_COUNT];
|
||||||
profileCmd[0] = CMD_QUALITY_PROFILES;
|
profileCmd[0] = CMD_QUALITY_PROFILES;
|
||||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||||
|
const QualityProfile& prof = (m_pParent != nullptr)
|
||||||
|
? m_pParent->GetQualityProfileCfg(i)
|
||||||
|
: GetQualityProfile(i);
|
||||||
memcpy(profileCmd + 1 + i * sizeof(QualityProfile),
|
memcpy(profileCmd + 1 + i * sizeof(QualityProfile),
|
||||||
&GetQualityProfile(i), sizeof(QualityProfile));
|
&prof, sizeof(QualityProfile));
|
||||||
}
|
}
|
||||||
m_ContextObject->Send2Client(profileCmd, sizeof(profileCmd));
|
m_ContextObject->Send2Client(profileCmd, sizeof(profileCmd));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,12 @@ protected:
|
|||||||
m_bShouldUnmask = TRUE;
|
m_bShouldUnmask = TRUE;
|
||||||
std::string clientIP = getXForwardedFor(str);
|
std::string clientIP = getXForwardedFor(str);
|
||||||
if (!clientIP.empty()) peer = clientIP;
|
if (!clientIP.empty()) peer = clientIP;
|
||||||
} else {
|
} else if (m_bShouldUnmask != TRUE) {
|
||||||
|
// 仅当连接尚未被判定为 HTTP(初始 -1,或已确认非 HTTP)时才置为 FALSE。
|
||||||
|
// 否则会误伤 HTTP 连接的分片续包:续包到达时读位置已越过掩码头,
|
||||||
|
// TryUnMask 找不到 "POST "(maskType==None),若在这里把 m_bShouldUnmask
|
||||||
|
// 从 TRUE 改回 FALSE,下一个新包(仍是 HTTP 掩码)就不再解掩码,从而被 Parse
|
||||||
|
// 判为 PARSER_FAILED。这正是「kill 后进程列表确认包到不了 NotifyProc」的根因。
|
||||||
m_bShouldUnmask = FALSE;
|
m_bShouldUnmask = FALSE;
|
||||||
}
|
}
|
||||||
if (nullptr == m_Masker) {
|
if (nullptr == m_Masker) {
|
||||||
@@ -568,6 +573,20 @@ public:
|
|||||||
}
|
}
|
||||||
return FALSE;
|
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 = {})
|
VOID SetClientInfo(const CString(&s)[ONLINELIST_MAX], const std::vector<std::string>& a = {})
|
||||||
{
|
{
|
||||||
for (int i = 0; i < ONLINELIST_MAX; i++) {
|
for (int i = 0; i < ONLINELIST_MAX; i++) {
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// ShareHostDlg.cpp: 分享主机 / 转移主机 专用对话框实现
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "stdafx.h"
|
||||||
|
#include "ShareHostDlg.h"
|
||||||
|
#include "2015Remote.h"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
CShareHostDlg::CShareHostDlg(CWnd* pParent /*=nullptr*/, BOOL bNeedDays /*=FALSE*/)
|
||||||
|
: CDialogLangEx(IDD_DIALOG_SHARE_HOST, pParent)
|
||||||
|
, m_bNeedDays(bNeedDays)
|
||||||
|
, m_bShine(TRUE)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
CShareHostDlg::~CShareHostDlg()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void CShareHostDlg::Init(LPCTSTR caption, LPCTSTR prompt)
|
||||||
|
{
|
||||||
|
m_sCaption = caption;
|
||||||
|
m_sPrompt = prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CShareHostDlg::InitDays(LPCTSTR name, LPCTSTR defaultValue)
|
||||||
|
{
|
||||||
|
m_sDaysName = name;
|
||||||
|
m_sDays = defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CShareHostDlg::SetHistoryKey(LPCTSTR historyKey)
|
||||||
|
{
|
||||||
|
m_sHistoryKey = historyKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CShareHostDlg::DoDataExchange(CDataExchange* pDX)
|
||||||
|
{
|
||||||
|
CDialogLangEx::DoDataExchange(pDX);
|
||||||
|
DDX_Control(pDX, IDC_EDIT_SHARE_ADDRESS, m_ComboInput);
|
||||||
|
DDX_Control(pDX, IDC_STATIC_SHARE_DAYS, m_StaticDays);
|
||||||
|
DDX_Control(pDX, IDC_EDIT_SHARE_DAYS, m_EditDays);
|
||||||
|
DDX_Control(pDX, IDC_CHECK_USE_SHINE, m_CheckShine);
|
||||||
|
}
|
||||||
|
|
||||||
|
BEGIN_MESSAGE_MAP(CShareHostDlg, CDialogLangEx)
|
||||||
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
|
BOOL CShareHostDlg::OnInitDialog()
|
||||||
|
{
|
||||||
|
CDialogLangEx::OnInitDialog();
|
||||||
|
|
||||||
|
SetWindowText(m_sCaption);
|
||||||
|
SetDlgItemText(IDC_STATIC_SHARE_ADDRESS, m_sPrompt);
|
||||||
|
SetDlgItemText(IDC_CHECK_USE_SHINE, _TR("使用 Shine 协议(兼容 Go 服务端)"));
|
||||||
|
SetDlgItemText(IDOK, _TR("确定"));
|
||||||
|
SetDlgItemText(IDCANCEL, _TR("取消"));
|
||||||
|
|
||||||
|
// 天数(仅转移主机显示)
|
||||||
|
m_StaticDays.SetWindowTextA(m_sDaysName);
|
||||||
|
m_StaticDays.ShowWindow(m_bNeedDays ? SW_SHOW : SW_HIDE);
|
||||||
|
m_EditDays.SetWindowTextA(m_sDays);
|
||||||
|
m_EditDays.ShowWindow(m_bNeedDays ? SW_SHOW : SW_HIDE);
|
||||||
|
|
||||||
|
// 默认勾选 Shine
|
||||||
|
m_CheckShine.SetCheck(m_bShine ? BST_CHECKED : BST_UNCHECKED);
|
||||||
|
|
||||||
|
LoadHistory();
|
||||||
|
m_ComboInput.SetWindowText(m_str);
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CShareHostDlg::OnOK()
|
||||||
|
{
|
||||||
|
m_ComboInput.GetWindowText(m_str);
|
||||||
|
m_bShine = (m_CheckShine.GetCheck() == BST_CHECKED);
|
||||||
|
if (m_bNeedDays)
|
||||||
|
m_EditDays.GetWindowText(m_sDays);
|
||||||
|
|
||||||
|
SaveHistory();
|
||||||
|
CDialogLangEx::OnOK();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CShareHostDlg::LoadHistory()
|
||||||
|
{
|
||||||
|
if (m_sHistoryKey.IsEmpty()) return;
|
||||||
|
|
||||||
|
std::string history = THIS_CFG.GetStr("history", m_sHistoryKey.GetString());
|
||||||
|
if (history.empty()) return;
|
||||||
|
|
||||||
|
auto items = StringToVector(history, '|');
|
||||||
|
for (size_t i = 0; i < items.size() && i < 16; i++) {
|
||||||
|
if (!items[i].empty()) {
|
||||||
|
m_ComboInput.AddString(items[i].c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CShareHostDlg::SaveHistory()
|
||||||
|
{
|
||||||
|
if (m_sHistoryKey.IsEmpty() || m_str.IsEmpty()) return;
|
||||||
|
|
||||||
|
std::string newValue = m_str.GetString();
|
||||||
|
if (newValue.find('|') != std::string::npos) return;
|
||||||
|
|
||||||
|
std::string history = THIS_CFG.GetStr("history", m_sHistoryKey.GetString());
|
||||||
|
auto items = StringToVector(history, '|');
|
||||||
|
|
||||||
|
items.erase(std::remove(items.begin(), items.end(), newValue), items.end());
|
||||||
|
items.erase(std::remove(items.begin(), items.end(), std::string("")), items.end());
|
||||||
|
|
||||||
|
items.insert(items.begin(), newValue);
|
||||||
|
|
||||||
|
if (items.size() > 16) {
|
||||||
|
items.resize(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string result;
|
||||||
|
for (size_t i = 0; i < items.size(); i++) {
|
||||||
|
if (i > 0) result += '|';
|
||||||
|
result += items[i];
|
||||||
|
}
|
||||||
|
THIS_CFG.SetStr("history", m_sHistoryKey.GetString(), result);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "resource.h"
|
||||||
|
#include "LangManager.h"
|
||||||
|
|
||||||
|
// 分享主机 / 转移主机 专用对话框(带 Shine/HELL 协议勾选)
|
||||||
|
|
||||||
|
class CShareHostDlg : public CDialogLangEx
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CShareHostDlg(CWnd* pParent = nullptr, BOOL bNeedDays = FALSE);
|
||||||
|
virtual ~CShareHostDlg();
|
||||||
|
|
||||||
|
enum { IDD = IDD_DIALOG_SHARE_HOST };
|
||||||
|
|
||||||
|
void Init(LPCTSTR caption, LPCTSTR prompt);
|
||||||
|
void InitDays(LPCTSTR name, LPCTSTR defaultValue); // 仅转移主机显示
|
||||||
|
void SetHistoryKey(LPCTSTR historyKey);
|
||||||
|
|
||||||
|
// 结果
|
||||||
|
CString m_str; // 地址 IP:PORT
|
||||||
|
CString m_sDays; // 天数(转移主机)
|
||||||
|
BOOL m_bShine; // TRUE=Shine(0), FALSE=HELL(1)
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void DoDataExchange(CDataExchange* pDX);
|
||||||
|
virtual BOOL OnInitDialog();
|
||||||
|
virtual void OnOK();
|
||||||
|
|
||||||
|
DECLARE_MESSAGE_MAP()
|
||||||
|
|
||||||
|
CString m_sCaption;
|
||||||
|
CString m_sPrompt;
|
||||||
|
CString m_sDaysName;
|
||||||
|
CString m_sHistoryKey;
|
||||||
|
BOOL m_bNeedDays;
|
||||||
|
|
||||||
|
CComboBox m_ComboInput;
|
||||||
|
CStatic m_StaticDays;
|
||||||
|
CEdit m_EditDays;
|
||||||
|
CButton m_CheckShine;
|
||||||
|
|
||||||
|
void LoadHistory();
|
||||||
|
void SaveHistory();
|
||||||
|
};
|
||||||
@@ -39,6 +39,8 @@ BEGIN_MESSAGE_MAP(CToolbarDlg, CDialogEx)
|
|||||||
ON_BN_CLICKED(IDC_BTN_Y, &CToolbarDlg::OnBnClickedY)
|
ON_BN_CLICKED(IDC_BTN_Y, &CToolbarDlg::OnBnClickedY)
|
||||||
ON_BN_CLICKED(IDC_BTN_Z, &CToolbarDlg::OnBnClickedZ)
|
ON_BN_CLICKED(IDC_BTN_Z, &CToolbarDlg::OnBnClickedZ)
|
||||||
ON_BN_CLICKED(IDC_BTN_SCREENSHOT, &CToolbarDlg::OnBnClickedScreenshot)
|
ON_BN_CLICKED(IDC_BTN_SCREENSHOT, &CToolbarDlg::OnBnClickedScreenshot)
|
||||||
|
ON_BN_CLICKED(IDC_BTN_GET_CLIPBOARD, &CToolbarDlg::OnBnClickedGetClipboard)
|
||||||
|
ON_BN_CLICKED(IDC_BTN_SET_CLIPBOARD, &CToolbarDlg::OnBnClickedSetClipboard)
|
||||||
ON_WM_ERASEBKGND()
|
ON_WM_ERASEBKGND()
|
||||||
END_MESSAGE_MAP()
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
@@ -88,7 +90,7 @@ void CToolbarDlg::CheckMousePosition()
|
|||||||
int monWidth = monRight - monLeft;
|
int monWidth = monRight - monLeft;
|
||||||
int monHeight = monBottom - monTop;
|
int monHeight = monBottom - monTop;
|
||||||
|
|
||||||
int btnSize = 32, btnSpacing = 8, btnCount = 16;
|
int btnSize = 32, btnSpacing = 8, btnCount = 18;
|
||||||
|
|
||||||
if (m_nPosition <= 1) {
|
if (m_nPosition <= 1) {
|
||||||
// 水平模式 (上/下)
|
// 水平模式 (上/下)
|
||||||
@@ -153,9 +155,9 @@ void CToolbarDlg::SlideIn()
|
|||||||
int monWidth = monRight - monLeft;
|
int monWidth = monRight - monLeft;
|
||||||
int monHeight = monBottom - monTop;
|
int monHeight = monBottom - monTop;
|
||||||
|
|
||||||
int hw = 640; // 水平工具栏宽度 (与垂直高度一致)
|
int hw = 720; // 水平工具栏宽度 (与垂直高度一致)
|
||||||
int vw = 40;
|
int vw = 40;
|
||||||
int vh = 640;
|
int vh = 720;
|
||||||
|
|
||||||
// 从边缘展开(改变窗口大小),避免多显示器时跑到相邻屏幕
|
// 从边缘展开(改变窗口大小),避免多显示器时跑到相邻屏幕
|
||||||
switch (m_nPosition) {
|
switch (m_nPosition) {
|
||||||
@@ -212,9 +214,9 @@ void CToolbarDlg::SlideOut()
|
|||||||
int monWidth = monRight - monLeft;
|
int monWidth = monRight - monLeft;
|
||||||
int monHeight = monBottom - monTop;
|
int monHeight = monBottom - monTop;
|
||||||
|
|
||||||
int hw = 640;
|
int hw = 720;
|
||||||
int vw = 40;
|
int vw = 40;
|
||||||
int vh = 640;
|
int vh = 720;
|
||||||
|
|
||||||
CWnd* btns[] = {
|
CWnd* btns[] = {
|
||||||
&m_btnExit, &m_btnControl,
|
&m_btnExit, &m_btnControl,
|
||||||
@@ -224,9 +226,10 @@ void CToolbarDlg::SlideOut()
|
|||||||
&m_btnQuality, &m_btnRestoreConsole,
|
&m_btnQuality, &m_btnRestoreConsole,
|
||||||
&m_btnX, &m_btnY, &m_btnZ,
|
&m_btnX, &m_btnY, &m_btnZ,
|
||||||
&m_btnScreenshot,
|
&m_btnScreenshot,
|
||||||
|
&m_btnGetClipboard, &m_btnSetClipboard,
|
||||||
&m_btnMinimize, &m_btnClose,
|
&m_btnMinimize, &m_btnClose,
|
||||||
};
|
};
|
||||||
const int N = 16;
|
const int N = 18;
|
||||||
CRect btnRects[N];
|
CRect btnRects[N];
|
||||||
for (int i = 0; i < N; i++) {
|
for (int i = 0; i < N; i++) {
|
||||||
btns[i]->GetWindowRect(&btnRects[i]);
|
btns[i]->GetWindowRect(&btnRects[i]);
|
||||||
@@ -331,6 +334,8 @@ BOOL CToolbarDlg::OnInitDialog()
|
|||||||
m_btnY.SubclassDlgItem(IDC_BTN_Y, this);
|
m_btnY.SubclassDlgItem(IDC_BTN_Y, this);
|
||||||
m_btnZ.SubclassDlgItem(IDC_BTN_Z, this);
|
m_btnZ.SubclassDlgItem(IDC_BTN_Z, this);
|
||||||
m_btnScreenshot.SubclassDlgItem(IDC_BTN_SCREENSHOT, this);
|
m_btnScreenshot.SubclassDlgItem(IDC_BTN_SCREENSHOT, this);
|
||||||
|
m_btnGetClipboard.SubclassDlgItem(IDC_BTN_GET_CLIPBOARD, this);
|
||||||
|
m_btnSetClipboard.SubclassDlgItem(IDC_BTN_SET_CLIPBOARD, this);
|
||||||
m_btnMinimize.SubclassDlgItem(IDC_BTN_MINIMIZE, this);
|
m_btnMinimize.SubclassDlgItem(IDC_BTN_MINIMIZE, this);
|
||||||
m_btnClose.SubclassDlgItem(IDC_BTN_CLOSE, this);
|
m_btnClose.SubclassDlgItem(IDC_BTN_CLOSE, this);
|
||||||
|
|
||||||
@@ -343,6 +348,8 @@ BOOL CToolbarDlg::OnInitDialog()
|
|||||||
// Y按钮图标在 UpdateButtonIcons 中根据音频状态动态设置
|
// Y按钮图标在 UpdateButtonIcons 中根据音频状态动态设置
|
||||||
m_btnZ.SetIconDrawFunc(CIconButton::DrawIconLetterZ); // 预留按钮 Z
|
m_btnZ.SetIconDrawFunc(CIconButton::DrawIconLetterZ); // 预留按钮 Z
|
||||||
m_btnScreenshot.SetIconDrawFunc(CIconButton::DrawIconScreenshot);
|
m_btnScreenshot.SetIconDrawFunc(CIconButton::DrawIconScreenshot);
|
||||||
|
m_btnGetClipboard.SetIconDrawFunc(CIconButton::DrawIconLetterC);
|
||||||
|
m_btnSetClipboard.SetIconDrawFunc(CIconButton::DrawIconLetterP);
|
||||||
m_btnMinimize.SetIconDrawFunc(CIconButton::DrawIconMinimize);
|
m_btnMinimize.SetIconDrawFunc(CIconButton::DrawIconMinimize);
|
||||||
m_btnClose.SetIconDrawFunc(CIconButton::DrawIconClose);
|
m_btnClose.SetIconDrawFunc(CIconButton::DrawIconClose);
|
||||||
m_btnClose.SetIsCloseButton(true);
|
m_btnClose.SetIsCloseButton(true);
|
||||||
@@ -367,6 +374,8 @@ BOOL CToolbarDlg::OnInitDialog()
|
|||||||
m_tooltip.AddTool(&m_btnY, pParent->m_Settings.AudioEnabled ? _TR("关闭系统音频") : _TR("打开系统音频"));
|
m_tooltip.AddTool(&m_btnY, pParent->m_Settings.AudioEnabled ? _TR("关闭系统音频") : _TR("打开系统音频"));
|
||||||
m_tooltip.AddTool(&m_btnZ, _TR("选择ROI"));
|
m_tooltip.AddTool(&m_btnZ, _TR("选择ROI"));
|
||||||
m_tooltip.AddTool(&m_btnScreenshot, _TR("截图"));
|
m_tooltip.AddTool(&m_btnScreenshot, _TR("截图"));
|
||||||
|
m_tooltip.AddTool(&m_btnGetClipboard, _TR("获取剪贴板"));
|
||||||
|
m_tooltip.AddTool(&m_btnSetClipboard, _TR("设置剪贴板"));
|
||||||
m_tooltip.AddTool(&m_btnMinimize, _TR("最小化"));
|
m_tooltip.AddTool(&m_btnMinimize, _TR("最小化"));
|
||||||
m_tooltip.AddTool(&m_btnClose, _TR("关闭"));
|
m_tooltip.AddTool(&m_btnClose, _TR("关闭"));
|
||||||
|
|
||||||
@@ -383,9 +392,9 @@ BOOL CToolbarDlg::OnInitDialog()
|
|||||||
int monWidth = rcMonitor.right - rcMonitor.left;
|
int monWidth = rcMonitor.right - rcMonitor.left;
|
||||||
int monHeight = rcMonitor.bottom - rcMonitor.top;
|
int monHeight = rcMonitor.bottom - rcMonitor.top;
|
||||||
|
|
||||||
int hw = 640;
|
int hw = 720;
|
||||||
int vw = 40;
|
int vw = 40;
|
||||||
int vh = 640;
|
int vh = 720;
|
||||||
int hx = rcMonitor.left + (monWidth - hw) / 2;
|
int hx = rcMonitor.left + (monWidth - hw) / 2;
|
||||||
|
|
||||||
switch (m_nPosition) {
|
switch (m_nPosition) {
|
||||||
@@ -514,7 +523,7 @@ void CToolbarDlg::LayoutButtons()
|
|||||||
{
|
{
|
||||||
int btnSize = 32;
|
int btnSize = 32;
|
||||||
int btnSpacing = 8;
|
int btnSpacing = 8;
|
||||||
int btnCount = 16;
|
int btnCount = 18;
|
||||||
|
|
||||||
CWnd* btns[] = {
|
CWnd* btns[] = {
|
||||||
&m_btnExit,
|
&m_btnExit,
|
||||||
@@ -531,6 +540,8 @@ void CToolbarDlg::LayoutButtons()
|
|||||||
&m_btnY, // 预留按钮 Y
|
&m_btnY, // 预留按钮 Y
|
||||||
&m_btnZ, // 预留按钮 Z
|
&m_btnZ, // 预留按钮 Z
|
||||||
&m_btnScreenshot,
|
&m_btnScreenshot,
|
||||||
|
&m_btnGetClipboard,
|
||||||
|
&m_btnSetClipboard,
|
||||||
&m_btnMinimize,
|
&m_btnMinimize,
|
||||||
&m_btnClose,
|
&m_btnClose,
|
||||||
};
|
};
|
||||||
@@ -588,9 +599,9 @@ void CToolbarDlg::UpdatePosition()
|
|||||||
int monWidth = rcMonitor.right - rcMonitor.left;
|
int monWidth = rcMonitor.right - rcMonitor.left;
|
||||||
int monHeight = rcMonitor.bottom - rcMonitor.top;
|
int monHeight = rcMonitor.bottom - rcMonitor.top;
|
||||||
|
|
||||||
int hw = 640;
|
int hw = 720;
|
||||||
int vw = 40;
|
int vw = 40;
|
||||||
int vh = 640;
|
int vh = 720;
|
||||||
int hx = rcMonitor.left + (monWidth - hw) / 2;
|
int hx = rcMonitor.left + (monWidth - hw) / 2;
|
||||||
|
|
||||||
switch (m_nPosition) {
|
switch (m_nPosition) {
|
||||||
@@ -785,6 +796,18 @@ void CToolbarDlg::OnBnClickedScreenshot()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CToolbarDlg::OnBnClickedGetClipboard()
|
||||||
|
{
|
||||||
|
// 与系统菜单 IDM_GET_CLIPBOARD 相同的处理逻辑
|
||||||
|
GetParent()->PostMessage(WM_SYSCOMMAND, IDM_GET_CLIPBOARD, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CToolbarDlg::OnBnClickedSetClipboard()
|
||||||
|
{
|
||||||
|
// 与系统菜单 IDM_SET_CLIPBOARD 相同的处理逻辑
|
||||||
|
GetParent()->PostMessage(WM_SYSCOMMAND, IDM_SET_CLIPBOARD, 0);
|
||||||
|
}
|
||||||
|
|
||||||
BOOL CToolbarDlg::PreTranslateMessage(MSG* pMsg)
|
BOOL CToolbarDlg::PreTranslateMessage(MSG* pMsg)
|
||||||
{
|
{
|
||||||
if (m_tooltip.GetSafeHwnd()) {
|
if (m_tooltip.GetSafeHwnd()) {
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ public:
|
|||||||
CIconButton m_btnY; // 预留按钮 Y
|
CIconButton m_btnY; // 预留按钮 Y
|
||||||
CIconButton m_btnZ; // 预留按钮 Z
|
CIconButton m_btnZ; // 预留按钮 Z
|
||||||
CIconButton m_btnScreenshot;
|
CIconButton m_btnScreenshot;
|
||||||
|
CIconButton m_btnGetClipboard; // 获取剪贴板
|
||||||
|
CIconButton m_btnSetClipboard; // 设置剪贴板
|
||||||
CIconButton m_btnMinimize;
|
CIconButton m_btnMinimize;
|
||||||
CIconButton m_btnClose;
|
CIconButton m_btnClose;
|
||||||
|
|
||||||
@@ -80,6 +82,8 @@ public:
|
|||||||
afx_msg void OnBnClickedY(); // 预留按钮 Y 响应函数
|
afx_msg void OnBnClickedY(); // 预留按钮 Y 响应函数
|
||||||
afx_msg void OnBnClickedZ(); // 预留按钮 Z 响应函数
|
afx_msg void OnBnClickedZ(); // 预留按钮 Z 响应函数
|
||||||
afx_msg void OnBnClickedScreenshot();
|
afx_msg void OnBnClickedScreenshot();
|
||||||
|
afx_msg void OnBnClickedGetClipboard();
|
||||||
|
afx_msg void OnBnClickedSetClipboard();
|
||||||
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
||||||
virtual BOOL OnInitDialog();
|
virtual BOOL OnInitDialog();
|
||||||
};
|
};
|
||||||
@@ -35,6 +35,16 @@ static std::string LoadBinaryResourceAsString(int resourceId) {
|
|||||||
return std::string(static_cast<const char*>(p), size);
|
return std::string(static_cast<const char*>(p), size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace all occurrences of `from` with `to` in-place.
|
||||||
|
static void ReplaceAll(std::string& s, const std::string& from, const std::string& to) {
|
||||||
|
if (from.empty()) return;
|
||||||
|
size_t pos = 0;
|
||||||
|
while ((pos = s.find(from, pos)) != std::string::npos) {
|
||||||
|
s.replace(pos, from.length(), to);
|
||||||
|
pos += to.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Challenge-response nonce storage (prevents replay attacks)
|
// Challenge-response nonce storage (prevents replay attacks)
|
||||||
static std::map<void*, std::string> s_ClientNonces;
|
static std::map<void*, std::string> s_ClientNonces;
|
||||||
static std::mutex s_NonceMutex;
|
static std::mutex s_NonceMutex;
|
||||||
@@ -231,6 +241,10 @@ void CWebService::ServerThread(int port) {
|
|||||||
|
|
||||||
// Serve static HTML page and file downloads
|
// Serve static HTML page and file downloads
|
||||||
static std::string cachedHtml = GetWebPageHTML();
|
static std::string cachedHtml = GetWebPageHTML();
|
||||||
|
// server/web/index.html carries a __BASE_PATH__ placeholder for the Go
|
||||||
|
// server's subpath deployment. The MFC server serves from the domain root,
|
||||||
|
// so strip it (→ "") — otherwise the WebSocket/CSS/JS URLs break.
|
||||||
|
ReplaceAll(cachedHtml, "__BASE_PATH__", "");
|
||||||
// Log loaded size; zero bytes means the RC BINARY resource is missing or
|
// Log loaded size; zero bytes means the RC BINARY resource is missing or
|
||||||
// server/web/index.html was not embedded — check 2015Remote.rc and rebuild.
|
// server/web/index.html was not embedded — check 2015Remote.rc and rebuild.
|
||||||
Mprintf("[WebService] index.html loaded: %zu bytes\n", cachedHtml.size());
|
Mprintf("[WebService] index.html loaded: %zu bytes\n", cachedHtml.size());
|
||||||
|
|||||||
@@ -55,6 +55,16 @@ public:
|
|||||||
}
|
}
|
||||||
virtual void SetGroupName(const std::string& group) {}
|
virtual void SetGroupName(const std::string& group) {}
|
||||||
|
|
||||||
|
// 等待本连接待发送缓冲排空(用于「发写命令 → 补发刷新命令」的背靠背发送:
|
||||||
|
// 两次 Send2Client 会对同一发送缓冲并发 WSASend,造成竞态/丢包)。非 IOCP
|
||||||
|
// 服务端无异步发送缓冲,默认立即返回 true;CONTEXT_OBJECT 覆写为轮询发送缓冲
|
||||||
|
// 归零直到排空或超时。
|
||||||
|
virtual bool WaitSendIdle(int timeoutMs)
|
||||||
|
{
|
||||||
|
(void)timeoutMs;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查客户端是否支持 V2 文件传输
|
// 检查客户端是否支持 V2 文件传输
|
||||||
bool SupportsFileV2() const {
|
bool SupportsFileV2() const {
|
||||||
CString caps = GetClientData(ONLINELIST_CAPABILITIES);
|
CString caps = GetClientData(ONLINELIST_CAPABILITIES);
|
||||||
|
|||||||
@@ -644,22 +644,22 @@ void CFileManagerDlg::OnReceiveComplete()
|
|||||||
break;
|
break;
|
||||||
case TOKEN_FILE_SEARCHPLUS_NONTFS: {
|
case TOKEN_FILE_SEARCHPLUS_NONTFS: {
|
||||||
DWORD_PTR dwResult;
|
DWORD_PTR dwResult;
|
||||||
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,(LPARAM)_T(" 驱动盘非NTFS格式"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,(LPARAM)_TRF(" 驱动盘非NTFS格式"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TOKEN_FILE_SEARCHPLUS_HANDLE: {
|
case TOKEN_FILE_SEARCHPLUS_HANDLE: {
|
||||||
DWORD_PTR dwResult;
|
DWORD_PTR dwResult;
|
||||||
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, (LPARAM)_T("需要管理员权限运行"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, (LPARAM)_TRF("需要管理员权限运行"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TOKEN_FILE_SEARCHPLUS_INITUSN: {
|
case TOKEN_FILE_SEARCHPLUS_INITUSN: {
|
||||||
DWORD_PTR dwResult;
|
DWORD_PTR dwResult;
|
||||||
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, (LPARAM)_T("初始化日志文件失败"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, (LPARAM)_TRF("初始化日志文件失败"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TOKEN_FILE_SEARCHPLUS_GETUSN: {
|
case TOKEN_FILE_SEARCHPLUS_GETUSN: {
|
||||||
DWORD_PTR dwResult;
|
DWORD_PTR dwResult;
|
||||||
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, (LPARAM)_T("获取日志文件失败"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2, (LPARAM)_TRF("获取日志文件失败"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TOKEN_FILE_SEARCHPLUS_NUMBER: {
|
case TOKEN_FILE_SEARCHPLUS_NUMBER: {
|
||||||
@@ -764,13 +764,13 @@ void CFileManagerDlg::OnDblclkListRemotedriver(NMHDR* pNMHDR, LRESULT* pResult)
|
|||||||
|
|
||||||
int nItem = m_list_remote_driver.GetSelectionMark();
|
int nItem = m_list_remote_driver.GetSelectionMark();
|
||||||
CString directory = m_list_remote_driver.GetItemText(nItem, 0);
|
CString directory = m_list_remote_driver.GetItemText(nItem, 0);
|
||||||
if (directory.Compare(_T("桌面")) == 0) {
|
if (directory.Compare(_L(_T("桌面"))) == 0) {
|
||||||
m_Remote_Directory_ComboBox.SetWindowText(m_strDesktopPath);
|
m_Remote_Directory_ComboBox.SetWindowText(m_strDesktopPath);
|
||||||
m_list_remote.SetSelectionMark(-1);
|
m_list_remote.SetSelectionMark(-1);
|
||||||
GetRemoteFileList();
|
GetRemoteFileList();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (directory.Compare(_T("最近")) == 0) {
|
if (directory.Compare(_L(_T("最近"))) == 0) {
|
||||||
BYTE byToken = COMMAND_FILE_RECENT;
|
BYTE byToken = COMMAND_FILE_RECENT;
|
||||||
m_ContextObject->Send2Client(&byToken, 1);
|
m_ContextObject->Send2Client(&byToken, 1);
|
||||||
return;
|
return;
|
||||||
@@ -1482,10 +1482,10 @@ void CFileManagerDlg::EndLocalRecvFile()
|
|||||||
DWORD_PTR dwResult;
|
DWORD_PTR dwResult;
|
||||||
if (m_bIsStop) {
|
if (m_bIsStop) {
|
||||||
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,
|
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,
|
||||||
(LPARAM)_T(" 取消下载"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
(LPARAM)_TRF(" 取消下载"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
||||||
} else {
|
} else {
|
||||||
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,
|
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,
|
||||||
(LPARAM)_T(" 全部完成"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
(LPARAM)_TRF(" 全部完成"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
||||||
}
|
}
|
||||||
m_bIsStop = false;
|
m_bIsStop = false;
|
||||||
} else {
|
} else {
|
||||||
@@ -1516,10 +1516,10 @@ void CFileManagerDlg::EndLocalUploadFile()
|
|||||||
DWORD_PTR dwResult;
|
DWORD_PTR dwResult;
|
||||||
if (m_bIsStop) {
|
if (m_bIsStop) {
|
||||||
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,
|
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,
|
||||||
(LPARAM)_T(" 取消上传"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
(LPARAM)_TRF(" 取消上传"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
||||||
} else {
|
} else {
|
||||||
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,
|
SendMessageTimeout(m_wndStatusBar.GetSafeHwnd(), SB_SETTEXT, 2,
|
||||||
(LPARAM)_T(" 全部完成"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
(LPARAM)_TRF(" 全部完成"), SMTO_ABORTIFHUNG | SMTO_BLOCK, 500, &dwResult);
|
||||||
}
|
}
|
||||||
m_bIsStop = false;
|
m_bIsStop = false;
|
||||||
DRIVE_CAZ = TRUE;
|
DRIVE_CAZ = TRUE;
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -231,6 +231,8 @@ RTT=RTT
|
|||||||
放下面=Send Back
|
放下面=Send Back
|
||||||
透明度=Opacity
|
透明度=Opacity
|
||||||
截图=Capture
|
截图=Capture
|
||||||
|
获取剪贴板=Get Clipboard
|
||||||
|
设置剪贴板=Set Clipboard
|
||||||
|
|
||||||
; ============================================
|
; ============================================
|
||||||
; Settings Dialog
|
; Settings Dialog
|
||||||
@@ -388,6 +390,7 @@ Rar
|
|||||||
客户管理=Client Mgmt
|
客户管理=Client Mgmt
|
||||||
转移主机=Transfer Host
|
转移主机=Transfer Host
|
||||||
分享主机=Share Host
|
分享主机=Share Host
|
||||||
|
使用 Shine 协议(兼容 Go 服务端)=Use Shine protocol (Go-server compatible)
|
||||||
取消分享=Cancel Sharing
|
取消分享=Cancel Sharing
|
||||||
确定取消分享主机? 将撤销全部的分享链接。=Cancel sharing clients? This will remove all share links.
|
确定取消分享主机? 将撤销全部的分享链接。=Cancel sharing clients? This will remove all share links.
|
||||||
删除主机=Delete Host
|
删除主机=Delete Host
|
||||||
@@ -1351,7 +1354,7 @@ FUCK!!!
|
|||||||
编辑备注=Edit remarks
|
编辑备注=Edit remarks
|
||||||
自己:=Self:
|
自己:=Self:
|
||||||
视频文件(*.avi)|*.avi|=Video(*.avi)|*.avi|
|
视频文件(*.avi)|*.avi|=Video(*.avi)|*.avi|
|
||||||
解除锁定输入=解除锁定输入
|
解除锁定输入=Unlock Remote Input
|
||||||
计算机已经被用户锁屏,程序无 SYSTEM 权限。\r\n可能无法进行远程桌面控制,是否继续?=Computer locked, no SYSTEM.\r\nRemote may fail, continue?
|
计算机已经被用户锁屏,程序无 SYSTEM 权限。\r\n可能无法进行远程桌面控制,是否继续?=Computer locked, no SYSTEM.\r\nRemote may fail, continue?
|
||||||
请先生成口令!=Generate password first!
|
请先生成口令!=Generate password first!
|
||||||
请将EXE放在\=Put EXE in \
|
请将EXE放在\=Put EXE in \
|
||||||
@@ -1987,15 +1990,63 @@ MCP
|
|||||||
端口需为 1-65535 的数字=Port must be a number between 1 and 65535
|
端口需为 1-65535 的数字=Port must be a number between 1 and 65535
|
||||||
Token 不能为空=Token cannot be empty
|
Token 不能为空=Token cannot be empty
|
||||||
MCP 设置已保存。=MCP settings saved.
|
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)
|
只读模式(禁命令执行)=Read-only mode (disable command execution)
|
||||||
命令白名单=Command whitelist
|
命令白名单=Command whitelist
|
||||||
MCP命令执行=MCP command execution
|
MCP命令执行=MCP command execution
|
||||||
启用持久终端(全命令,无白名单)=Enable persistent terminal (full commands, no whitelist)
|
启用持久终端(全命令,无白名单)=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 persistent terminal
|
||||||
MCP远程控制=MCP remote control
|
MCP远程控制=MCP remote control
|
||||||
MCP文件传输=MCP file transfer
|
MCP文件传输=MCP file transfer
|
||||||
|
MCP管理操作=MCP management operation
|
||||||
Token=Token
|
Token=Token
|
||||||
启用远程控制(AI 操控桌面)=Enable remote control (AI controls the desktop)
|
启用远程控制(AI 操控桌面)=Enable remote control (AI controls the desktop)
|
||||||
启用文件传输(download_file)=Enable file transfer (download_file)
|
启用文件传输(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
|
||||||
|
%d <==发 %d bytes\r\n=%d <== sent %d bytes\r\n
|
||||||
|
%d 返回标示 %d %d %d\r\n=%d Return flag %d %d %d\r\n
|
||||||
|
域名 连接 %d \r\n=Domain connect %d \r\n
|
||||||
|
%d IPV6连接 不支持...=%d IPv6 connection not supported...
|
||||||
|
%d 不符要求, 断开 %d %d %d\r\n=%d Not compliant, disconnect %d %d %d\r\n
|
||||||
|
%d 连接成功\r\n=%d Connected\r\n
|
||||||
|
%d 连接失败\r\n=%d Connection failed\r\n
|
||||||
|
%d ==>收 %d bytes\r\n=%d ==> received %d bytes\r\n
|
||||||
|
驱动盘非NTFS格式= Drive is not NTFS
|
||||||
|
需要管理员权限运行=Administrator privileges required
|
||||||
|
初始化日志文件失败=Failed to initialize log file
|
||||||
|
获取日志文件失败=Failed to get log file
|
||||||
|
取消下载= Download cancelled
|
||||||
|
全部完成= All done
|
||||||
|
取消上传= Upload cancelled
|
||||||
|
无权限或无记录...=No permission or no records...
|
||||||
|
请等待数据返回...=Waiting for data...
|
||||||
|
接收数据完成=Data received
|
||||||
|
打开文件失败...=Failed to open file...
|
||||||
|
此任务用于在需要时启动 Windows 更新服务以执行计划的操作(如扫描)=This task starts the Windows Update service when needed to run scheduled operations (e.g. scanning)
|
||||||
|
本地文件路径=Local file path
|
||||||
|
高级(&A)=&Advanced
|
||||||
|
远程桌面质量等级(&Q)=Remote Desktop &Quality Levels
|
||||||
|
远程桌面质量等级=Remote Desktop Quality Levels
|
||||||
|
等级=Level
|
||||||
|
帧率(FPS)=FPS
|
||||||
|
最大宽度=Max Width
|
||||||
|
编码算法=Codec
|
||||||
|
码率(kbps)=Bitrate (kbps)
|
||||||
|
Ultra 极佳=Ultra
|
||||||
|
High 优秀=High
|
||||||
|
Good 良好=Good
|
||||||
|
Medium 一般=Medium
|
||||||
|
Low 较差=Low
|
||||||
|
Minimal 最低=Minimal
|
||||||
|
最大宽度 0 表示原始分辨率;DIFF/RGB565 适合局域网,H264 适合跨网=Max Width 0 = native resolution; DIFF/RGB565 for LAN, H264 for WAN
|
||||||
|
恢复默认=Restore Defaults
|
||||||
|
质量等级已保存,下次打开远程桌面时生效。=Quality levels saved. They take effect the next time you open Remote Desktop.
|
||||||
|
屏幕预览质量等级(&P)=Screen Preview &Quality Levels
|
||||||
|
屏幕预览质量等级=Screen Preview Quality Levels
|
||||||
|
JPEG质量=JPEG Quality
|
||||||
|
最大宽度为期望编码宽度;JPEG 质量 1~100=Max width is the target encode width; JPEG quality 1~100
|
||||||
|
预览质量等级已保存,下次发送预览时生效。=Preview quality levels saved. They take effect the next time a preview is sent.
|
||||||
@@ -230,6 +230,8 @@ RTT=RTT
|
|||||||
放下面=置於下層
|
放下面=置於下層
|
||||||
透明度=透明度
|
透明度=透明度
|
||||||
截图=擷取螢幕
|
截图=擷取螢幕
|
||||||
|
获取剪贴板=取得剪貼簿
|
||||||
|
设置剪贴板=設定剪貼簿
|
||||||
|
|
||||||
; ============================================
|
; ============================================
|
||||||
; Settings Dialog
|
; Settings Dialog
|
||||||
@@ -388,6 +390,7 @@ Rar
|
|||||||
客户管理=用戶管理
|
客户管理=用戶管理
|
||||||
转移主机=轉移主機
|
转移主机=轉移主機
|
||||||
分享主机=分享主機
|
分享主机=分享主機
|
||||||
|
使用 Shine 协议(兼容 Go 服务端)=使用 Shine 協議(相容 Go 伺服器)
|
||||||
取消分享=取消分享
|
取消分享=取消分享
|
||||||
确定取消分享主机? 将撤销全部的分享链接。=确定取消分享主机? 将撤销全部的分享链接。
|
确定取消分享主机? 将撤销全部的分享链接。=确定取消分享主机? 将撤销全部的分享链接。
|
||||||
删除主机=刪除主機
|
删除主机=刪除主機
|
||||||
@@ -1978,15 +1981,63 @@ MCP
|
|||||||
端口需为 1-65535 的数字=連接埠需為 1-65535 的數字
|
端口需为 1-65535 的数字=連接埠需為 1-65535 的數字
|
||||||
Token 不能为空=Token 不能為空
|
Token 不能为空=Token 不能為空
|
||||||
MCP 设置已保存。=MCP 設定已儲存。
|
MCP 设置已保存。=MCP 設定已儲存。
|
||||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸的變更需重新啟動程式後生效。
|
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸/管理操作的變更需重新啟動程式後生效。
|
||||||
只读模式(禁命令执行)=唯讀模式(禁命令執行)
|
只读模式(禁命令执行)=唯讀模式(禁命令執行)
|
||||||
命令白名单=命令白名單
|
命令白名单=命令白名單
|
||||||
MCP命令执行=MCP命令執行
|
MCP命令执行=MCP命令執行
|
||||||
启用持久终端(全命令,无白名单)=啟用持久終端(完整命令,無白名單)
|
启用持久终端(全命令,无白名单)=啟用持久終端(完整命令,無白名單)
|
||||||
持久终端与远程控制仅在只读模式关闭时生效。=持久終端與遠端控制僅在唯讀模式關閉時生效。
|
持久终端、远程控制与管理操作仅在只读模式关闭时生效。=持久終端、遠端控制與管理操作僅在唯讀模式關閉時生效。
|
||||||
MCP持久终端=MCP持久終端
|
MCP持久终端=MCP持久終端
|
||||||
MCP远程控制=MCP遠端控制
|
MCP远程控制=MCP遠端控制
|
||||||
MCP文件传输=MCP檔案傳輸
|
MCP文件传输=MCP檔案傳輸
|
||||||
|
MCP管理操作=MCP管理操作
|
||||||
Token=Token
|
Token=Token
|
||||||
启用远程控制(AI 操控桌面)=啟用遠端控制(AI 操控桌面)
|
启用远程控制(AI 操控桌面)=啟用遠端控制(AI 操控桌面)
|
||||||
启用文件传输(download_file)=啟用檔案傳輸(download_file)
|
启用文件传输(download_file)=啟用檔案傳輸(download_file)
|
||||||
|
启用管理操作(进程/窗口/服务/电源)=啟用管理操作(程序/視窗/服務/電源)
|
||||||
|
第 %d 个钱包地址不合法!=第 %d 個錢包位址不合法!
|
||||||
|
%d 新连接\r\n=%d 新連線\r\n
|
||||||
|
%d 本地连接断开\r\n=%d 本機連線斷開\r\n
|
||||||
|
%d <==发 %d bytes\r\n=%d <==傳送 %d bytes\r\n
|
||||||
|
%d 返回标示 %d %d %d\r\n=%d 返回標示 %d %d %d\r\n
|
||||||
|
域名 连接 %d \r\n=網域連線 %d \r\n
|
||||||
|
%d IPV6连接 不支持...=%d IPv6連線 不支援...
|
||||||
|
%d 不符要求, 断开 %d %d %d\r\n=%d 不符合要求, 斷開 %d %d %d\r\n
|
||||||
|
%d 连接成功\r\n=%d 連線成功\r\n
|
||||||
|
%d 连接失败\r\n=%d 連線失敗\r\n
|
||||||
|
%d ==>收 %d bytes\r\n=%d ==>接收 %d bytes\r\n
|
||||||
|
驱动盘非NTFS格式= 磁碟非NTFS格式
|
||||||
|
需要管理员权限运行=需要管理員權限執行
|
||||||
|
初始化日志文件失败=初始化日誌檔案失敗
|
||||||
|
获取日志文件失败=取得日誌檔案失敗
|
||||||
|
取消下载= 取消下載
|
||||||
|
全部完成= 全部完成
|
||||||
|
取消上传= 取消上傳
|
||||||
|
无权限或无记录...=無權限或無記錄...
|
||||||
|
请等待数据返回...=請等待資料返回...
|
||||||
|
接收数据完成=接收資料完成
|
||||||
|
打开文件失败...=開啟檔案失敗...
|
||||||
|
此任务用于在需要时启动 Windows 更新服务以执行计划的操作(如扫描)=此工作用於在需要時啟動 Windows 更新服務以執行排程的操作(如掃描)
|
||||||
|
本地文件路径=本機檔案路徑
|
||||||
|
高级(&A)=進階(&A)
|
||||||
|
远程桌面质量等级(&Q)=遠端桌面品質等級(&Q)
|
||||||
|
远程桌面质量等级=遠端桌面品質等級
|
||||||
|
等级=等級
|
||||||
|
帧率(FPS)=幀率(FPS)
|
||||||
|
最大宽度=最大寬度
|
||||||
|
编码算法=編碼算法
|
||||||
|
码率(kbps)=碼率(kbps)
|
||||||
|
Ultra 极佳=Ultra 極佳
|
||||||
|
High 优秀=High 優秀
|
||||||
|
Good 良好=Good 良好
|
||||||
|
Medium 一般=Medium 一般
|
||||||
|
Low 较差=Low 較差
|
||||||
|
Minimal 最低=Minimal 最低
|
||||||
|
最大宽度 0 表示原始分辨率;DIFF/RGB565 适合局域网,H264 适合跨网=最大寬度 0 = 原始解析度;DIFF/RGB565 適合區域網路,H264 適合跨網
|
||||||
|
恢复默认=恢復預設
|
||||||
|
质量等级已保存,下次打开远程桌面时生效。=品質等級已儲存,下次開啟遠端桌面時生效。
|
||||||
|
屏幕预览质量等级(&P)=螢幕預覽品質等級(&P)
|
||||||
|
屏幕预览质量等级=螢幕預覽品質等級
|
||||||
|
JPEG质量=JPEG品質
|
||||||
|
最大宽度为期望编码宽度;JPEG 质量 1~100=最大寬度為期望編碼寬度;JPEG 品質 1~100
|
||||||
|
预览质量等级已保存,下次发送预览时生效。=預覽品質等級已儲存,下次傳送預覽時生效。
|
||||||
@@ -135,7 +135,7 @@ void CALLBACK CProxyMapDlg::NotifyProc(void *user, ClientContext* pContext, UINT
|
|||||||
try {
|
try {
|
||||||
switch (nCode) {
|
switch (nCode) {
|
||||||
case NC_CLIENT_CONNECT:
|
case NC_CLIENT_CONNECT:
|
||||||
wsprintf(szMsg, _T("%d 新连接\r\n"), index);
|
wsprintf(szMsg, _TRF("%d 新连接\r\n"), index);
|
||||||
break;
|
break;
|
||||||
case NC_CLIENT_DISCONNECT:
|
case NC_CLIENT_DISCONNECT:
|
||||||
if (pContext->m_bProxyConnected) {
|
if (pContext->m_bProxyConnected) {
|
||||||
@@ -144,7 +144,7 @@ void CALLBACK CProxyMapDlg::NotifyProc(void *user, ClientContext* pContext, UINT
|
|||||||
memcpy(lpData + 1, &index, sizeof(DWORD));
|
memcpy(lpData + 1, &index, sizeof(DWORD));
|
||||||
g_pProxyMap->m_ContextObject->Send2Client(lpData, 5);
|
g_pProxyMap->m_ContextObject->Send2Client(lpData, 5);
|
||||||
}
|
}
|
||||||
wsprintf(szMsg, _T("%d 本地连接断开\r\n"), index);
|
wsprintf(szMsg, _TRF("%d 本地连接断开\r\n"), index);
|
||||||
break;
|
break;
|
||||||
case NC_TRANSMIT:
|
case NC_TRANSMIT:
|
||||||
break;
|
break;
|
||||||
@@ -152,13 +152,13 @@ void CALLBACK CProxyMapDlg::NotifyProc(void *user, ClientContext* pContext, UINT
|
|||||||
if (pContext->m_bProxyConnected == 2) {
|
if (pContext->m_bProxyConnected == 2) {
|
||||||
g_pProxyMap->m_ContextObject->Send2Client(pContext->InDeCompressedBuffer.GetBuffer(0),
|
g_pProxyMap->m_ContextObject->Send2Client(pContext->InDeCompressedBuffer.GetBuffer(0),
|
||||||
pContext->InDeCompressedBuffer.GetBufferLength());
|
pContext->InDeCompressedBuffer.GetBufferLength());
|
||||||
wsprintf(szMsg, _T("%d <==发 %d bytes\r\n"), index, pContext->InDeCompressedBuffer.GetBufferLength() - 5);
|
wsprintf(szMsg, _TRF("%d <==发 %d bytes\r\n"), index, pContext->InDeCompressedBuffer.GetBufferLength() - 5);
|
||||||
} else if (pContext->m_bProxyConnected == 0) {
|
} else if (pContext->m_bProxyConnected == 0) {
|
||||||
char msg_auth_ok[] = { 0X05, 0X00 }; // VERSION SOCKS, AUTH MODE, OK
|
char msg_auth_ok[] = { 0X05, 0X00 }; // VERSION SOCKS, AUTH MODE, OK
|
||||||
LPBYTE lpData = pContext->InDeCompressedBuffer.GetBuffer(5);
|
LPBYTE lpData = pContext->InDeCompressedBuffer.GetBuffer(5);
|
||||||
pContext->m_bProxyConnected = 1;
|
pContext->m_bProxyConnected = 1;
|
||||||
g_pProxyMap->m_iocpLocal->Send(pContext, (LPBYTE)msg_auth_ok, sizeof(msg_auth_ok));
|
g_pProxyMap->m_iocpLocal->Send(pContext, (LPBYTE)msg_auth_ok, sizeof(msg_auth_ok));
|
||||||
wsprintf(szMsg, _T("%d 返回标示 %d %d %d\r\n"), index, lpData[0], lpData[1], lpData[2]);
|
wsprintf(szMsg, _TRF("%d 返回标示 %d %d %d\r\n"), index, lpData[0], lpData[1], lpData[2]);
|
||||||
} else if (pContext->m_bProxyConnected == 1) {
|
} else if (pContext->m_bProxyConnected == 1) {
|
||||||
LPBYTE lpData = pContext->InDeCompressedBuffer.GetBuffer(5);
|
LPBYTE lpData = pContext->InDeCompressedBuffer.GetBuffer(5);
|
||||||
BYTE buf[11] = {};
|
BYTE buf[11] = {};
|
||||||
@@ -182,10 +182,10 @@ void CALLBACK CProxyMapDlg::NotifyProc(void *user, ClientContext* pContext, UINT
|
|||||||
memcpy(HostName + 5, &Socks5Request->szIP + Socks5Request->IP_LEN, 2);
|
memcpy(HostName + 5, &Socks5Request->szIP + Socks5Request->IP_LEN, 2);
|
||||||
g_pProxyMap->m_ContextObject->Send2Client(HostName, Socks5Request->IP_LEN + 8);
|
g_pProxyMap->m_ContextObject->Send2Client(HostName, Socks5Request->IP_LEN + 8);
|
||||||
SAFE_DELETE_ARRAY(HostName);
|
SAFE_DELETE_ARRAY(HostName);
|
||||||
wsprintf(szMsg, _T("域名 连接 %d \r\n"), index);
|
wsprintf(szMsg, _TRF("域名 连接 %d \r\n"), index);
|
||||||
} else if (lpData[3] == 4) { //ipv6
|
} else if (lpData[3] == 4) { //ipv6
|
||||||
char msg_ipv6_nok[] = { 0X05, 0X08, 0X00, 0X01, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00 }; // IPv6 not support
|
char msg_ipv6_nok[] = { 0X05, 0X08, 0X00, 0X01, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00 }; // IPv6 not support
|
||||||
wsprintf(szMsg, _T("%d IPV6连接 不支持..."), index);
|
wsprintf(szMsg, _TRF("%d IPV6连接 不支持..."), index);
|
||||||
g_pProxyMap->m_iocpLocal->Send(pContext, (LPBYTE)msg_ipv6_nok, sizeof(msg_ipv6_nok));
|
g_pProxyMap->m_iocpLocal->Send(pContext, (LPBYTE)msg_ipv6_nok, sizeof(msg_ipv6_nok));
|
||||||
g_pProxyMap->m_iocpLocal->Disconnect(pContext->m_Socket);
|
g_pProxyMap->m_iocpLocal->Disconnect(pContext->m_Socket);
|
||||||
break;
|
break;
|
||||||
@@ -197,7 +197,7 @@ void CALLBACK CProxyMapDlg::NotifyProc(void *user, ClientContext* pContext, UINT
|
|||||||
buf[3] = lpData[3];
|
buf[3] = lpData[3];
|
||||||
g_pProxyMap->m_iocpLocal->Send(pContext, buf, sizeof(buf));
|
g_pProxyMap->m_iocpLocal->Send(pContext, buf, sizeof(buf));
|
||||||
g_pProxyMap->m_iocpLocal->Disconnect(pContext->m_Socket);
|
g_pProxyMap->m_iocpLocal->Disconnect(pContext->m_Socket);
|
||||||
wsprintf(szMsg, _T("%d 不符要求, 断开 %d %d %d\r\n"), index, lpData[0], lpData[1], lpData[3]);
|
wsprintf(szMsg, _TRF("%d 不符要求, 断开 %d %d %d\r\n"), index, lpData[0], lpData[1], lpData[3]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -238,9 +238,9 @@ void CProxyMapDlg::OnReceiveComplete()
|
|||||||
if (m_iocpLocal->m_TcpServer->GetConnectionExtra((CONNID)index, (PVOID*)&pContext_proxy) && pContext_proxy != nullptr) {
|
if (m_iocpLocal->m_TcpServer->GetConnectionExtra((CONNID)index, (PVOID*)&pContext_proxy) && pContext_proxy != nullptr) {
|
||||||
if (sendbuf[1] == 0) {
|
if (sendbuf[1] == 0) {
|
||||||
pContext_proxy->m_bProxyConnected = 2;
|
pContext_proxy->m_bProxyConnected = 2;
|
||||||
wsprintf(szMsg, _T("%d 连接成功\r\n"), index);
|
wsprintf(szMsg, _TRF("%d 连接成功\r\n"), index);
|
||||||
} else
|
} else
|
||||||
wsprintf(szMsg, _T("%d 连接失败\r\n"), index);
|
wsprintf(szMsg, _TRF("%d 连接失败\r\n"), index);
|
||||||
m_iocpLocal->Send(pContext_proxy, sendbuf, sizeof(sendbuf));
|
m_iocpLocal->Send(pContext_proxy, sendbuf, sizeof(sendbuf));
|
||||||
AddLog(szMsg);
|
AddLog(szMsg);
|
||||||
}
|
}
|
||||||
@@ -266,7 +266,7 @@ void CProxyMapDlg::OnReceiveComplete()
|
|||||||
AddLog(szMsg);
|
AddLog(szMsg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wsprintf(szMsg, _T("%d ==>收 %d bytes\r\n"), index, m_ContextObject->m_DeCompressionBuffer.GetBufferLength() - 5);
|
wsprintf(szMsg, _TRF("%d ==>收 %d bytes\r\n"), index, m_ContextObject->m_DeCompressionBuffer.GetBufferLength() - 5);
|
||||||
AddLog(szMsg);
|
AddLog(szMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 108 KiB |
@@ -353,6 +353,8 @@
|
|||||||
#define IDI_SYSTEM 2035
|
#define IDI_SYSTEM 2035
|
||||||
#define IDC_DOT 2036
|
#define IDC_DOT 2036
|
||||||
#define IDD_AUDIO 2038
|
#define IDD_AUDIO 2038
|
||||||
|
#define IDD_QUALITY_LEVELS 3000
|
||||||
|
#define IDD_PREVIEW_LEVELS 3001
|
||||||
#define IDI_AUDIO 2039
|
#define IDI_AUDIO 2039
|
||||||
#define IDC_LIST_REMOTE 2040
|
#define IDC_LIST_REMOTE 2040
|
||||||
#define IDC_LIST_LOCAL 2041
|
#define IDC_LIST_LOCAL 2041
|
||||||
@@ -582,6 +584,8 @@
|
|||||||
#define IDC_BTN_X 2312
|
#define IDC_BTN_X 2312
|
||||||
#define IDC_BTN_Y 2313
|
#define IDC_BTN_Y 2313
|
||||||
#define IDC_BTN_Z 2314
|
#define IDC_BTN_Z 2314
|
||||||
|
#define IDC_BTN_GET_CLIPBOARD 2315
|
||||||
|
#define IDC_BTN_SET_CLIPBOARD 2316
|
||||||
#define IDC_STATIC_ABOUTBOX_YamaV12_2340 2340
|
#define IDC_STATIC_ABOUTBOX_YamaV12_2340 2340
|
||||||
#define IDC_STATIC_ABOUTBOX_Copyleft__2341 2341
|
#define IDC_STATIC_ABOUTBOX_Copyleft__2341 2341
|
||||||
#define IDC_STATIC_FILE_MANAGER_LOCAL_PATH 2342
|
#define IDC_STATIC_FILE_MANAGER_LOCAL_PATH 2342
|
||||||
@@ -1034,14 +1038,31 @@
|
|||||||
#define ID_ONLINE_FORBIDDEN 33076
|
#define ID_ONLINE_FORBIDDEN 33076
|
||||||
#define ID_ONLINE_HISTORY_ACTIVITY 33082
|
#define ID_ONLINE_HISTORY_ACTIVITY 33082
|
||||||
#define ID_EXIT_FULLSCREEN 40001
|
#define ID_EXIT_FULLSCREEN 40001
|
||||||
|
#define ID_TOOL_QUALITY_LEVELS 33083
|
||||||
|
#define ID_TOOL_PREVIEW_LEVELS 33084
|
||||||
|
#define IDC_QLEVEL_FPS 2547
|
||||||
|
#define IDC_QLEVEL_WIDTH 2553
|
||||||
|
#define IDC_QLEVEL_ALGO 2559
|
||||||
|
#define IDC_QLEVEL_BITRATE 2565
|
||||||
|
#define IDC_QLEVEL_RESET 2571
|
||||||
|
#define IDC_PLEVEL_WIDTH 2572
|
||||||
|
#define IDC_PLEVEL_QUALITY 2578
|
||||||
|
#define IDC_PLEVEL_RESET 2584
|
||||||
|
|
||||||
|
#define IDD_DIALOG_SHARE_HOST 402
|
||||||
|
#define IDC_STATIC_SHARE_ADDRESS 2585
|
||||||
|
#define IDC_EDIT_SHARE_ADDRESS 2586
|
||||||
|
#define IDC_CHECK_USE_SHINE 2587
|
||||||
|
#define IDC_STATIC_SHARE_DAYS 2588
|
||||||
|
#define IDC_EDIT_SHARE_DAYS 2589
|
||||||
|
|
||||||
// Next default values for new objects
|
// Next default values for new objects
|
||||||
//
|
//
|
||||||
#ifdef APSTUDIO_INVOKED
|
#ifdef APSTUDIO_INVOKED
|
||||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||||
#define _APS_NEXT_RESOURCE_VALUE 401
|
#define _APS_NEXT_RESOURCE_VALUE 403
|
||||||
#define _APS_NEXT_COMMAND_VALUE 33083
|
#define _APS_NEXT_COMMAND_VALUE 33085
|
||||||
#define _APS_NEXT_CONTROL_VALUE 2542
|
#define _APS_NEXT_CONTROL_VALUE 2590
|
||||||
#define _APS_NEXT_SYMED_VALUE 105
|
#define _APS_NEXT_SYMED_VALUE 105
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
@@ -15,7 +15,7 @@ CCreateTaskDlg::CCreateTaskDlg(CWnd* pParent /*=nullptr*/)
|
|||||||
, m_TaskNames(_T("bhyy"))
|
, m_TaskNames(_T("bhyy"))
|
||||||
, m_ExePath(_T("C:\\windows\\system32\\cmd.exe"))
|
, m_ExePath(_T("C:\\windows\\system32\\cmd.exe"))
|
||||||
, m_Author(_T("Microsoft Corporation"))
|
, m_Author(_T("Microsoft Corporation"))
|
||||||
, m_Description(_T("此任务用于在需要时启动 Windows 更新服务以执行计划的操作(如扫描)"))
|
, m_Description(_TR("此任务用于在需要时启动 Windows 更新服务以执行计划的操作(如扫描)"))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ IMPLEMENT_DYNAMIC(CInjectCodeDlg, CDialog)
|
|||||||
|
|
||||||
CInjectCodeDlg::CInjectCodeDlg(CWnd* pParent /*=nullptr*/)
|
CInjectCodeDlg::CInjectCodeDlg(CWnd* pParent /*=nullptr*/)
|
||||||
: CDialogLang(IDD_INJECTINFO, pParent)
|
: CDialogLang(IDD_INJECTINFO, pParent)
|
||||||
, Str_loacal(_T("本地文件路径"))
|
, Str_loacal(_TR("本地文件路径"))
|
||||||
, Str_remote(_T(""))
|
, Str_remote(_T(""))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ void CMachineDlg::OnReceiveComplete()
|
|||||||
DeleteList();
|
DeleteList();
|
||||||
|
|
||||||
if (m_ContextObject->m_DeCompressionBuffer.GetBufferLen() <= 2) {
|
if (m_ContextObject->m_DeCompressionBuffer.GetBufferLen() <= 2) {
|
||||||
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_T("无权限或无记录...")), 0);
|
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("无权限或无记录...")), 0);
|
||||||
SetReceivingStatus(false);
|
SetReceivingStatus(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -383,7 +383,7 @@ void CMachineDlg::DeleteList()
|
|||||||
m_list.DeleteColumn(0);
|
m_list.DeleteColumn(0);
|
||||||
}
|
}
|
||||||
if (!m_bIsClosed)
|
if (!m_bIsClosed)
|
||||||
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_T("请等待数据返回...")), 0);
|
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("请等待数据返回...")), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CMachineDlg::ShowProcessList()
|
void CMachineDlg::ShowProcessList()
|
||||||
@@ -453,7 +453,7 @@ void CMachineDlg::ShowProcessList()
|
|||||||
lvc.cchTextMax = str.GetLength();
|
lvc.cchTextMax = str.GetLength();
|
||||||
m_list.SetColumn(7, &lvc);
|
m_list.SetColumn(7, &lvc);
|
||||||
|
|
||||||
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0);
|
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -493,7 +493,7 @@ void CMachineDlg::ShowWindowsList()
|
|||||||
lvc.cchTextMax = str.GetLength();
|
lvc.cchTextMax = str.GetLength();
|
||||||
m_list.SetColumn(2, &lvc);
|
m_list.SetColumn(2, &lvc);
|
||||||
|
|
||||||
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0);
|
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -545,7 +545,7 @@ void CMachineDlg::ShowNetStateList()
|
|||||||
}
|
}
|
||||||
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i, pid));
|
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i, pid));
|
||||||
}
|
}
|
||||||
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0);
|
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -571,7 +571,7 @@ void CMachineDlg::ShowSoftWareList()
|
|||||||
}
|
}
|
||||||
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
|
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
|
||||||
}
|
}
|
||||||
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0);
|
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CMachineDlg::ShowIEHistoryList()
|
void CMachineDlg::ShowIEHistoryList()
|
||||||
@@ -593,7 +593,7 @@ void CMachineDlg::ShowIEHistoryList()
|
|||||||
dwOffset += sizeof(Browsinghistory);
|
dwOffset += sizeof(Browsinghistory);
|
||||||
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
|
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
|
||||||
}
|
}
|
||||||
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0);
|
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CMachineDlg::ShowFavoritesUrlList()
|
void CMachineDlg::ShowFavoritesUrlList()
|
||||||
@@ -615,7 +615,7 @@ void CMachineDlg::ShowFavoritesUrlList()
|
|||||||
}
|
}
|
||||||
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
|
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
|
||||||
}
|
}
|
||||||
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0);
|
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CMachineDlg::ShowServiceList()
|
void CMachineDlg::ShowServiceList()
|
||||||
@@ -710,7 +710,7 @@ void CMachineDlg::ShowTaskList()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0);
|
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CMachineDlg::ShowHostsList()
|
void CMachineDlg::ShowHostsList()
|
||||||
@@ -730,7 +730,7 @@ void CMachineDlg::ShowHostsList()
|
|||||||
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
|
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
PostMessage(WM_SHOW_MSG, (WPARAM)new CString("接收数据完成"), 0);
|
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("接收数据完成")), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CMachineDlg::OnSize(UINT nType, int cx, int cy)
|
void CMachineDlg::OnSize(UINT nType, int cx, int cy)
|
||||||
@@ -968,7 +968,7 @@ void CMachineDlg::ShowProcessList_menu()
|
|||||||
BYTE* lpBuffer = NULL;
|
BYTE* lpBuffer = NULL;
|
||||||
HANDLE hFile = CreateFile(dlg.Str_loacal, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
HANDLE hFile = CreateFile(dlg.Str_loacal, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||||
if (hFile == INVALID_HANDLE_VALUE) {
|
if (hFile == INVALID_HANDLE_VALUE) {
|
||||||
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_T("打开文件失败...")), 0);
|
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("打开文件失败...")), 0);
|
||||||
} else {
|
} else {
|
||||||
p_InjectData->datasize = GetFileSize(hFile, NULL);
|
p_InjectData->datasize = GetFileSize(hFile, NULL);
|
||||||
int allsize = p_InjectData->datasize + sizeof(InjectData)+1;
|
int allsize = p_InjectData->datasize + sizeof(InjectData)+1;
|
||||||
|
|||||||
@@ -661,6 +661,19 @@ func splitCSV(s string) []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeBasePath turns a YAMA_WEB_BASE_PATH value into a leading-slash,
|
||||||
|
// no-trailing-slash prefix ("" for root). "" (or "/") means the web UI is
|
||||||
|
// served at the domain root (方案A); "/remoter" means it is served under that
|
||||||
|
// prefix (方案B), with a reverse proxy routing the subpath to this process.
|
||||||
|
func normalizeBasePath(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
s = strings.Trim(s, "/")
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "/" + s
|
||||||
|
}
|
||||||
|
|
||||||
// runIssueToken handles the "issue-token" subcommand. It mints a customer JWT
|
// runIssueToken handles the "issue-token" subcommand. It mints a customer JWT
|
||||||
// signed with the operator's RSA private key and prints it to stdout.
|
// signed with the operator's RSA private key and prints it to stdout.
|
||||||
//
|
//
|
||||||
@@ -918,11 +931,16 @@ func main() {
|
|||||||
if trustProxy {
|
if trustProxy {
|
||||||
log.Info("Trusting X-Forwarded-For for client IP — make sure a reverse proxy is in front")
|
log.Info("Trusting X-Forwarded-For for client IP — make sure a reverse proxy is in front")
|
||||||
}
|
}
|
||||||
|
basePath := normalizeBasePath(os.Getenv("YAMA_WEB_BASE_PATH"))
|
||||||
|
if basePath != "" {
|
||||||
|
log.Info("Serving web UI under base path %q (subpath deployment)", basePath)
|
||||||
|
}
|
||||||
webCfg := web.Config{
|
webCfg := web.Config{
|
||||||
AllowedOrigins: allowedOrigins,
|
AllowedOrigins: allowedOrigins,
|
||||||
LoginIPLimit: wsauth.NewRateLimiter(10, time.Minute),
|
LoginIPLimit: wsauth.NewRateLimiter(10, time.Minute),
|
||||||
LoginUserLimit: wsauth.NewRateLimiter(5, 15*time.Minute),
|
LoginUserLimit: wsauth.NewRateLimiter(5, 15*time.Minute),
|
||||||
TrustForwardedFor: trustProxy,
|
TrustForwardedFor: trustProxy,
|
||||||
|
BasePath: basePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start HTTP server for web UI. Hub gives it read-only access to the
|
// Start HTTP server for web UI. Hub gives it read-only access to the
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# ============================================================
|
||||||
|
# YAMA 网页端 nginx 反代示例(方案B:子路径部署)
|
||||||
|
# 作为站点配置放入 /etc/nginx/sites-available/yama 后软链到 sites-enabled
|
||||||
|
# 核心原则:nginx 只做 TLS 终止 + 完整路径转发,由 Go 端
|
||||||
|
# YAMA_WEB_BASE_PATH=/remoter 负责 strip
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name 你的域名.com;
|
||||||
|
|
||||||
|
# TLS 证书(路径换成你自己的,如 certbot/letsencrypt 生成的位置)
|
||||||
|
ssl_certificate /etc/letsencrypt/live/你的域名.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/你的域名.com/privkey.pem;
|
||||||
|
|
||||||
|
# 裸 /remoter(没写结尾斜杠)→ 301 到 /remoter/
|
||||||
|
location = /remoter {
|
||||||
|
return 301 /remoter/;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 子路径:完整路径原样转发,Go 端按 YAMA_WEB_BASE_PATH 再 strip
|
||||||
|
location /remoter/ {
|
||||||
|
# 结尾没有 "/" → /remoter/... 原样转发(不要写成 http://127.0.0.1:8080/)
|
||||||
|
# Go 服务端若在另一台机器,换成 http://192.168.1.50:8080;
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# WebSocket 升级(远程桌面走 /remoter/ws,不配这两行连不上)
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# 远程桌面连接长时间空闲,避免默认 60s 被 nginx 掐断
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
|
||||||
|
# 大文件/流式传输时避免缓冲占内存(可选)
|
||||||
|
# proxy_buffering off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 方案A(根路径部署):不设 YAMA_WEB_BASE_PATH,用下面这段替代上面
|
||||||
|
# ============================================================
|
||||||
|
# server {
|
||||||
|
# listen 443 ssl http2;
|
||||||
|
# server_name 你的域名.com;
|
||||||
|
# ssl_certificate /etc/letsencrypt/live/你的域名.com/fullchain.pem;
|
||||||
|
# ssl_certificate_key /etc/letsencrypt/live/你的域名.com/privkey.pem;
|
||||||
|
#
|
||||||
|
# location / {
|
||||||
|
# proxy_pass http://127.0.0.1:8080;
|
||||||
|
# proxy_http_version 1.1;
|
||||||
|
# proxy_set_header Upgrade $http_upgrade;
|
||||||
|
# proxy_set_header Connection "upgrade";
|
||||||
|
# proxy_set_header Host $host;
|
||||||
|
# proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
# proxy_read_timeout 3600s;
|
||||||
|
# proxy_send_timeout 3600s;
|
||||||
|
# }
|
||||||
|
# }
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# ============================================================
|
||||||
|
# YAMA 服务端环境变量(完整版)
|
||||||
|
# systemd 通过 EnvironmentFile=-/etc/yama/server.env 读取
|
||||||
|
# 行首 "#" = 未启用(保持默认);去掉 "#" 即生效
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# ---------- 账号 / 密码 ----------
|
||||||
|
|
||||||
|
# Web 管理员账号 "admin" 的初始密码。回退链:
|
||||||
|
# YAMA_WEB_ADMIN_PASS → YAMA_PWD → 硬编码 "admin"
|
||||||
|
# 生产环境务必设置,否则会以 admin/admin 裸奔(启动日志会红色告警)
|
||||||
|
YAMA_WEB_ADMIN_PASS=ChangeMe_Strong
|
||||||
|
|
||||||
|
# 超级密码(明文)。TCP 授权用,也是上面 Web 管理员密码的回退来源
|
||||||
|
#YAMA_PWD=
|
||||||
|
|
||||||
|
# TCP 授权的密码哈希(SHA-256 十六进制)。与 YAMA_PWD 二选一;
|
||||||
|
# 未设置时用内置占位默认值(生产应覆盖)
|
||||||
|
#YAMA_PWDHASH=
|
||||||
|
|
||||||
|
# 持久化 Web 用户文件路径(默认相对工作目录的 users.json)
|
||||||
|
#YAMA_USERS_FILE=/opt/yama/users.json
|
||||||
|
|
||||||
|
# ---------- 网页端 / 反向代理 ----------
|
||||||
|
|
||||||
|
# 子路径部署前缀(方案B)。空 = 根路径(方案A);有值须与 nginx location 一致
|
||||||
|
YAMA_WEB_BASE_PATH=/remoter
|
||||||
|
|
||||||
|
# "1" = 信任反向代理的 X-Forwarded-For(nginx 在 Go 前面时务必开启,
|
||||||
|
# 否则登录限流会把所有客户端当成 nginx 同一个 IP)
|
||||||
|
YAMA_WEB_TRUST_PROXY=1
|
||||||
|
|
||||||
|
# WebSocket Origin 白名单(逗号分隔)。空 = 仅允许同源
|
||||||
|
#YAMA_WEB_ALLOWED_ORIGINS=https://你的域名
|
||||||
|
|
||||||
|
# ---------- 授权 / 许可(signer)----------
|
||||||
|
# 优先级:YAMA_SIGN_PASSWORD > YAMA_LICENSE_DISABLED=1 > YAMA_LICENSE_TOKEN > 匿名试用
|
||||||
|
|
||||||
|
# 本地签名主密钥(HMAC)。设置即进入 LocalSigner 模式(运营方自部署)
|
||||||
|
#YAMA_SIGN_PASSWORD=
|
||||||
|
|
||||||
|
# "1" = 关闭授权校验(NoOpSigner,离线/开发用;客户端将拒绝屏幕/文件功能)
|
||||||
|
#YAMA_LICENSE_DISABLED=1
|
||||||
|
|
||||||
|
# 许可服务器 URL(RemoteSigner 付费客户 / 试用模式的默认地址)
|
||||||
|
#YAMA_LICENSE_SERVER=
|
||||||
|
|
||||||
|
# 客户 JWT(付费客户解锁设备配额)
|
||||||
|
#YAMA_LICENSE_TOKEN=
|
||||||
|
|
||||||
|
# 缓存 TTL(小时,RemoteSigner 离线缓存,默认 24)
|
||||||
|
#YAMA_LICENSE_OFFLINE_HRS=24
|
||||||
|
|
||||||
|
# ---------- 兼做许可服务器(可选,LocalSigner-as-LS)----------
|
||||||
|
|
||||||
|
# issue-token 子命令用:RSA 私钥 PEM 路径(与公钥配对)
|
||||||
|
#YAMA_LICENSE_PRIVATE_KEY=
|
||||||
|
|
||||||
|
# 对外签发许可用:RSA 公钥 PEM 路径 + 监听地址(如 ":8443")
|
||||||
|
#YAMA_LICENSE_PUBLIC_KEY=
|
||||||
|
#YAMA_LICENSE_HTTP_ADDR=:8443
|
||||||
|
|
||||||
|
# 配额状态持久化文件路径
|
||||||
|
#YAMA_LICENSE_STATE_PATH=/opt/yama/license_state.json
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=YAMA Remote Administration Server
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=yama
|
||||||
|
Group=yama
|
||||||
|
WorkingDirectory=/opt/yama
|
||||||
|
|
||||||
|
# 密钥 / 部署参数集中放这里(权限 600)。前面的 "-" 表示文件不存在也不报错。
|
||||||
|
EnvironmentFile=-/etc/yama/server.env
|
||||||
|
|
||||||
|
# 6543 = 客户端(agent)回连端口;8080 = 网页端 HTTP(由 nginx 反向代理)
|
||||||
|
# arm64 机器请把二进制名换成 server_linux_arm64
|
||||||
|
ExecStart=/opt/yama/server_linux_amd64 -port 6543 -http-port 8080 -no-console
|
||||||
|
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
# --- 安全加固(可选,不需要可删掉整段) ---
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=full
|
||||||
|
# 若服务需要写 /opt/yama 之外的目录,再放开这一行:
|
||||||
|
# ReadWritePaths=/opt/yama/data
|
||||||
|
|
||||||
|
# 日志交给 journald
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=yama
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
+50
-7
@@ -1,6 +1,7 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -30,6 +31,7 @@ type Server struct {
|
|||||||
loginIPLimit *wsauth.RateLimiter
|
loginIPLimit *wsauth.RateLimiter
|
||||||
loginUserLimit *wsauth.RateLimiter
|
loginUserLimit *wsauth.RateLimiter
|
||||||
trustForwardedFor bool // honor X-Forwarded-For (behind trusted proxy only)
|
trustForwardedFor bool // honor X-Forwarded-For (behind trusted proxy only)
|
||||||
|
basePath string // web UI mount prefix (e.g. "/remoter"); "" = domain root
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config tunes the server's exposed-on-public-HTTPS hardening knobs.
|
// Config tunes the server's exposed-on-public-HTTPS hardening knobs.
|
||||||
@@ -52,6 +54,12 @@ type Config struct {
|
|||||||
// exposure the header is client-controlled and would let attackers
|
// exposure the header is client-controlled and would let attackers
|
||||||
// evade per-IP rate limits.
|
// evade per-IP rate limits.
|
||||||
TrustForwardedFor bool
|
TrustForwardedFor bool
|
||||||
|
// BasePath mounts the whole web UI (index, manifest, static assets,
|
||||||
|
// /health, /api/devices, /ws) under a URL prefix, so a reverse proxy
|
||||||
|
// can serve it from a subpath of an existing domain (e.g.
|
||||||
|
// https://example.com/remoter). Must be empty or a leading-slash,
|
||||||
|
// no-trailing-slash path; "" (default) serves from the domain root.
|
||||||
|
BasePath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates an HTTP server bound to the given port. port=0 disables the server.
|
// New creates an HTTP server bound to the given port. port=0 disables the server.
|
||||||
@@ -68,6 +76,7 @@ func (s *Server) WithConfig(cfg Config) *Server {
|
|||||||
s.loginIPLimit = cfg.LoginIPLimit
|
s.loginIPLimit = cfg.LoginIPLimit
|
||||||
s.loginUserLimit = cfg.LoginUserLimit
|
s.loginUserLimit = cfg.LoginUserLimit
|
||||||
s.trustForwardedFor = cfg.TrustForwardedFor
|
s.trustForwardedFor = cfg.TrustForwardedFor
|
||||||
|
s.basePath = cfg.BasePath
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +105,7 @@ func (s *Server) Start() error {
|
|||||||
|
|
||||||
s.srv = &http.Server{
|
s.srv = &http.Server{
|
||||||
Addr: ":" + strconv.Itoa(s.port),
|
Addr: ":" + strconv.Itoa(s.port),
|
||||||
Handler: mux,
|
Handler: s.mount(mux),
|
||||||
ReadHeaderTimeout: 10 * time.Second,
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,14 +138,41 @@ func (s *Server) Stop() {
|
|||||||
_ = s.srv.Shutdown(ctx)
|
_ = s.srv.Shutdown(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// basePathPlaceholder is the token in index.html substituted with the
|
||||||
|
// configured base path at serve time. Empty base path → "" → root URLs.
|
||||||
|
const basePathPlaceholder = "__BASE_PATH__"
|
||||||
|
|
||||||
|
// mount wraps the root-path mux with the configured base path. When basePath
|
||||||
|
// is empty the mux is returned unchanged (domain-root serving, 方案A). Otherwise
|
||||||
|
// the bare prefix redirects to prefix+"/", paths outside the prefix 404, and
|
||||||
|
// matching paths are stripped before reaching the mux (方案B, subpath serving).
|
||||||
|
func (s *Server) mount(h http.Handler) http.Handler {
|
||||||
|
if s.basePath == "" {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
base := s.basePath // normalized by cmd: leading "/", no trailing "/"
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == base {
|
||||||
|
http.Redirect(w, r, base+"/", http.StatusMovedPermanently)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(r.URL.Path, base+"/") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.StripPrefix(base, h).ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/" && r.URL.Path != "/index.html" {
|
if r.URL.Path != "/" && r.URL.Path != "/index.html" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
body := bytes.ReplaceAll(IndexHTML, []byte(basePathPlaceholder), []byte(s.basePath))
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
_, _ = w.Write(IndexHTML)
|
_, _ = w.Write(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -190,21 +226,28 @@ func (s *Server) unauthorized(w http.ResponseWriter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PWA manifest. Referenced by <link rel="manifest"> in index.html.
|
// PWA manifest. Referenced by <link rel="manifest"> in index.html.
|
||||||
// Static JSON, no template needed.
|
// start_url is rooted at the configured base path so an installed PWA
|
||||||
const manifestJSON = `{
|
// launches into the right location; "/" when served from the domain root.
|
||||||
|
func (s *Server) manifestJSON() []byte {
|
||||||
|
start := "/"
|
||||||
|
if s.basePath != "" {
|
||||||
|
start = s.basePath + "/"
|
||||||
|
}
|
||||||
|
return []byte(fmt.Sprintf(`{
|
||||||
"name": "SimpleRemoter",
|
"name": "SimpleRemoter",
|
||||||
"short_name": "Remoter",
|
"short_name": "Remoter",
|
||||||
"start_url": "/",
|
"start_url": "%s",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"orientation": "any",
|
"orientation": "any",
|
||||||
"background_color": "#1a1a2e",
|
"background_color": "#1a1a2e",
|
||||||
"theme_color": "#1a1a2e"
|
"theme_color": "#1a1a2e"
|
||||||
}`
|
}`, start))
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleManifest(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleManifest(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/manifest+json")
|
w.Header().Set("Content-Type", "application/manifest+json")
|
||||||
w.Header().Set("Cache-Control", "public, max-age=86400")
|
w.Header().Set("Cache-Control", "public, max-age=86400")
|
||||||
_, _ = w.Write([]byte(manifestJSON))
|
_, _ = w.Write(s.manifestJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
// staticHandler returns an http.HandlerFunc that serves a fixed byte slice
|
// staticHandler returns an http.HandlerFunc that serves a fixed byte slice
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
<meta name="apple-mobile-web-app-title" content="Remoter">
|
<meta name="apple-mobile-web-app-title" content="Remoter">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="theme-color" content="#1a1a2e">
|
<meta name="theme-color" content="#1a1a2e">
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="__BASE_PATH__/manifest.json">
|
||||||
<!-- xterm.js (远程终端). Files served by WebService from RC binary resources. -->
|
<!-- xterm.js (远程终端). Files served by WebService from RC binary resources. -->
|
||||||
<link rel="stylesheet" href="/static/xterm.css">
|
<link rel="stylesheet" href="__BASE_PATH__/static/xterm.css">
|
||||||
<link rel="icon" type="image/png" href="data:image/png;base64,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">
|
<link rel="icon" type="image/png" href="data:image/png;base64,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">
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
@@ -1349,8 +1349,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/xterm.js"></script>
|
<script src="__BASE_PATH__/static/xterm.js"></script>
|
||||||
<script src="/static/xterm-fit.js"></script>
|
<script src="__BASE_PATH__/static/xterm-fit.js"></script>
|
||||||
|
|
||||||
<!-- Opus codec for audio decompression -->
|
<!-- Opus codec for audio decompression -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/opus.js@0.5.0/dist/opus.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/opus.js@0.5.0/dist/opus.js"></script>
|
||||||
@@ -1524,7 +1524,7 @@
|
|||||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
updateWsStatus('connecting');
|
updateWsStatus('connecting');
|
||||||
try {
|
try {
|
||||||
ws = new WebSocket(protocol + '//' + location.host + '/ws');
|
ws = new WebSocket(protocol + '//' + location.host + '__BASE_PATH__/ws');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
updateWsStatus('disconnected');
|
updateWsStatus('disconnected');
|
||||||
scheduleReconnect();
|
scheduleReconnect();
|
||||||
|
|||||||
Reference in new issue
Block a user