@hot-updater/react-native 1.0.0-rc.34 → 1.0.0-rc.36
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/HotUpdater.mm +19 -22
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +39 -0
- package/lib/commonjs/pluginHost.js +1 -0
- package/lib/commonjs/pluginHost.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/module/pluginHost.js +2 -1
- package/lib/module/pluginHost.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/pluginHost.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/package.json +3 -3
- package/src/pluginHost.ts +2 -0
- package/src/sdkVersion.ts +1 -1
|
@@ -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
|
|