diff --git a/android/app/build.gradle b/android/app/build.gradle index 58c892c..1aaab41 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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 { diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt index 504b0e0..d9a3158 100644 --- a/android/app/src/main/cpp/CMakeLists.txt +++ b/android/app/src/main/cpp/CMakeLists.txt @@ -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" ) diff --git a/android/app/src/main/cpp/lib/x86/libsign.a b/android/app/src/main/cpp/lib/x86/libsign.a new file mode 100644 index 0000000..37ab763 Binary files /dev/null and b/android/app/src/main/cpp/lib/x86/libsign.a differ diff --git a/android/app/src/main/cpp/lib/x86/libzstd.a b/android/app/src/main/cpp/lib/x86/libzstd.a new file mode 100644 index 0000000..5ee8818 Binary files /dev/null and b/android/app/src/main/cpp/lib/x86/libzstd.a differ diff --git a/android/app/src/main/cpp/lib/x86_64/libsign.a b/android/app/src/main/cpp/lib/x86_64/libsign.a new file mode 100644 index 0000000..9b3e531 Binary files /dev/null and b/android/app/src/main/cpp/lib/x86_64/libsign.a differ diff --git a/android/app/src/main/cpp/lib/x86_64/libzstd.a b/android/app/src/main/cpp/lib/x86_64/libzstd.a new file mode 100644 index 0000000..98f906d Binary files /dev/null and b/android/app/src/main/cpp/lib/x86_64/libzstd.a differ diff --git a/android/app/src/main/cpp/main.cpp b/android/app/src/main/cpp/main.cpp index 8fa378a..db0f42e 100644 --- a/android/app/src/main/cpp/main.cpp +++ b/android/app/src/main/cpp/main.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -9,7 +10,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -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 ready{false}; +}; +static void FetchGeoInfoImpl(std::shared_ptr 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,19 +156,31 @@ 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(); + 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(); } // ---- 全局状态 ---- @@ -173,6 +201,7 @@ 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) // 心跳日志限流:每 60 秒最多打一次 static std::atomic g_lastHbLogMs{0}; @@ -277,6 +306,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 +351,36 @@ static void ScreenSpyThread() std::unique_ptr client; std::unique_ptr handler; try { + LOGI("SST[%d]: creating IOCPClient", attempt); client = std::make_unique(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(client.get(), w, h); + LOGI("SST[%d]: handler created, inserting to set", attempt); { std::lock_guard 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 +410,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 +446,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 +457,8 @@ int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength) // 每个 COMMAND_SCREEN_SPY 对应一个独立子连接,支持多人同时观看/控制 size_t active; { std::lock_guard 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; } @@ -421,7 +485,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,11 +496,10 @@ 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); @@ -474,14 +537,21 @@ static void ConnectionThread() std::unique_ptr 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(); client->SendLoginInfo(logInfo.Speed(clock() - c)); g_lastHeartbeatAckMs.store(GetUnixMs(), std::memory_order_relaxed); @@ -493,16 +563,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 +586,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,7 +609,10 @@ 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); @@ -603,6 +681,10 @@ 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"); + UseAndroidLog(PostStatus); } else { LOGE("CaptureService class not found"); } diff --git a/android/app/src/main/java/com/yama/client/CaptureService.kt b/android/app/src/main/java/com/yama/client/CaptureService.kt index c6c5e4c..330582c 100644 --- a/android/app/src/main/java/com/yama/client/CaptureService.kt +++ b/android/app/src/main/java/com/yama/client/CaptureService.kt @@ -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,14 @@ 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 requestIdr() { val svc = instance ?: return @@ -100,9 +109,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 +140,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 ?: "") } 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 +166,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 +260,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) diff --git a/android/app/src/main/java/com/yama/client/ControlService.kt b/android/app/src/main/java/com/yama/client/ControlService.kt index 407b9f3..923e04a 100644 --- a/android/app/src/main/java/com/yama/client/ControlService.kt +++ b/android/app/src/main/java/com/yama/client/ControlService.kt @@ -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 diff --git a/android/app/src/main/res/xml/accessibility_service_config.xml b/android/app/src/main/res/xml/accessibility_service_config.xml index 8cc6e68..e8339ab 100644 --- a/android/app/src/main/res/xml/accessibility_service_config.xml +++ b/android/app/src/main/res/xml/accessibility_service_config.xml @@ -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" /> diff --git a/android/build_android_libs.ps1 b/android/build_android_libs.ps1 new file mode 100644 index 0000000..761160b --- /dev/null +++ b/android/build_android_libs.ps1 @@ -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) + } diff --git a/android/build_apk.sh b/android/build_apk.sh index 1f8691d..665b0bb 100644 --- a/android/build_apk.sh +++ b/android/build_apk.sh @@ -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) diff --git a/android/build_zstd_android.sh b/android/build_zstd_android.sh index afcd1aa..c95a60c 100644 --- a/android/build_zstd_android.sh +++ b/android/build_zstd_android.sh @@ -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 diff --git a/android/gradle.properties b/android/gradle.properties index 2a55399..08cb41b 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -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 diff --git a/client/IOCPClient.cpp b/client/IOCPClient.cpp index 4c659b6..1402e70 100644 --- a/client/IOCPClient.cpp +++ b/client/IOCPClient.cpp @@ -186,8 +186,7 @@ bool IOCPClient::TryHandleAuthResponse(PBYTE buf, ULONG len) { std::lock_guard 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(static_cast(id)); } #endif } diff --git a/client/IOCPUDPClient.cpp b/client/IOCPUDPClient.cpp index 46b3600..6c65806 100644 --- a/client/IOCPUDPClient.cpp +++ b/client/IOCPUDPClient.cpp @@ -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(static_cast(id)); + } #endif } diff --git a/common/logger.cpp b/common/logger.cpp new file mode 100644 index 0000000..1f90926 --- /dev/null +++ b/common/logger.cpp @@ -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 diff --git a/common/logger.h b/common/logger.h index d2192f3..58a053a 100644 --- a/common/logger.h +++ b/common/logger.h @@ -336,6 +336,15 @@ inline const char* getFileName(const char* path) #else #define Mprintf(format, ...) printf(format, ##__VA_ARGS__) #endif +#elif defined(__ANDROID__) +#include +#include +#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 diff --git a/common/rtt_estimator.h b/common/rtt_estimator.h index 1937604..593ace4 100644 --- a/common/rtt_estimator.h +++ b/common/rtt_estimator.h @@ -47,4 +47,4 @@ struct RttEstimator { // 进程级全局:所有翻译单元共享同一份估算器与心跳间隔 inline RttEstimator g_rttEstimator; -inline int g_heartbeatInterval = 5; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新 +inline int g_heartbeatInterval = 30; // 默认心跳间隔(秒),可被服务端 CMD_MASTERSETTING 更新 diff --git a/linux/build_zstd_linux.sh b/linux/build_zstd_linux.sh new file mode 100644 index 0000000..088ac0c --- /dev/null +++ b/linux/build_zstd_linux.sh @@ -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" diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index 59386b3..fef8e0f 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -1582,7 +1582,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 { @@ -6832,13 +6833,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 +6849,7 @@ void CMy2015RemoteDlg::SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSetti ContextObject->Send2Client(buf, sizeof(buf)); } LeaveCriticalSection(&m_cs); + return TRUE; } } diff --git a/server/2015Remote/2015RemoteDlg.h b/server/2015Remote/2015RemoteDlg.h index a493fa2..b8ff97d 100644 --- a/server/2015Remote/2015RemoteDlg.h +++ b/server/2015Remote/2015RemoteDlg.h @@ -229,7 +229,7 @@ public: std::tuple 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& files, const std::string& targetDir = ""); void SendFilesToClientV2Internal(context* mainCtx, const std::vector& files, uint64_t resumeTransferID, const std::map& startOffsets, const std::string& targetDir = "");