3 Commits
Author SHA1 Message Date
yuanyuanxianganddeepseek-v4-pro 8815c4f896 Feature: Add get_audit_log MCP tool
Expose the server main-window audit log (host online/offline, operation
results, alerts) as a read-only, no-arg, in-memory MCP tool. The log lives
in m_CList_Message, a CListCtrl that is only safe to touch on the UI
thread, so add a thread-safe mirror m_MessageLog (guarded by m_cs) and keep
it in sync at the four item-mutation sites: ShowMessage, OnShowErrMessage,
OnMsglogDelete and OnMsglogClear. The tool reads the mirror directly under
m_cs and returns up to MAX_MESSAGE_COUNT (1000) entries, newest first.

Co-Authored-By: deepseek-v4-pro
2026-08-21 20:33:59 +02:00
yuanyuanxianganddeepseek-v4-pro 8de5c00a39 Improve: Park duplicate online connections as standby, promote on old removal
Replacing the old connection on a duplicate login made the client
reconnect in a tight loop. Instead, park the new connection in a standby
list keyed by clientID and promote it into the main host list only after
the old connection is removed by heartbeat timeout or disconnect.

Fix heartbeat attribution so a parked standby refreshes its own
last-heartbeat time rather than the main connection's, otherwise the main
never times out and the standby can never take over. Gate the offline
notification on an actual removal with no promoted standby to avoid a
spurious "host offline" when takeover happens. Skip promoting a standby
already marked removed, and reuse the normal login sign/settings path when
a standby is promoted.

Co-Authored-By: deepseek-v4-pro
2026-08-21 19:50:11 +02:00
yuanyuanxiang d359148841 doc: Add the "docs\macOS_Support_Design.md" to git 2026-08-21 14:15:31 +02:00
9 changed files with 846 additions and 27 deletions

No files matched your search

+1
View File
@@ -74,6 +74,7 @@ CAudio::~CAudio()
WAIT (m_hThreadCallBack, 30);
if (m_hThreadCallBack)
Mprintf("没有成功关闭waveInCallBack.\n");
Mprintf("TerminateThread: waveIn 线程 handle=%p threadId=%lu\n", m_Thread, GetThreadId(m_Thread));
TerminateThread(m_Thread, -999);
m_Thread = NULL;
}
+21 -4
View File
@@ -119,21 +119,38 @@ HDESK OpenActiveDesktop(ACCESS_MASK dwDesiredAccess)
HDESK hInputDesktop = OpenInputDesktop(0, FALSE, dwDesiredAccess);
if (!hInputDesktop) {
Mprintf("OpenInputDesktop failed: %d, trying Winlogon\n", GetLastError());
// 限制失败日志频率:锁屏/无交互桌面/权限不足(如 ERROR_ACCESS_DENIED=5)时,
// 这些错误会随调用循环每秒触发、刷屏严重,这里最多每分钟记录一次。
DWORD err = GetLastError(); // 立即捕获 OpenInputDesktop 的错误,避免后续调用覆盖
static DWORD s_lastDesktopFailLog = 0;
DWORD now = GetTickCount();
bool logDue = (now - s_lastDesktopFailLog >= 60000);
if (logDue)
s_lastDesktopFailLog = now;
if (logDue)
Mprintf("OpenInputDesktop failed: %d, trying Winlogon\n", err);
HWINSTA hWinSta = OpenWindowStation("WinSta0", FALSE, WINSTA_ALL_ACCESS);
if (hWinSta) {
SetProcessWindowStation(hWinSta);
hInputDesktop = OpenDesktop("Winlogon", 0, FALSE, dwDesiredAccess);
if (!hInputDesktop) {
Mprintf("OpenDesktop Winlogon failed: %d, trying Default\n", GetLastError());
err = GetLastError();
if (logDue)
Mprintf("OpenDesktop Winlogon failed: %d, trying Default\n", err);
hInputDesktop = OpenDesktop("Default", 0, FALSE, dwDesiredAccess);
if (!hInputDesktop) {
Mprintf("OpenDesktop Default failed: %d\n", GetLastError());
err = GetLastError();
if (logDue)
Mprintf("OpenDesktop Default failed: %d\n", err);
}
}
} else {
Mprintf("OpenWindowStation failed: %d\n", GetLastError());
err = GetLastError();
if (logDue)
Mprintf("OpenWindowStation failed: %d\n", err);
}
}
return hInputDesktop;
+1
View File
@@ -189,6 +189,7 @@ bool CScreenManager::RestartScreen(BOOL switchScreen)
m_bIsWorking = FALSE;
DWORD s = WaitForSingleObject(m_hWorkThread, 1000);
if (s == WAIT_TIMEOUT) {
Mprintf("TerminateThread: 截屏工作线程 handle=%p threadId=%lu\n", m_hWorkThread, GetThreadId(m_hWorkThread));
TerminateThread(m_hWorkThread, 0x20260215);
}
+1
View File
@@ -163,6 +163,7 @@ CShellManager::~CShellManager()
m_bStarting = FALSE;
TerminateProcess(m_hShellProcessHandle, 0); //结束我们自己创建的Cmd进程
Mprintf("TerminateThread: cmd 线程 handle=%p threadId=%lu\n", m_hShellThreadHandle, GetThreadId(m_hShellThreadHandle));
TerminateThread(m_hShellThreadHandle, 0); //结束我们自己创建的Cmd线程
Sleep(100);
+6 -1
View File
@@ -450,7 +450,12 @@ BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
param.User = g_Server.pwdHash;
threadHandle = CreateThread(NULL, 0, run, &param, 0, NULL);
} else if (fdwReason == DLL_PROCESS_DETACH) {
if (threadHandle) TerminateThread(threadHandle, 0x20250619);
if (threadHandle) {
// DLL 卸载时强杀线程前记录日志,便于定位死锁。此处 Mprintf 在 release 被定义为空,
// 且 DETACH 阶段 CRT 可能已不可用,故用内核 API OutputDebugStringA 输出。
OutputDebugStringA("TerminateThread: DllMain DLL_PROCESS_DETACH\n");
TerminateThread(threadHandle, 0x20250619);
}
}
return TRUE;
}
+576
View File
@@ -0,0 +1,576 @@
# macOS 远程桌面客户端设计文档
## 1. 概述
### 1.1 目标
在不影响现有 Windows/Linux 功能的基础上,为 YAMA 项目添加 macOS 客户端支持,前期实现远程桌面控制功能。
### 1.2 功能范围(Phase 1)
| 功能 | 优先级 | 状态 | 说明 |
|-----|--------|------|------|
| 连接服务器 | P0 | ⏳ Stub | 复用现有网络协议 |
| 屏幕捕获 | P0 | ✅ 完成 | CGDisplayCreateImage 实现 |
| 鼠标控制 | P0 | ✅ 完成 | CGEvent 实现 |
| 键盘控制 | P0 | ✅ 完成 | CGEvent + VK映射 |
| 光标同步 | P1 | ⏳ 待实现 | 显示当前光标类型 |
| 多显示器 | P2 | ⏳ 待实现 | 支持多屏幕选择 |
### 1.3 参考实现
- **Linux 客户端** (`linux/`): 架构相似,协议兼容
- **RustDesk**: 成熟的跨平台远程桌面开源项目
- **Sunshine**: 高性能游戏串流项目
---
## 2. 架构设计
### 2.1 目录结构(已实现)
```
YAMA/
├── common/ # 跨平台共享(已有)
│ ├── commands.h # 协议定义
│ └── ...
├── client/ # Windows 客户端(不动)
├── linux/ # Linux 客户端(不动)
└── macos/ # macOS 客户端(Phase 1 完成)
├── CMakeLists.txt # 构建配置
├── build.sh # 一键编译脚本
├── README.txt # 使用说明
├── IOCPClient.h # 网络客户端 stub
├── main.mm # 入口点 + 系统信息
├── Permissions.h # 权限检查声明
├── Permissions.mm # 权限检查实现
├── ScreenHandler.h # 屏幕捕获声明
├── ScreenHandler.mm # 屏幕捕获实现
├── InputHandler.h # 输入模拟声明
├── InputHandler.mm # 输入模拟实现
├── SystemManager.h # 进程管理声明
└── SystemManager.mm # 进程管理实现
```
### 2.2 模块依赖关系
```
┌─────────────────────────────────────────────────┐
│ main.mm │
│ (入口、权限检查、系统信息收集) │
└─────────────┬───────────────────────────────────┘
│
┌─────────┼─────────┬─────────────┐
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌────────────┐
│Screen │ │Input │ │System │ │IOCPClient │
│Handler│ │Handler│ │Manager│ │(网络 stub) │
└───┬───┘ └───┬───┘ └───────┘ └────────────┘
│ │
▼ ▼
┌─────────────────────────────────┐
│ macOS 系统框架 │
│ CoreGraphics (截屏) │
│ Carbon (键码定义) │
│ ApplicationServices (权限) │
└─────────────────────────────────┘
```
---
## 3. 技术选型
### 3.1 屏幕捕获方案(已选定)
| 方案 | 最低版本 | 性能 | 复杂度 | 选择 |
|-----|---------|------|--------|-----|
| `CGDisplayCreateImage` | 10.5 | 中 | 低 | **✅ 已采用** |
| `CGDisplayStream` | 10.8 | 中 | 中 | 备选 |
| `ScreenCaptureKit` | 12.3 | 高 | 高 | 未来考虑 |
**选择 CGDisplayCreateImage**:
- 实现简单,兼容性好
- 与 Linux 端 `XGetImage` 方案一致
- 足以支持 10-30 FPS 传输
### 3.2 输入模拟方案(已实现)
| 功能 | API | 实现状态 |
|-----|-----|---------|
| 鼠标移动 | `CGEventCreateMouseEvent` | ✅ 完成 |
| 鼠标点击 | `CGEventCreateMouseEvent` | ✅ 完成 |
| 鼠标拖动 | `kCGEventLeftMouseDragged` | ✅ 完成 |
| 鼠标滚轮 | `CGEventCreateScrollWheelEvent` | ✅ 完成 |
| 键盘输入 | `CGEventCreateKeyboardEvent` | ✅ 完成 |
| 事件发送 | `CGEventPost(kCGHIDEventTap)` | ✅ 完成 |
### 3.3 VK 键码映射(已实现)
Windows VK 码到 macOS CGKeyCode 的完整映射:
| 按键类型 | VK 范围 | macOS 映射 |
|---------|---------|-----------|
| 字母 A-Z | 0x41-0x5A | kVK_ANSI_A - kVK_ANSI_Z |
| 数字 0-9 | 0x30-0x39 | kVK_ANSI_0 - kVK_ANSI_9 |
| 小键盘 | 0x60-0x69 | kVK_ANSI_Keypad0 - 9 |
| F1-F12 | 0x70-0x7B | kVK_F1 - kVK_F12 |
| 方向键 | 0x25-0x28 | kVK_LeftArrow 等 |
| 修饰键 | 0x10-0x12 | kVK_Shift/Control/Option |
| Win 键 | 0x5B-0x5C | kVK_Command |
### 3.4 差分算法(与 Linux 一致)
| 算法 | ID | 说明 |
|-----|-----|------|
| ALGORITHM_GRAY | 0 | 灰度压缩 |
| ALGORITHM_DIFF | 1 | BGRA 差分 |
| ALGORITHM_H264 | 2 | 硬件编码(未实现)|
| ALGORITHM_RGB565 | 3 | RGB565 压缩 |
### 3.5 macOS 版本支持
| 版本 | 代号 | 发布年份 | 支持状态 |
|-----|------|---------|---------|
| 10.13 | High Sierra | 2017 | ✅ 目标最低版本 |
| 10.14 | Mojave | 2018 | ✅ 支持 |
| 10.15 | Catalina | 2019 | ✅ 支持(需权限) |
| 11.x | Big Sur | 2020 | ✅ 支持 |
| 12.x | Monterey | 2021 | ✅ 支持 |
| 13.x | Ventura | 2022 | ✅ 支持 |
| 14.x | Sonoma | 2023 | ✅ 支持 |
**注意**:macOS 10.15+ 需要用户授予屏幕录制权限。
---
## 4. 权限处理
### 4.1 所需权限
| 权限 | 用途 | 最低要求版本 | 授权方式 |
|-----|------|------------|---------|
| **屏幕录制** | 捕获屏幕内容 | 10.15 | 系统偏好设置 > 隐私 > 屏幕录制 |
| **辅助功能** | 模拟键盘/鼠标输入 | 所有版本 | 系统偏好设置 > 隐私 > 辅助功能 |
### 4.2 权限检查实现(Permissions.mm)
```objc
// 检查屏幕录制权限
bool Permissions::checkScreenCapture() {
if (@available(macOS 10.15, *)) {
CGImageRef image = CGWindowListCreateImage(
CGRectMake(0, 0, 1, 1),
kCGWindowListOptionOnScreenOnly,
kCGNullWindowID,
kCGWindowImageDefault
);
if (image == nil) return false;
CGImageRelease(image);
return true;
}
return true; // 10.15 之前不需要权限
}
// 检查辅助功能权限
bool Permissions::checkAccessibility() {
return AXIsProcessTrusted();
}
// 请求辅助功能权限(弹出系统对话框)
void Permissions::requestAccessibility() {
NSDictionary *options = @{
(__bridge id)kAXTrustedCheckOptionPrompt: @YES
};
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
}
```
### 4.3 权限引导流程
```
程序启动
│
▼
检查屏幕录制权限 ──否──▶ 打开系统偏好设置
│
是
│
▼
检查辅助功能权限 ──否──▶ 弹出授权对话框
│
是
│
▼
正常运行
```
---
## 5. 核心代码实现
### 5.1 ScreenHandler(已实现)
```objc
// ScreenHandler.h
class ScreenHandler {
public:
ScreenHandler();
~ScreenHandler();
bool init(); // 初始化
void start(IOCPClient* client, uint64_t id); // 开始捕获
void stop(); // 停止捕获
int getWidth() const;
int getHeight() const;
void sendBitmapInfo(); // 发送屏幕信息
void onReceive(const uint8_t* data, size_t size);
void applyQualityLevel(int8_t level, bool persist);
private:
bool captureScreen(std::vector<uint8_t>& buffer); // 截屏
void sendFirstScreen(); // 发送首帧
void sendDiffFrame(); // 发送差分帧
uint32_t compareBitmap(...); // 差分计算
void convertBGRAtoGray(...); // 灰度转换
void convertBGRAtoRGB565(...); // RGB565转换
void captureLoop(); // 捕获循环
// 成员变量
IOCPClient* m_client;
CGDirectDisplayID m_displayID;
BITMAPINFOHEADER_MAC m_bmpHeader;
std::vector<uint8_t> m_prevFrame, m_currFrame;
std::atomic<bool> m_running;
std::atomic<uint8_t> m_algorithm;
std::atomic<int> m_maxFPS;
};
```
### 5.2 InputHandler(已实现)
```objc
// InputHandler.h
class InputHandler {
public:
InputHandler();
~InputHandler();
bool init(); // 初始化(检查权限)
void handleInputEvent(const MSG64_MAC* msg); // 处理输入事件
bool hasAccessibilityPermission() const;
private:
void handleMouseMove(int x, int y);
void handleMouseButton(CGMouseButton button, bool down, int x, int y);
void handleMouseDoubleClick(CGMouseButton button, int x, int y);
void handleMouseWheel(int delta);
void handleKeyEvent(uint32_t vkCode, bool down);
static CGKeyCode vkToMacKeyCode(uint32_t vk); // VK 到 macOS 键码映射
bool m_hasPermission;
CGPoint m_lastMousePos;
bool m_leftButtonDown, m_rightButtonDown, m_middleButtonDown;
};
```
### 5.3 SystemManager(已实现)
```objc
// SystemManager.h
class SystemManager {
public:
SystemManager(IOCPClient* client, uint64_t clientID);
~SystemManager();
void onReceive(const uint8_t* data, size_t size);
private:
void sendProcessList(); // 进程列表
void killProcesses(const uint8_t* data, size_t size);
void sendWindowsList(); // 窗口列表
static std::vector<pid_t> getAllPids(); // 枚举进程
static std::string getProcessName(pid_t pid);
static std::string getProcessPath(pid_t pid);
};
```
### 5.4 main.mm(已实现)
主要功能:
- 命令行参数解析
- 权限检查和引导
- 系统信息收集(主机名、OS版本、CPU、内存等)
- 初始化各处理器
- 主事件循环
```objc
int main(int argc, const char* argv[]) {
@autoreleasepool {
// 1. 解析命令行参数
// 2. 检查权限
// 3. 收集系统信息
// 4. 初始化 ScreenHandler, InputHandler
// 5. 主循环
}
return 0;
}
```
---
## 6. 构建配置
### 6.1 CMakeLists.txt
```cmake
cmake_minimum_required(VERSION 3.15)
project(ghost_macos)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13")
set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64") # Universal Binary
set(SOURCES
main.mm
ScreenHandler.mm
InputHandler.mm
SystemManager.mm
Permissions.mm
)
add_executable(ghost ${SOURCES})
target_include_directories(ghost PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# 链接 macOS 框架
find_library(COCOA_FRAMEWORK Cocoa REQUIRED)
find_library(COREGRAPHICS_FRAMEWORK CoreGraphics REQUIRED)
find_library(IOKIT_FRAMEWORK IOKit REQUIRED)
find_library(IOSURFACE_FRAMEWORK IOSurface REQUIRED)
find_library(APPLICATIONSERVICES_FRAMEWORK ApplicationServices REQUIRED)
find_library(SECURITY_FRAMEWORK Security REQUIRED)
find_library(CARBON_FRAMEWORK Carbon REQUIRED)
target_link_libraries(ghost PRIVATE
${COCOA_FRAMEWORK}
${COREGRAPHICS_FRAMEWORK}
${IOKIT_FRAMEWORK}
${IOSURFACE_FRAMEWORK}
${APPLICATIONSERVICES_FRAMEWORK}
${SECURITY_FRAMEWORK}
${CARBON_FRAMEWORK}
)
target_compile_options(ghost PRIVATE -Wall -Wextra -fobjc-arc)
```
### 6.2 编译命令
```bash
# 方式 1: 使用脚本
chmod +x build.sh
./build.sh
# 方式 2: 手动编译
mkdir build && cd build
cmake ..
make -j$(sysctl -n hw.ncpu)
# 输出: build/bin/ghost
```
---
## 7. 实现进度
### Phase 1: 基础框架 ✅ 完成
| 任务 | 状态 | 产出 |
|-----|------|------|
| 创建目录结构 | ✅ | `macos/` 目录 |
| CMakeLists.txt | ✅ | 构建配置 |
| Permissions.h/mm | ✅ | 权限检查 |
| ScreenHandler.h/mm | ✅ | 屏幕捕获 |
| InputHandler.h/mm | ✅ | 输入模拟 |
| SystemManager.h/mm | ✅ | 进程管理 |
| main.mm | ✅ | 入口点 |
| IOCPClient.h | ✅ | 网络 stub |
### Phase 2: 网络集成 ⏳ 待实现
| 任务 | 状态 | 说明 |
|-----|------|------|
| IOCPClient 实现 | ⏳ | 复用 client/IOCPClient |
| 登录信息发送 | ⏳ | LOGIN_INFOR 结构 |
| 心跳保活 | ⏳ | Heartbeat 机制 |
| 命令分发 | ⏳ | DataProcess 回调 |
### Phase 3: 功能完善 ⏳ 待实现
| 任务 | 状态 |
|-----|------|
| 光标类型同步 | ⏳ |
| 多显示器支持 | ⏳ |
| 文件传输 | ⏳ |
| 剪贴板同步 | ⏳ |
### Phase 4: 代码保护 ⏳ 待实现
| 任务 | 状态 |
|-----|------|
| 服务端密钥对生成 | ⏳ |
| 服务端签名逻辑 | ⏳ |
| 闭源库 libYamaCore | ⏳ |
| 客户端绑定验证 | ⏳ |
---
## 8. 测试指南
### 8.1 编译前准备
```bash
# 安装 Xcode Command Line Tools
xcode-select --install
# 安装 CMake (通过 Homebrew)
brew install cmake
```
### 8.2 编译运行
```bash
# 获取代码
git clone <repo-url>
cd YAMA/macos
# 编译
./build.sh
# 运行
./build/bin/ghost [server_ip] [port]
```
### 8.3 首次运行权限授予
1. **屏幕录制权限**:
- 系统偏好设置 > 隐私与安全性 > 屏幕录制
- 添加 `ghost` 可执行文件
2. **辅助功能权限**:
- 系统偏好设置 > 隐私与安全性 > 辅助功能
- 添加 `ghost` 可执行文件
### 8.4 预期输出
```
=== macOS Ghost Client (Phase 1) ===
Server: 192.168.0.55:6543
Checking permissions...
Collecting system information...
Hostname: MacBook-Pro.local
OS: macOS 14.0.0
CPU: Apple M1 (0 MHz, 8 cores)
Memory: 16.0 GB
User: username
Resolution: 1:2560x1600
Executable: /path/to/ghost
Start Time: 2024-04-29 12:00:00
Initializing handlers...
Screen handler initialized: 2560x1600
Input handler initialized
=== Phase 1 Status ===
Screen Capture: READY
Input Control: READY
Network: STUB (Phase 2)
======================
Starting main loop (Ctrl+C to exit)...
```
---
## 9. 常见问题
### 9.1 编译问题
| 问题 | 原因 | 解决方案 |
|-----|------|---------|
| 找不到 Foundation.h | 缺少 Xcode CLT | `xcode-select --install` |
| CMake 找不到 | 未安装 CMake | `brew install cmake` |
| 架构不匹配 | Universal Binary 问题 | 检查 CMAKE_OSX_ARCHITECTURES |
### 9.2 运行时问题
| 问题 | 原因 | 解决方案 |
|-----|------|---------|
| 程序无法打开 | 未签名 | `xattr -d com.apple.quarantine ghost` |
| Screen Capture: NEED PERMISSION | 无录屏权限 | 系统偏好设置授权 |
| Input Control: NEED PERMISSION | 无辅助功能权限 | 系统偏好设置授权 |
### 9.3 权限重置
```bash
# 如果权限状态混乱,可重置
tccutil reset ScreenCapture
tccutil reset Accessibility
```
---
## 10. 代码保护机制(待实现)
### 10.1 保护策略
核心思路:客户端内嵌服务端公钥,连接时验证服务端身份。
```
┌─────────────────────┐ ┌─────────────────────┐
│ macOS Client │ │ Your Server │
│ │ 1. 挑战请求 │ │
│ ┌─────────────────┐ │ ─────────────────▶ │ ┌─────────────────┐ │
│ │ libYamaCore.a │ │ │ │ 服务端私钥 │ │
│ │ (闭源) │ │ ◀───────────────── │ │ │ │
│ │ 内嵌服务端公钥 │ │ 2. 签名响应 │ └─────────────────┘ │
│ │ │ │ │ │
│ │ 验证签名 ─────▶ 匹配 ──▶ 正常工作 │
│ │ ─────▶ 不匹配 ──▶ 拒绝连接 │
│ └─────────────────┘ │ │ │
└─────────────────────┘ └─────────────────────┘
```
### 10.2 实施步骤
1. 生成服务端密钥对
2. 服务端添加签名逻辑
3. 创建闭源库 libYamaCore
4. 嵌入公钥编译
5. 客户端集成验证
---
## 11. 参考资料
### 11.1 官方文档
- [CGDisplayCreateImage](https://developer.apple.com/documentation/coregraphics/1454434-cgdisplaycreateimage)
- [CGEvent Reference](https://developer.apple.com/documentation/coregraphics/cgevent)
- [Carbon Events.h](https://developer.apple.com/documentation/carbon/events_h)
- [Accessibility API](https://developer.apple.com/documentation/applicationservices/axuielement_h)
### 11.2 开源项目参考
- [RustDesk macOS](https://github.com/rustdesk/rustdesk/tree/master/src/platform/macos)
- [Sunshine Screen Capture](https://github.com/LizardByte/Sunshine/tree/master/src/platform/macos)
---
## 12. 修订历史
| 日期 | 版本 | 修改内容 |
|-----|------|---------|
| 2026-04-28 | 1.0 | 初始版本(设计文档) |
| 2026-04-29 | 1.1 | 更新为实现文档,Phase 1 完成 |
+143 -22
View File
@@ -1638,11 +1638,14 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
return;
}
if (!ctx->GetClientData(ONLINELIST_IP).IsEmpty()) {
Mprintf("上线消息 - 主机已经存在 [2]: %llu. IP= %s. Path= %s\n", id, data[ONLINELIST_IP], path);
#ifndef _DEBUG
Mprintf("上线消息 - 主机重复上线,暂存替补 [2]: %llu. IP= %s. Path= %s\n", id, data[ONLINELIST_IP], path);
// 网络抖动导致客户端用新连接重复上线:这里不立即替换/关闭旧连接,
// 否则旧连接被关会触发客户端反复重连抖动(更新前就一直在"替换旧连接")。
// 改为把新连接暂存到替补列表,旧连接因心跳超时或断开被移除后再由
// PromoteStandby 取替补转正。
AddToStandbyList(ContextObject);
LeaveCriticalSection(&m_cs);
return;
#endif
}
}
}
@@ -1793,6 +1796,19 @@ VOID CMy2015RemoteDlg::ShowMessage(CString strType, CString strMsg)
m_CList_Message.SetItemText(0, 2, strMsg);
m_CList_Message.SetItemData(0, (DWORD_PTR)MsgColorLevel(strType, strMsg));
// 同步线程安全镜像,供 MCP get_audit_log 读取(CListCtrl 仅 UI 线程可见)。
{
EnterCriticalSection(&m_cs);
LogMessage entry;
entry.type = strType.GetString();
entry.time = strTime.GetString();
entry.msg = strMsg.GetString();
m_MessageLog.insert(m_MessageLog.begin(), entry);
if ((int)m_MessageLog.size() > MAX_MESSAGE_COUNT)
m_MessageLog.pop_back();
LeaveCriticalSection(&m_cs);
}
CString strStatusMsg;
EnterCriticalSection(&m_cs);
@@ -1872,6 +1888,18 @@ LRESULT CMy2015RemoteDlg::OnShowErrMessage(WPARAM wParam, LPARAM lParam)
m_CList_Message.SetItemText(0, 1, strTime);
m_CList_Message.SetItemText(0, 2, strText);
m_CList_Message.SetItemData(0, (DWORD_PTR)MsgColorLevel(strTitle, strText));
// 同步线程安全镜像,供 MCP get_audit_log 读取(CListCtrl 仅 UI 线程可见)。
{
EnterCriticalSection(&m_cs);
LogMessage entry;
entry.type = strTitle.GetString();
entry.time = strTime.GetString();
entry.msg = strText.GetString();
m_MessageLog.insert(m_MessageLog.begin(), entry);
if ((int)m_MessageLog.size() > MAX_MESSAGE_COUNT)
m_MessageLog.pop_back();
LeaveCriticalSection(&m_cs);
}
if(title)delete title;
if(text)delete text;
@@ -3689,15 +3717,23 @@ void CMy2015RemoteDlg::CheckHeartbeat()
for (context* ContextObject : toRemove) {
auto host = ContextObject->GetAdditionalData(RES_CLIENT_PUBIP);
host = host.IsEmpty() ? std::to_string(ContextObject->GetClientID()).c_str() : host;
Mprintf("Client %s[%llu] heartbeat timeout!!! \n", host, ContextObject->GetClientID());
if (m_needNotify)
PostMessageA(WM_SHOWNOTIFY, (WPARAM)new CharMsg(_TR("主机掉线")),
(LPARAM)new CharMsg(_TR("主机长时间无心跳: ") + host));
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(_TR("[主机下线] 主机长时间无心跳: ") + host), NULL);
Mprintf("主机 %s[%llu]心跳超时\n", host, ContextObject->GetClientID());
int port = ContextObject->GetPort();
RemoveFromHostList(ContextObject);
uint64_t clientID = ContextObject->GetClientID();
// 旧连接长期无心跳被移除:若有替补连接在等待,取替补转正。
// 只有真正下线(无替补转正)时才提示"主机下线",避免替补接替时误报。
bool promoted = RemoveFromHostList(ContextObject) && PromoteStandby(clientID);
if (!promoted) {
Mprintf("Client %s[%llu] heartbeat timeout!!! \n", host, clientID);
if (m_needNotify)
PostMessageA(WM_SHOWNOTIFY, (WPARAM)new CharMsg(_TR("主机掉线")),
(LPARAM)new CharMsg(_TR("主机长时间无心跳: ") + host));
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(_TR("[主机下线] 主机长时间无心跳: ") + host), NULL);
Mprintf("主机 %s[%llu]心跳超时\n", host, clientID);
} else {
Mprintf("主机 %s[%llu]心跳超时,替补连接已转正接替\n", host, clientID);
}
// 从待上线队列中移除(防止定时器访问已释放的 context)
auto pit = std::find(m_PendingOnline.begin(), m_PendingOnline.end(), ContextObject);
@@ -5148,6 +5184,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject)
// Remove from host list and pending online queue
info->hasLogin = g_2015RemoteDlg->RemoveFromHostList(ContextObject);
// 无论断开的是主连接还是替补,都从替补列表里移除(幂等)。
g_2015RemoteDlg->RemoveFromStandbyList(ContextObject);
// 主连接被移除且有替补在等待时,立即取替补转正,避免主机短暂消失。
if (info->hasLogin) {
g_2015RemoteDlg->PromoteStandby(info->clientId);
}
auto& pending = g_2015RemoteDlg->m_PendingOnline;
auto it = std::find(pending.begin(), pending.end(), (context*)ContextObject);
if (it != pending.end()) {
@@ -6652,6 +6694,70 @@ bool CMy2015RemoteDlg::RemoveFromHostList(context* ctx)
return removed;
}
void CMy2015RemoteDlg::AddToStandbyList(CONTEXT_OBJECT* ctx)
{
if (!ctx) return;
uint64_t clientID = ctx->GetClientID();
auto it = m_StandbyHosts.find(clientID);
if (it != m_StandbyHosts.end() && it->second != ctx) {
// 已有旧替补:说明客户端又断线重连了,旧替补已失效,关闭它释放连接。
Mprintf("替补列表:替换旧替补 [%llu]\n", clientID);
it->second->CancelIO();
}
m_StandbyHosts[clientID] = ctx;
Mprintf("替补列表:加入替补 [%llu],当前替补数=%zu\n", clientID, m_StandbyHosts.size());
}
void CMy2015RemoteDlg::RemoveFromStandbyList(CONTEXT_OBJECT* ctx)
{
if (!ctx) return;
auto it = m_StandbyHosts.find(ctx->GetClientID());
// 只有当前替补正是该 ctx 时才移除,避免误删更新后的替补(幂等)。
if (it != m_StandbyHosts.end() && it->second == ctx) {
m_StandbyHosts.erase(it);
Mprintf("替补列表:移除替补 [%llu],当前替补数=%zu\n", ctx->GetClientID(), m_StandbyHosts.size());
}
}
bool CMy2015RemoteDlg::PromoteStandby(uint64_t clientID)
{
auto it = m_StandbyHosts.find(clientID);
if (it == m_StandbyHosts.end())
return false;
CONTEXT_OBJECT* standby = it->second;
// 替补连接可能已被 RemoveStaleContext 标记移除(其 OfflineProc 正在等 m_cs),
// 此时转正只会让主机短暂闪断后再次下线。跳过并清理,避免误转正已死的连接。
if (standby->IsRemoved.load()) {
m_StandbyHosts.erase(it);
Mprintf("替补转正:主机 [%llu] 的替补连接已被移除,放弃转正\n", clientID);
return false;
}
m_StandbyHosts.erase(it);
// 替补转正:加入主列表并更新索引(与 AddList 正常上线流程一致)。
m_ClientIndex[clientID] = m_HostList.size();
m_HostList.push_back(standby);
if (WebService().IsRunning()) {
WebService().MarkDeviceOnline(clientID);
}
m_PendingOnline.push_back(standby);
Mprintf("替补转正:主机 [%llu] 由替补连接接替上线\n", clientID);
// 重复上线时未补发过 MasterSettings,这里补发(含签名),让客户端完成登录配置。
std::string signMessage(const std::string & privateKey, BYTE * msg, int len);
CString startTime = standby->GetClientData(ONLINELIST_STARTTIME);
MasterSettings copy = m_settings;
std::string msg = startTime.GetString();
msg += "|" + std::to_string(clientID);
auto signature = signMessage("", (BYTE*)msg.c_str(), msg.length());
ASSERT(signature.size() <= sizeof(copy.Signature));
memcpy(copy.Signature, signature.data(), signature.size());
SendMasterSettings(standby, copy);
return true;
}
LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam)
{
// OfflineProc already removed context from m_HostList/m_PendingOnline and added to m_PendingOffline.
@@ -6661,23 +6767,24 @@ LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam)
return S_OK;
}
// Show offline notification
if (!info->ip.IsEmpty() && info->hasLogin) {
// 判断主连接是否仍在线:OfflineProc 已在 IO 线程持锁内完成 RemoveFromHostList,
// 若有替补连接被 PromoteStandby 转正,m_ClientIndex 里仍会有该 clientId。
// 直接查 m_ClientIndex 比依赖 hasLogin 更稳健:既不受子连接 auth 改造影响,
// 也能避免替补转正时误报"主机下线"。
bool stillOnline = false;
if (info->clientId != 0) {
CLock L(m_cs);
stillOnline = (m_ClientIndex.find(info->clientId) != m_ClientIndex.end());
}
// Show offline notification(仅在主连接断开且无替补转正时才提示真正下线)
if (!info->ip.IsEmpty() && info->hasLogin && !stillOnline) {
ShowMessage(_TR("操作成功"), info->ip + " " + _TR("主机下线") + "[" + info->aliveInfo.c_str() + "]");
Mprintf("%s 主机下线 [%s]\n", info->ip.GetString(), info->aliveInfo.c_str());
}
// 关闭对应客户端的循环快照浮窗(如有)。CloseLoopTip 内部 find 找不到会静默返回。
if (info->clientId != 0) {
// 判断主连接是否仍在线:OfflineProc 已在 IO 线程持锁内完成 RemoveFromHostList,
// 若 m_ClientIndex 里仍有该 clientId,说明还有另一条连接在列表中(即本次断开的
// 是子连接),不应清理主连接的 UI 状态;反之说明主机真正下线。
// 直接查 m_ClientIndex 比依赖 hasLogin 更稳健:不受未来子连接 auth 改造影响。
bool stillOnline;
{
CLock L(m_cs);
stillOnline = (m_ClientIndex.find(info->clientId) != m_ClientIndex.end());
}
if (!stillOnline) {
// 主连接真正下线:关循环窗、释放缩略图 HBITMAP、清调度状态。
CloseLoopTip(info->clientId);
@@ -7093,7 +7200,10 @@ void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx)
ctx->SetClientData(ONLINELIST_VIDEO, newVideo);
changed = true;
}
id->SetLastHeartbeat(time(0));
// 心跳必须刷新"实际发送者" ctx 的心跳,而不是按 clientID 查到的 id(主连接)。
// 否则替补连接(ctx != id)的心跳会把主连接的心跳也刷新掉,导致主连接永不超时、
// 替补永远无法转正。ctx 自身心跳已在函数前段记录,这里在锁内再做一次权威更新。
ctx->SetLastHeartbeat(time(0));
if (changed) {
m_DirtyClients.insert(clientID);
@@ -9571,6 +9681,13 @@ void CMy2015RemoteDlg::OnMsglogDelete()
for (auto it = selected.rbegin(); it != selected.rend(); ++it) {
m_CList_Message.DeleteItem(*it);
}
// 同步线程安全镜像(新在前,索引与 CListCtrl 一一对应;同样倒序避免索引变化)
EnterCriticalSection(&m_cs);
for (auto it = selected.rbegin(); it != selected.rend(); ++it) {
if (*it >= 0 && (size_t)*it < m_MessageLog.size())
m_MessageLog.erase(m_MessageLog.begin() + *it);
}
LeaveCriticalSection(&m_cs);
}
void CMy2015RemoteDlg::OnMsglogCopy() {
@@ -9608,6 +9725,10 @@ void CMy2015RemoteDlg::OnMsglogCopy() {
void CMy2015RemoteDlg::OnMsglogClear()
{
m_CList_Message.DeleteAllItems();
// 同步线程安全镜像,供 MCP get_audit_log 读取。
EnterCriticalSection(&m_cs);
m_MessageLog.clear();
LeaveCriticalSection(&m_cs);
}
void CMy2015RemoteDlg::OnMsglogSearch()
+16
View File
@@ -340,10 +340,23 @@ public:
std::vector<int> m_PendingOffline; // 存储端口号
CListCtrlEx m_CList_Online;
CListCtrl m_CList_Message;
// 消息/审计日志的线程安全镜像(供 MCP get_audit_log 工具读取)。m_CList_Message 是
// CListCtrl、只能在 UI 线程访问,MCP 线程据此镜像读取。受 m_cs 保护,新在前、旧在后,
// 与 CListCtrl 显示顺序一致(索引一一对应,便于删除同步)。
struct LogMessage {
std::string type; // 类型(操作成功/主机上线/主机下线…)
std::string time; // 时间戳 "YYYY-MM-DD HH:MM:SS"
std::string msg; // 内容
};
std::vector<LogMessage> m_MessageLog;
CSplitterBar m_SplitterBar;
int m_nSplitPos = 160; // 消息区高度(像素),可拖动调整
std::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端)
std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射
// 重复上线的替补连接:clientID -> 替补 context。
// 网络抖动导致客户端用新连接重复上线时,不立即替换/关闭旧连接(否则会触发客户端
// 反复重连抖动),而是把新连接暂存到这里;旧连接因心跳超时或断开被移除后,再取替补转正。
std::unordered_map<uint64_t, CONTEXT_OBJECT*> m_StandbyHosts;
std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表
int m_nSortColumn = -1; // 当前排序列,-1 表示未排序
bool m_bSortAscending = true; // 排序方向(true=升序)
@@ -366,6 +379,9 @@ public:
context* FindHostNoLock(int port); // caller must hold m_cs lock
context* FindHostNoLock(uint64_t id); // caller must hold m_cs lock
bool RemoveFromHostList(context* ctx); // 从 m_HostList 中移除并更新索引
void AddToStandbyList(CONTEXT_OBJECT* ctx); // 重复上线:暂存替补连接(替换旧替补)
void RemoveFromStandbyList(CONTEXT_OBJECT* ctx); // 连接断开时从替补列表移除(幂等)
bool PromoteStandby(uint64_t clientID); // 旧连接移除后,取替补转正(需持有 m_cs);返回是否真的转正
CStatusBar m_StatusBar; //状态条
ULONGLONG m_ullStartTime = 0; // 程序启动时间 (GetTickCount64)
+81
View File
@@ -639,6 +639,34 @@ Json::Value BuildClientLogOutputSchema() {
return schema;
}
// get_audit_log 的 outputSchema(服务端消息/审计日志条目数组)
Json::Value BuildAuditLogOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value entriesProp(Json::objectValue);
entriesProp["type"] = "array";
Json::Value items(Json::objectValue);
items["type"] = "object";
Json::Value itemProps(Json::objectValue);
const char* strFields[] = { "type", "time", "msg" };
for (const char* f : strFields) {
Json::Value s(Json::objectValue);
s["type"] = "string";
itemProps[f] = s;
}
items["properties"] = itemProps;
entriesProp["items"] = items;
props["entries"] = entriesProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("entries");
schema["required"] = required;
return schema;
}
// get_screenshot 的 outputSchema(image 元数据;base64 数据在 content 的 image 块中)
Json::Value BuildScreenshotOutputSchema() {
Json::Value props(Json::objectValue);
@@ -897,6 +925,23 @@ std::string BuildToolsListResult(const Json::Value& id) {
tools.append(tool);
}
// 11) get_audit_log(服务端本地:主界面消息/审计日志,纯内存,无子链接)
{
Json::Value tool(Json::objectValue);
tool["name"] = "get_audit_log";
tool["description"] = u8"获取 YAMA 服务端主界面的消息/审计日志(主机上线/下线、操作结果、告警等)。返回列表内当前全部条目(最多 1000 条,新在前)。只读、纯内存。";
Json::Value inputSchema(Json::objectValue);
inputSchema["type"] = "object";
inputSchema["properties"] = Json::Value(Json::objectValue);
inputSchema["required"] = Json::Value(Json::arrayValue);
tool["inputSchema"] = inputSchema;
tool["outputSchema"] = BuildAuditLogOutputSchema();
tools.append(tool);
}
result["tools"] = tools;
return BuildResult(id, result);
}
@@ -1418,6 +1463,41 @@ std::string BuildGetClientLog(const Json::Value& id, const Json::Value& args, CM
return BuildResult(id, result);
}
// tools/call:get_audit_log(服务端本地消息日志,读取 UI 日志列表的线程安全镜像)
std::string BuildGetAuditLog(const Json::Value& id, CMy2015RemoteDlg* parent) {
Json::Value entries(Json::arrayValue);
if (parent) {
EnterCriticalSection(&parent->m_cs);
// m_MessageLog 新在前、旧在后(与界面一致),直接顺序输出。
for (const auto& e : parent->m_MessageLog) {
Json::Value item(Json::objectValue);
item["type"] = ToUtf8(e.type.c_str(), 936);
item["time"] = ToUtf8(e.time.c_str(), 936);
item["msg"] = ToUtf8(e.msg.c_str(), 936);
entries.append(item);
}
LeaveCriticalSection(&parent->m_cs);
}
int count = (int)entries.size();
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["entries"] = entries;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = count > 0 ? std::string(u8"共 ") + std::to_string(count) + std::string(u8" 条日志。")
: std::string(u8"当前无日志。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/call 分派
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
const Json::Value& id = root["id"];
@@ -1440,6 +1520,7 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
if (toolName == "list_files") return BuildListFiles(id, args, parent);
if (toolName == "list_services") return BuildListServices(id, args, parent);
if (toolName == "get_client_log") return BuildGetClientLog(id, args, parent);
if (toolName == "get_audit_log") return BuildGetAuditLog(id, parent);
return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));