@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.
Files changed (145) hide show
  1. package/HotUpdater.podspec +14 -2
  2. package/Package.swift +24 -3
  3. package/android/build.gradle +2 -6
  4. package/android/hot-updater-fingerprint.gradle +135 -0
  5. package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
  6. package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
  7. package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
  8. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
  9. package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
  10. package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
  11. package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
  12. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
  13. package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
  14. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
  15. package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
  16. package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
  17. package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
  18. package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
  19. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +493 -0
  20. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
  21. package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
  22. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
  23. package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
  24. package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
  25. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
  26. package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
  27. package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
  28. package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
  29. package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
  30. package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
  31. package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
  32. package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
  33. package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
  34. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
  35. package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
  36. package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
  37. package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
  38. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
  39. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
  40. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
  41. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
  42. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
  43. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
  44. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
  45. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
  46. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
  47. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
  48. package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
  49. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
  50. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
  51. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
  52. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
  53. package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
  54. package/android/src/newarch/HotUpdater.kt +53 -15
  55. package/android/src/newarch/HotUpdaterModule.kt +42 -17
  56. package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
  57. package/android/src/oldarch/HotUpdater.kt +51 -12
  58. package/android/src/oldarch/HotUpdaterModule.kt +43 -17
  59. package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
  60. package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
  61. package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
  62. package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
  63. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
  64. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
  65. package/ios/HotUpdater/Internal/CohortService.swift +8 -0
  66. package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
  67. package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
  68. package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
  69. package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
  70. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
  71. package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
  72. package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
  73. package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
  74. package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
  75. package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
  76. package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
  77. package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
  78. package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
  79. package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
  80. package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
  81. package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
  82. package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
  83. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
  84. package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
  85. package/ios/HotUpdater/Package.swift +11 -0
  86. package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
  87. package/ios/HotUpdater/Public/HotUpdater.h +21 -2
  88. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
  89. package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
  90. package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
  91. package/lib/commonjs/checkForUpdate.js +3 -1
  92. package/lib/commonjs/checkForUpdate.js.map +1 -1
  93. package/lib/commonjs/index.js +9 -12
  94. package/lib/commonjs/index.js.map +1 -1
  95. package/lib/commonjs/native.js +24 -79
  96. package/lib/commonjs/native.js.map +1 -1
  97. package/lib/commonjs/nativeUpdateEvents.js +66 -0
  98. package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
  99. package/lib/commonjs/sdkVersion.js +1 -1
  100. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  101. package/lib/module/checkForUpdate.js +3 -1
  102. package/lib/module/checkForUpdate.js.map +1 -1
  103. package/lib/module/index.js +10 -13
  104. package/lib/module/index.js.map +1 -1
  105. package/lib/module/native.js +19 -73
  106. package/lib/module/native.js.map +1 -1
  107. package/lib/module/nativeUpdateEvents.js +61 -0
  108. package/lib/module/nativeUpdateEvents.js.map +1 -0
  109. package/lib/module/sdkVersion.js +1 -1
  110. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  111. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/index.d.ts +4 -14
  113. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/native.d.ts +9 -46
  115. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
  117. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  119. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
  120. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  121. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  122. package/lib/typescript/module/index.d.ts +4 -14
  123. package/lib/typescript/module/index.d.ts.map +1 -1
  124. package/lib/typescript/module/native.d.ts +9 -46
  125. package/lib/typescript/module/native.d.ts.map +1 -1
  126. package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
  127. package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
  128. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  129. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
  130. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  131. package/package.json +4 -4
  132. package/src/checkForUpdate.ts +7 -1
  133. package/src/index.ts +9 -17
  134. package/src/native.ts +19 -91
  135. package/src/nativeUpdateEvents.ts +77 -0
  136. package/src/sdkVersion.ts +1 -1
  137. package/src/specs/NativeHotUpdater.ts +20 -3
  138. package/android/libs/README.md +0 -50
  139. package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
  140. package/android/src/bridge/BridgeReactInstance.kt +0 -38
  141. package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
  142. package/android/src/main/java/com/hotupdater/ContentAppearedLog.kt +0 -31
  143. package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
  144. package/android/src/newarch/ReactIntegrationManager.kt +0 -212
  145. package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
@@ -0,0 +1,1553 @@
1
+ import Foundation
2
+
3
+ /// The file a launch leaves when its process dies before first content. Each
4
+ /// instance keeps its own in its own bundle store, so only that instance's
5
+ /// next start reads it, rolls back the bundle it names, and records the crash
6
+ /// in its own history. The file's location says whose it is.
7
+ enum HotUpdaterCrashMarker {
8
+ static let filename = "recovery-crash-marker.json"
9
+
10
+ /// `<documents>/bundle-store/recovery-crash-marker.json` for main,
11
+ /// `<documents>/hot-updater-instances/<name>/bundle-store/...` otherwise.
12
+ static func url(for environment: HotUpdaterInstanceEnvironment, documentsPath: String) -> URL {
13
+ URL(fileURLWithPath: environment.bundleStorePath(documentsPath: documentsPath), isDirectory: true)
14
+ .appendingPathComponent(filename)
15
+ }
16
+
17
+ /// Reads and deletes the marker; nil when there is none.
18
+ static func consume(at url: URL) -> PendingCrashRecovery? {
19
+ guard FileManager.default.fileExists(atPath: url.path) else {
20
+ return nil
21
+ }
22
+
23
+ defer {
24
+ try? FileManager.default.removeItem(at: url)
25
+ }
26
+
27
+ do {
28
+ let data = try Data(contentsOf: url)
29
+ let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:]
30
+ return PendingCrashRecovery.from(json: json)
31
+ } catch {
32
+ NSLog("[HotUpdaterRecovery] Failed to read crash marker: \(error)")
33
+ return nil
34
+ }
35
+ }
36
+
37
+ static func write(bundleId: String?, shouldRollback: Bool, to url: URL) {
38
+ do {
39
+ try FileManager.default.createDirectory(
40
+ at: url.deletingLastPathComponent(),
41
+ withIntermediateDirectories: true,
42
+ attributes: nil
43
+ )
44
+
45
+ let payload: [String: Any] = [
46
+ "bundleId": bundleId ?? NSNull(),
47
+ "shouldRollback": shouldRollback,
48
+ ]
49
+ let data = try JSONSerialization.data(withJSONObject: payload)
50
+ try data.write(to: url, options: .atomic)
51
+ } catch {
52
+ NSLog("[HotUpdaterRecovery] Failed to write crash marker: \(error)")
53
+ }
54
+ }
55
+ }
56
+
57
+ /// Where crash recovery mirrors its slots for the native signal handler,
58
+ /// which can't call into Swift: HotUpdater.mm keeps a fixed array of C slots
59
+ /// it writes markers from, and this keeps them in step.
60
+ protocol HotUpdaterCrashSlotSink: AnyObject {
61
+ /// Slot `slot` writes `{"bundleId":…,"shouldRollback":true}` to
62
+ /// `markerPath` on a native crash while `shouldRollback` is set.
63
+ func updateSlot(_ slot: Int, markerPath: String, bundleId: String?, shouldRollback: Bool)
64
+ /// `HotUpdaterRecoveryCoordinator.unattributed`, a slot, or
65
+ /// `attributedWithoutSlot`: which slots a native crash may write.
66
+ func attributeCrash(toSlot slot: Int)
67
+ }
68
+
69
+ /// Crash recovery of every instance in the process. The handlers that see a
70
+ /// crash are process-wide (one signal handler, one uncaught exception handler,
71
+ /// one React Native fatal handler), so they ask this which instances a crash
72
+ /// counts against:
73
+ ///
74
+ /// - A JavaScript fatal belongs to the instance whose runtime threw it, and
75
+ /// charges only that one. One that can't be attributed counts as a native
76
+ /// crash.
77
+ /// - A native crash, or an uncaught exception, kills the process: it counts
78
+ /// against each instance whose launch is unverified (started, first content
79
+ /// not yet shown), and never against one whose content appeared.
80
+ /// - First content verifies only the instance whose view showed it.
81
+ ///
82
+ /// Main keeps what it always did: its launch counts as started when it is
83
+ /// selected in the foreground, and its fatal before first content reloads it
84
+ /// in-process. A named launch counts as started only once a view of its
85
+ /// runtime is shown, since a runtime warmed up without UI must not be rolled
86
+ /// back at the next start.
87
+ final class HotUpdaterRecoveryCoordinator {
88
+ /// Native crash markers the signal handler can write, main's included.
89
+ /// More instances than this still roll back an unfinished launch at their
90
+ /// next start (`launchInProgress`), but get no marker from a native crash.
91
+ static let slotCount = 8
92
+ static let mainSlot = 0
93
+ /// A native crash writes every slot whose launch is unverified.
94
+ static let unattributed = -1
95
+ /// The crash is a fatal of an instance without a slot: a native crash
96
+ /// writes none.
97
+ static let attributedWithoutSlot = -2
98
+
99
+ /// One instance's launch as crash recovery watches it.
100
+ final class Launch {
101
+ let name: String
102
+ let markerURL: URL
103
+ let storage: BundleStorageService
104
+ var isMain: Bool { name == HotUpdaterInstanceEnvironment.mainName }
105
+
106
+ fileprivate(set) var slot: Int?
107
+ fileprivate(set) var generation = 0
108
+ fileprivate(set) var bundleId: String?
109
+ /// The launched bundle is pending, and its first content hasn't appeared.
110
+ fileprivate(set) var shouldRollback = false
111
+ /// Named only: a view of its runtime was shown, so its launch started.
112
+ fileprivate(set) var isArmed = false
113
+ /// From the launch until 10 seconds after its first content.
114
+ fileprivate(set) var isMonitoring = false
115
+ /// Until first content, or a JavaScript load failure without recovery.
116
+ fileprivate(set) var isAwaitingContent = false
117
+ /// Main only: an in-process recovery reload is on its way.
118
+ fileprivate(set) var recoveryRequested = false
119
+ /// This launch's runtime has a module that tells its JavaScript
120
+ /// thread apart, so its fatals are attributed directly.
121
+ fileprivate(set) var hasResolvedRuntime = false
122
+ /// This launch's first content appeared: its runtime finished loading.
123
+ fileprivate(set) var hasShownContent = false
124
+ /// The bundle URL this launch handed out to React Native.
125
+ fileprivate(set) var launchURL: URL?
126
+ /// The bundle this launch selected, kept after its monitoring ends.
127
+ fileprivate(set) var selectedBundleId: String?
128
+
129
+ init(name: String, markerURL: URL, storage: BundleStorageService) {
130
+ self.name = name
131
+ self.markerURL = markerURL
132
+ self.storage = storage
133
+ }
134
+
135
+ /// What a native crash counts against: for main, any launch it
136
+ /// selected as pending; for a named one, only once a view started it.
137
+ fileprivate var crashCountsAgainstLaunch: Bool {
138
+ shouldRollback && (isMain || isArmed || isCharged)
139
+ }
140
+ /// Named only: a crash of its own was charged to this launch, so its
141
+ /// native slot asks for rollback even if no view of it showed.
142
+ fileprivate(set) var isCharged = false
143
+ /// The directory of `markerURL` was created.
144
+ fileprivate var hasMarkerDirectory = false
145
+ }
146
+
147
+ enum RecoveryRequest: Equatable {
148
+ case notNeeded
149
+ case alreadyRequested
150
+ case requested(generation: Int, bundleId: String?)
151
+ }
152
+
153
+ private let lock = NSRecursiveLock()
154
+ private let sink: HotUpdaterCrashSlotSink
155
+ private var launches: [String: Launch] = [:]
156
+ private var slotNames: [String?] = Array(repeating: nil, count: HotUpdaterRecoveryCoordinator.slotCount)
157
+ private var slotFlags: [Bool] = Array(repeating: false, count: HotUpdaterRecoveryCoordinator.slotCount)
158
+ private var attributedSlot = HotUpdaterRecoveryCoordinator.unattributed
159
+ private var attributedName: String?
160
+ /// The launch of `attributedName` the claim belongs to: a later launch
161
+ /// of the same instance, started before the process ends, is not it.
162
+ private var attributedGeneration: Int?
163
+ /// The launches `attribute(to:)` stopped, to restart if the process
164
+ /// survives the fatal.
165
+ private var disarmed: [(name: String, generation: Int, bundleId: String, wasStarted: Bool, wasArmed: Bool)] = []
166
+ /// The latest claim: a fatal of another instance can arrive before an
167
+ /// earlier one's handler returns, and only the latest releases them all.
168
+ private var claimGeneration = 0
169
+ /// Every claim whose fatal handler hasn't returned, oldest first. The
170
+ /// latest is the one published: a crash now is that fatal's.
171
+ private var activeClaims: [Claim] = []
172
+
173
+ private struct Claim {
174
+ let id: Int
175
+ let name: String?
176
+ let generation: Int?
177
+ var slot: Int
178
+ let isEarlierRuntime: Bool
179
+ /// A load failure's claim, waiting since then for the fatal React
180
+ /// Native reports next, which takes it over.
181
+ var awaitingFatalSince: TimeInterval?
182
+ /// When the claim was made, for the fail-safe.
183
+ var startedAt: TimeInterval = 0
184
+ /// An earlier runtime's claim: the instance it belonged to and the
185
+ /// bundle it ran, which tell its follow-up fatal apart.
186
+ var earlierRuntimeOwner: String?
187
+ var earlierRuntimeURLKey: String?
188
+ }
189
+
190
+ /// How long a claim may stay active. A fatal handler that blocks without
191
+ /// ending the process (a custom one) would otherwise keep every other
192
+ /// launch stopped and every later crash attributed to it.
193
+ static let claimFailSafe: TimeInterval = 60
194
+ /// Called when a claim is made, so the host schedules
195
+ /// `releaseStaleClaims()` after `claimFailSafe`; called under the lock,
196
+ /// so it must only schedule. Set once, before any launch.
197
+ var onClaimStarted: (() -> Void)?
198
+
199
+ /// How long a load failure's claim waits for its fatal. React Native
200
+ /// reports it right after the load failure; if none comes (a
201
+ /// development build that shows the error instead), the claim must not
202
+ /// keep the other launches stopped for the rest of the process.
203
+ static let loadFailureClaimBound: TimeInterval = 10
204
+ /// Load failures no runtime claimed whose fatal hasn't come yet: several
205
+ /// runtimes can fail close together, and each fatal that no runtime
206
+ /// claims takes one. Each lives until `loadFailureClaimBound` passes, or
207
+ /// until every named launch it was deferred for is known not to have
208
+ /// failed (it launched again, its runtime resolved, or it showed
209
+ /// content): a fatal that never came must not turn a later, unrelated
210
+ /// one into a load failure.
211
+ private var deferredLoadFailures: [DeferredLoadFailure] = []
212
+
213
+ /// A load failure no runtime claimed, waiting for its fatal.
214
+ struct DeferredLoadFailure {
215
+ let deferredAt: TimeInterval
216
+ /// The launches that may have failed when it was reported, by
217
+ /// generation: the unresolved named ones, and main until its first
218
+ /// content. Only these can be its owner, or be charged for it.
219
+ let possibleOwners: [String: Int]
220
+ }
221
+ /// Called with a named instance whose own fatal or load failure marked
222
+ /// its launch for rollback, outside the lock. If the process lives on (a
223
+ /// development build, or a fatal handler that returns) and the host
224
+ /// starts the instance again, its next launch must be prepared anew, so
225
+ /// it consumes the marker and rolls back. Set once, before any launch.
226
+ var onRollbackCharged: ((String) -> Void)?
227
+ /// Named instances a charge in progress marked for rollback.
228
+ private var rolledBackNames: [String] = []
229
+ /// The current claim is of a runtime no current launch runs.
230
+ private var isEarlierRuntimeClaim = false
231
+ /// First content no runtime claims yet, held while a named launch's
232
+ /// runtime may still turn out to be its own.
233
+ private var heldContent: [HeldContent] = []
234
+ /// Seconds, monotonic.
235
+ private let now: () -> TimeInterval
236
+
237
+ private struct HeldContent {
238
+ weak var view: AnyObject?
239
+ let heldAt: TimeInterval
240
+ /// Each instance's launch when the content appeared: content of an
241
+ /// earlier launch never verifies a later one.
242
+ let generations: [String: Int]
243
+ /// Main was verified by it after the bound; kept so a named runtime
244
+ /// that resolves later still finds its own content.
245
+ var creditedToMain = false
246
+ }
247
+
248
+ /// How long content no runtime claims waits for a named runtime's module
249
+ /// before it is main's. A named runtime whose JavaScript never loads Hot
250
+ /// Updater never resolves; without a bound, main's launch would never be
251
+ /// verified and the next start would roll it back.
252
+ static let heldContentBound: TimeInterval = 10
253
+
254
+ init(sink: HotUpdaterCrashSlotSink, now: @escaping () -> TimeInterval = { ProcessInfo.processInfo.systemUptime }) {
255
+ self.sink = sink
256
+ self.now = now
257
+ }
258
+
259
+ // MARK: - Instances
260
+
261
+ /// The launch state of `name`, created on first use. An instance keeps
262
+ /// its marker and storage for the life of the process.
263
+ @discardableResult
264
+ func register(name: String, markerURL: URL, storage: BundleStorageService) -> Launch {
265
+ withLock {
266
+ if let launch = launches[name] {
267
+ return launch
268
+ }
269
+ let launch = Launch(name: name, markerURL: markerURL, storage: storage)
270
+ launches[name] = launch
271
+ return launch
272
+ }
273
+ }
274
+
275
+ /// Whether this process has created a named instance.
276
+ var hasNamedInstance: Bool {
277
+ withLock { hasNamedInstanceUnlocked }
278
+ }
279
+
280
+ private var hasNamedInstanceUnlocked: Bool {
281
+ launches.values.contains { !$0.isMain }
282
+ }
283
+
284
+ func launch(named name: String) -> Launch? {
285
+ withLock { launches[name] }
286
+ }
287
+
288
+ /// The instance an event of `owner`'s runtime belongs to. A view or
289
+ /// runtime that can't be attributed is main's, as it always was.
290
+ func owner(_ owner: String?) -> Launch? {
291
+ withLock {
292
+ if let owner, let launch = launches[owner] {
293
+ return launch
294
+ }
295
+ return launches[HotUpdaterInstanceEnvironment.mainName]
296
+ }
297
+ }
298
+
299
+ // MARK: - Launch
300
+
301
+ /// Starts watching a launch of `name`; returns its generation.
302
+ @discardableResult
303
+ func startMonitoring(name: String, bundleId: String?, shouldRollback: Bool, launchURL: URL? = nil) -> Int {
304
+ withLock {
305
+ guard let launch = launches[name] else { return 0 }
306
+ launch.generation += 1
307
+ // A load failure of this instance whose fatal never came: the
308
+ // instance starts again, so that crash is over.
309
+ // An earlier runtime's claim keeps its instance in earlierRuntimeOwner.
310
+ for claim in activeClaims
311
+ where (claim.name ?? claim.earlierRuntimeOwner) == name && claim.awaitingFatalSince != nil {
312
+ releaseClaim(claim.id)
313
+ }
314
+ if activeClaims.contains(where: { $0.name == name }) {
315
+ // A claimed crash is the previous launch's: its marker is
316
+ // written, and the native crash it causes writes no slot,
317
+ // so this launch's state never stands in for it.
318
+ for index in activeClaims.indices where activeClaims[index].name == name {
319
+ activeClaims[index].slot = Self.attributedWithoutSlot
320
+ }
321
+ publishLatestClaim()
322
+ }
323
+ launch.launchURL = launchURL
324
+ launch.bundleId = bundleId
325
+ launch.selectedBundleId = bundleId
326
+ launch.shouldRollback = shouldRollback
327
+ launch.isArmed = false
328
+ launch.recoveryRequested = false
329
+ launch.isMonitoring = true
330
+ launch.isAwaitingContent = true
331
+ launch.hasResolvedRuntime = false
332
+ launch.hasShownContent = false
333
+ launch.isCharged = false
334
+ // Every launch publishes its slot now (a named one not asking
335
+ // for rollback yet), so starting or charging it later only flips
336
+ // the flag in place, which a signal always reads whole.
337
+ assignSlot(to: launch)
338
+ mirror(launch)
339
+ return launch.generation
340
+ }
341
+ }
342
+
343
+ /// The generation of `name`'s launch when a new preparation selected the
344
+ /// same bundle at the same URL: that launch continues, with its
345
+ /// generation and monitoring, rather than a new one starting. A named
346
+ /// instance prepares again only after a channel reset, a built-in
347
+ /// commit, or its own rollback, each of which selects another bundle
348
+ /// (or the built-in, never pending), and it never reloads; so two
349
+ /// launches of one name never share a bundle, which is what tells their
350
+ /// runtimes apart. Main, which reloads its launch in place, always
351
+ /// starts a new one.
352
+ func continuingLaunch(name: String, bundleId: String?, launchURL: URL?) -> Int? {
353
+ withLock {
354
+ guard let launch = launches[name], !launch.isMain, launch.generation > 0,
355
+ launch.selectedBundleId == bundleId else {
356
+ return nil
357
+ }
358
+ let sameURL: Bool
359
+ switch (launch.launchURL, launchURL) {
360
+ case (nil, nil):
361
+ sameURL = true
362
+ case let (current?, new?):
363
+ sameURL = HotUpdaterInstanceRouter.comparableKey(current) == HotUpdaterInstanceRouter.comparableKey(new)
364
+ default:
365
+ sameURL = false
366
+ }
367
+ return sameURL ? launch.generation : nil
368
+ }
369
+ }
370
+
371
+ /// A view of a named launch's runtime was shown in the foreground: its
372
+ /// launch has started. From now on an unfinished launch rolls back at the
373
+ /// next start, and a native crash counts against it. Main's launch starts
374
+ /// when it is selected, so main is never armed here.
375
+ /// `generation`, when given, is the launch the view started in: a view of
376
+ /// an earlier launch never starts a later one.
377
+ @discardableResult
378
+ ///
379
+ /// `runtimeBundleURL` is the bundle the view's runtime runs, when known:
380
+ /// a view of a runtime of an earlier launch, such as one still torn down
381
+ /// while its host is recreated, never starts the current launch.
382
+ func arm(name: String, generation: Int? = nil, runtimeBundleURL: URL? = nil) -> Bool {
383
+ withLock {
384
+ guard let launch = launches[name],
385
+ generation == nil || launch.generation == generation,
386
+ isCurrentRuntime(of: launch, runtimeBundleURL: runtimeBundleURL),
387
+ !launch.isMain,
388
+ launch.isMonitoring,
389
+ launch.isAwaitingContent,
390
+ launch.shouldRollback,
391
+ !launch.isArmed else {
392
+ return false
393
+ }
394
+ return markStarted(launch)
395
+ }
396
+ }
397
+
398
+ /// Main's launch of `generation` is in the foreground, so it started:
399
+ /// an unfinished launch rolls back at the next start, as it always did.
400
+ /// Refused for a later launch or once its content appeared. Called after
401
+ /// `startMonitoring` registered the launch, so a claim of another
402
+ /// instance's fatal sees it.
403
+ @discardableResult
404
+ func markMainLaunchStarted(generation: Int) -> Bool {
405
+ withLock {
406
+ guard let launch = launches[HotUpdaterInstanceEnvironment.mainName],
407
+ launch.generation == generation,
408
+ launch.isMonitoring,
409
+ !launch.hasShownContent,
410
+ launch.shouldRollback,
411
+ !launch.isArmed else {
412
+ return false
413
+ }
414
+ return markStarted(launch)
415
+ }
416
+ }
417
+
418
+ /// Marks `launch` started, unless another instance's fatal holds a claim
419
+ /// that stopped every other launch: then it starts when that claim is
420
+ /// released, if the process lives on.
421
+ private func markStarted(_ launch: Launch) -> Bool {
422
+ let isClaimOwner = attributedName == launch.name && attributedGeneration == launch.generation
423
+ let claimHeld = !activeClaims.isEmpty && !isClaimOwner
424
+ if claimHeld, let bundleId = launch.bundleId {
425
+ disarmed.append((launch.name, launch.generation, bundleId, true, true))
426
+ return false
427
+ }
428
+ assignSlot(to: launch)
429
+ guard launch.slot != nil else {
430
+ // No native slot left: nothing asks for rollback if a signal
431
+ // comes, so the start is persisted before the launch counts as
432
+ // armed, and the unfinished-launch rule covers it.
433
+ launch.storage.markLaunchStarted(bundleId: launch.bundleId)
434
+ launch.isArmed = true
435
+ return true
436
+ }
437
+ // The slot first, then storage.
438
+ launch.isArmed = true
439
+ mirror(launch)
440
+ launch.storage.markLaunchStarted(bundleId: launch.bundleId)
441
+ return true
442
+ }
443
+
444
+ /// First content of `owner`'s runtime, which verifies only that instance's
445
+ /// launch. Returns the verified launch and its generation, whose
446
+ /// monitoring ends after a grace period.
447
+ /// `generation`, when given, is the launch the content appeared in.
448
+ /// `runtimeBundleURL` is the bundle the view's runtime runs, when known:
449
+ /// content of a runtime of an earlier launch verifies no launch.
450
+ /// `runtimeGeneration` is the launch the view's runtime was recorded
451
+ /// for (`HotUpdaterRuntimeLaunches`), when known: it tells a runtime of
452
+ /// an earlier launch apart even with the same bundle.
453
+ func contentAppeared(
454
+ owner: String?,
455
+ generation: Int? = nil,
456
+ runtimeBundleURL: URL? = nil,
457
+ runtimeGeneration: Int? = nil
458
+ ) -> (launch: Launch, generation: Int)? {
459
+ withLock {
460
+ guard let launch = self.owner(owner),
461
+ generation == nil || launch.generation == generation,
462
+ isCurrentRuntime(of: launch, runtimeBundleURL: runtimeBundleURL, runtimeGeneration: runtimeGeneration),
463
+ launch.isMonitoring,
464
+ launch.isAwaitingContent,
465
+ !launch.recoveryRequested else {
466
+ return nil
467
+ }
468
+ launch.isAwaitingContent = false
469
+ launch.hasShownContent = true
470
+ // The slot stops asking for rollback before the verification is
471
+ // written: a signal while it is written leaves no rollback marker
472
+ // for a launch that rendered.
473
+ launch.shouldRollback = false
474
+ launch.isArmed = false
475
+ mirror(launch)
476
+ launch.storage.markLaunchCompleted(bundleId: launch.bundleId)
477
+ return (launch, launch.generation)
478
+ }
479
+ }
480
+
481
+ /// First content whose view no runtime claims yet. In a process where
482
+ /// every named launch's runtime already has a module, it is main's, as it
483
+ /// always was (returns false). Otherwise a named runtime that renders
484
+ /// before its module exists may have shown it, so it is held (returns
485
+ /// true) until `releaseHeldContent` can tell whose it is, for at most
486
+ /// `heldContentBound` seconds; check again then.
487
+ func holdUnclaimedContent(_ view: AnyObject) -> Bool {
488
+ withLock {
489
+ guard hasUnresolvedNamedLaunch else { return false }
490
+ heldContent.append(HeldContent(view: view, heldAt: now(), generations: currentGenerations))
491
+ return true
492
+ }
493
+ }
494
+
495
+ /// Credits held content: each view `owner` now attributes verifies that
496
+ /// instance. Content still unclaimed `heldContentBound` seconds after it
497
+ /// appeared is main's (main may be verified a little early, never left
498
+ /// unverified), and so is all of it once no named launch's runtime is
499
+ /// left unresolved. Content credited to main after the bound stays
500
+ /// known: a named runtime that resolves later verifies its launch with
501
+ /// it, rather than starting a launch whose content already appeared.
502
+ /// Call when a runtime resolves, a named launch stops waiting for
503
+ /// content, or the bound passes. Returns the launches it verified.
504
+ func releaseHeldContent(
505
+ owner: (AnyObject) -> String?,
506
+ runtimeBundleURL: (AnyObject) -> URL? = { _ in nil },
507
+ runtimeGeneration: (AnyObject) -> Int? = { _ in nil }
508
+ ) -> [(launch: Launch, generation: Int)] {
509
+ withLock {
510
+ guard !heldContent.isEmpty else { return [] }
511
+ var verified: [(launch: Launch, generation: Int)] = []
512
+ var kept: [HeldContent] = []
513
+ // Main's launches the content credited to main appeared in.
514
+ var mainGenerations: Set<Int> = []
515
+ let mainName = HotUpdaterInstanceEnvironment.mainName
516
+ let currentTime = self.now()
517
+ for var held in heldContent {
518
+ if let view = held.view, let name = owner(view) {
519
+ // The launch its runtime was recorded for, else the one
520
+ // current when the content appeared.
521
+ if let generation = runtimeGeneration(view) ?? held.generations[name],
522
+ let launch = contentAppeared(
523
+ owner: name,
524
+ generation: generation,
525
+ runtimeBundleURL: runtimeBundleURL(view)
526
+ ) {
527
+ verified.append(launch)
528
+ }
529
+ continue
530
+ }
531
+ // A view already gone can't be attributed any more.
532
+ if held.view == nil, held.creditedToMain {
533
+ continue
534
+ }
535
+ if !held.creditedToMain, currentTime - held.heldAt >= Self.heldContentBound {
536
+ held.creditedToMain = true
537
+ held.generations[mainName].map { _ = mainGenerations.insert($0) }
538
+ }
539
+ kept.append(held)
540
+ }
541
+ if !hasUnresolvedNamedLaunch {
542
+ // Nothing left that could claim it.
543
+ for held in kept where !held.creditedToMain {
544
+ held.generations[mainName].map { _ = mainGenerations.insert($0) }
545
+ }
546
+ kept = []
547
+ }
548
+ heldContent = kept
549
+ if let generation = launches[mainName]?.generation,
550
+ mainGenerations.contains(generation),
551
+ let launch = contentAppeared(owner: nil, generation: generation) {
552
+ verified.append(launch)
553
+ }
554
+ return verified
555
+ }
556
+ }
557
+
558
+ /// The current launch generation of every instance. Callers on other
559
+ /// threads keep it with what they schedule, so a later callback acts
560
+ /// only on the launch it was meant for.
561
+ func generations() -> [String: Int] {
562
+ withLock { currentGenerations }
563
+ }
564
+
565
+ private var currentGenerations: [String: Int] {
566
+ launches.mapValues(\.generation)
567
+ }
568
+
569
+ /// A JavaScript load failure nothing attributes, while a named launch's
570
+ /// runtime may still be the one that failed: main must not recover
571
+ /// in-process for it. Returns true, and the fatal React Native reports
572
+ /// next (`takeDeferredLoadFailure`) decides whose it is. With only main,
573
+ /// or every named runtime known, false: it is main's, as it always was.
574
+ func deferUnclaimedLoadFailure() -> Bool {
575
+ withLock {
576
+ guard hasUnresolvedNamedLaunch else { return false }
577
+ let possibleOwners = possibleFatalOwners
578
+ deferredLoadFailures.append(DeferredLoadFailure(
579
+ deferredAt: now(),
580
+ possibleOwners: Dictionary(uniqueKeysWithValues: possibleOwners.map { ($0.name, $0.generation) })
581
+ ))
582
+ return true
583
+ }
584
+ }
585
+
586
+ /// The fatal of a load failure no runtime claimed, which crashes the
587
+ /// process: every launch whose runtime may be the one that failed is
588
+ /// charged, a named one whose view hasn't shown yet included, so the
589
+ /// failing bundle rolls back at its next start whoever it was.
590
+ ///
591
+ /// Returns the claim to release once the forwarded fatal's handler
592
+ /// returns: while it holds, every launch that can't have failed is
593
+ /// stopped, so a native crash or uncaught exception the fatal causes
594
+ /// writes only the possible owners' markers.
595
+ ///
596
+ /// `deferred`, the load failure the fatal follows, limits the charge to
597
+ /// the launches that could have failed when it was reported: a runtime
598
+ /// started since then is not charged.
599
+ @discardableResult
600
+ func chargeUnclaimedLoadFailure(_ deferred: DeferredLoadFailure? = nil) -> Int {
601
+ charging {
602
+ var owners = possibleFatalOwners
603
+ if let deferred {
604
+ owners = owners.filter { deferred.possibleOwners[$0.name] == $0.generation }
605
+ }
606
+ // As for an attributed fatal: their slots ask for rollback, the
607
+ // others stop, then the marker files are written.
608
+ for launch in owners where launch.isMonitoring {
609
+ prepareCharge(launch)
610
+ }
611
+ // Slotless owners' markers first: nothing native writes them.
612
+ for launch in owners where launch.isMonitoring && launch.slot == nil {
613
+ writeMarker(launch)
614
+ }
615
+ claimGeneration += 1
616
+ appendClaim(Claim(
617
+ id: claimGeneration,
618
+ name: nil,
619
+ generation: nil,
620
+ slot: Self.unattributed,
621
+ isEarlierRuntime: false
622
+ ))
623
+ publishLatestClaim()
624
+ let ownerNames = Set(owners.map(\.name))
625
+ disarm(launches.values.filter { !ownerNames.contains($0.name) })
626
+ for launch in owners where launch.isMonitoring && launch.slot != nil {
627
+ writeMarker(launch)
628
+ }
629
+ return claimGeneration
630
+ }
631
+ }
632
+
633
+ /// The launches of this process whose runtime hasn't shown it finished
634
+ /// loading: no first content yet and, for a named one, no module.
635
+ private var possibleFatalOwners: [Launch] {
636
+ launches.values.filter { launch in
637
+ guard launch.generation > 0, !launch.hasShownContent else { return false }
638
+ return launch.isMain || !launch.hasResolvedRuntime
639
+ }
640
+ }
641
+
642
+ /// Whether the fatal now reported follows a deferred load failure; the
643
+ /// next one doesn't. (`takeDeferredLoadFailure(for:owner:)` with no owner.)
644
+ func takeDeferredLoadFailure() -> Bool {
645
+ takeDeferredLoadFailure(for: .none) != nil
646
+ }
647
+
648
+ /// Takes the oldest live deferred load failure `matches` accepts.
649
+ private func takeLiveDeferredLoadFailure(where matches: (DeferredLoadFailure) -> Bool) -> DeferredLoadFailure? {
650
+ let currentTime = now()
651
+ // Live while one of its launches could still have failed: main
652
+ // until that launch's first content, a named one while its runtime
653
+ // is unresolved. With only main left, it is main's failure.
654
+ deferredLoadFailures.removeAll { deferred in
655
+ currentTime - deferred.deferredAt >= Self.loadFailureClaimBound
656
+ || !deferred.possibleOwners.contains { name, generation in
657
+ guard let launch = launches[name], launch.generation == generation else { return false }
658
+ return launch.isMain
659
+ ? launch.isMonitoring && !launch.hasShownContent
660
+ : isUnresolvedNamedLaunch(launch)
661
+ }
662
+ }
663
+ guard let index = deferredLoadFailures.firstIndex(where: matches) else { return nil }
664
+ return deferredLoadFailures.remove(at: index)
665
+ }
666
+
667
+ /// A fatal's owner, by the first that names one: the runtime's own
668
+ /// module, the bundles in its JavaScript stack, the claim of a crash
669
+ /// already going on (a follow-up fatal with nothing of its own to tell),
670
+ /// then inference. A module-less runtime's fatal with a stack of its own
671
+ /// is its own, even while another runtime's fatal holds a claim.
672
+ static func fatalOwner(
673
+ module: (name: String, runtimeBundleURL: URL?)?,
674
+ stack: (name: String, runtimeBundleURL: URL?)?,
675
+ claim: String?,
676
+ inference: () -> String?
677
+ ) -> (name: String, runtimeBundleURL: URL?, attribution: FatalAttribution)? {
678
+ if let module {
679
+ return (module.name, module.runtimeBundleURL, .runtimeModule)
680
+ }
681
+ if let stack {
682
+ return (stack.name, stack.runtimeBundleURL, .stack)
683
+ }
684
+ if let claim {
685
+ return (claim, nil, .claim)
686
+ }
687
+ return inference().map { ($0, nil, .inference) }
688
+ }
689
+
690
+ /// How a fatal's owner was found.
691
+ enum FatalAttribution {
692
+ /// The runtime's own module: a runtime that has one never deferred
693
+ /// a load failure.
694
+ case runtimeModule
695
+ /// The claim of a crash already going on.
696
+ case claim
697
+ /// The bundles in its JavaScript stack, or the only launch that may
698
+ /// have thrown it: how a module-less runtime's fatal, the kind that
699
+ /// follows a deferred load failure, is told apart.
700
+ case stack
701
+ case inference
702
+ /// Nothing: only the deferred path decides, unless an earlier
703
+ /// runtime's claim already accounts for it.
704
+ case none
705
+ }
706
+
707
+ /// The deferred load failure the fatal now reported follows, taken so
708
+ /// no later fatal takes it, or nil. A fatal attributed by its stack or by
709
+ /// inference to `owner` takes one only if `owner`'s current launch could
710
+ /// have failed when it was reported; an unattributed fatal takes the
711
+ /// oldest live one. A runtime with a module, or a claimed crash, never
712
+ /// deferred one.
713
+ @discardableResult
714
+ func takeDeferredLoadFailure(for attribution: FatalAttribution, owner: String? = nil) -> DeferredLoadFailure? {
715
+ withLock {
716
+ switch attribution {
717
+ case .runtimeModule, .claim:
718
+ return nil
719
+ case .none where isEarlierRuntimeClaim:
720
+ return nil
721
+ case .stack, .inference:
722
+ guard let owner, let generation = launches[owner]?.generation else { return nil }
723
+ return takeLiveDeferredLoadFailure { $0.possibleOwners[owner] == generation }
724
+ case .none:
725
+ return takeLiveDeferredLoadFailure { _ in true }
726
+ }
727
+ }
728
+ }
729
+
730
+ /// A named launch of this process still waiting for content whose
731
+ /// runtime has no module yet.
732
+ private var hasUnresolvedNamedLaunch: Bool {
733
+ launches.values.contains(where: isUnresolvedNamedLaunch)
734
+ }
735
+
736
+ private func isUnresolvedNamedLaunch(_ launch: Launch) -> Bool {
737
+ !launch.isMain && launch.isMonitoring && launch.isAwaitingContent && !launch.hasResolvedRuntime
738
+ }
739
+
740
+ /// Ends the grace period after first content.
741
+ func finishMonitoring(name: String, generation: Int) {
742
+ withLock {
743
+ guard let launch = launches[name], launch.generation == generation else { return }
744
+ launch.isMonitoring = false
745
+ launch.recoveryRequested = false
746
+ launch.bundleId = nil
747
+ launch.shouldRollback = false
748
+ launch.isArmed = false
749
+ launch.isAwaitingContent = false
750
+ mirror(launch)
751
+ }
752
+ }
753
+
754
+ /// A JavaScript load failure that recovery didn't take: the launch's later
755
+ /// content no longer verifies it.
756
+ /// `generation`, when given, is the launch whose load failed; a later
757
+ /// launch keeps waiting for its own content.
758
+ func stopAwaitingContent(name: String, generation: Int? = nil) {
759
+ withLock {
760
+ guard let launch = launches[name], generation == nil || launch.generation == generation else { return }
761
+ launch.isAwaitingContent = false
762
+ }
763
+ }
764
+
765
+ /// The generation of `name`'s launch if it still waits for its first
766
+ /// content, read under the lock every transition takes.
767
+ func awaitingContentGeneration(name: String) -> Int? {
768
+ withLock {
769
+ guard let launch = launches[name], launch.isMonitoring, launch.isAwaitingContent else { return nil }
770
+ return launch.generation
771
+ }
772
+ }
773
+
774
+ func isCurrent(name: String, generation: Int) -> Bool {
775
+ withLock { launches[name]?.generation == generation }
776
+ }
777
+
778
+ // MARK: - Main's in-process recovery
779
+
780
+ /// Main's fatal before first content reloads it in-process: writes its
781
+ /// marker once, so the reloaded launch rolls back.
782
+ ///
783
+ /// Not with a named instance in the process: React Native reloads every
784
+ /// host at once, and named instances apply rollbacks at their next start
785
+ /// only. Main's fatal then takes its course, charged to main, which rolls
786
+ /// back at its next start or next same-process launch.
787
+ func requestRecovery(name: String) -> RecoveryRequest {
788
+ charging {
789
+ guard let launch = launches[name], launch.isMonitoring, launch.shouldRollback,
790
+ !(launch.isMain && hasNamedInstanceUnlocked) else {
791
+ return .notNeeded
792
+ }
793
+ if launch.recoveryRequested {
794
+ return .alreadyRequested
795
+ }
796
+ launch.recoveryRequested = true
797
+ writeMarker(launch)
798
+ return .requested(generation: launch.generation, bundleId: launch.bundleId)
799
+ }
800
+ }
801
+
802
+ /// The recovery reload of `generation` didn't start; a later launch's
803
+ /// own request is left alone.
804
+ func recoveryReloadFailed(name: String, generation: Int) {
805
+ withLock {
806
+ guard let launch = launches[name], launch.generation == generation else { return }
807
+ launch.recoveryRequested = false
808
+ }
809
+ }
810
+
811
+ // MARK: - Crashes
812
+
813
+ /// A JavaScript fatal that will crash the process. Attributed to `owner`:
814
+ /// only `owner`'s marker, and the native crash it causes writes no other
815
+ /// slot (see `attribute(to:)`). Unattributed: every unverified launch's.
816
+ /// Returns the claim to release if the process lives on.
817
+ @discardableResult
818
+ ///
819
+ /// `runtimeBundleURL` is the bundle the failing runtime runs, when known.
820
+ /// A named instance's runtime of an earlier launch, still being torn down
821
+ /// after a later launch started, runs another bundle: its error charges
822
+ /// no launch at all (see `attributeToEarlierRuntime`).
823
+ ///
824
+ /// `runtimeGeneration`, the launch the failing runtime was recorded for,
825
+ /// tells a runtime of an earlier launch apart even with the same bundle.
826
+ func chargeFatal(owner: String?, runtimeBundleURL: URL? = nil, runtimeGeneration: Int? = nil) -> Int? {
827
+ charging {
828
+ guard let owner, let launch = launches[owner] else {
829
+ chargeUnattributedCrash()
830
+ return nil
831
+ }
832
+ let isCurrent = isCurrentRuntime(
833
+ of: launch,
834
+ runtimeBundleURL: runtimeBundleURL,
835
+ runtimeGeneration: runtimeGeneration
836
+ )
837
+ // The fatal of a load failure: the same crash, so the load
838
+ // failure's claim is its claim, found among every claim (another
839
+ // runtime's may have come in between). For this launch, by its
840
+ // generation; for an earlier runtime of this instance, by the
841
+ // bundle it ran when both are known.
842
+ let urlKey = runtimeBundleURL.map(HotUpdaterInstanceRouter.comparableKey)
843
+ if let index = activeClaims.firstIndex(where: { claim in
844
+ guard claim.awaitingFatalSince != nil else { return false }
845
+ if claim.isEarlierRuntime {
846
+ return !isCurrent
847
+ && claim.earlierRuntimeOwner == owner
848
+ && (urlKey == nil || claim.earlierRuntimeURLKey == nil || claim.earlierRuntimeURLKey == urlKey)
849
+ }
850
+ return isCurrent && claim.name == owner && claim.generation == launch.generation
851
+ }) {
852
+ activeClaims[index].awaitingFatalSince = nil
853
+ return activeClaims[index].id
854
+ }
855
+ guard isCurrent else {
856
+ return attributeToEarlierRuntime(owner: owner, urlKey: urlKey)
857
+ }
858
+ // The claimed crash going on, after its instance started a later
859
+ // launch: still the earlier launch's.
860
+ if runtimeBundleURL == nil, attributedName == owner, attributedGeneration != launch.generation {
861
+ return attributeToEarlierRuntime(owner: owner, urlKey: nil)
862
+ }
863
+ // Its slot asks for rollback, then the crash is published as its,
864
+ // then the marker file is written: a signal at any point writes
865
+ // its slot alone (unless every slot is taken).
866
+ if launch.isMonitoring {
867
+ prepareCharge(launch)
868
+ }
869
+ // Without a slot nothing native writes its marker: it is written
870
+ // before the attribution, which then stops every other slot.
871
+ let markerFirst = launch.isMonitoring && launch.slot == nil
872
+ if markerFirst {
873
+ writeMarker(launch)
874
+ }
875
+ let claim = attribute(to: owner)
876
+ if launch.isMonitoring, !markerFirst {
877
+ writeMarker(launch)
878
+ }
879
+ return claim
880
+ }
881
+ }
882
+
883
+ /// A load failure of `owner`'s runtime, which React Native follows with
884
+ /// a fatal: charged as `chargeFatal` does, and its claim kept for that
885
+ /// fatal, which takes it over and releases it when its handler returns.
886
+ /// Without a fatal it is released when the instance launches again or
887
+ /// after `loadFailureClaimBound` (`releaseExpiredLoadFailureClaims`).
888
+ @discardableResult
889
+ func chargeLoadFailure(owner: String, runtimeBundleURL: URL? = nil, runtimeGeneration: Int? = nil) -> Int? {
890
+ let claim = chargeFatal(owner: owner, runtimeBundleURL: runtimeBundleURL, runtimeGeneration: runtimeGeneration)
891
+ withLock {
892
+ if let claim, let index = activeClaims.firstIndex(where: { $0.id == claim }) {
893
+ activeClaims[index].awaitingFatalSince = now()
894
+ }
895
+ }
896
+ return claim
897
+ }
898
+
899
+ /// Releases the load failures' claims whose fatal hasn't come within
900
+ /// `loadFailureClaimBound`.
901
+ func releaseExpiredLoadFailureClaims() {
902
+ withLock {
903
+ let currentTime = now()
904
+ for claim in activeClaims {
905
+ if let since = claim.awaitingFatalSince, currentTime - since >= Self.loadFailureClaimBound {
906
+ releaseClaim(claim.id)
907
+ }
908
+ }
909
+ }
910
+ }
911
+
912
+ /// The number of claims still active, for tests.
913
+ var activeClaimCount: Int {
914
+ withLock { activeClaims.count }
915
+ }
916
+
917
+ /// The first step of charging `launch` with the crash under way: its
918
+ /// native slot asks for rollback (a named launch takes a free slot if it
919
+ /// has none), so a signal before or while its marker file is written
920
+ /// still leaves a marker. The caller publishes the attribution, then
921
+ /// writes the marker.
922
+ private func prepareCharge(_ launch: Launch) {
923
+ if !launch.isMain, launch.shouldRollback {
924
+ launch.isCharged = true
925
+ assignSlot(to: launch)
926
+ mirror(launch)
927
+ }
928
+ }
929
+
930
+ /// Whether a runtime that runs `runtimeBundleURL` is `name`'s current
931
+ /// launch: false only for a named instance whose launch handed out
932
+ /// another bundle. Main, and an unknown URL, count as current.
933
+ /// `runtimeGeneration`, when known, decides instead: the runtime runs
934
+ /// the current launch only if it was recorded for it.
935
+ func isCurrentRuntime(name: String, runtimeBundleURL: URL?, runtimeGeneration: Int? = nil) -> Bool {
936
+ withLock {
937
+ guard let launch = launches[name] else { return true }
938
+ return isCurrentRuntime(of: launch, runtimeBundleURL: runtimeBundleURL, runtimeGeneration: runtimeGeneration)
939
+ }
940
+ }
941
+
942
+ /// Main's runtime always counts as current, as it always has.
943
+ private func isCurrentRuntime(of launch: Launch, runtimeBundleURL: URL?, runtimeGeneration: Int? = nil) -> Bool {
944
+ guard !launch.isMain else { return true }
945
+ if let runtimeGeneration {
946
+ return runtimeGeneration == launch.generation
947
+ }
948
+ guard let runtimeBundleURL, let launchURL = launch.launchURL else {
949
+ return true
950
+ }
951
+ return HotUpdaterInstanceRouter.comparableKey(runtimeBundleURL)
952
+ == HotUpdaterInstanceRouter.comparableKey(launchURL)
953
+ }
954
+
955
+ /// The crash about to happen comes from a runtime no current launch runs:
956
+ /// no marker, no native slot, and every launch that started forgets it,
957
+ /// so no current launch is rolled back for it. Returns the claim, as
958
+ /// `attribute(to:)` does.
959
+ private func attributeToEarlierRuntime(owner: String, urlKey: String?) -> Int {
960
+ claimGeneration += 1
961
+ appendClaim(Claim(
962
+ id: claimGeneration,
963
+ name: nil,
964
+ generation: nil,
965
+ slot: Self.attributedWithoutSlot,
966
+ isEarlierRuntime: true,
967
+ earlierRuntimeOwner: owner,
968
+ earlierRuntimeURLKey: urlKey
969
+ ))
970
+ publishLatestClaim()
971
+ disarm(launches.values)
972
+ return claimGeneration
973
+ }
974
+
975
+ private func appendClaim(_ claim: Claim) {
976
+ var claim = claim
977
+ claim.startedAt = now()
978
+ activeClaims.append(claim)
979
+ onClaimStarted?()
980
+ }
981
+
982
+ /// Releases every claim older than `claimFailSafe`, the way a returning
983
+ /// fatal handler would.
984
+ func releaseStaleClaims() {
985
+ withLock {
986
+ let currentTime = now()
987
+ for claim in activeClaims where currentTime - claim.startedAt >= Self.claimFailSafe {
988
+ releaseClaim(claim.id)
989
+ }
990
+ }
991
+ }
992
+
993
+ /// Publishes the latest active claim, or no attribution without one.
994
+ private func publishLatestClaim() {
995
+ let latest = activeClaims.last
996
+ attributedName = latest?.name
997
+ attributedGeneration = latest?.generation
998
+ isEarlierRuntimeClaim = latest?.isEarlierRuntime ?? false
999
+ attributedSlot = latest?.slot ?? Self.unattributed
1000
+ sink.attributeCrash(toSlot: attributedSlot)
1001
+ }
1002
+
1003
+ /// The claim of an earlier runtime's crash in progress, if any: a fatal
1004
+ /// that follows it unattributed, such as RCTInstance's after a load
1005
+ /// failure, belongs to that runtime too.
1006
+ var earlierRuntimeClaim: Int? {
1007
+ withLock { activeClaims.last.flatMap { $0.isEarlierRuntime ? $0.id : nil } }
1008
+ }
1009
+
1010
+ private func disarm<Launches: Sequence>(_ launches: Launches) where Launches.Element == Launch {
1011
+ for launch in launches {
1012
+ guard launch.isMonitoring, launch.isAwaitingContent, let bundleId = launch.bundleId else {
1013
+ continue
1014
+ }
1015
+ // A launch charged earlier counts as started too.
1016
+ let wasArmed = launch.isArmed || launch.isCharged
1017
+ launch.isArmed = false
1018
+ launch.isCharged = false
1019
+ // Its slot stops asking for rollback before its storage is
1020
+ // written.
1021
+ mirror(launch)
1022
+ let wasStarted = launch.storage.clearLaunchStarted(bundleId: bundleId)
1023
+ disarmed.append((launch.name, launch.generation, bundleId, wasStarted, wasArmed))
1024
+ }
1025
+ }
1026
+
1027
+ /// The crash about to happen is `owner`'s alone. A native crash it causes
1028
+ /// writes no other slot, and the other launches that already started
1029
+ /// forget it, so the unfinished-launch rule doesn't roll them back at the
1030
+ /// next start either; they stay pending and launch the same bundle again.
1031
+ /// Returns the claim to release if the process lives on. A claim made
1032
+ /// while an earlier one holds takes over: the launches the earlier one
1033
+ /// stopped, this owner's included, stay listed to start again.
1034
+ @discardableResult
1035
+ func attribute(to owner: String) -> Int? {
1036
+ withLock {
1037
+ guard let launch = launches[owner] else { return nil }
1038
+ claimGeneration += 1
1039
+ appendClaim(Claim(
1040
+ id: claimGeneration,
1041
+ name: owner,
1042
+ generation: launch.generation,
1043
+ slot: launch.slot ?? Self.attributedWithoutSlot,
1044
+ isEarlierRuntime: false
1045
+ ))
1046
+ publishLatestClaim()
1047
+ disarm(launches.values.filter { $0.name != owner })
1048
+ return claimGeneration
1049
+ }
1050
+ }
1051
+
1052
+ /// React Native's default fatal handler can return in a debug build, and
1053
+ /// the process lives on: the launches the attribution stopped that are
1054
+ /// still unverified start again, so a later crash still counts against
1055
+ /// them. Fatals overlap: a handler that returns removes only its own
1056
+ /// claim, and the latest one still active is published again; only the
1057
+ /// last one's return releases the launches every claim stopped.
1058
+ func clearAttribution(claim: Int) {
1059
+ withLock {
1060
+ releaseClaim(claim)
1061
+ }
1062
+ }
1063
+
1064
+ private func releaseClaim(_ claim: Int) {
1065
+ guard let index = activeClaims.firstIndex(where: { $0.id == claim }) else { return }
1066
+ activeClaims.remove(at: index)
1067
+ publishLatestClaim()
1068
+ guard activeClaims.isEmpty else { return }
1069
+ for entry in disarmed {
1070
+ guard let launch = launches[entry.name],
1071
+ launch.generation == entry.generation,
1072
+ launch.isMonitoring,
1073
+ !launch.hasShownContent,
1074
+ launch.bundleId == entry.bundleId else {
1075
+ continue
1076
+ }
1077
+ assignSlot(to: launch)
1078
+ if launch.slot == nil {
1079
+ // Slotless: the start is persisted before it counts as armed.
1080
+ if entry.wasStarted {
1081
+ launch.storage.markLaunchStarted(bundleId: entry.bundleId)
1082
+ }
1083
+ if entry.wasArmed {
1084
+ launch.isArmed = true
1085
+ }
1086
+ continue
1087
+ }
1088
+ if entry.wasArmed {
1089
+ launch.isArmed = true
1090
+ }
1091
+ mirror(launch)
1092
+ if entry.wasStarted {
1093
+ launch.storage.markLaunchStarted(bundleId: entry.bundleId)
1094
+ }
1095
+ }
1096
+ disarmed = []
1097
+ }
1098
+
1099
+ /// The instance a crash in progress was attributed to, if any.
1100
+ var attributedOwner: String? {
1101
+ withLock { attributedName }
1102
+ }
1103
+
1104
+ /// A fatal no runtime claims, which main survives with its in-process
1105
+ /// recovery: it may have been a named runtime's, so each named launch it
1106
+ /// leaves unverified rolls back at its next start. A launch verified
1107
+ /// later in this process ignores its marker.
1108
+ func chargeUnverifiedNamedLaunches() {
1109
+ charging {
1110
+ for launch in launches.values where !launch.isMain && launch.crashCountsAgainstLaunch {
1111
+ writeMarker(launch)
1112
+ }
1113
+ }
1114
+ }
1115
+
1116
+ /// An uncaught native exception: the attributed instance's marker, or
1117
+ /// every unverified launch's.
1118
+ func chargeUncaughtException() {
1119
+ charging {
1120
+ if let attributedName, let launch = launches[attributedName] {
1121
+ // Not a later launch of the claimed instance.
1122
+ if launch.isMonitoring, launch.generation == attributedGeneration {
1123
+ writeMarker(launch)
1124
+ }
1125
+ return
1126
+ }
1127
+ // The exception the forwarded fatal of an earlier runtime raises.
1128
+ if isEarlierRuntimeClaim {
1129
+ return
1130
+ }
1131
+ chargeUnattributedCrash()
1132
+ }
1133
+ }
1134
+
1135
+ /// The markers the native signal handler writes now, by the rule in
1136
+ /// HotUpdater.mm's `HotUpdaterWriteCrashMarkers`, with whether each asks
1137
+ /// for a rollback: main's slot always writes once main launched, as it
1138
+ /// always has (`shouldRollback: false` after its first content); a named
1139
+ /// slot writes only while its launch is unverified. Only the attributed
1140
+ /// slot writes while there is one.
1141
+ func nativeCrashMarkers() -> [(slot: Int, shouldRollback: Bool)] {
1142
+ withLock {
1143
+ (0..<Self.slotCount).compactMap { slot in
1144
+ guard slotNames[slot] != nil, slot == Self.mainSlot || slotFlags[slot] else { return nil }
1145
+ guard attributedSlot == Self.unattributed || attributedSlot == slot else { return nil }
1146
+ return (slot, slotFlags[slot])
1147
+ }
1148
+ }
1149
+ }
1150
+
1151
+ /// The runtime of `name`'s launch now has a module, which attributes its
1152
+ /// fatals from here on.
1153
+ ///
1154
+ /// `runtimeBundleURL` is the bundle the resolving runtime runs: a module
1155
+ /// of a runtime of an earlier launch, resolving late, doesn't resolve the
1156
+ /// current launch, whose own runtime may still render or fail before it
1157
+ /// has a module.
1158
+ func runtimeResolved(name: String, runtimeBundleURL: URL? = nil) {
1159
+ withLock {
1160
+ guard let launch = launches[name],
1161
+ isCurrentRuntime(of: launch, runtimeBundleURL: runtimeBundleURL) else {
1162
+ return
1163
+ }
1164
+ launch.hasResolvedRuntime = true
1165
+ }
1166
+ }
1167
+
1168
+ /// The owner of a fatal on a JavaScript thread no module claims yet, when
1169
+ /// only one launch can have thrown it. Any launch of this process whose
1170
+ /// runtime hasn't shown it finished loading may have: it hasn't shown its
1171
+ /// first content and, for a named one, its runtime has no module yet.
1172
+ /// Main counts until its first content. Whether a launch would roll back
1173
+ /// says nothing about whether it finished loading, so a built-in or
1174
+ /// retried launch counts too. Only when exactly one may have thrown it,
1175
+ /// and that one is a named launch that would roll back, is it that one's;
1176
+ /// otherwise nil, and the fatal is unattributed.
1177
+ func inferredFatalOwner() -> String? {
1178
+ withLock {
1179
+ let possibleOwners = possibleFatalOwners
1180
+ guard possibleOwners.count == 1,
1181
+ let owner = possibleOwners.first,
1182
+ !owner.isMain,
1183
+ owner.shouldRollback else {
1184
+ return nil
1185
+ }
1186
+ return owner.name
1187
+ }
1188
+ }
1189
+
1190
+ // MARK: - Private
1191
+
1192
+ private func chargeUnattributedCrash() {
1193
+ for launch in launches.values {
1194
+ // Main writes while it watches its launch, as it always has; a
1195
+ // marker of a verified launch carries `shouldRollback: false`.
1196
+ if launch.isMain ? launch.isMonitoring : launch.crashCountsAgainstLaunch {
1197
+ writeMarker(launch)
1198
+ }
1199
+ }
1200
+ }
1201
+
1202
+ /// Every marker goes through here: one that rolls back a named
1203
+ /// instance's current launch is also reported through
1204
+ /// `onRollbackCharged` once the charge finishes (`charging`).
1205
+ private func writeMarker(_ launch: Launch) {
1206
+ HotUpdaterCrashMarker.write(
1207
+ bundleId: launch.bundleId,
1208
+ shouldRollback: launch.shouldRollback,
1209
+ to: launch.markerURL
1210
+ )
1211
+ // Main too once named instances exist: its in-process reload is off
1212
+ // then, so its next same-process launch must roll back instead.
1213
+ if launch.shouldRollback, !launch.isMain || hasNamedInstanceUnlocked,
1214
+ !rolledBackNames.contains(launch.name) {
1215
+ rolledBackNames.append(launch.name)
1216
+ }
1217
+ }
1218
+
1219
+ /// Runs a charge under the lock, then reports outside it each named
1220
+ /// instance whose launch it marked for rollback.
1221
+ private func charging<T>(_ body: () -> T) -> T {
1222
+ let result: T
1223
+ let rolledBack: [String]
1224
+ lock.lock()
1225
+ result = body()
1226
+ rolledBack = rolledBackNames
1227
+ rolledBackNames = []
1228
+ let notify = onRollbackCharged
1229
+ lock.unlock()
1230
+ rolledBack.forEach { notify?($0) }
1231
+ return result
1232
+ }
1233
+
1234
+ /// Main always has slot 0; a named instance takes the next free slot when
1235
+ /// its launch first starts, and keeps it for the process.
1236
+ private func assignSlot(to launch: Launch) {
1237
+ guard launch.slot == nil else { return }
1238
+ let slot: Int?
1239
+ if launch.isMain {
1240
+ slot = Self.mainSlot
1241
+ } else {
1242
+ slot = (1..<Self.slotCount).first { slotNames[$0] == nil }
1243
+ }
1244
+ guard let slot else {
1245
+ NSLog("[HotUpdaterRecovery] No native crash slot left for \(launch.name)")
1246
+ return
1247
+ }
1248
+ slotNames[slot] = launch.name
1249
+ launch.slot = slot
1250
+ }
1251
+
1252
+ /// Publishes `launch`'s state to its native slot, before anything else
1253
+ /// touches the file system: the directory the signal handler writes the
1254
+ /// marker into is created after, once per launch.
1255
+ private func mirror(_ launch: Launch) {
1256
+ guard let slot = launch.slot else { return }
1257
+ let flag = launch.crashCountsAgainstLaunch
1258
+ slotFlags[slot] = flag
1259
+ sink.updateSlot(slot, markerPath: launch.markerURL.path, bundleId: launch.bundleId, shouldRollback: flag)
1260
+ if !launch.hasMarkerDirectory {
1261
+ try? FileManager.default.createDirectory(
1262
+ at: launch.markerURL.deletingLastPathComponent(),
1263
+ withIntermediateDirectories: true,
1264
+ attributes: nil
1265
+ )
1266
+ launch.hasMarkerDirectory = true
1267
+ }
1268
+ }
1269
+
1270
+ private func withLock<T>(_ body: () -> T) -> T {
1271
+ lock.lock()
1272
+ defer { lock.unlock() }
1273
+ return body()
1274
+ }
1275
+ }
1276
+
1277
+ /// Fabric surfaces that started before their runtime could be told apart.
1278
+ /// A surface starts before its bundle runs, so before the runtime's
1279
+ /// TurboModule, which is what tells whose runtime it is, exists. Each is
1280
+ /// kept until that module appears.
1281
+ final class HotUpdaterPendingSurfaceStarts {
1282
+ private struct Entry {
1283
+ weak var surface: AnyObject?
1284
+ let inForeground: Bool
1285
+ /// Each instance's launch when the surface started.
1286
+ let generations: [String: Int]
1287
+ }
1288
+
1289
+ private let lock = NSLock()
1290
+ private var entries: [Entry] = []
1291
+
1292
+ /// `generations` is `HotUpdaterRecoveryCoordinator.generations()` when
1293
+ /// the surface started.
1294
+ func add(_ surface: AnyObject, inForeground: Bool, generations: [String: Int] = [:]) {
1295
+ lock.lock()
1296
+ entries.append(Entry(surface: surface, inForeground: inForeground, generations: generations))
1297
+ lock.unlock()
1298
+ }
1299
+
1300
+ /// The surfaces `owner` attributes now, with whether each started in the
1301
+ /// foreground and the owner's launch it started in (nil when the owner
1302
+ /// had none then); they and the surfaces already gone are forgotten.
1303
+ func resolve(_ owner: (AnyObject) -> String?) -> [(name: String, surface: AnyObject, inForeground: Bool, generation: Int?)] {
1304
+ lock.lock()
1305
+ let current = entries
1306
+ lock.unlock()
1307
+
1308
+ var resolved: [(name: String, surface: AnyObject, inForeground: Bool, generation: Int?)] = []
1309
+ var settled: [ObjectIdentifier] = []
1310
+ for entry in current {
1311
+ guard let surface = entry.surface else { continue }
1312
+ if let name = owner(surface) {
1313
+ resolved.append((name, surface, entry.inForeground, entry.generations[name]))
1314
+ settled.append(ObjectIdentifier(surface))
1315
+ }
1316
+ }
1317
+
1318
+ lock.lock()
1319
+ entries.removeAll { entry in
1320
+ guard let surface = entry.surface else { return true }
1321
+ return settled.contains(ObjectIdentifier(surface))
1322
+ }
1323
+ lock.unlock()
1324
+ return resolved
1325
+ }
1326
+
1327
+ var count: Int {
1328
+ lock.lock()
1329
+ defer { lock.unlock() }
1330
+ return entries.filter { $0.surface != nil }.count
1331
+ }
1332
+ }
1333
+
1334
+ /// The views of one named instance that started while the app was in the
1335
+ /// background. Its launch starts when the app comes to the foreground, but
1336
+ /// only if one of them is still shown then: a view stopped or discarded in
1337
+ /// the meantime leaves the launch unstarted, until a view starts again.
1338
+ final class HotUpdaterBackgroundViewStarts {
1339
+ private struct Entry {
1340
+ weak var view: AnyObject?
1341
+ }
1342
+
1343
+ private let lock = NSLock()
1344
+ private var entries: [Entry] = []
1345
+
1346
+ var isEmpty: Bool {
1347
+ lock.lock()
1348
+ defer { lock.unlock() }
1349
+ return entries.isEmpty
1350
+ }
1351
+
1352
+ func add(_ view: AnyObject) {
1353
+ lock.lock()
1354
+ entries.append(Entry(view: view))
1355
+ lock.unlock()
1356
+ }
1357
+
1358
+ /// Whether a view that started in the background is shown now, by
1359
+ /// `isShown`. They are forgotten once one is shown, or after the `final`
1360
+ /// check of a foreground transition (didBecomeActive): a surface not yet
1361
+ /// in its window at willEnterForeground may be by then.
1362
+ func takeAnyShown(_ isShown: (AnyObject) -> Bool, final: Bool = true) -> Bool {
1363
+ lock.lock()
1364
+ let views = entries.compactMap(\.view)
1365
+ lock.unlock()
1366
+ let shown = views.contains { isShown($0) }
1367
+ if shown || final {
1368
+ removeAll()
1369
+ }
1370
+ return shown
1371
+ }
1372
+
1373
+ /// What a view of a named launch's runtime does when it starts, is
1374
+ /// attached to a window, or is told apart after either: its launch
1375
+ /// starts only once the view is shown while the app is in the
1376
+ /// foreground. A surface started for pre-rendering is not shown yet: it
1377
+ /// waits for its view to be attached (the attachment is reported as
1378
+ /// another start). In the background it waits for the foreground.
1379
+ enum ViewStart: Equatable {
1380
+ case arm
1381
+ case waitForAttachment
1382
+ case waitForForeground
1383
+ }
1384
+
1385
+ static func viewStart(inForeground: Bool, isShown: () -> Bool) -> ViewStart {
1386
+ guard inForeground else { return .waitForForeground }
1387
+ return isShown() ? .arm : .waitForAttachment
1388
+ }
1389
+
1390
+ func removeAll() {
1391
+ lock.lock()
1392
+ entries = []
1393
+ lock.unlock()
1394
+ }
1395
+ }
1396
+
1397
+ /// The instance whose bundle a JavaScript error was thrown in, read from its
1398
+ /// stack: `RCTJSStackTraceKey`'s frames, `{"file", "methodName",
1399
+ /// "lineNumber", "column"}`, whose `file` is the source URL the runtime
1400
+ /// evaluated (a `file://` URL or path of a staged bundle, the `main.jsbundle`
1401
+ /// embedded in a framework, or a development server URL). This attributes a
1402
+ /// fatal that no runtime module claims, such as one thrown while two
1403
+ /// instances' bundles both still evaluate.
1404
+ enum HotUpdaterJSStackOwner {
1405
+ /// The bundle locations of the frames; frames without one, such as
1406
+ /// `unknown` or native ones, are skipped.
1407
+ static func bundleURLs(in stack: Any?) -> [URL] {
1408
+ guard let frames = stack as? [Any] else { return [] }
1409
+ return frames.compactMap { frame in
1410
+ ((frame as? [String: Any])?["file"] as? String).flatMap(bundleURL)
1411
+ }
1412
+ }
1413
+
1414
+ static func bundleURL(_ file: String) -> URL? {
1415
+ var location = file.trimmingCharacters(in: .whitespaces)
1416
+ // Hermes prints bytecode locations as "address at <url>".
1417
+ let bytecodePrefix = "address at "
1418
+ if location.hasPrefix(bytecodePrefix) {
1419
+ location.removeFirst(bytecodePrefix.count)
1420
+ }
1421
+ if location.hasPrefix("/") {
1422
+ return URL(fileURLWithPath: location)
1423
+ }
1424
+ guard let url = URL(string: location),
1425
+ let scheme = url.scheme?.lowercased(),
1426
+ ["file", "http", "https"].contains(scheme) else {
1427
+ return nil
1428
+ }
1429
+ return url
1430
+ }
1431
+
1432
+ /// The one instance every frame with a bundle location belongs to, by
1433
+ /// `resolve` (the C2 router); nil when no frame has one or they name
1434
+ /// more than one instance.
1435
+ static func owner(of stack: Any?, resolve: (URL) -> String) -> String? {
1436
+ let names = Set(bundleURLs(in: stack).map(resolve))
1437
+ return names.count == 1 ? names.first : nil
1438
+ }
1439
+ }
1440
+
1441
+ /// The invalidation count of one instance's prepared launch. Marking a
1442
+ /// rollback only advances it, without the launch lock (a crash may be
1443
+ /// reported on a thread that holds it); a launch prepared at an older count
1444
+ /// is prepared again, so a preparation running concurrently can't publish a
1445
+ /// selection that outlives the rollback.
1446
+ final class HotUpdaterLaunchEpoch {
1447
+ private let lock = NSLock()
1448
+ private var value = 0
1449
+
1450
+ var current: Int {
1451
+ lock.lock()
1452
+ defer { lock.unlock() }
1453
+ return value
1454
+ }
1455
+
1456
+ func invalidate() {
1457
+ lock.lock()
1458
+ value += 1
1459
+ lock.unlock()
1460
+ }
1461
+
1462
+ /// Whether a selection prepared at `epoch` is still current.
1463
+ func isCurrent(_ epoch: Int) -> Bool {
1464
+ current == epoch
1465
+ }
1466
+
1467
+ /// Runs `prepare` until no rollback was marked while it ran, so a marker
1468
+ /// written during a preparation is consumed by the next one instead of
1469
+ /// handing out the failing bundle. After `maxAttempts`, the latest
1470
+ /// result, with the epoch it started at: the next request prepares again.
1471
+ func prepareCurrent<Value>(maxAttempts: Int = 3, _ prepare: () -> Value) -> (value: Value, epoch: Int) {
1472
+ let prepared = prepareCurrent(maxAttempts: maxAttempts, prepare: prepare, startMonitoring: { _ in () })
1473
+ return (prepared.value, prepared.epoch)
1474
+ }
1475
+
1476
+ /// `prepareCurrent`, with the launch's monitoring started in each
1477
+ /// attempt (which replaces the running launch's; a later attempt
1478
+ /// replaces it again). The check comes after: until monitoring is
1479
+ /// replaced, the running launch's fatal can still mark a rollback.
1480
+ func prepareCurrent<Value, Monitoring>(
1481
+ maxAttempts: Int = 3,
1482
+ prepare: () -> Value,
1483
+ startMonitoring: (Value) -> Monitoring
1484
+ ) -> (value: Value, monitoring: Monitoring, epoch: Int) {
1485
+ var attempts = 0
1486
+ while true {
1487
+ let epoch = current
1488
+ let value = prepare()
1489
+ let monitoring = startMonitoring(value)
1490
+ attempts += 1
1491
+ if isCurrent(epoch) || attempts >= maxAttempts {
1492
+ return (value, monitoring, epoch)
1493
+ }
1494
+ }
1495
+ }
1496
+ }
1497
+
1498
+ /// The newest launch generation main-queue work was scheduled for. Work
1499
+ /// queued for an older launch can run after a newer launch's: it then does
1500
+ /// nothing, so it never removes or resets what the newer launch set up.
1501
+ struct HotUpdaterLatestGeneration {
1502
+ private(set) var latest = 0
1503
+
1504
+ /// Whether work for `generation` is still the newest seen; records it.
1505
+ mutating func admit(_ generation: Int) -> Bool {
1506
+ guard generation >= latest else { return false }
1507
+ latest = generation
1508
+ return true
1509
+ }
1510
+ }
1511
+
1512
+ /// The launch each React Native runtime runs, by the runtime's module,
1513
+ /// recorded when the module tells its instance apart. A host can start a
1514
+ /// replacement launch with the same bundle while its previous runtime is
1515
+ /// still torn down; that runtime's late content then names the same
1516
+ /// instance and bundle, and only the launch it was recorded for tells it
1517
+ /// apart. Weakly: a runtime gone is forgotten.
1518
+ final class HotUpdaterRuntimeLaunches {
1519
+ private let lock = NSLock()
1520
+ private let generations = NSMapTable<AnyObject, NSNumber>.weakToStrongObjects()
1521
+
1522
+ func record(runtime: AnyObject, generation: Int) {
1523
+ lock.lock()
1524
+ generations.setObject(NSNumber(value: generation), forKey: runtime)
1525
+ lock.unlock()
1526
+ }
1527
+
1528
+ private let firstStarts = NSMapTable<AnyObject, NSDictionary>.weakToStrongObjects()
1529
+
1530
+ /// Each instance's launch at a surface's first start (`current`, for a
1531
+ /// first start), kept for its later starts, such as its attachment to a
1532
+ /// window after a replacement launch began: the launch a surface whose
1533
+ /// runtime has no record belongs to.
1534
+ func generationsOfFirstStart(_ surface: AnyObject, current: [String: Int]) -> [String: Int] {
1535
+ lock.lock()
1536
+ defer { lock.unlock() }
1537
+ if let first = firstStarts.object(forKey: surface) as? [String: Int] {
1538
+ return first
1539
+ }
1540
+ firstStarts.setObject(current as NSDictionary, forKey: surface)
1541
+ return current
1542
+ }
1543
+
1544
+ /// The launch `runtime` was recorded for; nil when unknown (no runtime
1545
+ /// told apart yet), which keeps it attributed by name and bundle alone.
1546
+ func generation(of runtime: AnyObject?) -> Int? {
1547
+ guard let runtime else { return nil }
1548
+ lock.lock()
1549
+ defer { lock.unlock() }
1550
+ return generations.object(forKey: runtime)?.intValue
1551
+ }
1552
+ }
1553
+