@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
|
@@ -283,10 +283,24 @@ public protocol BundleStorageService {
|
|
|
283
283
|
func getCachedBundleURL() -> URL?
|
|
284
284
|
func getFallbackBundleURL(bundle: Bundle) -> URL? // Synchronous as it's lightweight
|
|
285
285
|
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection
|
|
286
|
+
/// Settles what the previous process left once per process, before the
|
|
287
|
+
/// first read of the selection: a crash rollback and a launch that never
|
|
288
|
+
/// finished. `prepareLaunch` runs it first; a native update check that runs
|
|
289
|
+
/// before React Native starts runs it earlier. `pendingRecovery` is called
|
|
290
|
+
/// only when this run settles.
|
|
291
|
+
func settlePreviousLaunch(pendingRecovery: () -> PendingCrashRecovery?)
|
|
292
|
+
/// The bundle the next `prepareLaunch` would run, read without preparing
|
|
293
|
+
/// the launch. Nil is the built-in bundle. Call `settlePreviousLaunch`
|
|
294
|
+
/// first, or a bundle the next launch rolls back is returned.
|
|
295
|
+
func peekLaunchBundleId() -> String?
|
|
286
296
|
|
|
287
297
|
// Bundle update
|
|
288
|
-
|
|
289
|
-
|
|
298
|
+
/// Stages a bundle. `selection` is the Release selection the staging
|
|
299
|
+
/// commits; it travels with this staging, and the staging fails as stale
|
|
300
|
+
/// when it is no longer current.
|
|
301
|
+
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL?, assets: [String: ChangedAssetDescriptor], fileResolver: ArtifactFileResolver?, selection: PersistedSelection?, progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void)
|
|
302
|
+
/// Whether a staging of `selection` would still commit.
|
|
303
|
+
func isSelectionCurrent(_ selection: PersistedSelection) -> Bool
|
|
290
304
|
func acceptReleaseCatalog(catalogId: String, scopeKey: String, generation: Int64, catalogHash: String, channel: String, selectionContextHash: String) -> Bool
|
|
291
305
|
func getActiveUpdateState() -> [String: Any]
|
|
292
306
|
func isReleaseSelectionCurrent(catalogId: String, scopeKey: String, generation: Int64, catalogHash: String, channel: String, selectionContextHash: String) -> Bool
|
|
@@ -294,6 +308,12 @@ public protocol BundleStorageService {
|
|
|
294
308
|
|
|
295
309
|
// Rollback support
|
|
296
310
|
func markLaunchStarted(bundleId: String?)
|
|
311
|
+
/// Forgets that the launch of `bundleId` started, leaving it pending: the
|
|
312
|
+
/// next start launches it again instead of counting an unfinished launch.
|
|
313
|
+
/// For a launch whose process another instance's crash takes down.
|
|
314
|
+
/// Returns whether the launch had started.
|
|
315
|
+
@discardableResult
|
|
316
|
+
func clearLaunchStarted(bundleId: String?) -> Bool
|
|
297
317
|
func markLaunchCompleted(bundleId: String?)
|
|
298
318
|
func notifyAppReady() -> [String: Any]
|
|
299
319
|
func getCrashHistory() -> CrashedHistory
|
|
@@ -327,7 +347,11 @@ public protocol BundleStorageService {
|
|
|
327
347
|
}
|
|
328
348
|
|
|
329
349
|
public extension BundleStorageService {
|
|
330
|
-
func
|
|
350
|
+
func settlePreviousLaunch(pendingRecovery _: () -> PendingCrashRecovery?) {}
|
|
351
|
+
@discardableResult
|
|
352
|
+
func clearLaunchStarted(bundleId _: String?) -> Bool { false }
|
|
353
|
+
func peekLaunchBundleId() -> String? { nil }
|
|
354
|
+
func isSelectionCurrent(_: PersistedSelection) -> Bool { false }
|
|
331
355
|
func acceptReleaseCatalog(catalogId _: String, scopeKey _: String, generation _: Int64, catalogHash _: String, channel _: String, selectionContextHash _: String) -> Bool { false }
|
|
332
356
|
func getActiveUpdateState() -> [String: Any] { [:] }
|
|
333
357
|
func isReleaseSelectionCurrent(catalogId _: String, scopeKey _: String, generation _: Int64, catalogHash _: String, channel _: String, selectionContextHash _: String) -> Bool { false }
|
|
@@ -374,11 +398,16 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
374
398
|
private let downloadRetryDelay: TimeInterval
|
|
375
399
|
private let preferences: PreferencesService
|
|
376
400
|
private let isolationKey: String
|
|
401
|
+
/// Whose store this is: main's `Documents/bundle-store`, or a named
|
|
402
|
+
/// instance's own, with its public key and update strategy.
|
|
403
|
+
private let instance: HotUpdaterInstanceEnvironment
|
|
377
404
|
|
|
378
405
|
// Queue for potentially long-running sequences within updateBundle or for explicit background tasks.
|
|
379
406
|
private let fileOperationQueue: DispatchQueue
|
|
380
407
|
|
|
381
408
|
private var hasPreparedLaunch = false
|
|
409
|
+
private let settleLock = NSLock()
|
|
410
|
+
private var hasSettledPreviousLaunch = false
|
|
382
411
|
private var currentLaunchReport: LaunchReport?
|
|
383
412
|
// The staged bundle this process launched before its first content. Only that
|
|
384
413
|
// launch is pending; a bundle staged later waits for the next launch.
|
|
@@ -393,16 +422,20 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
393
422
|
private let builtInAssetResolver: BuiltInAssetResolver
|
|
394
423
|
private let updateStrategyProvider: () -> UpdateStrategy
|
|
395
424
|
private let releaseStateLock = NSRecursiveLock()
|
|
396
|
-
private var pendingInstallSelections: [String: PersistedSelection] = [:]
|
|
397
425
|
|
|
398
|
-
|
|
399
|
-
|
|
426
|
+
/// App-wide: every instance's storage reads the same install identity
|
|
427
|
+
/// file, so one lock and cache per file keep two instances that start at
|
|
428
|
+
/// once from creating different ids.
|
|
429
|
+
private static let installIdentityLock = NSLock()
|
|
430
|
+
private static var installIdentities: [String: InstallIdentity] = [:]
|
|
400
431
|
|
|
401
432
|
private let protectedBundleStoreEntries: Set<String> = [
|
|
402
433
|
BundleMetadata.metadataFilename,
|
|
403
434
|
CrashedHistory.crashedHistoryFilename,
|
|
404
435
|
LaunchReport.launchReportFilename,
|
|
405
436
|
"builtin-index-v1.json",
|
|
437
|
+
// Written when a launch crashes, read at the next start.
|
|
438
|
+
HotUpdaterCrashMarker.filename,
|
|
406
439
|
]
|
|
407
440
|
|
|
408
441
|
private func createDiffProgressFiles(
|
|
@@ -725,7 +758,8 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
725
758
|
throw BundleStorageError.signatureVerificationFailed(.fileHashMismatch)
|
|
726
759
|
}
|
|
727
760
|
|
|
728
|
-
|
|
761
|
+
let publicKey = instance.publicKey()
|
|
762
|
+
guard SignatureVerifier.isSigningEnabled(publicKey: publicKey) else {
|
|
729
763
|
return
|
|
730
764
|
}
|
|
731
765
|
|
|
@@ -735,7 +769,8 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
735
769
|
|
|
736
770
|
switch SignatureVerifier.verifyHashSignature(
|
|
737
771
|
fileHash: asset.fileHash,
|
|
738
|
-
signatureBase64: signature
|
|
772
|
+
signatureBase64: signature,
|
|
773
|
+
publicKey: publicKey
|
|
739
774
|
) {
|
|
740
775
|
case .success:
|
|
741
776
|
return
|
|
@@ -748,13 +783,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
748
783
|
downloadService: DownloadService,
|
|
749
784
|
preferences: PreferencesService,
|
|
750
785
|
isolationKey: String,
|
|
786
|
+
instance: HotUpdaterInstanceEnvironment = .main,
|
|
751
787
|
builtInBundleIdProvider: @escaping () -> String = { "" },
|
|
752
788
|
builtInAssetResolver: BuiltInAssetResolver? = nil,
|
|
753
|
-
updateStrategyProvider:
|
|
754
|
-
let fingerprintHash = HotUpdaterConfig.shared.fingerprintHash
|
|
755
|
-
?? Bundle.main.object(forInfoDictionaryKey: "HOT_UPDATER_FINGERPRINT_HASH") as? String
|
|
756
|
-
return (fingerprintHash?.isEmpty == false) ? .fingerprint : .appVersion
|
|
757
|
-
},
|
|
789
|
+
updateStrategyProvider: (() -> UpdateStrategy)? = nil,
|
|
758
790
|
downloadRetryDelay: TimeInterval = 1) {
|
|
759
791
|
|
|
760
792
|
self.fileSystem = fileSystem
|
|
@@ -762,13 +794,14 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
762
794
|
self.downloadService = downloadService
|
|
763
795
|
self.preferences = preferences
|
|
764
796
|
self.isolationKey = isolationKey
|
|
797
|
+
self.instance = instance
|
|
765
798
|
self.builtInBundleIdProvider = builtInBundleIdProvider
|
|
766
799
|
self.builtInAssetResolver = builtInAssetResolver ?? IOSBuiltInAssetResolver(
|
|
767
|
-
cacheURL: URL(fileURLWithPath: fileSystem.documentsPath())
|
|
768
|
-
.appendingPathComponent("
|
|
769
|
-
|
|
800
|
+
cacheURL: URL(fileURLWithPath: instance.bundleStorePath(documentsPath: fileSystem.documentsPath()), isDirectory: true)
|
|
801
|
+
.appendingPathComponent("builtin-index-v1.json"),
|
|
802
|
+
defaultBundle: { instance.bundle }
|
|
770
803
|
)
|
|
771
|
-
self.updateStrategyProvider = updateStrategyProvider
|
|
804
|
+
self.updateStrategyProvider = updateStrategyProvider ?? { instance.updateStrategy() }
|
|
772
805
|
|
|
773
806
|
// Create queue for file operations
|
|
774
807
|
self.fileOperationQueue = DispatchQueue(label: "com.hotupdater.fileoperations",
|
|
@@ -860,21 +893,21 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
860
893
|
/// Loads the install identity, creating it once when its file is missing.
|
|
861
894
|
@discardableResult
|
|
862
895
|
private func loadOrCreateInstallIdentity() -> InstallIdentity {
|
|
863
|
-
installIdentityLock.lock()
|
|
864
|
-
defer { installIdentityLock.unlock() }
|
|
896
|
+
Self.installIdentityLock.lock()
|
|
897
|
+
defer { Self.installIdentityLock.unlock() }
|
|
865
898
|
|
|
866
|
-
if let currentInstallIdentity {
|
|
867
|
-
return currentInstallIdentity
|
|
868
|
-
}
|
|
869
899
|
let file = installIdentityFileURL()
|
|
900
|
+
if let identity = Self.installIdentities[file.path] {
|
|
901
|
+
return identity
|
|
902
|
+
}
|
|
870
903
|
if let identity = InstallIdentity.load(from: file) {
|
|
871
|
-
|
|
904
|
+
Self.installIdentities[file.path] = identity
|
|
872
905
|
return identity
|
|
873
906
|
}
|
|
874
907
|
|
|
875
908
|
let identity = InstallIdentity()
|
|
876
909
|
// Keep the new id for this process even when it cannot be saved.
|
|
877
|
-
|
|
910
|
+
Self.installIdentities[file.path] = identity
|
|
878
911
|
_ = identity.save(to: file)
|
|
879
912
|
return identity
|
|
880
913
|
}
|
|
@@ -1285,11 +1318,12 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1285
1318
|
return metadata.verificationPending && metadata.stagingBundleId != nil
|
|
1286
1319
|
}
|
|
1287
1320
|
|
|
1288
|
-
private func prepareMetadataForNewStagingBundle(
|
|
1321
|
+
private func prepareMetadataForNewStagingBundle(
|
|
1322
|
+
_ metadata: BundleMetadata,
|
|
1323
|
+
bundleId: String,
|
|
1324
|
+
incomingSelection: PersistedSelection?
|
|
1325
|
+
) -> BundleMetadata? {
|
|
1289
1326
|
let currentVerifiedBundleId = getCurrentVerifiedBundleId(metadata).flatMap { $0 == bundleId ? nil : $0 }
|
|
1290
|
-
releaseStateLock.lock()
|
|
1291
|
-
let incomingSelection = pendingInstallSelections.removeValue(forKey: bundleId)
|
|
1292
|
-
releaseStateLock.unlock()
|
|
1293
1327
|
if let incomingSelection, !isSelectionCurrent(incomingSelection) {
|
|
1294
1328
|
return nil
|
|
1295
1329
|
}
|
|
@@ -1325,7 +1359,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1325
1359
|
toBundleId: stagingSelection.bundleId
|
|
1326
1360
|
),
|
|
1327
1361
|
highestSeenCatalogs: metadata.highestSeenCatalogs,
|
|
1328
|
-
currentSelectionContexts: metadata.currentSelectionContexts,
|
|
1362
|
+
currentSelectionContexts: contextsAfterCommitting(metadata.currentSelectionContexts, incomingSelection),
|
|
1329
1363
|
updatedAt: Date().timeIntervalSince1970 * 1000
|
|
1330
1364
|
)
|
|
1331
1365
|
}
|
|
@@ -1424,6 +1458,21 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1424
1458
|
"\(catalogId)|\(scopeKey)"
|
|
1425
1459
|
}
|
|
1426
1460
|
|
|
1461
|
+
/// The selection contexts that stay current once `selection` commits: only
|
|
1462
|
+
/// its own scope's, or none for a staging without a Release selection. A
|
|
1463
|
+
/// check made before this commit, of another scope such as another channel,
|
|
1464
|
+
/// no longer stages over it.
|
|
1465
|
+
private func contextsAfterCommitting(
|
|
1466
|
+
_ contexts: [String: String],
|
|
1467
|
+
_ selection: PersistedSelection?
|
|
1468
|
+
) -> [String: String] {
|
|
1469
|
+
guard let catalogId = selection?.catalogId, let scopeKey = selection?.scopeKey else {
|
|
1470
|
+
return [:]
|
|
1471
|
+
}
|
|
1472
|
+
let stateKey = releaseScopeStateKey(catalogId: catalogId, scopeKey: scopeKey)
|
|
1473
|
+
return contexts.filter { $0.key == stateKey }
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1427
1476
|
private func selectionContextValue(channel: String, selectionContextHash: String) -> String {
|
|
1428
1477
|
"\(channel)\n\(selectionContextHash)"
|
|
1429
1478
|
}
|
|
@@ -1511,7 +1560,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1511
1560
|
)
|
|
1512
1561
|
}
|
|
1513
1562
|
|
|
1514
|
-
|
|
1563
|
+
func isSelectionCurrent(_ selection: PersistedSelection) -> Bool {
|
|
1515
1564
|
guard let catalogId = selection.catalogId,
|
|
1516
1565
|
let scopeKey = selection.scopeKey,
|
|
1517
1566
|
let generation = selection.generation,
|
|
@@ -1529,13 +1578,6 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1529
1578
|
)
|
|
1530
1579
|
}
|
|
1531
1580
|
|
|
1532
|
-
func stageReleaseSelection(_ selection: PersistedSelection) -> Bool {
|
|
1533
|
-
releaseStateLock.lock()
|
|
1534
|
-
defer { releaseStateLock.unlock() }
|
|
1535
|
-
guard isSelectionCurrent(selection) else { return false }
|
|
1536
|
-
pendingInstallSelections[selection.bundleId] = selection
|
|
1537
|
-
return true
|
|
1538
|
-
}
|
|
1539
1581
|
|
|
1540
1582
|
func commitReleaseSelection(_ selection: PersistedSelection) -> Bool {
|
|
1541
1583
|
releaseStateLock.lock()
|
|
@@ -1547,6 +1589,15 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1547
1589
|
|| metadata.stagingBundleId == selection.bundleId {
|
|
1548
1590
|
metadata.stagingBundleId = selection.bundleId
|
|
1549
1591
|
metadata.stagingSelection = selection
|
|
1592
|
+
// The launch of the staged bundle reports this Release as applied.
|
|
1593
|
+
if let transition = metadata.pendingSelectionTransition, transition.toBundleId == selection.bundleId {
|
|
1594
|
+
metadata.pendingSelectionTransition = PendingSelectionTransition(
|
|
1595
|
+
fromReleaseId: transition.fromReleaseId,
|
|
1596
|
+
fromBundleId: transition.fromBundleId,
|
|
1597
|
+
toReleaseId: selection.releaseId,
|
|
1598
|
+
toBundleId: transition.toBundleId
|
|
1599
|
+
)
|
|
1600
|
+
}
|
|
1550
1601
|
} else if metadata.stableSelection?.bundleId == selection.bundleId
|
|
1551
1602
|
|| metadata.stableBundleId == selection.bundleId {
|
|
1552
1603
|
metadata.stableBundleId = selection.bundleId
|
|
@@ -1566,6 +1617,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1566
1617
|
metadata.pendingSelectionTransition = nil
|
|
1567
1618
|
}
|
|
1568
1619
|
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
1620
|
+
metadata.currentSelectionContexts = contextsAfterCommitting(metadata.currentSelectionContexts, selection)
|
|
1569
1621
|
guard saveMetadata(metadata) else { return false }
|
|
1570
1622
|
do {
|
|
1571
1623
|
try preferences.setItem(
|
|
@@ -1608,7 +1660,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1608
1660
|
* @return Result with the directory path or error
|
|
1609
1661
|
*/
|
|
1610
1662
|
func bundleStoreDir() -> Result<String, Error> {
|
|
1611
|
-
let path = (fileSystem.documentsPath()
|
|
1663
|
+
let path = instance.bundleStorePath(documentsPath: fileSystem.documentsPath())
|
|
1612
1664
|
return ensureDirectoryExists(path: path)
|
|
1613
1665
|
}
|
|
1614
1666
|
|
|
@@ -1618,7 +1670,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1618
1670
|
* @return Result with the directory path or error
|
|
1619
1671
|
*/
|
|
1620
1672
|
func tempDir() -> Result<String, Error> {
|
|
1621
|
-
let path = (fileSystem.documentsPath()
|
|
1673
|
+
let path = instance.tempPath(documentsPath: fileSystem.documentsPath())
|
|
1622
1674
|
return ensureDirectoryExists(path: path)
|
|
1623
1675
|
}
|
|
1624
1676
|
|
|
@@ -1998,20 +2050,69 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1998
2050
|
)
|
|
1999
2051
|
}
|
|
2000
2052
|
|
|
2001
|
-
func
|
|
2002
|
-
|
|
2053
|
+
func peekLaunchBundleId() -> String? {
|
|
2054
|
+
// Reads the store without bundleStoreDir(), which creates it.
|
|
2055
|
+
let storeDir = instance.bundleStorePath(documentsPath: fileSystem.documentsPath())
|
|
2056
|
+
let metadataFile = URL(fileURLWithPath: storeDir).appendingPathComponent(BundleMetadata.metadataFilename)
|
|
2057
|
+
guard let metadata = BundleMetadata.load(from: metadataFile, expectedIsolationKey: isolationKey) else {
|
|
2058
|
+
return nil
|
|
2059
|
+
}
|
|
2060
|
+
func hasBundleFile(_ bundleId: String) -> Bool {
|
|
2061
|
+
let directory = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
2062
|
+
guard case .success(let bundlePath) = findBundleFile(in: directory, expectedBundleId: bundleId) else {
|
|
2063
|
+
return false
|
|
2064
|
+
}
|
|
2065
|
+
return bundlePath != nil
|
|
2066
|
+
}
|
|
2067
|
+
if let stagingBundleId = metadata.stagingBundleId, hasBundleFile(stagingBundleId) {
|
|
2068
|
+
return stagingBundleId
|
|
2069
|
+
}
|
|
2070
|
+
if let stableBundleId = metadata.stableBundleId, hasBundleFile(stableBundleId) {
|
|
2071
|
+
return stableBundleId
|
|
2072
|
+
}
|
|
2073
|
+
return nil
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
func settlePreviousLaunch(pendingRecovery: () -> PendingCrashRecovery?) {
|
|
2077
|
+
settleLock.lock()
|
|
2078
|
+
defer { settleLock.unlock() }
|
|
2079
|
+
guard !hasSettledPreviousLaunch else { return }
|
|
2080
|
+
hasSettledPreviousLaunch = true
|
|
2081
|
+
// Cleared before the rollbacks below, so the launch reports their RECOVERED.
|
|
2003
2082
|
saveLaunchReport(nil)
|
|
2004
|
-
|
|
2005
|
-
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
2083
|
+
applyPendingRecoveryIfNeeded(pendingRecovery())
|
|
2006
2084
|
// Only a new storage instance consumes an unfinished launch. Repeated bundle
|
|
2007
2085
|
// lookups or bridge reloads in this process must not reject its own launch.
|
|
2008
|
-
if
|
|
2009
|
-
let metadata = loadMetadataOrNull(),
|
|
2086
|
+
if let metadata = loadMetadataOrNull(),
|
|
2010
2087
|
metadata.verificationPending,
|
|
2011
2088
|
metadata.launchInProgress,
|
|
2012
2089
|
let stagingBundleId = metadata.stagingBundleId {
|
|
2013
2090
|
rollbackPendingBundle(stagingBundleId, unfinishedLaunch: true)
|
|
2014
2091
|
}
|
|
2092
|
+
// A pending bundle whose file is gone rolls back, as selectLaunch does.
|
|
2093
|
+
if let metadata = loadMetadataOrNull(),
|
|
2094
|
+
metadata.verificationPending,
|
|
2095
|
+
let stagingBundleId = metadata.stagingBundleId,
|
|
2096
|
+
case .success(let storeDir) = bundleStoreDir() {
|
|
2097
|
+
let stagingBundleDir = (storeDir as NSString).appendingPathComponent(stagingBundleId)
|
|
2098
|
+
if case .success(let bundlePath) = findBundleFile(in: stagingBundleDir, expectedBundleId: stagingBundleId),
|
|
2099
|
+
bundlePath != nil {
|
|
2100
|
+
return
|
|
2101
|
+
}
|
|
2102
|
+
rollbackPendingBundle(stagingBundleId)
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection {
|
|
2107
|
+
builtInAssetResolver.use(bundle: bundle)
|
|
2108
|
+
loadOrCreateInstallIdentity()
|
|
2109
|
+
if hasPreparedLaunch {
|
|
2110
|
+
// A later launch in this process, after a reset or a reload.
|
|
2111
|
+
saveLaunchReport(nil)
|
|
2112
|
+
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
2113
|
+
} else {
|
|
2114
|
+
settlePreviousLaunch { pendingRecovery }
|
|
2115
|
+
}
|
|
2015
2116
|
hasPreparedLaunch = true
|
|
2016
2117
|
// A launch is recorded only once it can show UI (markLaunchStarted).
|
|
2017
2118
|
let selection = selectLaunch(bundle: bundle)
|
|
@@ -2029,7 +2130,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2029
2130
|
* @param progressHandler Callback for download and extraction progress (0.0 to 1.0)
|
|
2030
2131
|
* @param completion Callback with result of the operation
|
|
2031
2132
|
*/
|
|
2032
|
-
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL? = nil, assets: [String: ChangedAssetDescriptor], fileResolver: ArtifactFileResolver? = nil, progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void) {
|
|
2133
|
+
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL? = nil, assets: [String: ChangedAssetDescriptor], fileResolver: ArtifactFileResolver? = nil, selection: PersistedSelection? = nil, progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void) {
|
|
2033
2134
|
// Check if bundle is in crashed history
|
|
2034
2135
|
if getCrashHistory().contains(bundleId) {
|
|
2035
2136
|
NSLog("[BundleStorage] Bundle '\(bundleId)' is in crashed history, rejecting update")
|
|
@@ -2053,6 +2154,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2053
2154
|
archiveUrl: archiveUrl,
|
|
2054
2155
|
changedAssets: assets,
|
|
2055
2156
|
fileResolver: fileResolver,
|
|
2157
|
+
selection: selection,
|
|
2056
2158
|
storeDir: storeDir,
|
|
2057
2159
|
progressHandler: progressHandler,
|
|
2058
2160
|
completion: completion
|
|
@@ -2079,6 +2181,16 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2079
2181
|
guard let file = files[path], let descriptor = downloads[path] else {
|
|
2080
2182
|
throw BundleStorageError.invalidBundle
|
|
2081
2183
|
}
|
|
2184
|
+
// A download needs an absolute HTTP(S) URL with a host.
|
|
2185
|
+
guard ["http", "https"].contains(file.url.scheme?.lowercased() ?? ""),
|
|
2186
|
+
file.url.host?.isEmpty == false else {
|
|
2187
|
+
throw UpdateFailureError(
|
|
2188
|
+
underlying: NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2189
|
+
NSLocalizedDescriptionKey: "Invalid '\(path)' provided: \(file.url.absoluteString)"
|
|
2190
|
+
]),
|
|
2191
|
+
failure: UpdateFailure.invalidResponse.with(resource: .file)
|
|
2192
|
+
)
|
|
2193
|
+
}
|
|
2082
2194
|
downloads[path] = ChangedAssetDescriptor(
|
|
2083
2195
|
fileUrl: file.url,
|
|
2084
2196
|
fileHash: descriptor.fileHash,
|
|
@@ -2481,6 +2593,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2481
2593
|
|
|
2482
2594
|
private func promoteStagedBundle(
|
|
2483
2595
|
bundleId: String,
|
|
2596
|
+
selection: PersistedSelection?,
|
|
2484
2597
|
stagingDirectory: String,
|
|
2485
2598
|
finalDirectory: String,
|
|
2486
2599
|
bundlePathInStaging: String
|
|
@@ -2488,7 +2601,11 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2488
2601
|
releaseStateLock.lock()
|
|
2489
2602
|
defer { releaseStateLock.unlock() }
|
|
2490
2603
|
let currentMetadata = loadMetadataOrNull() ?? createInitialMetadata()
|
|
2491
|
-
guard let updatedMetadata = prepareMetadataForNewStagingBundle(
|
|
2604
|
+
guard let updatedMetadata = prepareMetadataForNewStagingBundle(
|
|
2605
|
+
currentMetadata,
|
|
2606
|
+
bundleId: bundleId,
|
|
2607
|
+
incomingSelection: selection
|
|
2608
|
+
) else {
|
|
2492
2609
|
throw BundleStorageError.unknown(StaleReleaseSelectionError())
|
|
2493
2610
|
}
|
|
2494
2611
|
let finalBundlePath = try resolveBundlePathAfterMove(
|
|
@@ -2535,6 +2652,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2535
2652
|
archiveUrl: URL?,
|
|
2536
2653
|
changedAssets: [String: ChangedAssetDescriptor],
|
|
2537
2654
|
fileResolver: ArtifactFileResolver?,
|
|
2655
|
+
selection: PersistedSelection?,
|
|
2538
2656
|
storeDir: String,
|
|
2539
2657
|
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
2540
2658
|
completion: @escaping (Result<UpdateDelivery, Error>) -> Void
|
|
@@ -2604,7 +2722,8 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2604
2722
|
failurePhase = .verification
|
|
2605
2723
|
let manifestVerificationResult = SignatureVerifier.verifyBundle(
|
|
2606
2724
|
fileURL: URL(fileURLWithPath: tempManifestPath),
|
|
2607
|
-
fileHash: manifestFileHash
|
|
2725
|
+
fileHash: manifestFileHash,
|
|
2726
|
+
publicKey: instance.publicKey()
|
|
2608
2727
|
)
|
|
2609
2728
|
guard case .success = manifestVerificationResult else {
|
|
2610
2729
|
if case .failure(let error) = manifestVerificationResult {
|
|
@@ -2831,6 +2950,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2831
2950
|
|
|
2832
2951
|
let updatedMetadata = try promoteStagedBundle(
|
|
2833
2952
|
bundleId: bundleId,
|
|
2953
|
+
selection: selection,
|
|
2834
2954
|
stagingDirectory: tmpDir,
|
|
2835
2955
|
finalDirectory: realDir,
|
|
2836
2956
|
bundlePathInStaging: bundlePathInTmp
|
|
@@ -2881,6 +3001,21 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2881
3001
|
_ = saveMetadata(metadata)
|
|
2882
3002
|
}
|
|
2883
3003
|
|
|
3004
|
+
@discardableResult
|
|
3005
|
+
func clearLaunchStarted(bundleId: String?) -> Bool {
|
|
3006
|
+
releaseStateLock.lock()
|
|
3007
|
+
defer { releaseStateLock.unlock() }
|
|
3008
|
+
guard let bundleId,
|
|
3009
|
+
var metadata = loadMetadataOrNull(),
|
|
3010
|
+
metadata.verificationPending,
|
|
3011
|
+
metadata.launchInProgress,
|
|
3012
|
+
metadata.stagingBundleId == bundleId else {
|
|
3013
|
+
return false
|
|
3014
|
+
}
|
|
3015
|
+
metadata.launchInProgress = false
|
|
3016
|
+
return saveMetadata(metadata)
|
|
3017
|
+
}
|
|
3018
|
+
|
|
2884
3019
|
func markLaunchCompleted(bundleId: String?) {
|
|
2885
3020
|
// Before the guard: content from the built-in bundle (nil) also counts.
|
|
2886
3021
|
readyInterruptedLaunchRetry(bundleId)
|
|
@@ -3077,6 +3212,9 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
3077
3212
|
clearedMetadata.verificationPending = false
|
|
3078
3213
|
clearedMetadata.pendingTransition = nil
|
|
3079
3214
|
clearedMetadata.pendingSelectionTransition = nil
|
|
3215
|
+
// Selections made before the reset no longer stage; the high-water
|
|
3216
|
+
// marks stay, so an older catalog is still refused.
|
|
3217
|
+
clearedMetadata.currentSelectionContexts = [:]
|
|
3080
3218
|
clearedMetadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
3081
3219
|
|
|
3082
3220
|
guard saveMetadata(clearedMetadata) else {
|
|
@@ -2,6 +2,10 @@ import Foundation
|
|
|
2
2
|
import UIKit
|
|
3
3
|
|
|
4
4
|
final class CohortService {
|
|
5
|
+
/// App-wide: every instance's service reads and creates the same stored
|
|
6
|
+
/// cohort, so instances that check at once never pick different ones.
|
|
7
|
+
private static let lock = NSLock()
|
|
8
|
+
|
|
5
9
|
private let userDefaults: UserDefaults
|
|
6
10
|
|
|
7
11
|
private let cohortKey = "HotUpdater_CustomCohort"
|
|
@@ -41,10 +45,14 @@ final class CohortService {
|
|
|
41
45
|
if cohort.isEmpty {
|
|
42
46
|
return
|
|
43
47
|
}
|
|
48
|
+
Self.lock.lock()
|
|
49
|
+
defer { Self.lock.unlock() }
|
|
44
50
|
userDefaults.set(cohort, forKey: cohortKey)
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
func getCohort() -> String {
|
|
54
|
+
Self.lock.lock()
|
|
55
|
+
defer { Self.lock.unlock() }
|
|
48
56
|
if let cohort = userDefaults.string(forKey: cohortKey), !cohort.isEmpty {
|
|
49
57
|
return cohort
|
|
50
58
|
}
|