@onekeyfe/react-native-split-bundle-loader 3.0.17 → 3.0.19
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.
|
@@ -30,10 +30,87 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
30
30
|
// #18: Limit concurrent asset extractions to avoid I/O contention
|
|
31
31
|
private const val MAX_CONCURRENT_EXTRACTS = 2
|
|
32
32
|
private val extractSemaphore = Semaphore(MAX_CONCURRENT_EXTRACTS)
|
|
33
|
+
|
|
34
|
+
// Wipe-on-APK-replace: avoids stale extracted HBC after overwrite install.
|
|
35
|
+
// `lastUpdateTime` changes on every APK replacement (adb install -r,
|
|
36
|
+
// Play Store upgrade, sideload, TestFlight-equivalent). If it differs
|
|
37
|
+
// from what we persisted, nuke the whole extract tree so the new APK's
|
|
38
|
+
// assets get extracted fresh on first load.
|
|
39
|
+
private const val PREFS_NAME = "split_bundle_loader"
|
|
40
|
+
private const val KEY_LAST_INSTALL_STAMP = "last_install_stamp"
|
|
41
|
+
// Double-checked locking: loser threads must BLOCK until the wipe
|
|
42
|
+
// finishes, not just skip. AtomicBoolean.compareAndSet would let
|
|
43
|
+
// losers race ahead and read half-nuked state.
|
|
44
|
+
@Volatile private var wipeCheckDone: Boolean = false
|
|
45
|
+
private val wipeLock = Any()
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
override fun getName(): String = NAME
|
|
36
49
|
|
|
50
|
+
// -----------------------------------------------------------------------
|
|
51
|
+
// Wipe extract dir when APK install/upgrade detected.
|
|
52
|
+
//
|
|
53
|
+
// Without this, an overwrite install (adb install -r, Play Store upgrade,
|
|
54
|
+
// etc.) leaves last-run's extracted HBC files in /data/.../files/, and
|
|
55
|
+
// extractBuiltinSegmentIfNeeded reuses them because its size check can
|
|
56
|
+
// pass even when Metro module IDs drifted. Nuking the tree on every
|
|
57
|
+
// install-stamp change forces the new APK's assets to be re-extracted.
|
|
58
|
+
//
|
|
59
|
+
// Atomic gate means this runs at most once per process regardless of how
|
|
60
|
+
// many entry points call it.
|
|
61
|
+
// -----------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
private fun ensureExtractDirFreshForCurrentInstall(context: Context) {
|
|
64
|
+
// Fast path: already done, no locking needed (volatile read).
|
|
65
|
+
if (wipeCheckDone) return
|
|
66
|
+
synchronized(wipeLock) {
|
|
67
|
+
// Re-check inside the lock: another thread may have finished
|
|
68
|
+
// the wipe while we were waiting to enter.
|
|
69
|
+
if (wipeCheckDone) return
|
|
70
|
+
|
|
71
|
+
val currentStamp = try {
|
|
72
|
+
context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
|
|
73
|
+
} catch (e: Exception) {
|
|
74
|
+
SBLLogger.warn("[install-stamp] failed to read lastUpdateTime: ${e.message}")
|
|
75
|
+
// Mark done even on failure: retrying every call won't help and
|
|
76
|
+
// would defeat the gate. Conservative default is to NOT wipe.
|
|
77
|
+
wipeCheckDone = true
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
82
|
+
val savedStamp = prefs.getLong(KEY_LAST_INSTALL_STAMP, -1L)
|
|
83
|
+
if (savedStamp != currentStamp) {
|
|
84
|
+
val baseDir = File(context.filesDir, BUILTIN_EXTRACT_DIR)
|
|
85
|
+
if (baseDir.exists()) {
|
|
86
|
+
SBLLogger.info(
|
|
87
|
+
"[install-stamp] changed saved=$savedStamp current=$currentStamp, wiping ${baseDir.absolutePath}"
|
|
88
|
+
)
|
|
89
|
+
// rename-then-delete so the baseDir is gone atomically
|
|
90
|
+
// before we release the lock; waiters never see a half-nuked dir.
|
|
91
|
+
val tomb = File(
|
|
92
|
+
baseDir.parentFile,
|
|
93
|
+
".${BUILTIN_EXTRACT_DIR}.stale-${System.nanoTime()}"
|
|
94
|
+
)
|
|
95
|
+
if (baseDir.renameTo(tomb)) {
|
|
96
|
+
tomb.deleteRecursively()
|
|
97
|
+
} else {
|
|
98
|
+
baseDir.deleteRecursively()
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
SBLLogger.info(
|
|
102
|
+
"[install-stamp] first seen current=$currentStamp (no extract dir yet)"
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
// Persist synchronously (commit) so that a crash mid-wipe doesn't
|
|
106
|
+
// leave us with a stale stamp + still-wiped dir next launch.
|
|
107
|
+
prefs.edit().putLong(KEY_LAST_INSTALL_STAMP, currentStamp).commit()
|
|
108
|
+
}
|
|
109
|
+
// Publish via volatile write; fast path on other threads now sees true.
|
|
110
|
+
wipeCheckDone = true
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
37
114
|
// -----------------------------------------------------------------------
|
|
38
115
|
// getRuntimeBundleContext
|
|
39
116
|
// -----------------------------------------------------------------------
|
|
@@ -41,6 +118,7 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
41
118
|
override fun getRuntimeBundleContext(promise: Promise) {
|
|
42
119
|
try {
|
|
43
120
|
val context = reactApplicationContext
|
|
121
|
+
ensureExtractDirFreshForCurrentInstall(context)
|
|
44
122
|
val runtimeKind = "main"
|
|
45
123
|
var sourceKind = "builtin"
|
|
46
124
|
var bundleRoot = ""
|
|
@@ -91,6 +169,7 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
91
169
|
|
|
92
170
|
override fun resolveSegmentPath(relativePath: String, sha256: String, promise: Promise) {
|
|
93
171
|
try {
|
|
172
|
+
ensureExtractDirFreshForCurrentInstall(reactApplicationContext)
|
|
94
173
|
val absolutePath = resolveSegmentPath(relativePath, sha256)
|
|
95
174
|
if (absolutePath != null) {
|
|
96
175
|
promise.resolve(absolutePath)
|
|
@@ -121,6 +200,7 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
121
200
|
// segment integrity. Builtin segments are signed as part of the APK/IPA.
|
|
122
201
|
// If runtime SHA-256 verification is needed, add it here.
|
|
123
202
|
try {
|
|
203
|
+
ensureExtractDirFreshForCurrentInstall(reactApplicationContext)
|
|
124
204
|
val segId = segmentId.toInt()
|
|
125
205
|
|
|
126
206
|
val absolutePath = resolveSegmentPath(relativePath, sha256)
|
|
@@ -132,54 +212,21 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
132
212
|
return
|
|
133
213
|
}
|
|
134
214
|
|
|
135
|
-
//
|
|
136
|
-
//
|
|
215
|
+
// Use ReactContext.registerSegment which works in both bridge
|
|
216
|
+
// and bridgeless modes. In bridge mode it delegates to
|
|
217
|
+
// CatalystInstance; in bridgeless mode it delegates to ReactHost.
|
|
137
218
|
val reactContext = reactApplicationContext
|
|
138
219
|
val segStart = System.nanoTime()
|
|
139
|
-
|
|
140
|
-
reactContext.catalystInstance.registerSegment(segId, absolutePath)
|
|
220
|
+
reactContext.registerSegment(segId, absolutePath) {
|
|
141
221
|
val segMs = (System.nanoTime() - segStart) / 1_000_000.0
|
|
142
222
|
SBLLogger.info("[SplitBundle] segment $segmentKey (id=$segId) registered in ${String.format("%.1f", segMs)}ms")
|
|
143
223
|
promise.resolve(null)
|
|
144
|
-
} else {
|
|
145
|
-
// Bridgeless: try ReactHost via reflection
|
|
146
|
-
val registered = tryRegisterViaBridgeless(segId, absolutePath)
|
|
147
|
-
val segMs = (System.nanoTime() - segStart) / 1_000_000.0
|
|
148
|
-
if (registered) {
|
|
149
|
-
SBLLogger.info("[SplitBundle] segment $segmentKey (id=$segId) registered via bridgeless in ${String.format("%.1f", segMs)}ms")
|
|
150
|
-
promise.resolve(null)
|
|
151
|
-
} else {
|
|
152
|
-
promise.reject(
|
|
153
|
-
"SPLIT_BUNDLE_NO_INSTANCE",
|
|
154
|
-
"Neither CatalystInstance nor ReactHost available"
|
|
155
|
-
)
|
|
156
|
-
}
|
|
157
224
|
}
|
|
158
225
|
} catch (e: Exception) {
|
|
159
226
|
promise.reject("SPLIT_BUNDLE_LOAD_ERROR", e.message, e)
|
|
160
227
|
}
|
|
161
228
|
}
|
|
162
229
|
|
|
163
|
-
// -----------------------------------------------------------------------
|
|
164
|
-
// Bridgeless support (#19)
|
|
165
|
-
// -----------------------------------------------------------------------
|
|
166
|
-
|
|
167
|
-
private fun tryRegisterViaBridgeless(segmentId: Int, path: String): Boolean {
|
|
168
|
-
return try {
|
|
169
|
-
val appContext = reactApplicationContext.applicationContext
|
|
170
|
-
val appClass = appContext.javaClass
|
|
171
|
-
val hostMethod = appClass.getMethod("getReactHost")
|
|
172
|
-
val host = hostMethod.invoke(appContext) ?: return false
|
|
173
|
-
val registerMethod = host.javaClass.getMethod(
|
|
174
|
-
"registerSegment", Int::class.java, String::class.java
|
|
175
|
-
)
|
|
176
|
-
registerMethod.invoke(host, segmentId, path)
|
|
177
|
-
true
|
|
178
|
-
} catch (_: Exception) {
|
|
179
|
-
false
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
230
|
// -----------------------------------------------------------------------
|
|
184
231
|
// Path resolution helpers
|
|
185
232
|
// -----------------------------------------------------------------------
|
|
@@ -206,14 +253,27 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
206
253
|
val otaRoot = File(otaBundlePath).parentFile
|
|
207
254
|
if (otaRoot != null) {
|
|
208
255
|
val candidate = File(otaRoot, relativePath)
|
|
209
|
-
|
|
256
|
+
val exists = candidate.exists()
|
|
257
|
+
val withinRoot = isPathWithinRoot(otaRoot, candidate)
|
|
258
|
+
SBLLogger.info("[resolveSeg] rel=$relativePath ota root=${otaRoot.absolutePath} cand=${candidate.absolutePath} exists=$exists withinRoot=$withinRoot")
|
|
259
|
+
if (exists && withinRoot) {
|
|
210
260
|
return candidate.absolutePath
|
|
211
261
|
}
|
|
262
|
+
} else {
|
|
263
|
+
SBLLogger.info("[resolveSeg] rel=$relativePath otaBundlePath=$otaBundlePath parentFile=null")
|
|
212
264
|
}
|
|
265
|
+
} else {
|
|
266
|
+
SBLLogger.info("[resolveSeg] rel=$relativePath otaBundlePath=(empty) — skipping OTA")
|
|
213
267
|
}
|
|
214
268
|
|
|
215
269
|
// 2. Try builtin: extract from assets if needed
|
|
216
|
-
|
|
270
|
+
val result = extractBuiltinSegmentIfNeeded(relativePath, expectedSha256)
|
|
271
|
+
if (result == null) {
|
|
272
|
+
SBLLogger.warn("[resolveSeg] rel=$relativePath → null (builtin extract failed)")
|
|
273
|
+
} else {
|
|
274
|
+
SBLLogger.info("[resolveSeg] rel=$relativePath → builtin $result")
|
|
275
|
+
}
|
|
276
|
+
return result
|
|
217
277
|
}
|
|
218
278
|
|
|
219
279
|
/**
|
|
@@ -225,6 +285,7 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
225
285
|
*/
|
|
226
286
|
private fun extractBuiltinSegmentIfNeeded(relativePath: String, expectedSha256: String): String? {
|
|
227
287
|
val context = reactApplicationContext
|
|
288
|
+
ensureExtractDirFreshForCurrentInstall(context)
|
|
228
289
|
val nativeVersion = try {
|
|
229
290
|
context.packageManager
|
|
230
291
|
.getPackageInfo(context.packageName, 0).versionName ?: "unknown"
|
|
@@ -272,12 +333,15 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
272
333
|
}
|
|
273
334
|
// Atomic rename prevents partial file observation
|
|
274
335
|
if (tempFile.renameTo(extractedFile)) {
|
|
336
|
+
SBLLogger.info("[extractBuiltin] extracted $relativePath → ${extractedFile.absolutePath} (${extractedFile.length()} bytes)")
|
|
275
337
|
extractedFile.absolutePath
|
|
276
338
|
} else {
|
|
339
|
+
SBLLogger.warn("[extractBuiltin] rename failed for $relativePath: ${tempFile.absolutePath} → ${extractedFile.absolutePath}")
|
|
277
340
|
tempFile.delete()
|
|
278
341
|
null
|
|
279
342
|
}
|
|
280
|
-
} catch (
|
|
343
|
+
} catch (e: IOException) {
|
|
344
|
+
SBLLogger.warn("[extractBuiltin] IOException for $relativePath: ${e.javaClass.simpleName}: ${e.message}")
|
|
281
345
|
null
|
|
282
346
|
}
|
|
283
347
|
} finally {
|
package/ios/SplitBundleLoader.mm
CHANGED
|
@@ -130,22 +130,32 @@
|
|
|
130
130
|
// 1. Try OTA bundle root first
|
|
131
131
|
NSString *otaPath = [SplitBundleLoader otaBundlePath];
|
|
132
132
|
if (otaPath) {
|
|
133
|
-
|
|
133
|
+
// Standardize root so hasPrefix matches candidate (iOS resolves /private/var → /var).
|
|
134
|
+
NSString *otaRoot = [[otaPath stringByDeletingLastPathComponent] stringByStandardizingPath];
|
|
134
135
|
NSString *candidate = [[otaRoot stringByAppendingPathComponent:relativePath] stringByStandardizingPath];
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
BOOL otaPrefixOk = [candidate hasPrefix:otaRoot];
|
|
137
|
+
BOOL otaExists = [[NSFileManager defaultManager] fileExistsAtPath:candidate];
|
|
138
|
+
[SBLLogger info:[NSString stringWithFormat:@"[resolveAbs] rel=%@ ota root=%@ cand=%@ prefixOk=%d exists=%d",
|
|
139
|
+
relativePath, otaRoot, candidate, otaPrefixOk, otaExists]];
|
|
140
|
+
if (otaPrefixOk && otaExists) {
|
|
137
141
|
return candidate;
|
|
138
142
|
}
|
|
143
|
+
} else {
|
|
144
|
+
[SBLLogger info:[NSString stringWithFormat:@"[resolveAbs] rel=%@ otaPath=(nil) — skipping OTA", relativePath]];
|
|
139
145
|
}
|
|
140
146
|
|
|
141
147
|
// 2. Fallback to builtin resource path
|
|
142
|
-
NSString *builtinRoot = [[NSBundle mainBundle] resourcePath];
|
|
148
|
+
NSString *builtinRoot = [[[NSBundle mainBundle] resourcePath] stringByStandardizingPath];
|
|
143
149
|
NSString *candidate = [[builtinRoot stringByAppendingPathComponent:relativePath] stringByStandardizingPath];
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
BOOL builtinPrefixOk = [candidate hasPrefix:builtinRoot];
|
|
151
|
+
BOOL builtinExists = [[NSFileManager defaultManager] fileExistsAtPath:candidate];
|
|
152
|
+
[SBLLogger info:[NSString stringWithFormat:@"[resolveAbs] rel=%@ builtin root=%@ cand=%@ prefixOk=%d exists=%d",
|
|
153
|
+
relativePath, builtinRoot, candidate, builtinPrefixOk, builtinExists]];
|
|
154
|
+
if (builtinPrefixOk && builtinExists) {
|
|
146
155
|
return candidate;
|
|
147
156
|
}
|
|
148
157
|
|
|
158
|
+
[SBLLogger warn:[NSString stringWithFormat:@"[resolveAbs] rel=%@ → nil (not found in OTA nor builtin)", relativePath]];
|
|
149
159
|
return nil;
|
|
150
160
|
}
|
|
151
161
|
|