21 Commits

Author SHA1 Message Date
yuanyuanxiang
05a9bb1245 Feature: Allow external resource override from res/ directory 2026-05-05 21:21:38 +02:00
yuanyuanxiang
a89f8dd28f Feature: Add zoom functionality for remote desktop viewer in non-control mode 2026-05-05 15:09:16 +02:00
yuanyuanxiang
6113b4653d Fix: Resend login info after group change for macOS/Linux clients 2026-05-05 13:35:02 +02:00
yuanyuanxiang
f11fc93ba8 Feature: Embed language resources, disk files act as optional patches 2026-05-05 13:22:47 +02:00
yuanyuanxiang
773c78ac0f Improve master authorization logs and web remote desktop cursor 2026-05-05 12:46:05 +02:00
yuanyuanxiang
92f3df8464 Perf: Optimize macOS screen capture with CGDisplayStream
Core optimization:
- Use CGDisplayStream instead of per-frame CGDisplayCreateImage
- Push model: CPU sleeps when screen is static (condition_variable wait)
- IOSurface capture avoids expensive image creation per frame
- ~47% CPU reduction during active remote desktop (45% → 24%)

Additional optimizations:
- vImageVerticalReflect (SIMD) replaces manual row-by-row flip
- Cache CGColorSpaceRef to avoid per-frame creation/release
- Cache tempBuffer to avoid per-frame memory allocation
- Throttle getCursorTypeIndex to 250ms (Accessibility API is expensive)

Bug fixes:
- Fix unreliable screen capture permission check (use actual capture test)
- Improve permission logging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-03 23:36:23 +02:00
yuanyuanxiang
b732f841d0 fix(server): Prevent crash from dangling pointers in file dialog map
Fixed two bugs when closing ScreenSpyDlg with file transfer dialogs:

1. Access violation (0xC0000005): CDlgFileSend self-destructs via
   PostNcDestroy (delete this) when closed, leaving dangling pointers
   in m_FileRecvDlgs map.

2. Double-free: Original code called DestroyWindow() then delete,
   but DestroyWindow already triggers delete this via PostNcDestroy.

Solution:
- Store {HWND, pointer} pairs instead of raw pointers
- Check HWND validity with IsWindow() before accessing pointer
- Use SendMessage(WM_CLOSE) to let dialog self-destruct safely
- Always erase map entries to prevent accumulation of invalid data

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-03 15:29:31 +02:00
yuanyuanxiang
1df2a7b321 feat(macos): Add clipboard support to match Linux implementation 2026-05-03 13:53:58 +02:00
yuanyuanxiang
3d8e90da14 Feature: Add daemon mode (-d) support for macOS 2026-05-03 13:35:30 +02:00
yuanyuanxiang
12e2a33062 Fix: Prevent reconnect crash by clearing callback before destruction 2026-05-03 13:16:02 +02:00
yuanyuanxiang
a8b0932080 Feature: Implement Linux cursor type detection using XFixes extension 2026-05-03 12:58:29 +02:00
yuanyuanxiang
ca37fa419a Feat: Implement H264 for Linux client with dynamic libx264 loading 2026-05-03 12:15:22 +02:00
yuanyuanxiang
36423b1c7c Refactor: Move FileManager to common, add macOS file management support 2026-05-03 10:30:30 +02:00
yuanyuanxiang
a3611d9fc1 Feature: Add terminal support for macOS client with shared PTYHandler 2026-05-03 09:33:47 +02:00
yuanyuanxiang
9ae5529458 Fix: Ensure MFC and Web remote desktop sessions are fully independent 2026-05-02 19:16:30 +02:00
yuanyuanxiang
171fa750e5 Feature: Persist group name to config file and include in login info 2026-05-02 19:14:21 +02:00
yuanyuanxiang
8ed9ba8426 Fix: Wake display on remote desktop start when macOS is locked 2026-05-02 19:14:14 +02:00
yuanyuanxiang
fd3838a151 Feature: filter device visibility by allowed groups for web user 2026-05-02 00:30:08 +02:00
yuanyuanxiang
56419f8ecb Fix: MFC remote desktop touchpad two-finger scroll not working 2026-05-01 23:25:32 +02:00
yuanyuanxiang
bb6fd7b1b9 Feature: Add user idle and screen lock detection for macOS 2026-05-01 23:20:46 +02:00
yuanyuanxiang
3607f1d768 Feature: Add power management to keep macOS client always responsive 2026-05-01 21:43:55 +02:00
42 changed files with 3787 additions and 519 deletions

View File

@@ -494,27 +494,31 @@ make
**系统要求**
- macOS 10.15 (Catalina) 及以上
- 架构支持Intel (x64) 和 Apple Silicon (arm64) 通用二进制
- 需要授予系统权限:屏幕录制、辅助功能、完全磁盘访问
**功能支持**
| 功能 | 状态 | 实现 |
|------|------|------|
| 远程桌面 | ✅ | CoreGraphics 屏幕捕获H.264 硬件编码 |
| 远程桌面 | ✅ | CoreGraphics 屏幕捕获,VideoToolbox H.264 硬件编码 |
| 鼠标控制 | ✅ | CGEvent 模拟,支持双击、拖拽 |
| 键盘控制 | ✅ | CGEvent 模拟,完整键码映射 |
| 光标同步 | ✅ | 实时同步远程光标样式 |
| 远程终端 | ✅ | PTY 交互式 Shellzsh/bash |
| 文件管理 | ✅ | 双向传输、V2 协议、大文件支持 |
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
| 文件管理 | | 开发中 |
| 远程终端 | ⏳ | 开发中 |
| 分组管理 | | 持久化配置文件 |
| 进程管理 | ⏳ | 开发中 |
| 剪贴板 | ⏳ | 开发中 |
**编译方式**
```bash
cd macos
mkdir build && cd build
cmake ..
make
./build.sh
# 或手动编译:
# mkdir build && cd build && cmake .. && make
```
---

View File

@@ -479,27 +479,31 @@ make
**System Requirements**:
- macOS 10.15 (Catalina) or later
- Architecture: Universal Binary (Intel x64 + Apple Silicon arm64)
- Required permissions: Screen Recording, Accessibility, Full Disk Access
**Feature Support**:
| Feature | Status | Implementation |
|---------|--------|----------------|
| Remote Desktop | ✅ | CoreGraphics screen capture, H.264 hardware encoding |
| Remote Desktop | ✅ | CoreGraphics screen capture, VideoToolbox 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 |
| Remote Terminal | ✅ | PTY interactive shell (zsh/bash) |
| File Management | ✅ | Bidirectional transfer, V2 protocol, large file support |
| Heartbeat/RTT | ✅ | RFC 6298 RTT estimation |
| File Management | | In development |
| Remote Terminal | ⏳ | In development |
| Group Management | | Persistent configuration file |
| Process Management | ⏳ | In development |
| Clipboard | ⏳ | In development |
**Build Instructions**:
```bash
cd macos
mkdir build && cd build
cmake ..
make
./build.sh
# Or manually:
# mkdir build && cd build && cmake .. && make
```
---

View File

@@ -478,27 +478,31 @@ make
**系統要求**
- macOS 10.15 (Catalina) 及以上
- 架構支援Intel (x64) 和 Apple Silicon (arm64) 通用二進位
- 需要授予系統權限:螢幕錄製、輔助使用、完全磁碟存取
**功能支援**
| 功能 | 狀態 | 實作 |
|------|------|------|
| 遠端桌面 | ✅ | CoreGraphics 螢幕擷取H.264 硬體編碼 |
| 遠端桌面 | ✅ | CoreGraphics 螢幕擷取,VideoToolbox H.264 硬體編碼 |
| 滑鼠控制 | ✅ | CGEvent 模擬,支援雙擊、拖曳 |
| 鍵盤控制 | ✅ | CGEvent 模擬,完整鍵碼對應 |
| 游標同步 | ✅ | 即時同步遠端游標樣式 |
| 遠端終端 | ✅ | PTY 互動式 Shellzsh/bash |
| 檔案管理 | ✅ | 雙向傳輸、V2 協定、大檔案支援 |
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
| 檔案管理 | | 開發中 |
| 遠端終端 | ⏳ | 開發中 |
| 分組管理 | | 持久化設定檔 |
| 程序管理 | ⏳ | 開發中 |
| 剪貼簿 | ⏳ | 開發中 |
**編譯方式**
```bash
cd macos
mkdir build && cd build
cmake ..
make
./build.sh
# 或手動編譯:
# mkdir build && cd build && cmake .. && make
```
---

View File

@@ -272,6 +272,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
FrpcParam* f = (FrpcParam*)user;
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
int r = 0;
uint64_t start = time(0);
if (proc) {
r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit);
@@ -279,7 +280,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
else {
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
}
if (r) {
if (r || (time(0)-start < 15)) {
char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf);
@@ -295,6 +296,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
FrpcParam* f = (FrpcParam*)user;
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
int r = 0;
uint64_t start = time(0);
if (proc) {
r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit);
@@ -302,7 +304,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
else {
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
}
if (r) {
if (r || (time(0)-start < 15)) {
char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf);

View File

@@ -1,8 +1,24 @@
/**
* FileManager.h - Unix File Manager
*
* Implements file transfer between Windows server and Unix client.
* Supports: browse, upload, download, delete, rename, create folder
*
* PLATFORM SUPPORT:
* - Linux: Supported
* - macOS: Supported
* - Windows: NOT SUPPORTED (Windows uses different file APIs)
*/
#pragma once
#if defined(_WIN32) || defined(_WIN64)
#error "FileManager.h is not supported on Windows."
#endif
#include <dirent.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <iconv.h>
#include <unistd.h>
#include <cstring>
#include <string>
@@ -11,15 +27,19 @@
#include <fstream>
#include <sstream>
#include <cerrno>
#ifdef __APPLE__
#include <sys/mount.h> // macOS: statfs for filesystem type
#endif
#include <iconv.h> // Character encoding conversion (GBK <-> UTF-8)
// FileTransferV2 is in the same directory (common/)
#include "FileTransferV2.h"
// 外部声明 clientID在 main.cpp 中定义)
// External declaration of clientID (defined in main.cpp/main.mm)
extern uint64_t g_myClientID;
// ============== Linux File Manager ==============
// Implements file transfer between Windows server and Linux client
// Supports: browse, upload, download, delete, rename, create folder
#define MAX_SEND_BUFFER 65535
class FileManager : public IOCPManager
@@ -222,6 +242,13 @@ private:
// ---- Get root filesystem type ----
static std::string getRootFsType()
{
#ifdef __APPLE__
struct statfs sf;
if (statfs("/", &sf) == 0) {
return std::string(sf.f_fstypename); // "apfs", "hfs", etc.
}
return "apfs";
#else
std::ifstream f("/proc/mounts");
std::string line;
while (std::getline(f, line)) {
@@ -232,6 +259,7 @@ private:
}
}
return "ext4";
#endif
}
// ---- Ensure parent directory exists (mkdir -p for parent of file path) ----
@@ -307,7 +335,11 @@ private:
memcpy(buf + offset + 2, &totalMB, sizeof(unsigned long));
memcpy(buf + offset + 6, &freeMB, sizeof(unsigned long));
#ifdef __APPLE__
const char* typeName = "macOS";
#else
const char* typeName = "Linux";
#endif
int typeNameLen = strlen(typeName) + 1;
memcpy(buf + offset + 10, typeName, typeNameLen);

View File

@@ -1,7 +1,24 @@
/**
* FileTransferV2.h - Unix V2 File Transfer Protocol
*
* Implements V2 file transfer protocol for Unix clients.
* Supports: receive files from server/C2C, send files to server/C2C
*
* PLATFORM SUPPORT:
* - Linux: Supported
* - macOS: Supported
* - Windows: NOT SUPPORTED (Windows uses different file APIs)
*/
#pragma once
#include "common/commands.h"
#include "common/file_upload.h"
#include "client/IOCPClient.h"
#if defined(_WIN32) || defined(_WIN64)
#error "FileTransferV2.h is not supported on Windows."
#endif
#include "commands.h"
#include "file_upload.h"
#include "../client/IOCPClient.h"
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
@@ -15,10 +32,6 @@
#include <fstream>
#include <thread>
// ============== Linux V2 File Transfer ==============
// Implements V2 file transfer protocol for Linux client
// Supports: receive files from server/C2C, send files to server/C2C
class FileTransferV2
{
public:

View File

@@ -50,47 +50,71 @@ public:
char line[4096];
while (fgets(line, sizeof(line), f)) {
// 去除行尾换行符
size_t len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
line[--len] = '\0';
ParseLine(line, currentSection);
}
if (len == 0)
continue;
fclose(f);
return true;
}
// 跳过注释
if (line[0] == ';' || line[0] == '#')
continue;
// 从内存加载 INI 数据,返回是否成功
// 用于加载嵌入的资源数据
bool LoadFromMemory(const char* data, size_t size)
{
Clear();
// 检测 section 头: [SectionName]
// 真正的 section 头:']' 后面没有 '='(否则是 key=value
if (line[0] == '[') {
char* end = strchr(line, ']');
if (end) {
char* eqAfter = strchr(end + 1, '=');
if (!eqAfter) {
// 纯 section 头,如 [Strings]
*end = '\0';
currentSection = line + 1;
continue;
}
// ']' 后有 '=',如 [使用FRP]=[Using FRP],当作 key=value 处理
}
if (!data || size == 0)
return false;
std::string currentSection;
const char* p = data;
const char* end = data + size;
while (p < end) {
// 找到行尾
const char* lineEnd = p;
while (lineEnd < end && *lineEnd != '\n' && *lineEnd != '\r')
lineEnd++;
// 复制行内容
size_t lineLen = lineEnd - p;
if (lineLen > 0 && lineLen < 4096) {
char line[4096];
memcpy(line, p, lineLen);
line[lineLen] = '\0';
ParseLine(line, currentSection);
}
// 不在任何 section 内则跳过
if (currentSection.empty())
continue;
// 跳过换行符
p = lineEnd;
while (p < end && (*p == '\n' || *p == '\r'))
p++;
}
// 解析 key=value只按第一个 '=' 分割,不 trim
// key 和 value 均做反转义(\n \r \t \\ \"
char* eq = strchr(line, '=');
if (eq && eq != line) {
*eq = '\0';
std::string key = Unescape(std::string(line));
std::string value = Unescape(std::string(eq + 1));
m_sections[currentSection][key] = value;
}
return true;
}
// 追加加载(不清除现有数据,用于覆盖)
bool LoadFileAppend(const char* filePath)
{
if (!filePath || !filePath[0])
return false;
FILE* f = nullptr;
#ifdef _MSC_VER
if (fopen_s(&f, filePath, "r") != 0 || !f)
return false;
#else
f = fopen(filePath, "r");
if (!f)
return false;
#endif
std::string currentSection;
char line[4096];
while (fgets(line, sizeof(line), f)) {
ParseLine(line, currentSection);
}
fclose(f);
@@ -138,6 +162,52 @@ public:
private:
TSections m_sections;
// 解析单行 INI 内容
void ParseLine(char* line, std::string& currentSection)
{
// 去除行尾换行符
size_t len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
line[--len] = '\0';
if (len == 0)
return;
// 跳过注释
if (line[0] == ';' || line[0] == '#')
return;
// 检测 section 头: [SectionName]
// 真正的 section 头:']' 后面没有 '='(否则是 key=value
if (line[0] == '[') {
char* end = strchr(line, ']');
if (end) {
char* eqAfter = strchr(end + 1, '=');
if (!eqAfter) {
// 纯 section 头,如 [Strings]
*end = '\0';
currentSection = line + 1;
return;
}
// ']' 后有 '=',如 [使用FRP]=[Using FRP],当作 key=value 处理
}
}
// 不在任何 section 内则跳过
if (currentSection.empty())
return;
// 解析 key=value只按第一个 '=' 分割,不 trim
// key 和 value 均做反转义(\n \r \t \\ \"
char* eq = strchr(line, '=');
if (eq && eq != line) {
*eq = '\0';
std::string key = Unescape(std::string(line));
std::string value = Unescape(std::string(eq + 1));
m_sections[currentSection][key] = value;
}
}
// 反转义:将字面量 \n \r \t \\ \" 转为对应的控制字符
static std::string Unescape(const std::string& s)
{

272
common/PTYHandler.h Normal file
View File

@@ -0,0 +1,272 @@
/**
* PTYHandler.h - Unix Pseudo Terminal Handler
*
* This file provides pseudo terminal (PTY) functionality for remote shell access.
*
* PLATFORM SUPPORT:
* - Linux: Supported
* - macOS: Supported
* - Windows: NOT SUPPORTED (Windows uses different terminal APIs)
*
* USAGE:
* #include "common/PTYHandler.h"
*
* PTYHandler* handler = new PTYHandler(clientObject);
* clientObject->setManagerCallBack(handler, ...);
*/
#pragma once
#if defined(_WIN32) || defined(_WIN64)
#error "PTYHandler.h is not supported on Windows. Use Windows ConPTY or other APIs instead."
#endif
// Platform-specific includes
#ifdef __APPLE__
#include <util.h> // macOS: openpty()
#else
#include <pty.h> // Linux: openpty()
#endif
// Common Unix includes
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <thread>
#include <atomic>
#include <stdexcept>
#include "commands.h"
#include "../client/IOCPClient.h"
/**
* PTYHandler - Pseudo Terminal Handler
*
* Manages a pseudo terminal for remote shell access.
* Inherits from IOCPManager to integrate with the IOCP client framework.
*/
class PTYHandler : public IOCPManager
{
public:
// Non-copyable, non-movable (owns system resources)
PTYHandler(const PTYHandler&) = delete;
PTYHandler& operator=(const PTYHandler&) = delete;
PTYHandler(PTYHandler&&) = delete;
PTYHandler& operator=(PTYHandler&&) = delete;
PTYHandler(IOCPClient* client) : m_client(client), m_running(false), m_master_fd(-1), m_slave_fd(-1), m_child_pid(-1)
{
if (!client) {
throw std::invalid_argument("IOCPClient pointer cannot be null");
}
// Create pseudo terminal pair
if (openpty(&m_master_fd, &m_slave_fd, nullptr, nullptr, nullptr) == -1) {
throw std::runtime_error("Failed to create pseudo terminal");
}
// Set master fd to non-blocking mode
int flags = fcntl(m_master_fd, F_GETFL, 0);
fcntl(m_master_fd, F_SETFL, flags | O_NONBLOCK);
// Start shell process
startShell();
}
~PTYHandler()
{
m_running = false;
if (m_readThread.joinable()) {
m_readThread.join();
}
if (m_master_fd >= 0) {
close(m_master_fd);
}
if (m_slave_fd >= 0) {
close(m_slave_fd);
}
if (m_child_pid > 0) {
// Check if child is still running before killing
int status;
pid_t result = waitpid(m_child_pid, &status, WNOHANG);
if (result == 0) {
// Child still running, terminate it
kill(m_child_pid, SIGTERM);
waitpid(m_child_pid, nullptr, 0);
}
// If result == m_child_pid, child already exited and was reaped
// If result == -1, child was already reaped elsewhere
}
}
// Start the PTY read thread
void Start()
{
bool expected = false;
if (!m_running.compare_exchange_strong(expected, true)) return;
m_readThread = std::thread(&PTYHandler::readFromPTY, this);
}
// Handle incoming data from server
virtual VOID OnReceive(PBYTE data, ULONG size) override
{
if (size && data[0] == COMMAND_NEXT) {
Start();
return;
}
// Handle terminal resize command
if (size >= 5 && data[0] == CMD_TERMINAL_RESIZE) {
short cols, rows;
memcpy(&cols, data + 1, sizeof(short));
memcpy(&rows, data + 3, sizeof(short));
SetWindowSize(cols, rows);
return;
}
// Write data to PTY
if (size > 0) {
ssize_t total = 0;
while (total < (ssize_t)size) {
ssize_t written = write(m_master_fd, (char*)data + total, size - total);
if (written == -1) {
if (errno == EAGAIN || errno == EINTR) continue;
break;
}
total += written;
}
}
}
// Set terminal window size
void SetWindowSize(int cols, int rows)
{
struct winsize ws;
ws.ws_col = cols;
ws.ws_row = rows;
ws.ws_xpixel = 0;
ws.ws_ypixel = 0;
if (ioctl(m_master_fd, TIOCSWINSZ, &ws) == -1) {
return;
}
// Send SIGWINCH to child process to notify window size change
if (m_child_pid > 0) {
kill(m_child_pid, SIGWINCH);
}
}
private:
int m_master_fd;
int m_slave_fd;
IOCPClient* m_client;
std::thread m_readThread;
std::atomic<bool> m_running;
pid_t m_child_pid;
void startShell()
{
m_child_pid = fork();
if (m_child_pid == -1) {
close(m_master_fd);
close(m_slave_fd);
throw std::runtime_error("Failed to fork shell process");
}
if (m_child_pid == 0) {
// Child process
setsid(); // Create new session, become session leader
// Set slave PTY as controlling terminal (required for Ctrl+C to work)
// This must be done after setsid() and before dup2()
ioctl(m_slave_fd, TIOCSCTTY, 0);
// Redirect stdin/stdout/stderr to slave PTY
dup2(m_slave_fd, STDIN_FILENO);
dup2(m_slave_fd, STDOUT_FILENO);
dup2(m_slave_fd, STDERR_FILENO);
close(m_master_fd);
close(m_slave_fd);
// Set terminal environment for xterm.js compatibility
setenv("TERM", "xterm-256color", 1);
setenv("COLORTERM", "truecolor", 1);
#ifdef __APPLE__
// macOS locale settings
setenv("LANG", "en_US.UTF-8", 1);
setenv("LC_ALL", "en_US.UTF-8", 1);
// Disable zsh session save/restore (causes errors in PTY)
setenv("SHELL_SESSIONS_DISABLE", "1", 1);
// Try zsh first (macOS default), fallback to bash
if (access("/bin/zsh", X_OK) == 0) {
execl("/bin/zsh", "zsh", "-i", nullptr);
}
execl("/bin/bash", "bash", "-i", nullptr);
#else
// Linux locale settings (C.UTF-8 is most portable)
setenv("LANG", "C.UTF-8", 1);
setenv("LC_ALL", "C.UTF-8", 1);
// Start interactive bash
execl("/bin/bash", "bash", "-i", nullptr);
#endif
_exit(1);
}
}
void readFromPTY()
{
char buffer[4096];
while (m_running) {
// Check if child process has exited
int status;
pid_t result = waitpid(m_child_pid, &status, WNOHANG);
if (result == m_child_pid) {
// Shell exited, send close notification
if (m_client) {
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
m_client->Send2Server((char*)&closeToken, 1);
}
m_running = false;
break;
}
ssize_t bytes_read = read(m_master_fd, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
if (m_client) {
m_client->Send2Server(buffer, bytes_read);
}
} else if (bytes_read == 0) {
// EOF - PTY closed
if (m_client) {
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
m_client->Send2Server((char*)&closeToken, 1);
}
m_running = false;
break;
} else if (bytes_read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
usleep(10000); // 10ms
} else if (errno == EIO) {
// EIO typically means PTY slave closed (shell exited)
if (m_client) {
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
m_client->Send2Server((char*)&closeToken, 1);
}
m_running = false;
break;
} else {
break;
}
}
}
}
};

View File

@@ -330,6 +330,7 @@ enum {
CMD_SET_GROUP = 242, // 修改分组
CMD_EXECUTE_DLL_NEW = 243, // 执行代码
CMD_PEER_TO_PEER = 244, // P2P通信
TOKEN_CLIENTID = 245,
};
enum MachineCommand {
@@ -1048,6 +1049,14 @@ enum QualityLevel {
QUALITY_COUNT = 6,
};
// 屏幕压缩算法常量 (所有平台共用)
#ifndef ALGORITHM_GRAY
#define ALGORITHM_GRAY 0 // 灰度压缩
#define ALGORITHM_DIFF 1 // 差分压缩 (BGRA)
#define ALGORITHM_H264 2 // H264 硬件编码
#define ALGORITHM_RGB565 3 // RGB565 压缩
#endif
/* 质量配置(与 QualityLevel 对应)
- strategy = 01080p 限制
- strategy = 1原始分辨率

View File

@@ -321,6 +321,16 @@ inline const char* getFileName(const char* path)
#endif
#elif defined(_WIN32)
#define Mprintf(format, ...) Logger::getInstance().log(getFileName((__FILE__)), __LINE__, (format), __VA_ARGS__)
#elif defined(__APPLE__)
// macOS: 使用 NSLog 输出到系统日志(可通过 Console.app 查看)
#ifdef Mprintf
#undef Mprintf
#endif
#ifdef __OBJC__
#define Mprintf(format, ...) NSLog(@"%@", [NSString stringWithFormat:@(format), ##__VA_ARGS__])
#else
#define Mprintf(format, ...) printf(format, ##__VA_ARGS__)
#endif
#else
// Linux: 覆盖 commands.h 中的 printf 回退定义,改用 Logger 写文件
#ifdef Mprintf

View File

@@ -3,7 +3,8 @@
#include "client/IOCPClient.h"
#include "LinuxConfig.h"
#include "ClipboardHandler.h"
#include "FileTransferV2.h"
#include "common/FileTransferV2.h"
#include "X264Encoder.h"
#include <dlfcn.h>
#include <sys/stat.h>
#include <thread>
@@ -11,7 +12,9 @@
#include <atomic>
#include <vector>
#include <cstring>
#include <cstdlib>
#include <stdexcept>
#include <memory>
// 客户端 ID定义在 main.cpp
extern uint64_t g_myClientID;
@@ -110,27 +113,39 @@ struct XGCValues_LNX {
#define IncludeInferiors 1
// ============== 屏幕算法常量 ==============
#define ALGORITHM_GRAY 0
#define ALGORITHM_DIFF 1
#define ALGORITHM_H264 2
#define ALGORITHM_RGB565 3
// 常量定义已移至 commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565
// 算法支持表(编译时常量,日后支持 H264 时改为 true
static const bool g_SupportedAlgo[] = {
true, // ALGORITHM_GRAY = 0
true, // ALGORITHM_DIFF = 1
false, // ALGORITHM_H264 = 2
true, // ALGORITHM_RGB565 = 3
};
// 检查算法是否支持H264 需要运行时检测
inline bool IsAlgorithmSupported(uint8_t algo) {
switch (algo) {
case ALGORITHM_GRAY:
case ALGORITHM_DIFF:
case ALGORITHM_RGB565:
return true;
case ALGORITHM_H264:
return X264Encoder::IsAvailable();
default:
return false;
}
}
// 不支持的算法降级为 RGB565
inline uint8_t GetEffectiveAlgorithm(uint8_t algo) {
if (algo > 3 || !g_SupportedAlgo[algo]) {
if (!IsAlgorithmSupported(algo)) {
Mprintf(">>> Algorithm %d not supported, fallback to RGB565\n", algo);
return ALGORITHM_RGB565;
}
return algo;
}
// 码率到 CRF 映射 (参考 Windows/macOS 实现)
inline int BitRateToCRF(int bitrate) {
if (bitrate >= 3000) return 20; // 高质量
if (bitrate >= 2000) return 23; // 中等
if (bitrate >= 1200) return 26; // 较低
return 30; // 最低
}
// ============== 颜色转换函数 ==============
// BGRA → 灰度 (Y = 0.299R + 0.587G + 0.114B)
@@ -375,6 +390,16 @@ static unsigned long VKtoKeySym(unsigned int vk)
}
}
// XFixes cursor image structure (for cursor type detection)
struct XFixesCursorImage {
short x, y;
unsigned short width, height;
unsigned short xhot, yhot;
unsigned long cursor_serial;
unsigned long* pixels;
// Atom cursor_name; // Only in XFixes 2.0+
};
// X11 函数指针类型
typedef Display* (*fn_XOpenDisplay)(const char*);
typedef int (*fn_XCloseDisplay)(Display*);
@@ -391,12 +416,18 @@ typedef int (*fn_XSync)(Display*, int);
typedef unsigned long (*fn_XKeysymToKeycode)(Display*, unsigned long);
typedef int (*fn_XFlush)(Display*);
typedef int (*fn_XClearArea)(Display*, Window, int, int, unsigned int, unsigned int, int);
typedef int (*fn_XQueryPointer)(Display*, Window, Window*, Window*, int*, int*, int*, int*, unsigned int*);
typedef int (*fn_XFree)(void*);
// XTest 扩展函数指针类型(用于模拟鼠标/键盘输入)
typedef int (*fn_XTestFakeMotionEvent)(Display*, int, int, int, unsigned long);
typedef int (*fn_XTestFakeButtonEvent)(Display*, unsigned int, int, unsigned long);
typedef int (*fn_XTestFakeKeyEvent)(Display*, unsigned int, int, unsigned long);
// XFixes 扩展函数指针类型(用于光标类型检测)
typedef int (*fn_XFixesQueryExtension)(Display*, int*, int*);
typedef XFixesCursorImage* (*fn_XFixesGetCursorImage)(Display*);
// X11 动态加载包装
class X11Loader
{
@@ -430,13 +461,19 @@ public:
fn_XKeysymToKeycode pXKeysymToKeycode;
fn_XFlush pXFlush;
fn_XClearArea pXClearArea;
fn_XQueryPointer pXQueryPointer;
fn_XFree pXFree;
// XTest 扩展(用于模拟输入)
fn_XTestFakeMotionEvent pXTestFakeMotionEvent;
fn_XTestFakeButtonEvent pXTestFakeButtonEvent;
fn_XTestFakeKeyEvent pXTestFakeKeyEvent;
X11Loader() : m_handle(nullptr), m_xtst_handle(nullptr)
// XFixes 扩展(用于光标类型检测)
fn_XFixesQueryExtension pXFixesQueryExtension;
fn_XFixesGetCursorImage pXFixesGetCursorImage;
X11Loader() : m_handle(nullptr), m_xtst_handle(nullptr), m_xfixes_handle(nullptr)
{
pXOpenDisplay = nullptr;
pXCloseDisplay = nullptr;
@@ -457,9 +494,13 @@ public:
pXKeysymToKeycode = nullptr;
pXFlush = nullptr;
pXClearArea = nullptr;
pXQueryPointer = nullptr;
pXFree = nullptr;
pXTestFakeMotionEvent = nullptr;
pXTestFakeButtonEvent = nullptr;
pXTestFakeKeyEvent = nullptr;
pXFixesQueryExtension = nullptr;
pXFixesGetCursorImage = nullptr;
}
bool Load()
@@ -489,6 +530,8 @@ public:
pXKeysymToKeycode = (fn_XKeysymToKeycode)dlsym(m_handle, "XKeysymToKeycode");
pXFlush = (fn_XFlush)dlsym(m_handle, "XFlush");
pXClearArea = (fn_XClearArea)dlsym(m_handle, "XClearArea");
pXQueryPointer = (fn_XQueryPointer)dlsym(m_handle, "XQueryPointer");
pXFree = (fn_XFree)dlsym(m_handle, "XFree");
// 加载 XTest 扩展库(用于模拟鼠标/键盘输入)
m_xtst_handle = dlopen("libXtst.so.6", RTLD_LAZY);
@@ -499,7 +542,15 @@ public:
pXTestFakeKeyEvent = (fn_XTestFakeKeyEvent)dlsym(m_xtst_handle, "XTestFakeKeyEvent");
}
// 基本 X11 函数必须全部存在XTest 函数可选(没有时无法控制输入
// 加载 XFixes 扩展库(用于光标类型检测
m_xfixes_handle = dlopen("libXfixes.so.3", RTLD_LAZY);
if (!m_xfixes_handle) m_xfixes_handle = dlopen("libXfixes.so", RTLD_LAZY);
if (m_xfixes_handle) {
pXFixesQueryExtension = (fn_XFixesQueryExtension)dlsym(m_xfixes_handle, "XFixesQueryExtension");
pXFixesGetCursorImage = (fn_XFixesGetCursorImage)dlsym(m_xfixes_handle, "XFixesGetCursorImage");
}
// 基本 X11 函数必须全部存在XTest/XFixes 函数可选
return pXOpenDisplay && pXCloseDisplay && pXGetImage && pXDestroyImage &&
pXDefaultScreen && pXDisplayWidth && pXDisplayHeight && pXRootWindow &&
pXSetErrorHandler && pXCreatePixmap && pXFreePixmap &&
@@ -513,8 +564,18 @@ public:
return pXTestFakeMotionEvent && pXTestFakeButtonEvent && pXTestFakeKeyEvent;
}
// 检查 XFixes 扩展是否可用
bool HasXFixes() const
{
return pXFixesGetCursorImage != nullptr;
}
~X11Loader()
{
if (m_xfixes_handle) {
dlclose(m_xfixes_handle);
m_xfixes_handle = nullptr;
}
if (m_xtst_handle) {
dlclose(m_xtst_handle);
m_xtst_handle = nullptr;
@@ -528,6 +589,7 @@ public:
private:
void* m_handle;
void* m_xtst_handle;
void* m_xfixes_handle;
};
class ScreenHandler : public IOCPManager
@@ -538,7 +600,8 @@ public:
m_inputDisplay(nullptr),
m_width(0), m_height(0),
m_pixmap(0), m_gc(nullptr), m_xtestWarned(false),
m_bAlgorithm(ALGORITHM_DIFF), m_maxFPS(10), m_qualityLevel(QUALITY_ADAPTIVE)
m_bAlgorithm(ALGORITHM_DIFF), m_maxFPS(10), m_qualityLevel(QUALITY_ADAPTIVE),
m_h264Bitrate(2000)
{
if (!client) {
throw std::invalid_argument("IOCPClient pointer cannot be null");
@@ -651,11 +714,21 @@ public:
// Double-check after acquiring lock
if (m_destroyed) return;
// Prevent starting if thread is already running or joinable
if (m_captureThread.joinable()) return;
// If already running, just send TOKEN_BITMAPINFO again
// This allows server to create additional dialogs (MFC can open while Web is active)
if (m_captureThread.joinable() || m_running.load()) {
Mprintf(">>> ScreenHandler already running, sending TOKEN_BITMAPINFO for new dialog\n");
SendBitmapInfo();
return;
}
bool expected = false;
if (!m_running.compare_exchange_strong(expected, true)) return;
if (!m_running.compare_exchange_strong(expected, true)) {
// Race condition: another thread started first, send bitmap info
Mprintf(">>> ScreenHandler race, sending TOKEN_BITMAPINFO for new dialog\n");
SendBitmapInfo();
return;
}
m_captureThread = std::thread(&ScreenHandler::CaptureLoop, this);
}
@@ -873,12 +946,28 @@ public:
// 应用帧率
m_maxFPS.store(profile.maxFPS);
// 应用码率H264 使用)
int oldBitrate = m_h264Bitrate;
m_h264Bitrate = profile.bitRate;
// 应用算法(带降级处理)
uint8_t algo = GetEffectiveAlgorithm(profile.algorithm);
uint8_t oldAlgo = m_bAlgorithm.load();
m_bAlgorithm.store(algo);
Mprintf(">>> Quality: Level=%d, FPS=%d, Algo=%d->%d\n",
level, profile.maxFPS, profile.algorithm, algo);
// 如果 H264 参数变化,需要重新初始化编码器
if (algo == ALGORITHM_H264 && oldAlgo == ALGORITHM_H264 &&
(oldBitrate != m_h264Bitrate)) {
// 码率变化,重置编码器(下次编码时重新初始化)
if (m_h264Encoder) {
m_h264Encoder->close();
m_h264Encoder.reset();
Mprintf(">>> H264 encoder reset due to bitrate change\n");
}
}
Mprintf(">>> Quality: Level=%d, FPS=%d, Algo=%d->%d, Bitrate=%d\n",
level, profile.maxFPS, profile.algorithm, algo, profile.bitRate);
} else {
// 自适应模式 (level=-1):由服务端动态调整,不做处理
Mprintf(">>> Quality: Adaptive mode\n");
@@ -1044,11 +1133,15 @@ private:
std::vector<uint8_t> m_diffBuffer;
// 自适应质量控制
std::atomic<uint8_t> m_bAlgorithm; // 当前算法 (ALGORITHM_DIFF/RGB565/GRAY)
std::atomic<uint8_t> m_bAlgorithm; // 当前算法 (ALGORITHM_DIFF/RGB565/GRAY/H264)
std::atomic<int> m_maxFPS; // 最大帧率
int8_t m_qualityLevel; // 当前质量等级 (-1=自适应, 0-5=具体等级)
LinuxConfig m_config; // 配置持久化 (~/.config/ghost/config.conf)
// H264 编码器
std::unique_ptr<X264Encoder> m_h264Encoder;
int m_h264Bitrate; // 码率 (kbps)
// X11 截屏,输出 BGRA 格式(自底向上,与 BMP 一致)
// 使用 XCopyArea 将 root window 拷贝到离屏 Pixmap再对 Pixmap 调用 XGetImage
// 这样可以避免合成窗口管理器Mutter 等)导致的 BadMatch 错误
@@ -1120,13 +1213,14 @@ private:
uint8_t algo = m_bAlgorithm.load();
memcpy(data, &algo, sizeof(uint8_t));
// 写入光标位置 (Linux 端简单置 0)
// 写入光标位置
int32_t cursorX = 0, cursorY = 0;
GetCursorPosition(cursorX, cursorY);
memcpy(data + 1, &cursorX, sizeof(int32_t));
memcpy(data + 1 + sizeof(int32_t), &cursorY, sizeof(int32_t));
// 写入光标类型
uint8_t cursorType = 0;
// 写入光标类型 (使用 XFixes 检测)
uint8_t cursorType = GetCursorTypeIndex();
memcpy(data + 1 + 2 * sizeof(int32_t), &cursorType, sizeof(uint8_t));
uint32_t headerSize = 1 + 2 * sizeof(int32_t) + 1; // algo + cursor + cursorType
@@ -1141,6 +1235,60 @@ private:
std::swap(m_prevFrame, m_currFrame);
}
// 发送 H264 编码帧
void SendH264Frame(bool forceKeyframe = false)
{
if (!CaptureScreen(m_currFrame)) return;
if (!m_client) return;
// 惰性初始化编码器
if (!m_h264Encoder) {
m_h264Encoder.reset(new X264Encoder());
int fps = m_maxFPS.load();
if (fps <= 0) fps = 20;
int crf = BitRateToCRF(m_h264Bitrate);
if (!m_h264Encoder->open(m_bmpHeader.biWidth, m_bmpHeader.biHeight, fps, crf)) {
Mprintf("*** H264 encoder init failed, falling back to RGB565\n");
m_bAlgorithm.store(ALGORITHM_RGB565);
m_h264Encoder.reset();
SendDiffFrame();
return;
}
Mprintf(">>> H264 encoder initialized: %dx%d @ %d fps, CRF=%d\n",
m_bmpHeader.biWidth, m_bmpHeader.biHeight, fps, crf);
}
// 编码当前帧
uint8_t* encodedData = nullptr;
uint32_t encodedSize = 0;
// direction=1 表示 bottom-up (BMP 格式)
int result = m_h264Encoder->encode(
m_currFrame.data(), 32, m_bmpHeader.biWidth * 4,
m_bmpHeader.biWidth, m_bmpHeader.biHeight,
&encodedData, &encodedSize, 1);
if (result != 0 || !encodedData || encodedSize == 0) {
return;
}
// 构建数据包: [TOKEN_NEXTSCREEN][algo][cursorX][cursorY][cursorType][H264Data]
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;
int32_t cursorX = 0, cursorY = 0;
GetCursorPosition(cursorX, cursorY);
memcpy(&packet[2], &cursorX, sizeof(int32_t));
memcpy(&packet[6], &cursorY, sizeof(int32_t));
packet[10] = GetCursorTypeIndex(); // 使用 XFixes 检测光标类型
memcpy(&packet[headerSize], encodedData, encodedSize);
m_client->Send2Server((char*)packet.data(), packet.size());
}
// 差异比较算法(支持 DIFF/RGB565/GRAY
// 输出格式: [byteOffset(4) + length(4) + pixel data] ...
// DIFF: length = 字节数, data = BGRA 原始数据
@@ -1224,6 +1372,118 @@ private:
return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
// 获取光标位置
void GetCursorPosition(int32_t& x, int32_t& y)
{
x = 0;
y = 0;
// 检查是否正在运行和资源是否有效
if (!m_running.load() || m_destroyed.load()) {
static bool warned = false;
if (!warned) {
Mprintf("*** GetCursorPosition: skipped (running=%d, destroyed=%d)\n",
m_running.load(), m_destroyed.load());
warned = true;
}
return;
}
Display* display = m_display; // 局部拷贝
if (!display || !m_x11.pXQueryPointer) {
static bool warned = false;
if (!warned) {
Mprintf("*** GetCursorPosition: display=%p, pXQueryPointer=%p\n",
(void*)display, (void*)m_x11.pXQueryPointer);
warned = true;
}
return;
}
Window root_return, child_return;
int root_x, root_y, win_x, win_y;
unsigned int mask;
if (m_x11.pXQueryPointer(display, m_root, &root_return, &child_return,
&root_x, &root_y, &win_x, &win_y, &mask)) {
x = root_x;
y = root_y;
// 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;
}
}
// 获取光标类型索引(映射到 Windows 光标类型)
// 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
uint8_t GetCursorTypeIndex()
{
// Cache result and throttle to avoid performance impact
static uint8_t cachedIndex = 1; // Default: IDC_ARROW
static uint64_t lastCheckTime = 0;
static unsigned long lastCursorSerial = 0;
// Throttle: check at most every 100ms
uint64_t now = GetTickMs();
if ((now - lastCheckTime) < 100) {
return cachedIndex;
}
lastCheckTime = now;
// Check if XFixes is available and XFree is loaded
if (!m_x11.HasXFixes() || !m_x11.pXFree || !m_display) {
return 1; // IDC_ARROW
}
// Get current cursor image
XFixesCursorImage* cursorImg = m_x11.pXFixesGetCursorImage(m_display);
if (!cursorImg) {
return cachedIndex;
}
// Check if cursor changed (using serial number)
if (cursorImg->cursor_serial == lastCursorSerial) {
// Cursor hasn't changed, use cached value
// Note: We need to free the cursor image
// XFixes allocates this with Xlib's allocator
m_x11.pXFree(cursorImg);
return cachedIndex;
}
lastCursorSerial = cursorImg->cursor_serial;
// Analyze cursor characteristics to determine type
uint8_t index = 1; // Default to IDC_ARROW
unsigned short w = cursorImg->width;
unsigned short h = cursorImg->height;
unsigned short xhot = cursorImg->xhot;
unsigned short yhot = cursorImg->yhot;
// Heuristic-based cursor type detection (conservative approach):
// Only detect distinctive cursor types to minimize false positives
// IBEAM (text cursor): very narrow, tall cursor
if (w <= 8 && h >= 12 && xhot <= w/2 + 1) {
index = 5; // IDC_IBEAM
}
// HAND (pointing): hotspot at top-left area (finger tip)
else if (w >= 18 && h >= 20 && xhot <= 10 && yhot <= 5) {
index = 3; // IDC_HAND
}
// All other cursors default to ARROW
cachedIndex = index;
m_x11.pXFree(cursorImg);
return index;
}
// 截屏主循环
void CaptureLoop()
{
@@ -1233,10 +1493,34 @@ private:
// 发送第一帧
SendFirstScreen();
uint8_t currentAlgo = m_bAlgorithm.load();
while (m_running) {
uint64_t start = GetTickMs();
uint8_t algo = m_bAlgorithm.load();
SendDiffFrame();
// 算法切换处理
if (algo != currentAlgo) {
currentAlgo = algo;
if (algo == ALGORITHM_H264) {
// 切换到 H264发送关键帧
SendH264Frame(true);
} else {
// 切换离开 H264关闭编码器并发送完整帧
if (m_h264Encoder) {
m_h264Encoder->close();
m_h264Encoder.reset();
}
SendFirstScreen();
}
} else {
// 正常帧
if (algo == ALGORITHM_H264) {
SendH264Frame(false);
} else {
SendDiffFrame();
}
}
// 动态计算帧间隔(根据当前 maxFPS
int fps = m_maxFPS.load();
@@ -1250,6 +1534,12 @@ private:
}
}
// 清理 H264 编码器
if (m_h264Encoder) {
m_h264Encoder->close();
m_h264Encoder.reset();
}
Mprintf(">>> ScreenHandler CaptureLoop stopped\n");
} catch (const std::exception& e) {
Mprintf("*** CaptureLoop exception: %s ***\n", e.what());

471
linux/X264Encoder.h Normal file
View File

@@ -0,0 +1,471 @@
#pragma once
/**
* X264Encoder.h - Linux H264 Encoder using libx264
*
* Features:
* - Dynamic library loading (dlopen/dlsym)
* - Automatic fallback if libx264 not available
* - Manual BGRA→I420 conversion (no libyuv dependency)
* - API compatible with Windows X264Encoder
*
* Requirements:
* - libx264 installed (apt install libx264-dev)
* - If not installed, H264 encoding is disabled
*/
#include <stdint.h>
#include <cstring>
#include <cstdlib>
#include <dlfcn.h>
#include <vector>
// Include x264 header for struct definitions
// The library is dynamically loaded at runtime
extern "C" {
#include "../compress/x264/x264.h"
}
// ============== X264Encoder Class ==============
class X264Encoder {
public:
// Check if libx264 is available on this system
static bool IsAvailable() {
static int available = -1;
if (available < 0) {
void* handle = TryLoadLibrary();
available = (handle != nullptr) ? 1 : 0;
if (handle) {
dlclose(handle);
fprintf(stderr, ">>> X264Encoder: libx264 available\n");
} else {
fprintf(stderr, "*** X264Encoder: libx264 not found (%s)\n", dlerror());
}
}
return available == 1;
}
X264Encoder()
: m_x264Handle(nullptr)
, m_encoder(nullptr)
, m_picIn(nullptr)
, m_picOut(nullptr)
, m_width(0)
, m_height(0)
{
memset(&m_param, 0, sizeof(m_param));
clearFunctionPointers();
}
~X264Encoder() {
close();
}
bool open(int width, int height, int fps, int crf) {
close();
// Load library
if (!loadLibrary()) {
fprintf(stderr, "*** X264Encoder::open: loadLibrary failed\n");
return false;
}
// Round to even dimensions (H264 requirement)
m_width = width & ~1;
m_height = height & ~1;
// Initialize parameters
if (fn_x264_param_default_preset) {
fn_x264_param_default_preset(&m_param, "ultrafast", "zerolatency");
}
// Set encoder parameters
m_param.i_width = m_width;
m_param.i_height = m_height;
m_param.i_log_level = X264_LOG_NONE;
m_param.i_threads = 1;
m_param.i_frame_total = 0;
m_param.i_keyint_max = fps * 15; // Keyframe every 15 seconds
m_param.i_bframe = 0; // No B-frames for low latency
m_param.b_open_gop = 0;
m_param.i_fps_num = fps;
m_param.i_fps_den = 1;
m_param.i_csp = X264_CSP_I420;
// Rate control: CRF mode
m_param.rc.i_rc_method = X264_RC_CRF;
m_param.rc.f_rf_constant = (float)crf;
// Apply baseline profile for compatibility
if (fn_x264_param_apply_profile) {
fn_x264_param_apply_profile(&m_param, "baseline");
}
// Allocate pictures
m_picIn = (x264_picture_t*)calloc(1, sizeof(x264_picture_t));
m_picOut = (x264_picture_t*)calloc(1, sizeof(x264_picture_t));
if (!m_picIn || !m_picOut) {
close();
return false;
}
// Initialize input picture
if (fn_x264_picture_init) {
fn_x264_picture_init(m_picIn);
}
// Allocate picture buffer
if (fn_x264_picture_alloc) {
if (fn_x264_picture_alloc(m_picIn, X264_CSP_I420, m_width, m_height) < 0) {
close();
return false;
}
}
// Open encoder
m_encoder = fn_x264_encoder_open(&m_param);
if (!m_encoder) {
close();
return false;
}
return true;
}
void close() {
if (m_encoder && fn_x264_encoder_close) {
fn_x264_encoder_close(m_encoder);
m_encoder = nullptr;
}
if (m_picIn) {
if (fn_x264_picture_clean) {
fn_x264_picture_clean(m_picIn);
}
free(m_picIn);
m_picIn = nullptr;
}
if (m_picOut) {
free(m_picOut);
m_picOut = nullptr;
}
unloadLibrary();
m_width = m_height = 0;
}
/**
* Encode a frame
* @param bgra Input BGRA image data
* @param bpp Bits per pixel (24 or 32)
* @param stride Bytes per row
* @param width Image width
* @param height Image height
* @param outData Output: pointer to encoded H264 data
* @param outSize Output: size of encoded data
* @param direction 1 = normal, -1 = vertical flip
* @return 0 on success, negative on error
*/
int encode(uint8_t* bgra, uint8_t bpp, uint32_t stride,
uint32_t width, uint32_t height,
uint8_t** outData, uint32_t* outSize,
int direction = 1)
{
if (!m_encoder || !m_picIn || !fn_x264_encoder_encode) {
return -1;
}
// Check dimensions match
if ((int)(width & ~1) != m_width || (int)(height & ~1) != m_height) {
return -2;
}
// Convert BGRA to I420 directly into x264 picture planes
if (bpp == 32) {
convertBGRAtoI420(bgra, stride, direction,
m_picIn->img.plane[0], m_picIn->img.i_stride[0],
m_picIn->img.plane[1], m_picIn->img.i_stride[1],
m_picIn->img.plane[2], m_picIn->img.i_stride[2]);
} else if (bpp == 24) {
convertRGB24toI420(bgra, stride, direction,
m_picIn->img.plane[0], m_picIn->img.i_stride[0],
m_picIn->img.plane[1], m_picIn->img.i_stride[1],
m_picIn->img.plane[2], m_picIn->img.i_stride[2]);
} else {
return -3;
}
// Encode
x264_nal_t* pNal = nullptr;
int iNal = 0;
int encodeSize = fn_x264_encoder_encode(m_encoder, &pNal, &iNal, m_picIn, m_picOut);
if (encodeSize < 0) {
return -4;
}
if (encodeSize == 0 || !pNal) {
*outData = nullptr;
*outSize = 0;
return 0;
}
*outData = pNal->p_payload;
*outSize = encodeSize;
return 0;
}
private:
// Library handle
void* m_x264Handle;
// Encoder state
x264_t* m_encoder;
x264_param_t m_param;
x264_picture_t* m_picIn;
x264_picture_t* m_picOut;
int m_width, m_height;
// x264 function pointers
void (*fn_x264_param_default_preset)(x264_param_t*, const char*, const char*);
int (*fn_x264_param_apply_profile)(x264_param_t*, const char*);
x264_t* (*fn_x264_encoder_open)(x264_param_t*);
void (*fn_x264_encoder_close)(x264_t*);
int (*fn_x264_encoder_encode)(x264_t*, x264_nal_t**, int*, x264_picture_t*, x264_picture_t*);
void (*fn_x264_picture_init)(x264_picture_t*);
int (*fn_x264_picture_alloc)(x264_picture_t*, int, int, int);
void (*fn_x264_picture_clean)(x264_picture_t*);
void clearFunctionPointers() {
fn_x264_param_default_preset = nullptr;
fn_x264_param_apply_profile = nullptr;
fn_x264_encoder_open = nullptr;
fn_x264_encoder_close = nullptr;
fn_x264_encoder_encode = nullptr;
fn_x264_picture_init = nullptr;
fn_x264_picture_alloc = nullptr;
fn_x264_picture_clean = nullptr;
}
static void* TryLoadLibrary() {
// Try multiple library versions (newest first)
const char* libNames[] = {
"libx264.so", // symlink (if exists)
"libx264.so.164", // Ubuntu 24, Debian 12+
"libx264.so.163",
"libx264.so.162",
"libx264.so.161",
"libx264.so.160",
"libx264.so.159",
"libx264.so.157",
"libx264.so.155", // Ubuntu 20
"libx264.so.152",
"libx264.so.148", // older distros
nullptr
};
for (int i = 0; libNames[i]; i++) {
void* handle = dlopen(libNames[i], RTLD_LAZY);
if (handle) return handle;
}
return nullptr;
}
bool loadLibrary() {
m_x264Handle = TryLoadLibrary();
if (!m_x264Handle) {
return false;
}
// Load functions
fn_x264_param_default_preset = (decltype(fn_x264_param_default_preset))
dlsym(m_x264Handle, "x264_param_default_preset");
fn_x264_param_apply_profile = (decltype(fn_x264_param_apply_profile))
dlsym(m_x264Handle, "x264_param_apply_profile");
fn_x264_picture_init = (decltype(fn_x264_picture_init))
dlsym(m_x264Handle, "x264_picture_init");
fn_x264_picture_alloc = (decltype(fn_x264_picture_alloc))
dlsym(m_x264Handle, "x264_picture_alloc");
fn_x264_picture_clean = (decltype(fn_x264_picture_clean))
dlsym(m_x264Handle, "x264_picture_clean");
fn_x264_encoder_close = (decltype(fn_x264_encoder_close))
dlsym(m_x264Handle, "x264_encoder_close");
// x264_encoder_open has version suffix based on X264_BUILD
// Try common versions in order (newest first)
const char* openNames[] = {
"x264_encoder_open_164",
"x264_encoder_open_163",
"x264_encoder_open_162",
"x264_encoder_open_161",
"x264_encoder_open_160",
"x264_encoder_open_159",
"x264_encoder_open_157",
"x264_encoder_open_155",
"x264_encoder_open_152",
"x264_encoder_open_148",
nullptr
};
for (int i = 0; openNames[i]; i++) {
fn_x264_encoder_open = (decltype(fn_x264_encoder_open))
dlsym(m_x264Handle, openNames[i]);
if (fn_x264_encoder_open) {
fprintf(stderr, ">>> X264Encoder: found %s\n", openNames[i]);
break;
}
}
fn_x264_encoder_encode = (decltype(fn_x264_encoder_encode))
dlsym(m_x264Handle, "x264_encoder_encode");
// Check required functions
if (!fn_x264_encoder_open || !fn_x264_encoder_encode || !fn_x264_encoder_close ||
!fn_x264_param_default_preset || !fn_x264_picture_alloc) {
fprintf(stderr, "*** X264Encoder: missing functions - open=%p encode=%p close=%p preset=%p alloc=%p\n",
(void*)fn_x264_encoder_open, (void*)fn_x264_encoder_encode,
(void*)fn_x264_encoder_close, (void*)fn_x264_param_default_preset,
(void*)fn_x264_picture_alloc);
unloadLibrary();
return false;
}
return true;
}
void unloadLibrary() {
if (m_x264Handle) {
dlclose(m_x264Handle);
m_x264Handle = nullptr;
}
clearFunctionPointers();
}
/**
* Convert BGRA to I420 (YUV 4:2:0 planar) directly into output planes
* Using ITU-R BT.601 coefficients
*/
void convertBGRAtoI420(const uint8_t* bgra, int stride, int direction,
uint8_t* yPlane, int yStride,
uint8_t* uPlane, int uStride,
uint8_t* vPlane, int vStride) {
int srcStride = stride;
int w = m_width;
int h = m_height;
// Direction: 1 = normal, -1 = flip vertically
int startY = (direction > 0) ? 0 : (h - 1);
int stepY = (direction > 0) ? 1 : -1;
// Y plane: full resolution
for (int j = 0; j < h; j++) {
int srcY = startY + j * stepY;
const uint8_t* srcRow = bgra + srcY * srcStride;
uint8_t* dstRow = yPlane + j * yStride;
for (int i = 0; i < w; i++) {
uint8_t b = srcRow[i * 4 + 0];
uint8_t g = srcRow[i * 4 + 1];
uint8_t r = srcRow[i * 4 + 2];
// Y = 0.257*R + 0.504*G + 0.098*B + 16
int y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
dstRow[i] = (uint8_t)(y < 0 ? 0 : (y > 255 ? 255 : y));
}
}
// U/V planes: half resolution (2x2 block averaging)
int uvW = w / 2;
int uvH = h / 2;
for (int j = 0; j < uvH; j++) {
int srcY0 = startY + (j * 2) * stepY;
int srcY1 = startY + (j * 2 + 1) * stepY;
const uint8_t* row0 = bgra + srcY0 * srcStride;
const uint8_t* row1 = bgra + srcY1 * srcStride;
for (int i = 0; i < uvW; i++) {
// Average 4 pixels
int r = 0, g = 0, b = 0;
b += row0[(i*2+0)*4 + 0]; g += row0[(i*2+0)*4 + 1]; r += row0[(i*2+0)*4 + 2];
b += row0[(i*2+1)*4 + 0]; g += row0[(i*2+1)*4 + 1]; r += row0[(i*2+1)*4 + 2];
b += row1[(i*2+0)*4 + 0]; g += row1[(i*2+0)*4 + 1]; r += row1[(i*2+0)*4 + 2];
b += row1[(i*2+1)*4 + 0]; g += row1[(i*2+1)*4 + 1]; r += row1[(i*2+1)*4 + 2];
r >>= 2; g >>= 2; b >>= 2;
// U = -0.148*R - 0.291*G + 0.439*B + 128
int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
// V = 0.439*R - 0.368*G - 0.071*B + 128
int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
uPlane[j * uStride + i] = (uint8_t)(u < 0 ? 0 : (u > 255 ? 255 : u));
vPlane[j * vStride + i] = (uint8_t)(v < 0 ? 0 : (v > 255 ? 255 : v));
}
}
}
/**
* Convert RGB24 to I420 (YUV 4:2:0 planar) directly into output planes
*/
void convertRGB24toI420(const uint8_t* rgb, int stride, int direction,
uint8_t* yPlane, int yStride,
uint8_t* uPlane, int uStride,
uint8_t* vPlane, int vStride) {
int srcStride = stride;
int w = m_width;
int h = m_height;
int startY = (direction > 0) ? 0 : (h - 1);
int stepY = (direction > 0) ? 1 : -1;
// Y plane
for (int j = 0; j < h; j++) {
int srcY = startY + j * stepY;
const uint8_t* srcRow = rgb + srcY * srcStride;
uint8_t* dstRow = yPlane + j * yStride;
for (int i = 0; i < w; i++) {
uint8_t r = srcRow[i * 3 + 0];
uint8_t g = srcRow[i * 3 + 1];
uint8_t b = srcRow[i * 3 + 2];
int y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
dstRow[i] = (uint8_t)(y < 0 ? 0 : (y > 255 ? 255 : y));
}
}
// U/V planes
int uvW = w / 2;
int uvH = h / 2;
for (int j = 0; j < uvH; j++) {
int srcY0 = startY + (j * 2) * stepY;
int srcY1 = startY + (j * 2 + 1) * stepY;
const uint8_t* row0 = rgb + srcY0 * srcStride;
const uint8_t* row1 = rgb + srcY1 * srcStride;
for (int i = 0; i < uvW; i++) {
int r = 0, g = 0, b = 0;
r += row0[(i*2+0)*3 + 0]; g += row0[(i*2+0)*3 + 1]; b += row0[(i*2+0)*3 + 2];
r += row0[(i*2+1)*3 + 0]; g += row0[(i*2+1)*3 + 1]; b += row0[(i*2+1)*3 + 2];
r += row1[(i*2+0)*3 + 0]; g += row1[(i*2+0)*3 + 1]; b += row1[(i*2+0)*3 + 2];
r += row1[(i*2+1)*3 + 0]; g += row1[(i*2+1)*3 + 1]; b += row1[(i*2+1)*3 + 2];
r >>= 2; g >>= 2; b >>= 2;
int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
uPlane[j * uStride + i] = (uint8_t)(u < 0 ? 0 : (u > 255 ? 255 : u));
vPlane[j * vStride + i] = (uint8_t)(v < 0 ? 0 : (v > 255 ? 255 : v));
}
}
}
};

View File

@@ -14,7 +14,7 @@
#include <csignal>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <pty.h>
#include "common/PTYHandler.h"
#include <iostream>
#include <stdexcept>
#include <cstdio>
@@ -26,9 +26,9 @@
#include <cmath>
#include "ScreenHandler.h"
#include "SystemManager.h"
#include "FileManager.h"
#include "common/FileManager.h"
#include "ClipboardHandler.h"
#include "FileTransferV2.h"
#include "common/FileTransferV2.h"
#include "common/logger.h"
#define XXH_INLINE_ALL
#include "common/xxhash.h"
@@ -37,10 +37,11 @@
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength);
// 远程地址:当前为写死状态,如需调试,请按实际情况修改
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "192.168.0.55", "6543", CLIENT_TYPE_LINUX };
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_LINUX };
// 全局状态
State g_bExit = S_CLIENT_NORMAL;
static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重发登录信息
// 客户端 IDV2 文件传输需要)
uint64_t g_myClientID = 0;
@@ -338,187 +339,7 @@ struct RttEstimator {
RttEstimator g_rttEstimator;
int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
// 伪终端处理类继承自IOCPManager.
class PTYHandler : public IOCPManager
{
public:
PTYHandler(IOCPClient* client) : m_client(client), m_running(false)
{
if (!client) {
throw std::invalid_argument("IOCPClient pointer cannot be null");
}
// 创建伪终端
if (openpty(&m_master_fd, &m_slave_fd, nullptr, nullptr, nullptr) == -1) {
throw std::runtime_error("Failed to create pseudo terminal");
}
// 设置伪终端为非阻塞模式
int flags = fcntl(m_master_fd, F_GETFL, 0);
fcntl(m_master_fd, F_SETFL, flags | O_NONBLOCK);
// 启动 Shell 进程
startShell();
}
~PTYHandler()
{
m_running = false;
if (m_readThread.joinable()) m_readThread.join();
close(m_master_fd);
close(m_slave_fd);
if (m_child_pid > 0) {
kill(m_child_pid, SIGTERM);
waitpid(m_child_pid, nullptr, 0);
}
}
// 启动读取线程
void Start()
{
bool expected = false;
if (!m_running.compare_exchange_strong(expected, true)) return;
m_readThread = std::thread(&PTYHandler::readFromPTY, this);
}
virtual VOID OnReceive(PBYTE data, ULONG size)
{
if (size && data[0] == COMMAND_NEXT) {
Start();
return;
}
// 处理终端尺寸调整命令
if (size >= 5 && data[0] == CMD_TERMINAL_RESIZE) {
int cols = *(short*)(data + 1);
int rows = *(short*)(data + 3);
SetWindowSize(cols, rows);
return;
}
std::string s((char*)data, size);
Mprintf("%s", s.c_str());
if (size > 0) {
ssize_t total = 0;
while (total < (ssize_t)size) {
ssize_t written = write(m_master_fd, (char*)data + total, size - total);
if (written == -1) {
if (errno == EAGAIN || errno == EINTR) continue;
Mprintf("OnReceive: write error %d\n", errno);
break;
}
total += written;
}
}
}
// 设置终端窗口尺寸
void SetWindowSize(int cols, int rows)
{
struct winsize ws;
ws.ws_col = cols;
ws.ws_row = rows;
ws.ws_xpixel = 0;
ws.ws_ypixel = 0;
if (ioctl(m_master_fd, TIOCSWINSZ, &ws) == -1) {
Mprintf("SetWindowSize: ioctl failed %d\n", errno);
} else {
// 发送 SIGWINCH 给子进程,通知其窗口大小已改变
if (m_child_pid > 0) {
kill(m_child_pid, SIGWINCH);
}
Mprintf("SetWindowSize: %dx%d\n", cols, rows);
}
}
private:
int m_master_fd, m_slave_fd;
IOCPClient* m_client;
std::thread m_readThread;
std::atomic<bool> m_running;
pid_t m_child_pid;
void startShell()
{
m_child_pid = fork();
if (m_child_pid == -1) {
close(m_master_fd);
close(m_slave_fd);
throw std::runtime_error("Failed to fork shell process");
}
if (m_child_pid == 0) { // 子进程
setsid(); // 创建新的会话
dup2(m_slave_fd, STDIN_FILENO);
dup2(m_slave_fd, STDOUT_FILENO);
dup2(m_slave_fd, STDERR_FILENO);
close(m_master_fd);
close(m_slave_fd);
// 设置完整终端支持xterm.js 终端仿真)
setenv("TERM", "xterm-256color", 1);
setenv("COLORTERM", "truecolor", 1);
// 使用 C.UTF-8 是最通用的 UTF-8 locale几乎所有 Linux 都支持
setenv("LANG", "C.UTF-8", 1);
setenv("LC_ALL", "C.UTF-8", 1);
// 启动交互式 Bash
execl("/bin/bash", "bash", "-i", nullptr);
exit(1);
}
}
void readFromPTY()
{
char buffer[4096];
while (m_running) {
// 检查子进程是否已退出
int status;
pid_t result = waitpid(m_child_pid, &status, WNOHANG);
if (result == m_child_pid) {
// Shell 已退出,发送关闭通知
Mprintf("readFromPTY: shell exited (status=%d)\n", WEXITSTATUS(status));
if (m_client) {
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
m_client->Send2Server((char*)&closeToken, 1);
}
m_running = false;
break;
}
ssize_t bytes_read = read(m_master_fd, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
if (m_client) {
buffer[bytes_read] = '\0';
Mprintf("%s", buffer);
m_client->Send2Server(buffer, bytes_read);
}
} else if (bytes_read == 0) {
// EOF - PTY 已关闭
Mprintf("readFromPTY: EOF (shell closed)\n");
if (m_client) {
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
m_client->Send2Server((char*)&closeToken, 1);
}
m_running = false;
break;
} else if (bytes_read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
usleep(10000);
} else if (errno == EIO) {
// EIO 通常表示 PTY slave 已关闭shell 退出)
Mprintf("readFromPTY: EIO (shell closed)\n");
if (m_client) {
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
m_client->Send2Server((char*)&closeToken, 1);
}
m_running = false;
break;
} else {
Mprintf("readFromPTY: read error %d\n", errno);
break;
}
}
}
}
};
// PTYHandler moved to common/PTYHandler.h (shared between Linux and macOS)
void* ShellworkingThread(void* param)
{
@@ -534,6 +355,8 @@ void* ShellworkingThread(void* param)
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
Mprintf(">>> Leave ShellworkingThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
@@ -556,6 +379,8 @@ void* ScreenworkingThread(void* param)
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
@@ -576,6 +401,8 @@ void* SystemManagerThread(void* param)
Mprintf(">>> SystemManagerThread [%p] Send: TOKEN_PSLIST\n", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
Mprintf(">>> Leave SystemManagerThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
@@ -596,6 +423,8 @@ void* FileManagerThread(void* param)
Mprintf(">>> FileManagerThread [%p] Send: TOKEN_DRIVE_LIST\n", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
Mprintf(">>> Leave FileManagerThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
@@ -672,6 +501,26 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
if (result != 0) {
Mprintf("** [%p] V2 File recv error: %d ***\n", user, result);
}
} else if (szBuffer[0] == CMD_SET_GROUP) {
// Extract group name from message (starts at byte 1)
std::string groupName;
if (ulLength > 1) {
groupName = std::string((char*)szBuffer + 1, ulLength - 1);
// Remove trailing nulls
size_t pos = groupName.find('\0');
if (pos != std::string::npos) {
groupName = groupName.substr(0, pos);
}
}
// Save to config file
LinuxConfig cfg;
cfg.SetStr("group_name", groupName);
// Update global settings
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
// 标记需要重发登录信息(让服务端更新分组显示)
g_needResendLogin.store(true);
Mprintf("** [%p] Group changed to: %s ***\n", user, groupName.c_str());
} else {
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
}
@@ -1077,8 +926,23 @@ int main(int argc, char* argv[])
LOGIN_INFOR logInfo;
// 主机名
strncpy(logInfo.szPCName, hostname, sizeof(logInfo.szPCName) - 1);
// 读取分组名称(从配置文件或 g_SETTINGS
LinuxConfig cfgGroup;
std::string groupName = cfgGroup.GetStr("group_name");
if (!groupName.empty()) {
// 更新 g_SETTINGS
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
} else if (g_SETTINGS.szGroupName[0] != 0) {
groupName = g_SETTINGS.szGroupName;
}
// 主机名带分组hostname/groupname
if (!groupName.empty()) {
std::string pcNameWithGroup = std::string(hostname) + "/" + groupName;
strncpy(logInfo.szPCName, pcNameWithGroup.c_str(), sizeof(logInfo.szPCName) - 1);
} else {
strncpy(logInfo.szPCName, hostname, sizeof(logInfo.szPCName) - 1);
}
logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0';
// 操作系统版本(如 "Ubuntu 24.04 LTS"
@@ -1178,6 +1042,21 @@ int main(int argc, char* argv[])
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) {
// 检查是否需要重发登录信息(分组变更后)
if (g_needResendLogin.exchange(false)) {
// 更新 szPCNamehostname/groupname 格式)
std::string grp = g_SETTINGS.szGroupName;
if (!grp.empty()) {
std::string pcNameWithGroup = std::string(hostname) + "/" + grp;
strncpy(logInfo.szPCName, pcNameWithGroup.c_str(), sizeof(logInfo.szPCName) - 1);
} else {
strncpy(logInfo.szPCName, hostname, sizeof(logInfo.szPCName) - 1);
}
logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0';
ClientObject->SendLoginInfo(logInfo);
Mprintf(">> Resent login info after group change\n");
}
// 等待心跳间隔(每秒检查一次退出条件,保证及时响应)
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
for (int i = 0; i < interval; ++i) {

View File

@@ -45,6 +45,8 @@ find_library(CARBON_FRAMEWORK Carbon REQUIRED)
find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox REQUIRED)
find_library(COREMEDIA_FRAMEWORK CoreMedia REQUIRED)
find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED)
find_library(ACCELERATE_FRAMEWORK Accelerate REQUIRED)
find_library(ICONV_LIBRARY iconv REQUIRED)
target_link_libraries(ghost PRIVATE
${COCOA_FRAMEWORK}
@@ -57,6 +59,8 @@ target_link_libraries(ghost PRIVATE
${VIDEOTOOLBOX_FRAMEWORK}
${COREMEDIA_FRAMEWORK}
${COREVIDEO_FRAMEWORK}
${ACCELERATE_FRAMEWORK}
${ICONV_LIBRARY}
"${CMAKE_SOURCE_DIR}/lib/libzstd.a"
)

186
macos/ClipboardHandler.h Normal file
View File

@@ -0,0 +1,186 @@
#pragma once
#import <Cocoa/Cocoa.h>
#include <string>
#include <vector>
// macOS 剪贴板操作封装
// 使用 NSPasteboard API 实现
class ClipboardHandler
{
public:
// 检查剪贴板功能是否可用 (macOS 总是可用)
static bool IsAvailable()
{
return true;
}
// 获取剪贴板中的文件列表
// 返回文件的完整路径列表UTF-8失败返回空列表
static std::vector<std::string> GetFiles()
{
std::vector<std::string> files;
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
// 方法1: 尝试获取文件 URL 列表 (macOS 10.13+)
NSArray<NSURL*>* urls = [pasteboard readObjectsForClasses:@[[NSURL class]]
options:@{NSPasteboardURLReadingFileURLsOnlyKey: @YES}];
if (urls && urls.count > 0) {
for (NSURL* url in urls) {
if (url.isFileURL) {
NSString* path = url.path;
if (path) {
files.push_back([path UTF8String]);
}
}
}
return files;
}
// 方法2: 兼容旧版 API (NSFilenamesPboardType)
NSArray* filenames = [pasteboard propertyListForType:NSFilenamesPboardType];
if (filenames && [filenames isKindOfClass:[NSArray class]]) {
for (NSString* path in filenames) {
if ([path isKindOfClass:[NSString class]]) {
files.push_back([path UTF8String]);
}
}
}
}
return files;
}
// 获取剪贴板文本
// 返回 UTF-8 编码的文本,失败返回空字符串
static std::string GetText()
{
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
NSString* text = [pasteboard stringForType:NSPasteboardTypeString];
if (text) {
return [text UTF8String];
}
}
return "";
}
// 设置剪贴板文本
// text: UTF-8 编码的文本
// 返回是否成功
static bool SetText(const std::string& text)
{
if (text.empty()) return true;
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
NSString* nsText = [NSString stringWithUTF8String:text.c_str()];
if (nsText) {
return [pasteboard setString:nsText forType:NSPasteboardTypeString];
}
}
return false;
}
// 设置剪贴板文本(从原始字节)
// data: 文本数据(可能是 GBK 或 UTF-8
// len: 数据长度
static bool SetTextRaw(const char* data, size_t len)
{
if (!data || len == 0) return true;
// 服务端发来的文本可能是 GBK 编码,尝试转换为 UTF-8
std::string text = ConvertToUtf8(data, len);
return SetText(text);
}
// 设置剪贴板文件列表
// files: UTF-8 编码的文件路径列表
// 返回是否成功
static bool SetFiles(const std::vector<std::string>& files)
{
if (files.empty()) return true;
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
NSMutableArray<NSURL*>* urls = [NSMutableArray arrayWithCapacity:files.size()];
for (const auto& path : files) {
NSString* nsPath = [NSString stringWithUTF8String:path.c_str()];
if (nsPath) {
NSURL* url = [NSURL fileURLWithPath:nsPath];
if (url) {
[urls addObject:url];
}
}
}
if (urls.count > 0) {
return [pasteboard writeObjects:urls];
}
}
return false;
}
private:
// 检查是否是有效的 UTF-8 序列
static bool IsValidUtf8(const char* data, size_t len)
{
const unsigned char* bytes = (const unsigned char*)data;
size_t i = 0;
while (i < len) {
if (bytes[i] <= 0x7F) {
// ASCII
i++;
} else if ((bytes[i] & 0xE0) == 0xC0) {
// 2-byte sequence
if (i + 1 >= len || (bytes[i + 1] & 0xC0) != 0x80) return false;
i += 2;
} else if ((bytes[i] & 0xF0) == 0xE0) {
// 3-byte sequence
if (i + 2 >= len || (bytes[i + 1] & 0xC0) != 0x80 || (bytes[i + 2] & 0xC0) != 0x80) return false;
i += 3;
} else if ((bytes[i] & 0xF8) == 0xF0) {
// 4-byte sequence
if (i + 3 >= len || (bytes[i + 1] & 0xC0) != 0x80 ||
(bytes[i + 2] & 0xC0) != 0x80 || (bytes[i + 3] & 0xC0) != 0x80) return false;
i += 4;
} else {
return false;
}
}
return true;
}
// 尝试将 GBK 转换为 UTF-8
// 如果已经是 UTF-8直接返回
static std::string ConvertToUtf8(const char* data, size_t len)
{
// 检查是否已经是有效的 UTF-8
if (IsValidUtf8(data, len)) {
return std::string(data, len);
}
// 使用 NSString 进行编码转换 (GBK = CFStringEncodingGB_18030_2000)
@autoreleasepool {
// 尝试 GBK (GB18030) 编码
NSStringEncoding gbkEncoding = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);
NSString* str = [[NSString alloc] initWithBytes:data length:len encoding:gbkEncoding];
if (str) {
const char* utf8 = [str UTF8String];
if (utf8) {
return std::string(utf8);
}
}
}
// 转换失败,返回原始数据
return std::string(data, len);
}
};

View File

@@ -6,8 +6,27 @@
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
// CGPreflightScreenCaptureAccess() is unreliable - it can return false
// even when permission is granted (especially after code re-signing).
// Instead, actually try to capture the screen to verify permission.
CGDirectDisplayID displayID = CGMainDisplayID();
CGImageRef image = CGDisplayCreateImage(displayID);
if (image != NULL) {
// Got an image - permission is granted
// Additional check: verify image has actual content (not blank)
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
CGImageRelease(image);
if (width > 0 && height > 0) {
return true;
}
}
// Failed to capture - permission not granted or display issue
// Fall back to preflight check for triggering dialog
return CGPreflightScreenCaptureAccess();
}

View File

@@ -2,13 +2,17 @@
#import <CoreGraphics/CoreGraphics.h>
#import <dispatch/dispatch.h>
#import <IOKit/pwr_mgt/IOPMLib.h>
#import <IOSurface/IOSurface.h>
#import "../client/IOCPClient.h"
#import "../common/commands.h" // QualityLevel, QualityProfile, ALGORITHM_*
#include <vector>
#include <atomic>
#include <mutex>
#include <thread>
#include <cstdint>
#include <memory>
#include <condition_variable>
// Forward declarations
class IOCPClient;
@@ -32,11 +36,7 @@ struct BITMAPINFOHEADER_MAC {
};
#pragma pack(pop)
// Screen algorithm constants
#define ALGORITHM_GRAY 0
#define ALGORITHM_DIFF 1
#define ALGORITHM_H264 2
#define ALGORITHM_RGB565 3
// Algorithm constants from commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565
class ScreenHandler : public IOCPManager {
public:
@@ -120,6 +120,7 @@ private:
std::vector<uint8_t> m_prevFrame;
std::vector<uint8_t> m_currFrame;
std::vector<uint8_t> m_diffBuffer;
std::vector<uint8_t> m_tempBuffer; // 临时缓冲区,避免每帧分配
// Quality settings
std::atomic<uint8_t> m_algorithm;
@@ -132,4 +133,28 @@ private:
// Input handler for mouse/keyboard control
std::unique_ptr<InputHandler> m_inputHandler;
// Power management: prevent display sleep during remote desktop
IOPMAssertionID m_displayAssertionID;
// Cached color space (avoid per-frame creation)
CGColorSpaceRef m_colorSpace;
// CGDisplayStream (efficient continuous capture)
CGDisplayStreamRef m_displayStream;
dispatch_queue_t m_streamQueue;
IOSurfaceRef m_latestSurface;
std::mutex m_surfaceMutex;
std::condition_variable m_surfaceCond;
std::atomic<bool> m_hasNewFrame;
// Initialize/cleanup display stream
bool initDisplayStream();
void cleanupDisplayStream();
// Process frame from IOSurface (called by stream callback)
void processIOSurface(IOSurfaceRef surface);
// Capture from IOSurface to buffer (with vertical flip)
bool captureFromIOSurface(IOSurfaceRef surface, std::vector<uint8_t>& buffer);
};

View File

@@ -1,13 +1,18 @@
#import "ScreenHandler.h"
#import "H264Encoder.h"
#import "InputHandler.h"
#import "ClipboardHandler.h"
#import "../client/IOCPClient.h"
#import "../common/commands.h"
#import "../common/FileTransferV2.h"
#import "../common/logger.h"
#import "Permissions.h"
#import <Cocoa/Cocoa.h>
#import <chrono>
#import <CoreGraphics/CoreGraphics.h>
#import <ApplicationServices/ApplicationServices.h>
#import <mach/mach_time.h>
#import <Accelerate/Accelerate.h>
// Global client ID (calculated in main.mm)
extern uint64_t g_myClientID;
@@ -26,9 +31,18 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
, m_maxFPS(15)
, m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility
, m_h264Bitrate(3000000) // 3 Mbps (matches Windows QUALITY_GOOD)
, m_displayAssertionID(0)
, m_colorSpace(nullptr)
, m_displayStream(nullptr)
, m_streamQueue(nullptr)
, m_latestSurface(nullptr)
, m_hasNewFrame(false)
{
memset(&m_bmpHeader, 0, sizeof(m_bmpHeader));
// Cache color space (avoid per-frame creation)
m_colorSpace = CGColorSpaceCreateDeviceRGB();
// Initialize input handler for mouse/keyboard control
m_inputHandler = std::make_unique<InputHandler>();
if (m_inputHandler->init()) {
@@ -41,6 +55,13 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
ScreenHandler::~ScreenHandler()
{
stop();
cleanupDisplayStream();
// Release cached color space
if (m_colorSpace) {
CGColorSpaceRelease(m_colorSpace);
m_colorSpace = nullptr;
}
}
bool ScreenHandler::init()
@@ -103,24 +124,273 @@ bool ScreenHandler::init()
m_currFrame.resize(m_bmpHeader.biSizeImage, 0);
m_diffBuffer.resize(1 + 1 + 8 + 1 + m_bmpHeader.biSizeImage * 2);
// Wake display if needed (do this early, before sending TOKEN_BITMAPINFO)
bool wasAsleep = CGDisplayIsAsleep(m_displayID);
bool isLocked = false;
CFDictionaryRef sessionInfo = CGSessionCopyCurrentDictionary();
if (sessionInfo) {
CFBooleanRef screenLocked = (CFBooleanRef)CFDictionaryGetValue(
sessionInfo, CFSTR("CGSSessionScreenIsLocked"));
if (screenLocked && CFBooleanGetValue(screenLocked)) {
isLocked = true;
}
CFRelease(sessionInfo);
}
if (wasAsleep || isLocked) {
NSLog(@"Waking display in init (asleep=%d, locked=%d)...", wasAsleep, isLocked);
// Create NoDisplaySleep assertion - this wakes the display
if (m_displayAssertionID == 0) {
IOReturn result = IOPMAssertionCreateWithName(
kIOPMAssertionTypeNoDisplaySleep,
kIOPMAssertionLevelOn,
CFSTR("SimpleRemoter - remote desktop session active"),
&m_displayAssertionID
);
if (result == kIOReturnSuccess) {
NSLog(@"Display assertion created (ID: %u)", m_displayAssertionID);
}
}
// Declare user activity to ensure wake
IOPMAssertionID wakeAssertionID = 0;
IOPMAssertionDeclareUserActivity(
CFSTR("SimpleRemoter - waking display"),
kIOPMUserActiveLocal,
&wakeAssertionID
);
if (wakeAssertionID) {
IOPMAssertionRelease(wakeAssertionID);
}
// Brief wait for loginwindow to render
std::this_thread::sleep_for(std::chrono::milliseconds(500));
NSLog(@"Display wake complete");
}
// Initialize CGDisplayStream for efficient capture
if (!initDisplayStream()) {
NSLog(@"Warning: CGDisplayStream init failed, falling back to legacy capture");
}
NSLog(@"ScreenHandler initialized: %dx%d", m_width, m_height);
return true;
}
bool ScreenHandler::initDisplayStream()
{
// Create dispatch queue for stream callbacks
m_streamQueue = dispatch_queue_create("com.ghost.screenstream", DISPATCH_QUEUE_SERIAL);
if (!m_streamQueue) {
NSLog(@"Failed to create dispatch queue for display stream");
return false;
}
// Stream properties
CFMutableDictionaryRef properties = CFDictionaryCreateMutable(
kCFAllocatorDefault, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks
);
// Request minimum frame interval based on FPS (e.g., 15 FPS = 1/15 sec)
int fps = m_maxFPS.load();
if (fps <= 0) fps = 15;
double interval = 1.0 / (double)fps;
CFNumberRef intervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberDoubleType, &interval);
CFDictionarySetValue(properties, kCGDisplayStreamMinimumFrameTime, intervalRef);
CFRelease(intervalRef);
// Show cursor in stream
CFDictionarySetValue(properties, kCGDisplayStreamShowCursor, kCFBooleanFalse);
// Preserve aspect ratio
CFDictionarySetValue(properties, kCGDisplayStreamPreserveAspectRatio, kCFBooleanTrue);
// Create the display stream with BGRA format
__block ScreenHandler* handler = this;
m_displayStream = CGDisplayStreamCreateWithDispatchQueue(
m_displayID,
m_width,
m_height,
'BGRA', // Pixel format
properties,
m_streamQueue,
^(CGDisplayStreamFrameStatus status,
uint64_t displayTime,
IOSurfaceRef frameSurface,
CGDisplayStreamUpdateRef updateRef) {
(void)displayTime;
(void)updateRef;
if (status == kCGDisplayStreamFrameStatusFrameComplete && frameSurface) {
handler->processIOSurface(frameSurface);
} else if (status == kCGDisplayStreamFrameStatusFrameIdle) {
// Screen not changed, still notify for FPS timing
handler->m_hasNewFrame.store(true);
handler->m_surfaceCond.notify_one();
} else if (status == kCGDisplayStreamFrameStatusStopped) {
NSLog(@"CGDisplayStream stopped");
}
}
);
CFRelease(properties);
if (!m_displayStream) {
NSLog(@"Failed to create CGDisplayStream");
m_streamQueue = nullptr; // ARC manages dispatch objects
return false;
}
// Start the stream
CGError err = CGDisplayStreamStart(m_displayStream);
if (err != kCGErrorSuccess) {
NSLog(@"Failed to start CGDisplayStream: %d", err);
CFRelease(m_displayStream);
m_displayStream = nullptr;
m_streamQueue = nullptr; // ARC manages dispatch objects
return false;
}
NSLog(@"CGDisplayStream started: %dx%d @ %d FPS", m_width, m_height, fps);
return true;
}
void ScreenHandler::cleanupDisplayStream()
{
if (m_displayStream) {
CGDisplayStreamStop(m_displayStream);
CFRelease(m_displayStream);
m_displayStream = nullptr;
}
// ARC manages dispatch objects, just nil the pointer
m_streamQueue = nullptr;
std::lock_guard<std::mutex> lock(m_surfaceMutex);
if (m_latestSurface) {
CFRelease(m_latestSurface);
m_latestSurface = nullptr;
}
}
void ScreenHandler::processIOSurface(IOSurfaceRef surface)
{
// Retain the surface and store it
std::lock_guard<std::mutex> lock(m_surfaceMutex);
if (m_latestSurface) {
CFRelease(m_latestSurface);
}
m_latestSurface = (IOSurfaceRef)CFRetain(surface);
m_hasNewFrame.store(true);
m_surfaceCond.notify_one();
}
bool ScreenHandler::captureFromIOSurface(IOSurfaceRef surface, std::vector<uint8_t>& buffer)
{
if (!surface) return false;
// Lock the surface for CPU read
IOSurfaceLock(surface, kIOSurfaceLockReadOnly, nullptr);
size_t width = IOSurfaceGetWidth(surface);
size_t height = IOSurfaceGetHeight(surface);
size_t bytesPerRow = IOSurfaceGetBytesPerRow(surface);
void* baseAddr = IOSurfaceGetBaseAddress(surface);
if (!baseAddr || width != (size_t)m_width || height != (size_t)m_height) {
IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, nullptr);
return false;
}
// Ensure temp buffer is allocated
size_t requiredSize = m_width * 4 * m_height;
if (m_tempBuffer.size() != requiredSize) {
m_tempBuffer.resize(requiredSize);
}
// Copy from IOSurface to temp buffer (handle different bytesPerRow)
size_t dstBytesPerRow = m_width * 4;
if (bytesPerRow == dstBytesPerRow) {
memcpy(m_tempBuffer.data(), baseAddr, requiredSize);
} else {
// Row by row copy for different strides
uint8_t* src = (uint8_t*)baseAddr;
uint8_t* dst = m_tempBuffer.data();
for (size_t y = 0; y < height; y++) {
memcpy(dst + y * dstBytesPerRow, src + y * bytesPerRow, dstBytesPerRow);
}
}
IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, nullptr);
// Flip vertically using Accelerate framework (SIMD optimized)
vImage_Buffer src = {
.data = m_tempBuffer.data(),
.height = (vImagePixelCount)height,
.width = (vImagePixelCount)width,
.rowBytes = dstBytesPerRow
};
vImage_Buffer dst = {
.data = buffer.data(),
.height = (vImagePixelCount)height,
.width = (vImagePixelCount)width,
.rowBytes = dstBytesPerRow
};
vImage_Error err = vImageVerticalReflect_ARGB8888(&src, &dst, kvImageNoFlags);
if (err != kvImageNoError) {
// Fallback to manual flip
for (size_t y = 0; y < height; y++) {
memcpy(buffer.data() + (height - 1 - y) * dstBytesPerRow,
m_tempBuffer.data() + y * dstBytesPerRow,
dstBytesPerRow);
}
}
return true;
}
void ScreenHandler::start(IOCPClient* client, uint64_t clientID)
{
if (m_running) return;
// If already running, just send TOKEN_BITMAPINFO again
// This allows server to create additional dialogs (MFC can open while Web is active)
if (m_running) {
NSLog(@"ScreenHandler already running, sending TOKEN_BITMAPINFO for new dialog");
sendBitmapInfo();
return;
}
m_client = client;
m_clientID = clientID;
m_running = true;
// Display wake was already done in init(), just ensure assertion exists
if (m_displayAssertionID == 0) {
IOReturn result = IOPMAssertionCreateWithName(
kIOPMAssertionTypeNoDisplaySleep,
kIOPMAssertionLevelOn,
CFSTR("SimpleRemoter - remote desktop session active"),
&m_displayAssertionID
);
if (result == kIOReturnSuccess) {
NSLog(@"Display sleep disabled (ID: %u)", m_displayAssertionID);
}
}
m_captureThread = std::thread(&ScreenHandler::captureLoop, this);
}
void ScreenHandler::stop()
{
m_running = false;
// Wake up capture thread if waiting
m_surfaceCond.notify_all();
if (m_captureThread.joinable()) {
m_captureThread.join();
}
@@ -130,6 +400,13 @@ void ScreenHandler::stop()
m_h264Encoder->close();
m_h264Encoder.reset();
}
// Release display sleep assertion - allow screen to turn off
if (m_displayAssertionID != 0) {
IOPMAssertionRelease(m_displayAssertionID);
NSLog(@"Display sleep re-enabled (released ID: %u)", m_displayAssertionID);
m_displayAssertionID = 0;
}
}
void ScreenHandler::sendBitmapInfo()
@@ -207,6 +484,125 @@ void ScreenHandler::OnReceive(uint8_t* data, ULONG size)
}
break;
case COMMAND_SCREEN_SET_CLIPBOARD:
// 服务端设置剪贴板: [cmd:1][text:N]
if (size > 1) {
if (ClipboardHandler::SetTextRaw((const char*)(data + 1), size - 1)) {
NSLog(@">>> Clipboard SET: %zu bytes", size - 1);
} else {
NSLog(@"*** Clipboard SET failed");
}
}
break;
case COMMAND_SCREEN_GET_CLIPBOARD:
// 服务端请求剪贴板: [cmd:1][hash:64][hmac:16]
// 返回: [TOKEN_CLIPBOARD_TEXT:1][text:N] 或 [COMMAND_GET_FOLDER:1][files]
{
// 优先检查剪贴板中的文件
auto files = ClipboardHandler::GetFiles();
if (!files.empty()) {
// 返回 COMMAND_GET_FOLDER + 文件列表多字符串格式file1\0file2\0\0
std::vector<uint8_t> buf;
buf.push_back(COMMAND_GET_FOLDER);
for (const auto& f : files) {
// 文件路径需要转换为 GBK 编码(服务端预期)
std::string gbkPath = FileTransferV2::utf8ToGbk(f);
buf.insert(buf.end(), gbkPath.begin(), gbkPath.end());
buf.push_back(0); // 每个路径后的 null 终止符
}
buf.push_back(0); // 结束标记
m_client->Send2Server((char*)buf.data(), buf.size());
NSLog(@">>> Clipboard GET: %zu files", files.size());
break;
}
// 没有文件,返回文本
std::string text = ClipboardHandler::GetText();
if (!text.empty()) {
std::vector<uint8_t> buf(1 + text.size());
buf[0] = TOKEN_CLIPBOARD_TEXT;
memcpy(&buf[1], text.data(), text.size());
m_client->Send2Server((char*)buf.data(), buf.size());
NSLog(@">>> Clipboard GET: %zu bytes text", text.size());
} else {
// 返回空剪贴板
uint8_t empty = TOKEN_CLIPBOARD_TEXT;
m_client->Send2Server((char*)&empty, 1);
NSLog(@">>> Clipboard GET: empty");
}
}
break;
case COMMAND_GET_FILE:
// Server requests file download: [cmd:1][targetDir\0][file1\0file2\0...\0]
// Use V2 protocol to upload files
{
if (size < 3) break;
// Parse target directory (GBK encoding)
const char* ptr = (const char*)(data + 1);
const char* end = (const char*)(data + size);
std::string targetDirGbk = ptr;
std::string targetDir = FileTransferV2::gbkToUtf8(targetDirGbk);
ptr += targetDirGbk.length() + 1;
// Parse file list
std::vector<std::string> files;
while (ptr < end && *ptr != '\0') {
std::string fileGbk = ptr;
files.push_back(FileTransferV2::gbkToUtf8(fileGbk));
ptr += fileGbk.length() + 1;
}
// 如果没有文件列表,从剪贴板获取
if (files.empty()) {
files = ClipboardHandler::GetFiles();
}
if (!files.empty() && !targetDir.empty()) {
NSLog(@">>> COMMAND_GET_FILE: %zu files -> %s", files.size(), targetDir.c_str());
// Use V2 protocol to send files
IOCPClient* client = m_client;
std::thread([files, targetDir, client]() {
// Collect all files (expand directories)
std::vector<std::string> allFiles;
std::vector<std::string> rootCandidates;
for (const auto& path : files) {
struct stat st;
if (stat(path.c_str(), &st) != 0) continue;
if (S_ISDIR(st.st_mode)) {
std::string dirPath = path;
if (dirPath.back() != '/') dirPath += '/';
size_t pos = dirPath.rfind('/', dirPath.length() - 2);
std::string parentPath = (pos != std::string::npos) ? dirPath.substr(0, pos + 1) : dirPath;
rootCandidates.push_back(parentPath);
FileTransferV2::CollectFiles(dirPath, allFiles);
} else {
rootCandidates.push_back(path);
allFiles.push_back(path);
}
}
if (allFiles.empty()) {
NSLog(@"*** No files to send");
return;
}
std::string commonRoot = FileTransferV2::GetCommonRoot(rootCandidates);
NSLog(@">>> Sending %zu files, root=%s", allFiles.size(), commonRoot.c_str());
FileTransferV2::SendFilesV2(allFiles, targetDir, commonRoot, client, g_myClientID);
}).detach();
} else {
NSLog(@"*** COMMAND_GET_FILE: no files or empty target");
}
}
break;
default:
break;
}
@@ -216,35 +612,67 @@ void ScreenHandler::applyQualityLevel(int8_t level, bool persist)
{
m_qualityLevel = level;
// TODO: persist to config file if needed
(void)persist;
if (level == QUALITY_DISABLED) {
NSLog(@"Quality: Disabled");
// Disabled mode: keep current settings
NSLog(@"Quality: Disabled (keep current)");
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]);
// Get profile from commands.h (shared with Windows/Linux)
const QualityProfile& profile = GetQualityProfile(level);
// Apply FPS
m_maxFPS.store(profile.maxFPS);
// Apply algorithm (macOS supports all algorithms including H264 via VideoToolbox)
m_algorithm.store(profile.algorithm);
// Update H264 bitrate if applicable
if (profile.algorithm == ALGORITHM_H264 && profile.bitRate > 0) {
m_h264Bitrate = profile.bitRate * 1000; // kbps -> bps
}
NSLog(@"Quality: Level=%d (%s), FPS=%d, Algo=%d, BitRate=%d kbps",
level,
level == QUALITY_ULTRA ? "Ultra" :
level == QUALITY_HIGH ? "High" :
level == QUALITY_GOOD ? "Good" :
level == QUALITY_MEDIUM ? "Medium" :
level == QUALITY_LOW ? "Low" : "Minimal",
profile.maxFPS, profile.algorithm, profile.bitRate);
} else {
// Adaptive mode (level=-1): server adjusts dynamically
NSLog(@"Quality: Adaptive mode");
}
}
bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
{
// Create image from display
// Try to use IOSurface from display stream (more efficient)
if (m_displayStream) {
IOSurfaceRef surface = nullptr;
{
std::lock_guard<std::mutex> lock(m_surfaceMutex);
if (m_latestSurface) {
surface = (IOSurfaceRef)CFRetain(m_latestSurface);
}
}
if (surface) {
bool result = captureFromIOSurface(surface, buffer);
CFRelease(surface);
if (result) {
return true;
}
}
// Fall through to legacy method if IOSurface failed
}
// Legacy method: CGDisplayCreateImage (fallback)
CGImageRef image = CGDisplayCreateImage(m_displayID);
if (!image) {
NSLog(@"Failed to capture screen image");
@@ -255,49 +683,58 @@ bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
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);
size_t requiredSize = bytesPerRow * height;
if (m_tempBuffer.size() != requiredSize) {
m_tempBuffer.resize(requiredSize);
}
CGContextRef context = CGBitmapContextCreate(
tempBuffer.data(),
m_tempBuffer.data(),
width,
height,
8,
bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little // BGRA
m_colorSpace,
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little
);
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);
// Flip vertically using Accelerate framework
vImage_Buffer src = {
.data = m_tempBuffer.data(),
.height = (vImagePixelCount)height,
.width = (vImagePixelCount)width,
.rowBytes = bytesPerRow
};
vImage_Buffer dst = {
.data = buffer.data(),
.height = (vImagePixelCount)height,
.width = (vImagePixelCount)width,
.rowBytes = bytesPerRow
};
vImage_Error err = vImageVerticalReflect_ARGB8888(&src, &dst, kvImageNoFlags);
if (err != kvImageNoError) {
for (size_t y = 0; y < height; y++) {
memcpy(buffer.data() + (height - 1 - y) * bytesPerRow,
m_tempBuffer.data() + y * bytesPerRow,
bytesPerRow);
}
}
return true;
@@ -559,10 +996,11 @@ uint8_t ScreenHandler::getCursorTypeIndex()
// Reuse cursor position from getCursorPosition (called before this)
CGPoint pos = s_cachedLogicalPos;
// Throttle: only check if cursor moved significantly or 100ms elapsed
// Throttle: only check if cursor moved significantly or 250ms elapsed
// (Accessibility API is expensive, cursor type is just a visual hint)
uint64_t now = getTickMs();
bool posChanged = (fabs(pos.x - lastPos.x) > 5 || fabs(pos.y - lastPos.y) > 5);
if (!posChanged && (now - lastCheckTime) < 100) {
bool posChanged = (fabs(pos.x - lastPos.x) > 10 || fabs(pos.y - lastPos.y) > 10);
if (!posChanged && (now - lastCheckTime) < 250) {
return cachedIndex;
}
lastCheckTime = now;
@@ -635,13 +1073,12 @@ uint8_t ScreenHandler::getCursorTypeIndex()
void ScreenHandler::captureLoop()
{
NSLog(@"ScreenHandler CaptureLoop started (%dx%d)", m_width, m_height);
NSLog(@"ScreenHandler CaptureLoop started (%dx%d)%s", m_width, m_height,
m_displayStream ? " [CGDisplayStream]" : " [Legacy]");
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
@@ -650,6 +1087,23 @@ void ScreenHandler::captureLoop()
while (m_running) {
uint64_t start = getTickMs();
// Wait for new frame from display stream (push model)
// This is key optimization: CPU sleeps when screen is static
if (m_displayStream) {
std::unique_lock<std::mutex> lock(m_surfaceMutex);
int fps = m_maxFPS.load();
if (fps <= 0) fps = 15;
int waitMs = 1000 / fps;
// Wait for new frame or timeout (maintains FPS even if no change)
m_surfaceCond.wait_for(lock, std::chrono::milliseconds(waitMs), [this] {
return m_hasNewFrame.load() || !m_running;
});
m_hasNewFrame.store(false);
if (!m_running) break;
}
uint8_t algo = m_algorithm.load();
// Check if algorithm changed
@@ -657,18 +1111,14 @@ void ScreenHandler::captureLoop()
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
sendFirstScreen();
}
} else {
// Normal frame
if (algo == ALGORITHM_H264) {
sendH264Frame(false);
} else {
@@ -676,14 +1126,17 @@ void ScreenHandler::captureLoop()
}
}
int fps = m_maxFPS.load();
if (fps <= 0) fps = 10;
int sleepMs = 1000 / fps;
// Only use sleep-based FPS control for legacy mode
if (!m_displayStream) {
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);
int elapsed = (int)(getTickMs() - start);
int wait = sleepMs - elapsed;
if (wait > 0) {
usleep(wait * 1000);
}
}
}

104
macos/install.sh Normal file
View File

@@ -0,0 +1,104 @@
#!/bin/bash
# macOS Ghost Client 安装脚本
# 用法: ./install.sh [ghost路径]
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
GHOST_SRC="${1:-$SCRIPT_DIR/build/bin/ghost}"
APP_DIR="/Applications/GhostClient.app"
APP_BIN="$APP_DIR/Contents/MacOS/ghost"
echo "=== GhostClient 安装程序 ==="
echo ""
# 检查源文件
if [ ! -f "$GHOST_SRC" ]; then
echo "错误: 找不到 $GHOST_SRC"
echo ""
echo "请先编译: ./build.sh"
echo "或指定路径: $0 <ghost可执行文件路径>"
exit 1
fi
echo "源文件: $GHOST_SRC"
echo ""
set -e
# 1. 停止旧进程
echo "[1/6] 停止旧进程..."
pkill -9 -f "$APP_BIN" 2>/dev/null || true
# 2. 重置系统权限(关键步骤!避免权限缓存导致空白桌面)
echo "[2/6] 重置系统权限..."
echo " (这会清除屏幕录制和辅助功能的旧授权,需要重新授权)"
tccutil reset ScreenCapture 2>/dev/null || true
tccutil reset Accessibility 2>/dev/null || true
# 3. 创建应用程序包
echo "[3/6] 创建应用程序..."
sudo rm -rf "$APP_DIR"
sudo mkdir -p "$APP_DIR/Contents/MacOS"
sudo mkdir -p "$APP_DIR/Contents/Resources"
# 复制 ghost 到 app bundle 内部
sudo cp "$GHOST_SRC" "$APP_BIN"
sudo chmod +x "$APP_BIN"
# 创建 Info.plist
sudo tee "$APP_DIR/Contents/Info.plist" > /dev/null << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>ghost</string>
<key>CFBundleIdentifier</key>
<string>com.ghost.client</string>
<key>CFBundleName</key>
<string>GhostClient</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>LSUIElement</key>
<true/>
</dict>
</plist>
EOF
# 4. 清除隔离属性
echo "[4/6] 清除隔离属性..."
sudo xattr -cr "$APP_DIR"
# 5. 签名应用
echo "[5/6] 签名应用..."
sudo codesign --force --deep --sign - "$APP_DIR"
# 6. 添加到登录项(开机自启)
echo "[6/7] 添加到登录项..."
osascript -e 'tell application "System Events" to delete login item "GhostClient"' 2>/dev/null || true
osascript -e 'tell application "System Events" to make login item at end with properties {path:"/Applications/GhostClient.app", hidden:true}' 2>/dev/null && echo " 已添加开机自启" || echo " 添加失败,请手动添加"
# 7. 完成
echo "[7/7] 安装完成!"
echo ""
echo "========================================"
echo " 下一步: 授权系统权限"
echo "========================================"
echo ""
echo "1. 启动应用 (会自动弹出权限请求):"
echo ""
echo " open /Applications/GhostClient.app"
echo ""
echo "2. 授权以下权限 (在系统设置中勾选 GhostClient):"
echo " - 屏幕录制: 允许捕获屏幕画面"
echo " - 辅助功能: 允许控制鼠标键盘"
echo ""
echo "3. 授权后重启应用:"
echo ""
echo " pkill -f GhostClient && open /Applications/GhostClient.app"
echo ""
echo "4. 查看日志确认运行状态:"
echo ""
echo " tail -f /tmp/ghost.log"
echo ""
echo "========================================"
echo ""

View File

@@ -1,16 +1,20 @@
#import <Cocoa/Cocoa.h>
#import <sys/sysctl.h>
#import <sys/stat.h>
#import <mach/mach.h>
#import <mach-o/dyld.h>
#import <pwd.h>
#import <signal.h>
#import <unistd.h>
#import <fcntl.h>
#import <IOKit/IOKitLib.h>
#import <IOKit/pwr_mgt/IOPMLib.h>
#import <fstream>
#import <thread>
#import <atomic>
#import <memory>
#import <string>
#import <map>
#import "../client/IOCPClient.h"
#define XXH_INLINE_ALL
@@ -19,9 +23,15 @@
#import "ScreenHandler.h"
#import "InputHandler.h"
#import "SystemManager.h"
#import "../common/PTYHandler.h"
#import "../common/FileManager.h"
#import "../common/FileTransferV2.h"
#import "../common/logger.h"
#import "ClipboardHandler.h"
// Global state
static std::atomic<bool> g_running(true);
static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重发登录信息
// Client ID (calculated from system info, used by ScreenHandler)
uint64_t g_myClientID = 0;
@@ -31,6 +41,103 @@ CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_M
State g_bExit = S_CLIENT_NORMAL;
// ============== Configuration File Functions ==============
// Config path: ~/.config/ghost/config.conf (same as Linux)
// Format: key=value (one per line)
static std::string g_configDir;
static std::string g_configPath;
static std::map<std::string, std::string> g_configData;
// Initialize config paths
static void initConfigPaths()
{
if (!g_configDir.empty()) return; // Already initialized
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
if (pw) home = pw->pw_dir;
}
if (!home) home = "/tmp";
g_configDir = std::string(home) + "/.config/ghost";
g_configPath = g_configDir + "/config.conf";
}
// Recursively create directory
static void mkdirRecursive(const std::string& path)
{
size_t pos = 0;
while ((pos = path.find('/', pos + 1)) != std::string::npos) {
mkdir(path.substr(0, pos).c_str(), 0755);
}
mkdir(path.c_str(), 0755);
}
// Load all config from file
static void loadConfig()
{
initConfigPaths();
g_configData.clear();
std::ifstream file(g_configPath);
if (!file.is_open()) return;
std::string line;
while (std::getline(file, line)) {
size_t eq = line.find('=');
if (eq != std::string::npos) {
g_configData[line.substr(0, eq)] = line.substr(eq + 1);
}
}
}
// Save all config to file
static void saveConfig()
{
initConfigPaths();
mkdirRecursive(g_configDir);
std::ofstream file(g_configPath, std::ios::trunc);
if (!file.is_open()) {
NSLog(@"Failed to save config to %s", g_configPath.c_str());
return;
}
for (const auto& kv : g_configData) {
file << kv.first << "=" << kv.second << "\n";
}
NSLog(@"Config saved to %s", g_configPath.c_str());
}
// Get config string value
static std::string getConfigStr(const std::string& key, const std::string& def = "")
{
auto it = g_configData.find(key);
return it != g_configData.end() ? it->second : def;
}
// Set config string value
static void setConfigStr(const std::string& key, const std::string& value)
{
g_configData[key] = value;
saveConfig();
}
// Save group name to config file
static void saveGroupName(const std::string& groupName)
{
setConfigStr("group_name", groupName);
NSLog(@"Group name saved: %s", groupName.c_str());
}
// Load group name from config file
static std::string loadGroupName()
{
return getConfigStr("group_name");
}
// ============== System Information Functions ==============
// Get macOS version string (e.g., "macOS 14.0 Sonoma")
@@ -140,9 +247,113 @@ static std::string getTimeString()
return std::string([dateString UTF8String]);
}
// Get active application name
// Get user idle time in seconds (time since last keyboard/mouse input)
static double getUserIdleTime()
{
// CGEventSourceSecondsSinceLastEventType returns seconds since last event
// kCGEventSourceStateCombinedSessionState includes all input sources
CFTimeInterval idleTime = CGEventSourceSecondsSinceLastEventType(
kCGEventSourceStateCombinedSessionState,
kCGAnyInputEventType
);
// Defensive: ensure non-negative (edge case protection)
return idleTime > 0 ? idleTime : 0;
}
// Check if screen is locked
static bool isScreenLocked()
{
// Method 1: Check CGSession dictionary for screen lock status
CFDictionaryRef sessionDict = CGSessionCopyCurrentDictionary();
if (sessionDict) {
// Check for "CGSSessionScreenIsLocked" key
CFBooleanRef screenLocked = (CFBooleanRef)CFDictionaryGetValue(
sessionDict, CFSTR("CGSSessionScreenIsLocked"));
if (screenLocked && CFBooleanGetValue(screenLocked)) {
CFRelease(sessionDict);
return true;
}
CFRelease(sessionDict);
}
// Method 2: Check if loginwindow is frontmost (screen saver / lock screen)
NSRunningApplication* frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
if (frontApp) {
NSString* bundleId = [frontApp bundleIdentifier];
if ([bundleId isEqualToString:@"com.apple.loginwindow"] ||
[bundleId isEqualToString:@"com.apple.ScreenSaver.Engine"]) {
return true;
}
}
return false;
}
// Format time as HH:MM:SS with prefix
static std::string formatStatusTime(const char* prefix, double seconds)
{
int totalSecs = (int)seconds;
int hours = totalSecs / 3600;
int mins = (totalSecs % 3600) / 60;
int secs = totalSecs % 60;
char buffer[64];
snprintf(buffer, sizeof(buffer), "%s: %02d:%02d:%02d", prefix, hours, mins, secs);
return std::string(buffer);
}
// Get active application name or idle/locked status (works for background processes)
static std::string getActiveApp()
{
double idleTime = getUserIdleTime();
// Check if screen is locked first
if (isScreenLocked()) {
return formatStatusTime("Locked", idleTime);
}
// Check user idle time (matches Windows/Linux: 6 seconds threshold)
// If idle for more than 6 seconds, report inactive status
if (idleTime >= 6.0) {
return formatStatusTime("Inactive", idleTime);
}
// Use CGWindowListCopyWindowInfo to get the frontmost window
// This works reliably even when running as a background/nohup process
CFArrayRef windowList = CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
kCGNullWindowID
);
if (windowList) {
CFIndex count = CFArrayGetCount(windowList);
for (CFIndex i = 0; i < count; i++) {
CFDictionaryRef window = (CFDictionaryRef)CFArrayGetValueAtIndex(windowList, i);
// Get window layer - layer 0 is normal windows
CFNumberRef layerRef = (CFNumberRef)CFDictionaryGetValue(window, kCGWindowLayer);
int layer = 0;
if (layerRef) {
CFNumberGetValue(layerRef, kCFNumberIntType, &layer);
}
// Skip non-normal windows (menu bar, dock, etc.)
if (layer != 0) continue;
// Get owner name (application name)
CFStringRef ownerName = (CFStringRef)CFDictionaryGetValue(window, kCGWindowOwnerName);
if (ownerName) {
char buffer[256] = {};
if (CFStringGetCString(ownerName, buffer, sizeof(buffer), kCFStringEncodingUTF8)) {
CFRelease(windowList);
return std::string(buffer);
}
}
}
CFRelease(windowList);
}
// Fallback to NSWorkspace (may not work for background processes)
NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication];
if (app) {
NSString* name = [app localizedName];
@@ -258,9 +469,25 @@ static void fillLoginInfo(LOGIN_INFOR& info)
// CPU MHz
info.dwCPUMHz = getCPUFrequencyMHz();
// PC Name (hostname)
// PC Name (hostname) - with group name if set
std::string hostname = getHostname();
strncpy(info.szPCName, hostname.c_str(), sizeof(info.szPCName) - 1);
std::string groupName = loadGroupName();
if (!groupName.empty()) {
// Also update g_SETTINGS for consistency
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
g_SETTINGS.szGroupName[sizeof(g_SETTINGS.szGroupName) - 1] = '\0';
// Format: "hostname/groupname"
std::string pcNameWithGroup = hostname + "/" + groupName;
strncpy(info.szPCName, pcNameWithGroup.c_str(), sizeof(info.szPCName) - 1);
} else if (g_SETTINGS.szGroupName[0] != 0) {
// Use group from g_SETTINGS (set at build time)
groupName = g_SETTINGS.szGroupName;
std::string pcNameWithGroup = hostname + "/" + groupName;
strncpy(info.szPCName, pcNameWithGroup.c_str(), sizeof(info.szPCName) - 1);
} else {
strncpy(info.szPCName, hostname.c_str(), sizeof(info.szPCName) - 1);
}
info.szPCName[sizeof(info.szPCName) - 1] = '\0';
// Webcam
info.bWebCamIsExist = hasCameraDevice() ? 1 : 0;
@@ -347,6 +574,7 @@ static void signalHandler(int sig)
{
NSLog(@"Received signal %d, shutting down...", sig);
g_running = false;
g_bExit = S_CLIENT_EXIT; // 通知所有工作线程退出
}
static void setupSignals()
@@ -357,6 +585,28 @@ static void setupSignals()
signal(SIGPIPE, SIG_IGN);
}
// 经典 Unix 双 fork 守护进程
static void daemonize()
{
pid_t pid = fork();
if (pid < 0) exit(1);
if (pid > 0) exit(0); // 父进程退出
setsid(); // 新会话,脱离终端
pid = fork(); // 第二次 fork防止重新获取控制终端
if (pid < 0) exit(1);
if (pid > 0) exit(0);
// 关闭标准文件描述符,重定向到 /dev/null
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
open("/dev/null", O_RDONLY); // fd 0 = stdin
open("/dev/null", O_WRONLY); // fd 1 = stdout
open("/dev/null", O_WRONLY); // fd 2 = stderr
}
// ============== Main Entry Point ==============
// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致)
@@ -395,6 +645,30 @@ struct RttEstimator {
RttEstimator g_rttEstimator;
int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整
void* ShellworkingThread(void* param)
{
try {
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
void* clientAddr = ClientObject.get();
NSLog(@">>> Enter ShellworkingThread [%p]", clientAddr);
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
std::unique_ptr<PTYHandler> handler(new PTYHandler(ClientObject.get()));
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
BYTE bToken = TOKEN_TERMINAL_START;
ClientObject->Send2Server((char*)&bToken, 1);
NSLog(@">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
NSLog(@">>> Leave ShellworkingThread [%p]", clientAddr);
} catch (const std::exception& e) {
NSLog(@"*** ShellworkingThread exception: %s ***", e.what());
}
return NULL;
}
void* ScreenworkingThread(void* param)
{
try {
@@ -413,6 +687,8 @@ void* ScreenworkingThread(void* param)
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
@@ -421,6 +697,28 @@ void* ScreenworkingThread(void* param)
return NULL;
}
void* FileManagerworkingThread(void* param)
{
try {
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
void* clientAddr = ClientObject.get();
Mprintf(">>> Enter FileManagerworkingThread [%p]\n", clientAddr);
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
std::unique_ptr<FileManager> handler(new FileManager(ClientObject.get()));
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
Mprintf(">>> FileManagerworkingThread [%p] initialized\n", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
Mprintf(">>> Leave FileManagerworkingThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
Mprintf("*** FileManagerworkingThread exception: %s ***\n", e.what());
}
return NULL;
}
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
{
if (szBuffer == nullptr || ulLength == 0)
@@ -431,6 +729,7 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
g_bExit = S_CLIENT_EXIT;
g_running = false; // Stop main loop to prevent reconnection
} else if (szBuffer[0] == COMMAND_SHELL) {
std::thread(ShellworkingThread, nullptr).detach();
Mprintf("** [%p] Received 'SHELL' command ***\n", user);
} else if (szBuffer[0] == COMMAND_SCREEN_SPY) {
std::thread(ScreenworkingThread, nullptr).detach();
@@ -438,7 +737,34 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
} else if (szBuffer[0] == COMMAND_SYSTEM) {
Mprintf("** [%p] Received 'SYSTEM' command ***\n", user);
} else if (szBuffer[0] == COMMAND_LIST_DRIVE) {
std::thread(FileManagerworkingThread, nullptr).detach();
Mprintf("** [%p] Received 'LIST_DRIVE' command ***\n", user);
} else if (szBuffer[0] == COMMAND_C2C_PREPARE) {
// C2C 准备接收通知
FileTransferV2::HandleC2CPrepare(szBuffer, ulLength, nullptr);
Mprintf("** [%p] C2C Prepare received ***\n", user);
} else if (szBuffer[0] == COMMAND_C2C_TEXT) {
// C2C 文本剪贴板: [cmd:1][dstClientID:8][textLen:4][text:N]
if (ulLength >= 13) {
uint32_t textLen;
memcpy(&textLen, szBuffer + 9, 4);
if (ulLength >= 13 + textLen && textLen > 0) {
if (!ClipboardHandler::IsAvailable()) {
Mprintf("** [%p] C2C Text: clipboard unavailable ***\n", user);
} else {
std::string utf8Text((const char*)szBuffer + 13, textLen);
if (ClipboardHandler::SetText(utf8Text)) {
Mprintf("** [%p] C2C Text received: %u bytes ***\n", user, textLen);
}
}
}
}
} else if (szBuffer[0] == COMMAND_SEND_FILE_V2 || szBuffer[0] == COMMAND_FILE_COMPLETE_V2) {
// V2 文件接收
int result = FileTransferV2::RecvFileChunkV2(szBuffer, ulLength, g_myClientID);
if (result != 0) {
Mprintf("** [%p] V2 File recv error: %d ***\n", user, result);
}
} else if (szBuffer[0] == CMD_HEARTBEAT_ACK) {
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
@@ -464,33 +790,99 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
}
} else if (szBuffer[0] == COMMAND_NEXT) {
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
} else if (szBuffer[0] == CMD_SET_GROUP) {
// Extract group name from message (starts at byte 1)
std::string groupName;
if (ulLength > 1) {
groupName = std::string((char*)szBuffer + 1, ulLength - 1);
// Remove trailing nulls
size_t pos = groupName.find('\0');
if (pos != std::string::npos) {
groupName = groupName.substr(0, pos);
}
}
// Save to config file
saveGroupName(groupName);
// Update global settings
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
// 标记需要重发登录信息(让服务端更新分组显示)
g_needResendLogin.store(true);
Mprintf("** [%p] Group changed to: %s ***\n", user, groupName.c_str());
} else {
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
}
return TRUE;
}
// 用法: ./ghost [-d]
// -d 后台守护进程模式
int main(int argc, const char* argv[])
{
(void)argc;
(void)argv;
// 解析 -d 参数
bool daemon_mode = (argc > 1 && strcmp(argv[1], "-d") == 0);
// 守护进程模式:在进入 autoreleasepool 之前 fork
if (daemon_mode) {
daemonize();
}
@autoreleasepool {
NSLog(@"=== macOS Ghost Client ===");
NSLog(@"=== macOS Ghost Client%s ===", daemon_mode ? " (daemon)" : "");
// ============== Power Management: Keep System Awake ==============
// 1. Disable App Nap - prevent macOS from suspending this process
id<NSObject> powerActivity = [[NSProcessInfo processInfo]
beginActivityWithOptions:(NSActivityUserInitiated | NSActivityIdleSystemSleepDisabled)
reason:@"Remote control client must maintain persistent connection"];
NSLog(@"App Nap disabled, activity token acquired");
// 2. Prevent system idle sleep using IOKit power assertion
IOPMAssertionID sleepAssertionID = 0;
IOReturn result = IOPMAssertionCreateWithName(
kIOPMAssertionTypeNoIdleSleep,
kIOPMAssertionLevelOn,
CFSTR("SimpleRemoter macOS client - maintaining remote connection"),
&sleepAssertionID
);
if (result == kIOReturnSuccess) {
NSLog(@"Power assertion created: system idle sleep disabled (ID: %u)", sleepAssertionID);
} else {
NSLog(@"Warning: Failed to create power assertion (error: 0x%x)", result);
}
// 3. Display sleep: managed by ScreenHandler - only prevents display sleep
// when remote desktop is actively connected (saves power when idle)
// Setup signal handlers
setupSignals();
// Load configuration file (~/.config/ghost/config.conf)
loadConfig();
NSLog(@"Config loaded from %s", g_configPath.c_str());
// Check permissions
NSLog(@"Checking permissions...");
if (!Permissions::checkScreenCapture()) {
bool hasScreenCapture = Permissions::checkScreenCapture();
if (hasScreenCapture) {
NSLog(@"Screen capture permission: OK");
} else {
NSLog(@"Screen capture permission not granted.");
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Screen Recording");
Permissions::openScreenCaptureSettings();
// Request permission (triggers system dialog on first run)
Permissions::requestScreenCapture();
// Only open settings if this appears to be a re-run without permission
// Check again after request (dialog may have been shown)
if (!Permissions::checkScreenCapture()) {
Permissions::openScreenCaptureSettings();
}
}
if (!Permissions::checkAccessibility()) {
bool hasAccessibility = Permissions::checkAccessibility();
if (hasAccessibility) {
NSLog(@"Accessibility permission: OK");
} else {
NSLog(@"Accessibility permission not granted.");
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Accessibility");
Permissions::requestAccessibility();
@@ -501,6 +893,8 @@ int main(int argc, const char* argv[])
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
} else {
NSLog(@"Full Disk Access: OK");
}
// Create client
@@ -523,6 +917,13 @@ int main(int argc, const char* argv[])
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) {
// 检查是否需要重发登录信息(分组变更后)
if (g_needResendLogin.exchange(false)) {
fillLoginInfo(logInfo);
ClientObject->SendLoginInfo(logInfo);
Mprintf(">> Resent login info after group change\n");
}
// 等待心跳间隔(每秒检查一次退出条件,保证及时响应)
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
for (int i = 0; i < interval; ++i) {
@@ -549,6 +950,15 @@ int main(int argc, const char* argv[])
}
NSLog(@"Shutting down...");
// Release power assertions
if (sleepAssertionID) {
IOPMAssertionRelease(sleepAssertionID);
NSLog(@"Released sleep assertion");
}
// Display assertion is managed by ScreenHandler (released in stop())
// powerActivity is automatically released when exiting @autoreleasepool
(void)powerActivity; // Suppress unused variable warning
}
return 0;

32
macos/uninstall.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
# macOS Ghost Client 卸载脚本
APP_DIR="/Applications/GhostClient.app"
echo "=== GhostClient 卸载程序 ==="
echo ""
# 1. 停止进程
echo "[1/3] 停止进程..."
pkill -9 -f "$APP_DIR" 2>/dev/null || true
# 2. 删除文件
echo "[2/3] 删除文件..."
sudo rm -rf "$APP_DIR"
rm -rf ~/.config/ghost 2>/dev/null || true
rm -f /tmp/ghost.log 2>/dev/null || true
# 3. 移除登录项
echo "[3/4] 移除登录项..."
osascript -e 'tell application "System Events" to delete login item "GhostClient"' 2>/dev/null || true
# 4. 重置系统权限
echo "[4/4] 重置系统权限..."
tccutil reset ScreenCapture 2>/dev/null || true
tccutil reset Accessibility 2>/dev/null || true
echo ""
echo "========================================"
echo " 卸载完成"
echo "========================================"
echo ""

View File

@@ -526,14 +526,13 @@ BOOL CMy2015RemoteApp::InitInstance()
SetChineseThreadLocale();
// 加载语言包(必须在显示任何文本之前)
// 内嵌资源支持 en_US 和 zh_TW无需外部文件
auto lang = THIS_CFG.GetStr("settings", "Language", "en_US");
auto langDir = THIS_CFG.GetStr("settings", "LangDir", "./lang");
langDir = langDir.empty() ? "./lang" : langDir;
if (PathFileExists(langDir.c_str())) {
g_Lang.Init(langDir.c_str());
g_Lang.Load(lang.c_str());
Mprintf("语言包目录已经指定[%s], 语言数量: %d\n", langDir.c_str(), g_Lang.GetLanguageCount());
}
g_Lang.Init(langDir.c_str()); // 初始化目录(用于磁盘补丁文件)
g_Lang.Load(lang.c_str()); // 加载语言(优先内嵌资源,再覆盖磁盘文件)
Mprintf("语言: %s, 目录: %s\n", lang.c_str(), langDir.c_str());
// 创建并显示启动画面
CSplashDlg* pSplash = new CSplashDlg();

Binary file not shown.

View File

@@ -176,9 +176,9 @@ bool SupportsFileTransferV2(context* ctx) {
}
// 授权日志频率控制:首次必须记录,状态变化必须记录,相同状态每小时记录一次
static bool ShouldLogAuth(const std::string& sn, bool success) {
static bool ShouldLogAuth(const std::string& sn, int success) {
struct AuthLogState {
bool lastStatus;
int lastStatus;
time_t lastLogTime;
};
static std::map<std::string, AuthLogState> s_cache;
@@ -397,7 +397,7 @@ DllInfo* ReadTinyRunDll(int pid)
{
std::string name = TINY_DLL_NAME;
DWORD fileSize = 0;
BYTE * dllData = ReadResource(IDR_TINYRUN_X64, fileSize);
BYTE * dllData = ReadResource(IDR_TINYRUN_X64, fileSize, ResFileName::TINYRUN_X64);
std::string s(skCrypt(FLAG_FINDEN)), ip, port;
int offset = MemoryFind((char*)dllData, s.c_str(), fileSize, s.length());
if (offset != -1) {
@@ -433,7 +433,7 @@ DllInfo* ReadFrpcDll(int callType)
{
std::string name = FRPC_DLL_NAME;
DWORD fileSize = 0;
BYTE* dllData = ReadResource(IDR_BINARY_FRPC, fileSize);
BYTE* dllData = ReadResource(IDR_BINARY_FRPC, fileSize, ResFileName::FRPC_DLL);
// 设置输出参数
auto md5 = CalcMD5FromBytes(dllData, fileSize);
DllExecuteInfoNew info = { MEMORYDLL, fileSize, callType, {}, {}, 0, 0, sizeof(DllExecuteInfoNew)};
@@ -1862,7 +1862,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
}
if (strlen(v->Admin) && v->Port > 0) {
DWORD size = 0;
LPBYTE data = ReadResource(sizeof(void*) == 8 ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, size);
LPBYTE data = ReadResource(sizeof(void*) == 8 ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, size,
sizeof(void*) == 8 ? ResFileName::TINYRUN_X64 : ResFileName::TINYRUN_X86);
if (data) {
int offset = MemoryFind((char*)data, FLAG_FINDEN, size, strlen(FLAG_FINDEN));
if (offset != -1) {
@@ -2212,7 +2213,7 @@ void CMy2015RemoteDlg::InitFrpClients()
// 加载 FRP DLL只加载一次
DWORD size = 0;
LPBYTE frpcData = ReadResource(IDR_BINARY_FRPC, size);
LPBYTE frpcData = ReadResource(IDR_BINARY_FRPC, size, ResFileName::FRPC_DLL);
if (frpcData == nullptr) {
Mprintf("[FRP] Failed to read FRP DLL\n");
return;
@@ -2301,7 +2302,7 @@ bool CMy2015RemoteDlg::InitLocalFrpsServer()
// 加载 FRPS DLL
DWORD size = 0;
LPBYTE frpsData = ReadResource(IDR_BINARY_FRPS, size);
LPBYTE frpsData = ReadResource(IDR_BINARY_FRPS, size, ResFileName::FRPS_DLL);
if (frpsData == nullptr) {
Mprintf("[FRPS] Failed to read FRPS DLL from resource\n");
return false;
@@ -2634,7 +2635,7 @@ void CMy2015RemoteDlg::StartFrpcAuto(const FrpAutoConfig& cfg)
// 加载 FRP DLL复用现有的 m_hFrpDll如果还没加载则加载
if (!m_hFrpDll) {
DWORD size = 0;
LPBYTE frpcData = ReadResource(IDR_BINARY_FRPC, size);
LPBYTE frpcData = ReadResource(IDR_BINARY_FRPC, size, ResFileName::FRPC_DLL);
if (frpcData == nullptr) {
Mprintf("[FRP-Auto] 读取 FRP DLL 资源失败\n");
return;
@@ -3221,13 +3222,13 @@ void CMy2015RemoteDlg::UpdateStatusBarStats()
}
else if (m_settings.UsingFRPProxy) {
strFrpDisplay.Format(_T("%s:%d"), THIS_CFG.GetStr("settings", "master").c_str(),
THIS_CFG.Get1Int("settings", "ghost", 6543));
THIS_CFG.Get1Int("settings", "ghost", ';', 6543));
}
else if (!m_localPublicIP.empty()) {
strFrpDisplay.Format(_T("WAN %s:%d"), m_localPublicIP.c_str(), THIS_CFG.Get1Int("settings", "ghost", 6543));
strFrpDisplay.Format(_T("WAN %s:%d"), m_localPublicIP.c_str(), THIS_CFG.Get1Int("settings", "ghost", ';', 6543));
}
else {
strFrpDisplay.Format(_T("LAN %s:%d"), m_localPrivateIP.c_str(), THIS_CFG.Get1Int("settings", "ghost", 6543));
strFrpDisplay.Format(_T("LAN %s:%d"), m_localPrivateIP.c_str(), THIS_CFG.Get1Int("settings", "ghost", ';', 6543));
}
}
// 根据是否有内容设置分区宽度
@@ -3678,11 +3679,11 @@ void CMy2015RemoteDlg::OnOnlineMessage()
}
// 从资源中读取被控端文件,并根据用户选择修改连接信息后返回给发送线程
BYTE* ReadExeFromResource(DWORD& outSize, int resourceId, int iType, int iStartup,
BYTE* ReadExeFromResource(DWORD& outSize, int resourceId, const char* resName, int iType, int iStartup,
const std::string& dir, const std::string& name, int cmd, int sizeofSize)
{
DWORD dwFileSize = 0;
BYTE* szBuffer = ReadResource(resourceId, dwFileSize);
BYTE* szBuffer = ReadResource(resourceId, dwFileSize, resName);
outSize = dwFileSize;
CONNECT_ADDRESS g_ConnectAddress = { FLAG_FINDEN };
char* pSearchStart = (char*)szBuffer;
@@ -3753,8 +3754,11 @@ void CMy2015RemoteDlg::OnOnlineUpdate()
std::filesystem::path path = ContextObject->GetAdditionalData(RES_FILE_PATH).GetString();
std::string stem = path.stem().string();
std::string dirName = path.parent_path().filename().string();
const char* resName = dlg.m_nSelected
? (is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86)
: (is64bit ? ResFileName::TESTRUN_X64 : ResFileName::TESTRUN_X86);
buffer = ReadExeFromResource(dwFileSize, dlg.m_nSelected ? (is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86) :
(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86), dlg.m_nSelected ? CLIENT_TYPE_ONE : CLIENT_TYPE_MEMDLL,
(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86), resName, dlg.m_nSelected ? CLIENT_TYPE_ONE : CLIENT_TYPE_MEMDLL,
dlg.m_nSelected ? Startup_GhostMsc : Startup_TestRunMsc, dirName, stem, COMMAND_UPDATE, 8);
fileSize = dwFileSize;
} else if (clientType == "DLL") {
@@ -3872,6 +3876,9 @@ BOOL CMy2015RemoteDlg::ShouldRemoteControl()
void screenParamModifier(context* ctx, void* user)
{
// Mark as MFC-triggered so dialog will be visible
WebService().SetMfcTriggered(ctx->GetClientID());
auto version = ctx->GetClientData(ONLINELIST_VERSION);
if (!IsDateGreaterOrEqual(version, "Feb 8 2026")) {
char* param = (char*)user;
@@ -4523,12 +4530,12 @@ bool IsDateInRange(const std::string& startDate, const std::string& endDate)
return (today >= startDate && today <= endDate);
}
BOOL CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired)
int CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired)
{
if (outExpired) *outExpired = false;
if (sn.empty() || passcode.empty() || hmac == 0) {
return FALSE;
return 1;
}
auto v = splitString(passcode, '-');
if (v.size() == 6 || v.size() == 7) {
@@ -4538,7 +4545,7 @@ BOOL CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, cons
std::string hash256 = joinString(subvector, '-');
std::string fixedKey = getFixedLengthID(finalKey);
if (hash256 != fixedKey)
return FALSE;
return 2;
}
static const char* superAdmin = getenv(BRAND_ENV_VAR);
@@ -4547,14 +4554,13 @@ BOOL CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, cons
Mprintf("请设置环境变量 " BRAND_ENV_VAR " 来给下级授权!\n");
}
BOOL b = VerifyMessage(pwd, (BYTE*)passcode.c_str(), passcode.length(), hmac);
if (!b) return FALSE;
if (!b) return 3;
auto list = StringToVector(passcode, '-', 2);
BOOL valid = IsDateInRange(list[0], list[1]);
std::string hmacStr = std::to_string(hmac);
// 授权过期,更新或创建记录并标记为过期
if (!valid) {
Mprintf("授权已过期: %s\n", sn.c_str());
if (outExpired) *outExpired = true; // 签名有效但已过期
if (ctx != nullptr) {
std::string ip = ctx->GetClientData(ONLINELIST_IP);
@@ -4565,13 +4571,12 @@ BOOL CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, cons
UpdateLicenseActivity(sn, passcode, hmacStr);
}
SetLicenseStatus(sn, LICENSE_STATUS_EXPIRED);
return FALSE;
return 4;
}
// 检查授权是否已被撤销
if (IsLicenseRevoked(sn)) {
Mprintf("授权已被撤销: %s\n", sn.c_str());
return FALSE;
return 5;
}
// 授权成功时更新 license 活跃信息
@@ -4584,21 +4589,20 @@ BOOL CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, cons
UpdateLicenseActivity(sn, passcode, hmacStr);
}
return TRUE;
return 0;
}
BOOL CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, const std::string& passcode, const std::string& hmacV2, bool* outExpired)
int CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, const std::string& passcode, const std::string& hmacV2, bool* outExpired)
{
if (outExpired) *outExpired = false;
if (sn.empty() || passcode.empty() || hmacV2.empty()) {
return FALSE;
return 1;
}
// 检查 V2 前缀
if (hmacV2.substr(0, 3) != "v2:") {
Mprintf("V2 HMAC 格式错误: %s\n", hmacV2.c_str());
return FALSE;
return 2;
}
// 检查公钥是否已配置(全零表示未配置)
@@ -4610,15 +4614,13 @@ BOOL CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, co
}
}
if (!keyConfigured) {
Mprintf("V2 公钥未配置,无法验证 V2 授权\n");
return FALSE;
return 3;
}
// 使用 V2 验证
BOOL b = verifyPasswordV2(sn, passcode, hmacV2, g_LicensePublicKey);
if (!b) {
Mprintf("V2 签名验证失败: %s\n", sn.c_str());
return FALSE;
return 4;
}
auto list = StringToVector(passcode, '-', 2);
@@ -4626,7 +4628,6 @@ BOOL CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, co
// 授权过期
if (!valid) {
Mprintf("V2 授权已过期: %s\n", sn.c_str());
if (outExpired) *outExpired = true; // 签名有效但已过期
if (ctx != nullptr) {
std::string ip = ctx->GetClientData(ONLINELIST_IP);
@@ -4637,13 +4638,12 @@ BOOL CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, co
UpdateLicenseActivity(sn, passcode, hmacV2);
}
SetLicenseStatus(sn, LICENSE_STATUS_EXPIRED);
return FALSE;
return 5;
}
// 检查授权是否已被撤销
if (IsLicenseRevoked(sn)) {
Mprintf("V2 授权已被撤销: %s\n", sn.c_str());
return FALSE;
return 6;
}
// 授权成功时更新 license 活跃信息
@@ -4656,7 +4656,7 @@ BOOL CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, co
UpdateLicenseActivity(sn, passcode, hmacV2);
}
return TRUE;
return 0;
}
BOOL IsTrail(const std::string& passcode)
@@ -5782,7 +5782,7 @@ std::tuple<bool, bool, bool, bool> CMy2015RemoteDlg::VerifyClientAuth(context* h
const std::string& sn, const std::string& passcode, uint64_t hmac,
const std::string& hmacV2, const std::string& ip, const char* source)
{
bool authorized = false;
BOOL authorized = -1;
bool isV2 = false;
bool isTrail = false;
bool expired = false;
@@ -5791,19 +5791,19 @@ std::tuple<bool, bool, bool, bool> CMy2015RemoteDlg::VerifyClientAuth(context* h
// V2 授权验证
isV2 = true;
authorized = AuthorizeClientV2(host, sn, passcode, hmacV2, &expired);
if (authorized) {
if (authorized == 0) {
if (host) {
m_ClientMap->SetClientMapInteger(host->GetClientID(), MAP_AUTH, TRUE);
}
isTrail = IsTrail(passcode.c_str());
}
if (ShouldLogAuth(sn, authorized)) {
if (authorized) {
if (authorized == 0) {
Mprintf("[%s] %s V2 授权成功: %s [%s]\n", source, passcode.c_str(), sn.c_str(), ip.c_str());
std::string tip = passcode + std::string(_L(" V2 授权成功: ")) + sn + "[" + ip + "]";
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL);
} else {
Mprintf("[%s] %s V2 授权失败: %s [%s]\n", source, passcode.c_str(), sn.c_str(), ip.c_str());
Mprintf("[%s] %s V2 授权失败 %d: %s [%s]\n", source, passcode.c_str(), authorized, sn.c_str(), ip.c_str());
std::string tip = passcode + std::string(_L(" V2 授权失败: ")) + sn + "[" + ip + "]";
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL);
}
@@ -5812,26 +5812,26 @@ std::tuple<bool, bool, bool, bool> CMy2015RemoteDlg::VerifyClientAuth(context* h
// V1 授权验证
isV2 = false;
authorized = AuthorizeClient(host, sn, passcode, hmac, &expired);
if (authorized) {
if (authorized == 0) {
if (host) {
m_ClientMap->SetClientMapInteger(host->GetClientID(), MAP_AUTH, TRUE);
}
isTrail = IsTrail(passcode.c_str());
}
if (ShouldLogAuth(sn, authorized)) {
if (authorized) {
if (authorized == 0) {
Mprintf("[%s] %s V1 授权成功: %s [%s]\n", source, passcode.c_str(), sn.c_str(), ip.c_str());
std::string tip = passcode + std::string(_L(" V1 授权成功: ")) + sn + "[" + ip + "]";
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL);
} else {
Mprintf("[%s] %s V1 授权失败: %s [%s]\n", source, passcode.c_str(), sn.c_str(), ip.c_str());
Mprintf("[%s] %s V1 授权失败 %d: %s [%s]\n", source, passcode.c_str(), authorized, sn.c_str(), ip.c_str());
std::string tip = passcode + std::string(_L(" V1 授权失败: ")) + sn + "[" + ip + "]";
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL);
}
}
}
return std::make_tuple(authorized, isV2, isTrail, expired);
return std::make_tuple(authorized==0, isV2, isTrail, expired);
}
// 检查并发送预设续期(多点验证)
@@ -6350,8 +6350,17 @@ LRESULT CMy2015RemoteDlg::OnOpenScreenSpyDialog(WPARAM wParam, LPARAM lParam)
BYTE bToken = COMMAND_BYE;
return ContextObject->Send2Client(&bToken, 1) ? 0 : 0x20260223;
}
if (clientID && WebService().IsRunning() && WebService().IsWebTriggered(clientID) && WebService().GetHideWebSessions()) {
return OpenDialog<CScreenSpyDlg, IDD_DIALOG_SCREEN_SPY, SW_HIDE>(wParam, lParam);
// Check trigger source: MFC-triggered dialogs are always visible
// Note: Don't clear MfcTriggered here - let OnInitDialog check it to determine session type
if (clientID && WebService().IsRunning()) {
if (WebService().IsMfcTriggered(clientID)) {
// MFC-triggered: show dialog (flag will be cleared in OnInitDialog)
return OpenDialog<CScreenSpyDlg, IDD_DIALOG_SCREEN_SPY, SW_SHOWMAXIMIZED>(wParam, lParam);
}
if (WebService().IsWebTriggered(clientID) && WebService().GetHideWebSessions()) {
// Web-triggered: hide dialog (Web users share this hidden dialog)
return OpenDialog<CScreenSpyDlg, IDD_DIALOG_SCREEN_SPY, SW_HIDE>(wParam, lParam);
}
}
return OpenDialog<CScreenSpyDlg, IDD_DIALOG_SCREEN_SPY, SW_SHOWMAXIMIZED>(wParam, lParam);
}
@@ -6725,7 +6734,7 @@ int run_upx(const std::string& upx, const std::string &file, bool isCompress)
std::string ReleaseUPX()
{
return ReleaseEXE(IDR_BINARY_UPX, "upx.exe");
return ReleaseEXE(IDR_BINARY_UPX, "upx.exe", ResFileName::UPX_EXE);
}
// 解压UPX对当前应用程序进行操作
@@ -7120,10 +7129,16 @@ void CMy2015RemoteDlg::OnDynamicSubMenu(UINT nID)
}
LeaveCriticalSection(&m_cs);
}
// Mark as MFC-triggered when MFC opens remote desktop
void setMfcTriggeredCallback(context* ctx, void* user)
{
WebService().SetMfcTriggered(ctx->GetClientID());
}
void CMy2015RemoteDlg::OnOnlineVirtualDesktop()
{
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 2, ALGORITHM_DIFF, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) };
SendSelectedCommand(bToken, sizeof(bToken));
SendSelectedCommand(bToken, sizeof(bToken), setMfcTriggeredCallback, nullptr);
}
@@ -7132,7 +7147,7 @@ void CMy2015RemoteDlg::OnOnlineGrayDesktop()
if (!ShouldRemoteControl())
return;
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 0, ALGORITHM_GRAY, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) };
SendSelectedCommand(bToken, sizeof(bToken));
SendSelectedCommand(bToken, sizeof(bToken), setMfcTriggeredCallback, nullptr);
}
@@ -7141,7 +7156,7 @@ void CMy2015RemoteDlg::OnOnlineRemoteDesktop()
if (!ShouldRemoteControl())
return;
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 1, ALGORITHM_DIFF, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) };
SendSelectedCommand(bToken, sizeof(bToken));
SendSelectedCommand(bToken, sizeof(bToken), setMfcTriggeredCallback, nullptr);
}
@@ -7150,7 +7165,7 @@ void CMy2015RemoteDlg::OnOnlineH264Desktop()
if (!ShouldRemoteControl())
return;
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 0, ALGORITHM_H264, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) };
SendSelectedCommand(bToken, sizeof(bToken));
SendSelectedCommand(bToken, sizeof(bToken), setMfcTriggeredCallback, nullptr);
}
@@ -8074,8 +8089,8 @@ void CMy2015RemoteDlg::OnExecuteUpload()
void CMy2015RemoteDlg::OnExecuteTestrun()
{
DWORD dwSize = 0;
BYTE* buffer = ReadExeFromResource(dwSize, IDR_TESTRUN_X64, CLIENT_TYPE_ONE, Startup_TestRunMsc,
"YAMA", "ServerD11", COMMAND_UPLOAD_EXEC, 4);
BYTE* buffer = ReadExeFromResource(dwSize, IDR_TESTRUN_X64, ResFileName::TESTRUN_X64,
CLIENT_TYPE_ONE, Startup_TestRunMsc, "YAMA", "ServerD11", COMMAND_UPLOAD_EXEC, 4);
if (buffer && dwSize > 0) {
SendSelectedCommand(buffer, 5 + dwSize);
delete[] buffer;
@@ -8087,8 +8102,8 @@ void CMy2015RemoteDlg::OnExecuteTestrun()
void CMy2015RemoteDlg::OnExecuteGhost()
{
DWORD dwSize = 0;
BYTE *buffer = ReadExeFromResource(dwSize, IDR_GHOST_X64, CLIENT_TYPE_ONE, Startup_GhostMsc,
"YAMA", "ServerDll", COMMAND_UPLOAD_EXEC, 4);
BYTE *buffer = ReadExeFromResource(dwSize, IDR_GHOST_X64, ResFileName::GHOST_X64,
CLIENT_TYPE_ONE, Startup_GhostMsc, "YAMA", "ServerDll", COMMAND_UPLOAD_EXEC, 4);
if (buffer && dwSize > 0) {
SendSelectedCommand(buffer, 5 + dwSize);
delete[] buffer;
@@ -8212,6 +8227,28 @@ void CMy2015RemoteDlg::CloseRemoteDesktopByClientID(uint64_t clientID)
}
}
void CMy2015RemoteDlg::CloseWebRemoteDesktopByClientID(uint64_t clientID)
{
CScreenSpyDlg* targetDlg = nullptr;
HWND hWnd = NULL;
EnterCriticalSection(&m_cs);
for (auto& pair : m_RemoteWnds) {
CScreenSpyDlg* dlg = dynamic_cast<CScreenSpyDlg*>(pair.second);
// Only close Web session dialogs, leave MFC dialogs open
if (dlg && dlg->GetClientID() == clientID && dlg->IsWebSession()) {
targetDlg = dlg;
hWnd = dlg->GetSafeHwnd();
break;
}
}
LeaveCriticalSection(&m_cs);
if (targetDlg && hWnd && ::IsWindow(hWnd)) {
::SendMessage(hWnd, WM_CLOSE, 0, 0);
}
}
void CMy2015RemoteDlg::UpdateActiveRemoteSession(CDialogBase *sess)
{
EnterCriticalSection(&m_cs);
@@ -8569,6 +8606,22 @@ context* CMy2015RemoteDlg::FindHostByIP(const std::string& ip)
return NULL;
}
uint64_t CMy2015RemoteDlg::FindClientIDByIP(const std::string& ip)
{
CString clientIP(ip.c_str());
uint64_t clientID = 0;
EnterCriticalSection(&m_cs);
for (auto i = m_HostList.begin(); i != m_HostList.end(); ++i) {
context* ContextObject = *i;
if (ContextObject->GetClientData(ONLINELIST_IP) == clientIP || ContextObject->GetAdditionalData(RES_CLIENT_PUBIP) == clientIP) {
clientID = ContextObject->GetClientID();
break;
}
}
LeaveCriticalSection(&m_cs);
return clientID;
}
LRESULT CMy2015RemoteDlg::InjectShellcode(WPARAM wParam, LPARAM lParam)
{
std::string* ip = (std::string*)wParam;

View File

@@ -215,8 +215,8 @@ public:
MasterSettings m_settings;
static BOOL CALLBACK NotifyProc(CONTEXT_OBJECT* ContextObject);
static BOOL CALLBACK OfflineProc(CONTEXT_OBJECT* ContextObject);
BOOL AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired = nullptr);
BOOL AuthorizeClientV2(context* ctx, const std::string& sn, const std::string& passcode, const std::string& hmacV2, bool* outExpired = nullptr);
int AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired = nullptr);
int AuthorizeClientV2(context* ctx, const std::string& sn, const std::string& passcode, const std::string& hmacV2, bool* outExpired = nullptr);
VOID MessageHandle(CONTEXT_OBJECT* ContextObject);
VOID SendSelectedCommand(PBYTE szBuffer, ULONG ulLength, contextModifier cb = NULL, void* user=NULL);
VOID SendAllCommand(PBYTE szBuffer, ULONG ulLength);
@@ -255,6 +255,7 @@ public:
CGridDialog * m_gridDlg = NULL;
std::vector<DllInfo*> m_DllList;
context* FindHostByIP(const std::string& ip);
uint64_t FindClientIDByIP(const std::string& ip); // 线程安全在锁内获取ID
void InjectTinyRunDll(const std::string& ip, int pid);
NOTIFYICONDATA m_Nid;
HANDLE m_hExit;
@@ -275,6 +276,7 @@ public:
CDialogBase* GetRemoteWindow(CDialogBase* dlg);
void RemoveRemoteWindow(HWND wnd);
void CloseRemoteDesktopByClientID(uint64_t clientID);
void CloseWebRemoteDesktopByClientID(uint64_t clientID); // Only close Web session dialog
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
void UpdateActiveRemoteSession(CDialogBase* sess);
CDialogBase* GetActiveRemoteSession();

View File

@@ -10,6 +10,8 @@
#include "InputDlg.h"
#include <bcrypt.h>
#include <wincrypt.h>
#include <Shlwapi.h>
#pragma comment(lib, "Shlwapi.lib")
#include "Resource.h"
extern "C" {
#include "client/reg_startup.h"
@@ -49,11 +51,66 @@ std::string GetPwdHash();
int MemoryFind(const char *szBuffer, const char *Key, int iBufferSize, int iKeySize);
LPBYTE ReadResource(int resourceId, DWORD &dwSize)
// 获取程序目录下 res 子目录的路径
static CString GetResDirectoryPath()
{
TCHAR szPath[MAX_PATH];
GetModuleFileName(NULL, szPath, MAX_PATH);
PathRemoveFileSpec(szPath);
PathAppend(szPath, _T("res"));
return CString(szPath);
}
// 从外部文件读取资源(优先级高于内嵌资源)
static LPBYTE ReadResourceFromFile(const char* resName, DWORD &dwSize)
{
if (!resName || !resName[0]) {
return NULL;
}
CString resDir = GetResDirectoryPath();
CString filePath;
filePath.Format(_T("%s\\%hs"), (LPCTSTR)resDir, resName);
// 检查文件是否存在
if (GetFileAttributes(filePath) == INVALID_FILE_ATTRIBUTES) {
return NULL;
}
// 打开文件
HANDLE hFile = CreateFile(filePath, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
return NULL;
}
// 获取文件大小
LARGE_INTEGER fileSize;
if (!GetFileSizeEx(hFile, &fileSize) || fileSize.QuadPart == 0) {
CloseHandle(hFile);
return NULL;
}
// 分配内存并读取文件
dwSize = (DWORD)fileSize.QuadPart;
LPBYTE buffer = new BYTE[dwSize];
DWORD bytesRead = 0;
if (!ReadFile(hFile, buffer, dwSize, &bytesRead, NULL) || bytesRead != dwSize) {
delete[] buffer;
CloseHandle(hFile);
dwSize = 0;
return NULL;
}
CloseHandle(hFile);
return buffer;
}
// 从内嵌资源读取
static LPBYTE ReadResourceFromEmbedded(int resourceId, DWORD &dwSize)
{
dwSize = 0;
auto id = resourceId;
HRSRC hResource = FindResourceA(NULL, MAKEINTRESOURCE(id), "BINARY");
HRSRC hResource = FindResourceA(NULL, MAKEINTRESOURCE(resourceId), "BINARY");
if (hResource == NULL) {
return NULL;
}
@@ -76,6 +133,54 @@ LPBYTE ReadResource(int resourceId, DWORD &dwSize)
return r;
}
// 读取资源:优先从 res 目录读取外部文件,如果不存在则使用内嵌资源
// resName: 外部文件名(如 "ghost_x64.exe"),为空时直接使用内嵌资源
LPBYTE ReadResource(int resourceId, DWORD &dwSize, const char* resName)
{
dwSize = 0;
// 1. 优先尝试从 res 目录读取外部文件
if (resName && resName[0]) {
LPBYTE data = ReadResourceFromFile(resName, dwSize);
if (data) {
return data;
}
}
// 2. 回退到内嵌资源
return ReadResourceFromEmbedded(resourceId, dwSize);
}
// ========== res 目录外部资源文件名定义 ==========
// 命名规范:<模块名>_<架构>.exe/.dll/.bin
// 架构x86 / x64 / linux / macos
namespace ResFileName {
// Ghost 主程序
const char* GHOST_X86 = "ghost_x86.exe";
const char* GHOST_X64 = "ghost_x64.exe";
const char* GHOST_LINUX = "ghost_linux";
const char* GHOST_MACOS = "ghost_macos"; // 预留
// TestRun 加载器
const char* TESTRUN_X86 = "testrun_x86.dll";
const char* TESTRUN_X64 = "testrun_x64.dll";
// ServerDll
const char* SERVERDLL_X86 = "serverdll_x86.dll";
const char* SERVERDLL_X64 = "serverdll_x64.dll";
// TinyRun
const char* TINYRUN_X86 = "tinyrun_x86.exe";
const char* TINYRUN_X64 = "tinyrun_x64.exe";
// SCLoader (Shellcode加载器)
const char* SCLOADER_X86 = "scloader_x86.bin";
const char* SCLOADER_X64 = "scloader_x64.bin";
const char* SCLOADER_X86_OLD = "scloader_old_x86.bin";
const char* SCLOADER_X64_OLD = "scloader_old_x64.bin";
// FRP 相关 (无架构区分64位DLL)
const char* FRPC_DLL = "frpc.dll";
const char* FRPS_DLL = "frps.dll";
// 工具
const char* UPX_EXE = "upx.exe";
const char* RCEDIT_EXE = "rcedit.exe";
}
CString GenerateRandomName(int nLength)
{
@@ -180,10 +285,10 @@ bool MakeShellcode(LPBYTE& compressedBuffer, int& ulTotalSize, LPBYTE originBuff
BOOL WriteBinaryToFile(const char* path, const char* data, ULONGLONG size, LONGLONG offset = 0);
std::string ReleaseEXE(int resID, const char* name)
std::string ReleaseEXE(int resID, const char* name, const char* resName)
{
DWORD dwSize = 0;
LPBYTE data = ReadResource(resID, dwSize);
LPBYTE data = ReadResource(resID, dwSize, resName);
if (!data)
return "";
@@ -329,42 +434,48 @@ void CBuildDlg::OnBnClickedOk()
startup = std::map<int, int> {
{IndexTestRun_DLL, Startup_DLL},{IndexTestRun_MemDLL, Startup_MEMDLL},{IndexTestRun_InjSC, Startup_InjSC},
} [index];
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize);
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
is64bit ? ResFileName::TESTRUN_X64 : ResFileName::TESTRUN_X86);
break;
case IndexGhost:
file = "ghost.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
typ = CLIENT_TYPE_ONE;
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize);
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86);
break;
case IndexGhostMsc:
file = "ghost.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
typ = CLIENT_TYPE_ONE;
startup = Startup_GhostMsc;
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize);
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86);
break;
case IndexTestRunMsc:
file = "TestRun.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
typ = CLIENT_TYPE_MEMDLL;
startup = Startup_TestRunMsc;
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize);
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
is64bit ? ResFileName::TESTRUN_X64 : ResFileName::TESTRUN_X86);
break;
case IndexServerDll:
file = "ServerDll.dll";
typ = CLIENT_TYPE_DLL;
szBuffer = ReadResource(is64bit ? IDR_SERVERDLL_X64 : IDR_SERVERDLL_X86, dwFileSize);
szBuffer = ReadResource(is64bit ? IDR_SERVERDLL_X64 : IDR_SERVERDLL_X86, dwFileSize,
is64bit ? ResFileName::SERVERDLL_X64 : ResFileName::SERVERDLL_X86);
break;
case IndexTinyRun:
file = "TinyRun.dll";
typ = CLIENT_TYPE_SHELLCODE;
szBuffer = ReadResource(is64bit ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, dwFileSize);
szBuffer = ReadResource(is64bit ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, dwFileSize,
is64bit ? ResFileName::TINYRUN_X64 : ResFileName::TINYRUN_X86);
break;
case IndexLinuxGhost:
file = "ghost";
typ = CLIENT_TYPE_LINUX;
szBuffer = ReadResource(IDR_LINUX_GHOST, dwFileSize);
szBuffer = ReadResource(IDR_LINUX_GHOST, dwFileSize, ResFileName::GHOST_LINUX);
break;
case OTHER_ITEM: {
m_OtherItem.GetWindowTextA(file);
@@ -470,7 +581,8 @@ void CBuildDlg::OnBnClickedOk()
} else {
if (sel == CLIENT_COMPRESS_SC_AES) {
DWORD dwSize = 0;
LPBYTE data = ReadResource(is64bit ? IDR_SCLOADER_X64 : IDR_SCLOADER_X86, dwSize);
LPBYTE data = ReadResource(is64bit ? IDR_SCLOADER_X64 : IDR_SCLOADER_X86, dwSize,
is64bit ? ResFileName::SCLOADER_X64 : ResFileName::SCLOADER_X86);
if (data) {
int iOffset = MemoryFind((char*)data, (char*)g_ConnectAddress.Flag(), dwSize, g_ConnectAddress.FlagLen());
if (iOffset != -1) {
@@ -534,7 +646,8 @@ void CBuildDlg::OnBnClickedOk()
} else if (sel == CLIENT_COMPRESS_SC_AES_OLD || // 兼容旧版本
sel == CLIENT_COMP_SC_AES_OLD_UPX) {
DWORD dwSize = 0;
LPBYTE data = ReadResource(is64bit ? IDR_SCLOADER_X64_OLD : IDR_SCLOADER_X86_OLD, dwSize);
LPBYTE data = ReadResource(is64bit ? IDR_SCLOADER_X64_OLD : IDR_SCLOADER_X86_OLD, dwSize,
is64bit ? ResFileName::SCLOADER_X64_OLD : ResFileName::SCLOADER_X86_OLD);
if (data) {
int iOffset = MemoryFind((char*)data, (char*)g_ConnectAddress.Flag(), dwSize, g_ConnectAddress.FlagLen());
if (iOffset != -1) {

View File

@@ -3,9 +3,42 @@
#include "Buffer.h"
#include "LangManager.h"
LPBYTE ReadResource(int resourceId, DWORD& dwSize);
// 读取资源:优先从 res 目录读取外部文件,如果不存在则使用内嵌资源
// resName: 外部文件名(如 "ghost_x64.exe"),为空时直接使用内嵌资源
LPBYTE ReadResource(int resourceId, DWORD& dwSize, const char* resName = nullptr);
std::string ReleaseEXE(int resID, const char* name);
std::string ReleaseEXE(int resID, const char* name, const char* resName = nullptr);
// ========== res 目录外部资源文件名定义 ==========
// 命名规范:<模块名>_<架构>.exe/.dll/.bin
// 架构x86 / x64 / linux / macos
namespace ResFileName {
// Ghost 主程序
extern const char* GHOST_X86;
extern const char* GHOST_X64;
extern const char* GHOST_LINUX;
extern const char* GHOST_MACOS; // 预留
// TestRun 加载器
extern const char* TESTRUN_X86;
extern const char* TESTRUN_X64;
// ServerDll
extern const char* SERVERDLL_X86;
extern const char* SERVERDLL_X64;
// TinyRun
extern const char* TINYRUN_X86;
extern const char* TINYRUN_X64;
// SCLoader (Shellcode加载器)
extern const char* SCLOADER_X86;
extern const char* SCLOADER_X64;
extern const char* SCLOADER_X86_OLD;
extern const char* SCLOADER_X64_OLD;
// FRP 相关 (无架构区分64位DLL)
extern const char* FRPC_DLL;
extern const char* FRPS_DLL;
// 工具
extern const char* UPX_EXE;
extern const char* RCEDIT_EXE;
}
CString BuildPayloadUrl(const char* ip, const char* name);

View File

@@ -5,6 +5,7 @@
#include "CRcEditDlg.h"
#include "afxdialogex.h"
#include "Resource.h"
#include "BuildDlg.h"
// CRcEditDlg 对话框
@@ -78,10 +79,9 @@ void CRcEditDlg::OnOK()
MessageBoxL("请选择[*.ico]图标文件或输入进程描述!", "提示", MB_ICONINFORMATION);
return;
}
std::string ReleaseEXE(int resID, const char* name);
int run_cmd(std::string cmdLine);
std::string rcedit = ReleaseEXE(IDR_BIN_RCEDIT, "rcedit.exe");
std::string rcedit = ReleaseEXE(IDR_BIN_RCEDIT, "rcedit.exe", ResFileName::RCEDIT_EXE);
if (rcedit.empty()) {
MessageBoxL("解压程序失败无法操作PE!", "提示", MB_ICONINFORMATION);
return;

View File

@@ -25,6 +25,26 @@ static UINT indicators[] = {
#define MAX_SEND_BUFFER 65535
#define MAX_RECV_BUFFER 65535
// 静态成员变量定义 - 历史路径记录
CString CFileManagerDlg::s_strLocalHistoryPath;
std::map<uint64_t, CString> CFileManagerDlg::s_mapRemoteHistoryPath;
CLock CFileManagerDlg::s_lockHistory;
// 获取有效的客户端ID优先用 m_ClientID否则通过 IP 找主连接
uint64_t CFileManagerDlg::GetClientID() const
{
// 优先使用已设置的 m_ClientID未来 TOKEN_CLIENTID 会设置这个)
if (m_ClientID != 0) {
return m_ClientID;
}
// 回退:通过 IP 找主连接获取 ClientID线程安全
if (g_2015RemoteDlg && m_ContextObject) {
std::string peerIP = m_ContextObject->GetPeerName();
return g_2015RemoteDlg->FindClientIDByIP(peerIP);
}
return 0;
}
typedef struct {
LVITEM* plvi;
CString sCol2;
@@ -137,10 +157,12 @@ BEGIN_MESSAGE_MAP(CFileManagerDlg, CDialog)
ON_COMMAND(IDT_LOCAL_DOWNLOADS, OnLocalDownloads)
ON_COMMAND(IDT_LOCAL_HOME, OnLocalHome)
ON_COMMAND(IDT_LOCAL_SEARCH, OnLocalSearch)
ON_COMMAND(IDT_LOCAL_HISTORY, OnLocalHistory)
ON_COMMAND(IDT_REMOTE_DESKTOP, OnRemoteDesktop)
ON_COMMAND(IDT_REMOTE_DOWNLOADS, OnRemoteDownloads)
ON_COMMAND(IDT_REMOTE_HOME, OnRemoteHome)
ON_COMMAND(IDT_REMOTE_SEARCH, OnRemoteSearch)
ON_COMMAND(IDT_REMOTE_HISTORY, OnRemoteHistory)
ON_COMMAND(IDM_TRANSFER, OnTransfer)
ON_COMMAND(IDM_RENAME, OnRename)
ON_NOTIFY(LVN_ENDLABELEDIT, IDC_LIST_LOCAL, OnEndlabeleditListLocal)
@@ -494,6 +516,12 @@ void CFileManagerDlg::FixedLocalFileList(CString directory)
}
ShowMessage(_TRF("本地:装载目录 %s 完成"), m_Local_Path);
// 记录本地历史路径
if (m_Local_Path.GetLength() > 0) {
CAutoCLock lock(s_lockHistory);
s_strLocalHistoryPath = m_Local_Path;
}
}
void CFileManagerDlg::DropItemOnList(CListCtrl* pDragList, CListCtrl* pDropList)
@@ -966,6 +994,8 @@ void CFileManagerDlg::OnReceiveComplete()
ShowMessage(_TRF("搜索 \"%s\" 在 %s 完成,共 %d 个结果 (耗时 %d秒)"), m_strSearchName, m_strSearchPath, m_nSearchResultCount, dwElapsed);
}
break;
case TOKEN_CLIENTID:
break;
default:
SendException();
break;
@@ -1024,6 +1054,13 @@ void CFileManagerDlg::GetRemoteFileList(CString directory)
m_Remote_Directory_ComboBox.InsertStringL(0, m_Remote_Path);
m_Remote_Directory_ComboBox.SetCurSel(0);
// 记录远程历史路径按客户端ID区分
uint64_t clientID = GetClientID();
if (m_Remote_Path.GetLength() > 0 && clientID != 0) {
CAutoCLock lock(s_lockHistory);
s_mapRemoteHistoryPath[clientID] = m_Remote_Path;
}
// 得到返回数据前禁窗口
m_list_remote.EnableWindow(FALSE);
m_ProgressCtrl->SetPos(0);
@@ -1594,6 +1631,57 @@ void CFileManagerDlg::OnRemoteSearch()
}
}
void CFileManagerDlg::OnLocalHistory()
{
// 跳转到上次打开的本地文件夹
CString historyPath;
{
CAutoCLock lock(s_lockHistory);
historyPath = s_strLocalHistoryPath;
}
if (historyPath.IsEmpty()) {
ShowMessage(_TR("没有本地历史记录"));
return;
}
// 检查目录是否存在
if (GetFileAttributesA(historyPath) == INVALID_FILE_ATTRIBUTES) {
ShowMessage(_TRF("历史目录不存在: %s"), historyPath);
return;
}
m_Local_Path = historyPath;
FixedLocalFileList(".");
}
void CFileManagerDlg::OnRemoteHistory()
{
// 跳转到上次打开的远程文件夹按客户端ID区分
uint64_t clientID = GetClientID();
if (clientID == 0) {
ShowMessage(_TR("无法识别远程主机"));
return;
}
CString historyPath;
{
CAutoCLock lock(s_lockHistory);
auto it = s_mapRemoteHistoryPath.find(clientID);
if (it != s_mapRemoteHistoryPath.end()) {
historyPath = it->second;
}
}
if (historyPath.IsEmpty()) {
ShowMessage(_TR("没有远程历史记录"));
return;
}
m_Remote_Path = historyPath;
GetRemoteFileList(".");
}
void CFileManagerDlg::OnLocalView()
{
// TODO: Add your command handler code here

View File

@@ -7,6 +7,7 @@
#include "IOCPServer.h"
#include "SortListCtrl.h"
#include "../../common/locker.h"
#include <map>
#include <string>
#include <vector>
@@ -246,10 +247,12 @@ protected:
afx_msg void OnLocalDownloads();
afx_msg void OnLocalHome();
afx_msg void OnLocalSearch();
afx_msg void OnLocalHistory();
afx_msg void OnRemoteDesktop();
afx_msg void OnRemoteDownloads();
afx_msg void OnRemoteHome();
afx_msg void OnRemoteSearch();
afx_msg void OnRemoteHistory();
afx_msg void OnTransferV2ToRemote(); // V2: 本地文件传输到远程
afx_msg void OnTransferV2ToLocal(); // V2: 远程文件传输到本地
//}}AFX_MSG
@@ -274,6 +277,14 @@ private:
void EnableControl(BOOL bEnable = TRUE);
void CollectFilesRecursive(const std::string& dirPath, std::vector<std::string>& files);
float m_fScalingFactor;
// 历史路径记录(静态,跨实例共享)
static CString s_strLocalHistoryPath;
static std::map<uint64_t, CString> s_mapRemoteHistoryPath;
static CLock s_lockHistory; // 保护历史路径的锁
// 获取有效的客户端ID优先用 m_ClientID否则通过 IP 找主连接)
uint64_t GetClientID() const;
public:
afx_msg void OnFilemangerCompress();
afx_msg void OnFilemangerUncompress();

View File

@@ -228,7 +228,7 @@ public:
{
return m_bIsClosed;
}
uint64_t GetClientID() const {
virtual uint64_t GetClientID() const {
return m_ClientID;
}
BOOL SayByeBye()

View File

@@ -1,11 +1,13 @@
#pragma once
#include <map>
#include <set>
#include <string>
#include <vector>
#include <locale.h>
#include <afxwin.h>
#include "common/IniParser.h"
#include "resource.h" // 用于内嵌语言资源 ID
// 设置线程区域为简体中文
// 这样 MBCS 程序在非中文系统上创建对话框时,也能正确解码 RC 资源中的 GBK 中文
@@ -60,12 +62,18 @@ public:
CreateDirectory(m_langDir, NULL);
}
// 获取可用的语言列表
// 获取可用的语言列表(包括内嵌语言)
std::vector<CString> GetAvailableLanguages()
{
std::vector<CString> langs;
CString searchPath = m_langDir + _T("\\*.ini");
std::set<CString> langSet; // 用于去重
// 1. 添加内嵌语言(始终可用)
langSet.insert(_T("en_US"));
langSet.insert(_T("zh_TW"));
// 2. 扫描磁盘上的语言文件
CString searchPath = m_langDir + _T("\\*.ini");
WIN32_FIND_DATA fd;
HANDLE hFind = FindFirstFile(searchPath, &fd);
if (hFind != INVALID_HANDLE_VALUE) {
@@ -73,30 +81,43 @@ public:
CString filename(fd.cFileName);
int dotPos = filename.ReverseFind(_T('.'));
if (dotPos > 0) {
langs.push_back(filename.Left(dotPos));
langSet.insert(filename.Left(dotPos));
}
} while (FindNextFile(hFind, &fd));
FindClose(hFind);
}
// 转为 vector 返回
for (const auto& lang : langSet) {
langs.push_back(lang);
}
return langs;
}
// 检查语言文件编码是否为 ANSI
// 返回 false 表示文件不存在或编码不是 ANSI检测 BOM 和 UTF-8 无 BOM
// 返回 false 表示编码不是 ANSI检测 BOM 和 UTF-8 无 BOM
// 内嵌语言en_US, zh_TW直接返回 true
bool CheckEncoding(const CString& langCode)
{
// 中文模式无需检查
if (langCode == _T("zh_CN") || langCode.IsEmpty()) {
TRACE("[LangEnc] zh_CN or empty, skip check\n");
return true;
}
// 内嵌语言无需检查(已确保编码正确)
if (langCode == _T("en_US") || langCode == _T("zh_TW")) {
TRACE("[LangEnc] builtin language, skip check\n");
return true;
}
CString langFile = m_langDir + _T("\\") + langCode + _T(".ini");
TRACE("[LangEnc] Checking: %s\n", (LPCSTR)langFile);
FILE* f = nullptr;
if (fopen_s(&f, (LPCSTR)langFile, "rb") != 0 || !f) {
TRACE("[LangEnc] fopen failed\n");
return false;
return false; // 非内嵌语言必须有磁盘文件
}
// 读取文件内容(最多检测前 4KB 即可判断)
@@ -164,26 +185,103 @@ public:
}
// 加载语言文件
// 优先从内嵌资源加载,然后用磁盘文件覆盖(如果存在)
bool Load(const CString& langCode)
{
m_strings.clear();
m_currentLang = langCode;
// 如果是中文,不需要加载翻译
// 中文模式:检查是否有补丁文件
if (langCode == _T("zh_CN") || langCode.IsEmpty()) {
// 尝试加载中文补丁文件(可选)
CString patchFile = m_langDir + _T("\\zh_CN.ini");
if (GetFileAttributes(patchFile) != INVALID_FILE_ATTRIBUTES) {
CIniParser ini;
if (ini.LoadFile((LPCSTR)patchFile)) {
const CIniParser::TKeyVal* pSection = ini.GetSection("Strings");
if (pSection) {
for (const auto& kv : *pSection) {
m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str());
}
}
TRACE("[Lang] Loaded zh_CN patch: %d strings\n", (int)m_strings.size());
}
}
return true;
}
CString langFile = m_langDir + _T("\\") + langCode + _T(".ini");
// 1. 先从内嵌资源加载(英语和繁体中文)
bool hasBuiltin = LoadFromResource(langCode);
// 检查文件是否存在
if (GetFileAttributes(langFile) == INVALID_FILE_ATTRIBUTES) {
// 2. 再从磁盘文件加载(覆盖内嵌翻译)
CString langFile = m_langDir + _T("\\") + langCode + _T(".ini");
if (GetFileAttributes(langFile) != INVALID_FILE_ATTRIBUTES) {
CIniParser ini;
// 如果有内嵌资源,使用追加模式覆盖;否则使用普通加载
if (hasBuiltin) {
// 追加模式:磁盘文件中的翻译会覆盖内嵌翻译
if (ini.LoadFile((LPCSTR)langFile)) {
const CIniParser::TKeyVal* pSection = ini.GetSection("Strings");
if (pSection) {
for (const auto& kv : *pSection) {
m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str());
}
}
TRACE("[Lang] Loaded disk file (override): %s\n", (LPCSTR)langFile);
}
} else {
// 无内嵌资源,直接从磁盘加载
if (ini.LoadFile((LPCSTR)langFile)) {
const CIniParser::TKeyVal* pSection = ini.GetSection("Strings");
if (pSection) {
for (const auto& kv : *pSection) {
m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str());
}
}
TRACE("[Lang] Loaded disk file: %s\n", (LPCSTR)langFile);
return true;
}
}
}
return hasBuiltin || !m_strings.empty();
}
// 从内嵌资源加载语言数据
bool LoadFromResource(const CString& langCode)
{
UINT resID = 0;
if (langCode == _T("en_US")) {
resID = IDR_LANG_EN_US;
} else if (langCode == _T("zh_TW")) {
resID = IDR_LANG_ZH_TW;
} else {
return false; // 无内嵌资源
}
HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(resID), RT_RCDATA);
if (!hRes) {
TRACE("[Lang] Resource not found: %d\n", resID);
return false;
}
// 使用 CIniParser 解析,无文件大小限制,且不 trim key
HGLOBAL hData = LoadResource(NULL, hRes);
if (!hData) {
TRACE("[Lang] Failed to load resource: %d\n", resID);
return false;
}
const char* data = (const char*)LockResource(hData);
DWORD size = SizeofResource(NULL, hRes);
if (!data || size == 0) {
TRACE("[Lang] Empty resource: %d\n", resID);
return false;
}
// 使用 CIniParser 从内存解析
CIniParser ini;
if (!ini.LoadFile((LPCSTR)langFile)) {
if (!ini.LoadFromMemory(data, size)) {
TRACE("[Lang] Failed to parse resource: %d\n", resID);
return false;
}
@@ -194,6 +292,8 @@ public:
}
}
TRACE("[Lang] Loaded builtin resource: %s (%d strings)\n",
(LPCSTR)langCode, (int)m_strings.size());
return true;
}
@@ -625,24 +725,24 @@ protected:
AppendData(&dlgTemplate, sizeof(DLGTEMPLATE));
AppendWord(0); // 菜单
AppendWord(0); // 窗口类
AppendString(_T("选择语言 / Select Language"));
AppendString(_T("Select Language"));
AlignToDword();
// 静态文本
AddControl(0x0082, 15, 15, 40, 12, (WORD)-1,
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("语言:"));
AddControl(0x0082, 15, 15, 50, 12, (WORD)-1,
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("Language:"));
// ComboBox
AddControl(0x0085, 55, 13, 130, 150, 1001,
AddControl(0x0085, 65, 13, 120, 150, 1001,
CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T(""));
// 确定按钮
// OK 按钮
AddControl(0x0080, 45, 50, 50, 14, IDOK,
BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("确定"));
BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("OK"));
// 取消按钮
// Cancel 按钮
AddControl(0x0080, 105, 50, 50, 14, IDCANCEL,
BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("取消"));
BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("Cancel"));
return (LPCDLGTEMPLATE)m_templateBuffer.data();
}
@@ -703,8 +803,8 @@ protected:
m_comboLang.SubclassDlgItem(1001, this);
// 添加简体中文
int idx = m_comboLang.AddString(_T("简体中文"));
// 添加简体中文(显示为英语避免乱码)
int idx = m_comboLang.AddString(GetLanguageDisplayName(_T("zh_CN")));
m_langCodes.push_back(_T("zh_CN"));
m_comboLang.SetItemData(idx, 0);

View File

@@ -157,8 +157,9 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
if (pClientID) {
m_ClientID = *((uint64_t*)pClientID);
// Notify web clients of resolution (important for clients that only send TOKEN_BITMAPINFO once)
if (WebService().IsRunning()) {
// Notify web clients of resolution (only for Web sessions, not MFC sessions)
// At this point, IsMfcTriggered is still set if MFC triggered this dialog
if (WebService().IsRunning() && !WebService().IsMfcTriggered(m_ClientID)) {
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
WebService().NotifyResolutionChange(m_ClientID, width, height);
@@ -237,10 +238,14 @@ CScreenSpyDlg::~CScreenSpyDlg()
StopAudioPlayback();
// 清理所有文件接收对话框
for (auto& pair : m_FileRecvDlgs) {
if (pair.second) {
pair.second->DestroyWindow();
delete pair.second;
// 注意对话框可能已经被用户关闭并自我销毁PostNcDestroy 中 delete this
// 存储了 HWND 用于安全检查,避免访问野指针
for (auto& entry : m_FileRecvDlgs) {
HWND hWnd = entry.second.first;
if (hWnd && ::IsWindow(hWnd)) {
// 通过 HWND 同步发送关闭消息,确保对话框在析构前完全关闭
// 使用 SendMessage 而非 PostMessage避免异步问题
::SendMessage(hWnd, WM_CLOSE, 0, 0);
}
}
m_FileRecvDlgs.clear();
@@ -500,6 +505,7 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CScreenSpyDlg::OnRecvFileV2Chunk)
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CScreenSpyDlg::OnRecvFileV2Complete)
ON_WM_DROPFILES()
ON_WM_CAPTURECHANGED()
END_MESSAGE_MAP()
@@ -761,23 +767,43 @@ BOOL CScreenSpyDlg::OnInitDialog()
if (pMain)
::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)this, 0);
// 注册屏幕上下文到 WebService用于 Web 端鼠标/键盘控制)
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
// Determine session type: MFC or Web
// Must check MfcTriggered FIRST - if MFC triggered this dialog, it's NOT a web session
// even if WebTriggered is also true (happens when Web is already open for same device)
bool isMfcSession = WebService().IsMfcTriggered(m_ClientID);
bool isWebSession = false;
if (isMfcSession) {
// MFC session: clear the flag, don't register with WebService
WebService().ClearMfcTriggered(m_ClientID);
// m_bIsWebSession remains false (default)
} else {
// Check if this is a Web session
isWebSession = WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions();
// Hide window if this session was triggered by web client
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
m_bHide = true;
ShowWindow(SW_HIDE);
// Only register screen context for Web sessions
// MFC dialogs handle input directly via m_ContextObject, don't need WebService registry
// This prevents MFC close from deleting Web's context (they share same device_id key)
if (isWebSession) {
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
m_bHide = true;
m_bIsWebSession = true;
ShowWindow(SW_HIDE);
}
}
Mprintf("[ScreenSpy] Dialog created for device %llu, isMfcSession=%d, isWebSession=%d\n",
m_ClientID, isMfcSession ? 1 : 0, isWebSession ? 1 : 0);
return TRUE;
}
VOID CScreenSpyDlg::OnClose()
{
// 注销屏幕上下文Web 端控制)
WebService().UnregisterScreenContext(m_ClientID);
// Only unregister if this is a Web session (we only registered for Web sessions)
if (m_bIsWebSession) {
WebService().UnregisterScreenContext(m_ClientID);
}
m_bIsClosed = true;
m_bIsCtrl = FALSE;
@@ -840,13 +866,15 @@ LRESULT CScreenSpyDlg::OnRecvFileV2Chunk(WPARAM wParam, LPARAM lParam)
uint64_t transferID = msgData->transferID;
// 创建或获取进度对话框(按 transferID 管理)
CDlgFileSend*& dlg = m_FileRecvDlgs[transferID];
if (dlg == nullptr) {
auto& entry = m_FileRecvDlgs[transferID];
CDlgFileSend* dlg = entry.second;
if (dlg == nullptr || !::IsWindow(entry.first)) {
dlg = new CDlgFileSend(m_pParent, m_ContextObject->GetServer(), m_ContextObject, FALSE);
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
dlg->SetWindowTextA(_TR("接收文件"));
dlg->ShowWindow(SW_SHOW);
dlg->m_bKeepConnection = TRUE; // 不断开连接
entry = { dlg->GetSafeHwnd(), dlg };
}
// 接收文件
@@ -889,7 +917,11 @@ LRESULT CScreenSpyDlg::OnRecvFileV2Complete(WPARAM wParam, LPARAM lParam)
// 关闭进度对话框
auto it = m_FileRecvDlgs.find(transferID);
if (it != m_FileRecvDlgs.end()) {
it->second->FinishFileSend(verifyOk);
// 只有窗口有效时才调用 FinishFileSend
if (::IsWindow(it->second.first)) {
it->second.second->FinishFileSend(verifyOk);
}
// 无论窗口是否有效,都要移除条目(避免累积无效条目)
m_FileRecvDlgs.erase(it);
}
@@ -964,18 +996,11 @@ VOID CScreenSpyDlg::OnReceiveComplete()
PrepareDrawing(m_BitmapInfor_Full);
// 分辨率切换完成,允许解码
m_bResolutionChanging = false;
// Notify web clients of resolution change
if (WebService().IsRunning()) {
// Notify web clients of resolution change (only for Web session dialogs)
if (m_bIsWebSession && WebService().IsRunning()) {
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
WebService().NotifyResolutionChange(m_ClientID, width, height);
// Hide window if this session was triggered by web client (and hiding is enabled)
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
m_bHide = true;
ShowWindow(SW_HIDE);
Mprintf("[ScreenSpyDlg] Web-triggered session, hiding window for device %llu\n", m_ClientID);
}
}
break;
}
@@ -1266,8 +1291,8 @@ 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()) {
// 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
if (m_bIsWebSession && WebService().IsRunning()) {
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
}
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
@@ -1317,8 +1342,8 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
bChange = TRUE;
}
}
// Broadcast H264 keyframe to web clients
if (NextScreenLength > 0 && WebService().IsRunning()) {
// Broadcast H264 keyframe to web clients (only for Web session dialogs)
if (m_bIsWebSession && 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
@@ -1376,9 +1401,9 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
bChange = TRUE;
}
}
// Broadcast H264 frame to web clients
// Broadcast H264 frame to web clients (only for Web session dialogs)
// Format: [DeviceID:4][FrameType:1][DataLen:4][H264Data:N]
if (NextScreenLength > 0 && WebService().IsRunning()) {
if (m_bIsWebSession && NextScreenLength > 0 && WebService().IsRunning()) {
// Detect H264 keyframe by checking NAL unit type
// NAL type 5 = IDR slice (keyframe), NAL type 7 = SPS, NAL type 8 = PPS
bool isKeyFrame = false;
@@ -1463,8 +1488,8 @@ VOID CScreenSpyDlg::DrawScrollFrame()
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0];
if (bOldCursorIndex != m_bCursorIndex) {
bChange = TRUE;
// 通知 Web 客户端光标变化
if (WebService().IsRunning()) {
// 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
if (m_bIsWebSession && WebService().IsRunning()) {
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
}
}
@@ -1594,9 +1619,18 @@ void CScreenSpyDlg::OnPaint()
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
if (m_bAdaptiveSize) {
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
// 放大模式渲染
if (m_bZoomedIn && !m_rcZoomSrc.IsRectEmpty()) {
// 使用放大区域作为源进行StretchBlt
StretchBlt(m_hFullDC, 0, 0, dstW, dstH,
m_hFullMemDC,
m_rcZoomSrc.left, m_rcZoomSrc.top,
m_rcZoomSrc.Width(), m_rcZoomSrc.Height(),
SRCCOPY);
} else if (m_bAdaptiveSize) {
// 尺寸相同时用 BitBlt更快否则用 StretchBlt
if (srcW == dstW && srcH == dstH) {
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, 0, 0, SRCCOPY);
@@ -1607,6 +1641,24 @@ void CScreenSpyDlg::OnPaint()
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, m_ulHScrollPos, m_ulVScrollPos, SRCCOPY);
}
// 绘制框选矩形
if (m_bSelectingZoom) {
CRect rcSelect;
rcSelect.left = min(m_ptZoomStart.x, m_ptZoomCurrent.x);
rcSelect.top = min(m_ptZoomStart.y, m_ptZoomCurrent.y);
rcSelect.right = max(m_ptZoomStart.x, m_ptZoomCurrent.x);
rcSelect.bottom = max(m_ptZoomStart.y, m_ptZoomCurrent.y);
// 使用虚线边框绘制选择框
HPEN hPen = CreatePen(PS_DASH, 1, RGB(255, 0, 0));
HPEN hOldPen = (HPEN)SelectObject(m_hFullDC, hPen);
HBRUSH hOldBrush = (HBRUSH)SelectObject(m_hFullDC, GetStockObject(NULL_BRUSH));
Rectangle(m_hFullDC, rcSelect.left, rcSelect.top, rcSelect.right, rcSelect.bottom);
SelectObject(m_hFullDC, hOldBrush);
SelectObject(m_hFullDC, hOldPen);
DeleteObject(hPen);
}
if ((m_bIsCtrl && m_Settings.RemoteCursor) || m_bIsTraceCursor) {
CPoint ptLocal;
GetCursorPos(&ptLocal);
@@ -1789,6 +1841,10 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
switch (nID) {
case IDM_CONTROL: {
m_bIsCtrl = !m_bIsCtrl;
// 进入控制模式时重置放大状态
if (m_bIsCtrl && m_bZoomedIn) {
ResetZoom();
}
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
// 控制模式:禁用本地 IME查看模式启用本地 IME
@@ -2305,8 +2361,8 @@ BOOL CScreenSpyDlg::PreTranslateMessage(MSG* pMsg)
MSG wheelMsg = *pMsg;
wheelMsg.lParam = MAKELPARAM(pt.x, pt.y);
SendScaledMouseMessage(&wheelMsg, true);
return TRUE; // 已处理,阻止继续分发到 OnMouseWheel
}
break;
case WM_KEYDOWN:
case WM_KEYUP:
case WM_SYSKEYDOWN:
@@ -2541,6 +2597,11 @@ void CScreenSpyDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
void CScreenSpyDlg::EnterFullScreen()
{
// 进入全屏时重置放大状态
if (m_bZoomedIn) {
ResetZoom();
}
if (1) {
// 1. 获取对话框当前所在的显示器
HMONITOR hMonitor = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTONEAREST);
@@ -2628,6 +2689,11 @@ void CScreenSpyDlg::EnterFullScreen()
// 全屏退出成功则返回true
bool CScreenSpyDlg::LeaveFullScreen()
{
// 退出全屏时重置放大状态
if (m_bZoomedIn) {
ResetZoom();
}
if (1) {
KillTimer(1);
if (m_pToolbar) {
@@ -2668,26 +2734,250 @@ bool CScreenSpyDlg::LeaveFullScreen()
return false;
}
// ========== 局部放大功能辅助函数 ==========
// 重置放大状态
void CScreenSpyDlg::ResetZoom()
{
m_bZoomedIn = false;
m_bSelectingZoom = false;
m_bZoomDragging = false;
m_rcZoomSrc.SetRectEmpty();
Invalidate();
}
// 屏幕坐标转原图坐标(考虑放大状态)
CPoint CScreenSpyDlg::ScreenToImage(CPoint pt)
{
if (!m_BitmapInfor_Full) return pt;
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
if (dstW <= 0 || dstH <= 0) return pt; // 防止除零
if (m_bZoomedIn && !m_rcZoomSrc.IsRectEmpty()) {
// 放大状态:从显示区域映射到放大区域
double scaleX = (double)m_rcZoomSrc.Width() / dstW;
double scaleY = (double)m_rcZoomSrc.Height() / dstH;
return CPoint(
(int)(m_rcZoomSrc.left + pt.x * scaleX),
(int)(m_rcZoomSrc.top + pt.y * scaleY)
);
} else if (m_bAdaptiveSize) {
// 自适应模式:按比例缩放
return CPoint((int)(pt.x * m_wZoom), (int)(pt.y * m_hZoom));
} else {
// 滚动模式:加上滚动偏移
return CPoint(pt.x + m_ulHScrollPos, pt.y + m_ulVScrollPos);
}
}
// 原图坐标转屏幕坐标(考虑放大状态)
CPoint CScreenSpyDlg::ImageToScreen(CPoint pt)
{
if (!m_BitmapInfor_Full) return pt;
int zoomW = m_rcZoomSrc.Width();
int zoomH = m_rcZoomSrc.Height();
if (m_bZoomedIn && zoomW > 0 && zoomH > 0) {
// 放大状态:从放大区域映射到显示区域
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
double scaleX = (double)dstW / zoomW;
double scaleY = (double)dstH / zoomH;
return CPoint(
(int)((pt.x - m_rcZoomSrc.left) * scaleX),
(int)((pt.y - m_rcZoomSrc.top) * scaleY)
);
} else if (m_bAdaptiveSize) {
if (m_wZoom > 0 && m_hZoom > 0) {
return CPoint((int)(pt.x / m_wZoom), (int)(pt.y / m_hZoom));
}
return pt;
} else {
return CPoint(pt.x - m_ulHScrollPos, pt.y - m_ulVScrollPos);
}
}
void CScreenSpyDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
// 非控制模式下的放大功能
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full) {
if (m_bZoomedIn) {
// 放大状态:开始拖拽平移
m_bZoomDragging = true;
m_ptZoomDragStart = point; // 保存起点用于点击检测
m_ptZoomDragLast = point; // 用于增量拖拽计算
SetCapture();
return;
} else {
// 正常状态:开始框选放大区域
m_bSelectingZoom = true;
m_ptZoomStart = point;
m_ptZoomCurrent = point;
SetCapture();
return;
}
}
__super::OnLButtonDown(nFlags, point);
}
void CScreenSpyDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
// 处理放大功能的鼠标释放
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full) {
if (m_bSelectingZoom) {
// 完成框选
ReleaseCapture();
m_bSelectingZoom = false;
// 计算选择区域确保left<right, top<bottom
CRect rcSelect;
rcSelect.left = min(m_ptZoomStart.x, point.x);
rcSelect.top = min(m_ptZoomStart.y, point.y);
rcSelect.right = max(m_ptZoomStart.x, point.x);
rcSelect.bottom = max(m_ptZoomStart.y, point.y);
// 框选区域太小时视为点击,如果已放大则还原
if (rcSelect.Width() < 20 || rcSelect.Height() < 20) {
if (m_bZoomedIn) {
ResetZoom();
}
return;
}
// 将屏幕坐标转换为原图坐标
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
if (m_bAdaptiveSize) {
m_rcZoomSrc.left = (int)(rcSelect.left * m_wZoom);
m_rcZoomSrc.top = (int)(rcSelect.top * m_hZoom);
m_rcZoomSrc.right = (int)(rcSelect.right * m_wZoom);
m_rcZoomSrc.bottom = (int)(rcSelect.bottom * m_hZoom);
} else {
m_rcZoomSrc.left = rcSelect.left + m_ulHScrollPos;
m_rcZoomSrc.top = rcSelect.top + m_ulVScrollPos;
m_rcZoomSrc.right = rcSelect.right + m_ulHScrollPos;
m_rcZoomSrc.bottom = rcSelect.bottom + m_ulVScrollPos;
}
// 限制在原图范围内
m_rcZoomSrc.left = max(0L, min(m_rcZoomSrc.left, (LONG)srcW));
m_rcZoomSrc.top = max(0L, min(m_rcZoomSrc.top, (LONG)srcH));
m_rcZoomSrc.right = max(0L, min(m_rcZoomSrc.right, (LONG)srcW));
m_rcZoomSrc.bottom = max(0L, min(m_rcZoomSrc.bottom, (LONG)srcH));
// 进入放大状态
m_bZoomedIn = true;
Invalidate();
return;
}
if (m_bZoomDragging) {
// 完成拖拽
ReleaseCapture();
m_bZoomDragging = false;
// 检查是否为点击(几乎没有移动)
int dx = abs(point.x - m_ptZoomDragStart.x);
int dy = abs(point.y - m_ptZoomDragStart.y);
if (dx < 5 && dy < 5) {
// 点击还原
ResetZoom();
}
return;
}
}
__super::OnLButtonUp(nFlags, point);
}
BOOL CScreenSpyDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
return __super::OnMouseWheel(nFlags, zDelta, pt);
// Convert screen coordinates to client coordinates
ScreenToClient(&pt);
// Build MSG structure for SendScaledMouseMessage
MSG msg = {};
msg.hwnd = m_hWnd;
msg.message = WM_MOUSEWHEEL;
msg.wParam = MAKEWPARAM(nFlags, zDelta);
msg.lParam = MAKELPARAM(pt.x, pt.y);
msg.time = GetTickCount();
msg.pt = { pt.x, pt.y };
SendScaledMouseMessage(&msg, true);
return TRUE; // Message handled, don't pass to parent
}
void CScreenSpyDlg::OnMouseMove(UINT nFlags, CPoint point)
{
// 处理放大功能的鼠标移动
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full) {
if (m_bSelectingZoom) {
// 框选中:更新当前点并重绘选择框
m_ptZoomCurrent = point;
Invalidate(FALSE); // FALSE表示不擦除背景减少闪烁
return;
}
if (m_bZoomDragging) {
// 拖拽平移:计算偏移量并移动放大区域
int dx = point.x - m_ptZoomDragLast.x;
int dy = point.y - m_ptZoomDragLast.y;
m_ptZoomDragLast = point; // 更新上一点保持m_ptZoomDragStart不变用于点击检测
// 计算缩放比例(添加除零保护)
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
int zoomW = m_rcZoomSrc.Width();
int zoomH = m_rcZoomSrc.Height();
if (dstW <= 0 || dstH <= 0 || zoomW <= 0 || zoomH <= 0) {
return; // 防止除零
}
double scaleX = (double)zoomW / dstW;
double scaleY = (double)zoomH / dstH;
// 将屏幕偏移转换为原图偏移(方向相反)
int imgDx = (int)(-dx * scaleX);
int imgDy = (int)(-dy * scaleY);
// 移动放大区域
m_rcZoomSrc.OffsetRect(imgDx, imgDy);
// 限制在原图范围内
if (m_rcZoomSrc.left < 0) {
m_rcZoomSrc.right -= m_rcZoomSrc.left;
m_rcZoomSrc.left = 0;
}
if (m_rcZoomSrc.top < 0) {
m_rcZoomSrc.bottom -= m_rcZoomSrc.top;
m_rcZoomSrc.top = 0;
}
if (m_rcZoomSrc.right > srcW) {
m_rcZoomSrc.left -= (m_rcZoomSrc.right - srcW);
m_rcZoomSrc.right = srcW;
}
if (m_rcZoomSrc.bottom > srcH) {
m_rcZoomSrc.top -= (m_rcZoomSrc.bottom - srcH);
m_rcZoomSrc.bottom = srcH;
}
Invalidate(FALSE);
return;
}
}
if (m_Settings.RemoteCursor) {
if (m_pToolbar != NULL && ::IsWindow(m_pToolbar->m_hWnd) && m_pToolbar->IsWindowVisible()) {
CRect rcToolbar;
@@ -2770,11 +3060,26 @@ void CScreenSpyDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
void CScreenSpyDlg::UpdateCtrlStatus(BOOL ctrl)
{
m_bIsCtrl = ctrl;
// 进入控制模式时重置放大状态
if (m_bIsCtrl && m_bZoomedIn) {
ResetZoom();
}
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
// 控制模式:禁用本地 IME查看模式启用本地 IME
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
}
void CScreenSpyDlg::OnCaptureChanged(CWnd* pWnd)
{
// 捕获丢失时重置框选/拖拽状态
if (m_bSelectingZoom || m_bZoomDragging) {
m_bSelectingZoom = false;
m_bZoomDragging = false;
Invalidate();
}
__super::OnCaptureChanged(pWnd);
}
void CScreenSpyDlg::OnDropFiles(HDROP hDropInfo)
{
if (m_bIsCtrl && m_bConnected) {

View File

@@ -1,6 +1,7 @@
#pragma once
#include <imm.h>
#include <map>
#include <atomic>
#include "IOCPServer.h"
#include "..\..\client\CursorInfo.h"
#include "VideoDlg.h"
@@ -153,6 +154,10 @@ public:
return TRUE;
}
// Check if this dialog was created by Web request (shared by Web users)
bool IsWebSession() const { return m_bIsWebSession.load(); }
void SetWebSession(bool isWeb) { m_bIsWebSession.store(isWeb); }
VOID SendNext(void);
VOID OnReceiveComplete();
HDC m_hFullDC;
@@ -186,13 +191,15 @@ public:
int m_FrameID;
HIMC m_hOldIMC = NULL; // 保存原始 IME 上下文,控制模式切换时使用
bool m_bHide = false;
std::atomic<bool> m_bIsWebSession{false}; // True if this dialog was created by Web request (atomic for thread safety)
std::string m_strSaveNotice; // 截图保存路径提示
ULONGLONG m_nSaveNoticeTime = 0; // 截图提示开始时间
BOOL m_bUsingFRP = FALSE;
// 文件接收进度对话框(用于 Linux Ctrl+C -> 服务端 Ctrl+V
// 按 transferID 管理多个并发传输
std::map<uint64_t, class CDlgFileSend*> m_FileRecvDlgs;
// 存储 {HWND, 指针} 对HWND 用于安全检查(指针可能变成野指针)
std::map<uint64_t, std::pair<HWND, class CDlgFileSend*>> m_FileRecvDlgs;
void SaveSnapshot(void);
// 对话框数据
@@ -216,6 +223,20 @@ public:
double m_wZoom=1, m_hZoom=1;
bool m_bMouseTracking = false;
// ========== 局部放大功能 ==========
bool m_bZoomedIn = false; // 是否处于放大状态
CRect m_rcZoomSrc; // 放大区域(原图坐标)
bool m_bSelectingZoom = false; // 是否正在框选
CPoint m_ptZoomStart; // 框选起点(屏幕坐标)
CPoint m_ptZoomCurrent; // 框选当前点(屏幕坐标)
bool m_bZoomDragging = false; // 是否正在拖拽平移
CPoint m_ptZoomDragStart; // 拖拽起点(用于点击检测)
CPoint m_ptZoomDragLast; // 拖拽上一点(用于增量计算)
void ResetZoom(); // 重置放大状态
CPoint ScreenToImage(CPoint pt); // 屏幕坐标转原图坐标
CPoint ImageToScreen(CPoint pt); // 原图坐标转屏幕坐标
CString m_aviFile;
CBmpToAvi m_aviStream;
@@ -295,6 +316,7 @@ public:
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg void OnMouseLeave();
afx_msg void OnKillFocus(CWnd* pNewWnd);
afx_msg void OnCaptureChanged(CWnd* pWnd);
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized);
afx_msg LRESULT OnDisconnect(WPARAM wParam, LPARAM lParam);

View File

@@ -997,10 +997,14 @@ inline std::string GetWebPageHTML() {
<h4>Create New User</h4>
<input type="text" id="new-username" placeholder="Username" autocomplete="off">
<input type="password" id="new-password" placeholder="Password" autocomplete="new-password">
<select id="new-role">
<select id="new-role" onchange="onRoleChange()">
<option value="viewer">Viewer (read-only)</option>
<option value="admin">Admin (full access)</option>
</select>
<div class="groups-section" id="groups-section">
<label style="font-size:13px;color:#aaa;display:block;margin:8px 0 4px;">Allowed Groups:</label>
<div id="groups-checkboxes" style="max-height:120px;overflow-y:auto;background:rgba(0,0,0,0.2);border-radius:6px;padding:6px 8px;"></div>
</div>
<button onclick="createUser()">Create User</button>
</div>
<div class="user-list">
@@ -1286,6 +1290,11 @@ inline std::string GetWebPageHTML() {
renderUsersList(msg.users);
}
break;
case 'groups':
if (msg.ok) {
renderGroupsCheckboxes(msg.groups);
}
break;
}
}
)HTML";
@@ -1661,7 +1670,35 @@ inline std::string GetWebPageHTML() {
function openUsersModal() {
document.getElementById('users-modal').classList.add('active');
document.getElementById('user-msg').innerHTML = '';
document.getElementById('new-role').value = 'viewer'; // Reset to default
onRoleChange(); // Update groups section visibility
listUsers();
getGroups();
}
function getGroups() {
if (ws && ws.readyState === WebSocket.OPEN && token) {
ws.send(JSON.stringify({ cmd: 'get_groups', token }));
}
}
function renderGroupsCheckboxes(groups) {
const container = document.getElementById('groups-checkboxes');
if (!groups || groups.length === 0) {
container.innerHTML = '<span style="color:#666;font-size:12px;">No groups available</span>';
return;
}
container.innerHTML = groups.map(g =>
'<label style="display:flex;align-items:center;padding:3px 0;cursor:pointer;white-space:nowrap;">' +
'<input type="checkbox" value="' + escapeHtml(g) + '" style="margin:0 6px 0 0;flex-shrink:0;width:14px;height:14px;">' +
escapeHtml(g) + '</label>'
).join('');
}
function onRoleChange() {
const role = document.getElementById('new-role').value;
const groupsSection = document.getElementById('groups-section');
groupsSection.style.display = (role === 'admin') ? 'none' : 'block';
}
function closeUsersModal() {
@@ -1685,8 +1722,12 @@ inline std::string GetWebPageHTML() {
return;
}
// Collect selected groups
const checkboxes = document.querySelectorAll('#groups-checkboxes input[type="checkbox"]:checked');
const allowed_groups = Array.from(checkboxes).map(cb => cb.value);
if (ws && ws.readyState === WebSocket.OPEN && token) {
ws.send(JSON.stringify({ cmd: 'create_user', token, username, password, role }));
ws.send(JSON.stringify({ cmd: 'create_user', token, username, password, role, allowed_groups }));
}
}
@@ -1712,10 +1753,14 @@ inline std::string GetWebPageHTML() {
container.innerHTML = users.map(u => {
const isAdmin = u.role === 'admin';
const canDelete = u.username !== 'admin'; // Cannot delete built-in admin
const groups = u.allowed_groups || [];
const groupsText = u.username === 'admin' ? '(all)' :
(groups.length > 0 ? groups.join(', ') : '(none)');
return '<div class="user-item">' +
'<div class="user-info">' +
'<div class="username">' + escapeHtml(u.username) + '</div>' +
'<div class="role ' + (isAdmin ? 'admin' : '') + '">' + u.role + '</div>' +
'<div class="groups" style="font-size:11px;color:#888;margin-top:2px;">Groups: ' + escapeHtml(groupsText) + '</div>' +
'</div>' +
(canDelete ? '<button class="delete-btn" onclick="deleteUser(\'' + escapeHtml(u.username) + '\')">Delete</button>' : '') +
'</div>';
@@ -1801,13 +1846,15 @@ inline std::string GetWebPageHTML() {
// Remote cursor mapping (Windows cursor index -> CSS cursor)
// Index matches CursorInfo.h: IDC_APPSTARTING(0) to IDC_WAIT(15), 254=custom, 255=unsupported
// Custom I-beam cursor with white fill and black stroke for visibility on any background
const ibeamCursor = "url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"24\" viewBox=\"0 0 16 24\"><path fill=\"none\" stroke=\"white\" stroke-width=\"3\" d=\"M4 3h8M8 3v18M4 21h8\"/><path fill=\"none\" stroke=\"black\" stroke-width=\"1\" d=\"M4 3h8M8 3v18M4 21h8\"/></svg>') 8 12, text";
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
ibeamCursor, // 5: IDC_IBEAM - custom cursor with outline
'default', // 6: IDC_ICON (no direct CSS equivalent)
'not-allowed', // 7: IDC_NO
'default', // 8: IDC_SIZE (deprecated, use default)

View File

@@ -11,6 +11,7 @@
#include <filesystem>
#include <fstream>
#include <shlobj.h>
#include <set>
// Algorithm constants (same as ScreenSpyDlg.cpp)
#define ALGORITHM_H264 2
@@ -363,6 +364,8 @@ void CWebService::ServerThread(int port) {
HandleDeleteUser(ws_ptr, msg);
} else if (cmd == "list_users") {
HandleListUsers(ws_ptr, token);
} else if (cmd == "get_groups") {
HandleGetGroups(ws_ptr, token);
}
}
});
@@ -566,7 +569,7 @@ void CWebService::HandleGetDevices(void* ws_ptr, const std::string& token) {
return;
}
SendText(ws_ptr, BuildDeviceListJson());
SendText(ws_ptr, BuildDeviceListJson(username));
}
void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id) {
@@ -588,6 +591,32 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
return;
}
// Check group permission (admin can access all devices)
if (username != "admin") {
std::string deviceGroup = ctx->GetGroupName();
if (deviceGroup.empty()) deviceGroup = "default";
bool hasAccess = false;
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (const auto& u : m_Users) {
if (u.username == username) {
for (const auto& g : u.allowed_groups) {
if (g == deviceGroup) {
hasAccess = true;
break;
}
}
break;
}
}
}
if (!hasAccess) {
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Permission denied"));
return;
}
}
// Check max clients per device
int current_count = GetWebClientCount(device_id);
if (current_count >= m_nMaxClientsPerDevice) {
@@ -954,12 +983,23 @@ void CWebService::HandleCreateUser(void* ws_ptr, const std::string& msg) {
std::string newPassword = root.get("password", "").asString();
std::string newRole = root.get("role", "viewer").asString();
// Parse allowed_groups array
std::vector<std::string> allowedGroups;
const Json::Value& groups = root["allowed_groups"];
if (groups.isArray()) {
for (const auto& g : groups) {
if (g.isString() && !g.asString().empty()) {
allowedGroups.push_back(g.asString());
}
}
}
if (newUsername.empty() || newPassword.empty()) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required"));
return;
}
if (CreateUser(newUsername, newPassword, newRole)) {
if (CreateUser(newUsername, newPassword, newRole, allowedGroups)) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", true));
} else {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)"));
@@ -1009,18 +1049,27 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
return;
}
auto users = ListUsers();
Json::Value res;
res["cmd"] = "list_users_result";
res["ok"] = true;
Json::Value usersArray(Json::arrayValue);
for (const auto& u : users) {
Json::Value user;
user["username"] = u.first;
user["role"] = u.second;
usersArray.append(user);
{
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (const auto& u : m_Users) {
Json::Value user;
user["username"] = u.username;
user["role"] = u.role;
// Include allowed_groups
Json::Value groups(Json::arrayValue);
for (const auto& g : u.allowed_groups) {
groups.append(g);
}
user["allowed_groups"] = groups;
usersArray.append(user);
}
}
res["users"] = usersArray;
@@ -1030,6 +1079,48 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
SendText(ws_ptr, json);
}
void CWebService::HandleGetGroups(void* ws_ptr, const std::string& token) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
SendText(ws_ptr, BuildJsonResponse("groups", false, "Invalid token"));
return;
}
// Only admin can get groups list (for user management)
if (role != "admin") {
SendText(ws_ptr, BuildJsonResponse("groups", false, "Permission denied"));
return;
}
// Collect all unique groups from online devices
std::set<std::string> groups;
groups.insert("default"); // Always include default group
if (m_pParentDlg) {
EnterCriticalSection(&m_pParentDlg->m_cs);
for (context* ctx : m_pParentDlg->m_HostList) {
if (!ctx || !ctx->IsLogin()) continue;
std::string g = ctx->GetGroupName();
groups.insert(g.empty() ? "default" : g);
}
LeaveCriticalSection(&m_pParentDlg->m_cs);
}
// Build response
Json::Value res;
res["cmd"] = "groups";
res["ok"] = true;
res["groups"] = Json::Value(Json::arrayValue);
for (const auto& g : groups) {
res["groups"].append(g);
}
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string json = Json::writeString(builder, res);
SendText(ws_ptr, json);
}
//////////////////////////////////////////////////////////////////////////
// Token Management (delegated to WebServiceAuth module)
//////////////////////////////////////////////////////////////////////////
@@ -1170,6 +1261,14 @@ void CWebService::LoadUsers() {
user.salt = u.get("salt", "").asString();
user.role = u.get("role", "viewer").asString();
// Load allowed_groups
const Json::Value& groups = u["allowed_groups"];
if (groups.isArray()) {
for (const auto& g : groups) {
user.allowed_groups.push_back(g.asString());
}
}
if (!user.password_hash.empty()) {
m_Users.push_back(user);
loaded++;
@@ -1197,6 +1296,14 @@ void CWebService::SaveUsers() {
user["password_hash"] = u.password_hash;
user["salt"] = u.salt;
user["role"] = u.role;
// Save allowed_groups
Json::Value groups(Json::arrayValue);
for (const auto& g : u.allowed_groups) {
groups.append(g);
}
user["allowed_groups"] = groups;
users.append(user);
}
@@ -1217,7 +1324,8 @@ void CWebService::SaveUsers() {
Mprintf("[WebService] Saved %d users to users.json\n", (int)users.size());
}
bool CWebService::CreateUser(const std::string& username, const std::string& password, const std::string& role) {
bool CWebService::CreateUser(const std::string& username, const std::string& password, const std::string& role,
const std::vector<std::string>& allowed_groups) {
if (username.empty() || password.empty()) return false;
if (username == "admin") return false; // Cannot create user named "admin"
if (role != "admin" && role != "viewer") return false;
@@ -1236,9 +1344,11 @@ bool CWebService::CreateUser(const std::string& username, const std::string& pas
user.salt = GenerateSalt();
user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
user.role = role;
user.allowed_groups = allowed_groups;
m_Users.push_back(user);
Mprintf("[WebService] Created user: %s (role: %s)\n", username.c_str(), role.c_str());
Mprintf("[WebService] Created user: %s (role: %s, groups: %d)\n",
username.c_str(), role.c_str(), (int)allowed_groups.size());
}
// Save to file (outside lock scope since SaveUsers acquires its own lock)
@@ -1295,17 +1405,47 @@ std::string CWebService::BuildJsonResponse(const std::string& cmd, bool ok, cons
return Json::writeString(builder, res);
}
std::string CWebService::BuildDeviceListJson() {
std::string CWebService::BuildDeviceListJson(const std::string& username) {
Json::Value res;
res["cmd"] = "device_list";
res["devices"] = Json::Value(Json::arrayValue);
// Get user's allowed groups for filtering (skip for admin or empty username)
std::vector<std::string> allowedGroups;
bool filterByGroup = false;
if (!username.empty() && username != "admin") {
std::lock_guard<std::mutex> lock(m_UsersMutex);
for (const auto& u : m_Users) {
if (u.username == username) {
allowedGroups = u.allowed_groups;
filterByGroup = true;
break;
}
}
}
if (m_pParentDlg) {
// Access device list with lock
EnterCriticalSection(&m_pParentDlg->m_cs);
for (context* ctx : m_pParentDlg->m_HostList) {
if (!ctx || !ctx->IsLogin()) continue;
// Get device group (empty = "default")
std::string deviceGroup = ctx->GetGroupName();
if (deviceGroup.empty()) deviceGroup = "default";
// Filter by allowed groups if user is not admin
if (filterByGroup) {
bool allowed = false;
for (const auto& g : allowedGroups) {
if (g == deviceGroup) {
allowed = true;
break;
}
}
if (!allowed) continue; // Skip device not in allowed groups
}
Json::Value device;
// Use string for ID to avoid JavaScript number precision loss
device["id"] = std::to_string(ctx->GetClientID());
@@ -1332,6 +1472,9 @@ std::string CWebService::BuildDeviceListJson() {
device["activeWindow"] = AnsiToUtf8(activeWindow);
device["online"] = true;
// Add device group to response
device["group"] = deviceGroup;
// Get screen info from client's reported resolution
// Format: "n:MxN" where n=monitor count, M=width, N=height
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
@@ -1509,9 +1652,13 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
context* ctx = m_pParentDlg->FindHost(device_id);
if (!ctx) return false;
// Close any existing remote desktop for this device first
// This prevents duplicate dialogs when user reconnects quickly
m_pParentDlg->CloseRemoteDesktopByClientID(device_id);
// Check if there's already a Web session for this device
// Only reuse if Web has already triggered AND a Web dialog exists
// This ensures MFC and Web have independent dialogs
if (IsWebTriggered(device_id) && HasActiveSession(device_id)) {
Mprintf("[WebService] Reusing existing Web session for device %llu\n", device_id);
return true; // Web session exists, new web user joins watching
}
// Mark as web-triggered (dialog should be hidden)
{
@@ -1520,7 +1667,8 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
}
// Send COMMAND_SCREEN_SPY with H264 algorithm
// Format: [COMMAND_SCREEN_SPY:1][DXGI:1][Algorithm:1][MultiScreen:1]
// If client is already capturing (MFC opened first), it will re-send TOKEN_BITMAPINFO
// This creates a new hidden Web dialog while MFC dialog remains visible
BYTE bToken[32] = { 0 };
bToken[0] = COMMAND_SCREEN_SPY;
bToken[1] = 0; // DXGI mode: 0=GDI
@@ -1544,10 +1692,11 @@ void CWebService::StopRemoteDesktop(uint64_t device_id) {
}
}
// If no more web clients watching, close the remote desktop
// If no more web clients watching, close only the Web session dialog
// MFC dialogs remain open
if (watchingCount == 0) {
ClearWebTriggered(device_id);
m_pParentDlg->CloseRemoteDesktopByClientID(device_id);
m_pParentDlg->CloseWebRemoteDesktopByClientID(device_id);
}
}
@@ -1563,10 +1712,13 @@ void CWebService::RegisterScreenContext(uint64_t device_id, CONTEXT_OBJECT* ctx)
}
void CWebService::UnregisterScreenContext(uint64_t device_id) {
if (!m_bRunning) return;
// Always clean up, even if WebService is stopping
// This prevents stale pointers in m_ScreenContexts
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
m_ScreenContexts.erase(device_id);
Mprintf("[WebService] Unregistered screen context for device %llu\n", device_id);
if (m_bRunning) {
Mprintf("[WebService] Unregistered screen context for device %llu\n", device_id);
}
}
CONTEXT_OBJECT* CWebService::GetScreenContext(uint64_t device_id) {
@@ -1666,6 +1818,26 @@ void CWebService::ClearWebTriggered(uint64_t device_id) {
m_WebTriggeredDevices.erase(device_id);
}
void CWebService::SetMfcTriggered(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_MfcTriggeredMutex);
m_MfcTriggeredDevices.insert(device_id);
}
bool CWebService::IsMfcTriggered(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_MfcTriggeredMutex);
return m_MfcTriggeredDevices.find(device_id) != m_MfcTriggeredDevices.end();
}
void CWebService::ClearMfcTriggered(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_MfcTriggeredMutex);
m_MfcTriggeredDevices.erase(device_id);
}
bool CWebService::HasActiveSession(uint64_t device_id) {
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
return m_ScreenContexts.find(device_id) != m_ScreenContexts.end();
}
void CWebService::NotifyDeviceUpdate(uint64_t device_id, const std::string& rtt, const std::string& activeWindow) {
if (!m_bRunning || m_bStopping) return;

View File

@@ -43,6 +43,7 @@ struct WebUser {
std::string password_hash; // SHA256(password + salt)
std::string salt;
std::string role; // "admin" | "viewer"
std::vector<std::string> allowed_groups; // Groups this user can view (empty = no access, admin = all)
};
// Device info for web clients
@@ -79,7 +80,8 @@ public:
void SetAdminPassword(const std::string& password);
// User management
bool CreateUser(const std::string& username, const std::string& password, const std::string& role);
bool CreateUser(const std::string& username, const std::string& password, const std::string& role,
const std::vector<std::string>& allowed_groups = {});
bool DeleteUser(const std::string& username);
std::vector<std::pair<std::string, std::string>> ListUsers(); // Returns [(username, role), ...]
@@ -144,7 +146,7 @@ private:
// JSON helpers
std::string BuildJsonResponse(const std::string& cmd, bool ok, const std::string& msg = "");
std::string BuildDeviceListJson();
std::string BuildDeviceListJson(const std::string& username = "");
// Password verification
bool VerifyPassword(const std::string& input, const WebUser& user);
@@ -157,6 +159,7 @@ private:
void HandleCreateUser(void* ws_ptr, const std::string& msg);
void HandleDeleteUser(void* ws_ptr, const std::string& msg);
void HandleListUsers(void* ws_ptr, const std::string& token);
void HandleGetGroups(void* ws_ptr, const std::string& token);
// Send to WebSocket
void SendText(void* ws_ptr, const std::string& text);
@@ -224,6 +227,14 @@ public:
bool IsWebTriggered(uint64_t device_id);
void ClearWebTriggered(uint64_t device_id);
// MFC trigger management - MFC dialogs should always be visible
void SetMfcTriggered(uint64_t device_id);
bool IsMfcTriggered(uint64_t device_id);
void ClearMfcTriggered(uint64_t device_id);
// Check if a remote desktop session already exists for device
bool HasActiveSession(uint64_t device_id);
// Config accessors
void SetHideWebSessions(bool hide) { m_bHideWebSessions = hide; }
bool GetHideWebSessions() const { return m_bHideWebSessions; }
@@ -240,6 +251,10 @@ private:
// Screen context registry: device_id -> ScreenManager's CONTEXT_OBJECT
std::map<uint64_t, CONTEXT_OBJECT*> m_ScreenContexts;
std::mutex m_ScreenContextsMutex;
// MFC triggered devices: dialogs created by MFC should always be visible
std::set<uint64_t> m_MfcTriggeredDevices;
std::mutex m_MfcTriggeredMutex;
};
// Global accessor

View File

@@ -1819,3 +1819,7 @@ IOCP
插件列表为空,无法创建触发器=Plugin list is empty, cannot create trigger
请先选择至少一个插件=Please select at least one plugin
没有本地历史记录=No local history
历史目录不存在: %s=History folder not exist: %s
无法识别远程主机=Unknown remote machine
没有远程历史记录=No remote history

View File

@@ -1810,3 +1810,7 @@ IOCP
<< 移除=<< 移除
插件列表为空,无法创建触发器=外掛列表為空,無法建立觸發器
请先选择至少一个插件=請先選擇至少一個外掛
没有本地历史记录=没有本地历史记录
历史目录不存在: %s=历史目录不存在: %s
无法识别远程主机=无法识别远程主机
没有远程历史记录=没有远程历史记录

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -506,6 +506,8 @@
#define IDT_REMOTE_DOWNLOADS 2235
#define IDT_REMOTE_HOME 2236
#define IDT_REMOTE_SEARCH 2237
#define IDT_LOCAL_HISTORY 2238
#define IDT_REMOTE_HISTORY 2239
#define IDC_BUTTON_SAVE_LICENSE 2240
#define IDC_LICENSE_LIST 2241
#define IDC_COMBO_VERSION 2245
@@ -966,6 +968,11 @@
#define IDC_STATIC_TRIGGER_TYPE 2544
#define IDC_STATIC_TRIGGER_ACTION 2545
// 内嵌语言资源 (RCDATA)
// 注意:避免与 IDB_BITMAP_TRIGGER(372) 和 IDB_BITMAP_WEBDESKTOP(373) 冲突
#define IDR_LANG_EN_US 380
#define IDR_LANG_ZH_TW 381
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED