@hot-updater/react-native 0.27.1 → 0.29.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/build.gradle +12 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +3 -0
- package/android/src/main/cpp/CMakeLists.txt +9 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +143 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +325 -210
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +73 -16
- package/android/src/main/java/com/hotupdater/CohortService.kt +73 -0
- package/android/src/main/java/com/hotupdater/DecompressService.kt +28 -22
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +1 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +51 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +533 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryReceiver.kt +14 -0
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +55 -0
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +19 -7
- package/android/src/newarch/HotUpdaterModule.kt +16 -25
- package/android/src/oldarch/HotUpdaterModule.kt +20 -26
- package/android/src/oldarch/HotUpdaterSpec.kt +12 -2
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +340 -232
- package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -8
- package/ios/HotUpdater/Internal/CohortService.swift +63 -0
- package/ios/HotUpdater/Internal/DecompressService.swift +53 -30
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +9 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +376 -70
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.h +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.mm +4 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +321 -9
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +24 -8
- package/lib/commonjs/DefaultResolver.js +3 -5
- package/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/checkForUpdate.js +2 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +211 -39
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +443 -0
- package/lib/commonjs/native.spec.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/wrap.js +4 -5
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/DefaultResolver.js +3 -5
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/checkForUpdate.js +3 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +14 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +204 -34
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +442 -0
- package/lib/module/native.spec.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/wrap.js +5 -6
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +14 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +43 -23
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +32 -8
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +6 -3
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +3 -6
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +14 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +43 -23
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +32 -8
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +6 -3
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +3 -6
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/DefaultResolver.ts +4 -4
- package/src/checkForUpdate.ts +4 -0
- package/src/index.ts +21 -0
- package/src/native.spec.ts +480 -0
- package/src/native.ts +285 -39
- package/src/specs/NativeHotUpdater.ts +36 -6
- package/src/types.ts +7 -3
- package/src/wrap.tsx +8 -12
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
|
+
public typealias ManifestAssets = [String: Any]
|
|
4
|
+
|
|
3
5
|
public enum BundleStorageError: Error, CustomNSError {
|
|
4
6
|
case directoryCreationFailed
|
|
5
7
|
case downloadFailed(Error)
|
|
@@ -54,9 +56,9 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
54
56
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The download was interrupted. Check network connection and try again"
|
|
55
57
|
|
|
56
58
|
case .extractionFormatError(let underlyingError):
|
|
57
|
-
userInfo[NSLocalizedDescriptionKey] = "
|
|
59
|
+
userInfo[NSLocalizedDescriptionKey] = "The downloaded bundle file is not a valid compressed archive"
|
|
58
60
|
userInfo[NSUnderlyingErrorKey] = underlyingError
|
|
59
|
-
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The
|
|
61
|
+
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The downloaded file is not a supported bundle archive. Try downloading again"
|
|
60
62
|
|
|
61
63
|
case .invalidBundle:
|
|
62
64
|
userInfo[NSLocalizedDescriptionKey] = "Bundle missing required platform files (index.ios.bundle or main.jsbundle)"
|
|
@@ -103,13 +105,14 @@ public protocol BundleStorageService {
|
|
|
103
105
|
func setBundleURL(localPath: String?) -> Result<Void, Error>
|
|
104
106
|
func getCachedBundleURL() -> URL?
|
|
105
107
|
func getFallbackBundleURL(bundle: Bundle) -> URL? // Synchronous as it's lightweight
|
|
106
|
-
func
|
|
108
|
+
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection
|
|
107
109
|
|
|
108
110
|
// Bundle update
|
|
109
111
|
func updateBundle(bundleId: String, fileUrl: URL?, fileHash: String?, progressHandler: @escaping (Double) -> Void, completion: @escaping (Result<Bool, Error>) -> Void)
|
|
110
112
|
|
|
111
113
|
// Rollback support
|
|
112
|
-
func
|
|
114
|
+
func markLaunchCompleted(bundleId: String?)
|
|
115
|
+
func notifyAppReady() -> [String: Any]
|
|
113
116
|
func getCrashHistory() -> CrashedHistory
|
|
114
117
|
func clearCrashHistory() -> Bool
|
|
115
118
|
|
|
@@ -119,6 +122,18 @@ public protocol BundleStorageService {
|
|
|
119
122
|
*/
|
|
120
123
|
func getBaseURL() -> String
|
|
121
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Gets the current active bundle ID from bundle storage.
|
|
127
|
+
* Reads manifest.json first and falls back to older metadata when needed.
|
|
128
|
+
*/
|
|
129
|
+
func getBundleId() -> String?
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Gets the current manifest from bundle storage.
|
|
133
|
+
* Returns an empty object when manifest.json is missing or invalid.
|
|
134
|
+
*/
|
|
135
|
+
func getManifest() -> ManifestAssets
|
|
136
|
+
|
|
122
137
|
/**
|
|
123
138
|
* Restores the original bundle and clears downloaded bundle state.
|
|
124
139
|
*/
|
|
@@ -126,6 +141,12 @@ public protocol BundleStorageService {
|
|
|
126
141
|
}
|
|
127
142
|
|
|
128
143
|
class BundleFileStorageService: BundleStorageService {
|
|
144
|
+
private struct ActiveBundleMetadataSnapshot {
|
|
145
|
+
let activeBundleId: String
|
|
146
|
+
let bundleId: String?
|
|
147
|
+
let manifest: ManifestAssets
|
|
148
|
+
}
|
|
149
|
+
|
|
129
150
|
private let fileSystem: FileSystemService
|
|
130
151
|
private let downloadService: DownloadService
|
|
131
152
|
private let decompressService: DecompressService
|
|
@@ -139,8 +160,9 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
139
160
|
|
|
140
161
|
private var activeTasks: [URLSessionTask] = []
|
|
141
162
|
|
|
142
|
-
|
|
143
|
-
private
|
|
163
|
+
private var currentLaunchReport: LaunchReport?
|
|
164
|
+
private let activeBundleMetadataLock = NSLock()
|
|
165
|
+
private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot?
|
|
144
166
|
|
|
145
167
|
public init(fileSystem: FileSystemService,
|
|
146
168
|
downloadService: DownloadService,
|
|
@@ -182,6 +204,13 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
182
204
|
return URL(fileURLWithPath: storeDir).appendingPathComponent(CrashedHistory.crashedHistoryFilename)
|
|
183
205
|
}
|
|
184
206
|
|
|
207
|
+
private func launchReportFileURL() -> URL? {
|
|
208
|
+
guard case .success(let storeDir) = bundleStoreDir() else {
|
|
209
|
+
return nil
|
|
210
|
+
}
|
|
211
|
+
return URL(fileURLWithPath: storeDir).appendingPathComponent(LaunchReport.launchReportFilename)
|
|
212
|
+
}
|
|
213
|
+
|
|
185
214
|
// MARK: - Metadata Operations
|
|
186
215
|
|
|
187
216
|
private func loadMetadataOrNull() -> BundleMetadata? {
|
|
@@ -200,6 +229,170 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
200
229
|
return updatedMetadata.save(to: file)
|
|
201
230
|
}
|
|
202
231
|
|
|
232
|
+
private func loadLaunchReport() -> LaunchReport? {
|
|
233
|
+
if let currentLaunchReport {
|
|
234
|
+
return currentLaunchReport
|
|
235
|
+
}
|
|
236
|
+
guard let file = launchReportFileURL(),
|
|
237
|
+
let report = LaunchReport.load(from: file) else {
|
|
238
|
+
return nil
|
|
239
|
+
}
|
|
240
|
+
currentLaunchReport = report
|
|
241
|
+
return report
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private func saveLaunchReport(_ report: LaunchReport?) {
|
|
245
|
+
currentLaunchReport = report
|
|
246
|
+
guard let file = launchReportFileURL() else {
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
guard let report else {
|
|
251
|
+
if FileManager.default.fileExists(atPath: file.path) {
|
|
252
|
+
try? FileManager.default.removeItem(at: file)
|
|
253
|
+
}
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
_ = report.save(to: file)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private func createInitialMetadata() -> BundleMetadata {
|
|
261
|
+
let currentBundleId = getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
|
|
262
|
+
return BundleMetadata(
|
|
263
|
+
isolationKey: isolationKey,
|
|
264
|
+
stableBundleId: nil,
|
|
265
|
+
stagingBundleId: currentBundleId,
|
|
266
|
+
verificationPending: false
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private func getCurrentVerifiedBundleId(_ metadata: BundleMetadata) -> String? {
|
|
271
|
+
if let stagingBundleId = metadata.stagingBundleId, !metadata.verificationPending {
|
|
272
|
+
return stagingBundleId
|
|
273
|
+
}
|
|
274
|
+
return metadata.stableBundleId
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private func getActiveBundleId() -> String? {
|
|
278
|
+
let metadata = loadMetadataOrNull()
|
|
279
|
+
|
|
280
|
+
if let stagingBundleId = metadata?.stagingBundleId {
|
|
281
|
+
return stagingBundleId
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if let stableBundleId = metadata?.stableBundleId {
|
|
285
|
+
return stableBundleId
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private func withActiveBundleMetadataLock<T>(_ body: () -> T) -> T {
|
|
292
|
+
activeBundleMetadataLock.lock()
|
|
293
|
+
defer { activeBundleMetadataLock.unlock() }
|
|
294
|
+
return body()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private func clearActiveBundleMetadataSnapshot() {
|
|
298
|
+
withActiveBundleMetadataLock {
|
|
299
|
+
activeBundleMetadataSnapshot = nil
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private func getActiveBundleMetadataSnapshot() -> ActiveBundleMetadataSnapshot? {
|
|
304
|
+
guard let activeBundleId = getActiveBundleId(),
|
|
305
|
+
case .success(let storeDir) = bundleStoreDir() else {
|
|
306
|
+
clearActiveBundleMetadataSnapshot()
|
|
307
|
+
return nil
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if let snapshot = withActiveBundleMetadataLock({
|
|
311
|
+
activeBundleMetadataSnapshot?.activeBundleId == activeBundleId
|
|
312
|
+
? activeBundleMetadataSnapshot
|
|
313
|
+
: nil
|
|
314
|
+
}) {
|
|
315
|
+
return snapshot
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let bundleDir = (storeDir as NSString).appendingPathComponent(activeBundleId)
|
|
319
|
+
guard fileSystem.fileExists(atPath: bundleDir) else {
|
|
320
|
+
clearActiveBundleMetadataSnapshot()
|
|
321
|
+
return nil
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let snapshot = resolveActiveBundleMetadataSnapshot(
|
|
325
|
+
activeBundleId: activeBundleId,
|
|
326
|
+
bundleDirectory: bundleDir
|
|
327
|
+
)
|
|
328
|
+
return withActiveBundleMetadataLock {
|
|
329
|
+
activeBundleMetadataSnapshot = snapshot
|
|
330
|
+
return snapshot
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private func resolveActiveBundleMetadataSnapshot(
|
|
335
|
+
activeBundleId: String,
|
|
336
|
+
bundleDirectory: String
|
|
337
|
+
) -> ActiveBundleMetadataSnapshot {
|
|
338
|
+
let manifest = readManifest(in: bundleDirectory) ?? [:]
|
|
339
|
+
let manifestBundleId =
|
|
340
|
+
(manifest["bundleId"] as? String)?
|
|
341
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
342
|
+
let resolvedBundleId =
|
|
343
|
+
(manifestBundleId?.isEmpty == false ? manifestBundleId : nil) ??
|
|
344
|
+
readCompatibilityBundleId(in: bundleDirectory)
|
|
345
|
+
|
|
346
|
+
return ActiveBundleMetadataSnapshot(
|
|
347
|
+
activeBundleId: activeBundleId,
|
|
348
|
+
bundleId: resolvedBundleId,
|
|
349
|
+
manifest: manifest
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private func readCompatibilityBundleId(in bundleDirectory: String) -> String? {
|
|
354
|
+
let compatibilityBundleIdPath = (bundleDirectory as NSString)
|
|
355
|
+
.appendingPathComponent(compatibilityBundleIdFilename())
|
|
356
|
+
if fileSystem.fileExists(atPath: compatibilityBundleIdPath) {
|
|
357
|
+
do {
|
|
358
|
+
let compatibilityBundleId = try String(
|
|
359
|
+
contentsOfFile: compatibilityBundleIdPath,
|
|
360
|
+
encoding: .utf8
|
|
361
|
+
)
|
|
362
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
363
|
+
|
|
364
|
+
if !compatibilityBundleId.isEmpty {
|
|
365
|
+
return compatibilityBundleId
|
|
366
|
+
}
|
|
367
|
+
} catch {
|
|
368
|
+
NSLog(
|
|
369
|
+
"[BundleStorage] Failed to read compatibility bundle metadata at \(compatibilityBundleIdPath): \(error.localizedDescription)"
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return nil
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private func compatibilityBundleIdFilename() -> String {
|
|
378
|
+
"BUNDLE_ID"
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private func readManifest(in bundleDirectory: String) -> [String: Any]? {
|
|
382
|
+
let manifestPath = (bundleDirectory as NSString).appendingPathComponent("manifest.json")
|
|
383
|
+
guard fileSystem.fileExists(atPath: manifestPath) else {
|
|
384
|
+
return nil
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
do {
|
|
388
|
+
let manifestData = try Data(contentsOf: URL(fileURLWithPath: manifestPath))
|
|
389
|
+
return try JSONSerialization.jsonObject(with: manifestData) as? [String: Any]
|
|
390
|
+
} catch {
|
|
391
|
+
NSLog("[BundleStorage] Failed to read manifest at \(manifestPath): \(error.localizedDescription)")
|
|
392
|
+
return nil
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
203
396
|
/**
|
|
204
397
|
* Checks if isolationKey has changed and cleans up old bundles if needed.
|
|
205
398
|
* This handles migration when isolationKey format changes.
|
|
@@ -289,100 +482,78 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
289
482
|
return metadata.verificationPending && metadata.stagingBundleId != nil
|
|
290
483
|
}
|
|
291
484
|
|
|
292
|
-
private func
|
|
293
|
-
|
|
485
|
+
private func prepareMetadataForNewStagingBundle(_ metadata: BundleMetadata, bundleId: String) -> BundleMetadata {
|
|
486
|
+
let currentVerifiedBundleId = getCurrentVerifiedBundleId(metadata).flatMap { $0 == bundleId ? nil : $0 }
|
|
487
|
+
return BundleMetadata(
|
|
488
|
+
isolationKey: isolationKey,
|
|
489
|
+
stableBundleId: currentVerifiedBundleId,
|
|
490
|
+
stagingBundleId: bundleId,
|
|
491
|
+
verificationPending: true,
|
|
492
|
+
updatedAt: Date().timeIntervalSince1970 * 1000
|
|
493
|
+
)
|
|
294
494
|
}
|
|
295
495
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
496
|
+
@discardableResult
|
|
497
|
+
private func rollbackPendingBundle(_ stagingId: String) -> Bool {
|
|
498
|
+
guard var metadata = loadMetadataOrNull(), metadata.stagingBundleId == stagingId else {
|
|
499
|
+
return false
|
|
299
500
|
}
|
|
300
|
-
metadata.verificationAttemptedAt = Date().timeIntervalSince1970 * 1000
|
|
301
|
-
let _ = saveMetadata(metadata)
|
|
302
|
-
NSLog("[BundleStorage] Marked verification attempted for staging bundle: \(metadata.stagingBundleId ?? "nil")")
|
|
303
|
-
}
|
|
304
501
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
metadata.stagingExecutionCount = (metadata.stagingExecutionCount ?? 0) + 1
|
|
310
|
-
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
311
|
-
let _ = saveMetadata(metadata)
|
|
312
|
-
NSLog("[BundleStorage] Incremented staging execution count to: \(metadata.stagingExecutionCount ?? 0)")
|
|
313
|
-
}
|
|
502
|
+
var crashedHistory = loadCrashedHistory()
|
|
503
|
+
crashedHistory.addEntry(stagingId)
|
|
504
|
+
let _ = saveCrashedHistory(crashedHistory)
|
|
314
505
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
506
|
+
let fallbackBundleId = metadata.stableBundleId.flatMap { candidate in
|
|
507
|
+
if case .success(let storeDir) = bundleStoreDir() {
|
|
508
|
+
let stableBundleDir = (storeDir as NSString).appendingPathComponent(candidate)
|
|
509
|
+
if case .success(let bundlePath) = findBundleFile(in: stableBundleDir), bundlePath != nil {
|
|
510
|
+
return candidate
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return nil
|
|
322
514
|
}
|
|
323
515
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
516
|
+
metadata = BundleMetadata(
|
|
517
|
+
isolationKey: isolationKey,
|
|
518
|
+
stableBundleId: nil,
|
|
519
|
+
stagingBundleId: fallbackBundleId,
|
|
520
|
+
verificationPending: false,
|
|
521
|
+
updatedAt: Date().timeIntervalSince1970 * 1000
|
|
522
|
+
)
|
|
331
523
|
|
|
332
|
-
|
|
333
|
-
|
|
524
|
+
guard saveMetadata(metadata) else {
|
|
525
|
+
return false
|
|
526
|
+
}
|
|
334
527
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
528
|
+
if let fallbackBundleId,
|
|
529
|
+
case .success(let storeDir) = bundleStoreDir() {
|
|
530
|
+
let fallbackBundleDir = (storeDir as NSString).appendingPathComponent(fallbackBundleId)
|
|
531
|
+
if case .success(let bundlePath) = findBundleFile(in: fallbackBundleDir), let bundlePath {
|
|
532
|
+
let _ = setBundleURL(localPath: bundlePath)
|
|
338
533
|
}
|
|
534
|
+
} else {
|
|
535
|
+
let _ = setBundleURL(localPath: nil)
|
|
339
536
|
}
|
|
340
|
-
}
|
|
341
537
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
538
|
+
if case .success(let storeDir) = bundleStoreDir() {
|
|
539
|
+
let stagingDir = (storeDir as NSString).appendingPathComponent(stagingId)
|
|
540
|
+
try? fileSystem.removeItem(atPath: stagingDir)
|
|
345
541
|
}
|
|
346
|
-
guard let stagingId = metadata.stagingBundleId else {
|
|
347
|
-
NSLog("[BundleStorage] No staging bundle to rollback from")
|
|
348
|
-
return
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Add crashed bundle to history
|
|
352
|
-
var crashedHistory = loadCrashedHistory()
|
|
353
|
-
crashedHistory.addEntry(stagingId)
|
|
354
|
-
let _ = saveCrashedHistory(crashedHistory)
|
|
355
|
-
NSLog("[BundleStorage('\(id)')] Added bundle '\(stagingId)' to crashed history")
|
|
356
542
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// Clear staging
|
|
361
|
-
metadata.stagingBundleId = nil
|
|
362
|
-
metadata.verificationPending = false
|
|
363
|
-
metadata.verificationAttemptedAt = nil
|
|
364
|
-
metadata.stagingExecutionCount = nil
|
|
365
|
-
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
366
|
-
|
|
367
|
-
if saveMetadata(metadata) {
|
|
368
|
-
NSLog("[BundleStorage] Rolled back to stable bundle: \(metadata.stableBundleId ?? "fallback")")
|
|
369
|
-
|
|
370
|
-
// Update HotUpdaterBundleURL to point to stable bundle
|
|
371
|
-
if let stableId = metadata.stableBundleId {
|
|
372
|
-
if case .success(let storeDir) = bundleStoreDir() {
|
|
373
|
-
let stableBundleDir = (storeDir as NSString).appendingPathComponent(stableId)
|
|
374
|
-
if case .success(let bundlePath) = findBundleFile(in: stableBundleDir), let path = bundlePath {
|
|
375
|
-
let _ = setBundleURL(localPath: path)
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
} else {
|
|
379
|
-
// Reset to fallback
|
|
380
|
-
let _ = setBundleURL(localPath: nil)
|
|
381
|
-
}
|
|
543
|
+
saveLaunchReport(LaunchReport(status: "RECOVERED", crashedBundleId: stagingId))
|
|
544
|
+
return true
|
|
545
|
+
}
|
|
382
546
|
|
|
383
|
-
|
|
384
|
-
|
|
547
|
+
private func applyPendingRecoveryIfNeeded(_ pendingRecovery: PendingCrashRecovery?) {
|
|
548
|
+
guard let metadata = loadMetadataOrNull(),
|
|
549
|
+
let stagingBundleId = metadata.stagingBundleId,
|
|
550
|
+
pendingRecovery?.shouldRollback == true,
|
|
551
|
+
pendingRecovery?.launchedBundleId == stagingBundleId,
|
|
552
|
+
isVerificationPending(metadata) else {
|
|
553
|
+
return
|
|
385
554
|
}
|
|
555
|
+
|
|
556
|
+
_ = rollbackPendingBundle(stagingBundleId)
|
|
386
557
|
}
|
|
387
558
|
|
|
388
559
|
// MARK: - Directory Management
|
|
@@ -568,6 +739,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
568
739
|
do {
|
|
569
740
|
NSLog("[BundleStorage] Setting bundle URL to: \(localPath ?? "nil")")
|
|
570
741
|
try self.preferences.setItem(localPath, forKey: "HotUpdaterBundleURL")
|
|
742
|
+
clearActiveBundleMetadataSnapshot()
|
|
571
743
|
return .success(())
|
|
572
744
|
} catch let error {
|
|
573
745
|
return .failure(error)
|
|
@@ -599,66 +771,55 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
599
771
|
func getFallbackBundleURL(bundle: Bundle) -> URL? {
|
|
600
772
|
return bundle.url(forResource: "main", withExtension: "jsbundle")
|
|
601
773
|
}
|
|
602
|
-
|
|
603
|
-
public func getBundleURL(bundle: Bundle) -> URL? {
|
|
604
|
-
// Try to load metadata
|
|
605
|
-
let metadata = loadMetadataOrNull()
|
|
606
774
|
|
|
607
|
-
|
|
608
|
-
guard let metadata =
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if isVerificationPending(metadata) {
|
|
615
|
-
let executionCount = metadata.stagingExecutionCount ?? 0
|
|
616
|
-
|
|
617
|
-
if executionCount == 0 {
|
|
618
|
-
// First execution - give staging bundle a chance
|
|
619
|
-
NSLog("[BundleStorage] First execution of staging bundle, incrementing counter")
|
|
620
|
-
incrementStagingExecutionCount()
|
|
621
|
-
// Don't mark verificationAttempted yet!
|
|
622
|
-
} else if wasVerificationAttempted(metadata) {
|
|
623
|
-
// Already executed once and verificationAttempted is set → crash!
|
|
624
|
-
NSLog("[BundleStorage] Crash detected: staging bundle executed but didn't call notifyAppReady")
|
|
625
|
-
rollbackToStable()
|
|
626
|
-
} else {
|
|
627
|
-
// Second execution - now mark verification attempted
|
|
628
|
-
NSLog("[BundleStorage] Second execution of staging bundle, marking verification attempted")
|
|
629
|
-
markVerificationAttempted()
|
|
630
|
-
}
|
|
775
|
+
private func selectLaunch(bundle: Bundle) -> LaunchSelection {
|
|
776
|
+
guard let metadata = loadMetadataOrNull() else {
|
|
777
|
+
return LaunchSelection(
|
|
778
|
+
bundleURL: getCachedBundleURL() ?? getFallbackBundleURL(bundle: bundle),
|
|
779
|
+
launchedBundleId: getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent,
|
|
780
|
+
shouldRollbackOnCrash: false
|
|
781
|
+
)
|
|
631
782
|
}
|
|
632
783
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
784
|
+
if let stagingId = metadata.stagingBundleId,
|
|
785
|
+
case .success(let storeDir) = bundleStoreDir() {
|
|
786
|
+
let stagingBundleDir = (storeDir as NSString).appendingPathComponent(stagingId)
|
|
787
|
+
if case .success(let bundlePath) = findBundleFile(in: stagingBundleDir), let bundlePath {
|
|
788
|
+
return LaunchSelection(
|
|
789
|
+
bundleURL: URL(fileURLWithPath: bundlePath),
|
|
790
|
+
launchedBundleId: stagingId,
|
|
791
|
+
shouldRollbackOnCrash: metadata.verificationPending
|
|
792
|
+
)
|
|
793
|
+
}
|
|
637
794
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if case .success(let storeDir) = bundleStoreDir() {
|
|
641
|
-
let stagingBundleDir = (storeDir as NSString).appendingPathComponent(stagingId)
|
|
642
|
-
if case .success(let bundlePath) = findBundleFile(in: stagingBundleDir), let path = bundlePath {
|
|
643
|
-
NSLog("[BundleStorage] Returning staging bundle URL: \(path)")
|
|
644
|
-
return URL(fileURLWithPath: path)
|
|
645
|
-
}
|
|
795
|
+
if metadata.verificationPending, rollbackPendingBundle(stagingId) {
|
|
796
|
+
return selectLaunch(bundle: bundle)
|
|
646
797
|
}
|
|
647
798
|
}
|
|
648
799
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
800
|
+
if let stableId = metadata.stableBundleId,
|
|
801
|
+
case .success(let storeDir) = bundleStoreDir() {
|
|
802
|
+
let stableBundleDir = (storeDir as NSString).appendingPathComponent(stableId)
|
|
803
|
+
if case .success(let bundlePath) = findBundleFile(in: stableBundleDir), let bundlePath {
|
|
804
|
+
return LaunchSelection(
|
|
805
|
+
bundleURL: URL(fileURLWithPath: bundlePath),
|
|
806
|
+
launchedBundleId: stableId,
|
|
807
|
+
shouldRollbackOnCrash: false
|
|
808
|
+
)
|
|
657
809
|
}
|
|
658
810
|
}
|
|
659
811
|
|
|
660
|
-
|
|
661
|
-
|
|
812
|
+
return LaunchSelection(
|
|
813
|
+
bundleURL: getFallbackBundleURL(bundle: bundle),
|
|
814
|
+
launchedBundleId: getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent,
|
|
815
|
+
shouldRollbackOnCrash: false
|
|
816
|
+
)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection {
|
|
820
|
+
saveLaunchReport(nil)
|
|
821
|
+
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
822
|
+
return selectLaunch(bundle: bundle)
|
|
662
823
|
}
|
|
663
824
|
|
|
664
825
|
// MARK: - Bundle Update
|
|
@@ -691,6 +852,8 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
691
852
|
let setResult = self.setBundleURL(localPath: nil)
|
|
692
853
|
switch setResult {
|
|
693
854
|
case .success:
|
|
855
|
+
let _ = self.saveMetadata(self.createInitialMetadata())
|
|
856
|
+
self.saveLaunchReport(nil)
|
|
694
857
|
let cleanupResult = self.cleanupOldBundles(currentBundleId: currentBundleId, bundleId: bundleId)
|
|
695
858
|
switch cleanupResult {
|
|
696
859
|
case .success:
|
|
@@ -727,19 +890,13 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
727
890
|
let setResult = self.setBundleURL(localPath: bundlePath)
|
|
728
891
|
switch setResult {
|
|
729
892
|
case .success:
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
metadata.verificationPending = true
|
|
734
|
-
metadata.verificationAttemptedAt = nil
|
|
735
|
-
metadata.stagingExecutionCount = 0
|
|
736
|
-
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
737
|
-
let _ = self.saveMetadata(metadata)
|
|
893
|
+
let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
|
|
894
|
+
let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId)
|
|
895
|
+
let _ = self.saveMetadata(updatedMetadata)
|
|
738
896
|
NSLog("[BundleStorage] Set staging bundle (cached): \(bundleId), verificationPending: true")
|
|
739
897
|
|
|
740
|
-
// Clean up old bundles, preserving
|
|
741
|
-
let
|
|
742
|
-
let bundleIdsToKeep = [stableId, bundleId].compactMap { $0 }
|
|
898
|
+
// Clean up old bundles, preserving the fallback and new staging bundle.
|
|
899
|
+
let bundleIdsToKeep = [updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
743
900
|
if bundleIdsToKeep.count > 0 {
|
|
744
901
|
let _ = self.cleanupOldBundles(currentBundleId: bundleIdsToKeep.first, bundleId: bundleIdsToKeep.count > 1 ? bundleIdsToKeep[1] : nil)
|
|
745
902
|
}
|
|
@@ -1045,21 +1202,16 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1045
1202
|
NSLog("[BundleStorage] Successfully set bundle URL: \(finalBundlePath)")
|
|
1046
1203
|
|
|
1047
1204
|
// 13) Set staging metadata for rollback support
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
metadata.verificationAttemptedAt = nil
|
|
1052
|
-
metadata.stagingExecutionCount = 0
|
|
1053
|
-
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
1054
|
-
let _ = self.saveMetadata(metadata)
|
|
1205
|
+
let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
|
|
1206
|
+
let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId)
|
|
1207
|
+
let _ = self.saveMetadata(updatedMetadata)
|
|
1055
1208
|
NSLog("[BundleStorage] Set staging bundle: \(bundleId), verificationPending: true")
|
|
1056
1209
|
|
|
1057
1210
|
// 14) Clean up the temporary directory
|
|
1058
1211
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
1059
1212
|
|
|
1060
|
-
// 15) Clean up old bundles, preserving
|
|
1061
|
-
let
|
|
1062
|
-
let bundleIdsToKeep = [stableId, bundleId].compactMap { $0 }
|
|
1213
|
+
// 15) Clean up old bundles, preserving the fallback and new staging bundle.
|
|
1214
|
+
let bundleIdsToKeep = [updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
1063
1215
|
if bundleIdsToKeep.count > 0 {
|
|
1064
1216
|
let _ = self.cleanupOldBundles(currentBundleId: bundleIdsToKeep.first, bundleId: bundleIdsToKeep.count > 1 ? bundleIdsToKeep[1] : nil)
|
|
1065
1217
|
}
|
|
@@ -1110,57 +1262,31 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1110
1262
|
// MARK: - Rollback Support
|
|
1111
1263
|
|
|
1112
1264
|
/**
|
|
1113
|
-
*
|
|
1114
|
-
* If the bundle matches the staging bundle, promotes it to stable.
|
|
1115
|
-
* @param bundleId The ID of the currently running bundle
|
|
1116
|
-
* @return true if promotion was successful or no action was needed
|
|
1265
|
+
* Marks the current launch as successful after the first content appeared.
|
|
1117
1266
|
*/
|
|
1118
|
-
func
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
return
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
// Check if there was a recent rollback (session variable)
|
|
1127
|
-
if let crashedBundleId = self.sessionRollbackBundleId {
|
|
1128
|
-
NSLog("[BundleStorage] notifyAppReady: Detected rollback recovery from '\(crashedBundleId)'")
|
|
1129
|
-
|
|
1130
|
-
// Clear rollback info (one-time read)
|
|
1131
|
-
self.sessionRollbackBundleId = nil
|
|
1132
|
-
|
|
1133
|
-
return [
|
|
1134
|
-
"status": "RECOVERED",
|
|
1135
|
-
"crashedBundleId": crashedBundleId
|
|
1136
|
-
]
|
|
1267
|
+
func markLaunchCompleted(bundleId: String?) {
|
|
1268
|
+
guard let bundleId,
|
|
1269
|
+
var metadata = loadMetadataOrNull(),
|
|
1270
|
+
metadata.verificationPending,
|
|
1271
|
+
metadata.stagingBundleId == bundleId else {
|
|
1272
|
+
return
|
|
1137
1273
|
}
|
|
1138
1274
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
return ["status": "PROMOTED"]
|
|
1144
|
-
}
|
|
1275
|
+
metadata.verificationPending = false
|
|
1276
|
+
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
1277
|
+
let _ = saveMetadata(metadata)
|
|
1278
|
+
}
|
|
1145
1279
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
// Already stable, clear any pending verification state
|
|
1149
|
-
if metadata.verificationPending {
|
|
1150
|
-
metadata.verificationPending = false
|
|
1151
|
-
metadata.verificationAttemptedAt = nil
|
|
1152
|
-
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
1153
|
-
let _ = saveMetadata(metadata)
|
|
1154
|
-
NSLog("[BundleStorage] notifyAppReady: Bundle '\(bundleId)' is stable, cleared pending verification")
|
|
1155
|
-
} else {
|
|
1156
|
-
NSLog("[BundleStorage] notifyAppReady: Bundle '\(bundleId)' is already stable")
|
|
1157
|
-
}
|
|
1280
|
+
func notifyAppReady() -> [String: Any] {
|
|
1281
|
+
guard let report = loadLaunchReport() else {
|
|
1158
1282
|
return ["status": "STABLE"]
|
|
1159
1283
|
}
|
|
1160
1284
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1285
|
+
var result: [String: Any] = ["status": report.status]
|
|
1286
|
+
if let crashedBundleId = report.crashedBundleId {
|
|
1287
|
+
result["crashedBundleId"] = crashedBundleId
|
|
1288
|
+
}
|
|
1289
|
+
return result
|
|
1164
1290
|
}
|
|
1165
1291
|
|
|
1166
1292
|
/**
|
|
@@ -1187,35 +1313,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1187
1313
|
*/
|
|
1188
1314
|
func getBaseURL() -> String {
|
|
1189
1315
|
do {
|
|
1190
|
-
let
|
|
1191
|
-
let activeBundleId: String?
|
|
1192
|
-
|
|
1193
|
-
// Prefer staging bundle if verification is pending
|
|
1194
|
-
if let meta = metadata, meta.verificationPending, let staging = meta.stagingBundleId {
|
|
1195
|
-
activeBundleId = staging
|
|
1196
|
-
} else if let stable = metadata?.stableBundleId {
|
|
1197
|
-
activeBundleId = stable
|
|
1198
|
-
} else {
|
|
1199
|
-
// Fall back to current bundle ID from preferences
|
|
1200
|
-
if let savedURL = try preferences.getItem(forKey: "HotUpdaterBundleURL") {
|
|
1201
|
-
// Extract bundle ID from path like "bundle-store/abc123/index.ios.bundle"
|
|
1202
|
-
if let range = savedURL.range(of: "bundle-store/([^/]+)/", options: .regularExpression) {
|
|
1203
|
-
let match = savedURL[range]
|
|
1204
|
-
let components = match.split(separator: "/")
|
|
1205
|
-
if components.count >= 2 {
|
|
1206
|
-
activeBundleId = String(components[1])
|
|
1207
|
-
} else {
|
|
1208
|
-
activeBundleId = nil
|
|
1209
|
-
}
|
|
1210
|
-
} else {
|
|
1211
|
-
activeBundleId = nil
|
|
1212
|
-
}
|
|
1213
|
-
} else {
|
|
1214
|
-
activeBundleId = nil
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
if let bundleId = activeBundleId {
|
|
1316
|
+
if let bundleId = getActiveBundleId() {
|
|
1219
1317
|
if case .success(let storeDir) = bundleStoreDir() {
|
|
1220
1318
|
let bundleDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
1221
1319
|
if fileSystem.fileExists(atPath: bundleDir) {
|
|
@@ -1231,6 +1329,14 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1231
1329
|
}
|
|
1232
1330
|
}
|
|
1233
1331
|
|
|
1332
|
+
func getBundleId() -> String? {
|
|
1333
|
+
return getActiveBundleMetadataSnapshot()?.bundleId
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
func getManifest() -> ManifestAssets {
|
|
1337
|
+
return getActiveBundleMetadataSnapshot()?.manifest ?? [:]
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1234
1340
|
func resetChannel() -> Result<Bool, Error> {
|
|
1235
1341
|
guard case .success = setBundleURL(localPath: nil) else {
|
|
1236
1342
|
return .failure(BundleStorageError.unknown(nil))
|
|
@@ -1240,22 +1346,24 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1240
1346
|
isolationKey: isolationKey,
|
|
1241
1347
|
stableBundleId: nil,
|
|
1242
1348
|
stagingBundleId: nil,
|
|
1243
|
-
verificationPending: false
|
|
1244
|
-
verificationAttemptedAt: nil,
|
|
1245
|
-
stagingExecutionCount: nil
|
|
1349
|
+
verificationPending: false
|
|
1246
1350
|
)
|
|
1247
1351
|
|
|
1248
1352
|
guard saveMetadata(clearedMetadata) else {
|
|
1249
1353
|
return .failure(BundleStorageError.unknown(nil))
|
|
1250
1354
|
}
|
|
1251
1355
|
|
|
1356
|
+
saveLaunchReport(nil)
|
|
1357
|
+
|
|
1252
1358
|
guard case .success(let storeDir) = bundleStoreDir() else {
|
|
1253
1359
|
return .failure(BundleStorageError.unknown(nil))
|
|
1254
1360
|
}
|
|
1255
1361
|
|
|
1256
1362
|
do {
|
|
1257
1363
|
for item in try fileSystem.contentsOfDirectory(atPath: storeDir) {
|
|
1258
|
-
if item == BundleMetadata.metadataFilename ||
|
|
1364
|
+
if item == BundleMetadata.metadataFilename ||
|
|
1365
|
+
item == CrashedHistory.crashedHistoryFilename ||
|
|
1366
|
+
item == LaunchReport.launchReportFilename {
|
|
1259
1367
|
continue
|
|
1260
1368
|
}
|
|
1261
1369
|
|