@hot-updater/react-native 1.0.7 → 1.1.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/HotUpdater.podspec +14 -2
- package/Package.swift +24 -3
- package/android/build.gradle +2 -6
- package/android/hot-updater-fingerprint.gradle +135 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
- package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
- package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
- package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
- package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
- package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +492 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
- package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
- package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
- package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
- package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
- package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
- package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
- package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
- package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
- package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
- package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
- package/android/src/newarch/HotUpdater.kt +53 -15
- package/android/src/newarch/HotUpdaterModule.kt +42 -17
- package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
- package/android/src/oldarch/HotUpdater.kt +51 -12
- package/android/src/oldarch/HotUpdaterModule.kt +43 -17
- package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
- package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
- package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
- package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
- package/ios/HotUpdater/Internal/CohortService.swift +8 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
- package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
- package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
- package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
- package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
- package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
- package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
- package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
- package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
- package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
- package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
- package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
- package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
- package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
- package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
- package/ios/HotUpdater/Package.swift +11 -0
- package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
- package/ios/HotUpdater/Public/HotUpdater.h +21 -2
- package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
- package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
- package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
- package/lib/commonjs/checkForUpdate.js +3 -1
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +9 -12
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +24 -79
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/nativeUpdateEvents.js +66 -0
- package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +3 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +10 -13
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +19 -73
- package/lib/module/native.js.map +1 -1
- package/lib/module/nativeUpdateEvents.js +61 -0
- package/lib/module/nativeUpdateEvents.js.map +1 -0
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -14
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +9 -46
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -14
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +9 -46
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/checkForUpdate.ts +7 -1
- package/src/index.ts +9 -17
- package/src/native.ts +19 -91
- package/src/nativeUpdateEvents.ts +77 -0
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +20 -3
- package/android/libs/README.md +0 -50
- package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
- package/android/src/bridge/BridgeReactInstance.kt +0 -38
- package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
- package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
- package/android/src/newarch/ReactIntegrationManager.kt +0 -185
- package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
package/HotUpdater.podspec
CHANGED
|
@@ -16,12 +16,14 @@ Pod::Spec.new do |s|
|
|
|
16
16
|
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
17
17
|
s.public_header_files = "ios/HotUpdater/Public/*.h"
|
|
18
18
|
s.private_header_files = "ios/HotUpdater/Internal/*.h"
|
|
19
|
-
# ios/HotUpdater/Interface, ios/Plugins
|
|
20
|
-
# for Swift Package Manager builds
|
|
19
|
+
# ios/HotUpdater/Interface, ios/HotUpdater/Public/HotUpdater, ios/Plugins
|
|
20
|
+
# and HotUpdaterInterface.swift are for Swift Package Manager builds
|
|
21
|
+
# (Package.swift) only.
|
|
21
22
|
s.exclude_files = [
|
|
22
23
|
"ios/HotUpdater/Package.swift",
|
|
23
24
|
"ios/HotUpdater/Test/**/*.{swift,h,m,mm}",
|
|
24
25
|
"ios/HotUpdater/Interface/**/*",
|
|
26
|
+
"ios/HotUpdater/Public/HotUpdater/**/*",
|
|
25
27
|
"ios/HotUpdater/Internal/HotUpdaterInterface.swift",
|
|
26
28
|
"ios/Plugins/**/*"
|
|
27
29
|
]
|
|
@@ -43,6 +45,16 @@ Pod::Spec.new do |s|
|
|
|
43
45
|
:execution_position => :before_compile,
|
|
44
46
|
:output_files => ["$(DERIVED_FILE_DIR)/HotUpdaterBuildMinBundleId.h"],
|
|
45
47
|
:always_out_of_date => "1"
|
|
48
|
+
}, {
|
|
49
|
+
# Every build, before compiling, writes the fingerprint of the native
|
|
50
|
+
# interface the app builds with into the header
|
|
51
|
+
# HotUpdaterFingerprintHash.m includes.
|
|
52
|
+
:name => "[HotUpdater] Generate fingerprint",
|
|
53
|
+
:script => File.read(File.join(__dir__, "ios/scripts/generate-fingerprint-hash.zsh")),
|
|
54
|
+
:shell_path => "/bin/zsh",
|
|
55
|
+
:execution_position => :before_compile,
|
|
56
|
+
:output_files => ["$(DERIVED_FILE_DIR)/HotUpdaterBuildFingerprintHash.h"],
|
|
57
|
+
:always_out_of_date => "1"
|
|
46
58
|
}]
|
|
47
59
|
|
|
48
60
|
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
|
package/Package.swift
CHANGED
|
@@ -11,10 +11,11 @@ import PackageDescription
|
|
|
11
11
|
// relative to that symlink.
|
|
12
12
|
//
|
|
13
13
|
// SwiftPM compiles one language per target, so the module is split:
|
|
14
|
-
// HotUpdaterSwift holds the Swift implementation, and
|
|
14
|
+
// HotUpdaterSwift holds the Swift implementation, and HotUpdaterObjC, the
|
|
15
15
|
// Objective-C++ module React Native loads, calls it through the protocols in
|
|
16
16
|
// HotUpdaterInterface (see HotUpdaterInterface.h for why). Swift reaches the
|
|
17
|
-
// Objective-C++ side through `@_silgen_name` symbols only.
|
|
17
|
+
// Objective-C++ side through `@_silgen_name` symbols only. HotUpdater, the
|
|
18
|
+
// module apps import, re-exports both and adds the async Swift API.
|
|
18
19
|
let reactNativeDependencies: [Target.Dependency] = [
|
|
19
20
|
.product(name: "ReactHeaders", package: "ReactNative"),
|
|
20
21
|
.product(name: "ReactNativeHeaders", package: "ReactNative"),
|
|
@@ -39,8 +40,19 @@ let package = Package(
|
|
|
39
40
|
.package(name: "React-GeneratedCode", path: "../../../ios"),
|
|
40
41
|
],
|
|
41
42
|
targets: [
|
|
43
|
+
// The module apps import: re-exports HotUpdaterObjC and
|
|
44
|
+
// HotUpdaterSwift, and adds the async API, which needs both.
|
|
42
45
|
.target(
|
|
43
46
|
name: "HotUpdater",
|
|
47
|
+
dependencies: [
|
|
48
|
+
"HotUpdaterObjC",
|
|
49
|
+
"HotUpdaterSwift",
|
|
50
|
+
] + reactNativeDependencies,
|
|
51
|
+
path: "ios/HotUpdater/Internal",
|
|
52
|
+
sources: ["HotUpdaterAsync.swift"]
|
|
53
|
+
),
|
|
54
|
+
.target(
|
|
55
|
+
name: "HotUpdaterObjC",
|
|
44
56
|
dependencies: [
|
|
45
57
|
"HotUpdaterInterface",
|
|
46
58
|
"HotUpdaterSwift",
|
|
@@ -63,9 +75,11 @@ let package = Package(
|
|
|
63
75
|
"BsdiffPatchBridge.mm",
|
|
64
76
|
"HotUpdater-Bridging-Header.h",
|
|
65
77
|
"HotUpdater.mm",
|
|
78
|
+
"HotUpdaterAsync.swift",
|
|
66
79
|
"HotUpdaterMinBundleId.m",
|
|
80
|
+
"HotUpdaterFingerprintHash.m",
|
|
67
81
|
],
|
|
68
|
-
plugins: ["HotUpdaterMinBundleIdPlugin"]
|
|
82
|
+
plugins: ["HotUpdaterMinBundleIdPlugin", "HotUpdaterFingerprintPlugin"]
|
|
69
83
|
),
|
|
70
84
|
.target(
|
|
71
85
|
name: "HotUpdaterInterface",
|
|
@@ -94,6 +108,13 @@ let package = Package(
|
|
|
94
108
|
capability: .buildTool(),
|
|
95
109
|
path: "ios/Plugins/HotUpdaterMinBundleIdPlugin"
|
|
96
110
|
),
|
|
111
|
+
// Writes the fingerprint of the native interface the app builds
|
|
112
|
+
// with, as the podspec's script phase does for CocoaPods.
|
|
113
|
+
.plugin(
|
|
114
|
+
name: "HotUpdaterFingerprintPlugin",
|
|
115
|
+
capability: .buildTool(),
|
|
116
|
+
path: "ios/Plugins/HotUpdaterFingerprintPlugin"
|
|
117
|
+
),
|
|
97
118
|
],
|
|
98
119
|
swiftLanguageModes: [.v5],
|
|
99
120
|
cxxLanguageStandard: .cxx20
|
package/android/build.gradle
CHANGED
|
@@ -116,11 +116,7 @@ android {
|
|
|
116
116
|
"src/newarch",
|
|
117
117
|
// Codegen specs
|
|
118
118
|
"generated/java",
|
|
119
|
-
"generated/jni"
|
|
120
|
-
// React Native 0.81 and older can run the New Architecture on the
|
|
121
|
-
// bridge. Later versions have none, so their builds never compile
|
|
122
|
-
// ReactNativeHost.
|
|
123
|
-
project.ext.reactNativeMinorVersion >= 82 ? "src/bridgeless" : "src/bridge"
|
|
119
|
+
"generated/jni"
|
|
124
120
|
]
|
|
125
121
|
}
|
|
126
122
|
}
|
|
@@ -134,6 +130,7 @@ android {
|
|
|
134
130
|
}
|
|
135
131
|
|
|
136
132
|
apply from: "$projectDir/hot-updater-min-bundle-id.gradle"
|
|
133
|
+
apply from: "$projectDir/hot-updater-fingerprint.gradle"
|
|
137
134
|
|
|
138
135
|
repositories {
|
|
139
136
|
mavenCentral()
|
|
@@ -160,7 +157,6 @@ dependencies {
|
|
|
160
157
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
|
161
158
|
implementation "org.apache.commons:commons-compress:1.27.1"
|
|
162
159
|
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
|
163
|
-
implementation files('libs/hot-updater-brotli-dec-1.2.0.jar')
|
|
164
160
|
testImplementation "junit:junit:4.13.2"
|
|
165
161
|
testImplementation "org.json:json:20240303"
|
|
166
162
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Writes the fingerprint each build variant embeds into a generated source, at
|
|
2
|
+
// execution time on every build, so neither an up-to-date check nor the
|
|
3
|
+
// configuration cache keeps an earlier build's value.
|
|
4
|
+
//
|
|
5
|
+
// The app root is the HOT_UPDATER_APP_ROOT Gradle property when set (relative
|
|
6
|
+
// to the Android project), else the directory above the Android project
|
|
7
|
+
// (<app>/android). Without a
|
|
8
|
+
// fingerprint.json there, the app uses the appVersion strategy and the
|
|
9
|
+
// fingerprint is empty. The debug build type embeds the hash fingerprint.json
|
|
10
|
+
// holds; any other runs `hot-updater fingerprint hash`, which computes it from
|
|
11
|
+
// the inputs fingerprint.json records, so the binary carries the fingerprint
|
|
12
|
+
// of the native interface it was built from.
|
|
13
|
+
abstract class GenerateHotUpdaterFingerprintHashTask extends DefaultTask {
|
|
14
|
+
@Input
|
|
15
|
+
abstract Property<String> getAppRoot()
|
|
16
|
+
|
|
17
|
+
@Input
|
|
18
|
+
abstract Property<Boolean> getRecordedOnly()
|
|
19
|
+
|
|
20
|
+
@Input
|
|
21
|
+
abstract Property<String> getNodeBinary()
|
|
22
|
+
|
|
23
|
+
@OutputDirectory
|
|
24
|
+
abstract DirectoryProperty getOutputDirectory()
|
|
25
|
+
|
|
26
|
+
private String run(List<String> command, File workingDirectory) {
|
|
27
|
+
def builder = new ProcessBuilder(command).directory(workingDirectory)
|
|
28
|
+
// Build adapters that spawn npm's tools, such as Rock's `npx rock config`,
|
|
29
|
+
// find them beside a node given by path.
|
|
30
|
+
def executable = new File(command[0])
|
|
31
|
+
if (executable.isAbsolute()) {
|
|
32
|
+
def environment = builder.environment()
|
|
33
|
+
environment.put("PATH", executable.parent + File.pathSeparator + (environment.get("PATH") ?: ""))
|
|
34
|
+
}
|
|
35
|
+
def process = builder.start()
|
|
36
|
+
def stdout = new StringBuilder()
|
|
37
|
+
def stderr = new StringBuilder()
|
|
38
|
+
process.waitForProcessOutput(stdout, stderr)
|
|
39
|
+
stderr.toString().readLines().findAll { it.trim() }.each { logger.warn(it) }
|
|
40
|
+
if (process.exitValue() != 0) {
|
|
41
|
+
throw new GradleException("[HotUpdater] `${command.join(' ')}` failed with exit code ${process.exitValue()}.")
|
|
42
|
+
}
|
|
43
|
+
return stdout.toString().trim()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@TaskAction
|
|
47
|
+
void generate() {
|
|
48
|
+
def appRoot = new File(getAppRoot().get()).canonicalFile
|
|
49
|
+
def fingerprintJson = new File(appRoot, "fingerprint.json")
|
|
50
|
+
def fingerprintHash = ""
|
|
51
|
+
if (fingerprintJson.isFile()) {
|
|
52
|
+
def recorded = new groovy.json.JsonSlurper().parse(fingerprintJson)
|
|
53
|
+
// Only a CLI with `fingerprint hash` records `inputs`; with an older one
|
|
54
|
+
// the build embeds the recorded hash.
|
|
55
|
+
if (getRecordedOnly().get() || recorded?.inputs == null) {
|
|
56
|
+
if (!getRecordedOnly().get()) {
|
|
57
|
+
logger.warn("[HotUpdater] fingerprint.json records no inputs, so the build embeds the hash it holds. Run `npx hot-updater fingerprint create` with this version of hot-updater.")
|
|
58
|
+
}
|
|
59
|
+
fingerprintHash = recorded?.android?.hash ?: ""
|
|
60
|
+
} else {
|
|
61
|
+
def node = getNodeBinary().get()
|
|
62
|
+
def cli = run([
|
|
63
|
+
node,
|
|
64
|
+
"-e",
|
|
65
|
+
'''
|
|
66
|
+
const path = require("node:path");
|
|
67
|
+
const manifest = require.resolve("hot-updater/package.json", { paths: [process.argv[1]] });
|
|
68
|
+
const { bin } = require(manifest);
|
|
69
|
+
console.log(path.join(path.dirname(manifest), typeof bin === "string" ? bin : bin["hot-updater"]));
|
|
70
|
+
''',
|
|
71
|
+
appRoot.path,
|
|
72
|
+
], appRoot)
|
|
73
|
+
fingerprintHash = run([node, cli, "fingerprint", "hash", "-p", "android"], appRoot)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def source = getOutputDirectory().file("com/hotupdater/HotUpdaterBuildFingerprintHash.java").get().asFile
|
|
78
|
+
source.parentFile.mkdirs()
|
|
79
|
+
source.text = """package com.hotupdater;
|
|
80
|
+
|
|
81
|
+
// Generated by generateHotUpdaterFingerprintHash on every build.
|
|
82
|
+
final class HotUpdaterBuildFingerprintHash {
|
|
83
|
+
private HotUpdaterBuildFingerprintHash() {}
|
|
84
|
+
|
|
85
|
+
static String get() {
|
|
86
|
+
return "${fingerprintHash}";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
"""
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// A relative HOT_UPDATER_APP_ROOT is relative to the Android project, not to
|
|
94
|
+
// the directory Gradle was started from.
|
|
95
|
+
def hotUpdaterAndroidRoot = rootProject.projectDir
|
|
96
|
+
def hotUpdaterAppRoot = providers.gradleProperty("HOT_UPDATER_APP_ROOT")
|
|
97
|
+
.map { String root ->
|
|
98
|
+
def file = new File(root)
|
|
99
|
+
(file.isAbsolute() ? file : new File(hotUpdaterAndroidRoot, root)).absolutePath
|
|
100
|
+
}
|
|
101
|
+
.orElse(hotUpdaterAndroidRoot.parentFile.absolutePath)
|
|
102
|
+
|
|
103
|
+
// The node that runs the CLI: the HOT_UPDATER_NODE_BINARY Gradle property,
|
|
104
|
+
// else NODE_BINARY, as iOS builds read from ios/.xcode.env, else node on PATH.
|
|
105
|
+
def hotUpdaterNodeBinary = providers.gradleProperty("HOT_UPDATER_NODE_BINARY")
|
|
106
|
+
.orElse(providers.environmentVariable("NODE_BINARY"))
|
|
107
|
+
.orElse("node")
|
|
108
|
+
|
|
109
|
+
def registerHotUpdaterFingerprintTask = { String variantName, String buildType ->
|
|
110
|
+
tasks.register("generateHotUpdaterFingerprintHash${variantName.capitalize()}", GenerateHotUpdaterFingerprintHashTask) {
|
|
111
|
+
appRoot.set(hotUpdaterAppRoot)
|
|
112
|
+
recordedOnly.set(buildType == "debug")
|
|
113
|
+
nodeBinary.set(hotUpdaterNodeBinary)
|
|
114
|
+
outputDirectory.set(layout.buildDirectory.dir("generated/source/hotUpdaterFingerprintHash/${variantName}"))
|
|
115
|
+
outputs.upToDateWhen { false }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// AGP 7.3 added generated source directories to the Variant API. Older AGP,
|
|
120
|
+
// which React Native 0.70 and earlier ship, registers the task on the legacy
|
|
121
|
+
// variant API instead.
|
|
122
|
+
def hotUpdaterAgpVersion = androidComponents.pluginVersion
|
|
123
|
+
if (hotUpdaterAgpVersion.major > 7 || (hotUpdaterAgpVersion.major == 7 && hotUpdaterAgpVersion.minor >= 3)) {
|
|
124
|
+
androidComponents {
|
|
125
|
+
onVariants(selector().all()) { variant ->
|
|
126
|
+
def task = registerHotUpdaterFingerprintTask(variant.name, variant.buildType)
|
|
127
|
+
variant.sources.java.addGeneratedSourceDirectory(task) { it.outputDirectory }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
android.libraryVariants.all { variant ->
|
|
132
|
+
def task = registerHotUpdaterFingerprintTask(variant.name, variant.buildType.name)
|
|
133
|
+
variant.registerJavaGeneratingTask(task, task.get().outputDirectory.get().asFile)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -2,16 +2,93 @@
|
|
|
2
2
|
#include <signal.h>
|
|
3
3
|
#include <unistd.h>
|
|
4
4
|
#include <fcntl.h>
|
|
5
|
+
#include <time.h>
|
|
5
6
|
#include <cstring>
|
|
6
7
|
#include <limits.h>
|
|
8
|
+
#include <atomic>
|
|
9
|
+
#include <cstdint>
|
|
10
|
+
#include <mutex>
|
|
7
11
|
|
|
8
12
|
namespace {
|
|
9
13
|
constexpr size_t kMaxBundleIdLength = 128;
|
|
10
14
|
constexpr int kSignals[] = {SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGTRAP};
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
// One slot per instance that monitors a launch (HotUpdaterRecoverySlots.kt,
|
|
17
|
+
// SLOT_COUNT); main is slot 0. The handler can't allocate, so an instance that
|
|
18
|
+
// finds every slot taken gets no marker from a native crash.
|
|
19
|
+
constexpr int kSlotCount = 8;
|
|
20
|
+
constexpr int kNotAttributed = -1;
|
|
21
|
+
// A crash claimed by an instance that has no slot: no slot writes a marker.
|
|
22
|
+
constexpr int kAttributedWithoutSlot = -2;
|
|
23
|
+
// Kotlin clears the claim when it releases it: when the claimed error's
|
|
24
|
+
// handler returns, or after HotUpdaterRecoverySlots.CLAIM_FAIL_SAFE_MS. The
|
|
25
|
+
// claim also expires on its own after that same fail-safe, only so a release
|
|
26
|
+
// that never runs can't leave it claiming crashes forever.
|
|
27
|
+
constexpr uint32_t kNativeClaimFallbackMilliseconds = 60000;
|
|
28
|
+
|
|
29
|
+
struct SlotData {
|
|
30
|
+
// Empty while the slot is free.
|
|
31
|
+
char crashMarkerPath[PATH_MAX];
|
|
32
|
+
char bundleId[kMaxBundleIdLength];
|
|
33
|
+
int shouldRollback;
|
|
34
|
+
// Main records a verified launch too, as it always has.
|
|
35
|
+
int writesWhenVerified;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// The published copy of SlotData. Every field is atomic, copied with relaxed
|
|
39
|
+
// loads and stores, so a reader racing a writer reads defined (if mixed)
|
|
40
|
+
// values, which the sequence then rejects. The flags are stored last, with
|
|
41
|
+
// release, after the strings: see readFirstWrite.
|
|
42
|
+
struct AtomicSlotData {
|
|
43
|
+
std::atomic<char> crashMarkerPath[PATH_MAX];
|
|
44
|
+
std::atomic<char> bundleId[kMaxBundleIdLength];
|
|
45
|
+
std::atomic<int> shouldRollback;
|
|
46
|
+
std::atomic<int> writesWhenVerified;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// A sequence lock: a crash on any thread may read a slot while another thread
|
|
50
|
+
// rewrites it. The writer makes the sequence odd while it copies, and a
|
|
51
|
+
// reader keeps a copy only when the sequence was even and unchanged across it.
|
|
52
|
+
struct SlotBuffer {
|
|
53
|
+
std::atomic<uint32_t> sequence;
|
|
54
|
+
AtomicSlotData data;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Two buffers per slot: the writer fills the one not published, then
|
|
58
|
+
// publishes it. A crash during a rewrite, even on the writer's own thread,
|
|
59
|
+
// still reads the previous whole copy.
|
|
60
|
+
// `published`: bit 0 is the published buffer, bit 1 says anything was
|
|
61
|
+
// published at all (a freshly started slot has neither).
|
|
62
|
+
constexpr uint32_t kPublishedIndexMask = 1U;
|
|
63
|
+
constexpr uint32_t kPublishedOnceFlag = 2U;
|
|
64
|
+
|
|
65
|
+
struct Slot {
|
|
66
|
+
std::atomic<uint32_t> published;
|
|
67
|
+
SlotBuffer buffers[2];
|
|
68
|
+
};
|
|
69
|
+
// C++11, as old React Native builds compile it: the handler needs a
|
|
70
|
+
// lock-free sequence.
|
|
71
|
+
static_assert(ATOMIC_INT_LOCK_FREE == 2 && sizeof(uint32_t) == sizeof(int), "the crash handler needs a lock-free sequence");
|
|
72
|
+
static_assert(ATOMIC_CHAR_LOCK_FREE == 2, "the crash handler needs lock-free slot bytes");
|
|
73
|
+
|
|
74
|
+
// Zero-initialized: static storage. Every slot starts free.
|
|
75
|
+
Slot gSlots[kSlotCount];
|
|
76
|
+
// The sequence lock takes one writer at a time; launch preparation and the
|
|
77
|
+
// main thread can update one slot at once. Never taken by the handler.
|
|
78
|
+
std::mutex gSlotWriters[kSlotCount];
|
|
79
|
+
// The claimed slot and when it was claimed, in one word, so a crash during
|
|
80
|
+
// a new claim reads either the old claim or the new one, never a mix:
|
|
81
|
+
// bits 4..31 hold the claim's monotonic milliseconds (modulo 2^28, about 74
|
|
82
|
+
// hours, compared with wraparound), bits 0..3 the slot offset by
|
|
83
|
+
// kClaimSlotOffset. Zero is no claim.
|
|
84
|
+
constexpr uint32_t kClaimSlotBits = 4;
|
|
85
|
+
constexpr uint32_t kClaimSlotMask = (1u << kClaimSlotBits) - 1;
|
|
86
|
+
constexpr uint32_t kClaimTimeMask = 0xFFFFFFFFu >> kClaimSlotBits;
|
|
87
|
+
static_assert(kNativeClaimFallbackMilliseconds < kClaimTimeMask / 2, "a claim's window fits its time bits");
|
|
88
|
+
constexpr int kClaimSlotOffset = 3;
|
|
89
|
+
static_assert(kSlotCount + kClaimSlotOffset <= static_cast<int>(kClaimSlotMask), "a claimed slot fits its bits");
|
|
90
|
+
static_assert(kAttributedWithoutSlot + kClaimSlotOffset > 0, "a claim is never zero");
|
|
91
|
+
std::atomic<uint32_t> gClaim(0);
|
|
15
92
|
struct sigaction gPreviousActions[NSIG];
|
|
16
93
|
|
|
17
94
|
void signalHandler(int signum, siginfo_t *info, void *context);
|
|
@@ -53,12 +130,126 @@ void safeCopyJString(JNIEnv *env, jstring source, char *destination, size_t dest
|
|
|
53
130
|
env->ReleaseStringUTFChars(source, utfChars);
|
|
54
131
|
}
|
|
55
132
|
|
|
56
|
-
|
|
57
|
-
|
|
133
|
+
// clock_gettime is async-signal-safe. Milliseconds of the monotonic clock,
|
|
134
|
+
// modulo the claim's time bits.
|
|
135
|
+
uint32_t claimTime() {
|
|
136
|
+
struct timespec now {};
|
|
137
|
+
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
const uint64_t milliseconds =
|
|
141
|
+
static_cast<uint64_t>(now.tv_sec) * 1000u + static_cast<uint64_t>(now.tv_nsec) / 1000000u;
|
|
142
|
+
return static_cast<uint32_t>(milliseconds) & kClaimTimeMask;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
int attributedSlot() {
|
|
146
|
+
const uint32_t claim = gClaim.load(std::memory_order_acquire);
|
|
147
|
+
const uint32_t slotCode = claim & kClaimSlotMask;
|
|
148
|
+
if (slotCode == 0) {
|
|
149
|
+
return kNotAttributed;
|
|
150
|
+
}
|
|
151
|
+
const uint32_t claimedAt = claim >> kClaimSlotBits;
|
|
152
|
+
const uint32_t elapsed = (claimTime() - claimedAt) & kClaimTimeMask;
|
|
153
|
+
if (elapsed > kNativeClaimFallbackMilliseconds) {
|
|
154
|
+
return kNotAttributed;
|
|
155
|
+
}
|
|
156
|
+
return static_cast<int>(slotCode) - kClaimSlotOffset;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
void storeBytes(std::atomic<char> *destination, const char *source, size_t size) {
|
|
160
|
+
for (size_t index = 0; index < size; ++index) {
|
|
161
|
+
destination[index].store(source[index], std::memory_order_relaxed);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
void loadBytes(const std::atomic<char> *source, char *destination, size_t size) {
|
|
166
|
+
for (size_t index = 0; index < size; ++index) {
|
|
167
|
+
destination[index] = source[index].load(std::memory_order_relaxed);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
void storeSlotData(AtomicSlotData &destination, const SlotData &source) {
|
|
172
|
+
storeBytes(destination.crashMarkerPath, source.crashMarkerPath, sizeof(source.crashMarkerPath));
|
|
173
|
+
storeBytes(destination.bundleId, source.bundleId, sizeof(source.bundleId));
|
|
174
|
+
// The flags last, each with release: a reader that sees one set sees the
|
|
175
|
+
// strings stored before it (readFirstWrite).
|
|
176
|
+
destination.shouldRollback.store(source.shouldRollback, std::memory_order_release);
|
|
177
|
+
destination.writesWhenVerified.store(source.writesWhenVerified, std::memory_order_release);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
void loadStrings(const AtomicSlotData &source, SlotData &destination) {
|
|
181
|
+
loadBytes(source.crashMarkerPath, destination.crashMarkerPath, sizeof(destination.crashMarkerPath));
|
|
182
|
+
loadBytes(source.bundleId, destination.bundleId, sizeof(destination.bundleId));
|
|
183
|
+
// A copy taken mid-rewrite is rejected by the sequence, but the strings must
|
|
184
|
+
// still end inside their buffers.
|
|
185
|
+
destination.crashMarkerPath[sizeof(destination.crashMarkerPath) - 1] = '\0';
|
|
186
|
+
destination.bundleId[sizeof(destination.bundleId) - 1] = '\0';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
void loadSlotData(const AtomicSlotData &source, SlotData &destination) {
|
|
190
|
+
loadStrings(source, destination);
|
|
191
|
+
destination.shouldRollback = source.shouldRollback.load(std::memory_order_relaxed);
|
|
192
|
+
destination.writesWhenVerified = source.writesWhenVerified.load(std::memory_order_relaxed);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
bool readBuffer(const SlotBuffer &buffer, SlotData &snapshot) {
|
|
196
|
+
const uint32_t before = buffer.sequence.load(std::memory_order_acquire);
|
|
197
|
+
if ((before & 1U) != 0) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
loadSlotData(buffer.data, snapshot);
|
|
201
|
+
std::atomic_thread_fence(std::memory_order_acquire);
|
|
202
|
+
return buffer.sequence.load(std::memory_order_relaxed) == before;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// The slot's first write, before anything was published: its buffer was
|
|
206
|
+
// empty, and the write only fills it, strings first and the flags last with
|
|
207
|
+
// release. A write that finished (a crash between the copy and the flip,
|
|
208
|
+
// even on the writer's own thread) is whole. One interrupted mid-copy, as by
|
|
209
|
+
// a crash on the writer's own thread, which sees its stores in program
|
|
210
|
+
// order, holds whole strings once either flag is set, and nothing that asks
|
|
211
|
+
// for a marker while neither is.
|
|
212
|
+
bool readFirstWrite(const SlotBuffer &buffer, SlotData &snapshot) {
|
|
213
|
+
if (!readBuffer(buffer, snapshot)) {
|
|
214
|
+
const int shouldRollback = buffer.data.shouldRollback.load(std::memory_order_acquire);
|
|
215
|
+
const int writesWhenVerified = buffer.data.writesWhenVerified.load(std::memory_order_acquire);
|
|
216
|
+
if (shouldRollback == 0 && writesWhenVerified == 0) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
loadStrings(buffer.data, snapshot);
|
|
220
|
+
snapshot.shouldRollback = shouldRollback;
|
|
221
|
+
snapshot.writesWhenVerified = writesWhenVerified;
|
|
222
|
+
}
|
|
223
|
+
return snapshot.crashMarkerPath[0] != '\0';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// A consistent snapshot of the slot; false when it holds nothing a marker
|
|
227
|
+
// can be written from, or stays mid-rewrite.
|
|
228
|
+
bool readSlot(const Slot &slot, SlotData &snapshot) {
|
|
229
|
+
for (int attempt = 0; attempt < 4; ++attempt) {
|
|
230
|
+
const uint32_t state = slot.published.load(std::memory_order_acquire);
|
|
231
|
+
const uint32_t published = state & kPublishedIndexMask;
|
|
232
|
+
if ((state & kPublishedOnceFlag) == 0) {
|
|
233
|
+
// Nothing published yet: only the first write's buffer, the one the
|
|
234
|
+
// flip will publish, can hold anything.
|
|
235
|
+
return readFirstWrite(slot.buffers[published ^ 1U], snapshot);
|
|
236
|
+
}
|
|
237
|
+
// Once anything was published, an empty slot is a cleared one.
|
|
238
|
+
if (readBuffer(slot.buffers[published], snapshot)) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
// Only a buffer published before the latest flip is ever rewritten;
|
|
242
|
+
// read the slot again.
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
void writeCrashMarker(const SlotData &slot) {
|
|
248
|
+
if (slot.crashMarkerPath[0] == '\0') {
|
|
58
249
|
return;
|
|
59
250
|
}
|
|
60
251
|
|
|
61
|
-
const int fd = open(
|
|
252
|
+
const int fd = open(slot.crashMarkerPath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
|
62
253
|
if (fd < 0) {
|
|
63
254
|
return;
|
|
64
255
|
}
|
|
@@ -70,11 +261,11 @@ void writeCrashMarker() {
|
|
|
70
261
|
constexpr char suffix[] = "}\n";
|
|
71
262
|
|
|
72
263
|
write(fd, prefix, sizeof(prefix) - 1);
|
|
73
|
-
if (
|
|
74
|
-
write(fd,
|
|
264
|
+
if (slot.bundleId[0] != '\0') {
|
|
265
|
+
write(fd, slot.bundleId, safeStringLength(slot.bundleId, kMaxBundleIdLength));
|
|
75
266
|
}
|
|
76
267
|
write(fd, middle, sizeof(middle) - 1);
|
|
77
|
-
if (
|
|
268
|
+
if (slot.shouldRollback != 0) {
|
|
78
269
|
write(fd, trueLiteral, sizeof(trueLiteral) - 1);
|
|
79
270
|
} else {
|
|
80
271
|
write(fd, falseLiteral, sizeof(falseLiteral) - 1);
|
|
@@ -83,6 +274,26 @@ void writeCrashMarker() {
|
|
|
83
274
|
close(fd);
|
|
84
275
|
}
|
|
85
276
|
|
|
277
|
+
// HotUpdaterRecoverySlots.markerTargets: a claimed crash writes only its
|
|
278
|
+
// instance's marker; otherwise every launch still waiting for its first
|
|
279
|
+
// content writes one, and main records its launch either way.
|
|
280
|
+
void writeCrashMarkers() {
|
|
281
|
+
const int attributed = attributedSlot();
|
|
282
|
+
for (int index = 0; index < kSlotCount; ++index) {
|
|
283
|
+
if (attributed != kNotAttributed && attributed != index) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
SlotData slot;
|
|
287
|
+
if (!readSlot(gSlots[index], slot) || slot.crashMarkerPath[0] == '\0') {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (slot.shouldRollback == 0 && slot.writesWhenVerified == 0) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
writeCrashMarker(slot);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
86
297
|
void forwardToPreviousHandler(int signum, siginfo_t *info, void *context) {
|
|
87
298
|
const struct sigaction &previousAction = gPreviousActions[signum];
|
|
88
299
|
|
|
@@ -110,34 +321,93 @@ void forwardToPreviousHandler(int signum, siginfo_t *info, void *context) {
|
|
|
110
321
|
}
|
|
111
322
|
|
|
112
323
|
void signalHandler(int signum, siginfo_t *info, void *context) {
|
|
113
|
-
|
|
324
|
+
writeCrashMarkers();
|
|
114
325
|
forwardToPreviousHandler(signum, info, context);
|
|
115
326
|
}
|
|
116
327
|
} // namespace
|
|
117
328
|
|
|
118
329
|
extern "C" JNIEXPORT void JNICALL
|
|
119
|
-
|
|
120
|
-
JNIEnv *env
|
|
121
|
-
|
|
122
|
-
jstring crashMarkerPath) {
|
|
123
|
-
safeCopyJString(env, crashMarkerPath, gCrashMarkerPath, sizeof(gCrashMarkerPath));
|
|
124
|
-
|
|
330
|
+
Java_com_hotupdater_HotUpdaterRecoveryNative_nativeInstallSignalHandler(
|
|
331
|
+
JNIEnv * /* env */,
|
|
332
|
+
jclass /* clazz */) {
|
|
125
333
|
struct sigaction action {};
|
|
126
334
|
action.sa_sigaction = signalHandler;
|
|
127
335
|
action.sa_flags = SA_SIGINFO | SA_ONSTACK;
|
|
128
336
|
sigemptyset(&action.sa_mask);
|
|
129
337
|
|
|
130
338
|
for (const int signum : kSignals) {
|
|
339
|
+
// Installed once per process: recording this handler as the previous one
|
|
340
|
+
// would make forwarding call itself.
|
|
341
|
+
struct sigaction current {};
|
|
342
|
+
if (sigaction(signum, nullptr, ¤t) == 0 && (current.sa_flags & SA_SIGINFO) != 0 &&
|
|
343
|
+
current.sa_sigaction == signalHandler) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
131
346
|
sigaction(signum, &action, &gPreviousActions[signum]);
|
|
132
347
|
}
|
|
133
348
|
}
|
|
134
349
|
|
|
135
350
|
extern "C" JNIEXPORT void JNICALL
|
|
136
|
-
|
|
351
|
+
Java_com_hotupdater_HotUpdaterRecoveryNative_nativeUpdateSlot(
|
|
137
352
|
JNIEnv *env,
|
|
138
|
-
|
|
353
|
+
jclass /* clazz */,
|
|
354
|
+
jint index,
|
|
355
|
+
jstring crashMarkerPath,
|
|
139
356
|
jstring bundleId,
|
|
140
|
-
jboolean shouldRollback
|
|
141
|
-
|
|
142
|
-
|
|
357
|
+
jboolean shouldRollback,
|
|
358
|
+
jboolean writesWhenVerified) {
|
|
359
|
+
if (index < 0 || index >= kSlotCount) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Copied before the sequence turns odd, so the window a crash skips stays
|
|
364
|
+
// one copy long.
|
|
365
|
+
SlotData next {};
|
|
366
|
+
safeCopyJString(env, crashMarkerPath, next.crashMarkerPath, sizeof(next.crashMarkerPath));
|
|
367
|
+
safeCopyJString(env, bundleId, next.bundleId, sizeof(next.bundleId));
|
|
368
|
+
next.shouldRollback = shouldRollback ? 1 : 0;
|
|
369
|
+
next.writesWhenVerified = writesWhenVerified ? 1 : 0;
|
|
370
|
+
|
|
371
|
+
std::lock_guard<std::mutex> writer(gSlotWriters[index]);
|
|
372
|
+
Slot &slot = gSlots[index];
|
|
373
|
+
const uint32_t state = slot.published.load(std::memory_order_relaxed);
|
|
374
|
+
if ((state & kPublishedOnceFlag) != 0) {
|
|
375
|
+
// Same marker and bundle, only the flags change (a launch charged or
|
|
376
|
+
// verified): flip them in the published buffer with single atomic stores,
|
|
377
|
+
// so no crash ever falls inside a copy and reads the old flags.
|
|
378
|
+
SlotBuffer ¤t = slot.buffers[state & kPublishedIndexMask];
|
|
379
|
+
SlotData published;
|
|
380
|
+
loadSlotData(current.data, published);
|
|
381
|
+
if (std::strcmp(published.crashMarkerPath, next.crashMarkerPath) == 0 &&
|
|
382
|
+
std::strcmp(published.bundleId, next.bundleId) == 0) {
|
|
383
|
+
current.data.shouldRollback.store(next.shouldRollback, std::memory_order_release);
|
|
384
|
+
current.data.writesWhenVerified.store(next.writesWhenVerified, std::memory_order_release);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const uint32_t target = (state & kPublishedIndexMask) ^ 1U;
|
|
389
|
+
SlotBuffer &buffer = slot.buffers[target];
|
|
390
|
+
const uint32_t sequence = buffer.sequence.load(std::memory_order_relaxed);
|
|
391
|
+
buffer.sequence.store(sequence + 1, std::memory_order_relaxed);
|
|
392
|
+
std::atomic_thread_fence(std::memory_order_release);
|
|
393
|
+
storeSlotData(buffer.data, next);
|
|
394
|
+
buffer.sequence.store(sequence + 2, std::memory_order_release);
|
|
395
|
+
slot.published.store(target | kPublishedOnceFlag, std::memory_order_release);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
extern "C" JNIEXPORT void JNICALL
|
|
399
|
+
Java_com_hotupdater_HotUpdaterRecoveryNative_nativeAttributeCrash(
|
|
400
|
+
JNIEnv * /* env */,
|
|
401
|
+
jclass /* clazz */,
|
|
402
|
+
jint index) {
|
|
403
|
+
const int slot = (index >= 0 && index < kSlotCount) ? index : kAttributedWithoutSlot;
|
|
404
|
+
const uint32_t slotCode = static_cast<uint32_t>(slot + kClaimSlotOffset);
|
|
405
|
+
gClaim.store((claimTime() << kClaimSlotBits) | slotCode, std::memory_order_release);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
extern "C" JNIEXPORT void JNICALL
|
|
409
|
+
Java_com_hotupdater_HotUpdaterRecoveryNative_nativeClearCrashAttribution(
|
|
410
|
+
JNIEnv * /* env */,
|
|
411
|
+
jclass /* clazz */) {
|
|
412
|
+
gClaim.store(0, std::memory_order_release);
|
|
143
413
|
}
|