@hot-updater/react-native 1.0.8 → 1.1.1

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.
Files changed (145) hide show
  1. package/HotUpdater.podspec +14 -2
  2. package/Package.swift +24 -3
  3. package/android/build.gradle +2 -6
  4. package/android/hot-updater-fingerprint.gradle +135 -0
  5. package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
  6. package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
  7. package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
  8. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
  9. package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
  10. package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
  11. package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
  12. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
  13. package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
  14. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
  15. package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
  16. package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
  17. package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
  18. package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
  19. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +493 -0
  20. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
  21. package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
  22. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
  23. package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
  24. package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
  25. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
  26. package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
  27. package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
  28. package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
  29. package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
  30. package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
  31. package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
  32. package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
  33. package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
  34. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
  35. package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
  36. package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
  37. package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
  38. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
  39. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
  40. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
  41. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
  42. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
  43. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
  44. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
  45. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
  46. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
  47. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
  48. package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
  49. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
  50. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
  51. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
  52. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
  53. package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
  54. package/android/src/newarch/HotUpdater.kt +53 -15
  55. package/android/src/newarch/HotUpdaterModule.kt +42 -17
  56. package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
  57. package/android/src/oldarch/HotUpdater.kt +51 -12
  58. package/android/src/oldarch/HotUpdaterModule.kt +43 -17
  59. package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
  60. package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
  61. package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
  62. package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
  63. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
  64. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
  65. package/ios/HotUpdater/Internal/CohortService.swift +8 -0
  66. package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
  67. package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
  68. package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
  69. package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
  70. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
  71. package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
  72. package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
  73. package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
  74. package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
  75. package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
  76. package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
  77. package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
  78. package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
  79. package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
  80. package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
  81. package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
  82. package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
  83. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
  84. package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
  85. package/ios/HotUpdater/Package.swift +11 -0
  86. package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
  87. package/ios/HotUpdater/Public/HotUpdater.h +21 -2
  88. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
  89. package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
  90. package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
  91. package/lib/commonjs/checkForUpdate.js +3 -1
  92. package/lib/commonjs/checkForUpdate.js.map +1 -1
  93. package/lib/commonjs/index.js +9 -12
  94. package/lib/commonjs/index.js.map +1 -1
  95. package/lib/commonjs/native.js +24 -79
  96. package/lib/commonjs/native.js.map +1 -1
  97. package/lib/commonjs/nativeUpdateEvents.js +66 -0
  98. package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
  99. package/lib/commonjs/sdkVersion.js +1 -1
  100. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  101. package/lib/module/checkForUpdate.js +3 -1
  102. package/lib/module/checkForUpdate.js.map +1 -1
  103. package/lib/module/index.js +10 -13
  104. package/lib/module/index.js.map +1 -1
  105. package/lib/module/native.js +19 -73
  106. package/lib/module/native.js.map +1 -1
  107. package/lib/module/nativeUpdateEvents.js +61 -0
  108. package/lib/module/nativeUpdateEvents.js.map +1 -0
  109. package/lib/module/sdkVersion.js +1 -1
  110. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  111. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/index.d.ts +4 -14
  113. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/native.d.ts +9 -46
  115. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
  117. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  119. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
  120. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  121. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  122. package/lib/typescript/module/index.d.ts +4 -14
  123. package/lib/typescript/module/index.d.ts.map +1 -1
  124. package/lib/typescript/module/native.d.ts +9 -46
  125. package/lib/typescript/module/native.d.ts.map +1 -1
  126. package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
  127. package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
  128. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  129. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
  130. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  131. package/package.json +4 -4
  132. package/src/checkForUpdate.ts +7 -1
  133. package/src/index.ts +9 -17
  134. package/src/native.ts +19 -91
  135. package/src/nativeUpdateEvents.ts +77 -0
  136. package/src/sdkVersion.ts +1 -1
  137. package/src/specs/NativeHotUpdater.ts +20 -3
  138. package/android/libs/README.md +0 -50
  139. package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
  140. package/android/src/bridge/BridgeReactInstance.kt +0 -38
  141. package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
  142. package/android/src/main/java/com/hotupdater/ContentAppearedLog.kt +0 -31
  143. package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
  144. package/android/src/newarch/ReactIntegrationManager.kt +0 -212
  145. package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
@@ -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 and HotUpdaterInterface.swift are
20
- # for Swift Package Manager builds (Package.swift) only.
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 HotUpdater, the
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
@@ -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
- char gCrashMarkerPath[PATH_MAX] = {0};
13
- char gBundleId[kMaxBundleIdLength] = {0};
14
- volatile sig_atomic_t gShouldRollback = 0;
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
- void writeCrashMarker() {
57
- if (gCrashMarkerPath[0] == '\0') {
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(gCrashMarkerPath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
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 (gBundleId[0] != '\0') {
74
- write(fd, gBundleId, safeStringLength(gBundleId, kMaxBundleIdLength));
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 (gShouldRollback != 0) {
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
- writeCrashMarker();
324
+ writeCrashMarkers();
114
325
  forwardToPreviousHandler(signum, info, context);
115
326
  }
116
327
  } // namespace
117
328
 
118
329
  extern "C" JNIEXPORT void JNICALL
119
- Java_com_hotupdater_HotUpdaterRecoveryManager_nativeInstallSignalHandler(
120
- JNIEnv *env,
121
- jobject /* this */,
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, &current) == 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
- Java_com_hotupdater_HotUpdaterRecoveryManager_nativeUpdateLaunchState(
351
+ Java_com_hotupdater_HotUpdaterRecoveryNative_nativeUpdateSlot(
137
352
  JNIEnv *env,
138
- jobject /* this */,
353
+ jclass /* clazz */,
354
+ jint index,
355
+ jstring crashMarkerPath,
139
356
  jstring bundleId,
140
- jboolean shouldRollback) {
141
- safeCopyJString(env, bundleId, gBundleId, sizeof(gBundleId));
142
- gShouldRollback = shouldRollback ? 1 : 0;
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 &current = 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
  }