@onekeyfe/react-native-bundle-update 3.0.65 → 3.0.67
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
|
@@ -20,6 +20,7 @@ import java.nio.file.Path
|
|
|
20
20
|
import java.nio.file.Paths
|
|
21
21
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
22
22
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
23
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
23
24
|
import java.util.concurrent.atomic.AtomicLong
|
|
24
25
|
import java.util.zip.ZipEntry
|
|
25
26
|
import java.util.zip.ZipInputStream
|
|
@@ -1083,6 +1084,12 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1083
1084
|
|
|
1084
1085
|
companion object {
|
|
1085
1086
|
private const val PREFS_NAME = "BundleUpdatePrefs"
|
|
1087
|
+
|
|
1088
|
+
// Number of concurrent segment files (`<partial>.seg0..seg{N-1}`) the
|
|
1089
|
+
// ConcurrentRangeDownloader produces. MUST equal the segmentCount passed
|
|
1090
|
+
// to ConcurrentRangeDownloader (currently the default 8). Every place
|
|
1091
|
+
// that cleans up `.segN` files must iterate `0 until CONCURRENT_SEGMENT_COUNT`.
|
|
1092
|
+
private const val CONCURRENT_SEGMENT_COUNT = 8
|
|
1086
1093
|
}
|
|
1087
1094
|
|
|
1088
1095
|
private val listeners = CopyOnWriteArrayList<BundleListener>()
|
|
@@ -1234,9 +1241,12 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1234
1241
|
if (verifyBundleSHA256(filePath, sha256)) {
|
|
1235
1242
|
OneKeyLog.info("BundleUpdate", "downloadBundle: existing file SHA256 valid, skipping download")
|
|
1236
1243
|
// Final file is authoritative — drop any stale concurrent
|
|
1237
|
-
// partial
|
|
1244
|
+
// partial + segment files left by an earlier interrupted
|
|
1245
|
+
// attempt. (".progress" is a legacy manifest from the old
|
|
1246
|
+
// pre-allocated model; deleting it is a harmless no-op now.)
|
|
1238
1247
|
if (partialFile.exists()) partialFile.delete()
|
|
1239
1248
|
File("$partialFilePath.progress").delete()
|
|
1249
|
+
for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
|
|
1240
1250
|
// Keep isDownloading held across the skip delay below. Clearing
|
|
1241
1251
|
// it before the sleep opens a ~1s window where a second
|
|
1242
1252
|
// downloadBundle could pass the getAndSet guard and run
|
|
@@ -1248,8 +1258,11 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1248
1258
|
} else {
|
|
1249
1259
|
OneKeyLog.warn("BundleUpdate", "downloadBundle: existing file SHA256 mismatch, re-downloading")
|
|
1250
1260
|
downloadedFile.delete()
|
|
1251
|
-
// Stale completed file invalidates any partial too
|
|
1261
|
+
// Stale completed file invalidates any partial too — including
|
|
1262
|
+
// the concurrent segment files, otherwise the next resume would
|
|
1263
|
+
// pick up bytes belonging to the rejected build.
|
|
1252
1264
|
if (partialFile.exists()) partialFile.delete()
|
|
1265
|
+
for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
|
|
1253
1266
|
}
|
|
1254
1267
|
}
|
|
1255
1268
|
|
|
@@ -1261,16 +1274,22 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1261
1274
|
// downloader keeps its partial + manifest for resume).
|
|
1262
1275
|
sendEvent("update/start")
|
|
1263
1276
|
run {
|
|
1264
|
-
|
|
1277
|
+
// onProgress is invoked concurrently by all 8 worker threads, so
|
|
1278
|
+
// a plain `var` read-compare-write races (duplicate/out-of-order
|
|
1279
|
+
// progress events). Use AtomicInteger + CAS: only the thread that
|
|
1280
|
+
// wins the compareAndSet to a strictly higher value emits, which
|
|
1281
|
+
// also keeps progress monotonic. (Worst case a race only affects
|
|
1282
|
+
// progress eventing, never file bytes.)
|
|
1283
|
+
val concurrentProgress = AtomicInteger(-1)
|
|
1265
1284
|
val concurrentOutcome = ConcurrentRangeDownloader(
|
|
1266
1285
|
httpClient = httpClient,
|
|
1267
1286
|
log = { msg -> OneKeyLog.info("BundleUpdate", msg) },
|
|
1268
1287
|
).download(downloadUrl, partialFilePath) { transferred, total ->
|
|
1269
1288
|
if (total > 0) {
|
|
1270
1289
|
val p = ((transferred * 100) / total).toInt().coerceIn(0, 100)
|
|
1271
|
-
|
|
1290
|
+
val prev = concurrentProgress.get()
|
|
1291
|
+
if (p > prev && concurrentProgress.compareAndSet(prev, p)) {
|
|
1272
1292
|
sendEvent("update/downloading", progress = p)
|
|
1273
|
-
concurrentProgress = p
|
|
1274
1293
|
}
|
|
1275
1294
|
}
|
|
1276
1295
|
}
|
|
@@ -1304,7 +1323,14 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1304
1323
|
// before discarding so we save a full re-download.
|
|
1305
1324
|
val expectedSize = if (params.fileSize > 0) params.fileSize.toLong() else 0L
|
|
1306
1325
|
var partialBytes = 0L
|
|
1307
|
-
|
|
1326
|
+
// If any concurrent `.segN` files survive, the `.partial` here is the
|
|
1327
|
+
// concurrent committed cursor, not a single-stream partial. Defer to
|
|
1328
|
+
// the concurrent downloader (which already ran above and may resume on
|
|
1329
|
+
// a later attempt) and skip size-based promote/discard, which would
|
|
1330
|
+
// otherwise misjudge a bare `.partial` when the concurrent path
|
|
1331
|
+
// returned FALLBACK but left `.segN` residue. Mirrors app-update.
|
|
1332
|
+
val hasConcurrentSegments = (0 until CONCURRENT_SEGMENT_COUNT).any { File("$partialFilePath.seg$it").exists() }
|
|
1333
|
+
if (partialFile.exists() && !hasConcurrentSegments) {
|
|
1308
1334
|
val partialSize = partialFile.length()
|
|
1309
1335
|
when {
|
|
1310
1336
|
expectedSize > 0 && partialSize == expectedSize -> {
|
|
@@ -1325,6 +1351,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1325
1351
|
expectedSize > 0 && partialSize > expectedSize -> {
|
|
1326
1352
|
OneKeyLog.warn("BundleUpdate", "downloadBundle: stale partial (>expected), discarding: $partialSize/$expectedSize")
|
|
1327
1353
|
partialFile.delete()
|
|
1354
|
+
for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
|
|
1328
1355
|
}
|
|
1329
1356
|
partialSize > 0 -> {
|
|
1330
1357
|
partialBytes = partialSize
|
|
@@ -1369,6 +1396,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1369
1396
|
}
|
|
1370
1397
|
OneKeyLog.warn("BundleUpdate", "downloadBundle: HTTP 416 (range not satisfiable), discarding partial and failing this attempt")
|
|
1371
1398
|
if (partialFile.exists()) partialFile.delete()
|
|
1399
|
+
for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
|
|
1372
1400
|
// Don't pre-emit update/error here; the outer catch is the
|
|
1373
1401
|
// single source of error events. sanitizeErrorMessageForEvent
|
|
1374
1402
|
// recognizes "HTTP " prefix and forwards this string verbatim.
|
|
@@ -1376,7 +1404,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1376
1404
|
}
|
|
1377
1405
|
|
|
1378
1406
|
val expectsResume = partialBytes > 0
|
|
1379
|
-
|
|
1407
|
+
var isPartialResponse = response.code == 206
|
|
1380
1408
|
|
|
1381
1409
|
if (!response.isSuccessful || (response.code != 200 && response.code != 206)) {
|
|
1382
1410
|
OneKeyLog.error("BundleUpdate", "downloadBundle: HTTP error, statusCode=${response.code}")
|
|
@@ -1390,9 +1418,36 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1390
1418
|
if (expectsResume && !isPartialResponse) {
|
|
1391
1419
|
OneKeyLog.warn("BundleUpdate", "downloadBundle: requested Range but server returned 200, restarting from scratch")
|
|
1392
1420
|
if (partialFile.exists()) partialFile.delete()
|
|
1421
|
+
for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
|
|
1393
1422
|
partialBytes = 0L
|
|
1394
1423
|
}
|
|
1395
1424
|
|
|
1425
|
+
// On a 206 the server's `Content-Range` start MUST equal the offset
|
|
1426
|
+
// we asked to resume from (`partialBytes`). A misconfigured server or
|
|
1427
|
+
// proxy can return a 206 whose range starts somewhere else; appending
|
|
1428
|
+
// that slice onto our `.partial` would splice mismatched bytes and the
|
|
1429
|
+
// final SHA256 would fail only after a full download. Guard here: if
|
|
1430
|
+
// the start is missing or != partialBytes, drop the partial+segments
|
|
1431
|
+
// and abort this attempt. We must NOT reuse this body as a 200-style
|
|
1432
|
+
// full rewrite: a mismatched 206 body is still a range slice, not the
|
|
1433
|
+
// whole file, so writing it would produce a corrupt bundle. Close the
|
|
1434
|
+
// response and throw a retryable error — with partial+segN already
|
|
1435
|
+
// gone, the next attempt naturally restarts from 0.
|
|
1436
|
+
if (isPartialResponse && partialBytes > 0) {
|
|
1437
|
+
val contentRangeStart = response.header("Content-Range")
|
|
1438
|
+
?.let { Regex("""bytes\s+(\d+)-\d+/\d+""").find(it)?.groupValues?.getOrNull(1)?.toLongOrNull() }
|
|
1439
|
+
if (contentRangeStart == null || contentRangeStart != partialBytes) {
|
|
1440
|
+
OneKeyLog.warn(
|
|
1441
|
+
"BundleUpdate",
|
|
1442
|
+
"downloadBundle: 206 Content-Range start=$contentRangeStart != partialBytes=$partialBytes, discarding partial and aborting attempt"
|
|
1443
|
+
)
|
|
1444
|
+
if (partialFile.exists()) partialFile.delete()
|
|
1445
|
+
for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
|
|
1446
|
+
response.close()
|
|
1447
|
+
throw java.io.IOException("206 Content-Range start mismatch (got=$contentRangeStart, want=$partialBytes); discarded partial, retry from scratch")
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1396
1451
|
// Close the response before throwing on a null body — OkHttp
|
|
1397
1452
|
// holds connection resources on the response wrapper itself,
|
|
1398
1453
|
// and `throw` here exits the function before any byteStream()
|
|
@@ -1844,8 +1899,12 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
|
|
|
1844
1899
|
downloadDir.listFiles()?.forEach { file ->
|
|
1845
1900
|
val name = file.name
|
|
1846
1901
|
// Strip the trailing extension chain to recover the
|
|
1847
|
-
// "{appV}-{bV}" stem (e.g. "6.3.0-123.zip.partial"
|
|
1902
|
+
// "{appV}-{bV}" stem (e.g. "6.3.0-123.zip.partial", or a
|
|
1903
|
+
// concurrent segment file "6.3.0-123.zip.partial.seg3").
|
|
1848
1904
|
var stem = name
|
|
1905
|
+
// Concurrent segment files end in ".segN" — peel that off
|
|
1906
|
+
// first so the rest of the chain strips as usual.
|
|
1907
|
+
stem = stem.replace(Regex("""\.seg\d+$"""), "")
|
|
1849
1908
|
for (suffix in listOf(".resume", ".progress", ".partial", ".zip")) {
|
|
1850
1909
|
if (stem.endsWith(suffix)) {
|
|
1851
1910
|
stem = stem.substring(0, stem.length - suffix.length)
|