@hot-updater/react-native 0.27.1 → 0.28.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 +170 -204
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +73 -16
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +39 -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/newarch/HotUpdaterModule.kt +2 -8
- package/android/src/oldarch/HotUpdaterModule.kt +2 -8
- package/android/src/oldarch/HotUpdaterSpec.kt +1 -1
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +189 -203
- package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -8
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +9 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +265 -11
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.h +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.mm +4 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +293 -9
- package/lib/commonjs/native.js +18 -21
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +86 -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/native.js +17 -20
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +85 -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/native.d.ts +4 -15
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.spec.d.ts +2 -0
- package/lib/typescript/commonjs/native.spec.d.ts.map +1 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +4 -8
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +2 -3
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +2 -5
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +4 -15
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/native.spec.d.ts +2 -0
- package/lib/typescript/module/native.spec.d.ts.map +1 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +4 -8
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +2 -3
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +2 -5
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/native.spec.ts +84 -0
- package/src/native.ts +20 -19
- package/src/specs/NativeHotUpdater.ts +4 -6
- package/src/types.ts +2 -3
- package/src/wrap.tsx +7 -11
|
@@ -31,11 +31,11 @@ interface BundleStorageService {
|
|
|
31
31
|
fun getFallbackBundleURL(): String
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Prepares the bundle launch for the current process.
|
|
35
|
+
* Applies any pending rollback decision from the previous launch and returns
|
|
36
|
+
* the bundle that should be loaded now.
|
|
37
37
|
*/
|
|
38
|
-
fun
|
|
38
|
+
fun prepareLaunch(pendingRecovery: PendingCrashRecovery?): LaunchSelection
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Updates the bundle from the specified URL
|
|
@@ -53,11 +53,14 @@ interface BundleStorageService {
|
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
* @param currentBundleId The bundle ID that JS reports as currently loaded
|
|
58
|
-
* @return Map containing status and optional crashedBundleId
|
|
56
|
+
* Marks the current launch as successful after the first content appeared.
|
|
59
57
|
*/
|
|
60
|
-
fun
|
|
58
|
+
fun markLaunchCompleted(currentBundleId: String?)
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns the launch report for the current process.
|
|
62
|
+
*/
|
|
63
|
+
fun notifyAppReady(): Map<String, Any?>
|
|
61
64
|
|
|
62
65
|
/**
|
|
63
66
|
* Gets the crashed bundle history
|
|
@@ -107,8 +110,7 @@ class BundleFileStorageService(
|
|
|
107
110
|
checkAndCleanupIfIsolationKeyChanged()
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
private var sessionRollbackBundleId: String? = null
|
|
113
|
+
private var currentLaunchReport: LaunchReport? = null
|
|
112
114
|
|
|
113
115
|
// MARK: - Bundle Store Directory
|
|
114
116
|
|
|
@@ -121,6 +123,8 @@ class BundleFileStorageService(
|
|
|
121
123
|
|
|
122
124
|
private fun getCrashedHistoryFile(): File = File(getBundleStoreDir(), CrashedHistory.CRASHED_HISTORY_FILENAME)
|
|
123
125
|
|
|
126
|
+
private fun getLaunchReportFile(): File = File(getBundleStoreDir(), LaunchReport.LAUNCH_REPORT_FILENAME)
|
|
127
|
+
|
|
124
128
|
// MARK: - Metadata Operations
|
|
125
129
|
|
|
126
130
|
private fun loadMetadataOrNull(): BundleMetadata? = BundleMetadata.loadFromFile(getMetadataFile(), isolationKey)
|
|
@@ -130,14 +134,30 @@ class BundleFileStorageService(
|
|
|
130
134
|
return updatedMetadata.saveToFile(getMetadataFile())
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
private fun loadLaunchReport(): LaunchReport? =
|
|
138
|
+
currentLaunchReport ?: LaunchReport.loadFromFile(getLaunchReportFile())?.also {
|
|
139
|
+
currentLaunchReport = it
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private fun saveLaunchReport(report: LaunchReport?) {
|
|
143
|
+
currentLaunchReport = report
|
|
144
|
+
val file = getLaunchReportFile()
|
|
145
|
+
if (report == null) {
|
|
146
|
+
if (file.exists()) {
|
|
147
|
+
file.delete()
|
|
148
|
+
}
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
report.saveToFile(file)
|
|
152
|
+
}
|
|
153
|
+
|
|
133
154
|
private fun createInitialMetadata(): BundleMetadata {
|
|
134
155
|
val currentBundleId = extractBundleIdFromCurrentURL()
|
|
135
|
-
Log.d(TAG, "Creating initial metadata with
|
|
156
|
+
Log.d(TAG, "Creating initial metadata with stagingBundleId: $currentBundleId")
|
|
136
157
|
return BundleMetadata(
|
|
137
|
-
stableBundleId =
|
|
138
|
-
stagingBundleId =
|
|
158
|
+
stableBundleId = null,
|
|
159
|
+
stagingBundleId = currentBundleId,
|
|
139
160
|
verificationPending = false,
|
|
140
|
-
verificationAttemptedAt = null,
|
|
141
161
|
)
|
|
142
162
|
}
|
|
143
163
|
|
|
@@ -148,6 +168,20 @@ class BundleFileStorageService(
|
|
|
148
168
|
return regex.find(currentUrl)?.groupValues?.get(1)
|
|
149
169
|
}
|
|
150
170
|
|
|
171
|
+
private fun findBundleFile(bundleId: String): File? {
|
|
172
|
+
val bundleDir = File(getBundleStoreDir(), bundleId)
|
|
173
|
+
return bundleDir.walk().find { it.name == "index.android.bundle" && it.exists() }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private fun getBundleUrlForId(bundleId: String): String? = findBundleFile(bundleId)?.absolutePath
|
|
177
|
+
|
|
178
|
+
private fun getCurrentVerifiedBundleId(metadata: BundleMetadata): String? =
|
|
179
|
+
when {
|
|
180
|
+
metadata.stagingBundleId != null && !metadata.verificationPending -> metadata.stagingBundleId
|
|
181
|
+
metadata.stableBundleId != null -> metadata.stableBundleId
|
|
182
|
+
else -> null
|
|
183
|
+
}
|
|
184
|
+
|
|
151
185
|
/**
|
|
152
186
|
* Checks if isolationKey has changed and cleans up old bundles if needed.
|
|
153
187
|
* This handles migration when isolationKey format changes.
|
|
@@ -213,95 +247,110 @@ class BundleFileStorageService(
|
|
|
213
247
|
|
|
214
248
|
private fun isVerificationPending(metadata: BundleMetadata): Boolean = metadata.verificationPending && metadata.stagingBundleId != null
|
|
215
249
|
|
|
216
|
-
private fun
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
val
|
|
221
|
-
metadata
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
250
|
+
private fun prepareMetadataForNewStagingBundle(
|
|
251
|
+
metadata: BundleMetadata,
|
|
252
|
+
bundleId: String,
|
|
253
|
+
): BundleMetadata {
|
|
254
|
+
val currentVerifiedBundleId =
|
|
255
|
+
getCurrentVerifiedBundleId(metadata)?.takeIf { it != bundleId }
|
|
256
|
+
|
|
257
|
+
return metadata.copy(
|
|
258
|
+
stableBundleId = currentVerifiedBundleId,
|
|
259
|
+
stagingBundleId = bundleId,
|
|
260
|
+
verificationPending = true,
|
|
261
|
+
updatedAt = System.currentTimeMillis(),
|
|
262
|
+
)
|
|
227
263
|
}
|
|
228
264
|
|
|
229
|
-
private fun
|
|
230
|
-
val metadata = loadMetadataOrNull() ?: return
|
|
231
|
-
|
|
265
|
+
private fun rollbackPendingBundle(stagingBundleId: String): Boolean {
|
|
266
|
+
val metadata = loadMetadataOrNull() ?: return false
|
|
267
|
+
if (metadata.stagingBundleId != stagingBundleId) {
|
|
268
|
+
return false
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
Log.w(TAG, "Rolling back crashed staging bundle: $stagingBundleId")
|
|
232
272
|
|
|
233
|
-
|
|
273
|
+
val crashedHistory = loadCrashedHistory()
|
|
274
|
+
crashedHistory.addEntry(stagingBundleId)
|
|
275
|
+
saveCrashedHistory(crashedHistory)
|
|
276
|
+
|
|
277
|
+
val fallbackBundleId =
|
|
278
|
+
metadata.stableBundleId?.takeIf { candidate ->
|
|
279
|
+
getBundleUrlForId(candidate) != null
|
|
280
|
+
}
|
|
234
281
|
|
|
235
282
|
val updatedMetadata =
|
|
236
283
|
metadata.copy(
|
|
237
|
-
stableBundleId =
|
|
238
|
-
stagingBundleId =
|
|
284
|
+
stableBundleId = null,
|
|
285
|
+
stagingBundleId = fallbackBundleId,
|
|
239
286
|
verificationPending = false,
|
|
240
|
-
verificationAttemptedAt = null,
|
|
241
287
|
updatedAt = System.currentTimeMillis(),
|
|
242
288
|
)
|
|
243
289
|
saveMetadata(updatedMetadata)
|
|
244
290
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
val stableBundleDir = File(bundleStoreDir, stagingBundleId)
|
|
248
|
-
val bundleFile = stableBundleDir.walk().find { it.name == "index.android.bundle" }
|
|
249
|
-
if (bundleFile != null) {
|
|
250
|
-
preferences.setItem("HotUpdaterBundleURL", bundleFile.absolutePath)
|
|
251
|
-
}
|
|
291
|
+
val fallbackBundleUrl = fallbackBundleId?.let { getBundleUrlForId(it) }
|
|
292
|
+
setBundleURL(fallbackBundleUrl)
|
|
252
293
|
|
|
253
|
-
|
|
254
|
-
|
|
294
|
+
File(getBundleStoreDir(), stagingBundleId).deleteRecursively()
|
|
295
|
+
saveLaunchReport(LaunchReport(status = "RECOVERED", crashedBundleId = stagingBundleId))
|
|
296
|
+
return true
|
|
255
297
|
}
|
|
256
298
|
|
|
257
|
-
private fun
|
|
299
|
+
private fun applyPendingRecoveryIfNeeded(pendingRecovery: PendingCrashRecovery?) {
|
|
258
300
|
val metadata = loadMetadataOrNull() ?: return
|
|
259
301
|
val stagingBundleId = metadata.stagingBundleId ?: return
|
|
260
302
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// Save rollback info to session variable (memory only)
|
|
269
|
-
sessionRollbackBundleId = stagingBundleId
|
|
303
|
+
if (pendingRecovery?.shouldRollback == true &&
|
|
304
|
+
pendingRecovery.launchedBundleId == stagingBundleId &&
|
|
305
|
+
isVerificationPending(metadata)
|
|
306
|
+
) {
|
|
307
|
+
rollbackPendingBundle(stagingBundleId)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
270
310
|
|
|
271
|
-
|
|
272
|
-
val
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
311
|
+
private fun selectLaunch(): LaunchSelection {
|
|
312
|
+
val metadata = loadMetadataOrNull()
|
|
313
|
+
if (metadata == null) {
|
|
314
|
+
val cached = getCachedBundleURL()
|
|
315
|
+
return LaunchSelection(
|
|
316
|
+
bundleUrl = cached ?: getFallbackBundleURL(),
|
|
317
|
+
launchedBundleId = extractBundleIdFromCurrentURL(),
|
|
318
|
+
shouldRollbackOnCrash = false,
|
|
279
319
|
)
|
|
280
|
-
|
|
320
|
+
}
|
|
281
321
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
322
|
+
metadata.stagingBundleId?.let { stagingBundleId ->
|
|
323
|
+
val stagingBundleUrl = getBundleUrlForId(stagingBundleId)
|
|
324
|
+
if (stagingBundleUrl != null) {
|
|
325
|
+
return LaunchSelection(
|
|
326
|
+
bundleUrl = stagingBundleUrl,
|
|
327
|
+
launchedBundleId = stagingBundleId,
|
|
328
|
+
shouldRollbackOnCrash = metadata.verificationPending,
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (metadata.verificationPending && rollbackPendingBundle(stagingBundleId)) {
|
|
333
|
+
return selectLaunch()
|
|
291
334
|
}
|
|
292
|
-
} else {
|
|
293
|
-
// No stable bundle available, clear bundle URL (fallback to assets)
|
|
294
|
-
setBundleURL(null)
|
|
295
|
-
Log.d(TAG, "Cleared bundle URL (no stable bundle)")
|
|
296
335
|
}
|
|
297
336
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
337
|
+
metadata.stableBundleId?.let { stableBundleId ->
|
|
338
|
+
val stableBundleUrl = getBundleUrlForId(stableBundleId)
|
|
339
|
+
if (stableBundleUrl != null) {
|
|
340
|
+
return LaunchSelection(
|
|
341
|
+
bundleUrl = stableBundleUrl,
|
|
342
|
+
launchedBundleId = stableBundleId,
|
|
343
|
+
shouldRollbackOnCrash = false,
|
|
344
|
+
)
|
|
345
|
+
}
|
|
304
346
|
}
|
|
347
|
+
|
|
348
|
+
val cached = getCachedBundleURL()
|
|
349
|
+
return LaunchSelection(
|
|
350
|
+
bundleUrl = cached ?: getFallbackBundleURL(),
|
|
351
|
+
launchedBundleId = extractBundleIdFromCurrentURL(),
|
|
352
|
+
shouldRollbackOnCrash = false,
|
|
353
|
+
)
|
|
305
354
|
}
|
|
306
355
|
|
|
307
356
|
// MARK: - Crashed History
|
|
@@ -321,41 +370,29 @@ class BundleFileStorageService(
|
|
|
321
370
|
return true
|
|
322
371
|
}
|
|
323
372
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
?: return mapOf("status" to "STABLE")
|
|
330
|
-
|
|
331
|
-
// Check if there was a recent rollback (session variable)
|
|
332
|
-
sessionRollbackBundleId?.let { crashedBundleId ->
|
|
333
|
-
// Clear rollback info (one-time read)
|
|
334
|
-
sessionRollbackBundleId = null
|
|
335
|
-
|
|
336
|
-
Log.d(TAG, "notifyAppReady: recovered from rollback (crashed bundle: $crashedBundleId)")
|
|
337
|
-
return mapOf(
|
|
338
|
-
"status" to "RECOVERED",
|
|
339
|
-
"crashedBundleId" to crashedBundleId,
|
|
340
|
-
)
|
|
373
|
+
override fun markLaunchCompleted(currentBundleId: String?) {
|
|
374
|
+
val metadata = loadMetadataOrNull() ?: return
|
|
375
|
+
val stagingBundleId = metadata.stagingBundleId ?: return
|
|
376
|
+
if (!metadata.verificationPending || stagingBundleId != currentBundleId) {
|
|
377
|
+
return
|
|
341
378
|
}
|
|
342
379
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
} else {
|
|
354
|
-
Log.d(TAG, "notifyAppReady: no verification pending")
|
|
355
|
-
}
|
|
380
|
+
saveMetadata(
|
|
381
|
+
metadata.copy(
|
|
382
|
+
verificationPending = false,
|
|
383
|
+
updatedAt = System.currentTimeMillis(),
|
|
384
|
+
),
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// MARK: - notifyAppReady
|
|
356
389
|
|
|
357
|
-
|
|
358
|
-
return mapOf("status" to "STABLE")
|
|
390
|
+
override fun notifyAppReady(): Map<String, Any?> {
|
|
391
|
+
val report = loadLaunchReport() ?: return mapOf("status" to "STABLE")
|
|
392
|
+
return buildMap {
|
|
393
|
+
put("status", report.status)
|
|
394
|
+
report.crashedBundleId?.let { put("crashedBundleId", it) }
|
|
395
|
+
}
|
|
359
396
|
}
|
|
360
397
|
|
|
361
398
|
// MARK: - Bundle URL Operations
|
|
@@ -387,73 +424,16 @@ class BundleFileStorageService(
|
|
|
387
424
|
|
|
388
425
|
override fun getFallbackBundleURL(): String = "assets://index.android.bundle"
|
|
389
426
|
|
|
390
|
-
|
|
391
|
-
|
|
427
|
+
override fun prepareLaunch(pendingRecovery: PendingCrashRecovery?): LaunchSelection {
|
|
428
|
+
saveLaunchReport(null)
|
|
429
|
+
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
392
430
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
val result = cached ?: getFallbackBundleURL()
|
|
400
|
-
Log.d(TAG, "getBundleURL (legacy): returning $result")
|
|
401
|
-
return result
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// New rollback-aware mode - only run crash detection ONCE per process
|
|
405
|
-
if (isVerificationPending(metadata) && !crashDetectionCompleted) {
|
|
406
|
-
crashDetectionCompleted = true
|
|
407
|
-
|
|
408
|
-
if (wasVerificationAttempted(metadata)) {
|
|
409
|
-
// Already executed once but didn't call notifyAppReady → crash!
|
|
410
|
-
Log.w(TAG, "Crash detected: staging bundle executed but didn't call notifyAppReady")
|
|
411
|
-
rollbackToStable()
|
|
412
|
-
} else {
|
|
413
|
-
// First execution - mark verification attempted and give it a chance
|
|
414
|
-
Log.d(TAG, "First execution of staging bundle, marking verification attempted")
|
|
415
|
-
markVerificationAttempted()
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Reload metadata after potential rollback
|
|
420
|
-
val currentMetadata = loadMetadataOrNull()
|
|
421
|
-
|
|
422
|
-
// Return staging bundle if verification pending
|
|
423
|
-
if (currentMetadata != null && isVerificationPending(currentMetadata)) {
|
|
424
|
-
val stagingId = currentMetadata.stagingBundleId
|
|
425
|
-
if (stagingId != null) {
|
|
426
|
-
val bundleStoreDir = getBundleStoreDir()
|
|
427
|
-
val stagingBundleDir = File(bundleStoreDir, stagingId)
|
|
428
|
-
val bundleFile = stagingBundleDir.walk().find { it.name == "index.android.bundle" }
|
|
429
|
-
if (bundleFile != null && bundleFile.exists()) {
|
|
430
|
-
Log.d(TAG, "getBundleURL: returning STAGING bundle $stagingId")
|
|
431
|
-
return bundleFile.absolutePath
|
|
432
|
-
} else {
|
|
433
|
-
Log.w(TAG, "getBundleURL: staging bundle file not found for $stagingId")
|
|
434
|
-
// Staging bundle file missing, rollback to stable
|
|
435
|
-
rollbackToStable()
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Return stable bundle URL
|
|
441
|
-
val stableBundleId = currentMetadata?.stableBundleId
|
|
442
|
-
if (stableBundleId != null) {
|
|
443
|
-
val bundleStoreDir = getBundleStoreDir()
|
|
444
|
-
val stableBundleDir = File(bundleStoreDir, stableBundleId)
|
|
445
|
-
val bundleFile = stableBundleDir.walk().find { it.name == "index.android.bundle" }
|
|
446
|
-
if (bundleFile != null && bundleFile.exists()) {
|
|
447
|
-
Log.d(TAG, "getBundleURL: returning stable bundle $stableBundleId")
|
|
448
|
-
return bundleFile.absolutePath
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Fallback
|
|
453
|
-
val cached = getCachedBundleURL()
|
|
454
|
-
val result = cached ?: getFallbackBundleURL()
|
|
455
|
-
Log.d(TAG, "getBundleURL: returning $result (cached=$cached)")
|
|
456
|
-
return result
|
|
431
|
+
val selection = selectLaunch()
|
|
432
|
+
Log.d(
|
|
433
|
+
TAG,
|
|
434
|
+
"prepareLaunch: bundleId=${selection.launchedBundleId} shouldRollback=${selection.shouldRollbackOnCrash} url=${selection.bundleUrl}",
|
|
435
|
+
)
|
|
436
|
+
return selection
|
|
457
437
|
}
|
|
458
438
|
|
|
459
439
|
override suspend fun updateBundle(
|
|
@@ -525,23 +505,16 @@ class BundleFileStorageService(
|
|
|
525
505
|
|
|
526
506
|
// Update metadata: set as staging
|
|
527
507
|
val currentMetadata = loadMetadataOrNull() ?: createInitialMetadata()
|
|
528
|
-
val updatedMetadata =
|
|
529
|
-
currentMetadata.copy(
|
|
530
|
-
stagingBundleId = bundleId,
|
|
531
|
-
verificationPending = true,
|
|
532
|
-
verificationAttemptedAt = null,
|
|
533
|
-
updatedAt = System.currentTimeMillis(),
|
|
534
|
-
)
|
|
508
|
+
val updatedMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId)
|
|
535
509
|
saveMetadata(updatedMetadata)
|
|
536
510
|
|
|
537
511
|
// Set bundle URL for backwards compatibility
|
|
538
512
|
setBundleURL(existingIndexFile.absolutePath)
|
|
539
513
|
|
|
540
|
-
// Keep
|
|
541
|
-
|
|
542
|
-
cleanupOldBundles(bundleStoreDir, stableBundleId, bundleId)
|
|
514
|
+
// Keep the current verified bundle as a fallback if one exists.
|
|
515
|
+
cleanupOldBundles(bundleStoreDir, updatedMetadata.stableBundleId, bundleId)
|
|
543
516
|
|
|
544
|
-
Log.d(TAG, "Existing bundle set as staging
|
|
517
|
+
Log.d(TAG, "Existing bundle set as staging bundle for next launch")
|
|
545
518
|
return
|
|
546
519
|
} else {
|
|
547
520
|
// If index.android.bundle is missing, delete and re-download
|
|
@@ -721,13 +694,7 @@ class BundleFileStorageService(
|
|
|
721
694
|
|
|
722
695
|
// Update metadata: set new bundle as staging
|
|
723
696
|
val currentMetadata = loadMetadataOrNull() ?: createInitialMetadata()
|
|
724
|
-
val updatedMetadata =
|
|
725
|
-
currentMetadata.copy(
|
|
726
|
-
stagingBundleId = bundleId,
|
|
727
|
-
verificationPending = true,
|
|
728
|
-
verificationAttemptedAt = null,
|
|
729
|
-
updatedAt = System.currentTimeMillis(),
|
|
730
|
-
)
|
|
697
|
+
val updatedMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId)
|
|
731
698
|
saveMetadata(updatedMetadata)
|
|
732
699
|
|
|
733
700
|
// Also update HotUpdaterBundleURL for backwards compatibility
|
|
@@ -737,11 +704,10 @@ class BundleFileStorageService(
|
|
|
737
704
|
// 11) Clean up temporary and download folders
|
|
738
705
|
tempDir.deleteRecursively()
|
|
739
706
|
|
|
740
|
-
// 12) Keep
|
|
741
|
-
|
|
742
|
-
cleanupOldBundles(bundleStoreDir, stableBundleId, bundleId)
|
|
707
|
+
// 12) Keep the fallback bundle and the new staging bundle.
|
|
708
|
+
cleanupOldBundles(bundleStoreDir, updatedMetadata.stableBundleId, bundleId)
|
|
743
709
|
|
|
744
|
-
Log.d(TAG, "Downloaded and set bundle as staging successfully
|
|
710
|
+
Log.d(TAG, "Downloaded and set bundle as staging successfully for the next launch.")
|
|
745
711
|
// Progress already at 1.0 from unzip completion
|
|
746
712
|
}
|
|
747
713
|
}
|
|
@@ -816,8 +782,7 @@ class BundleFileStorageService(
|
|
|
816
782
|
val metadata = loadMetadataOrNull()
|
|
817
783
|
val activeBundleId =
|
|
818
784
|
when {
|
|
819
|
-
metadata?.
|
|
820
|
-
metadata.stagingBundleId
|
|
785
|
+
metadata?.stagingBundleId != null -> metadata.stagingBundleId
|
|
821
786
|
metadata?.stableBundleId != null -> metadata.stableBundleId
|
|
822
787
|
else -> extractBundleIdFromCurrentURL()
|
|
823
788
|
}
|
|
@@ -848,18 +813,19 @@ class BundleFileStorageService(
|
|
|
848
813
|
stableBundleId = null,
|
|
849
814
|
stagingBundleId = null,
|
|
850
815
|
verificationPending = false,
|
|
851
|
-
verificationAttemptedAt = null,
|
|
852
|
-
stagingExecutionCount = null,
|
|
853
816
|
)
|
|
854
817
|
|
|
855
818
|
if (!saveMetadata(clearedMetadata)) {
|
|
856
819
|
return@withContext false
|
|
857
820
|
}
|
|
858
821
|
|
|
822
|
+
saveLaunchReport(null)
|
|
823
|
+
|
|
859
824
|
getBundleStoreDir().listFiles()?.forEach { file ->
|
|
860
825
|
if (
|
|
861
826
|
file.name == BundleMetadata.METADATA_FILENAME ||
|
|
862
|
-
file.name == CrashedHistory.CRASHED_HISTORY_FILENAME
|
|
827
|
+
file.name == CrashedHistory.CRASHED_HISTORY_FILENAME ||
|
|
828
|
+
file.name == LaunchReport.LAUNCH_REPORT_FILENAME
|
|
863
829
|
) {
|
|
864
830
|
return@forEach
|
|
865
831
|
}
|
|
@@ -14,8 +14,6 @@ data class BundleMetadata(
|
|
|
14
14
|
val stableBundleId: String? = null,
|
|
15
15
|
val stagingBundleId: String? = null,
|
|
16
16
|
val verificationPending: Boolean = false,
|
|
17
|
-
val verificationAttemptedAt: Long? = null,
|
|
18
|
-
val stagingExecutionCount: Int? = null,
|
|
19
17
|
val updatedAt: Long = System.currentTimeMillis(),
|
|
20
18
|
) {
|
|
21
19
|
companion object {
|
|
@@ -45,18 +43,6 @@ data class BundleMetadata(
|
|
|
45
43
|
null
|
|
46
44
|
},
|
|
47
45
|
verificationPending = json.optBoolean("verificationPending", false),
|
|
48
|
-
verificationAttemptedAt =
|
|
49
|
-
if (json.has("verificationAttemptedAt") && !json.isNull("verificationAttemptedAt")) {
|
|
50
|
-
json.getLong("verificationAttemptedAt")
|
|
51
|
-
} else {
|
|
52
|
-
null
|
|
53
|
-
},
|
|
54
|
-
stagingExecutionCount =
|
|
55
|
-
if (json.has("stagingExecutionCount") && !json.isNull("stagingExecutionCount")) {
|
|
56
|
-
json.getInt("stagingExecutionCount")
|
|
57
|
-
} else {
|
|
58
|
-
null
|
|
59
|
-
},
|
|
60
46
|
updatedAt = json.optLong("updatedAt", System.currentTimeMillis()),
|
|
61
47
|
)
|
|
62
48
|
|
|
@@ -100,8 +86,6 @@ data class BundleMetadata(
|
|
|
100
86
|
put("stableBundleId", stableBundleId ?: JSONObject.NULL)
|
|
101
87
|
put("stagingBundleId", stagingBundleId ?: JSONObject.NULL)
|
|
102
88
|
put("verificationPending", verificationPending)
|
|
103
|
-
put("verificationAttemptedAt", verificationAttemptedAt ?: JSONObject.NULL)
|
|
104
|
-
put("stagingExecutionCount", stagingExecutionCount ?: JSONObject.NULL)
|
|
105
89
|
put("updatedAt", updatedAt)
|
|
106
90
|
}
|
|
107
91
|
|
|
@@ -237,3 +221,76 @@ data class CrashedHistory(
|
|
|
237
221
|
bundles.clear()
|
|
238
222
|
}
|
|
239
223
|
}
|
|
224
|
+
|
|
225
|
+
data class PendingCrashRecovery(
|
|
226
|
+
val launchedBundleId: String?,
|
|
227
|
+
val shouldRollback: Boolean,
|
|
228
|
+
) {
|
|
229
|
+
companion object {
|
|
230
|
+
fun fromJson(json: JSONObject): PendingCrashRecovery =
|
|
231
|
+
PendingCrashRecovery(
|
|
232
|
+
launchedBundleId =
|
|
233
|
+
if (json.has("bundleId") && !json.isNull("bundleId")) {
|
|
234
|
+
json.getString("bundleId").takeIf { it.isNotEmpty() }
|
|
235
|
+
} else {
|
|
236
|
+
null
|
|
237
|
+
},
|
|
238
|
+
shouldRollback = json.optBoolean("shouldRollback", false),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
data class LaunchSelection(
|
|
244
|
+
val bundleUrl: String,
|
|
245
|
+
val launchedBundleId: String?,
|
|
246
|
+
val shouldRollbackOnCrash: Boolean,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
data class LaunchReport(
|
|
250
|
+
val status: String = "STABLE",
|
|
251
|
+
val crashedBundleId: String? = null,
|
|
252
|
+
) {
|
|
253
|
+
companion object {
|
|
254
|
+
private const val TAG = "LaunchReport"
|
|
255
|
+
const val LAUNCH_REPORT_FILENAME = "launch-report.json"
|
|
256
|
+
|
|
257
|
+
fun fromJson(json: JSONObject): LaunchReport =
|
|
258
|
+
LaunchReport(
|
|
259
|
+
status = json.optString("status", "STABLE"),
|
|
260
|
+
crashedBundleId =
|
|
261
|
+
if (json.has("crashedBundleId") && !json.isNull("crashedBundleId")) {
|
|
262
|
+
json.getString("crashedBundleId").takeIf { it.isNotEmpty() }
|
|
263
|
+
} else {
|
|
264
|
+
null
|
|
265
|
+
},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
fun loadFromFile(file: File): LaunchReport? =
|
|
269
|
+
try {
|
|
270
|
+
if (!file.exists()) {
|
|
271
|
+
null
|
|
272
|
+
} else {
|
|
273
|
+
fromJson(JSONObject(file.readText()))
|
|
274
|
+
}
|
|
275
|
+
} catch (e: Exception) {
|
|
276
|
+
Log.e(TAG, "Failed to load launch report", e)
|
|
277
|
+
null
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fun toJson(): JSONObject =
|
|
282
|
+
JSONObject().apply {
|
|
283
|
+
put("status", status)
|
|
284
|
+
put("crashedBundleId", crashedBundleId ?: JSONObject.NULL)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fun saveToFile(file: File): Boolean =
|
|
288
|
+
try {
|
|
289
|
+
file.parentFile?.mkdirs()
|
|
290
|
+
file.writeText(toJson().toString(2))
|
|
291
|
+
true
|
|
292
|
+
} catch (e: Exception) {
|
|
293
|
+
Log.e(TAG, "Failed to save launch report", e)
|
|
294
|
+
false
|
|
295
|
+
}
|
|
296
|
+
}
|