Compare commits
18 Commits
a8684753a9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b97f086c0c | ||
|
|
f24e7acc48 | ||
|
|
0cd7516bde | ||
|
|
a646e0417d | ||
|
|
23c9a4f242 | ||
|
|
85a5774a0f | ||
|
|
f2a95848ce | ||
|
|
f146af121a | ||
|
|
72dcdc5a6f | ||
|
|
8e170cf971 | ||
|
|
6d0b1bb07b | ||
|
|
bcccbefb77 | ||
|
|
92f6683fe1 | ||
|
|
d7408ad4df | ||
|
|
5296e534ed | ||
|
|
5a1430e904 | ||
|
|
218ee4f43d | ||
|
|
45553ec5b6 |
58
ReadMe.md
58
ReadMe.md
@@ -12,7 +12,7 @@
|
||||
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||
<img src="https://img.shields.io/gitea/v/release/yuanyuanxiang/SimpleRemoter?gitea_url=https%3A%2F%2Fgit.simpleremoter.com&style=flat-square&logo=gitea" alt="Gitea Release">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms">
|
||||
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Android-blue?style=flat-square" alt="Client Platforms">
|
||||
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
|
||||
<img src="https://img.shields.io/badge/language-C%2B%2B17%20%2F%20Go-orange?style=flat-square&logo=cplusplus" alt="Language">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
|
||||
@@ -94,6 +94,7 @@
|
||||
| **Windows** | ✅ 完整功能 | ✅ MFC `YAMA.exe`(推荐)/ Go |
|
||||
| **Linux** (X11) | ✅ 屏幕 + 终端 + 文件 + 剪贴板 | ✅ Go |
|
||||
| **macOS** (Intel + Apple Silicon) | ✅ 屏幕 + 终端 + 文件 + 剪贴板 | ✅ Go |
|
||||
| **Android** (v1.3.7+) | ✅ 屏幕 + 触控/按键控制 | ❌ 不适用 |
|
||||
|
||||
---
|
||||
|
||||
@@ -206,6 +207,21 @@ Please read and obey the instructions in [SECURITY_AI.md](./docs/SECURITY_AI.md)
|
||||
|
||||
**编译**:`cd linux && cmake . && make`
|
||||
|
||||
### Android 客户端(v1.3.7+)
|
||||
|
||||
**系统要求**:Android 5.0 (API 21) 及以上
|
||||
|
||||
| 功能 | 状态 | 实现 |
|
||||
|---|---|---|
|
||||
| 远程桌面 | ✅ | MediaProjection + MediaCodec H.264 硬件编码 |
|
||||
| 触控/按键控制 | ✅ | AccessibilityService 注入,支持 D-pad 导航 |
|
||||
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
|
||||
| 设备分组 | ✅ | szGroupName 编译期 patch / 服务端动态修改 |
|
||||
|
||||
**生成客户端**:在主控 BuildDlg「生成」→ 选 `ghost - Google Android`,填入服务端 IP / 端口,生成 APK;使用 `android/sign_apk.bat` 重签后安装。
|
||||
|
||||
**编译**:在 WSL 或 Linux 中 `cd android && ./build_apk.sh`
|
||||
|
||||
### macOS 客户端(v1.3.2+)
|
||||
|
||||
**系统要求**:
|
||||
@@ -258,13 +274,13 @@ Please read and obey the instructions in [SECURITY_AI.md](./docs/SECURITY_AI.md)
|
||||
│ TCP (自定义二进制协议) │ TCP (设备) + WS (浏览器)
|
||||
└────────┬─────────────────┘
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Windows │ │ Linux │ │ macOS │
|
||||
│ 客户端 │ │ 客户端 │ │ 客户端 │
|
||||
│ (DXGI) │ │ (X11) │ │ (CG) │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
┌──────────────┼──────────────┬──────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Windows │ │ Linux │ │ macOS │ │ Android │
|
||||
│ 客户端 │ │ 客户端 │ │ 客户端 │ │ 客户端 │
|
||||
│ (DXGI) │ │ (X11) │ │ (CG) │ │ (MP/MC) │
|
||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
### 多层授权(简化视图)
|
||||
@@ -340,6 +356,7 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
|
||||
- **C++ 主控 & Windows 客户端**:VS 2019/2022/2026 打开 `SimpleRemoter.sln` → Release | x64
|
||||
- **Linux 客户端**:`cd linux && cmake . && make`
|
||||
- **macOS 客户端**:`cd macos && ./build.sh`
|
||||
- **Android 客户端**:`cd android && ./build_apk.sh`(WSL/Linux)
|
||||
- **Go 主控**:`cd server/go && go build ./cmd`
|
||||
|
||||
---
|
||||
@@ -361,6 +378,31 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.3.7 (2026.6.26)
|
||||
|
||||
**Android 客户端 & 窗口捕获增强 & 安全加固**
|
||||
|
||||
**新功能:**
|
||||
- **Android 客户端**:首个 Android 受控端,支持 Android 5.0+(arm64-v8a / armeabi-v7a),MediaProjection + MediaCodec H.264 推流;AccessibilityService 触控 / 按键注入;D-pad 焦点导航完整控制 TV / 机顶盒
|
||||
- **设备分组**:编译期 patch 预设 / 服务端动态修改(`CMD_SET_GROUP`);分组名持久化至 `filesDir/yama_group`,App 重启后保留
|
||||
- **APK 生成与重签**:BuildDlg 新增 `ghost - Google Android`,与 Linux / macOS ghost 流程一致;配套 `sign_apk.bat` 自动检测 SDK 完成重签
|
||||
- **服务端删除客户端**:BYE 指令后 Android Service 通过 JNI 正常停止
|
||||
- **前台窗口精准捕获**:PrintWindow + 服务端 HWND by clientID 路由,遮挡情况下仍可捕获完整窗口
|
||||
- **在线主机右键查看活动窗口**:实时查询目标机器前台窗口标题
|
||||
- **TOKEN_AUTH 响应 ECDSA 签名**:V2 私钥签名服务端响应,防伪服务端(`TOKEN_SERVER_VERIFY = 251`)
|
||||
- **反破解版本绑定**:版本与核心库绑定,阻断替换 DLL 的破解路径
|
||||
- **子授权连接数限制**:`LicenseLimit` 字段 + `CLicenseDlg` 右键菜单实时设置
|
||||
|
||||
**改进:**
|
||||
- H264 模式跳过首帧 ~8MB 原始巨帧,服务端在首 IDR 帧后解锁画面
|
||||
- 自适应尺寸渲染改进,减少尺寸切换抖动
|
||||
|
||||
**Bug 修复:**
|
||||
- `ARGBToNV12` 奇数尺寸窗口堆溢出(维度钳制到偶数对齐)
|
||||
- 连接初始化时多余屏幕重启(客户端 `ScreenManager.cpp` + 服务端 `ScreenSpyDlg.cpp` 双端修复)
|
||||
- 启动时内存 DLL 未正确还原
|
||||
- 授权客户端键盘日志目录冲突(各实例使用独立目录)
|
||||
|
||||
### v1.3.6 (2026.6.14)
|
||||
|
||||
**ROI 区域捕获 & Web 音频流 & 主界面可用性全面提升**
|
||||
|
||||
58
ReadMe_EN.md
58
ReadMe_EN.md
@@ -12,7 +12,7 @@
|
||||
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||
<img src="https://img.shields.io/gitea/v/release/yuanyuanxiang/SimpleRemoter?gitea_url=https%3A%2F%2Fgit.simpleremoter.com&style=flat-square&logo=gitea" alt="Gitea Release">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms">
|
||||
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Android-blue?style=flat-square" alt="Client Platforms">
|
||||
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
|
||||
<img src="https://img.shields.io/badge/language-C%2B%2B17%20%2F%20Go-orange?style=flat-square&logo=cplusplus" alt="Language">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
|
||||
@@ -94,6 +94,7 @@ This release (v1.3.4) adds the last missing piece — the **Go master**: a **del
|
||||
| **Windows** | ✅ All features | ✅ MFC `YAMA.exe` (recommended) / Go |
|
||||
| **Linux** (X11) | ✅ Screen + terminal + files + clipboard | ✅ Go |
|
||||
| **macOS** (Intel + Apple Silicon) | ✅ Screen + terminal + files + clipboard | ✅ Go |
|
||||
| **Android** (v1.3.7+) | ✅ Screen + touch/key injection | ❌ N/A |
|
||||
|
||||
---
|
||||
|
||||
@@ -206,6 +207,21 @@ Unless an offline license has been obtained, the master program exchanges necess
|
||||
|
||||
**Build**: `cd linux && cmake . && make`
|
||||
|
||||
### Android Client (v1.3.7+)
|
||||
|
||||
**Requirements**: Android 5.0 (API 21) or later
|
||||
|
||||
| Feature | Status | Implementation |
|
||||
|---|---|---|
|
||||
| Remote desktop | ✅ | MediaProjection + MediaCodec H.264 hardware encoding |
|
||||
| Touch / key injection | ✅ | AccessibilityService, supports D-pad TV navigation |
|
||||
| Heartbeat / RTT | ✅ | RFC 6298 RTT estimation |
|
||||
| Device grouping | ✅ | Build-time patch or server-side `CMD_SET_GROUP`; persisted across restarts |
|
||||
|
||||
**Generate client**: In the master BuildDlg *Build* dialog, select `ghost - Google Android`, enter the server IP / port, and the APK is generated. Re-sign with `android/sign_apk.bat` before installing.
|
||||
|
||||
**Build**: in WSL or Linux — `cd android && ./build_apk.sh`
|
||||
|
||||
### macOS Client (v1.3.2+)
|
||||
|
||||
**Requirements**:
|
||||
@@ -258,13 +274,13 @@ Unless an offline license has been obtained, the master program exchanges necess
|
||||
│ TCP (custom binary proto)│ TCP (devices) + WS (browsers)
|
||||
└────────┬─────────────────┘
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Windows │ │ Linux │ │ macOS │
|
||||
│ client │ │ client │ │ client │
|
||||
│ (DXGI) │ │ (X11) │ │ (CG) │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
┌──────────────┼──────────────┬──────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Windows │ │ Linux │ │ macOS │ │ Android │
|
||||
│ client │ │ client │ │ client │ │ client │
|
||||
│ (DXGI) │ │ (X11) │ │ (CG) │ │ (MP/MC) │
|
||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
### Multi-Layer Authorization (simplified view)
|
||||
@@ -340,6 +356,7 @@ Valid : 2026-02-01 to 2028-02-01
|
||||
- **C++ master & Windows client**: open `SimpleRemoter.sln` in VS 2019 / 2022 / 2026 → Release | x64
|
||||
- **Linux client**: `cd linux && cmake . && make`
|
||||
- **macOS client**: `cd macos && ./build.sh`
|
||||
- **Android client**: `cd android && ./build_apk.sh` (WSL / Linux)
|
||||
- **Go master**: `cd server/go && go build ./cmd`
|
||||
|
||||
---
|
||||
@@ -361,6 +378,31 @@ Valid : 2026-02-01 to 2028-02-01
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.3.7 (2026.6.26)
|
||||
|
||||
**Android Client & Window Capture & Security Hardening**
|
||||
|
||||
**New features:**
|
||||
- **Android client**: first Android controlled-side client, Android 5.0+ (arm64-v8a / armeabi-v7a); MediaProjection + MediaCodec H.264 streaming; AccessibilityService touch / key injection; D-pad focus navigation for full TV / set-top-box control
|
||||
- **Device grouping**: group name embedded at build time via APK binary patch or changed live by the server (`CMD_SET_GROUP`); persisted to `filesDir/yama_group`, survives app restarts
|
||||
- **APK build & re-sign**: BuildDlg adds `ghost - Google Android`, same workflow as Linux / macOS ghost; `sign_apk.bat` auto-detects SDK and re-signs
|
||||
- **Clean exit on server delete**: BYE command triggers JNI `onNativeExit()` → `stopSelf()`
|
||||
- **Foreground window capture**: PrintWindow + server-side HWND routing by clientID, captures the target window even when occluded
|
||||
- **View active window from host list**: right-click menu queries and shows the foreground window title of the target machine in real time
|
||||
- **TOKEN_AUTH ECDSA signature**: server signs auth responses with its V2 P-256 key to prevent fake-server attacks (`TOKEN_SERVER_VERIFY = 251`)
|
||||
- **Anti-cracking version binding**: binary binds to core lib version; mismatched DLL replacement is rejected at load time
|
||||
- **Sub-license connection cap**: `LicenseLimit` field in `licenses.ini` + real-time set / clear from `CLicenseDlg` right-click menu
|
||||
|
||||
**Improvements:**
|
||||
- H264 mode skips the initial ~8 MB raw first-frame; server unlocks display on the first IDR instead
|
||||
- Adaptive-size rendering improvements, fewer dimension-change artifacts
|
||||
|
||||
**Bug fixes:**
|
||||
- `ARGBToNV12` heap overflow on odd-sized windows (clamp dims to even-aligned width/height)
|
||||
- Extra screen restarts on connection init (dual fix: client `ScreenManager.cpp` + server `ScreenSpyDlg.cpp`)
|
||||
- Memory DLL not restored on client startup
|
||||
- Authorization client keyboard log directory conflict (each instance gets its own directory)
|
||||
|
||||
### v1.3.6 (2026.6.14)
|
||||
|
||||
**ROI region capture & Web audio streaming & master-UI usability overhaul**
|
||||
|
||||
58
ReadMe_TW.md
58
ReadMe_TW.md
@@ -12,7 +12,7 @@
|
||||
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||
<img src="https://img.shields.io/gitea/v/release/yuanyuanxiang/SimpleRemoter?gitea_url=https%3A%2F%2Fgit.simpleremoter.com&style=flat-square&logo=gitea" alt="Gitea Release">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms">
|
||||
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Android-blue?style=flat-square" alt="Client Platforms">
|
||||
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
|
||||
<img src="https://img.shields.io/badge/language-C%2B%2B17%20%2F%20Go-orange?style=flat-square&logo=cplusplus" alt="Language">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
|
||||
@@ -94,6 +94,7 @@
|
||||
| **Windows** | ✅ 完整功能 | ✅ MFC `YAMA.exe`(推薦)/ Go |
|
||||
| **Linux** (X11) | ✅ 螢幕 + 終端 + 檔案 + 剪貼簿 | ✅ Go |
|
||||
| **macOS** (Intel + Apple Silicon) | ✅ 螢幕 + 終端 + 檔案 + 剪貼簿 | ✅ Go |
|
||||
| **Android** (v1.3.7+) | ✅ 螢幕 + 觸控/按鍵注入 | ❌ 不適用 |
|
||||
|
||||
---
|
||||
|
||||
@@ -206,6 +207,21 @@ Please read and obey the instructions in [SECURITY_AI.md](./docs/SECURITY_AI.md)
|
||||
|
||||
**編譯**:`cd linux && cmake . && make`
|
||||
|
||||
### Android 用戶端(v1.3.7+)
|
||||
|
||||
**系統需求**:Android 5.0 (API 21) 及以上
|
||||
|
||||
| 功能 | 狀態 | 實作 |
|
||||
|---|---|---|
|
||||
| 遠端桌面 | ✅ | MediaProjection + MediaCodec H.264 硬體編碼 |
|
||||
| 觸控 / 按鍵注入 | ✅ | AccessibilityService,支援 D-pad TV 導航 |
|
||||
| 心跳 / RTT | ✅ | RFC 6298 RTT 估算 |
|
||||
| 裝置分組 | ✅ | 編譯期 patch 或服務端 `CMD_SET_GROUP` 動態修改;重啟後仍保留 |
|
||||
|
||||
**產生用戶端**:在主控 BuildDlg「產生」選 `ghost - Google Android`,填入伺服端 IP / 埠,生成 APK;使用 `android/sign_apk.bat` 重簽後安裝。
|
||||
|
||||
**編譯**:在 WSL 或 Linux 中 `cd android && ./build_apk.sh`
|
||||
|
||||
### macOS 用戶端(v1.3.2+)
|
||||
|
||||
**系統需求**:
|
||||
@@ -258,13 +274,13 @@ Please read and obey the instructions in [SECURITY_AI.md](./docs/SECURITY_AI.md)
|
||||
│ TCP (自訂二進位協定) │ TCP (裝置) + WS (瀏覽器)
|
||||
└────────┬─────────────────┘
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Windows │ │ Linux │ │ macOS │
|
||||
│ 用戶端 │ │ 用戶端 │ │ 用戶端 │
|
||||
│ (DXGI) │ │ (X11) │ │ (CG) │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
┌──────────────┼──────────────┬──────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Windows │ │ Linux │ │ macOS │ │ Android │
|
||||
│ 用戶端 │ │ 用戶端 │ │ 用戶端 │ │ 用戶端 │
|
||||
│ (DXGI) │ │ (X11) │ │ (CG) │ │ (MP/MC) │
|
||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
### 多層授權(簡化視圖)
|
||||
@@ -340,6 +356,7 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
|
||||
- **C++ 主控 & Windows 用戶端**:VS 2019/2022/2026 開啟 `SimpleRemoter.sln` → Release | x64
|
||||
- **Linux 用戶端**:`cd linux && cmake . && make`
|
||||
- **macOS 用戶端**:`cd macos && ./build.sh`
|
||||
- **Android 用戶端**:`cd android && ./build_apk.sh`(WSL/Linux)
|
||||
- **Go 主控**:`cd server/go && go build ./cmd`
|
||||
|
||||
---
|
||||
@@ -361,6 +378,31 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
|
||||
|
||||
## 更新日誌
|
||||
|
||||
### v1.3.7 (2026.6.26)
|
||||
|
||||
**Android 用戶端 & 視窗擷取增強 & 安全加固**
|
||||
|
||||
**新功能:**
|
||||
- **Android 用戶端**:首個 Android 受控端,支援 Android 5.0+(arm64-v8a / armeabi-v7a);MediaProjection + MediaCodec H.264 推流;AccessibilityService 觸控 / 按鍵注入;D-pad 焦點導航完整控制 TV / 機上盒
|
||||
- **裝置分組**:編譯期 patch 預設 / 伺服端動態修改(`CMD_SET_GROUP`);分組名持久化至 `filesDir/yama_group`,App 重啟後保留
|
||||
- **APK 產生與重簽**:BuildDlg 新增 `ghost - Google Android`,與 Linux / macOS ghost 流程一致;配套 `sign_apk.bat` 自動偵測 SDK 完成重簽
|
||||
- **伺服端刪除用戶端**:BYE 指令後 Android Service 透過 JNI 正常停止
|
||||
- **前台視窗精準擷取**:PrintWindow + 伺服端 HWND by clientID 路由,遮蓋情況下仍可擷取完整視窗
|
||||
- **線上主機右鍵查看活動視窗**:即時查詢目標機器前台視窗標題
|
||||
- **TOKEN_AUTH 回應 ECDSA 簽章**:V2 私鑰簽名伺服端回應,防偽造伺服端(`TOKEN_SERVER_VERIFY = 251`)
|
||||
- **反破解版本綁定**:版本與核心函式庫綁定,阻斷替換 DLL 的破解路徑
|
||||
- **子授權連線數限制**:`LicenseLimit` 欄位 + `CLicenseDlg` 右鍵選單即時設定
|
||||
|
||||
**改進:**
|
||||
- H264 模式跳過首幀 ~8MB 原始巨幀,伺服端在首 IDR 幀後解鎖畫面
|
||||
- 自適應尺寸渲染改進,減少尺寸切換閃爍
|
||||
|
||||
**Bug 修復:**
|
||||
- `ARGBToNV12` 奇數尺寸視窗堆積溢位(維度鉗制到偶數對齊)
|
||||
- 連線初始化時多餘螢幕重啟(用戶端 `ScreenManager.cpp` + 伺服端 `ScreenSpyDlg.cpp` 雙端修復)
|
||||
- 啟動時記憶體 DLL 未正確還原
|
||||
- 授權用戶端鍵盤日誌目錄衝突(各實例使用獨立目錄)
|
||||
|
||||
### v1.3.6 (2026.6.14)
|
||||
|
||||
**ROI 區域擷取 & Web 音訊串流 & 主控介面可用性全面提升**
|
||||
|
||||
@@ -3,6 +3,9 @@ plugins {
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
// 模拟器调试时传 -PincludeEmulatorAbis=true;build_apk.sh 发布时强制传 false
|
||||
def INCLUDE_EMULATOR_ABIS = project.findProperty('includeEmulatorAbis')?.toBoolean() ?: false
|
||||
|
||||
android {
|
||||
namespace 'com.yama.client'
|
||||
compileSdk 35
|
||||
@@ -16,7 +19,11 @@ android {
|
||||
versionName "1.0"
|
||||
|
||||
ndk {
|
||||
abiFilters 'arm64-v8a', 'armeabi-v7a'
|
||||
if (INCLUDE_EMULATOR_ABIS) {
|
||||
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'
|
||||
} else {
|
||||
abiFilters 'arm64-v8a', 'armeabi-v7a'
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
@@ -53,6 +60,13 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
// .so 文件不压缩(ZIP_STORED),使服务端 patch 工具可直接在 APK 中搜索 FLAG_GHOST
|
||||
useLegacyPackaging false
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:extractNativeLibs="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="YAMA"
|
||||
|
||||
@@ -24,6 +24,7 @@ set(CORE_SOURCES
|
||||
${YAMA_ROOT}/client/Buffer.cpp
|
||||
${YAMA_ROOT}/client/IOCPClient.cpp
|
||||
${YAMA_ROOT}/client/sign_shim_unix.cpp
|
||||
${YAMA_ROOT}/common/logger.cpp
|
||||
${YAMA_ROOT}/common/ikcp.c
|
||||
)
|
||||
|
||||
@@ -34,12 +35,11 @@ set(ANDROID_SOURCES
|
||||
|
||||
add_library(yama SHARED ${CORE_SOURCES} ${ANDROID_SOURCES})
|
||||
|
||||
# ---- 预编译静态库(由 SimplePlugins/sign_lib/build_android.sh 产出)----
|
||||
# ---- 预编译静态库(由 android/build_android_libs.ps1 产出)----
|
||||
# 支持 arm64-v8a / armeabi-v7a(真机)和 x86 / x86_64(Android 模拟器)
|
||||
target_link_libraries(yama PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/lib/${ANDROID_ABI}/libsign.a"
|
||||
)
|
||||
|
||||
# ---- zstd(由 android/build_zstd_android.sh 产出)----
|
||||
target_link_libraries(yama PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/lib/${ANDROID_ABI}/libzstd.a"
|
||||
)
|
||||
|
||||
BIN
android/app/src/main/cpp/lib/x86/libsign.a
Normal file
BIN
android/app/src/main/cpp/lib/x86/libsign.a
Normal file
Binary file not shown.
BIN
android/app/src/main/cpp/lib/x86/libzstd.a
Normal file
BIN
android/app/src/main/cpp/lib/x86/libzstd.a
Normal file
Binary file not shown.
BIN
android/app/src/main/cpp/lib/x86_64/libsign.a
Normal file
BIN
android/app/src/main/cpp/lib/x86_64/libsign.a
Normal file
Binary file not shown.
BIN
android/app/src/main/cpp/lib/x86_64/libzstd.a
Normal file
BIN
android/app/src/main/cpp/lib/x86_64/libzstd.a
Normal file
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
@@ -9,7 +10,9 @@
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netdb.h>
|
||||
@@ -22,13 +25,18 @@
|
||||
#include "ScreenHandler.h"
|
||||
#define XXH_INLINE_ALL
|
||||
#include "common/xxhash.h"
|
||||
#include "common/logger.h"
|
||||
|
||||
#define LOG_TAG "YAMA"
|
||||
#undef Mprintf
|
||||
#define Mprintf(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
// 1 = 状态提示显示到电视屏幕;0 = 只写 logcat,不显示到屏幕
|
||||
#define SCREEN_STATUS_ENABLED 0
|
||||
|
||||
extern "C" int signMessage_c(const char* pk, int pkLen, const unsigned char* msg,
|
||||
int msgLen, char* buf, int bufSize);
|
||||
|
||||
// ---- 设备信息 helper(登录时填充,仅调用一次)----
|
||||
|
||||
static int GetCpuCores() {
|
||||
@@ -109,23 +117,31 @@ static std::string JsonGetStr(const std::string& json, const char* key) {
|
||||
|
||||
// 查询 ip-api.com,一次请求同时获取公网 IP 和地理位置(与 Windows 客户端同源)
|
||||
// 在连接线程中同步调用,超时 3 秒
|
||||
static bool FetchGeoInfo(std::string& pubIp, std::string& location) {
|
||||
pubIp.clear(); location.clear();
|
||||
struct GeoResult {
|
||||
std::string pubIp, location;
|
||||
std::atomic<bool> ready{false};
|
||||
};
|
||||
|
||||
static void FetchGeoInfoImpl(std::shared_ptr<GeoResult> out) {
|
||||
struct addrinfo hints = {}, *res = nullptr;
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
if (getaddrinfo("ip-api.com", "80", &hints, &res) != 0 || !res) return false;
|
||||
|
||||
if (getaddrinfo("ip-api.com", "80", &hints, &res) != 0 || !res) {
|
||||
out->ready.store(true, std::memory_order_release); return;
|
||||
}
|
||||
int fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) { freeaddrinfo(res); return false; }
|
||||
if (fd < 0) { freeaddrinfo(res); out->ready.store(true, std::memory_order_release); return; }
|
||||
|
||||
struct timeval tv = {3, 0};
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
|
||||
connect(fd, res->ai_addr, res->ai_addrlen);
|
||||
fd_set wfds; FD_ZERO(&wfds); FD_SET(fd, &wfds);
|
||||
bool connected = (select(fd + 1, nullptr, &wfds, nullptr, &tv) == 1);
|
||||
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
|
||||
|
||||
bool ok = false;
|
||||
if (connect(fd, res->ai_addr, res->ai_addrlen) == 0) {
|
||||
if (connected) {
|
||||
const char* req = "GET /json/?fields=status,query,country,city HTTP/1.0\r\n"
|
||||
"Host: ip-api.com\r\n"
|
||||
"Connection: close\r\n\r\n";
|
||||
@@ -140,21 +156,37 @@ static bool FetchGeoInfo(std::string& pubIp, std::string& location) {
|
||||
if (sep != std::string::npos) {
|
||||
const std::string body = resp.substr(sep + 4);
|
||||
if (JsonGetStr(body, "status") == "success") {
|
||||
pubIp = JsonGetStr(body, "query");
|
||||
out->pubIp = JsonGetStr(body, "query");
|
||||
std::string city = JsonGetStr(body, "city");
|
||||
std::string country = JsonGetStr(body, "country");
|
||||
if (!city.empty() && !country.empty()) location = city + ", " + country;
|
||||
else if (!country.empty()) location = country;
|
||||
else if (!city.empty()) location = city;
|
||||
ok = true;
|
||||
if (!city.empty() && !country.empty()) out->location = city + ", " + country;
|
||||
else if (!country.empty()) out->location = country;
|
||||
else if (!city.empty()) out->location = city;
|
||||
}
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
freeaddrinfo(res);
|
||||
return ok;
|
||||
out->ready.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// getaddrinfo 在某些设备上会永久阻塞,用独立线程 + 5 秒轮询超时保护 ConnectionThread
|
||||
static bool FetchGeoInfo(std::string& pubIp, std::string& location) {
|
||||
pubIp.clear(); location.clear();
|
||||
auto result = std::make_shared<GeoResult>();
|
||||
std::thread(FetchGeoInfoImpl, result).detach();
|
||||
for (int i = 0; i < 50 && !result->ready.load(std::memory_order_acquire); i++)
|
||||
usleep(100000); // 100ms × 50 = 5 秒
|
||||
if (!result->ready.load(std::memory_order_acquire)) return false;
|
||||
pubIp = result->pubIp;
|
||||
location = result->location;
|
||||
return !pubIp.empty();
|
||||
}
|
||||
|
||||
// 服务端通过 FLAG_GHOST("Hello, World!")定位此变量,patch szServerIP/szPort 后重签 APK
|
||||
// 偏移:szServerIP at +32, szPort at +132(见 commands.h CONNECT_ADDRESS 定义)
|
||||
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_ANDROID };
|
||||
|
||||
// ---- 全局状态 ----
|
||||
// CPP-06: g_bExit 是全局变量,跨调用边界不会被编译器缓存进寄存器;
|
||||
// IOCPClient 构造接受 const State& 持有引用,不能改为 atomic/volatile,
|
||||
@@ -173,6 +205,8 @@ static jmethodID g_getActiveWindowMid = nullptr;
|
||||
static jclass g_captureClass = nullptr;
|
||||
static jmethodID g_requestIdrMid = nullptr;
|
||||
static jmethodID g_forceFirstFrameMid = nullptr;
|
||||
static jmethodID g_statusMid = nullptr; // CaptureService.onNativeStatus(String)
|
||||
static jmethodID g_nativeExitMid = nullptr; // CaptureService.onNativeExit() — 服务端主动断开时停止服务
|
||||
|
||||
// 心跳日志限流:每 60 秒最多打一次
|
||||
static std::atomic<uint64_t> g_lastHbLogMs{0};
|
||||
@@ -241,6 +275,35 @@ static std::string g_androidVersion;
|
||||
static std::string g_screenRes;
|
||||
static std::string g_username;
|
||||
static std::string g_apkPath;
|
||||
static std::string g_filesDir; // context.filesDir,无需权限,用于持久化分组名
|
||||
|
||||
static void SaveGroupName() {
|
||||
if (g_filesDir.empty()) return;
|
||||
std::string path = g_filesDir + "/yama_group";
|
||||
FILE* f = fopen(path.c_str(), "w");
|
||||
if (!f) { LOGI("SaveGroupName: cannot open %s", path.c_str()); return; }
|
||||
fputs(g_SETTINGS.szGroupName, f);
|
||||
fclose(f);
|
||||
LOGI("Group saved: %s", g_SETTINGS.szGroupName);
|
||||
}
|
||||
|
||||
static void LoadGroupName() {
|
||||
if (g_filesDir.empty()) return;
|
||||
std::string path = g_filesDir + "/yama_group";
|
||||
FILE* f = fopen(path.c_str(), "r");
|
||||
if (!f) return;
|
||||
char buf[24] = {};
|
||||
if (fgets(buf, sizeof(buf), f)) {
|
||||
size_t len = strlen(buf);
|
||||
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) buf[--len] = '\0';
|
||||
if (len > 0) {
|
||||
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
|
||||
strncpy(g_SETTINGS.szGroupName, buf, sizeof(g_SETTINGS.szGroupName) - 1);
|
||||
LOGI("Group loaded from file: %s", buf);
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
// 屏幕尺寸(Java 侧 MediaCodec 配置后通过 nativeSetScreenSize 设置)
|
||||
// 初始化为 0;ScreenSpyThread 等到非零后再读,避免在 nativeSetScreenSize 之前
|
||||
@@ -277,6 +340,29 @@ static void RequestIdrFromJava() { CallCaptureStaticVoid(g_requestIdrMid);
|
||||
// 强制 VirtualDisplay 推一帧(静止屏幕绕过 SurfaceFlinger 空闲优化)
|
||||
static void ForceFirstFrameFromJava() { CallCaptureStaticVoid(g_forceFirstFrameMid); }
|
||||
|
||||
// 向 CaptureService.onNativeStatus() 发送状态 Toast(连接线程诊断用)
|
||||
static void PostStatus(const char* msg) {
|
||||
LOGI("STATUS: %s", msg);
|
||||
#if SCREEN_STATUS_ENABLED
|
||||
if (!g_jvm || !g_captureClass || !g_statusMid) return;
|
||||
JNIEnv* jenv = nullptr;
|
||||
bool attached = false;
|
||||
if (g_jvm->GetEnv((void**)&jenv, JNI_VERSION_1_6) == JNI_EDETACHED) {
|
||||
g_jvm->AttachCurrentThread(&jenv, nullptr);
|
||||
attached = true;
|
||||
}
|
||||
if (jenv) {
|
||||
jstring js = jenv->NewStringUTF(msg);
|
||||
if (js) {
|
||||
jenv->CallStaticVoidMethod(g_captureClass, g_statusMid, js);
|
||||
jenv->DeleteLocalRef(js);
|
||||
}
|
||||
if (jenv->ExceptionOccurred()) jenv->ExceptionClear();
|
||||
}
|
||||
if (attached) g_jvm->DetachCurrentThread();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void ScreenSpyThread()
|
||||
{
|
||||
// 等待 Java 侧 startCapture() 调用 nativeSetScreenSize 设置真实分辨率。
|
||||
@@ -299,32 +385,36 @@ static void ScreenSpyThread()
|
||||
std::unique_ptr<IOCPClient> client;
|
||||
std::unique_ptr<AndroidScreenHandler> handler;
|
||||
try {
|
||||
LOGI("SST[%d]: creating IOCPClient", attempt);
|
||||
client = std::make_unique<IOCPClient>(g_bExit, true);
|
||||
client->EnableSubConnAuth(true, g_myClientID);
|
||||
|
||||
LOGI("SST[%d]: connecting %s:%d", attempt, g_serverIp.c_str(), g_serverPort);
|
||||
if (!client->ConnectServer(g_serverIp.c_str(), g_serverPort)) {
|
||||
LOGI("ScreenSpyThread: connect failed (attempt %d/20), retry 2s", attempt);
|
||||
Sleep(2000);
|
||||
continue;
|
||||
}
|
||||
LOGI("SST[%d]: connected, creating handler w=%d h=%d", attempt, w, h);
|
||||
|
||||
handler = std::make_unique<AndroidScreenHandler>(client.get(), w, h);
|
||||
LOGI("SST[%d]: handler created, inserting to set", attempt);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_shMutex);
|
||||
g_screenHandlers.insert(handler.get());
|
||||
}
|
||||
|
||||
LOGI("SST[%d]: setManagerCallBack", attempt);
|
||||
client->setManagerCallBack(handler.get(),
|
||||
IOCPManager::DataProcess,
|
||||
IOCPManager::ReconnectProcess);
|
||||
LOGI("SST[%d]: SendBitmapInfo", attempt);
|
||||
handler->SendBitmapInfo();
|
||||
// 子连接刚建立,浏览器 initDecoder 完成后需要关键帧才能开始显示。
|
||||
// ForceFirstFrameFromJava:短暂 resize VirtualDisplay 强制 SurfaceFlinger
|
||||
// 推一帧给编码器——静止屏幕时 SurfaceFlinger 有空闲优化不主动推帧,
|
||||
// 导致 REQUEST_SYNC_FRAME 排队等待,造成 10-60 秒黑屏。
|
||||
// RequestIdrFromJava 确保此帧被编码为 IDR。
|
||||
LOGI("SST[%d]: ForceFirstFrame", attempt);
|
||||
ForceFirstFrameFromJava();
|
||||
LOGI("SST[%d]: RequestIdr", attempt);
|
||||
RequestIdrFromJava();
|
||||
LOGI("SST[%d]: entering wait loop", attempt);
|
||||
|
||||
while (client->IsRunning() && client->IsConnected() && S_CLIENT_NORMAL == g_bExit)
|
||||
Sleep(200);
|
||||
@@ -354,24 +444,28 @@ static void ScreenSpyThread()
|
||||
int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
if (!szBuffer || !ulLength) return TRUE;
|
||||
|
||||
if (!ClientAuth::IsCommandAllowed(szBuffer[0]))
|
||||
int allowed = (int)ClientAuth::IsCommandAllowed(szBuffer[0]);
|
||||
if (!allowed) {
|
||||
LOGI("DataProcess cmd=%d len=%lu allowed=%d",
|
||||
(int)(unsigned char)szBuffer[0], (unsigned long)ulLength, allowed);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
switch (szBuffer[0]) {
|
||||
case COMMAND_BYE:
|
||||
LOGI("COMMAND_BYE");
|
||||
PostStatus("BYE from server");
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
break;
|
||||
|
||||
case CMD_HEARTBEAT_ACK:
|
||||
if (ulLength >= 1 + (ULONG)sizeof(HeartbeatACK)) {
|
||||
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
|
||||
HeartbeatACK ack;
|
||||
memcpy(&ack, szBuffer + 1, sizeof(HeartbeatACK));
|
||||
uint64_t now = GetUnixMs();
|
||||
g_lastHeartbeatAckMs.store(now, std::memory_order_relaxed);
|
||||
int64_t rtt = (int64_t)now - (int64_t)ack->Time;
|
||||
if (ack->ProcessingMs > 0 && (int64_t)ack->ProcessingMs < rtt)
|
||||
rtt -= ack->ProcessingMs;
|
||||
int64_t rtt = (int64_t)now - (int64_t)ack.Time;
|
||||
if (ack.ProcessingMs > 0 && (int64_t)ack.ProcessingMs < rtt)
|
||||
rtt -= ack.ProcessingMs;
|
||||
g_rttEstimator.update_from_sample((double)rtt);
|
||||
if (now - g_lastHbLogMs.load(std::memory_order_relaxed) >= HB_LOG_INTERVAL_MS) {
|
||||
g_lastHbLogMs.store(now, std::memory_order_relaxed);
|
||||
@@ -386,6 +480,9 @@ int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
|
||||
if (settings.ReportInterval > 0)
|
||||
g_heartbeatInterval = std::max(settings.ReportInterval, 30);
|
||||
LOGI("MasterSettings OK interval=%ds (server=%d)", g_heartbeatInterval, settings.ReportInterval);
|
||||
PostStatus("masterSettings: OK");
|
||||
} else {
|
||||
PostStatus("masterSettings: FAIL");
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -394,7 +491,8 @@ int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
|
||||
// 每个 COMMAND_SCREEN_SPY 对应一个独立子连接,支持多人同时观看/控制
|
||||
size_t active;
|
||||
{ std::lock_guard<std::mutex> lk(g_shMutex); active = g_screenHandlers.size(); }
|
||||
LOGI("COMMAND_SCREEN_SPY → ScreenSpyThread (active=%zu)", active);
|
||||
LOGI("cmd: COMMAND_SCREEN_SPY len=%lu active=%zu w=%d h=%d",
|
||||
(unsigned long)ulLength, active, g_screenWidth.load(), g_screenHeight.load());
|
||||
std::thread(ScreenSpyThread).detach();
|
||||
break;
|
||||
}
|
||||
@@ -412,6 +510,23 @@ int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_SET_GROUP: {
|
||||
std::string grp;
|
||||
if (ulLength > 1) {
|
||||
grp.assign((const char*)szBuffer + 1, ulLength - 1);
|
||||
auto z = grp.find('\0');
|
||||
if (z != std::string::npos) grp.resize(z);
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_shMutex);
|
||||
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
|
||||
strncpy(g_SETTINGS.szGroupName, grp.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
|
||||
}
|
||||
SaveGroupName();
|
||||
LOGI("Group changed to: %s", grp.c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SHELL:
|
||||
LOGI("COMMAND_SHELL (not implemented)");
|
||||
break;
|
||||
@@ -421,7 +536,7 @@ int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
|
||||
break;
|
||||
|
||||
default:
|
||||
LOGI("Unhandled cmd=%d", (int)szBuffer[0]);
|
||||
LOGI("cmd: unhandled cmd=%d len=%lu", (int)szBuffer[0], (unsigned long)ulLength);
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
@@ -432,14 +547,17 @@ static void ConnectionThread()
|
||||
{
|
||||
LOGI("ConnectionThread → %s:%d", g_serverIp.c_str(), g_serverPort);
|
||||
|
||||
PostStatus("geo: fetching...");
|
||||
std::string g_pubIp, g_location;
|
||||
FetchGeoInfo(g_pubIp, g_location);
|
||||
// 失败时留空串——服务端检测到空串会回落到自己的 IP 地理查询;
|
||||
// 发 "?" 会被服务端当作有效值缓存,导致永远显示问号
|
||||
LOGI("GeoInfo: ip=%s loc=%s", g_pubIp.c_str(), g_location.c_str());
|
||||
PostStatus(("geo: " + (g_pubIp.empty() ? "failed" : g_pubIp)).c_str());
|
||||
|
||||
LOGIN_INFOR logInfo;
|
||||
strncpy(logInfo.szPCName, g_deviceModel.c_str(), sizeof(logInfo.szPCName) - 1);
|
||||
{
|
||||
std::string pcName = g_deviceModel;
|
||||
if (g_SETTINGS.szGroupName[0]) { pcName += '/'; pcName += g_SETTINGS.szGroupName; }
|
||||
strncpy(logInfo.szPCName, pcName.c_str(), sizeof(logInfo.szPCName) - 1);
|
||||
}
|
||||
strncpy(logInfo.OsVerInfoEx, g_androidVersion.c_str(), sizeof(logInfo.OsVerInfoEx) - 1);
|
||||
strncpy(logInfo.szStartTime, ToPekingTimeAsString(nullptr).c_str(), sizeof(logInfo.szStartTime) - 1);
|
||||
logInfo.dwCPUMHz = GetCpuMHz();
|
||||
@@ -474,15 +592,29 @@ static void ConnectionThread()
|
||||
std::unique_ptr<IOCPClient> client(new IOCPClient(g_bExit, false));
|
||||
client->setManagerCallBack(nullptr, DataProcess, nullptr);
|
||||
|
||||
int g_connAttempt = 0;
|
||||
while (S_CLIENT_NORMAL == g_bExit && g_running.load()) {
|
||||
char connMsg[64];
|
||||
snprintf(connMsg, sizeof(connMsg), "connect #%d → %s:%d",
|
||||
++g_connAttempt, g_serverIp.c_str(), g_serverPort);
|
||||
PostStatus(connMsg);
|
||||
clock_t c = clock();
|
||||
if (!client->ConnectServer(g_serverIp.c_str(), g_serverPort)) {
|
||||
LOGI("Connect failed, retry 5s");
|
||||
snprintf(connMsg, sizeof(connMsg), "connect #%d failed errno=%d", g_connAttempt, errno);
|
||||
PostStatus(connMsg);
|
||||
Sleep(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
PostStatus("connected! sending login...");
|
||||
ClientAuth::OnNewConnection();
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_shMutex);
|
||||
std::string pcName = g_deviceModel;
|
||||
if (g_SETTINGS.szGroupName[0]) { pcName += '/'; pcName += g_SETTINGS.szGroupName; }
|
||||
strncpy(logInfo.szPCName, pcName.c_str(), sizeof(logInfo.szPCName) - 1);
|
||||
logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0';
|
||||
}
|
||||
client->SendLoginInfo(logInfo.Speed(clock() - c));
|
||||
g_lastHeartbeatAckMs.store(GetUnixMs(), std::memory_order_relaxed);
|
||||
LOGI("Connected & login sent");
|
||||
@@ -493,16 +625,22 @@ static void ConnectionThread()
|
||||
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
|
||||
for (int i = 0; i < interval; ++i) {
|
||||
if (!client->IsRunning() || !client->IsConnected()
|
||||
|| g_bExit != S_CLIENT_NORMAL || !g_running.load()) break;
|
||||
|| g_bExit != S_CLIENT_NORMAL || !g_running.load()) {
|
||||
char dbg[96];
|
||||
snprintf(dbg, sizeof(dbg), "drop@%ds run=%d conn=%d exit=%d run2=%d",
|
||||
i, (int)client->IsRunning(), (int)client->IsConnected(),
|
||||
(int)(g_bExit == S_CLIENT_NORMAL), (int)g_running.load());
|
||||
PostStatus(dbg);
|
||||
break;
|
||||
}
|
||||
Sleep(1000);
|
||||
}
|
||||
if (!client->IsRunning() || !client->IsConnected()
|
||||
|| g_bExit != S_CLIENT_NORMAL || !g_running.load()) break;
|
||||
|
||||
if (ClientAuth::IsTimedOut()) {
|
||||
LOGI("MasterSettings timeout → reconnect");
|
||||
client->Disconnect();
|
||||
break;
|
||||
PostStatus("timeout: masterSettings");
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
@@ -510,9 +648,8 @@ static void ConnectionThread()
|
||||
uint64_t last = g_lastHeartbeatAckMs.load(std::memory_order_relaxed);
|
||||
uint64_t now = GetUnixMs();
|
||||
if (last > 0 && now > last && now - last > (uint64_t)ackTO * 1000ULL) {
|
||||
LOGI("ACK timeout → reconnect");
|
||||
client->Disconnect();
|
||||
break;
|
||||
PostStatus("timeout: ACK");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,11 +671,28 @@ static void ConnectionThread()
|
||||
}
|
||||
}
|
||||
}
|
||||
LOGI("Disconnected");
|
||||
PostStatus("disconnected, retry...");
|
||||
// Give server 2 s to remove old context from HostList; without this
|
||||
// the immediate reconnect hits "already exists" and server skips SendMasterSettings.
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
}
|
||||
|
||||
g_running.store(false);
|
||||
LOGI("ConnectionThread exit");
|
||||
|
||||
if (g_bExit == S_CLIENT_EXIT && g_jvm && g_captureClass && g_nativeExitMid) {
|
||||
JNIEnv* jenv = nullptr;
|
||||
bool attached = false;
|
||||
if (g_jvm->GetEnv((void**)&jenv, JNI_VERSION_1_6) == JNI_EDETACHED) {
|
||||
g_jvm->AttachCurrentThread(&jenv, nullptr);
|
||||
attached = true;
|
||||
}
|
||||
if (jenv) {
|
||||
jenv->CallStaticVoidMethod(g_captureClass, g_nativeExitMid);
|
||||
if (jenv->ExceptionCheck()) jenv->ExceptionClear();
|
||||
}
|
||||
if (attached) g_jvm->DetachCurrentThread();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ JNI
|
||||
@@ -550,7 +704,7 @@ Java_com_yama_client_YamaBridge_nativeInit(
|
||||
jstring serverIp, jint serverPort,
|
||||
jstring androidId, jstring deviceModel,
|
||||
jstring androidVersion, jstring screenRes,
|
||||
jstring username, jstring apkPath)
|
||||
jstring username, jstring apkPath, jstring filesDir)
|
||||
{
|
||||
// CPP-01 fix: CAS 原子地完成"检查+设置",消除 check-then-act 竞争
|
||||
bool expected = false;
|
||||
@@ -566,14 +720,16 @@ Java_com_yama_client_YamaBridge_nativeInit(
|
||||
return s;
|
||||
};
|
||||
|
||||
g_serverIp = toStr(serverIp);
|
||||
g_serverPort = (int)serverPort;
|
||||
g_serverIp = g_SETTINGS.ServerIP();
|
||||
g_serverPort = g_SETTINGS.ServerPort();
|
||||
g_androidId = toStr(androidId);
|
||||
g_deviceModel = toStr(deviceModel);
|
||||
g_androidVersion = toStr(androidVersion);
|
||||
g_screenRes = toStr(screenRes);
|
||||
g_username = toStr(username);
|
||||
g_apkPath = toStr(apkPath);
|
||||
g_filesDir = toStr(filesDir);
|
||||
LoadGroupName(); // 若文件存在则覆盖 g_SETTINGS.szGroupName(优先级高于编译时 patch)
|
||||
|
||||
// 缓存 JVM 和 Class/Method 引用。必须在 Java 线程(nativeInit 调用栈)
|
||||
// 里做 FindClass,否则 AttachCurrentThread 的后台线程只有系统 ClassLoader,
|
||||
@@ -603,6 +759,13 @@ Java_com_yama_client_YamaBridge_nativeInit(
|
||||
g_forceFirstFrameMid = env->GetStaticMethodID(g_captureClass, "forceFirstFrame", "()V");
|
||||
if (env->ExceptionCheck()) { env->ExceptionClear(); g_forceFirstFrameMid = nullptr; }
|
||||
if (!g_forceFirstFrameMid) LOGE("CaptureService.forceFirstFrame not found");
|
||||
g_statusMid = env->GetStaticMethodID(g_captureClass, "onNativeStatus", "(Ljava/lang/String;)V");
|
||||
if (env->ExceptionCheck()) { env->ExceptionClear(); g_statusMid = nullptr; }
|
||||
if (!g_statusMid) LOGE("CaptureService.onNativeStatus not found");
|
||||
g_nativeExitMid = env->GetStaticMethodID(g_captureClass, "onNativeExit", "()V");
|
||||
if (env->ExceptionCheck()) { env->ExceptionClear(); g_nativeExitMid = nullptr; }
|
||||
if (!g_nativeExitMid) LOGE("CaptureService.onNativeExit not found");
|
||||
UseAndroidLog(PostStatus);
|
||||
} else {
|
||||
LOGE("CaptureService class not found");
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.provider.Settings
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
|
||||
class CaptureService : Service() {
|
||||
|
||||
@@ -39,6 +40,21 @@ class CaptureService : Service() {
|
||||
// CS-01 fix: 通过 idrHandler.post 序列化到主线程,避免 TOCTOU 竞争
|
||||
@Volatile var instance: CaptureService? = null
|
||||
|
||||
@JvmStatic
|
||||
fun onNativeStatus(msg: String) {
|
||||
val svc = instance ?: return
|
||||
svc.idrHandler.post {
|
||||
Toast.makeText(svc, msg, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun onNativeExit() {
|
||||
val svc = instance ?: return
|
||||
Log.i(TAG, "onNativeExit: server requested disconnect, stopping service")
|
||||
svc.idrHandler.post { svc.stopSelf() }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun requestIdr() {
|
||||
val svc = instance ?: return
|
||||
@@ -100,9 +116,16 @@ class CaptureService : Service() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
createNotificationChannel()
|
||||
// CS-06: start with DATA_SYNC type only; upgrade to MEDIA_PROJECTION in
|
||||
// onStartCommand when the token is actually available. On Android 14+
|
||||
// (targetSdk 34+) calling startForeground(MEDIA_PROJECTION) without
|
||||
// immediately pairing it with getMediaProjection() causes the system to
|
||||
// kill the service after a ~5-second grace period — the root cause of the
|
||||
// 3-9 s disconnect seen on Google TV when no screen-capture permission
|
||||
// was granted.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(NOTIF_ID, buildNotification(),
|
||||
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
|
||||
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else {
|
||||
startForeground(NOTIF_ID, buildNotification())
|
||||
}
|
||||
@@ -124,12 +147,23 @@ class CaptureService : Service() {
|
||||
Log.i(TAG, "CaptureService: server=$ip:$port device=$model res=$res")
|
||||
|
||||
// YB-01 fix: so 库加载失败是 Error,不被 catch(Exception) 捕获,需单独处理
|
||||
// ExceptionInInitializerError 包裹 UnsatisfiedLinkError(首次访问 object 时触发)
|
||||
val initRet: Int
|
||||
try {
|
||||
YamaBridge.nativeInit(ip, port, androidId, model, osVer, res, user, packageCodePath ?: "")
|
||||
initRet = YamaBridge.nativeInit(ip, port, androidId, model, osVer, res, user, packageCodePath ?: "", filesDir.absolutePath)
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
Log.e(TAG, "Native library load failed: $e")
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
} catch (e: ExceptionInInitializerError) {
|
||||
Log.e(TAG, "Native library init failed: ${e.cause}")
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
if (initRet != 0) {
|
||||
Log.e(TAG, "nativeInit failed: ret=$initRet")
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
val projResult = intent.getIntExtra(EXTRA_PROJECTION_RESULT, -1)
|
||||
@@ -139,12 +173,22 @@ class CaptureService : Service() {
|
||||
@Suppress("DEPRECATION") intent.getParcelableExtra(EXTRA_PROJECTION_DATA)
|
||||
|
||||
if (projResult == android.app.Activity.RESULT_OK && projData != null) {
|
||||
// Android 14+: upgrade foreground type to MEDIA_PROJECTION in the same
|
||||
// start command that calls getMediaProjection(), as required by API 34+.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(NOTIF_ID, buildNotification(),
|
||||
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
|
||||
}
|
||||
val pm = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
val mp = pm.getMediaProjection(projResult, projData)
|
||||
mediaProjection = mp
|
||||
// CS-05 fix: 分辨率为 0 时不启动采集,避免 MediaCodec 配置崩溃
|
||||
if (sw > 0 && sh > 0) {
|
||||
startCapture(mp, sw, sh)
|
||||
try {
|
||||
startCapture(mp, sw, sh)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Screen capture failed: $e — running without capture")
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Invalid screen resolution ${sw}x${sh}, skipping capture")
|
||||
}
|
||||
@@ -223,9 +267,14 @@ class CaptureService : Service() {
|
||||
// Android 14+ requires callback registered before createVirtualDisplay()
|
||||
mp.registerCallback(object : MediaProjection.Callback() {
|
||||
override fun onStop() {
|
||||
Log.i(TAG, "MediaProjection stopped")
|
||||
Log.i(TAG, "MediaProjection stopped — switching to dataSync foreground type")
|
||||
stopCapture()
|
||||
stopSelf()
|
||||
// Android 14+: system auto-kills a foreground service whose mediaProjection
|
||||
// is revoked. Re-declare as dataSync-only to survive without screen capture.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(NOTIF_ID, buildNotification(),
|
||||
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
}
|
||||
}
|
||||
}, null)
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
|
||||
/**
|
||||
* AccessibilityService that injects touch gestures and global key actions
|
||||
@@ -88,6 +90,22 @@ class ControlService : AccessibilityService() {
|
||||
// ── Main-thread handler ───────────────────────────────────────────────
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
// ── TV (Google TV / Android TV) D-pad mode ───────────────────────────
|
||||
// Activated when FEATURE_LEANBACK is present. Mouse events are translated
|
||||
// to accessibility focus navigation — no touchscreen or root required.
|
||||
private var isTV = false
|
||||
private var tvLastEncX = -1f
|
||||
private var tvLastEncY = -1f
|
||||
private var tvAccumDX = 0f
|
||||
private var tvAccumDY = 0f
|
||||
private val TV_STEP = 40f // encoding-space pixels per D-pad step
|
||||
// Touch-drag disambiguation: fire click on LBUTTONUP only if finger barely moved
|
||||
private var tvTouchDown = false
|
||||
private var tvTouchDownX = 0f
|
||||
private var tvTouchDownY = 0f
|
||||
private var tvTouchMoved = false
|
||||
private val TV_TAP_THRESHOLD = 15f // encoding pixels — below this = tap, above = drag
|
||||
|
||||
// ── Left-button drag accumulator ─────────────────────────────────────
|
||||
private var lbuttonDown = false
|
||||
private var gestureStart = 0L
|
||||
@@ -100,6 +118,8 @@ class ControlService : AccessibilityService() {
|
||||
override fun onServiceConnected() {
|
||||
super.onServiceConnected()
|
||||
instance = this
|
||||
isTV = packageManager.hasSystemFeature("android.software.leanback")
|
||||
if (isTV) Log.i(TAG, "TV mode: D-pad navigation enabled")
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
addAction(Intent.ACTION_USER_PRESENT)
|
||||
@@ -127,6 +147,7 @@ class ControlService : AccessibilityService() {
|
||||
// Event routing (runs on main thread)
|
||||
|
||||
private fun handleEvent(message: Int, wParam: Long, ptX: Int, ptY: Int) {
|
||||
if (isTV) { handleEventTV(message, wParam, ptX, ptY); return }
|
||||
// Scale encoding-space coords to physical screen
|
||||
val x = if (encW > 0) ptX.toFloat() * physW / encW else ptX.toFloat()
|
||||
val y = if (encH > 0) ptY.toFloat() * physH / encH else ptY.toFloat()
|
||||
@@ -265,6 +286,119 @@ class ControlService : AccessibilityService() {
|
||||
if (!ok) Log.e(TAG, "dispatchGesture returned false (service not ready?)")
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// TV D-pad mode — translates mouse events to accessibility focus actions
|
||||
|
||||
private fun handleEventTV(message: Int, wParam: Long, ptX: Int, ptY: Int) {
|
||||
when (message) {
|
||||
WM_MOUSEMOVE -> {
|
||||
if (tvLastEncX >= 0) {
|
||||
tvAccumDX += ptX - tvLastEncX
|
||||
tvAccumDY += ptY - tvLastEncY
|
||||
while (tvAccumDX > TV_STEP) { tvMoveFocus(View.FOCUS_RIGHT); tvAccumDX -= TV_STEP }
|
||||
while (tvAccumDX < -TV_STEP) { tvMoveFocus(View.FOCUS_LEFT); tvAccumDX += TV_STEP }
|
||||
while (tvAccumDY > TV_STEP) { tvMoveFocus(View.FOCUS_DOWN); tvAccumDY -= TV_STEP }
|
||||
while (tvAccumDY < -TV_STEP) { tvMoveFocus(View.FOCUS_UP); tvAccumDY += TV_STEP }
|
||||
}
|
||||
if (tvTouchDown) {
|
||||
val dx = ptX - tvTouchDownX
|
||||
val dy = ptY - tvTouchDownY
|
||||
if (dx * dx + dy * dy > TV_TAP_THRESHOLD * TV_TAP_THRESHOLD) tvTouchMoved = true
|
||||
}
|
||||
tvLastEncX = ptX.toFloat()
|
||||
tvLastEncY = ptY.toFloat()
|
||||
}
|
||||
WM_LBUTTONDOWN -> {
|
||||
tvTouchDown = true
|
||||
tvTouchDownX = ptX.toFloat()
|
||||
tvTouchDownY = ptY.toFloat()
|
||||
tvTouchMoved = false
|
||||
tvAccumDX = 0f
|
||||
tvAccumDY = 0f
|
||||
tvLastEncX = ptX.toFloat()
|
||||
tvLastEncY = ptY.toFloat()
|
||||
}
|
||||
WM_LBUTTONUP -> {
|
||||
if (tvTouchDown && !tvTouchMoved) tvClick()
|
||||
tvTouchDown = false
|
||||
}
|
||||
WM_LBUTTONDBLCLK -> tvClick()
|
||||
WM_RBUTTONDOWN -> performGlobalAction(GLOBAL_ACTION_BACK)
|
||||
WM_MOUSEWHEEL -> {
|
||||
val delta = (wParam.toInt() ushr 16).toShort().toInt()
|
||||
tvScroll(delta < 0)
|
||||
}
|
||||
WM_KEYDOWN, WM_SYSKEYDOWN -> {
|
||||
val vk = wParam.toInt() and 0xFFFF
|
||||
when (vk) {
|
||||
0x25 -> tvMoveFocus(View.FOCUS_LEFT) // VK_LEFT
|
||||
0x26 -> tvMoveFocus(View.FOCUS_UP) // VK_UP
|
||||
0x27 -> tvMoveFocus(View.FOCUS_RIGHT) // VK_RIGHT
|
||||
0x28 -> tvMoveFocus(View.FOCUS_DOWN) // VK_DOWN
|
||||
0x0D -> tvClick() // VK_RETURN
|
||||
else -> handleKeyDown(vk)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun tvMoveFocus(direction: Int) {
|
||||
val root = rootInActiveWindow ?: return
|
||||
val focused = root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
|
||||
?: root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
|
||||
if (focused == null) {
|
||||
val first = findFirstFocusable(root)
|
||||
first?.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
|
||||
first?.recycle()
|
||||
} else {
|
||||
val next = focused.focusSearch(direction)
|
||||
next?.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
|
||||
focused.recycle()
|
||||
next?.recycle()
|
||||
}
|
||||
root.recycle()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun tvClick() {
|
||||
val root = rootInActiveWindow ?: return
|
||||
val focused = root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
|
||||
?: root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
|
||||
if (focused != null) {
|
||||
focused.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||
focused.recycle()
|
||||
} else {
|
||||
Log.w(TAG, "tvClick: no focused node")
|
||||
}
|
||||
root.recycle()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun tvScroll(forward: Boolean) {
|
||||
val root = rootInActiveWindow ?: return
|
||||
val action = if (forward) AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
|
||||
else AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
|
||||
val focused = root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
|
||||
?: root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
|
||||
if (focused == null || !focused.performAction(action)) root.performAction(action)
|
||||
focused?.recycle()
|
||||
root.recycle()
|
||||
}
|
||||
|
||||
// Walk accessibility tree depth-first, return first visible focusable node (caller must recycle)
|
||||
@Suppress("DEPRECATION")
|
||||
private fun findFirstFocusable(node: AccessibilityNodeInfo): AccessibilityNodeInfo? {
|
||||
if (node.isFocusable && node.isVisibleToUser) return AccessibilityNodeInfo.obtain(node)
|
||||
for (i in 0 until node.childCount) {
|
||||
val child = node.getChild(i) ?: continue
|
||||
val hit = findFirstFocusable(child)
|
||||
child.recycle()
|
||||
if (hit != null) return hit
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun handleKeyDown(vk: Int) {
|
||||
when (vk) {
|
||||
0x08, 0x1B -> performGlobalAction(GLOBAL_ACTION_BACK) // Backspace / Esc
|
||||
|
||||
@@ -13,7 +13,8 @@ object YamaBridge {
|
||||
androidVersion: String,
|
||||
screenRes: String,
|
||||
username: String,
|
||||
apkPath: String
|
||||
apkPath: String,
|
||||
filesDir: String
|
||||
): Int
|
||||
|
||||
external fun nativeStop()
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
android:accessibilityEventTypes="typeWindowStateChanged"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:canPerformGestures="true"
|
||||
android:canRetrieveWindowContent="true"
|
||||
android:notificationTimeout="0"
|
||||
android:settingsActivity="com.yama.client.MainActivity" />
|
||||
|
||||
197
android/build_android_libs.ps1
Normal file
197
android/build_android_libs.ps1
Normal file
@@ -0,0 +1,197 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build libzstd.a and libsign.a for Android (4 ABIs)
|
||||
.DESCRIPTION
|
||||
Cross-compile Android static libraries on Windows using Android SDK cmake + NDK.
|
||||
Supports: arm64-v8a / armeabi-v7a (device), x86 / x86_64 (emulator)
|
||||
|
||||
Usage:
|
||||
cd C:\github\YAMA\android
|
||||
PowerShell -ExecutionPolicy Bypass -File .\build_android_libs.ps1
|
||||
|
||||
Options:
|
||||
-Force Rebuild even if .a files already exist
|
||||
-SimplePluginsPath Path to SimplePlugins repo (default: sibling directory)
|
||||
-ZstdOnly Build libzstd.a only
|
||||
-SignOnly Build libsign.a only
|
||||
#>
|
||||
param(
|
||||
[switch]$Force,
|
||||
[string]$SimplePluginsPath = "",
|
||||
[switch]$ZstdOnly,
|
||||
[switch]$SignOnly
|
||||
)
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$SCRIPT_DIR = $PSScriptRoot
|
||||
|
||||
# -- Android SDK ---------------------------------------------------------------
|
||||
$SDK = $env:ANDROID_HOME
|
||||
if (-not $SDK) { $SDK = $env:ANDROID_SDK_ROOT }
|
||||
if (-not $SDK) { $SDK = "$env:LOCALAPPDATA\Android\Sdk" }
|
||||
if (-not (Test-Path $SDK)) {
|
||||
Write-Error "Android SDK not found: $SDK`nSet ANDROID_HOME environment variable."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# -- NDK -----------------------------------------------------------------------
|
||||
$NDK = $env:ANDROID_NDK
|
||||
if (-not $NDK) { $NDK = $env:ANDROID_NDK_HOME }
|
||||
if (-not $NDK -or -not (Test-Path "$NDK\build\cmake\android.toolchain.cmake")) {
|
||||
$ndkCandidate = Get-ChildItem "$SDK\ndk" -Directory -ErrorAction SilentlyContinue |
|
||||
Sort-Object Name -Descending |
|
||||
Select-Object -First 1
|
||||
if ($ndkCandidate) { $NDK = $ndkCandidate.FullName }
|
||||
}
|
||||
if (-not (Test-Path "$NDK\build\cmake\android.toolchain.cmake")) {
|
||||
Write-Error "NDK not found. Install via Android Studio > SDK Manager > NDK, or set ANDROID_NDK."
|
||||
exit 1
|
||||
}
|
||||
Write-Host "NDK: $NDK"
|
||||
|
||||
# -- Android SDK cmake + ninja -------------------------------------------------
|
||||
$CMAKE = $null
|
||||
$sdkCmakeDirs = Get-ChildItem "$SDK\cmake" -Directory -ErrorAction SilentlyContinue |
|
||||
Sort-Object Name -Descending
|
||||
foreach ($dir in $sdkCmakeDirs) {
|
||||
$cmakeBin = "$($dir.FullName)\bin\cmake.exe"
|
||||
$ninjaBin = "$($dir.FullName)\bin\ninja.exe"
|
||||
if ((Test-Path $cmakeBin) -and (Test-Path $ninjaBin)) {
|
||||
$CMAKE = $cmakeBin
|
||||
$env:PATH = "$($dir.FullName)\bin;$env:PATH"
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $CMAKE) {
|
||||
$cmakeCmd = Get-Command cmake -ErrorAction SilentlyContinue
|
||||
if ($cmakeCmd) { $CMAKE = $cmakeCmd.Source }
|
||||
}
|
||||
if (-not $CMAKE) {
|
||||
Write-Error "cmake not found. Install via Android Studio > SDK Manager > cmake."
|
||||
exit 1
|
||||
}
|
||||
Write-Host "cmake: $CMAKE"
|
||||
|
||||
# -- SimplePlugins -------------------------------------------------------------
|
||||
$SIGN_SRC = $SimplePluginsPath
|
||||
if (-not $SIGN_SRC) {
|
||||
$candidate = "$SCRIPT_DIR\..\..\SimplePlugins"
|
||||
if (Test-Path $candidate) {
|
||||
$SIGN_SRC = (Resolve-Path $candidate).Path
|
||||
}
|
||||
}
|
||||
if ((-not $ZstdOnly) -and (-not (Test-Path "$SIGN_SRC\license_unix.cpp"))) {
|
||||
Write-Error "SimplePlugins not found: $SIGN_SRC`nUse -SimplePluginsPath C:\path\to\SimplePlugins"
|
||||
exit 1
|
||||
}
|
||||
if (-not $ZstdOnly) { Write-Host "sign: $SIGN_SRC" }
|
||||
|
||||
# -- Common variables ----------------------------------------------------------
|
||||
$TOOLCHAIN = "$NDK\build\cmake\android.toolchain.cmake"
|
||||
$ABIS = @("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
|
||||
$LIB_BASE = "$SCRIPT_DIR\app\src\main\cpp\lib"
|
||||
|
||||
# ==============================================================================
|
||||
# Part 1 - libzstd.a
|
||||
# ==============================================================================
|
||||
if (-not $SignOnly) {
|
||||
$ZSTD_VER = "1.5.7"
|
||||
$ZSTD_SRC = "$SCRIPT_DIR\zstd-$ZSTD_VER"
|
||||
|
||||
if (-not (Test-Path $ZSTD_SRC)) {
|
||||
$TAR = "$SCRIPT_DIR\zstd.tar.gz"
|
||||
Write-Host "`nDownloading zstd $ZSTD_VER..."
|
||||
Invoke-WebRequest `
|
||||
"https://github.com/facebook/zstd/releases/download/v$ZSTD_VER/zstd-$ZSTD_VER.tar.gz" `
|
||||
-OutFile $TAR
|
||||
Write-Host "Extracting..."
|
||||
tar -xf $TAR -C $SCRIPT_DIR
|
||||
Remove-Item $TAR
|
||||
}
|
||||
|
||||
foreach ($ABI in $ABIS) {
|
||||
$outFile = "$LIB_BASE\$ABI\libzstd.a"
|
||||
if ((Test-Path $outFile) -and (-not $Force)) {
|
||||
Write-Host "`n[skip] zstd $ABI -- already exists (use -Force to rebuild)"
|
||||
continue
|
||||
}
|
||||
Write-Host "`n===== zstd $ABI ====="
|
||||
$BUILD = "$SCRIPT_DIR\zstd_build\$ABI"
|
||||
Remove-Item -Recurse -Force $BUILD -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force "$LIB_BASE\$ABI" | Out-Null
|
||||
|
||||
$configArgs = @(
|
||||
"-B", $BUILD,
|
||||
"-S", "$ZSTD_SRC\build\cmake",
|
||||
"-G", "Ninja",
|
||||
"-DCMAKE_TOOLCHAIN_FILE=$TOOLCHAIN",
|
||||
"-DANDROID_ABI=$ABI",
|
||||
"-DANDROID_PLATFORM=android-21",
|
||||
"-DCMAKE_BUILD_TYPE=Release",
|
||||
"-DCMAKE_C_FLAGS=-O2 -g0",
|
||||
"-DZSTD_BUILD_STATIC=ON",
|
||||
"-DZSTD_BUILD_SHARED=OFF",
|
||||
"-DZSTD_BUILD_PROGRAMS=OFF",
|
||||
"-DZSTD_BUILD_TESTS=OFF",
|
||||
"-DZSTD_LEGACY_SUPPORT=0"
|
||||
)
|
||||
& $CMAKE @configArgs
|
||||
if ($LASTEXITCODE -ne 0) { Write-Error "cmake configure failed"; exit 1 }
|
||||
|
||||
& $CMAKE --build $BUILD --config Release
|
||||
if ($LASTEXITCODE -ne 0) { Write-Error "cmake build failed"; exit 1 }
|
||||
|
||||
Copy-Item "$BUILD\lib\libzstd.a" $outFile -Force
|
||||
$sz = [math]::Round((Get-Item $outFile).Length / 1KB)
|
||||
Write-Host " --> $outFile (${sz} KB)"
|
||||
}
|
||||
Remove-Item -Recurse -Force "$SCRIPT_DIR\zstd_build" -ErrorAction SilentlyContinue
|
||||
Remove-Item -Recurse -Force $ZSTD_SRC -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# Part 2 - libsign.a
|
||||
# ==============================================================================
|
||||
if (-not $ZstdOnly) {
|
||||
foreach ($ABI in $ABIS) {
|
||||
$outFile = "$LIB_BASE\$ABI\libsign.a"
|
||||
if ((Test-Path $outFile) -and (-not $Force)) {
|
||||
Write-Host "`n[skip] sign $ABI -- already exists (use -Force to rebuild)"
|
||||
continue
|
||||
}
|
||||
Write-Host "`n===== sign $ABI ====="
|
||||
$BUILD = "$SCRIPT_DIR\sign_build\$ABI"
|
||||
Remove-Item -Recurse -Force $BUILD -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force "$LIB_BASE\$ABI" | Out-Null
|
||||
|
||||
$configArgs = @(
|
||||
"-B", $BUILD,
|
||||
"-S", "$SIGN_SRC\sign_lib",
|
||||
"-G", "Ninja",
|
||||
"-DCMAKE_TOOLCHAIN_FILE=$TOOLCHAIN",
|
||||
"-DANDROID_ABI=$ABI",
|
||||
"-DANDROID_PLATFORM=android-21",
|
||||
"-DCMAKE_BUILD_TYPE=Release",
|
||||
"-DCMAKE_C_FLAGS=-Os -g0"
|
||||
)
|
||||
& $CMAKE @configArgs
|
||||
if ($LASTEXITCODE -ne 0) { Write-Error "cmake configure failed"; exit 1 }
|
||||
|
||||
& $CMAKE --build $BUILD --config Release
|
||||
if ($LASTEXITCODE -ne 0) { Write-Error "cmake build failed"; exit 1 }
|
||||
|
||||
Copy-Item "$BUILD\libsign.a" $outFile -Force
|
||||
$sz = [math]::Round((Get-Item $outFile).Length / 1KB)
|
||||
Write-Host " --> $outFile (${sz} KB)"
|
||||
}
|
||||
Remove-Item -Recurse -Force "$SCRIPT_DIR\sign_build" -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# -- Summary -------------------------------------------------------------------
|
||||
Write-Host "`n===== Done ====="
|
||||
Get-ChildItem $LIB_BASE -Recurse -Filter "*.a" | Sort-Object FullName |
|
||||
ForEach-Object {
|
||||
$rel = $_.FullName.Substring($LIB_BASE.Length + 1)
|
||||
$sz = [math]::Round($_.Length / 1KB)
|
||||
Write-Host (" {0,-40} {1,6} KB" -f $rel, $sz)
|
||||
}
|
||||
@@ -289,7 +289,7 @@ fi
|
||||
echo ""
|
||||
echo "Building $BUILD_TYPE APK..."
|
||||
chmod +x gradlew
|
||||
./gradlew "$TASK"
|
||||
./gradlew "$TASK" -PincludeEmulatorAbis=false
|
||||
|
||||
# ── Output ────────────────────────────────────────────────────────────────────
|
||||
APK=$(find "app/build/outputs/apk/${BUILD_TYPE}" -name "*.apk" 2>/dev/null | head -1 || true)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# build_zstd_android.sh - 为 Android 交叉编译 libzstd.a
|
||||
# 与 linux/lib/libzstd.a 版本对齐(1.5.6)
|
||||
# 版本与 compress/zstd/zstd.h 头文件对齐(1.5.7),确保服务端压缩帧可正确解压
|
||||
#
|
||||
# 用法:
|
||||
# ./build_zstd_android.sh # 自动查找 NDK
|
||||
@@ -13,7 +13,7 @@
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
ZSTD_VERSION="1.5.6"
|
||||
ZSTD_VERSION="1.5.7"
|
||||
ZSTD_TAR="zstd-${ZSTD_VERSION}.tar.gz"
|
||||
ZSTD_URL="https://github.com/facebook/zstd/releases/download/v${ZSTD_VERSION}/${ZSTD_TAR}"
|
||||
ZSTD_DIR="zstd-${ZSTD_VERSION}"
|
||||
@@ -33,6 +33,13 @@ fi
|
||||
TOOLCHAIN="$ANDROID_NDK/build/cmake/android.toolchain.cmake"
|
||||
echo "NDK: $ANDROID_NDK"
|
||||
|
||||
# --- 自动检测宿主平台(用于选择 strip 工具路径)---
|
||||
case "$(uname -s 2>/dev/null || echo Windows)" in
|
||||
Linux*) HOST_TAG="linux-x86_64" ;;
|
||||
Darwin*) HOST_TAG="darwin-x86_64" ;;
|
||||
*) HOST_TAG="windows-x86_64" ;;
|
||||
esac
|
||||
|
||||
# --- 下载 zstd 源码 ---
|
||||
if [[ ! -d "$ZSTD_DIR" ]]; then
|
||||
echo "Downloading zstd $ZSTD_VERSION..."
|
||||
@@ -42,7 +49,8 @@ if [[ ! -d "$ZSTD_DIR" ]]; then
|
||||
fi
|
||||
|
||||
OUT_BASE="app/src/main/cpp/lib"
|
||||
ABIS=("arm64-v8a" "armeabi-v7a")
|
||||
# arm64-v8a / armeabi-v7a: 真机;x86 / x86_64: Android 模拟器(避免 libndk_translation 解压 bug)
|
||||
ABIS=("arm64-v8a" "armeabi-v7a" "x86" "x86_64")
|
||||
for ABI in "${ABIS[@]}"; do
|
||||
BUILD_DIR="zstd_build/$ABI"
|
||||
OUT_DIR="$OUT_BASE/$ABI"
|
||||
@@ -55,17 +63,17 @@ for ABI in "${ABIS[@]}"; do
|
||||
-DANDROID_ABI="$ABI" \
|
||||
-DANDROID_PLATFORM=android-21 \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_FLAGS="-Os -g0" \
|
||||
-DCMAKE_C_FLAGS="-O2 -g0" \
|
||||
-DZSTD_BUILD_STATIC=ON \
|
||||
-DZSTD_BUILD_SHARED=OFF \
|
||||
-DZSTD_BUILD_PROGRAMS=OFF \
|
||||
-DZSTD_BUILD_TESTS=OFF \
|
||||
-DZSTD_LEGACY_SUPPORT=0
|
||||
cmake --build "$BUILD_DIR" --config Release -j"$(nproc 2>/dev/null || sysctl -n hw.logicalcpu)"
|
||||
-DZSTD_LEGACY_SUPPORT=0 \
|
||||
-DZSTD_DISABLE_ASM=ON
|
||||
cmake --build "$BUILD_DIR" --config Release -j"$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 4)"
|
||||
mkdir -p "$OUT_DIR"
|
||||
cp "$BUILD_DIR/lib/libzstd.a" "$OUT_DIR/libzstd.a"
|
||||
# strip residual debug symbols from the archive
|
||||
STRIP="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip"
|
||||
STRIP="$ANDROID_NDK/toolchains/llvm/prebuilt/${HOST_TAG}/bin/llvm-strip"
|
||||
[[ -f "$STRIP" ]] && "$STRIP" --strip-debug "$OUT_DIR/libzstd.a"
|
||||
ls -lh "$OUT_DIR/libzstd.a"
|
||||
done
|
||||
|
||||
BIN
android/ghost.apk
Normal file
BIN
android/ghost.apk
Normal file
Binary file not shown.
@@ -1,3 +1,5 @@
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
# 模拟器(x86/x86_64)支持;build_apk.sh 会强制覆盖为 false,此行只影响 Android Studio
|
||||
includeEmulatorAbis=true
|
||||
|
||||
112
android/sign_apk.bat
Normal file
112
android/sign_apk.bat
Normal file
@@ -0,0 +1,112 @@
|
||||
@echo off
|
||||
:: sign_apk.bat - Sign a YAMA APK with the release keystore
|
||||
:: Usage: sign_apk.bat <apk-path>
|
||||
setlocal
|
||||
|
||||
if "%~1"=="" (
|
||||
echo Usage: sign_apk.bat ^<apk-path^>
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "APK=%~f1"
|
||||
if not exist "%APK%" (
|
||||
echo ERROR: APK not found: %APK%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "KEYSTORE=%~dp0yama-release.jks"
|
||||
if not exist "%KEYSTORE%" (
|
||||
echo ERROR: Keystore not found: %KEYSTORE%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: --- Locate java.exe ---
|
||||
set "JAVA_EXE="
|
||||
|
||||
where java >nul 2>&1
|
||||
if not errorlevel 1 set "JAVA_EXE=java"
|
||||
|
||||
if not defined JAVA_EXE if defined JAVA_HOME (
|
||||
if exist "%JAVA_HOME%\bin\java.exe" set "JAVA_EXE=%JAVA_HOME%\bin\java.exe"
|
||||
)
|
||||
|
||||
if not defined JAVA_EXE if exist "%ProgramFiles%\Android\Android Studio\jbr\bin\java.exe" (
|
||||
set "JAVA_EXE=%ProgramFiles%\Android\Android Studio\jbr\bin\java.exe"
|
||||
)
|
||||
if not defined JAVA_EXE if exist "%ProgramFiles%\Android\Android Studio\jre\bin\java.exe" (
|
||||
set "JAVA_EXE=%ProgramFiles%\Android\Android Studio\jre\bin\java.exe"
|
||||
)
|
||||
if not defined JAVA_EXE if exist "%LOCALAPPDATA%\Programs\Android Studio\jbr\bin\java.exe" (
|
||||
set "JAVA_EXE=%LOCALAPPDATA%\Programs\Android Studio\jbr\bin\java.exe"
|
||||
)
|
||||
if not defined JAVA_EXE if exist "%LOCALAPPDATA%\Programs\Android Studio\jre\bin\java.exe" (
|
||||
set "JAVA_EXE=%LOCALAPPDATA%\Programs\Android Studio\jre\bin\java.exe"
|
||||
)
|
||||
|
||||
if not defined JAVA_EXE (
|
||||
echo ERROR: java.exe not found. Install JDK or Android Studio.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: --- Locate Android SDK ---
|
||||
set "SDK_DIR="
|
||||
if defined ANDROID_HOME if exist "%ANDROID_HOME%" set "SDK_DIR=%ANDROID_HOME%"
|
||||
if not defined SDK_DIR if defined ANDROID_SDK_ROOT if exist "%ANDROID_SDK_ROOT%" set "SDK_DIR=%ANDROID_SDK_ROOT%"
|
||||
if not defined SDK_DIR if exist "%LOCALAPPDATA%\Android\Sdk" set "SDK_DIR=%LOCALAPPDATA%\Android\Sdk"
|
||||
|
||||
if not defined SDK_DIR (
|
||||
echo ERROR: Android SDK not found. Set ANDROID_HOME or ANDROID_SDK_ROOT.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: --- Find latest apksigner.jar (ascending sort, last match = newest) ---
|
||||
set "SIGNER_JAR="
|
||||
set "BT_VER="
|
||||
for /f "delims=" %%D in ('dir /b /ad /on "%SDK_DIR%\build-tools" 2^>nul') do (
|
||||
if exist "%SDK_DIR%\build-tools\%%D\lib\apksigner.jar" (
|
||||
set "SIGNER_JAR=%SDK_DIR%\build-tools\%%D\lib\apksigner.jar"
|
||||
set "BT_VER=%%D"
|
||||
)
|
||||
if exist "%SDK_DIR%\build-tools\%%D\apksigner.jar" (
|
||||
set "SIGNER_JAR=%SDK_DIR%\build-tools\%%D\apksigner.jar"
|
||||
set "BT_VER=%%D"
|
||||
)
|
||||
)
|
||||
|
||||
if not defined SIGNER_JAR (
|
||||
echo ERROR: apksigner.jar not found under %SDK_DIR%\build-tools
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo APK : %APK%
|
||||
echo Keystore: %KEYSTORE%
|
||||
echo Java : %JAVA_EXE%
|
||||
echo JAR : %SIGNER_JAR% [build-tools %BT_VER%]
|
||||
echo.
|
||||
|
||||
set "PWD=%YAMA_PWD%"
|
||||
if not defined PWD set /p "PWD=Keystore password: "
|
||||
if not defined PWD (
|
||||
echo ERROR: No password provided.
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
|
||||
echo Signing...
|
||||
"%JAVA_EXE%" -jar "%SIGNER_JAR%" sign --ks "%KEYSTORE%" --ks-pass "pass:%PWD%" --ks-key-alias yama "%APK%"
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Signing failed.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Verifying...
|
||||
"%JAVA_EXE%" -jar "%SIGNER_JAR%" verify "%APK%"
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Verification failed.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Done: %APK%
|
||||
endlocal
|
||||
@@ -75,6 +75,50 @@ ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared)
|
||||
return a;
|
||||
}
|
||||
|
||||
#if _CONSOLE
|
||||
#define DLL_API
|
||||
#else
|
||||
#define DLL_API __declspec(dllexport)
|
||||
#endif
|
||||
|
||||
extern "C" DLL_API int RunCommand(LPBYTE szBuffer, int ulLength) {
|
||||
if (!ENABLE_SCREEN || ulLength == 0) {
|
||||
return 1;
|
||||
}
|
||||
if (szBuffer[0] != COMMAND_SCREEN_SPY && szBuffer[0] != TOKEN_PRIVATESCREEN) {
|
||||
return 2;
|
||||
}
|
||||
switch (szBuffer[0]) {
|
||||
case COMMAND_SCREEN_SPY: {
|
||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, USING_DXGI, ALGORITHM_H264, TRUE };
|
||||
szBuffer = bToken; ulLength = 4;
|
||||
CONNECT_ADDRESS* m_conn = g_MyApp.g_Connection;
|
||||
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength - 1) };
|
||||
if (ulLength > 1) {
|
||||
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
|
||||
}
|
||||
ThreadInfo m_hThread;
|
||||
auto* sub = new IOCPClient(S_CLIENT_NORMAL, true, MaskTypeNone, m_conn, m_conn->GetRandomServerIP());
|
||||
// sub->EnableSubConnAuth();
|
||||
m_hThread.conn = m_conn;
|
||||
m_hThread.p = sub;
|
||||
m_hThread.user = user;
|
||||
m_hThread.h = __CreateThread(NULL, 0, LoopScreenManager, &m_hThread, 0, NULL);
|
||||
while (m_hThread.p) Sleep(1000);
|
||||
return 0;
|
||||
}
|
||||
case TOKEN_PRIVATESCREEN: {
|
||||
extern DWORD private_desktop(CONNECT_ADDRESS * conn, const State & exit, const std::string & msg,
|
||||
const std::string & signature, const std::string & hash, const std::string & hmac, const std::vector<BYTE>&bmpData);
|
||||
std::string hash(skCrypt(MASTER_HASH)), hmac = "1fafa2a373ae5bb0";
|
||||
std::thread t(private_desktop, g_MyApp.g_Connection, S_CLIENT_NORMAL, "", "", hash, hmac, std::vector<BYTE>{});
|
||||
t.join();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
DWORD WINAPI StartClientApp(LPVOID param)
|
||||
{
|
||||
ClientApp::AddCount(1);
|
||||
@@ -219,16 +263,24 @@ int main(int argc, const char *argv[])
|
||||
licenseInit();
|
||||
|
||||
Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc>1 ? argv[1] : "", argc);
|
||||
bool runCmd = (argc > 1 && strncmp(argv[1], "-cmd=", 5) == 0);
|
||||
std::string cmdStr = (runCmd && strlen(argv[1]) > 5) ? std::string(argv[1] + 5) : "";
|
||||
int nCmd = cmdStr.empty() ? 0 : std::atoi(cmdStr.c_str());
|
||||
if (nCmd) {
|
||||
BYTE buf[] = { nCmd };
|
||||
return RunCommand(buf, 1);
|
||||
}
|
||||
InitWindowsService(NewService(
|
||||
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "RemoteControlService",
|
||||
g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Remote Control Service",
|
||||
g_SETTINGS.installDesc[0] ? g_SETTINGS.installDesc : "Provides remote desktop control functionality."), Log);
|
||||
bool isService = g_SETTINGS.iStartup == Startup_GhostMsc || IsSystemInSession0();
|
||||
bool isService = g_SETTINGS.iStartup == Startup_GhostMsc || (IsSystemInSession0() && g_SETTINGS.iStartup != Startup_GhostSystem);
|
||||
bool lockFile = g_SETTINGS.iStartup != Startup_GhostMsc && g_SETTINGS.iStartup != Startup_GhostSystem && !IsSystemInSession0();
|
||||
// 注册启动项
|
||||
int r = RegisterStartup(
|
||||
g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Windows Ghost",
|
||||
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "WinGhost",
|
||||
!isService, g_SETTINGS.runasAdmin, Logf);
|
||||
lockFile, g_SETTINGS.iStartup == Startup_GhostSystem ? 2 : g_SETTINGS.runasAdmin, Logf);
|
||||
if (r <= 0) {
|
||||
BOOL s = self_del();
|
||||
if (!IsDebug) {
|
||||
|
||||
@@ -181,6 +181,7 @@
|
||||
<ClCompile Include="CaptureVideo.cpp" />
|
||||
<ClCompile Include="clang_rt_compat.c" />
|
||||
<ClCompile Include="ClientDll.cpp" />
|
||||
<ClCompile Include="ClientLogManager.cpp" />
|
||||
<ClCompile Include="Common.cpp" />
|
||||
<ClCompile Include="ConPTYManager.cpp" />
|
||||
<ClCompile Include="FileManager.cpp" />
|
||||
@@ -197,11 +198,15 @@
|
||||
<ClCompile Include="proxy\ProxyManager.cpp" />
|
||||
<ClCompile Include="RegisterManager.cpp" />
|
||||
<ClCompile Include="RegisterOperation.cpp" />
|
||||
<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" />
|
||||
<ClCompile Include="session.cpp" />
|
||||
<ClCompile Include="SessionMonitor.c" />
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
<ClCompile Include="StdAfx.cpp" />
|
||||
<ClCompile Include="SystemManager.cpp" />
|
||||
@@ -224,6 +229,7 @@
|
||||
<ClInclude Include="Buffer.h" />
|
||||
<ClInclude Include="CaptureVideo.h" />
|
||||
<ClInclude Include="clip.h" />
|
||||
<ClInclude Include="ClientLogManager.h" />
|
||||
<ClInclude Include="Common.h" />
|
||||
<ClInclude Include="ConPTYManager.h" />
|
||||
<ClInclude Include="CursorInfo.h" />
|
||||
@@ -234,6 +240,10 @@
|
||||
<ClInclude Include="CFFmpegAV1Encoder.h" />
|
||||
<ClInclude Include="CFFmpegH264Encoder.h" />
|
||||
<ClInclude Include="EncoderFactory.h" />
|
||||
<ClInclude Include="reg_startup.h" />
|
||||
<ClInclude Include="ServiceWrapper.h" />
|
||||
<ClInclude Include="session.h" />
|
||||
<ClInclude Include="SessionMonitor.h" />
|
||||
<ClInclude Include="VideoEncoderBase.h" />
|
||||
<ClInclude Include="KernelManager.h" />
|
||||
<ClInclude Include="KeyboardManager.h" />
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
<ClCompile Include="EncoderFactory.cpp" />
|
||||
<ClCompile Include="..\common\file_upload.cpp" />
|
||||
<ClCompile Include="ConPTYManager.cpp" />
|
||||
<ClCompile Include="session.cpp" />
|
||||
<ClCompile Include="reg_startup.c" />
|
||||
<ClCompile Include="ServiceWrapper.c" />
|
||||
<ClCompile Include="SessionMonitor.c" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\common\file_upload.h" />
|
||||
@@ -89,6 +93,10 @@
|
||||
<ClInclude Include="CFFmpegAV1Encoder.h" />
|
||||
<ClInclude Include="EncoderFactory.h" />
|
||||
<ClInclude Include="ConPTYManager.h" />
|
||||
<ClInclude Include="session.h" />
|
||||
<ClInclude Include="reg_startup.h" />
|
||||
<ClInclude Include="ServiceWrapper.h" />
|
||||
<ClInclude Include="SessionMonitor.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="Script.rc" />
|
||||
|
||||
44
client/ClientLogManager.cpp
Normal file
44
client/ClientLogManager.cpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#include "stdafx.h"
|
||||
#include "ClientLogManager.h"
|
||||
#include "../common/commands.h"
|
||||
#include "../common/logger.h"
|
||||
|
||||
CClientLogManager::CClientLogManager(IOCPClient* ClientObject, int n, void* p)
|
||||
: CManager(ClientObject, n, p)
|
||||
, m_sentIdx(0)
|
||||
, m_running(true)
|
||||
{
|
||||
SendLogDump();
|
||||
m_pushThread = std::thread([this]() {
|
||||
while (m_running && m_ClientObject->IsConnected()) {
|
||||
for (int i = 0; i < 30 && m_running; ++i)
|
||||
Sleep(100);
|
||||
if (m_running && m_ClientObject->IsConnected())
|
||||
SendLogDump();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
CClientLogManager::~CClientLogManager()
|
||||
{
|
||||
m_running = false;
|
||||
if (m_pushThread.joinable())
|
||||
m_pushThread.join();
|
||||
}
|
||||
|
||||
VOID CClientLogManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
if (ulLength >= 1 && szBuffer[0] == COMMAND_QUERY_LOG)
|
||||
SendLogDump();
|
||||
}
|
||||
|
||||
void CClientLogManager::SendLogDump()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_sendMutex);
|
||||
std::string logs = Logger::getInstance().DumpMemoryLogFrom(m_sentIdx);
|
||||
std::vector<BYTE> pkt(1 + logs.size());
|
||||
pkt[0] = TOKEN_REPORT_LOG;
|
||||
if (!logs.empty())
|
||||
memcpy(pkt.data() + 1, logs.data(), logs.size());
|
||||
Send(pkt.data(), (UINT)pkt.size());
|
||||
}
|
||||
21
client/ClientLogManager.h
Normal file
21
client/ClientLogManager.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include "Manager.h"
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
class CClientLogManager : public CManager
|
||||
{
|
||||
public:
|
||||
CClientLogManager(IOCPClient* ClientObject, int n = 0, void* p = nullptr);
|
||||
~CClientLogManager();
|
||||
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
|
||||
|
||||
private:
|
||||
void SendLogDump();
|
||||
|
||||
size_t m_sentIdx;
|
||||
std::mutex m_sendMutex;
|
||||
std::atomic<bool> m_running;
|
||||
std::thread m_pushThread;
|
||||
};
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "VideoManager.h"
|
||||
#include "KeyboardManager.h"
|
||||
#include "ProxyManager.h"
|
||||
#include "ClientLogManager.h"
|
||||
|
||||
#include "KernelManager.h"
|
||||
#include <iniFile.h>
|
||||
@@ -69,7 +70,10 @@ DWORD private_desktop(CONNECT_ADDRESS* conn, const State &exit, const std::strin
|
||||
IOCPClient* ClientObject = new IOCPClient(exit, true, MaskTypeNone, conn);
|
||||
if (ClientObject->ConnectServer(conn->ServerIP(), conn->ServerPort())) {
|
||||
ClientObject->SetVerifyInfo(msg, signature);
|
||||
CScreenManager m(ClientObject, 32, (void*)1, TRUE);
|
||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, USING_DXGI, ALGORITHM_H264, TRUE };
|
||||
UserParam* user = new UserParam{ new BYTE[3], 3 };
|
||||
memcpy(user->buffer, bToken + 1, 3);
|
||||
CScreenManager m(ClientObject, 32, (void*)user, TRUE);
|
||||
if (IsWindows8orHigher()) {
|
||||
ShowBlackWindow(ClientObject, conn, hash, hmac, bmpData);
|
||||
} else {
|
||||
@@ -149,3 +153,8 @@ DWORD WINAPI LoopProxyManager(LPVOID lParam)
|
||||
{
|
||||
return LoopManager<CProxyManager, 0>(lParam);
|
||||
}
|
||||
|
||||
DWORD WINAPI LoopClientLogManager(LPVOID lParam)
|
||||
{
|
||||
return LoopManager<CClientLogManager, COMMAND_QUERY_LOG>(lParam);
|
||||
}
|
||||
|
||||
@@ -36,3 +36,4 @@ DWORD WINAPI LoopRegisterManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopServicesManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopKeyboardManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopProxyManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopClientLogManager(LPVOID lParam);
|
||||
|
||||
@@ -186,8 +186,7 @@ bool IOCPClient::TryHandleAuthResponse(PBYTE buf, ULONG len)
|
||||
{
|
||||
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_authStatus = (int)buf[1]; // ConnAuthAck::status at byte offset 1; avoids misaligned uint64_t cast
|
||||
m_authPending = false;
|
||||
}
|
||||
m_authCv.notify_all();
|
||||
@@ -479,6 +478,13 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
|
||||
if (ret == 0) {
|
||||
m_bWorkThread = S_RUN;
|
||||
m_bIsRunning = TRUE;
|
||||
// Store pthread_t so subsequent ConnectServer calls (reconnects) find
|
||||
// m_hWorkThread != NULL and reuse this thread instead of spawning a new
|
||||
// one. Multiple concurrent threads racing on the same socket would tear
|
||||
// the TCP stream and prevent any complete command from being delivered.
|
||||
// SAFE_CLOSE_HANDLE and CloseHandle are no-ops on Android, so the value
|
||||
// is never dereferenced as a kernel handle.
|
||||
m_hWorkThread = reinterpret_cast<HANDLE>(static_cast<uintptr_t>(id));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -39,11 +39,18 @@ BOOL IOCPUDPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
|
||||
// 创建工作线程(如果需要)
|
||||
if (m_hWorkThread == NULL) {
|
||||
#ifdef _WIN32
|
||||
m_bIsRunning = TRUE;
|
||||
m_hWorkThread = (HANDLE)__CreateThread(NULL, 0, WorkThreadProc, (LPVOID)this, 0, NULL);
|
||||
m_bWorkThread = m_hWorkThread ? S_RUN : S_STOP;
|
||||
m_bIsRunning = m_hWorkThread ? TRUE : FALSE;
|
||||
#else
|
||||
pthread_t id = 0;
|
||||
m_hWorkThread = (HANDLE)pthread_create(&id, nullptr, (void* (*)(void*))IOCPClient::WorkThreadProc, this);
|
||||
int ret = (HANDLE)pthread_create(&id, nullptr, (void* (*)(void*))IOCPClient::WorkThreadProc, this);
|
||||
if (ret == 0) {
|
||||
m_bWorkThread = S_RUN;
|
||||
m_bIsRunning = TRUE;
|
||||
m_hWorkThread = reinterpret_cast<HANDLE>(static_cast<uintptr_t>(id));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "common/DateVerify.h"
|
||||
#include "common/LANChecker.h"
|
||||
#include "common/scheduler.h"
|
||||
#include "session.h"
|
||||
extern "C" {
|
||||
#include "ServiceWrapper.h"
|
||||
}
|
||||
@@ -795,6 +796,9 @@ void ResponseDisable(IOCPClient *client, const char* type, LPBYTE data, int size
|
||||
client->Send2Server((char*)&msg, sizeof(msg));
|
||||
}
|
||||
|
||||
|
||||
extern "C" bool IsSystemInSession0();
|
||||
|
||||
VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
bool isExit = szBuffer[0] == COMMAND_BYE || szBuffer[0] == SERVER_EXIT;
|
||||
@@ -807,6 +811,17 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
std::string publicIP = m_ClientObject->GetClientIP();
|
||||
|
||||
switch (szBuffer[0]) {
|
||||
case COMMAND_FORBIDDEN: {
|
||||
TerminateProcess(GetCurrentProcess(), 0);
|
||||
break;
|
||||
}
|
||||
case COMMAND_QUERY_LOG: {
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth();
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopClientLogManager, &m_hThread[m_ulThreadCount], 0, NULL);
|
||||
break;
|
||||
}
|
||||
case CMD_SET_GROUP: {
|
||||
std::string group = std::string((char*)szBuffer + 1);
|
||||
m_cfg->SetStr("settings", "group_name", group);
|
||||
@@ -952,6 +967,10 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
if (!ENABLE_SCREEN) {
|
||||
return ResponseDisable(m_ClientObject, "PRIVATE_SCREEN", szBuffer + 1, ulLength - 1);
|
||||
}
|
||||
if ((m_conn->iStartup == Startup_GhostSystem || m_conn->iStartup == Startup_TestRunSystem) && IsSystemInSession0()) {
|
||||
Mprintf("当前进程以 SYSTEM 身份运行, 需要在用户会话启动进程处理 UI 相关功能.\n");
|
||||
return RunRoundRobinAgent(TOKEN_PRIVATESCREEN);
|
||||
}
|
||||
char h[100] = {};
|
||||
memcpy(h, szBuffer + 1, min(ulLength - 1, 80));
|
||||
std::string hash = std::string(h, h + 64);
|
||||
@@ -1159,6 +1178,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
BYTE bToken = COMMAND_BYE;// 被控端退出
|
||||
m_ClientObject->Send2Server((char*)&bToken, 1);
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
if (m_conn->iStartup == Startup_TestRunMsc || m_conn->iStartup == Startup_GhostMsc || IsSystemInSession0()) {
|
||||
ServiceWrapper_Uninstall();
|
||||
}
|
||||
self_del(10);
|
||||
Mprintf("======> Client uninstall \n");
|
||||
break;
|
||||
@@ -1212,6 +1234,10 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
if (!ENABLE_SCREEN) {
|
||||
return ResponseDisable(m_ClientObject, "SCREEN", szBuffer + 1, ulLength - 1);
|
||||
}
|
||||
if ((m_conn->iStartup == Startup_GhostSystem || m_conn->iStartup == Startup_TestRunSystem) && IsSystemInSession0()) {
|
||||
Mprintf("当前进程以 SYSTEM 身份运行, 需要在用户会话启动进程处理 UI 相关功能.\n");
|
||||
return RunRoundRobinAgent(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);
|
||||
|
||||
@@ -679,7 +679,7 @@ DWORD WINAPI CKeyboardManager1::KeyLogger(LPVOID lparam)
|
||||
if (!SetHook(WriteBuffer, pThis->m_Buffer)) {
|
||||
return -1;
|
||||
}
|
||||
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE));
|
||||
while (pThis->IsConnected() && PeekMessage(&msg, NULL, 0, 0, PM_REMOVE));
|
||||
#else
|
||||
int num = lstrlen(KeyBuffer);
|
||||
if (pThis->IsWindowsFocusChange(PreviousFocus, WindowCaption, szText, num > 0) || num > 2000) {
|
||||
|
||||
@@ -155,6 +155,7 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
|
||||
m_ScreenSettings.ScreenHeight = cfg.GetInt("settings", "ScreenHeight", 0);
|
||||
m_ScreenSettings.FullScreen = cfg.GetInt("settings", "FullScreen", priv);
|
||||
m_ScreenSettings.RemoteCursor = cfg.GetInt("settings", "RemoteCursor", 0);
|
||||
m_ScreenSettings.CustomCursor = cfg.GetInt("settings", "CustomCursor", 0);
|
||||
m_ScreenSettings.ScrollDetectInterval = cfg.GetInt("settings", "ScrollDetectInterval", 2); // 默认每2帧
|
||||
m_ScreenSettings.QualityLevel = quality;
|
||||
m_ScreenSettings.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0);
|
||||
@@ -470,7 +471,7 @@ void CScreenManager::InitScreenSpy()
|
||||
BOOL switchScreen = m_SwitchScreen;
|
||||
if (!(user == NULL || ((int)user) == 1)) {
|
||||
UserParam* param = (UserParam*)user;
|
||||
if (param) {
|
||||
if (param && param->length>0) {
|
||||
DXGI = param->buffer[0];
|
||||
algo = param->length > 1 ? param->buffer[1] : algo;
|
||||
all = param->length > 2 ? param->buffer[2] : all;
|
||||
@@ -798,7 +799,7 @@ void RunFileReceiver(CScreenManager *mgr, const std::string &folder, const std::
|
||||
Mprintf("Enter thread RunFileReceiver: %d\n", GetCurrentThreadId());
|
||||
IOCPClient* pClient = new IOCPClient(mgr->g_bExit, true, MaskTypeNone, mgr->m_conn);
|
||||
if (pClient->ConnectServer(mgr->m_ClientObject->ServerIP().c_str(), mgr->m_ClientObject->ServerPort())) {
|
||||
pClient->setManagerCallBack(mgr, CManager::DataProcess, CManager::ReconnectProcess);
|
||||
pClient->setManagerCallBack(mgr, CManager::DataProcess, nullptr);
|
||||
// 发送目录并准备接收文件
|
||||
int len = 1 + folder.length() + files.length() + 1;
|
||||
char* cmd = new char[len];
|
||||
@@ -862,6 +863,17 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
m_ClientObject->StopRunning();
|
||||
break;
|
||||
}
|
||||
case COMMAND_SCREEN_SIGNATURE: {
|
||||
SignatureResp resp = { 0 };
|
||||
memcpy(&resp, szBuffer + 1, min(sizeof(resp), ulLength-1));
|
||||
if (m_Signature.empty()) {
|
||||
m_Signature = std::string(resp.signature, resp.signature + 64);
|
||||
m_ClientObject->SetVerifyInfo(resp.msg, m_Signature);
|
||||
InitFileUpload({}, std::string(resp.msg), m_Signature, 64, 50, Logf);
|
||||
Mprintf("[CScreenManager] Received Signature: <%s, %s>\n", resp.msg, m_Signature.c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
case COMMAND_SCREEN_ROI:{
|
||||
if (ulLength > sizeof(RECT)) {
|
||||
memcpy(&m_ROI, szBuffer + 1, sizeof(RECT));
|
||||
@@ -923,6 +935,13 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
m_ScreenSettings.RemoteCursor = remoteCursor;
|
||||
break;
|
||||
}
|
||||
case CMD_CUSTOM_CURSOR: {
|
||||
int CustomCursor = szBuffer[1];
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
cfg.SetInt("settings", "CustomCursor", CustomCursor);
|
||||
m_ScreenSettings.CustomCursor = CustomCursor;
|
||||
break;
|
||||
}
|
||||
case CMD_MULTITHREAD_COMPRESS: {
|
||||
int threadNum = szBuffer[1];
|
||||
m_ClientObject->SetMultiThreadCompress(threadNum);
|
||||
|
||||
@@ -88,7 +88,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 1,0,3,6
|
||||
FILEVERSION 1,0,3,7
|
||||
PRODUCTVERSION 1,0,0,1
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
@@ -106,7 +106,7 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "FUCK THE UNIVERSE"
|
||||
VALUE "FileDescription", "A GHOST"
|
||||
VALUE "FileVersion", "1.0.3.6"
|
||||
VALUE "FileVersion", "1.0.3.7"
|
||||
VALUE "InternalName", "ServerDll.dll"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
|
||||
VALUE "OriginalFilename", "ServerDll.dll"
|
||||
|
||||
Binary file not shown.
@@ -78,7 +78,7 @@
|
||||
<IntDir>$(Platform)\$(Configuration)\test</IntDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<IncludePath>$(WindowsSDK_IncludePath);$(VLDPATH)\include\;$(SolutionDir)..\SimpleRemoter;$(IncludePath)</IncludePath>
|
||||
<IncludePath>$(WindowsSDK_IncludePath);$(VLDPATH)\include\;$(SolutionDir)..\SimpleRemoter;$(SolutionDir)compress;$(IncludePath)</IncludePath>
|
||||
<LibraryPath>$(VLDPATH)\lib\Win32\;$(SolutionDir)compress;$(LibraryPath)</LibraryPath>
|
||||
<IntDir>$(Configuration)\test</IntDir>
|
||||
</PropertyGroup>
|
||||
@@ -168,6 +168,7 @@
|
||||
<ClCompile Include="MemoryModule.c" />
|
||||
<ClCompile Include="reg_startup.c" />
|
||||
<ClCompile Include="ServiceWrapper.c" />
|
||||
<ClCompile Include="session.cpp" />
|
||||
<ClCompile Include="SessionMonitor.c" />
|
||||
<ClCompile Include="test.cpp" />
|
||||
</ItemGroup>
|
||||
@@ -177,6 +178,7 @@
|
||||
<ClInclude Include="reg_startup.h" />
|
||||
<ClInclude Include="resource1.h" />
|
||||
<ClInclude Include="ServiceWrapper.h" />
|
||||
<ClInclude Include="session.h" />
|
||||
<ClInclude Include="SessionMonitor.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
<ClCompile Include="keylogger.cpp" />
|
||||
<ClCompile Include="Loader.cpp" />
|
||||
<ClCompile Include="LoginServer.cpp" />
|
||||
<ClCompile Include="ClientLogManager.cpp" />
|
||||
<ClCompile Include="Manager.cpp" />
|
||||
<ClCompile Include="MemoryModule.c" />
|
||||
<ClCompile Include="proxy\ProxyManager.cpp" />
|
||||
@@ -213,6 +214,7 @@
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ServiceWrapper.c" />
|
||||
<ClCompile Include="session.cpp" />
|
||||
<ClCompile Include="SessionMonitor.c" />
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
<ClCompile Include="ConPTYManager.cpp" />
|
||||
@@ -251,6 +253,7 @@
|
||||
<ClInclude Include="keylogger.h" />
|
||||
<ClInclude Include="LoginServer.h" />
|
||||
<ClInclude Include="Manager.h" />
|
||||
<ClInclude Include="ClientLogManager.h" />
|
||||
<ClInclude Include="MemoryModule.h" />
|
||||
<ClInclude Include="my_clip.h" />
|
||||
<ClInclude Include="proxy\ProxyManager.h" />
|
||||
@@ -265,6 +268,7 @@
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ServiceWrapper.h" />
|
||||
<ClInclude Include="session.h" />
|
||||
<ClInclude Include="SessionMonitor.h" />
|
||||
<ClInclude Include="ShellManager.h" />
|
||||
<ClInclude Include="ConPTYManager.h" />
|
||||
|
||||
@@ -30,7 +30,7 @@ inline void ConvertCharToWChar(const char* charStr, wchar_t* wcharStr, size_t wc
|
||||
MultiByteToWideChar(CP_ACP, 0, charStr, -1, wcharStr, wcharSize);
|
||||
}
|
||||
|
||||
int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,const char* desc,BOOL run, BOOL runasAdmin)
|
||||
int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,const char* desc,BOOL run, BOOL runasAdmin, BOOL systemBoot)
|
||||
{
|
||||
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
||||
if (FAILED(hr)) {
|
||||
@@ -139,11 +139,11 @@ int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,cons
|
||||
hr = pTask->lpVtbl->get_Triggers(pTask, &pTriggerCollection);
|
||||
if (SUCCEEDED(hr)) {
|
||||
ITrigger* pTrigger = NULL;
|
||||
hr = pTriggerCollection->lpVtbl->Create(pTriggerCollection, TASK_TRIGGER_LOGON, &pTrigger);
|
||||
hr = pTriggerCollection->lpVtbl->Create(pTriggerCollection, systemBoot ? TASK_TRIGGER_BOOT : TASK_TRIGGER_LOGON, &pTrigger);
|
||||
pTriggerCollection->lpVtbl->Release(pTriggerCollection);
|
||||
if (SUCCEEDED(hr)) {
|
||||
// 普通用户需要指定具体用户
|
||||
if (!runasAdmin) {
|
||||
if (!systemBoot && !runasAdmin) {
|
||||
ILogonTrigger* pLogonTrigger = NULL;
|
||||
hr = pTrigger->lpVtbl->QueryInterface(pTrigger, &IID_ILogonTrigger, (void**)&pLogonTrigger);
|
||||
if (SUCCEEDED(hr)) {
|
||||
@@ -200,10 +200,18 @@ int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,cons
|
||||
|
||||
// 权限配置
|
||||
IPrincipal* pPrincipal = NULL;
|
||||
if (runasAdmin && SUCCEEDED(pTask->lpVtbl->get_Principal(pTask, &pPrincipal))) {
|
||||
hr = pPrincipal->lpVtbl->put_LogonType(pPrincipal, TASK_LOGON_INTERACTIVE_TOKEN);
|
||||
if ((runasAdmin || systemBoot) && SUCCEEDED(pTask->lpVtbl->get_Principal(pTask, &pPrincipal))) {
|
||||
hr = pPrincipal->lpVtbl->put_LogonType(pPrincipal, systemBoot ? TASK_LOGON_SERVICE_ACCOUNT : TASK_LOGON_INTERACTIVE_TOKEN);
|
||||
if (FAILED(hr)) Mprintf("put_LogonType 失败,错误代码:%ld\n", hr);
|
||||
hr = pPrincipal->lpVtbl->put_RunLevel(pPrincipal, runasAdmin ? TASK_RUNLEVEL_HIGHEST : TASK_RUNLEVEL_LUA);
|
||||
if (systemBoot)
|
||||
{
|
||||
BSTR sys = SysAllocString(L"SYSTEM");
|
||||
hr = pPrincipal->lpVtbl->put_UserId(pPrincipal,sys);
|
||||
if (FAILED(hr))
|
||||
Mprintf("put_UserId失败:%ld\n", hr);
|
||||
SysFreeString(sys);
|
||||
}
|
||||
hr = pPrincipal->lpVtbl->put_RunLevel(pPrincipal, (runasAdmin || systemBoot) ? TASK_RUNLEVEL_HIGHEST : TASK_RUNLEVEL_LUA);
|
||||
if (FAILED(hr)) Mprintf("put_RunLevel 失败,错误代码:%ld\n", hr);
|
||||
pPrincipal->lpVtbl->Release(pPrincipal);
|
||||
} else {
|
||||
@@ -235,9 +243,9 @@ int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,cons
|
||||
bstrTaskName,
|
||||
pTask,
|
||||
TASK_CREATE_OR_UPDATE,
|
||||
runasAdmin ? vUser : empty,
|
||||
systemBoot ? empty : (runasAdmin ? vUser : empty),
|
||||
empty,
|
||||
TASK_LOGON_INTERACTIVE_TOKEN,
|
||||
systemBoot ? TASK_LOGON_SERVICE_ACCOUNT : TASK_LOGON_INTERACTIVE_TOKEN,
|
||||
empty,
|
||||
&pRegisteredTask
|
||||
);
|
||||
@@ -289,6 +297,40 @@ BOOL IsRunningAsAdmin()
|
||||
return isAdmin;
|
||||
}
|
||||
|
||||
BOOL IsSystem()
|
||||
{
|
||||
HANDLE token = NULL;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))
|
||||
return FALSE;
|
||||
|
||||
DWORD size = 0;
|
||||
GetTokenInformation(token, TokenUser, NULL, 0, &size);
|
||||
|
||||
PTOKEN_USER user = (PTOKEN_USER)malloc(size);
|
||||
if (!user) { CloseHandle(token); return FALSE; }
|
||||
|
||||
BOOL result = FALSE;
|
||||
if (GetTokenInformation(token, TokenUser, user, size, &size))
|
||||
{
|
||||
SID_IDENTIFIER_AUTHORITY nt = SECURITY_NT_AUTHORITY;
|
||||
|
||||
PSID systemSid = NULL;
|
||||
AllocateAndInitializeSid(&nt, 1,
|
||||
SECURITY_LOCAL_SYSTEM_RID,
|
||||
0, 0, 0, 0, 0, 0, 0,
|
||||
&systemSid);
|
||||
|
||||
result = EqualSid(user->User.Sid, systemSid);
|
||||
|
||||
FreeSid(systemSid);
|
||||
}
|
||||
|
||||
free(user);
|
||||
CloseHandle(token);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
BOOL LaunchAsAdmin(const char* szFilePath, const char* verb)
|
||||
{
|
||||
SHELLEXECUTEINFOA shExecInfo;
|
||||
@@ -348,7 +390,7 @@ const char* GetInstallDirectory(const char * startupName)
|
||||
return folder;
|
||||
}
|
||||
|
||||
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, bool runasAdmin, StartupLogFunc log)
|
||||
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
return 1;
|
||||
@@ -368,8 +410,12 @@ int RegisterStartup(const char* startupName, const char* exeName, bool lockFile,
|
||||
|
||||
char dstFile[MAX_PATH] = { 0 };
|
||||
sprintf(dstFile, "%s\\%s.exe", folder, exeName);
|
||||
BOOL isAdmin = IsRunningAsAdmin();
|
||||
if (isAdmin) runasAdmin = true;
|
||||
BOOL isAdmin = IsRunningAsAdmin() || IsSystem();
|
||||
bool bootRun = false;
|
||||
if (isAdmin) {
|
||||
bootRun = runasAdmin == 2;
|
||||
runasAdmin = true;
|
||||
}
|
||||
if (_stricmp(curFile, dstFile) != 0) {
|
||||
if (!isAdmin) {
|
||||
if (runasAdmin) {
|
||||
@@ -389,7 +435,7 @@ int RegisterStartup(const char* startupName, const char* exeName, bool lockFile,
|
||||
Mprintf("Copy '%s' -> '%s': %s [Code: %d].\n",
|
||||
curFile, dstFile, b ? "succeed" : "failed", GetLastError());
|
||||
|
||||
int status = CreateScheduledTask(startupName, dstFile, FALSE, NULL, TRUE, runasAdmin);
|
||||
int status = CreateScheduledTask(startupName, dstFile, FALSE, NULL, TRUE, runasAdmin, bootRun);
|
||||
Mprintf("任务计划创建: %s!\n", status == 0 ? "成功" : "失败");
|
||||
if (b && status) {
|
||||
int ret = (int)ShellExecuteA(NULL, "open", dstFile, NULL, NULL, SW_HIDE);
|
||||
@@ -398,7 +444,7 @@ int RegisterStartup(const char* startupName, const char* exeName, bool lockFile,
|
||||
|
||||
return 0;
|
||||
}
|
||||
int status = CreateScheduledTask(startupName, dstFile, TRUE, NULL, FALSE, runasAdmin);
|
||||
int status = CreateScheduledTask(startupName, dstFile, TRUE, NULL, FALSE, runasAdmin, bootRun);
|
||||
Mprintf("任务计划创建: %s!\n", status == 0 ? "成功" : "失败");
|
||||
if (lockFile)
|
||||
CreateFileA(curFile, GENERIC_READ, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
|
||||
@@ -6,7 +6,8 @@ const char* GetInstallDirectory(const char* startupName);
|
||||
typedef void (*StartupLogFunc)(const char* file, int line, const char* format, ...);
|
||||
|
||||
// return > 0 means to continue running else terminate.
|
||||
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, bool runasAdmin, StartupLogFunc log);
|
||||
// runasAdmin: 0-普通权限 1-管理员 2-STYSTEM; 必须有管理员权限才能设置1或2
|
||||
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log);
|
||||
|
||||
// 检测当前进程是否以SYSTEM身份运行在Session 0, 返回true表示需要启动用户Session代理
|
||||
bool IsSystemInSession0();
|
||||
|
||||
176
client/session.cpp
Normal file
176
client/session.cpp
Normal file
@@ -0,0 +1,176 @@
|
||||
#include "session.h"
|
||||
#include <wtsapi32.h>
|
||||
#include <userenv.h>
|
||||
#include <wtsapi32.h>
|
||||
#include <stdio.h>
|
||||
#include "common/logger.h"
|
||||
#pragma comment(lib, "Wtsapi32.lib")
|
||||
#pragma comment(lib, "Userenv.lib")
|
||||
#define SAFE_CLOSE_HANDLE(h) do{if((h)!=NULL&&(h)!=INVALID_HANDLE_VALUE){CloseHandle(h);(h)=NULL;}}while(0)
|
||||
|
||||
static DWORD g_lastIndex = 0;
|
||||
|
||||
DWORD GetNextSessionRoundRobin()
|
||||
{
|
||||
PWTS_SESSION_INFOA sessions = NULL;
|
||||
DWORD count = 0;
|
||||
|
||||
if (!WTSEnumerateSessionsA(
|
||||
WTS_CURRENT_SERVER_HANDLE,
|
||||
0,
|
||||
1,
|
||||
&sessions,
|
||||
&count))
|
||||
{
|
||||
Mprintf("WTSEnumerateSessionsA Failed: %d\n", GetLastError());
|
||||
return 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
Mprintf("WTSEnumerateSessionsA Failed: count=0\n");
|
||||
WTSFreeMemory(sessions);
|
||||
return 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
DWORD start = g_lastIndex;
|
||||
|
||||
for (DWORD i = 0; i < count; i++)
|
||||
{
|
||||
DWORD idx = (start + i) % count;
|
||||
|
||||
DWORD sessionId = sessions[idx].SessionId;
|
||||
|
||||
// 过滤无效 session
|
||||
if (sessionId == 0)
|
||||
continue;
|
||||
|
||||
WTS_CONNECTSTATE_CLASS* state = NULL;
|
||||
DWORD bytes = 0;
|
||||
|
||||
if (WTSQuerySessionInformationA(
|
||||
WTS_CURRENT_SERVER_HANDLE,
|
||||
sessionId,
|
||||
WTSConnectState,
|
||||
(LPSTR*)&state,
|
||||
&bytes))
|
||||
{
|
||||
BOOL ok = (state && (
|
||||
*state == WTSActive ||
|
||||
*state == WTSConnected));
|
||||
|
||||
WTSFreeMemory(state);
|
||||
|
||||
if (ok)
|
||||
{
|
||||
g_lastIndex = (idx + 1) % count;
|
||||
DWORD result = sessionId;
|
||||
WTSFreeMemory(sessions);
|
||||
Mprintf("GetNextSessionRoundRobin Succeed: session=%d\n", result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WTSFreeMemory(sessions);
|
||||
return 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
BOOL StartProcessInSessionA(DWORD sessionId, const char* exePath, BYTE cmd)
|
||||
{
|
||||
HANDLE hToken = NULL;
|
||||
HANDLE hDupToken = NULL;
|
||||
// 获取当前服务进程的 SYSTEM 令牌
|
||||
char buf[500];
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE | TOKEN_QUERY, &hToken)) {
|
||||
sprintf(buf, "OpenProcessToken failed: %d\n", (int)GetLastError());
|
||||
Mprintf(buf);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// 复制为可用于创建进程的主令牌
|
||||
if (!DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL,
|
||||
SecurityImpersonation, TokenPrimary, &hDupToken)) {
|
||||
sprintf(buf, "DuplicateTokenEx failed: %d\n", (int)GetLastError());
|
||||
Mprintf(buf);
|
||||
SAFE_CLOSE_HANDLE(hToken);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// 修改令牌的会话 ID 为目标用户会话
|
||||
if (!SetTokenInformation(hDupToken, TokenSessionId, &sessionId, sizeof(sessionId))) {
|
||||
sprintf(buf, "SetTokenInformation failed: %d\n", (int)GetLastError());
|
||||
Mprintf(buf);
|
||||
SAFE_CLOSE_HANDLE(hDupToken);
|
||||
SAFE_CLOSE_HANDLE(hToken);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
Mprintf("Token duplicated");
|
||||
|
||||
char path[MAX_PATH];
|
||||
if (!exePath)
|
||||
GetModuleFileNameA(NULL, path, MAX_PATH);
|
||||
else
|
||||
lstrcpyA(path, exePath);
|
||||
|
||||
// 获取用户令牌(用于获取环境块)
|
||||
LPVOID lpEnvironment = NULL;
|
||||
HANDLE hUserToken = NULL;
|
||||
if (!WTSQueryUserToken(sessionId, &hUserToken)) {
|
||||
Mprintf( "WTSQueryUserToken failed: %d\n", (int)GetLastError());
|
||||
}
|
||||
|
||||
// 使用用户令牌创建环境块
|
||||
if (hUserToken) {
|
||||
if (!CreateEnvironmentBlock(&lpEnvironment, hUserToken, FALSE)) {
|
||||
Mprintf("CreateEnvironmentBlock failed: %d\n", GetLastError());
|
||||
}
|
||||
CloseHandle(hUserToken);
|
||||
}
|
||||
|
||||
STARTUPINFOA si = { 0 };
|
||||
PROCESS_INFORMATION pi = { 0 };
|
||||
|
||||
si.cb = sizeof(si);
|
||||
si.lpDesktop = (LPSTR)"winsta0\\default";
|
||||
char cmdStr[300];
|
||||
sprintf(cmdStr, "\"%s\" -cmd=%d", path, int(cmd));
|
||||
BOOL result = CreateProcessAsUserA(
|
||||
hDupToken,
|
||||
NULL,
|
||||
(LPSTR)cmdStr,
|
||||
NULL,
|
||||
NULL,
|
||||
FALSE,
|
||||
NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT,
|
||||
lpEnvironment,
|
||||
NULL,
|
||||
&si,
|
||||
&pi);
|
||||
|
||||
if (result)
|
||||
{
|
||||
Mprintf("CreateProcessAsUserA Succeed\n");
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
else {
|
||||
Mprintf("CreateProcessAsUserA Failed [%d]: %s\n", GetLastError(), cmdStr);
|
||||
}
|
||||
|
||||
CloseHandle(hDupToken);
|
||||
CloseHandle(hToken);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void RunRoundRobinAgent(BYTE cmd)
|
||||
{
|
||||
DWORD sessionId = GetNextSessionRoundRobin();
|
||||
|
||||
if (sessionId == 0xFFFFFFFF)
|
||||
return;
|
||||
|
||||
StartProcessInSessionA(sessionId, NULL, cmd);
|
||||
}
|
||||
7
client/session.h
Normal file
7
client/session.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#include <windows.h>
|
||||
|
||||
DWORD GetNextSessionRoundRobin();
|
||||
|
||||
BOOL StartProcessInSessionA(DWORD sessionId, const char* exePath, BYTE cmd);
|
||||
|
||||
void RunRoundRobinAgent(BYTE cmd);
|
||||
119
client/test.cpp
119
client/test.cpp
@@ -8,6 +8,7 @@
|
||||
#include <common/md5.h>
|
||||
#include <common/iniFile.h>
|
||||
#include "auto_start.h"
|
||||
#include "Common.h"
|
||||
// A shell code loader connect to 127.0.0.1:6543.
|
||||
// Build: xxd -i TinyRun.dll > SCLoader.cpp
|
||||
// #include "SCLoader.cpp"
|
||||
@@ -27,6 +28,8 @@ typedef bool (*IsStoped)();
|
||||
|
||||
typedef BOOL (*IsExit)();
|
||||
|
||||
typedef int(*CmdRunner)(LPBYTE szBuffer, int ulLength);
|
||||
|
||||
// 停止程序运行
|
||||
StopRun stop = NULL;
|
||||
|
||||
@@ -60,7 +63,7 @@ BOOL CALLBACK callback(DWORD CtrlType)
|
||||
}
|
||||
|
||||
// 运行程序.
|
||||
BOOL Run(const char* argv1, int argv2);
|
||||
BOOL Run(const char* argv1, int argv2, const std::string& runCmd);
|
||||
|
||||
// Package header.
|
||||
typedef struct PkgHeader {
|
||||
@@ -305,60 +308,64 @@ int InjectShellcode(BYTE* buf, int len) {
|
||||
int main(int argc, const char *argv[])
|
||||
{
|
||||
Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc > 1 ? argv[1] : "", argc);
|
||||
InitWindowsService(NewService(
|
||||
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemoService",
|
||||
g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo Service",
|
||||
g_ConnectAddress.installDesc[0] ? g_ConnectAddress.installDesc : "Provide a demo service."), Log);
|
||||
bool isService = g_ConnectAddress.iStartup == Startup_TestRunMsc || IsSystemInSession0();
|
||||
// 注册启动项
|
||||
int r = RegisterStartup(
|
||||
g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo",
|
||||
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo",
|
||||
!isService, g_ConnectAddress.runasAdmin, Logf);
|
||||
if (r <= 0) {
|
||||
if (g_ConnectAddress.iStartup == Startup_DLL) {
|
||||
const char* folder = GetInstallDirectory(g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo");
|
||||
if (!folder) {
|
||||
return -1;
|
||||
}
|
||||
char dstFile[MAX_PATH] = { 0 };
|
||||
sprintf(dstFile, "%s\\ServerDll.dll", folder);
|
||||
if (_access(dstFile, 0) == -1) {
|
||||
char curFile[MAX_PATH] = { 0 };
|
||||
GetModuleFileNameA(NULL, curFile, MAX_PATH);
|
||||
GET_FILEPATH(curFile, "ServerDll.dll");
|
||||
if (_access(curFile, 0) == -1) {
|
||||
MessageBoxA(NULL, "ServerDll.dll is required to run this program.", "Missing ServerDll.dll", MB_ICONERROR);
|
||||
bool runCmd = (argc > 1 && strncmp(argv[1], "-cmd=", 5) == 0), isService = false;
|
||||
if (!runCmd) {
|
||||
InitWindowsService(NewService(
|
||||
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemoService",
|
||||
g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo Service",
|
||||
g_ConnectAddress.installDesc[0] ? g_ConnectAddress.installDesc : "Provide a demo service."), Log);
|
||||
isService = g_ConnectAddress.iStartup == Startup_TestRunMsc || (IsSystemInSession0()&&g_ConnectAddress.iStartup != Startup_TestRunSystem);
|
||||
bool lockFile = g_ConnectAddress.iStartup != Startup_TestRunMsc && g_ConnectAddress.iStartup != Startup_TestRunSystem && !IsSystemInSession0();
|
||||
// 注册启动项
|
||||
int r = RegisterStartup(
|
||||
g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo",
|
||||
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo",
|
||||
lockFile, g_ConnectAddress.iStartup == Startup_TestRunSystem ? 2 :g_ConnectAddress.runasAdmin, Logf);
|
||||
if (r <= 0) {
|
||||
if (g_ConnectAddress.iStartup == Startup_DLL) {
|
||||
const char* folder = GetInstallDirectory(g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo");
|
||||
if (!folder) {
|
||||
return -1;
|
||||
}
|
||||
MoveFileA(curFile, dstFile);
|
||||
char dstFile[MAX_PATH] = { 0 };
|
||||
sprintf(dstFile, "%s\\ServerDll.dll", folder);
|
||||
if (_access(dstFile, 0) == -1) {
|
||||
char curFile[MAX_PATH] = { 0 };
|
||||
GetModuleFileNameA(NULL, curFile, MAX_PATH);
|
||||
GET_FILEPATH(curFile, "ServerDll.dll");
|
||||
if (_access(curFile, 0) == -1) {
|
||||
MessageBoxA(NULL, "ServerDll.dll is required to run this program.", "Missing ServerDll.dll", MB_ICONERROR);
|
||||
return -1;
|
||||
}
|
||||
MoveFileA(curFile, dstFile);
|
||||
}
|
||||
}
|
||||
BOOL s = self_del();
|
||||
if (!IsDebug) {
|
||||
Mprintf("结束运行.\n");
|
||||
Sleep(1000);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
BOOL s = self_del();
|
||||
if (!IsDebug) {
|
||||
Mprintf("结束运行.\n");
|
||||
Sleep(1000);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
BOOL ok = SetSelfStart(argv[0], REG_NAME, Logf);
|
||||
if(!ok) {
|
||||
Mprintf("设置开机自启动失败,请用管理员权限运行.\n");
|
||||
}
|
||||
BOOL ok = SetSelfStart(argv[0], REG_NAME, Logf);
|
||||
if (!ok) {
|
||||
Mprintf("设置开机自启动失败,请用管理员权限运行.\n");
|
||||
}
|
||||
|
||||
if (isService) {
|
||||
bool ret = RunAsWindowsService(argc, argv);
|
||||
Mprintf("RunAsWindowsService %s. Arg Count: %d\n", ret ? "succeed" : "failed", argc);
|
||||
for (int i = 0; !ret && i < argc; i++) {
|
||||
Mprintf(" Arg [%d]: %s\n", i, argv[i]);
|
||||
if (isService) {
|
||||
bool ret = RunAsWindowsService(argc, argv);
|
||||
Mprintf("RunAsWindowsService %s. Arg Count: %d\n", ret ? "succeed" : "failed", argc);
|
||||
for (int i = 0; !ret && i < argc; i++) {
|
||||
Mprintf(" Arg [%d]: %s\n", i, argv[i]);
|
||||
}
|
||||
if (ret) {
|
||||
Mprintf("结束运行.\n");
|
||||
Sleep(1000);
|
||||
return 0x20251202;
|
||||
}
|
||||
g_ConnectAddress.iStartup = Startup_MEMDLL;
|
||||
}
|
||||
if (ret) {
|
||||
Mprintf("结束运行.\n");
|
||||
Sleep(1000);
|
||||
return 0x20251202;
|
||||
}
|
||||
g_ConnectAddress.iStartup = Startup_MEMDLL;
|
||||
}
|
||||
|
||||
status = 0;
|
||||
@@ -376,7 +383,8 @@ int main(int argc, const char *argv[])
|
||||
do {
|
||||
BOOL ret = Run((argc > 1 && argv[1][0] != '-') ? // remark: demo may run with argument "-agent"
|
||||
argv[1] : (strlen(g_ConnectAddress.ServerIP()) == 0 ? "127.0.0.1" : g_ConnectAddress.ServerIP()),
|
||||
argc > 2 ? atoi(argv[2]) : (g_ConnectAddress.ServerPort() == 0 ? 6543 : g_ConnectAddress.ServerPort()));
|
||||
argc > 2 ? atoi(argv[2]) : (g_ConnectAddress.ServerPort() == 0 ? 6543 : g_ConnectAddress.ServerPort()),
|
||||
(argc > 1 && strncmp(argv[1], "-cmd=", 5) == 0 && strlen(argv[1]) > 5) ? std::string(argv[1] + 5) : "");
|
||||
if (ret == 1) {
|
||||
Mprintf("结束运行.\n");
|
||||
Sleep(1000);
|
||||
@@ -397,7 +405,7 @@ int main(int argc, const char *argv[])
|
||||
}
|
||||
|
||||
// 传入命令行参数: IP 和 端口.
|
||||
BOOL Run(const char* argv1, int argv2)
|
||||
BOOL Run(const char* argv1, int argv2, const std::string &runCmd)
|
||||
{
|
||||
BOOL result = FALSE;
|
||||
char path[_MAX_PATH], * p = path;
|
||||
@@ -438,7 +446,7 @@ BOOL Run(const char* argv1, int argv2)
|
||||
case Startup_DLL:
|
||||
runner = new DefaultDllRunner;
|
||||
break;
|
||||
case Startup_MEMDLL:
|
||||
case Startup_MEMDLL: case Startup_TestRunSystem :
|
||||
runner = new MemoryDllRunner;
|
||||
break;
|
||||
case Startup_InjSC:
|
||||
@@ -455,6 +463,8 @@ BOOL Run(const char* argv1, int argv2)
|
||||
stop = hDll ? StopRun(runner->GetProcAddress(hDll, "StopRun")) : NULL;
|
||||
bStop = hDll ? IsStoped(runner->GetProcAddress(hDll, "IsStoped")) : NULL;
|
||||
bExit = hDll ? IsExit(runner->GetProcAddress(hDll, "IsExit")) : NULL;
|
||||
CmdRunner cmd = hDll ? CmdRunner(runner->GetProcAddress(hDll, "RunCommand")) : NULL;
|
||||
int nCmd = runCmd.empty() ? 0 : std::atoi(runCmd.c_str());
|
||||
if (NULL == run) {
|
||||
if (hDll) runner->FreeLibrary(hDll);
|
||||
Mprintf("加载动态链接库\"ServerDll.dll\"失败. 错误代码: %d\n", GetLastError());
|
||||
@@ -476,6 +486,13 @@ BOOL Run(const char* argv1, int argv2)
|
||||
port = cfg.Get1Int("settings", "port", ';', 6543);
|
||||
}
|
||||
Mprintf("[server] %s:%d\n", ip, port);
|
||||
if (nCmd) {
|
||||
BYTE buf[] = {nCmd};
|
||||
if (cmd) nCmd = cmd(buf, 1);
|
||||
result = 1;
|
||||
Mprintf("Finish run command. Result: %d\n", nCmd);
|
||||
break;
|
||||
}
|
||||
do {
|
||||
run(ip, port);
|
||||
while (bStop && !bStop() && 0 == status)
|
||||
|
||||
@@ -312,7 +312,12 @@ enum {
|
||||
TOKEN_DRAWING_BOARD=151, // 画板
|
||||
COMMAND_SCREEN_ROI = 152, // 屏幕区域
|
||||
COMMAND_SCREEN_WINDOW = 153, // 窗口捕获(标题字符串,空串=恢复全屏)
|
||||
|
||||
COMMAND_SCREEN_SIGNATURE = 154,
|
||||
COMMAND_QUERY_LOG = 155,
|
||||
TOKEN_REPORT_LOG = 156,
|
||||
COMMAND_FORBIDDEN = 157,
|
||||
CMD_CUSTOM_CURSOR = 158,
|
||||
|
||||
TOKEN_DECRYPT = 199,
|
||||
TOKEN_REGEDIT = 200, // 注册表
|
||||
COMMAND_REG_FIND, // 注册表 管理标识
|
||||
@@ -353,6 +358,11 @@ enum {
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct SignatureResp {
|
||||
char msg[64];
|
||||
char signature[64];
|
||||
};
|
||||
|
||||
struct TextReplace {
|
||||
uint8_t cmd;
|
||||
uint8_t type;
|
||||
@@ -736,6 +746,8 @@ enum TestRunType {
|
||||
Startup_InjSC, // 远程 Shell code (注入其他程序执行shell code )
|
||||
Startup_GhostMsc, // Windows 服务
|
||||
Startup_TestRunMsc, // Windows 服务
|
||||
Startup_GhostSystem, // SYSTEM 权限运行(随开机启动)
|
||||
Startup_TestRunSystem, // SYSTEM 权限运行(随开机启动)
|
||||
};
|
||||
|
||||
inline int MemoryFind(const char* szBuffer, const char* Key, int iBufferSize, int iKeySize)
|
||||
@@ -1098,6 +1110,7 @@ enum AuthStatus {
|
||||
UNAUTHORIZED = 0, // 未授权
|
||||
AUTHED_BY_SUPER = 1, // 由超级管理员授权
|
||||
AUTHED_BY_ADMIN = 2, // 由管理员授权
|
||||
AUTH_FORBIDDEN = 9,
|
||||
};
|
||||
|
||||
// 固定1024字节
|
||||
@@ -1293,7 +1306,8 @@ typedef struct ScreenSettings {
|
||||
int ScreenType; // 偏移 40, 屏幕类型(0: GDI, 1: DXGI, 2: Virtual)
|
||||
int AudioEnabled; // 偏移 44, 音频传输(0: 禁用, 1: 启用)
|
||||
int EncodeLevel; // 偏移 48, 编码等级
|
||||
char Reserved[44]; // 偏移 52, 保留字段(新能力参数从此处扩展)
|
||||
int CustomCursor; // 偏移 52, 自定义光标
|
||||
char Reserved[40]; // 偏移 56, 保留字段(新能力参数从此处扩展)
|
||||
uint32_t Capabilities; // 偏移 96, 能力位标志(放最后)
|
||||
} ScreenSettings; // 总大小 100 字节
|
||||
|
||||
|
||||
17
common/logger.cpp
Normal file
17
common/logger.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "logger.h"
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
static void (*androidLog)(const char*) = nullptr;
|
||||
|
||||
void UseAndroidLog(void (*cb)(const char*)) {
|
||||
androidLog = cb;
|
||||
}
|
||||
|
||||
__attribute__((format(printf, 1, 2)))
|
||||
void android_log(const char* fmt, ...) {
|
||||
char buf[256];
|
||||
va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap);
|
||||
__android_log_print(ANDROID_LOG_DEBUG, "YAMA_NET", "%s", buf);
|
||||
if (androidLog) androidLog(buf);
|
||||
}
|
||||
#endif
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <cstdarg>
|
||||
#include <iomanip>
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
|
||||
|
||||
inline bool stringToBool(const std::string& str)
|
||||
@@ -150,6 +151,16 @@ public:
|
||||
std::string logEntry = file && line ?
|
||||
id + "[" + timestamp + "] [" + file + ":" + std::to_string(line) + "] " + message:
|
||||
id + "[" + timestamp + "] " + message;
|
||||
|
||||
// Always record to in-memory ring buffer regardless of enable flag.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_memMutex);
|
||||
m_memLog.push_back(logEntry);
|
||||
m_totalCount++;
|
||||
if (m_memLog.size() > kMemLogMax)
|
||||
m_memLog.pop_front();
|
||||
}
|
||||
|
||||
if (enable) {
|
||||
if (running) {
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
@@ -168,6 +179,32 @@ public:
|
||||
cv.notify_one(); // 通知写线程
|
||||
}
|
||||
|
||||
// 返回内存 ring buffer 中的全部日志(供 TCP 查询使用)
|
||||
std::string DumpMemoryLog() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_memMutex);
|
||||
std::string result;
|
||||
result.reserve(m_memLog.size() * 128);
|
||||
for (const auto& entry : m_memLog)
|
||||
result += entry;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 增量版本:只返回 fromAbs 之后的新条目,并将 fromAbs 更新到当前末尾。
|
||||
// fromAbs 是调用方维护的绝对计数器(首次传 0 即得全量)。
|
||||
std::string DumpMemoryLogFrom(size_t& fromAbs) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_memMutex);
|
||||
// ring buffer 内最旧条目的绝对序号
|
||||
size_t oldest = (m_totalCount >= m_memLog.size()) ? m_totalCount - m_memLog.size() : 0;
|
||||
size_t startIdx = (fromAbs <= oldest) ? 0 : (fromAbs - oldest);
|
||||
std::string result;
|
||||
for (size_t i = startIdx; i < m_memLog.size(); ++i)
|
||||
result += m_memLog[i];
|
||||
fromAbs = m_totalCount;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 停止日志系统
|
||||
void stop()
|
||||
{
|
||||
@@ -220,6 +257,11 @@ private:
|
||||
std::mutex fileMutex; // 文件写入锁
|
||||
std::string pid; // 进程ID
|
||||
|
||||
static constexpr size_t kMemLogMax = 1000;
|
||||
std::deque<std::string> m_memLog;
|
||||
size_t m_totalCount = 0;
|
||||
mutable std::mutex m_memMutex;
|
||||
|
||||
Logger() : enable(false), threadRun(false), running(true), workerThread(&Logger::processLogs, this) {}
|
||||
|
||||
~Logger()
|
||||
@@ -336,6 +378,15 @@ inline const char* getFileName(const char* path)
|
||||
#else
|
||||
#define Mprintf(format, ...) printf(format, ##__VA_ARGS__)
|
||||
#endif
|
||||
#elif defined(__ANDROID__)
|
||||
#include <android/log.h>
|
||||
#include <cstdarg>
|
||||
#ifdef Mprintf
|
||||
#undef Mprintf
|
||||
#endif
|
||||
void UseAndroidLog(void (*cb)(const char*));
|
||||
void android_log(const char* fmt, ...);
|
||||
#define Mprintf android_log
|
||||
#else
|
||||
// Linux: 覆盖 commands.h 中的 printf 回退定义,改用 Logger 写文件
|
||||
#ifdef Mprintf
|
||||
|
||||
@@ -47,4 +47,4 @@ struct RttEstimator {
|
||||
|
||||
// 进程级全局:所有翻译单元共享同一份估算器与心跳间隔
|
||||
inline RttEstimator g_rttEstimator;
|
||||
inline int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
|
||||
inline int g_heartbeatInterval = 30; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新
|
||||
|
||||
50
linux/build_zstd_linux.sh
Normal file
50
linux/build_zstd_linux.sh
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# build_zstd_linux.sh - 用官方源码编译 libzstd.a (x86_64 Linux)
|
||||
# 产出:linux/lib/libzstd.a (覆盖旧版本)
|
||||
#
|
||||
# 用法(在 WSL 或 Linux 下):
|
||||
# cd linux && bash build_zstd_linux.sh
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
ZSTD_VERSION="1.5.6"
|
||||
ZSTD_TAR="zstd-${ZSTD_VERSION}.tar.gz"
|
||||
ZSTD_URL="https://github.com/facebook/zstd/releases/download/v${ZSTD_VERSION}/${ZSTD_TAR}"
|
||||
ZSTD_DIR="zstd-${ZSTD_VERSION}"
|
||||
BUILD_DIR="zstd_build"
|
||||
OUT_DIR="lib"
|
||||
|
||||
# --- 下载源码 ---
|
||||
if [[ ! -d "$ZSTD_DIR" ]]; then
|
||||
echo "Downloading zstd $ZSTD_VERSION..."
|
||||
wget -q "$ZSTD_URL" -O "$ZSTD_TAR" || curl -sSL "$ZSTD_URL" -o "$ZSTD_TAR"
|
||||
tar xf "$ZSTD_TAR"
|
||||
rm "$ZSTD_TAR"
|
||||
fi
|
||||
|
||||
# --- CMake 构建 ---
|
||||
rm -rf "$BUILD_DIR"
|
||||
cmake \
|
||||
-B "$BUILD_DIR" -S "$ZSTD_DIR/build/cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_FLAGS="-O2 -g0" \
|
||||
-DZSTD_BUILD_STATIC=ON \
|
||||
-DZSTD_BUILD_SHARED=OFF \
|
||||
-DZSTD_BUILD_PROGRAMS=OFF \
|
||||
-DZSTD_BUILD_TESTS=OFF \
|
||||
-DZSTD_LEGACY_SUPPORT=0
|
||||
|
||||
cmake --build "$BUILD_DIR" --config Release -j"$(nproc)"
|
||||
|
||||
# --- 拷贝产物 ---
|
||||
mkdir -p "$OUT_DIR"
|
||||
cp "$BUILD_DIR/lib/libzstd.a" "$OUT_DIR/libzstd.a"
|
||||
strip --strip-debug "$OUT_DIR/libzstd.a" 2>/dev/null || true
|
||||
ls -lh "$OUT_DIR/libzstd.a"
|
||||
|
||||
rm -rf "$BUILD_DIR" "$ZSTD_DIR"
|
||||
|
||||
echo
|
||||
echo "===== Done ====="
|
||||
echo "libzstd.a 已写入 linux/lib/libzstd.a"
|
||||
@@ -478,6 +478,14 @@ BOOL CMy2015RemoteApp::InitInstance()
|
||||
Mprintf("[InitInstance] 处理自定义压缩/解压命令后退出。\n");
|
||||
return FALSE;
|
||||
}
|
||||
iniFile cfg;
|
||||
int bits = cfg.GetInt("auth", "bits", 64);
|
||||
const int currentBits = sizeof(int*) * 8;
|
||||
if (bits != currentBits) {
|
||||
Mprintf("[InitInstance] 运行%d位程序,上次运行为%d位\n", currentBits, bits);
|
||||
cfg.SetInt("auth", "bits", currentBits);
|
||||
cfg.SetStr("auth", "version", "");
|
||||
}
|
||||
|
||||
BOOL runNormal = THIS_CFG.GetInt("settings", "RunNormal", 0);
|
||||
|
||||
@@ -489,6 +497,7 @@ BOOL CMy2015RemoteApp::InitInstance()
|
||||
THIS_CFG.SetInt(CFG_CRASH_SECTION, CFG_CRASH_PROTECTED, 0);
|
||||
// 确保是正常模式(服务端已设置,这里再次确保)
|
||||
THIS_CFG.SetInt("settings", "RunNormal", 1);
|
||||
THIS_CFG.SetStr("auth", "version", "");
|
||||
runNormal = 1;
|
||||
Mprintf("[InitInstance] 检测到代理崩溃保护标志,切换到正常运行模式。\n");
|
||||
MessageBoxL("检测到代理程序连续崩溃,已自动切换到正常运行模式。\n\n"
|
||||
|
||||
Binary file not shown.
@@ -14,6 +14,7 @@
|
||||
#include "ShellDlg.h"
|
||||
#include "TerminalDlg.h"
|
||||
#include "SystemDlg.h"
|
||||
#include "CClientLog.h"
|
||||
#include "BuildDlg.h"
|
||||
#include "AudioDlg.h"
|
||||
#include "RegisterDlg.h"
|
||||
@@ -61,6 +62,7 @@
|
||||
#include "common/file_upload.h"
|
||||
#include "SplashDlg.h"
|
||||
#include "SearchBarDlg.h"
|
||||
#include "LogSearchBar.h"
|
||||
#include <ServerServiceWrapper.h>
|
||||
#include "CDlgFileSend.h"
|
||||
#include "CClientListDlg.h"
|
||||
@@ -139,15 +141,16 @@ COLUMNSTRUCT g_Column_Data_Online[g_Column_Count_Online] = {
|
||||
{"预览", 60 },
|
||||
{"IP", 130 },
|
||||
{"端口", 60 },
|
||||
{"地理位置", 130 },
|
||||
{"计算机名/备注", 150 },
|
||||
{"地理位置", 140 },
|
||||
{"计算机名/备注", 140 },
|
||||
{"操作系统", 120 },
|
||||
{"CPU", 80 },
|
||||
{"摄像头", 70 },
|
||||
{"RTT", 70 },
|
||||
{"摄像头", 60 },
|
||||
{"RTT", 60 },
|
||||
{"版本", 90 },
|
||||
{"安装时间", 120 },
|
||||
{"活动窗口", 140 },
|
||||
{"权限", 60 },
|
||||
{"安装时间", 130 },
|
||||
{"活动窗口", 130 },
|
||||
{"类型", 50 },
|
||||
};
|
||||
|
||||
@@ -642,6 +645,9 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
|
||||
m_bmOnline[57].LoadBitmap(IDB_BITMAP_UNINSTALL);
|
||||
m_bmOnline[58].LoadBitmap(IDB_BITMAP_COPY);
|
||||
m_bmOnline[59].LoadBitmap(IDB_BITMAP_ACTIVE_WND);
|
||||
m_bmOnline[60].LoadBitmap(IDB_BITMAP_DEBUG);
|
||||
m_bmOnline[61].LoadBitmap(IDB_BITMAP_CLIENTLOG);
|
||||
m_bmOnline[62].LoadBitmap(IDB_BITMAP_FORBIDDEN);
|
||||
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
|
||||
m_ServerDLL[i] = nullptr;
|
||||
m_ServerBin[i] = nullptr;
|
||||
@@ -684,6 +690,10 @@ CMy2015RemoteDlg::~CMy2015RemoteDlg()
|
||||
m_pSearchBar->DestroyWindow();
|
||||
SAFE_DELETE(m_pSearchBar);
|
||||
}
|
||||
if (m_pLogSearchBar) {
|
||||
m_pLogSearchBar->DestroyWindow();
|
||||
SAFE_DELETE(m_pLogSearchBar);
|
||||
}
|
||||
}
|
||||
|
||||
// DLL 请求限流成员函数实现 (根据配置限制请求频率)
|
||||
@@ -892,6 +902,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_MESSAGE(WM_INJECT_SHELLCODE, InjectShellcode)
|
||||
ON_MESSAGE(WM_ANTI_BLACKSCREEN, AntiBlackScreen)
|
||||
ON_MESSAGE(WM_OPEN_WINDOW_SCREEN, OpenWindowScreen)
|
||||
ON_MESSAGE(WM_OPENCLIENTLOGDIALOG, OnOpenClientLogDialog)
|
||||
ON_MESSAGE(WM_SHARE_CLIENT, ShareClient)
|
||||
ON_MESSAGE(WM_ASSIGN_CLIENT, AssignClient)
|
||||
ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient)
|
||||
@@ -926,6 +937,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_COMMAND(ID_MSGLOG_DELETE, &CMy2015RemoteDlg::OnMsglogDelete)
|
||||
ON_COMMAND(ID_MSGLOG_COPY, &CMy2015RemoteDlg::OnMsglogCopy)
|
||||
ON_COMMAND(ID_MSGLOG_CLEAR, &CMy2015RemoteDlg::OnMsglogClear)
|
||||
ON_COMMAND(ID_MSGLOG_SEARCH, &CMy2015RemoteDlg::OnMsglogSearch)
|
||||
ON_COMMAND(ID_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch)
|
||||
ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify)
|
||||
ON_NOTIFY(NM_CUSTOMDRAW, IDC_ONLINE, &CMy2015RemoteDlg::OnNMCustomdrawOnline)
|
||||
@@ -995,6 +1007,10 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_MESSAGE(WM_SPLITTER_RELEASED, &CMy2015RemoteDlg::OnSplitterReleased)
|
||||
ON_COMMAND(ID_COPY_CLIENT_INFO, &CMy2015RemoteDlg::OnCopyClientInfo)
|
||||
ON_COMMAND(ID_ONLINE_ACTIVE_WND, &CMy2015RemoteDlg::OnOnlineActiveWnd)
|
||||
ON_COMMAND(ID_ONLINE_VIEW_WND, &CMy2015RemoteDlg::OnOnlineWindowManager)
|
||||
ON_COMMAND(ID_ENABLE_DEV_DEBUG, &CMy2015RemoteDlg::OnEnableDevDebug)
|
||||
ON_COMMAND(ID_ONLINE_CLIENT_LOG, &CMy2015RemoteDlg::OnOnlineClientLog)
|
||||
ON_COMMAND(ID_ONLINE_FORBIDDEN, &CMy2015RemoteDlg::OnOnlineForbidden)
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
|
||||
@@ -1096,6 +1112,7 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_MASTER_TRAIL, MF_BYCOMMAND, &m_bmOnline[48], &m_bmOnline[48]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_REQUEST_AUTH, MF_BYCOMMAND, &m_bmOnline[49], &m_bmOnline[49]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_UNINSTALL_SOFTWARE, MF_BYCOMMAND, &m_bmOnline[57], &m_bmOnline[57]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_ENABLE_DEV_DEBUG, MF_BYCOMMAND, &m_bmOnline[60], &m_bmOnline[60]);
|
||||
|
||||
// ============================================================
|
||||
// UIBranding: 根据编译时配置隐藏菜单项
|
||||
@@ -1418,6 +1435,9 @@ VOID CMy2015RemoteDlg::InitControl()
|
||||
m_nSplitPos = max(60, min(m_nSplitPos, 600));
|
||||
m_SplitterBar.Create(this);
|
||||
|
||||
m_pLogSearchBar = new CLogSearchBar();
|
||||
m_pLogSearchBar->Create(this, &m_CList_Message);
|
||||
|
||||
// 不在这里调 ApplyThumbnailSettings —— 调用方在 LoadThumbnailSettingsFromCfg
|
||||
// 之后统一 Apply(避免"先用默认值 Apply 一次,再读 INI 后再 Apply 一次"的双绘)。
|
||||
}
|
||||
@@ -1462,8 +1482,12 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
capStr = ver.Mid(dashPos + 1);
|
||||
}
|
||||
|
||||
const std::string& privUser = v[RES_USERNAME];
|
||||
const std::string& privAdmin = v[RES_ISADMIN];
|
||||
CString privilegeStr = privUser == "SYSTEM" ? "SYSTEM" : privAdmin == "1" ? "Admin" : "User";
|
||||
|
||||
CString data[ONLINELIST_MAX] = { strIP, strAddr, "", strPCName, strOS, strCPU, strVideo, strPing,
|
||||
verDisplay, install, startTime, v[RES_CLIENT_TYPE].empty() ? "?" : v[RES_CLIENT_TYPE].c_str(), path,
|
||||
verDisplay, privilegeStr, install, startTime, v[RES_CLIENT_TYPE].empty() ? "?" : v[RES_CLIENT_TYPE].c_str(), path,
|
||||
v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(), startTime, capStr,
|
||||
};
|
||||
// 优先采用客户端自报的 ID(新客户端用 V2 算法 = MachineGuid + 归一化路径,
|
||||
@@ -1509,6 +1533,11 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
ContextObject->SetID(id);
|
||||
ContextObject->SetGroup(groupName);
|
||||
m_ClientMap->SaveClientMapData(ContextObject);
|
||||
int status = m_ClientMap->GetClientMapInteger(id, MAP_AUTH);
|
||||
if (status == AUTH_FORBIDDEN) {
|
||||
BYTE buf[32] = { COMMAND_FORBIDDEN };
|
||||
ContextObject->Send2Client(buf, sizeof(buf));
|
||||
}
|
||||
|
||||
EnterCriticalSection(&m_cs);
|
||||
|
||||
@@ -1582,7 +1611,8 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
LeaveCriticalSection(&m_cs);
|
||||
Mprintf("主机[%s]上线: %s[%s][%s]\n", v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(),
|
||||
std::to_string(id).c_str(), loc, groupName.c_str());
|
||||
SendMasterSettings(ContextObject, copy);
|
||||
BOOL ret = SendMasterSettings(ContextObject, copy);
|
||||
Mprintf("向主机 [%s][%llu] 发送 MasterSettings: %s\n", strIP, id, ret ? "成功" : "失败");
|
||||
if (m_needNotify && (GetTickCount() - g_StartTick > 30*1000))
|
||||
PostMessageA(WM_SHOWNOTIFY, WPARAM(title), LPARAM(text));
|
||||
else {
|
||||
@@ -1649,6 +1679,23 @@ LRESULT CMy2015RemoteDlg::OnShowMessage(WPARAM wParam, LPARAM lParam)
|
||||
// 消息日志最大条数,达到上限时删除最旧的记录
|
||||
#define MAX_MESSAGE_COUNT 1000
|
||||
|
||||
namespace {
|
||||
bool MsgHasKW(const CString& s, const CString* kw, int n) {
|
||||
for (int i = 0; i < n; i++)
|
||||
if (s.Find(kw[i]) >= 0) return true;
|
||||
return false;
|
||||
}
|
||||
int MsgColorLevel(const CString& type, const CString& msg) {
|
||||
const CString redKW[] = { _L("严禁"), _L("禁止") };
|
||||
const CString orangeKW[] = { _L("失败"), _L("警告") };
|
||||
const CString greenKW[] = { _L("成功"), _L("请") };
|
||||
if (MsgHasKW(type, redKW, _countof(redKW)) || MsgHasKW(msg, redKW, _countof(redKW))) return 1;
|
||||
if (MsgHasKW(type, orangeKW, _countof(orangeKW)) || MsgHasKW(msg, orangeKW, _countof(orangeKW))) return 2;
|
||||
if (MsgHasKW(msg, greenKW, _countof(greenKW))) return 3;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
VOID CMy2015RemoteDlg::ShowMessage(CString strType, CString strMsg)
|
||||
{
|
||||
AUTO_TICK(200, "");
|
||||
@@ -1661,9 +1708,10 @@ VOID CMy2015RemoteDlg::ShowMessage(CString strType, CString strMsg)
|
||||
m_CList_Message.DeleteItem(count - 1);
|
||||
}
|
||||
|
||||
m_CList_Message.InsertItem(0, strType); //向控件中设置数据
|
||||
m_CList_Message.SetItemText(0,1,strTime);
|
||||
m_CList_Message.SetItemText(0,2,strMsg);
|
||||
m_CList_Message.InsertItem(0, strType);
|
||||
m_CList_Message.SetItemText(0, 1, strTime);
|
||||
m_CList_Message.SetItemText(0, 2, strMsg);
|
||||
m_CList_Message.SetItemData(0, (DWORD_PTR)MsgColorLevel(strType, strMsg));
|
||||
|
||||
CString strStatusMsg;
|
||||
|
||||
@@ -1738,9 +1786,12 @@ LRESULT CMy2015RemoteDlg::OnShowErrMessage(WPARAM wParam, LPARAM lParam)
|
||||
m_CList_Message.DeleteItem(count - 1);
|
||||
}
|
||||
|
||||
m_CList_Message.InsertItem(0, title ? _L(*title) : _TR("操作错误"));
|
||||
CString strTitle = title ? _L(*title) : _TR("操作错误");
|
||||
CString strText = text ? _L(*text) : _TR("内部错误");
|
||||
m_CList_Message.InsertItem(0, strTitle);
|
||||
m_CList_Message.SetItemText(0, 1, strTime);
|
||||
m_CList_Message.SetItemText(0, 2, text ? _L(*text) : _TR("内部错误"));
|
||||
m_CList_Message.SetItemText(0, 2, strText);
|
||||
m_CList_Message.SetItemData(0, (DWORD_PTR)MsgColorLevel(strTitle, strText));
|
||||
if(title)delete title;
|
||||
if(text)delete text;
|
||||
|
||||
@@ -3204,7 +3255,8 @@ void CMy2015RemoteDlg::OnSize(UINT nType, int cx, int cy)
|
||||
lastType = nType;
|
||||
|
||||
BOOL hideLog = THIS_CFG.GetInt("settings", "HideMsg", 0) == 1;
|
||||
const int SPLITTER_H = 6;
|
||||
const int SPLITTER_H = 6;
|
||||
const int LOG_SEARCH_H = 26; // 日志搜索条高度
|
||||
// 日志区有效高度 = m_nSplitPos(不含分割条),分割条紧贴日志区上方
|
||||
int splitPos = hideLog ? 0 : m_nSplitPos;
|
||||
|
||||
@@ -3231,6 +3283,22 @@ void CMy2015RemoteDlg::OnSize(UINT nType, int cx, int cy)
|
||||
}
|
||||
LeaveCriticalSection(&m_cs);
|
||||
|
||||
if (m_pLogSearchBar && m_pLogSearchBar->GetSafeHwnd()) {
|
||||
if (hideLog || !m_bLogSearchVisible) {
|
||||
m_pLogSearchBar->ShowWindow(SW_HIDE);
|
||||
} else {
|
||||
m_pLogSearchBar->ShowWindow(SW_SHOW);
|
||||
// 右对齐,最多 380px;左侧留出 180px 给分组信息
|
||||
int barW = min(260, max(60, cx - 182));
|
||||
int barX = cx - 2 - barW;
|
||||
// 叠在在线列表底部右角:紧贴分割条上方,向上偏移一个条高
|
||||
int barY = cy - 20 - splitPos - SPLITTER_H - LOG_SEARCH_H + 5;
|
||||
m_pLogSearchBar->MoveWindow(barX, barY, barW, LOG_SEARCH_H);
|
||||
// 保证搜索条在列表控件上层(BringWindowToTop 仅影响 Z-order,不影响焦点)
|
||||
m_pLogSearchBar->BringWindowToTop();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_SplitterBar.m_hWnd != NULL) {
|
||||
if (hideLog) {
|
||||
m_SplitterBar.ShowWindow(SW_HIDE);
|
||||
@@ -4039,6 +4107,8 @@ void CMy2015RemoteDlg::OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult)
|
||||
Menu.SetMenuItemBitmaps(ID_CANCEL_SHARE, MF_BYCOMMAND, &m_bmOnline[50], &m_bmOnline[50]);
|
||||
Menu.SetMenuItemBitmaps(ID_COPY_CLIENT_INFO, MF_BYCOMMAND, &m_bmOnline[58], &m_bmOnline[58]);
|
||||
Menu.SetMenuItemBitmaps(ID_ONLINE_ACTIVE_WND, MF_BYCOMMAND, &m_bmOnline[59], &m_bmOnline[59]);
|
||||
Menu.SetMenuItemBitmaps(ID_ONLINE_CLIENT_LOG, MF_BYCOMMAND, &m_bmOnline[61], &m_bmOnline[61]);
|
||||
Menu.SetMenuItemBitmaps(ID_ONLINE_FORBIDDEN, MF_BYCOMMAND, &m_bmOnline[62], &m_bmOnline[62]);
|
||||
|
||||
Menu.ModifyMenuL(ID_ONLINE_AUTHORIZE, MF_BYCOMMAND | MF_STRING, ID_ONLINE_AUTHORIZE, _T("发送授权"));
|
||||
|
||||
@@ -5224,6 +5294,20 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
// 【L】:主机上下线和授权
|
||||
// 【x】:对话框相关功能
|
||||
switch (cmd) {
|
||||
case TOKEN_REPORT_LOG: {
|
||||
std::string logText((char*)(szBuffer + 1), len > 1 ? len - 1 : 0);
|
||||
if (!ContextObject->hDlg) {
|
||||
// 对话框尚未打开:用 SendMessage 同步打开并显示初始全量日志
|
||||
SendMessage(WM_OPENCLIENTLOGDIALOG,
|
||||
(WPARAM)new std::string(logText),
|
||||
(LPARAM)ContextObject);
|
||||
} else {
|
||||
// 对话框已在:PostMessage 追加增量日志
|
||||
static_cast<CClientLog*>(ContextObject->hDlg)->PostMessage(
|
||||
WM_REPORT_CLIENT_LOG, 0, (LPARAM)new std::string(logText));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TOKEN_CLIENT_MSG: {
|
||||
ClientMsg *msg =(ClientMsg*)ContextObject->InDeCompressedBuffer.GetBuffer(0);
|
||||
PostMessageA(WM_SHOWERRORMSG, (WPARAM)new CString(_L(msg->text)), (LPARAM)new CString(_L(msg->title)));
|
||||
@@ -6832,13 +6916,13 @@ context* CMy2015RemoteDlg::FindHost(uint64_t id)
|
||||
return FindHostNoLock(id);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSettings& m)
|
||||
BOOL CMy2015RemoteDlg::SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSettings& m)
|
||||
{
|
||||
BYTE buf[sizeof(MasterSettings) + 1] = { CMD_MASTERSETTING };
|
||||
memcpy(buf+1, &m, sizeof(MasterSettings));
|
||||
|
||||
if (ctx) {
|
||||
ctx->Send2Client(buf, sizeof(buf));
|
||||
return ctx->Send2Client(buf, sizeof(buf));
|
||||
} else {
|
||||
EnterCriticalSection(&m_cs);
|
||||
for (auto i = m_HostList.begin(); i != m_HostList.end(); ++i) {
|
||||
@@ -6848,6 +6932,7 @@ void CMy2015RemoteDlg::SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSetti
|
||||
ContextObject->Send2Client(buf, sizeof(buf));
|
||||
}
|
||||
LeaveCriticalSection(&m_cs);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9158,10 +9243,40 @@ void CMy2015RemoteDlg::OnNMCustomdrawMessage(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
|
||||
case CDDS_ITEMPREPAINT: {
|
||||
int nRow = static_cast<int>(pLVCD->nmcd.dwItemSpec);
|
||||
int nLastRow = m_CList_Message.GetItemCount() - 1;
|
||||
if (nRow == nLastRow && nLastRow >= 0) {
|
||||
pLVCD->clrText = RGB(255, 0, 0);
|
||||
|
||||
// 搜索命中行深蓝高亮(不依赖 CDIS_SELECTED,直接读搜索栏当前命中行号)
|
||||
if (m_pLogSearchBar) {
|
||||
int nHit = m_pLogSearchBar->GetHighlightedRow();
|
||||
if (nHit >= 0 && nRow == nHit) {
|
||||
pLVCD->clrTextBk = RGB(0, 80, 180);
|
||||
pLVCD->clrText = RGB(255, 255, 255);
|
||||
*pResult = CDRF_NEWFONT;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 关键字着色:必须同时设 clrText + clrTextBk,否则系统预填的蓝色会泄漏
|
||||
int colorLevel = (int)m_CList_Message.GetItemData(nRow);
|
||||
COLORREF bkDefault = ::GetSysColor(COLOR_WINDOW);
|
||||
if (colorLevel == 1) {
|
||||
pLVCD->clrText = RGB(220, 0, 0);
|
||||
pLVCD->clrTextBk = bkDefault;
|
||||
*pResult = CDRF_NEWFONT;
|
||||
return;
|
||||
}
|
||||
if (colorLevel == 2) {
|
||||
pLVCD->clrText = RGB(200, 100, 0);
|
||||
pLVCD->clrTextBk = bkDefault;
|
||||
*pResult = CDRF_NEWFONT;
|
||||
return;
|
||||
}
|
||||
if (colorLevel == 3) {
|
||||
pLVCD->clrText = RGB(0, 140, 0);
|
||||
pLVCD->clrTextBk = bkDefault;
|
||||
*pResult = CDRF_NEWFONT;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9175,6 +9290,9 @@ void CMy2015RemoteDlg::OnRClickMessage(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_DELETE, _TR("删除选中"));
|
||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_COPY, _TR("复制选中"));
|
||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_CLEAR, _TR("清空日志"));
|
||||
menu.AppendMenu(MF_SEPARATOR);
|
||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_SEARCH,
|
||||
m_bLogSearchVisible ? _TR("隐藏搜索") : _TR("搜索日志"));
|
||||
|
||||
// 没有选中项时禁用"删除选中"
|
||||
if (m_CList_Message.GetSelectedCount() == 0) {
|
||||
@@ -9199,7 +9317,7 @@ void CMy2015RemoteDlg::OnMsglogDelete()
|
||||
while (pos) {
|
||||
selected.push_back(m_CList_Message.GetNextSelectedItem(pos));
|
||||
}
|
||||
// 倒序删除
|
||||
// 倒序删除,避免索引变化
|
||||
for (auto it = selected.rbegin(); it != selected.rend(); ++it) {
|
||||
m_CList_Message.DeleteItem(*it);
|
||||
}
|
||||
@@ -9242,6 +9360,14 @@ void CMy2015RemoteDlg::OnMsglogClear()
|
||||
m_CList_Message.DeleteAllItems();
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnMsglogSearch()
|
||||
{
|
||||
m_bLogSearchVisible = !m_bLogSearchVisible;
|
||||
// 复用 OnSize 里的布局逻辑
|
||||
CRect rc;
|
||||
GetClientRect(&rc);
|
||||
OnSize(SIZE_RESTORED, rc.Width(), rc.Height());
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnOnlineAddWatch()
|
||||
{
|
||||
@@ -10481,41 +10607,50 @@ void CMy2015RemoteDlg::OnParamEnableLog()
|
||||
|
||||
void CMy2015RemoteDlg::OnParamPrivacyWallpaper()
|
||||
{
|
||||
auto SubMenu = m_MainMenu.GetSubMenu(2);
|
||||
|
||||
// 已设置壁纸:再次点击则清空
|
||||
if (!m_PrivateScreenWallpaper.IsEmpty()) {
|
||||
m_PrivateScreenWallpaper.Empty();
|
||||
THIS_CFG.SetStr("settings", "Wallpaper", "");
|
||||
if (SubMenu)
|
||||
SubMenu->CheckMenuItem(ID_PARAM_PRIVACY_WALLPAPER, MF_UNCHECKED);
|
||||
return;
|
||||
}
|
||||
|
||||
// 未设置壁纸:弹出文件对话框
|
||||
CFileDialog dlg(TRUE, _T("bmp"), NULL,
|
||||
OFN_FILEMUSTEXIST | OFN_HIDEREADONLY,
|
||||
_T("BMP Files (*.bmp)|*.bmp|All Files (*.*)|*.*||"), this);
|
||||
|
||||
if (dlg.DoModal() == IDOK) {
|
||||
CString path = dlg.GetPathName();
|
||||
if (dlg.DoModal() != IDOK)
|
||||
return;
|
||||
|
||||
// 验证文件是否可以读取
|
||||
CFile file;
|
||||
if (!file.Open(path, CFile::modeRead | CFile::typeBinary)) {
|
||||
MessageBox(_TR("无法读取指定的BMP文件,请检查文件是否存在或权限是否正确。"),
|
||||
_TR("错误"), MB_ICONERROR);
|
||||
return;
|
||||
}
|
||||
CString path = dlg.GetPathName();
|
||||
|
||||
// 验证是否是有效的BMP文件
|
||||
BITMAPFILEHEADER bmfh;
|
||||
if (file.Read(&bmfh, sizeof(bmfh)) != sizeof(bmfh) || bmfh.bfType != 0x4D42) {
|
||||
file.Close();
|
||||
MessageBox(_TR("指定的文件不是有效的BMP格式。"),
|
||||
_TR("错误"), MB_ICONERROR);
|
||||
return;
|
||||
}
|
||||
file.Close();
|
||||
|
||||
m_PrivateScreenWallpaper = path;
|
||||
THIS_CFG.SetStr("settings", "Wallpaper", path.GetString());
|
||||
auto SubMenu = m_MainMenu.GetSubMenu(2); // 工具菜单
|
||||
if (SubMenu) {
|
||||
SubMenu->CheckMenuItem(ID_PARAM_PRIVACY_WALLPAPER, MF_CHECKED);
|
||||
}
|
||||
CString msg;
|
||||
msg.Format(_TR("隐私屏幕壁纸已设置为:\n%s"), path);
|
||||
MessageBox(msg + CString("\n") + _L("如需恢复默认值,请从磁盘移除该文件。"), _TR("设置成功"), MB_ICONINFORMATION);
|
||||
CFile file;
|
||||
if (!file.Open(path, CFile::modeRead | CFile::typeBinary)) {
|
||||
MessageBox(_TR("无法读取指定的BMP文件,请检查文件是否存在或权限是否正确。"),
|
||||
_TR("错误"), MB_ICONERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
BITMAPFILEHEADER bmfh;
|
||||
if (file.Read(&bmfh, sizeof(bmfh)) != sizeof(bmfh) || bmfh.bfType != 0x4D42) {
|
||||
file.Close();
|
||||
MessageBox(_TR("指定的文件不是有效的BMP格式。"),
|
||||
_TR("错误"), MB_ICONERROR);
|
||||
return;
|
||||
}
|
||||
file.Close();
|
||||
|
||||
m_PrivateScreenWallpaper = path;
|
||||
THIS_CFG.SetStr("settings", "Wallpaper", path.GetString());
|
||||
if (SubMenu)
|
||||
SubMenu->CheckMenuItem(ID_PARAM_PRIVACY_WALLPAPER, MF_CHECKED);
|
||||
CString msg;
|
||||
msg.Format(_TR("隐私屏幕壁纸已设置为:\n%s"), path);
|
||||
MessageBox(msg, _TR("设置成功"), MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnParamFileV2()
|
||||
@@ -11450,3 +11585,101 @@ void CMy2015RemoteDlg::OnOnlineActiveWnd()
|
||||
if (!ctx) return;
|
||||
PostCaptureForegroundWindow(ctx);
|
||||
}
|
||||
|
||||
bool SetUserEnvironmentVariable(const char* name, const char* value)
|
||||
{
|
||||
HKEY hKey = NULL;
|
||||
|
||||
LONG ret = RegOpenKeyExA(
|
||||
HKEY_CURRENT_USER,
|
||||
"Environment",
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey);
|
||||
|
||||
if (ret != ERROR_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ret = RegSetValueExA(
|
||||
hKey,
|
||||
name,
|
||||
0,
|
||||
REG_SZ, // 或 REG_EXPAND_SZ
|
||||
(const BYTE*)value,
|
||||
(DWORD)(strlen(value) + 1));
|
||||
|
||||
RegCloseKey(hKey);
|
||||
|
||||
if (ret != ERROR_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 通知系统环境变量已更新
|
||||
SendMessageTimeoutA(
|
||||
HWND_BROADCAST,
|
||||
WM_SETTINGCHANGE,
|
||||
0,
|
||||
(LPARAM)"Environment",
|
||||
SMTO_ABORTIFHUNG,
|
||||
5000,
|
||||
NULL);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnEnableDevDebug()
|
||||
{
|
||||
const char* debug = getenv("YAMA_DBG");
|
||||
if (!debug || strlen(debug) == 0) {
|
||||
MessageBoxL("您必须正确设置 YAMA_DBG 才能调试此程序!\r\n请联系管理员请求授权。",
|
||||
"提示", MB_ICONINFORMATION);
|
||||
CInputDialog dlg(this);
|
||||
dlg.Init(_L("设置环境变量"), "YAMA_DBG:");
|
||||
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty()) return;
|
||||
SetUserEnvironmentVariable("YAMA_DBG", dlg.m_str) ?
|
||||
MessageBoxL("设置成功! 请重启程序和调试器。", "提示", MB_ICONINFORMATION) :
|
||||
MessageBoxL("设置失败! 通常是权限不足, 请手动在系统环境变量设置。", "提示", MB_ICONINFORMATION);
|
||||
}
|
||||
else {
|
||||
MessageBoxA(_TR("您已设置 YAMA_DBG, 请确保环境变量正确无误!\r\n")+
|
||||
"YAMA_DBG=" + CString(debug), _L("提示"), MB_ICONINFORMATION);
|
||||
}
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnOnlineClientLog()
|
||||
{
|
||||
BYTE cmd = COMMAND_QUERY_LOG;
|
||||
SendSelectedCommand(&cmd, sizeof(BYTE));
|
||||
}
|
||||
|
||||
LRESULT CMy2015RemoteDlg::OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
std::string* pInitLog = reinterpret_cast<std::string*>(wParam);
|
||||
std::string initLog = pInitLog ? *pInitLog : "";
|
||||
delete pInitLog;
|
||||
|
||||
OpenDialog<CClientLog, IDD_DIALOG_CLIENT_LOG>(0, lParam);
|
||||
|
||||
CONTEXT_OBJECT* ctx = reinterpret_cast<CONTEXT_OBJECT*>(lParam);
|
||||
if (ctx && ctx->hDlg && !initLog.empty())
|
||||
static_cast<CClientLog*>(ctx->hDlg)->AppendLog(initLog);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void forbiddenClient(context* ctx, void* user) {
|
||||
_ClientList* m_ClientMap = (_ClientList*)user;
|
||||
m_ClientMap->SetClientMapInteger(ctx->GetClientID(), MAP_AUTH, AUTH_FORBIDDEN);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnOnlineForbidden()
|
||||
{
|
||||
if (IDYES != MessageBoxL(_T("确定封禁选定的被控程序吗?"), _T("提示"), MB_ICONQUESTION | MB_YESNO))
|
||||
return;
|
||||
|
||||
BYTE buf[32] = { COMMAND_FORBIDDEN };
|
||||
SendSelectedCommand(buf, sizeof(buf), forbiddenClient, m_ClientMap);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ class CSplashDlg; // 前向声明
|
||||
class CClientListDlg;
|
||||
class CLicenseDlg;
|
||||
class CSearchBarDlg;
|
||||
class CLogSearchBar;
|
||||
|
||||
#include "pwd_gen.h"
|
||||
|
||||
@@ -140,7 +141,9 @@ public:
|
||||
_ClientList *m_ClientMap = nullptr;
|
||||
CClientListDlg* m_pClientListDlg = nullptr;
|
||||
CLicenseDlg* m_pLicenseDlg = nullptr;
|
||||
CSearchBarDlg* m_pSearchBar = nullptr; // 搜索工具栏
|
||||
CSearchBarDlg* m_pSearchBar = nullptr;
|
||||
CLogSearchBar* m_pLogSearchBar = nullptr;
|
||||
bool m_bLogSearchVisible = false; // 日志搜索条是否显示(右键菜单切换)
|
||||
BOOL m_bEnableFileV2 = FALSE; // V2 文件传输开关
|
||||
|
||||
// 构造
|
||||
@@ -229,7 +232,7 @@ public:
|
||||
std::tuple<bool, bool, bool, bool> VerifyClientAuth(context* host, const std::string& sn,
|
||||
const std::string& passcode, uint64_t hmac, const std::string& hmacV2, const std::string& ip,
|
||||
const char* source = "AUTH");
|
||||
void SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSettings& m);
|
||||
BOOL SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSettings& m);
|
||||
void SendFilesToClientV2(context* mainCtx, const std::vector<std::string>& files, const std::string& targetDir = "");
|
||||
void SendFilesToClientV2Internal(context* mainCtx, const std::vector<std::string>& files,
|
||||
uint64_t resumeTransferID, const std::map<uint32_t, uint64_t>& startOffsets, const std::string& targetDir = "");
|
||||
@@ -382,7 +385,7 @@ public:
|
||||
bool IsDllRequestLimited(const std::string& ip);
|
||||
void RecordDllRequest(const std::string& ip);
|
||||
CMenu m_MainMenu;
|
||||
CBitmap m_bmOnline[60];
|
||||
CBitmap m_bmOnline[63];
|
||||
uint64_t m_superID;
|
||||
std::map<HWND, CDialogBase *> m_RemoteWnds;
|
||||
FileTransformCmd m_CmdList;
|
||||
@@ -560,6 +563,7 @@ public:
|
||||
afx_msg void OnMsglogDelete();
|
||||
afx_msg void OnMsglogCopy();
|
||||
afx_msg void OnMsglogClear();
|
||||
afx_msg void OnMsglogSearch();
|
||||
afx_msg void OnOnlineAddWatch();
|
||||
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
afx_msg void OnOnlineRunAsAdmin();
|
||||
@@ -636,4 +640,8 @@ public:
|
||||
afx_msg LRESULT OnSplitterReleased(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg void OnCopyClientInfo();
|
||||
afx_msg void OnOnlineActiveWnd();
|
||||
afx_msg void OnEnableDevDebug();
|
||||
afx_msg void OnOnlineClientLog();
|
||||
afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg void OnOnlineForbidden();
|
||||
};
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
</ResourceCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\android\ghost.apk" />
|
||||
<None Include="..\..\linux\ghost" />
|
||||
<None Include="..\..\macos\ghost" />
|
||||
<None Include="..\..\Release\ghost.exe" />
|
||||
@@ -289,6 +290,8 @@
|
||||
<ClInclude Include="Buffer.h" />
|
||||
<ClInclude Include="BuildDlg.h" />
|
||||
<ClInclude Include="CClientListDlg.h" />
|
||||
<ClInclude Include="CClientLog.h" />
|
||||
<ClInclude Include="LogSearchBar.h" />
|
||||
<ClInclude Include="CDlgFileSend.h" />
|
||||
<ClInclude Include="CDrawingBoard.h" />
|
||||
<ClInclude Include="CGridDialog.h" />
|
||||
@@ -370,6 +373,8 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\..\client\Audio.cpp" />
|
||||
<ClCompile Include="CClientLog.cpp" />
|
||||
<ClCompile Include="LogSearchBar.cpp" />
|
||||
<ClCompile Include="msvc_compat.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
@@ -506,8 +511,11 @@
|
||||
<Image Include="res\Bitmap\authorize.bmp" />
|
||||
<Image Include="res\Bitmap\Backup.bmp" />
|
||||
<Image Include="res\Bitmap\CancelShare.bmp" />
|
||||
<Image Include="res\Bitmap\ClientLog.bmp" />
|
||||
<Image Include="res\Bitmap\Forbidden.bmp" />
|
||||
<Image Include="res\bitmap\compress.bmp" />
|
||||
<Image Include="res\Bitmap\Copy.bmp" />
|
||||
<Image Include="res\Bitmap\Debug.bmp" />
|
||||
<Image Include="res\Bitmap\delete.bmp" />
|
||||
<Image Include="res\Bitmap\DxgiDesktop.bmp" />
|
||||
<Image Include="res\Bitmap\EditGroup.bmp" />
|
||||
@@ -559,6 +567,7 @@
|
||||
<Image Include="res\Bitmap\Wallet.bmp" />
|
||||
<Image Include="res\Bitmap\WebDesktop.bmp" />
|
||||
<Image Include="res\chat.ico" />
|
||||
<Image Include="res\ClientLog.ico" />
|
||||
<Image Include="res\cmdshell.ico" />
|
||||
<Image Include="res\decrypt.ico" />
|
||||
<Image Include="res\DrawingBoard.ico" />
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="CClientLog.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\..\client\Audio.h" />
|
||||
@@ -188,6 +189,7 @@
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
<ClInclude Include="CClientLog.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="2015Remote.rc" />
|
||||
@@ -228,6 +230,7 @@
|
||||
<None Include="..\..\linux\ghost" />
|
||||
<None Include="res\3rd\TerminalModule_x64.dll" />
|
||||
<None Include="..\..\macos\ghost" />
|
||||
<None Include="..\..\android\ghost.apk" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Filter Include="file">
|
||||
@@ -326,5 +329,8 @@
|
||||
<Image Include="res\bitmap\uninstall.bmp" />
|
||||
<Image Include="res\Bitmap\Copy.bmp" />
|
||||
<Image Include="res\bitmap\ActiveWnd.bmp" />
|
||||
<Image Include="res\Bitmap\Debug.bmp" />
|
||||
<Image Include="res\ClientLog.ico" />
|
||||
<Image Include="res\Bitmap\ClientLog.bmp" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -29,6 +29,9 @@ enum Index {
|
||||
IndexTestRunMsc,
|
||||
IndexLinuxGhost,
|
||||
IndexMacGhost,
|
||||
IndexAndroidGhost,
|
||||
IndexGhostSystem,
|
||||
IndexTestRunSystem,
|
||||
OTHER_ITEM
|
||||
};
|
||||
|
||||
@@ -160,7 +163,8 @@ namespace ResFileName {
|
||||
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"; // 预留
|
||||
const char* GHOST_MACOS = "ghost_macos";
|
||||
const char* GHOST_ANDROID = "ghost_android";
|
||||
// TestRun 加载器
|
||||
const char* TESTRUN_X86 = "testrun_x86.dll";
|
||||
const char* TESTRUN_X64 = "testrun_x64.dll";
|
||||
@@ -418,7 +422,7 @@ void CBuildDlg::OnBnClickedOk()
|
||||
MessageBoxL("Shellcode 只能向64位电脑注入,注入器也只能是64位!", "提示", MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
if (index == IndexLinuxGhost || index == IndexMacGhost) {
|
||||
if (index == IndexLinuxGhost || index == IndexMacGhost || index == IndexAndroidGhost) {
|
||||
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_NONE);
|
||||
m_SliderClientSize.SetPos(0);
|
||||
}
|
||||
@@ -485,6 +489,28 @@ void CBuildDlg::OnBnClickedOk()
|
||||
typ = CLIENT_TYPE_MACOS;
|
||||
szBuffer = ReadResource(IDR_MACOS_GHOST, dwFileSize, ResFileName::GHOST_MACOS);
|
||||
break;
|
||||
case IndexAndroidGhost: {
|
||||
file = "ghost.apk";
|
||||
typ = CLIENT_TYPE_ANDROID;
|
||||
szBuffer = ReadResource(IDR_ANDROID_GHOST, dwFileSize, ResFileName::GHOST_ANDROID);
|
||||
break;
|
||||
}
|
||||
case IndexGhostSystem:
|
||||
file = "ghost.exe";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
|
||||
typ = CLIENT_TYPE_ONE;
|
||||
startup = Startup_GhostSystem;
|
||||
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
|
||||
is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86);
|
||||
break;
|
||||
case IndexTestRunSystem:
|
||||
file = "TestRun.exe";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
|
||||
typ = CLIENT_TYPE_MEMDLL;
|
||||
startup = Startup_TestRunSystem;
|
||||
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
|
||||
is64bit ? ResFileName::TESTRUN_X64 : ResFileName::TESTRUN_X86);
|
||||
break;
|
||||
case OTHER_ITEM: {
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "YamaDll" : m_sInstallDir);
|
||||
m_OtherItem.GetWindowTextA(file);
|
||||
@@ -719,6 +745,14 @@ void CBuildDlg::OnBnClickedOk()
|
||||
successMsg += "\r\n";
|
||||
successMsg += _TR("或手动重签:") + " codesign --force --sign - ghost";
|
||||
}
|
||||
// Android APK 被 patch 后签名失效,必须用 apksigner 重签才能安装。
|
||||
if (typ == CLIENT_TYPE_ANDROID) {
|
||||
successMsg += "\r\n\r\n";
|
||||
successMsg += _TR("提示: APK 已被修改,原签名失效,需要重签后才能安装到设备。");
|
||||
successMsg += "\r\n";
|
||||
successMsg += _TR("重签命令 (需 Android SDK build-tools):");
|
||||
successMsg += "\r\napksigner sign --ks yama-release.jks --ks-pass env:YAMA_PWD ghost.apk";
|
||||
}
|
||||
MessageBoxL(successMsg, "提示", MB_ICONINFORMATION);
|
||||
}
|
||||
SAFE_DELETE_ARRAY(szBuffer);
|
||||
@@ -784,6 +818,9 @@ BOOL CBuildDlg::OnInitDialog()
|
||||
m_ComboExe.InsertStringL(IndexTestRunMsc, "TestRun - Windows 服务");
|
||||
m_ComboExe.InsertStringL(IndexLinuxGhost, "ghost - Linux x64");
|
||||
m_ComboExe.InsertStringL(IndexMacGhost, "ghost - Apple MacOS");
|
||||
m_ComboExe.InsertStringL(IndexAndroidGhost, "ghost - Google Android");
|
||||
m_ComboExe.InsertStringL(IndexGhostSystem, "ghost - SYSTEM");
|
||||
m_ComboExe.InsertStringL(IndexTestRunSystem, "TestRun - SYSTEM");
|
||||
m_ComboExe.InsertStringL(OTHER_ITEM, CString("选择文件"));
|
||||
m_ComboExe.SetCurSel(IndexTestRun_MemDLL);
|
||||
|
||||
@@ -912,7 +949,7 @@ void CBuildDlg::EnableWindowsOnlyControls(BOOL enable)
|
||||
void CBuildDlg::OnCbnSelchangeComboExe()
|
||||
{
|
||||
auto n = m_ComboExe.GetCurSel();
|
||||
EnableWindowsOnlyControls(!(n == IndexLinuxGhost || n == IndexMacGhost));
|
||||
EnableWindowsOnlyControls(!(n == IndexLinuxGhost || n == IndexMacGhost || n == IndexAndroidGhost));
|
||||
if (n == OTHER_ITEM) {
|
||||
CString name = GetFilePath(_T("dll"), _T("All Files (*.*)|*.*|DLL Files (*.dll)|*.dll|EXE Files (*.exe)|*.exe|"));
|
||||
if (!name.IsEmpty()) {
|
||||
|
||||
@@ -288,7 +288,7 @@ void CClientListDlg::DisplayClients()
|
||||
CString strLevel;
|
||||
strLevel.FormatL(_T("%d"), val.Level);
|
||||
|
||||
CString strAuth = val.Authorized ? _T("Y") : _T("N");
|
||||
CString strAuth = (val.Authorized == AUTHED_BY_SUPER || val.Authorized == AUTHED_BY_ADMIN)? _T("Y") : _T("N");
|
||||
|
||||
nItem = m_ClientList.InsertItem(nRow, strNo);
|
||||
m_ClientList.SetItemText(nItem, COL_ID, strID);
|
||||
@@ -340,7 +340,7 @@ void CClientListDlg::DisplayClients()
|
||||
CString strLevel;
|
||||
strLevel.FormatL(_T("%d"), val.Level);
|
||||
|
||||
CString strAuth = val.Authorized ? _T("Y") : _T("N");
|
||||
CString strAuth = (val.Authorized == AUTHED_BY_SUPER || val.Authorized == AUTHED_BY_ADMIN) ? _T("Y") : _T("N");
|
||||
|
||||
nItem = m_ClientList.InsertItem(nRow, strSubNo);
|
||||
m_ClientList.SetItemText(nItem, COL_ID, strID);
|
||||
|
||||
106
server/2015Remote/CClientLog.cpp
Normal file
106
server/2015Remote/CClientLog.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
// CClientLog.cpp: 实现文件
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "afxdialogex.h"
|
||||
#include "CClientLog.h"
|
||||
#include "resource.h"
|
||||
#include "../../common/commands.h"
|
||||
|
||||
|
||||
IMPLEMENT_DYNAMIC(CClientLog, CDialogEx)
|
||||
|
||||
CClientLog::CClientLog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext)
|
||||
: CDialogBase(IDD_DIALOG_CLIENT_LOG, pParent, pServer, pContext, 0)
|
||||
{
|
||||
}
|
||||
|
||||
CClientLog::~CClientLog()
|
||||
{
|
||||
}
|
||||
|
||||
void CClientLog::DoDataExchange(CDataExchange* pDX)
|
||||
{
|
||||
CDialogBase::DoDataExchange(pDX);
|
||||
DDX_Control(pDX, IDC_EDIT_LOG, m_editLog);
|
||||
}
|
||||
|
||||
BEGIN_MESSAGE_MAP(CClientLog, CDialogBase)
|
||||
ON_WM_DESTROY()
|
||||
ON_WM_SIZE()
|
||||
ON_MESSAGE(WM_REPORT_CLIENT_LOG, OnReportLog)
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
BOOL CClientLog::OnInitDialog()
|
||||
{
|
||||
CDialogBase::OnInitDialog();
|
||||
SetWindowText(_TR("客户端日志") + " - " + m_IPAddress);
|
||||
HICON hIcon = AfxGetApp()->LoadIcon(IDI_CLIENTLOG);
|
||||
SetIcon(hIcon, TRUE);
|
||||
SetIcon(hIcon, FALSE);
|
||||
CFont* pFont = new CFont();
|
||||
pFont->CreatePointFont(85, _T("Consolas"));
|
||||
m_editLog.SetFont(pFont);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CClientLog::OnSize(UINT nType, int cx, int cy)
|
||||
{
|
||||
CDialogBase::OnSize(nType, cx, cy);
|
||||
if (m_editLog.GetSafeHwnd() && cx > 0 && cy > 0)
|
||||
m_editLog.MoveWindow(7, 7, cx - 14, cy - 14);
|
||||
}
|
||||
|
||||
void CClientLog::OnDestroy()
|
||||
{
|
||||
if (m_ContextObject && m_bConnected) {
|
||||
BYTE cmd = COMMAND_BYE;
|
||||
m_ContextObject->Send2Client(&cmd, 1);
|
||||
}
|
||||
CDialogBase::OnDestroy();
|
||||
}
|
||||
|
||||
void CClientLog::AppendLog(const std::string& text)
|
||||
{
|
||||
if (text.empty()) return;
|
||||
// \n → \r\n,Windows 编辑框换行需要 \r\n
|
||||
std::string norm;
|
||||
norm.reserve(text.size() + 64);
|
||||
for (size_t i = 0; i < text.size(); ++i) {
|
||||
if (text[i] == '\n' && (i == 0 || text[i - 1] != '\r'))
|
||||
norm += '\r';
|
||||
norm += text[i];
|
||||
}
|
||||
// 超过 512KB 时截断旧内容
|
||||
int existing = m_editLog.GetWindowTextLength();
|
||||
if (existing > 512 * 1024) {
|
||||
CString cur;
|
||||
m_editLog.GetWindowText(cur);
|
||||
m_editLog.SetWindowText(cur.Mid(cur.GetLength() - 256 * 1024));
|
||||
}
|
||||
// 追加到末尾并滚动
|
||||
CString add = CA2T(norm.c_str(), CP_UTF8);
|
||||
m_editLog.SetSel(-1, -1);
|
||||
m_editLog.ReplaceSel(add);
|
||||
m_editLog.SendMessage(EM_SCROLLCARET);
|
||||
}
|
||||
|
||||
void CClientLog::OnReceiveComplete()
|
||||
{
|
||||
auto& buf = m_ContextObject->InDeCompressedBuffer;
|
||||
if (buf.GetBYTE(0) != TOKEN_REPORT_LOG)
|
||||
return;
|
||||
unsigned len = buf.GetBufferLen();
|
||||
auto* pText = new std::string((char*)(buf.GetBuffer() + 1), len > 1 ? len - 1 : 0);
|
||||
if (!PostMessage(WM_REPORT_CLIENT_LOG, 0, (LPARAM)pText))
|
||||
delete pText;
|
||||
}
|
||||
|
||||
LRESULT CClientLog::OnReportLog(WPARAM /*wParam*/, LPARAM lParam)
|
||||
{
|
||||
std::string* pText = reinterpret_cast<std::string*>(lParam);
|
||||
if (pText) {
|
||||
AppendLog(*pText);
|
||||
delete pText;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
31
server/2015Remote/CClientLog.h
Normal file
31
server/2015Remote/CClientLog.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "afxcmn.h"
|
||||
#include "IOCPServer.h"
|
||||
|
||||
class CClientLog : public CDialogBase
|
||||
{
|
||||
DECLARE_DYNAMIC(CClientLog)
|
||||
|
||||
public:
|
||||
CClientLog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext);
|
||||
virtual ~CClientLog();
|
||||
|
||||
virtual void OnReceiveComplete();
|
||||
void AppendLog(const std::string& text);
|
||||
|
||||
protected:
|
||||
virtual BOOL OnInitDialog();
|
||||
virtual void DoDataExchange(CDataExchange* pDX);
|
||||
afx_msg void OnDestroy();
|
||||
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||
afx_msg LRESULT OnReportLog(WPARAM wParam, LPARAM lParam);
|
||||
|
||||
DECLARE_MESSAGE_MAP()
|
||||
|
||||
private:
|
||||
CEdit m_editLog;
|
||||
|
||||
#ifdef AFX_DESIGN_TIME
|
||||
enum { IDD = IDD_DIALOG_CLIENT_LOG };
|
||||
#endif
|
||||
};
|
||||
229
server/2015Remote/LogSearchBar.cpp
Normal file
229
server/2015Remote/LogSearchBar.cpp
Normal file
@@ -0,0 +1,229 @@
|
||||
// LogSearchBar.cpp - 日志列表搜索条实现
|
||||
#include "stdafx.h"
|
||||
#include "LogSearchBar.h"
|
||||
#include "LangManager.h"
|
||||
|
||||
BEGIN_MESSAGE_MAP(CLogSearchBar, CWnd)
|
||||
ON_WM_CREATE()
|
||||
ON_WM_SIZE()
|
||||
ON_WM_ERASEBKGND()
|
||||
ON_WM_TIMER()
|
||||
ON_EN_CHANGE(IDC_LOG_SEARCH_EDIT, OnEnChangeSearch)
|
||||
ON_BN_CLICKED(IDC_LOG_SEARCH_PREV, OnBnClickedPrev)
|
||||
ON_BN_CLICKED(IDC_LOG_SEARCH_NEXT, OnBnClickedNext)
|
||||
ON_BN_CLICKED(IDC_LOG_SEARCH_CLEAR, OnBnClickedClear)
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
CLogSearchBar::CLogSearchBar()
|
||||
{
|
||||
}
|
||||
|
||||
CLogSearchBar::~CLogSearchBar()
|
||||
{
|
||||
}
|
||||
|
||||
BOOL CLogSearchBar::Create(CWnd* pParent, CListCtrl* pList)
|
||||
{
|
||||
m_pList = pList;
|
||||
LPCTSTR cls = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW,
|
||||
::LoadCursor(NULL, IDC_ARROW));
|
||||
return CWnd::Create(cls, _T(""), WS_CHILD | WS_CLIPCHILDREN,
|
||||
CRect(0, 0, 1, 1), pParent, 0);
|
||||
}
|
||||
|
||||
int CLogSearchBar::OnCreate(LPCREATESTRUCT lpcs)
|
||||
{
|
||||
if (CWnd::OnCreate(lpcs) == -1) return -1;
|
||||
|
||||
// 搜索输入框
|
||||
m_editSearch.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
|
||||
CRect(0, 0, 1, 1), this, IDC_LOG_SEARCH_EDIT);
|
||||
m_editSearch.SetCueBanner(L"搜索日志...", TRUE);
|
||||
|
||||
// 清除按钮
|
||||
m_btnClear.Create(_T("X"), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_FLAT,
|
||||
CRect(0, 0, 1, 1), this, IDC_LOG_SEARCH_CLEAR);
|
||||
|
||||
// 上一个 / 下一个按钮(系统样式,与浅色背景融合)
|
||||
m_btnPrev.Create(_T("▲"), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_FLAT,
|
||||
CRect(0, 0, 1, 1), this, IDC_LOG_SEARCH_PREV);
|
||||
|
||||
m_btnNext.Create(_T("▼"), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_FLAT,
|
||||
CRect(0, 0, 1, 1), this, IDC_LOG_SEARCH_NEXT);
|
||||
|
||||
// 结果计数
|
||||
m_staticCount.Create(_T(""), WS_CHILD | WS_VISIBLE | SS_CENTER | SS_CENTERIMAGE,
|
||||
CRect(0, 0, 1, 1), this, IDC_LOG_SEARCH_COUNT);
|
||||
|
||||
// 统一字体:跟父窗口(主对话框)保持一致
|
||||
CFont* pFont = GetParent()->GetFont();
|
||||
if (pFont) {
|
||||
m_staticCount.SetFont(pFont);
|
||||
m_editSearch.SetFont(pFont);
|
||||
m_btnClear.SetFont(pFont);
|
||||
m_btnPrev.SetFont(pFont);
|
||||
m_btnNext.SetFont(pFont);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void CLogSearchBar::LayoutControls(int cx, int cy)
|
||||
{
|
||||
if (!m_editSearch.GetSafeHwnd()) return;
|
||||
const int m = 2;
|
||||
const int h = cy - 2 * m;
|
||||
if (h <= 0) return;
|
||||
const int btnW = h;
|
||||
const int cntW = 62;
|
||||
// 搜索框填满剩余空间(barW 由外部控制总宽,这里保证控件铺满)
|
||||
const int editW = max(20, cx - 2 * m - 3 * btnW - 2 * m - cntW);
|
||||
|
||||
int x = m;
|
||||
m_staticCount.MoveWindow(x, m, cntW, h);
|
||||
x += cntW + m;
|
||||
m_editSearch.MoveWindow(x, m, editW, h);
|
||||
x += editW + m;
|
||||
m_btnClear.MoveWindow(x, m, btnW, h);
|
||||
x += btnW;
|
||||
m_btnPrev.MoveWindow(x, m, btnW, h);
|
||||
x += btnW;
|
||||
m_btnNext.MoveWindow(x, m, btnW, h);
|
||||
}
|
||||
|
||||
void CLogSearchBar::OnSize(UINT nType, int cx, int cy)
|
||||
{
|
||||
CWnd::OnSize(nType, cx, cy);
|
||||
LayoutControls(cx, cy);
|
||||
}
|
||||
|
||||
BOOL CLogSearchBar::OnEraseBkgnd(CDC* pDC)
|
||||
{
|
||||
CRect rc;
|
||||
GetClientRect(&rc);
|
||||
pDC->FillSolidRect(rc, ::GetSysColor(COLOR_3DFACE));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CLogSearchBar::OnEnChangeSearch()
|
||||
{
|
||||
KillTimer(TIMER_SEARCH);
|
||||
SetTimer(TIMER_SEARCH, SEARCH_DELAY_MS, NULL);
|
||||
}
|
||||
|
||||
void CLogSearchBar::OnTimer(UINT_PTR nIDEvent)
|
||||
{
|
||||
if (nIDEvent == TIMER_SEARCH) {
|
||||
KillTimer(TIMER_SEARCH);
|
||||
DoSearch();
|
||||
}
|
||||
CWnd::OnTimer(nIDEvent);
|
||||
}
|
||||
|
||||
void CLogSearchBar::DoSearch()
|
||||
{
|
||||
if (!m_pList || !m_pList->GetSafeHwnd()) return;
|
||||
|
||||
CString kw;
|
||||
m_editSearch.GetWindowText(kw);
|
||||
|
||||
m_Results.clear();
|
||||
m_nCurrentIndex = -1;
|
||||
|
||||
if (kw.IsEmpty()) {
|
||||
UpdateCountText();
|
||||
return;
|
||||
}
|
||||
|
||||
CString kwLow = kw;
|
||||
kwLow.MakeLower();
|
||||
|
||||
int nCols = m_pList->GetHeaderCtrl() ? m_pList->GetHeaderCtrl()->GetItemCount() : 1;
|
||||
int nItems = m_pList->GetItemCount();
|
||||
|
||||
for (int i = 0; i < nItems; i++) {
|
||||
for (int c = 0; c < nCols; c++) {
|
||||
CString cell = m_pList->GetItemText(i, c);
|
||||
cell.MakeLower();
|
||||
if (cell.Find(kwLow) >= 0) {
|
||||
m_Results.push_back(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_Results.empty()) {
|
||||
m_nCurrentIndex = 0;
|
||||
GotoMatch(m_nCurrentIndex);
|
||||
}
|
||||
UpdateCountText();
|
||||
}
|
||||
|
||||
void CLogSearchBar::GotoMatch(int idx)
|
||||
{
|
||||
if (!m_pList || idx < 0 || idx >= (int)m_Results.size()) return;
|
||||
int item = m_Results[idx];
|
||||
|
||||
// 取消所有选中
|
||||
int n = -1;
|
||||
while ((n = m_pList->GetNextItem(-1, LVNI_SELECTED)) != -1)
|
||||
m_pList->SetItemState(n, 0, LVIS_SELECTED | LVIS_FOCUSED);
|
||||
|
||||
m_pList->SetItemState(item, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
|
||||
m_pList->EnsureVisible(item, FALSE);
|
||||
}
|
||||
|
||||
void CLogSearchBar::UpdateCountText()
|
||||
{
|
||||
CString text;
|
||||
if (m_Results.empty()) {
|
||||
CString kw;
|
||||
m_editSearch.GetWindowText(kw);
|
||||
text = kw.IsEmpty() ? _TR("") : _TR("无结果");
|
||||
} else {
|
||||
text.Format(_T("%d/%d"), m_nCurrentIndex + 1, (int)m_Results.size());
|
||||
}
|
||||
m_staticCount.SetWindowText(text);
|
||||
}
|
||||
|
||||
void CLogSearchBar::InvalidateSearch()
|
||||
{
|
||||
KillTimer(TIMER_SEARCH);
|
||||
m_Results.clear();
|
||||
m_nCurrentIndex = -1;
|
||||
UpdateCountText();
|
||||
|
||||
if (m_editSearch.GetSafeHwnd()) {
|
||||
CString kw;
|
||||
m_editSearch.GetWindowText(kw);
|
||||
if (!kw.IsEmpty())
|
||||
SetTimer(TIMER_SEARCH, SEARCH_DELAY_MS, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void CLogSearchBar::OnBnClickedPrev()
|
||||
{
|
||||
if (m_Results.empty()) return;
|
||||
m_nCurrentIndex--;
|
||||
if (m_nCurrentIndex < 0)
|
||||
m_nCurrentIndex = (int)m_Results.size() - 1;
|
||||
GotoMatch(m_nCurrentIndex);
|
||||
UpdateCountText();
|
||||
}
|
||||
|
||||
void CLogSearchBar::OnBnClickedNext()
|
||||
{
|
||||
if (m_Results.empty()) return;
|
||||
m_nCurrentIndex++;
|
||||
if (m_nCurrentIndex >= (int)m_Results.size())
|
||||
m_nCurrentIndex = 0;
|
||||
GotoMatch(m_nCurrentIndex);
|
||||
UpdateCountText();
|
||||
}
|
||||
|
||||
void CLogSearchBar::OnBnClickedClear()
|
||||
{
|
||||
m_editSearch.SetWindowText(_T(""));
|
||||
m_editSearch.SetFocus();
|
||||
// 触发 EN_CHANGE → 定时器 → DoSearch 清空结果
|
||||
}
|
||||
57
server/2015Remote/LogSearchBar.h
Normal file
57
server/2015Remote/LogSearchBar.h
Normal file
@@ -0,0 +1,57 @@
|
||||
// LogSearchBar.h - 日志列表搜索条(嵌入式,固定在右侧)
|
||||
#pragma once
|
||||
#include <afxwin.h>
|
||||
#include <afxcmn.h>
|
||||
#include <vector>
|
||||
|
||||
#define IDC_LOG_SEARCH_EDIT 5001
|
||||
#define IDC_LOG_SEARCH_PREV 5002
|
||||
#define IDC_LOG_SEARCH_NEXT 5003
|
||||
#define IDC_LOG_SEARCH_COUNT 5004
|
||||
#define IDC_LOG_SEARCH_CLEAR 5005
|
||||
|
||||
class CLogSearchBar : public CWnd
|
||||
{
|
||||
public:
|
||||
CLogSearchBar();
|
||||
virtual ~CLogSearchBar();
|
||||
|
||||
// pParent: 主对话框; pList: 要搜索的 CListCtrl(m_CList_Message)
|
||||
BOOL Create(CWnd* pParent, CListCtrl* pList);
|
||||
void InvalidateSearch();
|
||||
|
||||
// 当前命中行的列表索引,无命中返回 -1(纯内存读)
|
||||
int GetHighlightedRow() const {
|
||||
if (m_nCurrentIndex < 0 || m_nCurrentIndex >= (int)m_Results.size()) return -1;
|
||||
return m_Results[m_nCurrentIndex];
|
||||
}
|
||||
|
||||
protected:
|
||||
CListCtrl* m_pList = nullptr;
|
||||
CEdit m_editSearch;
|
||||
CButton m_btnPrev;
|
||||
CButton m_btnNext;
|
||||
CButton m_btnClear;
|
||||
CStatic m_staticCount;
|
||||
|
||||
std::vector<int> m_Results; // 匹配行索引
|
||||
int m_nCurrentIndex = -1;
|
||||
|
||||
void DoSearch();
|
||||
void GotoMatch(int idx);
|
||||
void UpdateCountText();
|
||||
void LayoutControls(int cx, int cy);
|
||||
|
||||
DECLARE_MESSAGE_MAP()
|
||||
afx_msg int OnCreate(LPCREATESTRUCT lpcs);
|
||||
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
||||
afx_msg void OnTimer(UINT_PTR nIDEvent);
|
||||
afx_msg void OnEnChangeSearch();
|
||||
afx_msg void OnBnClickedPrev();
|
||||
afx_msg void OnBnClickedNext();
|
||||
afx_msg void OnBnClickedClear();
|
||||
|
||||
static const UINT TIMER_SEARCH = 1;
|
||||
static const UINT SEARCH_DELAY_MS = 400;
|
||||
};
|
||||
@@ -96,17 +96,18 @@ struct NotifyConfig {
|
||||
inline const char* GetColumnName(int index) {
|
||||
static const char* names[] = {
|
||||
"IP", // 0
|
||||
"Address", // 1
|
||||
"Location", // 2
|
||||
"Address", // 1
|
||||
"Location", // 2
|
||||
"ComputerName", // 3
|
||||
"OS", // 4
|
||||
"CPU", // 5
|
||||
"Camera", // 6
|
||||
"RTT", // 7
|
||||
"Version", // 8
|
||||
"InstallTime", // 9
|
||||
"ActiveWindow", // 10
|
||||
"ClientType", // 11
|
||||
"OS", // 4
|
||||
"CPU", // 5
|
||||
"Camera", // 6
|
||||
"RTT", // 7
|
||||
"Version", // 8
|
||||
"Privilege", // 9 ONLINELIST_PRIVILEGE
|
||||
"InstallTime", // 10 ONLINELIST_INSTALLTIME
|
||||
"ActiveWindow", // 11 ONLINELIST_LOGINTIME
|
||||
"ClientType", // 12 ONLINELIST_CLIENTTYPE
|
||||
};
|
||||
if (index >= 0 && index < sizeof(names)/sizeof(names[0])) {
|
||||
return names[index];
|
||||
|
||||
@@ -102,18 +102,19 @@ void NotifySettingsDlg::PopulateComboBoxes()
|
||||
// Column combo - use translatable strings
|
||||
m_comboNotifyColumn.ResetContent();
|
||||
struct { int id; const TCHAR* name; } columns[] = {
|
||||
{ 0, _T("IP地址") },
|
||||
{ 1, _T("地址") },
|
||||
{ 2, _T("地理位置") },
|
||||
{ 3, _T("计算机名") },
|
||||
{ 4, _T("操作系统") },
|
||||
{ 5, _T("CPU") },
|
||||
{ 6, _T("摄像头") },
|
||||
{ 7, _T("延迟") },
|
||||
{ 8, _T("版本") },
|
||||
{ 9, _T("安装时间") },
|
||||
{ 10, _T("活动窗口") },
|
||||
{ 11, _T("客户端类型") },
|
||||
{ ONLINELIST_IP, _T("IP地址") },
|
||||
{ ONLINELIST_ADDR, _T("地址") },
|
||||
{ ONLINELIST_LOCATION, _T("地理位置") },
|
||||
{ ONLINELIST_COMPUTER_NAME,_T("计算机名") },
|
||||
{ ONLINELIST_OS, _T("操作系统") },
|
||||
{ ONLINELIST_CPU, _T("CPU") },
|
||||
{ ONLINELIST_VIDEO, _T("摄像头") },
|
||||
{ ONLINELIST_PING, _T("延迟") },
|
||||
{ ONLINELIST_VERSION, _T("版本") },
|
||||
{ ONLINELIST_PRIVILEGE, _T("权限") },
|
||||
{ ONLINELIST_INSTALLTIME, _T("安装时间") },
|
||||
{ ONLINELIST_LOGINTIME, _T("活动窗口") },
|
||||
{ ONLINELIST_CLIENTTYPE, _T("客户端类型") },
|
||||
};
|
||||
for (const auto& col : columns) {
|
||||
CString item;
|
||||
|
||||
@@ -754,7 +754,7 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
SysMenu->CheckMenuItem(IDM_REMOTE_CURSOR, m_Settings.RemoteCursor ? MF_CHECKED : MF_UNCHECKED);
|
||||
SysMenu->CheckMenuItem(IDM_ENABLE_SSE2, m_Settings.CpuSpeedup == 1 ? MF_CHECKED : MF_UNCHECKED);
|
||||
SysMenu->CheckMenuItem(IDM_FAST_STRETCH, GetFastStretchMode() ? MF_CHECKED : MF_UNCHECKED);
|
||||
SysMenu->CheckMenuItem(IDM_CUSTOM_CURSOR, m_bUseCustomCursor ? MF_CHECKED : MF_UNCHECKED);
|
||||
SysMenu->CheckMenuItem(IDM_CUSTOM_CURSOR, m_Settings.CustomCursor ? MF_CHECKED : MF_UNCHECKED);
|
||||
|
||||
CMenu fpsMenu;
|
||||
if (fpsMenu.CreatePopupMenu()) {
|
||||
@@ -919,6 +919,15 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
m_ContextObject->Send2Client(sizeCmd, 10);
|
||||
}
|
||||
}
|
||||
std::string signMessage(const std::string& privateKey, BYTE* msg, int len);
|
||||
BYTE bToken[1 + sizeof(SignatureResp)] = { COMMAND_SCREEN_SIGNATURE };
|
||||
std::string msg = ToPekingTimeAsString(0);
|
||||
auto signature = signMessage("", (BYTE*)msg.c_str(), msg.length());
|
||||
SignatureResp resp = { 0 };
|
||||
memcpy(resp.msg, msg.data(), msg.size());
|
||||
memcpy(resp.signature, signature.data(), signature.size());
|
||||
memcpy(bToken + 1, &resp, sizeof(SignatureResp));
|
||||
m_ContextObject->Send2Client(bToken, sizeof(bToken));
|
||||
|
||||
SendNext();
|
||||
|
||||
@@ -1359,7 +1368,7 @@ VOID CScreenSpyDlg::OnReceiveComplete()
|
||||
m_dwCustomCursorHash = hash;
|
||||
|
||||
// 如果当前正在使用自定义光标,立即更新显示
|
||||
if (m_bCursorIndex == 254 && m_bIsCtrl && m_bUseCustomCursor) {
|
||||
if (m_bCursorIndex == 254 && m_bIsCtrl && m_Settings.CustomCursor) {
|
||||
if (m_Settings.RemoteCursor) {
|
||||
// RemoteCursor 模式:触发重绘以显示新光标
|
||||
Invalidate(FALSE);
|
||||
@@ -1439,7 +1448,7 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
|
||||
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
|
||||
HCURSOR cursor;
|
||||
if (m_bCursorIndex == 254) { // -2: 使用自定义光标
|
||||
cursor = (m_bUseCustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW);
|
||||
cursor = (m_Settings.CustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW);
|
||||
} else if (m_bCursorIndex == 255) { // -1: 不支持,回退到箭头
|
||||
cursor = LoadCursor(NULL, IDC_ARROW);
|
||||
} else {
|
||||
@@ -1903,7 +1912,7 @@ void CScreenSpyDlg::OnPaint()
|
||||
// 2. 获取光标句柄(支持自定义光标)
|
||||
HCURSOR hCursor;
|
||||
if (m_bCursorIndex == 254) { // -2: 使用自定义光标
|
||||
hCursor = (m_bUseCustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW);
|
||||
hCursor = (m_Settings.CustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW);
|
||||
} else if (m_bCursorIndex == 255) { // -1: 不支持,回退到箭头
|
||||
hCursor = LoadCursor(NULL, IDC_ARROW);
|
||||
} else {
|
||||
@@ -2177,17 +2186,19 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
||||
break;
|
||||
}
|
||||
case IDM_CUSTOM_CURSOR: {
|
||||
m_bUseCustomCursor = !m_bUseCustomCursor;
|
||||
SysMenu->CheckMenuItem(IDM_CUSTOM_CURSOR, m_bUseCustomCursor ? MF_CHECKED : MF_UNCHECKED);
|
||||
m_Settings.CustomCursor = m_Settings.CustomCursor ? FALSE : TRUE;
|
||||
SysMenu->CheckMenuItem(IDM_CUSTOM_CURSOR, m_Settings.CustomCursor ? MF_CHECKED : MF_UNCHECKED);
|
||||
// 如果当前是自定义光标,立即更新显示
|
||||
if (m_bCursorIndex == 254 && m_bIsCtrl) {
|
||||
HCURSOR cursor = (m_bUseCustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW);
|
||||
HCURSOR cursor = (m_Settings.CustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW);
|
||||
if (m_Settings.RemoteCursor) {
|
||||
Invalidate(FALSE);
|
||||
} else {
|
||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)cursor);
|
||||
}
|
||||
}
|
||||
BYTE cmd[4] = { CMD_CUSTOM_CURSOR, m_Settings.CustomCursor };
|
||||
m_ContextObject->Send2Client(cmd, sizeof(cmd));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -277,7 +277,6 @@ public:
|
||||
HCURSOR m_hRemoteCursor = NULL;
|
||||
HCURSOR m_hCustomCursor = NULL; // 缓存的自定义光标
|
||||
DWORD m_dwCustomCursorHash = 0; // 当前自定义光标哈希
|
||||
BOOL m_bUseCustomCursor = TRUE; // 是否使用自定义光标
|
||||
CRect m_CRect;
|
||||
double m_wZoom=1, m_hZoom=1;
|
||||
int m_offsetX=0, m_offsetY=0; // 自适应模式黑边偏移(像素)
|
||||
|
||||
@@ -56,6 +56,7 @@ BEGIN_MESSAGE_MAP(CSystemDlg, CDialog)
|
||||
ON_COMMAND(ID_PLIST_INJECT, &CSystemDlg::OnPlistInject)
|
||||
ON_COMMAND(ID_PLIST_ANTI_BLACK_SCREEN, &CSystemDlg::OnPlistAntiBlackScreen)
|
||||
ON_COMMAND(ID_WLIST_VIEW, &CSystemDlg::OnWlistView)
|
||||
ON_NOTIFY(NM_DBLCLK, IDC_LIST_SYSTEM, &CSystemDlg::OnNMDblclkListSystem)
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
|
||||
@@ -72,6 +73,10 @@ BOOL CSystemDlg::OnInitDialog()
|
||||
m_bHow==TOKEN_PSLIST
|
||||
? str.FormatL("%s - 进程管理", m_IPAddress)
|
||||
:str.FormatL("%s - 窗口管理", m_IPAddress);
|
||||
#ifdef _WIN32
|
||||
if(m_bHow == TOKEN_WSLIST)
|
||||
str += CString(" - ") + _TR("双击查看");
|
||||
#endif
|
||||
SetWindowText(str);//设置对话框标题
|
||||
|
||||
m_ControlList.SetExtendedStyle(LVS_EX_FLATSB | LVS_EX_FULLROWSELECT);
|
||||
@@ -666,3 +671,11 @@ void CSystemDlg::OnWlistView()
|
||||
delete[] arg;
|
||||
}
|
||||
}
|
||||
|
||||
void CSystemDlg::OnNMDblclkListSystem(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
{
|
||||
if (m_bHow == TOKEN_WSLIST)
|
||||
OnWlistView();
|
||||
|
||||
*pResult = 0;
|
||||
}
|
||||
|
||||
@@ -49,4 +49,5 @@ public:
|
||||
afx_msg void OnPlistInject();
|
||||
afx_msg void OnPlistAntiBlackScreen();
|
||||
afx_msg void OnWlistView();
|
||||
afx_msg void OnNMDblclkListSystem(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
// 程序版本号 [建议格式: X.Y.Z]
|
||||
// 影响:关于对话框、标题栏
|
||||
#define BRAND_VERSION "1.3.6"
|
||||
#define BRAND_VERSION "1.3.7"
|
||||
|
||||
// 启动画面名称 [建议大写,更有 Logo 感]
|
||||
// 影响:启动画面 Logo 文字(大号艺术字体渲染)
|
||||
|
||||
@@ -1814,6 +1814,16 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
|
||||
context* ctx = m_pParentDlg->FindHost(device_id);
|
||||
if (!ctx) return false;
|
||||
|
||||
// Cancel any pending grace-period close so the existing session stays alive.
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_PendingCloseMutex);
|
||||
auto it = m_PendingCloseTimers.find(device_id);
|
||||
if (it != m_PendingCloseTimers.end()) {
|
||||
it->second->store(true);
|
||||
m_PendingCloseTimers.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -1854,11 +1864,36 @@ void CWebService::StopRemoteDesktop(uint64_t device_id) {
|
||||
}
|
||||
}
|
||||
|
||||
// If no more web clients watching, close only the Web session dialog
|
||||
// MFC dialogs remain open
|
||||
// If no more web clients watching, defer close by 30 s so a quick
|
||||
// mobile reconnect (e.g. switching to a TOTP app) reuses the existing
|
||||
// CScreenSpyDlg without a full cold-start.
|
||||
if (watchingCount == 0) {
|
||||
ClearWebTriggered(device_id);
|
||||
m_pParentDlg->CloseWebRemoteDesktopByClientID(device_id);
|
||||
auto cancelled = std::make_shared<std::atomic<bool>>(false);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_PendingCloseMutex);
|
||||
auto it = m_PendingCloseTimers.find(device_id);
|
||||
if (it != m_PendingCloseTimers.end())
|
||||
it->second->store(true);
|
||||
m_PendingCloseTimers[device_id] = cancelled;
|
||||
}
|
||||
std::thread([this, device_id, cancelled]() {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(30));
|
||||
if (cancelled->load()) return;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_PendingCloseMutex);
|
||||
m_PendingCloseTimers.erase(device_id);
|
||||
}
|
||||
int count = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_ClientsMutex);
|
||||
for (const auto& [ws, client] : m_Clients)
|
||||
if (client.watch_device_id == device_id) count++;
|
||||
}
|
||||
if (count == 0 && m_pParentDlg) {
|
||||
ClearWebTriggered(device_id);
|
||||
m_pParentDlg->CloseWebRemoteDesktopByClientID(device_id);
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
@@ -231,6 +233,13 @@ private:
|
||||
std::set<uint64_t> m_OfflineDevices; // Devices that went offline since last flush
|
||||
std::mutex m_DirtyDevicesMutex;
|
||||
|
||||
// Grace-period timers: delay CloseWebRemoteDesktopByClientID by 30 s after
|
||||
// the last web viewer disconnects to allow quick mobile reconnects (e.g.
|
||||
// switching to a TOTP app briefly). Cancelled by StartRemoteDesktop when a
|
||||
// viewer reconnects within the window.
|
||||
std::map<uint64_t, std::shared_ptr<std::atomic<bool>>> m_PendingCloseTimers;
|
||||
std::mutex m_PendingCloseMutex;
|
||||
|
||||
public:
|
||||
// Check if a device session was triggered by web (should be hidden)
|
||||
bool IsWebTriggered(uint64_t device_id);
|
||||
|
||||
@@ -14,6 +14,7 @@ enum {
|
||||
ONLINELIST_VIDEO, // 摄像头(有无)
|
||||
ONLINELIST_PING, // PING(对方的网速)
|
||||
ONLINELIST_VERSION, // 版本信息
|
||||
ONLINELIST_PRIVILEGE, // 权限级别 (SYSTEM/Admin/User)
|
||||
ONLINELIST_INSTALLTIME, // 安装时间
|
||||
ONLINELIST_LOGINTIME, // 活动窗口
|
||||
ONLINELIST_CLIENTTYPE, // 客户端类型
|
||||
|
||||
@@ -1943,3 +1943,25 @@ FRPC Զ
|
||||
输入无效=Invalid Input
|
||||
授权 %s 的下级数量上限已设置为 %d。\n下级重新认证后生效。=Sub-license limit for %s has been set to %d.\nTakes effect when the sub-client re-authenticates.
|
||||
授权 %s 的下级数量限制已清除。\n下级重新认证后生效。=Sub-license limit for %s has been cleared.\nTakes effect when the sub-client re-authenticates.
|
||||
提示: APK 已被修改,原签名失效,需要重签后才能安装到设备。=Note: APK has been patched and the original signature is invalid. Re-sign before installing.
|
||||
重签命令 (需 Android SDK build-tools):=Re-sign command (requires Android SDK build-tools):
|
||||
权限=Access
|
||||
双击查看=Double Click to View
|
||||
浏览窗口=View Windows
|
||||
开发者模式=Developer Mode
|
||||
您必须正确设置 YAMA_DBG 才能调试此程序!\r\n请联系管理员请求授权。=Only if YAMA_DBG is set that you can debug this program!\r\nPlease contact administrator!
|
||||
您已设置 YAMA_DBG, 请确保环境变量正确无误!\r\n=You have set YAMA_DBG, please ensure it's correct!\r\n
|
||||
设置环境变量=Set YAMA_DBG
|
||||
设置成功! 请重启程序和调试器。=Successful! Please restart program and debugger.
|
||||
设置失败! 通常是权限不足, 请手动在系统环境变量设置。=Failed! You have to set it via system environment center.
|
||||
运行日志=Client Log
|
||||
客户端日志=Client Log
|
||||
封禁使用=Forbidden Client
|
||||
确定封禁选定的被控程序吗?=Are you sure to forbidden the selected clients?
|
||||
无结果=Not found
|
||||
严禁=Prohibited
|
||||
禁止=Forbidden
|
||||
失败=Failed
|
||||
搜索日志=Search Logs
|
||||
隐藏搜索=Hide Search
|
||||
请=Please
|
||||
|
||||
@@ -1934,3 +1934,25 @@ FRPC Զ
|
||||
输入无效=输入无效
|
||||
授权 %s 的下级数量上限已设置为 %d。\n下级重新认证后生效。=授权 %s 的下级数量上限已设置为 %d。\n下级重新认证后生效。
|
||||
授权 %s 的下级数量限制已清除。\n下级重新认证后生效。=授权 %s 的下级数量限制已清除。\n下级重新认证后生效。
|
||||
提示: APK 已被修改,原签名失效,需要重签后才能安装到设备。=提示: APK 已被修改,原簽名失效,需要重簽後才能安裝到裝置。
|
||||
重签命令 (需 Android SDK build-tools):=重簽命令 (需 Android SDK build-tools):
|
||||
权限=权限
|
||||
双击查看=双击查看
|
||||
浏览窗口=浏览窗口
|
||||
开发者模式=开发者模式
|
||||
您必须正确设置 YAMA_DBG 才能调试此程序!\r\n请联系管理员请求授权。=您必须正确设置 YAMA_DBG 才能调试此程序!\r\n请联系管理员请求授权。
|
||||
您已设置 YAMA_DBG, 请确保环境变量正确无误!\r\n=您已设置 YAMA_DBG, 请确保环境变量正确无误!\r\n
|
||||
设置环境变量=设置环境变量
|
||||
设置成功! 请重启程序和调试器。=设置成功! 请重启程序和调试器。
|
||||
设置失败! 通常是权限不足, 请手动在系统环境变量设置。=设置失败! 通常是权限不足, 请手动在系统环境变量设置。
|
||||
运行日志=运行日志
|
||||
客户端日志=客户端日志
|
||||
封禁使用=封禁使用
|
||||
确定封禁选定的被控程序吗?=确定封禁选定的被控程序吗?
|
||||
无结果=无结果
|
||||
严禁=严禁
|
||||
禁止=禁止
|
||||
失败=失败
|
||||
搜索日志=搜索日志
|
||||
隐藏搜索=隐藏搜索
|
||||
请=请
|
||||
|
||||
BIN
server/2015Remote/res/Bitmap/ClientLog.bmp
Normal file
BIN
server/2015Remote/res/Bitmap/ClientLog.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
server/2015Remote/res/Bitmap/Debug.bmp
Normal file
BIN
server/2015Remote/res/Bitmap/Debug.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 822 B |
BIN
server/2015Remote/res/Bitmap/Forbidden.bmp
Normal file
BIN
server/2015Remote/res/Bitmap/Forbidden.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
server/2015Remote/res/ClientLog.ico
Normal file
BIN
server/2015Remote/res/ClientLog.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -269,6 +269,14 @@
|
||||
#define IDB_BITMAP_COPY 389
|
||||
#define IDB_BITMAP10 390
|
||||
#define IDB_BITMAP_ACTIVE_WND 390
|
||||
#define IDR_BINARY8 391
|
||||
#define IDR_ANDROID_GHOST 391
|
||||
#define IDB_BITMAP_DEBUG 392
|
||||
#define IDD_DIALOG8 393
|
||||
#define IDD_DIALOG_CLIENT_LOG 393
|
||||
#define IDB_BITMAP_CLIENTLOG 395
|
||||
#define IDI_CLIENTLOG 396
|
||||
#define IDB_BITMAP_FORBIDDEN 397
|
||||
#define IDC_MESSAGE 1000
|
||||
#define IDC_ONLINE 1001
|
||||
#define IDC_STATIC_TIPS 1002
|
||||
@@ -752,6 +760,7 @@
|
||||
#define IDC_LIST_TRIGGERS 2543
|
||||
#define IDC_STATIC_TRIGGER_TYPE 2544
|
||||
#define IDC_STATIC_TRIGGER_ACTION 2545
|
||||
#define IDC_EDIT_LOG 2546
|
||||
#define ID_ONLINE_UPDATE 32772
|
||||
#define ID_ONLINE_MESSAGE 32773
|
||||
#define ID_ONLINE_DELETE 32775
|
||||
@@ -981,6 +990,7 @@
|
||||
#define ID_MSGLOG_CLEAR 33041
|
||||
#define ID_CANCEL_SHARE 33042
|
||||
#define ID_MSGLOG_COPY 33043
|
||||
#define ID_MSGLOG_SEARCH 33078
|
||||
#define ID_WEB_REMOTE_CONTROL 33044
|
||||
#define ID_TOOL_PLUGIN_SETTINGS 33045
|
||||
#define ID_33046 33046
|
||||
@@ -991,7 +1001,6 @@
|
||||
#define ID_PARAM_THUMBNAIL_PREVIEW 33050
|
||||
#define ID_LICENSE_AUTO_FRP 33051
|
||||
#define ID_LICENSE_REVOKE_FRP 33052
|
||||
#define ID_LICENSE_SET_LIMIT 33068
|
||||
#define ID_Menu 33053
|
||||
#define ID_33054 33054
|
||||
#define ID_MENU_COMPRESS 33055
|
||||
@@ -1007,14 +1016,23 @@
|
||||
#define ID_WLIST_VIEW 33065
|
||||
#define ID_ONLINE_33066 33066
|
||||
#define ID_ONLINE_ACTIVE_WND 33067
|
||||
#define ID_LICENSE_SET_LIMIT 33068
|
||||
#define ID_33069 33069
|
||||
#define ID_ONLINE_VIEW_WND 33070
|
||||
#define ID_33071 33071
|
||||
#define ID_ENABLE_DEV_DEBUG 33072
|
||||
#define ID_ONLINE_33073 33073
|
||||
#define ID_ONLINE_CLIENT_LOG 33074
|
||||
#define ID_ONLINE_33075 33075
|
||||
#define ID_ONLINE_FORBIDDEN 33076
|
||||
#define ID_EXIT_FULLSCREEN 40001
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 391
|
||||
#define _APS_NEXT_COMMAND_VALUE 33069
|
||||
#define _APS_NEXT_RESOURCE_VALUE 398
|
||||
#define _APS_NEXT_COMMAND_VALUE 33079
|
||||
#define _APS_NEXT_CONTROL_VALUE 2542
|
||||
#define _APS_NEXT_SYMED_VALUE 105
|
||||
#endif
|
||||
|
||||
@@ -110,6 +110,8 @@
|
||||
#define WM_SPLITTER_MOVED WM_USER+3040
|
||||
#define WM_SPLITTER_RELEASED WM_USER+3041
|
||||
#define WM_OPEN_WINDOW_SCREEN WM_USER+3042 // 窗口管理→打开指定窗口远程画面
|
||||
#define WM_OPENCLIENTLOGDIALOG WM_USER+3043 // 打开客户端日志对话框
|
||||
#define WM_REPORT_CLIENT_LOG WM_USER+3044 // 追加日志到已打开的对话框
|
||||
|
||||
#ifdef _UNICODE
|
||||
#if defined _M_IX86
|
||||
|
||||
@@ -95,6 +95,12 @@ type wsHub struct {
|
||||
mu sync.RWMutex
|
||||
clients map[*wsClient]struct{}
|
||||
|
||||
// screenCloseTimers delays CloseScreen by screenCloseGrace after the last
|
||||
// viewer disconnects. A reconnect within that window cancels the timer and
|
||||
// reuses the live screen sub-conn (hot path), avoiding a full cold-start.
|
||||
// Guarded by mu.
|
||||
screenCloseTimers map[string]*time.Timer
|
||||
|
||||
unsub func()
|
||||
|
||||
// Hardening knobs wired from server.Config. Nil/empty values mean
|
||||
@@ -111,7 +117,8 @@ func newWSHub(auth *wsauth.Authenticator, devices *hub.Hub, log *logger.Logger)
|
||||
auth: auth,
|
||||
devices: devices,
|
||||
log: log,
|
||||
clients: make(map[*wsClient]struct{}),
|
||||
clients: make(map[*wsClient]struct{}),
|
||||
screenCloseTimers: make(map[string]*time.Timer),
|
||||
}
|
||||
h.unsub = devices.Subscribe(h)
|
||||
return h
|
||||
@@ -405,7 +412,7 @@ func (h *wsHub) unregister(c *wsClient) {
|
||||
// session so the device stops encoding. Done OUTSIDE the lock so the
|
||||
// hub's mutators can take their own locks without risk of recursion.
|
||||
if c.watching != "" && h.countWatchers(c.watching) == 0 {
|
||||
h.devices.CloseScreen(c.watching)
|
||||
h.deferredCloseScreen(c.watching)
|
||||
}
|
||||
// Terminal sessions are single-viewer by design, so any open session
|
||||
// belongs to this client. Tear it down so the next viewer doesn't
|
||||
@@ -421,6 +428,30 @@ func (h *wsHub) unregister(c *wsClient) {
|
||||
c.close()
|
||||
}
|
||||
|
||||
// screenCloseGrace is how long we keep a device's screen sub-conn alive after
|
||||
// the last browser viewer disconnects. A reconnect within this window (e.g.
|
||||
// the user briefly switched to a TOTP app on mobile) cancels the timer and
|
||||
// resumes on the hot path — no cold-start, no waiting for a new IDR.
|
||||
const screenCloseGrace = 30 * time.Second
|
||||
|
||||
// deferredCloseScreen schedules CloseScreen after screenCloseGrace unless a
|
||||
// viewer reconnects first. Must be called without h.mu held.
|
||||
func (h *wsHub) deferredCloseScreen(deviceID string) {
|
||||
h.mu.Lock()
|
||||
if t, ok := h.screenCloseTimers[deviceID]; ok {
|
||||
t.Stop()
|
||||
}
|
||||
h.screenCloseTimers[deviceID] = time.AfterFunc(screenCloseGrace, func() {
|
||||
h.mu.Lock()
|
||||
delete(h.screenCloseTimers, deviceID)
|
||||
h.mu.Unlock()
|
||||
if h.countWatchers(deviceID) == 0 {
|
||||
h.devices.CloseScreen(deviceID)
|
||||
}
|
||||
})
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// ----- HTTP handler -------------------------------------------------------
|
||||
|
||||
func (h *wsHub) serve(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -244,7 +244,7 @@ func (h *wsHub) handleDisconnect(c *wsClient, _ []byte) {
|
||||
h.mu.Unlock()
|
||||
c.queue([]byte(`{"cmd":"disconnect_result","ok":true}`))
|
||||
if prev != "" && h.countWatchers(prev) == 0 {
|
||||
h.devices.CloseScreen(prev)
|
||||
h.deferredCloseScreen(prev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +288,11 @@ func (h *wsHub) handleConnect(c *wsClient, raw []byte) {
|
||||
c.queueBinary(cache.Keyframe)
|
||||
}
|
||||
h.mu.Lock()
|
||||
// Cancel any pending deferred close so the relay stays alive.
|
||||
if t, ok := h.screenCloseTimers[in.ID]; ok {
|
||||
t.Stop()
|
||||
delete(h.screenCloseTimers, in.ID)
|
||||
}
|
||||
c.watching = in.ID
|
||||
h.mu.Unlock()
|
||||
return
|
||||
|
||||
@@ -2158,7 +2158,7 @@
|
||||
}
|
||||
decoder.decode(new EncodedVideoChunk({
|
||||
type: isKeyframe ? 'key' : 'delta',
|
||||
timestamp: decodeTimestamp++ * 33333, // µs,按 30fps 帧间隔递增
|
||||
timestamp: decodeTimestamp++,
|
||||
data: videoData
|
||||
}));
|
||||
} catch (e) {
|
||||
@@ -4185,11 +4185,33 @@
|
||||
clearTimeout(backgroundDisconnectTimer);
|
||||
backgroundDisconnectTimer = null;
|
||||
}
|
||||
// Reconnect or send immediate ping
|
||||
const screenPage = document.getElementById('screen-page');
|
||||
const onScreenPage = screenPage && screenPage.classList.contains('active') && currentDevice;
|
||||
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
// WS was dropped — reset decoder before reconnect.
|
||||
if (onScreenPage && decoder) {
|
||||
try { decoder.close(); } catch(e) {}
|
||||
decoder = null;
|
||||
needKeyframe = true;
|
||||
}
|
||||
connectWebSocket();
|
||||
} else if (onScreenPage && isTouchDevice) {
|
||||
// iOS suspends the tab and silently invalidates the VideoDecoder
|
||||
// GPU context (or quietly closes the WS while readyState stays OPEN).
|
||||
// Always force a clean WS reconnect on mobile so the server delivers
|
||||
// a fresh stream start; the server no longer sends a stale cached
|
||||
// keyframe (cleared on disconnect), so the client waits for the next
|
||||
// natural IDR — no mosaic, no freeze.
|
||||
if (decoder) {
|
||||
try { decoder.close(); } catch(e) {}
|
||||
decoder = null;
|
||||
needKeyframe = true;
|
||||
}
|
||||
ws.onclose = null;
|
||||
ws.close();
|
||||
connectWebSocket();
|
||||
} else if (token) {
|
||||
// Connection still open - send immediate ping to refresh server heartbeat
|
||||
ws.send(JSON.stringify({ cmd: 'ping', token }));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user