@hot-updater/react-native 1.0.0-rc.3 → 1.0.0-rc.30
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/proguard-rules.pro +17 -2
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +954 -837
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +15 -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 +43 -31
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +37 -54
- 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/PreviousProcessExit.kt +89 -0
- 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/oldarch/HotUpdater.kt +7 -9
- package/android/src/oldarch/HotUpdaterModule.kt +98 -74
- package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
- package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +950 -1112
- package/ios/HotUpdater/Internal/BundleMetadata.swift +8 -53
- 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 +13 -45
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +190 -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 +111 -0
- package/lib/commonjs/appReady.js.map +1 -0
- package/lib/commonjs/checkForUpdate.js +124 -69
- 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 +54 -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 +78 -60
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/pluginHost.js +44 -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 +132 -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 +103 -0
- package/lib/module/appReady.js.map +1 -0
- package/lib/module/checkForUpdate.js +117 -64
- 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 +49 -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 +71 -53
- package/lib/module/native.js.map +1 -1
- package/lib/module/pluginHost.js +35 -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 +126 -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 +23 -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 +31 -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 +42 -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 +23 -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 +31 -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 +42 -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 +149 -0
- package/src/checkForUpdate.ts +151 -88
- package/src/clientPlugin.ts +18 -0
- package/src/error.ts +4 -14
- package/src/fetchJSON.ts +14 -30
- package/src/fetchUpdateResponse.ts +54 -0
- package/src/httpClient.ts +88 -159
- package/src/index.ts +90 -24
- package/src/native.ts +106 -102
- package/src/pluginHost.ts +45 -0
- package/src/releaseCatalogCache.ts +43 -172
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +41 -48
- package/src/store.ts +2 -23
- package/src/types.ts +1 -1
- package/src/updateError.ts +153 -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 -615
- 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 -614
- 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 -611
- 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
|
@@ -23,27 +23,118 @@ protocol DownloadService {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
enum DownloadError: Error {
|
|
26
|
+
enum DownloadError: Error, Equatable {
|
|
27
27
|
case incompleteDownload(expected: Int64, actual: Int64)
|
|
28
28
|
case invalidContentLength
|
|
29
|
+
/// The server answered with a status outside 200-299. `originCode` is the
|
|
30
|
+
/// `<Code>` of a storage origin's XML error body, when it has one.
|
|
31
|
+
case httpStatus(Int, originCode: String? = nil)
|
|
32
|
+
|
|
33
|
+
/// How much of an error body is searched for a storage origin's code.
|
|
34
|
+
static let maximumErrorBodyPrefixByteCount = 4 * 1024
|
|
35
|
+
private static let maximumOriginCodeLength = 64
|
|
36
|
+
|
|
37
|
+
/// The failure for an HTTP response outside 200-299, or nil for a
|
|
38
|
+
/// successful or non-HTTP response. `body` is the saved response body.
|
|
39
|
+
static func httpStatusError(for response: URLResponse?, body: URL?) -> DownloadError? {
|
|
40
|
+
guard let httpResponse = response as? HTTPURLResponse,
|
|
41
|
+
!(200..<300).contains(httpResponse.statusCode) else {
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
let originCode = body.flatMap { body -> String? in
|
|
45
|
+
guard let handle = try? FileHandle(forReadingFrom: body) else {
|
|
46
|
+
return nil
|
|
47
|
+
}
|
|
48
|
+
defer { try? handle.close() }
|
|
49
|
+
let prefix = try? FileUtilities.readUpToCount(
|
|
50
|
+
from: handle,
|
|
51
|
+
count: maximumErrorBodyPrefixByteCount
|
|
52
|
+
)
|
|
53
|
+
return prefix.flatMap { storageOriginErrorCode(in: $0) }
|
|
54
|
+
}
|
|
55
|
+
return .httpStatus(httpResponse.statusCode, originCode: originCode)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// The `<Code>` of an S3, R2, or GCS style `<Error>` document in the
|
|
59
|
+
/// first 4 KB of `body`, such as "AccessDenied". Nothing else from the
|
|
60
|
+
/// body is kept, and a code must be 1-64 characters of [A-Za-z0-9._-].
|
|
61
|
+
static func storageOriginErrorCode(in body: Data) -> String? {
|
|
62
|
+
let text = String(decoding: body.prefix(maximumErrorBodyPrefixByteCount), as: UTF8.self)
|
|
63
|
+
guard let errorElement = text.range(of: "<Error"),
|
|
64
|
+
let codeStart = text.range(of: "<Code>", range: errorElement.upperBound..<text.endIndex),
|
|
65
|
+
let codeEnd = text.range(of: "</Code>", range: codeStart.upperBound..<text.endIndex) else {
|
|
66
|
+
return nil
|
|
67
|
+
}
|
|
68
|
+
let code = text[codeStart.upperBound..<codeEnd.lowerBound]
|
|
69
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
70
|
+
guard (1...maximumOriginCodeLength).contains(code.unicodeScalars.count),
|
|
71
|
+
code.unicodeScalars.allSatisfy(isOriginCodeCharacter) else {
|
|
72
|
+
return nil
|
|
73
|
+
}
|
|
74
|
+
return code
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static func isOriginCodeCharacter(_ scalar: Unicode.Scalar) -> Bool {
|
|
78
|
+
switch scalar {
|
|
79
|
+
case "A"..."Z", "a"..."z", "0"..."9", ".", "_", "-":
|
|
80
|
+
return true
|
|
81
|
+
default:
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
29
85
|
}
|
|
30
86
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
let
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
87
|
+
extension DownloadError {
|
|
88
|
+
/// Attempts per download that retries, the first one included.
|
|
89
|
+
static let maximumAttempts = 3
|
|
90
|
+
|
|
91
|
+
/// Whether a failed download may succeed if tried again: a network
|
|
92
|
+
/// failure or timeout, a body that ended early, or a 408, 429, or 5xx
|
|
93
|
+
/// answer. Another 4xx, a TLS failure, a cancelled task, and a local file
|
|
94
|
+
/// failure would fail the same way again.
|
|
95
|
+
static func isRetryable(_ error: Error) -> Bool {
|
|
96
|
+
if let downloadError = error as? DownloadError {
|
|
97
|
+
switch downloadError {
|
|
98
|
+
case .httpStatus(let status, _):
|
|
99
|
+
return status == 408 || status == 429 || status >= 500
|
|
100
|
+
case .incompleteDownload:
|
|
101
|
+
return true
|
|
102
|
+
case .invalidContentLength:
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
let nsError = error as NSError
|
|
107
|
+
guard nsError.domain == NSURLErrorDomain else {
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
switch URLError.Code(rawValue: nsError.code) {
|
|
111
|
+
case .timedOut,
|
|
112
|
+
.cannotFindHost,
|
|
113
|
+
.cannotConnectToHost,
|
|
114
|
+
.networkConnectionLost,
|
|
115
|
+
.dnsLookupFailed,
|
|
116
|
+
.notConnectedToInternet,
|
|
117
|
+
.cannotLoadFromNetwork,
|
|
118
|
+
.callIsActive,
|
|
119
|
+
.dataNotAllowed,
|
|
120
|
+
.internationalRoamingOff,
|
|
121
|
+
.backgroundSessionWasDisconnected,
|
|
122
|
+
.badServerResponse:
|
|
123
|
+
return true
|
|
124
|
+
default:
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
37
128
|
}
|
|
38
129
|
|
|
39
130
|
class URLSessionDownloadService: NSObject, DownloadService {
|
|
131
|
+
private let stateLock = NSRecursiveLock()
|
|
40
132
|
private var session: URLSession!
|
|
41
133
|
private var backgroundSession: URLSession!
|
|
42
134
|
private var progressHandlers: [URLSessionTask: (DownloadProgress) -> Void] = [:]
|
|
43
135
|
private var completionHandlers: [URLSessionTask: (Result<URL, Error>) -> Void] = [:]
|
|
44
136
|
private var destinations: [URLSessionTask: String] = [:]
|
|
45
137
|
private var fileSizeHandlers: [URLSessionTask: (Int64) -> Void] = [:]
|
|
46
|
-
private var taskStates: [Int: TaskState] = [:]
|
|
47
138
|
|
|
48
139
|
override init() {
|
|
49
140
|
super.init()
|
|
@@ -61,51 +152,22 @@ class URLSessionDownloadService: NSObject, DownloadService {
|
|
|
61
152
|
backgroundConfig.sessionSendsLaunchEvents = true
|
|
62
153
|
}
|
|
63
154
|
backgroundSession = URLSession(configuration: backgroundConfig, delegate: self, delegateQueue: nil)
|
|
64
|
-
|
|
65
|
-
// Load persisted task states
|
|
66
|
-
taskStates = loadTaskStates()
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// MARK: - State Persistence
|
|
70
|
-
|
|
71
|
-
private var stateFileURL: URL {
|
|
72
|
-
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
73
|
-
return documentsPath.appendingPathComponent("download-state.json")
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private func saveTaskState(_ state: TaskState) {
|
|
77
|
-
taskStates[state.taskIdentifier] = state
|
|
78
|
-
|
|
79
|
-
if let data = try? JSONEncoder().encode(taskStates) {
|
|
80
|
-
try? data.write(to: stateFileURL)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private func loadTaskStates() -> [Int: TaskState] {
|
|
85
|
-
guard let data = try? Data(contentsOf: stateFileURL),
|
|
86
|
-
let states = try? JSONDecoder().decode([Int: TaskState].self, from: data) else {
|
|
87
|
-
return [:]
|
|
88
|
-
}
|
|
89
|
-
return states
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private func removeTaskState(_ taskIdentifier: Int) {
|
|
93
|
-
taskStates.removeValue(forKey: taskIdentifier)
|
|
94
|
-
|
|
95
|
-
if let data = try? JSONEncoder().encode(taskStates) {
|
|
96
|
-
try? data.write(to: stateFileURL)
|
|
97
|
-
}
|
|
98
155
|
}
|
|
99
156
|
|
|
100
157
|
func downloadFile(from url: URL, to destination: String, fileSizeHandler: ((Int64) -> Void)?, progressHandler: @escaping (DownloadProgress) -> Void, completion: @escaping (Result<URL, Error>) -> Void) -> URLSessionDownloadTask? {
|
|
158
|
+
// UIKit state is read on the main thread before taking the task-state lock.
|
|
101
159
|
// Determine if we should use background session
|
|
102
160
|
#if !os(macOS)
|
|
103
|
-
let appState =
|
|
161
|
+
let appState = Thread.isMainThread
|
|
162
|
+
? UIApplication.shared.applicationState
|
|
163
|
+
: DispatchQueue.main.sync { UIApplication.shared.applicationState }
|
|
104
164
|
let useBackgroundSession = (appState == .background || appState == .inactive)
|
|
105
165
|
#else
|
|
106
166
|
let useBackgroundSession = false
|
|
107
167
|
#endif
|
|
108
168
|
|
|
169
|
+
stateLock.lock()
|
|
170
|
+
defer { stateLock.unlock() }
|
|
109
171
|
let selectedSession = useBackgroundSession ? backgroundSession : session
|
|
110
172
|
let task = selectedSession?.downloadTask(with: url)
|
|
111
173
|
|
|
@@ -120,20 +182,6 @@ class URLSessionDownloadService: NSObject, DownloadService {
|
|
|
120
182
|
fileSizeHandlers[task] = handler
|
|
121
183
|
}
|
|
122
184
|
|
|
123
|
-
// Extract bundleId from destination path (e.g., "bundle-store/{bundleId}/bundle.zip")
|
|
124
|
-
let bundleId = (destination as NSString).pathComponents
|
|
125
|
-
.dropFirst()
|
|
126
|
-
.first(where: { $0 != "bundle-store" }) ?? "unknown"
|
|
127
|
-
|
|
128
|
-
// Save task metadata for background recovery
|
|
129
|
-
let taskState = TaskState(
|
|
130
|
-
taskIdentifier: task.taskIdentifier,
|
|
131
|
-
destination: destination,
|
|
132
|
-
bundleId: bundleId,
|
|
133
|
-
startedAt: Date().timeIntervalSince1970
|
|
134
|
-
)
|
|
135
|
-
saveTaskState(taskState)
|
|
136
|
-
|
|
137
185
|
task.resume()
|
|
138
186
|
return task
|
|
139
187
|
}
|
|
@@ -141,6 +189,8 @@ class URLSessionDownloadService: NSObject, DownloadService {
|
|
|
141
189
|
|
|
142
190
|
extension URLSessionDownloadService: URLSessionDownloadDelegate {
|
|
143
191
|
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
|
192
|
+
stateLock.lock()
|
|
193
|
+
defer { stateLock.unlock() }
|
|
144
194
|
let completion = completionHandlers[downloadTask]
|
|
145
195
|
let destination = destinations[downloadTask]
|
|
146
196
|
|
|
@@ -149,10 +199,6 @@ extension URLSessionDownloadService: URLSessionDownloadDelegate {
|
|
|
149
199
|
completionHandlers.removeValue(forKey: downloadTask)
|
|
150
200
|
destinations.removeValue(forKey: downloadTask)
|
|
151
201
|
fileSizeHandlers.removeValue(forKey: downloadTask)
|
|
152
|
-
removeTaskState(downloadTask.taskIdentifier)
|
|
153
|
-
|
|
154
|
-
// 다운로드 완료 알림
|
|
155
|
-
NotificationCenter.default.post(name: .downloadDidFinish, object: downloadTask)
|
|
156
202
|
}
|
|
157
203
|
|
|
158
204
|
guard let destination = destination else {
|
|
@@ -160,6 +206,14 @@ extension URLSessionDownloadService: URLSessionDownloadDelegate {
|
|
|
160
206
|
return
|
|
161
207
|
}
|
|
162
208
|
|
|
209
|
+
// URLSession saves error bodies too, so reject them before they reach verification.
|
|
210
|
+
if let httpError = DownloadError.httpStatusError(for: downloadTask.response, body: location) {
|
|
211
|
+
NSLog("[DownloadService] Download failed: \(httpError)")
|
|
212
|
+
try? FileManager.default.removeItem(at: location)
|
|
213
|
+
completion?(.failure(httpError))
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
163
217
|
// Verify file size
|
|
164
218
|
let expectedSize = downloadTask.response?.expectedContentLength ?? -1
|
|
165
219
|
let actualSize: Int64?
|
|
@@ -197,15 +251,14 @@ extension URLSessionDownloadService: URLSessionDownloadDelegate {
|
|
|
197
251
|
}
|
|
198
252
|
|
|
199
253
|
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
|
254
|
+
stateLock.lock()
|
|
255
|
+
defer { stateLock.unlock() }
|
|
200
256
|
let completion = completionHandlers[task]
|
|
201
257
|
defer {
|
|
202
258
|
progressHandlers.removeValue(forKey: task)
|
|
203
259
|
completionHandlers.removeValue(forKey: task)
|
|
204
260
|
destinations.removeValue(forKey: task)
|
|
205
261
|
fileSizeHandlers.removeValue(forKey: task)
|
|
206
|
-
removeTaskState(task.taskIdentifier)
|
|
207
|
-
|
|
208
|
-
NotificationCenter.default.post(name: .downloadDidFinish, object: task)
|
|
209
262
|
}
|
|
210
263
|
|
|
211
264
|
if let error = error {
|
|
@@ -214,6 +267,8 @@ extension URLSessionDownloadService: URLSessionDownloadDelegate {
|
|
|
214
267
|
}
|
|
215
268
|
|
|
216
269
|
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
|
270
|
+
stateLock.lock()
|
|
271
|
+
defer { stateLock.unlock() }
|
|
217
272
|
let progressHandler = progressHandlers[downloadTask]
|
|
218
273
|
|
|
219
274
|
// Call file size handler on first callback when size is known
|
|
@@ -232,16 +287,9 @@ extension URLSessionDownloadService: URLSessionDownloadDelegate {
|
|
|
232
287
|
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
|
|
233
288
|
progressHandler?(DownloadProgress(progress: progress, downloadedBytes: totalBytesWritten, totalBytes: totalBytesExpectedToWrite))
|
|
234
289
|
|
|
235
|
-
let progressInfo: [String: Any] = [
|
|
236
|
-
"progress": progress,
|
|
237
|
-
"totalBytesReceived": totalBytesWritten,
|
|
238
|
-
"totalBytesExpected": totalBytesExpectedToWrite
|
|
239
|
-
]
|
|
240
|
-
NotificationCenter.default.post(name: .downloadProgressUpdate, object: downloadTask, userInfo: progressInfo)
|
|
241
290
|
} else {
|
|
242
291
|
progressHandler?(DownloadProgress(progress: 0, downloadedBytes: totalBytesWritten, totalBytes: nil))
|
|
243
292
|
|
|
244
|
-
NotificationCenter.default.post(name: .downloadProgressUpdate, object: downloadTask, userInfo: ["progress": 0.0, "totalBytesReceived": 0, "totalBytesExpected": 0])
|
|
245
293
|
}
|
|
246
294
|
}
|
|
247
295
|
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Where an update failed: `download` covers transferring the update's files
|
|
4
|
+
/// and verifying the downloaded bytes, and `install` covers producing the
|
|
5
|
+
/// bundle on disk (patching, extracting, and moving files into place).
|
|
6
|
+
enum UpdateFailureStage: String {
|
|
7
|
+
case download
|
|
8
|
+
case install
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/// Why an update failed.
|
|
12
|
+
enum UpdateFailureReason: String {
|
|
13
|
+
case network
|
|
14
|
+
case http
|
|
15
|
+
case invalidResponse = "invalid_response"
|
|
16
|
+
case hashMismatch = "hash_mismatch"
|
|
17
|
+
case signature
|
|
18
|
+
case patch
|
|
19
|
+
case extract
|
|
20
|
+
case storage
|
|
21
|
+
case unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// What was being fetched or applied when an update failed.
|
|
25
|
+
enum UpdateFailureResource: String {
|
|
26
|
+
case manifest
|
|
27
|
+
case file
|
|
28
|
+
case patch
|
|
29
|
+
case archive
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// How a transfer failed when no HTTP response arrived.
|
|
33
|
+
enum UpdateFailureTransport: String {
|
|
34
|
+
case timeout
|
|
35
|
+
case dns
|
|
36
|
+
case tls
|
|
37
|
+
case connection
|
|
38
|
+
case offline
|
|
39
|
+
case cancelled
|
|
40
|
+
|
|
41
|
+
/// Returns nil for codes that do not name a transport failure.
|
|
42
|
+
init?(_ code: URLError.Code) {
|
|
43
|
+
switch code {
|
|
44
|
+
case .timedOut:
|
|
45
|
+
self = .timeout
|
|
46
|
+
case .cannotFindHost, .dnsLookupFailed:
|
|
47
|
+
self = .dns
|
|
48
|
+
case .secureConnectionFailed,
|
|
49
|
+
.serverCertificateHasBadDate,
|
|
50
|
+
.serverCertificateUntrusted,
|
|
51
|
+
.serverCertificateHasUnknownRoot,
|
|
52
|
+
.serverCertificateNotYetValid,
|
|
53
|
+
.clientCertificateRejected,
|
|
54
|
+
.clientCertificateRequired:
|
|
55
|
+
self = .tls
|
|
56
|
+
case .cannotConnectToHost, .networkConnectionLost:
|
|
57
|
+
self = .connection
|
|
58
|
+
case .notConnectedToInternet, .dataNotAllowed, .internationalRoamingOff:
|
|
59
|
+
self = .offline
|
|
60
|
+
case .cancelled:
|
|
61
|
+
self = .cancelled
|
|
62
|
+
default:
|
|
63
|
+
return nil
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// The classification an `updateBundle` rejection carries in its userInfo.
|
|
69
|
+
struct UpdateFailure: Equatable {
|
|
70
|
+
static let stageKey = "stage"
|
|
71
|
+
static let reasonKey = "reason"
|
|
72
|
+
static let resourceKey = "resource"
|
|
73
|
+
static let httpStatusKey = "httpStatus"
|
|
74
|
+
static let transportKey = "transport"
|
|
75
|
+
static let originCodeKey = "originCode"
|
|
76
|
+
|
|
77
|
+
static let network = UpdateFailure(stage: .download, reason: .network)
|
|
78
|
+
static let invalidResponse = UpdateFailure(stage: .download, reason: .invalidResponse)
|
|
79
|
+
static let hashMismatch = UpdateFailure(stage: .download, reason: .hashMismatch)
|
|
80
|
+
static let signature = UpdateFailure(stage: .download, reason: .signature)
|
|
81
|
+
static let patch = UpdateFailure(stage: .install, reason: .patch)
|
|
82
|
+
static let extract = UpdateFailure(stage: .install, reason: .extract)
|
|
83
|
+
static let storage = UpdateFailure(stage: .install, reason: .storage)
|
|
84
|
+
|
|
85
|
+
static func network(transport: UpdateFailureTransport?) -> UpdateFailure {
|
|
86
|
+
UpdateFailure(stage: .download, reason: .network, transport: transport)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static func http(status: Int, originCode: String? = nil) -> UpdateFailure {
|
|
90
|
+
UpdateFailure(
|
|
91
|
+
stage: .download,
|
|
92
|
+
reason: .http,
|
|
93
|
+
httpStatus: status,
|
|
94
|
+
originCode: originCode
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static func unknown(_ stage: UpdateFailureStage) -> UpdateFailure {
|
|
99
|
+
UpdateFailure(stage: stage, reason: .unknown)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let stage: UpdateFailureStage
|
|
103
|
+
let reason: UpdateFailureReason
|
|
104
|
+
/// What was being fetched or applied, when the failure concerns one.
|
|
105
|
+
private(set) var resource: UpdateFailureResource?
|
|
106
|
+
/// The response status; only set when `reason` is `.http`.
|
|
107
|
+
let httpStatus: Int?
|
|
108
|
+
/// Only set when `reason` is `.network` and no response arrived.
|
|
109
|
+
let transport: UpdateFailureTransport?
|
|
110
|
+
/// The storage origin's XML error code; only set when `reason` is `.http`.
|
|
111
|
+
let originCode: String?
|
|
112
|
+
|
|
113
|
+
private init(
|
|
114
|
+
stage: UpdateFailureStage,
|
|
115
|
+
reason: UpdateFailureReason,
|
|
116
|
+
httpStatus: Int? = nil,
|
|
117
|
+
transport: UpdateFailureTransport? = nil,
|
|
118
|
+
originCode: String? = nil
|
|
119
|
+
) {
|
|
120
|
+
self.stage = stage
|
|
121
|
+
self.reason = reason
|
|
122
|
+
self.httpStatus = httpStatus
|
|
123
|
+
self.transport = transport
|
|
124
|
+
self.originCode = originCode
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// This failure, attributed to `resource` unless it already names one.
|
|
128
|
+
func with(resource: UpdateFailureResource?) -> UpdateFailure {
|
|
129
|
+
guard self.resource == nil, let resource else {
|
|
130
|
+
return self
|
|
131
|
+
}
|
|
132
|
+
var failure = self
|
|
133
|
+
failure.resource = resource
|
|
134
|
+
return failure
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// JSON-safe entries React Native copies into the JS error's `userInfo`.
|
|
138
|
+
var userInfo: [String: Any] {
|
|
139
|
+
var userInfo: [String: Any] = [
|
|
140
|
+
Self.stageKey: stage.rawValue,
|
|
141
|
+
Self.reasonKey: reason.rawValue,
|
|
142
|
+
]
|
|
143
|
+
if let resource {
|
|
144
|
+
userInfo[Self.resourceKey] = resource.rawValue
|
|
145
|
+
}
|
|
146
|
+
if let httpStatus {
|
|
147
|
+
userInfo[Self.httpStatusKey] = NSNumber(value: httpStatus)
|
|
148
|
+
}
|
|
149
|
+
if let transport {
|
|
150
|
+
userInfo[Self.transportKey] = transport.rawValue
|
|
151
|
+
}
|
|
152
|
+
if let originCode {
|
|
153
|
+
userInfo[Self.originCodeKey] = originCode
|
|
154
|
+
}
|
|
155
|
+
return userInfo
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// An error carrying this classification, for rejections raised before
|
|
159
|
+
/// the update pipeline starts, such as invalid `updateBundle` parameters.
|
|
160
|
+
func error(description: String) -> NSError {
|
|
161
|
+
var userInfo = self.userInfo
|
|
162
|
+
userInfo[NSLocalizedDescriptionKey] = description
|
|
163
|
+
return NSError(domain: "HotUpdater", code: 0, userInfo: userInfo)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// The update step an error came from. It classifies the errors whose type
|
|
168
|
+
/// does not: a hash mismatch is a download failure after a transfer but a
|
|
169
|
+
/// patch failure after patching, and an unexpected error keeps the stage.
|
|
170
|
+
enum UpdatePhase {
|
|
171
|
+
/// Transferring manifest, archive, asset, or patch bytes.
|
|
172
|
+
case transfer
|
|
173
|
+
/// Checking downloaded bytes against the manifest.
|
|
174
|
+
case verification
|
|
175
|
+
/// Parsing and validating the manifest against the update parameters.
|
|
176
|
+
case manifest
|
|
177
|
+
/// Applying a bsdiff patch and checking its output.
|
|
178
|
+
case patch
|
|
179
|
+
/// Decoding brotli or tar data and checking the extracted files.
|
|
180
|
+
case extraction
|
|
181
|
+
/// Creating directories and copying, moving, or writing files.
|
|
182
|
+
case storage
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// An update pipeline error and its classification. `underlying` still
|
|
186
|
+
/// decides the rejection code, so classifying an error never changes it.
|
|
187
|
+
struct UpdateFailureError: Error {
|
|
188
|
+
let underlying: Error
|
|
189
|
+
let failure: UpdateFailure
|
|
190
|
+
|
|
191
|
+
/// Classifies `error` as raised during `phase` while fetching or applying
|
|
192
|
+
/// `resource`. An error that is already classified keeps its
|
|
193
|
+
/// classification and only gains a missing resource. Returns `error`
|
|
194
|
+
/// unchanged when it is not an update failure.
|
|
195
|
+
static func classifying(
|
|
196
|
+
_ error: Error,
|
|
197
|
+
during phase: UpdatePhase,
|
|
198
|
+
resource: UpdateFailureResource? = nil
|
|
199
|
+
) -> Error {
|
|
200
|
+
if let classified = error as? UpdateFailureError {
|
|
201
|
+
return UpdateFailureError(
|
|
202
|
+
underlying: classified.underlying,
|
|
203
|
+
failure: classified.failure.with(resource: resource)
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
guard let failure = UpdateFailureClassifier.classify(error, during: phase) else {
|
|
207
|
+
return error
|
|
208
|
+
}
|
|
209
|
+
return UpdateFailureError(underlying: error, failure: failure.with(resource: resource))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// Runs `body`, classifying what it throws as raised during `phase`.
|
|
213
|
+
static func during<T>(
|
|
214
|
+
_ phase: UpdatePhase,
|
|
215
|
+
resource: UpdateFailureResource? = nil,
|
|
216
|
+
_ body: () throws -> T
|
|
217
|
+
) throws -> T {
|
|
218
|
+
do {
|
|
219
|
+
return try body()
|
|
220
|
+
} catch {
|
|
221
|
+
throw classifying(error, during: phase, resource: resource)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// The error that decides the rejection code.
|
|
226
|
+
static func unwrap(_ error: Error) -> Error {
|
|
227
|
+
(error as? UpdateFailureError)?.underlying ?? error
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// The error `updateBundle` rejects with: the underlying error's domain,
|
|
231
|
+
/// code, and userInfo, plus the classification entries.
|
|
232
|
+
static func rejectionError(for error: Error) -> NSError {
|
|
233
|
+
guard let classified = error as? UpdateFailureError else {
|
|
234
|
+
return error as NSError
|
|
235
|
+
}
|
|
236
|
+
let underlying = classified.underlying as NSError
|
|
237
|
+
var userInfo = underlying.userInfo
|
|
238
|
+
userInfo[NSLocalizedDescriptionKey] = underlying.localizedDescription
|
|
239
|
+
userInfo.merge(classified.failure.userInfo) { _, classification in classification }
|
|
240
|
+
return NSError(domain: underlying.domain, code: underlying.code, userInfo: userInfo)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
extension UpdateFailureError: LocalizedError {
|
|
245
|
+
var errorDescription: String? {
|
|
246
|
+
underlying.localizedDescription
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// The Release selection an install was staged for changed before the bundle
|
|
251
|
+
/// was activated. The rejection is not an update failure.
|
|
252
|
+
struct StaleReleaseSelectionError: Error, CustomNSError {
|
|
253
|
+
static var errorDomain: String {
|
|
254
|
+
"HotUpdater"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
var errorCode: Int {
|
|
258
|
+
0
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
var errorUserInfo: [String: Any] {
|
|
262
|
+
[NSLocalizedDescriptionKey: "Release catalog selection is stale"]
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
enum UpdateFailureClassifier {
|
|
267
|
+
/// Classifies an error raised during `phase`, or returns nil when the
|
|
268
|
+
/// error is not an update failure.
|
|
269
|
+
static func classify(_ error: Error, during phase: UpdatePhase) -> UpdateFailure? {
|
|
270
|
+
switch error {
|
|
271
|
+
case let error as UpdateFailureError:
|
|
272
|
+
return error.failure
|
|
273
|
+
case is StaleReleaseSelectionError:
|
|
274
|
+
return nil
|
|
275
|
+
case let error as BundleStorageError:
|
|
276
|
+
return classifyStorageError(error, during: phase)
|
|
277
|
+
case let error as SignatureVerificationError:
|
|
278
|
+
return classifySignatureError(error, during: phase)
|
|
279
|
+
case let error as DownloadError:
|
|
280
|
+
return classifyDownloadError(error)
|
|
281
|
+
case let error as URLError:
|
|
282
|
+
return classifyURLError(error)
|
|
283
|
+
default:
|
|
284
|
+
return isFileSystemError(error) ? .storage : fallback(during: phase)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private static func classifyStorageError(
|
|
289
|
+
_ error: BundleStorageError,
|
|
290
|
+
during phase: UpdatePhase
|
|
291
|
+
) -> UpdateFailure? {
|
|
292
|
+
switch error {
|
|
293
|
+
case .directoryCreationFailed, .moveOperationFailed:
|
|
294
|
+
return .storage
|
|
295
|
+
case .downloadFailed(let underlying):
|
|
296
|
+
return classify(underlying, during: .transfer)
|
|
297
|
+
case .incompleteDownload:
|
|
298
|
+
return .network
|
|
299
|
+
case .invalidBundle:
|
|
300
|
+
return .invalidResponse
|
|
301
|
+
case .signatureVerificationFailed(let underlying):
|
|
302
|
+
return classifySignatureError(underlying, during: phase)
|
|
303
|
+
case .bundleInCrashedHistory:
|
|
304
|
+
return nil
|
|
305
|
+
case .unknown(.some(let underlying)):
|
|
306
|
+
return classify(underlying, during: phase)
|
|
307
|
+
case .unknown(.none):
|
|
308
|
+
return fallback(during: phase)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private static func classifySignatureError(
|
|
313
|
+
_ error: SignatureVerificationError,
|
|
314
|
+
during phase: UpdatePhase
|
|
315
|
+
) -> UpdateFailure {
|
|
316
|
+
switch error {
|
|
317
|
+
case .fileHashMismatch:
|
|
318
|
+
switch phase {
|
|
319
|
+
case .patch:
|
|
320
|
+
return .patch
|
|
321
|
+
case .extraction:
|
|
322
|
+
return .extract
|
|
323
|
+
case .transfer, .verification, .manifest, .storage:
|
|
324
|
+
return .hashMismatch
|
|
325
|
+
}
|
|
326
|
+
case .fileReadFailed:
|
|
327
|
+
return .storage
|
|
328
|
+
case .missingFileHash:
|
|
329
|
+
return .invalidResponse
|
|
330
|
+
case .publicKeyNotConfigured,
|
|
331
|
+
.invalidPublicKeyFormat,
|
|
332
|
+
.invalidSignatureFormat,
|
|
333
|
+
.signatureVerificationFailed,
|
|
334
|
+
.unsignedNotAllowed,
|
|
335
|
+
.securityFrameworkError:
|
|
336
|
+
return .signature
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private static func classifyDownloadError(_ error: DownloadError) -> UpdateFailure {
|
|
341
|
+
switch error {
|
|
342
|
+
case .httpStatus(let status, let originCode):
|
|
343
|
+
return .http(status: status, originCode: originCode)
|
|
344
|
+
case .incompleteDownload, .invalidContentLength:
|
|
345
|
+
// A response arrived, so the failure names no transport.
|
|
346
|
+
return .network
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private static func classifyURLError(_ error: URLError) -> UpdateFailure {
|
|
351
|
+
switch error.code {
|
|
352
|
+
case .badURL, .unsupportedURL:
|
|
353
|
+
return .invalidResponse
|
|
354
|
+
case .cannotCreateFile,
|
|
355
|
+
.cannotOpenFile,
|
|
356
|
+
.cannotCloseFile,
|
|
357
|
+
.cannotWriteToFile,
|
|
358
|
+
.cannotRemoveFile,
|
|
359
|
+
.cannotMoveFile:
|
|
360
|
+
return .storage
|
|
361
|
+
default:
|
|
362
|
+
return .network(transport: UpdateFailureTransport(error.code))
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private static func isFileSystemError(_ error: Error) -> Bool {
|
|
367
|
+
if error is FileSystemError {
|
|
368
|
+
return true
|
|
369
|
+
}
|
|
370
|
+
if let cocoaError = error as? CocoaError {
|
|
371
|
+
return cocoaError.isFileError
|
|
372
|
+
}
|
|
373
|
+
return (error as NSError).domain == NSPOSIXErrorDomain
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private static func fallback(during phase: UpdatePhase) -> UpdateFailure {
|
|
377
|
+
switch phase {
|
|
378
|
+
case .transfer, .verification:
|
|
379
|
+
return .unknown(.download)
|
|
380
|
+
case .manifest:
|
|
381
|
+
return .invalidResponse
|
|
382
|
+
case .patch:
|
|
383
|
+
return .patch
|
|
384
|
+
case .extraction:
|
|
385
|
+
return .extract
|
|
386
|
+
case .storage:
|
|
387
|
+
return .unknown(.install)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
#ifdef __cplusplus
|
|
4
|
+
extern "C" {
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
void HotUpdaterMarkFailedRuntime(void);
|
|
8
|
+
BOOL HotUpdaterIsFailedRuntime(void);
|
|
9
|
+
void HotUpdaterInstallRecoveryHooks(void);
|
|
10
|
+
BOOL HotUpdaterBeginSurfaceStart(const void *surfaceHandler);
|
|
11
|
+
void HotUpdaterFinishSurfaceStart(const void *surfaceHandler);
|
|
12
|
+
void HotUpdaterAfterSurfaceStarts(dispatch_block_t action);
|
|
13
|
+
|
|
14
|
+
#ifdef __cplusplus
|
|
15
|
+
}
|
|
16
|
+
#endif
|