@hot-updater/react-native 1.0.0-rc.4 → 1.0.0-rc.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +13 -2
- package/android/proguard-rules.pro +8 -2
- package/android/src/bridge/BridgeReactInstance.kt +38 -0
- package/android/src/bridgeless/BridgeReactInstance.kt +24 -0
- package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +94 -0
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +1064 -844
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +50 -11
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +287 -22
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +46 -30
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +75 -192
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
- package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
- package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
- package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
- package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +127 -50
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
- package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
- package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
- package/android/src/newarch/HotUpdater.kt +7 -9
- package/android/src/newarch/HotUpdaterModule.kt +109 -84
- package/android/src/newarch/JavaScriptExceptionHooks.kt +48 -0
- package/android/src/newarch/ReactIntegrationManager.kt +6 -32
- package/android/src/oldarch/HotUpdater.kt +7 -9
- package/android/src/oldarch/HotUpdaterModule.kt +98 -74
- package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
- package/android/src/oldarch/JavaScriptExceptionHooks.kt +71 -0
- package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1032 -1116
- package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -54
- package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
- package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +32 -67
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +229 -73
- package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
- package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
- package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +119 -71
- package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
- package/lib/commonjs/appReady.js +102 -0
- package/lib/commonjs/appReady.js.map +1 -0
- package/lib/commonjs/checkForUpdate.js +120 -68
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/clientPlugin.js +13 -0
- package/lib/commonjs/clientPlugin.js.map +1 -0
- package/lib/commonjs/error.js +4 -13
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchJSON.js +16 -27
- package/lib/commonjs/fetchJSON.js.map +1 -1
- package/lib/commonjs/fetchUpdateResponse.js +60 -0
- package/lib/commonjs/fetchUpdateResponse.js.map +1 -0
- package/lib/commonjs/httpClient.js +63 -94
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/index.js +71 -18
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +74 -60
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/pluginHost.js +45 -0
- package/lib/commonjs/pluginHost.js.map +1 -0
- package/lib/commonjs/releaseCatalogCache.js +36 -87
- package/lib/commonjs/releaseCatalogCache.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/sdkVersion.js.map +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +3 -10
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/updateError.js +145 -0
- package/lib/commonjs/updateError.js.map +1 -0
- package/lib/commonjs/wrap.js +43 -26
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/appReady.js +95 -0
- package/lib/module/appReady.js.map +1 -0
- package/lib/module/checkForUpdate.js +113 -63
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/clientPlugin.js +4 -0
- package/lib/module/clientPlugin.js.map +1 -0
- package/lib/module/error.js +4 -13
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchJSON.js +14 -26
- package/lib/module/fetchJSON.js.map +1 -1
- package/lib/module/fetchUpdateResponse.js +55 -0
- package/lib/module/fetchUpdateResponse.js.map +1 -0
- package/lib/module/httpClient.js +62 -93
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/index.js +58 -20
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +67 -53
- package/lib/module/native.js.map +1 -1
- package/lib/module/pluginHost.js +36 -0
- package/lib/module/pluginHost.js.map +1 -0
- package/lib/module/releaseCatalogCache.js +32 -82
- package/lib/module/releaseCatalogCache.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/sdkVersion.js.map +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +3 -10
- package/lib/module/store.js.map +1 -1
- package/lib/module/updateError.js +139 -0
- package/lib/module/updateError.js.map +1 -0
- package/lib/module/wrap.js +43 -26
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/appReady.d.ts +21 -0
- package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
- package/lib/typescript/commonjs/checkForUpdate.d.ts +16 -4
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/clientPlugin.d.ts +2 -0
- package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
- package/lib/typescript/commonjs/error.d.ts +4 -13
- package/lib/typescript/commonjs/error.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchJSON.d.ts +5 -5
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchUpdateResponse.d.ts +14 -0
- package/lib/typescript/commonjs/fetchUpdateResponse.d.ts.map +1 -0
- package/lib/typescript/commonjs/httpClient.d.ts +4 -33
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -8
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +29 -22
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/pluginHost.d.ts +4 -0
- package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts +8 -14
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +35 -47
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +1 -3
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +1 -1
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/updateError.d.ts +26 -0
- package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +6 -7
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/appReady.d.ts +21 -0
- package/lib/typescript/module/appReady.d.ts.map +1 -0
- package/lib/typescript/module/checkForUpdate.d.ts +16 -4
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/clientPlugin.d.ts +2 -0
- package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
- package/lib/typescript/module/error.d.ts +4 -13
- package/lib/typescript/module/error.d.ts.map +1 -1
- package/lib/typescript/module/fetchJSON.d.ts +5 -5
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/module/fetchUpdateResponse.d.ts +14 -0
- package/lib/typescript/module/fetchUpdateResponse.d.ts.map +1 -0
- package/lib/typescript/module/httpClient.d.ts +4 -33
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -8
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +29 -22
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/pluginHost.d.ts +4 -0
- package/lib/typescript/module/pluginHost.d.ts.map +1 -0
- package/lib/typescript/module/releaseCatalogCache.d.ts +8 -14
- package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +35 -47
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +1 -3
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +1 -1
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/updateError.d.ts +26 -0
- package/lib/typescript/module/updateError.d.ts.map +1 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +6 -7
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +7 -4
- package/src/appReady.ts +141 -0
- package/src/checkForUpdate.ts +145 -86
- package/src/clientPlugin.ts +18 -0
- package/src/error.ts +4 -14
- package/src/fetchJSON.ts +14 -30
- package/src/fetchUpdateResponse.ts +60 -0
- package/src/httpClient.ts +88 -159
- package/src/index.ts +90 -24
- package/src/native.ts +98 -102
- package/src/pluginHost.ts +47 -0
- package/src/releaseCatalogCache.ts +43 -172
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +34 -48
- package/src/store.ts +2 -23
- package/src/types.ts +1 -1
- package/src/updateError.ts +167 -0
- package/src/wrap.tsx +57 -26
- package/src/wrap.types.ts +6 -7
- package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
- package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
- package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -799
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -273
- package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
- package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -111
- package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
- package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
- package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
- package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
- package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
- package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
- package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
- package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
- package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
- package/lib/commonjs/catalogCacheNative.spec.js +0 -31
- package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -616
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
- package/lib/commonjs/httpClient.spec.js +0 -102
- package/lib/commonjs/httpClient.spec.js.map +0 -1
- package/lib/commonjs/index.spec.js +0 -302
- package/lib/commonjs/index.spec.js.map +0 -1
- package/lib/commonjs/iosInterop.spec.js +0 -18
- package/lib/commonjs/iosInterop.spec.js.map +0 -1
- package/lib/commonjs/native.spec.js +0 -883
- package/lib/commonjs/native.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -218
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.js +0 -133
- package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -246
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js +0 -24
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -117
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/commonjs/releaseCatalogCache.spec.js +0 -195
- package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
- package/lib/commonjs/store.notification.spec.js +0 -76
- package/lib/commonjs/store.notification.spec.js.map +0 -1
- package/lib/commonjs/store.spec.js +0 -316
- package/lib/commonjs/store.spec.js.map +0 -1
- package/lib/commonjs/wrap.spec.js +0 -329
- package/lib/commonjs/wrap.spec.js.map +0 -1
- package/lib/module/catalogCacheNative.spec.js +0 -31
- package/lib/module/catalogCacheNative.spec.js.map +0 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -615
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
- package/lib/module/httpClient.spec.js +0 -102
- package/lib/module/httpClient.spec.js.map +0 -1
- package/lib/module/index.spec.js +0 -301
- package/lib/module/index.spec.js.map +0 -1
- package/lib/module/iosInterop.spec.js +0 -18
- package/lib/module/iosInterop.spec.js.map +0 -1
- package/lib/module/native.spec.js +0 -882
- package/lib/module/native.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -217
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.js +0 -128
- package/lib/module/notifyAppReadyInsights.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.spec.js +0 -245
- package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.test-utils.js +0 -18
- package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -116
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/module/releaseCatalogCache.spec.js +0 -195
- package/lib/module/releaseCatalogCache.spec.js.map +0 -1
- package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
- package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
- package/lib/module/store.notification.spec.js +0 -77
- package/lib/module/store.notification.spec.js.map +0 -1
- package/lib/module/store.spec.js +0 -315
- package/lib/module/store.spec.js.map +0 -1
- package/lib/module/wrap.spec.js +0 -330
- package/lib/module/wrap.spec.js.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -13
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -13
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
- package/src/catalogCacheNative.spec.ts +0 -63
- package/src/checkForUpdate.releaseCatalog.spec.ts +0 -612
- package/src/httpClient.spec.ts +0 -132
- package/src/index.spec.ts +0 -337
- package/src/iosInterop.spec.ts +0 -30
- package/src/native.spec.ts +0 -945
- package/src/notifyAppReadyInsights.boundary.spec.ts +0 -234
- package/src/notifyAppReadyInsights.spec.ts +0 -243
- package/src/notifyAppReadyInsights.test-utils.ts +0 -31
- package/src/notifyAppReadyInsights.transport.spec.ts +0 -130
- package/src/notifyAppReadyInsights.ts +0 -194
- package/src/releaseCatalogCache.spec.ts +0 -282
- package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
- package/src/store.notification.spec.ts +0 -92
- package/src/store.spec.ts +0 -372
- package/src/wrap.spec.ts +0 -406
|
@@ -52,27 +52,28 @@ public struct BsdiffPatchDescriptor {
|
|
|
52
52
|
public let baseFileHash: String
|
|
53
53
|
public let patchFileHash: String
|
|
54
54
|
public let patchUrl: URL
|
|
55
|
+
public let byteSize: Int64?
|
|
55
56
|
|
|
56
57
|
public init(
|
|
57
58
|
algorithm: String,
|
|
58
59
|
baseBundleId: String,
|
|
59
60
|
baseFileHash: String,
|
|
60
61
|
patchFileHash: String,
|
|
61
|
-
patchUrl: URL
|
|
62
|
+
patchUrl: URL,
|
|
63
|
+
byteSize: Int64? = nil
|
|
62
64
|
) {
|
|
63
65
|
self.algorithm = algorithm
|
|
64
66
|
self.baseBundleId = baseBundleId
|
|
65
67
|
self.baseFileHash = baseFileHash
|
|
66
68
|
self.patchFileHash = patchFileHash
|
|
67
69
|
self.patchUrl = patchUrl
|
|
70
|
+
self.byteSize = byteSize
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
public struct UpdateProgressPayload {
|
|
72
75
|
public let progress: Double
|
|
73
76
|
public let artifactType: String
|
|
74
|
-
public let downloadedBytes: Int64?
|
|
75
|
-
public let totalBytes: Int64?
|
|
76
77
|
public let details: DiffProgressDetails?
|
|
77
78
|
|
|
78
79
|
public struct DiffProgressFileSnapshot {
|
|
@@ -147,30 +148,47 @@ public struct UpdateProgressPayload {
|
|
|
147
148
|
public init(
|
|
148
149
|
progress: Double,
|
|
149
150
|
artifactType: String,
|
|
150
|
-
downloadedBytes: Int64? = nil,
|
|
151
|
-
totalBytes: Int64? = nil,
|
|
152
151
|
details: DiffProgressDetails? = nil
|
|
153
152
|
) {
|
|
154
153
|
self.progress = progress
|
|
155
154
|
self.artifactType = artifactType
|
|
156
|
-
self.downloadedBytes = downloadedBytes
|
|
157
|
-
self.totalBytes = totalBytes
|
|
158
155
|
self.details = details
|
|
159
156
|
}
|
|
160
157
|
|
|
161
158
|
public var userInfo: [String: Any] {
|
|
162
|
-
|
|
159
|
+
[
|
|
163
160
|
"artifactType": artifactType,
|
|
164
161
|
"progress": progress,
|
|
165
162
|
"details": details?.userInfo ?? NSNull()
|
|
166
163
|
]
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// How a staged bundle arrived. `updateBundle` resolves with `dictionary`.
|
|
168
|
+
public struct UpdateDelivery: Equatable {
|
|
169
|
+
public enum Method: String {
|
|
170
|
+
/// A bsdiff patch produced at least one file.
|
|
171
|
+
case patch
|
|
172
|
+
/// Only changed files were downloaded, possibly none.
|
|
173
|
+
case manifest
|
|
174
|
+
/// The full tar.br archive was downloaded.
|
|
175
|
+
case archive
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public let method: Method
|
|
179
|
+
/// A file's patch was tried, but the full file was downloaded instead.
|
|
180
|
+
public let patchFallback: Bool
|
|
181
|
+
|
|
182
|
+
public init(method: Method, patchFallback: Bool) {
|
|
183
|
+
self.method = method
|
|
184
|
+
self.patchFallback = patchFallback
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public var dictionary: [String: Any] {
|
|
188
|
+
[
|
|
189
|
+
"delivery": method.rawValue,
|
|
190
|
+
"patchFallback": patchFallback,
|
|
191
|
+
]
|
|
174
192
|
}
|
|
175
193
|
}
|
|
176
194
|
|
|
@@ -178,9 +196,7 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
178
196
|
case directoryCreationFailed
|
|
179
197
|
case downloadFailed(Error)
|
|
180
198
|
case incompleteDownload(expected: Int64, actual: Int64)
|
|
181
|
-
case extractionFormatError(Error)
|
|
182
199
|
case invalidBundle
|
|
183
|
-
case insufficientDiskSpace
|
|
184
200
|
case signatureVerificationFailed(SignatureVerificationError)
|
|
185
201
|
case moveOperationFailed(Error)
|
|
186
202
|
case bundleInCrashedHistory(String)
|
|
@@ -200,9 +216,7 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
200
216
|
case .directoryCreationFailed: return "DIRECTORY_CREATION_FAILED"
|
|
201
217
|
case .downloadFailed: return "DOWNLOAD_FAILED"
|
|
202
218
|
case .incompleteDownload: return "INCOMPLETE_DOWNLOAD"
|
|
203
|
-
case .extractionFormatError: return "EXTRACTION_FORMAT_ERROR"
|
|
204
219
|
case .invalidBundle: return "INVALID_BUNDLE"
|
|
205
|
-
case .insufficientDiskSpace: return "INSUFFICIENT_DISK_SPACE"
|
|
206
220
|
case .signatureVerificationFailed: return "SIGNATURE_VERIFICATION_FAILED"
|
|
207
221
|
case .moveOperationFailed: return "MOVE_OPERATION_FAILED"
|
|
208
222
|
case .bundleInCrashedHistory: return "BUNDLE_IN_CRASHED_HISTORY"
|
|
@@ -227,19 +241,10 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
227
241
|
userInfo[NSLocalizedDescriptionKey] = "Download incomplete: received \(actual) bytes, expected \(expected) bytes"
|
|
228
242
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The download was interrupted. Check network connection and try again"
|
|
229
243
|
|
|
230
|
-
case .extractionFormatError(let underlyingError):
|
|
231
|
-
userInfo[NSLocalizedDescriptionKey] = "The downloaded bundle file is not a valid compressed archive"
|
|
232
|
-
userInfo[NSUnderlyingErrorKey] = underlyingError
|
|
233
|
-
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The downloaded file is not a supported bundle archive. Try downloading again"
|
|
234
|
-
|
|
235
244
|
case .invalidBundle:
|
|
236
245
|
userInfo[NSLocalizedDescriptionKey] = "Bundle missing required platform files (index.ios.bundle or main.jsbundle)"
|
|
237
246
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Verify the bundle was built correctly with metro bundler"
|
|
238
247
|
|
|
239
|
-
case .insufficientDiskSpace:
|
|
240
|
-
userInfo[NSLocalizedDescriptionKey] = "Insufficient disk space to download and extract bundle"
|
|
241
|
-
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Free up device storage and try again"
|
|
242
|
-
|
|
243
248
|
case .signatureVerificationFailed(let underlyingError):
|
|
244
249
|
userInfo[NSLocalizedDescriptionKey] = "Bundle signature verification failed"
|
|
245
250
|
userInfo[NSUnderlyingErrorKey] = underlyingError
|
|
@@ -280,7 +285,7 @@ public protocol BundleStorageService {
|
|
|
280
285
|
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection
|
|
281
286
|
|
|
282
287
|
// Bundle update
|
|
283
|
-
func updateBundle(bundleId: String,
|
|
288
|
+
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL?, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void)
|
|
284
289
|
func stageReleaseSelection(_ selection: PersistedSelection) -> Bool
|
|
285
290
|
func acceptReleaseCatalog(catalogId: String, scopeKey: String, generation: Int64, catalogHash: String, channel: String, selectionContextHash: String) -> Bool
|
|
286
291
|
func getActiveUpdateState() -> [String: Any]
|
|
@@ -288,15 +293,13 @@ public protocol BundleStorageService {
|
|
|
288
293
|
func commitReleaseSelection(_ selection: PersistedSelection) -> Bool
|
|
289
294
|
|
|
290
295
|
// Rollback support
|
|
296
|
+
func markLaunchStarted(bundleId: String?)
|
|
291
297
|
func markLaunchCompleted(bundleId: String?)
|
|
292
298
|
func notifyAppReady() -> [String: Any]
|
|
293
299
|
func getCrashHistory() -> CrashedHistory
|
|
294
300
|
func clearCrashHistory() -> Bool
|
|
295
301
|
func getInstallId() -> String
|
|
296
|
-
|
|
297
|
-
func getUsername() -> String?
|
|
298
|
-
func setUser(userId: String?, username: String?)
|
|
299
|
-
|
|
302
|
+
|
|
300
303
|
/**
|
|
301
304
|
* Gets the base URL for the current active bundle directory
|
|
302
305
|
* @return Base URL string (e.g., "file:///data/.../bundle-store/abc123") or empty string
|
|
@@ -306,7 +309,7 @@ public protocol BundleStorageService {
|
|
|
306
309
|
|
|
307
310
|
/**
|
|
308
311
|
* Gets the current active bundle ID from bundle storage.
|
|
309
|
-
* Reads
|
|
312
|
+
* Reads the required OTA manifest.json.
|
|
310
313
|
*/
|
|
311
314
|
func getBundleId() -> String?
|
|
312
315
|
|
|
@@ -332,6 +335,8 @@ public extension BundleStorageService {
|
|
|
332
335
|
}
|
|
333
336
|
|
|
334
337
|
class BundleFileStorageService: BundleStorageService {
|
|
338
|
+
private static let maximumSafeByteSize: Int64 = 9_007_199_254_740_991
|
|
339
|
+
|
|
335
340
|
private struct ActiveBundleMetadataSnapshot {
|
|
336
341
|
let activeBundleId: String
|
|
337
342
|
let bundleId: String?
|
|
@@ -341,80 +346,59 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
341
346
|
private struct ParsedBundleManifest {
|
|
342
347
|
let bundleId: String
|
|
343
348
|
let assets: [String: ParsedManifestAsset]
|
|
349
|
+
let archive: ParsedArchive?
|
|
344
350
|
}
|
|
345
351
|
|
|
346
352
|
private struct ParsedManifestAsset {
|
|
347
353
|
let fileHash: String
|
|
348
354
|
let signature: String?
|
|
355
|
+
let byteSize: Int64?
|
|
356
|
+
let downloadByteSize: Int64?
|
|
349
357
|
}
|
|
350
358
|
|
|
351
|
-
private
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
static let extractionStart = 0.82
|
|
356
|
-
static let extractionEnd = 0.97
|
|
357
|
-
static let bundleValidation = 0.98
|
|
358
|
-
static let activationReady = 0.99
|
|
359
|
-
static let complete = 1.0
|
|
359
|
+
private struct ParsedArchive {
|
|
360
|
+
let downloadFileHash: String
|
|
361
|
+
let downloadByteSize: Int64
|
|
362
|
+
let tarByteSize: Int64
|
|
360
363
|
}
|
|
361
364
|
|
|
362
365
|
private let fileSystem: FileSystemService
|
|
363
366
|
private let downloadService: DownloadService
|
|
364
|
-
|
|
367
|
+
/// The wait before a download's second attempt; it doubles for each later one.
|
|
368
|
+
private let downloadRetryDelay: TimeInterval
|
|
365
369
|
private let preferences: PreferencesService
|
|
366
370
|
private let isolationKey: String
|
|
367
371
|
|
|
368
|
-
private let id = Int.random(in: 1..<100)
|
|
369
|
-
|
|
370
372
|
// Queue for potentially long-running sequences within updateBundle or for explicit background tasks.
|
|
371
373
|
private let fileOperationQueue: DispatchQueue
|
|
372
374
|
|
|
373
|
-
private var
|
|
374
|
-
|
|
375
|
+
private var hasPreparedLaunch = false
|
|
375
376
|
private var currentLaunchReport: LaunchReport?
|
|
377
|
+
// The staged bundle this process launched before its first content. Only that
|
|
378
|
+
// launch is pending; a bundle staged later waits for the next launch.
|
|
379
|
+
private var pendingLaunchBundleId: String?
|
|
380
|
+
// Crash history is written at first content on the main thread while JavaScript can read it.
|
|
381
|
+
private let crashedHistoryLock = NSLock()
|
|
382
|
+
// A bundle waiting for its retry that this process keeps refusing, see InterruptedLaunch.
|
|
383
|
+
private var retryHeldBundleId: String?
|
|
376
384
|
private let activeBundleMetadataLock = NSLock()
|
|
377
385
|
private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot?
|
|
378
386
|
private let builtInBundleIdProvider: () -> String
|
|
387
|
+
private let builtInAssetResolver: BuiltInAssetResolver
|
|
379
388
|
private let updateStrategyProvider: () -> UpdateStrategy
|
|
380
389
|
private let releaseStateLock = NSRecursiveLock()
|
|
381
390
|
private var pendingInstallSelections: [String: PersistedSelection] = [:]
|
|
382
391
|
|
|
392
|
+
private let installIdentityLock = NSLock()
|
|
383
393
|
private var currentInstallIdentity: InstallIdentity?
|
|
384
|
-
private var currentUserIdentity: UserIdentity?
|
|
385
394
|
|
|
386
395
|
private let protectedBundleStoreEntries: Set<String> = [
|
|
387
396
|
BundleMetadata.metadataFilename,
|
|
388
397
|
CrashedHistory.crashedHistoryFilename,
|
|
389
398
|
LaunchReport.launchReportFilename,
|
|
390
|
-
|
|
391
|
-
UserIdentity.userIdentityFilename,
|
|
399
|
+
"builtin-index-v1.json",
|
|
392
400
|
]
|
|
393
401
|
|
|
394
|
-
private func normalizeIdentityValue(_ value: String?) -> String? {
|
|
395
|
-
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
396
|
-
!trimmed.isEmpty else {
|
|
397
|
-
return nil
|
|
398
|
-
}
|
|
399
|
-
return trimmed
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
private func emitArchiveProgress(
|
|
403
|
-
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
404
|
-
progress: Double,
|
|
405
|
-
downloadedBytes: Int64? = nil,
|
|
406
|
-
totalBytes: Int64? = nil
|
|
407
|
-
) {
|
|
408
|
-
progressHandler(
|
|
409
|
-
UpdateProgressPayload(
|
|
410
|
-
progress: max(0, min(progress, 1)),
|
|
411
|
-
artifactType: "archive",
|
|
412
|
-
downloadedBytes: downloadedBytes,
|
|
413
|
-
totalBytes: totalBytes
|
|
414
|
-
)
|
|
415
|
-
)
|
|
416
|
-
}
|
|
417
|
-
|
|
418
402
|
private func createDiffProgressFiles(
|
|
419
403
|
changedAssets: [String: ChangedAssetDescriptor]
|
|
420
404
|
) -> [UpdateProgressPayload.DiffProgressFileSnapshot] {
|
|
@@ -576,10 +560,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
576
560
|
return
|
|
577
561
|
}
|
|
578
562
|
|
|
579
|
-
try
|
|
580
|
-
from: downloadedPath,
|
|
581
|
-
to: destinationPath
|
|
582
|
-
)
|
|
563
|
+
try BrotliFileDecompressor.decompress(from: downloadedPath, to: destinationPath)
|
|
583
564
|
try? fileSystem.removeItem(atPath: downloadedPath)
|
|
584
565
|
}
|
|
585
566
|
|
|
@@ -604,7 +585,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
604
585
|
|
|
605
586
|
let sourcePath: String
|
|
606
587
|
do {
|
|
607
|
-
sourcePath = try
|
|
588
|
+
sourcePath = try FileUtilities.fileURL(
|
|
608
589
|
for: assetPath,
|
|
609
590
|
destinationRoot: currentBundleDir
|
|
610
591
|
).path
|
|
@@ -722,6 +703,14 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
722
703
|
atPath path: String,
|
|
723
704
|
asset: ParsedManifestAsset
|
|
724
705
|
) throws {
|
|
706
|
+
if let expectedByteSize = asset.byteSize {
|
|
707
|
+
let attributes = try fileSystem.attributesOfItem(atPath: path)
|
|
708
|
+
guard let size = attributes[.size] as? NSNumber,
|
|
709
|
+
size.int64Value == expectedByteSize else {
|
|
710
|
+
throw BundleStorageError.signatureVerificationFailed(.fileHashMismatch)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
725
714
|
guard let actualHash = HashUtils.calculateSHA256(fileURL: URL(fileURLWithPath: path)) else {
|
|
726
715
|
throw BundleStorageError.signatureVerificationFailed(.fileReadFailed)
|
|
727
716
|
}
|
|
@@ -751,22 +740,28 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
751
740
|
|
|
752
741
|
public init(fileSystem: FileSystemService,
|
|
753
742
|
downloadService: DownloadService,
|
|
754
|
-
decompressService: DecompressService,
|
|
755
743
|
preferences: PreferencesService,
|
|
756
744
|
isolationKey: String,
|
|
757
745
|
builtInBundleIdProvider: @escaping () -> String = { "" },
|
|
746
|
+
builtInAssetResolver: BuiltInAssetResolver? = nil,
|
|
758
747
|
updateStrategyProvider: @escaping () -> UpdateStrategy = {
|
|
759
748
|
let fingerprintHash = HotUpdaterConfig.shared.fingerprintHash
|
|
760
749
|
?? Bundle.main.object(forInfoDictionaryKey: "HOT_UPDATER_FINGERPRINT_HASH") as? String
|
|
761
750
|
return (fingerprintHash?.isEmpty == false) ? .fingerprint : .appVersion
|
|
762
|
-
}
|
|
751
|
+
},
|
|
752
|
+
downloadRetryDelay: TimeInterval = 1) {
|
|
763
753
|
|
|
764
754
|
self.fileSystem = fileSystem
|
|
755
|
+
self.downloadRetryDelay = downloadRetryDelay
|
|
765
756
|
self.downloadService = downloadService
|
|
766
|
-
self.decompressService = decompressService
|
|
767
757
|
self.preferences = preferences
|
|
768
758
|
self.isolationKey = isolationKey
|
|
769
759
|
self.builtInBundleIdProvider = builtInBundleIdProvider
|
|
760
|
+
self.builtInAssetResolver = builtInAssetResolver ?? IOSBuiltInAssetResolver(
|
|
761
|
+
cacheURL: URL(fileURLWithPath: fileSystem.documentsPath())
|
|
762
|
+
.appendingPathComponent("bundle-store", isDirectory: true)
|
|
763
|
+
.appendingPathComponent("builtin-index-v1.json")
|
|
764
|
+
)
|
|
770
765
|
self.updateStrategyProvider = updateStrategyProvider
|
|
771
766
|
|
|
772
767
|
// Create queue for file operations
|
|
@@ -779,6 +774,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
779
774
|
|
|
780
775
|
// Clean up old bundles if isolationKey format changed
|
|
781
776
|
checkAndCleanupIfIsolationKeyChanged()
|
|
777
|
+
recoverInterruptedPromotions()
|
|
782
778
|
}
|
|
783
779
|
|
|
784
780
|
// MARK: - Metadata File Paths
|
|
@@ -804,18 +800,9 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
804
800
|
return URL(fileURLWithPath: storeDir).appendingPathComponent(LaunchReport.launchReportFilename)
|
|
805
801
|
}
|
|
806
802
|
|
|
807
|
-
private func installIdentityFileURL() -> URL
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
811
|
-
return URL(fileURLWithPath: storeDir).appendingPathComponent(InstallIdentity.installIdentityFilename)
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
private func userIdentityFileURL() -> URL? {
|
|
815
|
-
guard case .success(let storeDir) = bundleStoreDir() else {
|
|
816
|
-
return nil
|
|
817
|
-
}
|
|
818
|
-
return URL(fileURLWithPath: storeDir).appendingPathComponent(UserIdentity.userIdentityFilename)
|
|
803
|
+
private func installIdentityFileURL() -> URL {
|
|
804
|
+
NoBackupStorage.directory(in: fileSystem)
|
|
805
|
+
.appendingPathComponent(InstallIdentity.installIdentityFilename)
|
|
819
806
|
}
|
|
820
807
|
|
|
821
808
|
// MARK: - Metadata Operations
|
|
@@ -864,54 +851,28 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
864
851
|
_ = report.save(to: file)
|
|
865
852
|
}
|
|
866
853
|
|
|
867
|
-
|
|
854
|
+
/// Loads the install identity, creating it once when its file is missing.
|
|
855
|
+
@discardableResult
|
|
856
|
+
private func loadOrCreateInstallIdentity() -> InstallIdentity {
|
|
857
|
+
installIdentityLock.lock()
|
|
858
|
+
defer { installIdentityLock.unlock() }
|
|
859
|
+
|
|
868
860
|
if let currentInstallIdentity {
|
|
869
861
|
return currentInstallIdentity
|
|
870
862
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
863
|
+
let file = installIdentityFileURL()
|
|
864
|
+
if let identity = InstallIdentity.load(from: file) {
|
|
865
|
+
currentInstallIdentity = identity
|
|
866
|
+
return identity
|
|
874
867
|
}
|
|
875
|
-
currentInstallIdentity = identity
|
|
876
|
-
return identity
|
|
877
|
-
}
|
|
878
868
|
|
|
879
|
-
|
|
869
|
+
let identity = InstallIdentity()
|
|
870
|
+
// Keep the new id for this process even when it cannot be saved.
|
|
880
871
|
currentInstallIdentity = identity
|
|
881
|
-
|
|
882
|
-
return false
|
|
883
|
-
}
|
|
884
|
-
return identity.save(to: file)
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
private func loadUserIdentity() -> UserIdentity? {
|
|
888
|
-
if let currentUserIdentity {
|
|
889
|
-
return currentUserIdentity
|
|
890
|
-
}
|
|
891
|
-
guard let file = userIdentityFileURL(),
|
|
892
|
-
let identity = UserIdentity.load(from: file) else {
|
|
893
|
-
return nil
|
|
894
|
-
}
|
|
895
|
-
currentUserIdentity = identity
|
|
872
|
+
_ = identity.save(to: file)
|
|
896
873
|
return identity
|
|
897
874
|
}
|
|
898
875
|
|
|
899
|
-
private func saveUserIdentity(_ identity: UserIdentity?) -> Bool {
|
|
900
|
-
currentUserIdentity = identity
|
|
901
|
-
guard let file = userIdentityFileURL() else {
|
|
902
|
-
return false
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
guard let identity else {
|
|
906
|
-
if FileManager.default.fileExists(atPath: file.path) {
|
|
907
|
-
try? FileManager.default.removeItem(at: file)
|
|
908
|
-
}
|
|
909
|
-
return true
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
return identity.save(to: file)
|
|
913
|
-
}
|
|
914
|
-
|
|
915
876
|
private func resolveBuiltInBundleId() -> String? {
|
|
916
877
|
let bundleId = builtInBundleIdProvider().trimmingCharacters(in: .whitespacesAndNewlines)
|
|
917
878
|
return bundleId.isEmpty ? nil : bundleId
|
|
@@ -938,11 +899,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
938
899
|
}
|
|
939
900
|
|
|
940
901
|
private func createInitialMetadata() -> BundleMetadata {
|
|
941
|
-
let currentBundleId = getCachedBundleId()
|
|
942
902
|
return BundleMetadata(
|
|
943
903
|
isolationKey: isolationKey,
|
|
944
904
|
stableBundleId: nil,
|
|
945
|
-
stagingBundleId:
|
|
905
|
+
stagingBundleId: nil,
|
|
946
906
|
verificationPending: false,
|
|
947
907
|
pendingTransition: nil
|
|
948
908
|
)
|
|
@@ -956,18 +916,18 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
956
916
|
}
|
|
957
917
|
|
|
958
918
|
private func getActiveBundleId() -> String? {
|
|
959
|
-
|
|
919
|
+
guard let metadata = loadMetadataOrNull() else { return nil }
|
|
920
|
+
if let cachedBundleId = getCachedBundleId(),
|
|
921
|
+
cachedBundleId == metadata.stagingBundleId || cachedBundleId == metadata.stableBundleId {
|
|
960
922
|
return cachedBundleId
|
|
961
923
|
}
|
|
962
924
|
|
|
963
|
-
let
|
|
964
|
-
|
|
965
|
-
if let stagingBundleId = metadata?.stagingBundleId,
|
|
966
|
-
metadata?.verificationPending == false {
|
|
925
|
+
if let stagingBundleId = metadata.stagingBundleId,
|
|
926
|
+
metadata.verificationPending == false {
|
|
967
927
|
return stagingBundleId
|
|
968
928
|
}
|
|
969
929
|
|
|
970
|
-
if let stableBundleId = metadata
|
|
930
|
+
if let stableBundleId = metadata.stableBundleId {
|
|
971
931
|
return stableBundleId
|
|
972
932
|
}
|
|
973
933
|
|
|
@@ -1017,22 +977,6 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1017
977
|
}
|
|
1018
978
|
}
|
|
1019
979
|
|
|
1020
|
-
func canUseManifestDrivenInstall() -> Bool {
|
|
1021
|
-
guard let currentBundleId = getCachedBundleId(),
|
|
1022
|
-
case .success(let storeDir) = bundleStoreDir() else {
|
|
1023
|
-
return false
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
let currentBundleDir = (storeDir as NSString).appendingPathComponent(currentBundleId)
|
|
1027
|
-
guard fileSystem.fileExists(atPath: currentBundleDir),
|
|
1028
|
-
let snapshot = getActiveBundleMetadataSnapshot(),
|
|
1029
|
-
let currentManifest = parseBundleManifest(from: snapshot.manifest) else {
|
|
1030
|
-
return false
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
return currentManifest.assets.isEmpty == false
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
980
|
private func resolveActiveBundleMetadataSnapshot(
|
|
1037
981
|
activeBundleId: String,
|
|
1038
982
|
bundleDirectory: String
|
|
@@ -1041,9 +985,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1041
985
|
let manifestBundleId =
|
|
1042
986
|
(manifest["bundleId"] as? String)?
|
|
1043
987
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1044
|
-
let resolvedBundleId =
|
|
1045
|
-
(manifestBundleId?.isEmpty == false ? manifestBundleId : nil) ??
|
|
1046
|
-
readCompatibilityBundleId(in: bundleDirectory)
|
|
988
|
+
let resolvedBundleId = manifestBundleId?.isEmpty == false ? manifestBundleId : nil
|
|
1047
989
|
|
|
1048
990
|
return ActiveBundleMetadataSnapshot(
|
|
1049
991
|
activeBundleId: activeBundleId,
|
|
@@ -1070,34 +1012,6 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1070
1012
|
)
|
|
1071
1013
|
}
|
|
1072
1014
|
|
|
1073
|
-
private func readCompatibilityBundleId(in bundleDirectory: String) -> String? {
|
|
1074
|
-
let compatibilityBundleIdPath = (bundleDirectory as NSString)
|
|
1075
|
-
.appendingPathComponent(compatibilityBundleIdFilename())
|
|
1076
|
-
if fileSystem.fileExists(atPath: compatibilityBundleIdPath) {
|
|
1077
|
-
do {
|
|
1078
|
-
let compatibilityBundleId = try String(
|
|
1079
|
-
contentsOfFile: compatibilityBundleIdPath,
|
|
1080
|
-
encoding: .utf8
|
|
1081
|
-
)
|
|
1082
|
-
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1083
|
-
|
|
1084
|
-
if !compatibilityBundleId.isEmpty {
|
|
1085
|
-
return compatibilityBundleId
|
|
1086
|
-
}
|
|
1087
|
-
} catch {
|
|
1088
|
-
NSLog(
|
|
1089
|
-
"[BundleStorage] Failed to read compatibility bundle metadata at \(compatibilityBundleIdPath): \(error.localizedDescription)"
|
|
1090
|
-
)
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
return nil
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
private func compatibilityBundleIdFilename() -> String {
|
|
1098
|
-
"BUNDLE_ID"
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
1015
|
private func readManifest(in bundleDirectory: String) -> [String: Any]? {
|
|
1102
1016
|
let manifestPath = (bundleDirectory as NSString).appendingPathComponent("manifest.json")
|
|
1103
1017
|
guard fileSystem.fileExists(atPath: manifestPath) else {
|
|
@@ -1124,7 +1038,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1124
1038
|
|
|
1125
1039
|
var assets: [String: ParsedManifestAsset] = [:]
|
|
1126
1040
|
for (assetPath, assetValue) in rawAssets {
|
|
1127
|
-
guard
|
|
1041
|
+
guard FileUtilities.normalizedRelativePath(from: assetPath) == assetPath else {
|
|
1128
1042
|
return nil
|
|
1129
1043
|
}
|
|
1130
1044
|
guard let asset = assetValue as? [String: Any],
|
|
@@ -1137,42 +1051,136 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1137
1051
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1138
1052
|
assets[assetPath] = ParsedManifestAsset(
|
|
1139
1053
|
fileHash: fileHash,
|
|
1140
|
-
signature: signature?.isEmpty == false ? signature : nil
|
|
1054
|
+
signature: signature?.isEmpty == false ? signature : nil,
|
|
1055
|
+
byteSize: Self.parseSafeByteSize(asset["byteSize"]),
|
|
1056
|
+
downloadByteSize: Self.parseSafeByteSize(asset["downloadByteSize"])
|
|
1057
|
+
)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
let archive: ParsedArchive?
|
|
1061
|
+
if let payload = manifest["archive"] as? [String: Any],
|
|
1062
|
+
let downloadFileHash = payload["downloadFileHash"] as? String,
|
|
1063
|
+
Self.isPlainSHA256(downloadFileHash),
|
|
1064
|
+
let downloadByteSize = Self.parseSafeByteSize(payload["downloadByteSize"]),
|
|
1065
|
+
downloadByteSize > 0,
|
|
1066
|
+
let tarByteSize = Self.parseSafeByteSize(payload["tarByteSize"]),
|
|
1067
|
+
tarByteSize >= 1024 {
|
|
1068
|
+
archive = ParsedArchive(
|
|
1069
|
+
downloadFileHash: downloadFileHash,
|
|
1070
|
+
downloadByteSize: downloadByteSize,
|
|
1071
|
+
tarByteSize: tarByteSize
|
|
1141
1072
|
)
|
|
1073
|
+
} else {
|
|
1074
|
+
archive = nil
|
|
1142
1075
|
}
|
|
1143
1076
|
|
|
1144
|
-
return ParsedBundleManifest(
|
|
1077
|
+
return ParsedBundleManifest(
|
|
1078
|
+
bundleId: manifestBundleId,
|
|
1079
|
+
assets: assets,
|
|
1080
|
+
archive: archive
|
|
1081
|
+
)
|
|
1145
1082
|
}
|
|
1146
1083
|
|
|
1147
|
-
private func
|
|
1148
|
-
guard let
|
|
1084
|
+
private static func parseSafeByteSize(_ value: Any?) -> Int64? {
|
|
1085
|
+
guard let number = value as? NSNumber,
|
|
1086
|
+
String(cString: number.objCType) != "c" else {
|
|
1087
|
+
return nil
|
|
1088
|
+
}
|
|
1089
|
+
let doubleValue = number.doubleValue
|
|
1090
|
+
guard doubleValue.isFinite,
|
|
1091
|
+
doubleValue >= 0,
|
|
1092
|
+
doubleValue <= Double(maximumSafeByteSize),
|
|
1093
|
+
doubleValue.rounded(.towardZero) == doubleValue else {
|
|
1149
1094
|
return nil
|
|
1150
1095
|
}
|
|
1096
|
+
return Int64(doubleValue)
|
|
1097
|
+
}
|
|
1151
1098
|
|
|
1152
|
-
|
|
1099
|
+
private static func isPlainSHA256(_ value: String) -> Bool {
|
|
1100
|
+
value.count == 64 && value.allSatisfy {
|
|
1101
|
+
("0"..."9").contains($0) || ("a"..."f").contains($0)
|
|
1102
|
+
}
|
|
1153
1103
|
}
|
|
1154
1104
|
|
|
1155
|
-
private func
|
|
1105
|
+
private func copyVerifiedManifest(from source: String, to destination: String) throws {
|
|
1156
1106
|
let manifestDirectory = (destination as NSString).deletingLastPathComponent
|
|
1157
1107
|
guard fileSystem.createDirectory(atPath: manifestDirectory) else {
|
|
1158
1108
|
throw BundleStorageError.directoryCreationFailed
|
|
1159
1109
|
}
|
|
1110
|
+
if fileSystem.fileExists(atPath: destination) {
|
|
1111
|
+
try fileSystem.removeItem(atPath: destination)
|
|
1112
|
+
}
|
|
1113
|
+
do {
|
|
1114
|
+
try fileSystem.copyItem(atPath: source, toPath: destination)
|
|
1115
|
+
} catch {
|
|
1116
|
+
throw BundleStorageError.moveOperationFailed(error)
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
private func shouldUseArchive(
|
|
1121
|
+
archive: ParsedArchive?,
|
|
1122
|
+
archiveUrl: URL?,
|
|
1123
|
+
downloads: [String: ChangedAssetDescriptor],
|
|
1124
|
+
assets: [String: ParsedManifestAsset]
|
|
1125
|
+
) -> Bool {
|
|
1126
|
+
guard downloads.count >= 2,
|
|
1127
|
+
archive != nil,
|
|
1128
|
+
archiveUrl != nil,
|
|
1129
|
+
assets.values.allSatisfy({ $0.byteSize != nil }) else {
|
|
1130
|
+
return false
|
|
1131
|
+
}
|
|
1160
1132
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1133
|
+
var individualLowerBound: Int64 = 0
|
|
1134
|
+
var hasOfferedPatch = false
|
|
1135
|
+
for (path, descriptor) in downloads {
|
|
1136
|
+
guard let originalByteSize = assets[path]?.downloadByteSize else {
|
|
1137
|
+
return false
|
|
1138
|
+
}
|
|
1139
|
+
let plannedByteSize: Int64
|
|
1140
|
+
if let patch = descriptor.patch {
|
|
1141
|
+
hasOfferedPatch = true
|
|
1142
|
+
guard let patchByteSize = patch.byteSize,
|
|
1143
|
+
patchByteSize >= 0,
|
|
1144
|
+
patchByteSize <= Self.maximumSafeByteSize else {
|
|
1145
|
+
return false
|
|
1167
1146
|
}
|
|
1168
|
-
|
|
1147
|
+
plannedByteSize = min(originalByteSize, patchByteSize)
|
|
1148
|
+
} else {
|
|
1149
|
+
plannedByteSize = originalByteSize
|
|
1169
1150
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1151
|
+
let (sum, overflowed) = individualLowerBound.addingReportingOverflow(plannedByteSize)
|
|
1152
|
+
guard !overflowed,
|
|
1153
|
+
sum <= Self.maximumSafeByteSize else { return false }
|
|
1154
|
+
individualLowerBound = sum
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
guard let archive else { return false }
|
|
1158
|
+
var logicalByteSize: Int64 = 0
|
|
1159
|
+
for asset in assets.values {
|
|
1160
|
+
guard let byteSize = asset.byteSize else { return false }
|
|
1161
|
+
let (sum, overflowed) = logicalByteSize.addingReportingOverflow(byteSize)
|
|
1162
|
+
guard !overflowed,
|
|
1163
|
+
sum <= Self.maximumSafeByteSize else { return false }
|
|
1164
|
+
logicalByteSize = sum
|
|
1165
|
+
}
|
|
1166
|
+
let (tarOverhead, overheadUnderflowed) = archive.tarByteSize
|
|
1167
|
+
.subtractingReportingOverflow(logicalByteSize)
|
|
1168
|
+
guard !overheadUnderflowed,
|
|
1169
|
+
tarOverhead >= 0 else { return false }
|
|
1170
|
+
|
|
1171
|
+
if archive.downloadByteSize <= individualLowerBound {
|
|
1172
|
+
return true
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
guard downloads.count == assets.count,
|
|
1176
|
+
!hasOfferedPatch else {
|
|
1177
|
+
return false
|
|
1178
|
+
}
|
|
1179
|
+
let (archiveBound, boundOverflowed) = individualLowerBound
|
|
1180
|
+
.addingReportingOverflow(tarOverhead)
|
|
1181
|
+
guard !boundOverflowed,
|
|
1182
|
+
archiveBound <= Self.maximumSafeByteSize else { return false }
|
|
1183
|
+
return archive.downloadByteSize <= archiveBound
|
|
1176
1184
|
}
|
|
1177
1185
|
|
|
1178
1186
|
/**
|
|
@@ -1308,8 +1316,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1308
1316
|
)
|
|
1309
1317
|
}
|
|
1310
1318
|
|
|
1319
|
+
/// Rolls back the pending staging bundle. `unfinishedLaunch` means its launch
|
|
1320
|
+
/// ended before first content without a crash marker.
|
|
1311
1321
|
@discardableResult
|
|
1312
|
-
private func rollbackPendingBundle(_ stagingId: String) -> Bool {
|
|
1322
|
+
private func rollbackPendingBundle(_ stagingId: String, unfinishedLaunch: Bool = false) -> Bool {
|
|
1313
1323
|
guard var metadata = loadMetadataOrNull(), metadata.stagingBundleId == stagingId else {
|
|
1314
1324
|
return false
|
|
1315
1325
|
}
|
|
@@ -1318,9 +1328,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1318
1328
|
let pendingSelectionTransition = metadata.pendingSelectionTransition
|
|
1319
1329
|
let stableSelection = metadata.stableSelection
|
|
1320
1330
|
|
|
1321
|
-
|
|
1322
|
-
crashedHistory.addEntry(stagingId)
|
|
1323
|
-
let _ = saveCrashedHistory(crashedHistory)
|
|
1331
|
+
recordFailedLaunch(stagingId, unfinishedLaunch: unfinishedLaunch)
|
|
1324
1332
|
|
|
1325
1333
|
let fallbackBundleId = metadata.stableBundleId.flatMap { candidate in
|
|
1326
1334
|
if case .success(let storeDir) = bundleStoreDir() {
|
|
@@ -1539,6 +1547,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1539
1547
|
metadata.stagingBundleId = nil
|
|
1540
1548
|
metadata.stagingSelection = selection
|
|
1541
1549
|
metadata.verificationPending = false
|
|
1550
|
+
metadata.launchInProgress = false
|
|
1542
1551
|
metadata.pendingTransition = nil
|
|
1543
1552
|
metadata.pendingSelectionTransition = nil
|
|
1544
1553
|
}
|
|
@@ -1619,7 +1628,42 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1619
1628
|
DispatchQueue.global(qos: .background).async(execute: workItem)
|
|
1620
1629
|
}
|
|
1621
1630
|
|
|
1631
|
+
/**
|
|
1632
|
+
* Downloads `url`, trying again after a retryable failure (see
|
|
1633
|
+
* `DownloadError.isRetryable`) up to `DownloadError.maximumAttempts`
|
|
1634
|
+
* attempts in all, 1 and then 2 seconds apart, unless `retrying` is
|
|
1635
|
+
* false. The last attempt's result is returned, so a failure keeps what
|
|
1636
|
+
* classifies it. Blocks the calling thread.
|
|
1637
|
+
*/
|
|
1622
1638
|
private func downloadFileSynchronously(
|
|
1639
|
+
from url: URL,
|
|
1640
|
+
to destination: String,
|
|
1641
|
+
retrying: Bool = true,
|
|
1642
|
+
progressHandler: @escaping (DownloadProgress) -> Void
|
|
1643
|
+
) -> Result<URL, Error> {
|
|
1644
|
+
var attempt = 1
|
|
1645
|
+
while true {
|
|
1646
|
+
let result = downloadFileOnce(
|
|
1647
|
+
from: url,
|
|
1648
|
+
to: destination,
|
|
1649
|
+
progressHandler: progressHandler
|
|
1650
|
+
)
|
|
1651
|
+
guard case .failure(let error) = result,
|
|
1652
|
+
retrying,
|
|
1653
|
+
attempt < DownloadError.maximumAttempts,
|
|
1654
|
+
DownloadError.isRetryable(error) else {
|
|
1655
|
+
return result
|
|
1656
|
+
}
|
|
1657
|
+
let delay = downloadRetryDelay * pow(2, Double(attempt - 1))
|
|
1658
|
+
NSLog("[BundleStorage] Download failed (attempt \(attempt)/\(DownloadError.maximumAttempts)): \(error.localizedDescription). Retrying in \(delay)s")
|
|
1659
|
+
if delay > 0 {
|
|
1660
|
+
Thread.sleep(forTimeInterval: delay)
|
|
1661
|
+
}
|
|
1662
|
+
attempt += 1
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
private func downloadFileOnce(
|
|
1623
1667
|
from url: URL,
|
|
1624
1668
|
to destination: String,
|
|
1625
1669
|
progressHandler: @escaping (DownloadProgress) -> Void
|
|
@@ -1666,7 +1710,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1666
1710
|
do {
|
|
1667
1711
|
var bundlePaths: [String] = []
|
|
1668
1712
|
for assetPath in parsedManifest.assets.keys {
|
|
1669
|
-
let assetURL = try
|
|
1713
|
+
let assetURL = try FileUtilities.fileURL(
|
|
1670
1714
|
for: assetPath,
|
|
1671
1715
|
destinationRoot: directoryPath
|
|
1672
1716
|
)
|
|
@@ -1688,38 +1732,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1688
1732
|
}
|
|
1689
1733
|
}
|
|
1690
1734
|
|
|
1691
|
-
|
|
1692
|
-
if self.fileSystem.fileExists(atPath: iosBundlePath) {
|
|
1693
|
-
NSLog("[BundleStorage] Found iOS bundle atPath: \(iosBundlePath)")
|
|
1694
|
-
return .success(iosBundlePath)
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
let mainBundlePath = (directoryPath as NSString).appendingPathComponent("main.jsbundle")
|
|
1698
|
-
if self.fileSystem.fileExists(atPath: mainBundlePath) {
|
|
1699
|
-
NSLog("[BundleStorage] Found main bundle atPath: \(mainBundlePath)")
|
|
1700
|
-
return .success(mainBundlePath)
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
do {
|
|
1704
|
-
let contents = try self.fileSystem.contentsOfDirectory(atPath: directoryPath)
|
|
1705
|
-
NSLog("[BundleStorage] Directory contents: \(contents)")
|
|
1706
|
-
|
|
1707
|
-
// Additional search: check all .bundle files
|
|
1708
|
-
for file in contents {
|
|
1709
|
-
if file.hasSuffix(".bundle") {
|
|
1710
|
-
let bundlePath = (directoryPath as NSString).appendingPathComponent(file)
|
|
1711
|
-
NSLog("[BundleStorage] Found alternative bundle atPath: \(bundlePath)")
|
|
1712
|
-
return .success(bundlePath)
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
NSLog("[BundleStorage] No bundle file found in directory: \(directoryPath)")
|
|
1717
|
-
NSLog("[BundleStorage] Available files: \(contents)")
|
|
1718
|
-
return .success(nil)
|
|
1719
|
-
} catch let error {
|
|
1720
|
-
NSLog("[BundleStorage] Error reading directory contents: \(error.localizedDescription)")
|
|
1721
|
-
return .failure(error)
|
|
1722
|
-
}
|
|
1735
|
+
return .success(nil)
|
|
1723
1736
|
}
|
|
1724
1737
|
|
|
1725
1738
|
/**
|
|
@@ -1752,7 +1765,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1752
1765
|
let bundles = contents.compactMap { item -> String? in
|
|
1753
1766
|
let fullPath = (storeDir as NSString).appendingPathComponent(item)
|
|
1754
1767
|
|
|
1755
|
-
if protectedBundleStoreEntries.contains(item) {
|
|
1768
|
+
if protectedBundleStoreEntries.contains(item) || item.hasSuffix(".install-backup") {
|
|
1756
1769
|
return nil
|
|
1757
1770
|
}
|
|
1758
1771
|
|
|
@@ -1864,11 +1877,11 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1864
1877
|
}
|
|
1865
1878
|
|
|
1866
1879
|
let relativePath = String(bundleURL.path.dropFirst(sourcePrefix.count))
|
|
1867
|
-
guard
|
|
1880
|
+
guard FileUtilities.normalizedRelativePath(from: relativePath) == relativePath else {
|
|
1868
1881
|
throw BundleStorageError.invalidBundle
|
|
1869
1882
|
}
|
|
1870
1883
|
|
|
1871
|
-
return try
|
|
1884
|
+
return try FileUtilities.fileURL(
|
|
1872
1885
|
for: relativePath,
|
|
1873
1886
|
destinationRoot: destinationDirectory
|
|
1874
1887
|
).path
|
|
@@ -1924,8 +1937,8 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1924
1937
|
private func selectLaunch(bundle: Bundle) -> LaunchSelection {
|
|
1925
1938
|
guard let metadata = loadMetadataOrNull() else {
|
|
1926
1939
|
return LaunchSelection(
|
|
1927
|
-
bundleURL:
|
|
1928
|
-
launchedBundleId:
|
|
1940
|
+
bundleURL: getFallbackBundleURL(bundle: bundle),
|
|
1941
|
+
launchedBundleId: nil,
|
|
1929
1942
|
shouldRollbackOnCrash: false
|
|
1930
1943
|
)
|
|
1931
1944
|
}
|
|
@@ -1972,13 +1985,24 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1972
1985
|
}
|
|
1973
1986
|
|
|
1974
1987
|
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection {
|
|
1988
|
+
builtInAssetResolver.use(bundle: bundle)
|
|
1975
1989
|
saveLaunchReport(nil)
|
|
1976
|
-
|
|
1977
|
-
let identity = InstallIdentity()
|
|
1978
|
-
_ = saveInstallIdentity(identity)
|
|
1979
|
-
}
|
|
1990
|
+
loadOrCreateInstallIdentity()
|
|
1980
1991
|
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
1981
|
-
|
|
1992
|
+
// Only a new storage instance consumes an unfinished launch. Repeated bundle
|
|
1993
|
+
// lookups or bridge reloads in this process must not reject its own launch.
|
|
1994
|
+
if !hasPreparedLaunch,
|
|
1995
|
+
let metadata = loadMetadataOrNull(),
|
|
1996
|
+
metadata.verificationPending,
|
|
1997
|
+
metadata.launchInProgress,
|
|
1998
|
+
let stagingBundleId = metadata.stagingBundleId {
|
|
1999
|
+
rollbackPendingBundle(stagingBundleId, unfinishedLaunch: true)
|
|
2000
|
+
}
|
|
2001
|
+
hasPreparedLaunch = true
|
|
2002
|
+
// A launch is recorded only once it can show UI (markLaunchStarted).
|
|
2003
|
+
let selection = selectLaunch(bundle: bundle)
|
|
2004
|
+
pendingLaunchBundleId = selection.shouldRollbackOnCrash ? selection.launchedBundleId : nil
|
|
2005
|
+
return selection
|
|
1982
2006
|
}
|
|
1983
2007
|
|
|
1984
2008
|
// MARK: - Bundle Update
|
|
@@ -1986,943 +2010,805 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1986
2010
|
/**
|
|
1987
2011
|
* Updates the bundle from the specified URL. This operation is asynchronous.
|
|
1988
2012
|
* @param bundleId ID of the bundle to update
|
|
1989
|
-
* @param
|
|
1990
|
-
* @param
|
|
2013
|
+
* @param manifestUrl URL of the complete target manifest
|
|
2014
|
+
* @param manifestFileHash Hash or signature of the target manifest
|
|
1991
2015
|
* @param progressHandler Callback for download and extraction progress (0.0 to 1.0)
|
|
1992
2016
|
* @param completion Callback with result of the operation
|
|
1993
2017
|
*/
|
|
1994
|
-
func updateBundle(bundleId: String,
|
|
2018
|
+
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL? = nil, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void) {
|
|
1995
2019
|
// Check if bundle is in crashed history
|
|
1996
|
-
|
|
1997
|
-
if crashedHistory.contains(bundleId) {
|
|
2020
|
+
if getCrashHistory().contains(bundleId) {
|
|
1998
2021
|
NSLog("[BundleStorage] Bundle '\(bundleId)' is in crashed history, rejecting update")
|
|
1999
2022
|
completion(.failure(BundleStorageError.bundleInCrashedHistory(bundleId)))
|
|
2000
2023
|
return
|
|
2001
2024
|
}
|
|
2002
2025
|
|
|
2003
|
-
|
|
2004
|
-
let currentBundleId = self.getCachedBundleId()
|
|
2005
|
-
|
|
2006
|
-
guard let validFileUrl = fileUrl else {
|
|
2007
|
-
NSLog("[BundleStorage] fileUrl is nil, resetting bundle URL.")
|
|
2008
|
-
// Dispatch the sequence to the file operation queue to ensure completion is called asynchronously
|
|
2009
|
-
// and to keep file operations off the calling thread if it's the main thread.
|
|
2010
|
-
fileOperationQueue.async {
|
|
2011
|
-
let setResult = self.setBundleURL(localPath: nil)
|
|
2012
|
-
switch setResult {
|
|
2013
|
-
case .success:
|
|
2014
|
-
let previousMetadata = self.loadMetadataOrNull()
|
|
2015
|
-
var resetMetadata = self.createInitialMetadata()
|
|
2016
|
-
resetMetadata.highestSeenCatalogs = previousMetadata?.highestSeenCatalogs ?? [:]
|
|
2017
|
-
resetMetadata.currentSelectionContexts = previousMetadata?.currentSelectionContexts ?? [:]
|
|
2018
|
-
let _ = self.saveMetadata(resetMetadata)
|
|
2019
|
-
self.saveLaunchReport(nil)
|
|
2020
|
-
let cleanupResult = self.cleanupOldBundles(currentBundleId: currentBundleId, bundleId: bundleId)
|
|
2021
|
-
switch cleanupResult {
|
|
2022
|
-
case .success:
|
|
2023
|
-
completion(.success(true))
|
|
2024
|
-
case .failure(let error):
|
|
2025
|
-
NSLog("[BundleStorage] Error during cleanup after reset: \(error)")
|
|
2026
|
-
completion(.failure(error))
|
|
2027
|
-
}
|
|
2028
|
-
case .failure(let error):
|
|
2029
|
-
NSLog("[BundleStorage] Error resetting bundle URL: \(error)")
|
|
2030
|
-
completion(.failure(error))
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
return
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
// Start the bundle update process on a background queue
|
|
2037
|
-
fileOperationQueue.async {
|
|
2038
|
-
|
|
2026
|
+
fileOperationQueue.async(flags: .barrier) {
|
|
2039
2027
|
let storeDirResult = self.bundleStoreDir()
|
|
2040
2028
|
guard case .success(let storeDir) = storeDirResult else {
|
|
2041
|
-
completion(.failure(
|
|
2029
|
+
completion(.failure(UpdateFailureError.classifying(
|
|
2030
|
+
storeDirResult.failureError ?? BundleStorageError.unknown(nil),
|
|
2031
|
+
during: .storage
|
|
2032
|
+
)))
|
|
2042
2033
|
return
|
|
2043
2034
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
if let bundlePath = existingBundlePath {
|
|
2055
|
-
NSLog("[BundleStorage] Using cached bundle at path: \(bundlePath)")
|
|
2056
|
-
let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
|
|
2057
|
-
guard let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId) else {
|
|
2058
|
-
completion(.failure(BundleStorageError.unknown(
|
|
2059
|
-
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2060
|
-
NSLocalizedDescriptionKey: "Release catalog selection is stale"
|
|
2061
|
-
])
|
|
2062
|
-
)))
|
|
2063
|
-
return
|
|
2064
|
-
}
|
|
2065
|
-
let setResult = self.setBundleURL(localPath: bundlePath)
|
|
2066
|
-
switch setResult {
|
|
2067
|
-
case .success:
|
|
2068
|
-
let _ = self.saveMetadata(updatedMetadata)
|
|
2069
|
-
NSLog("[BundleStorage] Set staging bundle (cached): \(bundleId), verificationPending: true")
|
|
2070
|
-
|
|
2071
|
-
self.emitArchiveProgress(
|
|
2072
|
-
progressHandler: progressHandler,
|
|
2073
|
-
progress: UpdateProgress.complete
|
|
2074
|
-
)
|
|
2075
|
-
self.scheduleCleanupOldBundles(
|
|
2076
|
-
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
2077
|
-
)
|
|
2078
|
-
completion(.success(true))
|
|
2079
|
-
case .failure(let error):
|
|
2080
|
-
completion(.failure(error))
|
|
2081
|
-
}
|
|
2082
|
-
return
|
|
2083
|
-
} else {
|
|
2084
|
-
NSLog("[BundleStorage] Cached directory exists but invalid, removing: \(finalBundleDir)")
|
|
2085
|
-
do {
|
|
2086
|
-
try self.fileSystem.removeItem(atPath: finalBundleDir)
|
|
2087
|
-
// Continue with download process on success
|
|
2088
|
-
if let manifestUrl,
|
|
2089
|
-
let manifestFileHash,
|
|
2090
|
-
let changedAssets,
|
|
2091
|
-
self.canUseManifestDrivenInstall() {
|
|
2092
|
-
self.updateBundleFromManifest(
|
|
2093
|
-
bundleId: bundleId,
|
|
2094
|
-
fileUrl: validFileUrl,
|
|
2095
|
-
fileHash: fileHash,
|
|
2096
|
-
manifestUrl: manifestUrl,
|
|
2097
|
-
manifestFileHash: manifestFileHash,
|
|
2098
|
-
changedAssets: changedAssets,
|
|
2099
|
-
storeDir: storeDir,
|
|
2100
|
-
progressHandler: progressHandler,
|
|
2101
|
-
completion: completion
|
|
2102
|
-
)
|
|
2103
|
-
} else {
|
|
2104
|
-
if manifestUrl != nil && manifestFileHash != nil && changedAssets != nil {
|
|
2105
|
-
NSLog("[BundleStorage] Skipping manifest-driven install for \(bundleId) because no active OTA manifest is available. Using archive.")
|
|
2106
|
-
}
|
|
2107
|
-
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, fileHash: fileHash, storeDir: storeDir, progressHandler: progressHandler, completion: completion)
|
|
2108
|
-
}
|
|
2109
|
-
} catch let error {
|
|
2110
|
-
NSLog("[BundleStorage] Failed to remove invalid bundle dir: \(error.localizedDescription)")
|
|
2111
|
-
completion(.failure(BundleStorageError.unknown(error)))
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
case .failure(let error):
|
|
2115
|
-
completion(.failure(error))
|
|
2116
|
-
}
|
|
2117
|
-
} else {
|
|
2118
|
-
if let manifestUrl,
|
|
2119
|
-
let manifestFileHash,
|
|
2120
|
-
let changedAssets,
|
|
2121
|
-
self.canUseManifestDrivenInstall() {
|
|
2122
|
-
self.updateBundleFromManifest(
|
|
2123
|
-
bundleId: bundleId,
|
|
2124
|
-
fileUrl: validFileUrl,
|
|
2125
|
-
fileHash: fileHash,
|
|
2126
|
-
manifestUrl: manifestUrl,
|
|
2127
|
-
manifestFileHash: manifestFileHash,
|
|
2128
|
-
changedAssets: changedAssets,
|
|
2129
|
-
storeDir: storeDir,
|
|
2130
|
-
progressHandler: progressHandler,
|
|
2131
|
-
completion: completion
|
|
2132
|
-
)
|
|
2133
|
-
} else {
|
|
2134
|
-
if manifestUrl != nil && manifestFileHash != nil && changedAssets != nil {
|
|
2135
|
-
NSLog("[BundleStorage] Skipping manifest-driven install for \(bundleId) because no active OTA manifest is available. Using archive.")
|
|
2136
|
-
}
|
|
2137
|
-
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, fileHash: fileHash, storeDir: storeDir, progressHandler: progressHandler, completion: completion)
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2035
|
+
self.updateBundleFromManifest(
|
|
2036
|
+
bundleId: bundleId,
|
|
2037
|
+
manifestUrl: manifestUrl,
|
|
2038
|
+
manifestFileHash: manifestFileHash,
|
|
2039
|
+
archiveUrl: archiveUrl,
|
|
2040
|
+
changedAssets: assets,
|
|
2041
|
+
storeDir: storeDir,
|
|
2042
|
+
progressHandler: progressHandler,
|
|
2043
|
+
completion: completion
|
|
2044
|
+
)
|
|
2140
2045
|
}
|
|
2141
2046
|
}
|
|
2142
|
-
|
|
2143
|
-
/**
|
|
2144
|
-
* Prepares directories and starts the download process.
|
|
2145
|
-
* This method is part of the asynchronous `updateBundle` flow.
|
|
2146
|
-
* @param bundleId ID of the bundle to update
|
|
2147
|
-
* @param fileUrl URL of the bundle file to download
|
|
2148
|
-
* @param fileHash Combined hash string for verification (sig:<signature> or <hex_hash>)
|
|
2149
|
-
* @param storeDir Path to the bundle-store directory
|
|
2150
|
-
* @param progressHandler Callback for download and extraction progress
|
|
2151
|
-
* @param completion Callback with result of the operation
|
|
2152
|
-
*/
|
|
2153
|
-
private func updateBundleFromManifest(
|
|
2154
|
-
bundleId: String,
|
|
2155
|
-
fileUrl: URL,
|
|
2156
|
-
fileHash: String?,
|
|
2157
|
-
manifestUrl: URL,
|
|
2158
|
-
manifestFileHash: String,
|
|
2159
|
-
changedAssets: [String: ChangedAssetDescriptor],
|
|
2160
|
-
storeDir: String,
|
|
2161
|
-
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
2162
|
-
completion: @escaping (Result<Bool, Error>) -> Void
|
|
2163
|
-
) {
|
|
2164
|
-
let tempDirResult = tempDir()
|
|
2165
|
-
guard case .success(let tempDirectory) = tempDirResult else {
|
|
2166
|
-
completion(.failure(tempDirResult.failureError ?? BundleStorageError.unknown(nil)))
|
|
2167
|
-
return
|
|
2168
|
-
}
|
|
2169
2047
|
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2048
|
+
/// How the per-file install produced one file.
|
|
2049
|
+
private enum AssetInstallOutcome {
|
|
2050
|
+
case patched
|
|
2051
|
+
/// `patchFallback` is true when a patch for the running bundle was
|
|
2052
|
+
/// offered but the original file was downloaded instead.
|
|
2053
|
+
case downloaded(patchFallback: Bool)
|
|
2054
|
+
}
|
|
2175
2055
|
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2056
|
+
private func downloadManifestAsset(
|
|
2057
|
+
assetPath: String,
|
|
2058
|
+
expectedAsset: ParsedManifestAsset,
|
|
2059
|
+
changedAsset: ChangedAssetDescriptor,
|
|
2060
|
+
currentBundleId: String?,
|
|
2061
|
+
currentBundleDir: String?,
|
|
2062
|
+
destinationPath: String,
|
|
2063
|
+
tempDirectory: String,
|
|
2064
|
+
progressFile: UpdateProgressPayload.DiffProgressFileSnapshot,
|
|
2065
|
+
progressHandler: @escaping (UpdateProgressPayload) -> Void
|
|
2066
|
+
) throws -> AssetInstallOutcome {
|
|
2067
|
+
let expectedHash = expectedAsset.fileHash
|
|
2068
|
+
var diffFiles = [progressFile]
|
|
2069
|
+
let patched = applyPatchAssetIfPossible(
|
|
2070
|
+
assetPath: assetPath,
|
|
2071
|
+
changedAsset: changedAsset,
|
|
2072
|
+
currentBundleId: currentBundleId,
|
|
2073
|
+
currentBundleDir: currentBundleDir,
|
|
2074
|
+
destinationPath: destinationPath,
|
|
2075
|
+
expectedHash: expectedHash,
|
|
2076
|
+
tempDirectory: tempDirectory,
|
|
2077
|
+
files: &diffFiles,
|
|
2078
|
+
progressHandler: progressHandler
|
|
2079
|
+
)
|
|
2080
|
+
if patched {
|
|
2081
|
+
// The patch produced these bytes, so a mismatch is a patch failure.
|
|
2082
|
+
try UpdateFailureError.during(.patch, resource: .patch) {
|
|
2083
|
+
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
2084
|
+
}
|
|
2085
|
+
updateDiffProgressFile(
|
|
2086
|
+
files: &diffFiles,
|
|
2087
|
+
assetPath: assetPath,
|
|
2088
|
+
status: "downloaded",
|
|
2089
|
+
progress: 1
|
|
2090
|
+
)
|
|
2091
|
+
self.emitDiffProgress(
|
|
2092
|
+
progressHandler: progressHandler,
|
|
2093
|
+
phase: diffFiles.allSatisfy { $0.status == "downloaded" }
|
|
2094
|
+
? "finalizing"
|
|
2095
|
+
: "downloading",
|
|
2096
|
+
files: diffFiles
|
|
2097
|
+
)
|
|
2098
|
+
return .patched
|
|
2179
2099
|
}
|
|
2180
|
-
let tempManifestPath = (tempDirectory as NSString).appendingPathComponent("manifest.json")
|
|
2181
|
-
let tmpDir = (storeDir as NSString).appendingPathComponent("\(bundleId).tmp")
|
|
2182
|
-
let realDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
2183
|
-
var diffFiles = createDiffProgressFiles(changedAssets: changedAssets)
|
|
2184
2100
|
|
|
2185
|
-
|
|
2101
|
+
guard let changedAssetFileUrl = changedAsset.fileUrl else {
|
|
2102
|
+
updateDiffProgressFile(
|
|
2103
|
+
files: &diffFiles,
|
|
2104
|
+
assetPath: assetPath,
|
|
2105
|
+
status: "failed",
|
|
2106
|
+
progress: 0
|
|
2107
|
+
)
|
|
2186
2108
|
self.emitDiffProgress(
|
|
2187
2109
|
progressHandler: progressHandler,
|
|
2188
|
-
phase: "
|
|
2189
|
-
files: diffFiles
|
|
2190
|
-
manifestProgress: 0
|
|
2110
|
+
phase: "downloading",
|
|
2111
|
+
files: diffFiles
|
|
2191
2112
|
)
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
manifestProgress: progress.progress
|
|
2201
|
-
)
|
|
2202
|
-
}
|
|
2203
|
-
) {
|
|
2204
|
-
case .success:
|
|
2205
|
-
break
|
|
2206
|
-
case .failure(let error):
|
|
2207
|
-
if let downloadError = error as? DownloadError,
|
|
2208
|
-
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
2209
|
-
throw BundleStorageError.incompleteDownload(expected: expected, actual: actual)
|
|
2210
|
-
}
|
|
2211
|
-
throw BundleStorageError.downloadFailed(error)
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
let manifestVerificationResult = SignatureVerifier.verifyBundle(
|
|
2215
|
-
fileURL: URL(fileURLWithPath: tempManifestPath),
|
|
2216
|
-
fileHash: manifestFileHash
|
|
2113
|
+
// With no original file to fall back to, the patch decides the install.
|
|
2114
|
+
throw UpdateFailureError(
|
|
2115
|
+
underlying: BundleStorageError.downloadFailed(
|
|
2116
|
+
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2117
|
+
NSLocalizedDescriptionKey: "Changed asset fileUrl missing and patch could not be applied: \(assetPath)"
|
|
2118
|
+
])
|
|
2119
|
+
),
|
|
2120
|
+
failure: UpdateFailure.patch.with(resource: .patch)
|
|
2217
2121
|
)
|
|
2218
|
-
|
|
2219
|
-
if case .failure(let error) = manifestVerificationResult {
|
|
2220
|
-
throw BundleStorageError.signatureVerificationFailed(error)
|
|
2221
|
-
}
|
|
2222
|
-
throw BundleStorageError.unknown(nil)
|
|
2223
|
-
}
|
|
2122
|
+
}
|
|
2224
2123
|
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
files: diffFiles
|
|
2124
|
+
let changedAssetDownloadPath = downloadPath(
|
|
2125
|
+
assetPath: assetPath,
|
|
2126
|
+
changedAsset: changedAsset
|
|
2127
|
+
)
|
|
2128
|
+
let downloadDestinationPath: String
|
|
2129
|
+
if changedAsset.fileCompression == "br" {
|
|
2130
|
+
downloadDestinationPath = compressedTempPath(
|
|
2131
|
+
tempDirectory: tempDirectory,
|
|
2132
|
+
assetPath: assetPath,
|
|
2133
|
+
compression: "br"
|
|
2236
2134
|
)
|
|
2135
|
+
} else {
|
|
2136
|
+
downloadDestinationPath = destinationPath
|
|
2137
|
+
}
|
|
2237
2138
|
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
try self.fileSystem.removeItem(atPath: tmpDir)
|
|
2244
|
-
}
|
|
2245
|
-
guard self.fileSystem.createDirectory(atPath: tmpDir) else {
|
|
2246
|
-
throw BundleStorageError.directoryCreationFailed
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
let sortedAssets = targetManifest.assets.sorted { $0.key < $1.key }
|
|
2250
|
-
|
|
2251
|
-
for asset in sortedAssets {
|
|
2252
|
-
let assetPath = asset.key
|
|
2253
|
-
let expectedAsset = asset.value
|
|
2254
|
-
let expectedHash = expectedAsset.fileHash
|
|
2255
|
-
let destinationPath = try ArchiveExtractionUtilities.extractionURL(
|
|
2256
|
-
for: assetPath,
|
|
2257
|
-
destinationRoot: tmpDir
|
|
2258
|
-
).path
|
|
2259
|
-
let destinationDir = (destinationPath as NSString).deletingLastPathComponent
|
|
2260
|
-
guard self.fileSystem.createDirectory(atPath: destinationDir) else {
|
|
2261
|
-
throw BundleStorageError.directoryCreationFailed
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
if currentManifest?.assets[assetPath]?.fileHash == expectedHash {
|
|
2265
|
-
guard let currentBundleDir,
|
|
2266
|
-
self.fileSystem.fileExists(atPath: currentBundleDir)
|
|
2267
|
-
else {
|
|
2268
|
-
throw BundleStorageError.downloadFailed(
|
|
2269
|
-
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2270
|
-
NSLocalizedDescriptionKey: "Current bundle directory unavailable for reused asset: \(assetPath)"
|
|
2271
|
-
])
|
|
2272
|
-
)
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
let sourcePath = try ArchiveExtractionUtilities.extractionURL(
|
|
2276
|
-
for: assetPath,
|
|
2277
|
-
destinationRoot: currentBundleDir
|
|
2278
|
-
).path
|
|
2279
|
-
guard self.fileSystem.fileExists(atPath: sourcePath),
|
|
2280
|
-
HashUtils.verifyHash(fileURL: URL(fileURLWithPath: sourcePath), expectedHash: expectedHash)
|
|
2281
|
-
else {
|
|
2282
|
-
throw BundleStorageError.signatureVerificationFailed(.fileHashMismatch)
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
try self.fileSystem.copyItem(atPath: sourcePath, toPath: destinationPath)
|
|
2286
|
-
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
2287
|
-
continue
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
guard let changedAsset = changedAssets[assetPath] else {
|
|
2291
|
-
updateDiffProgressFile(
|
|
2292
|
-
files: &diffFiles,
|
|
2293
|
-
assetPath: assetPath,
|
|
2294
|
-
status: "failed",
|
|
2295
|
-
progress: 0
|
|
2296
|
-
)
|
|
2297
|
-
self.emitDiffProgress(
|
|
2298
|
-
progressHandler: progressHandler,
|
|
2299
|
-
phase: "downloading",
|
|
2300
|
-
files: diffFiles
|
|
2301
|
-
)
|
|
2302
|
-
throw BundleStorageError.downloadFailed(
|
|
2303
|
-
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2304
|
-
NSLocalizedDescriptionKey: "Changed asset missing from update response: \(assetPath)"
|
|
2305
|
-
])
|
|
2306
|
-
)
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
guard changedAsset.fileHash.caseInsensitiveCompare(expectedHash) == .orderedSame else {
|
|
2310
|
-
updateDiffProgressFile(
|
|
2311
|
-
files: &diffFiles,
|
|
2312
|
-
assetPath: assetPath,
|
|
2313
|
-
status: "failed",
|
|
2314
|
-
progress: 0
|
|
2315
|
-
)
|
|
2316
|
-
self.emitDiffProgress(
|
|
2317
|
-
progressHandler: progressHandler,
|
|
2318
|
-
phase: "downloading",
|
|
2319
|
-
files: diffFiles
|
|
2320
|
-
)
|
|
2321
|
-
throw BundleStorageError.signatureVerificationFailed(.fileHashMismatch)
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
let patched = applyPatchAssetIfPossible(
|
|
2325
|
-
assetPath: assetPath,
|
|
2326
|
-
changedAsset: changedAsset,
|
|
2327
|
-
currentBundleId: currentBundleId,
|
|
2328
|
-
currentBundleDir: currentBundleDir,
|
|
2329
|
-
destinationPath: destinationPath,
|
|
2330
|
-
expectedHash: expectedHash,
|
|
2331
|
-
tempDirectory: tempDirectory,
|
|
2139
|
+
switch self.downloadFileSynchronously(
|
|
2140
|
+
from: changedAssetFileUrl,
|
|
2141
|
+
to: downloadDestinationPath,
|
|
2142
|
+
progressHandler: { progress in
|
|
2143
|
+
self.updateDiffProgressFile(
|
|
2332
2144
|
files: &diffFiles,
|
|
2333
|
-
|
|
2145
|
+
assetPath: assetPath,
|
|
2146
|
+
status: "downloading",
|
|
2147
|
+
progress: progress.progress,
|
|
2148
|
+
downloadPath: changedAssetDownloadPath,
|
|
2149
|
+
downloadedBytes: progress.downloadedBytes,
|
|
2150
|
+
totalBytes: progress.totalBytes
|
|
2334
2151
|
)
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2152
|
+
self.emitDiffProgress(
|
|
2153
|
+
progressHandler: progressHandler,
|
|
2154
|
+
phase: "downloading",
|
|
2155
|
+
files: diffFiles
|
|
2156
|
+
)
|
|
2157
|
+
}
|
|
2158
|
+
) {
|
|
2159
|
+
case .success(let downloadedFileURL):
|
|
2160
|
+
do {
|
|
2161
|
+
try UpdateFailureError.during(.extraction, resource: .file) {
|
|
2162
|
+
try materializeDownloadedAsset(
|
|
2163
|
+
downloadedPath: downloadedFileURL.path,
|
|
2164
|
+
destinationPath: destinationPath,
|
|
2165
|
+
changedAsset: changedAsset
|
|
2349
2166
|
)
|
|
2350
|
-
continue
|
|
2351
2167
|
}
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
self.emitDiffProgress(
|
|
2361
|
-
progressHandler: progressHandler,
|
|
2362
|
-
phase: "downloading",
|
|
2363
|
-
files: diffFiles
|
|
2364
|
-
)
|
|
2365
|
-
throw BundleStorageError.downloadFailed(
|
|
2366
|
-
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2367
|
-
NSLocalizedDescriptionKey: "Changed asset fileUrl missing and patch could not be applied: \(assetPath)"
|
|
2368
|
-
])
|
|
2168
|
+
// Compressed files are checked after decoding, so their
|
|
2169
|
+
// mismatch is an extraction failure.
|
|
2170
|
+
let verificationPhase: UpdatePhase =
|
|
2171
|
+
changedAsset.fileCompression == "br" ? .extraction : .verification
|
|
2172
|
+
try UpdateFailureError.during(verificationPhase, resource: .file) {
|
|
2173
|
+
try verifyManifestAssetFile(
|
|
2174
|
+
atPath: destinationPath,
|
|
2175
|
+
asset: expectedAsset
|
|
2369
2176
|
)
|
|
2370
2177
|
}
|
|
2371
|
-
|
|
2372
|
-
|
|
2178
|
+
} catch {
|
|
2179
|
+
updateDiffProgressFile(
|
|
2180
|
+
files: &diffFiles,
|
|
2373
2181
|
assetPath: assetPath,
|
|
2374
|
-
|
|
2182
|
+
status: "failed",
|
|
2183
|
+
progress: 1
|
|
2375
2184
|
)
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
)
|
|
2383
|
-
} else {
|
|
2384
|
-
downloadDestinationPath = destinationPath
|
|
2385
|
-
}
|
|
2386
|
-
|
|
2387
|
-
switch self.downloadFileSynchronously(
|
|
2388
|
-
from: changedAssetFileUrl,
|
|
2389
|
-
to: downloadDestinationPath,
|
|
2390
|
-
progressHandler: { progress in
|
|
2391
|
-
self.updateDiffProgressFile(
|
|
2392
|
-
files: &diffFiles,
|
|
2393
|
-
assetPath: assetPath,
|
|
2394
|
-
status: "downloading",
|
|
2395
|
-
progress: progress.progress,
|
|
2396
|
-
downloadPath: changedAssetDownloadPath,
|
|
2397
|
-
downloadedBytes: progress.downloadedBytes,
|
|
2398
|
-
totalBytes: progress.totalBytes
|
|
2399
|
-
)
|
|
2400
|
-
self.emitDiffProgress(
|
|
2401
|
-
progressHandler: progressHandler,
|
|
2402
|
-
phase: "downloading",
|
|
2403
|
-
files: diffFiles
|
|
2404
|
-
)
|
|
2405
|
-
}
|
|
2406
|
-
) {
|
|
2407
|
-
case .success(let downloadedFileURL):
|
|
2408
|
-
do {
|
|
2409
|
-
try materializeDownloadedAsset(
|
|
2410
|
-
downloadedPath: downloadedFileURL.path,
|
|
2411
|
-
destinationPath: destinationPath,
|
|
2412
|
-
changedAsset: changedAsset
|
|
2413
|
-
)
|
|
2414
|
-
try verifyManifestAssetFile(
|
|
2415
|
-
atPath: destinationPath,
|
|
2416
|
-
asset: expectedAsset
|
|
2417
|
-
)
|
|
2418
|
-
} catch {
|
|
2419
|
-
updateDiffProgressFile(
|
|
2420
|
-
files: &diffFiles,
|
|
2421
|
-
assetPath: assetPath,
|
|
2422
|
-
status: "failed",
|
|
2423
|
-
progress: 1
|
|
2424
|
-
)
|
|
2425
|
-
self.emitDiffProgress(
|
|
2426
|
-
progressHandler: progressHandler,
|
|
2427
|
-
phase: "downloading",
|
|
2428
|
-
files: diffFiles
|
|
2429
|
-
)
|
|
2430
|
-
throw error
|
|
2431
|
-
}
|
|
2432
|
-
updateDiffProgressFile(
|
|
2433
|
-
files: &diffFiles,
|
|
2434
|
-
assetPath: assetPath,
|
|
2435
|
-
status: "downloaded",
|
|
2436
|
-
progress: 1
|
|
2437
|
-
)
|
|
2438
|
-
self.emitDiffProgress(
|
|
2439
|
-
progressHandler: progressHandler,
|
|
2440
|
-
phase: diffFiles.allSatisfy { $0.status == "downloaded" }
|
|
2441
|
-
? "finalizing"
|
|
2442
|
-
: "downloading",
|
|
2443
|
-
files: diffFiles
|
|
2444
|
-
)
|
|
2445
|
-
case .failure(let error):
|
|
2446
|
-
let lastKnownProgress = diffFiles.first(where: { $0.path == assetPath })?.progress ?? 0
|
|
2447
|
-
updateDiffProgressFile(
|
|
2448
|
-
files: &diffFiles,
|
|
2449
|
-
assetPath: assetPath,
|
|
2450
|
-
status: "failed",
|
|
2451
|
-
progress: lastKnownProgress
|
|
2452
|
-
)
|
|
2453
|
-
self.emitDiffProgress(
|
|
2454
|
-
progressHandler: progressHandler,
|
|
2455
|
-
phase: "downloading",
|
|
2456
|
-
files: diffFiles
|
|
2457
|
-
)
|
|
2458
|
-
if let downloadError = error as? DownloadError,
|
|
2459
|
-
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
2460
|
-
throw BundleStorageError.incompleteDownload(expected: expected, actual: actual)
|
|
2461
|
-
}
|
|
2462
|
-
throw BundleStorageError.downloadFailed(error)
|
|
2463
|
-
}
|
|
2185
|
+
self.emitDiffProgress(
|
|
2186
|
+
progressHandler: progressHandler,
|
|
2187
|
+
phase: "downloading",
|
|
2188
|
+
files: diffFiles
|
|
2189
|
+
)
|
|
2190
|
+
throw error
|
|
2464
2191
|
}
|
|
2465
|
-
|
|
2192
|
+
updateDiffProgressFile(
|
|
2193
|
+
files: &diffFiles,
|
|
2194
|
+
assetPath: assetPath,
|
|
2195
|
+
status: "downloaded",
|
|
2196
|
+
progress: 1
|
|
2197
|
+
)
|
|
2466
2198
|
self.emitDiffProgress(
|
|
2467
2199
|
progressHandler: progressHandler,
|
|
2468
|
-
phase: "
|
|
2200
|
+
phase: diffFiles.allSatisfy { $0.status == "downloaded" }
|
|
2201
|
+
? "finalizing"
|
|
2202
|
+
: "downloading",
|
|
2469
2203
|
files: diffFiles
|
|
2470
2204
|
)
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
try self.fileSystem.moveItem(atPath: tmpDir, toPath: realDir)
|
|
2487
|
-
} catch {
|
|
2488
|
-
throw BundleStorageError.moveOperationFailed(error)
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
let finalBundlePath = try resolveBundlePathAfterMove(
|
|
2492
|
-
bundlePathInTmp,
|
|
2493
|
-
from: tmpDir,
|
|
2494
|
-
to: realDir
|
|
2495
|
-
)
|
|
2496
|
-
let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
|
|
2497
|
-
guard let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId) else {
|
|
2498
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2499
|
-
completion(.failure(BundleStorageError.unknown(
|
|
2500
|
-
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2501
|
-
NSLocalizedDescriptionKey: "Release catalog selection is stale"
|
|
2502
|
-
])
|
|
2503
|
-
)))
|
|
2504
|
-
return
|
|
2505
|
-
}
|
|
2506
|
-
switch self.setBundleURL(localPath: finalBundlePath) {
|
|
2507
|
-
case .success:
|
|
2508
|
-
let _ = self.saveMetadata(updatedMetadata)
|
|
2509
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2510
|
-
self.scheduleCleanupOldBundles(
|
|
2511
|
-
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
2512
|
-
)
|
|
2513
|
-
progressHandler(
|
|
2514
|
-
UpdateProgressPayload(
|
|
2515
|
-
progress: UpdateProgress.complete,
|
|
2516
|
-
artifactType: "diff",
|
|
2517
|
-
details: UpdateProgressPayload.DiffProgressDetails(
|
|
2518
|
-
totalFilesCount: diffFiles.count,
|
|
2519
|
-
completedFilesCount: diffFiles.filter { $0.status == "downloaded" }.count,
|
|
2520
|
-
files: diffFiles
|
|
2521
|
-
)
|
|
2522
|
-
)
|
|
2523
|
-
)
|
|
2524
|
-
completion(.success(true))
|
|
2525
|
-
case .failure(let error):
|
|
2526
|
-
try? self.fileSystem.removeItem(atPath: realDir)
|
|
2527
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2528
|
-
completion(.failure(error))
|
|
2529
|
-
}
|
|
2530
|
-
case .failure(let error):
|
|
2531
|
-
throw error
|
|
2532
|
-
}
|
|
2533
|
-
} catch {
|
|
2534
|
-
NSLog("[BundleStorage] Manifest-driven install failed: \(error.localizedDescription). Falling back to archive.")
|
|
2535
|
-
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
2536
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2537
|
-
self.prepareAndDownloadBundle(
|
|
2538
|
-
bundleId: bundleId,
|
|
2539
|
-
fileUrl: fileUrl,
|
|
2540
|
-
fileHash: fileHash,
|
|
2541
|
-
storeDir: storeDir,
|
|
2205
|
+
// A patch for the running bundle was offered, but the original
|
|
2206
|
+
// file came instead; a patch for another base was never tried.
|
|
2207
|
+
let patchWasTried = changedAsset.patch.map {
|
|
2208
|
+
$0.algorithm == "bsdiff" && $0.baseBundleId == currentBundleId
|
|
2209
|
+
} ?? false
|
|
2210
|
+
return .downloaded(patchFallback: patchWasTried)
|
|
2211
|
+
case .failure(let error):
|
|
2212
|
+
let lastKnownProgress = diffFiles.first(where: { $0.path == assetPath })?.progress ?? 0
|
|
2213
|
+
updateDiffProgressFile(
|
|
2214
|
+
files: &diffFiles,
|
|
2215
|
+
assetPath: assetPath,
|
|
2216
|
+
status: "failed",
|
|
2217
|
+
progress: lastKnownProgress
|
|
2218
|
+
)
|
|
2219
|
+
self.emitDiffProgress(
|
|
2542
2220
|
progressHandler: progressHandler,
|
|
2543
|
-
|
|
2221
|
+
phase: "downloading",
|
|
2222
|
+
files: diffFiles
|
|
2544
2223
|
)
|
|
2224
|
+
let transferError: Error
|
|
2225
|
+
if let downloadError = error as? DownloadError,
|
|
2226
|
+
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
2227
|
+
transferError = BundleStorageError.incompleteDownload(expected: expected, actual: actual)
|
|
2228
|
+
} else {
|
|
2229
|
+
transferError = BundleStorageError.downloadFailed(error)
|
|
2230
|
+
}
|
|
2231
|
+
throw UpdateFailureError.classifying(transferError, during: .transfer, resource: .file)
|
|
2545
2232
|
}
|
|
2546
2233
|
}
|
|
2547
2234
|
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
completion(.failure(tempDirResult.failureError ?? BundleStorageError.unknown(nil)))
|
|
2560
|
-
return
|
|
2561
|
-
}
|
|
2562
|
-
|
|
2563
|
-
// 2) Clean up any previous temp dir
|
|
2564
|
-
try? self.fileSystem.removeItem(atPath: tempDirectory)
|
|
2565
|
-
|
|
2566
|
-
// 3) Create temp dir
|
|
2567
|
-
if !self.fileSystem.createDirectory(atPath: tempDirectory) {
|
|
2568
|
-
completion(.failure(BundleStorageError.directoryCreationFailed))
|
|
2569
|
-
return
|
|
2235
|
+
// Retain only target files on retry; partial files are rehashed before reuse.
|
|
2236
|
+
private func pruneStagingDirectory(_ directory: String, assets: Set<String>, prefix: String = "") throws {
|
|
2237
|
+
for name in try fileSystem.contentsOfDirectory(atPath: directory) {
|
|
2238
|
+
let relativePath = prefix + name
|
|
2239
|
+
let path = (directory as NSString).appendingPathComponent(name)
|
|
2240
|
+
let type = try fileSystem.attributesOfItem(atPath: path)[.type] as? FileAttributeType
|
|
2241
|
+
if type == .typeDirectory, assets.contains(where: { $0.hasPrefix(relativePath + "/") }) {
|
|
2242
|
+
try pruneStagingDirectory(path, assets: assets, prefix: relativePath + "/")
|
|
2243
|
+
} else if type != .typeRegular || !assets.contains(relativePath) {
|
|
2244
|
+
try fileSystem.removeItem(atPath: path)
|
|
2245
|
+
}
|
|
2570
2246
|
}
|
|
2247
|
+
}
|
|
2571
2248
|
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
NSLog("[BundleStorage] File size received: \(fileSize) bytes")
|
|
2587
|
-
|
|
2588
|
-
// Check available disk space
|
|
2589
|
-
do {
|
|
2590
|
-
let attributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory())
|
|
2591
|
-
if let freeSize = attributes[.systemFreeSize] as? Int64 {
|
|
2592
|
-
let requiredSpace = fileSize * 2 // ZIP + extracted files
|
|
2249
|
+
private func installArchive(
|
|
2250
|
+
archive: ParsedArchive,
|
|
2251
|
+
archiveUrl: URL,
|
|
2252
|
+
targetManifest: ParsedBundleManifest,
|
|
2253
|
+
preparedDirectory: String,
|
|
2254
|
+
tempDirectory: String,
|
|
2255
|
+
progressHandler: @escaping (UpdateProgressPayload) -> Void
|
|
2256
|
+
) throws -> [UpdateProgressPayload.DiffProgressFileSnapshot] {
|
|
2257
|
+
let archiveScratch = (tempDirectory as NSString).appendingPathComponent("archive")
|
|
2258
|
+
try? fileSystem.removeItem(atPath: archiveScratch)
|
|
2259
|
+
guard fileSystem.createDirectory(atPath: archiveScratch) else {
|
|
2260
|
+
throw BundleStorageError.directoryCreationFailed
|
|
2261
|
+
}
|
|
2262
|
+
defer { try? fileSystem.removeItem(atPath: archiveScratch) }
|
|
2593
2263
|
|
|
2594
|
-
|
|
2264
|
+
let compressedPath = (archiveScratch as NSString).appendingPathComponent("bundle.tar.br")
|
|
2265
|
+
let tarPath = (archiveScratch as NSString).appendingPathComponent("bundle.tar")
|
|
2266
|
+
let extractedDirectory = (archiveScratch as NSString).appendingPathComponent("extracted")
|
|
2267
|
+
var progressFiles = [UpdateProgressPayload.DiffProgressFileSnapshot(
|
|
2268
|
+
path: "bundle.tar.br",
|
|
2269
|
+
downloadPath: "bundle.tar.br",
|
|
2270
|
+
status: "pending",
|
|
2271
|
+
progress: 0,
|
|
2272
|
+
order: 0
|
|
2273
|
+
)]
|
|
2274
|
+
emitDiffProgress(
|
|
2275
|
+
progressHandler: progressHandler,
|
|
2276
|
+
phase: "downloading",
|
|
2277
|
+
files: progressFiles
|
|
2278
|
+
)
|
|
2595
2279
|
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2280
|
+
// One attempt: a failed archive falls back to per-file downloads, which retry.
|
|
2281
|
+
switch downloadFileSynchronously(
|
|
2282
|
+
from: archiveUrl,
|
|
2283
|
+
to: compressedPath,
|
|
2284
|
+
retrying: false,
|
|
2285
|
+
progressHandler: { progress in
|
|
2286
|
+
self.updateDiffProgressFile(
|
|
2287
|
+
files: &progressFiles,
|
|
2288
|
+
assetPath: "bundle.tar.br",
|
|
2289
|
+
status: "downloading",
|
|
2290
|
+
progress: progress.progress,
|
|
2291
|
+
downloadPath: "bundle.tar.br",
|
|
2292
|
+
downloadedBytes: progress.downloadedBytes,
|
|
2293
|
+
totalBytes: progress.totalBytes
|
|
2294
|
+
)
|
|
2295
|
+
self.emitDiffProgress(
|
|
2608
2296
|
progressHandler: progressHandler,
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
start: 0,
|
|
2612
|
-
end: UpdateProgress.downloadEnd
|
|
2613
|
-
),
|
|
2614
|
-
downloadedBytes: downloadProgress.downloadedBytes,
|
|
2615
|
-
totalBytes: downloadProgress.totalBytes
|
|
2297
|
+
phase: "downloading",
|
|
2298
|
+
files: progressFiles
|
|
2616
2299
|
)
|
|
2617
|
-
},
|
|
2618
|
-
completion: { [weak self] result in
|
|
2619
|
-
guard let self = self else {
|
|
2620
|
-
let error = NSError(domain: "HotUpdaterError", code: 998,
|
|
2621
|
-
userInfo: [NSLocalizedDescriptionKey: "Self deallocated during download"])
|
|
2622
|
-
completion(.failure(error))
|
|
2623
|
-
return
|
|
2624
2300
|
}
|
|
2301
|
+
) {
|
|
2302
|
+
case .success:
|
|
2303
|
+
break
|
|
2304
|
+
case .failure(let error):
|
|
2305
|
+
throw BundleStorageError.downloadFailed(error)
|
|
2306
|
+
}
|
|
2625
2307
|
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2308
|
+
let compressedAttributes = try fileSystem.attributesOfItem(atPath: compressedPath)
|
|
2309
|
+
guard let compressedSize = compressedAttributes[.size] as? NSNumber,
|
|
2310
|
+
compressedSize.int64Value == archive.downloadByteSize,
|
|
2311
|
+
HashUtils.verifyHash(
|
|
2312
|
+
fileURL: URL(fileURLWithPath: compressedPath),
|
|
2313
|
+
expectedHash: archive.downloadFileHash
|
|
2314
|
+
) else {
|
|
2315
|
+
throw UpdateFailureError(
|
|
2316
|
+
underlying: BundleStorageError.invalidBundle,
|
|
2317
|
+
failure: .hashMismatch
|
|
2318
|
+
)
|
|
2319
|
+
}
|
|
2633
2320
|
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
NSLog("[BundleStorage] Download failed: \(error.localizedDescription)")
|
|
2648
|
-
self.cleanupTemporaryFiles([tempDirectory]) // Sync cleanup
|
|
2649
|
-
|
|
2650
|
-
// Map DownloadError.incompleteDownload to BundleStorageError.incompleteDownload
|
|
2651
|
-
if let downloadError = error as? DownloadError,
|
|
2652
|
-
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
2653
|
-
completion(.failure(BundleStorageError.incompleteDownload(expected: expected, actual: actual)))
|
|
2654
|
-
} else {
|
|
2655
|
-
completion(.failure(BundleStorageError.downloadFailed(error)))
|
|
2656
|
-
}
|
|
2321
|
+
try UpdateFailureError.during(.extraction) {
|
|
2322
|
+
try BrotliFileDecompressor.decompress(
|
|
2323
|
+
from: compressedPath,
|
|
2324
|
+
to: tarPath,
|
|
2325
|
+
expectedOutputByteSize: archive.tarByteSize
|
|
2326
|
+
)
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
let expectedFiles = try targetManifest.assets.reduce(
|
|
2330
|
+
into: [String: Int64](),
|
|
2331
|
+
{ result, entry in
|
|
2332
|
+
guard let byteSize = entry.value.byteSize else {
|
|
2333
|
+
throw BundleStorageError.invalidBundle
|
|
2657
2334
|
}
|
|
2335
|
+
result[entry.key] = byteSize
|
|
2336
|
+
}
|
|
2337
|
+
)
|
|
2338
|
+
try UpdateFailureError.during(.extraction) {
|
|
2339
|
+
try TarArchiveExtractor.extract(
|
|
2340
|
+
from: tarPath,
|
|
2341
|
+
to: extractedDirectory,
|
|
2342
|
+
expectedFiles: expectedFiles
|
|
2343
|
+
)
|
|
2344
|
+
for (path, asset) in targetManifest.assets {
|
|
2345
|
+
let extractedPath = try FileUtilities.fileURL(
|
|
2346
|
+
for: path,
|
|
2347
|
+
destinationRoot: extractedDirectory
|
|
2348
|
+
).path
|
|
2349
|
+
try verifyManifestAssetFile(atPath: extractedPath, asset: asset)
|
|
2658
2350
|
}
|
|
2659
|
-
self.fileOperationQueue.async(execute: workItem)
|
|
2660
2351
|
}
|
|
2352
|
+
|
|
2353
|
+
try promoteVerifiedArchiveDirectory(
|
|
2354
|
+
preparedDirectory,
|
|
2355
|
+
withArchiveDirectory: extractedDirectory
|
|
2661
2356
|
)
|
|
2357
|
+
|
|
2358
|
+
updateDiffProgressFile(
|
|
2359
|
+
files: &progressFiles,
|
|
2360
|
+
assetPath: "bundle.tar.br",
|
|
2361
|
+
status: "downloaded",
|
|
2362
|
+
progress: 1,
|
|
2363
|
+
downloadedBytes: archive.downloadByteSize,
|
|
2364
|
+
totalBytes: archive.downloadByteSize
|
|
2365
|
+
)
|
|
2366
|
+
emitDiffProgress(
|
|
2367
|
+
progressHandler: progressHandler,
|
|
2368
|
+
phase: "finalizing",
|
|
2369
|
+
files: progressFiles
|
|
2370
|
+
)
|
|
2371
|
+
return progressFiles
|
|
2662
2372
|
}
|
|
2663
|
-
|
|
2664
|
-
/**
|
|
2665
|
-
* Logs detailed diagnostic information about a file system path.
|
|
2666
|
-
* @param path The path to diagnose
|
|
2667
|
-
* @param context Additional context for logging
|
|
2668
|
-
*/
|
|
2669
|
-
private func logFileSystemDiagnostics(path: String, context: String) {
|
|
2670
|
-
let fileManager = FileManager.default
|
|
2671
2373
|
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2374
|
+
private func promoteVerifiedArchiveDirectory(
|
|
2375
|
+
_ preparedDirectory: String,
|
|
2376
|
+
withArchiveDirectory archiveDirectory: String
|
|
2377
|
+
) throws {
|
|
2378
|
+
let backupDirectory = preparedDirectory + ".archive-backup"
|
|
2379
|
+
try? fileSystem.removeItem(atPath: backupDirectory)
|
|
2380
|
+
try fileSystem.moveItem(atPath: preparedDirectory, toPath: backupDirectory)
|
|
2381
|
+
|
|
2382
|
+
do {
|
|
2383
|
+
try fileSystem.moveItem(
|
|
2384
|
+
atPath: archiveDirectory,
|
|
2385
|
+
toPath: preparedDirectory
|
|
2386
|
+
)
|
|
2387
|
+
} catch {
|
|
2388
|
+
try? fileSystem.removeItem(atPath: preparedDirectory)
|
|
2389
|
+
do {
|
|
2390
|
+
try fileSystem.moveItem(atPath: backupDirectory, toPath: preparedDirectory)
|
|
2391
|
+
} catch let restoreError {
|
|
2392
|
+
throw BundleStorageError.moveOperationFailed(restoreError)
|
|
2393
|
+
}
|
|
2394
|
+
throw error
|
|
2395
|
+
}
|
|
2396
|
+
try? fileSystem.removeItem(atPath: backupDirectory)
|
|
2397
|
+
}
|
|
2675
2398
|
|
|
2676
|
-
|
|
2399
|
+
private func recoverInterruptedPromotions() {
|
|
2400
|
+
guard case .success(let storeDir) = bundleStoreDir(),
|
|
2401
|
+
let entries = try? fileSystem.contentsOfDirectory(atPath: storeDir) else { return }
|
|
2402
|
+
for entry in entries where entry.hasSuffix(".install-backup") {
|
|
2403
|
+
let finalDirectory = (storeDir as NSString).appendingPathComponent(String(entry.dropLast(".install-backup".count)))
|
|
2677
2404
|
do {
|
|
2678
|
-
|
|
2679
|
-
let size = attributes[.size] as? Int64 ?? 0
|
|
2680
|
-
let permissions = attributes[.posixPermissions] as? Int ?? 0
|
|
2681
|
-
NSLog("[BundleStorage] [\(context)] Size: \(size) bytes, Permissions: \(String(permissions, radix: 8))")
|
|
2405
|
+
try recoverInterruptedPromotion(finalDirectory: finalDirectory)
|
|
2682
2406
|
} catch {
|
|
2683
|
-
NSLog("[BundleStorage]
|
|
2407
|
+
NSLog("[BundleStorage] Cannot recover interrupted promotion: \(error)")
|
|
2684
2408
|
}
|
|
2685
2409
|
}
|
|
2410
|
+
}
|
|
2686
2411
|
|
|
2687
|
-
|
|
2688
|
-
let
|
|
2689
|
-
|
|
2690
|
-
|
|
2412
|
+
private func recoverInterruptedPromotion(finalDirectory: String) throws {
|
|
2413
|
+
let backup = finalDirectory + ".install-backup"
|
|
2414
|
+
guard fileSystem.fileExists(atPath: backup) else { return }
|
|
2415
|
+
if fileSystem.fileExists(atPath: finalDirectory) {
|
|
2416
|
+
if isCompleteBundleDirectory(finalDirectory) {
|
|
2417
|
+
try fileSystem.removeItem(atPath: backup)
|
|
2418
|
+
return
|
|
2419
|
+
}
|
|
2420
|
+
try fileSystem.removeItem(atPath: finalDirectory)
|
|
2421
|
+
}
|
|
2422
|
+
try fileSystem.moveItem(atPath: backup, toPath: finalDirectory)
|
|
2691
2423
|
}
|
|
2692
2424
|
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
tempBundleFile: String,
|
|
2708
|
-
fileHash: String?,
|
|
2709
|
-
storeDir: String,
|
|
2425
|
+
private func isCompleteBundleDirectory(_ directory: String) -> Bool {
|
|
2426
|
+
let bundleId = (directory as NSString).lastPathComponent
|
|
2427
|
+
guard let manifest = readManifest(in: directory),
|
|
2428
|
+
let parsed = parseBundleManifest(from: manifest),
|
|
2429
|
+
parsed.bundleId == bundleId,
|
|
2430
|
+
case .success(let bundlePath) = findBundleFile(in: directory, expectedBundleId: bundleId),
|
|
2431
|
+
bundlePath != nil else { return false }
|
|
2432
|
+
return parsed.assets.allSatisfy { path, asset in
|
|
2433
|
+
guard let file = try? FileUtilities.fileURL(for: path, destinationRoot: directory) else { return false }
|
|
2434
|
+
return (try? verifyManifestAssetFile(atPath: file.path, asset: asset)) != nil
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
private func promoteStagedBundle(
|
|
2710
2439
|
bundleId: String,
|
|
2711
|
-
|
|
2440
|
+
stagingDirectory: String,
|
|
2441
|
+
finalDirectory: String,
|
|
2442
|
+
bundlePathInStaging: String
|
|
2443
|
+
) throws -> BundleMetadata {
|
|
2444
|
+
releaseStateLock.lock()
|
|
2445
|
+
defer { releaseStateLock.unlock() }
|
|
2446
|
+
let currentMetadata = loadMetadataOrNull() ?? createInitialMetadata()
|
|
2447
|
+
guard let updatedMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId) else {
|
|
2448
|
+
throw BundleStorageError.unknown(StaleReleaseSelectionError())
|
|
2449
|
+
}
|
|
2450
|
+
let finalBundlePath = try resolveBundlePathAfterMove(
|
|
2451
|
+
bundlePathInStaging, from: stagingDirectory, to: finalDirectory
|
|
2452
|
+
)
|
|
2453
|
+
let previousPreference = try preferences.getItem(forKey: "HotUpdaterBundleURL")
|
|
2454
|
+
let backup = finalDirectory + ".install-backup"
|
|
2455
|
+
try recoverInterruptedPromotion(finalDirectory: finalDirectory)
|
|
2456
|
+
if fileSystem.fileExists(atPath: finalDirectory) {
|
|
2457
|
+
try fileSystem.moveItem(atPath: finalDirectory, toPath: backup)
|
|
2458
|
+
}
|
|
2459
|
+
var promoted = false
|
|
2460
|
+
do {
|
|
2461
|
+
try fileSystem.moveItem(atPath: stagingDirectory, toPath: finalDirectory)
|
|
2462
|
+
promoted = true
|
|
2463
|
+
try setBundleURL(localPath: finalBundlePath).get()
|
|
2464
|
+
// Metadata is the durable activation point. Launch never uses an
|
|
2465
|
+
// untracked preference if this write fails or the process terminates.
|
|
2466
|
+
guard saveMetadata(updatedMetadata) else {
|
|
2467
|
+
throw UpdateFailureError(
|
|
2468
|
+
underlying: BundleStorageError.unknown(NSError(domain: "HotUpdater", code: 0,
|
|
2469
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to persist bundle activation metadata"])),
|
|
2470
|
+
failure: .storage
|
|
2471
|
+
)
|
|
2472
|
+
}
|
|
2473
|
+
} catch {
|
|
2474
|
+
let installError = error
|
|
2475
|
+
let preferenceResult = setBundleURL(localPath: previousPreference)
|
|
2476
|
+
if promoted { try fileSystem.removeItem(atPath: finalDirectory) }
|
|
2477
|
+
if fileSystem.fileExists(atPath: backup) {
|
|
2478
|
+
try fileSystem.moveItem(atPath: backup, toPath: finalDirectory)
|
|
2479
|
+
}
|
|
2480
|
+
try preferenceResult.get()
|
|
2481
|
+
throw installError
|
|
2482
|
+
}
|
|
2483
|
+
try? fileSystem.removeItem(atPath: backup)
|
|
2484
|
+
return updatedMetadata
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
private func updateBundleFromManifest(
|
|
2488
|
+
bundleId: String,
|
|
2489
|
+
manifestUrl: URL,
|
|
2490
|
+
manifestFileHash: String,
|
|
2491
|
+
archiveUrl: URL?,
|
|
2492
|
+
changedAssets: [String: ChangedAssetDescriptor],
|
|
2493
|
+
storeDir: String,
|
|
2712
2494
|
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
2713
|
-
completion: @escaping (Result<
|
|
2495
|
+
completion: @escaping (Result<UpdateDelivery, Error>) -> Void
|
|
2714
2496
|
) {
|
|
2715
|
-
let
|
|
2716
|
-
|
|
2497
|
+
let tempDirResult = tempDir()
|
|
2498
|
+
guard case .success(let tempDirectory) = tempDirResult else {
|
|
2499
|
+
completion(.failure(UpdateFailureError.classifying(
|
|
2500
|
+
tempDirResult.failureError ?? BundleStorageError.unknown(nil),
|
|
2501
|
+
during: .storage
|
|
2502
|
+
)))
|
|
2503
|
+
return
|
|
2504
|
+
}
|
|
2717
2505
|
|
|
2718
|
-
|
|
2719
|
-
guard self.fileSystem.
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
code: 1,
|
|
2725
|
-
userInfo: [NSLocalizedDescriptionKey: "Downloaded file does not exist atPath: \(location.path)"]
|
|
2726
|
-
))))
|
|
2506
|
+
try? self.fileSystem.removeItem(atPath: tempDirectory)
|
|
2507
|
+
guard self.fileSystem.createDirectory(atPath: tempDirectory) else {
|
|
2508
|
+
completion(.failure(UpdateFailureError.classifying(
|
|
2509
|
+
BundleStorageError.directoryCreationFailed,
|
|
2510
|
+
during: .storage
|
|
2511
|
+
)))
|
|
2727
2512
|
return
|
|
2728
2513
|
}
|
|
2729
2514
|
|
|
2730
|
-
|
|
2515
|
+
let currentBundleId = self.getCachedBundleId()
|
|
2516
|
+
let currentBundleDir = currentBundleId.map {
|
|
2517
|
+
(storeDir as NSString).appendingPathComponent($0)
|
|
2518
|
+
}
|
|
2519
|
+
let tempManifestPath = (tempDirectory as NSString).appendingPathComponent("manifest.json")
|
|
2731
2520
|
let tmpDir = (storeDir as NSString).appendingPathComponent("\(bundleId).tmp")
|
|
2732
2521
|
let realDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
2522
|
+
var diffFiles: [UpdateProgressPayload.DiffProgressFileSnapshot] = []
|
|
2523
|
+
// The running step classifies the errors whose type does not.
|
|
2524
|
+
var failurePhase = UpdatePhase.storage
|
|
2525
|
+
var failureResource: UpdateFailureResource?
|
|
2733
2526
|
|
|
2734
2527
|
do {
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
|
-
// 4) Create tmpDir
|
|
2742
|
-
guard self.fileSystem.createDirectory(atPath: tmpDir) else {
|
|
2743
|
-
throw BundleStorageError.directoryCreationFailed
|
|
2744
|
-
}
|
|
2745
|
-
NSLog("[BundleStorage] Created tmpDir: \(tmpDir)")
|
|
2746
|
-
logFileSystemDiagnostics(path: tmpDir, context: "TmpDir Created")
|
|
2747
|
-
|
|
2748
|
-
// 5) Verify bundle integrity (hash or signature based on fileHash format)
|
|
2749
|
-
NSLog("[BundleStorage] Verifying bundle integrity...")
|
|
2750
|
-
self.emitArchiveProgress(
|
|
2528
|
+
try recoverInterruptedPromotion(finalDirectory: realDir)
|
|
2529
|
+
failurePhase = .transfer
|
|
2530
|
+
failureResource = .manifest
|
|
2531
|
+
self.emitDiffProgress(
|
|
2751
2532
|
progressHandler: progressHandler,
|
|
2752
|
-
|
|
2533
|
+
phase: "manifest",
|
|
2534
|
+
files: diffFiles,
|
|
2535
|
+
manifestProgress: 0
|
|
2753
2536
|
)
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2537
|
+
switch self.downloadFileSynchronously(
|
|
2538
|
+
from: manifestUrl,
|
|
2539
|
+
to: tempManifestPath,
|
|
2540
|
+
progressHandler: { progress in
|
|
2541
|
+
self.emitDiffProgress(
|
|
2542
|
+
progressHandler: progressHandler,
|
|
2543
|
+
phase: "manifest",
|
|
2544
|
+
files: diffFiles,
|
|
2545
|
+
manifestProgress: progress.progress
|
|
2546
|
+
)
|
|
2547
|
+
}
|
|
2548
|
+
) {
|
|
2757
2549
|
case .success:
|
|
2758
|
-
|
|
2759
|
-
self.emitArchiveProgress(
|
|
2760
|
-
progressHandler: progressHandler,
|
|
2761
|
-
progress: UpdateProgress.verificationEnd
|
|
2762
|
-
)
|
|
2550
|
+
break
|
|
2763
2551
|
case .failure(let error):
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2552
|
+
if let downloadError = error as? DownloadError,
|
|
2553
|
+
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
2554
|
+
throw BundleStorageError.incompleteDownload(expected: expected, actual: actual)
|
|
2555
|
+
}
|
|
2556
|
+
throw BundleStorageError.downloadFailed(error)
|
|
2769
2557
|
}
|
|
2770
2558
|
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
end: UpdateProgress.extractionEnd
|
|
2782
|
-
)
|
|
2783
|
-
)
|
|
2784
|
-
})
|
|
2785
|
-
NSLog("[BundleStorage] Extraction complete at \(tmpDir)")
|
|
2786
|
-
logFileSystemDiagnostics(path: tmpDir, context: "After Extraction")
|
|
2787
|
-
} catch {
|
|
2788
|
-
let nsError = error as NSError
|
|
2789
|
-
NSLog("[BundleStorage] Extraction failed - Domain: \(nsError.domain), Code: \(nsError.code), Description: \(nsError.localizedDescription)")
|
|
2790
|
-
logFileSystemDiagnostics(path: tmpDir, context: "Extraction Failed")
|
|
2791
|
-
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
2792
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2793
|
-
completion(.failure(BundleStorageError.extractionFormatError(error)))
|
|
2794
|
-
return
|
|
2559
|
+
failurePhase = .verification
|
|
2560
|
+
let manifestVerificationResult = SignatureVerifier.verifyBundle(
|
|
2561
|
+
fileURL: URL(fileURLWithPath: tempManifestPath),
|
|
2562
|
+
fileHash: manifestFileHash
|
|
2563
|
+
)
|
|
2564
|
+
guard case .success = manifestVerificationResult else {
|
|
2565
|
+
if case .failure(let error) = manifestVerificationResult {
|
|
2566
|
+
throw BundleStorageError.signatureVerificationFailed(error)
|
|
2567
|
+
}
|
|
2568
|
+
throw BundleStorageError.unknown(nil)
|
|
2795
2569
|
}
|
|
2796
2570
|
|
|
2797
|
-
|
|
2798
|
-
try?
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2571
|
+
failurePhase = .manifest
|
|
2572
|
+
guard let manifestData = try? Data(contentsOf: URL(fileURLWithPath: tempManifestPath)),
|
|
2573
|
+
let manifestJson = try JSONSerialization.jsonObject(with: manifestData) as? [String: Any],
|
|
2574
|
+
let targetManifest = parseBundleManifest(from: manifestJson),
|
|
2575
|
+
targetManifest.bundleId == bundleId
|
|
2576
|
+
else {
|
|
2577
|
+
throw BundleStorageError.invalidBundle
|
|
2578
|
+
}
|
|
2579
|
+
// Validate the entire wire contract even when every byte can be reused.
|
|
2580
|
+
guard Set(targetManifest.assets.keys) == Set(changedAssets.keys) else {
|
|
2581
|
+
throw BundleStorageError.invalidBundle
|
|
2582
|
+
}
|
|
2583
|
+
for (path, asset) in targetManifest.assets {
|
|
2584
|
+
guard FileUtilities.normalizedRelativePath(from: path) == path,
|
|
2585
|
+
path != "manifest.json",
|
|
2586
|
+
let descriptor = changedAssets[path],
|
|
2587
|
+
descriptor.fileUrl != nil,
|
|
2588
|
+
descriptor.fileHash.caseInsensitiveCompare(asset.fileHash) == .orderedSame else {
|
|
2589
|
+
throw BundleStorageError.invalidBundle
|
|
2590
|
+
}
|
|
2591
|
+
_ = try FileUtilities.fileURL(for: path, destinationRoot: tmpDir)
|
|
2592
|
+
}
|
|
2816
2593
|
|
|
2817
|
-
|
|
2594
|
+
// Downloads, patches, and extraction below classify their own errors.
|
|
2595
|
+
failurePhase = .storage
|
|
2596
|
+
failureResource = nil
|
|
2597
|
+
guard self.fileSystem.createDirectory(atPath: tmpDir) else {
|
|
2598
|
+
throw BundleStorageError.directoryCreationFailed
|
|
2599
|
+
}
|
|
2600
|
+
try pruneStagingDirectory(tmpDir, assets: Set(targetManifest.assets.keys))
|
|
2601
|
+
let sortedAssets = targetManifest.assets.sorted { $0.key < $1.key }
|
|
2602
|
+
let localDirectories = [currentBundleDir, realDir].compactMap { $0 }.filter { fileSystem.fileExists(atPath: $0) }
|
|
2603
|
+
var downloads: [String: ChangedAssetDescriptor] = [:]
|
|
2604
|
+
for (index, asset) in sortedAssets.enumerated() {
|
|
2605
|
+
// Local preparation has its own progress interval, with no download files.
|
|
2606
|
+
progressHandler(UpdateProgressPayload(
|
|
2607
|
+
progress: 0.15 + 0.05 * Double(index) / Double(max(1, sortedAssets.count)),
|
|
2608
|
+
artifactType: "diff",
|
|
2609
|
+
details: .init(totalFilesCount: 0, completedFilesCount: 0)
|
|
2610
|
+
))
|
|
2611
|
+
let assetPath = asset.key
|
|
2612
|
+
let expectedAsset = asset.value
|
|
2613
|
+
let destinationPath = try FileUtilities.fileURL(for: assetPath, destinationRoot: tmpDir).path
|
|
2614
|
+
guard self.fileSystem.createDirectory(atPath: (destinationPath as NSString).deletingLastPathComponent) else {
|
|
2615
|
+
throw BundleStorageError.directoryCreationFailed
|
|
2616
|
+
}
|
|
2617
|
+
if (try? fileSystem.attributesOfItem(atPath: destinationPath)[.type] as? FileAttributeType) == .typeRegular,
|
|
2618
|
+
(try? verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)) != nil {
|
|
2619
|
+
continue
|
|
2620
|
+
}
|
|
2621
|
+
if fileSystem.fileExists(atPath: destinationPath) { try self.fileSystem.removeItem(atPath: destinationPath) }
|
|
2622
|
+
|
|
2623
|
+
var reused = false
|
|
2624
|
+
// The cached target is only a candidate, just like the active OTA.
|
|
2625
|
+
for directory in localDirectories {
|
|
2626
|
+
guard let sourcePath = try? FileUtilities.fileURL(for: assetPath, destinationRoot: directory).path,
|
|
2627
|
+
(try? fileSystem.attributesOfItem(atPath: sourcePath)[.type] as? FileAttributeType) == .typeRegular,
|
|
2628
|
+
(try? verifyManifestAssetFile(atPath: sourcePath, asset: expectedAsset)) != nil else { continue }
|
|
2818
2629
|
do {
|
|
2819
|
-
try self.fileSystem.
|
|
2820
|
-
|
|
2821
|
-
|
|
2630
|
+
try self.fileSystem.copyItem(atPath: sourcePath, toPath: destinationPath)
|
|
2631
|
+
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
2632
|
+
reused = true
|
|
2633
|
+
break
|
|
2822
2634
|
} catch {
|
|
2823
|
-
|
|
2824
|
-
NSLog("[BundleStorage] Move operation failed - Domain: \(nsError.domain), Code: \(nsError.code), Description: \(nsError.localizedDescription)")
|
|
2825
|
-
logFileSystemDiagnostics(path: tmpDir, context: "Move Failed - Source")
|
|
2826
|
-
logFileSystemDiagnostics(path: realDir, context: "Move Failed - Destination")
|
|
2827
|
-
throw BundleStorageError.moveOperationFailed(error)
|
|
2635
|
+
try? self.fileSystem.removeItem(atPath: destinationPath)
|
|
2828
2636
|
}
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2637
|
+
}
|
|
2638
|
+
if reused { continue }
|
|
2639
|
+
if builtInAssetResolver.copyIfMatches(assetPath: assetPath, expectedHash: expectedAsset.fileHash, destination: destinationPath) {
|
|
2640
|
+
try UpdateFailureError.during(.verification, resource: .file) {
|
|
2641
|
+
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
2642
|
+
}
|
|
2643
|
+
continue
|
|
2644
|
+
}
|
|
2645
|
+
downloads[assetPath] = changedAssets[assetPath]
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
var archiveInstalled = false
|
|
2649
|
+
var patchedFiles = false
|
|
2650
|
+
// The archive is chosen before any patch is tried, so only the
|
|
2651
|
+
// per-file install can fall back from a patch.
|
|
2652
|
+
var patchFallbacks = false
|
|
2653
|
+
if shouldUseArchive(
|
|
2654
|
+
archive: targetManifest.archive,
|
|
2655
|
+
archiveUrl: archiveUrl,
|
|
2656
|
+
downloads: downloads,
|
|
2657
|
+
assets: targetManifest.assets
|
|
2658
|
+
), let archive = targetManifest.archive, let archiveUrl {
|
|
2659
|
+
do {
|
|
2660
|
+
diffFiles = try UpdateFailureError.during(.storage, resource: .archive) {
|
|
2661
|
+
try installArchive(
|
|
2662
|
+
archive: archive,
|
|
2663
|
+
archiveUrl: archiveUrl,
|
|
2664
|
+
targetManifest: targetManifest,
|
|
2665
|
+
preparedDirectory: tmpDir,
|
|
2666
|
+
tempDirectory: tempDirectory,
|
|
2667
|
+
progressHandler: progressHandler
|
|
2668
|
+
)
|
|
2669
|
+
}
|
|
2670
|
+
archiveInstalled = true
|
|
2671
|
+
} catch {
|
|
2672
|
+
NSLog("[BundleStorage] tar.br optimization failed; falling back to individual files: \(error.localizedDescription)")
|
|
2673
|
+
guard fileSystem.fileExists(atPath: tmpDir) else {
|
|
2674
|
+
throw error
|
|
2847
2675
|
}
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
case .success:
|
|
2851
|
-
NSLog("[BundleStorage] Successfully set bundle URL: \(finalBundlePath)")
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2852
2678
|
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2679
|
+
if !archiveInstalled {
|
|
2680
|
+
diffFiles = createDiffProgressFiles(changedAssets: downloads)
|
|
2681
|
+
emitDiffProgress(progressHandler: progressHandler, phase: "downloading", files: diffFiles)
|
|
2682
|
+
let progressLock = NSLock()
|
|
2683
|
+
let workers = OperationQueue()
|
|
2684
|
+
workers.maxConcurrentOperationCount = 4
|
|
2685
|
+
var installError: Error?
|
|
2686
|
+
var installOutcomes: [AssetInstallOutcome] = []
|
|
2687
|
+
for progressFile in diffFiles {
|
|
2688
|
+
let assetPath = progressFile.path
|
|
2689
|
+
let expectedAsset = targetManifest.assets[assetPath]!
|
|
2690
|
+
let descriptor = downloads[assetPath]!
|
|
2691
|
+
let destinationPath = try FileUtilities.fileURL(for: assetPath, destinationRoot: tmpDir).path
|
|
2692
|
+
// Separate temp namespaces also avoid collisions between flattened asset paths.
|
|
2693
|
+
let workerTemp = (tempDirectory as NSString).appendingPathComponent(String(progressFile.order))
|
|
2694
|
+
workers.addOperation {
|
|
2695
|
+
progressLock.lock()
|
|
2696
|
+
let shouldSkip = installError != nil
|
|
2697
|
+
progressLock.unlock()
|
|
2698
|
+
if shouldSkip { return }
|
|
2699
|
+
do {
|
|
2700
|
+
guard self.fileSystem.createDirectory(atPath: workerTemp) else { throw BundleStorageError.directoryCreationFailed }
|
|
2701
|
+
let outcome = try self.downloadManifestAsset(
|
|
2702
|
+
assetPath: assetPath, expectedAsset: expectedAsset, changedAsset: descriptor,
|
|
2703
|
+
currentBundleId: currentBundleId, currentBundleDir: currentBundleDir,
|
|
2704
|
+
destinationPath: destinationPath, tempDirectory: workerTemp, progressFile: progressFile,
|
|
2705
|
+
progressHandler: { payload in
|
|
2706
|
+
progressLock.lock()
|
|
2707
|
+
defer { progressLock.unlock() }
|
|
2708
|
+
if let snapshot = payload.details?.files.first,
|
|
2709
|
+
let index = diffFiles.firstIndex(where: { $0.path == assetPath }) {
|
|
2710
|
+
diffFiles[index] = snapshot
|
|
2711
|
+
}
|
|
2712
|
+
self.emitDiffProgress(progressHandler: progressHandler, phase: "downloading", files: diffFiles)
|
|
2713
|
+
}
|
|
2714
|
+
)
|
|
2715
|
+
progressLock.lock()
|
|
2716
|
+
installOutcomes.append(outcome)
|
|
2717
|
+
progressLock.unlock()
|
|
2718
|
+
} catch {
|
|
2719
|
+
progressLock.lock()
|
|
2720
|
+
if installError == nil { installError = error }
|
|
2721
|
+
progressLock.unlock()
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
// Join all workers before cleaning scratch files, even after a failure.
|
|
2726
|
+
workers.waitUntilAllOperationsAreFinished()
|
|
2727
|
+
if let installError { throw installError }
|
|
2728
|
+
for outcome in installOutcomes {
|
|
2729
|
+
switch outcome {
|
|
2730
|
+
case .patched:
|
|
2731
|
+
patchedFiles = true
|
|
2732
|
+
case .downloaded(let patchFallback):
|
|
2733
|
+
patchFallbacks = patchFallbacks || patchFallback
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2856
2737
|
|
|
2857
|
-
|
|
2858
|
-
|
|
2738
|
+
self.emitDiffProgress(
|
|
2739
|
+
progressHandler: progressHandler,
|
|
2740
|
+
phase: "finalizing",
|
|
2741
|
+
files: diffFiles
|
|
2742
|
+
)
|
|
2859
2743
|
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
progress: UpdateProgress.activationReady
|
|
2863
|
-
)
|
|
2864
|
-
self.scheduleCleanupOldBundles(
|
|
2865
|
-
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
2866
|
-
)
|
|
2744
|
+
let manifestDestination = (tmpDir as NSString).appendingPathComponent("manifest.json")
|
|
2745
|
+
try copyVerifiedManifest(from: tempManifestPath, to: manifestDestination)
|
|
2867
2746
|
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
NSLog("[BundleStorage] Failed to set bundle URL - Domain: \(nsError.domain), Code: \(nsError.code), Description: \(nsError.localizedDescription)")
|
|
2877
|
-
// Preferences save failed → remove realDir and clean up
|
|
2878
|
-
try? self.fileSystem.removeItem(atPath: realDir)
|
|
2879
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2880
|
-
completion(.failure(err))
|
|
2881
|
-
}
|
|
2882
|
-
} else {
|
|
2883
|
-
// No valid .jsbundle found → delete tmpDir and fail
|
|
2884
|
-
NSLog("[BundleStorage] No valid bundle file found in tmpDir")
|
|
2885
|
-
logFileSystemDiagnostics(path: tmpDir, context: "Invalid Bundle")
|
|
2886
|
-
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
2887
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2888
|
-
completion(.failure(BundleStorageError.invalidBundle))
|
|
2747
|
+
switch self.findBundleFile(in: tmpDir, expectedBundleId: bundleId) {
|
|
2748
|
+
case .success(let maybeBundlePath):
|
|
2749
|
+
guard let bundlePathInTmp = maybeBundlePath else {
|
|
2750
|
+
// The manifest does not name exactly one platform bundle file.
|
|
2751
|
+
throw UpdateFailureError(
|
|
2752
|
+
underlying: BundleStorageError.invalidBundle,
|
|
2753
|
+
failure: UpdateFailure.invalidResponse.with(resource: .manifest)
|
|
2754
|
+
)
|
|
2889
2755
|
}
|
|
2890
|
-
|
|
2891
|
-
let
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2756
|
+
|
|
2757
|
+
let updatedMetadata = try promoteStagedBundle(
|
|
2758
|
+
bundleId: bundleId,
|
|
2759
|
+
stagingDirectory: tmpDir,
|
|
2760
|
+
finalDirectory: realDir,
|
|
2761
|
+
bundlePathInStaging: bundlePathInTmp
|
|
2762
|
+
)
|
|
2895
2763
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
2896
|
-
|
|
2764
|
+
self.scheduleCleanupOldBundles(
|
|
2765
|
+
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
2766
|
+
)
|
|
2767
|
+
progressHandler(UpdateProgressPayload(
|
|
2768
|
+
progress: 1.0,
|
|
2769
|
+
artifactType: "diff",
|
|
2770
|
+
details: .init(
|
|
2771
|
+
totalFilesCount: diffFiles.count,
|
|
2772
|
+
completedFilesCount: diffFiles.filter { $0.status == "downloaded" }.count,
|
|
2773
|
+
files: diffFiles
|
|
2774
|
+
)
|
|
2775
|
+
))
|
|
2776
|
+
let delivery: UpdateDelivery.Method =
|
|
2777
|
+
archiveInstalled ? .archive : (patchedFiles ? .patch : .manifest)
|
|
2778
|
+
completion(.success(UpdateDelivery(method: delivery, patchFallback: patchFallbacks)))
|
|
2779
|
+
case .failure(let error):
|
|
2780
|
+
throw error
|
|
2897
2781
|
}
|
|
2898
|
-
} catch
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
NSLog("[BundleStorage] Error during tmpDir processing - Domain: \(nsError.domain), Code: \(nsError.code), Description: \(nsError.localizedDescription)")
|
|
2902
|
-
logFileSystemDiagnostics(path: tmpDir, context: "Processing Error")
|
|
2903
|
-
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
2782
|
+
} catch {
|
|
2783
|
+
NSLog("[BundleStorage] Manifest-driven install failed: \(error.localizedDescription).")
|
|
2784
|
+
// Preserve staging for a hash-checked retry. Never trust its presence alone.
|
|
2904
2785
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
completion(.failure(BundleStorageError.unknown(error)))
|
|
2911
|
-
}
|
|
2786
|
+
completion(.failure(UpdateFailureError.classifying(
|
|
2787
|
+
error,
|
|
2788
|
+
during: failurePhase,
|
|
2789
|
+
resource: failureResource
|
|
2790
|
+
)))
|
|
2912
2791
|
}
|
|
2913
2792
|
}
|
|
2914
2793
|
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2794
|
+
func markLaunchStarted(bundleId: String?) {
|
|
2795
|
+
// Unlike prepareLaunch, this can run while JavaScript stages a newer bundle.
|
|
2796
|
+
releaseStateLock.lock()
|
|
2797
|
+
defer { releaseStateLock.unlock() }
|
|
2798
|
+
guard let bundleId,
|
|
2799
|
+
var metadata = loadMetadataOrNull(),
|
|
2800
|
+
metadata.verificationPending,
|
|
2801
|
+
!metadata.launchInProgress,
|
|
2802
|
+
metadata.stagingBundleId == bundleId else {
|
|
2803
|
+
return
|
|
2804
|
+
}
|
|
2805
|
+
metadata.launchInProgress = true
|
|
2806
|
+
_ = saveMetadata(metadata)
|
|
2918
2807
|
}
|
|
2919
2808
|
|
|
2920
|
-
// MARK: - Rollback Support
|
|
2921
|
-
|
|
2922
|
-
/**
|
|
2923
|
-
* Marks the current launch as successful after the first content appeared.
|
|
2924
|
-
*/
|
|
2925
2809
|
func markLaunchCompleted(bundleId: String?) {
|
|
2810
|
+
// Before the guard: content from the built-in bundle (nil) also counts.
|
|
2811
|
+
readyInterruptedLaunchRetry(bundleId)
|
|
2926
2812
|
guard let bundleId,
|
|
2927
2813
|
var metadata = loadMetadataOrNull(),
|
|
2928
2814
|
metadata.verificationPending,
|
|
@@ -2935,6 +2821,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2935
2821
|
metadata.stableBundleId = nil
|
|
2936
2822
|
metadata.stableSelection = nil
|
|
2937
2823
|
metadata.verificationPending = false
|
|
2824
|
+
metadata.launchInProgress = false
|
|
2938
2825
|
metadata.pendingTransition = nil
|
|
2939
2826
|
metadata.pendingSelectionTransition = nil
|
|
2940
2827
|
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
@@ -2953,9 +2840,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2953
2840
|
|
|
2954
2841
|
func notifyAppReady() -> [String: Any] {
|
|
2955
2842
|
guard let report = loadLaunchReport() else {
|
|
2956
|
-
if let
|
|
2843
|
+
if let launchedBundleId = pendingLaunchBundleId,
|
|
2844
|
+
let metadata = loadMetadataOrNull(),
|
|
2957
2845
|
metadata.verificationPending,
|
|
2958
|
-
metadata.stagingBundleId
|
|
2846
|
+
metadata.stagingBundleId == launchedBundleId {
|
|
2959
2847
|
return ["status": "PENDING"]
|
|
2960
2848
|
}
|
|
2961
2849
|
return ["status": LaunchReportStatus.unchanged.rawValue]
|
|
@@ -2981,11 +2869,20 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2981
2869
|
}
|
|
2982
2870
|
|
|
2983
2871
|
/**
|
|
2984
|
-
* Returns the
|
|
2872
|
+
* Returns the bundles this device refuses to install: crash history, plus a
|
|
2873
|
+
* bundle waiting for its retry until a process that may retry it.
|
|
2985
2874
|
* @return The crashed history object
|
|
2986
2875
|
*/
|
|
2987
2876
|
func getCrashHistory() -> CrashedHistory {
|
|
2988
|
-
|
|
2877
|
+
crashedHistoryLock.lock()
|
|
2878
|
+
defer { crashedHistoryLock.unlock() }
|
|
2879
|
+
var history = loadCrashedHistory()
|
|
2880
|
+
if let interrupted = history.interruptedLaunch,
|
|
2881
|
+
!interrupted.retryReady || interrupted.bundleId == retryHeldBundleId,
|
|
2882
|
+
!history.contains(interrupted.bundleId) {
|
|
2883
|
+
history.bundles.append(CrashedBundleEntry(bundleId: interrupted.bundleId))
|
|
2884
|
+
}
|
|
2885
|
+
return history
|
|
2989
2886
|
}
|
|
2990
2887
|
|
|
2991
2888
|
/**
|
|
@@ -2993,40 +2890,59 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2993
2890
|
* @return true if clearing was successful
|
|
2994
2891
|
*/
|
|
2995
2892
|
func clearCrashHistory() -> Bool {
|
|
2893
|
+
crashedHistoryLock.lock()
|
|
2894
|
+
defer { crashedHistoryLock.unlock() }
|
|
2996
2895
|
var history = loadCrashedHistory()
|
|
2997
2896
|
history.clear()
|
|
2897
|
+
retryHeldBundleId = nil
|
|
2998
2898
|
return saveCrashedHistory(history)
|
|
2999
2899
|
}
|
|
3000
2900
|
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
2901
|
+
/// A crash, or a second unfinished launch of the same bundle, adds the bundle
|
|
2902
|
+
/// to crash history. A first unfinished launch cannot tell a hang from a user
|
|
2903
|
+
/// leaving early, so it leaves the bundle one retry instead.
|
|
2904
|
+
private func recordFailedLaunch(_ bundleId: String, unfinishedLaunch: Bool) {
|
|
2905
|
+
crashedHistoryLock.lock()
|
|
2906
|
+
defer { crashedHistoryLock.unlock() }
|
|
2907
|
+
var history = loadCrashedHistory()
|
|
2908
|
+
let interrupted = history.interruptedLaunch
|
|
2909
|
+
if unfinishedLaunch, interrupted?.bundleId != bundleId {
|
|
2910
|
+
NSLog("[BundleStorage] Launch of \(bundleId) ended before first content; it gets one retry")
|
|
2911
|
+
history.interruptedLaunch = InterruptedLaunch(bundleId: bundleId)
|
|
2912
|
+
retryHeldBundleId = bundleId
|
|
2913
|
+
} else {
|
|
2914
|
+
history.addEntry(bundleId)
|
|
2915
|
+
if interrupted?.bundleId == bundleId {
|
|
2916
|
+
history.interruptedLaunch = nil
|
|
2917
|
+
}
|
|
3009
2918
|
}
|
|
3010
|
-
|
|
3011
|
-
}
|
|
3012
|
-
|
|
3013
|
-
func getUserId() -> String? {
|
|
3014
|
-
loadUserIdentity()?.userId
|
|
3015
|
-
}
|
|
3016
|
-
|
|
3017
|
-
func getUsername() -> String? {
|
|
3018
|
-
loadUserIdentity()?.username
|
|
2919
|
+
_ = saveCrashedHistory(history)
|
|
3019
2920
|
}
|
|
3020
2921
|
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
2922
|
+
/// First content appeared, so the app works again. A bundle waiting for its
|
|
2923
|
+
/// retry may be installed from the next process on. This process keeps
|
|
2924
|
+
/// refusing it, so the session that recovered never reloads into it. Content
|
|
2925
|
+
/// from that bundle verifies it.
|
|
2926
|
+
private func readyInterruptedLaunchRetry(_ currentBundleId: String?) {
|
|
2927
|
+
crashedHistoryLock.lock()
|
|
2928
|
+
defer { crashedHistoryLock.unlock() }
|
|
2929
|
+
var history = loadCrashedHistory()
|
|
2930
|
+
guard let interrupted = history.interruptedLaunch else {
|
|
2931
|
+
return
|
|
2932
|
+
}
|
|
2933
|
+
if interrupted.bundleId == currentBundleId {
|
|
2934
|
+
history.interruptedLaunch = nil
|
|
2935
|
+
} else if interrupted.retryReady {
|
|
3027
2936
|
return
|
|
2937
|
+
} else {
|
|
2938
|
+
history.interruptedLaunch?.retryReady = true
|
|
2939
|
+
retryHeldBundleId = interrupted.bundleId
|
|
3028
2940
|
}
|
|
3029
|
-
_ =
|
|
2941
|
+
_ = saveCrashedHistory(history)
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
func getInstallId() -> String {
|
|
2945
|
+
loadOrCreateInstallIdentity().installId
|
|
3030
2946
|
}
|
|
3031
2947
|
|
|
3032
2948
|
/**
|