Compare commits
32 Commits
v1.3.2
...
f85cc8b86c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f85cc8b86c | ||
|
|
bc06fd5af5 | ||
|
|
731ff7a894 | ||
|
|
566f5b8d42 | ||
|
|
70a6b0128e | ||
|
|
b252cbbaf2 | ||
|
|
5f4fb62d20 | ||
|
|
ef8165c3b4 | ||
|
|
2c5b5ad628 | ||
|
|
0aa75882d1 | ||
|
|
11434653e9 | ||
|
|
05a9bb1245 | ||
|
|
a89f8dd28f | ||
|
|
6113b4653d | ||
|
|
f11fc93ba8 | ||
|
|
773c78ac0f | ||
|
|
92f3df8464 | ||
|
|
b732f841d0 | ||
|
|
1df2a7b321 | ||
|
|
3d8e90da14 | ||
|
|
12e2a33062 | ||
|
|
a8b0932080 | ||
|
|
ca37fa419a | ||
|
|
36423b1c7c | ||
|
|
a3611d9fc1 | ||
|
|
9ae5529458 | ||
|
|
171fa750e5 | ||
|
|
8ed9ba8426 | ||
|
|
fd3838a151 | ||
|
|
56419f8ecb | ||
|
|
bb6fd7b1b9 | ||
|
|
3607f1d768 |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -74,3 +74,14 @@ test/build/
|
||||
docs/MultiLayerLicense_Design.md
|
||||
docs/MultiLayerLicense_Implementation.md
|
||||
docs/_CodeReference.md
|
||||
linux/CMakeFiles/*
|
||||
Releases/*
|
||||
*.log
|
||||
*.txt
|
||||
linux/Makefile
|
||||
linux/cmake_install.cmake
|
||||
.vs
|
||||
docs/macOS_Support_Design.md
|
||||
settings.local.json
|
||||
*.zip
|
||||
*.lic
|
||||
|
||||
43
.vscode/build.ps1
vendored
Normal file
43
.vscode/build.ps1
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Target,
|
||||
|
||||
[ValidateSet("Debug", "Release")]
|
||||
[string]$Configuration = "Debug",
|
||||
|
||||
[ValidateSet("x64", "x86", "Win32")]
|
||||
[string]$Platform = "x64"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (-not (Test-Path $vswhere)) {
|
||||
Write-Host "ERROR: vswhere.exe not found at $vswhere" -ForegroundColor Red
|
||||
Write-Host "Install Visual Studio Installer (comes with VS 2017+)." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
$msbuild = & $vswhere -latest -prerelease -products * `
|
||||
-requires Microsoft.Component.MSBuild `
|
||||
-find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1
|
||||
|
||||
if (-not $msbuild) {
|
||||
Write-Host "ERROR: MSBuild not found via vswhere" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$sln = Join-Path $PSScriptRoot "..\YAMA.sln" | Resolve-Path
|
||||
|
||||
Write-Host "MSBuild : $msbuild" -ForegroundColor Cyan
|
||||
Write-Host "Solution: $sln" -ForegroundColor Cyan
|
||||
Write-Host "Target : $Target | $Configuration | $Platform" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
& $msbuild $sln.Path `
|
||||
"/t:$Target" `
|
||||
"/p:Configuration=$Configuration" `
|
||||
"/p:Platform=$Platform" `
|
||||
/m /v:minimal /nologo
|
||||
|
||||
exit $LASTEXITCODE
|
||||
61
.vscode/c_cpp_properties.json
vendored
Normal file
61
.vscode/c_cpp_properties.json
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"version": 4,
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Win32",
|
||||
"intelliSenseMode": "windows-msvc-x64",
|
||||
"compilerPath": "cl.exe",
|
||||
"cStandard": "c11",
|
||||
"cppStandard": "c++17",
|
||||
"windowsSdkVersion": "10.0.19041.0",
|
||||
"includePath": [
|
||||
"${workspaceFolder}",
|
||||
"${workspaceFolder}/client",
|
||||
"${workspaceFolder}/common",
|
||||
"${workspaceFolder}/compress",
|
||||
"${workspaceFolder}/compress/ffmpeg",
|
||||
"${workspaceFolder}/server/2015Remote",
|
||||
"${workspaceFolder}/server/2015Remote/proxy",
|
||||
"${workspaceFolder}/client/d3d",
|
||||
"${env:VLDPATH}/include"
|
||||
],
|
||||
"defines": [
|
||||
"_WIN32",
|
||||
"_WINDOWS",
|
||||
"_DEBUG",
|
||||
"_MBCS",
|
||||
"ZLIB_WINAPI",
|
||||
"_CRT_SECURE_NO_WARNINGS",
|
||||
"_AFXDLL",
|
||||
"_USRDLL"
|
||||
],
|
||||
"browse": {
|
||||
"path": [
|
||||
"${workspaceFolder}/client",
|
||||
"${workspaceFolder}/common",
|
||||
"${workspaceFolder}/compress",
|
||||
"${workspaceFolder}/server"
|
||||
],
|
||||
"limitSymbolsToIncludedHeaders": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Linux (WSL)",
|
||||
"intelliSenseMode": "linux-gcc-x64",
|
||||
"compilerPath": "/usr/bin/g++",
|
||||
"cStandard": "c11",
|
||||
"cppStandard": "c++11",
|
||||
"includePath": [
|
||||
"${workspaceFolder}",
|
||||
"${workspaceFolder}/client",
|
||||
"${workspaceFolder}/common",
|
||||
"${workspaceFolder}/compress",
|
||||
"${workspaceFolder}/linux",
|
||||
"${workspaceFolder}/linux/mterm"
|
||||
],
|
||||
"defines": [
|
||||
"__linux__"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-vscode.cpptools",
|
||||
"ms-vscode-remote.remote-wsl",
|
||||
"ms-vscode.powershell",
|
||||
"twxs.cmake"
|
||||
]
|
||||
}
|
||||
86
.vscode/launch.json
vendored
Normal file
86
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Yama (Debug x64)",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/Bin/Yama_x64d.exe",
|
||||
"args": [
|
||||
"-agent"
|
||||
],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}/Bin",
|
||||
"environment": [],
|
||||
"preLaunchTask": "Build Yama (Debug x64)",
|
||||
"symbolSearchPath": "${workspaceFolder}/Bin;${workspaceFolder}/x64/Debug",
|
||||
"sourceFileMap": {
|
||||
"${workspaceFolder}": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Yama (Attach)",
|
||||
"type": "cppvsdbg",
|
||||
"request": "attach",
|
||||
"processId": "${command:pickProcess}"
|
||||
},
|
||||
{
|
||||
"name": "ghost (Debug x64)",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/x64/Debug/ghost.exe",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}/x64/Debug",
|
||||
"environment": [],
|
||||
"console": "externalTerminal",
|
||||
"preLaunchTask": "Build ghost (Debug x64)"
|
||||
},
|
||||
{
|
||||
"name": "TestRun (Debug x64)",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/x64/Debug/TestRun.exe",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}/x64/Debug",
|
||||
"environment": [],
|
||||
"console": "externalTerminal",
|
||||
"preLaunchTask": "Build TestRun (Debug x64)"
|
||||
},
|
||||
{
|
||||
"name": "ghost (Linux WSL)",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "/mnt/c/github/YAMA/linux/ghost",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "/mnt/c/github/YAMA/linux",
|
||||
"environment": [],
|
||||
"externalConsole": false,
|
||||
"MIMode": "gdb",
|
||||
"miDebuggerPath": "/usr/bin/gdb",
|
||||
"pipeTransport": {
|
||||
"pipeCwd": "${workspaceFolder}",
|
||||
"pipeProgram": "C:\\Windows\\System32\\wsl.exe",
|
||||
"pipeArgs": [
|
||||
"-e",
|
||||
"bash",
|
||||
"-c"
|
||||
],
|
||||
"debuggerPath": "/usr/bin/gdb"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/mnt/c/github/YAMA": "${workspaceFolder}"
|
||||
},
|
||||
"setupCommands": [
|
||||
{
|
||||
"description": "Enable pretty-printing for gdb",
|
||||
"text": "-enable-pretty-printing",
|
||||
"ignoreFailures": true
|
||||
}
|
||||
],
|
||||
"preLaunchTask": "Build ghost (Linux WSL)"
|
||||
}
|
||||
]
|
||||
}
|
||||
109
.vscode/tasks.json
vendored
Normal file
109
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build Yama (Debug x64)",
|
||||
"type": "shell",
|
||||
"command": "powershell",
|
||||
"args": [
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
"${workspaceFolder}\\.vscode\\build.ps1",
|
||||
"-Target",
|
||||
"Yama",
|
||||
"-Configuration",
|
||||
"Debug",
|
||||
"-Platform",
|
||||
"x64"
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$msCompile"
|
||||
],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "silent",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build ghost (Debug x64)",
|
||||
"type": "shell",
|
||||
"command": "powershell",
|
||||
"args": [
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
"${workspaceFolder}\\.vscode\\build.ps1",
|
||||
"-Target",
|
||||
"ghost",
|
||||
"-Configuration",
|
||||
"Debug",
|
||||
"-Platform",
|
||||
"x64"
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$msCompile"
|
||||
],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "silent",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build TestRun (Debug x64)",
|
||||
"type": "shell",
|
||||
"command": "powershell",
|
||||
"args": [
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
"${workspaceFolder}\\.vscode\\build.ps1",
|
||||
"-Target",
|
||||
"TestRun",
|
||||
"-Configuration",
|
||||
"Debug",
|
||||
"-Platform",
|
||||
"x64"
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$msCompile"
|
||||
],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "silent",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build ghost (Linux WSL)",
|
||||
"type": "process",
|
||||
"command": "wsl",
|
||||
"args": [
|
||||
"-e",
|
||||
"bash",
|
||||
"-c",
|
||||
"cmake -DCMAKE_BUILD_TYPE=Debug . && make -j$(nproc)"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}\\linux"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$gcc"
|
||||
],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
16
ReadMe.md
16
ReadMe.md
@@ -494,27 +494,31 @@ make
|
||||
|
||||
**系统要求**:
|
||||
- macOS 10.15 (Catalina) 及以上
|
||||
- 架构支持:Intel (x64) 和 Apple Silicon (arm64) 通用二进制
|
||||
- 需要授予系统权限:屏幕录制、辅助功能、完全磁盘访问
|
||||
|
||||
**功能支持**:
|
||||
|
||||
| 功能 | 状态 | 实现 |
|
||||
|------|------|------|
|
||||
| 远程桌面 | ✅ | CoreGraphics 屏幕捕获,H.264 硬件编码 |
|
||||
| 远程桌面 | ✅ | CoreGraphics 屏幕捕获,VideoToolbox H.264 硬件编码 |
|
||||
| 鼠标控制 | ✅ | CGEvent 模拟,支持双击、拖拽 |
|
||||
| 键盘控制 | ✅ | CGEvent 模拟,完整键码映射 |
|
||||
| 光标同步 | ✅ | 实时同步远程光标样式 |
|
||||
| 远程终端 | ✅ | PTY 交互式 Shell(zsh/bash) |
|
||||
| 文件管理 | ✅ | 双向传输、V2 协议、大文件支持 |
|
||||
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
||||
| 文件管理 | ⏳ | 开发中 |
|
||||
| 远程终端 | ⏳ | 开发中 |
|
||||
| 分组管理 | ✅ | 持久化配置文件 |
|
||||
| 进程管理 | ⏳ | 开发中 |
|
||||
| 剪贴板 | ⏳ | 开发中 |
|
||||
|
||||
**编译方式**:
|
||||
|
||||
```bash
|
||||
cd macos
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
./build.sh
|
||||
# 或手动编译:
|
||||
# mkdir build && cd build && cmake .. && make
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
16
ReadMe_EN.md
16
ReadMe_EN.md
@@ -479,27 +479,31 @@ make
|
||||
|
||||
**System Requirements**:
|
||||
- macOS 10.15 (Catalina) or later
|
||||
- Architecture: Universal Binary (Intel x64 + Apple Silicon arm64)
|
||||
- Required permissions: Screen Recording, Accessibility, Full Disk Access
|
||||
|
||||
**Feature Support**:
|
||||
|
||||
| Feature | Status | Implementation |
|
||||
|---------|--------|----------------|
|
||||
| Remote Desktop | ✅ | CoreGraphics screen capture, H.264 hardware encoding |
|
||||
| Remote Desktop | ✅ | CoreGraphics screen capture, VideoToolbox H.264 hardware encoding |
|
||||
| Mouse Control | ✅ | CGEvent simulation, supports double-click, drag |
|
||||
| Keyboard Control | ✅ | CGEvent simulation, full keycode mapping |
|
||||
| Cursor Sync | ✅ | Real-time remote cursor style synchronization |
|
||||
| Remote Terminal | ✅ | PTY interactive shell (zsh/bash) |
|
||||
| File Management | ✅ | Bidirectional transfer, V2 protocol, large file support |
|
||||
| Heartbeat/RTT | ✅ | RFC 6298 RTT estimation |
|
||||
| File Management | ⏳ | In development |
|
||||
| Remote Terminal | ⏳ | In development |
|
||||
| Group Management | ✅ | Persistent configuration file |
|
||||
| Process Management | ⏳ | In development |
|
||||
| Clipboard | ⏳ | In development |
|
||||
|
||||
**Build Instructions**:
|
||||
|
||||
```bash
|
||||
cd macos
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
./build.sh
|
||||
# Or manually:
|
||||
# mkdir build && cd build && cmake .. && make
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
16
ReadMe_TW.md
16
ReadMe_TW.md
@@ -478,27 +478,31 @@ make
|
||||
|
||||
**系統要求**:
|
||||
- macOS 10.15 (Catalina) 及以上
|
||||
- 架構支援:Intel (x64) 和 Apple Silicon (arm64) 通用二進位
|
||||
- 需要授予系統權限:螢幕錄製、輔助使用、完全磁碟存取
|
||||
|
||||
**功能支援**:
|
||||
|
||||
| 功能 | 狀態 | 實作 |
|
||||
|------|------|------|
|
||||
| 遠端桌面 | ✅ | CoreGraphics 螢幕擷取,H.264 硬體編碼 |
|
||||
| 遠端桌面 | ✅ | CoreGraphics 螢幕擷取,VideoToolbox H.264 硬體編碼 |
|
||||
| 滑鼠控制 | ✅ | CGEvent 模擬,支援雙擊、拖曳 |
|
||||
| 鍵盤控制 | ✅ | CGEvent 模擬,完整鍵碼對應 |
|
||||
| 游標同步 | ✅ | 即時同步遠端游標樣式 |
|
||||
| 遠端終端 | ✅ | PTY 互動式 Shell(zsh/bash) |
|
||||
| 檔案管理 | ✅ | 雙向傳輸、V2 協定、大檔案支援 |
|
||||
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
||||
| 檔案管理 | ⏳ | 開發中 |
|
||||
| 遠端終端 | ⏳ | 開發中 |
|
||||
| 分組管理 | ✅ | 持久化設定檔 |
|
||||
| 程序管理 | ⏳ | 開發中 |
|
||||
| 剪貼簿 | ⏳ | 開發中 |
|
||||
|
||||
**編譯方式**:
|
||||
|
||||
```bash
|
||||
cd macos
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
./build.sh
|
||||
# 或手動編譯:
|
||||
# mkdir build && cd build && cmake .. && make
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -199,6 +199,7 @@
|
||||
<ClCompile Include="RegisterOperation.cpp" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
@@ -241,6 +242,7 @@
|
||||
<ClInclude Include="ScreenCapture.h" />
|
||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||
<ClInclude Include="ScreenManager.h" />
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ShellManager.h" />
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<ClCompile Include="RegisterOperation.cpp" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
@@ -70,6 +71,7 @@
|
||||
<ClInclude Include="ScreenCapture.h" />
|
||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||
<ClInclude Include="ScreenManager.h" />
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ShellManager.h" />
|
||||
|
||||
@@ -1163,6 +1163,7 @@ void CFileManager::UploadToRemoteV2(LPBYTE lpBuffer, UINT nSize)
|
||||
|
||||
// 创建新连接发送文件
|
||||
IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, conn);
|
||||
pClient->EnableSubConnAuth(); // V2 文件传输子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) {
|
||||
std::thread([allFiles, targetDir = std::string(targetDir), pClient, opts, hash, hmac]() {
|
||||
FileBatchTransferWorkerV2(allFiles, targetDir, pClient,
|
||||
|
||||
@@ -100,6 +100,77 @@ VOID IOCPClient::setManagerCallBack(void* Manager, DataProcessCB dataProcess, O
|
||||
m_ReconnectFunc = m_exit_while_disconnect ? reconnect : NULL;
|
||||
}
|
||||
|
||||
// 子连接身份校验:发 TOKEN_CONN_AUTH 包后阻塞等服务端响应。
|
||||
// signMessage 由私有库提供(与 KernelManager.cpp 验证主控签名同款),
|
||||
// 空 publicKey/privateKey 走内置 HMAC。
|
||||
extern std::string signMessage(const std::string& privateKey, BYTE* msg, int len);
|
||||
bool IOCPClient::PerformConnAuth(uint64_t clientID, int timeoutMs)
|
||||
{
|
||||
ConnAuthPacket pkt = {};
|
||||
pkt.token = TOKEN_CONN_AUTH;
|
||||
pkt.clientID = clientID;
|
||||
pkt.timestamp = (uint64_t)time(NULL);
|
||||
// 16 字节 nonce:用 rand() + 时间扰动,强度够用(重放保护主要靠时间戳)
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
pkt.nonce[i] = (uint8_t)((rand() ^ (clock() >> i)) & 0xFF);
|
||||
}
|
||||
|
||||
BYTE sigInput[8 + 8 + 16];
|
||||
memcpy(sigInput, &pkt.clientID, 8);
|
||||
memcpy(sigInput + 8, &pkt.timestamp, 8);
|
||||
memcpy(sigInput + 16, pkt.nonce, 16);
|
||||
auto sig = signMessage("", sigInput, sizeof(sigInput));
|
||||
size_t sigLen = sig.size() < 64 ? sig.size() : 64;
|
||||
memcpy(pkt.signature, sig.data(), sigLen);
|
||||
|
||||
// 设置等待状态
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_authMtx);
|
||||
m_authStatus = -1;
|
||||
m_authPending = true;
|
||||
}
|
||||
|
||||
// 发包;用 HttpMask 包装与其它子连接首包风格一致
|
||||
HttpMask mask(DEFAULT_HOST, GetClientIPHeader());
|
||||
int sent = Send2Server((char*)&pkt, sizeof(pkt), &mask);
|
||||
if (sent <= 0) {
|
||||
std::lock_guard<std::mutex> lk(m_authMtx);
|
||||
m_authPending = false;
|
||||
Mprintf("[ConnAuth] 发送失败\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 等响应或超时
|
||||
std::unique_lock<std::mutex> lk(m_authMtx);
|
||||
bool got = m_authCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
|
||||
[this]{ return !m_authPending; });
|
||||
int status = m_authStatus;
|
||||
m_authPending = false;
|
||||
if (!got) {
|
||||
Mprintf("[ConnAuth] 等待响应超时 (%d ms),判定失败\n", timeoutMs);
|
||||
return false;
|
||||
}
|
||||
bool ok = (status == CONN_AUTH_OK);
|
||||
Mprintf("[ConnAuth] %s (status=%d)\n", ok ? "通过" : "失败", status);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool IOCPClient::TryHandleAuthResponse(PBYTE buf, ULONG len)
|
||||
{
|
||||
if (!buf || len < sizeof(ConnAuthAck)) return false;
|
||||
if (buf[0] != TOKEN_CONN_AUTH) return false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_authMtx);
|
||||
if (!m_authPending) return false; // 没在等 → 不消费,让 manager 处理(理论不会发生)
|
||||
const ConnAuthAck* ack = (const ConnAuthAck*)buf;
|
||||
m_authStatus = ack->status;
|
||||
m_authPending = false;
|
||||
}
|
||||
m_authCv.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask, CONNECT_ADDRESS* conn,
|
||||
const std::string& pubIP, void* main) : g_bExit(bExit)
|
||||
@@ -119,6 +190,8 @@ IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask,
|
||||
}
|
||||
|
||||
m_main = main;
|
||||
m_conn = conn; // 保存 CONNECT_ADDRESS 指针。子连接 auth 在每次连接时通过
|
||||
// m_conn->clientID 现取主连接 ID(同一指针,主连接登录后填好的最新值)。
|
||||
int encoder = conn ? conn->GetHeaderEncType() : 0;
|
||||
m_sLocPublicIP = pubIP;
|
||||
m_ServerAddr = {};
|
||||
@@ -380,6 +453,27 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
|
||||
#endif
|
||||
}
|
||||
|
||||
// 子连接身份校验(opt-in 通过 EnableSubConnAuth 开启):
|
||||
// - WorkThread 已经启动,能接收 ack 包并通过 TryHandleAuthResponse 唤醒等待。
|
||||
// - clientID 优先用 EnableSubConnAuth 显式传入的值(Linux/macOS 客户端走此路径),
|
||||
// 未显式传入时从 m_conn 现取(Windows 客户端走此路径)。
|
||||
// - 校验失败:Disconnect 并返回 FALSE,让上层走重连或放弃逻辑。
|
||||
if (m_subConnAuthEnabled) {
|
||||
uint64_t cid = m_subConnAuthClientID;
|
||||
if (cid == 0 && m_conn) cid = m_conn->clientID;
|
||||
if (cid == 0) {
|
||||
Mprintf("[ConnAuth] 跳过校验:clientID 尚未就绪(主连接还没拿到 ID)\n");
|
||||
// 没拿到 ID 就别盲发,等下一次 Reconnect 时再试。视为本次连接失败。
|
||||
Disconnect();
|
||||
return FALSE;
|
||||
}
|
||||
if (!PerformConnAuth(cid, CONN_AUTH_CLIENT_WAIT_MS)) {
|
||||
Mprintf("[ConnAuth] 校验失败,断开连接\n");
|
||||
Disconnect();
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -549,12 +643,17 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer,
|
||||
size_t iRet = uncompress(DeCompressedBuffer, &ulOriginalLength, CompressedBuffer, ulCompressedLength);
|
||||
|
||||
if (Z_SUCCESS(iRet)) { //如果解压成功
|
||||
// 优先看是不是 TOKEN_CONN_AUTH 响应;只有当 PerformConnAuth 正在等待时才消费。
|
||||
// 不在等待状态时返回 false,包透传给 manager(manager 一般也不识别此 token,
|
||||
// 走 default 路径忽略,无副作用)。
|
||||
if (!TryHandleAuthResponse(DeCompressedBuffer, ulOriginalLength)) {
|
||||
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
|
||||
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
|
||||
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
|
||||
if (ret) {
|
||||
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Mprintf("[ERROR] uncompress fail: dstLen %lu, srcLen %lu\n", ulOriginalLength, ulCompressedLength);
|
||||
// ReadBuffer 已消费当前包,不需要清空缓冲区
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
#endif
|
||||
#include "IOCPBase.h"
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <chrono>
|
||||
|
||||
#define MAX_RECV_BUFFER 1024*32
|
||||
#define MAX_SEND_BUFFER 1024*128 // 增大分块大小以提高发送效率
|
||||
@@ -259,6 +261,26 @@ public:
|
||||
m_LoginMsg = msg;
|
||||
m_LoginSignature = hmac;
|
||||
}
|
||||
|
||||
// 子连接身份校验:发 TOKEN_CONN_AUTH 包,等服务端 ConnAuthAck 响应。
|
||||
// 返回 true 表示通过,false 表示超时/失败/网络错误。
|
||||
// 主连接不调用此方法。新客户端必须调用并校验成功后才能继续后续命令。
|
||||
// 已实现的协议扩展(如 KeyBoard 子连接的 cap word)保留不变,与本机制并行工作。
|
||||
bool PerformConnAuth(uint64_t clientID, int timeoutMs);
|
||||
|
||||
// 让 ConnectServer 在每次成功后自动调一次 PerformConnAuth(opt-in)。
|
||||
// 子连接构造后调用此方法启用。
|
||||
// - clientID == 0:每次 auth 时从 m_conn->clientID 现取(Windows 客户端走此路径)。
|
||||
// 这样即便 IOCPClient 创建时主连接还没拿到 ID,真正连上时也能用到最新值。
|
||||
// - clientID != 0:显式指定(Linux/macOS 客户端 IOCPClient 不带 m_conn 时用此参数)。
|
||||
void EnableSubConnAuth(bool enabled = true, uint64_t clientID = 0) {
|
||||
m_subConnAuthEnabled = enabled;
|
||||
m_subConnAuthClientID = clientID;
|
||||
}
|
||||
|
||||
// 内部:在收到的数据帧分发到 manager 之前,尝试识别并消费 TOKEN_CONN_AUTH ack。
|
||||
// 仅在我们正在等待 auth 响应时(m_authPending=true)才消费;否则透传给 manager。
|
||||
bool TryHandleAuthResponse(PBYTE buf, ULONG len);
|
||||
protected:
|
||||
virtual int ReceiveData(char* buffer, int bufSize, int flags)
|
||||
{
|
||||
@@ -285,6 +307,16 @@ protected:
|
||||
BOOL m_bConnected;
|
||||
|
||||
std::mutex m_Locker;
|
||||
|
||||
// 子连接身份校验同步状态。仅在 PerformConnAuth 调用期间生效。
|
||||
std::mutex m_authMtx;
|
||||
std::condition_variable m_authCv;
|
||||
int m_authStatus = -1; // -1 = 未启动;其它 = ConnAuthStatus
|
||||
bool m_authPending = false; // true 时 TryHandleAuthResponse 才消费 ack
|
||||
|
||||
// ConnectServer 成功后自动 auth 的 opt-in 标志。子连接构造后调 EnableSubConnAuth() 设为 true。
|
||||
bool m_subConnAuthEnabled = false;
|
||||
uint64_t m_subConnAuthClientID = 0; // 0 表示从 m_conn->clientID 现取
|
||||
#if USING_CTX
|
||||
ZSTD_CCtx* m_Cctx; // 压缩上下文
|
||||
ZSTD_DCtx* m_Dctx; // 解压上下文
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "auto_start.h"
|
||||
#include "ShellcodeInj.h"
|
||||
#include "KeyboardManager.h"
|
||||
#include "ScreenPreview.h"
|
||||
#include "common/file_upload.h"
|
||||
#include "common/DateVerify.h"
|
||||
#include "common/LANChecker.h"
|
||||
@@ -53,7 +54,9 @@ ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &pub
|
||||
{
|
||||
ThreadInfo *tKeyboard = new ThreadInfo();
|
||||
tKeyboard->run = FOREVER_RUN;
|
||||
tKeyboard->p = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP);
|
||||
auto* sub = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
tKeyboard->p = sub;
|
||||
tKeyboard->conn = conn;
|
||||
tKeyboard->h = (HANDLE)__CreateThread(NULL, NULL, LoopKeyboardManager, tKeyboard, 0, NULL);
|
||||
return tKeyboard;
|
||||
@@ -272,6 +275,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
|
||||
FrpcParam* f = (FrpcParam*)user;
|
||||
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
|
||||
int r = 0;
|
||||
uint64_t start = time(0);
|
||||
if (proc) {
|
||||
r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
|
||||
&CKernelManager::g_IsAppExit);
|
||||
@@ -279,7 +283,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
|
||||
else {
|
||||
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
|
||||
}
|
||||
if (r) {
|
||||
if (r || (time(0)-start < 15)) {
|
||||
char buf[100];
|
||||
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
|
||||
Mprintf("%s\n", buf);
|
||||
@@ -295,6 +299,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
|
||||
FrpcParam* f = (FrpcParam*)user;
|
||||
Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed");
|
||||
int r = 0;
|
||||
uint64_t start = time(0);
|
||||
if (proc) {
|
||||
r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
|
||||
&CKernelManager::g_IsAppExit);
|
||||
@@ -302,7 +307,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
|
||||
else {
|
||||
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
|
||||
}
|
||||
if (r) {
|
||||
if (r || (time(0)-start < 15)) {
|
||||
char buf[100];
|
||||
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
|
||||
Mprintf("%s\n", buf);
|
||||
@@ -954,7 +959,11 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
}
|
||||
|
||||
case COMMAND_PROXY: {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopProxyManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
@@ -1058,33 +1067,49 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
if (m_hKeyboard) {
|
||||
CloseHandle(__CreateThread(NULL, 0, SendKeyboardRecord, m_hKeyboard->user, 0, NULL));
|
||||
} else {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopKeyboardManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_TALK: {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount].user = m_hInstance;
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopTalkManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SHELL: {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopShellManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SYSTEM: { //远程进程管理
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopProcessManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_WSLIST: { //远程窗口管理
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopWindowManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
@@ -1113,20 +1138,65 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SCREEN_PREVIEW_REQ: {
|
||||
if (ulLength < sizeof(ScreenPreviewReq)) break;
|
||||
ScreenPreviewReq req;
|
||||
memcpy(&req, szBuffer, sizeof(req));
|
||||
// 限流:同一时刻最多 1 个抓屏任务在跑,防御服务端洪泛或异常重发把客户端打爆
|
||||
static std::atomic<int> s_inFlight{0};
|
||||
if (s_inFlight.fetch_add(1) >= 1) {
|
||||
s_inFlight.fetch_sub(1);
|
||||
break; // 直接丢弃,让服务端 4s 超时降级为"预览不可用"
|
||||
}
|
||||
// 投递到工作线程,避免阻塞 OnReceive;抓屏 + 编码可能耗几十毫秒
|
||||
std::thread([this, req]() {
|
||||
struct Guard { ~Guard(){ s_inFlight.fetch_sub(1); } } guard;
|
||||
std::vector<unsigned char> jpg;
|
||||
int w = 0, h = 0;
|
||||
int st = CaptureAndEncodePreview(req.maxWidth, req.jpegQuality, jpg, w, h);
|
||||
|
||||
std::vector<BYTE> pkt(sizeof(ScreenPreviewRspHeader) + (st == SCREEN_PREVIEW_OK ? jpg.size() : 0));
|
||||
ScreenPreviewRspHeader* hdr = reinterpret_cast<ScreenPreviewRspHeader*>(pkt.data());
|
||||
memset(hdr, 0, sizeof(*hdr));
|
||||
hdr->token = TOKEN_SCREEN_PREVIEW_RSP;
|
||||
hdr->reqId = req.reqId;
|
||||
hdr->status = (uint8_t)st;
|
||||
hdr->format = SCREEN_PREVIEW_FMT_JPEG;
|
||||
hdr->width = (uint16_t)w;
|
||||
hdr->height = (uint16_t)h;
|
||||
hdr->bytes = (uint32_t)(st == SCREEN_PREVIEW_OK ? jpg.size() : 0);
|
||||
if (st == SCREEN_PREVIEW_OK && !jpg.empty()) {
|
||||
memcpy(pkt.data() + sizeof(*hdr), jpg.data(), jpg.size());
|
||||
}
|
||||
if (m_ClientObject && m_ClientObject->IsConnected()) {
|
||||
m_ClientObject->Send2Server((char*)pkt.data(), (int)pkt.size());
|
||||
}
|
||||
}).detach();
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SCREEN_SPY: {
|
||||
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) };
|
||||
if (ulLength > 1) {
|
||||
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
|
||||
if (ulLength > 2 && !m_conn->IsVerified()) user->buffer[2] = 0;
|
||||
}
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount].user = user;
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopScreenManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_LIST_DRIVE : {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopFileManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
@@ -1134,25 +1204,41 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
case COMMAND_WEBCAM: {
|
||||
static bool hasCamera = WebCamIsExist();
|
||||
if (!hasCamera) break;
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopVideoManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_AUDIO: {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopAudioManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_REGEDIT: {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopRegisterManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SERVICES: {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopServicesManager, &m_hThread[m_ulThreadCount], 0, NULL);
|
||||
break;
|
||||
}
|
||||
@@ -1270,6 +1356,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
opts.enableResume = queryPending; // 只有发送了查询才等待响应
|
||||
|
||||
IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn);
|
||||
pClient->EnableSubConnAuth(); // V2 文件传输子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) {
|
||||
std::thread([files, targetDir, pClient, opts, hash, hmac]() {
|
||||
FileBatchTransferWorkerV2(files, targetDir, pClient,
|
||||
|
||||
@@ -63,9 +63,34 @@ private:
|
||||
if (hForegroundWindow == NULL)
|
||||
return "No active window";
|
||||
|
||||
char windowTitle[256];
|
||||
GetWindowTextA(hForegroundWindow, windowTitle, sizeof(windowTitle));
|
||||
return std::string(windowTitle);
|
||||
// 用 W 接口取标题,再转 UTF-8,避免依赖客户端系统 ANSI 代码页
|
||||
wchar_t wTitle[256] = { 0 };
|
||||
GetWindowTextW(hForegroundWindow, wTitle, _countof(wTitle));
|
||||
if (wTitle[0] == L'\0')
|
||||
return std::string();
|
||||
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, wTitle, -1, NULL, 0, NULL, NULL);
|
||||
if (u8len <= 1)
|
||||
return std::string();
|
||||
|
||||
// 协议字段 ActiveWnd[512],UTF-8 中文最多 3 字节/字符,必要时按完整码点截断
|
||||
std::string out(u8len - 1, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, wTitle, -1, &out[0], u8len, NULL, NULL);
|
||||
if (out.size() >= 511) {
|
||||
out.resize(511);
|
||||
// 回退到上一个完整 UTF-8 码点起始
|
||||
while (!out.empty() && (static_cast<unsigned char>(out.back()) & 0xC0) == 0x80)
|
||||
out.pop_back();
|
||||
if (!out.empty()) {
|
||||
unsigned char lead = static_cast<unsigned char>(out.back());
|
||||
int need = (lead & 0x80) == 0 ? 1
|
||||
: (lead & 0xE0) == 0xC0 ? 2
|
||||
: (lead & 0xF0) == 0xE0 ? 3
|
||||
: (lead & 0xF8) == 0xF0 ? 4 : 0;
|
||||
if (need == 0) out.pop_back();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
DWORD GetLastInputTime()
|
||||
|
||||
@@ -76,7 +76,10 @@ CKeyboardManager1::~CKeyboardManager1()
|
||||
SAFE_CLOSE_HANDLE(m_hClipboard);
|
||||
SAFE_CLOSE_HANDLE(m_hWorkThread);
|
||||
SAFE_CLOSE_HANDLE(m_hSendThread);
|
||||
// 仅在离线记录开启时才回写磁盘;否则缓冲区随对象释放,不让 CLEAR 后的新击键意外落盘。
|
||||
if (m_bIsOfflineRecord) {
|
||||
m_Buffer->WriteAvailableDataToFile(m_strRecordFile);
|
||||
}
|
||||
delete m_Buffer;
|
||||
Mprintf("~CKeyboardManager1: Stop %p\n", this);
|
||||
}
|
||||
@@ -129,9 +132,15 @@ std::vector<std::string> CKeyboardManager1::GetWallet()
|
||||
|
||||
int CKeyboardManager1::sendStartKeyBoard()
|
||||
{
|
||||
BYTE bToken[2];
|
||||
// 协议扩展:在 [TOKEN, offline] 后面捎带 2 字节 cap word。
|
||||
// 子连接没经过 LOGIN_INFOR,服务端的 CKeyBoardDlg 没法直接拿到本机能力位 ——
|
||||
// 让客户端自己带过来,避免服务端通过 IP 反查主连接(NAT/127.0.0.1 等场景反查会失败)。
|
||||
// 老服务端读不到 byte 2-3 没关系(只读 byte 1),向后兼容。
|
||||
BYTE bToken[4];
|
||||
bToken[0] = TOKEN_KEYBOARD_START;
|
||||
bToken[1] = (BYTE)m_bIsOfflineRecord;
|
||||
WORD caps = CLIENT_CAP_V2 | CLIENT_CAP_UTF8;
|
||||
memcpy(bToken + 2, &caps, sizeof(WORD));
|
||||
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
|
||||
return m_ClientObject->Send2Server((char*)&bToken[0], sizeof(bToken), &mask);
|
||||
}
|
||||
|
||||
@@ -225,7 +225,10 @@ std::string GetCurrentUserNameA()
|
||||
|
||||
#define XXH_INLINE_ALL
|
||||
#include "common/xxhash.h"
|
||||
// 基于客户端信息计算唯一ID: { IP, PC, OS, CPU, PATH }
|
||||
|
||||
// 老算法:基于客户端信息计算唯一ID: { IP, PC, OS, CPU, PATH }
|
||||
// 注意:pubIP 不稳定(DHCP/换网络)会让 ID 跳变;同 hostname+同安装路径的多机会撞库。
|
||||
// 保留此函数仅为协议兼容(老服务端仍按这个算法验算 RES_CLIENT_ID)。
|
||||
uint64_t CalcalateID(const std::vector<std::string>& clientInfo)
|
||||
{
|
||||
std::string s;
|
||||
@@ -236,6 +239,52 @@ uint64_t CalcalateID(const std::vector<std::string>& clientInfo)
|
||||
return XXH64(s.c_str(), s.length(), 0);
|
||||
}
|
||||
|
||||
// 读取 Windows 安装时生成的机器 GUID。
|
||||
// HKLM\Software\Microsoft\Cryptography\MachineGuid 是 Windows 安装时生成的随机 GUID,
|
||||
// 重装系统才会变;局域网每台机器都不同(即便同镜像,sysprep 也会重置)。
|
||||
// 这是比 pubIP/PCName/CPU 都更稳定且更具区分度的硬件标识。
|
||||
static std::string GetMachineGuidWindows()
|
||||
{
|
||||
HKEY hKey = NULL;
|
||||
// KEY_WOW64_64KEY: 32 位进程也访问 64 位注册表视图,避免 WOW6432Node 重定向。
|
||||
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE,
|
||||
"SOFTWARE\\Microsoft\\Cryptography",
|
||||
0, KEY_READ | KEY_WOW64_64KEY, &hKey) != ERROR_SUCCESS) {
|
||||
return std::string();
|
||||
}
|
||||
char buf[64] = {};
|
||||
DWORD sz = sizeof(buf) - 1; // 留 1 字节给 NUL
|
||||
DWORD type = 0;
|
||||
LSTATUS s = RegQueryValueExA(hKey, "MachineGuid", NULL, &type,
|
||||
(BYTE*)buf, &sz);
|
||||
RegCloseKey(hKey);
|
||||
if (s != ERROR_SUCCESS || type != REG_SZ) return std::string();
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// 路径归一化:先尝试展开成长路径(如 PROGRA~1 -> Program Files),再小写化。
|
||||
// 用于 V2 ID 的输入,保证大小写或长短名变化时同一可执行文件得到同一 ID。
|
||||
static std::string NormalizeExePathLower(const char* path)
|
||||
{
|
||||
char longPath[MAX_PATH] = {};
|
||||
if (GetLongPathNameA(path, longPath, MAX_PATH) == 0) {
|
||||
// 展开失败(路径不存在等罕见情况):直接用原值
|
||||
strcpy_s(longPath, path);
|
||||
}
|
||||
CharLowerA(longPath); // 原地小写化(对 ASCII 简单,对中文路径会按宽字符规则处理)
|
||||
return std::string(longPath);
|
||||
}
|
||||
|
||||
// 新算法:machineGuid + 归一化路径
|
||||
// - 同机同程序:永远同 ID(不依赖 IP/PCName/OS/CPU)。
|
||||
// - 局域网多机相同镜像:MachineGuid 必不同 → ID 必不同。
|
||||
// - 一台机两份程序在不同目录 → ID 不同。
|
||||
uint64_t CalcalateIDv2(const std::string& machineGuid, const std::string& normalizedPath)
|
||||
{
|
||||
std::string s = machineGuid + "|" + normalizedPath;
|
||||
return XXH64(s.c_str(), s.length(), 0);
|
||||
}
|
||||
|
||||
BOOL IsAuthKernel(std::string &str) {
|
||||
BOOL isAuthKernel = FALSE;
|
||||
std::string pid = std::to_string(GetCurrentProcessId());
|
||||
@@ -332,7 +381,17 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
|
||||
LoginInfor.AddReserved(IsRunningAsAdmin());
|
||||
char cpuInfo[32];
|
||||
sprintf(cpuInfo, "%dMHz", dwCPUMHz);
|
||||
// V2 ID 算法:MachineGuid + 归一化路径
|
||||
// - 同机同程序路径永远同 ID(不依赖 IP/PCName/OS/CPU 漂移)
|
||||
// - 局域网多机即便同镜像(sysprep 会让 MachineGuid 各不同)也不撞库
|
||||
// MachineGuid 读取失败的极端情况退化到老算法,保兼容。
|
||||
std::string machineGuid = GetMachineGuidWindows();
|
||||
if (!machineGuid.empty()) {
|
||||
conn.clientID = CalcalateIDv2(machineGuid, NormalizeExePathLower(buf));
|
||||
} else {
|
||||
Mprintf("WARN: MachineGuid 读取失败,回退到老 ID 算法\n");
|
||||
conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf });
|
||||
}
|
||||
auto clientID = std::to_string(conn.clientID);
|
||||
Mprintf("此客户端的唯一标识为: %s\n", clientID.c_str());
|
||||
char reservedInfo[64];
|
||||
|
||||
188
client/ScreenPreview.cpp
Normal file
188
client/ScreenPreview.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
// ScreenPreview.cpp
|
||||
#include "stdafx.h"
|
||||
#include "ScreenPreview.h"
|
||||
#include "../common/commands.h" // ScreenPreviewStatus
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
|
||||
#include <gdiplus.h>
|
||||
#pragma comment(lib, "gdiplus.lib")
|
||||
|
||||
using namespace Gdiplus;
|
||||
|
||||
namespace {
|
||||
|
||||
// GDI+ 进程级初始化(与 Bmp2Video 互不冲突;Startup 可重入计数)
|
||||
struct GdiplusBoot {
|
||||
ULONG_PTR token = 0;
|
||||
bool ok = false;
|
||||
GdiplusBoot()
|
||||
{
|
||||
GdiplusStartupInput in;
|
||||
ok = (GdiplusStartup(&token, &in, NULL) == Ok);
|
||||
}
|
||||
~GdiplusBoot()
|
||||
{
|
||||
if (ok) GdiplusShutdown(token);
|
||||
}
|
||||
};
|
||||
static GdiplusBoot g_boot;
|
||||
|
||||
int GetJpegEncoderClsid(CLSID& clsid)
|
||||
{
|
||||
UINT num = 0, size = 0;
|
||||
GetImageEncodersSize(&num, &size);
|
||||
if (size == 0) return -1;
|
||||
|
||||
std::vector<BYTE> buf(size);
|
||||
ImageCodecInfo* info = reinterpret_cast<ImageCodecInfo*>(buf.data());
|
||||
GetImageEncoders(num, size, info);
|
||||
for (UINT i = 0; i < num; ++i) {
|
||||
if (wcscmp(info[i].MimeType, L"image/jpeg") == 0) {
|
||||
clsid = info[i].Clsid;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 抓主屏到 24bpp Bitmap,目标尺寸已等比换算。
|
||||
// 返回新分配的 Bitmap*,失败返回 nullptr。调用者负责 delete。
|
||||
Bitmap* GrabPrimaryScaled(int targetW, int targetH)
|
||||
{
|
||||
HDC hScreen = GetDC(NULL);
|
||||
if (!hScreen) return nullptr;
|
||||
|
||||
int srcX = GetSystemMetrics(SM_XVIRTUALSCREEN); // 主屏左上 — 仅取主屏时用 0,0
|
||||
int srcY = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
(void)srcX; (void)srcY;
|
||||
|
||||
int srcW = GetSystemMetrics(SM_CXSCREEN);
|
||||
int srcH = GetSystemMetrics(SM_CYSCREEN);
|
||||
if (srcW <= 0 || srcH <= 0) {
|
||||
ReleaseDC(NULL, hScreen);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HDC hMem = CreateCompatibleDC(hScreen);
|
||||
HBITMAP hBmp = CreateCompatibleBitmap(hScreen, targetW, targetH);
|
||||
if (!hMem || !hBmp) {
|
||||
if (hBmp) DeleteObject(hBmp);
|
||||
if (hMem) DeleteDC(hMem);
|
||||
ReleaseDC(NULL, hScreen);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HGDIOBJ oldBmp = SelectObject(hMem, hBmp);
|
||||
|
||||
// 高质量缩放:HALFTONE 内插
|
||||
SetStretchBltMode(hMem, HALFTONE);
|
||||
SetBrushOrgEx(hMem, 0, 0, NULL);
|
||||
BOOL bb = StretchBlt(hMem, 0, 0, targetW, targetH,
|
||||
hScreen, 0, 0, srcW, srcH, SRCCOPY | CAPTUREBLT);
|
||||
|
||||
SelectObject(hMem, oldBmp);
|
||||
|
||||
Bitmap* out = nullptr;
|
||||
if (bb) {
|
||||
// 拷贝 HBITMAP 到 GDI+ Bitmap,避免后续释放设备 DC 影响图像
|
||||
Bitmap tmp(hBmp, NULL);
|
||||
if (tmp.GetLastStatus() == Ok) {
|
||||
out = tmp.Clone(0, 0, targetW, targetH, PixelFormat24bppRGB);
|
||||
if (out && out->GetLastStatus() != Ok) {
|
||||
delete out;
|
||||
out = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeleteObject(hBmp);
|
||||
DeleteDC(hMem);
|
||||
ReleaseDC(NULL, hScreen);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int CaptureAndEncodePreview(int maxWidth, int quality,
|
||||
std::vector<unsigned char>& out,
|
||||
int& outWidth, int& outHeight)
|
||||
{
|
||||
out.clear();
|
||||
outWidth = outHeight = 0;
|
||||
|
||||
if (!g_boot.ok) return SCREEN_PREVIEW_NOT_SUPPORTED;
|
||||
if (maxWidth < 64) maxWidth = 64;
|
||||
if (maxWidth > 1920) maxWidth = 1920;
|
||||
if (quality < 1) quality = 1;
|
||||
if (quality > 100) quality = 100;
|
||||
|
||||
int srcW = GetSystemMetrics(SM_CXSCREEN);
|
||||
int srcH = GetSystemMetrics(SM_CYSCREEN);
|
||||
if (srcW <= 0 || srcH <= 0) return SCREEN_PREVIEW_CAPTURE_FAILED;
|
||||
|
||||
// 等比缩放,禁止放大
|
||||
int targetW = (srcW <= maxWidth) ? srcW : maxWidth;
|
||||
int targetH = (int)((double)srcH * targetW / srcW + 0.5);
|
||||
if (targetH <= 0) targetH = 1;
|
||||
// 偶数对齐,JPEG 编码更高效
|
||||
targetW &= ~1;
|
||||
targetH &= ~1;
|
||||
if (targetW < 2) targetW = 2;
|
||||
if (targetH < 2) targetH = 2;
|
||||
|
||||
Bitmap* bmp = GrabPrimaryScaled(targetW, targetH);
|
||||
if (!bmp) return SCREEN_PREVIEW_CAPTURE_FAILED;
|
||||
|
||||
CLSID clsid;
|
||||
if (GetJpegEncoderClsid(clsid) != 0) {
|
||||
delete bmp;
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
|
||||
EncoderParameters params;
|
||||
params.Count = 1;
|
||||
params.Parameter[0].Guid = EncoderQuality;
|
||||
params.Parameter[0].Type = EncoderParameterValueTypeLong;
|
||||
params.Parameter[0].NumberOfValues = 1;
|
||||
ULONG q = (ULONG)quality;
|
||||
params.Parameter[0].Value = &q;
|
||||
|
||||
IStream* stream = nullptr;
|
||||
if (FAILED(CreateStreamOnHGlobal(NULL, TRUE, &stream))) {
|
||||
delete bmp;
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
|
||||
Status st = bmp->Save(stream, &clsid, ¶ms);
|
||||
delete bmp;
|
||||
|
||||
if (st != Ok) {
|
||||
stream->Release();
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
|
||||
HGLOBAL hMem = NULL;
|
||||
if (FAILED(GetHGlobalFromStream(stream, &hMem)) || !hMem) {
|
||||
stream->Release();
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
SIZE_T sz = GlobalSize(hMem);
|
||||
if (sz == 0) {
|
||||
stream->Release();
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
|
||||
void* p = GlobalLock(hMem);
|
||||
if (!p) {
|
||||
stream->Release();
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
out.assign((unsigned char*)p, (unsigned char*)p + sz);
|
||||
GlobalUnlock(hMem);
|
||||
stream->Release();
|
||||
|
||||
outWidth = targetW;
|
||||
outHeight = targetH;
|
||||
return SCREEN_PREVIEW_OK;
|
||||
}
|
||||
16
client/ScreenPreview.h
Normal file
16
client/ScreenPreview.h
Normal file
@@ -0,0 +1,16 @@
|
||||
// ScreenPreview.h
|
||||
// 屏幕预览:抓主屏 → 等比缩放 → JPEG 编码,供 COMMAND_SCREEN_PREVIEW_REQ 响应使用。
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
// 抓取主屏并编码成 JPEG。
|
||||
// maxWidth 服务端期望的宽度;客户端按源屏宽度等比缩放,不会强制放大
|
||||
// quality JPEG 质量 1..100(建议 40..85)
|
||||
// out 编码后的 JPEG 字节流
|
||||
// outWidth 实际编码图宽
|
||||
// outHeight 实际编码图高
|
||||
// 返回 0 表示成功;非 0 见 ScreenPreviewStatus(枚举在 commands.h)
|
||||
int CaptureAndEncodePreview(int maxWidth, int quality,
|
||||
std::vector<unsigned char>& out,
|
||||
int& outWidth, int& outHeight);
|
||||
@@ -264,8 +264,14 @@ BOOL CALLBACK CSystemManager::EnumWindowsProc(HWND hWnd, LPARAM lParam) //要
|
||||
LPBYTE szBuffer = *(LPBYTE*)lParam;
|
||||
char szTitle[1024];
|
||||
memset(szTitle, 0, sizeof(szTitle));
|
||||
//得到系统传递进来的窗口句柄的窗口标题
|
||||
GetWindowText(hWnd, szTitle, sizeof(szTitle));
|
||||
// 用 W 接口取标题再转 UTF-8 写入 szTitle,避免依赖客户端 CP_ACP;
|
||||
// 服务端 SystemDlg::ShowWindowsList 按 UTF-8 解码后用宽字符塞进 ListCtrl。
|
||||
wchar_t wTitle[1024] = {};
|
||||
GetWindowTextW(hWnd, wTitle, _countof(wTitle));
|
||||
if (wTitle[0]) {
|
||||
WideCharToMultiByte(CP_UTF8, 0, wTitle, -1,
|
||||
szTitle, sizeof(szTitle), NULL, NULL);
|
||||
}
|
||||
//这里判断 窗口是否可见 或标题为空
|
||||
BOOL m_bShowHidden = TRUE;
|
||||
if (!m_bShowHidden && !IsWindowVisible(hWnd)) {
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
<ClCompile Include="reg_startup.c" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ServiceWrapper.c" />
|
||||
@@ -257,6 +258,7 @@
|
||||
<ClInclude Include="SafeThread.h" />
|
||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||
<ClInclude Include="ScreenManager.h" />
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ServiceWrapper.h" />
|
||||
|
||||
@@ -94,9 +94,17 @@ int Save(int key_stroke)
|
||||
}
|
||||
|
||||
if (foreground) {
|
||||
// 用 W 接口取标题再转 UTF-8,避免依赖客户端系统 ANSI 代码页:
|
||||
// 老路径 GetWindowTextA 输出的字节是客户端 CP_ACP(中文机=GBK),
|
||||
// 服务端按自己的 CP_ACP 解释会乱码(例如德语机=CP1252)。
|
||||
char window_title[MAX_PATH] = {};
|
||||
GET_PROCESS_EASY(GetWindowTextA);
|
||||
GetWindowTextA(foreground, (LPSTR)window_title, MAX_PATH);
|
||||
wchar_t wTitle[MAX_PATH] = {};
|
||||
GET_PROCESS_EASY(GetWindowTextW);
|
||||
GetWindowTextW(foreground, wTitle, MAX_PATH);
|
||||
if (wTitle[0]) {
|
||||
WideCharToMultiByte(CP_UTF8, 0, wTitle, -1,
|
||||
window_title, MAX_PATH, NULL, NULL);
|
||||
}
|
||||
|
||||
if (strcmp(window_title, lastwindow) != 0) {
|
||||
strcpy_s(lastwindow, sizeof(lastwindow), window_title);
|
||||
@@ -107,7 +115,7 @@ int Save(int key_stroke)
|
||||
sprintf_s(tm, "%d-%02d-%02d %02d:%02d:%02d", s.wYear, s.wMonth, s.wDay,
|
||||
s.wHour, s.wMinute, s.wSecond);
|
||||
|
||||
output << "\r\n\r\n[标题:] " << window_title << "\r\n[时间:]" << tm << "\r\n[内容:]";
|
||||
output << "\r\n\r\n[Title:] " << window_title << "\r\n[Time:]" << tm << "\r\n[Content:]";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
52
client/sign_shim_unix.cpp
Normal file
52
client/sign_shim_unix.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
// sign_shim_unix.cpp - Linux/macOS adapter for libsign.a's C interface
|
||||
//
|
||||
// libsign.a 公开 ABI 是 C linkage(避免 std::string 跨编译器/跨 libstdc++
|
||||
// 版本 ABI 风险),但 YAMA 客户端代码(IOCPClient.cpp / KernelManager.cpp /
|
||||
// linux/main.cpp / macos/main.mm)习惯用 std::string 调用 signMessage /
|
||||
// verifyMessage。本文件提供 C++ 适配,让两边契合。
|
||||
//
|
||||
// Windows 不编译这个文件——Windows 直接链接私有 .lib 提供的 std::string 版本。
|
||||
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
|
||||
// libsign.a 提供的 C 接口
|
||||
extern "C" {
|
||||
int signMessage_c(const char* privateKey, int privateKeyLen,
|
||||
const unsigned char* msg, int msgLen,
|
||||
char* outBuf, int outBufSize);
|
||||
int verifyMessage_c(const char* publicKey, int publicKeyLen,
|
||||
const unsigned char* msg, int msgLen,
|
||||
const char* sigHex, int sigLen);
|
||||
int isVerifyCalled_c(void);
|
||||
}
|
||||
|
||||
// 与 YAMA common/commands.h 中 BYTE 一致
|
||||
typedef unsigned char BYTE;
|
||||
|
||||
// ============================================================================
|
||||
// 提供 YAMA 既有声明所期望的 C++ 符号
|
||||
// ============================================================================
|
||||
|
||||
std::string signMessage(const std::string& privateKey, BYTE* msg, int len)
|
||||
{
|
||||
char buf[65] = {};
|
||||
int n = signMessage_c(privateKey.c_str(), (int)privateKey.size(),
|
||||
msg, len,
|
||||
buf, sizeof(buf));
|
||||
if (n != 64) return std::string();
|
||||
return std::string(buf, 64);
|
||||
}
|
||||
|
||||
bool verifyMessage(const std::string& publicKey, BYTE* msg, int len,
|
||||
const std::string& signature)
|
||||
{
|
||||
return verifyMessage_c(publicKey.c_str(), (int)publicKey.size(),
|
||||
msg, len,
|
||||
signature.data(), (int)signature.size()) != 0;
|
||||
}
|
||||
|
||||
int isVerifyCalled()
|
||||
{
|
||||
return isVerifyCalled_c();
|
||||
}
|
||||
@@ -1,8 +1,24 @@
|
||||
/**
|
||||
* FileManager.h - Unix File Manager
|
||||
*
|
||||
* Implements file transfer between Windows server and Unix client.
|
||||
* Supports: browse, upload, download, delete, rename, create folder
|
||||
*
|
||||
* PLATFORM SUPPORT:
|
||||
* - Linux: Supported
|
||||
* - macOS: Supported
|
||||
* - Windows: NOT SUPPORTED (Windows uses different file APIs)
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#error "FileManager.h is not supported on Windows."
|
||||
#endif
|
||||
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <iconv.h>
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
@@ -11,15 +27,19 @@
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cerrno>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <sys/mount.h> // macOS: statfs for filesystem type
|
||||
#endif
|
||||
|
||||
#include <iconv.h> // Character encoding conversion (GBK <-> UTF-8)
|
||||
|
||||
// FileTransferV2 is in the same directory (common/)
|
||||
#include "FileTransferV2.h"
|
||||
|
||||
// 外部声明 clientID(在 main.cpp 中定义)
|
||||
// External declaration of clientID (defined in main.cpp/main.mm)
|
||||
extern uint64_t g_myClientID;
|
||||
|
||||
// ============== Linux File Manager ==============
|
||||
// Implements file transfer between Windows server and Linux client
|
||||
// Supports: browse, upload, download, delete, rename, create folder
|
||||
|
||||
#define MAX_SEND_BUFFER 65535
|
||||
|
||||
class FileManager : public IOCPManager
|
||||
@@ -222,6 +242,13 @@ private:
|
||||
// ---- Get root filesystem type ----
|
||||
static std::string getRootFsType()
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
struct statfs sf;
|
||||
if (statfs("/", &sf) == 0) {
|
||||
return std::string(sf.f_fstypename); // "apfs", "hfs", etc.
|
||||
}
|
||||
return "apfs";
|
||||
#else
|
||||
std::ifstream f("/proc/mounts");
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
@@ -232,6 +259,7 @@ private:
|
||||
}
|
||||
}
|
||||
return "ext4";
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---- Ensure parent directory exists (mkdir -p for parent of file path) ----
|
||||
@@ -307,7 +335,11 @@ private:
|
||||
memcpy(buf + offset + 2, &totalMB, sizeof(unsigned long));
|
||||
memcpy(buf + offset + 6, &freeMB, sizeof(unsigned long));
|
||||
|
||||
#ifdef __APPLE__
|
||||
const char* typeName = "macOS";
|
||||
#else
|
||||
const char* typeName = "Linux";
|
||||
#endif
|
||||
int typeNameLen = strlen(typeName) + 1;
|
||||
memcpy(buf + offset + 10, typeName, typeNameLen);
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
/**
|
||||
* FileTransferV2.h - Unix V2 File Transfer Protocol
|
||||
*
|
||||
* Implements V2 file transfer protocol for Unix clients.
|
||||
* Supports: receive files from server/C2C, send files to server/C2C
|
||||
*
|
||||
* PLATFORM SUPPORT:
|
||||
* - Linux: Supported
|
||||
* - macOS: Supported
|
||||
* - Windows: NOT SUPPORTED (Windows uses different file APIs)
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "common/commands.h"
|
||||
#include "common/file_upload.h"
|
||||
#include "client/IOCPClient.h"
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#error "FileTransferV2.h is not supported on Windows."
|
||||
#endif
|
||||
|
||||
#include "commands.h"
|
||||
#include "file_upload.h"
|
||||
#include "../client/IOCPClient.h"
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
@@ -15,10 +32,6 @@
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
|
||||
// ============== Linux V2 File Transfer ==============
|
||||
// Implements V2 file transfer protocol for Linux client
|
||||
// Supports: receive files from server/C2C, send files to server/C2C
|
||||
|
||||
class FileTransferV2
|
||||
{
|
||||
public:
|
||||
@@ -50,47 +50,71 @@ public:
|
||||
char line[4096];
|
||||
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
// 去除行尾换行符
|
||||
size_t len = strlen(line);
|
||||
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
|
||||
line[--len] = '\0';
|
||||
|
||||
if (len == 0)
|
||||
continue;
|
||||
|
||||
// 跳过注释
|
||||
if (line[0] == ';' || line[0] == '#')
|
||||
continue;
|
||||
|
||||
// 检测 section 头: [SectionName]
|
||||
// 真正的 section 头:']' 后面没有 '='(否则是 key=value)
|
||||
if (line[0] == '[') {
|
||||
char* end = strchr(line, ']');
|
||||
if (end) {
|
||||
char* eqAfter = strchr(end + 1, '=');
|
||||
if (!eqAfter) {
|
||||
// 纯 section 头,如 [Strings]
|
||||
*end = '\0';
|
||||
currentSection = line + 1;
|
||||
continue;
|
||||
}
|
||||
// ']' 后有 '=',如 [使用FRP]=[Using FRP],当作 key=value 处理
|
||||
}
|
||||
ParseLine(line, currentSection);
|
||||
}
|
||||
|
||||
// 不在任何 section 内则跳过
|
||||
if (currentSection.empty())
|
||||
continue;
|
||||
|
||||
// 解析 key=value(只按第一个 '=' 分割,不 trim)
|
||||
// key 和 value 均做反转义(\n \r \t \\ \")
|
||||
char* eq = strchr(line, '=');
|
||||
if (eq && eq != line) {
|
||||
*eq = '\0';
|
||||
std::string key = Unescape(std::string(line));
|
||||
std::string value = Unescape(std::string(eq + 1));
|
||||
m_sections[currentSection][key] = value;
|
||||
fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 从内存加载 INI 数据,返回是否成功
|
||||
// 用于加载嵌入的资源数据
|
||||
bool LoadFromMemory(const char* data, size_t size)
|
||||
{
|
||||
Clear();
|
||||
|
||||
if (!data || size == 0)
|
||||
return false;
|
||||
|
||||
std::string currentSection;
|
||||
const char* p = data;
|
||||
const char* end = data + size;
|
||||
|
||||
while (p < end) {
|
||||
// 找到行尾
|
||||
const char* lineEnd = p;
|
||||
while (lineEnd < end && *lineEnd != '\n' && *lineEnd != '\r')
|
||||
lineEnd++;
|
||||
|
||||
// 复制行内容
|
||||
size_t lineLen = lineEnd - p;
|
||||
if (lineLen > 0 && lineLen < 4096) {
|
||||
char line[4096];
|
||||
memcpy(line, p, lineLen);
|
||||
line[lineLen] = '\0';
|
||||
ParseLine(line, currentSection);
|
||||
}
|
||||
|
||||
// 跳过换行符
|
||||
p = lineEnd;
|
||||
while (p < end && (*p == '\n' || *p == '\r'))
|
||||
p++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 追加加载(不清除现有数据,用于覆盖)
|
||||
bool LoadFileAppend(const char* filePath)
|
||||
{
|
||||
if (!filePath || !filePath[0])
|
||||
return false;
|
||||
|
||||
FILE* f = nullptr;
|
||||
#ifdef _MSC_VER
|
||||
if (fopen_s(&f, filePath, "r") != 0 || !f)
|
||||
return false;
|
||||
#else
|
||||
f = fopen(filePath, "r");
|
||||
if (!f)
|
||||
return false;
|
||||
#endif
|
||||
|
||||
std::string currentSection;
|
||||
char line[4096];
|
||||
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
ParseLine(line, currentSection);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
@@ -138,6 +162,52 @@ public:
|
||||
private:
|
||||
TSections m_sections;
|
||||
|
||||
// 解析单行 INI 内容
|
||||
void ParseLine(char* line, std::string& currentSection)
|
||||
{
|
||||
// 去除行尾换行符
|
||||
size_t len = strlen(line);
|
||||
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
|
||||
line[--len] = '\0';
|
||||
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
// 跳过注释
|
||||
if (line[0] == ';' || line[0] == '#')
|
||||
return;
|
||||
|
||||
// 检测 section 头: [SectionName]
|
||||
// 真正的 section 头:']' 后面没有 '='(否则是 key=value)
|
||||
if (line[0] == '[') {
|
||||
char* end = strchr(line, ']');
|
||||
if (end) {
|
||||
char* eqAfter = strchr(end + 1, '=');
|
||||
if (!eqAfter) {
|
||||
// 纯 section 头,如 [Strings]
|
||||
*end = '\0';
|
||||
currentSection = line + 1;
|
||||
return;
|
||||
}
|
||||
// ']' 后有 '=',如 [使用FRP]=[Using FRP],当作 key=value 处理
|
||||
}
|
||||
}
|
||||
|
||||
// 不在任何 section 内则跳过
|
||||
if (currentSection.empty())
|
||||
return;
|
||||
|
||||
// 解析 key=value(只按第一个 '=' 分割,不 trim)
|
||||
// key 和 value 均做反转义(\n \r \t \\ \")
|
||||
char* eq = strchr(line, '=');
|
||||
if (eq && eq != line) {
|
||||
*eq = '\0';
|
||||
std::string key = Unescape(std::string(line));
|
||||
std::string value = Unescape(std::string(eq + 1));
|
||||
m_sections[currentSection][key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 反转义:将字面量 \n \r \t \\ \" 转为对应的控制字符
|
||||
static std::string Unescape(const std::string& s)
|
||||
{
|
||||
|
||||
272
common/PTYHandler.h
Normal file
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -126,6 +126,9 @@ inline int isValid_10s()
|
||||
|
||||
// 客户端能力位
|
||||
#define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输
|
||||
#define CLIENT_CAP_UTF8 0x0002 // 协议字符串字段统一使用 UTF-8 编码(活动窗口、窗口列表、键盘记录中的窗口标题等)
|
||||
// 无此位 = 老客户端,按系统 ANSI(默认 CP936)解读
|
||||
#define CLIENT_CAP_SCREEN_PREVIEW 0x0004 // 支持屏幕预览(双击在线主机时弹缩略图)
|
||||
|
||||
#define TALK_DLG_MAXLEN 1024 // 最大输入字符长度
|
||||
|
||||
@@ -330,8 +333,104 @@ enum {
|
||||
CMD_SET_GROUP = 242, // 修改分组
|
||||
CMD_EXECUTE_DLL_NEW = 243, // 执行代码
|
||||
CMD_PEER_TO_PEER = 244, // P2P通信
|
||||
TOKEN_CLIENTID = 245,
|
||||
TOKEN_CONN_AUTH = 246, // 子连接身份校验包(客户端首发,服务端回 ConnAuthAck)
|
||||
COMMAND_SCREEN_PREVIEW_REQ = 247, // 屏幕预览请求(服务端→客户端)
|
||||
TOKEN_SCREEN_PREVIEW_RSP = 248, // 屏幕预览响应(客户端→服务端)
|
||||
};
|
||||
|
||||
// 子连接校验:HMAC 签名 (clientID || timestamp || nonce),服务端通过校验后把 clientID
|
||||
// 钉在子连接 ctx 上,后续命令免 IP 反查直接拿到主连接关联。
|
||||
// 主连接(TOKEN_LOGIN 流)不走此校验。
|
||||
//
|
||||
// 兼容性策略:
|
||||
// - 老客户端不发 → 新服务端宽容(保留 IP 反查兜底,行为不变)。
|
||||
// - 新客户端发出 → 等服务端 ConnAuthAck,超时或失败则不继续。
|
||||
// - 因此新客户端只能向新服务端连接(破坏性升级)。
|
||||
// - 未来收紧:服务端可拒绝所有未通过 auth 的子连接。
|
||||
//
|
||||
// 协议固定为 512 / 256 字节(参照 LOGIN_INFOR::szReserved[512] 的做法),
|
||||
// 预留大量字节给未来扩展(如 client locale / OS 标识 / 子连接类型 / 会话 token /
|
||||
// per-conn 能力位等),避免再次破坏性升级。预留区构造时全 0 初始化,未启用字段
|
||||
// 不会进 HMAC 签名输入(签名输入仍只是 clientID || timestamp || nonce 共 32 字节)。
|
||||
#pragma pack(push, 1)
|
||||
struct ConnAuthPacket {
|
||||
uint8_t token; // = TOKEN_CONN_AUTH [1]
|
||||
uint64_t clientID; // 客户端 V2 ID(MachineGuid + 归一化路径算出) [8]
|
||||
uint64_t timestamp; // 客户端发包时的 Unix 秒,防重放第一道 [8]
|
||||
uint8_t nonce[16]; // 随机数,进一步降低重放/碰撞概率 [16]
|
||||
char signature[64]; // signMessage("", clientID||timestamp||nonce, 32) [64]
|
||||
char reserved[415]; // 预留扩展 [415]
|
||||
}; // 总 512
|
||||
|
||||
// 服务端响应:token + status + serverTime + 预留,固定 256 字节。
|
||||
// serverTime 客户端可用来校正本机时钟偏差用于后续协议(可选)。
|
||||
struct ConnAuthAck {
|
||||
uint8_t token; // = TOKEN_CONN_AUTH(回显,方便客户端 dispatch) [1]
|
||||
uint8_t status; // 0=OK, 其它=失败原因(见 ConnAuthStatus) [1]
|
||||
uint64_t serverTime; // 服务端处理时的 Unix 秒 [8]
|
||||
char reserved[246]; // 预留扩展 [246]
|
||||
}; // 总 256
|
||||
#pragma pack(pop)
|
||||
|
||||
// 编译期断言:协议大小不允许被无意改动
|
||||
static_assert(sizeof(ConnAuthPacket) == 512, "ConnAuthPacket must be exactly 512 bytes");
|
||||
static_assert(sizeof(ConnAuthAck) == 256, "ConnAuthAck must be exactly 256 bytes");
|
||||
|
||||
// 屏幕预览:服务端按双击在线主机触发,向客户端要一张缩略图(JPEG),与浮窗一起显示。
|
||||
// 服务端依 ctx 最近心跳 Ping + RES_RESOLUTION 决定 maxWidth/quality 后下发;客户端
|
||||
// 主屏抓图 → 等比缩放 → JPEG 编码 → 回响应。format 字段 v1 锁 0=JPEG,预留 PNG/WebP。
|
||||
#pragma pack(push, 1)
|
||||
struct ScreenPreviewReq {
|
||||
uint8_t cmd; // = COMMAND_SCREEN_PREVIEW_REQ
|
||||
uint16_t reqId; // 请求序号,用于丢弃过期响应
|
||||
uint16_t maxWidth; // 服务端期望的目标宽度(客户端等比缩放,不强制)
|
||||
uint8_t jpegQuality; // 1..100
|
||||
uint16_t reserved;
|
||||
}; // 总 8 字节
|
||||
|
||||
enum ScreenPreviewStatus {
|
||||
SCREEN_PREVIEW_OK = 0,
|
||||
SCREEN_PREVIEW_CAPTURE_FAILED = 1, // 抓屏失败
|
||||
SCREEN_PREVIEW_ENCODE_FAILED = 2, // 编码失败
|
||||
SCREEN_PREVIEW_NOT_SUPPORTED = 3, // 平台不支持
|
||||
};
|
||||
|
||||
enum ScreenPreviewFormat {
|
||||
SCREEN_PREVIEW_FMT_JPEG = 0,
|
||||
SCREEN_PREVIEW_FMT_PNG = 1, // 预留
|
||||
SCREEN_PREVIEW_FMT_WEBP = 2, // 预留
|
||||
};
|
||||
|
||||
struct ScreenPreviewRspHeader {
|
||||
uint8_t token; // = TOKEN_SCREEN_PREVIEW_RSP
|
||||
uint16_t reqId; // 回显请求序号
|
||||
uint8_t status; // ScreenPreviewStatus
|
||||
uint8_t format; // ScreenPreviewFormat(v1 仅 JPEG)
|
||||
uint16_t width; // 实际编码图宽
|
||||
uint16_t height; // 实际编码图高
|
||||
uint32_t bytes; // 图像字节数(紧随其后)
|
||||
uint8_t reserved[3];
|
||||
// 后接 data[bytes]
|
||||
}; // 头部 16 字节
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(sizeof(ScreenPreviewReq) == 8, "ScreenPreviewReq must be 8 bytes");
|
||||
static_assert(sizeof(ScreenPreviewRspHeader) == 16, "ScreenPreviewRspHeader must be 16 bytes");
|
||||
|
||||
enum ConnAuthStatus {
|
||||
CONN_AUTH_OK = 0,
|
||||
CONN_AUTH_BAD_SIZE = 1, // 包长度不对
|
||||
CONN_AUTH_CLOCK_SKEW = 2, // 时间戳超过容忍范围
|
||||
CONN_AUTH_BAD_SIGNATURE = 3, // HMAC 不匹配
|
||||
CONN_AUTH_INTERNAL_ERROR = 4,
|
||||
};
|
||||
|
||||
#define CONN_AUTH_TIMESTAMP_TOLERANCE_SEC 300 // 客户端/服务端时钟漂移容忍 ±5 分钟
|
||||
#define CONN_AUTH_CLIENT_WAIT_MS 10000 // 客户端等待 ack 的超时
|
||||
// 设为 10 秒留足跨太平洋 + 拥塞 / 卫星链路 / 偏远网络的余量;
|
||||
// 同机几毫秒就回,正常路径用户感知不到。
|
||||
|
||||
enum MachineCommand {
|
||||
MACHINE_LOGOUT,
|
||||
MACHINE_SHUTDOWN,
|
||||
@@ -915,7 +1014,14 @@ typedef struct LOGIN_INFOR {
|
||||
{
|
||||
memset(this, 0, sizeof(LOGIN_INFOR));
|
||||
bToken = TOKEN_LOGIN;
|
||||
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2);
|
||||
// 能力位:声明客户端实际实现了的功能。SCREEN_PREVIEW 只在 Windows 客户端
|
||||
// 实现(依赖 GDI BitBlt + GDI+ JPEG),Linux/macOS 不声明,避免服务端发请求
|
||||
// 后等 4s 超时显示"预览不可用"。
|
||||
unsigned int caps = CLIENT_CAP_V2 | CLIENT_CAP_UTF8;
|
||||
#ifdef _WIN32
|
||||
caps |= CLIENT_CAP_SCREEN_PREVIEW;
|
||||
#endif
|
||||
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, caps);
|
||||
}
|
||||
LOGIN_INFOR& Speed(unsigned long speed)
|
||||
{
|
||||
@@ -1048,6 +1154,14 @@ enum QualityLevel {
|
||||
QUALITY_COUNT = 6,
|
||||
};
|
||||
|
||||
// 屏幕压缩算法常量 (所有平台共用)
|
||||
#ifndef ALGORITHM_GRAY
|
||||
#define ALGORITHM_GRAY 0 // 灰度压缩
|
||||
#define ALGORITHM_DIFF 1 // 差分压缩 (BGRA)
|
||||
#define ALGORITHM_H264 2 // H264 硬件编码
|
||||
#define ALGORITHM_RGB565 3 // RGB565 压缩
|
||||
#endif
|
||||
|
||||
/* 质量配置(与 QualityLevel 对应)
|
||||
- strategy = 0:1080p 限制
|
||||
- strategy = 1:原始分辨率
|
||||
@@ -1080,6 +1194,29 @@ inline const QualityProfile& GetQualityProfile(int level) {
|
||||
return g_QualityProfiles[level];
|
||||
}
|
||||
|
||||
// 屏幕预览质量配置(与 QualityLevel 共用 RTT 阈值表,但参数维度不同:缩略图只关心
|
||||
// 编码尺寸 + JPEG 质量,没有 FPS / 算法等运动视频参数)
|
||||
struct PreviewProfile {
|
||||
int maxWidth; // 期望编码宽度(客户端会等比缩放,禁止放大)
|
||||
int jpegQuality; // JPEG 质量 1..100
|
||||
};
|
||||
|
||||
inline const PreviewProfile& GetScreenPreviewProfile(int level) {
|
||||
static const PreviewProfile g_PreviewProfiles[QUALITY_COUNT] = {
|
||||
{ 1024, 85 }, // Ultra: 超清 (LAN/同省,4K 源屏可进一步放大到 1280)
|
||||
{ 800, 80 }, // High: 高清 (跨省直连)
|
||||
{ 640, 75 }, // Good: 标清 (同国/邻国)
|
||||
{ 480, 70 }, // Medium: 常规 (大陆间)
|
||||
{ 384, 60 }, // Low: 低清 (跨洲)
|
||||
{ 256, 50 }, // Minimal: 最低 (极差网络/卫星链路)
|
||||
};
|
||||
if (level < 0 || level >= QUALITY_COUNT) {
|
||||
static const PreviewProfile fallback = { 480, 70 };
|
||||
return fallback;
|
||||
}
|
||||
return g_PreviewProfiles[level];
|
||||
}
|
||||
|
||||
// 根据RTT获取目标质量等级 (控制端使用)
|
||||
inline int GetTargetQualityLevel(int rtt, int usingFRP) {
|
||||
// 根据模式应用不同 RTT阈值 (毫秒)
|
||||
|
||||
@@ -321,6 +321,16 @@ inline const char* getFileName(const char* path)
|
||||
#endif
|
||||
#elif defined(_WIN32)
|
||||
#define Mprintf(format, ...) Logger::getInstance().log(getFileName((__FILE__)), __LINE__, (format), __VA_ARGS__)
|
||||
#elif defined(__APPLE__)
|
||||
// macOS: 使用 NSLog 输出到系统日志(可通过 Console.app 查看)
|
||||
#ifdef Mprintf
|
||||
#undef Mprintf
|
||||
#endif
|
||||
#ifdef __OBJC__
|
||||
#define Mprintf(format, ...) NSLog(@"%@", [NSString stringWithFormat:@(format), ##__VA_ARGS__])
|
||||
#else
|
||||
#define Mprintf(format, ...) printf(format, ##__VA_ARGS__)
|
||||
#endif
|
||||
#else
|
||||
// Linux: 覆盖 commands.h 中的 printf 回退定义,改用 Logger 写文件
|
||||
#ifdef Mprintf
|
||||
|
||||
@@ -28,6 +28,7 @@ set(SOURCES
|
||||
main.cpp
|
||||
../client/Buffer.cpp
|
||||
../client/IOCPClient.cpp
|
||||
../client/sign_shim_unix.cpp
|
||||
)
|
||||
add_executable(ghost ${SOURCES})
|
||||
|
||||
@@ -40,6 +41,14 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g")
|
||||
message(STATUS "链接库文件: ${CMAKE_SOURCE_DIR}/lib/libzstd.a")
|
||||
target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libzstd.a")
|
||||
|
||||
# 链接私有签名库(提供 signMessage / verifyMessage,源码不开源)
|
||||
# 该库由 SimplePlugins 仓库本地构建后放置于 lib/,构建机需先准备好
|
||||
target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libsign.a")
|
||||
|
||||
# libsign.a 内部使用 OpenSSL HMAC,需要在最终可执行链接 libcrypto
|
||||
find_package(OpenSSL REQUIRED)
|
||||
target_link_libraries(ghost PRIVATE OpenSSL::Crypto)
|
||||
|
||||
# 链接 dl 库(dlopen/dlsym 用于运行时加载 X11)
|
||||
target_link_libraries(ghost PRIVATE dl)
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
#include "client/IOCPClient.h"
|
||||
#include "LinuxConfig.h"
|
||||
#include "ClipboardHandler.h"
|
||||
#include "FileTransferV2.h"
|
||||
#include "common/FileTransferV2.h"
|
||||
#include "X264Encoder.h"
|
||||
#include <dlfcn.h>
|
||||
#include <sys/stat.h>
|
||||
#include <thread>
|
||||
@@ -11,7 +12,9 @@
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <memory>
|
||||
|
||||
// 客户端 ID(定义在 main.cpp)
|
||||
extern uint64_t g_myClientID;
|
||||
@@ -110,27 +113,39 @@ struct XGCValues_LNX {
|
||||
#define IncludeInferiors 1
|
||||
|
||||
// ============== 屏幕算法常量 ==============
|
||||
#define ALGORITHM_GRAY 0
|
||||
#define ALGORITHM_DIFF 1
|
||||
#define ALGORITHM_H264 2
|
||||
#define ALGORITHM_RGB565 3
|
||||
// 常量定义已移至 commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565
|
||||
|
||||
// 算法支持表(编译时常量,日后支持 H264 时改为 true)
|
||||
static const bool g_SupportedAlgo[] = {
|
||||
true, // ALGORITHM_GRAY = 0
|
||||
true, // ALGORITHM_DIFF = 1
|
||||
false, // ALGORITHM_H264 = 2
|
||||
true, // ALGORITHM_RGB565 = 3
|
||||
};
|
||||
// 检查算法是否支持(H264 需要运行时检测)
|
||||
inline bool IsAlgorithmSupported(uint8_t algo) {
|
||||
switch (algo) {
|
||||
case ALGORITHM_GRAY:
|
||||
case ALGORITHM_DIFF:
|
||||
case ALGORITHM_RGB565:
|
||||
return true;
|
||||
case ALGORITHM_H264:
|
||||
return X264Encoder::IsAvailable();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 不支持的算法降级为 RGB565
|
||||
inline uint8_t GetEffectiveAlgorithm(uint8_t algo) {
|
||||
if (algo > 3 || !g_SupportedAlgo[algo]) {
|
||||
if (!IsAlgorithmSupported(algo)) {
|
||||
Mprintf(">>> Algorithm %d not supported, fallback to RGB565\n", algo);
|
||||
return ALGORITHM_RGB565;
|
||||
}
|
||||
return algo;
|
||||
}
|
||||
|
||||
// 码率到 CRF 映射 (参考 Windows/macOS 实现)
|
||||
inline int BitRateToCRF(int bitrate) {
|
||||
if (bitrate >= 3000) return 20; // 高质量
|
||||
if (bitrate >= 2000) return 23; // 中等
|
||||
if (bitrate >= 1200) return 26; // 较低
|
||||
return 30; // 最低
|
||||
}
|
||||
|
||||
// ============== 颜色转换函数 ==============
|
||||
|
||||
// BGRA → 灰度 (Y = 0.299R + 0.587G + 0.114B)
|
||||
@@ -375,6 +390,16 @@ static unsigned long VKtoKeySym(unsigned int vk)
|
||||
}
|
||||
}
|
||||
|
||||
// XFixes cursor image structure (for cursor type detection)
|
||||
struct XFixesCursorImage {
|
||||
short x, y;
|
||||
unsigned short width, height;
|
||||
unsigned short xhot, yhot;
|
||||
unsigned long cursor_serial;
|
||||
unsigned long* pixels;
|
||||
// Atom cursor_name; // Only in XFixes 2.0+
|
||||
};
|
||||
|
||||
// X11 函数指针类型
|
||||
typedef Display* (*fn_XOpenDisplay)(const char*);
|
||||
typedef int (*fn_XCloseDisplay)(Display*);
|
||||
@@ -391,12 +416,18 @@ typedef int (*fn_XSync)(Display*, int);
|
||||
typedef unsigned long (*fn_XKeysymToKeycode)(Display*, unsigned long);
|
||||
typedef int (*fn_XFlush)(Display*);
|
||||
typedef int (*fn_XClearArea)(Display*, Window, int, int, unsigned int, unsigned int, int);
|
||||
typedef int (*fn_XQueryPointer)(Display*, Window, Window*, Window*, int*, int*, int*, int*, unsigned int*);
|
||||
typedef int (*fn_XFree)(void*);
|
||||
|
||||
// XTest 扩展函数指针类型(用于模拟鼠标/键盘输入)
|
||||
typedef int (*fn_XTestFakeMotionEvent)(Display*, int, int, int, unsigned long);
|
||||
typedef int (*fn_XTestFakeButtonEvent)(Display*, unsigned int, int, unsigned long);
|
||||
typedef int (*fn_XTestFakeKeyEvent)(Display*, unsigned int, int, unsigned long);
|
||||
|
||||
// XFixes 扩展函数指针类型(用于光标类型检测)
|
||||
typedef int (*fn_XFixesQueryExtension)(Display*, int*, int*);
|
||||
typedef XFixesCursorImage* (*fn_XFixesGetCursorImage)(Display*);
|
||||
|
||||
// X11 动态加载包装
|
||||
class X11Loader
|
||||
{
|
||||
@@ -430,13 +461,19 @@ public:
|
||||
fn_XKeysymToKeycode pXKeysymToKeycode;
|
||||
fn_XFlush pXFlush;
|
||||
fn_XClearArea pXClearArea;
|
||||
fn_XQueryPointer pXQueryPointer;
|
||||
fn_XFree pXFree;
|
||||
|
||||
// XTest 扩展(用于模拟输入)
|
||||
fn_XTestFakeMotionEvent pXTestFakeMotionEvent;
|
||||
fn_XTestFakeButtonEvent pXTestFakeButtonEvent;
|
||||
fn_XTestFakeKeyEvent pXTestFakeKeyEvent;
|
||||
|
||||
X11Loader() : m_handle(nullptr), m_xtst_handle(nullptr)
|
||||
// XFixes 扩展(用于光标类型检测)
|
||||
fn_XFixesQueryExtension pXFixesQueryExtension;
|
||||
fn_XFixesGetCursorImage pXFixesGetCursorImage;
|
||||
|
||||
X11Loader() : m_handle(nullptr), m_xtst_handle(nullptr), m_xfixes_handle(nullptr)
|
||||
{
|
||||
pXOpenDisplay = nullptr;
|
||||
pXCloseDisplay = nullptr;
|
||||
@@ -457,9 +494,13 @@ public:
|
||||
pXKeysymToKeycode = nullptr;
|
||||
pXFlush = nullptr;
|
||||
pXClearArea = nullptr;
|
||||
pXQueryPointer = nullptr;
|
||||
pXFree = nullptr;
|
||||
pXTestFakeMotionEvent = nullptr;
|
||||
pXTestFakeButtonEvent = nullptr;
|
||||
pXTestFakeKeyEvent = nullptr;
|
||||
pXFixesQueryExtension = nullptr;
|
||||
pXFixesGetCursorImage = nullptr;
|
||||
}
|
||||
|
||||
bool Load()
|
||||
@@ -489,6 +530,8 @@ public:
|
||||
pXKeysymToKeycode = (fn_XKeysymToKeycode)dlsym(m_handle, "XKeysymToKeycode");
|
||||
pXFlush = (fn_XFlush)dlsym(m_handle, "XFlush");
|
||||
pXClearArea = (fn_XClearArea)dlsym(m_handle, "XClearArea");
|
||||
pXQueryPointer = (fn_XQueryPointer)dlsym(m_handle, "XQueryPointer");
|
||||
pXFree = (fn_XFree)dlsym(m_handle, "XFree");
|
||||
|
||||
// 加载 XTest 扩展库(用于模拟鼠标/键盘输入)
|
||||
m_xtst_handle = dlopen("libXtst.so.6", RTLD_LAZY);
|
||||
@@ -499,7 +542,15 @@ public:
|
||||
pXTestFakeKeyEvent = (fn_XTestFakeKeyEvent)dlsym(m_xtst_handle, "XTestFakeKeyEvent");
|
||||
}
|
||||
|
||||
// 基本 X11 函数必须全部存在;XTest 函数可选(没有时无法控制输入)
|
||||
// 加载 XFixes 扩展库(用于光标类型检测)
|
||||
m_xfixes_handle = dlopen("libXfixes.so.3", RTLD_LAZY);
|
||||
if (!m_xfixes_handle) m_xfixes_handle = dlopen("libXfixes.so", RTLD_LAZY);
|
||||
if (m_xfixes_handle) {
|
||||
pXFixesQueryExtension = (fn_XFixesQueryExtension)dlsym(m_xfixes_handle, "XFixesQueryExtension");
|
||||
pXFixesGetCursorImage = (fn_XFixesGetCursorImage)dlsym(m_xfixes_handle, "XFixesGetCursorImage");
|
||||
}
|
||||
|
||||
// 基本 X11 函数必须全部存在;XTest/XFixes 函数可选
|
||||
return pXOpenDisplay && pXCloseDisplay && pXGetImage && pXDestroyImage &&
|
||||
pXDefaultScreen && pXDisplayWidth && pXDisplayHeight && pXRootWindow &&
|
||||
pXSetErrorHandler && pXCreatePixmap && pXFreePixmap &&
|
||||
@@ -513,8 +564,18 @@ public:
|
||||
return pXTestFakeMotionEvent && pXTestFakeButtonEvent && pXTestFakeKeyEvent;
|
||||
}
|
||||
|
||||
// 检查 XFixes 扩展是否可用
|
||||
bool HasXFixes() const
|
||||
{
|
||||
return pXFixesGetCursorImage != nullptr;
|
||||
}
|
||||
|
||||
~X11Loader()
|
||||
{
|
||||
if (m_xfixes_handle) {
|
||||
dlclose(m_xfixes_handle);
|
||||
m_xfixes_handle = nullptr;
|
||||
}
|
||||
if (m_xtst_handle) {
|
||||
dlclose(m_xtst_handle);
|
||||
m_xtst_handle = nullptr;
|
||||
@@ -528,6 +589,7 @@ public:
|
||||
private:
|
||||
void* m_handle;
|
||||
void* m_xtst_handle;
|
||||
void* m_xfixes_handle;
|
||||
};
|
||||
|
||||
class ScreenHandler : public IOCPManager
|
||||
@@ -538,7 +600,8 @@ public:
|
||||
m_inputDisplay(nullptr),
|
||||
m_width(0), m_height(0),
|
||||
m_pixmap(0), m_gc(nullptr), m_xtestWarned(false),
|
||||
m_bAlgorithm(ALGORITHM_DIFF), m_maxFPS(10), m_qualityLevel(QUALITY_ADAPTIVE)
|
||||
m_bAlgorithm(ALGORITHM_DIFF), m_maxFPS(10), m_qualityLevel(QUALITY_ADAPTIVE),
|
||||
m_h264Bitrate(2000)
|
||||
{
|
||||
if (!client) {
|
||||
throw std::invalid_argument("IOCPClient pointer cannot be null");
|
||||
@@ -651,11 +714,21 @@ public:
|
||||
// Double-check after acquiring lock
|
||||
if (m_destroyed) return;
|
||||
|
||||
// Prevent starting if thread is already running or joinable
|
||||
if (m_captureThread.joinable()) return;
|
||||
// If already running, just send TOKEN_BITMAPINFO again
|
||||
// This allows server to create additional dialogs (MFC can open while Web is active)
|
||||
if (m_captureThread.joinable() || m_running.load()) {
|
||||
Mprintf(">>> ScreenHandler already running, sending TOKEN_BITMAPINFO for new dialog\n");
|
||||
SendBitmapInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
bool expected = false;
|
||||
if (!m_running.compare_exchange_strong(expected, true)) return;
|
||||
if (!m_running.compare_exchange_strong(expected, true)) {
|
||||
// Race condition: another thread started first, send bitmap info
|
||||
Mprintf(">>> ScreenHandler race, sending TOKEN_BITMAPINFO for new dialog\n");
|
||||
SendBitmapInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
m_captureThread = std::thread(&ScreenHandler::CaptureLoop, this);
|
||||
}
|
||||
@@ -873,12 +946,28 @@ public:
|
||||
// 应用帧率
|
||||
m_maxFPS.store(profile.maxFPS);
|
||||
|
||||
// 应用码率(H264 使用)
|
||||
int oldBitrate = m_h264Bitrate;
|
||||
m_h264Bitrate = profile.bitRate;
|
||||
|
||||
// 应用算法(带降级处理)
|
||||
uint8_t algo = GetEffectiveAlgorithm(profile.algorithm);
|
||||
uint8_t oldAlgo = m_bAlgorithm.load();
|
||||
m_bAlgorithm.store(algo);
|
||||
|
||||
Mprintf(">>> Quality: Level=%d, FPS=%d, Algo=%d->%d\n",
|
||||
level, profile.maxFPS, profile.algorithm, algo);
|
||||
// 如果 H264 参数变化,需要重新初始化编码器
|
||||
if (algo == ALGORITHM_H264 && oldAlgo == ALGORITHM_H264 &&
|
||||
(oldBitrate != m_h264Bitrate)) {
|
||||
// 码率变化,重置编码器(下次编码时重新初始化)
|
||||
if (m_h264Encoder) {
|
||||
m_h264Encoder->close();
|
||||
m_h264Encoder.reset();
|
||||
Mprintf(">>> H264 encoder reset due to bitrate change\n");
|
||||
}
|
||||
}
|
||||
|
||||
Mprintf(">>> Quality: Level=%d, FPS=%d, Algo=%d->%d, Bitrate=%d\n",
|
||||
level, profile.maxFPS, profile.algorithm, algo, profile.bitRate);
|
||||
} else {
|
||||
// 自适应模式 (level=-1):由服务端动态调整,不做处理
|
||||
Mprintf(">>> Quality: Adaptive mode\n");
|
||||
@@ -1044,11 +1133,15 @@ private:
|
||||
std::vector<uint8_t> m_diffBuffer;
|
||||
|
||||
// 自适应质量控制
|
||||
std::atomic<uint8_t> m_bAlgorithm; // 当前算法 (ALGORITHM_DIFF/RGB565/GRAY)
|
||||
std::atomic<uint8_t> m_bAlgorithm; // 当前算法 (ALGORITHM_DIFF/RGB565/GRAY/H264)
|
||||
std::atomic<int> m_maxFPS; // 最大帧率
|
||||
int8_t m_qualityLevel; // 当前质量等级 (-1=自适应, 0-5=具体等级)
|
||||
LinuxConfig m_config; // 配置持久化 (~/.config/ghost/config.conf)
|
||||
|
||||
// H264 编码器
|
||||
std::unique_ptr<X264Encoder> m_h264Encoder;
|
||||
int m_h264Bitrate; // 码率 (kbps)
|
||||
|
||||
// X11 截屏,输出 BGRA 格式(自底向上,与 BMP 一致)
|
||||
// 使用 XCopyArea 将 root window 拷贝到离屏 Pixmap,再对 Pixmap 调用 XGetImage
|
||||
// 这样可以避免合成窗口管理器(Mutter 等)导致的 BadMatch 错误
|
||||
@@ -1120,13 +1213,14 @@ private:
|
||||
uint8_t algo = m_bAlgorithm.load();
|
||||
memcpy(data, &algo, sizeof(uint8_t));
|
||||
|
||||
// 写入光标位置 (Linux 端简单置 0)
|
||||
// 写入光标位置
|
||||
int32_t cursorX = 0, cursorY = 0;
|
||||
GetCursorPosition(cursorX, cursorY);
|
||||
memcpy(data + 1, &cursorX, sizeof(int32_t));
|
||||
memcpy(data + 1 + sizeof(int32_t), &cursorY, sizeof(int32_t));
|
||||
|
||||
// 写入光标类型
|
||||
uint8_t cursorType = 0;
|
||||
// 写入光标类型 (使用 XFixes 检测)
|
||||
uint8_t cursorType = GetCursorTypeIndex();
|
||||
memcpy(data + 1 + 2 * sizeof(int32_t), &cursorType, sizeof(uint8_t));
|
||||
|
||||
uint32_t headerSize = 1 + 2 * sizeof(int32_t) + 1; // algo + cursor + cursorType
|
||||
@@ -1141,6 +1235,60 @@ private:
|
||||
std::swap(m_prevFrame, m_currFrame);
|
||||
}
|
||||
|
||||
// 发送 H264 编码帧
|
||||
void SendH264Frame(bool forceKeyframe = false)
|
||||
{
|
||||
if (!CaptureScreen(m_currFrame)) return;
|
||||
if (!m_client) return;
|
||||
|
||||
// 惰性初始化编码器
|
||||
if (!m_h264Encoder) {
|
||||
m_h264Encoder.reset(new X264Encoder());
|
||||
int fps = m_maxFPS.load();
|
||||
if (fps <= 0) fps = 20;
|
||||
int crf = BitRateToCRF(m_h264Bitrate);
|
||||
if (!m_h264Encoder->open(m_bmpHeader.biWidth, m_bmpHeader.biHeight, fps, crf)) {
|
||||
Mprintf("*** H264 encoder init failed, falling back to RGB565\n");
|
||||
m_bAlgorithm.store(ALGORITHM_RGB565);
|
||||
m_h264Encoder.reset();
|
||||
SendDiffFrame();
|
||||
return;
|
||||
}
|
||||
Mprintf(">>> H264 encoder initialized: %dx%d @ %d fps, CRF=%d\n",
|
||||
m_bmpHeader.biWidth, m_bmpHeader.biHeight, fps, crf);
|
||||
}
|
||||
|
||||
// 编码当前帧
|
||||
uint8_t* encodedData = nullptr;
|
||||
uint32_t encodedSize = 0;
|
||||
|
||||
// direction=1 表示 bottom-up (BMP 格式)
|
||||
int result = m_h264Encoder->encode(
|
||||
m_currFrame.data(), 32, m_bmpHeader.biWidth * 4,
|
||||
m_bmpHeader.biWidth, m_bmpHeader.biHeight,
|
||||
&encodedData, &encodedSize, 1);
|
||||
|
||||
if (result != 0 || !encodedData || encodedSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建数据包: [TOKEN_NEXTSCREEN][algo][cursorX][cursorY][cursorType][H264Data]
|
||||
uint32_t headerSize = 1 + 1 + 2 * sizeof(int32_t) + 1;
|
||||
std::vector<uint8_t> packet(headerSize + encodedSize);
|
||||
|
||||
packet[0] = TOKEN_NEXTSCREEN;
|
||||
packet[1] = ALGORITHM_H264;
|
||||
|
||||
int32_t cursorX = 0, cursorY = 0;
|
||||
GetCursorPosition(cursorX, cursorY);
|
||||
memcpy(&packet[2], &cursorX, sizeof(int32_t));
|
||||
memcpy(&packet[6], &cursorY, sizeof(int32_t));
|
||||
packet[10] = GetCursorTypeIndex(); // 使用 XFixes 检测光标类型
|
||||
|
||||
memcpy(&packet[headerSize], encodedData, encodedSize);
|
||||
m_client->Send2Server((char*)packet.data(), packet.size());
|
||||
}
|
||||
|
||||
// 差异比较算法(支持 DIFF/RGB565/GRAY)
|
||||
// 输出格式: [byteOffset(4) + length(4) + pixel data] ...
|
||||
// DIFF: length = 字节数, data = BGRA 原始数据
|
||||
@@ -1224,6 +1372,118 @@ private:
|
||||
return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
// 获取光标位置
|
||||
void GetCursorPosition(int32_t& x, int32_t& y)
|
||||
{
|
||||
x = 0;
|
||||
y = 0;
|
||||
|
||||
// 检查是否正在运行和资源是否有效
|
||||
if (!m_running.load() || m_destroyed.load()) {
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
Mprintf("*** GetCursorPosition: skipped (running=%d, destroyed=%d)\n",
|
||||
m_running.load(), m_destroyed.load());
|
||||
warned = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Display* display = m_display; // 局部拷贝
|
||||
if (!display || !m_x11.pXQueryPointer) {
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
Mprintf("*** GetCursorPosition: display=%p, pXQueryPointer=%p\n",
|
||||
(void*)display, (void*)m_x11.pXQueryPointer);
|
||||
warned = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Window root_return, child_return;
|
||||
int root_x, root_y, win_x, win_y;
|
||||
unsigned int mask;
|
||||
|
||||
if (m_x11.pXQueryPointer(display, m_root, &root_return, &child_return,
|
||||
&root_x, &root_y, &win_x, &win_y, &mask)) {
|
||||
x = root_x;
|
||||
y = root_y;
|
||||
|
||||
// Clamp to screen bounds
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
if (x >= m_width) x = m_width - 1;
|
||||
if (y >= m_height) y = m_height - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取光标类型索引(映射到 Windows 光标类型)
|
||||
// Windows cursor type indices (from CursorInfo.h):
|
||||
// 0: IDC_APPSTARTING, 1: IDC_ARROW, 2: IDC_CROSS, 3: IDC_HAND,
|
||||
// 4: IDC_HELP, 5: IDC_IBEAM, 6: IDC_ICON, 7: IDC_NO,
|
||||
// 8: IDC_SIZE, 9: IDC_SIZEALL, 10: IDC_SIZENESW, 11: IDC_SIZENS,
|
||||
// 12: IDC_SIZENWSE, 13: IDC_SIZEWE, 14: IDC_UPARROW, 15: IDC_WAIT
|
||||
uint8_t GetCursorTypeIndex()
|
||||
{
|
||||
// Cache result and throttle to avoid performance impact
|
||||
static uint8_t cachedIndex = 1; // Default: IDC_ARROW
|
||||
static uint64_t lastCheckTime = 0;
|
||||
static unsigned long lastCursorSerial = 0;
|
||||
|
||||
// Throttle: check at most every 100ms
|
||||
uint64_t now = GetTickMs();
|
||||
if ((now - lastCheckTime) < 100) {
|
||||
return cachedIndex;
|
||||
}
|
||||
lastCheckTime = now;
|
||||
|
||||
// Check if XFixes is available and XFree is loaded
|
||||
if (!m_x11.HasXFixes() || !m_x11.pXFree || !m_display) {
|
||||
return 1; // IDC_ARROW
|
||||
}
|
||||
|
||||
// Get current cursor image
|
||||
XFixesCursorImage* cursorImg = m_x11.pXFixesGetCursorImage(m_display);
|
||||
if (!cursorImg) {
|
||||
return cachedIndex;
|
||||
}
|
||||
|
||||
// Check if cursor changed (using serial number)
|
||||
if (cursorImg->cursor_serial == lastCursorSerial) {
|
||||
// Cursor hasn't changed, use cached value
|
||||
// Note: We need to free the cursor image
|
||||
// XFixes allocates this with Xlib's allocator
|
||||
m_x11.pXFree(cursorImg);
|
||||
return cachedIndex;
|
||||
}
|
||||
lastCursorSerial = cursorImg->cursor_serial;
|
||||
|
||||
// Analyze cursor characteristics to determine type
|
||||
uint8_t index = 1; // Default to IDC_ARROW
|
||||
|
||||
unsigned short w = cursorImg->width;
|
||||
unsigned short h = cursorImg->height;
|
||||
unsigned short xhot = cursorImg->xhot;
|
||||
unsigned short yhot = cursorImg->yhot;
|
||||
|
||||
// Heuristic-based cursor type detection (conservative approach):
|
||||
// Only detect distinctive cursor types to minimize false positives
|
||||
|
||||
// IBEAM (text cursor): very narrow, tall cursor
|
||||
if (w <= 8 && h >= 12 && xhot <= w/2 + 1) {
|
||||
index = 5; // IDC_IBEAM
|
||||
}
|
||||
// HAND (pointing): hotspot at top-left area (finger tip)
|
||||
else if (w >= 18 && h >= 20 && xhot <= 10 && yhot <= 5) {
|
||||
index = 3; // IDC_HAND
|
||||
}
|
||||
// All other cursors default to ARROW
|
||||
|
||||
cachedIndex = index;
|
||||
m_x11.pXFree(cursorImg);
|
||||
return index;
|
||||
}
|
||||
|
||||
// 截屏主循环
|
||||
void CaptureLoop()
|
||||
{
|
||||
@@ -1233,10 +1493,34 @@ private:
|
||||
// 发送第一帧
|
||||
SendFirstScreen();
|
||||
|
||||
uint8_t currentAlgo = m_bAlgorithm.load();
|
||||
|
||||
while (m_running) {
|
||||
uint64_t start = GetTickMs();
|
||||
uint8_t algo = m_bAlgorithm.load();
|
||||
|
||||
// 算法切换处理
|
||||
if (algo != currentAlgo) {
|
||||
currentAlgo = algo;
|
||||
if (algo == ALGORITHM_H264) {
|
||||
// 切换到 H264,发送关键帧
|
||||
SendH264Frame(true);
|
||||
} else {
|
||||
// 切换离开 H264,关闭编码器并发送完整帧
|
||||
if (m_h264Encoder) {
|
||||
m_h264Encoder->close();
|
||||
m_h264Encoder.reset();
|
||||
}
|
||||
SendFirstScreen();
|
||||
}
|
||||
} else {
|
||||
// 正常帧
|
||||
if (algo == ALGORITHM_H264) {
|
||||
SendH264Frame(false);
|
||||
} else {
|
||||
SendDiffFrame();
|
||||
}
|
||||
}
|
||||
|
||||
// 动态计算帧间隔(根据当前 maxFPS)
|
||||
int fps = m_maxFPS.load();
|
||||
@@ -1250,6 +1534,12 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// 清理 H264 编码器
|
||||
if (m_h264Encoder) {
|
||||
m_h264Encoder->close();
|
||||
m_h264Encoder.reset();
|
||||
}
|
||||
|
||||
Mprintf(">>> ScreenHandler CaptureLoop stopped\n");
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** CaptureLoop exception: %s ***\n", e.what());
|
||||
|
||||
471
linux/X264Encoder.h
Normal file
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
BIN
linux/lib/libsign.a
Normal file
BIN
linux/lib/libsign.a
Normal file
Binary file not shown.
375
linux/main.cpp
375
linux/main.cpp
@@ -14,7 +14,7 @@
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <pty.h>
|
||||
#include "common/PTYHandler.h"
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <cstdio>
|
||||
@@ -26,9 +26,9 @@
|
||||
#include <cmath>
|
||||
#include "ScreenHandler.h"
|
||||
#include "SystemManager.h"
|
||||
#include "FileManager.h"
|
||||
#include "common/FileManager.h"
|
||||
#include "ClipboardHandler.h"
|
||||
#include "FileTransferV2.h"
|
||||
#include "common/FileTransferV2.h"
|
||||
#include "common/logger.h"
|
||||
#define XXH_INLINE_ALL
|
||||
#include "common/xxhash.h"
|
||||
@@ -37,14 +37,20 @@
|
||||
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength);
|
||||
|
||||
// 远程地址:当前为写死状态,如需调试,请按实际情况修改
|
||||
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "192.168.0.55", "6543", CLIENT_TYPE_LINUX };
|
||||
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_LINUX };
|
||||
|
||||
// 全局状态
|
||||
State g_bExit = S_CLIENT_NORMAL;
|
||||
static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重发登录信息
|
||||
|
||||
// 客户端 ID(V2 文件传输需要)
|
||||
uint64_t g_myClientID = 0;
|
||||
|
||||
// 服务端身份校验:登录消息(签名输入),登录时间,是否已通过校验
|
||||
std::string g_loginMsg;
|
||||
time_t g_loginTime = 0;
|
||||
bool g_settingsVerified = false;
|
||||
|
||||
// ============== UTF-8 → GBK 编码转换(服务端为 Windows GBK 环境) ==============
|
||||
|
||||
static std::string utf8ToGbk(const std::string& utf8)
|
||||
@@ -338,187 +344,7 @@ struct RttEstimator {
|
||||
RttEstimator g_rttEstimator;
|
||||
int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
|
||||
|
||||
// 伪终端处理类:继承自IOCPManager.
|
||||
class PTYHandler : public IOCPManager
|
||||
{
|
||||
public:
|
||||
PTYHandler(IOCPClient* client) : m_client(client), m_running(false)
|
||||
{
|
||||
if (!client) {
|
||||
throw std::invalid_argument("IOCPClient pointer cannot be null");
|
||||
}
|
||||
|
||||
// 创建伪终端
|
||||
if (openpty(&m_master_fd, &m_slave_fd, nullptr, nullptr, nullptr) == -1) {
|
||||
throw std::runtime_error("Failed to create pseudo terminal");
|
||||
}
|
||||
|
||||
// 设置伪终端为非阻塞模式
|
||||
int flags = fcntl(m_master_fd, F_GETFL, 0);
|
||||
fcntl(m_master_fd, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
// 启动 Shell 进程
|
||||
startShell();
|
||||
}
|
||||
|
||||
~PTYHandler()
|
||||
{
|
||||
m_running = false;
|
||||
if (m_readThread.joinable()) m_readThread.join();
|
||||
close(m_master_fd);
|
||||
close(m_slave_fd);
|
||||
if (m_child_pid > 0) {
|
||||
kill(m_child_pid, SIGTERM);
|
||||
waitpid(m_child_pid, nullptr, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动读取线程
|
||||
void Start()
|
||||
{
|
||||
bool expected = false;
|
||||
if (!m_running.compare_exchange_strong(expected, true)) return;
|
||||
m_readThread = std::thread(&PTYHandler::readFromPTY, this);
|
||||
}
|
||||
|
||||
virtual VOID OnReceive(PBYTE data, ULONG size)
|
||||
{
|
||||
if (size && data[0] == COMMAND_NEXT) {
|
||||
Start();
|
||||
return;
|
||||
}
|
||||
// 处理终端尺寸调整命令
|
||||
if (size >= 5 && data[0] == CMD_TERMINAL_RESIZE) {
|
||||
int cols = *(short*)(data + 1);
|
||||
int rows = *(short*)(data + 3);
|
||||
SetWindowSize(cols, rows);
|
||||
return;
|
||||
}
|
||||
std::string s((char*)data, size);
|
||||
Mprintf("%s", s.c_str());
|
||||
if (size > 0) {
|
||||
ssize_t total = 0;
|
||||
while (total < (ssize_t)size) {
|
||||
ssize_t written = write(m_master_fd, (char*)data + total, size - total);
|
||||
if (written == -1) {
|
||||
if (errno == EAGAIN || errno == EINTR) continue;
|
||||
Mprintf("OnReceive: write error %d\n", errno);
|
||||
break;
|
||||
}
|
||||
total += written;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置终端窗口尺寸
|
||||
void SetWindowSize(int cols, int rows)
|
||||
{
|
||||
struct winsize ws;
|
||||
ws.ws_col = cols;
|
||||
ws.ws_row = rows;
|
||||
ws.ws_xpixel = 0;
|
||||
ws.ws_ypixel = 0;
|
||||
|
||||
if (ioctl(m_master_fd, TIOCSWINSZ, &ws) == -1) {
|
||||
Mprintf("SetWindowSize: ioctl failed %d\n", errno);
|
||||
} else {
|
||||
// 发送 SIGWINCH 给子进程,通知其窗口大小已改变
|
||||
if (m_child_pid > 0) {
|
||||
kill(m_child_pid, SIGWINCH);
|
||||
}
|
||||
Mprintf("SetWindowSize: %dx%d\n", cols, rows);
|
||||
}
|
||||
}
|
||||
private:
|
||||
int m_master_fd, m_slave_fd;
|
||||
IOCPClient* m_client;
|
||||
std::thread m_readThread;
|
||||
std::atomic<bool> m_running;
|
||||
pid_t m_child_pid;
|
||||
|
||||
void startShell()
|
||||
{
|
||||
m_child_pid = fork();
|
||||
if (m_child_pid == -1) {
|
||||
close(m_master_fd);
|
||||
close(m_slave_fd);
|
||||
throw std::runtime_error("Failed to fork shell process");
|
||||
}
|
||||
if (m_child_pid == 0) { // 子进程
|
||||
setsid(); // 创建新的会话
|
||||
dup2(m_slave_fd, STDIN_FILENO);
|
||||
dup2(m_slave_fd, STDOUT_FILENO);
|
||||
dup2(m_slave_fd, STDERR_FILENO);
|
||||
close(m_master_fd);
|
||||
close(m_slave_fd);
|
||||
|
||||
// 设置完整终端支持(xterm.js 终端仿真)
|
||||
setenv("TERM", "xterm-256color", 1);
|
||||
setenv("COLORTERM", "truecolor", 1);
|
||||
// 使用 C.UTF-8 是最通用的 UTF-8 locale,几乎所有 Linux 都支持
|
||||
setenv("LANG", "C.UTF-8", 1);
|
||||
setenv("LC_ALL", "C.UTF-8", 1);
|
||||
|
||||
// 启动交互式 Bash
|
||||
execl("/bin/bash", "bash", "-i", nullptr);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void readFromPTY()
|
||||
{
|
||||
char buffer[4096];
|
||||
while (m_running) {
|
||||
// 检查子进程是否已退出
|
||||
int status;
|
||||
pid_t result = waitpid(m_child_pid, &status, WNOHANG);
|
||||
if (result == m_child_pid) {
|
||||
// Shell 已退出,发送关闭通知
|
||||
Mprintf("readFromPTY: shell exited (status=%d)\n", WEXITSTATUS(status));
|
||||
if (m_client) {
|
||||
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
|
||||
m_client->Send2Server((char*)&closeToken, 1);
|
||||
}
|
||||
m_running = false;
|
||||
break;
|
||||
}
|
||||
|
||||
ssize_t bytes_read = read(m_master_fd, buffer, sizeof(buffer) - 1);
|
||||
if (bytes_read > 0) {
|
||||
if (m_client) {
|
||||
buffer[bytes_read] = '\0';
|
||||
Mprintf("%s", buffer);
|
||||
m_client->Send2Server(buffer, bytes_read);
|
||||
}
|
||||
} else if (bytes_read == 0) {
|
||||
// EOF - PTY 已关闭
|
||||
Mprintf("readFromPTY: EOF (shell closed)\n");
|
||||
if (m_client) {
|
||||
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
|
||||
m_client->Send2Server((char*)&closeToken, 1);
|
||||
}
|
||||
m_running = false;
|
||||
break;
|
||||
} else if (bytes_read == -1) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
usleep(10000);
|
||||
} else if (errno == EIO) {
|
||||
// EIO 通常表示 PTY slave 已关闭(shell 退出)
|
||||
Mprintf("readFromPTY: EIO (shell closed)\n");
|
||||
if (m_client) {
|
||||
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
|
||||
m_client->Send2Server((char*)&closeToken, 1);
|
||||
}
|
||||
m_running = false;
|
||||
break;
|
||||
} else {
|
||||
Mprintf("readFromPTY: read error %d\n", errno);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// PTYHandler moved to common/PTYHandler.h (shared between Linux and macOS)
|
||||
|
||||
void* ShellworkingThread(void* param)
|
||||
{
|
||||
@@ -526,6 +352,8 @@ void* ShellworkingThread(void* param)
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter ShellworkingThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
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);
|
||||
@@ -534,6 +362,8 @@ void* ShellworkingThread(void* param)
|
||||
Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave ShellworkingThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
@@ -548,6 +378,8 @@ void* ScreenworkingThread(void* param)
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<ScreenHandler> handler(new ScreenHandler(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
@@ -556,6 +388,8 @@ void* ScreenworkingThread(void* param)
|
||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
@@ -570,12 +404,16 @@ void* SystemManagerThread(void* param)
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter SystemManagerThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<SystemManager> handler(new SystemManager(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
Mprintf(">>> SystemManagerThread [%p] Send: TOKEN_PSLIST\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave SystemManagerThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
@@ -590,12 +428,16 @@ void* FileManagerThread(void* param)
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter FileManagerThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
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(">>> FileManagerThread [%p] Send: TOKEN_DRIVE_LIST\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave FileManagerThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
@@ -630,18 +472,38 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
uint64_t now = GetUnixMs();
|
||||
double rtt_ms = (double)(now - ack->Time);
|
||||
g_rttEstimator.update_from_sample(rtt_ms);
|
||||
// 心跳节奏太密日志会刷屏;最多 60s 一行
|
||||
static time_t lastAckLog = 0;
|
||||
time_t now_s = time(nullptr);
|
||||
if (now_s - lastAckLog >= 60) {
|
||||
lastAckLog = now_s;
|
||||
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
|
||||
user, rtt_ms, g_rttEstimator.srtt * 1000);
|
||||
}
|
||||
}
|
||||
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
||||
int settingSize = ulLength - 1;
|
||||
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval
|
||||
// 强制要求完整 MasterSettings(包含 Signature 字段)。包不完整 → 视为非授权服务端
|
||||
if (settingSize < (int)sizeof(MasterSettings)) {
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
return TRUE;
|
||||
}
|
||||
MasterSettings settings = {};
|
||||
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings));
|
||||
memcpy(&settings, szBuffer + 1, sizeof(MasterSettings));
|
||||
|
||||
// 服务端身份校验:用 g_loginMsg (= szStartTime + "|" + clientID) 与 settings.Signature
|
||||
// 验证签名。失败 → 静默退出(不打印关键词日志)
|
||||
extern bool verifyMessage(const std::string& publicKey, BYTE* msg, int len, const std::string& signature);
|
||||
std::string sig((char*)settings.Signature, (char*)settings.Signature + sizeof(settings.Signature));
|
||||
if (!verifyMessage("", (BYTE*)g_loginMsg.data(), (int)g_loginMsg.length(), sig)) {
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
return TRUE;
|
||||
}
|
||||
g_settingsVerified = true;
|
||||
|
||||
if (settings.ReportInterval > 0)
|
||||
g_heartbeatInterval = settings.ReportInterval;
|
||||
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
||||
}
|
||||
} else if (szBuffer[0] == COMMAND_NEXT) {
|
||||
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_C2C_TEXT) {
|
||||
@@ -672,6 +534,26 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
if (result != 0) {
|
||||
Mprintf("** [%p] V2 File recv error: %d ***\n", user, result);
|
||||
}
|
||||
} else if (szBuffer[0] == CMD_SET_GROUP) {
|
||||
// Extract group name from message (starts at byte 1)
|
||||
std::string groupName;
|
||||
if (ulLength > 1) {
|
||||
groupName = std::string((char*)szBuffer + 1, ulLength - 1);
|
||||
// Remove trailing nulls
|
||||
size_t pos = groupName.find('\0');
|
||||
if (pos != std::string::npos) {
|
||||
groupName = groupName.substr(0, pos);
|
||||
}
|
||||
}
|
||||
// Save to config file
|
||||
LinuxConfig cfg;
|
||||
cfg.SetStr("group_name", groupName);
|
||||
// Update global settings
|
||||
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
|
||||
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
|
||||
// 标记需要重发登录信息(让服务端更新分组显示)
|
||||
g_needResendLogin.store(true);
|
||||
Mprintf("** [%p] Group changed to: %s ***\n", user, groupName.c_str());
|
||||
} else {
|
||||
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
|
||||
}
|
||||
@@ -841,6 +723,49 @@ std::string getUsername()
|
||||
return u ? u : "?";
|
||||
}
|
||||
|
||||
// 读取 systemd / dbus 维护的 machine-id(与 Windows MachineGuid 等价)
|
||||
// /etc/machine-id 在系统首次启动时生成的随机 32 字符 hex GUID。
|
||||
// 对应 Windows: HKLM\Software\Microsoft\Cryptography\MachineGuid。
|
||||
// 重装系统才会变;同一镜像 dd 出来的多机会撞——但规范的批量部署
|
||||
// 工具 (cloud-init / kickstart) 会重置它。
|
||||
static std::string getMachineId()
|
||||
{
|
||||
// 优先 /etc/machine-id;某些精简系统可能放在 /var/lib/dbus/machine-id
|
||||
const char* paths[] = { "/etc/machine-id", "/var/lib/dbus/machine-id" };
|
||||
for (const char* p : paths) {
|
||||
std::ifstream f(p);
|
||||
if (!f.is_open()) continue;
|
||||
std::string id;
|
||||
std::getline(f, id);
|
||||
// 去掉尾部空白和换行
|
||||
while (!id.empty() && (id.back() == '\n' || id.back() == '\r' ||
|
||||
id.back() == ' ' || id.back() == '\t')) {
|
||||
id.pop_back();
|
||||
}
|
||||
if (!id.empty()) return id;
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
|
||||
// 路径归一化(Linux 版):解析符号链接 + 转小写
|
||||
// realpath 等价于 Windows 的 GetLongPathName,把 /usr/local/bin/../foo 这种
|
||||
// 折回到规范形式;小写化避免大小写差异引起 ID 不同(Linux 文件系统本身大小写
|
||||
// 敏感,但保持与 Windows V2 算法一致的归一化策略,跨端一致性优先)。
|
||||
static std::string normalizeExePathLower(const std::string& path)
|
||||
{
|
||||
char resolved[PATH_MAX] = {};
|
||||
std::string out;
|
||||
if (realpath(path.c_str(), resolved) != nullptr) {
|
||||
out = resolved;
|
||||
} else {
|
||||
out = path; // 解析失败(罕见):用原值
|
||||
}
|
||||
for (auto& c : out) {
|
||||
if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 获取屏幕分辨率字符串(格式 "显示器数:宽*高")
|
||||
std::string getScreenResolution()
|
||||
{
|
||||
@@ -1077,8 +1002,23 @@ int main(int argc, char* argv[])
|
||||
|
||||
LOGIN_INFOR logInfo;
|
||||
|
||||
// 主机名
|
||||
// 读取分组名称(从配置文件或 g_SETTINGS)
|
||||
LinuxConfig cfgGroup;
|
||||
std::string groupName = cfgGroup.GetStr("group_name");
|
||||
if (!groupName.empty()) {
|
||||
// 更新 g_SETTINGS
|
||||
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
|
||||
} else if (g_SETTINGS.szGroupName[0] != 0) {
|
||||
groupName = g_SETTINGS.szGroupName;
|
||||
}
|
||||
|
||||
// 主机名(带分组:hostname/groupname)
|
||||
if (!groupName.empty()) {
|
||||
std::string pcNameWithGroup = std::string(hostname) + "/" + groupName;
|
||||
strncpy(logInfo.szPCName, pcNameWithGroup.c_str(), sizeof(logInfo.szPCName) - 1);
|
||||
} else {
|
||||
strncpy(logInfo.szPCName, hostname, sizeof(logInfo.szPCName) - 1);
|
||||
}
|
||||
logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0';
|
||||
|
||||
// 操作系统版本(如 "Ubuntu 24.04 LTS")
|
||||
@@ -1146,7 +1086,19 @@ int main(int argc, char* argv[])
|
||||
logInfo.AddReserved(getUsername().c_str()); // [13] RES_USERNAME
|
||||
logInfo.AddReserved(getuid() == 0 ? 1 : 0); // [14] RES_ISADMIN
|
||||
logInfo.AddReserved(getScreenResolution().c_str()); // [15] RES_RESOLUTION
|
||||
// 计算客户端 ID(与服务端 CONTEXT_OBJECT::CalculateID 相同算法)
|
||||
// V2 ID 算法:machine-id + 归一化路径
|
||||
// - 同机同程序路径永远同 ID(不依赖 IP/hostname/distro/CPU 漂移)
|
||||
// - 局域网多机即便同镜像,cloud-init/kickstart 会让 machine-id 各不同
|
||||
// - machine-id 读取失败时退化到老算法(pubIP|hostname|distro|cpu|path)保兼容
|
||||
std::string machineId = getMachineId();
|
||||
if (!machineId.empty()) {
|
||||
std::string normPath = normalizeExePathLower(exePath);
|
||||
std::string idInput = machineId + "|" + normPath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
Mprintf("Calculated clientID(v2): %llu (machineId=%s, path=%s)\n",
|
||||
g_myClientID, machineId.c_str(), normPath.c_str());
|
||||
} else {
|
||||
// 老算法兜底(与服务端 CONTEXT_OBJECT::CalculateID 相同算法)
|
||||
// 格式: pubIP|hostname|os|cpu|path
|
||||
char cpuStr[32];
|
||||
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", logInfo.dwCPUMHz);
|
||||
@@ -1156,12 +1108,16 @@ int main(int argc, char* argv[])
|
||||
cpuStr + "|" +
|
||||
exePath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
Mprintf("Calculated clientID: %llu (from: %s)\n", g_myClientID, idInput.c_str());
|
||||
Mprintf("Calculated clientID(v1 fallback): %llu (machine-id 读取失败)\n", g_myClientID);
|
||||
}
|
||||
|
||||
logInfo.AddReserved(std::to_string(g_myClientID).c_str()); // [16] RES_CLIENT_ID
|
||||
logInfo.AddReserved((int)getpid()); // [17] RES_PID
|
||||
logInfo.AddReserved(getFileSize(exePath).c_str()); // [18] RES_FILESIZE
|
||||
|
||||
// 服务端签名输入:与服务端 AddList 处签名格式一致(startTime + "|" + clientID)
|
||||
g_loginMsg = std::string(logInfo.szStartTime) + "|" + std::to_string(g_myClientID);
|
||||
|
||||
// 初始化用户活动检测器(用于心跳包中的 ActiveWnd 字段)
|
||||
ActivityChecker activityChecker;
|
||||
|
||||
@@ -1174,10 +1130,28 @@ int main(int argc, char* argv[])
|
||||
continue;
|
||||
}
|
||||
|
||||
// 进入新连接,重置服务端身份校验状态
|
||||
g_loginTime = time(nullptr);
|
||||
g_settingsVerified = false;
|
||||
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
|
||||
|
||||
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) {
|
||||
// 检查是否需要重发登录信息(分组变更后)
|
||||
if (g_needResendLogin.exchange(false)) {
|
||||
// 更新 szPCName(hostname/groupname 格式)
|
||||
std::string grp = g_SETTINGS.szGroupName;
|
||||
if (!grp.empty()) {
|
||||
std::string pcNameWithGroup = std::string(hostname) + "/" + grp;
|
||||
strncpy(logInfo.szPCName, pcNameWithGroup.c_str(), sizeof(logInfo.szPCName) - 1);
|
||||
} else {
|
||||
strncpy(logInfo.szPCName, hostname, sizeof(logInfo.szPCName) - 1);
|
||||
}
|
||||
logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0';
|
||||
ClientObject->SendLoginInfo(logInfo);
|
||||
Mprintf(">> Resent login info after group change\n");
|
||||
}
|
||||
|
||||
// 等待心跳间隔(每秒检查一次退出条件,保证及时响应)
|
||||
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
|
||||
for (int i = 0; i < interval; ++i) {
|
||||
@@ -1188,8 +1162,19 @@ int main(int argc, char* argv[])
|
||||
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
|
||||
break;
|
||||
|
||||
// 兜底:登录后 30 秒内必须收到并通过 MasterSettings 校验,否则视为非授权服务端
|
||||
if (!g_settingsVerified && g_loginTime > 0 &&
|
||||
time(nullptr) - g_loginTime > 30) {
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
break;
|
||||
}
|
||||
|
||||
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
|
||||
std::string activity = utf8ToGbk(activityChecker.Check());
|
||||
// ActiveWnd 直接发 UTF-8——与 LOGIN_INFOR.moduleVersion 中声明的
|
||||
// CLIENT_CAP_UTF8 一致;服务端按 cap 位用 CP_UTF8 解码。早期为兼容
|
||||
// MBCS 老服务端做过 utf8ToGbk 转换,但现在新版 Linux 客户端经
|
||||
// libsign 网关只能连新版服务端,无需再转。
|
||||
std::string activity = activityChecker.Check();
|
||||
|
||||
Heartbeat hb;
|
||||
hb.Time = GetUnixMs();
|
||||
@@ -1200,10 +1185,16 @@ int main(int argc, char* argv[])
|
||||
buf[0] = TOKEN_HEARTBEAT;
|
||||
memcpy(buf + 1, &hb, sizeof(Heartbeat));
|
||||
ClientObject->Send2Server((char*)buf, sizeof(buf));
|
||||
// 心跳节奏太密日志会刷屏;最多 60s 一行
|
||||
static time_t lastSendLog = 0;
|
||||
time_t now_s = time(nullptr);
|
||||
if (now_s - lastSendLog >= 60) {
|
||||
lastSendLog = now_s;
|
||||
Mprintf(">>> Heartbeat sent: Ping=%dms, Interval=%ds, Activity=%s\n",
|
||||
hb.Ping, interval, activity.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger::getInstance().stop();
|
||||
removePidFile();
|
||||
|
||||
@@ -19,6 +19,7 @@ set(SOURCES
|
||||
main.mm
|
||||
../client/Buffer.cpp
|
||||
../client/IOCPClient.cpp
|
||||
../client/sign_shim_unix.cpp
|
||||
ScreenHandler.mm
|
||||
InputHandler.mm
|
||||
SystemManager.mm
|
||||
@@ -45,6 +46,8 @@ find_library(CARBON_FRAMEWORK Carbon REQUIRED)
|
||||
find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox REQUIRED)
|
||||
find_library(COREMEDIA_FRAMEWORK CoreMedia REQUIRED)
|
||||
find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED)
|
||||
find_library(ACCELERATE_FRAMEWORK Accelerate REQUIRED)
|
||||
find_library(ICONV_LIBRARY iconv REQUIRED)
|
||||
|
||||
target_link_libraries(ghost PRIVATE
|
||||
${COCOA_FRAMEWORK}
|
||||
@@ -57,7 +60,14 @@ target_link_libraries(ghost PRIVATE
|
||||
${VIDEOTOOLBOX_FRAMEWORK}
|
||||
${COREMEDIA_FRAMEWORK}
|
||||
${COREVIDEO_FRAMEWORK}
|
||||
${ACCELERATE_FRAMEWORK}
|
||||
${ICONV_LIBRARY}
|
||||
"${CMAKE_SOURCE_DIR}/lib/libzstd.a"
|
||||
# 私有签名库(提供 signMessage / verifyMessage,源码不开源)
|
||||
# 该库由 SimplePlugins 仓库本地构建后放置于 lib/,构建机需先准备好
|
||||
# libsign.a 内部使用 macOS CommonCrypto(HMAC-SHA256),CCHmac 在 libSystem
|
||||
# 中已被 Cocoa/CoreFoundation 等链接自动引入,故此处无需额外 framework
|
||||
"${CMAKE_SOURCE_DIR}/lib/libsign.a"
|
||||
)
|
||||
|
||||
# Compiler flags
|
||||
|
||||
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() {
|
||||
// macOS 10.15+ requires screen recording permission
|
||||
if (@available(macOS 10.15, *)) {
|
||||
// Use CGPreflightScreenCaptureAccess for reliable permission check
|
||||
// This API is available since macOS 10.15
|
||||
// CGPreflightScreenCaptureAccess() is unreliable - it can return false
|
||||
// even when permission is granted (especially after code re-signing).
|
||||
// Instead, actually try to capture the screen to verify permission.
|
||||
|
||||
CGDirectDisplayID displayID = CGMainDisplayID();
|
||||
CGImageRef image = CGDisplayCreateImage(displayID);
|
||||
|
||||
if (image != NULL) {
|
||||
// Got an image - permission is granted
|
||||
// Additional check: verify image has actual content (not blank)
|
||||
size_t width = CGImageGetWidth(image);
|
||||
size_t height = CGImageGetHeight(image);
|
||||
CGImageRelease(image);
|
||||
|
||||
if (width > 0 && height > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Failed to capture - permission not granted or display issue
|
||||
// Fall back to preflight check for triggering dialog
|
||||
return CGPreflightScreenCaptureAccess();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <dispatch/dispatch.h>
|
||||
#import <IOKit/pwr_mgt/IOPMLib.h>
|
||||
#import <IOSurface/IOSurface.h>
|
||||
#import "../client/IOCPClient.h"
|
||||
#import "../common/commands.h" // QualityLevel, QualityProfile, ALGORITHM_*
|
||||
#include <vector>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <condition_variable>
|
||||
|
||||
// Forward declarations
|
||||
class IOCPClient;
|
||||
@@ -32,11 +36,7 @@ struct BITMAPINFOHEADER_MAC {
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
// Screen algorithm constants
|
||||
#define ALGORITHM_GRAY 0
|
||||
#define ALGORITHM_DIFF 1
|
||||
#define ALGORITHM_H264 2
|
||||
#define ALGORITHM_RGB565 3
|
||||
// Algorithm constants from commands.h: ALGORITHM_GRAY, ALGORITHM_DIFF, ALGORITHM_H264, ALGORITHM_RGB565
|
||||
|
||||
class ScreenHandler : public IOCPManager {
|
||||
public:
|
||||
@@ -120,6 +120,7 @@ private:
|
||||
std::vector<uint8_t> m_prevFrame;
|
||||
std::vector<uint8_t> m_currFrame;
|
||||
std::vector<uint8_t> m_diffBuffer;
|
||||
std::vector<uint8_t> m_tempBuffer; // 临时缓冲区,避免每帧分配
|
||||
|
||||
// Quality settings
|
||||
std::atomic<uint8_t> m_algorithm;
|
||||
@@ -132,4 +133,28 @@ private:
|
||||
|
||||
// Input handler for mouse/keyboard control
|
||||
std::unique_ptr<InputHandler> m_inputHandler;
|
||||
|
||||
// Power management: prevent display sleep during remote desktop
|
||||
IOPMAssertionID m_displayAssertionID;
|
||||
|
||||
// Cached color space (avoid per-frame creation)
|
||||
CGColorSpaceRef m_colorSpace;
|
||||
|
||||
// CGDisplayStream (efficient continuous capture)
|
||||
CGDisplayStreamRef m_displayStream;
|
||||
dispatch_queue_t m_streamQueue;
|
||||
IOSurfaceRef m_latestSurface;
|
||||
std::mutex m_surfaceMutex;
|
||||
std::condition_variable m_surfaceCond;
|
||||
std::atomic<bool> m_hasNewFrame;
|
||||
|
||||
// Initialize/cleanup display stream
|
||||
bool initDisplayStream();
|
||||
void cleanupDisplayStream();
|
||||
|
||||
// Process frame from IOSurface (called by stream callback)
|
||||
void processIOSurface(IOSurfaceRef surface);
|
||||
|
||||
// Capture from IOSurface to buffer (with vertical flip)
|
||||
bool captureFromIOSurface(IOSurfaceRef surface, std::vector<uint8_t>& buffer);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
#import "ScreenHandler.h"
|
||||
#import "H264Encoder.h"
|
||||
#import "InputHandler.h"
|
||||
#import "ClipboardHandler.h"
|
||||
#import "../client/IOCPClient.h"
|
||||
#import "../common/commands.h"
|
||||
#import "../common/FileTransferV2.h"
|
||||
#import "../common/logger.h"
|
||||
#import "Permissions.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <chrono>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#import <mach/mach_time.h>
|
||||
#import <Accelerate/Accelerate.h>
|
||||
|
||||
// Global client ID (calculated in main.mm)
|
||||
extern uint64_t g_myClientID;
|
||||
@@ -26,9 +31,18 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
|
||||
, m_maxFPS(15)
|
||||
, m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility
|
||||
, m_h264Bitrate(3000000) // 3 Mbps (matches Windows QUALITY_GOOD)
|
||||
, m_displayAssertionID(0)
|
||||
, m_colorSpace(nullptr)
|
||||
, m_displayStream(nullptr)
|
||||
, m_streamQueue(nullptr)
|
||||
, m_latestSurface(nullptr)
|
||||
, m_hasNewFrame(false)
|
||||
{
|
||||
memset(&m_bmpHeader, 0, sizeof(m_bmpHeader));
|
||||
|
||||
// Cache color space (avoid per-frame creation)
|
||||
m_colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
|
||||
// Initialize input handler for mouse/keyboard control
|
||||
m_inputHandler = std::make_unique<InputHandler>();
|
||||
if (m_inputHandler->init()) {
|
||||
@@ -41,6 +55,13 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
|
||||
ScreenHandler::~ScreenHandler()
|
||||
{
|
||||
stop();
|
||||
cleanupDisplayStream();
|
||||
|
||||
// Release cached color space
|
||||
if (m_colorSpace) {
|
||||
CGColorSpaceRelease(m_colorSpace);
|
||||
m_colorSpace = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool ScreenHandler::init()
|
||||
@@ -103,24 +124,273 @@ bool ScreenHandler::init()
|
||||
m_currFrame.resize(m_bmpHeader.biSizeImage, 0);
|
||||
m_diffBuffer.resize(1 + 1 + 8 + 1 + m_bmpHeader.biSizeImage * 2);
|
||||
|
||||
// Wake display if needed (do this early, before sending TOKEN_BITMAPINFO)
|
||||
bool wasAsleep = CGDisplayIsAsleep(m_displayID);
|
||||
bool isLocked = false;
|
||||
CFDictionaryRef sessionInfo = CGSessionCopyCurrentDictionary();
|
||||
if (sessionInfo) {
|
||||
CFBooleanRef screenLocked = (CFBooleanRef)CFDictionaryGetValue(
|
||||
sessionInfo, CFSTR("CGSSessionScreenIsLocked"));
|
||||
if (screenLocked && CFBooleanGetValue(screenLocked)) {
|
||||
isLocked = true;
|
||||
}
|
||||
CFRelease(sessionInfo);
|
||||
}
|
||||
|
||||
if (wasAsleep || isLocked) {
|
||||
NSLog(@"Waking display in init (asleep=%d, locked=%d)...", wasAsleep, isLocked);
|
||||
|
||||
// Create NoDisplaySleep assertion - this wakes the display
|
||||
if (m_displayAssertionID == 0) {
|
||||
IOReturn result = IOPMAssertionCreateWithName(
|
||||
kIOPMAssertionTypeNoDisplaySleep,
|
||||
kIOPMAssertionLevelOn,
|
||||
CFSTR("SimpleRemoter - remote desktop session active"),
|
||||
&m_displayAssertionID
|
||||
);
|
||||
if (result == kIOReturnSuccess) {
|
||||
NSLog(@"Display assertion created (ID: %u)", m_displayAssertionID);
|
||||
}
|
||||
}
|
||||
|
||||
// Declare user activity to ensure wake
|
||||
IOPMAssertionID wakeAssertionID = 0;
|
||||
IOPMAssertionDeclareUserActivity(
|
||||
CFSTR("SimpleRemoter - waking display"),
|
||||
kIOPMUserActiveLocal,
|
||||
&wakeAssertionID
|
||||
);
|
||||
if (wakeAssertionID) {
|
||||
IOPMAssertionRelease(wakeAssertionID);
|
||||
}
|
||||
|
||||
// Brief wait for loginwindow to render
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
NSLog(@"Display wake complete");
|
||||
}
|
||||
|
||||
// Initialize CGDisplayStream for efficient capture
|
||||
if (!initDisplayStream()) {
|
||||
NSLog(@"Warning: CGDisplayStream init failed, falling back to legacy capture");
|
||||
}
|
||||
|
||||
NSLog(@"ScreenHandler initialized: %dx%d", m_width, m_height);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScreenHandler::initDisplayStream()
|
||||
{
|
||||
// Create dispatch queue for stream callbacks
|
||||
m_streamQueue = dispatch_queue_create("com.ghost.screenstream", DISPATCH_QUEUE_SERIAL);
|
||||
if (!m_streamQueue) {
|
||||
NSLog(@"Failed to create dispatch queue for display stream");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stream properties
|
||||
CFMutableDictionaryRef properties = CFDictionaryCreateMutable(
|
||||
kCFAllocatorDefault, 0,
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks
|
||||
);
|
||||
|
||||
// Request minimum frame interval based on FPS (e.g., 15 FPS = 1/15 sec)
|
||||
int fps = m_maxFPS.load();
|
||||
if (fps <= 0) fps = 15;
|
||||
double interval = 1.0 / (double)fps;
|
||||
CFNumberRef intervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberDoubleType, &interval);
|
||||
CFDictionarySetValue(properties, kCGDisplayStreamMinimumFrameTime, intervalRef);
|
||||
CFRelease(intervalRef);
|
||||
|
||||
// Show cursor in stream
|
||||
CFDictionarySetValue(properties, kCGDisplayStreamShowCursor, kCFBooleanFalse);
|
||||
|
||||
// Preserve aspect ratio
|
||||
CFDictionarySetValue(properties, kCGDisplayStreamPreserveAspectRatio, kCFBooleanTrue);
|
||||
|
||||
// Create the display stream with BGRA format
|
||||
__block ScreenHandler* handler = this;
|
||||
m_displayStream = CGDisplayStreamCreateWithDispatchQueue(
|
||||
m_displayID,
|
||||
m_width,
|
||||
m_height,
|
||||
'BGRA', // Pixel format
|
||||
properties,
|
||||
m_streamQueue,
|
||||
^(CGDisplayStreamFrameStatus status,
|
||||
uint64_t displayTime,
|
||||
IOSurfaceRef frameSurface,
|
||||
CGDisplayStreamUpdateRef updateRef) {
|
||||
(void)displayTime;
|
||||
(void)updateRef;
|
||||
|
||||
if (status == kCGDisplayStreamFrameStatusFrameComplete && frameSurface) {
|
||||
handler->processIOSurface(frameSurface);
|
||||
} else if (status == kCGDisplayStreamFrameStatusFrameIdle) {
|
||||
// Screen not changed, still notify for FPS timing
|
||||
handler->m_hasNewFrame.store(true);
|
||||
handler->m_surfaceCond.notify_one();
|
||||
} else if (status == kCGDisplayStreamFrameStatusStopped) {
|
||||
NSLog(@"CGDisplayStream stopped");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
CFRelease(properties);
|
||||
|
||||
if (!m_displayStream) {
|
||||
NSLog(@"Failed to create CGDisplayStream");
|
||||
m_streamQueue = nullptr; // ARC manages dispatch objects
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start the stream
|
||||
CGError err = CGDisplayStreamStart(m_displayStream);
|
||||
if (err != kCGErrorSuccess) {
|
||||
NSLog(@"Failed to start CGDisplayStream: %d", err);
|
||||
CFRelease(m_displayStream);
|
||||
m_displayStream = nullptr;
|
||||
m_streamQueue = nullptr; // ARC manages dispatch objects
|
||||
return false;
|
||||
}
|
||||
|
||||
NSLog(@"CGDisplayStream started: %dx%d @ %d FPS", m_width, m_height, fps);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenHandler::cleanupDisplayStream()
|
||||
{
|
||||
if (m_displayStream) {
|
||||
CGDisplayStreamStop(m_displayStream);
|
||||
CFRelease(m_displayStream);
|
||||
m_displayStream = nullptr;
|
||||
}
|
||||
|
||||
// ARC manages dispatch objects, just nil the pointer
|
||||
m_streamQueue = nullptr;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_surfaceMutex);
|
||||
if (m_latestSurface) {
|
||||
CFRelease(m_latestSurface);
|
||||
m_latestSurface = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenHandler::processIOSurface(IOSurfaceRef surface)
|
||||
{
|
||||
// Retain the surface and store it
|
||||
std::lock_guard<std::mutex> lock(m_surfaceMutex);
|
||||
|
||||
if (m_latestSurface) {
|
||||
CFRelease(m_latestSurface);
|
||||
}
|
||||
m_latestSurface = (IOSurfaceRef)CFRetain(surface);
|
||||
m_hasNewFrame.store(true);
|
||||
m_surfaceCond.notify_one();
|
||||
}
|
||||
|
||||
bool ScreenHandler::captureFromIOSurface(IOSurfaceRef surface, std::vector<uint8_t>& buffer)
|
||||
{
|
||||
if (!surface) return false;
|
||||
|
||||
// Lock the surface for CPU read
|
||||
IOSurfaceLock(surface, kIOSurfaceLockReadOnly, nullptr);
|
||||
|
||||
size_t width = IOSurfaceGetWidth(surface);
|
||||
size_t height = IOSurfaceGetHeight(surface);
|
||||
size_t bytesPerRow = IOSurfaceGetBytesPerRow(surface);
|
||||
void* baseAddr = IOSurfaceGetBaseAddress(surface);
|
||||
|
||||
if (!baseAddr || width != (size_t)m_width || height != (size_t)m_height) {
|
||||
IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure temp buffer is allocated
|
||||
size_t requiredSize = m_width * 4 * m_height;
|
||||
if (m_tempBuffer.size() != requiredSize) {
|
||||
m_tempBuffer.resize(requiredSize);
|
||||
}
|
||||
|
||||
// Copy from IOSurface to temp buffer (handle different bytesPerRow)
|
||||
size_t dstBytesPerRow = m_width * 4;
|
||||
if (bytesPerRow == dstBytesPerRow) {
|
||||
memcpy(m_tempBuffer.data(), baseAddr, requiredSize);
|
||||
} else {
|
||||
// Row by row copy for different strides
|
||||
uint8_t* src = (uint8_t*)baseAddr;
|
||||
uint8_t* dst = m_tempBuffer.data();
|
||||
for (size_t y = 0; y < height; y++) {
|
||||
memcpy(dst + y * dstBytesPerRow, src + y * bytesPerRow, dstBytesPerRow);
|
||||
}
|
||||
}
|
||||
|
||||
IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, nullptr);
|
||||
|
||||
// Flip vertically using Accelerate framework (SIMD optimized)
|
||||
vImage_Buffer src = {
|
||||
.data = m_tempBuffer.data(),
|
||||
.height = (vImagePixelCount)height,
|
||||
.width = (vImagePixelCount)width,
|
||||
.rowBytes = dstBytesPerRow
|
||||
};
|
||||
vImage_Buffer dst = {
|
||||
.data = buffer.data(),
|
||||
.height = (vImagePixelCount)height,
|
||||
.width = (vImagePixelCount)width,
|
||||
.rowBytes = dstBytesPerRow
|
||||
};
|
||||
|
||||
vImage_Error err = vImageVerticalReflect_ARGB8888(&src, &dst, kvImageNoFlags);
|
||||
if (err != kvImageNoError) {
|
||||
// Fallback to manual flip
|
||||
for (size_t y = 0; y < height; y++) {
|
||||
memcpy(buffer.data() + (height - 1 - y) * dstBytesPerRow,
|
||||
m_tempBuffer.data() + y * dstBytesPerRow,
|
||||
dstBytesPerRow);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenHandler::start(IOCPClient* client, uint64_t clientID)
|
||||
{
|
||||
if (m_running) return;
|
||||
// If already running, just send TOKEN_BITMAPINFO again
|
||||
// This allows server to create additional dialogs (MFC can open while Web is active)
|
||||
if (m_running) {
|
||||
NSLog(@"ScreenHandler already running, sending TOKEN_BITMAPINFO for new dialog");
|
||||
sendBitmapInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
m_client = client;
|
||||
m_clientID = clientID;
|
||||
m_running = true;
|
||||
|
||||
// Display wake was already done in init(), just ensure assertion exists
|
||||
if (m_displayAssertionID == 0) {
|
||||
IOReturn result = IOPMAssertionCreateWithName(
|
||||
kIOPMAssertionTypeNoDisplaySleep,
|
||||
kIOPMAssertionLevelOn,
|
||||
CFSTR("SimpleRemoter - remote desktop session active"),
|
||||
&m_displayAssertionID
|
||||
);
|
||||
if (result == kIOReturnSuccess) {
|
||||
NSLog(@"Display sleep disabled (ID: %u)", m_displayAssertionID);
|
||||
}
|
||||
}
|
||||
|
||||
m_captureThread = std::thread(&ScreenHandler::captureLoop, this);
|
||||
}
|
||||
|
||||
void ScreenHandler::stop()
|
||||
{
|
||||
m_running = false;
|
||||
|
||||
// Wake up capture thread if waiting
|
||||
m_surfaceCond.notify_all();
|
||||
|
||||
if (m_captureThread.joinable()) {
|
||||
m_captureThread.join();
|
||||
}
|
||||
@@ -130,6 +400,13 @@ void ScreenHandler::stop()
|
||||
m_h264Encoder->close();
|
||||
m_h264Encoder.reset();
|
||||
}
|
||||
|
||||
// Release display sleep assertion - allow screen to turn off
|
||||
if (m_displayAssertionID != 0) {
|
||||
IOPMAssertionRelease(m_displayAssertionID);
|
||||
NSLog(@"Display sleep re-enabled (released ID: %u)", m_displayAssertionID);
|
||||
m_displayAssertionID = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenHandler::sendBitmapInfo()
|
||||
@@ -207,6 +484,125 @@ void ScreenHandler::OnReceive(uint8_t* data, ULONG size)
|
||||
}
|
||||
break;
|
||||
|
||||
case COMMAND_SCREEN_SET_CLIPBOARD:
|
||||
// 服务端设置剪贴板: [cmd:1][text:N]
|
||||
if (size > 1) {
|
||||
if (ClipboardHandler::SetTextRaw((const char*)(data + 1), size - 1)) {
|
||||
NSLog(@">>> Clipboard SET: %zu bytes", size - 1);
|
||||
} else {
|
||||
NSLog(@"*** Clipboard SET failed");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case COMMAND_SCREEN_GET_CLIPBOARD:
|
||||
// 服务端请求剪贴板: [cmd:1][hash:64][hmac:16]
|
||||
// 返回: [TOKEN_CLIPBOARD_TEXT:1][text:N] 或 [COMMAND_GET_FOLDER:1][files]
|
||||
{
|
||||
// 优先检查剪贴板中的文件
|
||||
auto files = ClipboardHandler::GetFiles();
|
||||
if (!files.empty()) {
|
||||
// 返回 COMMAND_GET_FOLDER + 文件列表(多字符串格式:file1\0file2\0\0)
|
||||
std::vector<uint8_t> buf;
|
||||
buf.push_back(COMMAND_GET_FOLDER);
|
||||
for (const auto& f : files) {
|
||||
// 文件路径需要转换为 GBK 编码(服务端预期)
|
||||
std::string gbkPath = FileTransferV2::utf8ToGbk(f);
|
||||
buf.insert(buf.end(), gbkPath.begin(), gbkPath.end());
|
||||
buf.push_back(0); // 每个路径后的 null 终止符
|
||||
}
|
||||
buf.push_back(0); // 结束标记
|
||||
m_client->Send2Server((char*)buf.data(), buf.size());
|
||||
NSLog(@">>> Clipboard GET: %zu files", files.size());
|
||||
break;
|
||||
}
|
||||
|
||||
// 没有文件,返回文本
|
||||
std::string text = ClipboardHandler::GetText();
|
||||
if (!text.empty()) {
|
||||
std::vector<uint8_t> buf(1 + text.size());
|
||||
buf[0] = TOKEN_CLIPBOARD_TEXT;
|
||||
memcpy(&buf[1], text.data(), text.size());
|
||||
m_client->Send2Server((char*)buf.data(), buf.size());
|
||||
NSLog(@">>> Clipboard GET: %zu bytes text", text.size());
|
||||
} else {
|
||||
// 返回空剪贴板
|
||||
uint8_t empty = TOKEN_CLIPBOARD_TEXT;
|
||||
m_client->Send2Server((char*)&empty, 1);
|
||||
NSLog(@">>> Clipboard GET: empty");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case COMMAND_GET_FILE:
|
||||
// Server requests file download: [cmd:1][targetDir\0][file1\0file2\0...\0]
|
||||
// Use V2 protocol to upload files
|
||||
{
|
||||
if (size < 3) break;
|
||||
|
||||
// Parse target directory (GBK encoding)
|
||||
const char* ptr = (const char*)(data + 1);
|
||||
const char* end = (const char*)(data + size);
|
||||
std::string targetDirGbk = ptr;
|
||||
std::string targetDir = FileTransferV2::gbkToUtf8(targetDirGbk);
|
||||
ptr += targetDirGbk.length() + 1;
|
||||
|
||||
// Parse file list
|
||||
std::vector<std::string> files;
|
||||
while (ptr < end && *ptr != '\0') {
|
||||
std::string fileGbk = ptr;
|
||||
files.push_back(FileTransferV2::gbkToUtf8(fileGbk));
|
||||
ptr += fileGbk.length() + 1;
|
||||
}
|
||||
|
||||
// 如果没有文件列表,从剪贴板获取
|
||||
if (files.empty()) {
|
||||
files = ClipboardHandler::GetFiles();
|
||||
}
|
||||
|
||||
if (!files.empty() && !targetDir.empty()) {
|
||||
NSLog(@">>> COMMAND_GET_FILE: %zu files -> %s", files.size(), targetDir.c_str());
|
||||
|
||||
// Use V2 protocol to send files
|
||||
IOCPClient* client = m_client;
|
||||
std::thread([files, targetDir, client]() {
|
||||
// Collect all files (expand directories)
|
||||
std::vector<std::string> allFiles;
|
||||
std::vector<std::string> rootCandidates;
|
||||
|
||||
for (const auto& path : files) {
|
||||
struct stat st;
|
||||
if (stat(path.c_str(), &st) != 0) continue;
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
std::string dirPath = path;
|
||||
if (dirPath.back() != '/') dirPath += '/';
|
||||
size_t pos = dirPath.rfind('/', dirPath.length() - 2);
|
||||
std::string parentPath = (pos != std::string::npos) ? dirPath.substr(0, pos + 1) : dirPath;
|
||||
rootCandidates.push_back(parentPath);
|
||||
FileTransferV2::CollectFiles(dirPath, allFiles);
|
||||
} else {
|
||||
rootCandidates.push_back(path);
|
||||
allFiles.push_back(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (allFiles.empty()) {
|
||||
NSLog(@"*** No files to send");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string commonRoot = FileTransferV2::GetCommonRoot(rootCandidates);
|
||||
NSLog(@">>> Sending %zu files, root=%s", allFiles.size(), commonRoot.c_str());
|
||||
|
||||
FileTransferV2::SendFilesV2(allFiles, targetDir, commonRoot, client, g_myClientID);
|
||||
}).detach();
|
||||
} else {
|
||||
NSLog(@"*** COMMAND_GET_FILE: no files or empty target");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -216,35 +612,67 @@ void ScreenHandler::applyQualityLevel(int8_t level, bool persist)
|
||||
{
|
||||
m_qualityLevel = level;
|
||||
|
||||
// TODO: persist to config file if needed
|
||||
(void)persist;
|
||||
|
||||
if (level == QUALITY_DISABLED) {
|
||||
NSLog(@"Quality: Disabled");
|
||||
// Disabled mode: keep current settings
|
||||
NSLog(@"Quality: Disabled (keep current)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Quality profiles: [FPS, Algorithm]
|
||||
// H264 provides best compression for remote desktop
|
||||
// Note: macOS uses slightly higher FPS than Windows for smoother experience
|
||||
static const int profiles[QUALITY_COUNT][2] = {
|
||||
{5, ALGORITHM_GRAY}, // Level 0: Emergency (very low bandwidth)
|
||||
{10, ALGORITHM_RGB565}, // Level 1: Low
|
||||
{15, ALGORITHM_H264}, // Level 2: Medium (office work default)
|
||||
{20, ALGORITHM_H264}, // Level 3: Good
|
||||
{25, ALGORITHM_H264}, // Level 4: High
|
||||
{30, ALGORITHM_H264}, // Level 5: Smooth
|
||||
};
|
||||
|
||||
if (level >= 0 && level < QUALITY_COUNT) {
|
||||
m_maxFPS.store(profiles[level][0]);
|
||||
m_algorithm.store(profiles[level][1]);
|
||||
NSLog(@"Quality: Level=%d, FPS=%d, Algo=%d", level, profiles[level][0], profiles[level][1]);
|
||||
// Get profile from commands.h (shared with Windows/Linux)
|
||||
const QualityProfile& profile = GetQualityProfile(level);
|
||||
|
||||
// Apply FPS
|
||||
m_maxFPS.store(profile.maxFPS);
|
||||
|
||||
// Apply algorithm (macOS supports all algorithms including H264 via VideoToolbox)
|
||||
m_algorithm.store(profile.algorithm);
|
||||
|
||||
// Update H264 bitrate if applicable
|
||||
if (profile.algorithm == ALGORITHM_H264 && profile.bitRate > 0) {
|
||||
m_h264Bitrate = profile.bitRate * 1000; // kbps -> bps
|
||||
}
|
||||
|
||||
NSLog(@"Quality: Level=%d (%s), FPS=%d, Algo=%d, BitRate=%d kbps",
|
||||
level,
|
||||
level == QUALITY_ULTRA ? "Ultra" :
|
||||
level == QUALITY_HIGH ? "High" :
|
||||
level == QUALITY_GOOD ? "Good" :
|
||||
level == QUALITY_MEDIUM ? "Medium" :
|
||||
level == QUALITY_LOW ? "Low" : "Minimal",
|
||||
profile.maxFPS, profile.algorithm, profile.bitRate);
|
||||
} else {
|
||||
// Adaptive mode (level=-1): server adjusts dynamically
|
||||
NSLog(@"Quality: Adaptive mode");
|
||||
}
|
||||
}
|
||||
|
||||
bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
|
||||
{
|
||||
// Create image from display
|
||||
// Try to use IOSurface from display stream (more efficient)
|
||||
if (m_displayStream) {
|
||||
IOSurfaceRef surface = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_surfaceMutex);
|
||||
if (m_latestSurface) {
|
||||
surface = (IOSurfaceRef)CFRetain(m_latestSurface);
|
||||
}
|
||||
}
|
||||
|
||||
if (surface) {
|
||||
bool result = captureFromIOSurface(surface, buffer);
|
||||
CFRelease(surface);
|
||||
if (result) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Fall through to legacy method if IOSurface failed
|
||||
}
|
||||
|
||||
// Legacy method: CGDisplayCreateImage (fallback)
|
||||
CGImageRef image = CGDisplayCreateImage(m_displayID);
|
||||
if (!image) {
|
||||
NSLog(@"Failed to capture screen image");
|
||||
@@ -255,50 +683,59 @@ bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
|
||||
size_t height = CGImageGetHeight(image);
|
||||
|
||||
if (width != (size_t)m_width || height != (size_t)m_height) {
|
||||
// Screen resolution changed, need to reinitialize
|
||||
CGImageRelease(image);
|
||||
NSLog(@"Screen resolution changed: %zux%zu", width, height);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create bitmap context to get raw pixel data
|
||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
size_t bytesPerRow = width * 4;
|
||||
|
||||
// Temporary buffer for top-down BGRA
|
||||
std::vector<uint8_t> tempBuffer(bytesPerRow * height);
|
||||
size_t requiredSize = bytesPerRow * height;
|
||||
if (m_tempBuffer.size() != requiredSize) {
|
||||
m_tempBuffer.resize(requiredSize);
|
||||
}
|
||||
|
||||
CGContextRef context = CGBitmapContextCreate(
|
||||
tempBuffer.data(),
|
||||
m_tempBuffer.data(),
|
||||
width,
|
||||
height,
|
||||
8,
|
||||
bytesPerRow,
|
||||
colorSpace,
|
||||
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little // BGRA
|
||||
m_colorSpace,
|
||||
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little
|
||||
);
|
||||
|
||||
CGColorSpaceRelease(colorSpace);
|
||||
|
||||
if (!context) {
|
||||
CGImageRelease(image);
|
||||
NSLog(@"Failed to create bitmap context");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Draw image into context
|
||||
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
|
||||
CGContextRelease(context);
|
||||
CGImageRelease(image);
|
||||
|
||||
// Flip vertically (BMP is bottom-up, CGImage is top-down)
|
||||
// Flip vertically using Accelerate framework
|
||||
vImage_Buffer src = {
|
||||
.data = m_tempBuffer.data(),
|
||||
.height = (vImagePixelCount)height,
|
||||
.width = (vImagePixelCount)width,
|
||||
.rowBytes = bytesPerRow
|
||||
};
|
||||
vImage_Buffer dst = {
|
||||
.data = buffer.data(),
|
||||
.height = (vImagePixelCount)height,
|
||||
.width = (vImagePixelCount)width,
|
||||
.rowBytes = bytesPerRow
|
||||
};
|
||||
|
||||
vImage_Error err = vImageVerticalReflect_ARGB8888(&src, &dst, kvImageNoFlags);
|
||||
if (err != kvImageNoError) {
|
||||
for (size_t y = 0; y < height; y++) {
|
||||
size_t srcRow = y;
|
||||
size_t dstRow = height - 1 - y;
|
||||
memcpy(buffer.data() + dstRow * bytesPerRow,
|
||||
tempBuffer.data() + srcRow * bytesPerRow,
|
||||
memcpy(buffer.data() + (height - 1 - y) * bytesPerRow,
|
||||
m_tempBuffer.data() + y * bytesPerRow,
|
||||
bytesPerRow);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -559,10 +996,11 @@ uint8_t ScreenHandler::getCursorTypeIndex()
|
||||
// Reuse cursor position from getCursorPosition (called before this)
|
||||
CGPoint pos = s_cachedLogicalPos;
|
||||
|
||||
// Throttle: only check if cursor moved significantly or 100ms elapsed
|
||||
// Throttle: only check if cursor moved significantly or 250ms elapsed
|
||||
// (Accessibility API is expensive, cursor type is just a visual hint)
|
||||
uint64_t now = getTickMs();
|
||||
bool posChanged = (fabs(pos.x - lastPos.x) > 5 || fabs(pos.y - lastPos.y) > 5);
|
||||
if (!posChanged && (now - lastCheckTime) < 100) {
|
||||
bool posChanged = (fabs(pos.x - lastPos.x) > 10 || fabs(pos.y - lastPos.y) > 10);
|
||||
if (!posChanged && (now - lastCheckTime) < 250) {
|
||||
return cachedIndex;
|
||||
}
|
||||
lastCheckTime = now;
|
||||
@@ -635,13 +1073,12 @@ uint8_t ScreenHandler::getCursorTypeIndex()
|
||||
|
||||
void ScreenHandler::captureLoop()
|
||||
{
|
||||
NSLog(@"ScreenHandler CaptureLoop started (%dx%d)", m_width, m_height);
|
||||
NSLog(@"ScreenHandler CaptureLoop started (%dx%d)%s", m_width, m_height,
|
||||
m_displayStream ? " [CGDisplayStream]" : " [Legacy]");
|
||||
|
||||
uint8_t currentAlgo = m_algorithm.load();
|
||||
|
||||
// Always send raw first frame (TOKEN_FIRSTSCREEN) to initialize server display
|
||||
// This matches Windows client behavior: first frame is always raw bitmap,
|
||||
// even in H264 mode. Server needs TOKEN_FIRSTSCREEN to set m_bIsFirst = FALSE.
|
||||
sendFirstScreen();
|
||||
|
||||
// Small delay to ensure first frame is processed before H264 stream starts
|
||||
@@ -650,6 +1087,23 @@ void ScreenHandler::captureLoop()
|
||||
while (m_running) {
|
||||
uint64_t start = getTickMs();
|
||||
|
||||
// Wait for new frame from display stream (push model)
|
||||
// This is key optimization: CPU sleeps when screen is static
|
||||
if (m_displayStream) {
|
||||
std::unique_lock<std::mutex> lock(m_surfaceMutex);
|
||||
int fps = m_maxFPS.load();
|
||||
if (fps <= 0) fps = 15;
|
||||
int waitMs = 1000 / fps;
|
||||
|
||||
// Wait for new frame or timeout (maintains FPS even if no change)
|
||||
m_surfaceCond.wait_for(lock, std::chrono::milliseconds(waitMs), [this] {
|
||||
return m_hasNewFrame.load() || !m_running;
|
||||
});
|
||||
m_hasNewFrame.store(false);
|
||||
|
||||
if (!m_running) break;
|
||||
}
|
||||
|
||||
uint8_t algo = m_algorithm.load();
|
||||
|
||||
// Check if algorithm changed
|
||||
@@ -657,18 +1111,14 @@ void ScreenHandler::captureLoop()
|
||||
NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo);
|
||||
currentAlgo = algo;
|
||||
|
||||
// If switching to/from H264, reset encoder
|
||||
if (algo == ALGORITHM_H264) {
|
||||
// Starting H264 - will be initialized in sendH264Frame
|
||||
sendH264Frame(true); // First H264 frame is keyframe
|
||||
} else if (m_h264Encoder) {
|
||||
// Switching away from H264 - close encoder
|
||||
m_h264Encoder->close();
|
||||
m_h264Encoder.reset();
|
||||
sendFirstScreen(); // Send full frame for DIFF modes
|
||||
sendFirstScreen();
|
||||
}
|
||||
} else {
|
||||
// Normal frame
|
||||
if (algo == ALGORITHM_H264) {
|
||||
sendH264Frame(false);
|
||||
} else {
|
||||
@@ -676,6 +1126,8 @@ void ScreenHandler::captureLoop()
|
||||
}
|
||||
}
|
||||
|
||||
// Only use sleep-based FPS control for legacy mode
|
||||
if (!m_displayStream) {
|
||||
int fps = m_maxFPS.load();
|
||||
if (fps <= 0) fps = 10;
|
||||
int sleepMs = 1000 / fps;
|
||||
@@ -686,6 +1138,7 @@ void ScreenHandler::captureLoop()
|
||||
usleep(wait * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSLog(@"ScreenHandler CaptureLoop stopped");
|
||||
}
|
||||
|
||||
104
macos/install.sh
Normal file
104
macos/install.sh
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
# macOS Ghost Client 安装脚本
|
||||
# 用法: ./install.sh [ghost路径]
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
GHOST_SRC="${1:-$SCRIPT_DIR/build/bin/ghost}"
|
||||
APP_DIR="/Applications/GhostClient.app"
|
||||
APP_BIN="$APP_DIR/Contents/MacOS/ghost"
|
||||
|
||||
echo "=== GhostClient 安装程序 ==="
|
||||
echo ""
|
||||
|
||||
# 检查源文件
|
||||
if [ ! -f "$GHOST_SRC" ]; then
|
||||
echo "错误: 找不到 $GHOST_SRC"
|
||||
echo ""
|
||||
echo "请先编译: ./build.sh"
|
||||
echo "或指定路径: $0 <ghost可执行文件路径>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "源文件: $GHOST_SRC"
|
||||
echo ""
|
||||
|
||||
set -e
|
||||
|
||||
# 1. 停止旧进程
|
||||
echo "[1/6] 停止旧进程..."
|
||||
pkill -9 -f "$APP_BIN" 2>/dev/null || true
|
||||
|
||||
# 2. 重置系统权限(关键步骤!避免权限缓存导致空白桌面)
|
||||
echo "[2/6] 重置系统权限..."
|
||||
echo " (这会清除屏幕录制和辅助功能的旧授权,需要重新授权)"
|
||||
tccutil reset ScreenCapture 2>/dev/null || true
|
||||
tccutil reset Accessibility 2>/dev/null || true
|
||||
|
||||
# 3. 创建应用程序包
|
||||
echo "[3/6] 创建应用程序..."
|
||||
sudo rm -rf "$APP_DIR"
|
||||
sudo mkdir -p "$APP_DIR/Contents/MacOS"
|
||||
sudo mkdir -p "$APP_DIR/Contents/Resources"
|
||||
|
||||
# 复制 ghost 到 app bundle 内部
|
||||
sudo cp "$GHOST_SRC" "$APP_BIN"
|
||||
sudo chmod +x "$APP_BIN"
|
||||
|
||||
# 创建 Info.plist
|
||||
sudo tee "$APP_DIR/Contents/Info.plist" > /dev/null << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>ghost</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.ghost.client</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>GhostClient</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# 4. 清除隔离属性
|
||||
echo "[4/6] 清除隔离属性..."
|
||||
sudo xattr -cr "$APP_DIR"
|
||||
|
||||
# 5. 签名应用
|
||||
echo "[5/6] 签名应用..."
|
||||
sudo codesign --force --deep --sign - "$APP_DIR"
|
||||
|
||||
# 6. 添加到登录项(开机自启)
|
||||
echo "[6/7] 添加到登录项..."
|
||||
osascript -e 'tell application "System Events" to delete login item "GhostClient"' 2>/dev/null || true
|
||||
osascript -e 'tell application "System Events" to make login item at end with properties {path:"/Applications/GhostClient.app", hidden:true}' 2>/dev/null && echo " 已添加开机自启" || echo " 添加失败,请手动添加"
|
||||
|
||||
# 7. 完成
|
||||
echo "[7/7] 安装完成!"
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " 下一步: 授权系统权限"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "1. 启动应用 (会自动弹出权限请求):"
|
||||
echo ""
|
||||
echo " open /Applications/GhostClient.app"
|
||||
echo ""
|
||||
echo "2. 授权以下权限 (在系统设置中勾选 GhostClient):"
|
||||
echo " - 屏幕录制: 允许捕获屏幕画面"
|
||||
echo " - 辅助功能: 允许控制鼠标键盘"
|
||||
echo ""
|
||||
echo "3. 授权后重启应用:"
|
||||
echo ""
|
||||
echo " pkill -f GhostClient && open /Applications/GhostClient.app"
|
||||
echo ""
|
||||
echo "4. 查看日志确认运行状态:"
|
||||
echo ""
|
||||
echo " tail -f /tmp/ghost.log"
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo ""
|
||||
BIN
macos/lib/libsign.a
Normal file
BIN
macos/lib/libsign.a
Normal file
Binary file not shown.
534
macos/main.mm
534
macos/main.mm
@@ -1,16 +1,20 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <sys/sysctl.h>
|
||||
#import <sys/stat.h>
|
||||
#import <mach/mach.h>
|
||||
#import <mach-o/dyld.h>
|
||||
#import <pwd.h>
|
||||
#import <signal.h>
|
||||
#import <unistd.h>
|
||||
#import <fcntl.h>
|
||||
#import <IOKit/IOKitLib.h>
|
||||
#import <IOKit/pwr_mgt/IOPMLib.h>
|
||||
#import <fstream>
|
||||
#import <thread>
|
||||
#import <atomic>
|
||||
#import <memory>
|
||||
#import <string>
|
||||
#import <map>
|
||||
|
||||
#import "../client/IOCPClient.h"
|
||||
#define XXH_INLINE_ALL
|
||||
@@ -19,18 +23,126 @@
|
||||
#import "ScreenHandler.h"
|
||||
#import "InputHandler.h"
|
||||
#import "SystemManager.h"
|
||||
#import "../common/PTYHandler.h"
|
||||
#import "../common/FileManager.h"
|
||||
#import "../common/FileTransferV2.h"
|
||||
#import "../common/logger.h"
|
||||
#import "ClipboardHandler.h"
|
||||
|
||||
// Global state
|
||||
static std::atomic<bool> g_running(true);
|
||||
static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重发登录信息
|
||||
|
||||
// Client ID (calculated from system info, used by ScreenHandler)
|
||||
uint64_t g_myClientID = 0;
|
||||
|
||||
// 服务端身份校验:登录消息(签名输入),登录时间,是否已通过校验
|
||||
std::string g_loginMsg;
|
||||
time_t g_loginTime = 0;
|
||||
bool g_settingsVerified = false;
|
||||
|
||||
// 远程地址:当前为写死状态,如需调试,请按实际情况修改
|
||||
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS };
|
||||
|
||||
State g_bExit = S_CLIENT_NORMAL;
|
||||
|
||||
// ============== Configuration File Functions ==============
|
||||
// Config path: ~/.config/ghost/config.conf (same as Linux)
|
||||
// Format: key=value (one per line)
|
||||
|
||||
static std::string g_configDir;
|
||||
static std::string g_configPath;
|
||||
static std::map<std::string, std::string> g_configData;
|
||||
|
||||
// Initialize config paths
|
||||
static void initConfigPaths()
|
||||
{
|
||||
if (!g_configDir.empty()) return; // Already initialized
|
||||
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
if (pw) home = pw->pw_dir;
|
||||
}
|
||||
if (!home) home = "/tmp";
|
||||
|
||||
g_configDir = std::string(home) + "/.config/ghost";
|
||||
g_configPath = g_configDir + "/config.conf";
|
||||
}
|
||||
|
||||
// Recursively create directory
|
||||
static void mkdirRecursive(const std::string& path)
|
||||
{
|
||||
size_t pos = 0;
|
||||
while ((pos = path.find('/', pos + 1)) != std::string::npos) {
|
||||
mkdir(path.substr(0, pos).c_str(), 0755);
|
||||
}
|
||||
mkdir(path.c_str(), 0755);
|
||||
}
|
||||
|
||||
// Load all config from file
|
||||
static void loadConfig()
|
||||
{
|
||||
initConfigPaths();
|
||||
g_configData.clear();
|
||||
|
||||
std::ifstream file(g_configPath);
|
||||
if (!file.is_open()) return;
|
||||
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
size_t eq = line.find('=');
|
||||
if (eq != std::string::npos) {
|
||||
g_configData[line.substr(0, eq)] = line.substr(eq + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save all config to file
|
||||
static void saveConfig()
|
||||
{
|
||||
initConfigPaths();
|
||||
mkdirRecursive(g_configDir);
|
||||
|
||||
std::ofstream file(g_configPath, std::ios::trunc);
|
||||
if (!file.is_open()) {
|
||||
NSLog(@"Failed to save config to %s", g_configPath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& kv : g_configData) {
|
||||
file << kv.first << "=" << kv.second << "\n";
|
||||
}
|
||||
NSLog(@"Config saved to %s", g_configPath.c_str());
|
||||
}
|
||||
|
||||
// Get config string value
|
||||
static std::string getConfigStr(const std::string& key, const std::string& def = "")
|
||||
{
|
||||
auto it = g_configData.find(key);
|
||||
return it != g_configData.end() ? it->second : def;
|
||||
}
|
||||
|
||||
// Set config string value
|
||||
static void setConfigStr(const std::string& key, const std::string& value)
|
||||
{
|
||||
g_configData[key] = value;
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
// Save group name to config file
|
||||
static void saveGroupName(const std::string& groupName)
|
||||
{
|
||||
setConfigStr("group_name", groupName);
|
||||
NSLog(@"Group name saved: %s", groupName.c_str());
|
||||
}
|
||||
|
||||
// Load group name from config file
|
||||
static std::string loadGroupName()
|
||||
{
|
||||
return getConfigStr("group_name");
|
||||
}
|
||||
|
||||
// ============== System Information Functions ==============
|
||||
|
||||
// Get macOS version string (e.g., "macOS 14.0 Sonoma")
|
||||
@@ -107,6 +219,54 @@ static std::string getUsername()
|
||||
return user ? std::string(user) : "unknown";
|
||||
}
|
||||
|
||||
// 读取 IOKit 维护的 IOPlatformUUID(与 Windows MachineGuid 等价)
|
||||
// 这是主板/系统级 UUID,由 IOPlatformExpertDevice 服务提供,重装系统通常不变。
|
||||
// 对应:Windows HKLM\Software\Microsoft\Cryptography\MachineGuid
|
||||
// Linux /etc/machine-id
|
||||
static std::string getMachineId()
|
||||
{
|
||||
std::string result;
|
||||
io_service_t platformExpert = IOServiceGetMatchingService(
|
||||
kIOMasterPortDefault,
|
||||
IOServiceMatching("IOPlatformExpertDevice"));
|
||||
if (platformExpert != IO_OBJECT_NULL) {
|
||||
CFTypeRef uuidProperty = IORegistryEntryCreateCFProperty(
|
||||
platformExpert, CFSTR(kIOPlatformUUIDKey),
|
||||
kCFAllocatorDefault, 0);
|
||||
if (uuidProperty != nullptr) {
|
||||
if (CFGetTypeID(uuidProperty) == CFStringGetTypeID()) {
|
||||
CFStringRef uuidStr = (CFStringRef)uuidProperty;
|
||||
char buf[64] = {};
|
||||
if (CFStringGetCString(uuidStr, buf, sizeof(buf), kCFStringEncodingUTF8)) {
|
||||
result = buf;
|
||||
}
|
||||
}
|
||||
CFRelease(uuidProperty);
|
||||
}
|
||||
IOObjectRelease(platformExpert);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 路径归一化(macOS 版):解析符号链接 + 转小写
|
||||
// realpath 把 /Applications/foo/../bar 之类折回规范形式;
|
||||
// 小写化保持与 Windows/Linux 跨端一致。macOS HFS+/APFS 默认大小写不敏感,
|
||||
// 转小写不改变文件标识、但避免路径串大小写差异引起 ID 不同。
|
||||
static std::string normalizeExePathLower(const std::string& path)
|
||||
{
|
||||
char resolved[PATH_MAX] = {};
|
||||
std::string out;
|
||||
if (realpath(path.c_str(), resolved) != nullptr) {
|
||||
out = resolved;
|
||||
} else {
|
||||
out = path; // 解析失败:用原值
|
||||
}
|
||||
for (auto& c : out) {
|
||||
if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Get screen resolution
|
||||
static std::string getScreenResolution()
|
||||
{
|
||||
@@ -140,9 +300,113 @@ static std::string getTimeString()
|
||||
return std::string([dateString UTF8String]);
|
||||
}
|
||||
|
||||
// Get active application name
|
||||
// Get user idle time in seconds (time since last keyboard/mouse input)
|
||||
static double getUserIdleTime()
|
||||
{
|
||||
// CGEventSourceSecondsSinceLastEventType returns seconds since last event
|
||||
// kCGEventSourceStateCombinedSessionState includes all input sources
|
||||
CFTimeInterval idleTime = CGEventSourceSecondsSinceLastEventType(
|
||||
kCGEventSourceStateCombinedSessionState,
|
||||
kCGAnyInputEventType
|
||||
);
|
||||
// Defensive: ensure non-negative (edge case protection)
|
||||
return idleTime > 0 ? idleTime : 0;
|
||||
}
|
||||
|
||||
// Check if screen is locked
|
||||
static bool isScreenLocked()
|
||||
{
|
||||
// Method 1: Check CGSession dictionary for screen lock status
|
||||
CFDictionaryRef sessionDict = CGSessionCopyCurrentDictionary();
|
||||
if (sessionDict) {
|
||||
// Check for "CGSSessionScreenIsLocked" key
|
||||
CFBooleanRef screenLocked = (CFBooleanRef)CFDictionaryGetValue(
|
||||
sessionDict, CFSTR("CGSSessionScreenIsLocked"));
|
||||
if (screenLocked && CFBooleanGetValue(screenLocked)) {
|
||||
CFRelease(sessionDict);
|
||||
return true;
|
||||
}
|
||||
CFRelease(sessionDict);
|
||||
}
|
||||
|
||||
// Method 2: Check if loginwindow is frontmost (screen saver / lock screen)
|
||||
NSRunningApplication* frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||
if (frontApp) {
|
||||
NSString* bundleId = [frontApp bundleIdentifier];
|
||||
if ([bundleId isEqualToString:@"com.apple.loginwindow"] ||
|
||||
[bundleId isEqualToString:@"com.apple.ScreenSaver.Engine"]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Format time as HH:MM:SS with prefix
|
||||
static std::string formatStatusTime(const char* prefix, double seconds)
|
||||
{
|
||||
int totalSecs = (int)seconds;
|
||||
int hours = totalSecs / 3600;
|
||||
int mins = (totalSecs % 3600) / 60;
|
||||
int secs = totalSecs % 60;
|
||||
|
||||
char buffer[64];
|
||||
snprintf(buffer, sizeof(buffer), "%s: %02d:%02d:%02d", prefix, hours, mins, secs);
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
// Get active application name or idle/locked status (works for background processes)
|
||||
static std::string getActiveApp()
|
||||
{
|
||||
double idleTime = getUserIdleTime();
|
||||
|
||||
// Check if screen is locked first
|
||||
if (isScreenLocked()) {
|
||||
return formatStatusTime("Locked", idleTime);
|
||||
}
|
||||
|
||||
// Check user idle time (matches Windows/Linux: 6 seconds threshold)
|
||||
// If idle for more than 6 seconds, report inactive status
|
||||
if (idleTime >= 6.0) {
|
||||
return formatStatusTime("Inactive", idleTime);
|
||||
}
|
||||
|
||||
// Use CGWindowListCopyWindowInfo to get the frontmost window
|
||||
// This works reliably even when running as a background/nohup process
|
||||
CFArrayRef windowList = CGWindowListCopyWindowInfo(
|
||||
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
|
||||
kCGNullWindowID
|
||||
);
|
||||
|
||||
if (windowList) {
|
||||
CFIndex count = CFArrayGetCount(windowList);
|
||||
for (CFIndex i = 0; i < count; i++) {
|
||||
CFDictionaryRef window = (CFDictionaryRef)CFArrayGetValueAtIndex(windowList, i);
|
||||
|
||||
// Get window layer - layer 0 is normal windows
|
||||
CFNumberRef layerRef = (CFNumberRef)CFDictionaryGetValue(window, kCGWindowLayer);
|
||||
int layer = 0;
|
||||
if (layerRef) {
|
||||
CFNumberGetValue(layerRef, kCFNumberIntType, &layer);
|
||||
}
|
||||
|
||||
// Skip non-normal windows (menu bar, dock, etc.)
|
||||
if (layer != 0) continue;
|
||||
|
||||
// Get owner name (application name)
|
||||
CFStringRef ownerName = (CFStringRef)CFDictionaryGetValue(window, kCGWindowOwnerName);
|
||||
if (ownerName) {
|
||||
char buffer[256] = {};
|
||||
if (CFStringGetCString(ownerName, buffer, sizeof(buffer), kCFStringEncodingUTF8)) {
|
||||
CFRelease(windowList);
|
||||
return std::string(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
CFRelease(windowList);
|
||||
}
|
||||
|
||||
// Fallback to NSWorkspace (may not work for background processes)
|
||||
NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||
if (app) {
|
||||
NSString* name = [app localizedName];
|
||||
@@ -258,9 +522,25 @@ static void fillLoginInfo(LOGIN_INFOR& info)
|
||||
// CPU MHz
|
||||
info.dwCPUMHz = getCPUFrequencyMHz();
|
||||
|
||||
// PC Name (hostname)
|
||||
// PC Name (hostname) - with group name if set
|
||||
std::string hostname = getHostname();
|
||||
std::string groupName = loadGroupName();
|
||||
if (!groupName.empty()) {
|
||||
// Also update g_SETTINGS for consistency
|
||||
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
|
||||
g_SETTINGS.szGroupName[sizeof(g_SETTINGS.szGroupName) - 1] = '\0';
|
||||
// Format: "hostname/groupname"
|
||||
std::string pcNameWithGroup = hostname + "/" + groupName;
|
||||
strncpy(info.szPCName, pcNameWithGroup.c_str(), sizeof(info.szPCName) - 1);
|
||||
} else if (g_SETTINGS.szGroupName[0] != 0) {
|
||||
// Use group from g_SETTINGS (set at build time)
|
||||
groupName = g_SETTINGS.szGroupName;
|
||||
std::string pcNameWithGroup = hostname + "/" + groupName;
|
||||
strncpy(info.szPCName, pcNameWithGroup.c_str(), sizeof(info.szPCName) - 1);
|
||||
} else {
|
||||
strncpy(info.szPCName, hostname.c_str(), sizeof(info.szPCName) - 1);
|
||||
}
|
||||
info.szPCName[sizeof(info.szPCName) - 1] = '\0';
|
||||
|
||||
// Webcam
|
||||
info.bWebCamIsExist = hasCameraDevice() ? 1 : 0;
|
||||
@@ -325,8 +605,20 @@ static void fillLoginInfo(LOGIN_INFOR& info)
|
||||
std::string resolution = getScreenResolution();
|
||||
info.AddReserved(resolution.c_str());
|
||||
|
||||
// 17. Client ID (calculated from system info, same algorithm as server)
|
||||
// Format: pubIP|hostname|os|cpu|path
|
||||
// 17. Client ID
|
||||
// V2 算法:IOPlatformUUID + 归一化路径
|
||||
// - 同机同程序路径永远同 ID(不依赖 IP/hostname/os/CPU 漂移)
|
||||
// - IOPlatformUUID 主板级,重装系统通常不变;多机各不相同
|
||||
// - 读取失败时退化到老算法(pubIP|hostname|os|cpu|path)保兼容
|
||||
std::string machineId = getMachineId();
|
||||
if (!machineId.empty()) {
|
||||
std::string normPath = normalizeExePathLower(exePath);
|
||||
std::string idInput = machineId + "|" + normPath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
NSLog(@"ClientID(v2): %llu (machineId=%s, path=%s)",
|
||||
g_myClientID, machineId.c_str(), normPath.c_str());
|
||||
} else {
|
||||
// 老算法兜底
|
||||
char cpuStr[32];
|
||||
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz);
|
||||
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
|
||||
@@ -335,8 +627,13 @@ static void fillLoginInfo(LOGIN_INFOR& info)
|
||||
cpuStr + "|" +
|
||||
exePath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
NSLog(@"ClientID(v1 fallback): %llu (IOPlatformUUID 读取失败)", g_myClientID);
|
||||
}
|
||||
info.AddReserved(std::to_string(g_myClientID).c_str());
|
||||
|
||||
// 服务端签名输入:与服务端 AddList 处签名格式一致(startTime + "|" + clientID)
|
||||
g_loginMsg = std::string(info.szStartTime) + "|" + std::to_string(g_myClientID);
|
||||
|
||||
NSLog(@"LOGIN_INFOR filled: OS=%s, Host=%s, CPU=%dMHz, PubIP=%s, ClientID=%llu",
|
||||
osVer.c_str(), hostname.c_str(), info.dwCPUMHz, pubIP.c_str(), g_myClientID);
|
||||
}
|
||||
@@ -347,6 +644,7 @@ static void signalHandler(int sig)
|
||||
{
|
||||
NSLog(@"Received signal %d, shutting down...", sig);
|
||||
g_running = false;
|
||||
g_bExit = S_CLIENT_EXIT; // 通知所有工作线程退出
|
||||
}
|
||||
|
||||
static void setupSignals()
|
||||
@@ -357,6 +655,28 @@ static void setupSignals()
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
}
|
||||
|
||||
// 经典 Unix 双 fork 守护进程
|
||||
static void daemonize()
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) exit(1);
|
||||
if (pid > 0) exit(0); // 父进程退出
|
||||
|
||||
setsid(); // 新会话,脱离终端
|
||||
|
||||
pid = fork(); // 第二次 fork,防止重新获取控制终端
|
||||
if (pid < 0) exit(1);
|
||||
if (pid > 0) exit(0);
|
||||
|
||||
// 关闭标准文件描述符,重定向到 /dev/null
|
||||
close(STDIN_FILENO);
|
||||
close(STDOUT_FILENO);
|
||||
close(STDERR_FILENO);
|
||||
open("/dev/null", O_RDONLY); // fd 0 = stdin
|
||||
open("/dev/null", O_WRONLY); // fd 1 = stdout
|
||||
open("/dev/null", O_WRONLY); // fd 2 = stderr
|
||||
}
|
||||
|
||||
// ============== Main Entry Point ==============
|
||||
|
||||
// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致)
|
||||
@@ -395,12 +715,40 @@ struct RttEstimator {
|
||||
RttEstimator g_rttEstimator;
|
||||
int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整
|
||||
|
||||
void* ShellworkingThread(void* param)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
NSLog(@">>> Enter ShellworkingThread [%p]", clientAddr);
|
||||
// 子连接:开启 auth。macOS IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<PTYHandler> handler(new PTYHandler(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
BYTE bToken = TOKEN_TERMINAL_START;
|
||||
ClientObject->Send2Server((char*)&bToken, 1);
|
||||
NSLog(@">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
NSLog(@">>> Leave ShellworkingThread [%p]", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
NSLog(@"*** ShellworkingThread exception: %s ***", e.what());
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* ScreenworkingThread(void* param)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。macOS IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<ScreenHandler> handler(new ScreenHandler(ClientObject.get()));
|
||||
if (!handler->init()) {
|
||||
@@ -413,6 +761,8 @@ void* ScreenworkingThread(void* param)
|
||||
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
@@ -421,6 +771,30 @@ void* ScreenworkingThread(void* param)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* FileManagerworkingThread(void* param)
|
||||
{
|
||||
try {
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter FileManagerworkingThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。macOS IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<FileManager> handler(new FileManager(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
Mprintf(">>> FileManagerworkingThread [%p] initialized\n", clientAddr);
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(1000);
|
||||
// 清除回调,防止重连线程访问已销毁的 handler
|
||||
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
|
||||
}
|
||||
Mprintf(">>> Leave FileManagerworkingThread [%p]\n", clientAddr);
|
||||
} catch (const std::exception& e) {
|
||||
Mprintf("*** FileManagerworkingThread exception: %s ***\n", e.what());
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
if (szBuffer == nullptr || ulLength == 0)
|
||||
@@ -431,6 +805,7 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
g_running = false; // Stop main loop to prevent reconnection
|
||||
} else if (szBuffer[0] == COMMAND_SHELL) {
|
||||
std::thread(ShellworkingThread, nullptr).detach();
|
||||
Mprintf("** [%p] Received 'SHELL' command ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_SCREEN_SPY) {
|
||||
std::thread(ScreenworkingThread, nullptr).detach();
|
||||
@@ -438,7 +813,34 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
} else if (szBuffer[0] == COMMAND_SYSTEM) {
|
||||
Mprintf("** [%p] Received 'SYSTEM' command ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_LIST_DRIVE) {
|
||||
std::thread(FileManagerworkingThread, nullptr).detach();
|
||||
Mprintf("** [%p] Received 'LIST_DRIVE' command ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_C2C_PREPARE) {
|
||||
// C2C 准备接收通知
|
||||
FileTransferV2::HandleC2CPrepare(szBuffer, ulLength, nullptr);
|
||||
Mprintf("** [%p] C2C Prepare received ***\n", user);
|
||||
} else if (szBuffer[0] == COMMAND_C2C_TEXT) {
|
||||
// C2C 文本剪贴板: [cmd:1][dstClientID:8][textLen:4][text:N]
|
||||
if (ulLength >= 13) {
|
||||
uint32_t textLen;
|
||||
memcpy(&textLen, szBuffer + 9, 4);
|
||||
if (ulLength >= 13 + textLen && textLen > 0) {
|
||||
if (!ClipboardHandler::IsAvailable()) {
|
||||
Mprintf("** [%p] C2C Text: clipboard unavailable ***\n", user);
|
||||
} else {
|
||||
std::string utf8Text((const char*)szBuffer + 13, textLen);
|
||||
if (ClipboardHandler::SetText(utf8Text)) {
|
||||
Mprintf("** [%p] C2C Text received: %u bytes ***\n", user, textLen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (szBuffer[0] == COMMAND_SEND_FILE_V2 || szBuffer[0] == COMMAND_FILE_COMPLETE_V2) {
|
||||
// V2 文件接收
|
||||
int result = FileTransferV2::RecvFileChunkV2(szBuffer, ulLength, g_myClientID);
|
||||
if (result != 0) {
|
||||
Mprintf("** [%p] V2 File recv error: %d ***\n", user, result);
|
||||
}
|
||||
} else if (szBuffer[0] == CMD_HEARTBEAT_ACK) {
|
||||
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
|
||||
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
|
||||
@@ -455,42 +857,122 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
|
||||
}
|
||||
} else if (szBuffer[0] == CMD_MASTERSETTING) {
|
||||
int settingSize = ulLength - 1;
|
||||
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval
|
||||
// 强制要求完整 MasterSettings(包含 Signature 字段)。包不完整 → 视为非授权服务端
|
||||
if (settingSize < (int)sizeof(MasterSettings)) {
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
return TRUE;
|
||||
}
|
||||
MasterSettings settings = {};
|
||||
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings));
|
||||
memcpy(&settings, szBuffer + 1, sizeof(MasterSettings));
|
||||
|
||||
// 服务端身份校验:用 g_loginMsg (= szStartTime + "|" + clientID) 与 settings.Signature
|
||||
// 验证签名。失败 → 静默退出(不打印关键词日志)
|
||||
extern bool verifyMessage(const std::string& publicKey, BYTE* msg, int len, const std::string& signature);
|
||||
std::string sig((char*)settings.Signature, (char*)settings.Signature + sizeof(settings.Signature));
|
||||
if (!verifyMessage("", (BYTE*)g_loginMsg.data(), (int)g_loginMsg.length(), sig)) {
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
return TRUE;
|
||||
}
|
||||
g_settingsVerified = true;
|
||||
|
||||
if (settings.ReportInterval > 0)
|
||||
g_heartbeatInterval = settings.ReportInterval;
|
||||
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
|
||||
}
|
||||
} else if (szBuffer[0] == COMMAND_NEXT) {
|
||||
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
|
||||
} else if (szBuffer[0] == CMD_SET_GROUP) {
|
||||
// Extract group name from message (starts at byte 1)
|
||||
std::string groupName;
|
||||
if (ulLength > 1) {
|
||||
groupName = std::string((char*)szBuffer + 1, ulLength - 1);
|
||||
// Remove trailing nulls
|
||||
size_t pos = groupName.find('\0');
|
||||
if (pos != std::string::npos) {
|
||||
groupName = groupName.substr(0, pos);
|
||||
}
|
||||
}
|
||||
// Save to config file
|
||||
saveGroupName(groupName);
|
||||
// Update global settings
|
||||
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
|
||||
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
|
||||
// 标记需要重发登录信息(让服务端更新分组显示)
|
||||
g_needResendLogin.store(true);
|
||||
Mprintf("** [%p] Group changed to: %s ***\n", user, groupName.c_str());
|
||||
} else {
|
||||
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// 用法: ./ghost [-d]
|
||||
// -d 后台守护进程模式
|
||||
int main(int argc, const char* argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
// 解析 -d 参数
|
||||
bool daemon_mode = (argc > 1 && strcmp(argv[1], "-d") == 0);
|
||||
|
||||
// 守护进程模式:在进入 autoreleasepool 之前 fork
|
||||
if (daemon_mode) {
|
||||
daemonize();
|
||||
}
|
||||
|
||||
@autoreleasepool {
|
||||
NSLog(@"=== macOS Ghost Client ===");
|
||||
NSLog(@"=== macOS Ghost Client%s ===", daemon_mode ? " (daemon)" : "");
|
||||
|
||||
// ============== Power Management: Keep System Awake ==============
|
||||
// 1. Disable App Nap - prevent macOS from suspending this process
|
||||
id<NSObject> powerActivity = [[NSProcessInfo processInfo]
|
||||
beginActivityWithOptions:(NSActivityUserInitiated | NSActivityIdleSystemSleepDisabled)
|
||||
reason:@"Remote control client must maintain persistent connection"];
|
||||
NSLog(@"App Nap disabled, activity token acquired");
|
||||
|
||||
// 2. Prevent system idle sleep using IOKit power assertion
|
||||
IOPMAssertionID sleepAssertionID = 0;
|
||||
IOReturn result = IOPMAssertionCreateWithName(
|
||||
kIOPMAssertionTypeNoIdleSleep,
|
||||
kIOPMAssertionLevelOn,
|
||||
CFSTR("SimpleRemoter macOS client - maintaining remote connection"),
|
||||
&sleepAssertionID
|
||||
);
|
||||
if (result == kIOReturnSuccess) {
|
||||
NSLog(@"Power assertion created: system idle sleep disabled (ID: %u)", sleepAssertionID);
|
||||
} else {
|
||||
NSLog(@"Warning: Failed to create power assertion (error: 0x%x)", result);
|
||||
}
|
||||
|
||||
// 3. Display sleep: managed by ScreenHandler - only prevents display sleep
|
||||
// when remote desktop is actively connected (saves power when idle)
|
||||
|
||||
// Setup signal handlers
|
||||
setupSignals();
|
||||
|
||||
// Load configuration file (~/.config/ghost/config.conf)
|
||||
loadConfig();
|
||||
NSLog(@"Config loaded from %s", g_configPath.c_str());
|
||||
|
||||
// Check permissions
|
||||
NSLog(@"Checking permissions...");
|
||||
|
||||
if (!Permissions::checkScreenCapture()) {
|
||||
bool hasScreenCapture = Permissions::checkScreenCapture();
|
||||
if (hasScreenCapture) {
|
||||
NSLog(@"Screen capture permission: OK");
|
||||
} else {
|
||||
NSLog(@"Screen capture permission not granted.");
|
||||
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Screen Recording");
|
||||
// Request permission (triggers system dialog on first run)
|
||||
Permissions::requestScreenCapture();
|
||||
// Only open settings if this appears to be a re-run without permission
|
||||
// Check again after request (dialog may have been shown)
|
||||
if (!Permissions::checkScreenCapture()) {
|
||||
Permissions::openScreenCaptureSettings();
|
||||
}
|
||||
}
|
||||
|
||||
if (!Permissions::checkAccessibility()) {
|
||||
bool hasAccessibility = Permissions::checkAccessibility();
|
||||
if (hasAccessibility) {
|
||||
NSLog(@"Accessibility permission: OK");
|
||||
} else {
|
||||
NSLog(@"Accessibility permission not granted.");
|
||||
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Accessibility");
|
||||
Permissions::requestAccessibility();
|
||||
@@ -501,6 +983,8 @@ int main(int argc, const char* argv[])
|
||||
NSLog(@"Full Disk Access: not detected (may be false negative).");
|
||||
NSLog(@"If file access issues occur, grant FDA in System Preferences > Privacy & Security > Full Disk Access");
|
||||
// Don't auto-open settings since detection is unreliable
|
||||
} else {
|
||||
NSLog(@"Full Disk Access: OK");
|
||||
}
|
||||
|
||||
// Create client
|
||||
@@ -519,10 +1003,20 @@ int main(int argc, const char* argv[])
|
||||
continue;
|
||||
}
|
||||
|
||||
// 进入新连接,重置服务端身份校验状态
|
||||
g_loginTime = time(nullptr);
|
||||
g_settingsVerified = false;
|
||||
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
|
||||
|
||||
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
|
||||
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) {
|
||||
// 检查是否需要重发登录信息(分组变更后)
|
||||
if (g_needResendLogin.exchange(false)) {
|
||||
fillLoginInfo(logInfo);
|
||||
ClientObject->SendLoginInfo(logInfo);
|
||||
Mprintf(">> Resent login info after group change\n");
|
||||
}
|
||||
|
||||
// 等待心跳间隔(每秒检查一次退出条件,保证及时响应)
|
||||
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
|
||||
for (int i = 0; i < interval; ++i) {
|
||||
@@ -533,6 +1027,13 @@ int main(int argc, const char* argv[])
|
||||
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
|
||||
break;
|
||||
|
||||
// 兜底:登录后 30 秒内必须收到并通过 MasterSettings 校验,否则视为非授权服务端
|
||||
if (!g_settingsVerified && g_loginTime > 0 &&
|
||||
time(nullptr) - g_loginTime > 30) {
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
break;
|
||||
}
|
||||
|
||||
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
|
||||
std::string activity = getActiveApp();
|
||||
|
||||
@@ -549,6 +1050,15 @@ int main(int argc, const char* argv[])
|
||||
}
|
||||
|
||||
NSLog(@"Shutting down...");
|
||||
|
||||
// Release power assertions
|
||||
if (sleepAssertionID) {
|
||||
IOPMAssertionRelease(sleepAssertionID);
|
||||
NSLog(@"Released sleep assertion");
|
||||
}
|
||||
// Display assertion is managed by ScreenHandler (released in stop())
|
||||
// powerActivity is automatically released when exiting @autoreleasepool
|
||||
(void)powerActivity; // Suppress unused variable warning
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
32
macos/uninstall.sh
Normal file
32
macos/uninstall.sh
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# macOS Ghost Client 卸载脚本
|
||||
|
||||
APP_DIR="/Applications/GhostClient.app"
|
||||
|
||||
echo "=== GhostClient 卸载程序 ==="
|
||||
echo ""
|
||||
|
||||
# 1. 停止进程
|
||||
echo "[1/3] 停止进程..."
|
||||
pkill -9 -f "$APP_DIR" 2>/dev/null || true
|
||||
|
||||
# 2. 删除文件
|
||||
echo "[2/3] 删除文件..."
|
||||
sudo rm -rf "$APP_DIR"
|
||||
rm -rf ~/.config/ghost 2>/dev/null || true
|
||||
rm -f /tmp/ghost.log 2>/dev/null || true
|
||||
|
||||
# 3. 移除登录项
|
||||
echo "[3/4] 移除登录项..."
|
||||
osascript -e 'tell application "System Events" to delete login item "GhostClient"' 2>/dev/null || true
|
||||
|
||||
# 4. 重置系统权限
|
||||
echo "[4/4] 重置系统权限..."
|
||||
tccutil reset ScreenCapture 2>/dev/null || true
|
||||
tccutil reset Accessibility 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " 卸载完成"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
@@ -526,14 +526,13 @@ BOOL CMy2015RemoteApp::InitInstance()
|
||||
SetChineseThreadLocale();
|
||||
|
||||
// 加载语言包(必须在显示任何文本之前)
|
||||
// 内嵌资源支持 en_US 和 zh_TW,无需外部文件
|
||||
auto lang = THIS_CFG.GetStr("settings", "Language", "en_US");
|
||||
auto langDir = THIS_CFG.GetStr("settings", "LangDir", "./lang");
|
||||
langDir = langDir.empty() ? "./lang" : langDir;
|
||||
if (PathFileExists(langDir.c_str())) {
|
||||
g_Lang.Init(langDir.c_str());
|
||||
g_Lang.Load(lang.c_str());
|
||||
Mprintf("语言包目录已经指定[%s], 语言数量: %d\n", langDir.c_str(), g_Lang.GetLanguageCount());
|
||||
}
|
||||
g_Lang.Init(langDir.c_str()); // 初始化目录(用于磁盘补丁文件)
|
||||
g_Lang.Load(lang.c_str()); // 加载语言(优先内嵌资源,再覆盖磁盘文件)
|
||||
Mprintf("语言: %s, 目录: %s\n", lang.c_str(), langDir.c_str());
|
||||
|
||||
// 创建并显示启动画面
|
||||
CSplashDlg* pSplash = new CSplashDlg();
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -96,6 +96,14 @@ extern CMy2015RemoteDlg* g_2015RemoteDlg;
|
||||
// 注意:m_bEnableFileV2 是 CMy2015RemoteDlg 的成员变量
|
||||
bool SupportsFileTransferV2(context* ctx);
|
||||
|
||||
// 获取客户端协议字符串编码 (CP_UTF8 或 936)。
|
||||
// 适用于任意 context:
|
||||
// - 主连接:直接读自身的 CAPABILITIES
|
||||
// - 子连接(KeyBoardDlg / SystemDlg / FileManagerDlg 等):CAPABILITIES 为空,
|
||||
// 通过 peer IP 查 m_HostList 中的主连接获取能力位
|
||||
// 找不到主连接或老客户端:默认 CP936(覆盖 95% 简中/英语 ASCII 老客户端)。
|
||||
UINT GetClientEncoding(context* ctx);
|
||||
|
||||
// 服务端待续传的传输信息
|
||||
struct PendingTransferV2 {
|
||||
uint64_t clientID;
|
||||
@@ -215,13 +223,17 @@ public:
|
||||
MasterSettings m_settings;
|
||||
static BOOL CALLBACK NotifyProc(CONTEXT_OBJECT* ContextObject);
|
||||
static BOOL CALLBACK OfflineProc(CONTEXT_OBJECT* ContextObject);
|
||||
BOOL AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired = nullptr);
|
||||
BOOL AuthorizeClientV2(context* ctx, const std::string& sn, const std::string& passcode, const std::string& hmacV2, bool* outExpired = nullptr);
|
||||
int AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired = nullptr);
|
||||
int AuthorizeClientV2(context* ctx, const std::string& sn, const std::string& passcode, const std::string& hmacV2, bool* outExpired = nullptr);
|
||||
VOID MessageHandle(CONTEXT_OBJECT* ContextObject);
|
||||
VOID SendSelectedCommand(PBYTE szBuffer, ULONG ulLength, contextModifier cb = NULL, void* user=NULL);
|
||||
VOID SendAllCommand(PBYTE szBuffer, ULONG ulLength);
|
||||
// 显示用户上线信息
|
||||
CWnd* m_pFloatingTip = nullptr;
|
||||
// 屏幕预览:m_pFloatingTip 实际是 CPreviewTipWnd 时这里有同一指针的有类型副本,
|
||||
// 用于在收到 JPEG 后调用 SetImageFromJpeg;DeletePopupWindow 释放时一并置空。
|
||||
class CPreviewTipWnd* m_pPreviewTip = nullptr;
|
||||
WORD m_PreviewReqId = 0; // 当前期待的预览响应序号;0 = 无待响应
|
||||
// 记录 clientID(心跳更新)
|
||||
std::set<uint64_t> m_DirtyClients;
|
||||
// 待处理的上线/下线事件(批量更新减少闪烁)
|
||||
@@ -237,6 +249,12 @@ public:
|
||||
std::string m_v2KeyPath; // V2 密钥文件路径
|
||||
void RebuildFilteredIndices(); // 重建过滤索引
|
||||
context* GetContextByListIndex(int iItem); // 根据列表索引获取 context(考虑分组过滤)
|
||||
|
||||
// 启发式 ID 迁移:新客户端首次上线时,按 (ComputerName, ProgramPath) 在 m_ClientMap
|
||||
// 里找老条目,若唯一匹配则把元数据(备注/位置/级别/授权)拷贝到 newId。
|
||||
// 多于一个候选时保守跳过,写日志让运维手动处理,避免误聚合。
|
||||
// 返回 true 表示有迁移发生(调用方需触发 dat 文件落盘)。
|
||||
bool TryMigrateClientMetadata(uint64_t newId, const CString& pcName, const CString& exePath);
|
||||
void LoadListData(const std::string& group);
|
||||
void DeletePopupWindow(BOOL bForce = FALSE);
|
||||
void CheckHeartbeat();
|
||||
@@ -255,6 +273,7 @@ public:
|
||||
CGridDialog * m_gridDlg = NULL;
|
||||
std::vector<DllInfo*> m_DllList;
|
||||
context* FindHostByIP(const std::string& ip);
|
||||
uint64_t FindClientIDByIP(const std::string& ip); // 线程安全:在锁内获取ID
|
||||
void InjectTinyRunDll(const std::string& ip, int pid);
|
||||
NOTIFYICONDATA m_Nid;
|
||||
HANDLE m_hExit;
|
||||
@@ -275,6 +294,7 @@ public:
|
||||
CDialogBase* GetRemoteWindow(CDialogBase* dlg);
|
||||
void RemoveRemoteWindow(HWND wnd);
|
||||
void CloseRemoteDesktopByClientID(uint64_t clientID);
|
||||
void CloseWebRemoteDesktopByClientID(uint64_t clientID); // Only close Web session dialog
|
||||
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
|
||||
void UpdateActiveRemoteSession(CDialogBase* sess);
|
||||
CDialogBase* GetActiveRemoteSession();
|
||||
@@ -342,7 +362,13 @@ public:
|
||||
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||
afx_msg void OnExitSizeMove();
|
||||
afx_msg void OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult);
|
||||
afx_msg void OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult); // 虚拟列表数据回调
|
||||
afx_msg void OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult); // 虚拟列表数据回调(A 版,备用)
|
||||
afx_msg void OnGetDispInfoW(NMHDR* pNMHDR, LRESULT* pResult); // 虚拟列表数据回调(W 版,启用 LVM_SETUNICODEFORMAT 后实际触发的)
|
||||
|
||||
// "活动窗口"列的宽字符旁路表:clientID -> Unicode 标题。
|
||||
// 协议字段 hb.ActiveWnd 已约定为 UTF-8(老客户端 GBK 回退),由服务端解码后存入。
|
||||
// 由 m_cs 保护。
|
||||
std::map<uint64_t, std::wstring> m_ActiveWndW;
|
||||
afx_msg void OnOnlineMessage();
|
||||
afx_msg void OnOnlineDelete();
|
||||
afx_msg void OnOnlineUpdate();
|
||||
@@ -412,6 +438,12 @@ public:
|
||||
afx_msg void OnWhatIsThis();
|
||||
afx_msg void OnOnlineAuthorize();
|
||||
void OnListClick(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
// 屏幕预览:依 ctx 最近 RTT + 屏幕分辨率挑参数;4K/超宽屏在 LAN 档自适应放大
|
||||
void ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const;
|
||||
// 发起预览请求;reqId 应与 m_PreviewReqId 同步
|
||||
void SendScreenPreviewRequest(context* ctx, WORD reqId, WORD maxWidth, BYTE jpegQuality);
|
||||
// 收到 TOKEN_SCREEN_PREVIEW_RSP(在主线程处理)
|
||||
afx_msg LRESULT OnPreviewResponse(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg void OnOnlineUnauthorize();
|
||||
afx_msg void OnToolRequestAuth();
|
||||
afx_msg LRESULT OnPasswordCheck(WPARAM wParam, LPARAM lParam);
|
||||
|
||||
@@ -338,6 +338,7 @@
|
||||
<ClInclude Include="FeatureLimitsDlg.h" />
|
||||
<ClInclude Include="FrpsForSubDlg.h" />
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
<ClInclude Include="proxy\HPSocket.h" />
|
||||
<ClInclude Include="proxy\HPTypeDef.h" />
|
||||
@@ -388,6 +389,7 @@
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="..\..\client\MemoryModule.c">
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="msvc_compat.c" />
|
||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -185,6 +186,7 @@
|
||||
<ClInclude Include="WebPage.h" />
|
||||
<ClInclude Include="SimpleWebSocket.h" />
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "InputDlg.h"
|
||||
#include <bcrypt.h>
|
||||
#include <wincrypt.h>
|
||||
#include <Shlwapi.h>
|
||||
#pragma comment(lib, "Shlwapi.lib")
|
||||
#include "Resource.h"
|
||||
extern "C" {
|
||||
#include "client/reg_startup.h"
|
||||
@@ -49,11 +51,66 @@ std::string GetPwdHash();
|
||||
|
||||
int MemoryFind(const char *szBuffer, const char *Key, int iBufferSize, int iKeySize);
|
||||
|
||||
LPBYTE ReadResource(int resourceId, DWORD &dwSize)
|
||||
// 获取程序目录下 res 子目录的路径
|
||||
static CString GetResDirectoryPath()
|
||||
{
|
||||
TCHAR szPath[MAX_PATH];
|
||||
GetModuleFileName(NULL, szPath, MAX_PATH);
|
||||
PathRemoveFileSpec(szPath);
|
||||
PathAppend(szPath, _T("res"));
|
||||
return CString(szPath);
|
||||
}
|
||||
|
||||
// 从外部文件读取资源(优先级高于内嵌资源)
|
||||
static LPBYTE ReadResourceFromFile(const char* resName, DWORD &dwSize)
|
||||
{
|
||||
if (!resName || !resName[0]) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CString resDir = GetResDirectoryPath();
|
||||
CString filePath;
|
||||
filePath.Format(_T("%s\\%hs"), (LPCTSTR)resDir, resName);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (GetFileAttributes(filePath) == INVALID_FILE_ATTRIBUTES) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 打开文件
|
||||
HANDLE hFile = CreateFile(filePath, GENERIC_READ, FILE_SHARE_READ, NULL,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
if (hFile == INVALID_HANDLE_VALUE) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 获取文件大小
|
||||
LARGE_INTEGER fileSize;
|
||||
if (!GetFileSizeEx(hFile, &fileSize) || fileSize.QuadPart == 0) {
|
||||
CloseHandle(hFile);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 分配内存并读取文件
|
||||
dwSize = (DWORD)fileSize.QuadPart;
|
||||
LPBYTE buffer = new BYTE[dwSize];
|
||||
DWORD bytesRead = 0;
|
||||
if (!ReadFile(hFile, buffer, dwSize, &bytesRead, NULL) || bytesRead != dwSize) {
|
||||
delete[] buffer;
|
||||
CloseHandle(hFile);
|
||||
dwSize = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CloseHandle(hFile);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// 从内嵌资源读取
|
||||
static LPBYTE ReadResourceFromEmbedded(int resourceId, DWORD &dwSize)
|
||||
{
|
||||
dwSize = 0;
|
||||
auto id = resourceId;
|
||||
HRSRC hResource = FindResourceA(NULL, MAKEINTRESOURCE(id), "BINARY");
|
||||
HRSRC hResource = FindResourceA(NULL, MAKEINTRESOURCE(resourceId), "BINARY");
|
||||
if (hResource == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -76,6 +133,54 @@ LPBYTE ReadResource(int resourceId, DWORD &dwSize)
|
||||
return r;
|
||||
}
|
||||
|
||||
// 读取资源:优先从 res 目录读取外部文件,如果不存在则使用内嵌资源
|
||||
// resName: 外部文件名(如 "ghost_x64.exe"),为空时直接使用内嵌资源
|
||||
LPBYTE ReadResource(int resourceId, DWORD &dwSize, const char* resName)
|
||||
{
|
||||
dwSize = 0;
|
||||
|
||||
// 1. 优先尝试从 res 目录读取外部文件
|
||||
if (resName && resName[0]) {
|
||||
LPBYTE data = ReadResourceFromFile(resName, dwSize);
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 回退到内嵌资源
|
||||
return ReadResourceFromEmbedded(resourceId, dwSize);
|
||||
}
|
||||
|
||||
// ========== res 目录外部资源文件名定义 ==========
|
||||
// 命名规范:<模块名>_<架构>.exe/.dll/.bin
|
||||
// 架构:x86 / x64 / linux / macos
|
||||
namespace ResFileName {
|
||||
// Ghost 主程序
|
||||
const char* GHOST_X86 = "ghost_x86.exe";
|
||||
const char* GHOST_X64 = "ghost_x64.exe";
|
||||
const char* GHOST_LINUX = "ghost_linux";
|
||||
const char* GHOST_MACOS = "ghost_macos"; // 预留
|
||||
// TestRun 加载器
|
||||
const char* TESTRUN_X86 = "testrun_x86.dll";
|
||||
const char* TESTRUN_X64 = "testrun_x64.dll";
|
||||
// ServerDll
|
||||
const char* SERVERDLL_X86 = "serverdll_x86.dll";
|
||||
const char* SERVERDLL_X64 = "serverdll_x64.dll";
|
||||
// TinyRun
|
||||
const char* TINYRUN_X86 = "tinyrun_x86.exe";
|
||||
const char* TINYRUN_X64 = "tinyrun_x64.exe";
|
||||
// SCLoader (Shellcode加载器)
|
||||
const char* SCLOADER_X86 = "scloader_x86.bin";
|
||||
const char* SCLOADER_X64 = "scloader_x64.bin";
|
||||
const char* SCLOADER_X86_OLD = "scloader_old_x86.bin";
|
||||
const char* SCLOADER_X64_OLD = "scloader_old_x64.bin";
|
||||
// FRP 相关 (无架构区分,64位DLL)
|
||||
const char* FRPC_DLL = "frpc.dll";
|
||||
const char* FRPS_DLL = "frps.dll";
|
||||
// 工具
|
||||
const char* UPX_EXE = "upx.exe";
|
||||
const char* RCEDIT_EXE = "rcedit.exe";
|
||||
}
|
||||
|
||||
CString GenerateRandomName(int nLength)
|
||||
{
|
||||
@@ -180,10 +285,10 @@ bool MakeShellcode(LPBYTE& compressedBuffer, int& ulTotalSize, LPBYTE originBuff
|
||||
|
||||
BOOL WriteBinaryToFile(const char* path, const char* data, ULONGLONG size, LONGLONG offset = 0);
|
||||
|
||||
std::string ReleaseEXE(int resID, const char* name)
|
||||
std::string ReleaseEXE(int resID, const char* name, const char* resName)
|
||||
{
|
||||
DWORD dwSize = 0;
|
||||
LPBYTE data = ReadResource(resID, dwSize);
|
||||
LPBYTE data = ReadResource(resID, dwSize, resName);
|
||||
if (!data)
|
||||
return "";
|
||||
|
||||
@@ -329,42 +434,48 @@ void CBuildDlg::OnBnClickedOk()
|
||||
startup = std::map<int, int> {
|
||||
{IndexTestRun_DLL, Startup_DLL},{IndexTestRun_MemDLL, Startup_MEMDLL},{IndexTestRun_InjSC, Startup_InjSC},
|
||||
} [index];
|
||||
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize);
|
||||
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
|
||||
is64bit ? ResFileName::TESTRUN_X64 : ResFileName::TESTRUN_X86);
|
||||
break;
|
||||
case IndexGhost:
|
||||
file = "ghost.exe";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
|
||||
typ = CLIENT_TYPE_ONE;
|
||||
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize);
|
||||
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
|
||||
is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86);
|
||||
break;
|
||||
case IndexGhostMsc:
|
||||
file = "ghost.exe";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
|
||||
typ = CLIENT_TYPE_ONE;
|
||||
startup = Startup_GhostMsc;
|
||||
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize);
|
||||
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
|
||||
is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86);
|
||||
break;
|
||||
case IndexTestRunMsc:
|
||||
file = "TestRun.exe";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
|
||||
typ = CLIENT_TYPE_MEMDLL;
|
||||
startup = Startup_TestRunMsc;
|
||||
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize);
|
||||
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
|
||||
is64bit ? ResFileName::TESTRUN_X64 : ResFileName::TESTRUN_X86);
|
||||
break;
|
||||
case IndexServerDll:
|
||||
file = "ServerDll.dll";
|
||||
typ = CLIENT_TYPE_DLL;
|
||||
szBuffer = ReadResource(is64bit ? IDR_SERVERDLL_X64 : IDR_SERVERDLL_X86, dwFileSize);
|
||||
szBuffer = ReadResource(is64bit ? IDR_SERVERDLL_X64 : IDR_SERVERDLL_X86, dwFileSize,
|
||||
is64bit ? ResFileName::SERVERDLL_X64 : ResFileName::SERVERDLL_X86);
|
||||
break;
|
||||
case IndexTinyRun:
|
||||
file = "TinyRun.dll";
|
||||
typ = CLIENT_TYPE_SHELLCODE;
|
||||
szBuffer = ReadResource(is64bit ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, dwFileSize);
|
||||
szBuffer = ReadResource(is64bit ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, dwFileSize,
|
||||
is64bit ? ResFileName::TINYRUN_X64 : ResFileName::TINYRUN_X86);
|
||||
break;
|
||||
case IndexLinuxGhost:
|
||||
file = "ghost";
|
||||
typ = CLIENT_TYPE_LINUX;
|
||||
szBuffer = ReadResource(IDR_LINUX_GHOST, dwFileSize);
|
||||
szBuffer = ReadResource(IDR_LINUX_GHOST, dwFileSize, ResFileName::GHOST_LINUX);
|
||||
break;
|
||||
case OTHER_ITEM: {
|
||||
m_OtherItem.GetWindowTextA(file);
|
||||
@@ -470,7 +581,8 @@ void CBuildDlg::OnBnClickedOk()
|
||||
} else {
|
||||
if (sel == CLIENT_COMPRESS_SC_AES) {
|
||||
DWORD dwSize = 0;
|
||||
LPBYTE data = ReadResource(is64bit ? IDR_SCLOADER_X64 : IDR_SCLOADER_X86, dwSize);
|
||||
LPBYTE data = ReadResource(is64bit ? IDR_SCLOADER_X64 : IDR_SCLOADER_X86, dwSize,
|
||||
is64bit ? ResFileName::SCLOADER_X64 : ResFileName::SCLOADER_X86);
|
||||
if (data) {
|
||||
int iOffset = MemoryFind((char*)data, (char*)g_ConnectAddress.Flag(), dwSize, g_ConnectAddress.FlagLen());
|
||||
if (iOffset != -1) {
|
||||
@@ -534,7 +646,8 @@ void CBuildDlg::OnBnClickedOk()
|
||||
} else if (sel == CLIENT_COMPRESS_SC_AES_OLD || // 兼容旧版本
|
||||
sel == CLIENT_COMP_SC_AES_OLD_UPX) {
|
||||
DWORD dwSize = 0;
|
||||
LPBYTE data = ReadResource(is64bit ? IDR_SCLOADER_X64_OLD : IDR_SCLOADER_X86_OLD, dwSize);
|
||||
LPBYTE data = ReadResource(is64bit ? IDR_SCLOADER_X64_OLD : IDR_SCLOADER_X86_OLD, dwSize,
|
||||
is64bit ? ResFileName::SCLOADER_X64_OLD : ResFileName::SCLOADER_X86_OLD);
|
||||
if (data) {
|
||||
int iOffset = MemoryFind((char*)data, (char*)g_ConnectAddress.Flag(), dwSize, g_ConnectAddress.FlagLen());
|
||||
if (iOffset != -1) {
|
||||
|
||||
@@ -3,9 +3,42 @@
|
||||
#include "Buffer.h"
|
||||
#include "LangManager.h"
|
||||
|
||||
LPBYTE ReadResource(int resourceId, DWORD& dwSize);
|
||||
// 读取资源:优先从 res 目录读取外部文件,如果不存在则使用内嵌资源
|
||||
// resName: 外部文件名(如 "ghost_x64.exe"),为空时直接使用内嵌资源
|
||||
LPBYTE ReadResource(int resourceId, DWORD& dwSize, const char* resName = nullptr);
|
||||
|
||||
std::string ReleaseEXE(int resID, const char* name);
|
||||
std::string ReleaseEXE(int resID, const char* name, const char* resName = nullptr);
|
||||
|
||||
// ========== res 目录外部资源文件名定义 ==========
|
||||
// 命名规范:<模块名>_<架构>.exe/.dll/.bin
|
||||
// 架构:x86 / x64 / linux / macos
|
||||
namespace ResFileName {
|
||||
// Ghost 主程序
|
||||
extern const char* GHOST_X86;
|
||||
extern const char* GHOST_X64;
|
||||
extern const char* GHOST_LINUX;
|
||||
extern const char* GHOST_MACOS; // 预留
|
||||
// TestRun 加载器
|
||||
extern const char* TESTRUN_X86;
|
||||
extern const char* TESTRUN_X64;
|
||||
// ServerDll
|
||||
extern const char* SERVERDLL_X86;
|
||||
extern const char* SERVERDLL_X64;
|
||||
// TinyRun
|
||||
extern const char* TINYRUN_X86;
|
||||
extern const char* TINYRUN_X64;
|
||||
// SCLoader (Shellcode加载器)
|
||||
extern const char* SCLOADER_X86;
|
||||
extern const char* SCLOADER_X64;
|
||||
extern const char* SCLOADER_X86_OLD;
|
||||
extern const char* SCLOADER_X64_OLD;
|
||||
// FRP 相关 (无架构区分,64位DLL)
|
||||
extern const char* FRPC_DLL;
|
||||
extern const char* FRPS_DLL;
|
||||
// 工具
|
||||
extern const char* UPX_EXE;
|
||||
extern const char* RCEDIT_EXE;
|
||||
}
|
||||
|
||||
CString BuildPayloadUrl(const char* ip, const char* name);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "CRcEditDlg.h"
|
||||
#include "afxdialogex.h"
|
||||
#include "Resource.h"
|
||||
#include "BuildDlg.h"
|
||||
|
||||
|
||||
// CRcEditDlg 对话框
|
||||
@@ -78,10 +79,9 @@ void CRcEditDlg::OnOK()
|
||||
MessageBoxL("请选择[*.ico]图标文件或输入进程描述!", "提示", MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
std::string ReleaseEXE(int resID, const char* name);
|
||||
int run_cmd(std::string cmdLine);
|
||||
|
||||
std::string rcedit = ReleaseEXE(IDR_BIN_RCEDIT, "rcedit.exe");
|
||||
std::string rcedit = ReleaseEXE(IDR_BIN_RCEDIT, "rcedit.exe", ResFileName::RCEDIT_EXE);
|
||||
if (rcedit.empty()) {
|
||||
MessageBoxL("解压程序失败,无法操作PE!", "提示", MB_ICONINFORMATION);
|
||||
return;
|
||||
|
||||
@@ -25,6 +25,24 @@ static UINT indicators[] = {
|
||||
#define MAX_SEND_BUFFER 65535
|
||||
#define MAX_RECV_BUFFER 65535
|
||||
|
||||
// 静态成员变量定义 - 历史路径记录
|
||||
CString CFileManagerDlg::s_strLocalHistoryPath;
|
||||
std::map<uint64_t, CString> CFileManagerDlg::s_mapRemoteHistoryPath;
|
||||
CLock CFileManagerDlg::s_lockHistory;
|
||||
|
||||
// 获取有效的客户端ID:基类已经覆盖 m_ClientID + ctx->GetClientID()(含 auth 后钉的值),
|
||||
// 这里仅在它们都拿不到时(老客户端没走 auth)通过 IP 反查主连接做兜底。
|
||||
uint64_t CFileManagerDlg::GetClientID() const
|
||||
{
|
||||
uint64_t id = CDialogBase::GetClientID();
|
||||
if (id != 0) return id;
|
||||
// 老客户端兜底:通过 IP 找主连接获取 ClientID(线程安全)
|
||||
if (g_2015RemoteDlg && m_ContextObject) {
|
||||
return g_2015RemoteDlg->FindClientIDByIP(m_ContextObject->GetPeerName());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
LVITEM* plvi;
|
||||
CString sCol2;
|
||||
@@ -137,10 +155,12 @@ BEGIN_MESSAGE_MAP(CFileManagerDlg, CDialog)
|
||||
ON_COMMAND(IDT_LOCAL_DOWNLOADS, OnLocalDownloads)
|
||||
ON_COMMAND(IDT_LOCAL_HOME, OnLocalHome)
|
||||
ON_COMMAND(IDT_LOCAL_SEARCH, OnLocalSearch)
|
||||
ON_COMMAND(IDT_LOCAL_HISTORY, OnLocalHistory)
|
||||
ON_COMMAND(IDT_REMOTE_DESKTOP, OnRemoteDesktop)
|
||||
ON_COMMAND(IDT_REMOTE_DOWNLOADS, OnRemoteDownloads)
|
||||
ON_COMMAND(IDT_REMOTE_HOME, OnRemoteHome)
|
||||
ON_COMMAND(IDT_REMOTE_SEARCH, OnRemoteSearch)
|
||||
ON_COMMAND(IDT_REMOTE_HISTORY, OnRemoteHistory)
|
||||
ON_COMMAND(IDM_TRANSFER, OnTransfer)
|
||||
ON_COMMAND(IDM_RENAME, OnRename)
|
||||
ON_NOTIFY(LVN_ENDLABELEDIT, IDC_LIST_LOCAL, OnEndlabeleditListLocal)
|
||||
@@ -494,6 +514,12 @@ void CFileManagerDlg::FixedLocalFileList(CString directory)
|
||||
}
|
||||
|
||||
ShowMessage(_TRF("本地:装载目录 %s 完成"), m_Local_Path);
|
||||
|
||||
// 记录本地历史路径
|
||||
if (m_Local_Path.GetLength() > 0) {
|
||||
CAutoCLock lock(s_lockHistory);
|
||||
s_strLocalHistoryPath = m_Local_Path;
|
||||
}
|
||||
}
|
||||
|
||||
void CFileManagerDlg::DropItemOnList(CListCtrl* pDragList, CListCtrl* pDropList)
|
||||
@@ -966,6 +992,8 @@ void CFileManagerDlg::OnReceiveComplete()
|
||||
ShowMessage(_TRF("搜索 \"%s\" 在 %s 完成,共 %d 个结果 (耗时 %d秒)"), m_strSearchName, m_strSearchPath, m_nSearchResultCount, dwElapsed);
|
||||
}
|
||||
break;
|
||||
case TOKEN_CLIENTID:
|
||||
break;
|
||||
default:
|
||||
SendException();
|
||||
break;
|
||||
@@ -1024,6 +1052,13 @@ void CFileManagerDlg::GetRemoteFileList(CString directory)
|
||||
m_Remote_Directory_ComboBox.InsertStringL(0, m_Remote_Path);
|
||||
m_Remote_Directory_ComboBox.SetCurSel(0);
|
||||
|
||||
// 记录远程历史路径(按客户端ID区分)
|
||||
uint64_t clientID = GetClientID();
|
||||
if (m_Remote_Path.GetLength() > 0 && clientID != 0) {
|
||||
CAutoCLock lock(s_lockHistory);
|
||||
s_mapRemoteHistoryPath[clientID] = m_Remote_Path;
|
||||
}
|
||||
|
||||
// 得到返回数据前禁窗口
|
||||
m_list_remote.EnableWindow(FALSE);
|
||||
m_ProgressCtrl->SetPos(0);
|
||||
@@ -1594,6 +1629,57 @@ void CFileManagerDlg::OnRemoteSearch()
|
||||
}
|
||||
}
|
||||
|
||||
void CFileManagerDlg::OnLocalHistory()
|
||||
{
|
||||
// 跳转到上次打开的本地文件夹
|
||||
CString historyPath;
|
||||
{
|
||||
CAutoCLock lock(s_lockHistory);
|
||||
historyPath = s_strLocalHistoryPath;
|
||||
}
|
||||
|
||||
if (historyPath.IsEmpty()) {
|
||||
ShowMessage(_TR("没有本地历史记录"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查目录是否存在
|
||||
if (GetFileAttributesA(historyPath) == INVALID_FILE_ATTRIBUTES) {
|
||||
ShowMessage(_TRF("历史目录不存在: %s"), historyPath);
|
||||
return;
|
||||
}
|
||||
|
||||
m_Local_Path = historyPath;
|
||||
FixedLocalFileList(".");
|
||||
}
|
||||
|
||||
void CFileManagerDlg::OnRemoteHistory()
|
||||
{
|
||||
// 跳转到上次打开的远程文件夹(按客户端ID区分)
|
||||
uint64_t clientID = GetClientID();
|
||||
if (clientID == 0) {
|
||||
ShowMessage(_TR("无法识别远程主机"));
|
||||
return;
|
||||
}
|
||||
|
||||
CString historyPath;
|
||||
{
|
||||
CAutoCLock lock(s_lockHistory);
|
||||
auto it = s_mapRemoteHistoryPath.find(clientID);
|
||||
if (it != s_mapRemoteHistoryPath.end()) {
|
||||
historyPath = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (historyPath.IsEmpty()) {
|
||||
ShowMessage(_TR("没有远程历史记录"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_Remote_Path = historyPath;
|
||||
GetRemoteFileList(".");
|
||||
}
|
||||
|
||||
void CFileManagerDlg::OnLocalView()
|
||||
{
|
||||
// TODO: Add your command handler code here
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "IOCPServer.h"
|
||||
#include "SortListCtrl.h"
|
||||
#include "../../common/locker.h"
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -246,10 +247,12 @@ protected:
|
||||
afx_msg void OnLocalDownloads();
|
||||
afx_msg void OnLocalHome();
|
||||
afx_msg void OnLocalSearch();
|
||||
afx_msg void OnLocalHistory();
|
||||
afx_msg void OnRemoteDesktop();
|
||||
afx_msg void OnRemoteDownloads();
|
||||
afx_msg void OnRemoteHome();
|
||||
afx_msg void OnRemoteSearch();
|
||||
afx_msg void OnRemoteHistory();
|
||||
afx_msg void OnTransferV2ToRemote(); // V2: 本地文件传输到远程
|
||||
afx_msg void OnTransferV2ToLocal(); // V2: 远程文件传输到本地
|
||||
//}}AFX_MSG
|
||||
@@ -274,6 +277,14 @@ private:
|
||||
void EnableControl(BOOL bEnable = TRUE);
|
||||
void CollectFilesRecursive(const std::string& dirPath, std::vector<std::string>& files);
|
||||
float m_fScalingFactor;
|
||||
|
||||
// 历史路径记录(静态,跨实例共享)
|
||||
static CString s_strLocalHistoryPath;
|
||||
static std::map<uint64_t, CString> s_mapRemoteHistoryPath;
|
||||
static CLock s_lockHistory; // 保护历史路径的锁
|
||||
|
||||
// 获取有效的客户端ID(优先用 m_ClientID,否则通过 IP 找主连接)
|
||||
uint64_t GetClientID() const;
|
||||
public:
|
||||
afx_msg void OnFilemangerCompress();
|
||||
afx_msg void OnFilemangerUncompress();
|
||||
|
||||
@@ -228,8 +228,12 @@ public:
|
||||
{
|
||||
return m_bIsClosed;
|
||||
}
|
||||
uint64_t GetClientID() const {
|
||||
return m_ClientID;
|
||||
virtual uint64_t GetClientID() const {
|
||||
// 优先用 UpdateContext 设过的 m_ClientID(重连场景),否则取子连接 ctx 自身的 ID。
|
||||
// 子连接通过 TOKEN_CONN_AUTH 通过校验后,ctx->GetClientID() 已被钉成主连接的 clientID,
|
||||
// 这样 dialog 拿到的 ID 既准确又免去 IP 反查兜底(NAT/127.0.0.1 场景靠谱)。
|
||||
if (m_ClientID != 0) return m_ClientID;
|
||||
return m_ContextObject ? m_ContextObject->GetClientID() : 0;
|
||||
}
|
||||
BOOL SayByeBye()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#include "stdafx.h"
|
||||
#include <WinUser.h>
|
||||
#include <string>
|
||||
#include "KeyBoardDlg.h"
|
||||
#include "2015RemoteDlg.h" // GetClientEncoding helper
|
||||
|
||||
#ifdef _DEBUG
|
||||
#define new DEBUG_NEW
|
||||
@@ -22,7 +24,18 @@ static char THIS_FILE[] = __FILE__;
|
||||
CKeyBoardDlg::CKeyBoardDlg(CWnd* pParent, Server* pIOCPServer, ClientContext *pContext)
|
||||
: DialogBase(CKeyBoardDlg::IDD, pParent, pIOCPServer, pContext, IDI_KEYBOARD)
|
||||
{
|
||||
m_bIsOfflineRecord = (BYTE)m_ContextObject->m_DeCompressionBuffer.GetBuffer(0)[1];
|
||||
m_bIsOfflineRecord = m_ContextObject->m_DeCompressionBuffer.GetBYTE(1);
|
||||
|
||||
// 子连接从协议扩展字段(byte 2-3)拿到能力位,写入自身的 CAPABILITIES。
|
||||
// 这样 m_ContextObject->SupportsUtf8() 可直接生效,不再依赖 IP 反查主连接。
|
||||
// 老客户端只发 2 字节,GetBYTE 越界返回 0,等同 caps=0 -> 走 CP936 兜底,向后兼容。
|
||||
WORD caps = m_ContextObject->m_DeCompressionBuffer.GetBYTE(2)
|
||||
| (m_ContextObject->m_DeCompressionBuffer.GetBYTE(3) << 8);
|
||||
if (caps != 0) {
|
||||
CString capStr;
|
||||
capStr.Format(_T("%04X"), caps);
|
||||
m_ContextObject->SetClientData(ONLINELIST_CAPABILITIES, capStr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +86,32 @@ BOOL CKeyBoardDlg::OnInitDialog()
|
||||
|
||||
UpdateTitle();
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 把 m_edit 重建为 Unicode 类窗口。
|
||||
// 工程是 MBCS,MFC 默认用 A 版 CreateWindowEx 创建子控件,导致即便
|
||||
// 调 SendMessageW(EM_REPLACESEL,...) 系统也会在 W->A 边界用 CP_ACP
|
||||
// 转码,德语机器上中文窗口标题仍会乱码。直接用 CreateWindowExW 重建
|
||||
// 后,控件内部以 Unicode 存储,W 版消息直通,不再走 CP_ACP。
|
||||
// -----------------------------------------------------------------
|
||||
{
|
||||
CRect rc;
|
||||
m_edit.GetWindowRect(&rc);
|
||||
ScreenToClient(&rc);
|
||||
DWORD style = m_edit.GetStyle();
|
||||
DWORD exStyle = m_edit.GetExStyle();
|
||||
HFONT hFont = (HFONT)m_edit.SendMessage(WM_GETFONT, 0, 0);
|
||||
UINT ctrlID = m_edit.GetDlgCtrlID();
|
||||
m_edit.DestroyWindow();
|
||||
HWND hEdit = ::CreateWindowExW(
|
||||
exStyle, L"EDIT", L"", style,
|
||||
rc.left, rc.top, rc.Width(), rc.Height(),
|
||||
this->GetSafeHwnd(), (HMENU)(UINT_PTR)ctrlID,
|
||||
AfxGetInstanceHandle(), NULL);
|
||||
m_edit.Attach(hEdit);
|
||||
if (hFont)
|
||||
m_edit.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
}
|
||||
|
||||
m_edit.SetLimitText(MAXDWORD); // 设置最大长度
|
||||
|
||||
// 通知远程控制端对话框已经打开
|
||||
@@ -110,9 +149,33 @@ void CKeyBoardDlg::AddKeyBoardData()
|
||||
{
|
||||
// 最后填上0
|
||||
m_ContextObject->m_DeCompressionBuffer.Write((LPBYTE)"", 1);
|
||||
int len = m_edit.GetWindowTextLength();
|
||||
m_edit.SetSel(len, len);
|
||||
m_edit.ReplaceSel((TCHAR *)m_ContextObject->m_DeCompressionBuffer.GetBuffer(1));
|
||||
const char* utf8 = (const char*)m_ContextObject->m_DeCompressionBuffer.GetBuffer(1);
|
||||
if (!utf8 || !utf8[0])
|
||||
return;
|
||||
|
||||
// 客户端编码由能力位 CLIENT_CAP_UTF8 决定。
|
||||
// 注意:m_ContextObject 是键盘记录子连接,其自身 CAPABILITIES 为空;
|
||||
// helper 内部通过 peer IP 查主连接获取真正的能力位。
|
||||
UINT cp = GetClientEncoding(m_ContextObject);
|
||||
int wlen = MultiByteToWideChar(cp, 0, utf8, -1, NULL, 0);
|
||||
if (wlen <= 1)
|
||||
return;
|
||||
std::wstring wbuf(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, utf8, -1, &wbuf[0], wlen);
|
||||
|
||||
// 全程走 W 版消息直通 Unicode 控件。注意几个坑:
|
||||
// 1) MFC 的 m_edit.SetSel(...) 默认走 ::SendMessage (A 版) 并紧跟一次
|
||||
// EM_SCROLLCARET,时序变成 "SetSel→ScrollCaret→ReplaceSel",即
|
||||
// 先滚到旧末尾、再插入,部分场景控件状态会错乱(光标不在末尾、
|
||||
// 用户手动移动光标后插入位置不对等)。
|
||||
// 2) EM_SETSEL 用 0x7FFFFFFF 表示"末尾",由控件自行 clamp 到当前长度,
|
||||
// 不依赖 WM_GETTEXTLENGTH 计算结果。
|
||||
// 3) ReplaceSel 后再 ScrollCaret,确保滚到 *新* 末尾。
|
||||
HWND hEdit = m_edit.GetSafeHwnd();
|
||||
if (!hEdit) return;
|
||||
::SendMessageW(hEdit, EM_SETSEL, (WPARAM)0x7FFFFFFF, (LPARAM)0x7FFFFFFF);
|
||||
::SendMessageW(hEdit, EM_REPLACESEL, FALSE, (LPARAM)wbuf.c_str());
|
||||
::SendMessageW(hEdit, EM_SCROLLCARET, 0, 0);
|
||||
}
|
||||
|
||||
bool CKeyBoardDlg::SaveRecord()
|
||||
@@ -129,10 +192,30 @@ bool CKeyBoardDlg::SaveRecord()
|
||||
MessageBox(msg, _TR("提示"), MB_ICONINFORMATION);
|
||||
return false;
|
||||
}
|
||||
// Write the DIB header and the bits
|
||||
CString strRecord;
|
||||
m_edit.GetWindowText(strRecord);
|
||||
file.Write(strRecord, strRecord.GetLength());
|
||||
|
||||
// m_edit 已是 Unicode 控件:用 W 版取宽字符串,转 UTF-8 写入并加 BOM。
|
||||
// 这样保存的文件无视服务端 ACP,记事本/VS Code 等都能自动识别。
|
||||
int wlen = ::GetWindowTextLengthW(m_edit.GetSafeHwnd());
|
||||
std::wstring wbuf;
|
||||
if (wlen > 0) {
|
||||
wbuf.resize(wlen);
|
||||
::GetWindowTextW(m_edit.GetSafeHwnd(), &wbuf[0], wlen + 1);
|
||||
}
|
||||
|
||||
// UTF-8 BOM
|
||||
const BYTE bom[3] = { 0xEF, 0xBB, 0xBF };
|
||||
file.Write(bom, 3);
|
||||
|
||||
if (!wbuf.empty()) {
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen,
|
||||
NULL, 0, NULL, NULL);
|
||||
if (u8len > 0) {
|
||||
std::string u8(u8len, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen,
|
||||
&u8[0], u8len, NULL, NULL);
|
||||
file.Write(u8.data(), (UINT)u8.size());
|
||||
}
|
||||
}
|
||||
file.Close();
|
||||
|
||||
return true;
|
||||
@@ -156,7 +239,8 @@ void CKeyBoardDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
||||
} else if (nID == IDM_CLEAR_RECORD) {
|
||||
BYTE bToken = COMMAND_KEYBOARD_CLEAR;
|
||||
m_ContextObject->Send2Client(&bToken, 1);
|
||||
m_edit.SetWindowText("");
|
||||
// m_edit 是 Unicode 类控件,调 W 版避免 CP_ACP 边界转换
|
||||
::SetWindowTextW(m_edit.GetSafeHwnd(), L"");
|
||||
} else if (nID == IDM_SAVE_RECORD) {
|
||||
SaveRecord();
|
||||
} else {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <locale.h>
|
||||
#include <afxwin.h>
|
||||
#include "common/IniParser.h"
|
||||
#include "resource.h" // 用于内嵌语言资源 ID
|
||||
|
||||
// 设置线程区域为简体中文
|
||||
// 这样 MBCS 程序在非中文系统上创建对话框时,也能正确解码 RC 资源中的 GBK 中文
|
||||
@@ -55,17 +57,20 @@ public:
|
||||
} else {
|
||||
m_langDir = langDir;
|
||||
}
|
||||
|
||||
// 确保目录存在
|
||||
CreateDirectory(m_langDir, NULL);
|
||||
}
|
||||
|
||||
// 获取可用的语言列表
|
||||
// 获取可用的语言列表(包括内嵌语言)
|
||||
std::vector<CString> GetAvailableLanguages()
|
||||
{
|
||||
std::vector<CString> langs;
|
||||
CString searchPath = m_langDir + _T("\\*.ini");
|
||||
std::set<CString> langSet; // 用于去重
|
||||
|
||||
// 1. 添加内嵌语言(始终可用)
|
||||
langSet.insert(_T("en_US"));
|
||||
langSet.insert(_T("zh_TW"));
|
||||
|
||||
// 2. 扫描磁盘上的语言文件
|
||||
CString searchPath = m_langDir + _T("\\*.ini");
|
||||
WIN32_FIND_DATA fd;
|
||||
HANDLE hFind = FindFirstFile(searchPath, &fd);
|
||||
if (hFind != INVALID_HANDLE_VALUE) {
|
||||
@@ -73,30 +78,43 @@ public:
|
||||
CString filename(fd.cFileName);
|
||||
int dotPos = filename.ReverseFind(_T('.'));
|
||||
if (dotPos > 0) {
|
||||
langs.push_back(filename.Left(dotPos));
|
||||
langSet.insert(filename.Left(dotPos));
|
||||
}
|
||||
} while (FindNextFile(hFind, &fd));
|
||||
FindClose(hFind);
|
||||
}
|
||||
|
||||
// 转为 vector 返回
|
||||
for (const auto& lang : langSet) {
|
||||
langs.push_back(lang);
|
||||
}
|
||||
return langs;
|
||||
}
|
||||
|
||||
// 检查语言文件编码是否为 ANSI
|
||||
// 返回 false 表示文件不存在或编码不是 ANSI(检测 BOM 和 UTF-8 无 BOM)
|
||||
// 返回 false 表示编码不是 ANSI(检测 BOM 和 UTF-8 无 BOM)
|
||||
// 内嵌语言(en_US, zh_TW)直接返回 true
|
||||
bool CheckEncoding(const CString& langCode)
|
||||
{
|
||||
// 中文模式无需检查
|
||||
if (langCode == _T("zh_CN") || langCode.IsEmpty()) {
|
||||
TRACE("[LangEnc] zh_CN or empty, skip check\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 内嵌语言无需检查(已确保编码正确)
|
||||
if (langCode == _T("en_US") || langCode == _T("zh_TW")) {
|
||||
TRACE("[LangEnc] builtin language, skip check\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
CString langFile = m_langDir + _T("\\") + langCode + _T(".ini");
|
||||
TRACE("[LangEnc] Checking: %s\n", (LPCSTR)langFile);
|
||||
|
||||
FILE* f = nullptr;
|
||||
if (fopen_s(&f, (LPCSTR)langFile, "rb") != 0 || !f) {
|
||||
TRACE("[LangEnc] fopen failed\n");
|
||||
return false;
|
||||
return false; // 非内嵌语言必须有磁盘文件
|
||||
}
|
||||
|
||||
// 读取文件内容(最多检测前 4KB 即可判断)
|
||||
@@ -164,26 +182,103 @@ public:
|
||||
}
|
||||
|
||||
// 加载语言文件
|
||||
// 优先从内嵌资源加载,然后用磁盘文件覆盖(如果存在)
|
||||
bool Load(const CString& langCode)
|
||||
{
|
||||
m_strings.clear();
|
||||
m_currentLang = langCode;
|
||||
|
||||
// 如果是中文,不需要加载翻译
|
||||
// 中文模式:检查是否有补丁文件
|
||||
if (langCode == _T("zh_CN") || langCode.IsEmpty()) {
|
||||
// 尝试加载中文补丁文件(可选)
|
||||
CString patchFile = m_langDir + _T("\\zh_CN.ini");
|
||||
if (GetFileAttributes(patchFile) != INVALID_FILE_ATTRIBUTES) {
|
||||
CIniParser ini;
|
||||
if (ini.LoadFile((LPCSTR)patchFile)) {
|
||||
const CIniParser::TKeyVal* pSection = ini.GetSection("Strings");
|
||||
if (pSection) {
|
||||
for (const auto& kv : *pSection) {
|
||||
m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str());
|
||||
}
|
||||
}
|
||||
TRACE("[Lang] Loaded zh_CN patch: %d strings\n", (int)m_strings.size());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
CString langFile = m_langDir + _T("\\") + langCode + _T(".ini");
|
||||
// 1. 先从内嵌资源加载(英语和繁体中文)
|
||||
bool hasBuiltin = LoadFromResource(langCode);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (GetFileAttributes(langFile) == INVALID_FILE_ATTRIBUTES) {
|
||||
// 2. 再从磁盘文件加载(覆盖内嵌翻译)
|
||||
CString langFile = m_langDir + _T("\\") + langCode + _T(".ini");
|
||||
if (GetFileAttributes(langFile) != INVALID_FILE_ATTRIBUTES) {
|
||||
CIniParser ini;
|
||||
// 如果有内嵌资源,使用追加模式覆盖;否则使用普通加载
|
||||
if (hasBuiltin) {
|
||||
// 追加模式:磁盘文件中的翻译会覆盖内嵌翻译
|
||||
if (ini.LoadFile((LPCSTR)langFile)) {
|
||||
const CIniParser::TKeyVal* pSection = ini.GetSection("Strings");
|
||||
if (pSection) {
|
||||
for (const auto& kv : *pSection) {
|
||||
m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str());
|
||||
}
|
||||
}
|
||||
TRACE("[Lang] Loaded disk file (override): %s\n", (LPCSTR)langFile);
|
||||
}
|
||||
} else {
|
||||
// 无内嵌资源,直接从磁盘加载
|
||||
if (ini.LoadFile((LPCSTR)langFile)) {
|
||||
const CIniParser::TKeyVal* pSection = ini.GetSection("Strings");
|
||||
if (pSection) {
|
||||
for (const auto& kv : *pSection) {
|
||||
m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str());
|
||||
}
|
||||
}
|
||||
TRACE("[Lang] Loaded disk file: %s\n", (LPCSTR)langFile);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasBuiltin || !m_strings.empty();
|
||||
}
|
||||
|
||||
// 从内嵌资源加载语言数据
|
||||
bool LoadFromResource(const CString& langCode)
|
||||
{
|
||||
UINT resID = 0;
|
||||
if (langCode == _T("en_US")) {
|
||||
resID = IDR_LANG_EN_US;
|
||||
} else if (langCode == _T("zh_TW")) {
|
||||
resID = IDR_LANG_ZH_TW;
|
||||
} else {
|
||||
return false; // 无内嵌资源
|
||||
}
|
||||
|
||||
HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(resID), RT_RCDATA);
|
||||
if (!hRes) {
|
||||
TRACE("[Lang] Resource not found: %d\n", resID);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用 CIniParser 解析,无文件大小限制,且不 trim key
|
||||
HGLOBAL hData = LoadResource(NULL, hRes);
|
||||
if (!hData) {
|
||||
TRACE("[Lang] Failed to load resource: %d\n", resID);
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* data = (const char*)LockResource(hData);
|
||||
DWORD size = SizeofResource(NULL, hRes);
|
||||
if (!data || size == 0) {
|
||||
TRACE("[Lang] Empty resource: %d\n", resID);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用 CIniParser 从内存解析
|
||||
CIniParser ini;
|
||||
if (!ini.LoadFile((LPCSTR)langFile)) {
|
||||
if (!ini.LoadFromMemory(data, size)) {
|
||||
TRACE("[Lang] Failed to parse resource: %d\n", resID);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -194,6 +289,8 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
TRACE("[Lang] Loaded builtin resource: %s (%d strings)\n",
|
||||
(LPCSTR)langCode, (int)m_strings.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -625,24 +722,24 @@ protected:
|
||||
AppendData(&dlgTemplate, sizeof(DLGTEMPLATE));
|
||||
AppendWord(0); // 菜单
|
||||
AppendWord(0); // 窗口类
|
||||
AppendString(_T("选择语言 / Select Language"));
|
||||
AppendString(_T("Select Language"));
|
||||
AlignToDword();
|
||||
|
||||
// 静态文本
|
||||
AddControl(0x0082, 15, 15, 40, 12, (WORD)-1,
|
||||
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("语言:"));
|
||||
AddControl(0x0082, 15, 15, 50, 12, (WORD)-1,
|
||||
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("Language:"));
|
||||
|
||||
// ComboBox
|
||||
AddControl(0x0085, 55, 13, 130, 150, 1001,
|
||||
AddControl(0x0085, 65, 13, 120, 150, 1001,
|
||||
CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T(""));
|
||||
|
||||
// 确定按钮
|
||||
// OK 按钮
|
||||
AddControl(0x0080, 45, 50, 50, 14, IDOK,
|
||||
BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("确定"));
|
||||
BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("OK"));
|
||||
|
||||
// 取消按钮
|
||||
// Cancel 按钮
|
||||
AddControl(0x0080, 105, 50, 50, 14, IDCANCEL,
|
||||
BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("取消"));
|
||||
BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("Cancel"));
|
||||
|
||||
return (LPCDLGTEMPLATE)m_templateBuffer.data();
|
||||
}
|
||||
@@ -703,8 +800,8 @@ protected:
|
||||
|
||||
m_comboLang.SubclassDlgItem(1001, this);
|
||||
|
||||
// 添加简体中文
|
||||
int idx = m_comboLang.AddString(_T("简体中文"));
|
||||
// 添加简体中文(显示为英语避免乱码)
|
||||
int idx = m_comboLang.AddString(GetLanguageDisplayName(_T("zh_CN")));
|
||||
m_langCodes.push_back(_T("zh_CN"));
|
||||
m_comboLang.SetItemData(idx, 0);
|
||||
|
||||
|
||||
243
server/2015Remote/PreviewTipWnd.cpp
Normal file
243
server/2015Remote/PreviewTipWnd.cpp
Normal file
@@ -0,0 +1,243 @@
|
||||
// PreviewTipWnd.cpp
|
||||
#include "stdafx.h"
|
||||
#include "PreviewTipWnd.h"
|
||||
|
||||
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
|
||||
#include <gdiplus.h>
|
||||
#pragma comment(lib, "gdiplus.lib")
|
||||
|
||||
using namespace Gdiplus;
|
||||
|
||||
namespace {
|
||||
constexpr int PADDING = 8;
|
||||
constexpr int IMAGE_TEXT_GAP = 6; // 图像与下方文本之间的留白
|
||||
constexpr int MIN_TEXT_W = 360;
|
||||
constexpr int MAX_TEXT_W = 720; // 文本可换行宽度上限(垂直布局,宽度不再受图像挤压)
|
||||
constexpr int MAX_IMAGE_W = 960; // 显示上限(防止 LAN 档 1280 的 JPEG 把窗口顶到屏幕外)
|
||||
constexpr int LOADING_W = 480; // 占位的最小宽度(图像未到达时)
|
||||
constexpr int LOADING_H = 270;
|
||||
} // namespace
|
||||
|
||||
BEGIN_MESSAGE_MAP(CPreviewTipWnd, CWnd)
|
||||
ON_WM_PAINT()
|
||||
ON_WM_ERASEBKGND()
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
CPreviewTipWnd::CPreviewTipWnd() = default;
|
||||
|
||||
CPreviewTipWnd::~CPreviewTipWnd()
|
||||
{
|
||||
// m_image 通过 unique_ptr 自动释放
|
||||
}
|
||||
|
||||
BOOL CPreviewTipWnd::Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW)
|
||||
{
|
||||
m_text = text;
|
||||
m_imageReserveW = imageReserveW > 0 ? min(imageReserveW, MAX_IMAGE_W) : 0;
|
||||
m_imageReserveH = m_imageReserveW > 0 ? (m_imageReserveW * 9 / 16) : 0;
|
||||
if (m_imageReserveW > 0 && m_imageReserveW < LOADING_W) {
|
||||
m_imageReserveW = LOADING_W;
|
||||
m_imageReserveH = LOADING_H;
|
||||
}
|
||||
|
||||
// 创建字体(项目是 MBCS,但浮窗用宽字符;显式 LOGFONTW + W 版 API 避免编码混淆)
|
||||
LOGFONTW lf = {};
|
||||
HFONT hSysFont = (HFONT)::GetStockObject(DEFAULT_GUI_FONT);
|
||||
if (hSysFont && ::GetObjectW(hSysFont, sizeof(lf), &lf) == 0) {
|
||||
hSysFont = nullptr;
|
||||
}
|
||||
if (!hSysFont) {
|
||||
lf.lfHeight = -12;
|
||||
wcscpy_s(lf.lfFaceName, L"Microsoft YaHei");
|
||||
}
|
||||
HFONT hF = ::CreateFontIndirectW(&lf);
|
||||
if (hF) m_font.Attach(hF);
|
||||
|
||||
// 注册自绘窗口类:用 MFC 的 AfxRegisterWndClass 确保和 MFC 子类化机制兼容
|
||||
LPCTSTR kClass = AfxRegisterWndClass(
|
||||
CS_SAVEBITS,
|
||||
::LoadCursor(NULL, IDC_ARROW),
|
||||
(HBRUSH)(COLOR_INFOBK + 1),
|
||||
NULL);
|
||||
|
||||
// 临时尺寸;RecalcLayoutAndResize 会在创建后调整
|
||||
CRect rc(anchor.x, anchor.y, anchor.x + 400, anchor.y + 200);
|
||||
BOOL bOk = CWnd::CreateEx(
|
||||
WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
|
||||
kClass, _T(""),
|
||||
WS_POPUP | WS_BORDER,
|
||||
rc, pParent, 0);
|
||||
if (!bOk) return FALSE;
|
||||
|
||||
RecalcLayoutAndResize();
|
||||
|
||||
ShowWindow(SW_SHOWNOACTIVATE);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::SetImageFromJpeg(const BYTE* data, size_t bytes)
|
||||
{
|
||||
if (!data || bytes == 0) return;
|
||||
|
||||
HGLOBAL hMem = ::GlobalAlloc(GMEM_MOVEABLE, bytes);
|
||||
if (!hMem) return;
|
||||
void* p = ::GlobalLock(hMem);
|
||||
if (!p) { ::GlobalFree(hMem); return; }
|
||||
memcpy(p, data, bytes);
|
||||
::GlobalUnlock(hMem);
|
||||
|
||||
IStream* stream = nullptr;
|
||||
if (FAILED(::CreateStreamOnHGlobal(hMem, TRUE, &stream))) {
|
||||
::GlobalFree(hMem);
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_ptr<Bitmap> bmp(new Bitmap(stream, FALSE));
|
||||
stream->Release(); // CreateStreamOnHGlobal(..., TRUE) 释放 stream 时会释放 hMem
|
||||
|
||||
if (!bmp || bmp->GetLastStatus() != Ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_image = std::move(bmp);
|
||||
m_hasImage = true;
|
||||
m_unavailable = false;
|
||||
RecalcLayoutAndResize();
|
||||
if (GetSafeHwnd()) Invalidate();
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::MarkPreviewUnavailable()
|
||||
{
|
||||
if (m_hasImage) return; // 已经有图就不再覆盖
|
||||
m_unavailable = true;
|
||||
if (GetSafeHwnd()) Invalidate();
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::RecalcLayoutAndResize()
|
||||
{
|
||||
HWND hWnd = GetSafeHwnd();
|
||||
if (!hWnd) return;
|
||||
|
||||
// 估算图像尺寸(先算图像,因为文本宽度要参考图像宽度对齐)
|
||||
if (m_image) {
|
||||
int iw = (int)m_image->GetWidth();
|
||||
int ih = (int)m_image->GetHeight();
|
||||
if (iw > MAX_IMAGE_W) {
|
||||
ih = (int)((double)ih * MAX_IMAGE_W / iw + 0.5);
|
||||
iw = MAX_IMAGE_W;
|
||||
}
|
||||
m_imageDrawW = iw;
|
||||
m_imageDrawH = ih;
|
||||
} else {
|
||||
m_imageDrawW = m_imageReserveW;
|
||||
m_imageDrawH = m_imageReserveH;
|
||||
}
|
||||
|
||||
// 文本换行宽度:与图像同宽(让文本视觉上对齐到图像下方),但不超过 MAX_TEXT_W
|
||||
// 没有图像时退化到 MAX_TEXT_W
|
||||
int textWrapW = m_imageDrawW > 0 ? min((int)MAX_TEXT_W, m_imageDrawW) : (int)MAX_TEXT_W;
|
||||
if (textWrapW < MIN_TEXT_W) textWrapW = MIN_TEXT_W;
|
||||
|
||||
// 估算文本尺寸(项目是 MBCS,但浮窗文本是宽字符,必须显式调用 W 版本)
|
||||
CClientDC dc(this);
|
||||
CFont* old = dc.SelectObject(&m_font);
|
||||
CRect rcText(0, 0, textWrapW, 32767);
|
||||
::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &rcText,
|
||||
DT_CALCRECT | DT_LEFT | DT_WORDBREAK | DT_NOPREFIX);
|
||||
m_textW = max((int)MIN_TEXT_W, rcText.Width());
|
||||
if (m_imageDrawW > 0) m_textW = max(m_textW, m_imageDrawW); // 至少与图像同宽
|
||||
m_textH = rcText.Height();
|
||||
dc.SelectObject(old);
|
||||
|
||||
int contentW = max(m_imageDrawW, m_textW);
|
||||
int gap = (m_imageDrawW > 0 && m_textH > 0) ? IMAGE_TEXT_GAP : 0;
|
||||
int totalW = PADDING + contentW + PADDING;
|
||||
int totalH = PADDING + m_imageDrawH + gap + m_textH + PADDING;
|
||||
|
||||
// 当前左上角
|
||||
CRect rc;
|
||||
GetWindowRect(&rc);
|
||||
int newX = rc.left;
|
||||
int newY = rc.top;
|
||||
|
||||
// 防止越出屏幕:右下夹紧到工作区
|
||||
HMONITOR hMon = MonitorFromPoint(CPoint(newX, newY), MONITOR_DEFAULTTONEAREST);
|
||||
MONITORINFO mi = { sizeof(mi) };
|
||||
if (hMon && GetMonitorInfo(hMon, &mi)) {
|
||||
if (newX + totalW > mi.rcWork.right) newX = max((int)mi.rcWork.left, mi.rcWork.right - totalW);
|
||||
if (newY + totalH > mi.rcWork.bottom) newY = max((int)mi.rcWork.top, mi.rcWork.bottom - totalH);
|
||||
}
|
||||
|
||||
SetWindowPos(NULL, newX, newY, totalW, totalH, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
|
||||
BOOL CPreviewTipWnd::OnEraseBkgnd(CDC* /*pDC*/)
|
||||
{
|
||||
return TRUE; // 在 OnPaint 里整体填充
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::OnPaint()
|
||||
{
|
||||
CPaintDC pdc(this);
|
||||
CRect rcClient;
|
||||
GetClientRect(&rcClient);
|
||||
|
||||
// 双缓冲
|
||||
CDC memDC;
|
||||
memDC.CreateCompatibleDC(&pdc);
|
||||
CBitmap memBmp;
|
||||
memBmp.CreateCompatibleBitmap(&pdc, rcClient.Width(), rcClient.Height());
|
||||
CBitmap* oldBmp = memDC.SelectObject(&memBmp);
|
||||
|
||||
memDC.FillSolidRect(&rcClient, ::GetSysColor(COLOR_INFOBK));
|
||||
|
||||
CFont* oldFont = memDC.SelectObject(&m_font);
|
||||
memDC.SetTextColor(::GetSysColor(COLOR_INFOTEXT));
|
||||
memDC.SetBkMode(TRANSPARENT);
|
||||
|
||||
int curY = PADDING;
|
||||
if (m_imageDrawW > 0 && m_imageDrawH > 0) {
|
||||
CRect rcImg(PADDING, curY, PADDING + m_imageDrawW, curY + m_imageDrawH);
|
||||
DrawImageArea(memDC, rcImg);
|
||||
curY += m_imageDrawH + IMAGE_TEXT_GAP;
|
||||
}
|
||||
|
||||
CRect rcText(PADDING, curY, PADDING + m_textW, curY + m_textH);
|
||||
DrawTextArea(memDC, rcText);
|
||||
|
||||
memDC.SelectObject(oldFont);
|
||||
pdc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &memDC, 0, 0, SRCCOPY);
|
||||
memDC.SelectObject(oldBmp);
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::DrawImageArea(CDC& dc, const CRect& rc)
|
||||
{
|
||||
// 边框
|
||||
dc.Draw3dRect(&rc, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DSHADOW));
|
||||
|
||||
if (m_hasImage && m_image) {
|
||||
Graphics g(dc.GetSafeHdc());
|
||||
g.SetInterpolationMode(InterpolationModeHighQualityBicubic);
|
||||
g.SetSmoothingMode(SmoothingModeHighQuality);
|
||||
g.DrawImage(m_image.get(), rc.left + 1, rc.top + 1, rc.Width() - 2, rc.Height() - 2);
|
||||
} else {
|
||||
// 占位灰色背景
|
||||
CRect rcInner = rc;
|
||||
rcInner.DeflateRect(1, 1);
|
||||
dc.FillSolidRect(&rcInner, RGB(245, 245, 245));
|
||||
|
||||
const wchar_t* placeholder = m_unavailable ? L"Preview Unavailable" : L"Loading Preview ...";
|
||||
UINT fmt = DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX;
|
||||
dc.SetTextColor(m_unavailable ? RGB(160, 80, 80) : RGB(120, 120, 120));
|
||||
RECT rcInnerRaw = rcInner;
|
||||
::DrawTextW(dc.GetSafeHdc(), placeholder, -1, &rcInnerRaw, fmt);
|
||||
dc.SetTextColor(::GetSysColor(COLOR_INFOTEXT));
|
||||
}
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::DrawTextArea(CDC& dc, const CRect& rc)
|
||||
{
|
||||
RECT r = rc;
|
||||
::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &r,
|
||||
DT_LEFT | DT_TOP | DT_WORDBREAK | DT_NOPREFIX);
|
||||
}
|
||||
57
server/2015Remote/PreviewTipWnd.h
Normal file
57
server/2015Remote/PreviewTipWnd.h
Normal file
@@ -0,0 +1,57 @@
|
||||
// PreviewTipWnd.h
|
||||
// 双击在线主机时弹出的浮窗:上方 JPEG 缩略图,下方主机信息文本。
|
||||
// 无图片时显示"加载预览…"占位,供 OnListClick 即时弹窗、收到响应后再 SetImage 重画。
|
||||
#pragma once
|
||||
|
||||
#include <afxwin.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Gdiplus { class Bitmap; }
|
||||
|
||||
class CPreviewTipWnd : public CWnd
|
||||
{
|
||||
public:
|
||||
CPreviewTipWnd();
|
||||
virtual ~CPreviewTipWnd();
|
||||
|
||||
// 创建浮窗。
|
||||
// anchor 屏幕坐标,浮窗左上角
|
||||
// text 下方显示的主机详情文本(宽字符,确保跨语言系统正确渲染)
|
||||
// imageReserveW 上方图像区域预留宽度(即将到来的预览最大宽度,仅作初始布局)
|
||||
// 为 0 表示不预留 — 与老 STATIC 路径行为一致(仅文本)
|
||||
BOOL Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW);
|
||||
|
||||
// 收到 JPEG 后调用:解码并重画。线程安全前提是只在主 UI 线程调用。
|
||||
void SetImageFromJpeg(const BYTE* data, size_t bytes);
|
||||
// 标记预览不可用(请求超时 / 客户端报错)。
|
||||
void MarkPreviewUnavailable();
|
||||
|
||||
WORD GetReqId() const { return m_reqId; }
|
||||
void SetReqId(WORD id) { m_reqId = id; }
|
||||
|
||||
protected:
|
||||
afx_msg void OnPaint();
|
||||
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
||||
DECLARE_MESSAGE_MAP()
|
||||
|
||||
private:
|
||||
void RecalcLayoutAndResize();
|
||||
void DrawImageArea(CDC& dc, const CRect& rc);
|
||||
void DrawTextArea(CDC& dc, const CRect& rc);
|
||||
|
||||
CStringW m_text;
|
||||
int m_imageReserveW = 0; // 预留图像宽度(图像未到达时占位)
|
||||
int m_imageReserveH = 0; // 预留图像高度(按 16:9)
|
||||
int m_imageDrawW = 0; // 实际图像绘制宽度
|
||||
int m_imageDrawH = 0; // 实际图像绘制高度
|
||||
int m_textW = 0;
|
||||
int m_textH = 0;
|
||||
|
||||
bool m_hasImage = false;
|
||||
bool m_unavailable = false;
|
||||
std::unique_ptr<Gdiplus::Bitmap> m_image;
|
||||
|
||||
CFont m_font;
|
||||
WORD m_reqId = 0;
|
||||
};
|
||||
@@ -157,8 +157,9 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
|
||||
if (pClientID) {
|
||||
m_ClientID = *((uint64_t*)pClientID);
|
||||
|
||||
// Notify web clients of resolution (important for clients that only send TOKEN_BITMAPINFO once)
|
||||
if (WebService().IsRunning()) {
|
||||
// Notify web clients of resolution (only for Web sessions, not MFC sessions)
|
||||
// At this point, IsMfcTriggered is still set if MFC triggered this dialog
|
||||
if (WebService().IsRunning() && !WebService().IsMfcTriggered(m_ClientID)) {
|
||||
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
||||
@@ -237,10 +238,14 @@ CScreenSpyDlg::~CScreenSpyDlg()
|
||||
StopAudioPlayback();
|
||||
|
||||
// 清理所有文件接收对话框
|
||||
for (auto& pair : m_FileRecvDlgs) {
|
||||
if (pair.second) {
|
||||
pair.second->DestroyWindow();
|
||||
delete pair.second;
|
||||
// 注意:对话框可能已经被用户关闭并自我销毁(PostNcDestroy 中 delete this)
|
||||
// 存储了 HWND 用于安全检查,避免访问野指针
|
||||
for (auto& entry : m_FileRecvDlgs) {
|
||||
HWND hWnd = entry.second.first;
|
||||
if (hWnd && ::IsWindow(hWnd)) {
|
||||
// 通过 HWND 同步发送关闭消息,确保对话框在析构前完全关闭
|
||||
// 使用 SendMessage 而非 PostMessage,避免异步问题
|
||||
::SendMessage(hWnd, WM_CLOSE, 0, 0);
|
||||
}
|
||||
}
|
||||
m_FileRecvDlgs.clear();
|
||||
@@ -484,6 +489,8 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
|
||||
ON_WM_VSCROLL()
|
||||
ON_WM_LBUTTONDOWN()
|
||||
ON_WM_LBUTTONUP()
|
||||
ON_WM_RBUTTONDOWN()
|
||||
ON_WM_RBUTTONUP()
|
||||
ON_WM_MOUSEWHEEL()
|
||||
ON_WM_MOUSEMOVE()
|
||||
ON_WM_MOUSELEAVE()
|
||||
@@ -492,6 +499,7 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
|
||||
ON_WM_LBUTTONDBLCLK()
|
||||
ON_WM_ACTIVATE()
|
||||
ON_WM_TIMER()
|
||||
ON_WM_ERASEBKGND()
|
||||
ON_COMMAND(ID_EXIT_FULLSCREEN, &CScreenSpyDlg::OnExitFullscreen)
|
||||
ON_COMMAND(ID_SHOW_STATUS_INFO, &CScreenSpyDlg::OnShowStatusInfo)
|
||||
ON_COMMAND(ID_HIDE_STATUS_INFO, &CScreenSpyDlg::OnHideStatusInfo)
|
||||
@@ -500,6 +508,7 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
|
||||
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CScreenSpyDlg::OnRecvFileV2Chunk)
|
||||
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CScreenSpyDlg::OnRecvFileV2Complete)
|
||||
ON_WM_DROPFILES()
|
||||
ON_WM_CAPTURECHANGED()
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
|
||||
@@ -683,7 +692,7 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
if (m_bIsCtrl) {
|
||||
ImmAssociateContext(m_hWnd, NULL); // 控制模式:禁用 IME
|
||||
}
|
||||
m_bIsTraceCursor = FALSE; //不是跟踪
|
||||
m_bIsTraceCursor = !m_bIsCtrl; // 非控制状态,则跟踪鼠标
|
||||
m_ClientCursorPos.x = 0;
|
||||
m_ClientCursorPos.y = 0;
|
||||
m_bCursorIndex = 0;
|
||||
@@ -761,14 +770,32 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
if (pMain)
|
||||
::PostMessage(pMain->GetSafeHwnd(), WM_SESSION_ACTIVATED, (WPARAM)this, 0);
|
||||
|
||||
// 注册屏幕上下文到 WebService(用于 Web 端鼠标/键盘控制)
|
||||
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
|
||||
// Determine session type: MFC or Web
|
||||
// Must check MfcTriggered FIRST - if MFC triggered this dialog, it's NOT a web session
|
||||
// even if WebTriggered is also true (happens when Web is already open for same device)
|
||||
bool isMfcSession = WebService().IsMfcTriggered(m_ClientID);
|
||||
bool isWebSession = false;
|
||||
if (isMfcSession) {
|
||||
// MFC session: clear the flag, don't register with WebService
|
||||
WebService().ClearMfcTriggered(m_ClientID);
|
||||
// m_bIsWebSession remains false (default)
|
||||
} else {
|
||||
// Check if this is a Web session
|
||||
isWebSession = WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions();
|
||||
|
||||
// Hide window if this session was triggered by web client
|
||||
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
|
||||
// Only register screen context for Web sessions
|
||||
// MFC dialogs handle input directly via m_ContextObject, don't need WebService registry
|
||||
// This prevents MFC close from deleting Web's context (they share same device_id key)
|
||||
if (isWebSession) {
|
||||
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
|
||||
m_bHide = true;
|
||||
m_bIsWebSession = true;
|
||||
ShowWindow(SW_HIDE);
|
||||
}
|
||||
}
|
||||
|
||||
Mprintf("[ScreenSpy] Dialog created for device %llu, isMfcSession=%d, isWebSession=%d\n",
|
||||
m_ClientID, isMfcSession ? 1 : 0, isWebSession ? 1 : 0);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
@@ -776,8 +803,10 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
|
||||
VOID CScreenSpyDlg::OnClose()
|
||||
{
|
||||
// 注销屏幕上下文(Web 端控制)
|
||||
// Only unregister if this is a Web session (we only registered for Web sessions)
|
||||
if (m_bIsWebSession) {
|
||||
WebService().UnregisterScreenContext(m_ClientID);
|
||||
}
|
||||
|
||||
m_bIsClosed = true;
|
||||
m_bIsCtrl = FALSE;
|
||||
@@ -840,13 +869,15 @@ LRESULT CScreenSpyDlg::OnRecvFileV2Chunk(WPARAM wParam, LPARAM lParam)
|
||||
uint64_t transferID = msgData->transferID;
|
||||
|
||||
// 创建或获取进度对话框(按 transferID 管理)
|
||||
CDlgFileSend*& dlg = m_FileRecvDlgs[transferID];
|
||||
if (dlg == nullptr) {
|
||||
auto& entry = m_FileRecvDlgs[transferID];
|
||||
CDlgFileSend* dlg = entry.second;
|
||||
if (dlg == nullptr || !::IsWindow(entry.first)) {
|
||||
dlg = new CDlgFileSend(m_pParent, m_ContextObject->GetServer(), m_ContextObject, FALSE);
|
||||
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
|
||||
dlg->SetWindowTextA(_TR("接收文件"));
|
||||
dlg->ShowWindow(SW_SHOW);
|
||||
dlg->m_bKeepConnection = TRUE; // 不断开连接
|
||||
entry = { dlg->GetSafeHwnd(), dlg };
|
||||
}
|
||||
|
||||
// 接收文件
|
||||
@@ -889,7 +920,11 @@ LRESULT CScreenSpyDlg::OnRecvFileV2Complete(WPARAM wParam, LPARAM lParam)
|
||||
// 关闭进度对话框
|
||||
auto it = m_FileRecvDlgs.find(transferID);
|
||||
if (it != m_FileRecvDlgs.end()) {
|
||||
it->second->FinishFileSend(verifyOk);
|
||||
// 只有窗口有效时才调用 FinishFileSend
|
||||
if (::IsWindow(it->second.first)) {
|
||||
it->second.second->FinishFileSend(verifyOk);
|
||||
}
|
||||
// 无论窗口是否有效,都要移除条目(避免累积无效条目)
|
||||
m_FileRecvDlgs.erase(it);
|
||||
}
|
||||
|
||||
@@ -964,18 +999,11 @@ VOID CScreenSpyDlg::OnReceiveComplete()
|
||||
PrepareDrawing(m_BitmapInfor_Full);
|
||||
// 分辨率切换完成,允许解码
|
||||
m_bResolutionChanging = false;
|
||||
// Notify web clients of resolution change
|
||||
if (WebService().IsRunning()) {
|
||||
// Notify web clients of resolution change (only for Web session dialogs)
|
||||
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
||||
|
||||
// Hide window if this session was triggered by web client (and hiding is enabled)
|
||||
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
|
||||
m_bHide = true;
|
||||
ShowWindow(SW_HIDE);
|
||||
Mprintf("[ScreenSpyDlg] Web-triggered session, hiding window for device %llu\n", m_ClientID);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1266,8 +1294,8 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
||||
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2+sizeof(POINT))[0];
|
||||
if (bOldCursorIndex != m_bCursorIndex) {
|
||||
bChange = TRUE;
|
||||
// 通知 Web 客户端光标变化
|
||||
if (WebService().IsRunning()) {
|
||||
// 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
|
||||
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
||||
}
|
||||
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
|
||||
@@ -1317,8 +1345,8 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
||||
bChange = TRUE;
|
||||
}
|
||||
}
|
||||
// Broadcast H264 keyframe to web clients
|
||||
if (NextScreenLength > 0 && WebService().IsRunning()) {
|
||||
// Broadcast H264 keyframe to web clients (only for Web session dialogs)
|
||||
if (m_bIsWebSession && NextScreenLength > 0 && WebService().IsRunning()) {
|
||||
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
|
||||
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
|
||||
uint8_t frameType = 1; // Keyframe
|
||||
@@ -1376,9 +1404,9 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
||||
bChange = TRUE;
|
||||
}
|
||||
}
|
||||
// Broadcast H264 frame to web clients
|
||||
// Broadcast H264 frame to web clients (only for Web session dialogs)
|
||||
// Format: [DeviceID:4][FrameType:1][DataLen:4][H264Data:N]
|
||||
if (NextScreenLength > 0 && WebService().IsRunning()) {
|
||||
if (m_bIsWebSession && NextScreenLength > 0 && WebService().IsRunning()) {
|
||||
// Detect H264 keyframe by checking NAL unit type
|
||||
// NAL type 5 = IDR slice (keyframe), NAL type 7 = SPS, NAL type 8 = PPS
|
||||
bool isKeyFrame = false;
|
||||
@@ -1463,8 +1491,8 @@ VOID CScreenSpyDlg::DrawScrollFrame()
|
||||
m_bCursorIndex = m_ContextObject->InDeCompressedBuffer.GetBuffer(2 + sizeof(POINT))[0];
|
||||
if (bOldCursorIndex != m_bCursorIndex) {
|
||||
bChange = TRUE;
|
||||
// 通知 Web 客户端光标变化
|
||||
if (WebService().IsRunning()) {
|
||||
// 通知 Web 客户端光标变化 (只有 Web 会话的对话框才广播)
|
||||
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||
WebService().BroadcastCursor(m_ClientID, m_bCursorIndex);
|
||||
}
|
||||
}
|
||||
@@ -1581,6 +1609,19 @@ bool CScreenSpyDlg::Decode(LPBYTE Buffer, int size)
|
||||
return false;
|
||||
}
|
||||
|
||||
// 跳过默认背景擦除:随帧重绘时若先 FillRect 灰色再 BitBlt 帧,会在两步之间
|
||||
// 出现"瞬时灰背景",启用远程光标(应用层 DrawIconEx)时尤其明显——光标随每帧重绘,
|
||||
// 灰一闪 → 帧覆盖 → 重画光标,循环看上去就是光标频繁闪烁。
|
||||
// adaptive/zoom 模式下 BitBlt/StretchBlt 覆盖整个客户区,本就不需要先擦;
|
||||
// m_bIsFirst(首帧未到达)仍走默认擦除以避免显示残留内容。
|
||||
BOOL CScreenSpyDlg::OnEraseBkgnd(CDC* pDC)
|
||||
{
|
||||
if (m_bIsFirst) {
|
||||
return __super::OnEraseBkgnd(pDC);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CScreenSpyDlg::OnPaint()
|
||||
{
|
||||
if (m_bIsClosed) return;
|
||||
@@ -1594,9 +1635,18 @@ void CScreenSpyDlg::OnPaint()
|
||||
|
||||
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
|
||||
if (m_bAdaptiveSize) {
|
||||
int dstW = m_CRect.Width();
|
||||
int dstH = m_CRect.Height();
|
||||
|
||||
// 放大模式渲染
|
||||
if (m_bZoomedIn && !m_rcZoomSrc.IsRectEmpty()) {
|
||||
// 使用放大区域作为源进行StretchBlt
|
||||
StretchBlt(m_hFullDC, 0, 0, dstW, dstH,
|
||||
m_hFullMemDC,
|
||||
m_rcZoomSrc.left, m_rcZoomSrc.top,
|
||||
m_rcZoomSrc.Width(), m_rcZoomSrc.Height(),
|
||||
SRCCOPY);
|
||||
} else if (m_bAdaptiveSize) {
|
||||
// 尺寸相同时用 BitBlt(更快),否则用 StretchBlt
|
||||
if (srcW == dstW && srcH == dstH) {
|
||||
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, 0, 0, SRCCOPY);
|
||||
@@ -1607,6 +1657,27 @@ void CScreenSpyDlg::OnPaint()
|
||||
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, m_ulHScrollPos, m_ulVScrollPos, SRCCOPY);
|
||||
}
|
||||
|
||||
// 绘制框选矩形(左键放大用红色,右键截图用绿色,二者颜色错开避免误操作)
|
||||
if (m_bSelectingZoom || m_bSelectingShot) {
|
||||
CPoint ptStart = m_bSelectingZoom ? m_ptZoomStart : m_ptShotStart;
|
||||
CPoint ptCur = m_bSelectingZoom ? m_ptZoomCurrent : m_ptShotCurrent;
|
||||
COLORREF clr = m_bSelectingZoom ? RGB(255, 0, 0) : RGB(0, 180, 0);
|
||||
|
||||
CRect rcSelect;
|
||||
rcSelect.left = min(ptStart.x, ptCur.x);
|
||||
rcSelect.top = min(ptStart.y, ptCur.y);
|
||||
rcSelect.right = max(ptStart.x, ptCur.x);
|
||||
rcSelect.bottom = max(ptStart.y, ptCur.y);
|
||||
|
||||
HPEN hPen = CreatePen(PS_DASH, 1, clr);
|
||||
HPEN hOldPen = (HPEN)SelectObject(m_hFullDC, hPen);
|
||||
HBRUSH hOldBrush = (HBRUSH)SelectObject(m_hFullDC, GetStockObject(NULL_BRUSH));
|
||||
Rectangle(m_hFullDC, rcSelect.left, rcSelect.top, rcSelect.right, rcSelect.bottom);
|
||||
SelectObject(m_hFullDC, hOldBrush);
|
||||
SelectObject(m_hFullDC, hOldPen);
|
||||
DeleteObject(hPen);
|
||||
}
|
||||
|
||||
if ((m_bIsCtrl && m_Settings.RemoteCursor) || m_bIsTraceCursor) {
|
||||
CPoint ptLocal;
|
||||
GetCursorPos(&ptLocal);
|
||||
@@ -1789,6 +1860,10 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
||||
switch (nID) {
|
||||
case IDM_CONTROL: {
|
||||
m_bIsCtrl = !m_bIsCtrl;
|
||||
// 进入控制模式时重置放大状态
|
||||
if (m_bIsCtrl && m_bZoomedIn) {
|
||||
ResetZoom();
|
||||
}
|
||||
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
||||
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
||||
@@ -2305,8 +2380,8 @@ BOOL CScreenSpyDlg::PreTranslateMessage(MSG* pMsg)
|
||||
MSG wheelMsg = *pMsg;
|
||||
wheelMsg.lParam = MAKELPARAM(pt.x, pt.y);
|
||||
SendScaledMouseMessage(&wheelMsg, true);
|
||||
return TRUE; // 已处理,阻止继续分发到 OnMouseWheel
|
||||
}
|
||||
break;
|
||||
case WM_KEYDOWN:
|
||||
case WM_KEYUP:
|
||||
case WM_SYSKEYDOWN:
|
||||
@@ -2541,6 +2616,11 @@ void CScreenSpyDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
|
||||
|
||||
void CScreenSpyDlg::EnterFullScreen()
|
||||
{
|
||||
// 进入全屏时重置放大状态
|
||||
if (m_bZoomedIn) {
|
||||
ResetZoom();
|
||||
}
|
||||
|
||||
if (1) {
|
||||
// 1. 获取对话框当前所在的显示器
|
||||
HMONITOR hMonitor = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTONEAREST);
|
||||
@@ -2628,6 +2708,11 @@ void CScreenSpyDlg::EnterFullScreen()
|
||||
// 全屏退出成功则返回true
|
||||
bool CScreenSpyDlg::LeaveFullScreen()
|
||||
{
|
||||
// 退出全屏时重置放大状态
|
||||
if (m_bZoomedIn) {
|
||||
ResetZoom();
|
||||
}
|
||||
|
||||
if (1) {
|
||||
KillTimer(1);
|
||||
if (m_pToolbar) {
|
||||
@@ -2668,26 +2753,375 @@ bool CScreenSpyDlg::LeaveFullScreen()
|
||||
return false;
|
||||
}
|
||||
|
||||
// ========== 局部放大功能辅助函数 ==========
|
||||
|
||||
// 重置放大状态
|
||||
void CScreenSpyDlg::ResetZoom()
|
||||
{
|
||||
m_bZoomedIn = false;
|
||||
m_bSelectingZoom = false;
|
||||
m_bZoomDragging = false;
|
||||
m_rcZoomSrc.SetRectEmpty();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
// 屏幕坐标转原图坐标(考虑放大状态)
|
||||
CPoint CScreenSpyDlg::ScreenToImage(CPoint pt)
|
||||
{
|
||||
if (!m_BitmapInfor_Full) return pt;
|
||||
|
||||
int dstW = m_CRect.Width();
|
||||
int dstH = m_CRect.Height();
|
||||
if (dstW <= 0 || dstH <= 0) return pt; // 防止除零
|
||||
|
||||
if (m_bZoomedIn && !m_rcZoomSrc.IsRectEmpty()) {
|
||||
// 放大状态:从显示区域映射到放大区域
|
||||
double scaleX = (double)m_rcZoomSrc.Width() / dstW;
|
||||
double scaleY = (double)m_rcZoomSrc.Height() / dstH;
|
||||
return CPoint(
|
||||
(int)(m_rcZoomSrc.left + pt.x * scaleX),
|
||||
(int)(m_rcZoomSrc.top + pt.y * scaleY)
|
||||
);
|
||||
} else if (m_bAdaptiveSize) {
|
||||
// 自适应模式:按比例缩放
|
||||
return CPoint((int)(pt.x * m_wZoom), (int)(pt.y * m_hZoom));
|
||||
} else {
|
||||
// 滚动模式:加上滚动偏移
|
||||
return CPoint(pt.x + m_ulHScrollPos, pt.y + m_ulVScrollPos);
|
||||
}
|
||||
}
|
||||
|
||||
// 原图坐标转屏幕坐标(考虑放大状态)
|
||||
CPoint CScreenSpyDlg::ImageToScreen(CPoint pt)
|
||||
{
|
||||
if (!m_BitmapInfor_Full) return pt;
|
||||
|
||||
int zoomW = m_rcZoomSrc.Width();
|
||||
int zoomH = m_rcZoomSrc.Height();
|
||||
|
||||
if (m_bZoomedIn && zoomW > 0 && zoomH > 0) {
|
||||
// 放大状态:从放大区域映射到显示区域
|
||||
int dstW = m_CRect.Width();
|
||||
int dstH = m_CRect.Height();
|
||||
double scaleX = (double)dstW / zoomW;
|
||||
double scaleY = (double)dstH / zoomH;
|
||||
return CPoint(
|
||||
(int)((pt.x - m_rcZoomSrc.left) * scaleX),
|
||||
(int)((pt.y - m_rcZoomSrc.top) * scaleY)
|
||||
);
|
||||
} else if (m_bAdaptiveSize) {
|
||||
if (m_wZoom > 0 && m_hZoom > 0) {
|
||||
return CPoint((int)(pt.x / m_wZoom), (int)(pt.y / m_hZoom));
|
||||
}
|
||||
return pt;
|
||||
} else {
|
||||
return CPoint(pt.x - m_ulHScrollPos, pt.y - m_ulVScrollPos);
|
||||
}
|
||||
}
|
||||
|
||||
void CScreenSpyDlg::OnLButtonDown(UINT nFlags, CPoint point)
|
||||
{
|
||||
// 非控制模式下的放大功能
|
||||
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full) {
|
||||
if (m_bZoomedIn) {
|
||||
// 放大状态:开始拖拽平移
|
||||
m_bZoomDragging = true;
|
||||
m_ptZoomDragStart = point; // 保存起点用于点击检测
|
||||
m_ptZoomDragLast = point; // 用于增量拖拽计算
|
||||
SetCapture();
|
||||
return;
|
||||
} else {
|
||||
// 正常状态:开始框选放大区域
|
||||
m_bSelectingZoom = true;
|
||||
m_ptZoomStart = point;
|
||||
m_ptZoomCurrent = point;
|
||||
SetCapture();
|
||||
return;
|
||||
}
|
||||
}
|
||||
__super::OnLButtonDown(nFlags, point);
|
||||
}
|
||||
|
||||
|
||||
void CScreenSpyDlg::OnLButtonUp(UINT nFlags, CPoint point)
|
||||
{
|
||||
// 处理放大功能的鼠标释放
|
||||
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full) {
|
||||
if (m_bSelectingZoom) {
|
||||
// 完成框选
|
||||
ReleaseCapture();
|
||||
m_bSelectingZoom = false;
|
||||
|
||||
// 计算选择区域(确保left<right, top<bottom)
|
||||
CRect rcSelect;
|
||||
rcSelect.left = min(m_ptZoomStart.x, point.x);
|
||||
rcSelect.top = min(m_ptZoomStart.y, point.y);
|
||||
rcSelect.right = max(m_ptZoomStart.x, point.x);
|
||||
rcSelect.bottom = max(m_ptZoomStart.y, point.y);
|
||||
|
||||
// 框选区域太小时视为点击,如果已放大则还原
|
||||
if (rcSelect.Width() < 20 || rcSelect.Height() < 20) {
|
||||
if (m_bZoomedIn) {
|
||||
ResetZoom();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 将屏幕坐标转换为原图坐标
|
||||
if (!ScreenRectToImageRect(rcSelect, m_rcZoomSrc)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 进入放大状态
|
||||
m_bZoomedIn = true;
|
||||
Invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_bZoomDragging) {
|
||||
// 完成拖拽
|
||||
ReleaseCapture();
|
||||
m_bZoomDragging = false;
|
||||
|
||||
// 检查是否为点击(几乎没有移动)
|
||||
int dx = abs(point.x - m_ptZoomDragStart.x);
|
||||
int dy = abs(point.y - m_ptZoomDragStart.y);
|
||||
if (dx < 5 && dy < 5) {
|
||||
// 点击还原
|
||||
ResetZoom();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
__super::OnLButtonUp(nFlags, point);
|
||||
}
|
||||
|
||||
|
||||
void CScreenSpyDlg::OnRButtonDown(UINT nFlags, CPoint point)
|
||||
{
|
||||
// 非控制模式下:右键框选 → 截图保存。控制模式下右键由 PreTranslateMessage 转发给客户端。
|
||||
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full) {
|
||||
// 与左键互斥:左键正在框选/拖拽时不接管右键,避免冲突
|
||||
if (m_bSelectingZoom || m_bZoomDragging) {
|
||||
return;
|
||||
}
|
||||
m_bSelectingShot = true;
|
||||
m_ptShotStart = point;
|
||||
m_ptShotCurrent = point;
|
||||
SetCapture();
|
||||
return;
|
||||
}
|
||||
__super::OnRButtonDown(nFlags, point);
|
||||
}
|
||||
|
||||
|
||||
void CScreenSpyDlg::OnRButtonUp(UINT nFlags, CPoint point)
|
||||
{
|
||||
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full && m_bSelectingShot) {
|
||||
ReleaseCapture();
|
||||
m_bSelectingShot = false;
|
||||
|
||||
CRect rcSelect;
|
||||
rcSelect.left = min(m_ptShotStart.x, point.x);
|
||||
rcSelect.top = min(m_ptShotStart.y, point.y);
|
||||
rcSelect.right = max(m_ptShotStart.x, point.x);
|
||||
rcSelect.bottom = max(m_ptShotStart.y, point.y);
|
||||
|
||||
// 太小视为误触(与左键放大同阈值)
|
||||
if (rcSelect.Width() < 20 || rcSelect.Height() < 20) {
|
||||
Invalidate(FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
CRect rcImage;
|
||||
if (ScreenRectToImageRect(rcSelect, rcImage) &&
|
||||
rcImage.Width() > 0 && rcImage.Height() > 0)
|
||||
{
|
||||
SaveRegionScreenshot(rcImage);
|
||||
}
|
||||
Invalidate(FALSE); // 清掉绿色选框
|
||||
return;
|
||||
}
|
||||
__super::OnRButtonUp(nFlags, point);
|
||||
}
|
||||
|
||||
|
||||
// 屏幕(窗口)选框 → 原图坐标,考虑放大状态、自适应、滚动
|
||||
bool CScreenSpyDlg::ScreenRectToImageRect(const CRect& rcScreen, CRect& rcImage)
|
||||
{
|
||||
if (!m_BitmapInfor_Full) return false;
|
||||
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
|
||||
if (srcW <= 0 || srcH <= 0) return false;
|
||||
|
||||
if (m_bZoomedIn && !m_rcZoomSrc.IsRectEmpty()) {
|
||||
// 放大状态:屏幕坐标 → 当前可视的子区域内的原图坐标
|
||||
int dstW = m_CRect.Width();
|
||||
int dstH = m_CRect.Height();
|
||||
if (dstW <= 0 || dstH <= 0) return false;
|
||||
double scaleX = (double)m_rcZoomSrc.Width() / dstW;
|
||||
double scaleY = (double)m_rcZoomSrc.Height() / dstH;
|
||||
rcImage.left = (int)(m_rcZoomSrc.left + rcScreen.left * scaleX);
|
||||
rcImage.top = (int)(m_rcZoomSrc.top + rcScreen.top * scaleY);
|
||||
rcImage.right = (int)(m_rcZoomSrc.left + rcScreen.right * scaleX);
|
||||
rcImage.bottom = (int)(m_rcZoomSrc.top + rcScreen.bottom * scaleY);
|
||||
} else if (m_bAdaptiveSize) {
|
||||
rcImage.left = (int)(rcScreen.left * m_wZoom);
|
||||
rcImage.top = (int)(rcScreen.top * m_hZoom);
|
||||
rcImage.right = (int)(rcScreen.right * m_wZoom);
|
||||
rcImage.bottom = (int)(rcScreen.bottom * m_hZoom);
|
||||
} else {
|
||||
rcImage.left = rcScreen.left + m_ulHScrollPos;
|
||||
rcImage.top = rcScreen.top + m_ulVScrollPos;
|
||||
rcImage.right = rcScreen.right + m_ulHScrollPos;
|
||||
rcImage.bottom = rcScreen.bottom + m_ulVScrollPos;
|
||||
}
|
||||
|
||||
// 限制在原图范围内
|
||||
rcImage.left = max(0L, min(rcImage.left, (LONG)srcW));
|
||||
rcImage.top = max(0L, min(rcImage.top, (LONG)srcH));
|
||||
rcImage.right = max(0L, min(rcImage.right, (LONG)srcW));
|
||||
rcImage.bottom = max(0L, min(rcImage.bottom, (LONG)srcH));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// 把原图中 [rcImage] 区域裁出来,写成独立 BMP(24bpp 或 32bpp 由源图决定)
|
||||
void CScreenSpyDlg::SaveRegionScreenshot(const CRect& rcImage)
|
||||
{
|
||||
if (!m_BitmapInfor_Full || !m_BitmapData_Full) return;
|
||||
if (rcImage.Width() <= 0 || rcImage.Height() <= 0) return;
|
||||
|
||||
auto path = GetScreenShotPath(this, m_IPAddress, _TR("位图文件(*.bmp)|*.bmp|"), "bmp");
|
||||
if (path.empty()) return;
|
||||
|
||||
// 源 DIB 是 BGR 24bpp 或 BGRA 32bpp,bottom-up(biHeight > 0)
|
||||
const BITMAPINFOHEADER& srcHdr = m_BitmapInfor_Full->bmiHeader;
|
||||
int bpp = srcHdr.biBitCount;
|
||||
if (bpp != 24 && bpp != 32) return; // 仅支持当前实际使用的两种位深
|
||||
int srcW = srcHdr.biWidth;
|
||||
int srcH = srcHdr.biHeight;
|
||||
int srcStride = ((srcW * bpp + 31) / 32) * 4;
|
||||
|
||||
int dstW = rcImage.Width();
|
||||
int dstH = rcImage.Height();
|
||||
int dstStride = ((dstW * bpp + 31) / 32) * 4;
|
||||
int dstSize = dstStride * dstH;
|
||||
|
||||
std::vector<BYTE> dstPixels(dstSize, 0);
|
||||
const BYTE* srcBase = (const BYTE*)m_BitmapData_Full;
|
||||
|
||||
// bottom-up:原图第 y 行(从顶起算)位于 srcBase + (srcH - 1 - y) * srcStride
|
||||
int byteX = rcImage.left * (bpp / 8);
|
||||
int copyBytes = dstW * (bpp / 8);
|
||||
for (int y = 0; y < dstH; ++y) {
|
||||
int srcRowFromTop = rcImage.top + y;
|
||||
int srcRowOffset = (srcH - 1 - srcRowFromTop) * srcStride + byteX;
|
||||
int dstRowOffset = (dstH - 1 - y) * dstStride;
|
||||
memcpy(&dstPixels[dstRowOffset], &srcBase[srcRowOffset], copyBytes);
|
||||
}
|
||||
|
||||
// 拼装 BITMAPINFO(裁剪后只需要 BITMAPINFOHEADER;24/32bpp 不需要调色板)
|
||||
BITMAPINFO dstBmi = {};
|
||||
dstBmi.bmiHeader = srcHdr;
|
||||
dstBmi.bmiHeader.biWidth = dstW;
|
||||
dstBmi.bmiHeader.biHeight = dstH;
|
||||
dstBmi.bmiHeader.biSizeImage = dstSize;
|
||||
dstBmi.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
if (WriteBitmap(&dstBmi, dstPixels.data(), path)) {
|
||||
m_strSaveNotice = path;
|
||||
m_nSaveNoticeTime = GetTickCount64();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BOOL CScreenSpyDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
|
||||
{
|
||||
return __super::OnMouseWheel(nFlags, zDelta, pt);
|
||||
// Convert screen coordinates to client coordinates
|
||||
ScreenToClient(&pt);
|
||||
|
||||
// Build MSG structure for SendScaledMouseMessage
|
||||
MSG msg = {};
|
||||
msg.hwnd = m_hWnd;
|
||||
msg.message = WM_MOUSEWHEEL;
|
||||
msg.wParam = MAKEWPARAM(nFlags, zDelta);
|
||||
msg.lParam = MAKELPARAM(pt.x, pt.y);
|
||||
msg.time = GetTickCount();
|
||||
msg.pt = { pt.x, pt.y };
|
||||
|
||||
SendScaledMouseMessage(&msg, true);
|
||||
return TRUE; // Message handled, don't pass to parent
|
||||
}
|
||||
|
||||
|
||||
void CScreenSpyDlg::OnMouseMove(UINT nFlags, CPoint point)
|
||||
{
|
||||
// 处理放大功能的鼠标移动
|
||||
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full) {
|
||||
if (m_bSelectingZoom) {
|
||||
// 框选中:更新当前点并重绘选择框
|
||||
m_ptZoomCurrent = point;
|
||||
Invalidate(FALSE); // FALSE表示不擦除背景,减少闪烁
|
||||
return;
|
||||
}
|
||||
if (m_bSelectingShot) {
|
||||
m_ptShotCurrent = point;
|
||||
Invalidate(FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_bZoomDragging) {
|
||||
// 拖拽平移:计算偏移量并移动放大区域
|
||||
int dx = point.x - m_ptZoomDragLast.x;
|
||||
int dy = point.y - m_ptZoomDragLast.y;
|
||||
m_ptZoomDragLast = point; // 更新上一点(保持m_ptZoomDragStart不变用于点击检测)
|
||||
|
||||
// 计算缩放比例(添加除零保护)
|
||||
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
|
||||
int dstW = m_CRect.Width();
|
||||
int dstH = m_CRect.Height();
|
||||
int zoomW = m_rcZoomSrc.Width();
|
||||
int zoomH = m_rcZoomSrc.Height();
|
||||
|
||||
if (dstW <= 0 || dstH <= 0 || zoomW <= 0 || zoomH <= 0) {
|
||||
return; // 防止除零
|
||||
}
|
||||
|
||||
double scaleX = (double)zoomW / dstW;
|
||||
double scaleY = (double)zoomH / dstH;
|
||||
|
||||
// 将屏幕偏移转换为原图偏移(方向相反)
|
||||
int imgDx = (int)(-dx * scaleX);
|
||||
int imgDy = (int)(-dy * scaleY);
|
||||
|
||||
// 移动放大区域
|
||||
m_rcZoomSrc.OffsetRect(imgDx, imgDy);
|
||||
|
||||
// 限制在原图范围内
|
||||
if (m_rcZoomSrc.left < 0) {
|
||||
m_rcZoomSrc.right -= m_rcZoomSrc.left;
|
||||
m_rcZoomSrc.left = 0;
|
||||
}
|
||||
if (m_rcZoomSrc.top < 0) {
|
||||
m_rcZoomSrc.bottom -= m_rcZoomSrc.top;
|
||||
m_rcZoomSrc.top = 0;
|
||||
}
|
||||
if (m_rcZoomSrc.right > srcW) {
|
||||
m_rcZoomSrc.left -= (m_rcZoomSrc.right - srcW);
|
||||
m_rcZoomSrc.right = srcW;
|
||||
}
|
||||
if (m_rcZoomSrc.bottom > srcH) {
|
||||
m_rcZoomSrc.top -= (m_rcZoomSrc.bottom - srcH);
|
||||
m_rcZoomSrc.bottom = srcH;
|
||||
}
|
||||
|
||||
Invalidate(FALSE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_Settings.RemoteCursor) {
|
||||
if (m_pToolbar != NULL && ::IsWindow(m_pToolbar->m_hWnd) && m_pToolbar->IsWindowVisible()) {
|
||||
CRect rcToolbar;
|
||||
@@ -2770,11 +3204,32 @@ void CScreenSpyDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
|
||||
void CScreenSpyDlg::UpdateCtrlStatus(BOOL ctrl)
|
||||
{
|
||||
m_bIsCtrl = ctrl;
|
||||
// 进入控制模式时重置放大状态 + 中止任何正在进行的右键截图框选
|
||||
if (m_bIsCtrl) {
|
||||
if (m_bZoomedIn) ResetZoom();
|
||||
if (m_bSelectingShot) {
|
||||
m_bSelectingShot = false;
|
||||
if (GetCapture() == this) ReleaseCapture();
|
||||
Invalidate(FALSE);
|
||||
}
|
||||
}
|
||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
||||
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
||||
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
|
||||
}
|
||||
|
||||
void CScreenSpyDlg::OnCaptureChanged(CWnd* pWnd)
|
||||
{
|
||||
// 捕获丢失时重置框选/拖拽状态
|
||||
if (m_bSelectingZoom || m_bZoomDragging || m_bSelectingShot) {
|
||||
m_bSelectingZoom = false;
|
||||
m_bZoomDragging = false;
|
||||
m_bSelectingShot = false;
|
||||
Invalidate();
|
||||
}
|
||||
__super::OnCaptureChanged(pWnd);
|
||||
}
|
||||
|
||||
void CScreenSpyDlg::OnDropFiles(HDROP hDropInfo)
|
||||
{
|
||||
if (m_bIsCtrl && m_bConnected) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include <imm.h>
|
||||
#include <map>
|
||||
#include <atomic>
|
||||
#include "IOCPServer.h"
|
||||
#include "..\..\client\CursorInfo.h"
|
||||
#include "VideoDlg.h"
|
||||
@@ -153,6 +154,10 @@ public:
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Check if this dialog was created by Web request (shared by Web users)
|
||||
bool IsWebSession() const { return m_bIsWebSession.load(); }
|
||||
void SetWebSession(bool isWeb) { m_bIsWebSession.store(isWeb); }
|
||||
|
||||
VOID SendNext(void);
|
||||
VOID OnReceiveComplete();
|
||||
HDC m_hFullDC;
|
||||
@@ -186,13 +191,15 @@ public:
|
||||
int m_FrameID;
|
||||
HIMC m_hOldIMC = NULL; // 保存原始 IME 上下文,控制模式切换时使用
|
||||
bool m_bHide = false;
|
||||
std::atomic<bool> m_bIsWebSession{false}; // True if this dialog was created by Web request (atomic for thread safety)
|
||||
std::string m_strSaveNotice; // 截图保存路径提示
|
||||
ULONGLONG m_nSaveNoticeTime = 0; // 截图提示开始时间
|
||||
BOOL m_bUsingFRP = FALSE;
|
||||
|
||||
// 文件接收进度对话框(用于 Linux Ctrl+C -> 服务端 Ctrl+V)
|
||||
// 按 transferID 管理多个并发传输
|
||||
std::map<uint64_t, class CDlgFileSend*> m_FileRecvDlgs;
|
||||
// 存储 {HWND, 指针} 对,HWND 用于安全检查(指针可能变成野指针)
|
||||
std::map<uint64_t, std::pair<HWND, class CDlgFileSend*>> m_FileRecvDlgs;
|
||||
|
||||
void SaveSnapshot(void);
|
||||
// 对话框数据
|
||||
@@ -216,6 +223,27 @@ public:
|
||||
double m_wZoom=1, m_hZoom=1;
|
||||
bool m_bMouseTracking = false;
|
||||
|
||||
// ========== 局部放大功能 ==========
|
||||
bool m_bZoomedIn = false; // 是否处于放大状态
|
||||
CRect m_rcZoomSrc; // 放大区域(原图坐标)
|
||||
bool m_bSelectingZoom = false; // 是否正在框选
|
||||
CPoint m_ptZoomStart; // 框选起点(屏幕坐标)
|
||||
CPoint m_ptZoomCurrent; // 框选当前点(屏幕坐标)
|
||||
bool m_bZoomDragging = false; // 是否正在拖拽平移
|
||||
CPoint m_ptZoomDragStart; // 拖拽起点(用于点击检测)
|
||||
CPoint m_ptZoomDragLast; // 拖拽上一点(用于增量计算)
|
||||
|
||||
// ========== 区域截图(右键框选) ==========
|
||||
bool m_bSelectingShot = false; // 是否正在右键框选截图
|
||||
CPoint m_ptShotStart; // 右键框选起点(屏幕坐标)
|
||||
CPoint m_ptShotCurrent; // 右键框选当前点(屏幕坐标)
|
||||
|
||||
void ResetZoom(); // 重置放大状态
|
||||
CPoint ScreenToImage(CPoint pt); // 屏幕坐标转原图坐标
|
||||
CPoint ImageToScreen(CPoint pt); // 原图坐标转屏幕坐标
|
||||
bool ScreenRectToImageRect(const CRect& rcScreen, CRect& rcImage); // 选框坐标→原图坐标
|
||||
void SaveRegionScreenshot(const CRect& rcImage); // 保存裁剪区域为 BMP
|
||||
|
||||
CString m_aviFile;
|
||||
CBmpToAvi m_aviStream;
|
||||
|
||||
@@ -291,10 +319,13 @@ public:
|
||||
afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
|
||||
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
|
||||
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
|
||||
afx_msg void OnRButtonDown(UINT nFlags, CPoint point);
|
||||
afx_msg void OnRButtonUp(UINT nFlags, CPoint point);
|
||||
afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt);
|
||||
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
|
||||
afx_msg void OnMouseLeave();
|
||||
afx_msg void OnKillFocus(CWnd* pNewWnd);
|
||||
afx_msg void OnCaptureChanged(CWnd* pWnd);
|
||||
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||
afx_msg void OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized);
|
||||
afx_msg LRESULT OnDisconnect(WPARAM wParam, LPARAM lParam);
|
||||
@@ -325,6 +356,7 @@ public:
|
||||
virtual BOOL OnInitDialog();
|
||||
afx_msg void OnClose();
|
||||
afx_msg void OnPaint();
|
||||
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
||||
BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
|
||||
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
|
||||
virtual BOOL PreTranslateMessage(MSG* pMsg);
|
||||
|
||||
@@ -378,6 +378,11 @@ public:
|
||||
std::atomic<int> IoRefCount{0}; // I/O 处理引用计数
|
||||
std::atomic<bool> IsRemoved{false}; // 标记是否已被标记为移除
|
||||
|
||||
// 子连接身份校验:客户端发 TOKEN_CONN_AUTH 通过验证后置位。
|
||||
// 主连接(走 TOKEN_LOGIN 流程)不参与此机制。当前阶段宽容(未通过也接受),
|
||||
// 仅作为标记供后续命令处理 / 未来收紧策略使用。
|
||||
std::atomic<bool> m_bAuthenticated{false};
|
||||
|
||||
// 预分配的解压缩缓冲区,避免频繁内存分配
|
||||
PBYTE DecompressBuffer = nullptr;
|
||||
ULONG DecompressBufferSize = 0;
|
||||
@@ -510,7 +515,11 @@ public:
|
||||
// 注意:到达这里时,RemoveStaleContext 应该已经等待 IoRefCount==0
|
||||
IsRemoved.store(false, std::memory_order_release);
|
||||
IoRefCount.store(0, std::memory_order_release);
|
||||
// 复用对象池时清空校验状态
|
||||
m_bAuthenticated.store(false, std::memory_order_release);
|
||||
}
|
||||
void SetAuthenticated(bool v) { m_bAuthenticated.store(v, std::memory_order_release); }
|
||||
bool IsAuthenticated() const { return m_bAuthenticated.load(std::memory_order_acquire); }
|
||||
uint64_t GetAliveTime()const
|
||||
{
|
||||
return time(0) - OnlineTime;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "2015Remote.h"
|
||||
#include "2015RemoteDlg.h" // GetClientEncoding helper
|
||||
#include "SystemDlg.h"
|
||||
#include "afxdialogex.h"
|
||||
|
||||
@@ -85,6 +86,8 @@ BOOL CSystemDlg::OnInitDialog()
|
||||
m_ControlList.InsertColumnL(1, "窗口名称", LVCFMT_LEFT, 420);
|
||||
m_ControlList.InsertColumnL(2, "窗口状态", LVCFMT_LEFT, 200);
|
||||
m_ControlList.InsertColumnL(3, "所属进程ID", LVCFMT_LEFT, 100);
|
||||
// 工程是 MBCS,但下面"窗口名称"列里的标题需要原样显示客户端 UTF-8 内容,
|
||||
// 直接用 LVM_SETITEMTEXTW 写宽字符串(无须依赖控件 Unicode 标志)。
|
||||
ShowWindowsList();
|
||||
}
|
||||
|
||||
@@ -170,6 +173,11 @@ void CSystemDlg::ShowWindowsList(void)
|
||||
char *szTitle = NULL;
|
||||
bool isDel=false;
|
||||
|
||||
// 客户端编码由能力位 CLIENT_CAP_UTF8 决定。
|
||||
// 注意:m_ContextObject 是 WSLIST 子连接,其自身 CAPABILITIES 为空;
|
||||
// helper 内部通过 peer IP 查主连接获取真正的能力位。
|
||||
UINT cp = GetClientEncoding(m_ContextObject);
|
||||
|
||||
DeleteAllItems();
|
||||
CString str;
|
||||
int i ;
|
||||
@@ -181,10 +189,28 @@ void CSystemDlg::ShowWindowsList(void)
|
||||
str.FormatL("%5u", *lpPID);
|
||||
CString pidStr = attrs.dwPid ? std::to_string(attrs.dwPid).c_str() : "N/A";
|
||||
m_ControlList.InsertItem(i, str); // 句柄
|
||||
m_ControlList.SetItemText(i, 1, attrs.szTitle); // 标题
|
||||
m_ControlList.SetItemText(i, 2, attrs.szStatus); // 窗口状态
|
||||
m_ControlList.SetItemText(i, 3, pidStr); // 所属进程ID
|
||||
// ItemData 为窗口句柄
|
||||
|
||||
// 按客户端声明的编码解码到宽字符,用 LVM_SETITEMTEXTW 直接写入,
|
||||
// 绕开 ANSI -> CP_ACP 回转,即使在德语等非中文 ACP 服务端上中文窗口名也能正常显示。
|
||||
std::wstring wTitle;
|
||||
if (attrs.szTitle[0]) {
|
||||
int wlen = MultiByteToWideChar(cp, 0, attrs.szTitle, -1, NULL, 0);
|
||||
if (wlen > 0) {
|
||||
wTitle.resize(wlen - 1);
|
||||
MultiByteToWideChar(cp, 0, attrs.szTitle, -1, &wTitle[0], wlen);
|
||||
}
|
||||
}
|
||||
LVITEMW lvItemW = {};
|
||||
lvItemW.mask = LVIF_TEXT;
|
||||
lvItemW.iItem = i;
|
||||
lvItemW.iSubItem = 1;
|
||||
lvItemW.pszText = wTitle.empty() ? const_cast<LPWSTR>(L"") : &wTitle[0];
|
||||
::SendMessageW(m_ControlList.GetSafeHwnd(), LVM_SETITEMTEXTW,
|
||||
(WPARAM)i, (LPARAM)&lvItemW);
|
||||
|
||||
m_ControlList.SetItemText(i, 2, attrs.szStatus); // 窗口状态 (ASCII)
|
||||
m_ControlList.SetItemText(i, 3, pidStr); // 所属进程ID (ASCII)
|
||||
// ItemData 为窗口句柄;Data[1] 保留原始 UTF-8 字节供排序/右键菜单使用
|
||||
auto data = new ItemData{ *lpPID, {str, attrs.szTitle, attrs.szStatus, pidStr} };
|
||||
m_ControlList.SetItemData(i, (DWORD_PTR)data); //(d)
|
||||
dwOffset += sizeof(DWORD) + lstrlen(szTitle) + 1;
|
||||
|
||||
@@ -997,10 +997,14 @@ inline std::string GetWebPageHTML() {
|
||||
<h4>Create New User</h4>
|
||||
<input type="text" id="new-username" placeholder="Username" autocomplete="off">
|
||||
<input type="password" id="new-password" placeholder="Password" autocomplete="new-password">
|
||||
<select id="new-role">
|
||||
<select id="new-role" onchange="onRoleChange()">
|
||||
<option value="viewer">Viewer (read-only)</option>
|
||||
<option value="admin">Admin (full access)</option>
|
||||
</select>
|
||||
<div class="groups-section" id="groups-section">
|
||||
<label style="font-size:13px;color:#aaa;display:block;margin:8px 0 4px;">Allowed Groups:</label>
|
||||
<div id="groups-checkboxes" style="max-height:120px;overflow-y:auto;background:rgba(0,0,0,0.2);border-radius:6px;padding:6px 8px;"></div>
|
||||
</div>
|
||||
<button onclick="createUser()">Create User</button>
|
||||
</div>
|
||||
<div class="user-list">
|
||||
@@ -1286,6 +1290,11 @@ inline std::string GetWebPageHTML() {
|
||||
renderUsersList(msg.users);
|
||||
}
|
||||
break;
|
||||
case 'groups':
|
||||
if (msg.ok) {
|
||||
renderGroupsCheckboxes(msg.groups);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
)HTML";
|
||||
@@ -1661,7 +1670,35 @@ inline std::string GetWebPageHTML() {
|
||||
function openUsersModal() {
|
||||
document.getElementById('users-modal').classList.add('active');
|
||||
document.getElementById('user-msg').innerHTML = '';
|
||||
document.getElementById('new-role').value = 'viewer'; // Reset to default
|
||||
onRoleChange(); // Update groups section visibility
|
||||
listUsers();
|
||||
getGroups();
|
||||
}
|
||||
|
||||
function getGroups() {
|
||||
if (ws && ws.readyState === WebSocket.OPEN && token) {
|
||||
ws.send(JSON.stringify({ cmd: 'get_groups', token }));
|
||||
}
|
||||
}
|
||||
|
||||
function renderGroupsCheckboxes(groups) {
|
||||
const container = document.getElementById('groups-checkboxes');
|
||||
if (!groups || groups.length === 0) {
|
||||
container.innerHTML = '<span style="color:#666;font-size:12px;">No groups available</span>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = groups.map(g =>
|
||||
'<label style="display:flex;align-items:center;padding:3px 0;cursor:pointer;white-space:nowrap;">' +
|
||||
'<input type="checkbox" value="' + escapeHtml(g) + '" style="margin:0 6px 0 0;flex-shrink:0;width:14px;height:14px;">' +
|
||||
escapeHtml(g) + '</label>'
|
||||
).join('');
|
||||
}
|
||||
|
||||
function onRoleChange() {
|
||||
const role = document.getElementById('new-role').value;
|
||||
const groupsSection = document.getElementById('groups-section');
|
||||
groupsSection.style.display = (role === 'admin') ? 'none' : 'block';
|
||||
}
|
||||
|
||||
function closeUsersModal() {
|
||||
@@ -1685,8 +1722,12 @@ inline std::string GetWebPageHTML() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect selected groups
|
||||
const checkboxes = document.querySelectorAll('#groups-checkboxes input[type="checkbox"]:checked');
|
||||
const allowed_groups = Array.from(checkboxes).map(cb => cb.value);
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN && token) {
|
||||
ws.send(JSON.stringify({ cmd: 'create_user', token, username, password, role }));
|
||||
ws.send(JSON.stringify({ cmd: 'create_user', token, username, password, role, allowed_groups }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1712,10 +1753,14 @@ inline std::string GetWebPageHTML() {
|
||||
container.innerHTML = users.map(u => {
|
||||
const isAdmin = u.role === 'admin';
|
||||
const canDelete = u.username !== 'admin'; // Cannot delete built-in admin
|
||||
const groups = u.allowed_groups || [];
|
||||
const groupsText = u.username === 'admin' ? '(all)' :
|
||||
(groups.length > 0 ? groups.join(', ') : '(none)');
|
||||
return '<div class="user-item">' +
|
||||
'<div class="user-info">' +
|
||||
'<div class="username">' + escapeHtml(u.username) + '</div>' +
|
||||
'<div class="role ' + (isAdmin ? 'admin' : '') + '">' + u.role + '</div>' +
|
||||
'<div class="groups" style="font-size:11px;color:#888;margin-top:2px;">Groups: ' + escapeHtml(groupsText) + '</div>' +
|
||||
'</div>' +
|
||||
(canDelete ? '<button class="delete-btn" onclick="deleteUser(\'' + escapeHtml(u.username) + '\')">Delete</button>' : '') +
|
||||
'</div>';
|
||||
@@ -1801,13 +1846,15 @@ inline std::string GetWebPageHTML() {
|
||||
|
||||
// Remote cursor mapping (Windows cursor index -> CSS cursor)
|
||||
// Index matches CursorInfo.h: IDC_APPSTARTING(0) to IDC_WAIT(15), 254=custom, 255=unsupported
|
||||
// Custom I-beam cursor with white fill and black stroke for visibility on any background
|
||||
const ibeamCursor = "url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"24\" viewBox=\"0 0 16 24\"><path fill=\"none\" stroke=\"white\" stroke-width=\"3\" d=\"M4 3h8M8 3v18M4 21h8\"/><path fill=\"none\" stroke=\"black\" stroke-width=\"1\" d=\"M4 3h8M8 3v18M4 21h8\"/></svg>') 8 12, text";
|
||||
const cursorMap = [
|
||||
'progress', // 0: IDC_APPSTARTING
|
||||
'default', // 1: IDC_ARROW
|
||||
'crosshair', // 2: IDC_CROSS
|
||||
'pointer', // 3: IDC_HAND
|
||||
'help', // 4: IDC_HELP
|
||||
'text', // 5: IDC_IBEAM
|
||||
ibeamCursor, // 5: IDC_IBEAM - custom cursor with outline
|
||||
'default', // 6: IDC_ICON (no direct CSS equivalent)
|
||||
'not-allowed', // 7: IDC_NO
|
||||
'default', // 8: IDC_SIZE (deprecated, use default)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <shlobj.h>
|
||||
#include <set>
|
||||
|
||||
// Algorithm constants (same as ScreenSpyDlg.cpp)
|
||||
#define ALGORITHM_H264 2
|
||||
@@ -363,6 +364,8 @@ void CWebService::ServerThread(int port) {
|
||||
HandleDeleteUser(ws_ptr, msg);
|
||||
} else if (cmd == "list_users") {
|
||||
HandleListUsers(ws_ptr, token);
|
||||
} else if (cmd == "get_groups") {
|
||||
HandleGetGroups(ws_ptr, token);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -566,7 +569,7 @@ void CWebService::HandleGetDevices(void* ws_ptr, const std::string& token) {
|
||||
return;
|
||||
}
|
||||
|
||||
SendText(ws_ptr, BuildDeviceListJson());
|
||||
SendText(ws_ptr, BuildDeviceListJson(username));
|
||||
}
|
||||
|
||||
void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id) {
|
||||
@@ -588,6 +591,32 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
|
||||
return;
|
||||
}
|
||||
|
||||
// Check group permission (admin can access all devices)
|
||||
if (username != "admin") {
|
||||
std::string deviceGroup = ctx->GetGroupName();
|
||||
if (deviceGroup.empty()) deviceGroup = "default";
|
||||
|
||||
bool hasAccess = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_UsersMutex);
|
||||
for (const auto& u : m_Users) {
|
||||
if (u.username == username) {
|
||||
for (const auto& g : u.allowed_groups) {
|
||||
if (g == deviceGroup) {
|
||||
hasAccess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasAccess) {
|
||||
SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Permission denied"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check max clients per device
|
||||
int current_count = GetWebClientCount(device_id);
|
||||
if (current_count >= m_nMaxClientsPerDevice) {
|
||||
@@ -954,12 +983,23 @@ void CWebService::HandleCreateUser(void* ws_ptr, const std::string& msg) {
|
||||
std::string newPassword = root.get("password", "").asString();
|
||||
std::string newRole = root.get("role", "viewer").asString();
|
||||
|
||||
// Parse allowed_groups array
|
||||
std::vector<std::string> allowedGroups;
|
||||
const Json::Value& groups = root["allowed_groups"];
|
||||
if (groups.isArray()) {
|
||||
for (const auto& g : groups) {
|
||||
if (g.isString() && !g.asString().empty()) {
|
||||
allowedGroups.push_back(g.asString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newUsername.empty() || newPassword.empty()) {
|
||||
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (CreateUser(newUsername, newPassword, newRole)) {
|
||||
if (CreateUser(newUsername, newPassword, newRole, allowedGroups)) {
|
||||
SendText(ws_ptr, BuildJsonResponse("create_user_result", true));
|
||||
} else {
|
||||
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)"));
|
||||
@@ -1009,19 +1049,28 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto users = ListUsers();
|
||||
|
||||
Json::Value res;
|
||||
res["cmd"] = "list_users_result";
|
||||
res["ok"] = true;
|
||||
|
||||
Json::Value usersArray(Json::arrayValue);
|
||||
for (const auto& u : users) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_UsersMutex);
|
||||
for (const auto& u : m_Users) {
|
||||
Json::Value user;
|
||||
user["username"] = u.first;
|
||||
user["role"] = u.second;
|
||||
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;
|
||||
|
||||
Json::StreamWriterBuilder builder;
|
||||
@@ -1030,6 +1079,48 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
|
||||
SendText(ws_ptr, json);
|
||||
}
|
||||
|
||||
void CWebService::HandleGetGroups(void* ws_ptr, const std::string& token) {
|
||||
std::string username, role;
|
||||
if (!ValidateToken(token, username, role)) {
|
||||
SendText(ws_ptr, BuildJsonResponse("groups", false, "Invalid token"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Only admin can get groups list (for user management)
|
||||
if (role != "admin") {
|
||||
SendText(ws_ptr, BuildJsonResponse("groups", false, "Permission denied"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect all unique groups from online devices
|
||||
std::set<std::string> groups;
|
||||
groups.insert("default"); // Always include default group
|
||||
|
||||
if (m_pParentDlg) {
|
||||
EnterCriticalSection(&m_pParentDlg->m_cs);
|
||||
for (context* ctx : m_pParentDlg->m_HostList) {
|
||||
if (!ctx || !ctx->IsLogin()) continue;
|
||||
std::string g = ctx->GetGroupName();
|
||||
groups.insert(g.empty() ? "default" : g);
|
||||
}
|
||||
LeaveCriticalSection(&m_pParentDlg->m_cs);
|
||||
}
|
||||
|
||||
// Build response
|
||||
Json::Value res;
|
||||
res["cmd"] = "groups";
|
||||
res["ok"] = true;
|
||||
res["groups"] = Json::Value(Json::arrayValue);
|
||||
for (const auto& g : groups) {
|
||||
res["groups"].append(g);
|
||||
}
|
||||
|
||||
Json::StreamWriterBuilder builder;
|
||||
builder["indentation"] = "";
|
||||
std::string json = Json::writeString(builder, res);
|
||||
SendText(ws_ptr, json);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Token Management (delegated to WebServiceAuth module)
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
@@ -1170,6 +1261,14 @@ void CWebService::LoadUsers() {
|
||||
user.salt = u.get("salt", "").asString();
|
||||
user.role = u.get("role", "viewer").asString();
|
||||
|
||||
// Load allowed_groups
|
||||
const Json::Value& groups = u["allowed_groups"];
|
||||
if (groups.isArray()) {
|
||||
for (const auto& g : groups) {
|
||||
user.allowed_groups.push_back(g.asString());
|
||||
}
|
||||
}
|
||||
|
||||
if (!user.password_hash.empty()) {
|
||||
m_Users.push_back(user);
|
||||
loaded++;
|
||||
@@ -1197,6 +1296,14 @@ void CWebService::SaveUsers() {
|
||||
user["password_hash"] = u.password_hash;
|
||||
user["salt"] = u.salt;
|
||||
user["role"] = u.role;
|
||||
|
||||
// Save allowed_groups
|
||||
Json::Value groups(Json::arrayValue);
|
||||
for (const auto& g : u.allowed_groups) {
|
||||
groups.append(g);
|
||||
}
|
||||
user["allowed_groups"] = groups;
|
||||
|
||||
users.append(user);
|
||||
}
|
||||
|
||||
@@ -1217,7 +1324,8 @@ void CWebService::SaveUsers() {
|
||||
Mprintf("[WebService] Saved %d users to users.json\n", (int)users.size());
|
||||
}
|
||||
|
||||
bool CWebService::CreateUser(const std::string& username, const std::string& password, const std::string& role) {
|
||||
bool CWebService::CreateUser(const std::string& username, const std::string& password, const std::string& role,
|
||||
const std::vector<std::string>& allowed_groups) {
|
||||
if (username.empty() || password.empty()) return false;
|
||||
if (username == "admin") return false; // Cannot create user named "admin"
|
||||
if (role != "admin" && role != "viewer") return false;
|
||||
@@ -1236,9 +1344,11 @@ bool CWebService::CreateUser(const std::string& username, const std::string& pas
|
||||
user.salt = GenerateSalt();
|
||||
user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
|
||||
user.role = role;
|
||||
user.allowed_groups = allowed_groups;
|
||||
|
||||
m_Users.push_back(user);
|
||||
Mprintf("[WebService] Created user: %s (role: %s)\n", username.c_str(), role.c_str());
|
||||
Mprintf("[WebService] Created user: %s (role: %s, groups: %d)\n",
|
||||
username.c_str(), role.c_str(), (int)allowed_groups.size());
|
||||
}
|
||||
|
||||
// Save to file (outside lock scope since SaveUsers acquires its own lock)
|
||||
@@ -1295,17 +1405,47 @@ std::string CWebService::BuildJsonResponse(const std::string& cmd, bool ok, cons
|
||||
return Json::writeString(builder, res);
|
||||
}
|
||||
|
||||
std::string CWebService::BuildDeviceListJson() {
|
||||
std::string CWebService::BuildDeviceListJson(const std::string& username) {
|
||||
Json::Value res;
|
||||
res["cmd"] = "device_list";
|
||||
res["devices"] = Json::Value(Json::arrayValue);
|
||||
|
||||
// Get user's allowed groups for filtering (skip for admin or empty username)
|
||||
std::vector<std::string> allowedGroups;
|
||||
bool filterByGroup = false;
|
||||
if (!username.empty() && username != "admin") {
|
||||
std::lock_guard<std::mutex> lock(m_UsersMutex);
|
||||
for (const auto& u : m_Users) {
|
||||
if (u.username == username) {
|
||||
allowedGroups = u.allowed_groups;
|
||||
filterByGroup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_pParentDlg) {
|
||||
// Access device list with lock
|
||||
EnterCriticalSection(&m_pParentDlg->m_cs);
|
||||
for (context* ctx : m_pParentDlg->m_HostList) {
|
||||
if (!ctx || !ctx->IsLogin()) continue;
|
||||
|
||||
// Get device group (empty = "default")
|
||||
std::string deviceGroup = ctx->GetGroupName();
|
||||
if (deviceGroup.empty()) deviceGroup = "default";
|
||||
|
||||
// Filter by allowed groups if user is not admin
|
||||
if (filterByGroup) {
|
||||
bool allowed = false;
|
||||
for (const auto& g : allowedGroups) {
|
||||
if (g == deviceGroup) {
|
||||
allowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!allowed) continue; // Skip device not in allowed groups
|
||||
}
|
||||
|
||||
Json::Value device;
|
||||
// Use string for ID to avoid JavaScript number precision loss
|
||||
device["id"] = std::to_string(ctx->GetClientID());
|
||||
@@ -1328,10 +1468,29 @@ std::string CWebService::BuildDeviceListJson() {
|
||||
CString version = ctx->GetClientData(ONLINELIST_VERSION);
|
||||
device["version"] = AnsiToUtf8(version);
|
||||
|
||||
// 活动窗口编码由客户端能力位决定:新客户端是 UTF-8,老客户端是 CP_ACP(默认 936)。
|
||||
// 不能像其它字段那样无脑 AnsiToUtf8——会把新客户端的 UTF-8 字节再当 GBK 双重编码。
|
||||
CString activeWindow = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
||||
device["activeWindow"] = AnsiToUtf8(activeWindow);
|
||||
std::string activeWindowU8;
|
||||
if (!activeWindow.IsEmpty()) {
|
||||
UINT cp = GetClientEncoding(ctx);
|
||||
int wlen = MultiByteToWideChar(cp, 0, activeWindow, -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
std::wstring w(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, activeWindow, -1, &w[0], wlen);
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (u8len > 1) {
|
||||
activeWindowU8.resize(u8len - 1);
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &activeWindowU8[0], u8len, NULL, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
device["activeWindow"] = activeWindowU8;
|
||||
device["online"] = true;
|
||||
|
||||
// Add device group to response
|
||||
device["group"] = deviceGroup;
|
||||
|
||||
// Get screen info from client's reported resolution
|
||||
// Format: "n:MxN" where n=monitor count, M=width, N=height
|
||||
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
|
||||
@@ -1509,9 +1668,13 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
|
||||
context* ctx = m_pParentDlg->FindHost(device_id);
|
||||
if (!ctx) return false;
|
||||
|
||||
// Close any existing remote desktop for this device first
|
||||
// This prevents duplicate dialogs when user reconnects quickly
|
||||
m_pParentDlg->CloseRemoteDesktopByClientID(device_id);
|
||||
// Check if there's already a Web session for this device
|
||||
// Only reuse if Web has already triggered AND a Web dialog exists
|
||||
// This ensures MFC and Web have independent dialogs
|
||||
if (IsWebTriggered(device_id) && HasActiveSession(device_id)) {
|
||||
Mprintf("[WebService] Reusing existing Web session for device %llu\n", device_id);
|
||||
return true; // Web session exists, new web user joins watching
|
||||
}
|
||||
|
||||
// Mark as web-triggered (dialog should be hidden)
|
||||
{
|
||||
@@ -1520,7 +1683,8 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
|
||||
}
|
||||
|
||||
// Send COMMAND_SCREEN_SPY with H264 algorithm
|
||||
// Format: [COMMAND_SCREEN_SPY:1][DXGI:1][Algorithm:1][MultiScreen:1]
|
||||
// If client is already capturing (MFC opened first), it will re-send TOKEN_BITMAPINFO
|
||||
// This creates a new hidden Web dialog while MFC dialog remains visible
|
||||
BYTE bToken[32] = { 0 };
|
||||
bToken[0] = COMMAND_SCREEN_SPY;
|
||||
bToken[1] = 0; // DXGI mode: 0=GDI
|
||||
@@ -1544,10 +1708,11 @@ void CWebService::StopRemoteDesktop(uint64_t device_id) {
|
||||
}
|
||||
}
|
||||
|
||||
// If no more web clients watching, close the remote desktop
|
||||
// If no more web clients watching, close only the Web session dialog
|
||||
// MFC dialogs remain open
|
||||
if (watchingCount == 0) {
|
||||
ClearWebTriggered(device_id);
|
||||
m_pParentDlg->CloseRemoteDesktopByClientID(device_id);
|
||||
m_pParentDlg->CloseWebRemoteDesktopByClientID(device_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1563,11 +1728,14 @@ void CWebService::RegisterScreenContext(uint64_t device_id, CONTEXT_OBJECT* ctx)
|
||||
}
|
||||
|
||||
void CWebService::UnregisterScreenContext(uint64_t device_id) {
|
||||
if (!m_bRunning) return;
|
||||
// Always clean up, even if WebService is stopping
|
||||
// This prevents stale pointers in m_ScreenContexts
|
||||
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
|
||||
m_ScreenContexts.erase(device_id);
|
||||
if (m_bRunning) {
|
||||
Mprintf("[WebService] Unregistered screen context for device %llu\n", device_id);
|
||||
}
|
||||
}
|
||||
|
||||
CONTEXT_OBJECT* CWebService::GetScreenContext(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
|
||||
@@ -1666,6 +1834,26 @@ void CWebService::ClearWebTriggered(uint64_t device_id) {
|
||||
m_WebTriggeredDevices.erase(device_id);
|
||||
}
|
||||
|
||||
void CWebService::SetMfcTriggered(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lock(m_MfcTriggeredMutex);
|
||||
m_MfcTriggeredDevices.insert(device_id);
|
||||
}
|
||||
|
||||
bool CWebService::IsMfcTriggered(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lock(m_MfcTriggeredMutex);
|
||||
return m_MfcTriggeredDevices.find(device_id) != m_MfcTriggeredDevices.end();
|
||||
}
|
||||
|
||||
void CWebService::ClearMfcTriggered(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lock(m_MfcTriggeredMutex);
|
||||
m_MfcTriggeredDevices.erase(device_id);
|
||||
}
|
||||
|
||||
bool CWebService::HasActiveSession(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lock(m_ScreenContextsMutex);
|
||||
return m_ScreenContexts.find(device_id) != m_ScreenContexts.end();
|
||||
}
|
||||
|
||||
void CWebService::NotifyDeviceUpdate(uint64_t device_id, const std::string& rtt, const std::string& activeWindow) {
|
||||
if (!m_bRunning || m_bStopping) return;
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ struct WebUser {
|
||||
std::string password_hash; // SHA256(password + salt)
|
||||
std::string salt;
|
||||
std::string role; // "admin" | "viewer"
|
||||
std::vector<std::string> allowed_groups; // Groups this user can view (empty = no access, admin = all)
|
||||
};
|
||||
|
||||
// Device info for web clients
|
||||
@@ -79,7 +80,8 @@ public:
|
||||
void SetAdminPassword(const std::string& password);
|
||||
|
||||
// User management
|
||||
bool CreateUser(const std::string& username, const std::string& password, const std::string& role);
|
||||
bool CreateUser(const std::string& username, const std::string& password, const std::string& role,
|
||||
const std::vector<std::string>& allowed_groups = {});
|
||||
bool DeleteUser(const std::string& username);
|
||||
std::vector<std::pair<std::string, std::string>> ListUsers(); // Returns [(username, role), ...]
|
||||
|
||||
@@ -144,7 +146,7 @@ private:
|
||||
|
||||
// JSON helpers
|
||||
std::string BuildJsonResponse(const std::string& cmd, bool ok, const std::string& msg = "");
|
||||
std::string BuildDeviceListJson();
|
||||
std::string BuildDeviceListJson(const std::string& username = "");
|
||||
|
||||
// Password verification
|
||||
bool VerifyPassword(const std::string& input, const WebUser& user);
|
||||
@@ -157,6 +159,7 @@ private:
|
||||
void HandleCreateUser(void* ws_ptr, const std::string& msg);
|
||||
void HandleDeleteUser(void* ws_ptr, const std::string& msg);
|
||||
void HandleListUsers(void* ws_ptr, const std::string& token);
|
||||
void HandleGetGroups(void* ws_ptr, const std::string& token);
|
||||
|
||||
// Send to WebSocket
|
||||
void SendText(void* ws_ptr, const std::string& text);
|
||||
@@ -224,6 +227,14 @@ public:
|
||||
bool IsWebTriggered(uint64_t device_id);
|
||||
void ClearWebTriggered(uint64_t device_id);
|
||||
|
||||
// MFC trigger management - MFC dialogs should always be visible
|
||||
void SetMfcTriggered(uint64_t device_id);
|
||||
bool IsMfcTriggered(uint64_t device_id);
|
||||
void ClearMfcTriggered(uint64_t device_id);
|
||||
|
||||
// Check if a remote desktop session already exists for device
|
||||
bool HasActiveSession(uint64_t device_id);
|
||||
|
||||
// Config accessors
|
||||
void SetHideWebSessions(bool hide) { m_bHideWebSessions = hide; }
|
||||
bool GetHideWebSessions() const { return m_bHideWebSessions; }
|
||||
@@ -240,6 +251,10 @@ private:
|
||||
// Screen context registry: device_id -> ScreenManager's CONTEXT_OBJECT
|
||||
std::map<uint64_t, CONTEXT_OBJECT*> m_ScreenContexts;
|
||||
std::mutex m_ScreenContextsMutex;
|
||||
|
||||
// MFC triggered devices: dialogs created by MFC should always be visible
|
||||
std::set<uint64_t> m_MfcTriggeredDevices;
|
||||
std::mutex m_MfcTriggeredMutex;
|
||||
};
|
||||
|
||||
// Global accessor
|
||||
|
||||
@@ -60,4 +60,19 @@ public:
|
||||
if (caps.IsEmpty()) return false;
|
||||
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_V2) != 0;
|
||||
}
|
||||
|
||||
// 检查客户端是否使用 UTF-8 协议字符串编码。
|
||||
// 无此能力位的老客户端:服务端按 CP_ACP(CP936,覆盖 95% 的简中/英语 ASCII 老客户端)解读。
|
||||
bool SupportsUtf8() const {
|
||||
CString caps = GetClientData(ONLINELIST_CAPABILITIES);
|
||||
if (caps.IsEmpty()) return false;
|
||||
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_UTF8) != 0;
|
||||
}
|
||||
|
||||
// 检查客户端是否支持屏幕预览(双击主机时拉缩略图)。
|
||||
bool SupportsScreenPreview() const {
|
||||
CString caps = GetClientData(ONLINELIST_CAPABILITIES);
|
||||
if (caps.IsEmpty()) return false;
|
||||
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_SCREEN_PREVIEW) != 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1171,7 +1171,7 @@ WIN32
|
||||
请选择目录=Language location
|
||||
国际化(&N)=Internationalization
|
||||
语言包目录(&D)=Language Pack Directory
|
||||
请通过“扩展”菜单指定语言包目录以支持多语言=Please specify the language pack directory via the "Extensions" menu to enable multi-language support.
|
||||
请通过\"扩展\"菜单指定语言包目录以支持多语言=Please specify the language pack directory via the "Extensions" menu to enable multi-language support.
|
||||
请选择[*.ico]图标文件或输入进程描述!=Please select an [*.ico] icon file or enter a process description!
|
||||
PE 编辑=PE Edit
|
||||
PE 编辑(&R)=PE Edit(&R)
|
||||
@@ -1819,3 +1819,7 @@ IOCP
|
||||
插件列表为空,无法创建触发器=Plugin list is empty, cannot create trigger
|
||||
请先选择至少一个插件=Please select at least one plugin
|
||||
|
||||
没有本地历史记录=No local history
|
||||
历史目录不存在: %s=History folder not exist: %s
|
||||
无法识别远程主机=Unknown remote machine
|
||||
没有远程历史记录=No remote history
|
||||
|
||||
@@ -1169,7 +1169,7 @@ WIN32
|
||||
请选择目录=請選擇目錄
|
||||
国际化(&N)=國際化
|
||||
语言包目录(&D)=語言包目錄
|
||||
请通过“扩展”菜单指定语言包目录以支持多语言=請透過「擴充」選單指定語言包目錄,以支援多國語言。
|
||||
请通过\"扩展\"菜单指定语言包目录以支持多语言=請透過「擴充」選單指定語言包目錄,以支援多國語言。
|
||||
请选择[*.ico]图标文件或输入进程描述!=請選擇[*.ico]圖示檔案或輸入處理程序描述!
|
||||
PE 编辑=PE 編輯
|
||||
PE 编辑(&R)=PE 編輯(&R)
|
||||
@@ -1810,3 +1810,7 @@ IOCP
|
||||
<< 移除=<< 移除
|
||||
插件列表为空,无法创建触发器=外掛列表為空,無法建立觸發器
|
||||
请先选择至少一个插件=請先選擇至少一個外掛
|
||||
没有本地历史记录=没有本地历史记录
|
||||
历史目录不存在: %s=历史目录不存在: %s
|
||||
无法识别远程主机=无法识别远程主机
|
||||
没有远程历史记录=没有远程历史记录
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 19 KiB |
@@ -506,6 +506,8 @@
|
||||
#define IDT_REMOTE_DOWNLOADS 2235
|
||||
#define IDT_REMOTE_HOME 2236
|
||||
#define IDT_REMOTE_SEARCH 2237
|
||||
#define IDT_LOCAL_HISTORY 2238
|
||||
#define IDT_REMOTE_HISTORY 2239
|
||||
#define IDC_BUTTON_SAVE_LICENSE 2240
|
||||
#define IDC_LICENSE_LIST 2241
|
||||
#define IDC_COMBO_VERSION 2245
|
||||
@@ -966,6 +968,11 @@
|
||||
#define IDC_STATIC_TRIGGER_TYPE 2544
|
||||
#define IDC_STATIC_TRIGGER_ACTION 2545
|
||||
|
||||
// 内嵌语言资源 (RCDATA)
|
||||
// 注意:避免与 IDB_BITMAP_TRIGGER(372) 和 IDB_BITMAP_WEBDESKTOP(373) 冲突
|
||||
#define IDR_LANG_EN_US 380
|
||||
#define IDR_LANG_ZH_TW 381
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
#define WM_SHOWNOTIFY WM_USER+3031
|
||||
#define WM_DISCONNECT WM_USER+3032
|
||||
#define WM_OPENTERMINALDIALOG WM_USER+3033
|
||||
#define WM_PREVIEW_RESPONSE WM_USER+3034
|
||||
|
||||
#ifdef _UNICODE
|
||||
#if defined _M_IX86
|
||||
|
||||
Reference in New Issue
Block a user