@onekeyfe/react-native-bundle-update 3.0.29 → 3.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt
CHANGED
|
@@ -409,10 +409,18 @@ object BundleUpdateStoreAndroid {
|
|
|
409
409
|
/**
|
|
410
410
|
* Subtype of the most recent calculateSHA256 failure on this thread, or
|
|
411
411
|
* null if the last call succeeded. Surfaces the specific reason —
|
|
412
|
-
* FILE_NOT_FOUND /
|
|
412
|
+
* FILE_NOT_FOUND / FILE_TRUNCATED / OOM / IO_<class> /
|
|
413
413
|
* UNEXPECTED_<class> — so analytics can split the previously opaque
|
|
414
414
|
* "Failed to calculate SHA256" bucket (mixpanel: 91.3 percent of
|
|
415
415
|
* verifyPackage failures) into actionable categories.
|
|
416
|
+
*
|
|
417
|
+
* Note: 0-byte files are NOT treated as a failure. They hash to the
|
|
418
|
+
* well-known empty-content SHA-256 and the caller's expected/actual
|
|
419
|
+
* comparison handles legitimate vs. corrupt-empty cases. Rejecting
|
|
420
|
+
* empty files here would make any OTA bundle that legitimately
|
|
421
|
+
* contains a 0-byte file (touched marker, blank locale fallback)
|
|
422
|
+
* fail validateAllFilesInDir / validateWebEmbedSha256 / launch entry
|
|
423
|
+
* verification — all of which share this calculator.
|
|
416
424
|
*/
|
|
417
425
|
fun lastSHA256FailureReason(): String? = lastSHA256Failure.get()
|
|
418
426
|
|
|
@@ -424,11 +432,6 @@ object BundleUpdateStoreAndroid {
|
|
|
424
432
|
OneKeyLog.error("BundleUpdate", "calculateSHA256: file not found: $filePath")
|
|
425
433
|
return null
|
|
426
434
|
}
|
|
427
|
-
if (file.length() == 0L) {
|
|
428
|
-
lastSHA256Failure.set("FILE_EMPTY")
|
|
429
|
-
OneKeyLog.error("BundleUpdate", "calculateSHA256: file empty: $filePath")
|
|
430
|
-
return null
|
|
431
|
-
}
|
|
432
435
|
return try {
|
|
433
436
|
val digest = MessageDigest.getInstance("SHA-256")
|
|
434
437
|
BufferedInputStream(FileInputStream(filePath)).use { bis ->
|
|
@@ -1460,8 +1463,10 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1460
1463
|
}
|
|
1461
1464
|
OneKeyLog.warn("BundleUpdate", "downloadBundle: HTTP 416 (range not satisfiable), discarding partial and failing this attempt")
|
|
1462
1465
|
if (partialFile.exists()) partialFile.delete()
|
|
1463
|
-
|
|
1464
|
-
|
|
1466
|
+
// Don't pre-emit update/error here; the outer catch is the
|
|
1467
|
+
// single source of error events. sanitizeErrorMessageForEvent
|
|
1468
|
+
// recognizes "HTTP " prefix and forwards this string verbatim.
|
|
1469
|
+
throw Exception("HTTP 416 (range not satisfiable)")
|
|
1465
1470
|
}
|
|
1466
1471
|
|
|
1467
1472
|
val expectsResume = partialBytes > 0
|
|
@@ -1470,7 +1475,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1470
1475
|
if (!response.isSuccessful || (response.code != 200 && response.code != 206)) {
|
|
1471
1476
|
OneKeyLog.error("BundleUpdate", "downloadBundle: HTTP error, statusCode=${response.code}")
|
|
1472
1477
|
response.close()
|
|
1473
|
-
|
|
1478
|
+
// outer catch is the single source of update/error events.
|
|
1474
1479
|
throw Exception("HTTP ${response.code}")
|
|
1475
1480
|
}
|
|
1476
1481
|
|
|
@@ -1542,7 +1547,8 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1542
1547
|
if (downloadedFile.exists()) downloadedFile.delete()
|
|
1543
1548
|
if (!partialAfter.renameTo(downloadedFile)) {
|
|
1544
1549
|
OneKeyLog.error("BundleUpdate", "downloadBundle: rename .partial -> final failed")
|
|
1545
|
-
|
|
1550
|
+
// outer catch is the single source of update/error events;
|
|
1551
|
+
// "Failed to finalize download" is in the verbatim allowlist.
|
|
1546
1552
|
throw Exception("Failed to finalize download")
|
|
1547
1553
|
}
|
|
1548
1554
|
|
|
@@ -1551,7 +1557,8 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1551
1557
|
val reason = BundleUpdateStoreAndroid.lastSHA256FailureReason() ?: "MISMATCH"
|
|
1552
1558
|
File(filePath).delete()
|
|
1553
1559
|
OneKeyLog.error("BundleUpdate", "downloadBundle: SHA256 verification failed after download, reason=$reason")
|
|
1554
|
-
|
|
1560
|
+
// outer catch emits the verbatim "Bundle SHA256 verification
|
|
1561
|
+
// failed: <REASON>" payload (recognized by sanitize/JS).
|
|
1555
1562
|
throw Exception("Bundle SHA256 verification failed: $reason")
|
|
1556
1563
|
}
|
|
1557
1564
|
|
|
@@ -1563,12 +1570,13 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1563
1570
|
// event channel must NOT carry e.message verbatim — Android
|
|
1564
1571
|
// FileNotFoundException etc. embed the full /data/user/.../
|
|
1565
1572
|
// path including the package identifier, and downstream
|
|
1566
|
-
// listeners would forward that into analytics. Emit only
|
|
1567
|
-
//
|
|
1568
|
-
// ReactNativeBundleUpdate.swift's "update/error" sites
|
|
1573
|
+
// listeners would forward that into analytics. Emit only the
|
|
1574
|
+
// sanitized tag; mirrors the iOS sendEvent payload at
|
|
1575
|
+
// ReactNativeBundleUpdate.swift's "update/error" sites and
|
|
1576
|
+
// matches the verbatim guarantees documented on
|
|
1577
|
+
// sanitizeErrorMessageForEvent.
|
|
1569
1578
|
OneKeyLog.error("BundleUpdate", "downloadBundle: failed: ${e.javaClass.simpleName}: ${e.message}")
|
|
1570
|
-
|
|
1571
|
-
sendEvent("update/error", message = "${e.javaClass.simpleName}: $codeTag")
|
|
1579
|
+
sendEvent("update/error", message = sanitizeErrorMessageForEvent(e))
|
|
1572
1580
|
throw e
|
|
1573
1581
|
} finally {
|
|
1574
1582
|
isDownloading.set(false)
|
|
@@ -314,12 +314,20 @@ public class BundleUpdateStore: NSObject {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
/// Subtype of the most recent calculateSHA256 failure on this thread, or
|
|
317
|
-
/// nil if the last call succeeded. Surfaces FILE_NOT_FOUND /
|
|
317
|
+
/// nil if the last call succeeded. Surfaces FILE_NOT_FOUND /
|
|
318
318
|
/// FILE_DISAPPEARED / IO_<NSError code> / UNEXPECTED so analytics can
|
|
319
319
|
/// split the previously opaque "Failed to calculate SHA256" bucket
|
|
320
320
|
/// (mixpanel: 91.3% of verifyPackage failures are Android; iOS shares
|
|
321
321
|
/// the calculator and inherits the same blind spot for its 14 ASC +
|
|
322
322
|
/// 2 verifyPackage Promise-destroyed cases).
|
|
323
|
+
///
|
|
324
|
+
/// Note: 0-byte files are NOT treated as a failure. They hash to the
|
|
325
|
+
/// well-known empty-content SHA256 and the caller's expected/actual
|
|
326
|
+
/// comparison handles legitimate vs. corrupt-empty cases. Rejecting
|
|
327
|
+
/// empty files here would make any OTA bundle that legitimately
|
|
328
|
+
/// contains a 0-byte file (touched marker, blank locale fallback)
|
|
329
|
+
/// fail validateAllFilesInDir / validateWebEmbedSha256 / launch entry
|
|
330
|
+
/// verification — all of which share this calculator.
|
|
323
331
|
public static func lastSHA256FailureReason() -> String? {
|
|
324
332
|
return Thread.current.threadDictionary[kSHA256FailureKey] as? String
|
|
325
333
|
}
|
|
@@ -340,13 +348,6 @@ public class BundleUpdateStore: NSObject {
|
|
|
340
348
|
OneKeyLog.error("BundleUpdate", "calculateSHA256: file not found: \(filePath)")
|
|
341
349
|
return nil
|
|
342
350
|
}
|
|
343
|
-
let attrs = (try? fm.attributesOfItem(atPath: filePath)) ?? [:]
|
|
344
|
-
let fileSize = (attrs[.size] as? NSNumber)?.int64Value ?? -1
|
|
345
|
-
if fileSize == 0 {
|
|
346
|
-
setSHA256Failure("FILE_EMPTY")
|
|
347
|
-
OneKeyLog.error("BundleUpdate", "calculateSHA256: file empty: \(filePath)")
|
|
348
|
-
return nil
|
|
349
|
-
}
|
|
350
351
|
guard let fileHandle = FileHandle(forReadingAtPath: filePath) else {
|
|
351
352
|
setSHA256Failure("FILE_DISAPPEARED")
|
|
352
353
|
OneKeyLog.error("BundleUpdate", "calculateSHA256: open failed (file disappeared between stat and open): \(filePath)")
|
|
@@ -376,7 +377,11 @@ public class BundleUpdateStore: NSObject {
|
|
|
376
377
|
}) {}
|
|
377
378
|
if let err = threwError {
|
|
378
379
|
let nsErr = err as NSError
|
|
379
|
-
|
|
380
|
+
// Keep the failure tag low-cardinality (`IO_<code>`) so it
|
|
381
|
+
// matches the doc on lastSHA256FailureReason and stays under
|
|
382
|
+
// the analytics bucket cap. The full `domain code description`
|
|
383
|
+
// detail is still logged below for local debugging.
|
|
384
|
+
setSHA256Failure("IO_\(nsErr.code)")
|
|
380
385
|
OneKeyLog.error("BundleUpdate", "calculateSHA256: read failed: \(nsErr.domain) \(nsErr.code) \(nsErr.localizedDescription)")
|
|
381
386
|
return nil
|
|
382
387
|
}
|
|
@@ -1249,17 +1254,22 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1249
1254
|
/// On iOS, force-quit (user swipes the app off the App Switcher) cannot
|
|
1250
1255
|
/// fire `URLSession`'s `didCompleteWithError` — SIGKILL leaves no time
|
|
1251
1256
|
/// for callbacks. The kill is, however, *always* preceded by the app
|
|
1252
|
-
/// transitioning to the background. We hook that transition and
|
|
1253
|
-
/// `cancel(byProducingResumeData:)`
|
|
1254
|
-
///
|
|
1255
|
-
/// kills follow the same chain (the OS only reaps
|
|
1256
|
-
/// under memory pressure), so this also covers OOM
|
|
1257
|
+
/// transitioning to the background. We hook that transition and kick
|
|
1258
|
+
/// off `cancel(byProducingResumeData:)` for any in-flight downloads so
|
|
1259
|
+
/// the resume blob lands on disk before the app can be terminated.
|
|
1260
|
+
/// Memory-pressure kills follow the same chain (the OS only reaps
|
|
1261
|
+
/// backgrounded apps under memory pressure), so this also covers OOM
|
|
1262
|
+
/// termination.
|
|
1257
1263
|
///
|
|
1258
|
-
/// `
|
|
1259
|
-
///
|
|
1260
|
-
///
|
|
1261
|
-
///
|
|
1262
|
-
///
|
|
1264
|
+
/// Both `URLSession.getAllTasks(_:)` and `cancel(byProducingResumeData:)`
|
|
1265
|
+
/// deliver their results *asynchronously* via a closure — the resume
|
|
1266
|
+
/// data does not pop out synchronously. We wrap the work in a
|
|
1267
|
+
/// `beginBackgroundTask` window that extends the ~5s of guaranteed
|
|
1268
|
+
/// background runtime to ~30s so the closures actually have time to
|
|
1269
|
+
/// fire and write the few-KB blob before suspension. Persistence is
|
|
1270
|
+
/// best-effort: if iOS reaps us before the writer closure runs (very
|
|
1271
|
+
/// short background windows, expiration), the next launch simply
|
|
1272
|
+
/// re-downloads from scratch — a resume miss is correct, just slower.
|
|
1263
1273
|
private func registerBackgroundSnapshotObserver() {
|
|
1264
1274
|
didEnterBackgroundObserver = NotificationCenter.default.addObserver(
|
|
1265
1275
|
forName: UIApplication.didEnterBackgroundNotification,
|
|
@@ -1284,14 +1294,35 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1284
1294
|
|
|
1285
1295
|
let resumeDataPath = "\(filePath).resume"
|
|
1286
1296
|
let bgTaskName = "BundleUpdateResumeSnapshot"
|
|
1287
|
-
|
|
1288
|
-
bgTaskId
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1297
|
+
|
|
1298
|
+
// bgTaskId is mutated from three escaping closures: the
|
|
1299
|
+
// beginBackgroundTask expiration handler (called on main),
|
|
1300
|
+
// session.getAllTasks's completion (NOT guaranteed to be main),
|
|
1301
|
+
// and group.notify on .main. Wrap reads/writes in a tiny holder
|
|
1302
|
+
// serialized through a dedicated queue so we cannot double-end
|
|
1303
|
+
// the background task or leak it. `endOnce` guarantees endBackgroundTask
|
|
1304
|
+
// is invoked exactly once across whichever closure reaches it first.
|
|
1305
|
+
final class BgTaskHolder {
|
|
1306
|
+
private let q = DispatchQueue(label: "so.onekey.bundleupdate.bgtask")
|
|
1307
|
+
private var id: UIBackgroundTaskIdentifier = .invalid
|
|
1308
|
+
func set(_ newId: UIBackgroundTaskIdentifier) {
|
|
1309
|
+
q.sync { id = newId }
|
|
1310
|
+
}
|
|
1311
|
+
func endOnce() {
|
|
1312
|
+
q.sync {
|
|
1313
|
+
if id != .invalid {
|
|
1314
|
+
UIApplication.shared.endBackgroundTask(id)
|
|
1315
|
+
id = .invalid
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1293
1318
|
}
|
|
1294
1319
|
}
|
|
1320
|
+
let bgTask = BgTaskHolder()
|
|
1321
|
+
let started = UIApplication.shared.beginBackgroundTask(withName: bgTaskName) {
|
|
1322
|
+
// Expiration handler — system is reclaiming us. Best-effort end.
|
|
1323
|
+
bgTask.endOnce()
|
|
1324
|
+
}
|
|
1325
|
+
bgTask.set(started)
|
|
1295
1326
|
OneKeyLog.info("BundleUpdate", "didEnterBackground: snapshotting resumeData for \(filePath)")
|
|
1296
1327
|
|
|
1297
1328
|
session.getAllTasks { tasks in
|
|
@@ -1318,10 +1349,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1318
1349
|
})
|
|
1319
1350
|
}
|
|
1320
1351
|
group.notify(queue: .main) {
|
|
1321
|
-
|
|
1322
|
-
UIApplication.shared.endBackgroundTask(bgTaskId)
|
|
1323
|
-
bgTaskId = .invalid
|
|
1324
|
-
}
|
|
1352
|
+
bgTask.endOnce()
|
|
1325
1353
|
}
|
|
1326
1354
|
}
|
|
1327
1355
|
}
|