@hot-updater/react-native 1.0.0-rc.34 → 1.0.0-rc.35
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.
- package/android/build.gradle +13 -2
- package/android/proguard-rules.pro +0 -9
- package/android/src/bridge/BridgeReactInstance.kt +38 -0
- package/android/src/bridgeless/BridgeReactInstance.kt +24 -0
- package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +94 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +119 -16
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +35 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +13 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +38 -138
- package/android/src/newarch/JavaScriptExceptionHooks.kt +48 -0
- package/android/src/newarch/ReactIntegrationManager.kt +6 -32
- package/android/src/oldarch/JavaScriptExceptionHooks.kt +71 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +93 -15
- package/ios/HotUpdater/Internal/BundleMetadata.swift +53 -1
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +39 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/package.json +3 -3
- package/src/sdkVersion.ts +1 -1
package/android/build.gradle
CHANGED
|
@@ -20,8 +20,12 @@ def reactNativeArchitectures() {
|
|
|
20
20
|
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// The New Architecture is the default. React Native 0.82 and later run nothing
|
|
24
|
+
// else and set newArchEnabled to true on every library project, whatever the
|
|
25
|
+
// app's gradle.properties says, so only an older React Native app that sets
|
|
26
|
+
// newArchEnabled=false builds the old architecture.
|
|
23
27
|
def isNewArchitectureEnabled() {
|
|
24
|
-
return
|
|
28
|
+
return !project.hasProperty("newArchEnabled") || project.property("newArchEnabled").toString().toBoolean()
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
apply plugin: "com.android.library"
|
|
@@ -115,7 +119,11 @@ android {
|
|
|
115
119
|
"src/newarch",
|
|
116
120
|
// Codegen specs
|
|
117
121
|
"generated/java",
|
|
118
|
-
"generated/jni"
|
|
122
|
+
"generated/jni",
|
|
123
|
+
// React Native 0.81 and older can run the New Architecture on the
|
|
124
|
+
// bridge. Later versions have none, so their builds never compile
|
|
125
|
+
// ReactNativeHost.
|
|
126
|
+
project.ext.reactNativeMinorVersion >= 82 ? "src/bridgeless" : "src/bridge"
|
|
119
127
|
]
|
|
120
128
|
}
|
|
121
129
|
}
|
|
@@ -146,6 +154,9 @@ dependencies {
|
|
|
146
154
|
}
|
|
147
155
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
148
156
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
|
|
157
|
+
// ProcessLifecycleOwner, for an activity that started before Hot Updater was created.
|
|
158
|
+
// React Native's AppCompat already brings it at runtime.
|
|
159
|
+
implementation "androidx.lifecycle:lifecycle-process:2.6.1"
|
|
149
160
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
|
|
150
161
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
|
151
162
|
implementation "org.apache.commons:commons-compress:1.27.1"
|
|
@@ -6,15 +6,6 @@
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
# New Architecture
|
|
9
|
-
# Crash recovery discovers these accessors through reflection.
|
|
10
|
-
-keepclassmembers class * implements com.facebook.react.ReactApplication {
|
|
11
|
-
public *** getReactHost();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
-keepclassmembers class * implements com.facebook.react.ReactHost {
|
|
15
|
-
public *** getCurrentReactContext();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
9
|
# Keep fields accessed via reflection in ReactHost
|
|
19
10
|
# Support both Java (mReactHostDelegate) and Kotlin (reactHostDelegate) field names
|
|
20
11
|
-keepclassmembers class com.facebook.react.runtime.ReactHostImpl {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactApplication
|
|
4
|
+
import com.facebook.react.bridge.JSBundleLoader
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* React Native 0.81 and older can run the New Architecture on the bridge, which
|
|
8
|
+
* only ReactNativeHost reaches. Only builds for those versions compile this file.
|
|
9
|
+
*/
|
|
10
|
+
internal object BridgeReactInstance {
|
|
11
|
+
fun setJSBundle(
|
|
12
|
+
application: ReactApplication,
|
|
13
|
+
bundleLoader: JSBundleLoader?,
|
|
14
|
+
) {
|
|
15
|
+
@Suppress("DEPRECATION")
|
|
16
|
+
val instanceManager = application.reactNativeHost.reactInstanceManager
|
|
17
|
+
val bundleLoaderField = instanceManager::class.java.getDeclaredField("mBundleLoader")
|
|
18
|
+
bundleLoaderField.isAccessible = true
|
|
19
|
+
bundleLoaderField.set(instanceManager, bundleLoader)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fun reload(application: ReactApplication) {
|
|
23
|
+
@Suppress("DEPRECATION")
|
|
24
|
+
val reactNativeHost = application.reactNativeHost
|
|
25
|
+
try {
|
|
26
|
+
reactNativeHost.reactInstanceManager.recreateReactContextInBackground()
|
|
27
|
+
} catch (e: Exception) {
|
|
28
|
+
val currentActivity = reactNativeHost.reactInstanceManager.currentReactContext?.currentActivity
|
|
29
|
+
if (currentActivity == null) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
currentActivity.runOnUiThread {
|
|
34
|
+
currentActivity.recreate()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.facebook.react.ReactApplication
|
|
5
|
+
import com.facebook.react.bridge.JSBundleLoader
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* React Native 0.82 and later have no bridge, so an app without a ReactHost has
|
|
9
|
+
* no instance to update. Builds for those versions compile this file instead of
|
|
10
|
+
* the bridge one, and never reference ReactNativeHost.
|
|
11
|
+
*/
|
|
12
|
+
@Suppress("UNUSED_PARAMETER")
|
|
13
|
+
internal object BridgeReactInstance {
|
|
14
|
+
fun setJSBundle(
|
|
15
|
+
application: ReactApplication,
|
|
16
|
+
bundleLoader: JSBundleLoader?,
|
|
17
|
+
) {
|
|
18
|
+
Log.d("HotUpdater", "No ReactHost to set the JS bundle on")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fun reload(application: ReactApplication) {
|
|
22
|
+
Log.d("HotUpdater", "No ReactHost to reload")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.app.Application
|
|
5
|
+
import android.os.Bundle
|
|
6
|
+
import androidx.lifecycle.Lifecycle
|
|
7
|
+
import androidx.lifecycle.ProcessLifecycleOwner
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Runs an action once this process has a started activity.
|
|
11
|
+
*
|
|
12
|
+
* A headless JS task (HeadlessJsTaskService, which background push libraries
|
|
13
|
+
* start) loads the bundle with no activity and never renders. Waiting for an
|
|
14
|
+
* activity keeps that launch from counting as one that ended before its first
|
|
15
|
+
* render.
|
|
16
|
+
*
|
|
17
|
+
* Android does not replay lifecycle callbacks to a late registration. An
|
|
18
|
+
* activity that started before this gate existed, as when a brownfield app adds
|
|
19
|
+
* React Native to an activity already on screen, is read from
|
|
20
|
+
* [processHasStartedActivity] instead.
|
|
21
|
+
*/
|
|
22
|
+
internal class ActivityStartGate(
|
|
23
|
+
private val application: Application?,
|
|
24
|
+
private val processHasStartedActivity: () -> Boolean = ::hasStartedActivity,
|
|
25
|
+
) : Application.ActivityLifecycleCallbacks {
|
|
26
|
+
private var startedActivities = 0
|
|
27
|
+
private var pendingAction: (() -> Unit)? = null
|
|
28
|
+
|
|
29
|
+
init {
|
|
30
|
+
application?.registerActivityLifecycleCallbacks(this)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Runs [action] now when an activity is started, otherwise at the next
|
|
35
|
+
* activity start. It replaces an action still waiting; null cancels it.
|
|
36
|
+
* Without an application to observe, it runs [action] now.
|
|
37
|
+
*/
|
|
38
|
+
fun runWhenActivityStarted(action: (() -> Unit)?) {
|
|
39
|
+
val runNow =
|
|
40
|
+
synchronized(this) {
|
|
41
|
+
val waits =
|
|
42
|
+
application != null &&
|
|
43
|
+
startedActivities == 0 &&
|
|
44
|
+
!processHasStartedActivity()
|
|
45
|
+
pendingAction = action.takeIf { waits }
|
|
46
|
+
action.takeUnless { waits }
|
|
47
|
+
}
|
|
48
|
+
runNow?.invoke()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override fun onActivityStarted(activity: Activity) {
|
|
52
|
+
val action =
|
|
53
|
+
synchronized(this) {
|
|
54
|
+
startedActivities += 1
|
|
55
|
+
pendingAction.also { pendingAction = null }
|
|
56
|
+
}
|
|
57
|
+
action?.invoke()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override fun onActivityStopped(activity: Activity) {
|
|
61
|
+
synchronized(this) {
|
|
62
|
+
// An activity started before this gate existed was never counted.
|
|
63
|
+
if (startedActivities > 0) startedActivities -= 1
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
override fun onActivityCreated(
|
|
68
|
+
activity: Activity,
|
|
69
|
+
savedInstanceState: Bundle?,
|
|
70
|
+
) {}
|
|
71
|
+
|
|
72
|
+
override fun onActivityResumed(activity: Activity) {}
|
|
73
|
+
|
|
74
|
+
override fun onActivityPaused(activity: Activity) {}
|
|
75
|
+
|
|
76
|
+
override fun onActivitySaveInstanceState(
|
|
77
|
+
activity: Activity,
|
|
78
|
+
outState: Bundle,
|
|
79
|
+
) {}
|
|
80
|
+
|
|
81
|
+
override fun onActivityDestroyed(activity: Activity) {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ProcessLifecycleOwner observes activities from process start, through the
|
|
85
|
+
// androidx.startup initializer that lifecycle-process registers. It is STARTED
|
|
86
|
+
// while an activity of this process is started, and stays at INITIALIZED when
|
|
87
|
+
// an app removes that initializer, so the gate then waits for the next start.
|
|
88
|
+
private fun hasStartedActivity(): Boolean =
|
|
89
|
+
runCatching {
|
|
90
|
+
ProcessLifecycleOwner
|
|
91
|
+
.get()
|
|
92
|
+
.lifecycle.currentState
|
|
93
|
+
.isAtLeast(Lifecycle.State.STARTED)
|
|
94
|
+
}.getOrDefault(false)
|
|
@@ -151,6 +151,12 @@ interface BundleStorageService {
|
|
|
151
151
|
|
|
152
152
|
suspend fun commitReleaseSelection(selection: PersistedSelection): Boolean = false
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Records that the current launch of a verification-pending bundle can show UI.
|
|
156
|
+
* The next process rolls the bundle back unless first content verified it.
|
|
157
|
+
*/
|
|
158
|
+
fun markLaunchStarted(launchedBundleId: String?)
|
|
159
|
+
|
|
154
160
|
/**
|
|
155
161
|
* Marks the current launch as successful after the first content appeared.
|
|
156
162
|
*/
|
|
@@ -162,7 +168,8 @@ interface BundleStorageService {
|
|
|
162
168
|
fun notifyAppReady(): Map<String, Any?>
|
|
163
169
|
|
|
164
170
|
/**
|
|
165
|
-
* Gets the crashed bundle history
|
|
171
|
+
* Gets the crashed bundle history, plus a bundle that waits for its retry after
|
|
172
|
+
* an unfinished launch while this process still refuses it (see [InterruptedLaunch])
|
|
166
173
|
* @return CrashedHistory containing crashed bundles
|
|
167
174
|
*/
|
|
168
175
|
fun getCrashHistory(): CrashedHistory
|
|
@@ -592,6 +599,16 @@ class BundleFileStorageService(
|
|
|
592
599
|
private var hasPreparedLaunch = false
|
|
593
600
|
private var currentLaunchReport: LaunchReport? = null
|
|
594
601
|
|
|
602
|
+
// The staged bundle this process launched before its first content. Only that
|
|
603
|
+
// launch is pending; a bundle staged later waits for the next launch.
|
|
604
|
+
private var pendingLaunchBundleId: String? = null
|
|
605
|
+
|
|
606
|
+
// Crash history is written at first content on the main thread while JS can read it.
|
|
607
|
+
private val crashedHistoryLock = Any()
|
|
608
|
+
|
|
609
|
+
// A bundle waiting for its retry that this process keeps refusing, see InterruptedLaunch.
|
|
610
|
+
private var retryHeldBundleId: String? = null
|
|
611
|
+
|
|
595
612
|
@Volatile
|
|
596
613
|
private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot? = null
|
|
597
614
|
private val activeBundleMetadataLock = Any()
|
|
@@ -1092,7 +1109,14 @@ class BundleFileStorageService(
|
|
|
1092
1109
|
)
|
|
1093
1110
|
}
|
|
1094
1111
|
|
|
1095
|
-
|
|
1112
|
+
/**
|
|
1113
|
+
* Rolls back the pending staging bundle. [unfinishedLaunch] means its launch
|
|
1114
|
+
* ended before first content without a crash marker.
|
|
1115
|
+
*/
|
|
1116
|
+
private fun rollbackPendingBundle(
|
|
1117
|
+
stagingBundleId: String,
|
|
1118
|
+
unfinishedLaunch: Boolean = false,
|
|
1119
|
+
): Boolean {
|
|
1096
1120
|
val metadata = loadMetadataOrNull() ?: return false
|
|
1097
1121
|
if (metadata.stagingBundleId != stagingBundleId) {
|
|
1098
1122
|
return false
|
|
@@ -1100,9 +1124,7 @@ class BundleFileStorageService(
|
|
|
1100
1124
|
|
|
1101
1125
|
Log.w(TAG, "Rolling back crashed staging bundle: $stagingBundleId")
|
|
1102
1126
|
|
|
1103
|
-
|
|
1104
|
-
crashedHistory.addEntry(stagingBundleId)
|
|
1105
|
-
saveCrashedHistory(crashedHistory)
|
|
1127
|
+
recordFailedLaunch(stagingBundleId, unfinishedLaunch)
|
|
1106
1128
|
|
|
1107
1129
|
val fallbackBundleId =
|
|
1108
1130
|
metadata.stableBundleId?.takeIf { candidate ->
|
|
@@ -1369,18 +1391,98 @@ class BundleFileStorageService(
|
|
|
1369
1391
|
|
|
1370
1392
|
private fun saveCrashedHistory(history: CrashedHistory): Boolean = history.saveToFile(getCrashedHistoryFile())
|
|
1371
1393
|
|
|
1372
|
-
private fun isBundleInCrashedHistory(bundleId: String): Boolean =
|
|
1394
|
+
private fun isBundleInCrashedHistory(bundleId: String): Boolean = getCrashHistory().contains(bundleId)
|
|
1373
1395
|
|
|
1374
|
-
|
|
1396
|
+
/**
|
|
1397
|
+
* The bundles this device refuses to install: crash history, plus a bundle
|
|
1398
|
+
* waiting for its retry until a process that may retry it.
|
|
1399
|
+
*/
|
|
1400
|
+
override fun getCrashHistory(): CrashedHistory =
|
|
1401
|
+
synchronized(crashedHistoryLock) {
|
|
1402
|
+
val history = loadCrashedHistory()
|
|
1403
|
+
val interrupted = history.interruptedLaunch
|
|
1404
|
+
if (interrupted != null &&
|
|
1405
|
+
(!interrupted.retryReady || interrupted.bundleId == retryHeldBundleId) &&
|
|
1406
|
+
!history.contains(interrupted.bundleId)
|
|
1407
|
+
) {
|
|
1408
|
+
history.bundles.add(CrashedBundleEntry(interrupted.bundleId, crashedAt = System.currentTimeMillis()))
|
|
1409
|
+
}
|
|
1410
|
+
history
|
|
1411
|
+
}
|
|
1375
1412
|
|
|
1376
1413
|
override fun clearCrashHistory(): Boolean {
|
|
1377
|
-
|
|
1378
|
-
|
|
1414
|
+
synchronized(crashedHistoryLock) {
|
|
1415
|
+
saveCrashedHistory(CrashedHistory())
|
|
1416
|
+
retryHeldBundleId = null
|
|
1417
|
+
}
|
|
1379
1418
|
Log.d(TAG, "Cleared crash history")
|
|
1380
1419
|
return true
|
|
1381
1420
|
}
|
|
1382
1421
|
|
|
1422
|
+
/**
|
|
1423
|
+
* A crash, or a second unfinished launch of the same bundle, adds the bundle to
|
|
1424
|
+
* crash history. A first unfinished launch cannot tell a hang from a user leaving
|
|
1425
|
+
* early, so it leaves the bundle one retry instead.
|
|
1426
|
+
*/
|
|
1427
|
+
private fun recordFailedLaunch(
|
|
1428
|
+
bundleId: String,
|
|
1429
|
+
unfinishedLaunch: Boolean,
|
|
1430
|
+
) {
|
|
1431
|
+
synchronized(crashedHistoryLock) {
|
|
1432
|
+
val history = loadCrashedHistory()
|
|
1433
|
+
val interrupted = history.interruptedLaunch
|
|
1434
|
+
if (unfinishedLaunch && interrupted?.bundleId != bundleId) {
|
|
1435
|
+
Log.w(TAG, "Launch of $bundleId ended before first content; it gets one retry")
|
|
1436
|
+
history.interruptedLaunch = InterruptedLaunch(bundleId)
|
|
1437
|
+
retryHeldBundleId = bundleId
|
|
1438
|
+
} else {
|
|
1439
|
+
history.addEntry(bundleId)
|
|
1440
|
+
if (interrupted?.bundleId == bundleId) {
|
|
1441
|
+
history.interruptedLaunch = null
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
saveCrashedHistory(history)
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* First content appeared, so the app works again. A bundle waiting for its retry
|
|
1450
|
+
* may be installed from the next process on. This process keeps refusing it, so
|
|
1451
|
+
* the session that recovered never reloads into it. Content from that bundle
|
|
1452
|
+
* verifies it.
|
|
1453
|
+
*/
|
|
1454
|
+
private fun readyInterruptedLaunchRetry(currentBundleId: String?) {
|
|
1455
|
+
synchronized(crashedHistoryLock) {
|
|
1456
|
+
val history = loadCrashedHistory()
|
|
1457
|
+
val interrupted = history.interruptedLaunch ?: return
|
|
1458
|
+
if (interrupted.bundleId == currentBundleId) {
|
|
1459
|
+
history.interruptedLaunch = null
|
|
1460
|
+
} else if (interrupted.retryReady) {
|
|
1461
|
+
return
|
|
1462
|
+
} else {
|
|
1463
|
+
history.interruptedLaunch = interrupted.copy(retryReady = true)
|
|
1464
|
+
retryHeldBundleId = interrupted.bundleId
|
|
1465
|
+
}
|
|
1466
|
+
saveCrashedHistory(history)
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
override fun markLaunchStarted(launchedBundleId: String?) {
|
|
1471
|
+
// Unlike prepareLaunch, this can run while JS stages a newer bundle.
|
|
1472
|
+
synchronized(releaseStateLock) {
|
|
1473
|
+
val metadata = loadMetadataOrNull() ?: return
|
|
1474
|
+
if (!isVerificationPending(metadata) ||
|
|
1475
|
+
metadata.launchInProgress ||
|
|
1476
|
+
metadata.stagingBundleId != launchedBundleId
|
|
1477
|
+
) {
|
|
1478
|
+
return
|
|
1479
|
+
}
|
|
1480
|
+
saveMetadata(metadata.copy(launchInProgress = true))
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1383
1484
|
override fun markLaunchCompleted(currentBundleId: String?) {
|
|
1485
|
+
readyInterruptedLaunchRetry(currentBundleId)
|
|
1384
1486
|
val metadata = loadMetadataOrNull() ?: return
|
|
1385
1487
|
val stagingBundleId = metadata.stagingBundleId ?: return
|
|
1386
1488
|
if (!metadata.verificationPending || stagingBundleId != currentBundleId) {
|
|
@@ -1418,7 +1520,11 @@ class BundleFileStorageService(
|
|
|
1418
1520
|
val report = loadLaunchReport()
|
|
1419
1521
|
if (report == null) {
|
|
1420
1522
|
val metadata = loadMetadataOrNull()
|
|
1421
|
-
|
|
1523
|
+
val launchedBundleId = pendingLaunchBundleId
|
|
1524
|
+
if (launchedBundleId != null &&
|
|
1525
|
+
metadata?.verificationPending == true &&
|
|
1526
|
+
metadata.stagingBundleId == launchedBundleId
|
|
1527
|
+
) {
|
|
1422
1528
|
return mapOf("status" to "PENDING")
|
|
1423
1529
|
}
|
|
1424
1530
|
return mapOf("status" to "UNCHANGED")
|
|
@@ -1483,17 +1589,14 @@ class BundleFileStorageService(
|
|
|
1483
1589
|
if (!hasPreparedLaunch) {
|
|
1484
1590
|
val metadata = loadMetadataOrNull()
|
|
1485
1591
|
if (metadata?.verificationPending == true && metadata.launchInProgress) {
|
|
1486
|
-
metadata.stagingBundleId?.let { rollbackPendingBundle(it) }
|
|
1592
|
+
metadata.stagingBundleId?.let { rollbackPendingBundle(it, unfinishedLaunch = true) }
|
|
1487
1593
|
}
|
|
1488
1594
|
}
|
|
1489
1595
|
hasPreparedLaunch = true
|
|
1490
1596
|
|
|
1597
|
+
// A launch is recorded only once it can show UI (markLaunchStarted).
|
|
1491
1598
|
val selection = selectLaunch()
|
|
1492
|
-
|
|
1493
|
-
loadMetadataOrNull()?.let { metadata ->
|
|
1494
|
-
saveMetadata(metadata.copy(launchInProgress = true))
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1599
|
+
pendingLaunchBundleId = selection.launchedBundleId.takeIf { selection.shouldRollbackOnCrash }
|
|
1497
1600
|
Log.d(
|
|
1498
1601
|
TAG,
|
|
1499
1602
|
"prepareLaunch: bundleId=${selection.launchedBundleId} shouldRollback=${selection.shouldRollbackOnCrash} url=${selection.bundleUrl}",
|
|
@@ -296,12 +296,44 @@ data class CrashedBundleEntry(
|
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
/**
|
|
300
|
+
* A bundle whose launch ended before first content without a crash, waiting
|
|
301
|
+
* for its one retry. Such a launch cannot tell a hang from a user leaving
|
|
302
|
+
* early, so the bundle is offered again instead of entering crash history.
|
|
303
|
+
* A second unfinished launch of it adds it there.
|
|
304
|
+
*
|
|
305
|
+
* [retryReady] turns true once a later launch shows content. The retry
|
|
306
|
+
* starts with the next process, so the session that recovered never reloads
|
|
307
|
+
* into the same bundle.
|
|
308
|
+
*/
|
|
309
|
+
data class InterruptedLaunch(
|
|
310
|
+
val bundleId: String,
|
|
311
|
+
val retryReady: Boolean = false,
|
|
312
|
+
) {
|
|
313
|
+
companion object {
|
|
314
|
+
fun fromJson(json: JSONObject): InterruptedLaunch? =
|
|
315
|
+
json.optNullableString("bundleId")?.let { bundleId ->
|
|
316
|
+
InterruptedLaunch(
|
|
317
|
+
bundleId = bundleId,
|
|
318
|
+
retryReady = json.optBoolean("retryReady", false),
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
fun toJson(): JSONObject =
|
|
324
|
+
JSONObject().apply {
|
|
325
|
+
put("bundleId", bundleId)
|
|
326
|
+
put("retryReady", retryReady)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
299
330
|
/**
|
|
300
331
|
* History of crashed bundles
|
|
301
332
|
*/
|
|
302
333
|
data class CrashedHistory(
|
|
303
334
|
val bundles: MutableList<CrashedBundleEntry> = mutableListOf(),
|
|
304
335
|
val maxHistorySize: Int = DEFAULT_MAX_HISTORY_SIZE,
|
|
336
|
+
var interruptedLaunch: InterruptedLaunch? = null,
|
|
305
337
|
) {
|
|
306
338
|
companion object {
|
|
307
339
|
private const val TAG = "CrashedHistory"
|
|
@@ -317,6 +349,7 @@ data class CrashedHistory(
|
|
|
317
349
|
return CrashedHistory(
|
|
318
350
|
bundles = bundles,
|
|
319
351
|
maxHistorySize = json.optInt("maxHistorySize", DEFAULT_MAX_HISTORY_SIZE),
|
|
352
|
+
interruptedLaunch = json.optJSONObject("interruptedLaunch")?.let(InterruptedLaunch::fromJson),
|
|
320
353
|
)
|
|
321
354
|
}
|
|
322
355
|
|
|
@@ -342,6 +375,7 @@ data class CrashedHistory(
|
|
|
342
375
|
bundles.forEach { bundlesArray.put(it.toJson()) }
|
|
343
376
|
put("bundles", bundlesArray)
|
|
344
377
|
put("maxHistorySize", maxHistorySize)
|
|
378
|
+
interruptedLaunch?.let { put("interruptedLaunch", it.toJson()) }
|
|
345
379
|
}
|
|
346
380
|
|
|
347
381
|
fun saveToFile(file: File): Boolean =
|
|
@@ -389,6 +423,7 @@ data class CrashedHistory(
|
|
|
389
423
|
|
|
390
424
|
fun clear() {
|
|
391
425
|
bundles.clear()
|
|
426
|
+
interruptedLaunch = null
|
|
392
427
|
}
|
|
393
428
|
}
|
|
394
429
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.hotupdater
|
|
2
2
|
|
|
3
3
|
import android.app.ActivityOptions
|
|
4
|
+
import android.app.Application
|
|
4
5
|
import android.content.Context
|
|
5
6
|
import android.content.Intent
|
|
6
7
|
import android.os.Process
|
|
@@ -20,6 +21,7 @@ class HotUpdaterImpl {
|
|
|
20
21
|
private val releaseCatalogCache: ReleaseCatalogCacheService
|
|
21
22
|
private val installIdentity: InstallIdentityService
|
|
22
23
|
private val keyValueStorage: KeyValueStorageService
|
|
24
|
+
private val activityStartGate: ActivityStartGate
|
|
23
25
|
private var currentLaunchSelection: LaunchSelection? = null
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -33,6 +35,7 @@ class HotUpdaterImpl {
|
|
|
33
35
|
this.context = context.applicationContext
|
|
34
36
|
this.bundleStorage = bundleStorage
|
|
35
37
|
this.preferences = preferences
|
|
38
|
+
this.activityStartGate = ActivityStartGate(this.context as? Application)
|
|
36
39
|
this.recoveryManager = HotUpdaterRecoveryManager(this.context)
|
|
37
40
|
this.releaseCatalogCache =
|
|
38
41
|
ReleaseCatalogCacheService(
|
|
@@ -508,6 +511,16 @@ class HotUpdaterImpl {
|
|
|
508
511
|
|
|
509
512
|
val pendingRecovery = recoveryManager.consumePendingCrashRecovery()
|
|
510
513
|
val selection = bundleStorage.prepareLaunch(pendingRecovery)
|
|
514
|
+
// A headless JS task, such as a background push, loads the bundle without
|
|
515
|
+
// an activity and never renders. Only a launch with an activity may leave
|
|
516
|
+
// an unfinished launch for the next process to roll back.
|
|
517
|
+
activityStartGate.runWhenActivityStarted(
|
|
518
|
+
if (selection.shouldRollbackOnCrash) {
|
|
519
|
+
{ bundleStorage.markLaunchStarted(selection.launchedBundleId) }
|
|
520
|
+
} else {
|
|
521
|
+
null
|
|
522
|
+
},
|
|
523
|
+
)
|
|
511
524
|
recoveryManager.startMonitoring(
|
|
512
525
|
bundleId = selection.launchedBundleId,
|
|
513
526
|
shouldRollback = selection.shouldRollbackOnCrash,
|
|
@@ -234,141 +234,11 @@ internal class HotUpdaterRecoveryManager(
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// Each architecture's source set reaches its own React instance, so the old
|
|
238
|
+
// architecture's ReactNativeHost is compiled only into old architecture builds.
|
|
237
239
|
private fun installJavaScriptExceptionHooks() {
|
|
238
240
|
val application = appContext as? ReactApplication ?: return
|
|
239
|
-
|
|
240
|
-
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
|
241
|
-
installReactHostExceptionHook(application)
|
|
242
|
-
} else {
|
|
243
|
-
installLegacyExceptionHook(application)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private fun installReactHostExceptionHook(application: ReactApplication) {
|
|
248
|
-
val reactHost = getReactHost(application) ?: return
|
|
249
|
-
val reactHostDelegate =
|
|
250
|
-
findField(reactHost.javaClass, "mReactHostDelegate")?.let { field ->
|
|
251
|
-
field.isAccessible = true
|
|
252
|
-
field.get(reactHost)
|
|
253
|
-
}
|
|
254
|
-
?: findField(reactHost.javaClass, "reactHostDelegate")?.let { field ->
|
|
255
|
-
field.isAccessible = true
|
|
256
|
-
field.get(reactHost)
|
|
257
|
-
}
|
|
258
|
-
?: return
|
|
259
|
-
|
|
260
|
-
val delegateIdentity = System.identityHashCode(reactHostDelegate)
|
|
261
|
-
if (patchedReactHostDelegateIds.add(delegateIdentity)) {
|
|
262
|
-
val exceptionHandlerField = findField(reactHostDelegate.javaClass, "exceptionHandler")
|
|
263
|
-
if (exceptionHandlerField == null) {
|
|
264
|
-
patchedReactHostDelegateIds.remove(delegateIdentity)
|
|
265
|
-
return
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
exceptionHandlerField.isAccessible = true
|
|
269
|
-
@Suppress("UNCHECKED_CAST")
|
|
270
|
-
val previousHandler =
|
|
271
|
-
exceptionHandlerField.get(reactHostDelegate) as? (Exception) -> Unit
|
|
272
|
-
|
|
273
|
-
exceptionHandlerField.set(reactHostDelegate) { exception: Exception ->
|
|
274
|
-
if (activeManager?.handleJavaScriptException(exception) != true) {
|
|
275
|
-
previousHandler?.invoke(exception) ?: throw exception
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
val reactContext =
|
|
281
|
-
findMethod(reactHost.javaClass, "getCurrentReactContext")?.invoke(reactHost) as? ReactContext
|
|
282
|
-
reactContext?.let { patchReactContextExceptionHandler(it) }
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
private fun installLegacyExceptionHook(application: ReactApplication) {
|
|
286
|
-
val instanceManager = application.reactNativeHost.reactInstanceManager
|
|
287
|
-
val managerIdentity = System.identityHashCode(instanceManager)
|
|
288
|
-
if (patchedInstanceManagerIds.add(managerIdentity)) {
|
|
289
|
-
val exceptionHandlerField = findField(instanceManager.javaClass, "mJSExceptionHandler")
|
|
290
|
-
if (exceptionHandlerField == null) {
|
|
291
|
-
patchedInstanceManagerIds.remove(managerIdentity)
|
|
292
|
-
return
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
exceptionHandlerField.isAccessible = true
|
|
296
|
-
val previousHandler = exceptionHandlerField.get(instanceManager) as? JSExceptionHandler
|
|
297
|
-
exceptionHandlerField.set(instanceManager, RecoveryJSExceptionHandler(previousHandler))
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
instanceManager.currentReactContext?.let { reactContext ->
|
|
301
|
-
patchReactContextExceptionHandler(reactContext)
|
|
302
|
-
patchCatalystInstanceExceptionHandler(reactContext)
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
private fun patchReactContextExceptionHandler(reactContext: ReactContext) {
|
|
307
|
-
val contextIdentity = System.identityHashCode(reactContext)
|
|
308
|
-
if (!patchedReactContextIds.add(contextIdentity)) {
|
|
309
|
-
return
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
val previousHandler = reactContext.jsExceptionHandler
|
|
313
|
-
if (previousHandler is RecoveryJSExceptionHandler) {
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
reactContext.setJSExceptionHandler(RecoveryJSExceptionHandler(previousHandler))
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
private fun patchCatalystInstanceExceptionHandler(reactContext: ReactContext) {
|
|
321
|
-
val catalystInstance =
|
|
322
|
-
try {
|
|
323
|
-
reactContext.catalystInstance
|
|
324
|
-
} catch (_: Exception) {
|
|
325
|
-
null
|
|
326
|
-
} ?: return
|
|
327
|
-
|
|
328
|
-
val catalystIdentity = System.identityHashCode(catalystInstance)
|
|
329
|
-
if (!patchedCatalystInstanceIds.add(catalystIdentity)) {
|
|
330
|
-
return
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
val exceptionHandlerField = findField(catalystInstance.javaClass, "mJSExceptionHandler")
|
|
334
|
-
if (exceptionHandlerField == null) {
|
|
335
|
-
patchedCatalystInstanceIds.remove(catalystIdentity)
|
|
336
|
-
return
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
exceptionHandlerField.isAccessible = true
|
|
340
|
-
val previousHandler = exceptionHandlerField.get(catalystInstance) as? JSExceptionHandler
|
|
341
|
-
if (previousHandler is RecoveryJSExceptionHandler) {
|
|
342
|
-
return
|
|
343
|
-
}
|
|
344
|
-
exceptionHandlerField.set(catalystInstance, RecoveryJSExceptionHandler(previousHandler))
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
private fun getReactHost(application: ReactApplication): Any? =
|
|
348
|
-
try {
|
|
349
|
-
findMethod(application.javaClass, "getReactHost")?.invoke(application)
|
|
350
|
-
} catch (_: Exception) {
|
|
351
|
-
null
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
private fun findMethod(
|
|
355
|
-
clazz: Class<*>,
|
|
356
|
-
name: String,
|
|
357
|
-
) = runCatching { clazz.getMethod(name) }.getOrNull()
|
|
358
|
-
|
|
359
|
-
private fun findField(
|
|
360
|
-
clazz: Class<*>,
|
|
361
|
-
name: String,
|
|
362
|
-
): Field? {
|
|
363
|
-
var current: Class<*>? = clazz
|
|
364
|
-
while (true) {
|
|
365
|
-
val currentClass = current ?: break
|
|
366
|
-
runCatching { currentClass.getDeclaredField(name) }
|
|
367
|
-
.getOrNull()
|
|
368
|
-
?.let { return it }
|
|
369
|
-
current = currentClass.superclass
|
|
370
|
-
}
|
|
371
|
-
return null
|
|
241
|
+
JavaScriptExceptionHooks.install(application)
|
|
372
242
|
}
|
|
373
243
|
|
|
374
244
|
private fun loadNativeLibrary(): Boolean {
|
|
@@ -395,11 +265,11 @@ internal class HotUpdaterRecoveryManager(
|
|
|
395
265
|
shouldRollback: Boolean,
|
|
396
266
|
)
|
|
397
267
|
|
|
398
|
-
|
|
268
|
+
internal class RecoveryJSExceptionHandler(
|
|
399
269
|
private val previousHandler: JSExceptionHandler?,
|
|
400
270
|
) : JSExceptionHandler {
|
|
401
271
|
override fun handleException(e: Exception) {
|
|
402
|
-
if (
|
|
272
|
+
if (!handleJavaScriptExceptionForRecovery(e)) {
|
|
403
273
|
previousHandler?.handleException(e) ?: throw e
|
|
404
274
|
}
|
|
405
275
|
}
|
|
@@ -432,10 +302,40 @@ internal class HotUpdaterRecoveryManager(
|
|
|
432
302
|
@Volatile
|
|
433
303
|
private var activeManager: HotUpdaterRecoveryManager? = null
|
|
434
304
|
|
|
435
|
-
private val patchedInstanceManagerIds = mutableSetOf<Int>()
|
|
436
|
-
private val patchedCatalystInstanceIds = mutableSetOf<Int>()
|
|
437
305
|
private val patchedReactContextIds = mutableSetOf<Int>()
|
|
438
|
-
|
|
306
|
+
|
|
307
|
+
/** Hands a JavaScript error to the monitored launch; false when it does not recover it. */
|
|
308
|
+
internal fun handleJavaScriptExceptionForRecovery(exception: Exception): Boolean =
|
|
309
|
+
activeManager?.handleJavaScriptException(exception) == true
|
|
310
|
+
|
|
311
|
+
internal fun patchReactContextExceptionHandler(reactContext: ReactContext) {
|
|
312
|
+
val contextIdentity = System.identityHashCode(reactContext)
|
|
313
|
+
if (!patchedReactContextIds.add(contextIdentity)) {
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
val previousHandler = reactContext.jsExceptionHandler
|
|
318
|
+
if (previousHandler is RecoveryJSExceptionHandler) {
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
reactContext.setJSExceptionHandler(RecoveryJSExceptionHandler(previousHandler))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
internal fun findField(
|
|
326
|
+
clazz: Class<*>,
|
|
327
|
+
name: String,
|
|
328
|
+
): Field? {
|
|
329
|
+
var current: Class<*>? = clazz
|
|
330
|
+
while (true) {
|
|
331
|
+
val currentClass = current ?: break
|
|
332
|
+
runCatching { currentClass.getDeclaredField(name) }
|
|
333
|
+
.getOrNull()
|
|
334
|
+
?.let { return it }
|
|
335
|
+
current = currentClass.superclass
|
|
336
|
+
}
|
|
337
|
+
return null
|
|
338
|
+
}
|
|
439
339
|
|
|
440
340
|
@JvmStatic
|
|
441
341
|
fun handleRecoveryWatchdog(context: Context) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactApplication
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Routes JavaScript errors of the running React instance to crash recovery.
|
|
7
|
+
* The New Architecture reaches the instance through ReactHost.
|
|
8
|
+
*/
|
|
9
|
+
internal object JavaScriptExceptionHooks {
|
|
10
|
+
private val patchedReactHostDelegateIds = mutableSetOf<Int>()
|
|
11
|
+
|
|
12
|
+
fun install(application: ReactApplication) {
|
|
13
|
+
val reactHost = application.reactHost ?: return
|
|
14
|
+
val reactHostDelegate =
|
|
15
|
+
HotUpdaterRecoveryManager.findField(reactHost.javaClass, "mReactHostDelegate")?.let { field ->
|
|
16
|
+
field.isAccessible = true
|
|
17
|
+
field.get(reactHost)
|
|
18
|
+
}
|
|
19
|
+
?: HotUpdaterRecoveryManager.findField(reactHost.javaClass, "reactHostDelegate")?.let { field ->
|
|
20
|
+
field.isAccessible = true
|
|
21
|
+
field.get(reactHost)
|
|
22
|
+
}
|
|
23
|
+
?: return
|
|
24
|
+
|
|
25
|
+
val delegateIdentity = System.identityHashCode(reactHostDelegate)
|
|
26
|
+
if (patchedReactHostDelegateIds.add(delegateIdentity)) {
|
|
27
|
+
val exceptionHandlerField =
|
|
28
|
+
HotUpdaterRecoveryManager.findField(reactHostDelegate.javaClass, "exceptionHandler")
|
|
29
|
+
if (exceptionHandlerField == null) {
|
|
30
|
+
patchedReactHostDelegateIds.remove(delegateIdentity)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
exceptionHandlerField.isAccessible = true
|
|
35
|
+
@Suppress("UNCHECKED_CAST")
|
|
36
|
+
val previousHandler =
|
|
37
|
+
exceptionHandlerField.get(reactHostDelegate) as? (Exception) -> Unit
|
|
38
|
+
|
|
39
|
+
exceptionHandlerField.set(reactHostDelegate) { exception: Exception ->
|
|
40
|
+
if (!HotUpdaterRecoveryManager.handleJavaScriptExceptionForRecovery(exception)) {
|
|
41
|
+
previousHandler?.invoke(exception) ?: throw exception
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
reactHost.currentReactContext?.let(HotUpdaterRecoveryManager::patchReactContextExceptionHandler)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -5,12 +5,10 @@ import android.util.Log
|
|
|
5
5
|
import com.facebook.react.ReactApplication
|
|
6
6
|
import com.facebook.react.ReactHost
|
|
7
7
|
import com.facebook.react.ReactInstanceEventListener
|
|
8
|
-
import com.facebook.react.bridge.JSBundleLoader
|
|
9
8
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
10
9
|
import com.facebook.react.bridge.ReactContext
|
|
11
10
|
import com.facebook.react.common.LifecycleState
|
|
12
11
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
13
|
-
import java.lang.reflect.Field
|
|
14
12
|
import kotlin.coroutines.resume
|
|
15
13
|
|
|
16
14
|
class ReactIntegrationManager(
|
|
@@ -89,7 +87,7 @@ class ReactIntegrationManager(
|
|
|
89
87
|
|
|
90
88
|
/**
|
|
91
89
|
* Sets the JS bundle.
|
|
92
|
-
* Priority: ReactHostHolder (if set) > application.reactHost >
|
|
90
|
+
* Priority: ReactHostHolder (if set) > application.reactHost > the bridge (React Native 0.81 and older)
|
|
93
91
|
* @param bundleURL The bundle URL to set
|
|
94
92
|
*/
|
|
95
93
|
public fun setJSBundle(bundleURL: String) {
|
|
@@ -115,19 +113,8 @@ class ReactIntegrationManager(
|
|
|
115
113
|
return
|
|
116
114
|
}
|
|
117
115
|
|
|
118
|
-
// 4.
|
|
119
|
-
|
|
120
|
-
val instanceManager = application.reactNativeHost.reactInstanceManager
|
|
121
|
-
val bundleLoader: JSBundleLoader? = this.getJSBundlerLoader(bundleURL)
|
|
122
|
-
val bundleLoaderField: Field =
|
|
123
|
-
instanceManager::class.java.getDeclaredField("mBundleLoader")
|
|
124
|
-
bundleLoaderField.isAccessible = true
|
|
125
|
-
|
|
126
|
-
if (bundleLoader != null) {
|
|
127
|
-
bundleLoaderField.set(instanceManager, bundleLoader)
|
|
128
|
-
} else {
|
|
129
|
-
bundleLoaderField.set(instanceManager, null)
|
|
130
|
-
}
|
|
116
|
+
// 4. Fall back to the bridge, which React Native 0.81 and older can run
|
|
117
|
+
BridgeReactInstance.setJSBundle(application, getJSBundlerLoader(bundleURL))
|
|
131
118
|
} catch (e: Exception) {
|
|
132
119
|
Log.d("HotUpdater", "Failed to setJSBundle: ${e.message}")
|
|
133
120
|
}
|
|
@@ -135,7 +122,7 @@ class ReactIntegrationManager(
|
|
|
135
122
|
|
|
136
123
|
/**
|
|
137
124
|
* Reload the React Native application.
|
|
138
|
-
* Priority: ReactHostHolder (if set) > application.reactHost >
|
|
125
|
+
* Priority: ReactHostHolder (if set) > application.reactHost > the bridge (React Native 0.81 and older)
|
|
139
126
|
*/
|
|
140
127
|
public suspend fun reload() {
|
|
141
128
|
try {
|
|
@@ -160,21 +147,8 @@ class ReactIntegrationManager(
|
|
|
160
147
|
return
|
|
161
148
|
}
|
|
162
149
|
|
|
163
|
-
// 4.
|
|
164
|
-
|
|
165
|
-
val reactNativeHost = application.reactNativeHost
|
|
166
|
-
try {
|
|
167
|
-
reactNativeHost.reactInstanceManager.recreateReactContextInBackground()
|
|
168
|
-
} catch (e: Exception) {
|
|
169
|
-
val currentActivity = reactNativeHost.reactInstanceManager.currentReactContext?.currentActivity
|
|
170
|
-
if (currentActivity == null) {
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
currentActivity.runOnUiThread {
|
|
175
|
-
currentActivity.recreate()
|
|
176
|
-
}
|
|
177
|
-
}
|
|
150
|
+
// 4. Fall back to the bridge, which React Native 0.81 and older can run
|
|
151
|
+
BridgeReactInstance.reload(application)
|
|
178
152
|
} catch (e: Exception) {
|
|
179
153
|
Log.d("HotUpdater", "Failed to reload: ${e.message}")
|
|
180
154
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactApplication
|
|
4
|
+
import com.facebook.react.bridge.JSExceptionHandler
|
|
5
|
+
import com.facebook.react.bridge.ReactContext
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Routes JavaScript errors of the running React instance to crash recovery.
|
|
9
|
+
* The old architecture reaches the instance through ReactNativeHost, which only
|
|
10
|
+
* an old architecture build compiles.
|
|
11
|
+
*/
|
|
12
|
+
internal object JavaScriptExceptionHooks {
|
|
13
|
+
private val patchedInstanceManagerIds = mutableSetOf<Int>()
|
|
14
|
+
private val patchedCatalystInstanceIds = mutableSetOf<Int>()
|
|
15
|
+
|
|
16
|
+
fun install(application: ReactApplication) {
|
|
17
|
+
val instanceManager = application.reactNativeHost.reactInstanceManager
|
|
18
|
+
val managerIdentity = System.identityHashCode(instanceManager)
|
|
19
|
+
if (patchedInstanceManagerIds.add(managerIdentity)) {
|
|
20
|
+
val exceptionHandlerField =
|
|
21
|
+
HotUpdaterRecoveryManager.findField(instanceManager.javaClass, "mJSExceptionHandler")
|
|
22
|
+
if (exceptionHandlerField == null) {
|
|
23
|
+
patchedInstanceManagerIds.remove(managerIdentity)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
exceptionHandlerField.isAccessible = true
|
|
28
|
+
val previousHandler = exceptionHandlerField.get(instanceManager) as? JSExceptionHandler
|
|
29
|
+
exceptionHandlerField.set(
|
|
30
|
+
instanceManager,
|
|
31
|
+
HotUpdaterRecoveryManager.RecoveryJSExceptionHandler(previousHandler),
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
instanceManager.currentReactContext?.let { reactContext ->
|
|
36
|
+
HotUpdaterRecoveryManager.patchReactContextExceptionHandler(reactContext)
|
|
37
|
+
patchCatalystInstanceExceptionHandler(reactContext)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private fun patchCatalystInstanceExceptionHandler(reactContext: ReactContext) {
|
|
42
|
+
val catalystInstance =
|
|
43
|
+
try {
|
|
44
|
+
reactContext.catalystInstance
|
|
45
|
+
} catch (_: Exception) {
|
|
46
|
+
null
|
|
47
|
+
} ?: return
|
|
48
|
+
|
|
49
|
+
val catalystIdentity = System.identityHashCode(catalystInstance)
|
|
50
|
+
if (!patchedCatalystInstanceIds.add(catalystIdentity)) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
val exceptionHandlerField =
|
|
55
|
+
HotUpdaterRecoveryManager.findField(catalystInstance.javaClass, "mJSExceptionHandler")
|
|
56
|
+
if (exceptionHandlerField == null) {
|
|
57
|
+
patchedCatalystInstanceIds.remove(catalystIdentity)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
exceptionHandlerField.isAccessible = true
|
|
62
|
+
val previousHandler = exceptionHandlerField.get(catalystInstance) as? JSExceptionHandler
|
|
63
|
+
if (previousHandler is HotUpdaterRecoveryManager.RecoveryJSExceptionHandler) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
exceptionHandlerField.set(
|
|
67
|
+
catalystInstance,
|
|
68
|
+
HotUpdaterRecoveryManager.RecoveryJSExceptionHandler(previousHandler),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -293,6 +293,7 @@ public protocol BundleStorageService {
|
|
|
293
293
|
func commitReleaseSelection(_ selection: PersistedSelection) -> Bool
|
|
294
294
|
|
|
295
295
|
// Rollback support
|
|
296
|
+
func markLaunchStarted(bundleId: String?)
|
|
296
297
|
func markLaunchCompleted(bundleId: String?)
|
|
297
298
|
func notifyAppReady() -> [String: Any]
|
|
298
299
|
func getCrashHistory() -> CrashedHistory
|
|
@@ -373,6 +374,13 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
373
374
|
|
|
374
375
|
private var hasPreparedLaunch = false
|
|
375
376
|
private var currentLaunchReport: LaunchReport?
|
|
377
|
+
// The staged bundle this process launched before its first content. Only that
|
|
378
|
+
// launch is pending; a bundle staged later waits for the next launch.
|
|
379
|
+
private var pendingLaunchBundleId: String?
|
|
380
|
+
// Crash history is written at first content on the main thread while JavaScript can read it.
|
|
381
|
+
private let crashedHistoryLock = NSLock()
|
|
382
|
+
// A bundle waiting for its retry that this process keeps refusing, see InterruptedLaunch.
|
|
383
|
+
private var retryHeldBundleId: String?
|
|
376
384
|
private let activeBundleMetadataLock = NSLock()
|
|
377
385
|
private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot?
|
|
378
386
|
private let builtInBundleIdProvider: () -> String
|
|
@@ -1308,8 +1316,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1308
1316
|
)
|
|
1309
1317
|
}
|
|
1310
1318
|
|
|
1319
|
+
/// Rolls back the pending staging bundle. `unfinishedLaunch` means its launch
|
|
1320
|
+
/// ended before first content without a crash marker.
|
|
1311
1321
|
@discardableResult
|
|
1312
|
-
private func rollbackPendingBundle(_ stagingId: String) -> Bool {
|
|
1322
|
+
private func rollbackPendingBundle(_ stagingId: String, unfinishedLaunch: Bool = false) -> Bool {
|
|
1313
1323
|
guard var metadata = loadMetadataOrNull(), metadata.stagingBundleId == stagingId else {
|
|
1314
1324
|
return false
|
|
1315
1325
|
}
|
|
@@ -1318,9 +1328,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1318
1328
|
let pendingSelectionTransition = metadata.pendingSelectionTransition
|
|
1319
1329
|
let stableSelection = metadata.stableSelection
|
|
1320
1330
|
|
|
1321
|
-
|
|
1322
|
-
crashedHistory.addEntry(stagingId)
|
|
1323
|
-
let _ = saveCrashedHistory(crashedHistory)
|
|
1331
|
+
recordFailedLaunch(stagingId, unfinishedLaunch: unfinishedLaunch)
|
|
1324
1332
|
|
|
1325
1333
|
let fallbackBundleId = metadata.stableBundleId.flatMap { candidate in
|
|
1326
1334
|
if case .success(let storeDir) = bundleStoreDir() {
|
|
@@ -1988,14 +1996,12 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1988
1996
|
metadata.verificationPending,
|
|
1989
1997
|
metadata.launchInProgress,
|
|
1990
1998
|
let stagingBundleId = metadata.stagingBundleId {
|
|
1991
|
-
rollbackPendingBundle(stagingBundleId)
|
|
1999
|
+
rollbackPendingBundle(stagingBundleId, unfinishedLaunch: true)
|
|
1992
2000
|
}
|
|
1993
2001
|
hasPreparedLaunch = true
|
|
2002
|
+
// A launch is recorded only once it can show UI (markLaunchStarted).
|
|
1994
2003
|
let selection = selectLaunch(bundle: bundle)
|
|
1995
|
-
|
|
1996
|
-
metadata.launchInProgress = true
|
|
1997
|
-
_ = saveMetadata(metadata)
|
|
1998
|
-
}
|
|
2004
|
+
pendingLaunchBundleId = selection.shouldRollbackOnCrash ? selection.launchedBundleId : nil
|
|
1999
2005
|
return selection
|
|
2000
2006
|
}
|
|
2001
2007
|
|
|
@@ -2011,8 +2017,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2011
2017
|
*/
|
|
2012
2018
|
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL? = nil, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void) {
|
|
2013
2019
|
// Check if bundle is in crashed history
|
|
2014
|
-
|
|
2015
|
-
if crashedHistory.contains(bundleId) {
|
|
2020
|
+
if getCrashHistory().contains(bundleId) {
|
|
2016
2021
|
NSLog("[BundleStorage] Bundle '\(bundleId)' is in crashed history, rejecting update")
|
|
2017
2022
|
completion(.failure(BundleStorageError.bundleInCrashedHistory(bundleId)))
|
|
2018
2023
|
return
|
|
@@ -2786,7 +2791,24 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2786
2791
|
}
|
|
2787
2792
|
}
|
|
2788
2793
|
|
|
2794
|
+
func markLaunchStarted(bundleId: String?) {
|
|
2795
|
+
// Unlike prepareLaunch, this can run while JavaScript stages a newer bundle.
|
|
2796
|
+
releaseStateLock.lock()
|
|
2797
|
+
defer { releaseStateLock.unlock() }
|
|
2798
|
+
guard let bundleId,
|
|
2799
|
+
var metadata = loadMetadataOrNull(),
|
|
2800
|
+
metadata.verificationPending,
|
|
2801
|
+
!metadata.launchInProgress,
|
|
2802
|
+
metadata.stagingBundleId == bundleId else {
|
|
2803
|
+
return
|
|
2804
|
+
}
|
|
2805
|
+
metadata.launchInProgress = true
|
|
2806
|
+
_ = saveMetadata(metadata)
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2789
2809
|
func markLaunchCompleted(bundleId: String?) {
|
|
2810
|
+
// Before the guard: content from the built-in bundle (nil) also counts.
|
|
2811
|
+
readyInterruptedLaunchRetry(bundleId)
|
|
2790
2812
|
guard let bundleId,
|
|
2791
2813
|
var metadata = loadMetadataOrNull(),
|
|
2792
2814
|
metadata.verificationPending,
|
|
@@ -2818,9 +2840,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2818
2840
|
|
|
2819
2841
|
func notifyAppReady() -> [String: Any] {
|
|
2820
2842
|
guard let report = loadLaunchReport() else {
|
|
2821
|
-
if let
|
|
2843
|
+
if let launchedBundleId = pendingLaunchBundleId,
|
|
2844
|
+
let metadata = loadMetadataOrNull(),
|
|
2822
2845
|
metadata.verificationPending,
|
|
2823
|
-
metadata.stagingBundleId
|
|
2846
|
+
metadata.stagingBundleId == launchedBundleId {
|
|
2824
2847
|
return ["status": "PENDING"]
|
|
2825
2848
|
}
|
|
2826
2849
|
return ["status": LaunchReportStatus.unchanged.rawValue]
|
|
@@ -2846,11 +2869,20 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2846
2869
|
}
|
|
2847
2870
|
|
|
2848
2871
|
/**
|
|
2849
|
-
* Returns the
|
|
2872
|
+
* Returns the bundles this device refuses to install: crash history, plus a
|
|
2873
|
+
* bundle waiting for its retry until a process that may retry it.
|
|
2850
2874
|
* @return The crashed history object
|
|
2851
2875
|
*/
|
|
2852
2876
|
func getCrashHistory() -> CrashedHistory {
|
|
2853
|
-
|
|
2877
|
+
crashedHistoryLock.lock()
|
|
2878
|
+
defer { crashedHistoryLock.unlock() }
|
|
2879
|
+
var history = loadCrashedHistory()
|
|
2880
|
+
if let interrupted = history.interruptedLaunch,
|
|
2881
|
+
!interrupted.retryReady || interrupted.bundleId == retryHeldBundleId,
|
|
2882
|
+
!history.contains(interrupted.bundleId) {
|
|
2883
|
+
history.bundles.append(CrashedBundleEntry(bundleId: interrupted.bundleId))
|
|
2884
|
+
}
|
|
2885
|
+
return history
|
|
2854
2886
|
}
|
|
2855
2887
|
|
|
2856
2888
|
/**
|
|
@@ -2858,11 +2890,57 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2858
2890
|
* @return true if clearing was successful
|
|
2859
2891
|
*/
|
|
2860
2892
|
func clearCrashHistory() -> Bool {
|
|
2893
|
+
crashedHistoryLock.lock()
|
|
2894
|
+
defer { crashedHistoryLock.unlock() }
|
|
2861
2895
|
var history = loadCrashedHistory()
|
|
2862
2896
|
history.clear()
|
|
2897
|
+
retryHeldBundleId = nil
|
|
2863
2898
|
return saveCrashedHistory(history)
|
|
2864
2899
|
}
|
|
2865
2900
|
|
|
2901
|
+
/// A crash, or a second unfinished launch of the same bundle, adds the bundle
|
|
2902
|
+
/// to crash history. A first unfinished launch cannot tell a hang from a user
|
|
2903
|
+
/// leaving early, so it leaves the bundle one retry instead.
|
|
2904
|
+
private func recordFailedLaunch(_ bundleId: String, unfinishedLaunch: Bool) {
|
|
2905
|
+
crashedHistoryLock.lock()
|
|
2906
|
+
defer { crashedHistoryLock.unlock() }
|
|
2907
|
+
var history = loadCrashedHistory()
|
|
2908
|
+
let interrupted = history.interruptedLaunch
|
|
2909
|
+
if unfinishedLaunch, interrupted?.bundleId != bundleId {
|
|
2910
|
+
NSLog("[BundleStorage] Launch of \(bundleId) ended before first content; it gets one retry")
|
|
2911
|
+
history.interruptedLaunch = InterruptedLaunch(bundleId: bundleId)
|
|
2912
|
+
retryHeldBundleId = bundleId
|
|
2913
|
+
} else {
|
|
2914
|
+
history.addEntry(bundleId)
|
|
2915
|
+
if interrupted?.bundleId == bundleId {
|
|
2916
|
+
history.interruptedLaunch = nil
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
_ = saveCrashedHistory(history)
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
/// First content appeared, so the app works again. A bundle waiting for its
|
|
2923
|
+
/// retry may be installed from the next process on. This process keeps
|
|
2924
|
+
/// refusing it, so the session that recovered never reloads into it. Content
|
|
2925
|
+
/// from that bundle verifies it.
|
|
2926
|
+
private func readyInterruptedLaunchRetry(_ currentBundleId: String?) {
|
|
2927
|
+
crashedHistoryLock.lock()
|
|
2928
|
+
defer { crashedHistoryLock.unlock() }
|
|
2929
|
+
var history = loadCrashedHistory()
|
|
2930
|
+
guard let interrupted = history.interruptedLaunch else {
|
|
2931
|
+
return
|
|
2932
|
+
}
|
|
2933
|
+
if interrupted.bundleId == currentBundleId {
|
|
2934
|
+
history.interruptedLaunch = nil
|
|
2935
|
+
} else if interrupted.retryReady {
|
|
2936
|
+
return
|
|
2937
|
+
} else {
|
|
2938
|
+
history.interruptedLaunch?.retryReady = true
|
|
2939
|
+
retryHeldBundleId = interrupted.bundleId
|
|
2940
|
+
}
|
|
2941
|
+
_ = saveCrashedHistory(history)
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2866
2944
|
func getInstallId() -> String {
|
|
2867
2945
|
loadOrCreateInstallIdentity().installId
|
|
2868
2946
|
}
|
|
@@ -222,6 +222,37 @@ public struct CrashedBundleEntry: Codable {
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
// MARK: - InterruptedLaunch
|
|
226
|
+
|
|
227
|
+
/// A bundle whose launch ended before first content without a crash, waiting
|
|
228
|
+
/// for its one retry. Such a launch cannot tell a hang from a user leaving
|
|
229
|
+
/// early, so the bundle is offered again instead of entering crash history.
|
|
230
|
+
/// A second unfinished launch of it adds it there.
|
|
231
|
+
///
|
|
232
|
+
/// `retryReady` turns true once a later launch shows content. The retry starts
|
|
233
|
+
/// with the next process, so the session that recovered never reloads into the
|
|
234
|
+
/// same bundle.
|
|
235
|
+
public struct InterruptedLaunch: Codable {
|
|
236
|
+
let bundleId: String
|
|
237
|
+
var retryReady: Bool
|
|
238
|
+
|
|
239
|
+
enum CodingKeys: String, CodingKey {
|
|
240
|
+
case bundleId
|
|
241
|
+
case retryReady
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
init(bundleId: String, retryReady: Bool = false) {
|
|
245
|
+
self.bundleId = bundleId
|
|
246
|
+
self.retryReady = retryReady
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
public init(from decoder: Decoder) throws {
|
|
250
|
+
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
251
|
+
bundleId = try values.decode(String.self, forKey: .bundleId)
|
|
252
|
+
retryReady = try values.decodeIfPresent(Bool.self, forKey: .retryReady) ?? false
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
225
256
|
// MARK: - CrashedHistory
|
|
226
257
|
|
|
227
258
|
/// History of crashed bundles
|
|
@@ -231,10 +262,30 @@ public struct CrashedHistory: Codable {
|
|
|
231
262
|
|
|
232
263
|
var bundles: [CrashedBundleEntry]
|
|
233
264
|
var maxHistorySize: Int
|
|
265
|
+
var interruptedLaunch: InterruptedLaunch?
|
|
266
|
+
|
|
267
|
+
enum CodingKeys: String, CodingKey {
|
|
268
|
+
case bundles
|
|
269
|
+
case maxHistorySize
|
|
270
|
+
case interruptedLaunch
|
|
271
|
+
}
|
|
234
272
|
|
|
235
|
-
init(
|
|
273
|
+
init(
|
|
274
|
+
bundles: [CrashedBundleEntry] = [],
|
|
275
|
+
maxHistorySize: Int = CrashedHistory.defaultMaxHistorySize,
|
|
276
|
+
interruptedLaunch: InterruptedLaunch? = nil
|
|
277
|
+
) {
|
|
236
278
|
self.bundles = bundles
|
|
237
279
|
self.maxHistorySize = maxHistorySize
|
|
280
|
+
self.interruptedLaunch = interruptedLaunch
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public init(from decoder: Decoder) throws {
|
|
284
|
+
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
285
|
+
bundles = try values.decode([CrashedBundleEntry].self, forKey: .bundles)
|
|
286
|
+
maxHistorySize = try values.decode(Int.self, forKey: .maxHistorySize)
|
|
287
|
+
// A malformed retry record must not drop the crash history.
|
|
288
|
+
interruptedLaunch = try? values.decodeIfPresent(InterruptedLaunch.self, forKey: .interruptedLaunch)
|
|
238
289
|
}
|
|
239
290
|
|
|
240
291
|
static func load(from file: URL) -> CrashedHistory {
|
|
@@ -298,6 +349,7 @@ public struct CrashedHistory: Codable {
|
|
|
298
349
|
|
|
299
350
|
mutating func clear() {
|
|
300
351
|
bundles.removeAll()
|
|
352
|
+
interruptedLaunch = nil
|
|
301
353
|
}
|
|
302
354
|
}
|
|
303
355
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import React
|
|
3
|
+
import UIKit
|
|
3
4
|
|
|
4
5
|
private func hotUpdaterUncaughtExceptionHandler(_ exception: NSException) {
|
|
5
6
|
HotUpdaterRecoveryManager.shared.handleUncaughtException(exception)
|
|
@@ -58,6 +59,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
58
59
|
private let releaseCatalogCache: ReleaseCatalogCacheService
|
|
59
60
|
private let keyValueStorage: KeyValueStorageService
|
|
60
61
|
private var currentLaunchSelection: LaunchSelection?
|
|
62
|
+
private var launchStartObservers: [NSObjectProtocol] = []
|
|
61
63
|
|
|
62
64
|
private static let DEFAULT_CHANNEL = "production"
|
|
63
65
|
private static let CHANNEL_STORAGE_KEY = "HotUpdaterChannel"
|
|
@@ -703,12 +705,49 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
703
705
|
|
|
704
706
|
let pendingRecovery = recoveryManager.consumePendingCrashRecovery()
|
|
705
707
|
let selection = bundleStorage.prepareLaunch(bundle: bundle, pendingRecovery: pendingRecovery)
|
|
708
|
+
markLaunchStartedInForeground(selection)
|
|
706
709
|
recoveryManager.startMonitoring(bundleId: selection.launchedBundleId, shouldRollback: selection.shouldRollbackOnCrash) { [weak self] launchedBundleId in
|
|
707
710
|
self?.bundleStorage.markLaunchCompleted(bundleId: launchedBundleId)
|
|
708
711
|
}
|
|
709
712
|
currentLaunchSelection = selection
|
|
710
713
|
return selection
|
|
711
714
|
}
|
|
715
|
+
|
|
716
|
+
// A background launch, such as a silent push, may never render. Only a
|
|
717
|
+
// launch in the foreground may leave an unfinished launch for the next
|
|
718
|
+
// process to roll back.
|
|
719
|
+
private func markLaunchStartedInForeground(_ selection: LaunchSelection) {
|
|
720
|
+
let bundleId = selection.shouldRollbackOnCrash ? selection.launchedBundleId : nil
|
|
721
|
+
let mark = { [weak self] in
|
|
722
|
+
guard let self else { return }
|
|
723
|
+
self.removeLaunchStartObservers()
|
|
724
|
+
guard let bundleId else { return }
|
|
725
|
+
guard UIApplication.shared.applicationState == .background else {
|
|
726
|
+
self.bundleStorage.markLaunchStarted(bundleId: bundleId)
|
|
727
|
+
return
|
|
728
|
+
}
|
|
729
|
+
self.launchStartObservers = [
|
|
730
|
+
UIApplication.willEnterForegroundNotification,
|
|
731
|
+
UIApplication.didBecomeActiveNotification,
|
|
732
|
+
].map { name in
|
|
733
|
+
NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
|
|
734
|
+
self?.removeLaunchStartObservers()
|
|
735
|
+
self?.bundleStorage.markLaunchStarted(bundleId: bundleId)
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// UIApplication is read on the main thread.
|
|
740
|
+
if Thread.isMainThread {
|
|
741
|
+
mark()
|
|
742
|
+
} else {
|
|
743
|
+
DispatchQueue.main.async(execute: mark)
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private func removeLaunchStartObservers() {
|
|
748
|
+
launchStartObservers.forEach { NotificationCenter.default.removeObserver($0) }
|
|
749
|
+
launchStartObservers = []
|
|
750
|
+
}
|
|
712
751
|
}
|
|
713
752
|
|
|
714
753
|
@objcMembers
|
|
@@ -4,5 +4,5 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.HOT_UPDATER_SDK_VERSION = void 0;
|
|
7
|
-
const HOT_UPDATER_SDK_VERSION = exports.HOT_UPDATER_SDK_VERSION = "1.0.0-rc.
|
|
7
|
+
const HOT_UPDATER_SDK_VERSION = exports.HOT_UPDATER_SDK_VERSION = "1.0.0-rc.35";
|
|
8
8
|
//# sourceMappingURL=sdkVersion.js.map
|
package/lib/module/sdkVersion.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.
|
|
1
|
+
export declare const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.35";
|
|
2
2
|
//# sourceMappingURL=sdkVersion.d.ts.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.
|
|
1
|
+
export declare const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.35";
|
|
2
2
|
//# sourceMappingURL=sdkVersion.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/react-native",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.35",
|
|
4
4
|
"description": "React Native OTA solution for self-hosted",
|
|
5
5
|
"source": "./src/index.ts",
|
|
6
6
|
"main": "./lib/commonjs/index.js",
|
|
@@ -123,8 +123,8 @@
|
|
|
123
123
|
},
|
|
124
124
|
"dependencies": {
|
|
125
125
|
"use-sync-external-store": "1.5.0",
|
|
126
|
-
"@hot-updater/protocol": "1.0.0-rc.
|
|
127
|
-
"@hot-updater/plugin-insights": "1.0.0-rc.
|
|
126
|
+
"@hot-updater/protocol": "1.0.0-rc.35",
|
|
127
|
+
"@hot-updater/plugin-insights": "1.0.0-rc.35"
|
|
128
128
|
},
|
|
129
129
|
"scripts": {
|
|
130
130
|
"build": "node ./scripts/sync-sdk-version.mjs run -- bob build",
|
package/src/sdkVersion.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.
|
|
1
|
+
export const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.35";
|