@onekeyfe/react-native-background-thread 3.0.90 → 3.0.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  package com.backgroundthread
2
2
 
3
3
  import android.app.Activity
4
+ import android.content.Context
4
5
  import android.content.Intent
5
6
  import android.net.Uri
6
7
  import android.os.Handler
@@ -11,6 +12,7 @@ import com.facebook.react.ReactPackage
11
12
  import com.facebook.proguard.annotations.DoNotStrip
12
13
  import com.facebook.react.ReactInstanceEventListener
13
14
  import com.facebook.react.bridge.JSBundleLoader
15
+ import com.facebook.react.bridge.JavaScriptModule
14
16
  import com.facebook.react.bridge.ReactApplicationContext
15
17
  import com.facebook.react.bridge.ReactContext
16
18
  import com.facebook.react.common.annotations.UnstableReactNativeAPI
@@ -18,13 +20,17 @@ import com.facebook.react.defaults.DefaultComponentsRegistry
18
20
  import com.facebook.react.defaults.DefaultReactHostDelegate
19
21
  import com.facebook.react.defaults.DefaultTurboModuleManagerDelegate
20
22
  import com.facebook.react.fabric.ComponentFactory
23
+ import com.facebook.react.interfaces.TaskInterface
21
24
  import com.facebook.react.runtime.ReactHostImpl
22
25
  import com.facebook.react.runtime.hermes.HermesInstance
23
26
  import com.facebook.react.shell.MainReactPackage
24
27
  import java.io.File
25
28
  import java.lang.ref.WeakReference
29
+ import java.util.concurrent.CountDownLatch
26
30
  import java.util.concurrent.TimeUnit
27
31
  import java.util.concurrent.atomic.AtomicBoolean
32
+ import java.util.concurrent.atomic.AtomicLong
33
+ import java.util.concurrent.atomic.AtomicReference
28
34
 
29
35
  /**
30
36
  * Singleton manager for the background React Native runtime.
@@ -36,9 +42,38 @@ import java.util.concurrent.atomic.AtomicBoolean
36
42
  * - Cross-runtime communication via SharedRPC onWrite notifications
37
43
  */
38
44
  class BackgroundThreadManager private constructor() {
45
+ // OneKey patch: the prebuilt RN AAR exposes the legacy six-argument HMRClient
46
+ // interface, while the patched JS client accepts an exact bundle URL as argument seven.
47
+ private interface HMRClient : JavaScriptModule {
48
+ fun setup(
49
+ platform: String?,
50
+ bundleEntry: String?,
51
+ host: String?,
52
+ port: Int,
53
+ isEnabled: Boolean,
54
+ scheme: String?,
55
+ fullBundleUrlOverride: String?,
56
+ )
57
+ }
39
58
 
59
+ // OneKey patch: background uses the same local dev-vendor HBC as main,
60
+ // then evaluates its own modulesOnly Metro delta in the isolated runtime.
61
+ private data class DevVendorConfig(
62
+ val commonAssetName: String,
63
+ val fingerprint: String,
64
+ val backgroundHMREnabled: Boolean,
65
+ val runtimeGeneration: Long = 0,
66
+ )
40
67
  private var bgReactHost: ReactHostImpl? = null
41
68
  private var reactPackages: List<ReactPackage> = emptyList()
69
+ private val backgroundRuntimeGeneration = AtomicLong(0)
70
+ private val backgroundHMRRestartInFlight = AtomicBoolean(false)
71
+
72
+ @Volatile
73
+ private var lastBackgroundEntryURL: String? = null
74
+
75
+ @Volatile
76
+ private var lastDevVendorConfig: DevVendorConfig? = null
42
77
 
43
78
  // Tracks the last resumed Activity so we can replay it onto the bg
44
79
  // ReactContext as soon as the bg host finishes initializing (covers the
@@ -59,7 +94,18 @@ class BackgroundThreadManager private constructor() {
59
94
  // full process restart in that case.
60
95
  @Volatile
61
96
  private var mainReactHost: ReactHost? = null
62
- private var isStarted = false
97
+ private val runnerState = AtomicReference(BackgroundRunnerState.IDLE)
98
+
99
+ @Volatile
100
+ private var runnerStartFailureMessage: String? = null
101
+
102
+ private enum class BackgroundRunnerState {
103
+ IDLE,
104
+ STARTING,
105
+ RUNNING,
106
+ FAILED,
107
+ DESTROYING,
108
+ }
63
109
 
64
110
  companion object {
65
111
  private const val MODULE_NAME = "background"
@@ -101,10 +147,19 @@ class BackgroundThreadManager private constructor() {
101
147
 
102
148
  // ── JNI declarations ────────────────────────────────────────────────────
103
149
 
104
- private external fun nativeInstallSharedBridge(runtimePtr: Long, isMain: Boolean)
150
+ private external fun nativeInstallSharedBridge(
151
+ runtimePtr: Long,
152
+ isMain: Boolean,
153
+ runtimeGeneration: Long,
154
+ )
105
155
  private external fun nativeSetupErrorHandler(runtimePtr: Long)
106
156
  private external fun nativeDestroy()
107
- private external fun nativeExecuteWork(runtimePtr: Long, workId: Long)
157
+ private external fun nativeExecuteWork(
158
+ runtimePtr: Long,
159
+ workId: Long,
160
+ isMain: Boolean,
161
+ runtimeGeneration: Long,
162
+ )
108
163
 
109
164
  /**
110
165
  * Evaluate the segment at [segmentPath] into the BACKGROUND runtime on its
@@ -139,7 +194,11 @@ class BackgroundThreadManager private constructor() {
139
194
  * released and the JS promise resolves immediately instead of leaking until
140
195
  * teardown or the bg watchdog. Exactly-once on the native side.
141
196
  */
142
- private external fun nativeDropScheduledWork(isMain: Boolean, workId: Long)
197
+ private external fun nativeDropScheduledWork(
198
+ isMain: Boolean,
199
+ workId: Long,
200
+ runtimeGeneration: Long,
201
+ )
143
202
 
144
203
  /**
145
204
  * Synchronously mark the SharedRPC listener for `runtimeId` as dead
@@ -147,7 +206,19 @@ class BackgroundThreadManager private constructor() {
147
206
  * SharedRPC::invalidate (cpp/SharedRPC.cpp) for the cross-runtime
148
207
  * correctness rationale.
149
208
  */
150
- private external fun nativeInvalidateSharedRpc(runtimeId: String): Boolean
209
+ private external fun nativeInvalidateSharedRpc(
210
+ runtimeId: String,
211
+ runtimeGeneration: Long,
212
+ ): Boolean
213
+
214
+ /**
215
+ * Quiesce background-native work and release JSI callbacks while this call
216
+ * still runs on the outgoing background runtime's JS thread. Destroying
217
+ * those callbacks from the UI or teardown worker thread is unsafe.
218
+ */
219
+ private external fun nativeInvalidateBackgroundRuntimeOnJSThread(
220
+ runtimeGeneration: Long,
221
+ ): Boolean
151
222
 
152
223
  // ── SharedBridge ────────────────────────────────────────────────────────
153
224
 
@@ -183,7 +254,7 @@ class BackgroundThreadManager private constructor() {
183
254
  val ptr = context.javaScriptContextHolder?.get() ?: 0L
184
255
  if (ptr != 0L) {
185
256
  mainRuntimePtr = ptr
186
- nativeInstallSharedBridge(ptr, true)
257
+ nativeInstallSharedBridge(ptr, true, 0)
187
258
  BTLogger.info("SharedBridge installed in main runtime")
188
259
  } else {
189
260
  BTLogger.warn("Main runtime pointer is 0, cannot install SharedBridge")
@@ -332,7 +403,77 @@ class BackgroundThreadManager private constructor() {
332
403
  }
333
404
  }
334
405
  }
335
-
406
+ private fun createDevVendorBundleLoader(
407
+ appContext: android.content.Context,
408
+ entryURL: String,
409
+ config: DevVendorConfig,
410
+ ): JSBundleLoader {
411
+ return object : JSBundleLoader() {
412
+ override fun loadScript(delegate: com.facebook.react.bridge.JSBundleLoaderDelegate): String {
413
+ val totalStart = System.nanoTime()
414
+ delegate.loadScriptFromAssets(
415
+ appContext.assets,
416
+ "assets://${config.commonAssetName}",
417
+ false,
418
+ )
419
+ val assertionFile = File(appContext.cacheDir, "onekey-dev-vendor-assert-background.js")
420
+ val quotedEntryURL = org.json.JSONObject.quote(entryURL)
421
+ assertionFile.writeText(
422
+ "if(globalThis.__ONEKEY_DEV_VENDOR_FINGERPRINT__!==\"${config.fingerprint}\")" +
423
+ "{throw new Error(\"Dev-vendor common fingerprint mismatch\");}" +
424
+ "globalThis.__ONEKEY_DEV_VENDOR_FULL_BUNDLE_URL__=$quotedEntryURL;" +
425
+ "globalThis.__ONEKEY_RUNTIME_TARGET__=\"background\";" +
426
+ "globalThis.__ONEKEY_BACKGROUND_RUNTIME_GENERATION__=${config.runtimeGeneration};",
427
+ )
428
+ delegate.loadScriptFromFile(
429
+ assertionFile.absolutePath,
430
+ entryURL,
431
+ false,
432
+ )
433
+ val deltaFile = File(
434
+ appContext.cacheDir,
435
+ "onekey-dev-vendor-background-${config.fingerprint.take(12)}.bundle",
436
+ )
437
+ val temporaryFile = File.createTempFile(
438
+ ".${deltaFile.name}-",
439
+ ".tmp",
440
+ deltaFile.parentFile,
441
+ )
442
+ try {
443
+ val connection = java.net.URL(entryURL).openConnection() as java.net.HttpURLConnection
444
+ try {
445
+ connection.connectTimeout = 15_000
446
+ connection.readTimeout = 60_000
447
+ val statusCode = connection.responseCode
448
+ if (statusCode !in 200..299) {
449
+ throw IllegalStateException(
450
+ "Dev-vendor background delta returned HTTP $statusCode",
451
+ )
452
+ }
453
+ connection.inputStream.use { input ->
454
+ temporaryFile.outputStream().use { output -> input.copyTo(output) }
455
+ }
456
+ } finally {
457
+ connection.disconnect()
458
+ }
459
+ android.system.Os.rename(
460
+ temporaryFile.absolutePath,
461
+ deltaFile.absolutePath,
462
+ )
463
+ } catch (error: Exception) {
464
+ temporaryFile.delete()
465
+ throw RuntimeException("Failed to download dev-vendor background delta", error)
466
+ }
467
+ delegate.loadScriptFromFile(deltaFile.absolutePath, entryURL, false)
468
+ val totalMs = (System.nanoTime() - totalStart) / 1_000_000.0
469
+ BTLogger.info(
470
+ "[DevVendor] background common.hbc + Metro delta loaded in " +
471
+ "${String.format("%.1f", totalMs)}ms",
472
+ )
473
+ return entryURL
474
+ }
475
+ }
476
+ }
336
477
  private fun createLocalFileBundleLoader(localPath: String, sourceURL: String): JSBundleLoader {
337
478
  return object : JSBundleLoader() {
338
479
  override fun loadScript(delegate: com.facebook.react.bridge.JSBundleLoaderDelegate): String {
@@ -347,115 +488,332 @@ class BackgroundThreadManager private constructor() {
347
488
  }
348
489
  }
349
490
 
350
- @OptIn(UnstableReactNativeAPI::class)
351
491
  fun startBackgroundRunnerWithEntryURL(context: ReactApplicationContext, entryURL: String) {
352
- if (isStarted) {
353
- BTLogger.warn("Background runner already started")
354
- return
492
+ ensureBackgroundRunnerWithEntryURL(context.applicationContext, entryURL)
493
+ }
494
+
495
+ @OptIn(UnstableReactNativeAPI::class)
496
+ fun ensureBackgroundRunnerWithEntryURL(context: Context, entryURL: String): Boolean {
497
+ return ensureBackgroundRunner(context, entryURL, null)
498
+ }
499
+
500
+ @OptIn(UnstableReactNativeAPI::class)
501
+ fun ensureBackgroundRunnerWithDevVendor(
502
+ context: Context,
503
+ entryURL: String,
504
+ commonAssetName: String,
505
+ fingerprint: String,
506
+ backgroundHMREnabled: Boolean,
507
+ ): Boolean {
508
+ return ensureBackgroundRunner(
509
+ context,
510
+ entryURL,
511
+ DevVendorConfig(commonAssetName, fingerprint, backgroundHMREnabled),
512
+ )
513
+ }
514
+
515
+ @OptIn(UnstableReactNativeAPI::class)
516
+ private fun ensureBackgroundRunner(
517
+ context: Context,
518
+ entryURL: String,
519
+ devVendorConfig: DevVendorConfig?,
520
+ ): Boolean {
521
+ if (!runnerState.compareAndSet(BackgroundRunnerState.IDLE, BackgroundRunnerState.STARTING)) {
522
+ BTLogger.info("Background runner start coalesced: state=${getBackgroundRunnerState()}")
523
+ return false
355
524
  }
525
+
526
+ runnerStartFailureMessage = null
527
+ val runtimeGeneration = backgroundRuntimeGeneration.incrementAndGet()
528
+ val activeDevVendorConfig =
529
+ devVendorConfig?.copy(runtimeGeneration = runtimeGeneration)
530
+ lastBackgroundEntryURL = entryURL
531
+ lastDevVendorConfig = devVendorConfig
356
532
  val bgStartTime = System.nanoTime()
357
533
  BTLogger.info("[SplitBundle] background runner starting with entryURL: $entryURL")
358
534
 
359
535
  val appContext = context.applicationContext
360
- val packages =
361
- if (reactPackages.isNotEmpty()) {
362
- reactPackages
363
- } else {
364
- BTLogger.warn("No ReactPackages registered for background runtime; call setReactPackages(...) from host before start. Falling back to MainReactPackage only.")
365
- listOf(MainReactPackage())
366
- }
536
+ try {
537
+ val packages =
538
+ if (reactPackages.isNotEmpty()) {
539
+ reactPackages
540
+ } else {
541
+ BTLogger.warn("No ReactPackages registered for background runtime; call setReactPackages(...) from host before start. Falling back to MainReactPackage only.")
542
+ listOf(MainReactPackage())
543
+ }
367
544
 
368
- val localBundlePath = resolveLocalBundlePath(entryURL)
369
- val bundleLoader =
370
- when {
371
- // Debug mode: remote URL — use single bundle (Metro dev server)
372
- isRemoteBundleUrl(entryURL) -> createDownloadedBundleLoader(appContext, entryURL)
373
-
374
- // OTA / local file path try sequential loading with common bundle
375
- localBundlePath != null -> {
376
- val commonPath = resolveCommonBundlePath(localBundlePath)
377
- if (commonPath != null) {
378
- BTLogger.info("Using sequential file bundle loader: common=$commonPath, entry=$localBundlePath")
379
- createSequentialFileBundleLoader(commonPath, localBundlePath, entryURL)
380
- } else {
381
- BTLogger.info("No common bundle found for OTA path, using single bundle: $localBundlePath")
382
- createLocalFileBundleLoader(localBundlePath, entryURL)
545
+ val localBundlePath = resolveLocalBundlePath(entryURL)
546
+ val bundleLoader =
547
+ when {
548
+ activeDevVendorConfig != null && isRemoteBundleUrl(entryURL) ->
549
+ createDevVendorBundleLoader(appContext, entryURL, activeDevVendorConfig)
550
+
551
+ // Debug mode: remote URLuse single bundle (Metro dev server)
552
+ isRemoteBundleUrl(entryURL) -> createDownloadedBundleLoader(appContext, entryURL)
553
+
554
+ // OTA / local file path — try sequential loading with common bundle
555
+ localBundlePath != null -> {
556
+ val commonPath = resolveCommonBundlePath(localBundlePath)
557
+ if (commonPath != null) {
558
+ BTLogger.info("Using sequential file bundle loader: common=$commonPath, entry=$localBundlePath")
559
+ createSequentialFileBundleLoader(commonPath, localBundlePath, entryURL)
560
+ } else {
561
+ BTLogger.info("No common bundle found for OTA path, using single bundle: $localBundlePath")
562
+ createLocalFileBundleLoader(localBundlePath, entryURL)
563
+ }
383
564
  }
384
- }
385
565
 
386
- // Assets-based loading — try sequential loading with common.bundle in assets
387
- entryURL.startsWith("assets://") -> {
388
- val entryAssetName = entryURL.removePrefix("assets://")
389
- if (hasCommonBundleInAssets(appContext)) {
390
- BTLogger.info("Using sequential asset bundle loader: common=common.bundle, entry=$entryAssetName")
391
- createSequentialAssetBundleLoader(appContext, "common.bundle", entryAssetName)
392
- } else {
393
- BTLogger.info("No common.bundle in assets, using single bundle: $entryURL")
394
- JSBundleLoader.createAssetLoader(appContext, entryURL, true)
566
+ // Assets-based loading — try sequential loading with common.bundle in assets
567
+ entryURL.startsWith("assets://") -> {
568
+ val entryAssetName = entryURL.removePrefix("assets://")
569
+ if (hasCommonBundleInAssets(appContext)) {
570
+ BTLogger.info("Using sequential asset bundle loader: common=common.bundle, entry=$entryAssetName")
571
+ createSequentialAssetBundleLoader(appContext, "common.bundle", entryAssetName)
572
+ } else {
573
+ BTLogger.info("No common.bundle in assets, using single bundle: $entryURL")
574
+ JSBundleLoader.createAssetLoader(appContext, entryURL, true)
575
+ }
576
+ }
577
+
578
+ // Bare filename (e.g. "background.bundle") — treat as asset
579
+ else -> {
580
+ if (hasCommonBundleInAssets(appContext)) {
581
+ BTLogger.info("Using sequential asset bundle loader: common=common.bundle, entry=$entryURL")
582
+ createSequentialAssetBundleLoader(appContext, "common.bundle", entryURL)
583
+ } else {
584
+ BTLogger.info("No common.bundle in assets, using single bundle: assets://$entryURL")
585
+ JSBundleLoader.createAssetLoader(appContext, "assets://$entryURL", true)
586
+ }
395
587
  }
396
588
  }
397
589
 
398
- // Bare filename (e.g. "background.bundle") — treat as asset
399
- else -> {
400
- if (hasCommonBundleInAssets(appContext)) {
401
- BTLogger.info("Using sequential asset bundle loader: common=common.bundle, entry=$entryURL")
402
- createSequentialAssetBundleLoader(appContext, "common.bundle", entryURL)
403
- } else {
404
- BTLogger.info("No common.bundle in assets, using single bundle: assets://$entryURL")
405
- JSBundleLoader.createAssetLoader(appContext, "assets://$entryURL", true)
590
+ val delegate = DefaultReactHostDelegate(
591
+ jsMainModulePath = MODULE_NAME,
592
+ jsBundleLoader = bundleLoader,
593
+ reactPackages = packages,
594
+ jsRuntimeFactory = HermesInstance(),
595
+ turboModuleManagerDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder(),
596
+ )
597
+
598
+ val componentFactory = ComponentFactory()
599
+ DefaultComponentsRegistry.register(componentFactory)
600
+
601
+ val host = ReactHostImpl(
602
+ appContext,
603
+ delegate,
604
+ componentFactory,
605
+ true, /* allowPackagerServerAccess */
606
+ false, /* useDevSupport */
607
+ )
608
+ bgReactHost = host
609
+
610
+ host.addReactInstanceEventListener(object : ReactInstanceEventListener {
611
+ override fun onReactContextInitialized(context: ReactContext) {
612
+ if (backgroundRuntimeGeneration.get() != runtimeGeneration) {
613
+ BTLogger.info(
614
+ "[BackgroundHMR] ignored stale ReactContext generation=$runtimeGeneration",
615
+ )
616
+ return
617
+ }
618
+ val initMs = (System.nanoTime() - bgStartTime) / 1_000_000.0
619
+ BTLogger.info("[SplitBundle] background ReactContext initialized in ${String.format("%.1f", initMs)}ms")
620
+ // Replay the most recent Activity resume so TurboModules on the
621
+ // bg host can see getCurrentActivity()/ActivityEventListeners
622
+ // from the very first call, even when the bg host finishes
623
+ // initializing after the Activity is already resumed.
624
+ replayLastResumedActivityOnUi()
625
+ context.runOnJSQueueThread {
626
+ try {
627
+ if (
628
+ backgroundRuntimeGeneration.get() != runtimeGeneration ||
629
+ bgReactHost?.currentReactContext !== context
630
+ ) {
631
+ BTLogger.info(
632
+ "[BackgroundHMR] skipped stale binding install generation=$runtimeGeneration",
633
+ )
634
+ return@runOnJSQueueThread
635
+ }
636
+ val ptr = context.javaScriptContextHolder?.get() ?: 0L
637
+ if (ptr != 0L) {
638
+ bgRuntimePtr = ptr
639
+ nativeInstallSharedBridge(ptr, false, runtimeGeneration)
640
+ nativeSetupErrorHandler(ptr)
641
+ BTLogger.info("SharedBridge and error handler installed in background runtime")
642
+ } else {
643
+ BTLogger.error("Background runtime pointer is 0")
644
+ }
645
+ } catch (e: Exception) {
646
+ BTLogger.error("Error installing bindings in background runtime: ${e.message}")
647
+ }
648
+ }
649
+ if (activeDevVendorConfig?.backgroundHMREnabled == true) {
650
+ setupBackgroundHMRClient(context, entryURL, runtimeGeneration)
406
651
  }
407
652
  }
408
- }
653
+ })
409
654
 
410
- val delegate = DefaultReactHostDelegate(
411
- jsMainModulePath = MODULE_NAME,
412
- jsBundleLoader = bundleLoader,
413
- reactPackages = packages,
414
- jsRuntimeFactory = HermesInstance(),
415
- turboModuleManagerDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder(),
416
- )
655
+ watchBackgroundRunnerStart(host.start(), bgStartTime, runtimeGeneration)
656
+ return true
657
+ } catch (t: Throwable) {
658
+ markBackgroundRunnerFailed(t, bgStartTime, runtimeGeneration)
659
+ return false
660
+ }
661
+ }
417
662
 
418
- val componentFactory = ComponentFactory()
419
- DefaultComponentsRegistry.register(componentFactory)
663
+ private fun setupBackgroundHMRClient(
664
+ context: ReactContext,
665
+ entryURL: String,
666
+ runtimeGeneration: Long,
667
+ ) {
668
+ try {
669
+ val uri = Uri.parse(entryURL)
670
+ val hmrRegistrationUri = uri.buildUpon()
671
+ .path("/apps/mobile/background.bundle")
672
+ .build()
673
+ val scheme = hmrRegistrationUri.scheme ?: "http"
674
+ val port = if (hmrRegistrationUri.port != -1) {
675
+ hmrRegistrationUri.port
676
+ } else if (scheme == "https") {
677
+ 443
678
+ } else {
679
+ 80
680
+ }
681
+ val path = hmrRegistrationUri.path?.removePrefix("/")
682
+ ?: "apps/mobile/background.bundle"
683
+ context.getJSModule(HMRClient::class.java).setup(
684
+ "android",
685
+ path,
686
+ hmrRegistrationUri.host,
687
+ port,
688
+ true,
689
+ scheme,
690
+ hmrRegistrationUri.toString(),
691
+ )
692
+ BTLogger.info(
693
+ "[BackgroundHMR] client setup queued generation=$runtimeGeneration " +
694
+ "downloadURL=$entryURL registrationURL=$hmrRegistrationUri",
695
+ )
696
+ } catch (t: Throwable) {
697
+ BTLogger.error("[BackgroundHMR] client setup failed: ${t.message}")
698
+ }
699
+ }
700
+
701
+ private fun watchBackgroundRunnerStart(
702
+ task: TaskInterface<Void>,
703
+ bgStartTime: Long,
704
+ runtimeGeneration: Long,
705
+ ) {
706
+ Thread {
707
+ try {
708
+ task.waitForCompletion()
709
+ if (
710
+ runnerState.get() != BackgroundRunnerState.STARTING ||
711
+ backgroundRuntimeGeneration.get() != runtimeGeneration
712
+ ) {
713
+ return@Thread
714
+ }
715
+ when {
716
+ task.isFaulted() -> markBackgroundRunnerFailed(
717
+ task.getError() ?: RuntimeException("Background ReactHost start faulted"),
718
+ bgStartTime,
719
+ runtimeGeneration,
720
+ )
721
+ task.isCancelled() -> markBackgroundRunnerFailed(
722
+ RuntimeException("Background ReactHost start cancelled"),
723
+ bgStartTime,
724
+ runtimeGeneration,
725
+ )
726
+ runnerState.compareAndSet(
727
+ BackgroundRunnerState.STARTING,
728
+ BackgroundRunnerState.RUNNING,
729
+ ) -> {
730
+ val startMs = (System.nanoTime() - bgStartTime) / 1_000_000.0
731
+ BTLogger.info("Background runner start task completed in ${String.format("%.1f", startMs)}ms")
732
+ }
733
+ }
734
+ } catch (t: Throwable) {
735
+ markBackgroundRunnerFailed(t, bgStartTime, runtimeGeneration)
736
+ }
737
+ }.apply {
738
+ isDaemon = true
739
+ name = "OneKey-BgThread-StartWatch"
740
+ start()
741
+ }
742
+ }
420
743
 
421
- val host = ReactHostImpl(
422
- appContext,
423
- delegate,
424
- componentFactory,
425
- true, /* allowPackagerServerAccess */
426
- false, /* useDevSupport */
744
+ private fun markBackgroundRunnerFailed(
745
+ t: Throwable,
746
+ bgStartTime: Long,
747
+ runtimeGeneration: Long,
748
+ ) {
749
+ if (
750
+ backgroundRuntimeGeneration.get() != runtimeGeneration ||
751
+ !runnerState.compareAndSet(BackgroundRunnerState.STARTING, BackgroundRunnerState.FAILED)
752
+ ) {
753
+ return
754
+ }
755
+ val startMs = (System.nanoTime() - bgStartTime) / 1_000_000.0
756
+ runnerStartFailureMessage = t.message ?: t.javaClass.simpleName
757
+ bgRuntimePtr = 0
758
+ BTLogger.error(
759
+ "Background runner start failed after ${String.format("%.1f", startMs)}ms: " +
760
+ runnerStartFailureMessage
427
761
  )
428
- bgReactHost = host
429
-
430
- host.addReactInstanceEventListener(object : ReactInstanceEventListener {
431
- override fun onReactContextInitialized(context: ReactContext) {
432
- val initMs = (System.nanoTime() - bgStartTime) / 1_000_000.0
433
- BTLogger.info("[SplitBundle] background ReactContext initialized in ${String.format("%.1f", initMs)}ms")
434
- // Replay the most recent Activity resume so TurboModules on the
435
- // bg host can see getCurrentActivity()/ActivityEventListeners
436
- // from the very first call, even when the bg host finishes
437
- // initializing after the Activity is already resumed.
438
- replayLastResumedActivityOnUi()
439
- context.runOnJSQueueThread {
440
- try {
441
- val ptr = context.javaScriptContextHolder?.get() ?: 0L
442
- if (ptr != 0L) {
443
- bgRuntimePtr = ptr
444
- nativeInstallSharedBridge(ptr, false)
445
- nativeSetupErrorHandler(ptr)
446
- BTLogger.info("SharedBridge and error handler installed in background runtime")
447
- } else {
448
- BTLogger.error("Background runtime pointer is 0")
762
+ val failedHost = bgReactHost
763
+ val destroyTask = try {
764
+ failedHost?.destroy("Background runner start failed", t as? Exception)
765
+ } catch (destroyError: Throwable) {
766
+ BTLogger.error("Failed to destroy faulted background runner: ${destroyError.message}")
767
+ null
768
+ }
769
+ if (destroyTask == null) {
770
+ if (failedHost == null) {
771
+ runnerState.compareAndSet(
772
+ BackgroundRunnerState.FAILED,
773
+ BackgroundRunnerState.IDLE,
774
+ )
775
+ }
776
+ return
777
+ }
778
+ Thread {
779
+ try {
780
+ destroyTask.waitForCompletion()
781
+ if (destroyTask.isFaulted() || destroyTask.isCancelled()) {
782
+ BTLogger.error(
783
+ "Faulted background runner teardown did not complete cleanly " +
784
+ "(faulted=${destroyTask.isFaulted()}, " +
785
+ "cancelled=${destroyTask.isCancelled()}): " +
786
+ destroyTask.getError()?.message,
787
+ )
788
+ return@Thread
789
+ }
790
+ if (
791
+ runnerState.compareAndSet(
792
+ BackgroundRunnerState.FAILED,
793
+ BackgroundRunnerState.DESTROYING,
794
+ )
795
+ ) {
796
+ Handler(Looper.getMainLooper()).post {
797
+ if (bgReactHost === failedHost) {
798
+ bgReactHost = null
449
799
  }
450
- } catch (e: Exception) {
451
- BTLogger.error("Error installing bindings in background runtime: ${e.message}")
800
+ runnerState.set(BackgroundRunnerState.IDLE)
801
+ BTLogger.info(
802
+ "Faulted background runner teardown completed; retry is available",
803
+ )
452
804
  }
453
805
  }
806
+ } catch (destroyError: Throwable) {
807
+ BTLogger.error(
808
+ "Failed to await faulted background runner teardown: " +
809
+ destroyError.message,
810
+ )
454
811
  }
455
- })
456
-
457
- host.start()
458
- isStarted = true
812
+ }.apply {
813
+ isDaemon = true
814
+ name = "OneKey-BgThread-FailedHostDestroy"
815
+ start()
816
+ }
459
817
  }
460
818
 
461
819
  /**
@@ -463,9 +821,26 @@ class BackgroundThreadManager private constructor() {
463
821
  * Routes to main or background runtime's JS queue thread, then calls nativeExecuteWork.
464
822
  */
465
823
  @DoNotStrip
466
- fun scheduleOnJSThread(isMain: Boolean, workId: Long): Boolean {
467
- val context = if (isMain) mainReactContext else bgReactHost?.currentReactContext
468
- BTLogger.info("scheduleOnJSThread: isMain=$isMain, workId=$workId, context=${context != null}")
824
+ fun scheduleOnJSThread(
825
+ isMain: Boolean,
826
+ workId: Long,
827
+ runtimeGeneration: Long,
828
+ ): Boolean {
829
+ val backgroundState = runnerState.get()
830
+ val context = if (isMain) {
831
+ mainReactContext
832
+ } else if (
833
+ backgroundState == BackgroundRunnerState.STARTING ||
834
+ backgroundState == BackgroundRunnerState.RUNNING
835
+ ) {
836
+ bgReactHost?.currentReactContext
837
+ } else {
838
+ null
839
+ }
840
+ BTLogger.info(
841
+ "scheduleOnJSThread: isMain=$isMain, workId=$workId, " +
842
+ "generation=$runtimeGeneration, context=${context != null}",
843
+ )
469
844
  if (context == null) {
470
845
  BTLogger.error("scheduleOnJSThread: context is null! isMain=$isMain, mainCtx=${mainReactContext != null}, bgHost=${bgReactHost != null}, bgCtx=${bgReactHost?.currentReactContext != null}")
471
846
  // The just-enqueued native work will never reach the JS thread.
@@ -479,13 +854,27 @@ class BackgroundThreadManager private constructor() {
479
854
  }
480
855
  return try {
481
856
  val posted = context.runOnJSQueueThread {
482
- // Re-read ptr inside the block — if a reload happened between
483
- // scheduling and execution, the old ptr may be stale.
857
+ if (
858
+ !isMain &&
859
+ (
860
+ backgroundRuntimeGeneration.get() != runtimeGeneration ||
861
+ bgReactHost?.currentReactContext !== context
862
+ )
863
+ ) {
864
+ BTLogger.info(
865
+ "scheduleOnJSThread: dropped stale background workId=$workId " +
866
+ "generation=$runtimeGeneration active=${backgroundRuntimeGeneration.get()}",
867
+ )
868
+ nativeDropScheduledWork(isMain, workId, runtimeGeneration)
869
+ return@runOnJSQueueThread
870
+ }
871
+ // Re-read ptr only after proving this Runnable still belongs to
872
+ // the active context and generation.
484
873
  val ptr = if (isMain) mainRuntimePtr else bgRuntimePtr
485
874
  BTLogger.info("scheduleOnJSThread runOnJSQueueThread: isMain=$isMain, workId=$workId, ptr=$ptr")
486
875
  if (ptr != 0L) {
487
876
  try {
488
- nativeExecuteWork(ptr, workId)
877
+ nativeExecuteWork(ptr, workId, isMain, runtimeGeneration)
489
878
  } catch (e: Exception) {
490
879
  BTLogger.error("Error executing work on JS thread: ${e.message}")
491
880
  }
@@ -501,7 +890,7 @@ class BackgroundThreadManager private constructor() {
501
890
  // runtime recovers and main has no JS retry net).
502
891
  // drainPendingBgEvals inside the native fn is
503
892
  // gated to !isMain, so settling bg evals only happens for bg.
504
- nativeDropScheduledWork(isMain, workId)
893
+ nativeDropScheduledWork(isMain, workId, runtimeGeneration)
505
894
  }
506
895
  }
507
896
  if (!posted) {
@@ -541,7 +930,7 @@ class BackgroundThreadManager private constructor() {
541
930
  path: String,
542
931
  onComplete: (code: String?, message: String?) -> Unit
543
932
  ) {
544
- if (!isStarted) {
933
+ if (!isBackgroundStarted) {
545
934
  // Bg runtime not started yet → retryable (the loader will re-attempt
546
935
  // once the bg host is up).
547
936
  onComplete("SPLIT_BUNDLE_NO_RUNTIME", "Background runtime not started")
@@ -950,6 +1339,11 @@ class BackgroundThreadManager private constructor() {
950
1339
  fun restart(context: ReactApplicationContext, mode: String, reason: String, promise: com.facebook.react.bridge.Promise) {
951
1340
  val isUi = mode == "ui"
952
1341
  val isAll = mode == "all"
1342
+ val isBackgroundHMR = mode == "background"
1343
+ if (isBackgroundHMR) {
1344
+ restartBackgroundForHMR(context.applicationContext, reason, promise)
1345
+ return
1346
+ }
953
1347
  if (!isUi && !isAll) {
954
1348
  promise.reject(
955
1349
  "BG_RESTART_ERROR",
@@ -961,9 +1355,12 @@ class BackgroundThreadManager private constructor() {
961
1355
  BTLogger.info("restart: mode=$mode reason=$reason")
962
1356
 
963
1357
  try {
964
- nativeInvalidateSharedRpc("main")
1358
+ nativeInvalidateSharedRpc("main", 0)
965
1359
  if (isAll) {
966
- nativeInvalidateSharedRpc("background")
1360
+ nativeInvalidateSharedRpc(
1361
+ "background",
1362
+ backgroundRuntimeGeneration.incrementAndGet(),
1363
+ )
967
1364
  }
968
1365
  } catch (t: Throwable) {
969
1366
  // Invalidate is best-effort; not fatal if the JNI call somehow
@@ -1071,6 +1468,188 @@ class BackgroundThreadManager private constructor() {
1071
1468
  // any further code here is unreachable
1072
1469
  }
1073
1470
 
1471
+ private fun restartBackgroundForHMR(
1472
+ context: Context,
1473
+ reason: String,
1474
+ promise: com.facebook.react.bridge.Promise,
1475
+ ) {
1476
+ if (!BuildConfig.DEBUG) {
1477
+ promise.reject(
1478
+ "BG_RESTART_ERROR",
1479
+ "Background HMR restart is unavailable in release builds",
1480
+ )
1481
+ return
1482
+ }
1483
+ val generation = reason
1484
+ .removePrefix("onekey-bg-hmr:")
1485
+ .substringBefore(':')
1486
+ .toLongOrNull()
1487
+ if (!reason.startsWith("onekey-bg-hmr:") || generation == null) {
1488
+ promise.reject(
1489
+ "BG_RESTART_ERROR",
1490
+ "Background HMR restart is missing its runtime generation",
1491
+ )
1492
+ return
1493
+ }
1494
+
1495
+ Handler(Looper.getMainLooper()).post {
1496
+ val activeGeneration = backgroundRuntimeGeneration.get()
1497
+ if (generation != activeGeneration) {
1498
+ BTLogger.info(
1499
+ "[BackgroundHMR] ignored stale restart requested=$generation active=$activeGeneration",
1500
+ )
1501
+ promise.resolve(null)
1502
+ return@post
1503
+ }
1504
+ val entryURL = lastBackgroundEntryURL
1505
+ val config = lastDevVendorConfig
1506
+ if (
1507
+ entryURL == null ||
1508
+ config?.backgroundHMREnabled != true ||
1509
+ !backgroundHMRRestartInFlight.compareAndSet(false, true)
1510
+ ) {
1511
+ BTLogger.info("[BackgroundHMR] restart coalesced or disabled")
1512
+ promise.resolve(null)
1513
+ return@post
1514
+ }
1515
+
1516
+ val outgoingHost = bgReactHost
1517
+ val outgoingContext = outgoingHost?.currentReactContext
1518
+ val invalidatedGeneration = backgroundRuntimeGeneration.incrementAndGet()
1519
+ runnerState.set(BackgroundRunnerState.DESTROYING)
1520
+ bgRuntimePtr = 0
1521
+ val runtimeInvalidated = CountDownLatch(1)
1522
+ val runtimeInvalidationSucceeded = AtomicBoolean(false)
1523
+ val invalidationScheduled = try {
1524
+ outgoingContext?.runOnJSQueueThread {
1525
+ try {
1526
+ nativeInvalidateBackgroundRuntimeOnJSThread(
1527
+ invalidatedGeneration,
1528
+ )
1529
+ runtimeInvalidationSucceeded.set(true)
1530
+ } catch (t: Throwable) {
1531
+ BTLogger.error(
1532
+ "[BackgroundHMR] JS-thread invalidation failed: ${t.message}",
1533
+ )
1534
+ try {
1535
+ nativeInvalidateSharedRpc("background", invalidatedGeneration)
1536
+ runtimeInvalidationSucceeded.set(true)
1537
+ } catch (fallbackError: Throwable) {
1538
+ BTLogger.error(
1539
+ "[BackgroundHMR] fallback invalidation failed: " +
1540
+ fallbackError.message,
1541
+ )
1542
+ }
1543
+ } finally {
1544
+ runtimeInvalidated.countDown()
1545
+ }
1546
+ } == true
1547
+ } catch (t: Throwable) {
1548
+ BTLogger.error(
1549
+ "[BackgroundHMR] failed to schedule JS-thread invalidation: ${t.message}",
1550
+ )
1551
+ false
1552
+ }
1553
+ if (!invalidationScheduled) {
1554
+ try {
1555
+ // Safe fallback for a context whose JS queue is already
1556
+ // unavailable. This retains the old off-thread callback
1557
+ // disposal safeguards, but still quiesces SharedRPC.
1558
+ nativeInvalidateSharedRpc("background", invalidatedGeneration)
1559
+ runtimeInvalidationSucceeded.set(true)
1560
+ } catch (t: Throwable) {
1561
+ BTLogger.error(
1562
+ "[BackgroundHMR] SharedRPC invalidate failed: ${t.message}",
1563
+ )
1564
+ }
1565
+ runtimeInvalidated.countDown()
1566
+ }
1567
+ promise.resolve(null)
1568
+
1569
+ Thread {
1570
+ try {
1571
+ runtimeInvalidated.await()
1572
+ } catch (t: Throwable) {
1573
+ BTLogger.error(
1574
+ "[BackgroundHMR] outgoing runtime invalidation wait failed: ${t.message}",
1575
+ )
1576
+ backgroundHMRRestartInFlight.set(false)
1577
+ return@Thread
1578
+ }
1579
+ if (!runtimeInvalidationSucceeded.get()) {
1580
+ BTLogger.error(
1581
+ "[BackgroundHMR] replacement blocked: runtime invalidation failed",
1582
+ )
1583
+ backgroundHMRRestartInFlight.set(false)
1584
+ return@Thread
1585
+ }
1586
+ val destroyTask = try {
1587
+ outgoingHost?.destroy("Background HMR restart", null)
1588
+ } catch (t: Throwable) {
1589
+ BTLogger.error(
1590
+ "[BackgroundHMR] outgoing host destroy threw: ${t.message}",
1591
+ )
1592
+ null
1593
+ }
1594
+ if (outgoingHost != null && destroyTask == null) {
1595
+ backgroundHMRRestartInFlight.set(false)
1596
+ return@Thread
1597
+ }
1598
+ if (destroyTask != null) {
1599
+ try {
1600
+ destroyTask.waitForCompletion()
1601
+ } catch (t: Throwable) {
1602
+ BTLogger.error(
1603
+ "[BackgroundHMR] outgoing host destroy wait failed: ${t.message}",
1604
+ )
1605
+ backgroundHMRRestartInFlight.set(false)
1606
+ return@Thread
1607
+ }
1608
+ if (destroyTask.isFaulted() || destroyTask.isCancelled()) {
1609
+ BTLogger.error(
1610
+ "[BackgroundHMR] outgoing host destroy did not complete cleanly " +
1611
+ "(faulted=${destroyTask.isFaulted()}, " +
1612
+ "cancelled=${destroyTask.isCancelled()}): " +
1613
+ destroyTask.getError()?.message,
1614
+ )
1615
+ backgroundHMRRestartInFlight.set(false)
1616
+ return@Thread
1617
+ }
1618
+ }
1619
+ Handler(Looper.getMainLooper()).post {
1620
+ if (bgReactHost !== outgoingHost) {
1621
+ BTLogger.error(
1622
+ "[BackgroundHMR] replacement aborted: outgoing host changed",
1623
+ )
1624
+ backgroundHMRRestartInFlight.set(false)
1625
+ return@post
1626
+ }
1627
+ bgReactHost = null
1628
+ if (
1629
+ !runnerState.compareAndSet(
1630
+ BackgroundRunnerState.DESTROYING,
1631
+ BackgroundRunnerState.IDLE,
1632
+ )
1633
+ ) {
1634
+ BTLogger.error(
1635
+ "[BackgroundHMR] replacement aborted: unexpected state=" +
1636
+ getBackgroundRunnerState(),
1637
+ )
1638
+ backgroundHMRRestartInFlight.set(false)
1639
+ return@post
1640
+ }
1641
+ BTLogger.info("[BackgroundHMR] starting replacement background runtime")
1642
+ ensureBackgroundRunner(context, entryURL, config)
1643
+ backgroundHMRRestartInFlight.set(false)
1644
+ }
1645
+ }.apply {
1646
+ isDaemon = true
1647
+ name = "OneKey-BgThread-HMRRestart"
1648
+ start()
1649
+ }
1650
+ }
1651
+ }
1652
+
1074
1653
  /**
1075
1654
  * Returns true if Runtime.exit(0) was reached (in which case the process
1076
1655
  * is now terminating and any code after the call site is unreachable).
@@ -1097,9 +1676,24 @@ class BackgroundThreadManager private constructor() {
1097
1676
 
1098
1677
  // ── Lifecycle ───────────────────────────────────────────────────────────
1099
1678
 
1100
- val isBackgroundStarted: Boolean get() = isStarted
1679
+ val isBackgroundStarted: Boolean
1680
+ get() = when (runnerState.get()) {
1681
+ BackgroundRunnerState.STARTING, BackgroundRunnerState.RUNNING -> true
1682
+ else -> false
1683
+ }
1684
+
1685
+ fun getBackgroundRunnerState(): String = when (runnerState.get()) {
1686
+ BackgroundRunnerState.IDLE -> "idle"
1687
+ BackgroundRunnerState.STARTING -> "starting"
1688
+ BackgroundRunnerState.RUNNING -> "running"
1689
+ BackgroundRunnerState.FAILED -> "failed"
1690
+ BackgroundRunnerState.DESTROYING -> "destroying"
1691
+ }
1692
+
1693
+ fun getBackgroundRunnerFailureMessage(): String? = runnerStartFailureMessage
1101
1694
 
1102
1695
  fun destroy() {
1696
+ runnerState.set(BackgroundRunnerState.DESTROYING)
1103
1697
  nativeDestroy()
1104
1698
  bgRuntimePtr = 0
1105
1699
  mainRuntimePtr = 0
@@ -1107,7 +1701,8 @@ class BackgroundThreadManager private constructor() {
1107
1701
  mainReactHost = null
1108
1702
  bgReactHost?.destroy("BackgroundThreadManager destroyed", null)
1109
1703
  bgReactHost = null
1110
- isStarted = false
1704
+ runnerStartFailureMessage = null
1705
+ runnerState.set(BackgroundRunnerState.IDLE)
1111
1706
  lastResumedActivityRef = WeakReference(null)
1112
1707
  }
1113
1708
  }