826 lines
34 KiB
C++
826 lines
34 KiB
C++
#include <jni.h>
|
||
#include <android/log.h>
|
||
#include <thread>
|
||
#include <chrono>
|
||
#include <atomic>
|
||
#include <mutex>
|
||
#include <set>
|
||
#include <string>
|
||
#include <cstring>
|
||
#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>
|
||
#include <arpa/inet.h>
|
||
|
||
#include "common/commands.h"
|
||
#include "common/client_auth_state.h"
|
||
#include "common/rtt_estimator.h"
|
||
#include "client/IOCPClient.h"
|
||
#include "ScreenHandler.h"
|
||
#define XXH_INLINE_ALL
|
||
#include "common/xxhash.h"
|
||
#include "common/logger.h"
|
||
|
||
#define LOG_TAG "YAMA"
|
||
#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() {
|
||
long n = sysconf(_SC_NPROCESSORS_ONLN);
|
||
return (n > 0) ? (int)n : 1;
|
||
}
|
||
|
||
static double GetMemoryGB() {
|
||
FILE* f = fopen("/proc/meminfo", "r");
|
||
if (!f) return 0.0;
|
||
char line[128];
|
||
unsigned long kb = 0;
|
||
while (fgets(line, sizeof(line), f)) {
|
||
if (sscanf(line, "MemTotal: %lu kB", &kb) == 1) break;
|
||
}
|
||
fclose(f);
|
||
return kb / (1024.0 * 1024.0);
|
||
}
|
||
|
||
// 遍历 cpu0-cpu7,取 cpuinfo_max_freq 最大值(大核频率)
|
||
static int GetCpuMHz() {
|
||
// 优先从 sysfs 读取(真机有效)
|
||
unsigned long maxKhz = 0;
|
||
char path[80];
|
||
for (int i = 0; i < 8; i++) {
|
||
snprintf(path, sizeof(path),
|
||
"/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", i);
|
||
FILE* f = fopen(path, "r");
|
||
if (!f) continue;
|
||
unsigned long khz = 0;
|
||
if (fscanf(f, "%lu", &khz) == 1 && khz > maxKhz) maxKhz = khz;
|
||
fclose(f);
|
||
}
|
||
if (maxKhz > 0) return (int)(maxKhz / 1000);
|
||
|
||
// 回退:解析 /proc/cpuinfo 中的 "BogoMIPS"(模拟器可用)
|
||
FILE* f = fopen("/proc/cpuinfo", "r");
|
||
if (!f) return 0;
|
||
char line[128];
|
||
double bogomips = 0;
|
||
while (fgets(line, sizeof(line), f)) {
|
||
if (sscanf(line, "BogoMIPS : %lf", &bogomips) == 1 ||
|
||
sscanf(line, "bogomips : %lf", &bogomips) == 1) break;
|
||
}
|
||
fclose(f);
|
||
return bogomips > 0 ? (int)bogomips : 0;
|
||
}
|
||
|
||
static long GetFileSize(const std::string& path) {
|
||
if (path.empty()) return 0;
|
||
struct stat st;
|
||
return (stat(path.c_str(), &st) == 0) ? (long)st.st_size : 0;
|
||
}
|
||
|
||
static std::string FormatFileSize(long bytes) {
|
||
char buf[32];
|
||
if (bytes >= 1024 * 1024)
|
||
snprintf(buf, sizeof(buf), "%.1fM", bytes / (1024.0 * 1024.0));
|
||
else if (bytes >= 1024)
|
||
snprintf(buf, sizeof(buf), "%.1fK", bytes / 1024.0);
|
||
else
|
||
snprintf(buf, sizeof(buf), "%ldB", bytes);
|
||
return buf;
|
||
}
|
||
|
||
// 从 JSON 字符串中提取字符串字段值
|
||
static std::string JsonGetStr(const std::string& json, const char* key) {
|
||
std::string needle = std::string("\"") + key + "\":";
|
||
auto pos = json.find(needle);
|
||
if (pos == std::string::npos) return "";
|
||
pos += needle.size();
|
||
while (pos < json.size() && json[pos] == ' ') pos++;
|
||
if (pos >= json.size() || json[pos] != '"') return "";
|
||
pos++;
|
||
auto end = json.find('"', pos);
|
||
return (end == std::string::npos) ? "" : json.substr(pos, end - pos);
|
||
}
|
||
|
||
// 查询 ip-api.com,一次请求同时获取公网 IP 和地理位置(与 Windows 客户端同源)
|
||
// 在连接线程中同步调用,超时 3 秒
|
||
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) {
|
||
out->ready.store(true, std::memory_order_release); return;
|
||
}
|
||
int fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||
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);
|
||
|
||
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";
|
||
send(fd, req, strlen(req), 0);
|
||
|
||
std::string resp;
|
||
char buf[512];
|
||
ssize_t n;
|
||
while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) resp.append(buf, (size_t)n);
|
||
|
||
auto sep = resp.find("\r\n\r\n");
|
||
if (sep != std::string::npos) {
|
||
const std::string body = resp.substr(sep + 4);
|
||
if (JsonGetStr(body, "status") == "success") {
|
||
out->pubIp = JsonGetStr(body, "query");
|
||
std::string city = JsonGetStr(body, "city");
|
||
std::string country = JsonGetStr(body, "country");
|
||
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);
|
||
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,
|
||
// 直接用 State;ARM 4 字节对齐写入硬件层面原子,实际安全。
|
||
State g_bExit{S_CLIENT_NORMAL};
|
||
uint64_t g_myClientID = 0; // sub_conn_thread.h 需要的外部符号
|
||
static std::atomic<bool> g_running{false};
|
||
static std::atomic<uint64_t> g_lastHeartbeatAckMs{0};
|
||
|
||
// ---- JNI 反向调用(DataProcess → ControlService / CaptureService)----
|
||
static JavaVM* g_jvm = nullptr;
|
||
static jclass g_ctrlClass = nullptr;
|
||
static jmethodID g_ctrlMethod = nullptr;
|
||
static jmethodID g_getActiveWindowMid = nullptr;
|
||
// CaptureService.requestIdr() / forceFirstFrame() — ScreenSpyThread 子连接建立后触发
|
||
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};
|
||
|
||
static constexpr uint64_t HB_LOG_INTERVAL_MS = 60000;
|
||
|
||
// 供主连接和子连接共同调用:把 COMMAND_SCREEN_CONTROL 路由到 ControlService.onControlEvent()
|
||
// 定义在全局函数区;ScreenHandler.h 通过 extern 声明使用。
|
||
void DispatchControlEvent(uint32_t msgVal, uint64_t wParam, int32_t ptX, int32_t ptY)
|
||
{
|
||
if (!g_jvm || !g_ctrlClass || !g_ctrlMethod) {
|
||
LOGE("SCREEN_CONTROL: JNI not ready (ctrl=%p method=%p)", g_ctrlClass, g_ctrlMethod);
|
||
return;
|
||
}
|
||
if (msgVal != 0x200u)
|
||
LOGI("SCREEN_CONTROL: msg=0x%X pt=(%d,%d)", msgVal, ptX, ptY);
|
||
|
||
JNIEnv* jenv = nullptr;
|
||
bool attached = false;
|
||
jint st = g_jvm->GetEnv((void**)&jenv, JNI_VERSION_1_6);
|
||
if (st == JNI_EDETACHED) {
|
||
// CPP-03 fix: 检查 AttachCurrentThread 返回值,失败时 jenv 仍为 null
|
||
if (g_jvm->AttachCurrentThread(&jenv, nullptr) != JNI_OK || !jenv) return;
|
||
attached = true;
|
||
} else if (st != JNI_OK || !jenv) {
|
||
return;
|
||
}
|
||
jenv->CallStaticVoidMethod(g_ctrlClass, g_ctrlMethod,
|
||
(jint)msgVal, (jlong)wParam, (jint)ptX, (jint)ptY);
|
||
if (jenv->ExceptionOccurred()) jenv->ExceptionClear();
|
||
if (attached) g_jvm->DetachCurrentThread();
|
||
}
|
||
|
||
static std::string GetActiveWindowFromJava()
|
||
{
|
||
if (!g_jvm || !g_ctrlClass || !g_getActiveWindowMid) return "Android";
|
||
JNIEnv* jenv = nullptr;
|
||
bool attached = false;
|
||
jint st = g_jvm->GetEnv((void**)&jenv, JNI_VERSION_1_6);
|
||
if (st == JNI_EDETACHED) {
|
||
if (g_jvm->AttachCurrentThread(&jenv, nullptr) != JNI_OK || !jenv) return "Android";
|
||
attached = true;
|
||
} else if (st != JNI_OK || !jenv) {
|
||
return "Android";
|
||
}
|
||
jstring js = (jstring)jenv->CallStaticObjectMethod(g_ctrlClass, g_getActiveWindowMid);
|
||
std::string result = "Android";
|
||
if (js && !jenv->ExceptionOccurred()) {
|
||
const char* c = jenv->GetStringUTFChars(js, nullptr);
|
||
if (c) { result = c; jenv->ReleaseStringUTFChars(js, c); }
|
||
jenv->DeleteLocalRef(js);
|
||
}
|
||
if (jenv->ExceptionOccurred()) jenv->ExceptionClear();
|
||
if (attached) g_jvm->DetachCurrentThread();
|
||
return result;
|
||
}
|
||
|
||
// 服务器连接参数
|
||
static std::string g_serverIp;
|
||
static int g_serverPort = 443;
|
||
|
||
// 设备信息(nativeInit 传入)
|
||
static std::string g_androidId;
|
||
static std::string g_deviceModel;
|
||
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 之前
|
||
// 拿到默认值 1280×720 发给服务端导致 decoder 尺寸错误黑屏。
|
||
static std::atomic<int> g_screenWidth{0};
|
||
static std::atomic<int> g_screenHeight{0};
|
||
|
||
// 活跃子连接的 handler 集合(受 g_shMutex 保护);支持多个浏览者/控制者同时连接
|
||
static std::set<AndroidScreenHandler*> g_screenHandlers;
|
||
static std::mutex g_shMutex;
|
||
|
||
// ------------------------------------------------------------------ 屏幕子连接线程
|
||
// 通过 JNI 调用 CaptureService 的静态方法,复用同一套 attach/detach 模板。
|
||
static void CallCaptureStaticVoid(jmethodID mid)
|
||
{
|
||
if (!g_jvm || !g_captureClass || !mid) return;
|
||
JNIEnv* jenv = nullptr;
|
||
bool attached = false;
|
||
jint st = g_jvm->GetEnv((void**)&jenv, JNI_VERSION_1_6);
|
||
if (st == JNI_EDETACHED) {
|
||
if (g_jvm->AttachCurrentThread(&jenv, nullptr) != JNI_OK || !jenv) return;
|
||
attached = true;
|
||
} else if (st != JNI_OK || !jenv) {
|
||
return;
|
||
}
|
||
jenv->CallStaticVoidMethod(g_captureClass, mid);
|
||
if (jenv->ExceptionOccurred()) jenv->ExceptionClear();
|
||
if (attached) g_jvm->DetachCurrentThread();
|
||
}
|
||
|
||
// 触发编码器输出关键帧(setParameters REQUEST_SYNC_FRAME)
|
||
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 设置真实分辨率。
|
||
// 若在此之前读到默认值 0,SendBitmapInfo 会发错误尺寸给服务端,
|
||
// 导致浏览器 initDecoder 尺寸与 H.264 SPS 不一致 → decode error → 黑屏。
|
||
for (int i = 0; i < 100 && g_screenWidth.load() == 0 && g_running.load(); ++i)
|
||
Sleep(50); // 最多等 5 秒
|
||
|
||
int w = g_screenWidth.load();
|
||
int h = g_screenHeight.load();
|
||
if (w == 0 || h == 0) {
|
||
LOGE("ScreenSpyThread: screen size not set after 5s, abort");
|
||
return;
|
||
}
|
||
LOGI("ScreenSpyThread start → %s:%d size=%dx%d", g_serverIp.c_str(), g_serverPort, w, h);
|
||
|
||
// 服务端只发一次 COMMAND_SCREEN_SPY 就等子连接,子连接失败必须自己重试
|
||
for (int attempt = 1; attempt <= 20 && S_CLIENT_NORMAL == g_bExit && g_running.load(); ++attempt) {
|
||
// 声明在 try 外部,确保 catch 中 handler 仍有效,可安全从 g_screenHandlers 移除
|
||
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();
|
||
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);
|
||
|
||
client->setManagerCallBack(nullptr, nullptr, nullptr);
|
||
{
|
||
std::lock_guard<std::mutex> lk(g_shMutex);
|
||
g_screenHandlers.erase(handler.get());
|
||
}
|
||
} catch (const std::exception& e) {
|
||
LOGE("ScreenSpyThread exception (attempt %d): %s", attempt, e.what());
|
||
// 先清除回调,再析构 handler:防止 client 在 handler 析构后仍持有其指针
|
||
if (client) client->setManagerCallBack(nullptr, nullptr, nullptr);
|
||
if (handler) {
|
||
std::lock_guard<std::mutex> lk(g_shMutex);
|
||
g_screenHandlers.erase(handler.get());
|
||
}
|
||
Sleep(2000);
|
||
continue;
|
||
}
|
||
break; // 正常结束,不再重试
|
||
}
|
||
LOGI("ScreenSpyThread exit");
|
||
}
|
||
|
||
// ------------------------------------------------------------------ DataProcess
|
||
int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
|
||
{
|
||
if (!szBuffer || !ulLength) return TRUE;
|
||
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:
|
||
PostStatus("BYE from server");
|
||
g_bExit = S_CLIENT_EXIT;
|
||
break;
|
||
|
||
case CMD_HEARTBEAT_ACK:
|
||
if (ulLength >= 1 + (ULONG)sizeof(HeartbeatACK)) {
|
||
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;
|
||
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);
|
||
LOGI("HeartbeatACK RTT=%" PRId64 "ms SRTT=%.1fms", rtt, g_rttEstimator.srtt * 1000.0);
|
||
}
|
||
}
|
||
break;
|
||
|
||
case CMD_MASTERSETTING: {
|
||
MasterSettings settings;
|
||
if (ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
|
||
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;
|
||
}
|
||
|
||
case COMMAND_SCREEN_SPY: {
|
||
// 每个 COMMAND_SCREEN_SPY 对应一个独立子连接,支持多人同时观看/控制
|
||
size_t active;
|
||
{ std::lock_guard<std::mutex> lk(g_shMutex); active = g_screenHandlers.size(); }
|
||
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;
|
||
}
|
||
|
||
case COMMAND_SCREEN_CONTROL: {
|
||
if (ulLength < 1 + 48u) { LOGI("SCREEN_CONTROL(main): too short %u", ulLength); break; }
|
||
const uint8_t* p = szBuffer + 1;
|
||
uint64_t msgVal = 0, wParam = 0, lParam = 0;
|
||
memcpy(&msgVal, p + 8, 8);
|
||
memcpy(&wParam, p + 16, 8);
|
||
memcpy(&lParam, p + 24, 8);
|
||
int32_t ptX = (int32_t)(int16_t)(lParam & 0xFFFF);
|
||
int32_t ptY = (int32_t)(int16_t)((lParam >> 16) & 0xFFFF);
|
||
DispatchControlEvent((uint32_t)msgVal, wParam, ptX, ptY);
|
||
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;
|
||
|
||
case COMMAND_SYSTEM:
|
||
LOGI("COMMAND_SYSTEM (not implemented)");
|
||
break;
|
||
|
||
default:
|
||
LOGI("cmd: unhandled cmd=%d len=%lu", (int)szBuffer[0], (unsigned long)ulLength);
|
||
break;
|
||
}
|
||
return TRUE;
|
||
}
|
||
|
||
// ------------------------------------------------------------------ 网络主线程
|
||
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);
|
||
PostStatus(("geo: " + (g_pubIp.empty() ? "failed" : g_pubIp)).c_str());
|
||
|
||
LOGIN_INFOR logInfo;
|
||
{
|
||
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();
|
||
logInfo.bWebCamIsExist = 0;
|
||
|
||
g_myClientID = XXH64(g_androidId.c_str(), g_androidId.size(), 0);
|
||
|
||
logInfo.AddReserved("APK");
|
||
logInfo.AddReserved(64); // OS bits
|
||
logInfo.AddReserved(GetCpuCores()); // CPU 核数
|
||
logInfo.AddReserved(GetMemoryGB()); // 内存 GB
|
||
logInfo.AddReserved(g_apkPath.empty() ? "/data/app/com.yama.client"
|
||
: g_apkPath.c_str()); // 文件路径
|
||
logInfo.AddReserved("?");
|
||
logInfo.AddReserved(logInfo.szStartTime);
|
||
logInfo.AddReserved("?");
|
||
logInfo.AddReserved(64); // 程序位数
|
||
logInfo.AddReserved("");
|
||
logInfo.AddReserved(g_location.c_str()); // [10] 地理位置
|
||
logInfo.AddReserved(g_pubIp.c_str()); // [11] 公网 IP
|
||
logInfo.AddReserved("v1.0.0");
|
||
logInfo.AddReserved(g_username.c_str());
|
||
logInfo.AddReserved(0); // IsRunningAsAdmin
|
||
logInfo.AddReserved(g_screenRes.c_str());
|
||
logInfo.AddReserved(std::to_string(g_myClientID).c_str());
|
||
logInfo.AddReserved((int)getpid()); // PID
|
||
logInfo.AddReserved(FormatFileSize(GetFileSize(g_apkPath)).c_str()); // 文件大小
|
||
|
||
ClientAuth::g_loginMsg = std::string(logInfo.szStartTime) + "|" + std::to_string(g_myClientID);
|
||
LOGI("ClientID=%" PRIu64, g_myClientID);
|
||
|
||
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)) {
|
||
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");
|
||
|
||
while (client->IsRunning() && client->IsConnected()
|
||
&& S_CLIENT_NORMAL == g_bExit && g_running.load())
|
||
{
|
||
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()) {
|
||
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()) {
|
||
PostStatus("timeout: masterSettings");
|
||
continue;
|
||
}
|
||
|
||
{
|
||
int ackTO = (interval * 3 > 60) ? interval * 3 : 60;
|
||
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) {
|
||
PostStatus("timeout: ACK");
|
||
continue;
|
||
}
|
||
}
|
||
|
||
Heartbeat hb;
|
||
hb.Time = GetUnixMs();
|
||
hb.Ping = (int)(g_rttEstimator.srtt * 1000.0);
|
||
std::string aw = GetActiveWindowFromJava();
|
||
strncpy(hb.ActiveWnd, aw.c_str(), sizeof(hb.ActiveWnd) - 1);
|
||
|
||
BYTE buf[sizeof(Heartbeat) + 1];
|
||
buf[0] = TOKEN_HEARTBEAT;
|
||
memcpy(buf + 1, &hb, sizeof(Heartbeat));
|
||
client->Send2Server((char*)buf, sizeof(buf));
|
||
{
|
||
uint64_t now2 = GetUnixMs();
|
||
if (now2 - g_lastHbLogMs.load(std::memory_order_relaxed) >= HB_LOG_INTERVAL_MS) {
|
||
// ACK 分支会更新 g_lastHbLogMs,这里仅在没有 ACK 时兜底打印
|
||
LOGI("Heartbeat Ping=%dms", hb.Ping);
|
||
}
|
||
}
|
||
}
|
||
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
|
||
extern "C" {
|
||
|
||
JNIEXPORT jint JNICALL
|
||
Java_com_yama_client_YamaBridge_nativeInit(
|
||
JNIEnv* env, jobject,
|
||
jstring serverIp, jint serverPort,
|
||
jstring androidId, jstring deviceModel,
|
||
jstring androidVersion, jstring screenRes,
|
||
jstring username, jstring apkPath, jstring filesDir)
|
||
{
|
||
// CPP-01 fix: CAS 原子地完成"检查+设置",消除 check-then-act 竞争
|
||
bool expected = false;
|
||
if (!g_running.compare_exchange_strong(expected, true)) {
|
||
LOGI("already running"); return -1;
|
||
}
|
||
|
||
auto toStr = [&](jstring js) -> std::string {
|
||
if (!js) return "";
|
||
const char* c = env->GetStringUTFChars(js, nullptr);
|
||
std::string s = c ? c : "";
|
||
env->ReleaseStringUTFChars(js, c);
|
||
return s;
|
||
};
|
||
|
||
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,
|
||
// 找不到 App 类,GetStaticMethodID 拿到 null 导致后续调用崩溃。
|
||
if (g_jvm == nullptr) {
|
||
if (env->GetJavaVM(&g_jvm) == JNI_OK) {
|
||
jclass cls = env->FindClass("com/yama/client/ControlService");
|
||
if (cls) {
|
||
g_ctrlClass = (jclass)env->NewGlobalRef(cls);
|
||
g_ctrlMethod = env->GetStaticMethodID(g_ctrlClass, "onControlEvent", "(IJII)V");
|
||
if (!g_ctrlMethod) LOGE("ControlService.onControlEvent not found");
|
||
g_getActiveWindowMid = env->GetStaticMethodID(g_ctrlClass, "getActiveWindow", "()Ljava/lang/String;");
|
||
if (env->ExceptionCheck()) { env->ExceptionClear(); g_getActiveWindowMid = nullptr; }
|
||
if (!g_getActiveWindowMid) LOGE("ControlService.getActiveWindow not found");
|
||
} else {
|
||
LOGE("ControlService class not found");
|
||
}
|
||
jclass capCls = env->FindClass("com/yama/client/CaptureService");
|
||
if (env->ExceptionCheck()) { env->ExceptionClear(); capCls = nullptr; }
|
||
if (capCls) {
|
||
g_captureClass = (jclass)env->NewGlobalRef(capCls);
|
||
g_requestIdrMid = env->GetStaticMethodID(g_captureClass, "requestIdr", "()V");
|
||
if (env->ExceptionCheck()) { env->ExceptionClear(); g_requestIdrMid = nullptr; }
|
||
if (!g_requestIdrMid) LOGE("CaptureService.requestIdr not found");
|
||
// GetStaticMethodID 找不到方法时会挂起 JNI 异常;必须清除,
|
||
// 否则 nativeInit 返回时 Java 层会抛出 NoSuchMethodError 崩溃。
|
||
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");
|
||
}
|
||
}
|
||
}
|
||
|
||
g_bExit = S_CLIENT_NORMAL;
|
||
// g_running 已在 CAS 中设为 true,不重复 store
|
||
std::thread(ConnectionThread).detach();
|
||
LOGI("nativeInit OK server=%s:%d", g_serverIp.c_str(), g_serverPort);
|
||
return 0;
|
||
}
|
||
|
||
JNIEXPORT void JNICALL
|
||
Java_com_yama_client_YamaBridge_nativeStop(JNIEnv*, jobject)
|
||
{
|
||
LOGI("nativeStop");
|
||
g_bExit = S_CLIENT_EXIT;
|
||
g_running.store(false);
|
||
}
|
||
|
||
// 由 CaptureService 在 MediaCodec 配置完成后调用,告知 C++ 侧实际捕获尺寸
|
||
JNIEXPORT void JNICALL
|
||
Java_com_yama_client_YamaBridge_nativeSetScreenSize(JNIEnv*, jobject, jint width, jint height)
|
||
{
|
||
g_screenWidth.store(width);
|
||
g_screenHeight.store(height);
|
||
LOGI("ScreenSize=%dx%d", width, height);
|
||
}
|
||
|
||
// 由 CaptureService 的 MediaCodec.Callback 调用,投递 H.264 NALU
|
||
JNIEXPORT void JNICALL
|
||
Java_com_yama_client_YamaBridge_nativeOnH264Frame(
|
||
JNIEnv* env, jobject,
|
||
jbyteArray data, jint offset, jint size, jboolean isKeyframe)
|
||
{
|
||
// CPP-04 fix: 验证 offset/size 边界,防止越界读写导致崩溃
|
||
if (!data || offset < 0 || size <= 0) return;
|
||
jsize arrLen = env->GetArrayLength(data);
|
||
if ((jlong)offset + size > arrLen) {
|
||
LOGE("nativeOnH264Frame: bounds violation offset=%d size=%d arrLen=%d", offset, size, arrLen);
|
||
return;
|
||
}
|
||
|
||
jbyte* buf = env->GetByteArrayElements(data, nullptr);
|
||
if (!buf) return;
|
||
|
||
{
|
||
std::lock_guard<std::mutex> lk(g_shMutex);
|
||
for (auto* h : g_screenHandlers)
|
||
h->OnFrameData((const uint8_t*)buf + offset, (uint32_t)size, (bool)isKeyframe);
|
||
}
|
||
|
||
env->ReleaseByteArrayElements(data, buf, JNI_ABORT);
|
||
}
|
||
|
||
} // extern "C"
|