@hot-updater/react-native 1.0.7 → 1.1.0
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 +492 -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/ReactIntegrationManagerBase.kt +0 -34
- package/android/src/newarch/ReactIntegrationManager.kt +0 -185
- 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
|
+
|