From 4e5c7a02db06127b4b7fb0e90c788e0036aa0652 Mon Sep 17 00:00:00 2001 From: esefxdz Date: Sat, 17 Oct 2026 15:37:29 +0300 Subject: [PATCH] fix(runtime): make Xray core recovery work, and stop it leaking a core per try Core recovery could never succeed, and every attempt left a live libxray.so behind. startCore() calls startTimer(), which builds a CountDownTimer and so a Handler on the calling thread. handleXrayCoreExit() runs recovery on a bare Thread, so the constructor threw "Can't create handler inside thread ... that has not called Looper.prepare()" and took the whole restart down with it. The catch then deleted the ephemeral config and returned false without killing the process pb.start() had already started, so each retry stranded another core. Measured on an emulator by killing the live core: before 1 -> 5 -> 6 -> 8 processes over 120 s, never recovers after "Xray core recovered without dropping TUN" on attempt 1, exactly 1 process startTimer now posts to the main looper, re-checking RUNTIME_TOKEN inside the post so a stop that lands while it is queued cannot leave a timer running for a dead generation. stopTimer posts too, so the countDownTimer field is only touched from one thread. The catch clears xrayProcess before destroying it, so the monitor thread's `xrayProcess === process` guard fails and recovery is not re-entered for a core being killed here. Pre-existing on main; reachable whenever the core exits. Co-Authored-By: Claude Opus 5 --- .../xray/core/XrayCoreManager.kt | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/xray/core/XrayCoreManager.kt b/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/xray/core/XrayCoreManager.kt index 54975b6..e7deba1 100644 --- a/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/xray/core/XrayCoreManager.kt +++ b/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/xray/core/XrayCoreManager.kt @@ -11,6 +11,8 @@ import android.content.pm.PackageManager import android.graphics.Color import android.os.Build import android.os.CountDownTimer +import android.os.Handler +import android.os.Looper import android.util.Log import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat @@ -383,6 +385,18 @@ object XrayCoreManager { } catch (e: Exception) { Log.e(TAG, "Failed to start Xray process", e) + // The process may already be running: pb.start() succeeds before + // anything below it can throw. Leaving it alive leaked one live + // core per recovery attempt. Clear the field BEFORE destroying so + // the monitor thread's `xrayProcess === process` guard fails and + // recovery is not re-entered for a core we are killing here. + val orphan = xrayProcess + xrayProcess = null + try { + orphan?.destroy() + } catch (destroyError: Exception) { + Log.w(TAG, "Failed to destroy partially started Xray process", destroyError) + } deleteEphemeralConfigOnFailure(configFilesDir) return false } @@ -429,7 +443,25 @@ object XrayCoreManager { AppConfigs.V2RAY_STATE == AppConfigs.V2RAY_STATES.V2RAY_CONNECTING } + // CountDownTimer builds a Handler, so it only constructs on a looper thread. + // Core recovery runs on a bare Thread (XrayVPNService.handleXrayCoreExit), + // where this threw "Can't create handler inside thread ... that has not + // called Looper.prepare()" and took the whole restart down with it. + private val mainHandler = Handler(Looper.getMainLooper()) + + private fun runOnMain(block: () -> Unit) { + if (Looper.myLooper() == Looper.getMainLooper()) block() else mainHandler.post(block) + } + private fun startTimer(context: Context) { + // Captured now: a stop that lands while the post is queued must not + // leave a timer ticking for a generation that is already gone. + val token = AppConfigs.V2RAY_CONFIG?.RUNTIME_TOKEN + runOnMain { startTimerOnMain(context, token) } + } + + private fun startTimerOnMain(context: Context, token: String?) { + if (AppConfigs.V2RAY_CONFIG?.RUNTIME_TOKEN != token) return countDownTimer?.cancel() seconds = 0 countDownTimer = object : CountDownTimer(Long.MAX_VALUE, 1000) { @@ -454,9 +486,11 @@ object XrayCoreManager { } private fun stopTimer() { - countDownTimer?.cancel() - countDownTimer = null - seconds = 0 + runOnMain { + countDownTimer?.cancel() + countDownTimer = null + seconds = 0 + } } fun sendStatusBroadcast(context: Context, config: XrayConfig) {