Compare commits
16 Commits
v1.3.2
...
92f3df8464
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92f3df8464 | ||
|
|
b732f841d0 | ||
|
|
1df2a7b321 | ||
|
|
3d8e90da14 | ||
|
|
12e2a33062 | ||
|
|
a8b0932080 | ||
|
|
ca37fa419a | ||
|
|
36423b1c7c | ||
|
|
a3611d9fc1 | ||
|
|
9ae5529458 | ||
|
|
171fa750e5 | ||
|
|
8ed9ba8426 | ||
|
|
fd3838a151 | ||
|
|
56419f8ecb | ||
|
|
bb6fd7b1b9 | ||
|
|
3607f1d768 |
16
ReadMe.md
16
ReadMe.md
@@ -494,27 +494,31 @@ make
|
|||||||
|
|
||||||
**系统要求**:
|
**系统要求**:
|
||||||
- macOS 10.15 (Catalina) 及以上
|
- macOS 10.15 (Catalina) 及以上
|
||||||
|
- 架构支持:Intel (x64) 和 Apple Silicon (arm64) 通用二进制
|
||||||
- 需要授予系统权限:屏幕录制、辅助功能、完全磁盘访问
|
- 需要授予系统权限:屏幕录制、辅助功能、完全磁盘访问
|
||||||
|
|
||||||
**功能支持**:
|
**功能支持**:
|
||||||
|
|
||||||
| 功能 | 状态 | 实现 |
|
| 功能 | 状态 | 实现 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 远程桌面 | ✅ | CoreGraphics 屏幕捕获,H.264 硬件编码 |
|
| 远程桌面 | ✅ | CoreGraphics 屏幕捕获,VideoToolbox H.264 硬件编码 |
|
||||||
| 鼠标控制 | ✅ | CGEvent 模拟,支持双击、拖拽 |
|
| 鼠标控制 | ✅ | CGEvent 模拟,支持双击、拖拽 |
|
||||||
| 键盘控制 | ✅ | CGEvent 模拟,完整键码映射 |
|
| 键盘控制 | ✅ | CGEvent 模拟,完整键码映射 |
|
||||||
| 光标同步 | ✅ | 实时同步远程光标样式 |
|
| 光标同步 | ✅ | 实时同步远程光标样式 |
|
||||||
|
| 远程终端 | ✅ | PTY 交互式 Shell(zsh/bash) |
|
||||||
|
| 文件管理 | ✅ | 双向传输、V2 协议、大文件支持 |
|
||||||
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
||||||
| 文件管理 | ⏳ | 开发中 |
|
| 分组管理 | ✅ | 持久化配置文件 |
|
||||||
| 远程终端 | ⏳ | 开发中 |
|
| 进程管理 | ⏳ | 开发中 |
|
||||||
|
| 剪贴板 | ⏳ | 开发中 |
|
||||||
|
|
||||||
**编译方式**:
|
**编译方式**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd macos
|
cd macos
|
||||||
mkdir build && cd build
|
./build.sh
|
||||||
cmake ..
|
# 或手动编译:
|
||||||
make
|
# mkdir build && cd build && cmake .. && make
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
16
ReadMe_EN.md
16
ReadMe_EN.md
@@ -479,27 +479,31 @@ make
|
|||||||
|
|
||||||
**System Requirements**:
|
**System Requirements**:
|
||||||
- macOS 10.15 (Catalina) or later
|
- macOS 10.15 (Catalina) or later
|
||||||
|
- Architecture: Universal Binary (Intel x64 + Apple Silicon arm64)
|
||||||
- Required permissions: Screen Recording, Accessibility, Full Disk Access
|
- Required permissions: Screen Recording, Accessibility, Full Disk Access
|
||||||
|
|
||||||
**Feature Support**:
|
**Feature Support**:
|
||||||
|
|
||||||
| Feature | Status | Implementation |
|
| 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 |
|
| Mouse Control | ✅ | CGEvent simulation, supports double-click, drag |
|
||||||
| Keyboard Control | ✅ | CGEvent simulation, full keycode mapping |
|
| Keyboard Control | ✅ | CGEvent simulation, full keycode mapping |
|
||||||
| Cursor Sync | ✅ | Real-time remote cursor style synchronization |
|
| 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 |
|
| Heartbeat/RTT | ✅ | RFC 6298 RTT estimation |
|
||||||
| File Management | ⏳ | In development |
|
| Group Management | ✅ | Persistent configuration file |
|
||||||
| Remote Terminal | ⏳ | In development |
|
| Process Management | ⏳ | In development |
|
||||||
|
| Clipboard | ⏳ | In development |
|
||||||
|
|
||||||
**Build Instructions**:
|
**Build Instructions**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd macos
|
cd macos
|
||||||
mkdir build && cd build
|
./build.sh
|
||||||
cmake ..
|
# Or manually:
|
||||||
make
|
# mkdir build && cd build && cmake .. && make
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
16
ReadMe_TW.md
16
ReadMe_TW.md
@@ -478,27 +478,31 @@ make
|
|||||||
|
|
||||||
**系統要求**:
|
**系統要求**:
|
||||||
- macOS 10.15 (Catalina) 及以上
|
- macOS 10.15 (Catalina) 及以上
|
||||||
|
- 架構支援:Intel (x64) 和 Apple Silicon (arm64) 通用二進位
|
||||||
- 需要授予系統權限:螢幕錄製、輔助使用、完全磁碟存取
|
- 需要授予系統權限:螢幕錄製、輔助使用、完全磁碟存取
|
||||||
|
|
||||||
**功能支援**:
|
**功能支援**:
|
||||||
|
|
||||||
| 功能 | 狀態 | 實作 |
|
| 功能 | 狀態 | 實作 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 遠端桌面 | ✅ | CoreGraphics 螢幕擷取,H.264 硬體編碼 |
|
| 遠端桌面 | ✅ | CoreGraphics 螢幕擷取,VideoToolbox H.264 硬體編碼 |
|
||||||
| 滑鼠控制 | ✅ | CGEvent 模擬,支援雙擊、拖曳 |
|
| 滑鼠控制 | ✅ | CGEvent 模擬,支援雙擊、拖曳 |
|
||||||
| 鍵盤控制 | ✅ | CGEvent 模擬,完整鍵碼對應 |
|
| 鍵盤控制 | ✅ | CGEvent 模擬,完整鍵碼對應 |
|
||||||
| 游標同步 | ✅ | 即時同步遠端游標樣式 |
|
| 游標同步 | ✅ | 即時同步遠端游標樣式 |
|
||||||
|
| 遠端終端 | ✅ | PTY 互動式 Shell(zsh/bash) |
|
||||||
|
| 檔案管理 | ✅ | 雙向傳輸、V2 協定、大檔案支援 |
|
||||||
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
||||||
| 檔案管理 | ⏳ | 開發中 |
|
| 分組管理 | ✅ | 持久化設定檔 |
|
||||||
| 遠端終端 | ⏳ | 開發中 |
|
| 程序管理 | ⏳ | 開發中 |
|
||||||
|
| 剪貼簿 | ⏳ | 開發中 |
|
||||||
|
|
||||||
**編譯方式**:
|
**編譯方式**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd macos
|
cd macos
|
||||||
mkdir build && cd build
|
./build.sh
|
||||||
cmake ..
|
# 或手動編譯:
|
||||||
make
|
# mkdir build && cd build && cmake .. && make
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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
|
#pragma once
|
||||||
|
|
||||||
|
#if defined(_WIN32) || defined(_WIN64)
|
||||||
|
#error "FileManager.h is not supported on Windows."
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/statvfs.h>
|
#include <sys/statvfs.h>
|
||||||
#include <iconv.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -11,15 +27,19 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <cerrno>
|
#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"
|
#include "FileTransferV2.h"
|
||||||
|
|
||||||
// 外部声明 clientID(在 main.cpp 中定义)
|
// External declaration of clientID (defined in main.cpp/main.mm)
|
||||||
extern uint64_t g_myClientID;
|
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
|
#define MAX_SEND_BUFFER 65535
|
||||||
|
|
||||||
class FileManager : public IOCPManager
|
class FileManager : public IOCPManager
|
||||||
@@ -222,6 +242,13 @@ private:
|
|||||||
// ---- Get root filesystem type ----
|
// ---- Get root filesystem type ----
|
||||||
static std::string getRootFsType()
|
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::ifstream f("/proc/mounts");
|
||||||
std::string line;
|
std::string line;
|
||||||
while (std::getline(f, line)) {
|
while (std::getline(f, line)) {
|
||||||
@@ -232,6 +259,7 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "ext4";
|
return "ext4";
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Ensure parent directory exists (mkdir -p for parent of file path) ----
|
// ---- 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 + 2, &totalMB, sizeof(unsigned long));
|
||||||
memcpy(buf + offset + 6, &freeMB, sizeof(unsigned long));
|
memcpy(buf + offset + 6, &freeMB, sizeof(unsigned long));
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
const char* typeName = "macOS";
|
||||||
|
#else
|
||||||
const char* typeName = "Linux";
|
const char* typeName = "Linux";
|
||||||
|
#endif
|
||||||
int typeNameLen = strlen(typeName) + 1;
|
int typeNameLen = strlen(typeName) + 1;
|
||||||
memcpy(buf + offset + 10, typeName, typeNameLen);
|
memcpy(buf + offset + 10, typeName, typeNameLen);
|
||||||
|
|
||||||
@@ -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
|
#pragma once
|
||||||
#include "common/commands.h"
|
|
||||||
#include "common/file_upload.h"
|
#if defined(_WIN32) || defined(_WIN64)
|
||||||
#include "client/IOCPClient.h"
|
#error "FileTransferV2.h is not supported on Windows."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "commands.h"
|
||||||
|
#include "file_upload.h"
|
||||||
|
#include "../client/IOCPClient.h"
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
@@ -15,10 +32,6 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <thread>
|
#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
|
class FileTransferV2
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
272
common/PTYHandler.h
Normal file
272
common/PTYHandler.h
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1048,6 +1048,14 @@ enum QualityLevel {
|
|||||||
QUALITY_COUNT = 6,
|
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 对应)
|
/* 质量配置(与 QualityLevel 对应)
|
||||||
- strategy = 0:1080p 限制
|
- strategy = 0:1080p 限制
|
||||||
- strategy = 1:原始分辨率
|
- strategy = 1:原始分辨率
|
||||||
|
|||||||
@@ -321,6 +321,16 @@ inline const char* getFileName(const char* path)
|
|||||||
#endif
|
#endif
|
||||||
#elif defined(_WIN32)
|
#elif defined(_WIN32)
|
||||||
#define Mprintf(format, ...) Logger::getInstance().log(getFileName((__FILE__)), __LINE__, (format), __VA_ARGS__)
|
#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
|
#else
|
||||||
// Linux: 覆盖 commands.h 中的 printf 回退定义,改用 Logger 写文件
|
// Linux: 覆盖 commands.h 中的 printf 回退定义,改用 Logger 写文件
|
||||||
#ifdef Mprintf
|
#ifdef Mprintf
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
#include "client/IOCPClient.h"
|
#include "client/IOCPClient.h"
|
||||||
#include "LinuxConfig.h"
|
#include "LinuxConfig.h"
|
||||||
#include "ClipboardHandler.h"
|
#include "ClipboardHandler.h"
|
||||||
#include "FileTransferV2.h"
|
#include "common/FileTransferV2.h"
|
||||||
|
#include "X264Encoder.h"
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@@ -11,7 +12,9 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <cstdlib>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
// 客户端 ID(定义在 main.cpp)
|
// 客户端 ID(定义在 main.cpp)
|
||||||
extern uint64_t g_myClientID;
|
extern uint64_t g_myClientID;
|
||||||
@@ -110,27 +113,39 @@ struct XGCValues_LNX {
|
|||||||
#define IncludeInferiors 1
|
#define IncludeInferiors 1
|
||||||
|
|
||||||
// ============== 屏幕算法常量 ==============
|
// ============== 屏幕算法常量 ==============
|
||||||
#define ALGORITHM_GRAY 0
|
// 常量定义已移至 commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565
|
||||||
#define ALGORITHM_DIFF 1
|
|
||||||
#define ALGORITHM_H264 2
|
|
||||||
#define ALGORITHM_RGB565 3
|
|
||||||
|
|
||||||
// 算法支持表(编译时常量,日后支持 H264 时改为 true)
|
// 检查算法是否支持(H264 需要运行时检测)
|
||||||
static const bool g_SupportedAlgo[] = {
|
inline bool IsAlgorithmSupported(uint8_t algo) {
|
||||||
true, // ALGORITHM_GRAY = 0
|
switch (algo) {
|
||||||
true, // ALGORITHM_DIFF = 1
|
case ALGORITHM_GRAY:
|
||||||
false, // ALGORITHM_H264 = 2
|
case ALGORITHM_DIFF:
|
||||||
true, // ALGORITHM_RGB565 = 3
|
case ALGORITHM_RGB565:
|
||||||
};
|
return true;
|
||||||
|
case ALGORITHM_H264:
|
||||||
|
return X264Encoder::IsAvailable();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 不支持的算法降级为 RGB565
|
// 不支持的算法降级为 RGB565
|
||||||
inline uint8_t GetEffectiveAlgorithm(uint8_t algo) {
|
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 ALGORITHM_RGB565;
|
||||||
}
|
}
|
||||||
return algo;
|
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)
|
// 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 函数指针类型
|
// X11 函数指针类型
|
||||||
typedef Display* (*fn_XOpenDisplay)(const char*);
|
typedef Display* (*fn_XOpenDisplay)(const char*);
|
||||||
typedef int (*fn_XCloseDisplay)(Display*);
|
typedef int (*fn_XCloseDisplay)(Display*);
|
||||||
@@ -391,12 +416,18 @@ typedef int (*fn_XSync)(Display*, int);
|
|||||||
typedef unsigned long (*fn_XKeysymToKeycode)(Display*, unsigned long);
|
typedef unsigned long (*fn_XKeysymToKeycode)(Display*, unsigned long);
|
||||||
typedef int (*fn_XFlush)(Display*);
|
typedef int (*fn_XFlush)(Display*);
|
||||||
typedef int (*fn_XClearArea)(Display*, Window, int, int, unsigned int, unsigned int, int);
|
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 扩展函数指针类型(用于模拟鼠标/键盘输入)
|
// XTest 扩展函数指针类型(用于模拟鼠标/键盘输入)
|
||||||
typedef int (*fn_XTestFakeMotionEvent)(Display*, int, int, int, unsigned long);
|
typedef int (*fn_XTestFakeMotionEvent)(Display*, int, int, int, unsigned long);
|
||||||
typedef int (*fn_XTestFakeButtonEvent)(Display*, unsigned int, int, unsigned long);
|
typedef int (*fn_XTestFakeButtonEvent)(Display*, unsigned int, int, unsigned long);
|
||||||
typedef int (*fn_XTestFakeKeyEvent)(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 动态加载包装
|
// X11 动态加载包装
|
||||||
class X11Loader
|
class X11Loader
|
||||||
{
|
{
|
||||||
@@ -430,13 +461,19 @@ public:
|
|||||||
fn_XKeysymToKeycode pXKeysymToKeycode;
|
fn_XKeysymToKeycode pXKeysymToKeycode;
|
||||||
fn_XFlush pXFlush;
|
fn_XFlush pXFlush;
|
||||||
fn_XClearArea pXClearArea;
|
fn_XClearArea pXClearArea;
|
||||||
|
fn_XQueryPointer pXQueryPointer;
|
||||||
|
fn_XFree pXFree;
|
||||||
|
|
||||||
// XTest 扩展(用于模拟输入)
|
// XTest 扩展(用于模拟输入)
|
||||||
fn_XTestFakeMotionEvent pXTestFakeMotionEvent;
|
fn_XTestFakeMotionEvent pXTestFakeMotionEvent;
|
||||||
fn_XTestFakeButtonEvent pXTestFakeButtonEvent;
|
fn_XTestFakeButtonEvent pXTestFakeButtonEvent;
|
||||||
fn_XTestFakeKeyEvent pXTestFakeKeyEvent;
|
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;
|
pXOpenDisplay = nullptr;
|
||||||
pXCloseDisplay = nullptr;
|
pXCloseDisplay = nullptr;
|
||||||
@@ -457,9 +494,13 @@ public:
|
|||||||
pXKeysymToKeycode = nullptr;
|
pXKeysymToKeycode = nullptr;
|
||||||
pXFlush = nullptr;
|
pXFlush = nullptr;
|
||||||
pXClearArea = nullptr;
|
pXClearArea = nullptr;
|
||||||
|
pXQueryPointer = nullptr;
|
||||||
|
pXFree = nullptr;
|
||||||
pXTestFakeMotionEvent = nullptr;
|
pXTestFakeMotionEvent = nullptr;
|
||||||
pXTestFakeButtonEvent = nullptr;
|
pXTestFakeButtonEvent = nullptr;
|
||||||
pXTestFakeKeyEvent = nullptr;
|
pXTestFakeKeyEvent = nullptr;
|
||||||
|
pXFixesQueryExtension = nullptr;
|
||||||
|
pXFixesGetCursorImage = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Load()
|
bool Load()
|
||||||
@@ -489,6 +530,8 @@ public:
|
|||||||
pXKeysymToKeycode = (fn_XKeysymToKeycode)dlsym(m_handle, "XKeysymToKeycode");
|
pXKeysymToKeycode = (fn_XKeysymToKeycode)dlsym(m_handle, "XKeysymToKeycode");
|
||||||
pXFlush = (fn_XFlush)dlsym(m_handle, "XFlush");
|
pXFlush = (fn_XFlush)dlsym(m_handle, "XFlush");
|
||||||
pXClearArea = (fn_XClearArea)dlsym(m_handle, "XClearArea");
|
pXClearArea = (fn_XClearArea)dlsym(m_handle, "XClearArea");
|
||||||
|
pXQueryPointer = (fn_XQueryPointer)dlsym(m_handle, "XQueryPointer");
|
||||||
|
pXFree = (fn_XFree)dlsym(m_handle, "XFree");
|
||||||
|
|
||||||
// 加载 XTest 扩展库(用于模拟鼠标/键盘输入)
|
// 加载 XTest 扩展库(用于模拟鼠标/键盘输入)
|
||||||
m_xtst_handle = dlopen("libXtst.so.6", RTLD_LAZY);
|
m_xtst_handle = dlopen("libXtst.so.6", RTLD_LAZY);
|
||||||
@@ -499,7 +542,15 @@ public:
|
|||||||
pXTestFakeKeyEvent = (fn_XTestFakeKeyEvent)dlsym(m_xtst_handle, "XTestFakeKeyEvent");
|
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 &&
|
return pXOpenDisplay && pXCloseDisplay && pXGetImage && pXDestroyImage &&
|
||||||
pXDefaultScreen && pXDisplayWidth && pXDisplayHeight && pXRootWindow &&
|
pXDefaultScreen && pXDisplayWidth && pXDisplayHeight && pXRootWindow &&
|
||||||
pXSetErrorHandler && pXCreatePixmap && pXFreePixmap &&
|
pXSetErrorHandler && pXCreatePixmap && pXFreePixmap &&
|
||||||
@@ -513,8 +564,18 @@ public:
|
|||||||
return pXTestFakeMotionEvent && pXTestFakeButtonEvent && pXTestFakeKeyEvent;
|
return pXTestFakeMotionEvent && pXTestFakeButtonEvent && pXTestFakeKeyEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查 XFixes 扩展是否可用
|
||||||
|
bool HasXFixes() const
|
||||||
|
{
|
||||||
|
return pXFixesGetCursorImage != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
~X11Loader()
|
~X11Loader()
|
||||||
{
|
{
|
||||||
|
if (m_xfixes_handle) {
|
||||||
|
dlclose(m_xfixes_handle);
|
||||||
|
m_xfixes_handle = nullptr;
|
||||||
|
}
|
||||||
if (m_xtst_handle) {
|
if (m_xtst_handle) {
|
||||||
dlclose(m_xtst_handle);
|
dlclose(m_xtst_handle);
|
||||||
m_xtst_handle = nullptr;
|
m_xtst_handle = nullptr;
|
||||||
@@ -528,6 +589,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void* m_handle;
|
void* m_handle;
|
||||||
void* m_xtst_handle;
|
void* m_xtst_handle;
|
||||||
|
void* m_xfixes_handle;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ScreenHandler : public IOCPManager
|
class ScreenHandler : public IOCPManager
|
||||||
@@ -538,7 +600,8 @@ public:
|
|||||||
m_inputDisplay(nullptr),
|
m_inputDisplay(nullptr),
|
||||||
m_width(0), m_height(0),
|
m_width(0), m_height(0),
|
||||||
m_pixmap(0), m_gc(nullptr), m_xtestWarned(false),
|
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) {
|
if (!client) {
|
||||||
throw std::invalid_argument("IOCPClient pointer cannot be null");
|
throw std::invalid_argument("IOCPClient pointer cannot be null");
|
||||||
@@ -651,11 +714,21 @@ public:
|
|||||||
// Double-check after acquiring lock
|
// Double-check after acquiring lock
|
||||||
if (m_destroyed) return;
|
if (m_destroyed) return;
|
||||||
|
|
||||||
// Prevent starting if thread is already running or joinable
|
// If already running, just send TOKEN_BITMAPINFO again
|
||||||
if (m_captureThread.joinable()) return;
|
// 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;
|
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);
|
m_captureThread = std::thread(&ScreenHandler::CaptureLoop, this);
|
||||||
}
|
}
|
||||||
@@ -873,12 +946,28 @@ public:
|
|||||||
// 应用帧率
|
// 应用帧率
|
||||||
m_maxFPS.store(profile.maxFPS);
|
m_maxFPS.store(profile.maxFPS);
|
||||||
|
|
||||||
|
// 应用码率(H264 使用)
|
||||||
|
int oldBitrate = m_h264Bitrate;
|
||||||
|
m_h264Bitrate = profile.bitRate;
|
||||||
|
|
||||||
// 应用算法(带降级处理)
|
// 应用算法(带降级处理)
|
||||||
uint8_t algo = GetEffectiveAlgorithm(profile.algorithm);
|
uint8_t algo = GetEffectiveAlgorithm(profile.algorithm);
|
||||||
|
uint8_t oldAlgo = m_bAlgorithm.load();
|
||||||
m_bAlgorithm.store(algo);
|
m_bAlgorithm.store(algo);
|
||||||
|
|
||||||
Mprintf(">>> Quality: Level=%d, FPS=%d, Algo=%d->%d\n",
|
// 如果 H264 参数变化,需要重新初始化编码器
|
||||||
level, profile.maxFPS, profile.algorithm, algo);
|
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 {
|
} else {
|
||||||
// 自适应模式 (level=-1):由服务端动态调整,不做处理
|
// 自适应模式 (level=-1):由服务端动态调整,不做处理
|
||||||
Mprintf(">>> Quality: Adaptive mode\n");
|
Mprintf(">>> Quality: Adaptive mode\n");
|
||||||
@@ -1044,11 +1133,15 @@ private:
|
|||||||
std::vector<uint8_t> m_diffBuffer;
|
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; // 最大帧率
|
std::atomic<int> m_maxFPS; // 最大帧率
|
||||||
int8_t m_qualityLevel; // 当前质量等级 (-1=自适应, 0-5=具体等级)
|
int8_t m_qualityLevel; // 当前质量等级 (-1=自适应, 0-5=具体等级)
|
||||||
LinuxConfig m_config; // 配置持久化 (~/.config/ghost/config.conf)
|
LinuxConfig m_config; // 配置持久化 (~/.config/ghost/config.conf)
|
||||||
|
|
||||||
|
// H264 编码器
|
||||||
|
std::unique_ptr<X264Encoder> m_h264Encoder;
|
||||||
|
int m_h264Bitrate; // 码率 (kbps)
|
||||||
|
|
||||||
// X11 截屏,输出 BGRA 格式(自底向上,与 BMP 一致)
|
// X11 截屏,输出 BGRA 格式(自底向上,与 BMP 一致)
|
||||||
// 使用 XCopyArea 将 root window 拷贝到离屏 Pixmap,再对 Pixmap 调用 XGetImage
|
// 使用 XCopyArea 将 root window 拷贝到离屏 Pixmap,再对 Pixmap 调用 XGetImage
|
||||||
// 这样可以避免合成窗口管理器(Mutter 等)导致的 BadMatch 错误
|
// 这样可以避免合成窗口管理器(Mutter 等)导致的 BadMatch 错误
|
||||||
@@ -1120,13 +1213,14 @@ private:
|
|||||||
uint8_t algo = m_bAlgorithm.load();
|
uint8_t algo = m_bAlgorithm.load();
|
||||||
memcpy(data, &algo, sizeof(uint8_t));
|
memcpy(data, &algo, sizeof(uint8_t));
|
||||||
|
|
||||||
// 写入光标位置 (Linux 端简单置 0)
|
// 写入光标位置
|
||||||
int32_t cursorX = 0, cursorY = 0;
|
int32_t cursorX = 0, cursorY = 0;
|
||||||
|
GetCursorPosition(cursorX, cursorY);
|
||||||
memcpy(data + 1, &cursorX, sizeof(int32_t));
|
memcpy(data + 1, &cursorX, sizeof(int32_t));
|
||||||
memcpy(data + 1 + sizeof(int32_t), &cursorY, sizeof(int32_t));
|
memcpy(data + 1 + sizeof(int32_t), &cursorY, sizeof(int32_t));
|
||||||
|
|
||||||
// 写入光标类型
|
// 写入光标类型 (使用 XFixes 检测)
|
||||||
uint8_t cursorType = 0;
|
uint8_t cursorType = GetCursorTypeIndex();
|
||||||
memcpy(data + 1 + 2 * sizeof(int32_t), &cursorType, sizeof(uint8_t));
|
memcpy(data + 1 + 2 * sizeof(int32_t), &cursorType, sizeof(uint8_t));
|
||||||
|
|
||||||
uint32_t headerSize = 1 + 2 * sizeof(int32_t) + 1; // algo + cursor + cursorType
|
uint32_t headerSize = 1 + 2 * sizeof(int32_t) + 1; // algo + cursor + cursorType
|
||||||
@@ -1141,6 +1235,60 @@ private:
|
|||||||
std::swap(m_prevFrame, m_currFrame);
|
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)
|
// 差异比较算法(支持 DIFF/RGB565/GRAY)
|
||||||
// 输出格式: [byteOffset(4) + length(4) + pixel data] ...
|
// 输出格式: [byteOffset(4) + length(4) + pixel data] ...
|
||||||
// DIFF: length = 字节数, data = BGRA 原始数据
|
// DIFF: length = 字节数, data = BGRA 原始数据
|
||||||
@@ -1224,6 +1372,118 @@ private:
|
|||||||
return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
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()
|
void CaptureLoop()
|
||||||
{
|
{
|
||||||
@@ -1233,10 +1493,34 @@ private:
|
|||||||
// 发送第一帧
|
// 发送第一帧
|
||||||
SendFirstScreen();
|
SendFirstScreen();
|
||||||
|
|
||||||
|
uint8_t currentAlgo = m_bAlgorithm.load();
|
||||||
|
|
||||||
while (m_running) {
|
while (m_running) {
|
||||||
uint64_t start = GetTickMs();
|
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)
|
// 动态计算帧间隔(根据当前 maxFPS)
|
||||||
int fps = m_maxFPS.load();
|
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");
|
Mprintf(">>> ScreenHandler CaptureLoop stopped\n");
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
Mprintf("*** CaptureLoop exception: %s ***\n", e.what());
|
Mprintf("*** CaptureLoop exception: %s ***\n", e.what());
|
||||||
|
|||||||
471
linux/X264Encoder.h
Normal file
471
linux/X264Encoder.h
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
235
linux/main.cpp
235
linux/main.cpp
@@ -14,7 +14,7 @@
|
|||||||
#include <csignal>
|
#include <csignal>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
#include <pty.h>
|
#include "common/PTYHandler.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -26,9 +26,9 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include "ScreenHandler.h"
|
#include "ScreenHandler.h"
|
||||||
#include "SystemManager.h"
|
#include "SystemManager.h"
|
||||||
#include "FileManager.h"
|
#include "common/FileManager.h"
|
||||||
#include "ClipboardHandler.h"
|
#include "ClipboardHandler.h"
|
||||||
#include "FileTransferV2.h"
|
#include "common/FileTransferV2.h"
|
||||||
#include "common/logger.h"
|
#include "common/logger.h"
|
||||||
#define XXH_INLINE_ALL
|
#define XXH_INLINE_ALL
|
||||||
#include "common/xxhash.h"
|
#include "common/xxhash.h"
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength);
|
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;
|
State g_bExit = S_CLIENT_NORMAL;
|
||||||
@@ -338,187 +338,7 @@ struct RttEstimator {
|
|||||||
RttEstimator g_rttEstimator;
|
RttEstimator g_rttEstimator;
|
||||||
int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
|
int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
|
||||||
|
|
||||||
// 伪终端处理类:继承自IOCPManager.
|
// PTYHandler moved to common/PTYHandler.h (shared between Linux and macOS)
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void* ShellworkingThread(void* param)
|
void* ShellworkingThread(void* param)
|
||||||
{
|
{
|
||||||
@@ -534,6 +354,8 @@ void* ShellworkingThread(void* param)
|
|||||||
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", clientAddr);
|
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", clientAddr);
|
||||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||||
Sleep(1000);
|
Sleep(1000);
|
||||||
|
// 清除回调,防止重连线程访问已销毁的 handler
|
||||||
|
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||||
}
|
}
|
||||||
Mprintf(">>> Leave ShellworkingThread [%p]\n", clientAddr);
|
Mprintf(">>> Leave ShellworkingThread [%p]\n", clientAddr);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
@@ -556,6 +378,8 @@ void* ScreenworkingThread(void* param)
|
|||||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
|
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
|
||||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||||
Sleep(1000);
|
Sleep(1000);
|
||||||
|
// 清除回调,防止重连线程访问已销毁的 handler
|
||||||
|
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||||
}
|
}
|
||||||
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
|
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
@@ -576,6 +400,8 @@ void* SystemManagerThread(void* param)
|
|||||||
Mprintf(">>> SystemManagerThread [%p] Send: TOKEN_PSLIST\n", clientAddr);
|
Mprintf(">>> SystemManagerThread [%p] Send: TOKEN_PSLIST\n", clientAddr);
|
||||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||||
Sleep(1000);
|
Sleep(1000);
|
||||||
|
// 清除回调,防止重连线程访问已销毁的 handler
|
||||||
|
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||||
}
|
}
|
||||||
Mprintf(">>> Leave SystemManagerThread [%p]\n", clientAddr);
|
Mprintf(">>> Leave SystemManagerThread [%p]\n", clientAddr);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
@@ -596,6 +422,8 @@ void* FileManagerThread(void* param)
|
|||||||
Mprintf(">>> FileManagerThread [%p] Send: TOKEN_DRIVE_LIST\n", clientAddr);
|
Mprintf(">>> FileManagerThread [%p] Send: TOKEN_DRIVE_LIST\n", clientAddr);
|
||||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||||
Sleep(1000);
|
Sleep(1000);
|
||||||
|
// 清除回调,防止重连线程访问已销毁的 handler
|
||||||
|
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||||
}
|
}
|
||||||
Mprintf(">>> Leave FileManagerThread [%p]\n", clientAddr);
|
Mprintf(">>> Leave FileManagerThread [%p]\n", clientAddr);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
@@ -672,6 +500,24 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
|||||||
if (result != 0) {
|
if (result != 0) {
|
||||||
Mprintf("** [%p] V2 File recv error: %d ***\n", user, result);
|
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);
|
||||||
|
Mprintf("** [%p] Group changed to: %s ***\n", user, groupName.c_str());
|
||||||
} else {
|
} else {
|
||||||
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
|
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
|
||||||
}
|
}
|
||||||
@@ -1077,8 +923,23 @@ int main(int argc, char* argv[])
|
|||||||
|
|
||||||
LOGIN_INFOR logInfo;
|
LOGIN_INFOR logInfo;
|
||||||
|
|
||||||
// 主机名
|
// 读取分组名称(从配置文件或 g_SETTINGS)
|
||||||
strncpy(logInfo.szPCName, hostname, sizeof(logInfo.szPCName) - 1);
|
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';
|
logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0';
|
||||||
|
|
||||||
// 操作系统版本(如 "Ubuntu 24.04 LTS")
|
// 操作系统版本(如 "Ubuntu 24.04 LTS")
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ find_library(CARBON_FRAMEWORK Carbon REQUIRED)
|
|||||||
find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox REQUIRED)
|
find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox REQUIRED)
|
||||||
find_library(COREMEDIA_FRAMEWORK CoreMedia REQUIRED)
|
find_library(COREMEDIA_FRAMEWORK CoreMedia REQUIRED)
|
||||||
find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED)
|
find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED)
|
||||||
|
find_library(ACCELERATE_FRAMEWORK Accelerate REQUIRED)
|
||||||
|
find_library(ICONV_LIBRARY iconv REQUIRED)
|
||||||
|
|
||||||
target_link_libraries(ghost PRIVATE
|
target_link_libraries(ghost PRIVATE
|
||||||
${COCOA_FRAMEWORK}
|
${COCOA_FRAMEWORK}
|
||||||
@@ -57,6 +59,8 @@ target_link_libraries(ghost PRIVATE
|
|||||||
${VIDEOTOOLBOX_FRAMEWORK}
|
${VIDEOTOOLBOX_FRAMEWORK}
|
||||||
${COREMEDIA_FRAMEWORK}
|
${COREMEDIA_FRAMEWORK}
|
||||||
${COREVIDEO_FRAMEWORK}
|
${COREVIDEO_FRAMEWORK}
|
||||||
|
${ACCELERATE_FRAMEWORK}
|
||||||
|
${ICONV_LIBRARY}
|
||||||
"${CMAKE_SOURCE_DIR}/lib/libzstd.a"
|
"${CMAKE_SOURCE_DIR}/lib/libzstd.a"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
186
macos/ClipboardHandler.h
Normal file
186
macos/ClipboardHandler.h
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -6,8 +6,27 @@
|
|||||||
bool Permissions::checkScreenCapture() {
|
bool Permissions::checkScreenCapture() {
|
||||||
// macOS 10.15+ requires screen recording permission
|
// macOS 10.15+ requires screen recording permission
|
||||||
if (@available(macOS 10.15, *)) {
|
if (@available(macOS 10.15, *)) {
|
||||||
// Use CGPreflightScreenCaptureAccess for reliable permission check
|
// CGPreflightScreenCaptureAccess() is unreliable - it can return false
|
||||||
// This API is available since macOS 10.15
|
// 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();
|
return CGPreflightScreenCaptureAccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,17 @@
|
|||||||
|
|
||||||
#import <CoreGraphics/CoreGraphics.h>
|
#import <CoreGraphics/CoreGraphics.h>
|
||||||
#import <dispatch/dispatch.h>
|
#import <dispatch/dispatch.h>
|
||||||
|
#import <IOKit/pwr_mgt/IOPMLib.h>
|
||||||
|
#import <IOSurface/IOSurface.h>
|
||||||
#import "../client/IOCPClient.h"
|
#import "../client/IOCPClient.h"
|
||||||
|
#import "../common/commands.h" // QualityLevel, QualityProfile, ALGORITHM_*
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <condition_variable>
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
class IOCPClient;
|
class IOCPClient;
|
||||||
@@ -32,11 +36,7 @@ struct BITMAPINFOHEADER_MAC {
|
|||||||
};
|
};
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|
||||||
// Screen algorithm constants
|
// Algorithm constants from commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565
|
||||||
#define ALGORITHM_GRAY 0
|
|
||||||
#define ALGORITHM_DIFF 1
|
|
||||||
#define ALGORITHM_H264 2
|
|
||||||
#define ALGORITHM_RGB565 3
|
|
||||||
|
|
||||||
class ScreenHandler : public IOCPManager {
|
class ScreenHandler : public IOCPManager {
|
||||||
public:
|
public:
|
||||||
@@ -120,6 +120,7 @@ private:
|
|||||||
std::vector<uint8_t> m_prevFrame;
|
std::vector<uint8_t> m_prevFrame;
|
||||||
std::vector<uint8_t> m_currFrame;
|
std::vector<uint8_t> m_currFrame;
|
||||||
std::vector<uint8_t> m_diffBuffer;
|
std::vector<uint8_t> m_diffBuffer;
|
||||||
|
std::vector<uint8_t> m_tempBuffer; // 临时缓冲区,避免每帧分配
|
||||||
|
|
||||||
// Quality settings
|
// Quality settings
|
||||||
std::atomic<uint8_t> m_algorithm;
|
std::atomic<uint8_t> m_algorithm;
|
||||||
@@ -132,4 +133,28 @@ private:
|
|||||||
|
|
||||||
// Input handler for mouse/keyboard control
|
// Input handler for mouse/keyboard control
|
||||||
std::unique_ptr<InputHandler> m_inputHandler;
|
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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
#import "ScreenHandler.h"
|
#import "ScreenHandler.h"
|
||||||
#import "H264Encoder.h"
|
#import "H264Encoder.h"
|
||||||
#import "InputHandler.h"
|
#import "InputHandler.h"
|
||||||
|
#import "ClipboardHandler.h"
|
||||||
#import "../client/IOCPClient.h"
|
#import "../client/IOCPClient.h"
|
||||||
#import "../common/commands.h"
|
#import "../common/commands.h"
|
||||||
|
#import "../common/FileTransferV2.h"
|
||||||
|
#import "../common/logger.h"
|
||||||
#import "Permissions.h"
|
#import "Permissions.h"
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
|
#import <chrono>
|
||||||
#import <CoreGraphics/CoreGraphics.h>
|
#import <CoreGraphics/CoreGraphics.h>
|
||||||
#import <ApplicationServices/ApplicationServices.h>
|
#import <ApplicationServices/ApplicationServices.h>
|
||||||
#import <mach/mach_time.h>
|
#import <mach/mach_time.h>
|
||||||
|
#import <Accelerate/Accelerate.h>
|
||||||
|
|
||||||
// Global client ID (calculated in main.mm)
|
// Global client ID (calculated in main.mm)
|
||||||
extern uint64_t g_myClientID;
|
extern uint64_t g_myClientID;
|
||||||
@@ -26,9 +31,18 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
|
|||||||
, m_maxFPS(15)
|
, m_maxFPS(15)
|
||||||
, m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility
|
, m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility
|
||||||
, m_h264Bitrate(3000000) // 3 Mbps (matches Windows QUALITY_GOOD)
|
, 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));
|
memset(&m_bmpHeader, 0, sizeof(m_bmpHeader));
|
||||||
|
|
||||||
|
// Cache color space (avoid per-frame creation)
|
||||||
|
m_colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||||
|
|
||||||
// Initialize input handler for mouse/keyboard control
|
// Initialize input handler for mouse/keyboard control
|
||||||
m_inputHandler = std::make_unique<InputHandler>();
|
m_inputHandler = std::make_unique<InputHandler>();
|
||||||
if (m_inputHandler->init()) {
|
if (m_inputHandler->init()) {
|
||||||
@@ -41,6 +55,13 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
|
|||||||
ScreenHandler::~ScreenHandler()
|
ScreenHandler::~ScreenHandler()
|
||||||
{
|
{
|
||||||
stop();
|
stop();
|
||||||
|
cleanupDisplayStream();
|
||||||
|
|
||||||
|
// Release cached color space
|
||||||
|
if (m_colorSpace) {
|
||||||
|
CGColorSpaceRelease(m_colorSpace);
|
||||||
|
m_colorSpace = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ScreenHandler::init()
|
bool ScreenHandler::init()
|
||||||
@@ -103,24 +124,273 @@ bool ScreenHandler::init()
|
|||||||
m_currFrame.resize(m_bmpHeader.biSizeImage, 0);
|
m_currFrame.resize(m_bmpHeader.biSizeImage, 0);
|
||||||
m_diffBuffer.resize(1 + 1 + 8 + 1 + m_bmpHeader.biSizeImage * 2);
|
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);
|
NSLog(@"ScreenHandler initialized: %dx%d", m_width, m_height);
|
||||||
return true;
|
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)
|
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_client = client;
|
||||||
m_clientID = clientID;
|
m_clientID = clientID;
|
||||||
m_running = true;
|
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);
|
m_captureThread = std::thread(&ScreenHandler::captureLoop, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScreenHandler::stop()
|
void ScreenHandler::stop()
|
||||||
{
|
{
|
||||||
m_running = false;
|
m_running = false;
|
||||||
|
|
||||||
|
// Wake up capture thread if waiting
|
||||||
|
m_surfaceCond.notify_all();
|
||||||
|
|
||||||
if (m_captureThread.joinable()) {
|
if (m_captureThread.joinable()) {
|
||||||
m_captureThread.join();
|
m_captureThread.join();
|
||||||
}
|
}
|
||||||
@@ -130,6 +400,13 @@ void ScreenHandler::stop()
|
|||||||
m_h264Encoder->close();
|
m_h264Encoder->close();
|
||||||
m_h264Encoder.reset();
|
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()
|
void ScreenHandler::sendBitmapInfo()
|
||||||
@@ -207,6 +484,125 @@ void ScreenHandler::OnReceive(uint8_t* data, ULONG size)
|
|||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -216,35 +612,67 @@ void ScreenHandler::applyQualityLevel(int8_t level, bool persist)
|
|||||||
{
|
{
|
||||||
m_qualityLevel = level;
|
m_qualityLevel = level;
|
||||||
|
|
||||||
|
// TODO: persist to config file if needed
|
||||||
|
(void)persist;
|
||||||
|
|
||||||
if (level == QUALITY_DISABLED) {
|
if (level == QUALITY_DISABLED) {
|
||||||
NSLog(@"Quality: Disabled");
|
// Disabled mode: keep current settings
|
||||||
|
NSLog(@"Quality: Disabled (keep current)");
|
||||||
return;
|
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) {
|
if (level >= 0 && level < QUALITY_COUNT) {
|
||||||
m_maxFPS.store(profiles[level][0]);
|
// Get profile from commands.h (shared with Windows/Linux)
|
||||||
m_algorithm.store(profiles[level][1]);
|
const QualityProfile& profile = GetQualityProfile(level);
|
||||||
NSLog(@"Quality: Level=%d, FPS=%d, Algo=%d", level, profiles[level][0], profiles[level][1]);
|
|
||||||
|
// 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 {
|
} else {
|
||||||
|
// Adaptive mode (level=-1): server adjusts dynamically
|
||||||
NSLog(@"Quality: Adaptive mode");
|
NSLog(@"Quality: Adaptive mode");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
|
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);
|
CGImageRef image = CGDisplayCreateImage(m_displayID);
|
||||||
if (!image) {
|
if (!image) {
|
||||||
NSLog(@"Failed to capture screen image");
|
NSLog(@"Failed to capture screen image");
|
||||||
@@ -255,49 +683,58 @@ bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
|
|||||||
size_t height = CGImageGetHeight(image);
|
size_t height = CGImageGetHeight(image);
|
||||||
|
|
||||||
if (width != (size_t)m_width || height != (size_t)m_height) {
|
if (width != (size_t)m_width || height != (size_t)m_height) {
|
||||||
// Screen resolution changed, need to reinitialize
|
|
||||||
CGImageRelease(image);
|
CGImageRelease(image);
|
||||||
NSLog(@"Screen resolution changed: %zux%zu", width, height);
|
NSLog(@"Screen resolution changed: %zux%zu", width, height);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create bitmap context to get raw pixel data
|
|
||||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
||||||
size_t bytesPerRow = width * 4;
|
size_t bytesPerRow = width * 4;
|
||||||
|
size_t requiredSize = bytesPerRow * height;
|
||||||
// Temporary buffer for top-down BGRA
|
if (m_tempBuffer.size() != requiredSize) {
|
||||||
std::vector<uint8_t> tempBuffer(bytesPerRow * height);
|
m_tempBuffer.resize(requiredSize);
|
||||||
|
}
|
||||||
|
|
||||||
CGContextRef context = CGBitmapContextCreate(
|
CGContextRef context = CGBitmapContextCreate(
|
||||||
tempBuffer.data(),
|
m_tempBuffer.data(),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
8,
|
8,
|
||||||
bytesPerRow,
|
bytesPerRow,
|
||||||
colorSpace,
|
m_colorSpace,
|
||||||
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little // BGRA
|
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little
|
||||||
);
|
);
|
||||||
|
|
||||||
CGColorSpaceRelease(colorSpace);
|
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
CGImageRelease(image);
|
CGImageRelease(image);
|
||||||
NSLog(@"Failed to create bitmap context");
|
NSLog(@"Failed to create bitmap context");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw image into context
|
|
||||||
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
|
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
|
||||||
CGContextRelease(context);
|
CGContextRelease(context);
|
||||||
CGImageRelease(image);
|
CGImageRelease(image);
|
||||||
|
|
||||||
// Flip vertically (BMP is bottom-up, CGImage is top-down)
|
// Flip vertically using Accelerate framework
|
||||||
for (size_t y = 0; y < height; y++) {
|
vImage_Buffer src = {
|
||||||
size_t srcRow = y;
|
.data = m_tempBuffer.data(),
|
||||||
size_t dstRow = height - 1 - y;
|
.height = (vImagePixelCount)height,
|
||||||
memcpy(buffer.data() + dstRow * bytesPerRow,
|
.width = (vImagePixelCount)width,
|
||||||
tempBuffer.data() + srcRow * bytesPerRow,
|
.rowBytes = bytesPerRow
|
||||||
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;
|
return true;
|
||||||
@@ -559,10 +996,11 @@ uint8_t ScreenHandler::getCursorTypeIndex()
|
|||||||
// Reuse cursor position from getCursorPosition (called before this)
|
// Reuse cursor position from getCursorPosition (called before this)
|
||||||
CGPoint pos = s_cachedLogicalPos;
|
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();
|
uint64_t now = getTickMs();
|
||||||
bool posChanged = (fabs(pos.x - lastPos.x) > 5 || fabs(pos.y - lastPos.y) > 5);
|
bool posChanged = (fabs(pos.x - lastPos.x) > 10 || fabs(pos.y - lastPos.y) > 10);
|
||||||
if (!posChanged && (now - lastCheckTime) < 100) {
|
if (!posChanged && (now - lastCheckTime) < 250) {
|
||||||
return cachedIndex;
|
return cachedIndex;
|
||||||
}
|
}
|
||||||
lastCheckTime = now;
|
lastCheckTime = now;
|
||||||
@@ -635,13 +1073,12 @@ uint8_t ScreenHandler::getCursorTypeIndex()
|
|||||||
|
|
||||||
void ScreenHandler::captureLoop()
|
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();
|
uint8_t currentAlgo = m_algorithm.load();
|
||||||
|
|
||||||
// Always send raw first frame (TOKEN_FIRSTSCREEN) to initialize server display
|
// 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();
|
sendFirstScreen();
|
||||||
|
|
||||||
// Small delay to ensure first frame is processed before H264 stream starts
|
// Small delay to ensure first frame is processed before H264 stream starts
|
||||||
@@ -650,6 +1087,23 @@ void ScreenHandler::captureLoop()
|
|||||||
while (m_running) {
|
while (m_running) {
|
||||||
uint64_t start = getTickMs();
|
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();
|
uint8_t algo = m_algorithm.load();
|
||||||
|
|
||||||
// Check if algorithm changed
|
// Check if algorithm changed
|
||||||
@@ -657,18 +1111,14 @@ void ScreenHandler::captureLoop()
|
|||||||
NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo);
|
NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo);
|
||||||
currentAlgo = algo;
|
currentAlgo = algo;
|
||||||
|
|
||||||
// If switching to/from H264, reset encoder
|
|
||||||
if (algo == ALGORITHM_H264) {
|
if (algo == ALGORITHM_H264) {
|
||||||
// Starting H264 - will be initialized in sendH264Frame
|
|
||||||
sendH264Frame(true); // First H264 frame is keyframe
|
sendH264Frame(true); // First H264 frame is keyframe
|
||||||
} else if (m_h264Encoder) {
|
} else if (m_h264Encoder) {
|
||||||
// Switching away from H264 - close encoder
|
|
||||||
m_h264Encoder->close();
|
m_h264Encoder->close();
|
||||||
m_h264Encoder.reset();
|
m_h264Encoder.reset();
|
||||||
sendFirstScreen(); // Send full frame for DIFF modes
|
sendFirstScreen();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Normal frame
|
|
||||||
if (algo == ALGORITHM_H264) {
|
if (algo == ALGORITHM_H264) {
|
||||||
sendH264Frame(false);
|
sendH264Frame(false);
|
||||||
} else {
|
} else {
|
||||||
@@ -676,14 +1126,17 @@ void ScreenHandler::captureLoop()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int fps = m_maxFPS.load();
|
// Only use sleep-based FPS control for legacy mode
|
||||||
if (fps <= 0) fps = 10;
|
if (!m_displayStream) {
|
||||||
int sleepMs = 1000 / fps;
|
int fps = m_maxFPS.load();
|
||||||
|
if (fps <= 0) fps = 10;
|
||||||
|
int sleepMs = 1000 / fps;
|
||||||
|
|
||||||
int elapsed = (int)(getTickMs() - start);
|
int elapsed = (int)(getTickMs() - start);
|
||||||
int wait = sleepMs - elapsed;
|
int wait = sleepMs - elapsed;
|
||||||
if (wait > 0) {
|
if (wait > 0) {
|
||||||
usleep(wait * 1000);
|
usleep(wait * 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
103
macos/install.sh
Normal file
103
macos/install.sh
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# macOS Ghost Client 安装脚本
|
||||||
|
# 用法: ./install.sh [ghost路径]
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
GHOST_SRC="${1:-$SCRIPT_DIR/build/bin/ghost}"
|
||||||
|
GHOST_DST="/usr/local/bin/ghost"
|
||||||
|
PLIST_DST="/Library/LaunchDaemons/com.ghost.client.plist"
|
||||||
|
|
||||||
|
echo "=== Ghost Client 安装程序 ==="
|
||||||
|
echo "源文件: $GHOST_SRC"
|
||||||
|
|
||||||
|
# 检查源文件
|
||||||
|
if [ ! -f "$GHOST_SRC" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "错误: 找不到 $GHOST_SRC"
|
||||||
|
echo ""
|
||||||
|
echo "请先编译: ./build.sh"
|
||||||
|
echo ""
|
||||||
|
echo "或指定路径: $0 <ghost可执行文件路径>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 1. 停止旧服务(只停止安装目录的,不影响调试目录)
|
||||||
|
echo "[1/6] 停止旧服务..."
|
||||||
|
sudo launchctl unload "$PLIST_DST" 2>/dev/null || true
|
||||||
|
sudo pkill -9 -f "$GHOST_DST" 2>/dev/null || true
|
||||||
|
|
||||||
|
# 2. 复制程序
|
||||||
|
echo "[2/6] 安装程序到 $GHOST_DST..."
|
||||||
|
sudo cp "$GHOST_SRC" "$GHOST_DST"
|
||||||
|
sudo chmod +x "$GHOST_DST"
|
||||||
|
|
||||||
|
# 3. 清除隔离属性
|
||||||
|
echo "[3/6] 清除隔离属性..."
|
||||||
|
sudo xattr -cr "$GHOST_DST"
|
||||||
|
|
||||||
|
# 4. 签名
|
||||||
|
echo "[4/6] 签名程序..."
|
||||||
|
sudo codesign --force --deep --sign - "$GHOST_DST"
|
||||||
|
|
||||||
|
# 5. 创建 launchd plist
|
||||||
|
echo "[5/6] 创建 launchd 服务..."
|
||||||
|
sudo tee "$PLIST_DST" > /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>Label</key>
|
||||||
|
<string>com.ghost.client</string>
|
||||||
|
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/usr/local/bin/ghost</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/var/log/ghost.log</string>
|
||||||
|
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/var/log/ghost.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo chown root:wheel "$PLIST_DST"
|
||||||
|
sudo chmod 644 "$PLIST_DST"
|
||||||
|
|
||||||
|
# 6. 完成
|
||||||
|
echo "[6/6] 安装完成!"
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "重要: 首次运行需要授权系统权限"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "请执行以下步骤:"
|
||||||
|
echo ""
|
||||||
|
echo "1. 手动运行以触发权限请求:"
|
||||||
|
echo " $GHOST_DST"
|
||||||
|
echo ""
|
||||||
|
echo "2. 授权后按 Ctrl+C 停止程序(权限需重启生效)"
|
||||||
|
echo ""
|
||||||
|
echo "3. 启动服务:"
|
||||||
|
echo " sudo launchctl load $PLIST_DST"
|
||||||
|
echo ""
|
||||||
|
echo "如未弹出授权对话框,手动添加:"
|
||||||
|
echo " 系统设置 > 隐私与安全性 > 屏幕录制 > 添加 ghost"
|
||||||
|
echo " 系统设置 > 隐私与安全性 > 辅助功能 > 添加 ghost"
|
||||||
|
echo ""
|
||||||
|
echo "常用命令:"
|
||||||
|
echo " 启动: sudo launchctl start com.ghost.client"
|
||||||
|
echo " 停止: sudo launchctl stop com.ghost.client"
|
||||||
|
echo " 卸载: sudo launchctl unload $PLIST_DST"
|
||||||
|
echo " 日志: tail -f /var/log/ghost.log"
|
||||||
|
echo ""
|
||||||
418
macos/main.mm
418
macos/main.mm
@@ -1,16 +1,20 @@
|
|||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import <sys/sysctl.h>
|
#import <sys/sysctl.h>
|
||||||
|
#import <sys/stat.h>
|
||||||
#import <mach/mach.h>
|
#import <mach/mach.h>
|
||||||
#import <mach-o/dyld.h>
|
#import <mach-o/dyld.h>
|
||||||
#import <pwd.h>
|
#import <pwd.h>
|
||||||
#import <signal.h>
|
#import <signal.h>
|
||||||
#import <unistd.h>
|
#import <unistd.h>
|
||||||
|
#import <fcntl.h>
|
||||||
#import <IOKit/IOKitLib.h>
|
#import <IOKit/IOKitLib.h>
|
||||||
|
#import <IOKit/pwr_mgt/IOPMLib.h>
|
||||||
#import <fstream>
|
#import <fstream>
|
||||||
#import <thread>
|
#import <thread>
|
||||||
#import <atomic>
|
#import <atomic>
|
||||||
#import <memory>
|
#import <memory>
|
||||||
#import <string>
|
#import <string>
|
||||||
|
#import <map>
|
||||||
|
|
||||||
#import "../client/IOCPClient.h"
|
#import "../client/IOCPClient.h"
|
||||||
#define XXH_INLINE_ALL
|
#define XXH_INLINE_ALL
|
||||||
@@ -19,6 +23,11 @@
|
|||||||
#import "ScreenHandler.h"
|
#import "ScreenHandler.h"
|
||||||
#import "InputHandler.h"
|
#import "InputHandler.h"
|
||||||
#import "SystemManager.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
|
// Global state
|
||||||
static std::atomic<bool> g_running(true);
|
static std::atomic<bool> g_running(true);
|
||||||
@@ -31,6 +40,103 @@ CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_M
|
|||||||
|
|
||||||
State g_bExit = S_CLIENT_NORMAL;
|
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 ==============
|
// ============== System Information Functions ==============
|
||||||
|
|
||||||
// Get macOS version string (e.g., "macOS 14.0 Sonoma")
|
// Get macOS version string (e.g., "macOS 14.0 Sonoma")
|
||||||
@@ -140,9 +246,113 @@ static std::string getTimeString()
|
|||||||
return std::string([dateString UTF8String]);
|
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()
|
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];
|
NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||||
if (app) {
|
if (app) {
|
||||||
NSString* name = [app localizedName];
|
NSString* name = [app localizedName];
|
||||||
@@ -258,9 +468,25 @@ static void fillLoginInfo(LOGIN_INFOR& info)
|
|||||||
// CPU MHz
|
// CPU MHz
|
||||||
info.dwCPUMHz = getCPUFrequencyMHz();
|
info.dwCPUMHz = getCPUFrequencyMHz();
|
||||||
|
|
||||||
// PC Name (hostname)
|
// PC Name (hostname) - with group name if set
|
||||||
std::string hostname = getHostname();
|
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
|
// Webcam
|
||||||
info.bWebCamIsExist = hasCameraDevice() ? 1 : 0;
|
info.bWebCamIsExist = hasCameraDevice() ? 1 : 0;
|
||||||
@@ -347,6 +573,7 @@ static void signalHandler(int sig)
|
|||||||
{
|
{
|
||||||
NSLog(@"Received signal %d, shutting down...", sig);
|
NSLog(@"Received signal %d, shutting down...", sig);
|
||||||
g_running = false;
|
g_running = false;
|
||||||
|
g_bExit = S_CLIENT_EXIT; // 通知所有工作线程退出
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setupSignals()
|
static void setupSignals()
|
||||||
@@ -357,6 +584,28 @@ static void setupSignals()
|
|||||||
signal(SIGPIPE, SIG_IGN);
|
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 ==============
|
// ============== Main Entry Point ==============
|
||||||
|
|
||||||
// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致)
|
// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致)
|
||||||
@@ -395,6 +644,30 @@ struct RttEstimator {
|
|||||||
RttEstimator g_rttEstimator;
|
RttEstimator g_rttEstimator;
|
||||||
int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整
|
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)
|
void* ScreenworkingThread(void* param)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -413,6 +686,8 @@ void* ScreenworkingThread(void* param)
|
|||||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
|
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
|
||||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||||
Sleep(1000);
|
Sleep(1000);
|
||||||
|
// 清除回调,防止重连线程访问已销毁的 handler
|
||||||
|
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||||
}
|
}
|
||||||
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
|
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
@@ -421,6 +696,28 @@ void* ScreenworkingThread(void* param)
|
|||||||
return NULL;
|
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)
|
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||||
{
|
{
|
||||||
if (szBuffer == nullptr || ulLength == 0)
|
if (szBuffer == nullptr || ulLength == 0)
|
||||||
@@ -431,6 +728,7 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
|||||||
g_bExit = S_CLIENT_EXIT;
|
g_bExit = S_CLIENT_EXIT;
|
||||||
g_running = false; // Stop main loop to prevent reconnection
|
g_running = false; // Stop main loop to prevent reconnection
|
||||||
} else if (szBuffer[0] == COMMAND_SHELL) {
|
} else if (szBuffer[0] == COMMAND_SHELL) {
|
||||||
|
std::thread(ShellworkingThread, nullptr).detach();
|
||||||
Mprintf("** [%p] Received 'SHELL' command ***\n", user);
|
Mprintf("** [%p] Received 'SHELL' command ***\n", user);
|
||||||
} else if (szBuffer[0] == COMMAND_SCREEN_SPY) {
|
} else if (szBuffer[0] == COMMAND_SCREEN_SPY) {
|
||||||
std::thread(ScreenworkingThread, nullptr).detach();
|
std::thread(ScreenworkingThread, nullptr).detach();
|
||||||
@@ -438,7 +736,34 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
|||||||
} else if (szBuffer[0] == COMMAND_SYSTEM) {
|
} else if (szBuffer[0] == COMMAND_SYSTEM) {
|
||||||
Mprintf("** [%p] Received 'SYSTEM' command ***\n", user);
|
Mprintf("** [%p] Received 'SYSTEM' command ***\n", user);
|
||||||
} else if (szBuffer[0] == COMMAND_LIST_DRIVE) {
|
} else if (szBuffer[0] == COMMAND_LIST_DRIVE) {
|
||||||
|
std::thread(FileManagerworkingThread, nullptr).detach();
|
||||||
Mprintf("** [%p] Received 'LIST_DRIVE' command ***\n", user);
|
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) {
|
} else if (szBuffer[0] == CMD_HEARTBEAT_ACK) {
|
||||||
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
|
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
|
||||||
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
|
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
|
||||||
@@ -464,33 +789,97 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
|||||||
}
|
}
|
||||||
} else if (szBuffer[0] == COMMAND_NEXT) {
|
} else if (szBuffer[0] == COMMAND_NEXT) {
|
||||||
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
|
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);
|
||||||
|
Mprintf("** [%p] Group changed to: %s ***\n", user, groupName.c_str());
|
||||||
} else {
|
} else {
|
||||||
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
|
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
|
||||||
}
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用法: ./ghost [-d]
|
||||||
|
// -d 后台守护进程模式
|
||||||
int main(int argc, const char* argv[])
|
int main(int argc, const char* argv[])
|
||||||
{
|
{
|
||||||
(void)argc;
|
// 解析 -d 参数
|
||||||
(void)argv;
|
bool daemon_mode = (argc > 1 && strcmp(argv[1], "-d") == 0);
|
||||||
|
|
||||||
|
// 守护进程模式:在进入 autoreleasepool 之前 fork
|
||||||
|
if (daemon_mode) {
|
||||||
|
daemonize();
|
||||||
|
}
|
||||||
|
|
||||||
@autoreleasepool {
|
@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
|
// Setup signal handlers
|
||||||
setupSignals();
|
setupSignals();
|
||||||
|
|
||||||
|
// Load configuration file (~/.config/ghost/config.conf)
|
||||||
|
loadConfig();
|
||||||
|
NSLog(@"Config loaded from %s", g_configPath.c_str());
|
||||||
|
|
||||||
// Check permissions
|
// Check permissions
|
||||||
NSLog(@"Checking 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(@"Screen capture permission not granted.");
|
||||||
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Screen Recording");
|
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(@"Accessibility permission not granted.");
|
||||||
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Accessibility");
|
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Accessibility");
|
||||||
Permissions::requestAccessibility();
|
Permissions::requestAccessibility();
|
||||||
@@ -501,6 +890,8 @@ int main(int argc, const char* argv[])
|
|||||||
NSLog(@"Full Disk Access: not detected (may be false negative).");
|
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");
|
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
|
// Don't auto-open settings since detection is unreliable
|
||||||
|
} else {
|
||||||
|
NSLog(@"Full Disk Access: OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create client
|
// Create client
|
||||||
@@ -549,6 +940,15 @@ int main(int argc, const char* argv[])
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSLog(@"Shutting down...");
|
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;
|
return 0;
|
||||||
|
|||||||
31
macos/uninstall.sh
Normal file
31
macos/uninstall.sh
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# macOS Ghost Client 卸载脚本
|
||||||
|
|
||||||
|
echo "=== Ghost Client 卸载程序 ==="
|
||||||
|
|
||||||
|
# 1. 停止并卸载 launchd 服务
|
||||||
|
echo "[1/4] 停止服务..."
|
||||||
|
sudo launchctl unload /Library/LaunchDaemons/com.ghost.client.plist 2>/dev/null
|
||||||
|
launchctl unload ~/Library/LaunchAgents/com.ghost.client.plist 2>/dev/null
|
||||||
|
|
||||||
|
# 2. 杀死残留进程
|
||||||
|
echo "[2/4] 终止进程..."
|
||||||
|
sudo pkill -9 -f "/usr/local/bin/ghost" 2>/dev/null
|
||||||
|
|
||||||
|
# 3. 删除文件
|
||||||
|
echo "[3/4] 删除文件..."
|
||||||
|
sudo rm -f /Library/LaunchDaemons/com.ghost.client.plist
|
||||||
|
rm -f ~/Library/LaunchAgents/com.ghost.client.plist
|
||||||
|
sudo rm -f /usr/local/bin/ghost
|
||||||
|
rm -rf ~/.config/ghost
|
||||||
|
sudo rm -f /var/log/ghost.log
|
||||||
|
|
||||||
|
# 4. 完成
|
||||||
|
echo "[4/4] 卸载完成!"
|
||||||
|
echo ""
|
||||||
|
echo "注意: 系统权限(屏幕录制/辅助功能)未重置。"
|
||||||
|
echo ""
|
||||||
|
echo "如需重置系统权限(会影响所有应用),请手动执行:"
|
||||||
|
echo " tccutil reset ScreenCapture"
|
||||||
|
echo " tccutil reset Accessibility"
|
||||||
|
echo " tccutil reset SystemPolicyAllFiles"
|
||||||
@@ -3872,6 +3872,9 @@ BOOL CMy2015RemoteDlg::ShouldRemoteControl()
|
|||||||
|
|
||||||
void screenParamModifier(context* ctx, void* user)
|
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);
|
auto version = ctx->GetClientData(ONLINELIST_VERSION);
|
||||||
if (!IsDateGreaterOrEqual(version, "Feb 8 2026")) {
|
if (!IsDateGreaterOrEqual(version, "Feb 8 2026")) {
|
||||||
char* param = (char*)user;
|
char* param = (char*)user;
|
||||||
@@ -6350,8 +6353,17 @@ LRESULT CMy2015RemoteDlg::OnOpenScreenSpyDialog(WPARAM wParam, LPARAM lParam)
|
|||||||
BYTE bToken = COMMAND_BYE;
|
BYTE bToken = COMMAND_BYE;
|
||||||
return ContextObject->Send2Client(&bToken, 1) ? 0 : 0x20260223;
|
return ContextObject->Send2Client(&bToken, 1) ? 0 : 0x20260223;
|
||||||
}
|
}
|
||||||
if (clientID && WebService().IsRunning() && WebService().IsWebTriggered(clientID) && WebService().GetHideWebSessions()) {
|
// Check trigger source: MFC-triggered dialogs are always visible
|
||||||
return OpenDialog<CScreenSpyDlg, IDD_DIALOG_SCREEN_SPY, SW_HIDE>(wParam, lParam);
|
// 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);
|
return OpenDialog<CScreenSpyDlg, IDD_DIALOG_SCREEN_SPY, SW_SHOWMAXIMIZED>(wParam, lParam);
|
||||||
}
|
}
|
||||||
@@ -7120,10 +7132,16 @@ void CMy2015RemoteDlg::OnDynamicSubMenu(UINT nID)
|
|||||||
}
|
}
|
||||||
LeaveCriticalSection(&m_cs);
|
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()
|
void CMy2015RemoteDlg::OnOnlineVirtualDesktop()
|
||||||
{
|
{
|
||||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 2, ALGORITHM_DIFF, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) };
|
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 +7150,7 @@ void CMy2015RemoteDlg::OnOnlineGrayDesktop()
|
|||||||
if (!ShouldRemoteControl())
|
if (!ShouldRemoteControl())
|
||||||
return;
|
return;
|
||||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 0, ALGORITHM_GRAY, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) };
|
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 +7159,7 @@ void CMy2015RemoteDlg::OnOnlineRemoteDesktop()
|
|||||||
if (!ShouldRemoteControl())
|
if (!ShouldRemoteControl())
|
||||||
return;
|
return;
|
||||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 1, ALGORITHM_DIFF, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) };
|
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 +7168,7 @@ void CMy2015RemoteDlg::OnOnlineH264Desktop()
|
|||||||
if (!ShouldRemoteControl())
|
if (!ShouldRemoteControl())
|
||||||
return;
|
return;
|
||||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, 0, ALGORITHM_H264, THIS_CFG.GetInt("settings", "MultiScreen", TRUE) };
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -8212,6 +8230,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)
|
void CMy2015RemoteDlg::UpdateActiveRemoteSession(CDialogBase *sess)
|
||||||
{
|
{
|
||||||
EnterCriticalSection(&m_cs);
|
EnterCriticalSection(&m_cs);
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ public:
|
|||||||
CDialogBase* GetRemoteWindow(CDialogBase* dlg);
|
CDialogBase* GetRemoteWindow(CDialogBase* dlg);
|
||||||
void RemoveRemoteWindow(HWND wnd);
|
void RemoveRemoteWindow(HWND wnd);
|
||||||
void CloseRemoteDesktopByClientID(uint64_t clientID);
|
void CloseRemoteDesktopByClientID(uint64_t clientID);
|
||||||
|
void CloseWebRemoteDesktopByClientID(uint64_t clientID); // Only close Web session dialog
|
||||||
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
|
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
|
||||||
void UpdateActiveRemoteSession(CDialogBase* sess);
|
void UpdateActiveRemoteSession(CDialogBase* sess);
|
||||||
CDialogBase* GetActiveRemoteSession();
|
CDialogBase* GetActiveRemoteSession();
|
||||||
|
|||||||
@@ -157,8 +157,9 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
|
|||||||
if (pClientID) {
|
if (pClientID) {
|
||||||
m_ClientID = *((uint64_t*)pClientID);
|
m_ClientID = *((uint64_t*)pClientID);
|
||||||
|
|
||||||
// Notify web clients of resolution (important for clients that only send TOKEN_BITMAPINFO once)
|
// Notify web clients of resolution (only for Web sessions, not MFC sessions)
|
||||||
if (WebService().IsRunning()) {
|
// 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 width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||||
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||||
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
||||||
@@ -237,10 +238,14 @@ CScreenSpyDlg::~CScreenSpyDlg()
|
|||||||
StopAudioPlayback();
|
StopAudioPlayback();
|
||||||
|
|
||||||
// 清理所有文件接收对话框
|
// 清理所有文件接收对话框
|
||||||
for (auto& pair : m_FileRecvDlgs) {
|
// 注意:对话框可能已经被用户关闭并自我销毁(PostNcDestroy 中 delete this)
|
||||||
if (pair.second) {
|
// 存储了 HWND 用于安全检查,避免访问野指针
|
||||||
pair.second->DestroyWindow();
|
for (auto& entry : m_FileRecvDlgs) {
|
||||||
delete pair.second;
|
HWND hWnd = entry.second.first;
|
||||||
|
if (hWnd && ::IsWindow(hWnd)) {
|
||||||
|
// 通过 HWND 同步发送关闭消息,确保对话框在析构前完全关闭
|
||||||
|
// 使用 SendMessage 而非 PostMessage,避免异步问题
|
||||||
|
::SendMessage(hWnd, WM_CLOSE, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_FileRecvDlgs.clear();
|
m_FileRecvDlgs.clear();
|
||||||
@@ -761,23 +766,43 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
|||||||
if (pMain)
|
if (pMain)
|
||||||
::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)this, 0);
|
::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)this, 0);
|
||||||
|
|
||||||
// 注册屏幕上下文到 WebService(用于 Web 端鼠标/键盘控制)
|
// Determine session type: MFC or Web
|
||||||
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
|
// 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
|
// Only register screen context for Web sessions
|
||||||
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
|
// MFC dialogs handle input directly via m_ContextObject, don't need WebService registry
|
||||||
m_bHide = true;
|
// This prevents MFC close from deleting Web's context (they share same device_id key)
|
||||||
ShowWindow(SW_HIDE);
|
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;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
VOID CScreenSpyDlg::OnClose()
|
VOID CScreenSpyDlg::OnClose()
|
||||||
{
|
{
|
||||||
// 注销屏幕上下文(Web 端控制)
|
// Only unregister if this is a Web session (we only registered for Web sessions)
|
||||||
WebService().UnregisterScreenContext(m_ClientID);
|
if (m_bIsWebSession) {
|
||||||
|
WebService().UnregisterScreenContext(m_ClientID);
|
||||||
|
}
|
||||||
|
|
||||||
m_bIsClosed = true;
|
m_bIsClosed = true;
|
||||||
m_bIsCtrl = FALSE;
|
m_bIsCtrl = FALSE;
|
||||||
@@ -840,13 +865,15 @@ LRESULT CScreenSpyDlg::OnRecvFileV2Chunk(WPARAM wParam, LPARAM lParam)
|
|||||||
uint64_t transferID = msgData->transferID;
|
uint64_t transferID = msgData->transferID;
|
||||||
|
|
||||||
// 创建或获取进度对话框(按 transferID 管理)
|
// 创建或获取进度对话框(按 transferID 管理)
|
||||||
CDlgFileSend*& dlg = m_FileRecvDlgs[transferID];
|
auto& entry = m_FileRecvDlgs[transferID];
|
||||||
if (dlg == nullptr) {
|
CDlgFileSend* dlg = entry.second;
|
||||||
|
if (dlg == nullptr || !::IsWindow(entry.first)) {
|
||||||
dlg = new CDlgFileSend(m_pParent, m_ContextObject->GetServer(), m_ContextObject, FALSE);
|
dlg = new CDlgFileSend(m_pParent, m_ContextObject->GetServer(), m_ContextObject, FALSE);
|
||||||
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
|
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
|
||||||
dlg->SetWindowTextA(_TR("接收文件"));
|
dlg->SetWindowTextA(_TR("接收文件"));
|
||||||
dlg->ShowWindow(SW_SHOW);
|
dlg->ShowWindow(SW_SHOW);
|
||||||
dlg->m_bKeepConnection = TRUE; // 不断开连接
|
dlg->m_bKeepConnection = TRUE; // 不断开连接
|
||||||
|
entry = { dlg->GetSafeHwnd(), dlg };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 接收文件
|
// 接收文件
|
||||||
@@ -889,7 +916,11 @@ LRESULT CScreenSpyDlg::OnRecvFileV2Complete(WPARAM wParam, LPARAM lParam)
|
|||||||
// 关闭进度对话框
|
// 关闭进度对话框
|
||||||
auto it = m_FileRecvDlgs.find(transferID);
|
auto it = m_FileRecvDlgs.find(transferID);
|
||||||
if (it != m_FileRecvDlgs.end()) {
|
if (it != m_FileRecvDlgs.end()) {
|
||||||
it->second->FinishFileSend(verifyOk);
|
// 只有窗口有效时才调用 FinishFileSend
|
||||||
|
if (::IsWindow(it->second.first)) {
|
||||||
|
it->second.second->FinishFileSend(verifyOk);
|
||||||
|
}
|
||||||
|
// 无论窗口是否有效,都要移除条目(避免累积无效条目)
|
||||||
m_FileRecvDlgs.erase(it);
|
m_FileRecvDlgs.erase(it);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -964,18 +995,11 @@ VOID CScreenSpyDlg::OnReceiveComplete()
|
|||||||
PrepareDrawing(m_BitmapInfor_Full);
|
PrepareDrawing(m_BitmapInfor_Full);
|
||||||
// 分辨率切换完成,允许解码
|
// 分辨率切换完成,允许解码
|
||||||
m_bResolutionChanging = false;
|
m_bResolutionChanging = false;
|
||||||
// Notify web clients of resolution change
|
// Notify web clients of resolution change (only for Web session dialogs)
|
||||||
if (WebService().IsRunning()) {
|
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||||
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||||
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||||
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1266,8 +1290,8 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
|||||||
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2+sizeof(POINT))[0];
|
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2+sizeof(POINT))[0];
|
||||||
if (bOldCursorIndex != m_bCursorIndex) {
|
if (bOldCursorIndex != m_bCursorIndex) {
|
||||||
bChange = TRUE;
|
bChange = TRUE;
|
||||||
// 通知 Web 客户端光标变化
|
// 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
|
||||||
if (WebService().IsRunning()) {
|
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||||
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
||||||
}
|
}
|
||||||
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
|
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
|
||||||
@@ -1317,8 +1341,8 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
|||||||
bChange = TRUE;
|
bChange = TRUE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Broadcast H264 keyframe to web clients
|
// Broadcast H264 keyframe to web clients (only for Web session dialogs)
|
||||||
if (NextScreenLength > 0 && WebService().IsRunning()) {
|
if (m_bIsWebSession && NextScreenLength > 0 && WebService().IsRunning()) {
|
||||||
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
|
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
|
||||||
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
|
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
|
||||||
uint8_t frameType = 1; // Keyframe
|
uint8_t frameType = 1; // Keyframe
|
||||||
@@ -1376,9 +1400,9 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
|||||||
bChange = TRUE;
|
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]
|
// 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
|
// Detect H264 keyframe by checking NAL unit type
|
||||||
// NAL type 5 = IDR slice (keyframe), NAL type 7 = SPS, NAL type 8 = PPS
|
// NAL type 5 = IDR slice (keyframe), NAL type 7 = SPS, NAL type 8 = PPS
|
||||||
bool isKeyFrame = false;
|
bool isKeyFrame = false;
|
||||||
@@ -1463,8 +1487,8 @@ VOID CScreenSpyDlg::DrawScrollFrame()
|
|||||||
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0];
|
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0];
|
||||||
if (bOldCursorIndex != m_bCursorIndex) {
|
if (bOldCursorIndex != m_bCursorIndex) {
|
||||||
bChange = TRUE;
|
bChange = TRUE;
|
||||||
// 通知 Web 客户端光标变化
|
// 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
|
||||||
if (WebService().IsRunning()) {
|
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||||
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2305,8 +2329,8 @@ BOOL CScreenSpyDlg::PreTranslateMessage(MSG* pMsg)
|
|||||||
MSG wheelMsg = *pMsg;
|
MSG wheelMsg = *pMsg;
|
||||||
wheelMsg.lParam = MAKELPARAM(pt.x, pt.y);
|
wheelMsg.lParam = MAKELPARAM(pt.x, pt.y);
|
||||||
SendScaledMouseMessage(&wheelMsg, true);
|
SendScaledMouseMessage(&wheelMsg, true);
|
||||||
|
return TRUE; // 已处理,阻止继续分发到 OnMouseWheel
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case WM_KEYDOWN:
|
case WM_KEYDOWN:
|
||||||
case WM_KEYUP:
|
case WM_KEYUP:
|
||||||
case WM_SYSKEYDOWN:
|
case WM_SYSKEYDOWN:
|
||||||
@@ -2682,7 +2706,20 @@ void CScreenSpyDlg::OnLButtonUp(UINT nFlags, CPoint point)
|
|||||||
|
|
||||||
BOOL CScreenSpyDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <imm.h>
|
#include <imm.h>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <atomic>
|
||||||
#include "IOCPServer.h"
|
#include "IOCPServer.h"
|
||||||
#include "..\..\client\CursorInfo.h"
|
#include "..\..\client\CursorInfo.h"
|
||||||
#include "VideoDlg.h"
|
#include "VideoDlg.h"
|
||||||
@@ -153,6 +154,10 @@ public:
|
|||||||
return TRUE;
|
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 SendNext(void);
|
||||||
VOID OnReceiveComplete();
|
VOID OnReceiveComplete();
|
||||||
HDC m_hFullDC;
|
HDC m_hFullDC;
|
||||||
@@ -186,13 +191,15 @@ public:
|
|||||||
int m_FrameID;
|
int m_FrameID;
|
||||||
HIMC m_hOldIMC = NULL; // 保存原始 IME 上下文,控制模式切换时使用
|
HIMC m_hOldIMC = NULL; // 保存原始 IME 上下文,控制模式切换时使用
|
||||||
bool m_bHide = false;
|
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; // 截图保存路径提示
|
std::string m_strSaveNotice; // 截图保存路径提示
|
||||||
ULONGLONG m_nSaveNoticeTime = 0; // 截图提示开始时间
|
ULONGLONG m_nSaveNoticeTime = 0; // 截图提示开始时间
|
||||||
BOOL m_bUsingFRP = FALSE;
|
BOOL m_bUsingFRP = FALSE;
|
||||||
|
|
||||||
// 文件接收进度对话框(用于 Linux Ctrl+C -> 服务端 Ctrl+V)
|
// 文件接收进度对话框(用于 Linux Ctrl+C -> 服务端 Ctrl+V)
|
||||||
// 按 transferID 管理多个并发传输
|
// 按 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);
|
void SaveSnapshot(void);
|
||||||
// 对话框数据
|
// 对话框数据
|
||||||
|
|||||||
@@ -997,10 +997,14 @@ inline std::string GetWebPageHTML() {
|
|||||||
<h4>Create New User</h4>
|
<h4>Create New User</h4>
|
||||||
<input type="text" id="new-username" placeholder="Username" autocomplete="off">
|
<input type="text" id="new-username" placeholder="Username" autocomplete="off">
|
||||||
<input type="password" id="new-password" placeholder="Password" autocomplete="new-password">
|
<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="viewer">Viewer (read-only)</option>
|
||||||
<option value="admin">Admin (full access)</option>
|
<option value="admin">Admin (full access)</option>
|
||||||
</select>
|
</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>
|
<button onclick="createUser()">Create User</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-list">
|
<div class="user-list">
|
||||||
@@ -1286,6 +1290,11 @@ inline std::string GetWebPageHTML() {
|
|||||||
renderUsersList(msg.users);
|
renderUsersList(msg.users);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'groups':
|
||||||
|
if (msg.ok) {
|
||||||
|
renderGroupsCheckboxes(msg.groups);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)HTML";
|
)HTML";
|
||||||
@@ -1661,7 +1670,35 @@ inline std::string GetWebPageHTML() {
|
|||||||
function openUsersModal() {
|
function openUsersModal() {
|
||||||
document.getElementById('users-modal').classList.add('active');
|
document.getElementById('users-modal').classList.add('active');
|
||||||
document.getElementById('user-msg').innerHTML = '';
|
document.getElementById('user-msg').innerHTML = '';
|
||||||
|
document.getElementById('new-role').value = 'viewer'; // Reset to default
|
||||||
|
onRoleChange(); // Update groups section visibility
|
||||||
listUsers();
|
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() {
|
function closeUsersModal() {
|
||||||
@@ -1685,8 +1722,12 @@ inline std::string GetWebPageHTML() {
|
|||||||
return;
|
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) {
|
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 => {
|
container.innerHTML = users.map(u => {
|
||||||
const isAdmin = u.role === 'admin';
|
const isAdmin = u.role === 'admin';
|
||||||
const canDelete = u.username !== 'admin'; // Cannot delete built-in 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">' +
|
return '<div class="user-item">' +
|
||||||
'<div class="user-info">' +
|
'<div class="user-info">' +
|
||||||
'<div class="username">' + escapeHtml(u.username) + '</div>' +
|
'<div class="username">' + escapeHtml(u.username) + '</div>' +
|
||||||
'<div class="role ' + (isAdmin ? 'admin' : '') + '">' + u.role + '</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>' +
|
'</div>' +
|
||||||
(canDelete ? '<button class="delete-btn" onclick="deleteUser(\'' + escapeHtml(u.username) + '\')">Delete</button>' : '') +
|
(canDelete ? '<button class="delete-btn" onclick="deleteUser(\'' + escapeHtml(u.username) + '\')">Delete</button>' : '') +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <shlobj.h>
|
#include <shlobj.h>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
// Algorithm constants (same as ScreenSpyDlg.cpp)
|
// Algorithm constants (same as ScreenSpyDlg.cpp)
|
||||||
#define ALGORITHM_H264 2
|
#define ALGORITHM_H264 2
|
||||||
@@ -363,6 +364,8 @@ void CWebService::ServerThread(int port) {
|
|||||||
HandleDeleteUser(ws_ptr, msg);
|
HandleDeleteUser(ws_ptr, msg);
|
||||||
} else if (cmd == "list_users") {
|
} else if (cmd == "list_users") {
|
||||||
HandleListUsers(ws_ptr, token);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SendText(ws_ptr, BuildDeviceListJson());
|
SendText(ws_ptr, BuildDeviceListJson(username));
|
||||||
}
|
}
|
||||||
|
|
||||||
void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id) {
|
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;
|
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
|
// Check max clients per device
|
||||||
int current_count = GetWebClientCount(device_id);
|
int current_count = GetWebClientCount(device_id);
|
||||||
if (current_count >= m_nMaxClientsPerDevice) {
|
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 newPassword = root.get("password", "").asString();
|
||||||
std::string newRole = root.get("role", "viewer").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()) {
|
if (newUsername.empty() || newPassword.empty()) {
|
||||||
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required"));
|
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CreateUser(newUsername, newPassword, newRole)) {
|
if (CreateUser(newUsername, newPassword, newRole, allowedGroups)) {
|
||||||
SendText(ws_ptr, BuildJsonResponse("create_user_result", true));
|
SendText(ws_ptr, BuildJsonResponse("create_user_result", true));
|
||||||
} else {
|
} else {
|
||||||
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)"));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto users = ListUsers();
|
|
||||||
|
|
||||||
Json::Value res;
|
Json::Value res;
|
||||||
res["cmd"] = "list_users_result";
|
res["cmd"] = "list_users_result";
|
||||||
res["ok"] = true;
|
res["ok"] = true;
|
||||||
|
|
||||||
Json::Value usersArray(Json::arrayValue);
|
Json::Value usersArray(Json::arrayValue);
|
||||||
for (const auto& u : users) {
|
{
|
||||||
Json::Value user;
|
std::lock_guard<std::mutex> lock(m_UsersMutex);
|
||||||
user["username"] = u.first;
|
for (const auto& u : m_Users) {
|
||||||
user["role"] = u.second;
|
Json::Value user;
|
||||||
usersArray.append(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;
|
res["users"] = usersArray;
|
||||||
|
|
||||||
@@ -1030,6 +1079,48 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
|
|||||||
SendText(ws_ptr, json);
|
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)
|
// Token Management (delegated to WebServiceAuth module)
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
@@ -1170,6 +1261,14 @@ void CWebService::LoadUsers() {
|
|||||||
user.salt = u.get("salt", "").asString();
|
user.salt = u.get("salt", "").asString();
|
||||||
user.role = u.get("role", "viewer").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()) {
|
if (!user.password_hash.empty()) {
|
||||||
m_Users.push_back(user);
|
m_Users.push_back(user);
|
||||||
loaded++;
|
loaded++;
|
||||||
@@ -1197,6 +1296,14 @@ void CWebService::SaveUsers() {
|
|||||||
user["password_hash"] = u.password_hash;
|
user["password_hash"] = u.password_hash;
|
||||||
user["salt"] = u.salt;
|
user["salt"] = u.salt;
|
||||||
user["role"] = u.role;
|
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);
|
users.append(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1217,7 +1324,8 @@ void CWebService::SaveUsers() {
|
|||||||
Mprintf("[WebService] Saved %d users to users.json\n", (int)users.size());
|
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.empty() || password.empty()) return false;
|
||||||
if (username == "admin") return false; // Cannot create user named "admin"
|
if (username == "admin") return false; // Cannot create user named "admin"
|
||||||
if (role != "admin" && role != "viewer") return false;
|
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.salt = GenerateSalt();
|
||||||
user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
|
user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
|
||||||
user.role = role;
|
user.role = role;
|
||||||
|
user.allowed_groups = allowed_groups;
|
||||||
|
|
||||||
m_Users.push_back(user);
|
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)
|
// 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);
|
return Json::writeString(builder, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string CWebService::BuildDeviceListJson() {
|
std::string CWebService::BuildDeviceListJson(const std::string& username) {
|
||||||
Json::Value res;
|
Json::Value res;
|
||||||
res["cmd"] = "device_list";
|
res["cmd"] = "device_list";
|
||||||
res["devices"] = Json::Value(Json::arrayValue);
|
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) {
|
if (m_pParentDlg) {
|
||||||
// Access device list with lock
|
// Access device list with lock
|
||||||
EnterCriticalSection(&m_pParentDlg->m_cs);
|
EnterCriticalSection(&m_pParentDlg->m_cs);
|
||||||
for (context* ctx : m_pParentDlg->m_HostList) {
|
for (context* ctx : m_pParentDlg->m_HostList) {
|
||||||
if (!ctx || !ctx->IsLogin()) continue;
|
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;
|
Json::Value device;
|
||||||
// Use string for ID to avoid JavaScript number precision loss
|
// Use string for ID to avoid JavaScript number precision loss
|
||||||
device["id"] = std::to_string(ctx->GetClientID());
|
device["id"] = std::to_string(ctx->GetClientID());
|
||||||
@@ -1332,6 +1472,9 @@ std::string CWebService::BuildDeviceListJson() {
|
|||||||
device["activeWindow"] = AnsiToUtf8(activeWindow);
|
device["activeWindow"] = AnsiToUtf8(activeWindow);
|
||||||
device["online"] = true;
|
device["online"] = true;
|
||||||
|
|
||||||
|
// Add device group to response
|
||||||
|
device["group"] = deviceGroup;
|
||||||
|
|
||||||
// Get screen info from client's reported resolution
|
// Get screen info from client's reported resolution
|
||||||
// Format: "n:MxN" where n=monitor count, M=width, N=height
|
// Format: "n:MxN" where n=monitor count, M=width, N=height
|
||||||
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
|
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
|
||||||
@@ -1509,9 +1652,13 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
|
|||||||
context* ctx = m_pParentDlg->FindHost(device_id);
|
context* ctx = m_pParentDlg->FindHost(device_id);
|
||||||
if (!ctx) return false;
|
if (!ctx) return false;
|
||||||
|
|
||||||
// Close any existing remote desktop for this device first
|
// Check if there's already a Web session for this device
|
||||||
// This prevents duplicate dialogs when user reconnects quickly
|
// Only reuse if Web has already triggered AND a Web dialog exists
|
||||||
m_pParentDlg->CloseRemoteDesktopByClientID(device_id);
|
// 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)
|
// 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
|
// 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 };
|
BYTE bToken[32] = { 0 };
|
||||||
bToken[0] = COMMAND_SCREEN_SPY;
|
bToken[0] = COMMAND_SCREEN_SPY;
|
||||||
bToken[1] = 0; // DXGI mode: 0=GDI
|
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) {
|
if (watchingCount == 0) {
|
||||||
ClearWebTriggered(device_id);
|
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) {
|
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);
|
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
|
||||||
m_ScreenContexts.erase(device_id);
|
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) {
|
CONTEXT_OBJECT* CWebService::GetScreenContext(uint64_t device_id) {
|
||||||
@@ -1666,6 +1818,26 @@ void CWebService::ClearWebTriggered(uint64_t device_id) {
|
|||||||
m_WebTriggeredDevices.erase(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) {
|
void CWebService::NotifyDeviceUpdate(uint64_t device_id, const std::string& rtt, const std::string& activeWindow) {
|
||||||
if (!m_bRunning || m_bStopping) return;
|
if (!m_bRunning || m_bStopping) return;
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ struct WebUser {
|
|||||||
std::string password_hash; // SHA256(password + salt)
|
std::string password_hash; // SHA256(password + salt)
|
||||||
std::string salt;
|
std::string salt;
|
||||||
std::string role; // "admin" | "viewer"
|
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
|
// Device info for web clients
|
||||||
@@ -79,7 +80,8 @@ public:
|
|||||||
void SetAdminPassword(const std::string& password);
|
void SetAdminPassword(const std::string& password);
|
||||||
|
|
||||||
// User management
|
// 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);
|
bool DeleteUser(const std::string& username);
|
||||||
std::vector<std::pair<std::string, std::string>> ListUsers(); // Returns [(username, role), ...]
|
std::vector<std::pair<std::string, std::string>> ListUsers(); // Returns [(username, role), ...]
|
||||||
|
|
||||||
@@ -144,7 +146,7 @@ private:
|
|||||||
|
|
||||||
// JSON helpers
|
// JSON helpers
|
||||||
std::string BuildJsonResponse(const std::string& cmd, bool ok, const std::string& msg = "");
|
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
|
// Password verification
|
||||||
bool VerifyPassword(const std::string& input, const WebUser& user);
|
bool VerifyPassword(const std::string& input, const WebUser& user);
|
||||||
@@ -157,6 +159,7 @@ private:
|
|||||||
void HandleCreateUser(void* ws_ptr, const std::string& msg);
|
void HandleCreateUser(void* ws_ptr, const std::string& msg);
|
||||||
void HandleDeleteUser(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 HandleListUsers(void* ws_ptr, const std::string& token);
|
||||||
|
void HandleGetGroups(void* ws_ptr, const std::string& token);
|
||||||
|
|
||||||
// Send to WebSocket
|
// Send to WebSocket
|
||||||
void SendText(void* ws_ptr, const std::string& text);
|
void SendText(void* ws_ptr, const std::string& text);
|
||||||
@@ -224,6 +227,14 @@ public:
|
|||||||
bool IsWebTriggered(uint64_t device_id);
|
bool IsWebTriggered(uint64_t device_id);
|
||||||
void ClearWebTriggered(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
|
// Config accessors
|
||||||
void SetHideWebSessions(bool hide) { m_bHideWebSessions = hide; }
|
void SetHideWebSessions(bool hide) { m_bHideWebSessions = hide; }
|
||||||
bool GetHideWebSessions() const { return m_bHideWebSessions; }
|
bool GetHideWebSessions() const { return m_bHideWebSessions; }
|
||||||
@@ -240,6 +251,10 @@ private:
|
|||||||
// Screen context registry: device_id -> ScreenManager's CONTEXT_OBJECT
|
// Screen context registry: device_id -> ScreenManager's CONTEXT_OBJECT
|
||||||
std::map<uint64_t, CONTEXT_OBJECT*> m_ScreenContexts;
|
std::map<uint64_t, CONTEXT_OBJECT*> m_ScreenContexts;
|
||||||
std::mutex m_ScreenContextsMutex;
|
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
|
// Global accessor
|
||||||
|
|||||||
Reference in New Issue
Block a user