@hot-updater/react-native 1.0.8 → 1.1.1
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/HotUpdater.podspec +14 -2
- package/Package.swift +24 -3
- package/android/build.gradle +2 -6
- package/android/hot-updater-fingerprint.gradle +135 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
- package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
- package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
- package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
- package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
- package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +493 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
- package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
- package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
- package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
- package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
- package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
- package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
- package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
- package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
- package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
- package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
- package/android/src/newarch/HotUpdater.kt +53 -15
- package/android/src/newarch/HotUpdaterModule.kt +42 -17
- package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
- package/android/src/oldarch/HotUpdater.kt +51 -12
- package/android/src/oldarch/HotUpdaterModule.kt +43 -17
- package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
- package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
- package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
- package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
- package/ios/HotUpdater/Internal/CohortService.swift +8 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
- package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
- package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
- package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
- package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
- package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
- package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
- package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
- package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
- package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
- package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
- package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
- package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
- package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
- package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
- package/ios/HotUpdater/Package.swift +11 -0
- package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
- package/ios/HotUpdater/Public/HotUpdater.h +21 -2
- package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
- package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
- package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
- package/lib/commonjs/checkForUpdate.js +3 -1
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +9 -12
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +24 -79
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/nativeUpdateEvents.js +66 -0
- package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +3 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +10 -13
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +19 -73
- package/lib/module/native.js.map +1 -1
- package/lib/module/nativeUpdateEvents.js +61 -0
- package/lib/module/nativeUpdateEvents.js.map +1 -0
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -14
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +9 -46
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -14
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +9 -46
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/checkForUpdate.ts +7 -1
- package/src/index.ts +9 -17
- package/src/native.ts +19 -91
- package/src/nativeUpdateEvents.ts +77 -0
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +20 -3
- package/android/libs/README.md +0 -50
- package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
- package/android/src/bridge/BridgeReactInstance.kt +0 -38
- package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
- package/android/src/main/java/com/hotupdater/ContentAppearedLog.kt +0 -31
- package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
- package/android/src/newarch/ReactIntegrationManager.kt +0 -212
- package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
|
@@ -3,18 +3,79 @@ import React
|
|
|
3
3
|
import UIKit
|
|
4
4
|
|
|
5
5
|
private func hotUpdaterUncaughtExceptionHandler(_ exception: NSException) {
|
|
6
|
-
|
|
6
|
+
HotUpdaterRecoveryHub.shared.handleUncaughtException(exception)
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
@_silgen_name("
|
|
10
|
-
private func
|
|
9
|
+
@_silgen_name("HotUpdaterInstallCrashSignalHandlers")
|
|
10
|
+
private func hotUpdaterInstallCrashSignalHandlers()
|
|
11
11
|
|
|
12
|
-
@_silgen_name("
|
|
13
|
-
private func
|
|
12
|
+
@_silgen_name("HotUpdaterUpdateCrashSlot")
|
|
13
|
+
private func hotUpdaterUpdateCrashSlotSymbol(
|
|
14
|
+
_ slot: Int,
|
|
15
|
+
_ crashMarkerPath: NSString,
|
|
14
16
|
_ bundleId: NSString?,
|
|
15
17
|
_ shouldRollback: ObjCBool
|
|
16
18
|
)
|
|
17
19
|
|
|
20
|
+
@_silgen_name("HotUpdaterAttributeCrashToSlot")
|
|
21
|
+
private func hotUpdaterAttributeCrashToSlot(_ slot: Int)
|
|
22
|
+
|
|
23
|
+
@_silgen_name("HotUpdaterCopyImplForRootObject")
|
|
24
|
+
private func hotUpdaterCopyImplForRootObjectSymbol(_ object: AnyObject) -> UnsafeMutableRawPointer?
|
|
25
|
+
|
|
26
|
+
@_silgen_name("HotUpdaterCopyRuntimeImpl")
|
|
27
|
+
private func hotUpdaterCopyRuntimeImplSymbol() -> UnsafeMutableRawPointer?
|
|
28
|
+
|
|
29
|
+
@_silgen_name("HotUpdaterCopyBundleURLForRootObject")
|
|
30
|
+
private func hotUpdaterCopyBundleURLForRootObjectSymbol(_ object: AnyObject) -> UnsafeMutableRawPointer?
|
|
31
|
+
|
|
32
|
+
@_silgen_name("HotUpdaterCopyRuntimeIdentityForRootObject")
|
|
33
|
+
private func hotUpdaterCopyRuntimeIdentityForRootObjectSymbol(_ object: AnyObject) -> UnsafeMutableRawPointer?
|
|
34
|
+
|
|
35
|
+
@_silgen_name("HotUpdaterCopyRuntimeIdentity")
|
|
36
|
+
private func hotUpdaterCopyRuntimeIdentitySymbol() -> UnsafeMutableRawPointer?
|
|
37
|
+
|
|
38
|
+
/// The object standing for the runtime `object` belongs to (its module
|
|
39
|
+
/// bridgeless, its bridge in the old architecture), when one told it apart.
|
|
40
|
+
private func hotUpdaterRuntimeIdentity(forRootObject object: AnyObject) -> AnyObject? {
|
|
41
|
+
hotUpdaterCopyRuntimeIdentityForRootObjectSymbol(object).map {
|
|
42
|
+
Unmanaged<AnyObject>.fromOpaque($0).takeRetainedValue()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// The object standing for the runtime whose JavaScript thread this is.
|
|
47
|
+
private func hotUpdaterRuntimeIdentity() -> AnyObject? {
|
|
48
|
+
hotUpdaterCopyRuntimeIdentitySymbol().map {
|
|
49
|
+
Unmanaged<AnyObject>.fromOpaque($0).takeRetainedValue()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@_silgen_name("HotUpdaterCopyRuntimeBundleURL")
|
|
54
|
+
private func hotUpdaterCopyRuntimeBundleURLSymbol() -> UnsafeMutableRawPointer?
|
|
55
|
+
|
|
56
|
+
/// The bundle the runtime `object` belongs to runs.
|
|
57
|
+
private func hotUpdaterBundleURL(forRootObject object: AnyObject) -> URL? {
|
|
58
|
+
hotUpdaterCopyBundleURLForRootObjectSymbol(object).flatMap {
|
|
59
|
+
Unmanaged<AnyObject>.fromOpaque($0).takeRetainedValue() as? URL
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// The bundle the runtime whose JavaScript thread this is runs.
|
|
64
|
+
private func hotUpdaterRuntimeBundleURL() -> URL? {
|
|
65
|
+
hotUpdaterCopyRuntimeBundleURLSymbol().flatMap {
|
|
66
|
+
Unmanaged<AnyObject>.fromOpaque($0).takeRetainedValue() as? URL
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@_silgen_name("HotUpdaterIsViewShown")
|
|
71
|
+
private func hotUpdaterIsViewShownSymbol(_ object: AnyObject) -> ObjCBool
|
|
72
|
+
|
|
73
|
+
/// Whether a started Fabric surface, or an old-architecture root view, is in
|
|
74
|
+
/// a window. Main thread only.
|
|
75
|
+
private func hotUpdaterIsViewShown(_ object: AnyObject) -> Bool {
|
|
76
|
+
hotUpdaterIsViewShownSymbol(object).boolValue
|
|
77
|
+
}
|
|
78
|
+
|
|
18
79
|
@_silgen_name("HotUpdaterPerformRecoveryReload")
|
|
19
80
|
private func hotUpdaterPerformRecoveryReloadSymbol() -> ObjCBool
|
|
20
81
|
|
|
@@ -33,12 +94,38 @@ private func hotUpdaterAfterSurfaceStarts(_ action: @convention(block) @escaping
|
|
|
33
94
|
@_silgen_name("HotUpdaterCopyMinBundleId")
|
|
34
95
|
private func hotUpdaterCopyMinBundleIdSymbol() -> UnsafeMutableRawPointer
|
|
35
96
|
|
|
36
|
-
|
|
37
|
-
|
|
97
|
+
@_silgen_name("HotUpdaterCopyBuildFingerprintHash")
|
|
98
|
+
private func hotUpdaterCopyBuildFingerprintHashSymbol() -> UnsafeMutableRawPointer?
|
|
99
|
+
|
|
100
|
+
/// The fingerprint this build embedded: the hash of the native interface it
|
|
101
|
+
/// was built from, or nil for an app without one. Named apart from the
|
|
102
|
+
/// generated `hotUpdaterBuildFingerprintHash()`, which SwiftPM compiles into
|
|
103
|
+
/// this module.
|
|
104
|
+
private let embeddedFingerprintHash: String? = hotUpdaterCopyBuildFingerprintHashSymbol()
|
|
105
|
+
.map { Unmanaged<NSString>.fromOpaque($0).takeRetainedValue() as String }
|
|
106
|
+
|
|
107
|
+
extension HotUpdaterInstanceEnvironment {
|
|
108
|
+
/// Hands the shared config the fingerprint this build embedded, once,
|
|
109
|
+
/// before anything resolves an instance's values.
|
|
110
|
+
static let registerBuildFingerprintHash: Void = {
|
|
111
|
+
HotUpdaterConfig.shared.buildFingerprintHash = embeddedFingerprintHash
|
|
112
|
+
}()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// The impl whose runtime `object` belongs to: a Fabric surface or root
|
|
116
|
+
/// view, an RCTInstance or RCTHost, an old-architecture root view or bridge.
|
|
117
|
+
/// Nil when no runtime claims it yet.
|
|
118
|
+
private func hotUpdaterImpl(forRootObject object: AnyObject) -> HotUpdaterImpl? {
|
|
119
|
+
hotUpdaterCopyImplForRootObjectSymbol(object).flatMap {
|
|
120
|
+
Unmanaged<AnyObject>.fromOpaque($0).takeRetainedValue() as? HotUpdaterImpl
|
|
121
|
+
}
|
|
38
122
|
}
|
|
39
123
|
|
|
40
|
-
|
|
41
|
-
|
|
124
|
+
/// The impl of the React Native runtime whose JavaScript thread this is.
|
|
125
|
+
private func hotUpdaterRuntimeImpl() -> HotUpdaterImpl? {
|
|
126
|
+
hotUpdaterCopyRuntimeImplSymbol().flatMap {
|
|
127
|
+
Unmanaged<AnyObject>.fromOpaque($0).takeRetainedValue() as? HotUpdaterImpl
|
|
128
|
+
}
|
|
42
129
|
}
|
|
43
130
|
|
|
44
131
|
private func hotUpdaterPerformRecoveryReload() -> Bool {
|
|
@@ -55,13 +142,59 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
55
142
|
private let bundleStorage: BundleStorageService
|
|
56
143
|
private let preferences: PreferencesService
|
|
57
144
|
private let cohortService: CohortService
|
|
145
|
+
/// Whose state this impl keeps; see `HotUpdaterInstanceEnvironment`.
|
|
146
|
+
let environment: HotUpdaterInstanceEnvironment
|
|
147
|
+
/// This instance's crash recovery: its marker, its launch's watch, and
|
|
148
|
+
/// its rollback, apart from every other instance's.
|
|
58
149
|
private let recoveryManager: HotUpdaterRecoveryManager
|
|
59
150
|
private let releaseCatalogCache: ReleaseCatalogCacheService
|
|
60
151
|
private let keyValueStorage: KeyValueStorageService
|
|
61
|
-
|
|
152
|
+
// Read by native update checks on other threads while React Native sets it.
|
|
153
|
+
private let launchSelectionLock = NSLock()
|
|
154
|
+
private var launchSelectionValue: LaunchSelection?
|
|
155
|
+
private var currentLaunchSelection: LaunchSelection? {
|
|
156
|
+
get {
|
|
157
|
+
launchSelectionLock.lock()
|
|
158
|
+
defer { launchSelectionLock.unlock() }
|
|
159
|
+
return launchSelectionValue
|
|
160
|
+
}
|
|
161
|
+
set {
|
|
162
|
+
launchSelectionLock.lock()
|
|
163
|
+
launchSelectionValue = newValue
|
|
164
|
+
launchSelectionLock.unlock()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// The selection React Native runs in this process. A channel reset clears
|
|
168
|
+
// currentLaunchSelection, so React Native's next read picks the default
|
|
169
|
+
// channel's bundle, but the running bundle stays until a reload.
|
|
170
|
+
private var runningLaunchSelectionValue: LaunchSelection?
|
|
171
|
+
private var runningLaunchSelection: LaunchSelection? {
|
|
172
|
+
get {
|
|
173
|
+
launchSelectionLock.lock()
|
|
174
|
+
defer { launchSelectionLock.unlock() }
|
|
175
|
+
return runningLaunchSelectionValue
|
|
176
|
+
}
|
|
177
|
+
set {
|
|
178
|
+
launchSelectionLock.lock()
|
|
179
|
+
runningLaunchSelectionValue = newValue
|
|
180
|
+
launchSelectionLock.unlock()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Serializes the launch's bundle selection with the update check's read
|
|
184
|
+
// of it, and with a channel reset or built-in commit, which delete
|
|
185
|
+
// downloaded bundles.
|
|
186
|
+
private let launchPreparationLock = LaunchPreparationLock()
|
|
187
|
+
/// Advanced when this instance's own fatal marks its launch for
|
|
188
|
+
/// rollback; a selection prepared at an older epoch is prepared again.
|
|
189
|
+
private let launchEpoch = HotUpdaterLaunchEpoch()
|
|
190
|
+
// Guarded by launchPreparationLock.
|
|
191
|
+
private var currentLaunchSelectionEpoch = 0
|
|
62
192
|
private var launchStartObservers: [NSObjectProtocol] = []
|
|
193
|
+
// Main thread only: the launch the observers mark started, and the
|
|
194
|
+
// newest launch whose mark ran.
|
|
195
|
+
private var launchStartObserversGeneration: Int?
|
|
196
|
+
private var launchStartGenerations = HotUpdaterLatestGeneration()
|
|
63
197
|
|
|
64
|
-
private static let DEFAULT_CHANNEL = "production"
|
|
65
198
|
private static let CHANNEL_STORAGE_KEY = "HotUpdaterChannel"
|
|
66
199
|
|
|
67
200
|
private static func parseSafeByteSize(_ value: Any?) -> Int64? {
|
|
@@ -85,26 +218,43 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
85
218
|
* Convenience initializer that creates and configures all dependencies.
|
|
86
219
|
*/
|
|
87
220
|
public convenience override init() {
|
|
221
|
+
self.init(environment: .main)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Creates the impl of one instance name, with its own storage, keys, and
|
|
226
|
+
* config. Main's is `HotUpdaterNativeUpdateBridge.sharedImpl`.
|
|
227
|
+
*/
|
|
228
|
+
convenience init(environment: HotUpdaterInstanceEnvironment) {
|
|
229
|
+
_ = HotUpdaterInstanceEnvironment.registerBuildFingerprintHash
|
|
230
|
+
// Main is built from one snapshot of its values, which a later main
|
|
231
|
+
// instance must match; nothing below reads HotUpdaterConfig again.
|
|
232
|
+
let environment = environment.isMain ? HotUpdaterMainBundle.shared.implEnvironment() : environment
|
|
88
233
|
let fileSystem = FileManagerService()
|
|
89
|
-
let isolationKey =
|
|
234
|
+
let isolationKey = environment.isolationKey(appVersion: Self.appVersion, minBundleId: Self.minBundleId())
|
|
90
235
|
let preferences = VersionedPreferencesService()
|
|
91
|
-
let downloadService = URLSessionDownloadService(
|
|
236
|
+
let downloadService = URLSessionDownloadService(
|
|
237
|
+
backgroundSessionIdentifier: environment.backgroundSessionIdentifier
|
|
238
|
+
)
|
|
92
239
|
let bundleStorage = BundleFileStorageService(
|
|
93
240
|
fileSystem: fileSystem,
|
|
94
241
|
downloadService: downloadService,
|
|
95
242
|
preferences: preferences,
|
|
96
243
|
isolationKey: isolationKey,
|
|
244
|
+
instance: environment,
|
|
97
245
|
builtInBundleIdProvider: { HotUpdaterImpl.minBundleId() }
|
|
98
246
|
)
|
|
99
|
-
let recoveryManager = HotUpdaterRecoveryManager.shared
|
|
100
247
|
|
|
101
248
|
self.init(
|
|
102
249
|
bundleStorage: bundleStorage,
|
|
103
250
|
preferences: preferences,
|
|
104
|
-
|
|
105
|
-
|
|
251
|
+
environment: environment,
|
|
252
|
+
recoveryManager: HotUpdaterRecoveryManager(environment: environment, bundleStorage: bundleStorage),
|
|
253
|
+
releaseCatalogCache: ReleaseCatalogCacheService(
|
|
254
|
+
rootDirectory: environment.releaseCatalogCacheDirectory(in: fileSystem)
|
|
255
|
+
),
|
|
106
256
|
keyValueStorage: KeyValueStorageService(
|
|
107
|
-
directory:
|
|
257
|
+
directory: environment.keyValueStorageDirectory(in: fileSystem)
|
|
108
258
|
)
|
|
109
259
|
)
|
|
110
260
|
}
|
|
@@ -118,6 +268,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
118
268
|
internal init(
|
|
119
269
|
bundleStorage: BundleStorageService,
|
|
120
270
|
preferences: PreferencesService,
|
|
271
|
+
environment: HotUpdaterInstanceEnvironment = .main,
|
|
121
272
|
recoveryManager: HotUpdaterRecoveryManager,
|
|
122
273
|
releaseCatalogCache: ReleaseCatalogCacheService = ReleaseCatalogCacheService(),
|
|
123
274
|
keyValueStorage: KeyValueStorageService = KeyValueStorageService(
|
|
@@ -127,13 +278,17 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
127
278
|
self.bundleStorage = bundleStorage
|
|
128
279
|
self.preferences = preferences
|
|
129
280
|
self.cohortService = CohortService()
|
|
281
|
+
self.environment = environment
|
|
130
282
|
self.recoveryManager = recoveryManager
|
|
131
283
|
self.releaseCatalogCache = releaseCatalogCache
|
|
132
284
|
self.keyValueStorage = keyValueStorage
|
|
133
285
|
super.init()
|
|
286
|
+
recoveryManager.onRollbackCharged = { [weak self] in
|
|
287
|
+
self?.invalidateLaunchPreparation()
|
|
288
|
+
}
|
|
134
289
|
|
|
135
290
|
// Configure preferences with isolation key
|
|
136
|
-
let isolationKey =
|
|
291
|
+
let isolationKey = environment.isolationKey(appVersion: Self.appVersion, minBundleId: Self.minBundleId())
|
|
137
292
|
(preferences as? VersionedPreferencesService)?.configure(isolationKey: isolationKey)
|
|
138
293
|
}
|
|
139
294
|
|
|
@@ -143,17 +298,14 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
143
298
|
* Returns the app version from main bundle info.
|
|
144
299
|
*/
|
|
145
300
|
public static var appVersion: String? {
|
|
146
|
-
return
|
|
301
|
+
return HotUpdaterInstanceEnvironment.appVersion
|
|
147
302
|
}
|
|
148
303
|
|
|
149
304
|
/**
|
|
150
|
-
* Returns
|
|
305
|
+
* Returns main's default channel.
|
|
151
306
|
*/
|
|
152
307
|
public static var appChannel: String {
|
|
153
|
-
|
|
154
|
-
return override
|
|
155
|
-
}
|
|
156
|
-
return Bundle.main.object(forInfoDictionaryKey: "HOT_UPDATER_CHANNEL") as? String ?? Self.DEFAULT_CHANNEL
|
|
308
|
+
return HotUpdaterInstanceEnvironment.main.defaultChannel()
|
|
157
309
|
}
|
|
158
310
|
|
|
159
311
|
public static func minBundleId() -> String {
|
|
@@ -162,32 +314,13 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
162
314
|
|
|
163
315
|
/**
|
|
164
316
|
* Gets the complete isolation key for preferences storage.
|
|
165
|
-
* @return The isolation key in format: hotupdater_{
|
|
317
|
+
* @return The isolation key in format: hotupdater_{fingerprint}_{minBundleId}_{channel}_
|
|
318
|
+
* (fingerprint strategy) or hotupdater_{appVersion}_{channel}_
|
|
166
319
|
*/
|
|
167
320
|
public static func getIsolationKey() -> String {
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return override
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Get fingerprint hash from programmatic override or Info.plist
|
|
175
|
-
let fingerprintHash = HotUpdaterConfig.shared.fingerprintHash
|
|
176
|
-
?? Bundle.main.object(forInfoDictionaryKey: "HOT_UPDATER_FINGERPRINT_HASH") as? String
|
|
177
|
-
|
|
178
|
-
// Get app version and channel
|
|
179
|
-
let appVersion = self.appVersion ?? "unknown"
|
|
180
|
-
let appChannel = self.appChannel
|
|
181
|
-
|
|
182
|
-
// Include both fingerprint hash and app version for complete isolation
|
|
183
|
-
let baseKey: String
|
|
184
|
-
if let hash = fingerprintHash, !hash.isEmpty {
|
|
185
|
-
baseKey = "\(hash)_\(appVersion)"
|
|
186
|
-
} else {
|
|
187
|
-
baseKey = appVersion
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return "hotupdater_\(baseKey)_\(appChannel)_"
|
|
321
|
+
// The deprecated programmatic override is used verbatim.
|
|
322
|
+
_ = HotUpdaterInstanceEnvironment.registerBuildFingerprintHash
|
|
323
|
+
return HotUpdaterInstanceEnvironment.main.isolationKey(appVersion: appVersion, minBundleId: minBundleId())
|
|
191
324
|
}
|
|
192
325
|
|
|
193
326
|
// MARK: - Channel Management
|
|
@@ -202,19 +335,19 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
202
335
|
!channel.isEmpty {
|
|
203
336
|
return channel
|
|
204
337
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
338
|
+
// A runtime channel switch outranks the default channel, which
|
|
339
|
+
// `HotUpdaterOptions.channel` (or the deprecated
|
|
340
|
+
// `configure(channel:)`) only replaces, as on Android.
|
|
208
341
|
if let savedChannel = try? preferences.getItem(forKey: Self.CHANNEL_STORAGE_KEY),
|
|
209
342
|
!savedChannel.isEmpty {
|
|
210
343
|
return savedChannel
|
|
211
344
|
}
|
|
212
345
|
|
|
213
|
-
return
|
|
346
|
+
return environment.defaultChannel()
|
|
214
347
|
}
|
|
215
348
|
|
|
216
349
|
public func getDefaultChannel() -> String {
|
|
217
|
-
return
|
|
350
|
+
return environment.defaultChannel()
|
|
218
351
|
}
|
|
219
352
|
|
|
220
353
|
/**
|
|
@@ -222,10 +355,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
222
355
|
* @return The fingerprint hash or nil if not set
|
|
223
356
|
*/
|
|
224
357
|
public func getFingerprintHash() -> String? {
|
|
225
|
-
|
|
226
|
-
return override
|
|
227
|
-
}
|
|
228
|
-
return Bundle.main.object(forInfoDictionaryKey: "HOT_UPDATER_FINGERPRINT_HASH") as? String
|
|
358
|
+
return environment.fingerprintHash()
|
|
229
359
|
}
|
|
230
360
|
|
|
231
361
|
// MARK: - Cohort Management
|
|
@@ -243,10 +373,20 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
243
373
|
/**
|
|
244
374
|
* Gets the URL to the bundle file.
|
|
245
375
|
* @param bundle instance to lookup the JavaScript bundle resource. Defaults to Bundle.main.
|
|
376
|
+
* Main records it in `HotUpdaterConfig.shared.bundle` for reloads and Info.plist keys;
|
|
377
|
+
* a named impl reads its own bundle and leaves main's alone.
|
|
246
378
|
* @return URL to the bundle or nil
|
|
247
379
|
*/
|
|
248
380
|
public func bundleURL(bundle: Bundle = Bundle.main) -> URL? {
|
|
249
|
-
|
|
381
|
+
if environment.isMain {
|
|
382
|
+
HotUpdaterMainBundle.shared.note(bundle)
|
|
383
|
+
}
|
|
384
|
+
let bundleURL = prepareLaunchIfNeeded(bundle: bundle).bundleURL
|
|
385
|
+
if !environment.isMain, let bundleURL {
|
|
386
|
+
// The runtime started with this URL finds this impl by it.
|
|
387
|
+
HotUpdaterNativeUpdateBridge.router.record(launchURL: bundleURL, name: environment.name)
|
|
388
|
+
}
|
|
389
|
+
return bundleURL
|
|
250
390
|
}
|
|
251
391
|
|
|
252
392
|
// MARK: - Bundle Update
|
|
@@ -263,12 +403,24 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
263
403
|
public func updateBundle(_ params: [AnyHashable: Any]?,
|
|
264
404
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
265
405
|
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
406
|
+
updateBundle(params, onProgress: nil, onFilesResponse: nil, resolver: resolve, rejecter: reject)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* `updateBundle`, also reporting progress (0 to 1) to `onProgress` on the
|
|
411
|
+
* main thread, and each artifact files response to `onFilesResponse`.
|
|
412
|
+
*/
|
|
413
|
+
func updateBundle(_ params: [AnyHashable: Any]?,
|
|
414
|
+
onProgress: ((Double) -> Void)?,
|
|
415
|
+
onFilesResponse: ((Int, String?) -> Void)?,
|
|
416
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
417
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
266
418
|
// Legacy: artifact protocol v1, where every file comes with its URL.
|
|
267
419
|
// JavaScript from the first protocol v2 SDK also sends `filesUrl`
|
|
268
420
|
// here, with only the Hermes patch in `assets`; its OTA Bundles must
|
|
269
421
|
// still install.
|
|
270
|
-
stage(params, resolver: resolve, rejecter: reject) { data, reject in
|
|
271
|
-
switch Self.parseFileResolver(data, required: false) {
|
|
422
|
+
stage(params, onProgress: onProgress, resolver: resolve, rejecter: reject) { data, reject in
|
|
423
|
+
switch Self.parseFileResolver(data, required: false, onResponse: onFilesResponse) {
|
|
272
424
|
case .failure(let error):
|
|
273
425
|
reject("INVALID_FILE_URL", error.localizedDescription, error)
|
|
274
426
|
return nil
|
|
@@ -292,8 +444,20 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
292
444
|
public func installArtifact(_ params: [AnyHashable: Any]?,
|
|
293
445
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
294
446
|
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
295
|
-
|
|
296
|
-
|
|
447
|
+
installArtifact(params, onProgress: nil, onFilesResponse: nil, resolver: resolve, rejecter: reject)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* `installArtifact`, also reporting progress (0 to 1) to `onProgress` on
|
|
452
|
+
* the main thread, and each artifact files response to `onFilesResponse`.
|
|
453
|
+
*/
|
|
454
|
+
func installArtifact(_ params: [AnyHashable: Any]?,
|
|
455
|
+
onProgress: ((Double) -> Void)?,
|
|
456
|
+
onFilesResponse: ((Int, String?) -> Void)?,
|
|
457
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
458
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
459
|
+
stage(params, onProgress: onProgress, resolver: resolve, rejecter: reject) { data, reject in
|
|
460
|
+
Self.parseArtifactInstall(data, onFilesResponse: onFilesResponse, rejecter: reject)
|
|
297
461
|
}
|
|
298
462
|
}
|
|
299
463
|
|
|
@@ -303,6 +467,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
303
467
|
/// assets and file resolver `plan` returns; `plan` rejects and returns nil
|
|
304
468
|
/// when its own parameters are invalid.
|
|
305
469
|
private func stage(_ params: [AnyHashable: Any]?,
|
|
470
|
+
onProgress: ((Double) -> Void)?,
|
|
306
471
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
307
472
|
rejecter reject: @escaping RCTPromiseRejectBlock,
|
|
308
473
|
plan: ([AnyHashable: Any], RCTPromiseRejectBlock) -> StagePlan?) {
|
|
@@ -355,7 +520,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
355
520
|
return
|
|
356
521
|
}
|
|
357
522
|
let selection = (data["selection"] as? [String: Any]).flatMap(Self.parseSelection)
|
|
358
|
-
if let selection, !bundleStorage.
|
|
523
|
+
if let selection, !bundleStorage.isSelectionCurrent(selection) {
|
|
359
524
|
// Not an update failure, so the rejection carries no stage or reason.
|
|
360
525
|
let error = StaleReleaseSelectionError() as NSError
|
|
361
526
|
reject("UNKNOWN_ERROR", error.localizedDescription, error)
|
|
@@ -368,17 +533,20 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
368
533
|
NSLog("[HotUpdaterImpl] updateBundle called with bundleId: \(bundleId), manifestUrl: \(manifestUrl.absoluteString)")
|
|
369
534
|
|
|
370
535
|
// Heavy work is delegated to bundle storage service with safe error handling
|
|
371
|
-
bundleStorage.updateBundle(bundleId: bundleId, manifestUrl: manifestUrl, manifestFileHash: manifestFileHash, archiveUrl: archiveUrl, assets: planned.assets, fileResolver: planned.fileResolver, progressHandler: { payload in
|
|
536
|
+
bundleStorage.updateBundle(bundleId: bundleId, manifestUrl: manifestUrl, manifestFileHash: manifestFileHash, archiveUrl: archiveUrl, assets: planned.assets, fileResolver: planned.fileResolver, selection: selection, progressHandler: { payload in
|
|
372
537
|
DispatchQueue.main.async {
|
|
538
|
+
// The impl as the object: HotUpdater.mm forwards only
|
|
539
|
+
// main's progress to JavaScript.
|
|
373
540
|
NotificationCenter.default.post(
|
|
374
541
|
name: .updateProgressDidChange,
|
|
375
|
-
object:
|
|
542
|
+
object: self,
|
|
376
543
|
userInfo: payload.userInfo
|
|
377
544
|
)
|
|
378
545
|
|
|
379
546
|
if let callback = progressCallback {
|
|
380
547
|
callback([payload.progress])
|
|
381
548
|
}
|
|
549
|
+
onProgress?(payload.progress)
|
|
382
550
|
}
|
|
383
551
|
}) { [weak self] result in
|
|
384
552
|
guard let self = self else {
|
|
@@ -465,7 +633,8 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
465
633
|
/// by one, with the update check's headers and timeout; nil when
|
|
466
634
|
/// `filesUrl` is missing and not `required`.
|
|
467
635
|
private static func parseFileResolver(_ data: [AnyHashable: Any],
|
|
468
|
-
required: Bool
|
|
636
|
+
required: Bool,
|
|
637
|
+
onResponse: ((Int, String?) -> Void)?) -> Result<ArtifactFileResolver?, NSError> {
|
|
469
638
|
let filesUrlString = data["filesUrl"] as? String ?? ""
|
|
470
639
|
if filesUrlString.isEmpty && !required {
|
|
471
640
|
return .success(nil)
|
|
@@ -480,16 +649,22 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
480
649
|
.compactMapValues { $0 as? String }
|
|
481
650
|
let timeout = (data["requestTimeout"] as? NSNumber).map { $0.doubleValue / 1000 }
|
|
482
651
|
.flatMap { $0 > 0 ? $0 : nil } ?? HTTPArtifactFileResolver.defaultTimeout
|
|
483
|
-
return .success(HTTPArtifactFileResolver(
|
|
652
|
+
return .success(HTTPArtifactFileResolver(
|
|
653
|
+
filesUrl: filesUrl,
|
|
654
|
+
headers: headers,
|
|
655
|
+
timeout: timeout,
|
|
656
|
+
onResponse: onResponse
|
|
657
|
+
))
|
|
484
658
|
}
|
|
485
659
|
|
|
486
660
|
/// Artifact protocol v2: the Hermes patch as the only planned asset, and
|
|
487
661
|
/// the resolver that asks `filesUrl` for the files native downloads one
|
|
488
662
|
/// by one.
|
|
489
663
|
private static func parseArtifactInstall(_ data: [AnyHashable: Any],
|
|
664
|
+
onFilesResponse: ((Int, String?) -> Void)?,
|
|
490
665
|
rejecter reject: RCTPromiseRejectBlock) -> StagePlan? {
|
|
491
666
|
let fileResolver: ArtifactFileResolver?
|
|
492
|
-
switch parseFileResolver(data, required: true) {
|
|
667
|
+
switch parseFileResolver(data, required: true, onResponse: onFilesResponse) {
|
|
493
668
|
case .failure(let error):
|
|
494
669
|
reject("INVALID_FILE_URL", error.localizedDescription, error)
|
|
495
670
|
return nil
|
|
@@ -644,7 +819,11 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
644
819
|
reject("INVALID_SELECTION", error.localizedDescription, error)
|
|
645
820
|
return
|
|
646
821
|
}
|
|
647
|
-
|
|
822
|
+
// A built-in commit deletes every downloaded bundle, so it runs under
|
|
823
|
+
// the launch lock as a channel reset does.
|
|
824
|
+
resolve(launchPreparationLock.commitReleaseSelection(selection, in: bundleStorage) {
|
|
825
|
+
self.currentLaunchSelection = nil
|
|
826
|
+
})
|
|
648
827
|
}
|
|
649
828
|
|
|
650
829
|
/**
|
|
@@ -764,6 +943,13 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
764
943
|
return nil
|
|
765
944
|
}
|
|
766
945
|
|
|
946
|
+
/// The bundle this process runs. A channel reset clears
|
|
947
|
+
/// currentLaunchSelection for the next launch, but the running bundle
|
|
948
|
+
/// stays until React Native reloads.
|
|
949
|
+
func runningBundleId() -> String? {
|
|
950
|
+
(currentLaunchSelection ?? runningLaunchSelection)?.launchedBundleId
|
|
951
|
+
}
|
|
952
|
+
|
|
767
953
|
/**
|
|
768
954
|
* Gets the current manifest from bundle storage.
|
|
769
955
|
*/
|
|
@@ -777,21 +963,71 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
777
963
|
|
|
778
964
|
public func resetLaunchPreparation() {
|
|
779
965
|
currentLaunchSelection = nil
|
|
966
|
+
runningLaunchSelection = nil
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/// This named instance's own fatal marked its launch for rollback, and
|
|
970
|
+
/// the process may live on: the host's next `bundleURL()` prepares the
|
|
971
|
+
/// launch again, consuming the marker, instead of returning the bundle
|
|
972
|
+
/// that failed. The running bundle stays what React Native runs, as
|
|
973
|
+
/// after a channel reset.
|
|
974
|
+
///
|
|
975
|
+
/// Takes no lock: a crash can reach here on a thread that holds the
|
|
976
|
+
/// launch lock. A preparation already running keeps the epoch it started
|
|
977
|
+
/// with, so its selection is prepared again at the next `bundleURL()`.
|
|
978
|
+
func invalidateLaunchPreparation() {
|
|
979
|
+
launchEpoch.invalidate()
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// MARK: - Native Update Check
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* The bundle the native update check counts as running: the launched one,
|
|
986
|
+
* or before React Native starts, the one its launch would pick. Preparing
|
|
987
|
+
* the launch here would keep a forced update staged before React Native
|
|
988
|
+
* starts from applying in this session.
|
|
989
|
+
*/
|
|
990
|
+
func bundleIdForUpdateCheck() -> String? {
|
|
991
|
+
launchPreparationLock.withLock {
|
|
992
|
+
if let launchSelection = currentLaunchSelection ?? runningLaunchSelection {
|
|
993
|
+
return launchSelection.launchedBundleId
|
|
994
|
+
}
|
|
995
|
+
// Settles what the previous process left first, so the check reads the
|
|
996
|
+
// selection, launch state, and crash history this launch will use.
|
|
997
|
+
// Each instance consumes only its own crash marker.
|
|
998
|
+
bundleStorage.settlePreviousLaunch(pendingRecovery: { self.recoveryManager.consumePendingCrashRecovery() })
|
|
999
|
+
return bundleStorage.peekLaunchBundleId()
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
func releaseCatalogCacheValue(_ partition: String) -> String? {
|
|
1004
|
+
releaseCatalogCache.get(partition: partition)
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
func setReleaseCatalogCacheValue(_ partition: String, value: String) -> Bool {
|
|
1008
|
+
releaseCatalogCache.set(partition: partition, value: value)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
func removeReleaseCatalogCacheValue(_ partition: String) {
|
|
1012
|
+
releaseCatalogCache.remove(partition: partition)
|
|
780
1013
|
}
|
|
781
1014
|
|
|
782
1015
|
@objc
|
|
783
1016
|
public func resetChannel(_ resolver: @escaping RCTPromiseResolveBlock,
|
|
784
1017
|
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
case .success(let success):
|
|
1018
|
+
// Under the launch lock, so React Native never selects a bundle the
|
|
1019
|
+
// reset is about to delete.
|
|
1020
|
+
let result = launchPreparationLock.resetChannel(in: bundleStorage) {
|
|
789
1021
|
do {
|
|
790
1022
|
try preferences.setItem(nil, forKey: Self.CHANNEL_STORAGE_KEY)
|
|
791
1023
|
} catch {
|
|
792
1024
|
NSLog("[HotUpdaterImpl] Failed to clear channel override: \(error)")
|
|
793
1025
|
}
|
|
794
1026
|
self.currentLaunchSelection = nil
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
switch result {
|
|
1030
|
+
case .success(let success):
|
|
795
1031
|
resolver(success)
|
|
796
1032
|
case .failure(let error):
|
|
797
1033
|
let normalizedCode = HotUpdaterImpl.normalizeErrorCode(from: error)
|
|
@@ -801,40 +1037,80 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
801
1037
|
}
|
|
802
1038
|
|
|
803
1039
|
private func prepareLaunchIfNeeded(bundle: Bundle) -> LaunchSelection {
|
|
804
|
-
|
|
1040
|
+
launchPreparationLock.withLock { prepareLaunchLocked(bundle: bundle) }
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
private func prepareLaunchLocked(bundle: Bundle) -> LaunchSelection {
|
|
1044
|
+
if let currentLaunchSelection, launchEpoch.isCurrent(currentLaunchSelectionEpoch) {
|
|
805
1045
|
return currentLaunchSelection
|
|
806
1046
|
}
|
|
807
1047
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1048
|
+
// The epoch is read before the marker is consumed, and checked again
|
|
1049
|
+
// once this launch's monitoring replaced the running one's, after
|
|
1050
|
+
// which that launch can no longer mark a rollback: a rollback marked
|
|
1051
|
+
// meanwhile prepares again, consuming it. Monitoring is registered
|
|
1052
|
+
// before the foreground mark, so another instance's fatal that stops
|
|
1053
|
+
// every started launch also sees this one; starting it again on a
|
|
1054
|
+
// retry replaces it.
|
|
1055
|
+
let (selection, generation, epoch) = launchEpoch.prepareCurrent(
|
|
1056
|
+
prepare: {
|
|
1057
|
+
let pendingRecovery = recoveryManager.consumePendingCrashRecovery()
|
|
1058
|
+
return bundleStorage.prepareLaunch(bundle: bundle, pendingRecovery: pendingRecovery)
|
|
1059
|
+
},
|
|
1060
|
+
startMonitoring: { selection in
|
|
1061
|
+
// The same bundle again (a named instance): its launch goes on.
|
|
1062
|
+
if !environment.isMain, let generation = recoveryManager.continuingLaunch(
|
|
1063
|
+
bundleId: selection.launchedBundleId,
|
|
1064
|
+
launchURL: selection.bundleURL
|
|
1065
|
+
) {
|
|
1066
|
+
return generation
|
|
1067
|
+
}
|
|
1068
|
+
return recoveryManager.startMonitoring(
|
|
1069
|
+
bundleId: selection.launchedBundleId,
|
|
1070
|
+
shouldRollback: selection.shouldRollbackOnCrash,
|
|
1071
|
+
launchURL: selection.bundleURL
|
|
1072
|
+
)
|
|
1073
|
+
}
|
|
1074
|
+
)
|
|
1075
|
+
// A named runtime may be warmed up without UI, so its launch starts
|
|
1076
|
+
// only once one of its views is shown (HotUpdaterRecoveryHub), and
|
|
1077
|
+
// only its own first content verifies it.
|
|
1078
|
+
if environment.isMain {
|
|
1079
|
+
markLaunchStartedInForeground(selection, generation: generation)
|
|
813
1080
|
}
|
|
1081
|
+
runningLaunchSelection = selection
|
|
814
1082
|
currentLaunchSelection = selection
|
|
1083
|
+
currentLaunchSelectionEpoch = epoch
|
|
815
1084
|
return selection
|
|
816
1085
|
}
|
|
817
1086
|
|
|
818
1087
|
// A background launch, such as a silent push, may never render. Only a
|
|
819
1088
|
// launch in the foreground may leave an unfinished launch for the next
|
|
820
1089
|
// process to roll back.
|
|
821
|
-
|
|
1090
|
+
// Through crash recovery, which refuses a later launch's, one whose
|
|
1091
|
+
// content appeared, and holds it while another instance's fatal is on
|
|
1092
|
+
// its way.
|
|
1093
|
+
private func markLaunchStartedInForeground(_ selection: LaunchSelection, generation: Int) {
|
|
822
1094
|
let bundleId = selection.shouldRollbackOnCrash ? selection.launchedBundleId : nil
|
|
823
1095
|
let mark = { [weak self] in
|
|
824
1096
|
guard let self else { return }
|
|
825
|
-
|
|
826
|
-
|
|
1097
|
+
// Queued for an older launch, behind a newer launch's mark: the
|
|
1098
|
+
// newer launch's observers stay.
|
|
1099
|
+
guard self.launchStartGenerations.admit(generation) else { return }
|
|
1100
|
+
self.removeLaunchStartObservers(before: generation + 1)
|
|
1101
|
+
guard bundleId != nil else { return }
|
|
827
1102
|
guard UIApplication.shared.applicationState == .background else {
|
|
828
|
-
self.
|
|
1103
|
+
self.recoveryManager.markMainLaunchStarted(generation: generation)
|
|
829
1104
|
return
|
|
830
1105
|
}
|
|
1106
|
+
self.launchStartObserversGeneration = generation
|
|
831
1107
|
self.launchStartObservers = [
|
|
832
1108
|
UIApplication.willEnterForegroundNotification,
|
|
833
1109
|
UIApplication.didBecomeActiveNotification,
|
|
834
1110
|
].map { name in
|
|
835
1111
|
NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
|
|
836
|
-
self?.removeLaunchStartObservers()
|
|
837
|
-
self?.
|
|
1112
|
+
self?.removeLaunchStartObservers(before: generation + 1)
|
|
1113
|
+
self?.recoveryManager.markMainLaunchStarted(generation: generation)
|
|
838
1114
|
}
|
|
839
1115
|
}
|
|
840
1116
|
}
|
|
@@ -846,121 +1122,435 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
846
1122
|
}
|
|
847
1123
|
}
|
|
848
1124
|
|
|
849
|
-
|
|
1125
|
+
/// Removes the observers if a launch before `generation` installed
|
|
1126
|
+
/// them. Main thread only.
|
|
1127
|
+
private func removeLaunchStartObservers(before generation: Int) {
|
|
1128
|
+
guard let installed = launchStartObserversGeneration, installed < generation else { return }
|
|
850
1129
|
launchStartObservers.forEach { NotificationCenter.default.removeObserver($0) }
|
|
851
1130
|
launchStartObservers = []
|
|
1131
|
+
launchStartObserversGeneration = nil
|
|
852
1132
|
}
|
|
853
1133
|
}
|
|
854
1134
|
|
|
1135
|
+
/// One instance's crash recovery: consumes its marker at launch, watches its
|
|
1136
|
+
/// launch until 10 seconds after its first content, and for main, reloads
|
|
1137
|
+
/// in-process when its JavaScript fails before first content. The state lives
|
|
1138
|
+
/// in `HotUpdaterRecoveryHub.shared.coordinator`; the process-wide handlers in
|
|
1139
|
+
/// the hub dispatch to the instance a crash or content belongs to.
|
|
855
1140
|
@objcMembers
|
|
856
1141
|
final class HotUpdaterRecoveryManager: NSObject {
|
|
857
|
-
|
|
858
|
-
|
|
1142
|
+
let name: String
|
|
859
1143
|
private let crashMarkerURL: URL
|
|
860
|
-
private
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
private var signalHandlersInstalled = false
|
|
865
|
-
private var handlersInstalled = false
|
|
866
|
-
private var isMonitoring = false
|
|
867
|
-
private var recoveryRequested = false
|
|
868
|
-
private var monitoringGeneration = 0
|
|
869
|
-
private var currentBundleId: String?
|
|
870
|
-
private var shouldRollbackOnCrash = false
|
|
871
|
-
private var contentAppearedCallback: ((String?) -> Void)?
|
|
1144
|
+
private let hub: HotUpdaterRecoveryHub
|
|
1145
|
+
/// Set by the impl: invalidates its launch preparation.
|
|
1146
|
+
var onRollbackCharged: (() -> Void)?
|
|
872
1147
|
private var stopMonitoringWorkItem: DispatchWorkItem?
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1148
|
+
// Main thread only: a named launch whose view started in the background
|
|
1149
|
+
// starts once the app comes to the foreground.
|
|
1150
|
+
private var armObservers: [NSObjectProtocol] = []
|
|
1151
|
+
// The launch the observers start.
|
|
1152
|
+
private var armGeneration: Int?
|
|
1153
|
+
private let backgroundViewStarts = HotUpdaterBackgroundViewStarts()
|
|
1154
|
+
|
|
1155
|
+
init(environment: HotUpdaterInstanceEnvironment, bundleStorage: BundleStorageService) {
|
|
1156
|
+
name = environment.name
|
|
1157
|
+
if environment.isMain {
|
|
1158
|
+
// Main's path as it always was: Documents/bundle-store.
|
|
1159
|
+
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
1160
|
+
crashMarkerURL = HotUpdaterCrashMarker.url(for: environment, documentsPath: documentsPath.path)
|
|
1161
|
+
} else {
|
|
1162
|
+
crashMarkerURL = HotUpdaterCrashMarker.url(
|
|
1163
|
+
for: environment,
|
|
1164
|
+
documentsPath: FileManagerService().documentsPath()
|
|
1165
|
+
)
|
|
1166
|
+
}
|
|
1167
|
+
hub = .shared
|
|
879
1168
|
super.init()
|
|
1169
|
+
hub.coordinator.register(name: name, markerURL: crashMarkerURL, storage: bundleStorage)
|
|
1170
|
+
hub.register(self)
|
|
880
1171
|
}
|
|
881
1172
|
|
|
1173
|
+
private var isMain: Bool { name == HotUpdaterInstanceEnvironment.mainName }
|
|
1174
|
+
|
|
882
1175
|
func consumePendingCrashRecovery() -> PendingCrashRecovery? {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
1176
|
+
HotUpdaterCrashMarker.consume(at: crashMarkerURL)
|
|
1177
|
+
}
|
|
886
1178
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1179
|
+
@discardableResult
|
|
1180
|
+
func startMonitoring(bundleId: String?, shouldRollback: Bool, launchURL: URL?) -> Int {
|
|
1181
|
+
objc_sync_enter(self)
|
|
1182
|
+
defer { objc_sync_exit(self) }
|
|
890
1183
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1184
|
+
// Under this manager's lock, as are the content and grace callbacks
|
|
1185
|
+
// that check the generation, so none acts on a launch it wasn't for.
|
|
1186
|
+
let generation = hub.coordinator.startMonitoring(
|
|
1187
|
+
name: name,
|
|
1188
|
+
bundleId: bundleId,
|
|
1189
|
+
shouldRollback: shouldRollback,
|
|
1190
|
+
launchURL: launchURL
|
|
1191
|
+
)
|
|
1192
|
+
stopMonitoringWorkItem?.cancel()
|
|
1193
|
+
stopMonitoringWorkItem = nil
|
|
1194
|
+
if !isMain {
|
|
1195
|
+
DispatchQueue.main.async { [weak self] in self?.removeArmObservers(before: generation) }
|
|
898
1196
|
}
|
|
1197
|
+
|
|
1198
|
+
hub.installIfNeeded()
|
|
1199
|
+
return generation
|
|
899
1200
|
}
|
|
900
1201
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1202
|
+
/// The generation of the launch a preparation of the same bundle
|
|
1203
|
+
/// continues, if any.
|
|
1204
|
+
func continuingLaunch(bundleId: String?, launchURL: URL?) -> Int? {
|
|
1205
|
+
hub.coordinator.continuingLaunch(name: name, bundleId: bundleId, launchURL: launchURL)
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/// Main only: its launch of `generation` is in the foreground.
|
|
1209
|
+
func markMainLaunchStarted(generation: Int) {
|
|
1210
|
+
hub.coordinator.markMainLaunchStarted(generation: generation)
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/// This instance's first content: its monitoring ends after a grace period.
|
|
1214
|
+
func contentAppeared(generation: Int) {
|
|
906
1215
|
objc_sync_enter(self)
|
|
907
1216
|
defer { objc_sync_exit(self) }
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
currentBundleId = bundleId
|
|
911
|
-
shouldRollbackOnCrash = shouldRollback
|
|
912
|
-
recoveryRequested = false
|
|
913
|
-
contentAppearedCallback = onContentAppeared
|
|
914
|
-
isMonitoring = true
|
|
1217
|
+
// A launch started since then has its own grace period.
|
|
1218
|
+
guard hub.coordinator.isCurrent(name: name, generation: generation) else { return }
|
|
915
1219
|
|
|
916
1220
|
stopMonitoringWorkItem?.cancel()
|
|
1221
|
+
let workItem = DispatchWorkItem { [weak self] in
|
|
1222
|
+
self?.finishMonitoring(generation: generation)
|
|
1223
|
+
}
|
|
1224
|
+
stopMonitoringWorkItem = workItem
|
|
1225
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(10), execute: workItem)
|
|
1226
|
+
if !isMain {
|
|
1227
|
+
DispatchQueue.main.async { [weak self] in self?.removeArmObservers(before: generation + 1) }
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
private func finishMonitoring(generation: Int) {
|
|
1232
|
+
objc_sync_enter(self)
|
|
1233
|
+
defer { objc_sync_exit(self) }
|
|
1234
|
+
guard hub.coordinator.isCurrent(name: name, generation: generation) else { return }
|
|
1235
|
+
|
|
917
1236
|
stopMonitoringWorkItem = nil
|
|
1237
|
+
hub.coordinator.finishMonitoring(name: name, generation: generation)
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/// A view of this named instance's runtime started. Only a launch in the
|
|
1241
|
+
/// foreground may leave an unfinished launch for the next process to roll
|
|
1242
|
+
/// back, as for main. `generation` is the launch the view started in.
|
|
1243
|
+
/// Main thread only.
|
|
1244
|
+
func viewStarted(_ view: AnyObject, inForeground: Bool, generation: Int, runtimeBundleURL: URL?) {
|
|
1245
|
+
guard !isMain else { return }
|
|
1246
|
+
// A view of a runtime of an earlier launch, such as one torn down
|
|
1247
|
+
// while its host is recreated, starts nothing.
|
|
1248
|
+
guard hub.coordinator.isCurrentRuntime(name: name, runtimeBundleURL: runtimeBundleURL) else { return }
|
|
1249
|
+
switch HotUpdaterBackgroundViewStarts.viewStart(inForeground: inForeground, isShown: { hotUpdaterIsViewShown(view) }) {
|
|
1250
|
+
case .arm:
|
|
1251
|
+
removeArmObservers(before: generation + 1)
|
|
1252
|
+
hub.coordinator.arm(name: name, generation: generation, runtimeBundleURL: runtimeBundleURL)
|
|
1253
|
+
return
|
|
1254
|
+
case .waitForAttachment:
|
|
1255
|
+
// Started for pre-rendering: its attachment to a window is
|
|
1256
|
+
// reported as another start.
|
|
1257
|
+
return
|
|
1258
|
+
case .waitForForeground:
|
|
1259
|
+
break
|
|
1260
|
+
}
|
|
1261
|
+
// Views of an earlier launch never start this one, nor remove what
|
|
1262
|
+
// a later launch installed.
|
|
1263
|
+
if let armGeneration {
|
|
1264
|
+
if generation < armGeneration {
|
|
1265
|
+
return
|
|
1266
|
+
}
|
|
1267
|
+
removeArmObservers(before: generation)
|
|
1268
|
+
}
|
|
1269
|
+
// Only if the view is still shown when the app comes back.
|
|
1270
|
+
backgroundViewStarts.add(view)
|
|
1271
|
+
guard armObservers.isEmpty else { return }
|
|
1272
|
+
armGeneration = generation
|
|
1273
|
+
armObservers = [
|
|
1274
|
+
UIApplication.willEnterForegroundNotification,
|
|
1275
|
+
UIApplication.didBecomeActiveNotification,
|
|
1276
|
+
].map { notificationName in
|
|
1277
|
+
NotificationCenter.default.addObserver(forName: notificationName, object: nil, queue: .main) { [weak self] _ in
|
|
1278
|
+
guard let self else { return }
|
|
1279
|
+
// willEnterForeground may come before the view is in its
|
|
1280
|
+
// window; didBecomeActive decides.
|
|
1281
|
+
let final = notificationName == UIApplication.didBecomeActiveNotification
|
|
1282
|
+
let isShown = self.backgroundViewStarts.takeAnyShown({ hotUpdaterIsViewShown($0) }, final: final)
|
|
1283
|
+
guard isShown || final else { return }
|
|
1284
|
+
self.removeArmObservers(before: generation + 1)
|
|
1285
|
+
if isShown {
|
|
1286
|
+
self.hub.coordinator.arm(name: self.name, generation: generation, runtimeBundleURL: runtimeBundleURL)
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/// Drops the foreground observers if they were for a launch before
|
|
1293
|
+
/// `generation`. Main thread only.
|
|
1294
|
+
private func removeArmObservers(before generation: Int) {
|
|
1295
|
+
guard let armGeneration, armGeneration < generation else { return }
|
|
1296
|
+
armObservers.forEach { NotificationCenter.default.removeObserver($0) }
|
|
1297
|
+
armObservers = []
|
|
1298
|
+
self.armGeneration = nil
|
|
1299
|
+
backgroundViewStarts.removeAll()
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/// Main only: its fatal before first content reloads it in-process with
|
|
1303
|
+
/// the bundle its marker rolls back to.
|
|
1304
|
+
func requestRecoveryReloadIfNeeded(onFailure: @escaping () -> Void) -> Bool {
|
|
1305
|
+
guard isMain else { return false }
|
|
1306
|
+
let request = hub.coordinator.requestRecovery(name: name)
|
|
1307
|
+
if request == .notNeeded {
|
|
1308
|
+
return false
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
hotUpdaterMarkFailedRuntime()
|
|
1312
|
+
guard case .requested(let generation, let bundleId) = request else {
|
|
1313
|
+
return true
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
hotUpdaterAfterSurfaceStarts { [weak self] in
|
|
1317
|
+
guard let self else {
|
|
1318
|
+
return
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
guard self.hub.coordinator.isCurrent(name: self.name, generation: generation) else { return }
|
|
1322
|
+
|
|
1323
|
+
let started = hotUpdaterPerformRecoveryReload()
|
|
1324
|
+
if !started {
|
|
1325
|
+
self.hub.coordinator.recoveryReloadFailed(name: self.name, generation: generation)
|
|
1326
|
+
NSLog("[HotUpdaterRecovery] Failed to trigger recovery reload")
|
|
1327
|
+
onFailure()
|
|
1328
|
+
} else {
|
|
1329
|
+
NSLog("[HotUpdaterRecovery] Triggered recovery reload for bundleId=\(bundleId ?? "nil")")
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
return true
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/// HotUpdater.mm's signal-safe crash slots.
|
|
1338
|
+
private final class HotUpdaterSignalCrashSlots: HotUpdaterCrashSlotSink {
|
|
1339
|
+
func updateSlot(_ slot: Int, markerPath: String, bundleId: String?, shouldRollback: Bool) {
|
|
1340
|
+
hotUpdaterUpdateCrashSlotSymbol(slot, markerPath as NSString, bundleId as NSString?, ObjCBool(shouldRollback))
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
func attributeCrash(toSlot slot: Int) {
|
|
1344
|
+
hotUpdaterAttributeCrashToSlot(slot)
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/// The process-wide side of crash recovery. React Native's fatal handlers,
|
|
1349
|
+
/// the uncaught exception handler, the signal handler, and the content and
|
|
1350
|
+
/// load-failure observers exist once per process; each event goes to the
|
|
1351
|
+
/// instance whose runtime it comes from, found through the runtime's
|
|
1352
|
+
/// TurboModule (see HotUpdater.mm). An event no runtime claims is main's, as
|
|
1353
|
+
/// it always was, except that a crash then also counts against every named
|
|
1354
|
+
/// launch that is unverified.
|
|
1355
|
+
@objcMembers
|
|
1356
|
+
final class HotUpdaterRecoveryHub: NSObject {
|
|
1357
|
+
static let shared = HotUpdaterRecoveryHub()
|
|
1358
|
+
|
|
1359
|
+
let coordinator = HotUpdaterRecoveryCoordinator(sink: HotUpdaterSignalCrashSlots())
|
|
1360
|
+
/// The launch each runtime runs, by its module.
|
|
1361
|
+
private let runtimeLaunches = HotUpdaterRuntimeLaunches()
|
|
1362
|
+
|
|
1363
|
+
override init() {
|
|
1364
|
+
super.init()
|
|
1365
|
+
coordinator.onRollbackCharged = { [weak self] name in
|
|
1366
|
+
self?.manager(named: name)?.onRollbackCharged?()
|
|
1367
|
+
}
|
|
1368
|
+
// Off the main queue, which a blocked fatal handler may hold.
|
|
1369
|
+
coordinator.onClaimStarted = { [weak self] in
|
|
1370
|
+
DispatchQueue.global(qos: .utility).asyncAfter(
|
|
1371
|
+
deadline: .now() + HotUpdaterRecoveryCoordinator.claimFailSafe + 0.1
|
|
1372
|
+
) {
|
|
1373
|
+
self?.coordinator.releaseStaleClaims()
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
private let pendingSurfaceStarts = HotUpdaterPendingSurfaceStarts()
|
|
1378
|
+
private let managersLock = NSLock()
|
|
1379
|
+
private var managers: [String: HotUpdaterRecoveryManager] = [:]
|
|
1380
|
+
|
|
1381
|
+
private var previousFatalHandler: RCTFatalHandler?
|
|
1382
|
+
private var previousFatalExceptionHandler: RCTFatalExceptionHandler?
|
|
1383
|
+
private var previousUncaughtExceptionHandler: (@convention(c) (NSException) -> Void)?
|
|
1384
|
+
private var isInstalled = false
|
|
1385
|
+
|
|
1386
|
+
private static let surfaceWillStartNotification = Notification.Name("HotUpdaterSurfaceWillStart")
|
|
1387
|
+
private static let runtimeDidResolveNotification = Notification.Name("HotUpdaterRuntimeDidResolve")
|
|
1388
|
+
/// The bridgeless runtime's JavaScript thread, where RCTFatal reports its
|
|
1389
|
+
/// JavaScript fatals.
|
|
1390
|
+
private static let runtimeThreadName = "com.facebook.react.runtime.JavaScript"
|
|
1391
|
+
|
|
1392
|
+
func register(_ manager: HotUpdaterRecoveryManager) {
|
|
1393
|
+
managersLock.lock()
|
|
1394
|
+
managers[manager.name] = manager
|
|
1395
|
+
managersLock.unlock()
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
private func manager(named name: String) -> HotUpdaterRecoveryManager? {
|
|
1399
|
+
managersLock.lock()
|
|
1400
|
+
defer { managersLock.unlock() }
|
|
1401
|
+
return managers[name]
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
private var mainName: String { HotUpdaterInstanceEnvironment.mainName }
|
|
918
1405
|
|
|
919
|
-
|
|
1406
|
+
/// Once per process, at the first launch of any instance: a process that
|
|
1407
|
+
/// runs only named instances never launches main.
|
|
1408
|
+
func installIfNeeded() {
|
|
1409
|
+
objc_sync_enter(self)
|
|
1410
|
+
defer { objc_sync_exit(self) }
|
|
1411
|
+
guard !isInstalled else {
|
|
1412
|
+
return
|
|
1413
|
+
}
|
|
1414
|
+
isInstalled = true
|
|
1415
|
+
|
|
1416
|
+
installFatalHandlers()
|
|
920
1417
|
hotUpdaterInstallRecoveryHooks()
|
|
921
1418
|
registerObservers()
|
|
922
|
-
|
|
923
|
-
hotUpdaterUpdateSignalLaunchState(bundleId, shouldRollback: shouldRollback)
|
|
1419
|
+
hotUpdaterInstallCrashSignalHandlers()
|
|
924
1420
|
}
|
|
925
1421
|
|
|
926
1422
|
func handleUncaughtException(_ exception: NSException) {
|
|
927
|
-
|
|
1423
|
+
coordinator.chargeUncaughtException()
|
|
928
1424
|
// An uncaught native exception cannot safely continue in this process.
|
|
929
1425
|
// Preserve the marker so the next launch can select the safe bundle.
|
|
930
1426
|
previousUncaughtExceptionHandler?(exception)
|
|
931
1427
|
}
|
|
932
1428
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
1429
|
+
// MARK: - Attribution
|
|
1430
|
+
|
|
1431
|
+
private func owner(of object: AnyObject?) -> String? {
|
|
1432
|
+
guard let object else { return nil }
|
|
1433
|
+
return hotUpdaterImpl(forRootObject: object)?.environment.name
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/// The instance whose JavaScript thread is failing; a fatal right after a
|
|
1437
|
+
/// load failure already attributed belongs to the same instance. Else
|
|
1438
|
+
/// the instance whose bundle every frame of its JavaScript stack is in.
|
|
1439
|
+
/// Else, on a JavaScript thread whose runtime has no module yet, the one
|
|
1440
|
+
/// launch that can have thrown it, if there is exactly one.
|
|
1441
|
+
/// With the owner, the bundle its failing runtime runs when known, which
|
|
1442
|
+
/// tells a runtime of an earlier launch from the current one.
|
|
1443
|
+
private func fatalOwner(stack: Any?) -> (
|
|
1444
|
+
name: String,
|
|
1445
|
+
runtimeBundleURL: URL?,
|
|
1446
|
+
attribution: HotUpdaterRecoveryCoordinator.FatalAttribution
|
|
1447
|
+
)? {
|
|
1448
|
+
let module = hotUpdaterRuntimeImpl().map { ($0.environment.name, hotUpdaterRuntimeBundleURL()) }
|
|
1449
|
+
let stackOwner = HotUpdaterJSStackOwner.owner(of: stack, resolve: {
|
|
1450
|
+
HotUpdaterNativeUpdateBridge.impl(forBundleURL: $0).environment.name
|
|
1451
|
+
}).map { ($0, HotUpdaterJSStackOwner.bundleURLs(in: stack).first) }
|
|
1452
|
+
return HotUpdaterRecoveryCoordinator.fatalOwner(
|
|
1453
|
+
module: module,
|
|
1454
|
+
stack: stackOwner,
|
|
1455
|
+
claim: coordinator.attributedOwner,
|
|
1456
|
+
inference: {
|
|
1457
|
+
guard Thread.current.name == Self.runtimeThreadName else { return nil }
|
|
1458
|
+
return self.coordinator.inferredFatalOwner()
|
|
1459
|
+
}
|
|
1460
|
+
)
|
|
1461
|
+
}
|
|
937
1462
|
|
|
1463
|
+
// MARK: - Fatal errors
|
|
1464
|
+
|
|
1465
|
+
private func installFatalHandlers() {
|
|
938
1466
|
previousFatalHandler = RCTGetFatalHandler()
|
|
939
1467
|
previousFatalExceptionHandler = RCTGetFatalExceptionHandler()
|
|
940
1468
|
previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler()
|
|
941
1469
|
|
|
942
1470
|
RCTSetFatalHandler { [weak self] error in
|
|
943
1471
|
guard !hotUpdaterIsFailedRuntime().boolValue else { return }
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
}) != true {
|
|
947
|
-
self?.writeCrashMarker()
|
|
948
|
-
self?.forwardFatalError(error)
|
|
949
|
-
}
|
|
1472
|
+
let stack = (error as NSError?)?.userInfo[RCTJSStackTraceKey]
|
|
1473
|
+
self?.handleFatal(stack: stack) { self?.forwardFatalError(error) }
|
|
950
1474
|
}
|
|
951
1475
|
|
|
952
1476
|
RCTSetFatalExceptionHandler { [weak self] exception in
|
|
953
1477
|
guard !hotUpdaterIsFailedRuntime().boolValue else { return }
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
}) != true {
|
|
957
|
-
self?.writeCrashMarker()
|
|
958
|
-
self?.forwardFatalException(exception)
|
|
959
|
-
}
|
|
1478
|
+
let stack = exception?.userInfo?[RCTJSStackTraceKey]
|
|
1479
|
+
self?.handleFatal(stack: stack) { self?.forwardFatalException(exception) }
|
|
960
1480
|
}
|
|
961
1481
|
|
|
962
1482
|
NSSetUncaughtExceptionHandler(hotUpdaterUncaughtExceptionHandler)
|
|
963
|
-
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
private func handleFatal(stack: Any?, forward: @escaping () -> Void) {
|
|
1486
|
+
var attributed = fatalOwner(stack: stack)
|
|
1487
|
+
// A fatal of a module-less runtime (by stack, inference, or none)
|
|
1488
|
+
// takes a deferred load failure; one of a runtime with a module, or
|
|
1489
|
+
// of a claimed crash, leaves it to the runtime that failed.
|
|
1490
|
+
let loadFailure = coordinator.takeDeferredLoadFailure(
|
|
1491
|
+
for: attributed?.attribution ?? .none,
|
|
1492
|
+
owner: attributed?.name
|
|
1493
|
+
)
|
|
1494
|
+
var afterLoadFailure = false
|
|
1495
|
+
if attributed == nil, loadFailure != nil {
|
|
1496
|
+
afterLoadFailure = true
|
|
1497
|
+
// RCTInstance reports a load failure's fatal on the main thread.
|
|
1498
|
+
attributed = coordinator.inferredFatalOwner().map { ($0, nil, .inference) }
|
|
1499
|
+
}
|
|
1500
|
+
let owner = attributed?.name
|
|
1501
|
+
if owner == nil, let claim = coordinator.earlierRuntimeClaim {
|
|
1502
|
+
// The fatal of a runtime of an earlier launch: no launch is charged.
|
|
1503
|
+
forward()
|
|
1504
|
+
coordinator.clearAttribution(claim: claim)
|
|
1505
|
+
return
|
|
1506
|
+
}
|
|
1507
|
+
// A load failure no runtime claimed may be a named runtime's: then
|
|
1508
|
+
// main doesn't recover in-process, and the crash counts against
|
|
1509
|
+
// every unverified launch.
|
|
1510
|
+
if owner == mainName || (owner == nil && !afterLoadFailure), let main = manager(named: mainName) {
|
|
1511
|
+
// Main's fatal, or one no runtime claims: with only main, main
|
|
1512
|
+
// recovers in-process as it always has (never beside a named
|
|
1513
|
+
// instance, see HotUpdaterRecoveryCoordinator.requestRecovery).
|
|
1514
|
+
if main.requestRecoveryReloadIfNeeded(onFailure: { [weak self] in
|
|
1515
|
+
let claim = owner.flatMap { self?.coordinator.attribute(to: $0) }
|
|
1516
|
+
forward()
|
|
1517
|
+
if let claim {
|
|
1518
|
+
self?.coordinator.clearAttribution(claim: claim)
|
|
1519
|
+
}
|
|
1520
|
+
}) {
|
|
1521
|
+
if owner == nil {
|
|
1522
|
+
// Main lives on, but the fatal may have been a named
|
|
1523
|
+
// runtime's: a named launch it leaves unverified rolls back.
|
|
1524
|
+
coordinator.chargeUnverifiedNamedLaunches()
|
|
1525
|
+
}
|
|
1526
|
+
return
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if owner == nil, afterLoadFailure {
|
|
1530
|
+
let claim = coordinator.chargeUnclaimedLoadFailure(loadFailure)
|
|
1531
|
+
forward()
|
|
1532
|
+
coordinator.clearAttribution(claim: claim)
|
|
1533
|
+
return
|
|
1534
|
+
}
|
|
1535
|
+
// A named instance never reloads in-process: it applies updates and
|
|
1536
|
+
// rollbacks at its next start, and main's host must not reload for
|
|
1537
|
+
// another instance's fatal. React Native's default crashes the
|
|
1538
|
+
// process, and the crash is this instance's alone. The claim is
|
|
1539
|
+
// released only once the forwarded handler returns.
|
|
1540
|
+
// By its runtime, the launch it was recorded for, when the module told
|
|
1541
|
+
// it apart.
|
|
1542
|
+
let runtimeGeneration = attributed?.attribution == .runtimeModule
|
|
1543
|
+
? runtimeLaunches.generation(of: hotUpdaterRuntimeIdentity())
|
|
1544
|
+
: nil
|
|
1545
|
+
let claim = coordinator.chargeFatal(
|
|
1546
|
+
owner: owner,
|
|
1547
|
+
runtimeBundleURL: attributed?.runtimeBundleURL,
|
|
1548
|
+
runtimeGeneration: runtimeGeneration
|
|
1549
|
+
)
|
|
1550
|
+
forward()
|
|
1551
|
+
if let claim {
|
|
1552
|
+
coordinator.clearAttribution(claim: claim)
|
|
1553
|
+
}
|
|
964
1554
|
}
|
|
965
1555
|
|
|
966
1556
|
private func forwardFatalError(_ error: Error?) {
|
|
@@ -990,22 +1580,9 @@ final class HotUpdaterRecoveryManager: NSObject {
|
|
|
990
1580
|
}
|
|
991
1581
|
}
|
|
992
1582
|
|
|
993
|
-
|
|
994
|
-
guard !signalHandlersInstalled else {
|
|
995
|
-
return
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
try? FileManager.default.createDirectory(
|
|
999
|
-
at: crashMarkerURL.deletingLastPathComponent(),
|
|
1000
|
-
withIntermediateDirectories: true,
|
|
1001
|
-
attributes: nil
|
|
1002
|
-
)
|
|
1003
|
-
hotUpdaterInstallSignalHandlers(crashMarkerURL.path)
|
|
1004
|
-
signalHandlersInstalled = true
|
|
1005
|
-
}
|
|
1583
|
+
// MARK: - Observers
|
|
1006
1584
|
|
|
1007
1585
|
private func registerObservers() {
|
|
1008
|
-
unregisterObservers()
|
|
1009
1586
|
NotificationCenter.default.addObserver(
|
|
1010
1587
|
self,
|
|
1011
1588
|
selector: #selector(handleJavaScriptDidFailToLoad(_:)),
|
|
@@ -1014,21 +1591,20 @@ final class HotUpdaterRecoveryManager: NSObject {
|
|
|
1014
1591
|
)
|
|
1015
1592
|
NotificationCenter.default.addObserver(
|
|
1016
1593
|
self,
|
|
1017
|
-
selector: #selector(handleContentDidAppear),
|
|
1594
|
+
selector: #selector(handleContentDidAppear(_:)),
|
|
1018
1595
|
name: NSNotification.Name.RCTContentDidAppear,
|
|
1019
1596
|
object: nil
|
|
1020
1597
|
)
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
private func unregisterObservers() {
|
|
1024
|
-
NotificationCenter.default.removeObserver(
|
|
1598
|
+
NotificationCenter.default.addObserver(
|
|
1025
1599
|
self,
|
|
1026
|
-
|
|
1600
|
+
selector: #selector(handleSurfaceWillStart(_:)),
|
|
1601
|
+
name: Self.surfaceWillStartNotification,
|
|
1027
1602
|
object: nil
|
|
1028
1603
|
)
|
|
1029
|
-
NotificationCenter.default.
|
|
1604
|
+
NotificationCenter.default.addObserver(
|
|
1030
1605
|
self,
|
|
1031
|
-
|
|
1606
|
+
selector: #selector(handleRuntimeDidResolve(_:)),
|
|
1607
|
+
name: Self.runtimeDidResolveNotification,
|
|
1032
1608
|
object: nil
|
|
1033
1609
|
)
|
|
1034
1610
|
}
|
|
@@ -1039,115 +1615,218 @@ final class HotUpdaterRecoveryManager: NSObject {
|
|
|
1039
1615
|
code: 1,
|
|
1040
1616
|
userInfo: [NSLocalizedDescriptionKey: "JavaScript bundle failed to load"]
|
|
1041
1617
|
)
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1618
|
+
// Old architecture: the bridge, whose bundle URL names the instance.
|
|
1619
|
+
// Bridgeless: the RCTInstance, whose runtime has no module when its
|
|
1620
|
+
// bundle didn't load, so it is unattributed and main's, as before.
|
|
1621
|
+
let owner = self.owner(of: notification.object as AnyObject?)
|
|
1622
|
+
?? self.owner(of: notification.userInfo?["bridge"] as AnyObject?)
|
|
1623
|
+
|
|
1624
|
+
// A named runtime that has no module yet may have failed: leave it to
|
|
1625
|
+
// the fatal RCTInstance reports next, which also reads its stack.
|
|
1626
|
+
if owner == nil, coordinator.deferUnclaimedLoadFailure() {
|
|
1045
1627
|
return
|
|
1046
1628
|
}
|
|
1047
|
-
unregisterObservers()
|
|
1048
|
-
}
|
|
1049
1629
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1630
|
+
if let owner, owner != mainName {
|
|
1631
|
+
let runtimeBundleURL = (notification.object as AnyObject?).flatMap(hotUpdaterBundleURL(forRootObject:))
|
|
1632
|
+
?? (notification.userInfo?["bridge"] as AnyObject?).flatMap(hotUpdaterBundleURL(forRootObject:))
|
|
1633
|
+
let runtime = (notification.object as AnyObject?).flatMap(hotUpdaterRuntimeIdentity(forRootObject:))
|
|
1634
|
+
?? (notification.userInfo?["bridge"] as AnyObject?).flatMap(hotUpdaterRuntimeIdentity(forRootObject:))
|
|
1635
|
+
let runtimeGeneration = runtimeLaunches.generation(of: runtime)
|
|
1636
|
+
// A runtime of an earlier launch fails, not the current one: the
|
|
1637
|
+
// fatal that follows charges no launch.
|
|
1638
|
+
guard coordinator.isCurrentRuntime(
|
|
1639
|
+
name: owner,
|
|
1640
|
+
runtimeBundleURL: runtimeBundleURL,
|
|
1641
|
+
runtimeGeneration: runtimeGeneration
|
|
1642
|
+
) else {
|
|
1643
|
+
chargeLoadFailure(owner: owner, runtimeBundleURL: runtimeBundleURL, runtimeGeneration: runtimeGeneration)
|
|
1644
|
+
return
|
|
1645
|
+
}
|
|
1646
|
+
guard let generation = coordinator.awaitingContentGeneration(name: owner) else { return }
|
|
1647
|
+
// React Native's fatal follows and crashes the process: charge
|
|
1648
|
+
// this instance alone, so that fatal is attributed to it too.
|
|
1649
|
+
chargeLoadFailure(owner: owner, runtimeBundleURL: runtimeBundleURL, runtimeGeneration: runtimeGeneration)
|
|
1650
|
+
coordinator.stopAwaitingContent(name: owner, generation: generation)
|
|
1651
|
+
releaseHeldContent()
|
|
1055
1652
|
return
|
|
1056
1653
|
}
|
|
1057
1654
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
hotUpdaterUpdateSignalLaunchState(currentBundleId, shouldRollback: false)
|
|
1062
|
-
|
|
1063
|
-
stopMonitoringWorkItem?.cancel()
|
|
1064
|
-
let generation = monitoringGeneration
|
|
1065
|
-
let workItem = DispatchWorkItem { [weak self] in
|
|
1066
|
-
self?.finishMonitoring(generation: generation)
|
|
1655
|
+
guard let main = manager(named: mainName),
|
|
1656
|
+
let generation = coordinator.awaitingContentGeneration(name: mainName) else {
|
|
1657
|
+
return
|
|
1067
1658
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1659
|
+
if main.requestRecoveryReloadIfNeeded(onFailure: { [weak self] in
|
|
1660
|
+
self?.forwardFatalError(error)
|
|
1661
|
+
}) {
|
|
1662
|
+
return
|
|
1663
|
+
}
|
|
1664
|
+
if coordinator.hasNamedInstance {
|
|
1665
|
+
// No in-process reload beside named instances: the fatal React
|
|
1666
|
+
// Native reports next is main's, and main rolls back at its next
|
|
1667
|
+
// start.
|
|
1668
|
+
chargeLoadFailure(owner: mainName, runtimeBundleURL: nil, runtimeGeneration: nil)
|
|
1669
|
+
}
|
|
1670
|
+
coordinator.stopAwaitingContent(name: mainName, generation: generation)
|
|
1070
1671
|
}
|
|
1071
1672
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1673
|
+
/// Charges a load failure; its claim is the one the fatal React Native
|
|
1674
|
+
/// reports next takes over and releases. Without that fatal, the claim
|
|
1675
|
+
/// ends when the instance launches again or once the bound passes.
|
|
1676
|
+
private func chargeLoadFailure(owner: String, runtimeBundleURL: URL?, runtimeGeneration: Int?) {
|
|
1677
|
+
coordinator.chargeLoadFailure(
|
|
1678
|
+
owner: owner,
|
|
1679
|
+
runtimeBundleURL: runtimeBundleURL,
|
|
1680
|
+
runtimeGeneration: runtimeGeneration
|
|
1681
|
+
)
|
|
1682
|
+
DispatchQueue.main.asyncAfter(
|
|
1683
|
+
deadline: .now() + HotUpdaterRecoveryCoordinator.loadFailureClaimBound + 0.1
|
|
1684
|
+
) { [weak self] in
|
|
1685
|
+
self?.coordinator.releaseExpiredLoadFailureClaims()
|
|
1686
|
+
}
|
|
1084
1687
|
}
|
|
1085
1688
|
|
|
1086
|
-
private func
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1689
|
+
@objc private func handleContentDidAppear(_ notification: Notification) {
|
|
1690
|
+
let view = notification.object as AnyObject?
|
|
1691
|
+
let owner = owner(of: view)
|
|
1692
|
+
// A view no runtime claims is main's, as it always was, unless a
|
|
1693
|
+
// named runtime that has no module yet may have shown it.
|
|
1694
|
+
if owner == nil, let view, coordinator.holdUnclaimedContent(view) {
|
|
1695
|
+
// Main's at the latest once the bound passes.
|
|
1696
|
+
DispatchQueue.main.asyncAfter(
|
|
1697
|
+
deadline: .now() + HotUpdaterRecoveryCoordinator.heldContentBound + 0.1
|
|
1698
|
+
) { [weak self] in
|
|
1699
|
+
self?.releaseHeldContent()
|
|
1700
|
+
}
|
|
1701
|
+
return
|
|
1092
1702
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1703
|
+
// Content of a runtime of an earlier launch verifies nothing, and
|
|
1704
|
+
// is not main's either.
|
|
1705
|
+
let runtimeBundleURL = owner == nil ? nil : view.flatMap(hotUpdaterBundleURL(forRootObject:))
|
|
1706
|
+
// The launch the view's runtime runs: content of a runtime torn down
|
|
1707
|
+
// after its host started a replacement with the same bundle verifies
|
|
1708
|
+
// nothing. Unknown (its runtime not told apart yet): by name and
|
|
1709
|
+
// bundle alone, as before.
|
|
1710
|
+
let runtimeGeneration = owner == nil
|
|
1711
|
+
? nil
|
|
1712
|
+
: runtimeLaunches.generation(of: view.flatMap(hotUpdaterRuntimeIdentity(forRootObject:)))
|
|
1713
|
+
guard let verified = coordinator.contentAppeared(
|
|
1714
|
+
owner: owner,
|
|
1715
|
+
runtimeBundleURL: runtimeBundleURL,
|
|
1716
|
+
runtimeGeneration: runtimeGeneration
|
|
1717
|
+
) else {
|
|
1718
|
+
return
|
|
1097
1719
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
hotUpdaterAfterSurfaceStarts { [weak self] in
|
|
1104
|
-
guard let self else {
|
|
1105
|
-
return
|
|
1106
|
-
}
|
|
1720
|
+
manager(named: verified.launch.name)?.contentAppeared(generation: verified.generation)
|
|
1721
|
+
if verified.launch.name != mainName {
|
|
1722
|
+
releaseHeldContent()
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1107
1725
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1726
|
+
/// Credits the content held for runtimes that had no module yet. Main
|
|
1727
|
+
/// thread only.
|
|
1728
|
+
private func releaseHeldContent() {
|
|
1729
|
+
for verified in coordinator.releaseHeldContent(
|
|
1730
|
+
owner: { self.owner(of: $0) },
|
|
1731
|
+
runtimeBundleURL: { hotUpdaterBundleURL(forRootObject: $0) },
|
|
1732
|
+
runtimeGeneration: { self.runtimeLaunches.generation(of: hotUpdaterRuntimeIdentity(forRootObject: $0)) }
|
|
1733
|
+
) {
|
|
1734
|
+
manager(named: verified.launch.name)?.contentAppeared(generation: verified.generation)
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1112
1737
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1738
|
+
/// Posted by HotUpdaterRecoveryHooks.mm on the thread that shows a view:
|
|
1739
|
+
/// once a Fabric surface started and whenever its view moves into a
|
|
1740
|
+
/// window, or, in the old architecture, when an RCTRootView moves into a
|
|
1741
|
+
/// window. The launch starts only once a view is shown.
|
|
1742
|
+
@objc private func handleSurfaceWillStart(_ notification: Notification) {
|
|
1743
|
+
guard let surface = notification.object as AnyObject? else { return }
|
|
1744
|
+
// The launches the view started in, read now on the thread that
|
|
1745
|
+
// starts it: by the time the main queue runs `handle`, its host may
|
|
1746
|
+
// have started a replacement launch with the same bundle.
|
|
1747
|
+
let generations = coordinator.generations()
|
|
1748
|
+
let handle = { [weak self] in
|
|
1749
|
+
guard let self else { return }
|
|
1750
|
+
let inForeground = UIApplication.shared.applicationState != .background
|
|
1751
|
+
// Kept from its first start for its later ones.
|
|
1752
|
+
let firstGenerations = self.runtimeLaunches.generationsOfFirstStart(surface, current: generations)
|
|
1753
|
+
if let owner = self.owner(of: surface) {
|
|
1754
|
+
self.viewStarted(
|
|
1755
|
+
owner: owner,
|
|
1756
|
+
view: surface,
|
|
1757
|
+
inForeground: inForeground,
|
|
1758
|
+
generation: self.launchOfView(surface, owner: owner, firstGenerations: firstGenerations)
|
|
1759
|
+
)
|
|
1120
1760
|
} else {
|
|
1121
|
-
|
|
1761
|
+
// The surface starts before its bundle runs, so before the
|
|
1762
|
+
// runtime's module that tells whose it is.
|
|
1763
|
+
self.pendingSurfaceStarts.add(surface, inForeground: inForeground, generations: generations)
|
|
1122
1764
|
}
|
|
1123
1765
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1766
|
+
// UIApplication is read on the main thread.
|
|
1767
|
+
if Thread.isMainThread {
|
|
1768
|
+
handle()
|
|
1769
|
+
} else {
|
|
1770
|
+
DispatchQueue.main.async(execute: handle)
|
|
1771
|
+
}
|
|
1126
1772
|
}
|
|
1127
1773
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1774
|
+
/// Posted by HotUpdater.mm when a runtime's module knows its instance.
|
|
1775
|
+
@objc private func handleRuntimeDidResolve(_ notification: Notification) {
|
|
1776
|
+
if let impl = notification.userInfo?["impl"] as? HotUpdaterImpl {
|
|
1777
|
+
let name = impl.environment.name
|
|
1778
|
+
let runtimeBundleURL = notification.userInfo?["bundleURL"] as? URL
|
|
1779
|
+
coordinator.runtimeResolved(name: name, runtimeBundleURL: runtimeBundleURL)
|
|
1780
|
+
// The launch this runtime runs: the current one, when its bundle
|
|
1781
|
+
// is that launch's. A runtime of an earlier launch gets none.
|
|
1782
|
+
// Keyed by the runtime's identity: its module, or in the old
|
|
1783
|
+
// architecture its bridge, which root views also reach.
|
|
1784
|
+
if let runtime = (notification.userInfo?["runtime"] ?? notification.object) as AnyObject?,
|
|
1785
|
+
coordinator.isCurrentRuntime(name: name, runtimeBundleURL: runtimeBundleURL),
|
|
1786
|
+
let generation = coordinator.generations()[name] {
|
|
1787
|
+
runtimeLaunches.record(runtime: runtime, generation: generation)
|
|
1788
|
+
}
|
|
1134
1789
|
}
|
|
1790
|
+
DispatchQueue.main.async { [weak self] in
|
|
1791
|
+
guard let self else { return }
|
|
1792
|
+
// Content first: a launch whose content already appeared is
|
|
1793
|
+
// verified, and a surface start then no longer starts it.
|
|
1794
|
+
self.releaseHeldContent()
|
|
1795
|
+
// Decided by the app's state now, and whether the surface is
|
|
1796
|
+
// shown now: the app may have come to the foreground, and the
|
|
1797
|
+
// surface may have been attached, since it started.
|
|
1798
|
+
let inForegroundNow = UIApplication.shared.applicationState != .background
|
|
1799
|
+
for started in self.pendingSurfaceStarts.resolve({ self.owner(of: $0) }) {
|
|
1800
|
+
self.viewStarted(
|
|
1801
|
+
owner: started.name,
|
|
1802
|
+
view: started.surface,
|
|
1803
|
+
inForeground: inForegroundNow,
|
|
1804
|
+
generation: self.launchOfView(
|
|
1805
|
+
started.surface,
|
|
1806
|
+
owner: started.name,
|
|
1807
|
+
firstGenerations: self.runtimeLaunches.generationsOfFirstStart(started.surface, current: [:])
|
|
1808
|
+
) ?? started.generation
|
|
1809
|
+
)
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1135
1813
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1814
|
+
/// The launch a view started for: the one its runtime was recorded for,
|
|
1815
|
+
/// else the one current at its first start, so its later attachment
|
|
1816
|
+
/// after a replacement launch began starts nothing. Main thread only.
|
|
1817
|
+
private func launchOfView(_ view: AnyObject, owner: String, firstGenerations: [String: Int]) -> Int? {
|
|
1818
|
+
runtimeLaunches.generation(of: hotUpdaterRuntimeIdentity(forRootObject: view)) ?? firstGenerations[owner]
|
|
1819
|
+
}
|
|
1142
1820
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1821
|
+
private func viewStarted(owner: String, view: AnyObject, inForeground: Bool, generation: Int?) {
|
|
1822
|
+
// Main's launch starts when it is selected, as it always has; a view
|
|
1823
|
+
// that started before its instance launched starts nothing.
|
|
1824
|
+
guard owner != mainName, let generation else { return }
|
|
1825
|
+
manager(named: owner)?.viewStarted(
|
|
1826
|
+
view,
|
|
1827
|
+
inForeground: inForeground,
|
|
1828
|
+
generation: generation,
|
|
1829
|
+
runtimeBundleURL: hotUpdaterBundleURL(forRootObject: view)
|
|
1830
|
+
)
|
|
1152
1831
|
}
|
|
1153
1832
|
}
|