@hot-updater/react-native 0.23.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +393 -49
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +204 -0
- package/android/src/main/java/com/hotupdater/HotUpdater.kt +48 -36
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +134 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +168 -95
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +15 -3
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +17 -12
- package/android/src/newarch/HotUpdaterModule.kt +88 -23
- package/android/src/oldarch/HotUpdaterModule.kt +89 -22
- package/android/src/oldarch/HotUpdaterSpec.kt +6 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +401 -77
- package/ios/HotUpdater/Internal/BundleMetadata.swift +177 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +213 -47
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +96 -25
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +35 -29
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +2 -2
- package/ios/HotUpdater/Public/HotUpdater.h +8 -2
- package/lib/commonjs/checkForUpdate.js +31 -28
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/error.js +45 -1
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchUpdateInfo.js +7 -45
- package/lib/commonjs/fetchUpdateInfo.js.map +1 -1
- package/lib/commonjs/index.js +237 -208
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +103 -3
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/wrap.js +39 -1
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/checkForUpdate.js +32 -26
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/error.js +45 -0
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchUpdateInfo.js +7 -45
- package/lib/module/fetchUpdateInfo.js.map +1 -1
- package/lib/module/index.js +238 -203
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +87 -2
- package/lib/module/native.js.map +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/wrap.js +40 -2
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +11 -13
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/error.d.ts +120 -0
- package/lib/typescript/commonjs/error.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +3 -5
- package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +35 -41
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +58 -2
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +62 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +76 -5
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +11 -13
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/error.d.ts +120 -0
- package/lib/typescript/module/error.d.ts.map +1 -1
- package/lib/typescript/module/fetchUpdateInfo.d.ts +3 -5
- package/lib/typescript/module/fetchUpdateInfo.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +35 -41
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +58 -2
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +62 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +76 -5
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +7 -7
- package/plugin/build/withHotUpdater.js +3 -3
- package/src/checkForUpdate.ts +51 -40
- package/src/error.ts +153 -0
- package/src/fetchUpdateInfo.ts +10 -58
- package/src/index.ts +283 -206
- package/src/native.ts +88 -2
- package/src/specs/NativeHotUpdater.ts +63 -0
- package/src/wrap.tsx +131 -9
- package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +0 -52
- package/ios/HotUpdater/Internal/HotUpdaterFactory.swift +0 -24
- package/lib/commonjs/runUpdateProcess.js +0 -69
- package/lib/commonjs/runUpdateProcess.js.map +0 -1
- package/lib/module/runUpdateProcess.js +0 -64
- package/lib/module/runUpdateProcess.js.map +0 -1
- package/lib/typescript/commonjs/runUpdateProcess.d.ts +0 -49
- package/lib/typescript/commonjs/runUpdateProcess.d.ts.map +0 -1
- package/lib/typescript/module/runUpdateProcess.d.ts +0 -49
- package/lib/typescript/module/runUpdateProcess.d.ts.map +0 -1
- package/src/runUpdateProcess.ts +0 -80
|
@@ -1,40 +1,38 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
3
|
public enum BundleStorageError: Error, CustomNSError {
|
|
4
|
-
case bundleNotFound
|
|
5
4
|
case directoryCreationFailed
|
|
6
5
|
case downloadFailed(Error)
|
|
7
|
-
case
|
|
6
|
+
case incompleteDownload(expected: Int64, actual: Int64)
|
|
7
|
+
case extractionFormatError(Error)
|
|
8
8
|
case invalidBundle
|
|
9
|
-
case invalidZipFile
|
|
10
9
|
case insufficientDiskSpace
|
|
11
|
-
case hashMismatch
|
|
12
10
|
case signatureVerificationFailed(SignatureVerificationError)
|
|
13
11
|
case moveOperationFailed(Error)
|
|
14
|
-
case
|
|
15
|
-
case fileSystemError(Error)
|
|
12
|
+
case bundleInCrashedHistory(String)
|
|
16
13
|
case unknown(Error?)
|
|
17
14
|
|
|
18
15
|
// CustomNSError protocol implementation
|
|
19
16
|
public static var errorDomain: String {
|
|
20
|
-
return "
|
|
17
|
+
return "HotUpdater"
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
public var errorCode: Int {
|
|
21
|
+
return 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public var errorCodeString: String {
|
|
24
25
|
switch self {
|
|
25
|
-
case .
|
|
26
|
-
case .
|
|
27
|
-
case .
|
|
28
|
-
case .
|
|
29
|
-
case .invalidBundle: return
|
|
30
|
-
case .
|
|
31
|
-
case .
|
|
32
|
-
case .
|
|
33
|
-
case .
|
|
34
|
-
case .
|
|
35
|
-
case .copyOperationFailed: return 1011
|
|
36
|
-
case .fileSystemError: return 1012
|
|
37
|
-
case .unknown: return 1099
|
|
26
|
+
case .directoryCreationFailed: return "DIRECTORY_CREATION_FAILED"
|
|
27
|
+
case .downloadFailed: return "DOWNLOAD_FAILED"
|
|
28
|
+
case .incompleteDownload: return "INCOMPLETE_DOWNLOAD"
|
|
29
|
+
case .extractionFormatError: return "EXTRACTION_FORMAT_ERROR"
|
|
30
|
+
case .invalidBundle: return "INVALID_BUNDLE"
|
|
31
|
+
case .insufficientDiskSpace: return "INSUFFICIENT_DISK_SPACE"
|
|
32
|
+
case .signatureVerificationFailed: return "SIGNATURE_VERIFICATION_FAILED"
|
|
33
|
+
case .moveOperationFailed: return "MOVE_OPERATION_FAILED"
|
|
34
|
+
case .bundleInCrashedHistory: return "BUNDLE_IN_CRASHED_HISTORY"
|
|
35
|
+
case .unknown: return "UNKNOWN_ERROR"
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
|
|
@@ -42,10 +40,6 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
42
40
|
var userInfo: [String: Any] = [:]
|
|
43
41
|
|
|
44
42
|
switch self {
|
|
45
|
-
case .bundleNotFound:
|
|
46
|
-
userInfo[NSLocalizedDescriptionKey] = "Bundle file not found in the downloaded archive"
|
|
47
|
-
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Ensure the bundle archive contains index.ios.bundle or main.jsbundle"
|
|
48
|
-
|
|
49
43
|
case .directoryCreationFailed:
|
|
50
44
|
userInfo[NSLocalizedDescriptionKey] = "Failed to create required directory for bundle storage"
|
|
51
45
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Check app permissions and available disk space"
|
|
@@ -55,27 +49,23 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
55
49
|
userInfo[NSUnderlyingErrorKey] = underlyingError
|
|
56
50
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Check network connection and try again"
|
|
57
51
|
|
|
58
|
-
case .
|
|
59
|
-
userInfo[NSLocalizedDescriptionKey] = "
|
|
52
|
+
case .incompleteDownload(let expected, let actual):
|
|
53
|
+
userInfo[NSLocalizedDescriptionKey] = "Download incomplete: received \(actual) bytes, expected \(expected) bytes"
|
|
54
|
+
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The download was interrupted. Check network connection and try again"
|
|
55
|
+
|
|
56
|
+
case .extractionFormatError(let underlyingError):
|
|
57
|
+
userInfo[NSLocalizedDescriptionKey] = "Invalid or corrupted bundle archive format"
|
|
60
58
|
userInfo[NSUnderlyingErrorKey] = underlyingError
|
|
61
|
-
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The
|
|
59
|
+
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The bundle archive may be corrupted or in an unsupported format. Try downloading again"
|
|
62
60
|
|
|
63
61
|
case .invalidBundle:
|
|
64
|
-
userInfo[NSLocalizedDescriptionKey] = "
|
|
62
|
+
userInfo[NSLocalizedDescriptionKey] = "Bundle missing required platform files (index.ios.bundle or main.jsbundle)"
|
|
65
63
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Verify the bundle was built correctly with metro bundler"
|
|
66
64
|
|
|
67
|
-
case .invalidZipFile:
|
|
68
|
-
userInfo[NSLocalizedDescriptionKey] = "Downloaded file is not a valid ZIP archive"
|
|
69
|
-
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The file may be corrupted during download"
|
|
70
|
-
|
|
71
65
|
case .insufficientDiskSpace:
|
|
72
66
|
userInfo[NSLocalizedDescriptionKey] = "Insufficient disk space to download and extract bundle"
|
|
73
67
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Free up device storage and try again"
|
|
74
68
|
|
|
75
|
-
case .hashMismatch:
|
|
76
|
-
userInfo[NSLocalizedDescriptionKey] = "Downloaded bundle hash does not match expected hash"
|
|
77
|
-
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The file may have been corrupted or tampered with. Try downloading again"
|
|
78
|
-
|
|
79
69
|
case .signatureVerificationFailed(let underlyingError):
|
|
80
70
|
userInfo[NSLocalizedDescriptionKey] = "Bundle signature verification failed"
|
|
81
71
|
userInfo[NSUnderlyingErrorKey] = underlyingError
|
|
@@ -86,15 +76,9 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
86
76
|
userInfo[NSUnderlyingErrorKey] = underlyingError
|
|
87
77
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Check file system permissions"
|
|
88
78
|
|
|
89
|
-
case .
|
|
90
|
-
userInfo[NSLocalizedDescriptionKey] = "
|
|
91
|
-
userInfo[
|
|
92
|
-
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Check available disk space and permissions"
|
|
93
|
-
|
|
94
|
-
case .fileSystemError(let underlyingError):
|
|
95
|
-
userInfo[NSLocalizedDescriptionKey] = "File system operation failed"
|
|
96
|
-
userInfo[NSUnderlyingErrorKey] = underlyingError
|
|
97
|
-
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Check app permissions and disk space"
|
|
79
|
+
case .bundleInCrashedHistory(let bundleId):
|
|
80
|
+
userInfo[NSLocalizedDescriptionKey] = "Bundle '\(bundleId)' is in crashed history and cannot be applied"
|
|
81
|
+
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "This bundle previously caused a crash and was blocked for safety"
|
|
98
82
|
|
|
99
83
|
case .unknown(let underlyingError):
|
|
100
84
|
userInfo[NSLocalizedDescriptionKey] = "An unknown error occurred during bundle update"
|
|
@@ -114,15 +98,20 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
114
98
|
* Other operations are synchronous.
|
|
115
99
|
*/
|
|
116
100
|
public protocol BundleStorageService {
|
|
117
|
-
|
|
101
|
+
|
|
118
102
|
// Bundle URL operations
|
|
119
103
|
func setBundleURL(localPath: String?) -> Result<Void, Error>
|
|
120
104
|
func getCachedBundleURL() -> URL?
|
|
121
105
|
func getFallbackBundleURL() -> URL? // Synchronous as it's lightweight
|
|
122
106
|
func getBundleURL() -> URL?
|
|
123
|
-
|
|
107
|
+
|
|
124
108
|
// Bundle update
|
|
125
109
|
func updateBundle(bundleId: String, fileUrl: URL?, fileHash: String?, progressHandler: @escaping (Double) -> Void, completion: @escaping (Result<Bool, Error>) -> Void)
|
|
110
|
+
|
|
111
|
+
// Rollback support
|
|
112
|
+
func notifyAppReady(bundleId: String) -> [String: Any]
|
|
113
|
+
func getCrashHistory() -> CrashedHistory
|
|
114
|
+
func clearCrashHistory() -> Bool
|
|
126
115
|
}
|
|
127
116
|
|
|
128
117
|
class BundleFileStorageService: BundleStorageService {
|
|
@@ -131,11 +120,16 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
131
120
|
private let decompressService: DecompressService
|
|
132
121
|
private let preferences: PreferencesService
|
|
133
122
|
|
|
123
|
+
private let id = Int.random(in: 1..<100)
|
|
124
|
+
|
|
134
125
|
// Queue for potentially long-running sequences within updateBundle or for explicit background tasks.
|
|
135
126
|
private let fileOperationQueue: DispatchQueue
|
|
136
127
|
|
|
137
128
|
private var activeTasks: [URLSessionTask] = []
|
|
138
129
|
|
|
130
|
+
// Session-only rollback tracking (in-memory)
|
|
131
|
+
private var sessionRollbackBundleId: String?
|
|
132
|
+
|
|
139
133
|
public init(fileSystem: FileSystemService,
|
|
140
134
|
downloadService: DownloadService,
|
|
141
135
|
decompressService: DecompressService,
|
|
@@ -146,10 +140,161 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
146
140
|
self.decompressService = decompressService
|
|
147
141
|
self.preferences = preferences
|
|
148
142
|
|
|
143
|
+
// Create queue for file operations
|
|
149
144
|
self.fileOperationQueue = DispatchQueue(label: "com.hotupdater.fileoperations",
|
|
150
145
|
qos: .utility,
|
|
151
146
|
attributes: .concurrent)
|
|
152
147
|
}
|
|
148
|
+
|
|
149
|
+
// MARK: - Metadata File Paths
|
|
150
|
+
|
|
151
|
+
private func metadataFileURL() -> URL? {
|
|
152
|
+
guard case .success(let storeDir) = bundleStoreDir() else {
|
|
153
|
+
return nil
|
|
154
|
+
}
|
|
155
|
+
return URL(fileURLWithPath: storeDir).appendingPathComponent(BundleMetadata.metadataFilename)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private func crashedHistoryFileURL() -> URL? {
|
|
159
|
+
guard case .success(let storeDir) = bundleStoreDir() else {
|
|
160
|
+
return nil
|
|
161
|
+
}
|
|
162
|
+
return URL(fileURLWithPath: storeDir).appendingPathComponent(CrashedHistory.crashedHistoryFilename)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// MARK: - Metadata Operations
|
|
166
|
+
|
|
167
|
+
private func loadMetadataOrNull() -> BundleMetadata? {
|
|
168
|
+
guard let file = metadataFileURL() else {
|
|
169
|
+
return nil
|
|
170
|
+
}
|
|
171
|
+
return BundleMetadata.load(from: file)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private func saveMetadata(_ metadata: BundleMetadata) -> Bool {
|
|
175
|
+
guard let file = metadataFileURL() else {
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
return metadata.save(to: file)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// MARK: - Crashed History Operations
|
|
182
|
+
|
|
183
|
+
private func loadCrashedHistory() -> CrashedHistory {
|
|
184
|
+
guard let file = crashedHistoryFileURL() else {
|
|
185
|
+
return CrashedHistory()
|
|
186
|
+
}
|
|
187
|
+
return CrashedHistory.load(from: file)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private func saveCrashedHistory(_ history: CrashedHistory) -> Bool {
|
|
191
|
+
guard let file = crashedHistoryFileURL() else {
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
return history.save(to: file)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// MARK: - State Machine Methods
|
|
198
|
+
|
|
199
|
+
private func isVerificationPending(_ metadata: BundleMetadata) -> Bool {
|
|
200
|
+
return metadata.verificationPending && metadata.stagingBundleId != nil
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private func wasVerificationAttempted(_ metadata: BundleMetadata) -> Bool {
|
|
204
|
+
return metadata.verificationAttemptedAt != nil
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private func markVerificationAttempted() {
|
|
208
|
+
guard var metadata = loadMetadataOrNull() else {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
metadata.verificationAttemptedAt = Date().timeIntervalSince1970 * 1000
|
|
212
|
+
let _ = saveMetadata(metadata)
|
|
213
|
+
NSLog("[BundleStorage] Marked verification attempted for staging bundle: \(metadata.stagingBundleId ?? "nil")")
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private func incrementStagingExecutionCount() {
|
|
217
|
+
guard var metadata = loadMetadataOrNull() else {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
metadata.stagingExecutionCount = (metadata.stagingExecutionCount ?? 0) + 1
|
|
221
|
+
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
222
|
+
let _ = saveMetadata(metadata)
|
|
223
|
+
NSLog("[BundleStorage] Incremented staging execution count to: \(metadata.stagingExecutionCount ?? 0)")
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private func promoteStagingToStable() {
|
|
227
|
+
guard var metadata = loadMetadataOrNull() else {
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
guard let stagingId = metadata.stagingBundleId else {
|
|
231
|
+
NSLog("[BundleStorage] No staging bundle to promote")
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let oldStableId = metadata.stableBundleId
|
|
236
|
+
metadata.stableBundleId = stagingId
|
|
237
|
+
metadata.stagingBundleId = nil
|
|
238
|
+
metadata.verificationPending = false
|
|
239
|
+
metadata.verificationAttemptedAt = nil
|
|
240
|
+
metadata.stagingExecutionCount = nil
|
|
241
|
+
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
242
|
+
|
|
243
|
+
if saveMetadata(metadata) {
|
|
244
|
+
NSLog("[BundleStorage] Promoted staging '\(stagingId)' to stable (old stable: \(oldStableId ?? "nil"))")
|
|
245
|
+
|
|
246
|
+
// Clean up old stable bundle
|
|
247
|
+
if let oldId = oldStableId, oldId != stagingId {
|
|
248
|
+
let _ = cleanupOldBundles(currentBundleId: stagingId, bundleId: nil)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private func rollbackToStable() {
|
|
254
|
+
guard var metadata = loadMetadataOrNull() else {
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
guard let stagingId = metadata.stagingBundleId else {
|
|
258
|
+
NSLog("[BundleStorage] No staging bundle to rollback from")
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Add crashed bundle to history
|
|
263
|
+
var crashedHistory = loadCrashedHistory()
|
|
264
|
+
crashedHistory.addEntry(stagingId)
|
|
265
|
+
let _ = saveCrashedHistory(crashedHistory)
|
|
266
|
+
NSLog("[BundleStorage('\(id)')] Added bundle '\(stagingId)' to crashed history")
|
|
267
|
+
|
|
268
|
+
// Save rollback info to session variable (memory only)
|
|
269
|
+
self.sessionRollbackBundleId = stagingId
|
|
270
|
+
|
|
271
|
+
// Clear staging
|
|
272
|
+
metadata.stagingBundleId = nil
|
|
273
|
+
metadata.verificationPending = false
|
|
274
|
+
metadata.verificationAttemptedAt = nil
|
|
275
|
+
metadata.stagingExecutionCount = nil
|
|
276
|
+
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
277
|
+
|
|
278
|
+
if saveMetadata(metadata) {
|
|
279
|
+
NSLog("[BundleStorage] Rolled back to stable bundle: \(metadata.stableBundleId ?? "fallback")")
|
|
280
|
+
|
|
281
|
+
// Update HotUpdaterBundleURL to point to stable bundle
|
|
282
|
+
if let stableId = metadata.stableBundleId {
|
|
283
|
+
if case .success(let storeDir) = bundleStoreDir() {
|
|
284
|
+
let stableBundleDir = (storeDir as NSString).appendingPathComponent(stableId)
|
|
285
|
+
if case .success(let bundlePath) = findBundleFile(in: stableBundleDir), let path = bundlePath {
|
|
286
|
+
let _ = setBundleURL(localPath: path)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
// Reset to fallback
|
|
291
|
+
let _ = setBundleURL(localPath: nil)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Clean up failed staging bundle
|
|
295
|
+
let _ = cleanupOldBundles(currentBundleId: metadata.stableBundleId, bundleId: nil)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
153
298
|
|
|
154
299
|
// MARK: - Directory Management
|
|
155
300
|
|
|
@@ -276,11 +421,17 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
276
421
|
contents = try self.fileSystem.contentsOfDirectory(atPath: storeDir)
|
|
277
422
|
} catch let error {
|
|
278
423
|
NSLog("[BundleStorage] Failed to list contents of bundle store directory: \(storeDir)")
|
|
279
|
-
return .failure(BundleStorageError.
|
|
424
|
+
return .failure(BundleStorageError.unknown(error))
|
|
280
425
|
}
|
|
281
426
|
|
|
282
427
|
let bundles = contents.compactMap { item -> String? in
|
|
283
428
|
let fullPath = (storeDir as NSString).appendingPathComponent(item)
|
|
429
|
+
|
|
430
|
+
// Skip metadata files - DO NOT delete
|
|
431
|
+
if item == "metadata.json" || item == "crashed-history.json" {
|
|
432
|
+
return nil
|
|
433
|
+
}
|
|
434
|
+
|
|
284
435
|
return (!item.hasSuffix(".tmp") && self.fileSystem.fileExists(atPath: fullPath)) ? fullPath : nil
|
|
285
436
|
}
|
|
286
437
|
|
|
@@ -360,7 +511,64 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
360
511
|
}
|
|
361
512
|
|
|
362
513
|
public func getBundleURL() -> URL? {
|
|
363
|
-
|
|
514
|
+
// Try to load metadata
|
|
515
|
+
let metadata = loadMetadataOrNull()
|
|
516
|
+
|
|
517
|
+
// If no metadata exists, use legacy behavior (backwards compatible)
|
|
518
|
+
guard let metadata = metadata else {
|
|
519
|
+
let cached = getCachedBundleURL()
|
|
520
|
+
return cached ?? getFallbackBundleURL()
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Check if we need to handle crash recovery
|
|
524
|
+
if isVerificationPending(metadata) {
|
|
525
|
+
let executionCount = metadata.stagingExecutionCount ?? 0
|
|
526
|
+
|
|
527
|
+
if executionCount == 0 {
|
|
528
|
+
// First execution - give staging bundle a chance
|
|
529
|
+
NSLog("[BundleStorage] First execution of staging bundle, incrementing counter")
|
|
530
|
+
incrementStagingExecutionCount()
|
|
531
|
+
// Don't mark verificationAttempted yet!
|
|
532
|
+
} else if wasVerificationAttempted(metadata) {
|
|
533
|
+
// Already executed once and verificationAttempted is set → crash!
|
|
534
|
+
NSLog("[BundleStorage] Crash detected: staging bundle executed but didn't call notifyAppReady")
|
|
535
|
+
rollbackToStable()
|
|
536
|
+
} else {
|
|
537
|
+
// Second execution - now mark verification attempted
|
|
538
|
+
NSLog("[BundleStorage] Second execution of staging bundle, marking verification attempted")
|
|
539
|
+
markVerificationAttempted()
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Reload metadata after potential rollback
|
|
544
|
+
guard let currentMetadata = loadMetadataOrNull() else {
|
|
545
|
+
return getCachedBundleURL() ?? getFallbackBundleURL()
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// If verification is pending, return staging bundle URL
|
|
549
|
+
if isVerificationPending(currentMetadata), let stagingId = currentMetadata.stagingBundleId {
|
|
550
|
+
if case .success(let storeDir) = bundleStoreDir() {
|
|
551
|
+
let stagingBundleDir = (storeDir as NSString).appendingPathComponent(stagingId)
|
|
552
|
+
if case .success(let bundlePath) = findBundleFile(in: stagingBundleDir), let path = bundlePath {
|
|
553
|
+
NSLog("[BundleStorage] Returning staging bundle URL: \(path)")
|
|
554
|
+
return URL(fileURLWithPath: path)
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Return stable bundle URL
|
|
560
|
+
if let stableId = currentMetadata.stableBundleId {
|
|
561
|
+
if case .success(let storeDir) = bundleStoreDir() {
|
|
562
|
+
let stableBundleDir = (storeDir as NSString).appendingPathComponent(stableId)
|
|
563
|
+
if case .success(let bundlePath) = findBundleFile(in: stableBundleDir), let path = bundlePath {
|
|
564
|
+
NSLog("[BundleStorage] Returning stable bundle URL: \(path)")
|
|
565
|
+
return URL(fileURLWithPath: path)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Fallback to app bundle
|
|
571
|
+
return getFallbackBundleURL()
|
|
364
572
|
}
|
|
365
573
|
|
|
366
574
|
// MARK: - Bundle Update
|
|
@@ -374,9 +582,17 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
374
582
|
* @param completion Callback with result of the operation
|
|
375
583
|
*/
|
|
376
584
|
func updateBundle(bundleId: String, fileUrl: URL?, fileHash: String?, progressHandler: @escaping (Double) -> Void, completion: @escaping (Result<Bool, Error>) -> Void) {
|
|
585
|
+
// Check if bundle is in crashed history
|
|
586
|
+
let crashedHistory = loadCrashedHistory()
|
|
587
|
+
if crashedHistory.contains(bundleId) {
|
|
588
|
+
NSLog("[BundleStorage] Bundle '\(bundleId)' is in crashed history, rejecting update")
|
|
589
|
+
completion(.failure(BundleStorageError.bundleInCrashedHistory(bundleId)))
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
|
|
377
593
|
// Get the current bundle ID from the cached bundle URL (exclude fallback bundles)
|
|
378
594
|
let currentBundleId = self.getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
|
|
379
|
-
|
|
595
|
+
|
|
380
596
|
guard let validFileUrl = fileUrl else {
|
|
381
597
|
NSLog("[BundleStorage] fileUrl is nil, resetting bundle URL.")
|
|
382
598
|
// Dispatch the sequence to the file operation queue to ensure completion is called asynchronously
|
|
@@ -403,7 +619,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
403
619
|
|
|
404
620
|
// Start the bundle update process on a background queue
|
|
405
621
|
fileOperationQueue.async {
|
|
406
|
-
|
|
622
|
+
|
|
407
623
|
let storeDirResult = self.bundleStoreDir()
|
|
408
624
|
guard case .success(let storeDir) = storeDirResult else {
|
|
409
625
|
completion(.failure(storeDirResult.failureError ?? BundleStorageError.unknown(nil)))
|
|
@@ -418,22 +634,28 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
418
634
|
case .success(let existingBundlePath):
|
|
419
635
|
if let bundlePath = existingBundlePath {
|
|
420
636
|
NSLog("[BundleStorage] Using cached bundle at path: \(bundlePath)")
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
637
|
+
let setResult = self.setBundleURL(localPath: bundlePath)
|
|
638
|
+
switch setResult {
|
|
639
|
+
case .success:
|
|
640
|
+
// Set staging metadata for rollback support
|
|
641
|
+
var metadata = self.loadMetadataOrNull() ?? BundleMetadata()
|
|
642
|
+
metadata.stagingBundleId = bundleId
|
|
643
|
+
metadata.verificationPending = true
|
|
644
|
+
metadata.verificationAttemptedAt = nil
|
|
645
|
+
metadata.stagingExecutionCount = 0
|
|
646
|
+
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
647
|
+
let _ = self.saveMetadata(metadata)
|
|
648
|
+
NSLog("[BundleStorage] Set staging bundle (cached): \(bundleId), verificationPending: true")
|
|
649
|
+
|
|
650
|
+
// Clean up old bundles, preserving stable and new staging
|
|
651
|
+
let stableId = metadata.stableBundleId
|
|
652
|
+
let bundleIdsToKeep = [stableId, bundleId].compactMap { $0 }
|
|
653
|
+
if bundleIdsToKeep.count > 0 {
|
|
654
|
+
let _ = self.cleanupOldBundles(currentBundleId: bundleIdsToKeep.first, bundleId: bundleIdsToKeep.count > 1 ? bundleIdsToKeep[1] : nil)
|
|
435
655
|
}
|
|
436
|
-
|
|
656
|
+
|
|
657
|
+
completion(.success(true))
|
|
658
|
+
case .failure(let error):
|
|
437
659
|
completion(.failure(error))
|
|
438
660
|
}
|
|
439
661
|
return
|
|
@@ -445,7 +667,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
445
667
|
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, fileHash: fileHash, storeDir: storeDir, progressHandler: progressHandler, completion: completion)
|
|
446
668
|
} catch let error {
|
|
447
669
|
NSLog("[BundleStorage] Failed to remove invalid bundle dir: \(error.localizedDescription)")
|
|
448
|
-
completion(.failure(BundleStorageError.
|
|
670
|
+
completion(.failure(BundleStorageError.unknown(error)))
|
|
449
671
|
}
|
|
450
672
|
}
|
|
451
673
|
case .failure(let error):
|
|
@@ -558,7 +780,14 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
558
780
|
case .failure(let error):
|
|
559
781
|
NSLog("[BundleStorage] Download failed: \(error.localizedDescription)")
|
|
560
782
|
self.cleanupTemporaryFiles([tempDirectory]) // Sync cleanup
|
|
561
|
-
|
|
783
|
+
|
|
784
|
+
// Map DownloadError.incompleteDownload to BundleStorageError.incompleteDownload
|
|
785
|
+
if let downloadError = error as? DownloadError,
|
|
786
|
+
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
787
|
+
completion(.failure(BundleStorageError.incompleteDownload(expected: expected, actual: actual)))
|
|
788
|
+
} else {
|
|
789
|
+
completion(.failure(BundleStorageError.downloadFailed(error)))
|
|
790
|
+
}
|
|
562
791
|
}
|
|
563
792
|
}
|
|
564
793
|
self.fileOperationQueue.async(execute: workItem)
|
|
@@ -628,10 +857,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
628
857
|
guard self.fileSystem.fileExists(atPath: location.path) else {
|
|
629
858
|
logFileSystemDiagnostics(path: location.path, context: "Download Location Missing")
|
|
630
859
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
631
|
-
completion(.failure(BundleStorageError.
|
|
860
|
+
completion(.failure(BundleStorageError.downloadFailed(NSError(
|
|
632
861
|
domain: "HotUpdaterError",
|
|
633
862
|
code: 1,
|
|
634
|
-
userInfo: [NSLocalizedDescriptionKey: "
|
|
863
|
+
userInfo: [NSLocalizedDescriptionKey: "Downloaded file does not exist atPath: \(location.path)"]
|
|
635
864
|
))))
|
|
636
865
|
return
|
|
637
866
|
}
|
|
@@ -683,7 +912,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
683
912
|
logFileSystemDiagnostics(path: tmpDir, context: "Extraction Failed")
|
|
684
913
|
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
685
914
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
686
|
-
completion(.failure(BundleStorageError.
|
|
915
|
+
completion(.failure(BundleStorageError.extractionFormatError(error)))
|
|
687
916
|
return
|
|
688
917
|
}
|
|
689
918
|
|
|
@@ -719,18 +948,33 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
719
948
|
// 11) Construct final bundlePath for preferences
|
|
720
949
|
let finalBundlePath = (realDir as NSString).appendingPathComponent((bundlePathInTmp as NSString).lastPathComponent)
|
|
721
950
|
|
|
722
|
-
// 12) Set the bundle URL in preferences
|
|
951
|
+
// 12) Set the bundle URL in preferences (for backwards compatibility)
|
|
723
952
|
let setResult = self.setBundleURL(localPath: finalBundlePath)
|
|
724
953
|
switch setResult {
|
|
725
954
|
case .success:
|
|
726
955
|
NSLog("[BundleStorage] Successfully set bundle URL: \(finalBundlePath)")
|
|
727
|
-
|
|
956
|
+
|
|
957
|
+
// 13) Set staging metadata for rollback support
|
|
958
|
+
var metadata = self.loadMetadataOrNull() ?? BundleMetadata()
|
|
959
|
+
metadata.stagingBundleId = bundleId
|
|
960
|
+
metadata.verificationPending = true
|
|
961
|
+
metadata.verificationAttemptedAt = nil
|
|
962
|
+
metadata.stagingExecutionCount = 0
|
|
963
|
+
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
964
|
+
let _ = self.saveMetadata(metadata)
|
|
965
|
+
NSLog("[BundleStorage] Set staging bundle: \(bundleId), verificationPending: true")
|
|
966
|
+
|
|
967
|
+
// 14) Clean up the temporary directory
|
|
728
968
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
729
969
|
|
|
730
|
-
//
|
|
731
|
-
let
|
|
970
|
+
// 15) Clean up old bundles, preserving current, stable, and new staging
|
|
971
|
+
let stableId = metadata.stableBundleId
|
|
972
|
+
let bundleIdsToKeep = [stableId, bundleId].compactMap { $0 }
|
|
973
|
+
if bundleIdsToKeep.count > 0 {
|
|
974
|
+
let _ = self.cleanupOldBundles(currentBundleId: bundleIdsToKeep.first, bundleId: bundleIdsToKeep.count > 1 ? bundleIdsToKeep[1] : nil)
|
|
975
|
+
}
|
|
732
976
|
|
|
733
|
-
//
|
|
977
|
+
// 16) Complete with success
|
|
734
978
|
completion(.success(true))
|
|
735
979
|
case .failure(let err):
|
|
736
980
|
let nsError = err as NSError
|
|
@@ -763,9 +1007,89 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
763
1007
|
logFileSystemDiagnostics(path: tmpDir, context: "Processing Error")
|
|
764
1008
|
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
765
1009
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
766
|
-
|
|
1010
|
+
|
|
1011
|
+
// Re-throw specific BundleStorageError if it is one, otherwise wrap as unknown
|
|
1012
|
+
if let bundleError = error as? BundleStorageError {
|
|
1013
|
+
completion(.failure(bundleError))
|
|
1014
|
+
} else {
|
|
1015
|
+
completion(.failure(BundleStorageError.unknown(error)))
|
|
1016
|
+
}
|
|
767
1017
|
}
|
|
768
1018
|
}
|
|
1019
|
+
|
|
1020
|
+
// MARK: - Rollback Support
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Notifies the system that the app has successfully started with the given bundle.
|
|
1024
|
+
* If the bundle matches the staging bundle, promotes it to stable.
|
|
1025
|
+
* @param bundleId The ID of the currently running bundle
|
|
1026
|
+
* @return true if promotion was successful or no action was needed
|
|
1027
|
+
*/
|
|
1028
|
+
func notifyAppReady(bundleId: String) -> [String: Any] {
|
|
1029
|
+
NSLog("[BundleStorage('\(id)'] notifyAppReady: Called with bundleId '\(bundleId)'")
|
|
1030
|
+
guard var metadata = loadMetadataOrNull() else {
|
|
1031
|
+
// No metadata exists - legacy mode, nothing to do
|
|
1032
|
+
NSLog("[BundleStorage] notifyAppReady: No metadata exists (legacy mode)")
|
|
1033
|
+
return ["status": "STABLE"]
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Check if there was a recent rollback (session variable)
|
|
1037
|
+
if let crashedBundleId = self.sessionRollbackBundleId {
|
|
1038
|
+
NSLog("[BundleStorage] notifyAppReady: Detected rollback recovery from '\(crashedBundleId)'")
|
|
1039
|
+
|
|
1040
|
+
// Clear rollback info (one-time read)
|
|
1041
|
+
self.sessionRollbackBundleId = nil
|
|
1042
|
+
|
|
1043
|
+
return [
|
|
1044
|
+
"status": "RECOVERED",
|
|
1045
|
+
"crashedBundleId": crashedBundleId
|
|
1046
|
+
]
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Check if the bundle matches the staging bundle (promotion case)
|
|
1050
|
+
if let stagingId = metadata.stagingBundleId, stagingId == bundleId, metadata.verificationPending {
|
|
1051
|
+
NSLog("[BundleStorage] notifyAppReady: Bundle '\(bundleId)' matches staging, promoting to stable")
|
|
1052
|
+
promoteStagingToStable()
|
|
1053
|
+
return ["status": "PROMOTED"]
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Check if the bundle matches the stable bundle
|
|
1057
|
+
if let stableId = metadata.stableBundleId, stableId == bundleId {
|
|
1058
|
+
// Already stable, clear any pending verification state
|
|
1059
|
+
if metadata.verificationPending {
|
|
1060
|
+
metadata.verificationPending = false
|
|
1061
|
+
metadata.verificationAttemptedAt = nil
|
|
1062
|
+
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
1063
|
+
let _ = saveMetadata(metadata)
|
|
1064
|
+
NSLog("[BundleStorage] notifyAppReady: Bundle '\(bundleId)' is stable, cleared pending verification")
|
|
1065
|
+
} else {
|
|
1066
|
+
NSLog("[BundleStorage] notifyAppReady: Bundle '\(bundleId)' is already stable")
|
|
1067
|
+
}
|
|
1068
|
+
return ["status": "STABLE"]
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Bundle doesn't match staging or stable - might be fallback or unknown
|
|
1072
|
+
NSLog("[BundleStorage] notifyAppReady: Bundle '\(bundleId)' doesn't match staging or stable")
|
|
1073
|
+
return ["status": "STABLE"]
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Returns the crashed bundle history.
|
|
1078
|
+
* @return The crashed history object
|
|
1079
|
+
*/
|
|
1080
|
+
func getCrashHistory() -> CrashedHistory {
|
|
1081
|
+
return loadCrashedHistory()
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Clears the crashed bundle history.
|
|
1086
|
+
* @return true if clearing was successful
|
|
1087
|
+
*/
|
|
1088
|
+
func clearCrashHistory() -> Bool {
|
|
1089
|
+
var history = loadCrashedHistory()
|
|
1090
|
+
history.clear()
|
|
1091
|
+
return saveCrashedHistory(history)
|
|
1092
|
+
}
|
|
769
1093
|
}
|
|
770
1094
|
|
|
771
1095
|
// Helper to get the associated error from a Result, if it's a failure
|