@hot-updater/react-native 0.27.1 → 0.29.0
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/build.gradle +12 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +3 -0
- package/android/src/main/cpp/CMakeLists.txt +9 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +143 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +325 -210
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +73 -16
- package/android/src/main/java/com/hotupdater/CohortService.kt +73 -0
- package/android/src/main/java/com/hotupdater/DecompressService.kt +28 -22
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +1 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +51 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +533 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryReceiver.kt +14 -0
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +55 -0
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +19 -7
- package/android/src/newarch/HotUpdaterModule.kt +16 -25
- package/android/src/oldarch/HotUpdaterModule.kt +20 -26
- package/android/src/oldarch/HotUpdaterSpec.kt +12 -2
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +340 -232
- package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -8
- package/ios/HotUpdater/Internal/CohortService.swift +63 -0
- package/ios/HotUpdater/Internal/DecompressService.swift +53 -30
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +9 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +376 -70
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.h +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.mm +4 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +321 -9
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +24 -8
- package/lib/commonjs/DefaultResolver.js +3 -5
- package/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/checkForUpdate.js +2 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +211 -39
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +443 -0
- package/lib/commonjs/native.spec.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/wrap.js +4 -5
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/DefaultResolver.js +3 -5
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/checkForUpdate.js +3 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +14 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +204 -34
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +442 -0
- package/lib/module/native.spec.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/wrap.js +5 -6
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +14 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +43 -23
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +32 -8
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +6 -3
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +3 -6
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +14 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +43 -23
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +32 -8
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +6 -3
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +3 -6
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/DefaultResolver.ts +4 -4
- package/src/checkForUpdate.ts +4 -0
- package/src/index.ts +21 -0
- package/src/native.spec.ts +480 -0
- package/src/native.ts +285 -39
- package/src/specs/NativeHotUpdater.ts +36 -6
- package/src/types.ts +7 -3
- package/src/wrap.tsx +8 -12
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.app.AlarmManager
|
|
4
|
+
import android.app.PendingIntent
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.os.Handler
|
|
8
|
+
import android.os.Looper
|
|
9
|
+
import android.os.Process
|
|
10
|
+
import android.os.SystemClock
|
|
11
|
+
import android.util.Log
|
|
12
|
+
import com.facebook.react.ReactApplication
|
|
13
|
+
import com.facebook.react.bridge.JSExceptionHandler
|
|
14
|
+
import com.facebook.react.bridge.ReactContext
|
|
15
|
+
import com.facebook.react.bridge.ReactMarker
|
|
16
|
+
import com.facebook.react.bridge.ReactMarkerConstants
|
|
17
|
+
import org.json.JSONObject
|
|
18
|
+
import java.io.File
|
|
19
|
+
import java.lang.reflect.Field
|
|
20
|
+
import kotlin.system.exitProcess
|
|
21
|
+
|
|
22
|
+
internal class HotUpdaterRecoveryManager(
|
|
23
|
+
context: Context,
|
|
24
|
+
) {
|
|
25
|
+
private val appContext = context.applicationContext
|
|
26
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
27
|
+
private val bundleStoreDir = getBundleStoreDir(appContext)
|
|
28
|
+
private val crashMarkerFile = File(bundleStoreDir, CRASH_MARKER_FILENAME)
|
|
29
|
+
private val watchdogStateFile = File(bundleStoreDir, WATCHDOG_STATE_FILENAME)
|
|
30
|
+
|
|
31
|
+
private var currentBundleId: String? = null
|
|
32
|
+
private var shouldRollbackOnCrash = false
|
|
33
|
+
private var isMonitoring = false
|
|
34
|
+
private var recoveryRequested = false
|
|
35
|
+
private var contentAppearedCallback: ((String?) -> Unit)? = null
|
|
36
|
+
private var recoveryRestartCallback: (() -> Boolean)? = null
|
|
37
|
+
|
|
38
|
+
private val stopMonitoringRunnable =
|
|
39
|
+
Runnable {
|
|
40
|
+
cancelRecoveryWatchdog()
|
|
41
|
+
Log.d(TAG, "Stopping crash monitoring for current launch")
|
|
42
|
+
isMonitoring = false
|
|
43
|
+
recoveryRequested = false
|
|
44
|
+
shouldRollbackOnCrash = false
|
|
45
|
+
currentBundleId = null
|
|
46
|
+
contentAppearedCallback = null
|
|
47
|
+
recoveryRestartCallback = null
|
|
48
|
+
activeManager = null
|
|
49
|
+
updateNativeLaunchState(null, false)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private val installJsExceptionHooksRunnable =
|
|
53
|
+
object : Runnable {
|
|
54
|
+
override fun run() {
|
|
55
|
+
if (!isMonitoring) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
installJavaScriptExceptionHooks()
|
|
60
|
+
mainHandler.postDelayed(this, JS_EXCEPTION_HOOK_RETRY_DELAY_MS)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private val contentAppearedListener =
|
|
65
|
+
ReactMarker.MarkerListener { name, _, _ ->
|
|
66
|
+
if (name == ReactMarkerConstants.CONTENT_APPEARED) {
|
|
67
|
+
handleContentAppeared()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fun consumePendingCrashRecovery(): PendingCrashRecovery? {
|
|
72
|
+
if (!crashMarkerFile.exists()) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return try {
|
|
77
|
+
val recovery = PendingCrashRecovery.fromJson(JSONObject(crashMarkerFile.readText()))
|
|
78
|
+
Log.d(
|
|
79
|
+
TAG,
|
|
80
|
+
"Consumed pending crash marker bundleId=${recovery.launchedBundleId} shouldRollback=${recovery.shouldRollback}",
|
|
81
|
+
)
|
|
82
|
+
recovery
|
|
83
|
+
} catch (e: Exception) {
|
|
84
|
+
Log.e(TAG, "Failed to read crash marker", e)
|
|
85
|
+
null
|
|
86
|
+
} finally {
|
|
87
|
+
crashMarkerFile.delete()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fun startMonitoring(
|
|
92
|
+
bundleId: String?,
|
|
93
|
+
shouldRollback: Boolean,
|
|
94
|
+
onContentAppeared: (String?) -> Unit,
|
|
95
|
+
onRecoveryRestartRequested: () -> Boolean,
|
|
96
|
+
) {
|
|
97
|
+
crashMarkerFile.parentFile?.mkdirs()
|
|
98
|
+
contentAppearedCallback = onContentAppeared
|
|
99
|
+
currentBundleId = bundleId
|
|
100
|
+
shouldRollbackOnCrash = shouldRollback
|
|
101
|
+
isMonitoring = true
|
|
102
|
+
recoveryRequested = false
|
|
103
|
+
recoveryRestartCallback = onRecoveryRestartRequested
|
|
104
|
+
activeManager = this
|
|
105
|
+
|
|
106
|
+
ensureExceptionHandlerInstalled()
|
|
107
|
+
ensureNativeSignalHandlerInstalled()
|
|
108
|
+
|
|
109
|
+
mainHandler.removeCallbacks(installJsExceptionHooksRunnable)
|
|
110
|
+
mainHandler.removeCallbacks(stopMonitoringRunnable)
|
|
111
|
+
ReactMarker.removeListener(contentAppearedListener)
|
|
112
|
+
ReactMarker.addListener(contentAppearedListener)
|
|
113
|
+
updateNativeLaunchState(bundleId, shouldRollback)
|
|
114
|
+
if (shouldRollback) {
|
|
115
|
+
startRecoveryWatchdog()
|
|
116
|
+
} else {
|
|
117
|
+
cancelRecoveryWatchdog()
|
|
118
|
+
}
|
|
119
|
+
mainHandler.post(installJsExceptionHooksRunnable)
|
|
120
|
+
|
|
121
|
+
Log.d(TAG, "Started crash monitoring bundleId=$bundleId shouldRollback=$shouldRollback")
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private fun handleContentAppeared() {
|
|
125
|
+
if (!isMonitoring) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
Log.d(TAG, "First content appeared for bundleId=$currentBundleId")
|
|
130
|
+
ReactMarker.removeListener(contentAppearedListener)
|
|
131
|
+
mainHandler.removeCallbacks(installJsExceptionHooksRunnable)
|
|
132
|
+
mainHandler.removeCallbacks(stopMonitoringRunnable)
|
|
133
|
+
contentAppearedCallback?.invoke(currentBundleId)
|
|
134
|
+
shouldRollbackOnCrash = false
|
|
135
|
+
updateNativeLaunchState(currentBundleId, false)
|
|
136
|
+
cancelRecoveryWatchdog()
|
|
137
|
+
mainHandler.postDelayed(stopMonitoringRunnable, MONITORING_GRACE_PERIOD_MS)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private fun ensureExceptionHandlerInstalled() {
|
|
141
|
+
if (exceptionHandlerInstalled) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
previousExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
|
|
146
|
+
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
|
147
|
+
val manager = activeManager
|
|
148
|
+
manager?.writeCrashMarker()
|
|
149
|
+
|
|
150
|
+
if (manager?.requestAutomaticRecovery() == true) {
|
|
151
|
+
Process.killProcess(Process.myPid())
|
|
152
|
+
exitProcess(10)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
previousExceptionHandler?.uncaughtException(thread, throwable)
|
|
156
|
+
?: run {
|
|
157
|
+
Process.killProcess(Process.myPid())
|
|
158
|
+
exitProcess(10)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
exceptionHandlerInstalled = true
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private fun writeCrashMarker() {
|
|
165
|
+
if (!isMonitoring) {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
val payload =
|
|
171
|
+
JSONObject().apply {
|
|
172
|
+
put("bundleId", currentBundleId ?: JSONObject.NULL)
|
|
173
|
+
put("shouldRollback", shouldRollbackOnCrash)
|
|
174
|
+
}
|
|
175
|
+
crashMarkerFile.parentFile?.mkdirs()
|
|
176
|
+
crashMarkerFile.writeText(payload.toString())
|
|
177
|
+
} catch (e: Exception) {
|
|
178
|
+
Log.e(TAG, "Failed to write crash marker", e)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private fun startRecoveryWatchdog() {
|
|
183
|
+
try {
|
|
184
|
+
watchdogStateFile.parentFile?.mkdirs()
|
|
185
|
+
watchdogStateFile.writeText((System.currentTimeMillis() + MONITORING_GRACE_PERIOD_MS).toString())
|
|
186
|
+
scheduleRecoveryWatchdogTick(appContext, WATCHDOG_TICK_INTERVAL_MS)
|
|
187
|
+
} catch (e: Exception) {
|
|
188
|
+
Log.e(TAG, "Failed to schedule recovery watchdog", e)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private fun cancelRecoveryWatchdog() {
|
|
193
|
+
watchdogStateFile.delete()
|
|
194
|
+
cancelRecoveryWatchdogAlarm(appContext)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private fun requestAutomaticRecovery(): Boolean {
|
|
198
|
+
if (!isMonitoring || !shouldRollbackOnCrash) {
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
synchronized(this) {
|
|
203
|
+
if (recoveryRequested) {
|
|
204
|
+
return true
|
|
205
|
+
}
|
|
206
|
+
recoveryRequested = true
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
val started = recoveryRestartCallback?.invoke() == true
|
|
210
|
+
if (!started) {
|
|
211
|
+
synchronized(this) {
|
|
212
|
+
recoveryRequested = false
|
|
213
|
+
}
|
|
214
|
+
Log.w(TAG, "Failed to schedule automatic recovery restart")
|
|
215
|
+
} else {
|
|
216
|
+
Log.i(TAG, "Scheduled automatic recovery restart for bundleId=$currentBundleId")
|
|
217
|
+
}
|
|
218
|
+
return started
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private fun handleJavaScriptException(exception: Exception): Boolean {
|
|
222
|
+
Log.e(TAG, "Caught React startup exception for bundleId=$currentBundleId", exception)
|
|
223
|
+
writeCrashMarker()
|
|
224
|
+
return requestAutomaticRecovery()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private fun ensureNativeSignalHandlerInstalled() {
|
|
228
|
+
if (signalHandlerInstalled || !loadNativeLibrary()) {
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
nativeInstallSignalHandler(crashMarkerFile.absolutePath)
|
|
234
|
+
signalHandlerInstalled = true
|
|
235
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
236
|
+
Log.w(TAG, "Signal handler not available", e)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private fun updateNativeLaunchState(
|
|
241
|
+
bundleId: String?,
|
|
242
|
+
shouldRollback: Boolean,
|
|
243
|
+
) {
|
|
244
|
+
if (!signalHandlerInstalled || !nativeLibraryLoaded) {
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
nativeUpdateLaunchState(bundleId, shouldRollback)
|
|
250
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
251
|
+
Log.w(TAG, "Failed to update native launch state", e)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private fun installJavaScriptExceptionHooks() {
|
|
256
|
+
val application = appContext as? ReactApplication ?: return
|
|
257
|
+
|
|
258
|
+
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
|
259
|
+
installReactHostExceptionHook(application)
|
|
260
|
+
} else {
|
|
261
|
+
installLegacyExceptionHook(application)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private fun installReactHostExceptionHook(application: ReactApplication) {
|
|
266
|
+
val reactHost = getReactHost(application) ?: return
|
|
267
|
+
val reactHostDelegate =
|
|
268
|
+
findField(reactHost.javaClass, "mReactHostDelegate")?.let { field ->
|
|
269
|
+
field.isAccessible = true
|
|
270
|
+
field.get(reactHost)
|
|
271
|
+
}
|
|
272
|
+
?: findField(reactHost.javaClass, "reactHostDelegate")?.let { field ->
|
|
273
|
+
field.isAccessible = true
|
|
274
|
+
field.get(reactHost)
|
|
275
|
+
}
|
|
276
|
+
?: return
|
|
277
|
+
|
|
278
|
+
val delegateIdentity = System.identityHashCode(reactHostDelegate)
|
|
279
|
+
if (patchedReactHostDelegateIds.add(delegateIdentity)) {
|
|
280
|
+
val exceptionHandlerField = findField(reactHostDelegate.javaClass, "exceptionHandler")
|
|
281
|
+
if (exceptionHandlerField == null) {
|
|
282
|
+
patchedReactHostDelegateIds.remove(delegateIdentity)
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
exceptionHandlerField.isAccessible = true
|
|
287
|
+
@Suppress("UNCHECKED_CAST")
|
|
288
|
+
val previousHandler =
|
|
289
|
+
exceptionHandlerField.get(reactHostDelegate) as? (Exception) -> Unit
|
|
290
|
+
|
|
291
|
+
exceptionHandlerField.set(reactHostDelegate) { exception: Exception ->
|
|
292
|
+
if (activeManager?.handleJavaScriptException(exception) != true) {
|
|
293
|
+
previousHandler?.invoke(exception) ?: throw exception
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
val reactContext =
|
|
299
|
+
findMethod(reactHost.javaClass, "getCurrentReactContext")?.invoke(reactHost) as? ReactContext
|
|
300
|
+
reactContext?.let { patchReactContextExceptionHandler(it) }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private fun installLegacyExceptionHook(application: ReactApplication) {
|
|
304
|
+
val instanceManager = application.reactNativeHost.reactInstanceManager
|
|
305
|
+
val managerIdentity = System.identityHashCode(instanceManager)
|
|
306
|
+
if (patchedInstanceManagerIds.add(managerIdentity)) {
|
|
307
|
+
val exceptionHandlerField = findField(instanceManager.javaClass, "mJSExceptionHandler")
|
|
308
|
+
if (exceptionHandlerField == null) {
|
|
309
|
+
patchedInstanceManagerIds.remove(managerIdentity)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
exceptionHandlerField.isAccessible = true
|
|
314
|
+
val previousHandler = exceptionHandlerField.get(instanceManager) as? JSExceptionHandler
|
|
315
|
+
exceptionHandlerField.set(instanceManager, RecoveryJSExceptionHandler(previousHandler))
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
instanceManager.currentReactContext?.let { reactContext ->
|
|
319
|
+
patchReactContextExceptionHandler(reactContext)
|
|
320
|
+
patchCatalystInstanceExceptionHandler(reactContext)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private fun patchReactContextExceptionHandler(reactContext: ReactContext) {
|
|
325
|
+
val contextIdentity = System.identityHashCode(reactContext)
|
|
326
|
+
if (!patchedReactContextIds.add(contextIdentity)) {
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
val previousHandler = reactContext.jsExceptionHandler
|
|
331
|
+
if (previousHandler is RecoveryJSExceptionHandler) {
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
reactContext.setJSExceptionHandler(RecoveryJSExceptionHandler(previousHandler))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private fun patchCatalystInstanceExceptionHandler(reactContext: ReactContext) {
|
|
339
|
+
val catalystInstance =
|
|
340
|
+
try {
|
|
341
|
+
reactContext.catalystInstance
|
|
342
|
+
} catch (_: Exception) {
|
|
343
|
+
null
|
|
344
|
+
} ?: return
|
|
345
|
+
|
|
346
|
+
val catalystIdentity = System.identityHashCode(catalystInstance)
|
|
347
|
+
if (!patchedCatalystInstanceIds.add(catalystIdentity)) {
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
val exceptionHandlerField = findField(catalystInstance.javaClass, "mJSExceptionHandler")
|
|
352
|
+
if (exceptionHandlerField == null) {
|
|
353
|
+
patchedCatalystInstanceIds.remove(catalystIdentity)
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
exceptionHandlerField.isAccessible = true
|
|
358
|
+
val previousHandler = exceptionHandlerField.get(catalystInstance) as? JSExceptionHandler
|
|
359
|
+
if (previousHandler is RecoveryJSExceptionHandler) {
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
exceptionHandlerField.set(catalystInstance, RecoveryJSExceptionHandler(previousHandler))
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private fun getReactHost(application: ReactApplication): Any? =
|
|
366
|
+
try {
|
|
367
|
+
findMethod(application.javaClass, "getReactHost")?.invoke(application)
|
|
368
|
+
} catch (_: Exception) {
|
|
369
|
+
null
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private fun findMethod(
|
|
373
|
+
clazz: Class<*>,
|
|
374
|
+
name: String,
|
|
375
|
+
) = runCatching { clazz.getMethod(name) }.getOrNull()
|
|
376
|
+
|
|
377
|
+
private fun findField(
|
|
378
|
+
clazz: Class<*>,
|
|
379
|
+
name: String,
|
|
380
|
+
): Field? {
|
|
381
|
+
var current: Class<*>? = clazz
|
|
382
|
+
while (true) {
|
|
383
|
+
val currentClass = current ?: break
|
|
384
|
+
runCatching { currentClass.getDeclaredField(name) }
|
|
385
|
+
.getOrNull()
|
|
386
|
+
?.let { return it }
|
|
387
|
+
current = currentClass.superclass
|
|
388
|
+
}
|
|
389
|
+
return null
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private fun loadNativeLibrary(): Boolean {
|
|
393
|
+
if (nativeLibraryLoadAttempted) {
|
|
394
|
+
return nativeLibraryLoaded
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
nativeLibraryLoadAttempted = true
|
|
398
|
+
nativeLibraryLoaded =
|
|
399
|
+
try {
|
|
400
|
+
System.loadLibrary("hotupdater_recovery")
|
|
401
|
+
true
|
|
402
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
403
|
+
Log.w(TAG, "Failed to load recovery native library", e)
|
|
404
|
+
false
|
|
405
|
+
}
|
|
406
|
+
return nativeLibraryLoaded
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private external fun nativeInstallSignalHandler(crashMarkerPath: String)
|
|
410
|
+
|
|
411
|
+
private external fun nativeUpdateLaunchState(
|
|
412
|
+
bundleId: String?,
|
|
413
|
+
shouldRollback: Boolean,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
private class RecoveryJSExceptionHandler(
|
|
417
|
+
private val previousHandler: JSExceptionHandler?,
|
|
418
|
+
) : JSExceptionHandler {
|
|
419
|
+
override fun handleException(e: Exception) {
|
|
420
|
+
if (activeManager?.handleJavaScriptException(e) != true) {
|
|
421
|
+
previousHandler?.handleException(e) ?: throw e
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
companion object {
|
|
427
|
+
private const val TAG = "HotUpdaterRecovery"
|
|
428
|
+
private const val CRASH_MARKER_FILENAME = "recovery-crash-marker.json"
|
|
429
|
+
private const val WATCHDOG_STATE_FILENAME = "recovery-watchdog-state.txt"
|
|
430
|
+
private const val WATCHDOG_ACTION = "com.hotupdater.RECOVERY_WATCHDOG"
|
|
431
|
+
private const val MONITORING_GRACE_PERIOD_MS = 10_000L
|
|
432
|
+
private const val WATCHDOG_TICK_INTERVAL_MS = 1_500L
|
|
433
|
+
private const val JS_EXCEPTION_HOOK_RETRY_DELAY_MS = 50L
|
|
434
|
+
|
|
435
|
+
@Volatile
|
|
436
|
+
private var nativeLibraryLoadAttempted = false
|
|
437
|
+
|
|
438
|
+
@Volatile
|
|
439
|
+
private var nativeLibraryLoaded = false
|
|
440
|
+
|
|
441
|
+
@Volatile
|
|
442
|
+
private var signalHandlerInstalled = false
|
|
443
|
+
|
|
444
|
+
@Volatile
|
|
445
|
+
private var exceptionHandlerInstalled = false
|
|
446
|
+
|
|
447
|
+
@Volatile
|
|
448
|
+
private var previousExceptionHandler: Thread.UncaughtExceptionHandler? = null
|
|
449
|
+
|
|
450
|
+
@Volatile
|
|
451
|
+
private var activeManager: HotUpdaterRecoveryManager? = null
|
|
452
|
+
|
|
453
|
+
private val patchedInstanceManagerIds = mutableSetOf<Int>()
|
|
454
|
+
private val patchedCatalystInstanceIds = mutableSetOf<Int>()
|
|
455
|
+
private val patchedReactContextIds = mutableSetOf<Int>()
|
|
456
|
+
private val patchedReactHostDelegateIds = mutableSetOf<Int>()
|
|
457
|
+
|
|
458
|
+
@JvmStatic
|
|
459
|
+
fun handleRecoveryWatchdog(context: Context) {
|
|
460
|
+
val appContext = context.applicationContext
|
|
461
|
+
val watchdogStateFile = File(getBundleStoreDir(appContext), WATCHDOG_STATE_FILENAME)
|
|
462
|
+
val deadlineAt = watchdogStateFile.takeIf(File::exists)?.readText()?.toLongOrNull()
|
|
463
|
+
if (deadlineAt == null) {
|
|
464
|
+
cancelRecoveryWatchdogAlarm(appContext)
|
|
465
|
+
watchdogStateFile.delete()
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
val crashMarkerFile = File(getBundleStoreDir(appContext), CRASH_MARKER_FILENAME)
|
|
470
|
+
if (crashMarkerFile.exists()) {
|
|
471
|
+
Log.i(TAG, "Recovery watchdog detected crash marker, relaunching app")
|
|
472
|
+
watchdogStateFile.delete()
|
|
473
|
+
cancelRecoveryWatchdogAlarm(appContext)
|
|
474
|
+
launchRecoveryRestart(appContext)
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (System.currentTimeMillis() >= deadlineAt) {
|
|
479
|
+
watchdogStateFile.delete()
|
|
480
|
+
cancelRecoveryWatchdogAlarm(appContext)
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
scheduleRecoveryWatchdogTick(appContext, WATCHDOG_TICK_INTERVAL_MS)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private fun getBundleStoreDir(context: Context): File = File(context.getExternalFilesDir(null) ?: context.filesDir, "bundle-store")
|
|
488
|
+
|
|
489
|
+
private fun getRecoveryWatchdogIntent(context: Context): Intent =
|
|
490
|
+
Intent(context, HotUpdaterRecoveryReceiver::class.java).setAction(WATCHDOG_ACTION)
|
|
491
|
+
|
|
492
|
+
private fun getRecoveryWatchdogPendingIntent(context: Context): PendingIntent =
|
|
493
|
+
PendingIntent.getBroadcast(
|
|
494
|
+
context,
|
|
495
|
+
0,
|
|
496
|
+
getRecoveryWatchdogIntent(context),
|
|
497
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
private fun scheduleRecoveryWatchdogTick(
|
|
501
|
+
context: Context,
|
|
502
|
+
delayMs: Long,
|
|
503
|
+
) {
|
|
504
|
+
val alarmManager = context.getSystemService(AlarmManager::class.java) ?: return
|
|
505
|
+
val triggerAt = SystemClock.elapsedRealtime() + delayMs
|
|
506
|
+
alarmManager.set(
|
|
507
|
+
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
|
508
|
+
triggerAt,
|
|
509
|
+
getRecoveryWatchdogPendingIntent(context),
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private fun cancelRecoveryWatchdogAlarm(context: Context) {
|
|
514
|
+
val alarmManager = context.getSystemService(AlarmManager::class.java) ?: return
|
|
515
|
+
alarmManager.cancel(getRecoveryWatchdogPendingIntent(context))
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private fun launchRecoveryRestart(context: Context) {
|
|
519
|
+
val restartIntent =
|
|
520
|
+
Intent(context, HotUpdaterRestartActivity::class.java).apply {
|
|
521
|
+
putExtra(HotUpdaterRestartActivity.EXTRA_PACKAGE_NAME, context.packageName)
|
|
522
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
523
|
+
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
context.startActivity(restartIntent)
|
|
528
|
+
} catch (e: Exception) {
|
|
529
|
+
Log.e(TAG, "Failed to launch recovery restart", e)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
|
|
7
|
+
class HotUpdaterRecoveryReceiver : BroadcastReceiver() {
|
|
8
|
+
override fun onReceive(
|
|
9
|
+
context: Context,
|
|
10
|
+
intent: Intent,
|
|
11
|
+
) {
|
|
12
|
+
HotUpdaterRecoveryManager.handleRecoveryWatchdog(context)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.WritableArray
|
|
4
|
+
import com.facebook.react.bridge.WritableMap
|
|
5
|
+
import com.facebook.react.bridge.WritableNativeArray
|
|
6
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
7
|
+
|
|
8
|
+
internal fun Map<String, Any?>.toWritableNativeMap(): WritableNativeMap {
|
|
9
|
+
val result = WritableNativeMap()
|
|
10
|
+
forEach { (key, value) ->
|
|
11
|
+
result.putReactValue(key, value)
|
|
12
|
+
}
|
|
13
|
+
return result
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
internal fun List<*>.toWritableNativeArray(): WritableNativeArray {
|
|
17
|
+
val result = WritableNativeArray()
|
|
18
|
+
forEach { value ->
|
|
19
|
+
result.pushReactValue(value)
|
|
20
|
+
}
|
|
21
|
+
return result
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private fun WritableMap.putReactValue(
|
|
25
|
+
key: String,
|
|
26
|
+
value: Any?,
|
|
27
|
+
) {
|
|
28
|
+
when (value) {
|
|
29
|
+
null -> putNull(key)
|
|
30
|
+
is Boolean -> putBoolean(key, value)
|
|
31
|
+
is Number -> putDouble(key, value.toDouble())
|
|
32
|
+
is String -> putString(key, value)
|
|
33
|
+
is Map<*, *> -> {
|
|
34
|
+
@Suppress("UNCHECKED_CAST")
|
|
35
|
+
putMap(key, (value as Map<String, Any?>).toWritableNativeMap())
|
|
36
|
+
}
|
|
37
|
+
is List<*> -> putArray(key, value.toWritableNativeArray())
|
|
38
|
+
else -> putString(key, value.toString())
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private fun WritableArray.pushReactValue(value: Any?) {
|
|
43
|
+
when (value) {
|
|
44
|
+
null -> pushNull()
|
|
45
|
+
is Boolean -> pushBoolean(value)
|
|
46
|
+
is Number -> pushDouble(value.toDouble())
|
|
47
|
+
is String -> pushString(value)
|
|
48
|
+
is Map<*, *> -> {
|
|
49
|
+
@Suppress("UNCHECKED_CAST")
|
|
50
|
+
pushMap((value as Map<String, Any?>).toWritableNativeMap())
|
|
51
|
+
}
|
|
52
|
+
is List<*> -> pushArray(value.toWritableNativeArray())
|
|
53
|
+
else -> pushString(value.toString())
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -25,15 +25,27 @@ class TarBrDecompressionStrategy : DecompressionStrategy {
|
|
|
25
25
|
return false
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
return try {
|
|
29
|
+
FileInputStream(file).use { fileInputStream ->
|
|
30
|
+
BufferedInputStream(fileInputStream).use { bufferedInputStream ->
|
|
31
|
+
BrotliInputStream(bufferedInputStream).use { brotliInputStream ->
|
|
32
|
+
TarArchiveInputStream(brotliInputStream).use { tarInputStream ->
|
|
33
|
+
val firstEntry = tarInputStream.getNextEntry()
|
|
34
|
+
val isValid = firstEntry != null
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
if (!isValid) {
|
|
37
|
+
Log.d(TAG, "Invalid file: tar archive has no entries")
|
|
38
|
+
}
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
isValid
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (e: Exception) {
|
|
46
|
+
Log.d(TAG, "Invalid file: Brotli/TAR validation failed - ${e.message}")
|
|
47
|
+
false
|
|
48
|
+
}
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
override fun decompress(
|
|
@@ -19,6 +19,7 @@ class HotUpdaterModule internal constructor(
|
|
|
19
19
|
reactContext: ReactApplicationContext,
|
|
20
20
|
) : HotUpdaterSpec(reactContext) {
|
|
21
21
|
private val mReactApplicationContext: ReactApplicationContext = reactContext
|
|
22
|
+
private val cohortService = CohortService(reactContext)
|
|
22
23
|
|
|
23
24
|
// Managed coroutine scope for the module lifecycle
|
|
24
25
|
private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
@@ -145,32 +146,9 @@ class HotUpdaterModule internal constructor(
|
|
|
145
146
|
// No-op
|
|
146
147
|
}
|
|
147
148
|
|
|
148
|
-
override fun notifyAppReady(
|
|
149
|
-
val result = WritableNativeMap()
|
|
150
|
-
val bundleId = params.getString("bundleId")
|
|
151
|
-
if (bundleId == null) {
|
|
152
|
-
result.putString("status", "STABLE")
|
|
153
|
-
return result
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
val impl = getInstance()
|
|
157
|
-
val statusMap = impl.notifyAppReady(bundleId)
|
|
158
|
-
|
|
159
|
-
result.putString("status", statusMap["status"] as? String ?: "STABLE")
|
|
160
|
-
statusMap["crashedBundleId"]?.let {
|
|
161
|
-
result.putString("crashedBundleId", it as String)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return result
|
|
165
|
-
}
|
|
149
|
+
override fun notifyAppReady(): WritableNativeMap = getInstance().notifyAppReady().toWritableNativeMap()
|
|
166
150
|
|
|
167
|
-
override fun getCrashHistory(): WritableNativeArray
|
|
168
|
-
val impl = getInstance()
|
|
169
|
-
val crashHistory = impl.getCrashHistory()
|
|
170
|
-
val result = WritableNativeArray()
|
|
171
|
-
crashHistory.forEach { result.pushString(it) }
|
|
172
|
-
return result
|
|
173
|
-
}
|
|
151
|
+
override fun getCrashHistory(): WritableNativeArray = getInstance().getCrashHistory().toWritableNativeArray()
|
|
174
152
|
|
|
175
153
|
override fun clearCrashHistory(): Boolean {
|
|
176
154
|
val impl = getInstance()
|
|
@@ -182,6 +160,19 @@ class HotUpdaterModule internal constructor(
|
|
|
182
160
|
return impl.getBaseURL()
|
|
183
161
|
}
|
|
184
162
|
|
|
163
|
+
override fun setCohort(cohort: String) {
|
|
164
|
+
cohortService.setCohort(cohort)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
override fun getCohort(): String = cohortService.getCohort()
|
|
168
|
+
|
|
169
|
+
override fun getBundleId(): String? {
|
|
170
|
+
val impl = getInstance()
|
|
171
|
+
return impl.getBundleId()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
override fun getManifest(): WritableNativeMap = getInstance().getManifest().toWritableNativeMap()
|
|
175
|
+
|
|
185
176
|
override fun resetChannel(promise: Promise) {
|
|
186
177
|
moduleScope.launch {
|
|
187
178
|
try {
|