@onekeyfe/react-native-bundle-update 3.0.29 → 3.0.31
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,20 @@ 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 /
|
|
413
|
-
* UNEXPECTED_<class> — so
|
|
414
|
-
*
|
|
415
|
-
* verifyPackage failures)
|
|
412
|
+
* FILE_NOT_FOUND / FILE_DISAPPEARED / FILE_TRUNCATED /
|
|
413
|
+
* PERMISSION_DENIED / OOM / IO_<class> / UNEXPECTED_<class> — so
|
|
414
|
+
* analytics can split the previously opaque "Failed to calculate
|
|
415
|
+
* SHA256" bucket (mixpanel: 91.3 percent of verifyPackage failures)
|
|
416
|
+
* into actionable categories. Keep this list in sync with the
|
|
417
|
+
* lastSHA256Failure.set(...) call sites in calculateSHA256 below.
|
|
418
|
+
*
|
|
419
|
+
* Note: 0-byte files are NOT treated as a failure. They hash to the
|
|
420
|
+
* well-known empty-content SHA-256 and the caller's expected/actual
|
|
421
|
+
* comparison handles legitimate vs. corrupt-empty cases. Rejecting
|
|
422
|
+
* empty files here would make any OTA bundle that legitimately
|
|
423
|
+
* contains a 0-byte file (touched marker, blank locale fallback)
|
|
424
|
+
* fail validateAllFilesInDir / validateWebEmbedSha256 / launch entry
|
|
425
|
+
* verification — all of which share this calculator.
|
|
416
426
|
*/
|
|
417
427
|
fun lastSHA256FailureReason(): String? = lastSHA256Failure.get()
|
|
418
428
|
|
|
@@ -424,11 +434,6 @@ object BundleUpdateStoreAndroid {
|
|
|
424
434
|
OneKeyLog.error("BundleUpdate", "calculateSHA256: file not found: $filePath")
|
|
425
435
|
return null
|
|
426
436
|
}
|
|
427
|
-
if (file.length() == 0L) {
|
|
428
|
-
lastSHA256Failure.set("FILE_EMPTY")
|
|
429
|
-
OneKeyLog.error("BundleUpdate", "calculateSHA256: file empty: $filePath")
|
|
430
|
-
return null
|
|
431
|
-
}
|
|
432
437
|
return try {
|
|
433
438
|
val digest = MessageDigest.getInstance("SHA-256")
|
|
434
439
|
BufferedInputStream(FileInputStream(filePath)).use { bis ->
|
|
@@ -1460,8 +1465,10 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1460
1465
|
}
|
|
1461
1466
|
OneKeyLog.warn("BundleUpdate", "downloadBundle: HTTP 416 (range not satisfiable), discarding partial and failing this attempt")
|
|
1462
1467
|
if (partialFile.exists()) partialFile.delete()
|
|
1463
|
-
|
|
1464
|
-
|
|
1468
|
+
// Don't pre-emit update/error here; the outer catch is the
|
|
1469
|
+
// single source of error events. sanitizeErrorMessageForEvent
|
|
1470
|
+
// recognizes "HTTP " prefix and forwards this string verbatim.
|
|
1471
|
+
throw Exception("HTTP 416 (range not satisfiable)")
|
|
1465
1472
|
}
|
|
1466
1473
|
|
|
1467
1474
|
val expectsResume = partialBytes > 0
|
|
@@ -1470,7 +1477,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1470
1477
|
if (!response.isSuccessful || (response.code != 200 && response.code != 206)) {
|
|
1471
1478
|
OneKeyLog.error("BundleUpdate", "downloadBundle: HTTP error, statusCode=${response.code}")
|
|
1472
1479
|
response.close()
|
|
1473
|
-
|
|
1480
|
+
// outer catch is the single source of update/error events.
|
|
1474
1481
|
throw Exception("HTTP ${response.code}")
|
|
1475
1482
|
}
|
|
1476
1483
|
|
|
@@ -1542,7 +1549,8 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1542
1549
|
if (downloadedFile.exists()) downloadedFile.delete()
|
|
1543
1550
|
if (!partialAfter.renameTo(downloadedFile)) {
|
|
1544
1551
|
OneKeyLog.error("BundleUpdate", "downloadBundle: rename .partial -> final failed")
|
|
1545
|
-
|
|
1552
|
+
// outer catch is the single source of update/error events;
|
|
1553
|
+
// "Failed to finalize download" is in the verbatim allowlist.
|
|
1546
1554
|
throw Exception("Failed to finalize download")
|
|
1547
1555
|
}
|
|
1548
1556
|
|
|
@@ -1551,7 +1559,8 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1551
1559
|
val reason = BundleUpdateStoreAndroid.lastSHA256FailureReason() ?: "MISMATCH"
|
|
1552
1560
|
File(filePath).delete()
|
|
1553
1561
|
OneKeyLog.error("BundleUpdate", "downloadBundle: SHA256 verification failed after download, reason=$reason")
|
|
1554
|
-
|
|
1562
|
+
// outer catch emits the verbatim "Bundle SHA256 verification
|
|
1563
|
+
// failed: <REASON>" payload (recognized by sanitize/JS).
|
|
1555
1564
|
throw Exception("Bundle SHA256 verification failed: $reason")
|
|
1556
1565
|
}
|
|
1557
1566
|
|
|
@@ -1563,13 +1572,22 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1563
1572
|
// event channel must NOT carry e.message verbatim — Android
|
|
1564
1573
|
// FileNotFoundException etc. embed the full /data/user/.../
|
|
1565
1574
|
// path including the package identifier, and downstream
|
|
1566
|
-
// listeners would forward that into analytics. Emit only
|
|
1567
|
-
//
|
|
1568
|
-
// ReactNativeBundleUpdate.swift's "update/error" sites
|
|
1575
|
+
// listeners would forward that into analytics. Emit only the
|
|
1576
|
+
// sanitized tag; mirrors the iOS sendEvent payload at
|
|
1577
|
+
// ReactNativeBundleUpdate.swift's "update/error" sites and
|
|
1578
|
+
// matches the verbatim guarantees documented on
|
|
1579
|
+
// sanitizeErrorMessageForEvent.
|
|
1569
1580
|
OneKeyLog.error("BundleUpdate", "downloadBundle: failed: ${e.javaClass.simpleName}: ${e.message}")
|
|
1570
|
-
val
|
|
1571
|
-
sendEvent("update/error", message =
|
|
1572
|
-
|
|
1581
|
+
val sanitized = sanitizeErrorMessageForEvent(e)
|
|
1582
|
+
sendEvent("update/error", message = sanitized)
|
|
1583
|
+
// Rethrow with the same sanitized message so the Promise
|
|
1584
|
+
// rejection surfacing to JS carries no /data/user/<u>/<pkg>/
|
|
1585
|
+
// paths. Without this rewrap, FileNotFoundException etc.
|
|
1586
|
+
// would re-leak via Promise.reject's message channel even
|
|
1587
|
+
// though the event payload was already sanitized. Keep
|
|
1588
|
+
// the original exception as `cause` so OneKeyLog (and any
|
|
1589
|
+
// native crash reporter) still sees the full chain.
|
|
1590
|
+
throw Exception(sanitized, e)
|
|
1573
1591
|
} finally {
|
|
1574
1592
|
isDownloading.set(false)
|
|
1575
1593
|
}
|
|
@@ -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,
|
|
@@ -1273,9 +1283,14 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1273
1283
|
private func snapshotResumeDataForBackgrounding() {
|
|
1274
1284
|
guard let session = self.urlSession else { return }
|
|
1275
1285
|
// Read both isDownloading AND activeDownloadFilePath inside the
|
|
1276
|
-
// same stateQueue.sync block
|
|
1277
|
-
//
|
|
1278
|
-
//
|
|
1286
|
+
// same stateQueue.sync block. A concurrent downloadBundle entry
|
|
1287
|
+
// flips isDownloading=true at the start of the Promise body but
|
|
1288
|
+
// only writes activeDownloadFilePath later (after the early
|
|
1289
|
+
// guards / version + URL validation). Without this paired read,
|
|
1290
|
+
// didEnterBackground could observe isDownloading=true while
|
|
1291
|
+
// activeDownloadFilePath is still nil from a previous run, or —
|
|
1292
|
+
// on the unwind via defer — see isDownloading=false alongside a
|
|
1293
|
+
// not-yet-cleared path.
|
|
1279
1294
|
let snapshot: (Bool, String?) = self.stateQueue.sync {
|
|
1280
1295
|
(self.isDownloading, self.activeDownloadFilePath)
|
|
1281
1296
|
}
|
|
@@ -1284,14 +1299,35 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1284
1299
|
|
|
1285
1300
|
let resumeDataPath = "\(filePath).resume"
|
|
1286
1301
|
let bgTaskName = "BundleUpdateResumeSnapshot"
|
|
1287
|
-
|
|
1288
|
-
bgTaskId
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1302
|
+
|
|
1303
|
+
// bgTaskId is mutated from three escaping closures: the
|
|
1304
|
+
// beginBackgroundTask expiration handler (called on main),
|
|
1305
|
+
// session.getAllTasks's completion (NOT guaranteed to be main),
|
|
1306
|
+
// and group.notify on .main. Wrap reads/writes in a tiny holder
|
|
1307
|
+
// serialized through a dedicated queue so we cannot double-end
|
|
1308
|
+
// the background task or leak it. `endOnce` guarantees endBackgroundTask
|
|
1309
|
+
// is invoked exactly once across whichever closure reaches it first.
|
|
1310
|
+
final class BgTaskHolder {
|
|
1311
|
+
private let q = DispatchQueue(label: "so.onekey.bundleupdate.bgtask")
|
|
1312
|
+
private var id: UIBackgroundTaskIdentifier = .invalid
|
|
1313
|
+
func set(_ newId: UIBackgroundTaskIdentifier) {
|
|
1314
|
+
q.sync { id = newId }
|
|
1315
|
+
}
|
|
1316
|
+
func endOnce() {
|
|
1317
|
+
q.sync {
|
|
1318
|
+
if id != .invalid {
|
|
1319
|
+
UIApplication.shared.endBackgroundTask(id)
|
|
1320
|
+
id = .invalid
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1293
1323
|
}
|
|
1294
1324
|
}
|
|
1325
|
+
let bgTask = BgTaskHolder()
|
|
1326
|
+
let started = UIApplication.shared.beginBackgroundTask(withName: bgTaskName) {
|
|
1327
|
+
// Expiration handler — system is reclaiming us. Best-effort end.
|
|
1328
|
+
bgTask.endOnce()
|
|
1329
|
+
}
|
|
1330
|
+
bgTask.set(started)
|
|
1295
1331
|
OneKeyLog.info("BundleUpdate", "didEnterBackground: snapshotting resumeData for \(filePath)")
|
|
1296
1332
|
|
|
1297
1333
|
session.getAllTasks { tasks in
|
|
@@ -1318,10 +1354,7 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1318
1354
|
})
|
|
1319
1355
|
}
|
|
1320
1356
|
group.notify(queue: .main) {
|
|
1321
|
-
|
|
1322
|
-
UIApplication.shared.endBackgroundTask(bgTaskId)
|
|
1323
|
-
bgTaskId = .invalid
|
|
1324
|
-
}
|
|
1357
|
+
bgTask.endOnce()
|
|
1325
1358
|
}
|
|
1326
1359
|
}
|
|
1327
1360
|
}
|
|
@@ -1514,7 +1547,19 @@ class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec {
|
|
|
1514
1547
|
let nsError = error as NSError
|
|
1515
1548
|
OneKeyLog.error("BundleUpdate", "downloadBundle: download failed: \(nsError.domain) \(nsError.code) \(nsError.localizedDescription)")
|
|
1516
1549
|
self.sendEvent(type: "update/error", message: "\(nsError.domain) \(nsError.code)")
|
|
1517
|
-
|
|
1550
|
+
// Rethrow a sanitized NSError. The original `error` is a
|
|
1551
|
+
// URLSession/system error whose `localizedDescription` may
|
|
1552
|
+
// include the temp file path (e.g. NSURLErrorFailingURLString
|
|
1553
|
+
// userInfo, or the ~/Library/.../CFNetworkDownload_*.tmp
|
|
1554
|
+
// path on cancel-with-resume) — Promise rejections surface
|
|
1555
|
+
// that string to JS, which would re-leak the paths the
|
|
1556
|
+
// event-payload sanitization just stripped. Keep the full
|
|
1557
|
+
// detail in OneKeyLog (above) and emit only `domain code`.
|
|
1558
|
+
throw NSError(
|
|
1559
|
+
domain: nsError.domain,
|
|
1560
|
+
code: nsError.code,
|
|
1561
|
+
userInfo: [NSLocalizedDescriptionKey: "Download failed: \(nsError.domain) \(nsError.code)"]
|
|
1562
|
+
)
|
|
1518
1563
|
|
|
1519
1564
|
case .success(let (tempURL, response)):
|
|
1520
1565
|
// Successful completion ⇒ no longer need the resume blob.
|