react-native-update 10.55.1 → 10.56.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.
- package/README-CN.md +35 -0
- package/README.md +37 -0
- package/android/build.gradle +53 -27
- package/android/jni/Application.mk +6 -1
- package/android/jni/hpatch.c +59 -4
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/proguard.pro +70 -14
- package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
- package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
- package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
- package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
- package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
- package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
- package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
- package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
- package/cpp/patch_core/archive_limits.h +6 -0
- package/cpp/patch_core/jni_util.h +6 -2
- package/cpp/patch_core/patch_core.cpp +237 -84
- package/cpp/patch_core/patch_core.h +11 -0
- package/cpp/patch_core/patch_core_android.cpp +49 -33
- package/cpp/patch_core/update_core_android.cpp +134 -103
- package/cpp/update_flow_core/flow_json.cpp +150 -17
- package/cpp/update_flow_core/flow_json.h +13 -1
- package/cpp/update_flow_core/update_flow_core.cpp +78 -8
- package/cpp/update_flow_core/update_flow_core.h +9 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
- package/harmony/hvigor-plugin.ts +168 -21
- package/harmony/pushy/src/README.md +74 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
- package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
- package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
- package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
- package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
- package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
- package/harmony/pushy/src/main/ets/Logger.ts +42 -16
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
- package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
- package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
- package/harmony/pushy/src/main/module.json5 +5 -1
- package/harmony/pushy.har +0 -0
- package/ios/PrivacyInfo.xcprivacy +79 -0
- package/ios/RCTPushy/RCTPushy.mm +399 -170
- package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
- package/package.json +14 -24
- package/react-native-update.podspec +43 -3
- package/src/client.ts +236 -82
- package/src/context.ts +40 -27
- package/src/core.ts +41 -16
- package/src/endpoint.ts +2 -1
- package/src/error.ts +49 -11
- package/src/index.ts +14 -0
- package/src/locales/en.ts +1 -23
- package/src/locales/zh.ts +1 -22
- package/src/provider.tsx +159 -111
- package/src/type.ts +0 -2
- package/src/updateFlowCore.ts +10 -2
- package/src/utils.ts +61 -4
- package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +0 -45
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#include <cstdint>
|
|
4
4
|
#include <string>
|
|
5
|
+
#include <vector>
|
|
5
6
|
|
|
6
7
|
#include "flow_json.h"
|
|
7
8
|
|
|
@@ -24,6 +25,14 @@ uint32_t Murmur3_32(const std::string& key, uint32_t seed = 0);
|
|
|
24
25
|
// murmur(uuid) % 100 < rollout — the gray-release bucketing predicate.
|
|
25
26
|
bool IsInRollout(double rollout, const std::string& uuid);
|
|
26
27
|
|
|
28
|
+
// Whether a failed download attempt should move on to the next mirror of the
|
|
29
|
+
// same artifact. Anything but PATCH_FAILED is: every mirror serves the same
|
|
30
|
+
// bytes, so a patch that could not be applied fails on all of them and only
|
|
31
|
+
// the next strategy can help. An absent code (empty string) is retryable.
|
|
32
|
+
// Mirrors isMirrorRetryableCode; covered by the golden vectors so the native
|
|
33
|
+
// orchestrators' retry loops cannot drift from the TS reference.
|
|
34
|
+
bool IsMirrorRetryableCode(const std::string& code);
|
|
35
|
+
|
|
27
36
|
// paths × fileName -> candidate URL array; Undefined when fileName is falsy
|
|
28
37
|
// (mirrors joinUrls returning undefined without a file name).
|
|
29
38
|
flowjson::Value JoinUrls(const flowjson::Value& paths,
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// JNI surface of the update-flow decision layer for the Android orchestrator
|
|
2
2
|
// (NativeCheckOrchestrator.java). Pure string-in/string-out: every payload is
|
|
3
3
|
// JSON, matching the decision layer's own boundary. A null return means
|
|
4
|
-
// "input did not parse" and the caller skips the check round
|
|
4
|
+
// "input did not parse" and the caller skips the check round; a C++ exception
|
|
5
|
+
// (bad_alloc/length_error from an oversized body) maps onto that same path —
|
|
6
|
+
// it must never unwind through the JNI boundary.
|
|
5
7
|
#include <jni.h>
|
|
6
8
|
|
|
7
9
|
#include <cstdint>
|
|
10
|
+
#include <exception>
|
|
8
11
|
#include <limits>
|
|
9
12
|
#include <string>
|
|
10
13
|
#include <vector>
|
|
@@ -81,27 +84,35 @@ jstring ToJString(JNIEnv* env, const std::string& value) {
|
|
|
81
84
|
extern "C" JNIEXPORT jstring JNICALL
|
|
82
85
|
Java_cn_reactnative_modules_update_NativeUpdateFlow_buildCheckRequestBody(
|
|
83
86
|
JNIEnv* env, jclass, jstring inputJson) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
try {
|
|
88
|
+
bool ok = false;
|
|
89
|
+
flowjson::Value input = flowjson::Parse(
|
|
90
|
+
pushy::jni_util::JStringToString(env, inputJson), &ok);
|
|
91
|
+
if (!ok || !input.IsObject()) {
|
|
92
|
+
return nullptr;
|
|
93
|
+
}
|
|
94
|
+
return ToJString(
|
|
95
|
+
env, flowjson::Stringify(updateflow::BuildCheckRequestBody(input)));
|
|
96
|
+
} catch (...) {
|
|
88
97
|
return nullptr;
|
|
89
98
|
}
|
|
90
|
-
return ToJString(
|
|
91
|
-
env, flowjson::Stringify(updateflow::BuildCheckRequestBody(input)));
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
extern "C" JNIEXPORT jstring JNICALL
|
|
95
102
|
Java_cn_reactnative_modules_update_NativeUpdateFlow_orderEndpointCandidates(
|
|
96
103
|
JNIEnv* env, jclass, jstring endpointsJson, jdouble randomSample) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
try {
|
|
105
|
+
bool ok = false;
|
|
106
|
+
flowjson::Value endpoints = flowjson::Parse(
|
|
107
|
+
pushy::jni_util::JStringToString(env, endpointsJson), &ok);
|
|
108
|
+
if (!ok || !endpoints.IsArray()) {
|
|
109
|
+
return nullptr;
|
|
110
|
+
}
|
|
111
|
+
return ToJString(env, flowjson::Stringify(updateflow::OrderEndpointCandidates(
|
|
112
|
+
endpoints, randomSample)));
|
|
113
|
+
} catch (...) {
|
|
101
114
|
return nullptr;
|
|
102
115
|
}
|
|
103
|
-
return ToJString(env, flowjson::Stringify(updateflow::OrderEndpointCandidates(
|
|
104
|
-
endpoints, randomSample)));
|
|
105
116
|
}
|
|
106
117
|
|
|
107
118
|
extern "C" JNIEXPORT jboolean JNICALL
|
|
@@ -110,24 +121,32 @@ Java_cn_reactnative_modules_update_NativeUpdateFlow_isValidCheckResponse(
|
|
|
110
121
|
if (responseText == nullptr) {
|
|
111
122
|
return JNI_FALSE;
|
|
112
123
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
try {
|
|
125
|
+
return updateflow::IsValidCheckResponse(
|
|
126
|
+
pushy::jni_util::JStringToString(env, responseText))
|
|
127
|
+
? JNI_TRUE
|
|
128
|
+
: JNI_FALSE;
|
|
129
|
+
} catch (...) {
|
|
130
|
+
return JNI_FALSE;
|
|
131
|
+
}
|
|
117
132
|
}
|
|
118
133
|
|
|
119
134
|
extern "C" JNIEXPORT jstring JNICALL
|
|
120
135
|
Java_cn_reactnative_modules_update_NativeUpdateFlow_handleCheckResponse(
|
|
121
136
|
JNIEnv* env, jclass, jstring responseText, jstring identityJson,
|
|
122
137
|
jstring afterDownload) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
138
|
+
try {
|
|
139
|
+
bool ok = false;
|
|
140
|
+
flowjson::Value identity = flowjson::Parse(
|
|
141
|
+
pushy::jni_util::JStringToString(env, identityJson), &ok);
|
|
142
|
+
if (!ok || !identity.IsObject()) {
|
|
143
|
+
return nullptr;
|
|
144
|
+
}
|
|
145
|
+
return ToJString(env, flowjson::Stringify(updateflow::HandleCheckResponse(
|
|
146
|
+
pushy::jni_util::JStringToString(env, responseText),
|
|
147
|
+
identity, false,
|
|
148
|
+
pushy::jni_util::JStringToString(env, afterDownload))));
|
|
149
|
+
} catch (...) {
|
|
127
150
|
return nullptr;
|
|
128
151
|
}
|
|
129
|
-
return ToJString(env, flowjson::Stringify(updateflow::HandleCheckResponse(
|
|
130
|
-
pushy::jni_util::JStringToString(env, responseText),
|
|
131
|
-
identity, false,
|
|
132
|
-
pushy::jni_util::JStringToString(env, afterDownload))));
|
|
133
152
|
}
|
package/harmony/hvigor-plugin.ts
CHANGED
|
@@ -1,34 +1,181 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
|
+
// Structural view of the hvigor objects this plugin touches, so the file has
|
|
5
|
+
// no compile-time dependency on @ohos/hvigor typings (it is compiled by the
|
|
6
|
+
// consumer's hvigor, whatever version that is).
|
|
7
|
+
interface HvigorTaskContext {
|
|
8
|
+
getBuildMode?: () => string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface HvigorModuleContext {
|
|
12
|
+
getBuildMode?: () => string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface HvigorTask {
|
|
16
|
+
name: string;
|
|
17
|
+
run: (taskContext?: HvigorTaskContext) => void;
|
|
18
|
+
dependencies?: string[];
|
|
19
|
+
postDependencies?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface HvigorNodeLike {
|
|
23
|
+
getNodePath?: () => string;
|
|
24
|
+
getParentNode?: () => HvigorNodeLike | undefined;
|
|
25
|
+
getContext?: (pluginId: string) => HvigorModuleContext | undefined;
|
|
26
|
+
registerTask?: (task: HvigorTask) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const META_RELATIVE_PATH = 'src/main/resources/rawfile/meta.json';
|
|
30
|
+
const TASK_NAME = 'pushyWriteBuildMeta';
|
|
31
|
+
// OhosPluginId.OHOS_HAP_PLUGIN / OHOS_HSP_PLUGIN / OHOS_HAR_PLUGIN.
|
|
32
|
+
const MODULE_PLUGIN_IDS = ['ohosHapPlugin', 'ohosHspPlugin', 'ohosHarPlugin'];
|
|
33
|
+
|
|
34
|
+
function readVersionName(projectDir: string): string {
|
|
35
|
+
const appJsonPath = path.resolve(projectDir, 'AppScope/app.json5');
|
|
36
|
+
if (!fs.existsSync(appJsonPath)) {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
const content = fs.readFileSync(appJsonPath, 'utf-8');
|
|
40
|
+
const match = content.match(
|
|
41
|
+
/(?:"versionName"|versionName):\s*["']([^"']+)["']/,
|
|
42
|
+
);
|
|
43
|
+
return match?.[1] || '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Only `hvigorw assemble{Hap,App,Hsp,Har}` (DevEco's Build/Run actions pass
|
|
47
|
+
// the same task) should touch the consumer's source tree; IDE sync
|
|
48
|
+
// (`--sync`), `clean` and friends must not rewrite meta.json.
|
|
49
|
+
export function isAssembleInvocation(argv: string[]): boolean {
|
|
50
|
+
return argv.some(arg => /^assemble/i.test(arg));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveBuildMode(
|
|
54
|
+
node: HvigorNodeLike,
|
|
55
|
+
taskContext: HvigorTaskContext | undefined,
|
|
56
|
+
argv: string[],
|
|
57
|
+
): string {
|
|
58
|
+
if (taskContext && typeof taskContext.getBuildMode === 'function') {
|
|
59
|
+
return String(taskContext.getBuildMode());
|
|
60
|
+
}
|
|
61
|
+
if (typeof node.getContext === 'function') {
|
|
62
|
+
for (const pluginId of MODULE_PLUGIN_IDS) {
|
|
63
|
+
try {
|
|
64
|
+
const context = node.getContext(pluginId);
|
|
65
|
+
if (context && typeof context.getBuildMode === 'function') {
|
|
66
|
+
return String(context.getBuildMode());
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// not this module type
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// `hvigorw assembleHap -p buildMode=release` (DevEco passes the same
|
|
74
|
+
// property for its build actions); hvigor's default mode is debug.
|
|
75
|
+
const fromArgv = argv.find(arg => arg.startsWith('buildMode='));
|
|
76
|
+
return fromArgv ? fromArgv.substring('buildMode='.length) : 'debug';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The plugin is meant for a module hvigorfile (entry/hvigorfile.ts). When it
|
|
80
|
+
// is applied from the project hvigorfile instead, the node path is the
|
|
81
|
+
// project root (AppScope lives there): fall back to entry/ like the original
|
|
82
|
+
// plugin did, so an existing setup keeps working.
|
|
83
|
+
export function resolveModuleDir(node: HvigorNodeLike, cwd: string): string {
|
|
84
|
+
const nodePath =
|
|
85
|
+
typeof node.getNodePath === 'function' ? node.getNodePath() : '';
|
|
86
|
+
if (!nodePath) {
|
|
87
|
+
return path.resolve(cwd, 'entry');
|
|
88
|
+
}
|
|
89
|
+
if (fs.existsSync(path.join(nodePath, 'AppScope', 'app.json5'))) {
|
|
90
|
+
console.warn(
|
|
91
|
+
'reactNativeUpdatePlugin: applied at project level; writing meta.json ' +
|
|
92
|
+
'into entry/. Apply it from the module hvigorfile instead.',
|
|
93
|
+
);
|
|
94
|
+
return path.join(nodePath, 'entry');
|
|
95
|
+
}
|
|
96
|
+
return nodePath;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function resolveProjectDir(
|
|
100
|
+
node: HvigorNodeLike,
|
|
101
|
+
moduleDir: string,
|
|
102
|
+
): string {
|
|
103
|
+
const parent =
|
|
104
|
+
typeof node.getParentNode === 'function' ? node.getParentNode() : undefined;
|
|
105
|
+
if (parent && typeof parent.getNodePath === 'function') {
|
|
106
|
+
const parentPath = parent.getNodePath();
|
|
107
|
+
if (parentPath) {
|
|
108
|
+
return parentPath;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return path.dirname(moduleDir);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Debug builds get "0" like Android's `pushy_build_time` resValue: a debug
|
|
115
|
+
// rebuild must not look like a new binary and reset the update state.
|
|
116
|
+
export function buildMetaContent(
|
|
117
|
+
buildMode: string,
|
|
118
|
+
versionName: string,
|
|
119
|
+
now: number,
|
|
120
|
+
): { pushy_build_time: string; versionName: string } {
|
|
121
|
+
return {
|
|
122
|
+
pushy_build_time: buildMode.toLowerCase() === 'release' ? String(now) : '0',
|
|
123
|
+
versionName,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Writes rawfile/meta.json (pushy_build_time + versionName) into the module
|
|
129
|
+
* this plugin is applied to. The write happens in a build task that runs
|
|
130
|
+
* before resources are compiled, not in apply(): apply() runs on every hvigor
|
|
131
|
+
* invocation including IDE sync, and only assemble invocations register the
|
|
132
|
+
* task at all.
|
|
133
|
+
*/
|
|
4
134
|
export function reactNativeUpdatePlugin() {
|
|
5
135
|
return {
|
|
6
136
|
pluginId: 'reactNativeUpdatePlugin',
|
|
7
|
-
apply() {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'entry/src/main/resources/rawfile/meta.json',
|
|
12
|
-
);
|
|
13
|
-
fs.mkdirSync(path.dirname(metaFilePath), { recursive: true });
|
|
14
|
-
|
|
15
|
-
const moduleJsonPath = path.resolve(cwd, 'AppScope/app.json5');
|
|
16
|
-
let versionName = '';
|
|
17
|
-
if (fs.existsSync(moduleJsonPath)) {
|
|
18
|
-
const content = fs.readFileSync(moduleJsonPath, 'utf-8');
|
|
19
|
-
const match = content.match(
|
|
20
|
-
/(?:"versionName"|versionName):\s*["']([^"']+)["']/,
|
|
21
|
-
);
|
|
22
|
-
versionName = match?.[1] || '';
|
|
137
|
+
apply(node?: HvigorNodeLike) {
|
|
138
|
+
const argv = process.argv;
|
|
139
|
+
if (!isAssembleInvocation(argv)) {
|
|
140
|
+
return;
|
|
23
141
|
}
|
|
142
|
+
const safeNode: HvigorNodeLike = node ?? {};
|
|
143
|
+
const moduleDir = resolveModuleDir(safeNode, process.cwd());
|
|
144
|
+
const projectDir = resolveProjectDir(safeNode, moduleDir);
|
|
24
145
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
146
|
+
const writeMeta = (taskContext?: HvigorTaskContext) => {
|
|
147
|
+
const buildMode = resolveBuildMode(safeNode, taskContext, argv);
|
|
148
|
+
const metaFilePath = path.resolve(moduleDir, META_RELATIVE_PATH);
|
|
149
|
+
fs.mkdirSync(path.dirname(metaFilePath), { recursive: true });
|
|
150
|
+
const metaContent = buildMetaContent(
|
|
151
|
+
buildMode,
|
|
152
|
+
readVersionName(projectDir),
|
|
153
|
+
Date.now(),
|
|
154
|
+
);
|
|
155
|
+
fs.writeFileSync(metaFilePath, JSON.stringify(metaContent, null, 2));
|
|
156
|
+
console.log(`Build time written to ${metaFilePath} (${buildMode})`);
|
|
28
157
|
};
|
|
29
158
|
|
|
30
|
-
|
|
31
|
-
|
|
159
|
+
if (typeof safeNode.registerTask === 'function') {
|
|
160
|
+
try {
|
|
161
|
+
safeNode.registerTask({
|
|
162
|
+
name: TASK_NAME,
|
|
163
|
+
run: writeMeta,
|
|
164
|
+
dependencies: ['default@PreBuild'],
|
|
165
|
+
postDependencies: ['default@ProcessResource'],
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.warn(
|
|
170
|
+
`reactNativeUpdatePlugin: task registration failed (${
|
|
171
|
+
e instanceof Error ? e.message : String(e)
|
|
172
|
+
}), writing meta.json at apply time`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Very old hvigor without task registration: keep the previous
|
|
177
|
+
// behaviour (write at apply time), still only for assemble builds.
|
|
178
|
+
writeMeta();
|
|
32
179
|
},
|
|
33
180
|
};
|
|
34
181
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# react-native-update — HarmonyOS module (`pushy`)
|
|
2
|
+
|
|
3
|
+
Integration notes for the HAR that ships in the `react-native-update` npm
|
|
4
|
+
package (`harmony/pushy.har`). The general setup lives in the project README;
|
|
5
|
+
this file covers what the host app must declare itself.
|
|
6
|
+
|
|
7
|
+
## Permissions
|
|
8
|
+
|
|
9
|
+
The HAR does not declare `requestPermissions`: downloads and update checks run
|
|
10
|
+
inside the host app's process, so the **host** must request the network
|
|
11
|
+
permission in its entry `module.json5`:
|
|
12
|
+
|
|
13
|
+
```json5
|
|
14
|
+
{
|
|
15
|
+
"module": {
|
|
16
|
+
"requestPermissions": [
|
|
17
|
+
{ "name": "ohos.permission.INTERNET" }
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Without it every check and download fails with a network error.
|
|
24
|
+
|
|
25
|
+
## No https → http downgrade (`cleartextTrafficPermitted`)
|
|
26
|
+
|
|
27
|
+
`@ohos.net.http` follows redirects internally and exposes neither a switch nor
|
|
28
|
+
the final URL, so unlike Android/iOS the module cannot itself refuse an
|
|
29
|
+
`https → http` redirect. What it does: when the configured endpoints are all
|
|
30
|
+
`https://`, any `http://` endpoint injected through the remote endpoint list
|
|
31
|
+
and any `http://` artifact URL in a check decision are ignored.
|
|
32
|
+
|
|
33
|
+
To close the redirect gap, keep cleartext traffic disabled for the app in
|
|
34
|
+
`entry/src/main/resources/base/profile/network_config.json` (and reference it
|
|
35
|
+
from `module.json5` via `"metadata": [{ "name": "network_config", "resource": "$profile:network_config" }]`):
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"network-security-config": {
|
|
40
|
+
"base-config": {
|
|
41
|
+
"cleartextTrafficPermitted": false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Build-time metadata (`hvigor-plugin`)
|
|
48
|
+
|
|
49
|
+
Apply the plugin from the **module** hvigorfile (`entry/hvigorfile.ts`):
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
|
|
53
|
+
import { reactNativeUpdatePlugin } from 'pushy/hvigor-plugin';
|
|
54
|
+
|
|
55
|
+
export default {
|
|
56
|
+
system: hapTasks,
|
|
57
|
+
plugins: [reactNativeUpdatePlugin()],
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
It writes `src/main/resources/rawfile/meta.json` (`pushy_build_time`,
|
|
62
|
+
`versionName`) into that module during `assemble*` builds only, before
|
|
63
|
+
resources are compiled. Debug builds get `pushy_build_time: "0"` like Android,
|
|
64
|
+
so a debug rebuild does not look like a new binary. Add the generated file to
|
|
65
|
+
`.gitignore`.
|
|
66
|
+
|
|
67
|
+
## Versioning
|
|
68
|
+
|
|
69
|
+
`oh-package.json5` `version` mirrors the npm package version;
|
|
70
|
+
`scripts/build-harmony-har.js` rewrites it before `assembleHar` and
|
|
71
|
+
`scripts/check-release-version.js` asserts the committed value matches.
|
|
72
|
+
|
|
73
|
+
This file is packaged with the npm module (`harmony/pushy/src/`) so it is
|
|
74
|
+
available next to the sources in `node_modules/react-native-update`.
|
|
@@ -71,8 +71,18 @@ if(TARGET rnoh)
|
|
|
71
71
|
target_link_libraries(rnoh_pushy PUBLIC rnoh)
|
|
72
72
|
endif()
|
|
73
73
|
|
|
74
|
-
# Optimize binary size
|
|
74
|
+
# Optimize binary size for Release builds of the NAPI library. --gc-sections
|
|
75
|
+
# only drops anything when every function/data item sits in its own section,
|
|
76
|
+
# and the module registers itself through NAPI_MODULE's static constructor,
|
|
77
|
+
# so no symbol needs default visibility. Scoped to rnupdate: rnoh_pushy must
|
|
78
|
+
# keep exporting PushyTurboModule (JSI_EXPORT) for the host's PackageProvider.
|
|
75
79
|
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
target_compile_options(rnupdate PRIVATE
|
|
81
|
+
-ffunction-sections
|
|
82
|
+
-fdata-sections
|
|
83
|
+
-fvisibility=hidden
|
|
84
|
+
-fvisibility-inlines-hidden
|
|
85
|
+
)
|
|
86
|
+
# Strip debug symbols and garbage-collect the unused sections.
|
|
87
|
+
target_link_options(rnupdate PRIVATE -Wl,--gc-sections -s)
|
|
78
88
|
endif()
|
|
@@ -45,15 +45,21 @@ jsi::Value CallAsync(
|
|
|
45
45
|
return CallAsync(rt, turboModule, #method_name, args, count); \
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Only the constants and the RN event plumbing are synchronous. Every
|
|
49
|
+
// Promise-returning spec method (src/NativePushy.ts) MUST be registered
|
|
50
|
+
// with PUSHY_ASYNC_METHOD: a sync registration converts the ArkTS Promise
|
|
51
|
+
// into an empty jsi object and its rejection becomes an unhandled ArkTS
|
|
52
|
+
// rejection, so JS would report e.g. setNeedUpdate as successful while
|
|
53
|
+
// nothing was switched. scripts/check-native-spec-parity.js enforces this.
|
|
48
54
|
PUSHY_SYNC_METHOD(getConstants)
|
|
49
|
-
PUSHY_SYNC_METHOD(setLocalHashInfo)
|
|
50
|
-
PUSHY_SYNC_METHOD(getLocalHashInfo)
|
|
51
|
-
PUSHY_SYNC_METHOD(setUuid)
|
|
52
|
-
PUSHY_SYNC_METHOD(setNeedUpdate)
|
|
53
|
-
PUSHY_SYNC_METHOD(markSuccess)
|
|
54
55
|
PUSHY_SYNC_METHOD(addListener)
|
|
55
56
|
PUSHY_SYNC_METHOD(removeListeners)
|
|
56
57
|
|
|
58
|
+
PUSHY_ASYNC_METHOD(setLocalHashInfo)
|
|
59
|
+
PUSHY_ASYNC_METHOD(getLocalHashInfo)
|
|
60
|
+
PUSHY_ASYNC_METHOD(setUuid)
|
|
61
|
+
PUSHY_ASYNC_METHOD(setNeedUpdate)
|
|
62
|
+
PUSHY_ASYNC_METHOD(markSuccess)
|
|
57
63
|
PUSHY_ASYNC_METHOD(reloadUpdate)
|
|
58
64
|
PUSHY_ASYNC_METHOD(restartApp)
|
|
59
65
|
PUSHY_ASYNC_METHOD(downloadPatchFromPpk)
|
|
@@ -667,12 +667,24 @@ struct CleanupWork {
|
|
|
667
667
|
napi_async_work work = nullptr;
|
|
668
668
|
napi_deferred deferred = nullptr;
|
|
669
669
|
std::string root_dir;
|
|
670
|
-
|
|
671
|
-
|
|
670
|
+
// Every version that may still be in use: persisted current/last AND the
|
|
671
|
+
// version this process booted from (two switches without a restart must not
|
|
672
|
+
// evict the running bundle while its on-demand assets are still served).
|
|
673
|
+
std::vector<std::string> keep_names;
|
|
672
674
|
int32_t max_age_days = 0;
|
|
673
675
|
pushy::patch::Status status{false, ""};
|
|
674
676
|
};
|
|
675
677
|
|
|
678
|
+
// Streaming file digest on a worker thread. A full package or bundle is tens
|
|
679
|
+
// of MB; hashing it synchronously on the UI thread (where the TurboModule
|
|
680
|
+
// runs) froze the app for hundreds of ms per archive/bundle/activation.
|
|
681
|
+
struct Sha256FileWork {
|
|
682
|
+
napi_async_work work = nullptr;
|
|
683
|
+
napi_deferred deferred = nullptr;
|
|
684
|
+
std::string path;
|
|
685
|
+
std::string hex;
|
|
686
|
+
};
|
|
687
|
+
|
|
676
688
|
napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
|
|
677
689
|
napi_value args[1] = {nullptr};
|
|
678
690
|
size_t argc = 1;
|
|
@@ -785,10 +797,11 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
|
|
|
785
797
|
return promise;
|
|
786
798
|
}
|
|
787
799
|
|
|
800
|
+
// cleanupOldEntries(rootDir, keepNames: string[], maxAgeDays)
|
|
788
801
|
napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
|
|
789
|
-
napi_value args[
|
|
790
|
-
size_t argc =
|
|
791
|
-
if (!GetArgCount(env, info, &argc, args) || argc <
|
|
802
|
+
napi_value args[3] = {nullptr, nullptr, nullptr};
|
|
803
|
+
size_t argc = 3;
|
|
804
|
+
if (!GetArgCount(env, info, &argc, args) || argc < 3) {
|
|
792
805
|
ThrowError(env, "Wrong number of arguments");
|
|
793
806
|
return nullptr;
|
|
794
807
|
}
|
|
@@ -798,23 +811,18 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
|
|
|
798
811
|
if (!ok) {
|
|
799
812
|
return nullptr;
|
|
800
813
|
}
|
|
801
|
-
|
|
802
|
-
if (!
|
|
803
|
-
return nullptr;
|
|
804
|
-
}
|
|
805
|
-
const std::string keep_previous = GetString(env, args[2], &ok);
|
|
806
|
-
if (!ok) {
|
|
814
|
+
std::vector<std::string> keep_names;
|
|
815
|
+
if (!GetStringArray(env, args[1], &keep_names)) {
|
|
807
816
|
return nullptr;
|
|
808
817
|
}
|
|
809
818
|
int32_t max_age_days = 0;
|
|
810
|
-
if (!GetInt32(env, args[
|
|
819
|
+
if (!GetInt32(env, args[2], &max_age_days)) {
|
|
811
820
|
return nullptr;
|
|
812
821
|
}
|
|
813
822
|
|
|
814
823
|
auto* work_data = new CleanupWork();
|
|
815
824
|
work_data->root_dir = root_dir;
|
|
816
|
-
work_data->
|
|
817
|
-
work_data->keep_previous = keep_previous;
|
|
825
|
+
work_data->keep_names = std::move(keep_names);
|
|
818
826
|
work_data->max_age_days = max_age_days;
|
|
819
827
|
|
|
820
828
|
napi_value promise = nullptr;
|
|
@@ -834,7 +842,7 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
|
|
|
834
842
|
[](napi_env, void* data) {
|
|
835
843
|
auto* w = static_cast<CleanupWork*>(data);
|
|
836
844
|
w->status = pushy::patch::CleanupOldEntries(
|
|
837
|
-
w->root_dir, w->
|
|
845
|
+
w->root_dir, w->keep_names, w->max_age_days);
|
|
838
846
|
},
|
|
839
847
|
[](napi_env cb_env, napi_status status, void* data) {
|
|
840
848
|
auto* w = static_cast<CleanupWork*>(data);
|
|
@@ -872,6 +880,76 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
|
|
|
872
880
|
return promise;
|
|
873
881
|
}
|
|
874
882
|
|
|
883
|
+
// sha256HexFileAsync(path) -> Promise<hex>. Rejects when the file cannot be
|
|
884
|
+
// read (Sha256File returns an empty digest in that case).
|
|
885
|
+
napi_value Sha256HexFileAsync(napi_env env, napi_callback_info info) {
|
|
886
|
+
napi_value args[1] = {nullptr};
|
|
887
|
+
size_t argc = 1;
|
|
888
|
+
if (!GetArgCount(env, info, &argc, args) || argc < 1) {
|
|
889
|
+
ThrowError(env, "sha256HexFileAsync: missing path argument");
|
|
890
|
+
return nullptr;
|
|
891
|
+
}
|
|
892
|
+
bool ok = false;
|
|
893
|
+
std::string path = GetString(env, args[0], &ok);
|
|
894
|
+
if (!ok) {
|
|
895
|
+
return nullptr;
|
|
896
|
+
}
|
|
897
|
+
if (path.empty()) {
|
|
898
|
+
ThrowError(env, "sha256HexFileAsync: path must be a non-empty string");
|
|
899
|
+
return nullptr;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
auto* work_data = new Sha256FileWork();
|
|
903
|
+
work_data->path = std::move(path);
|
|
904
|
+
|
|
905
|
+
napi_value promise = nullptr;
|
|
906
|
+
if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
|
|
907
|
+
delete work_data;
|
|
908
|
+
ThrowError(env, "Unable to create promise");
|
|
909
|
+
return nullptr;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
napi_value resource_name = nullptr;
|
|
913
|
+
napi_create_string_utf8(
|
|
914
|
+
env, "sha256HexFileAsync", NAPI_AUTO_LENGTH, &resource_name);
|
|
915
|
+
if (napi_create_async_work(
|
|
916
|
+
env,
|
|
917
|
+
nullptr,
|
|
918
|
+
resource_name,
|
|
919
|
+
[](napi_env, void* data) {
|
|
920
|
+
auto* w = static_cast<Sha256FileWork*>(data);
|
|
921
|
+
w->hex = pushy::digest::Sha256File(w->path);
|
|
922
|
+
},
|
|
923
|
+
[](napi_env cb_env, napi_status status, void* data) {
|
|
924
|
+
auto* w = static_cast<Sha256FileWork*>(data);
|
|
925
|
+
if (status != napi_ok) {
|
|
926
|
+
RejectDeferredWithMessage(cb_env, w->deferred, "async work aborted");
|
|
927
|
+
} else if (w->hex.empty()) {
|
|
928
|
+
RejectDeferredWithMessage(
|
|
929
|
+
cb_env, w->deferred, "sha256HexFileAsync: cannot read file");
|
|
930
|
+
} else {
|
|
931
|
+
napi_resolve_deferred(cb_env, w->deferred, NewString(cb_env, w->hex));
|
|
932
|
+
}
|
|
933
|
+
napi_delete_async_work(cb_env, w->work);
|
|
934
|
+
delete w;
|
|
935
|
+
},
|
|
936
|
+
work_data,
|
|
937
|
+
&work_data->work) != napi_ok) {
|
|
938
|
+
RejectDeferredWithMessage(
|
|
939
|
+
env, work_data->deferred, "Unable to create async work");
|
|
940
|
+
delete work_data;
|
|
941
|
+
return promise;
|
|
942
|
+
}
|
|
943
|
+
if (napi_queue_async_work(env, work_data->work) != napi_ok) {
|
|
944
|
+
napi_delete_async_work(env, work_data->work);
|
|
945
|
+
RejectDeferredWithMessage(
|
|
946
|
+
env, work_data->deferred, "Unable to queue async work");
|
|
947
|
+
delete work_data;
|
|
948
|
+
return promise;
|
|
949
|
+
}
|
|
950
|
+
return promise;
|
|
951
|
+
}
|
|
952
|
+
|
|
875
953
|
bool ExportFunction(
|
|
876
954
|
napi_env env,
|
|
877
955
|
napi_value exports,
|
|
@@ -898,7 +976,8 @@ bool ExportFunction(
|
|
|
898
976
|
// rawfile 里的 bundle(已整体读入内存,与 pdiff 的读法一致),哈希本身毫秒级。
|
|
899
977
|
static napi_value MakeUtf8String(napi_env env, const std::string& value);
|
|
900
978
|
|
|
901
|
-
// Streaming file digest (bounded memory)
|
|
979
|
+
// Streaming file digest (bounded memory). Synchronous: small files only —
|
|
980
|
+
// archives and bundles go through sha256HexFileAsync.
|
|
902
981
|
static napi_value Sha256HexFile(napi_env env, napi_callback_info info) {
|
|
903
982
|
size_t argc = 1;
|
|
904
983
|
napi_value args[1] = {nullptr};
|
|
@@ -909,7 +988,7 @@ static napi_value Sha256HexFile(napi_env env, napi_callback_info info) {
|
|
|
909
988
|
bool ok = false;
|
|
910
989
|
std::string path = GetString(env, args[0], &ok);
|
|
911
990
|
if (!ok) {
|
|
912
|
-
|
|
991
|
+
// GetString already raised the pending exception.
|
|
913
992
|
return nullptr;
|
|
914
993
|
}
|
|
915
994
|
return MakeUtf8String(env, pushy::digest::Sha256File(path));
|
|
@@ -1089,8 +1168,13 @@ static napi_value FlowIsValidCheckResponse(napi_env env,
|
|
|
1089
1168
|
}
|
|
1090
1169
|
bool ok = false;
|
|
1091
1170
|
std::string response_text = GetString(env, args[0], &ok);
|
|
1171
|
+
if (!ok) {
|
|
1172
|
+
// Exception pending (non-string argument): propagate it as a throw
|
|
1173
|
+
// instead of a boolean that the caller would treat as a verdict.
|
|
1174
|
+
return nullptr;
|
|
1175
|
+
}
|
|
1092
1176
|
napi_value result = nullptr;
|
|
1093
|
-
napi_get_boolean(env,
|
|
1177
|
+
napi_get_boolean(env, updateflow::IsValidCheckResponse(response_text),
|
|
1094
1178
|
&result);
|
|
1095
1179
|
return result;
|
|
1096
1180
|
}
|
|
@@ -1135,6 +1219,7 @@ napi_value Init(napi_env env, napi_value exports) {
|
|
|
1135
1219
|
!ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries) ||
|
|
1136
1220
|
!ExportFunction(env, exports, "sha256Hex", Sha256Hex) ||
|
|
1137
1221
|
!ExportFunction(env, exports, "sha256HexFile", Sha256HexFile) ||
|
|
1222
|
+
!ExportFunction(env, exports, "sha256HexFileAsync", Sha256HexFileAsync) ||
|
|
1138
1223
|
!ExportFunction(env, exports, "crc32", Crc32) ||
|
|
1139
1224
|
!ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion) ||
|
|
1140
1225
|
!ExportFunction(env, exports, "buildCheckRequestBody", FlowBuildCheckRequestBody) ||
|