@onekeyfe/react-native-split-bundle-loader 3.0.135 → 3.0.137
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.
|
@@ -13,6 +13,7 @@ import java.io.FileInputStream
|
|
|
13
13
|
import java.io.FileOutputStream
|
|
14
14
|
import java.io.IOException
|
|
15
15
|
import java.security.MessageDigest
|
|
16
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
16
17
|
import java.util.concurrent.Semaphore
|
|
17
18
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
18
19
|
import android.os.Handler
|
|
@@ -94,6 +95,18 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
94
95
|
private const val MAX_CONCURRENT_EXTRACTS = 2
|
|
95
96
|
private val extractSemaphore = Semaphore(MAX_CONCURRENT_EXTRACTS)
|
|
96
97
|
|
|
98
|
+
// Per-path extraction locks. The semaphore above is an I/O THROTTLE,
|
|
99
|
+
// not mutual exclusion: with N permits, N threads can extract the SAME
|
|
100
|
+
// relativePath at once. Main and background runtimes resolve segments
|
|
101
|
+
// independently, so two concurrent extractions of one segment is the
|
|
102
|
+
// normal case on the first launch after an APK replace (the
|
|
103
|
+
// install-stamp wipe empties the whole tree, so every segment misses).
|
|
104
|
+
// Serializing per path means exactly one thread extracts and the rest
|
|
105
|
+
// wake up on the exists() fast path. Entries are never removed:
|
|
106
|
+
// dropping one would let a waiter lock an Object another thread has
|
|
107
|
+
// already replaced, and the map is bounded by the segment count.
|
|
108
|
+
private val extractPathLocks = ConcurrentHashMap<String, Any>()
|
|
109
|
+
|
|
97
110
|
// Wipe-on-APK-replace: avoids stale extracted HBC after overwrite install.
|
|
98
111
|
// `lastUpdateTime` changes on every APK replacement (adb install -r,
|
|
99
112
|
// Play Store upgrade, sideload, TestFlight-equivalent). If it differs
|
|
@@ -479,56 +492,101 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
479
492
|
val extractDir = File(context.filesDir, "$BUILTIN_EXTRACT_DIR/$nativeVersion")
|
|
480
493
|
val extractedFile = File(extractDir, relativePath)
|
|
481
494
|
|
|
482
|
-
// #16:
|
|
495
|
+
// #16: A complete extraction is reusable with no locking at all. Only
|
|
496
|
+
// the happy case short-circuits here — a size mismatch deliberately
|
|
497
|
+
// falls through to the lock rather than deleting, because deleting
|
|
498
|
+
// outside the per-path lock can destroy a file another thread has just
|
|
499
|
+
// published (its writer would then hand out a path we unlinked).
|
|
483
500
|
if (extractedFile.exists()) {
|
|
484
501
|
val assetSize = getAssetSize(context.assets, relativePath)
|
|
485
502
|
if (assetSize >= 0 && extractedFile.length() == assetSize) {
|
|
486
503
|
return extractedFile.absolutePath
|
|
487
504
|
}
|
|
488
|
-
// Truncated or size mismatch — delete and re-extract
|
|
489
|
-
SBLLogger.warn("Extracted file size mismatch for $relativePath, re-extracting")
|
|
490
|
-
extractedFile.delete()
|
|
491
505
|
}
|
|
492
506
|
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
507
|
+
// Serialize every extraction of THIS path (see extractPathLocks). The
|
|
508
|
+
// lock is taken OUTSIDE the semaphore so a thread waiting here never
|
|
509
|
+
// sits on a permit it isn't using, and so the re-check below can hand
|
|
510
|
+
// back an already-extracted file without spending one.
|
|
511
|
+
synchronized(extractPathLocks.computeIfAbsent(relativePath) { Any() }) {
|
|
512
|
+
// Re-check under the lock: another thread may have extracted while
|
|
513
|
+
// we waited. Nothing in this process can be mid-publish for this
|
|
514
|
+
// path now, so a stale file is safe to delete and re-extract.
|
|
497
515
|
if (extractedFile.exists()) {
|
|
498
|
-
|
|
516
|
+
val assetSize = getAssetSize(context.assets, relativePath)
|
|
517
|
+
if (assetSize >= 0 && extractedFile.length() == assetSize) {
|
|
518
|
+
return extractedFile.absolutePath
|
|
519
|
+
}
|
|
520
|
+
SBLLogger.warn("Extracted file size mismatch for $relativePath, re-extracting")
|
|
521
|
+
extractedFile.delete()
|
|
499
522
|
}
|
|
500
523
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
524
|
+
// #18: Limit concurrent extractions
|
|
525
|
+
extractSemaphore.acquire()
|
|
526
|
+
// Extract to a temp file first, then atomically rename. The temp
|
|
527
|
+
// name must be UNIQUE per attempt: a shared "<name>.tmp" lets two
|
|
528
|
+
// writers open the same file with O_TRUNC and interleave into each
|
|
529
|
+
// other's stream, so the winner can publish a partially zeroed HBC
|
|
530
|
+
// while the loser's renameTo fails on a source that was already
|
|
531
|
+
// moved away. extractPathLocks covers this process; the unique name
|
|
532
|
+
// keeps it safe across processes too. Because a unique temp file is
|
|
533
|
+
// never reused by a later attempt, it MUST be cleaned up on every
|
|
534
|
+
// failure path or partial writes accumulate until the next APK
|
|
535
|
+
// replace — hence the finally below.
|
|
536
|
+
val tempFile = File(
|
|
537
|
+
extractedFile.parentFile,
|
|
538
|
+
"${extractedFile.name}.${android.os.Process.myTid()}-${System.nanoTime()}.tmp"
|
|
539
|
+
)
|
|
540
|
+
try {
|
|
541
|
+
val assets: AssetManager = context.assets
|
|
542
|
+
return try {
|
|
543
|
+
var written = 0L
|
|
544
|
+
assets.open(relativePath).use { input ->
|
|
545
|
+
extractedFile.parentFile?.let { parent ->
|
|
546
|
+
if (!parent.exists()) parent.mkdirs()
|
|
547
|
+
}
|
|
548
|
+
FileOutputStream(tempFile).use { output ->
|
|
549
|
+
val buffer = ByteArray(8192)
|
|
550
|
+
var len: Int
|
|
551
|
+
while (input.read(buffer).also { len = it } != -1) {
|
|
552
|
+
output.write(buffer, 0, len)
|
|
553
|
+
written += len
|
|
554
|
+
}
|
|
555
|
+
}
|
|
508
556
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
557
|
+
// Atomic rename prevents partial file observation
|
|
558
|
+
if (tempFile.renameTo(extractedFile)) {
|
|
559
|
+
SBLLogger.info("[extractBuiltin] extracted $relativePath → ${extractedFile.absolutePath} (${extractedFile.length()} bytes)")
|
|
560
|
+
extractedFile.absolutePath
|
|
561
|
+
} else {
|
|
562
|
+
// A failed rename is NOT proof the segment is missing:
|
|
563
|
+
// another process may have published the same bytes at
|
|
564
|
+
// the destination. Returning null here would turn a
|
|
565
|
+
// transient race into SPLIT_BUNDLE_NOT_FOUND, which the
|
|
566
|
+
// JS loader caches as a permanent failure. Compare
|
|
567
|
+
// against what we just wrote rather than re-reading the
|
|
568
|
+
// asset, and require an exact match — accepting an
|
|
569
|
+
// unknown size would be laxer than the checks above.
|
|
570
|
+
if (extractedFile.exists() && extractedFile.length() == written) {
|
|
571
|
+
SBLLogger.warn("[extractBuiltin] rename lost the race for $relativePath, using file published by another writer: ${extractedFile.absolutePath} ($written bytes)")
|
|
572
|
+
extractedFile.absolutePath
|
|
573
|
+
} else {
|
|
574
|
+
SBLLogger.warn("[extractBuiltin] rename failed for $relativePath: ${tempFile.absolutePath} → ${extractedFile.absolutePath}")
|
|
575
|
+
null
|
|
514
576
|
}
|
|
515
577
|
}
|
|
578
|
+
} catch (e: IOException) {
|
|
579
|
+
SBLLogger.warn("[extractBuiltin] IOException for $relativePath: ${e.javaClass.simpleName}: ${e.message}")
|
|
580
|
+
null
|
|
516
581
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
} else {
|
|
522
|
-
SBLLogger.warn("[extractBuiltin] rename failed for $relativePath: ${tempFile.absolutePath} → ${extractedFile.absolutePath}")
|
|
582
|
+
} finally {
|
|
583
|
+
// No-op after a successful rename (the temp file is gone);
|
|
584
|
+
// reclaims the partial write on every other path.
|
|
585
|
+
if (tempFile.exists()) {
|
|
523
586
|
tempFile.delete()
|
|
524
|
-
null
|
|
525
587
|
}
|
|
526
|
-
|
|
527
|
-
SBLLogger.warn("[extractBuiltin] IOException for $relativePath: ${e.javaClass.simpleName}: ${e.message}")
|
|
528
|
-
null
|
|
588
|
+
extractSemaphore.release()
|
|
529
589
|
}
|
|
530
|
-
} finally {
|
|
531
|
-
extractSemaphore.release()
|
|
532
590
|
}
|
|
533
591
|
}
|
|
534
592
|
|
package/package.json
CHANGED