react-native-update 10.59.2 → 10.60.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/README-CN.md +7 -7
- package/README.md +7 -6
- package/android/build.gradle +25 -33
- package/android/jni/Android.mk +16 -24
- package/android/lib/arm64-v8a/librnpushy.so +0 -0
- package/android/lib/armeabi-v7a/librnpushy.so +0 -0
- package/android/lib/x86/librnpushy.so +0 -0
- package/android/lib/x86_64/librnpushy.so +0 -0
- package/android/proguard.pro +9 -7
- package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +2 -7
- package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +23 -6
- package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +50 -51
- package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +12 -13
- package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +3 -0
- package/android/src/main/java/cn/reactnative/modules/update/CrashHold.java +22 -30
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +181 -332
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -1
- package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +27 -0
- package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/FlowBridge.java +32 -8
- package/android/src/main/java/cn/reactnative/modules/update/HttpClients.java +63 -0
- package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +0 -11
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +66 -116
- package/android/src/main/java/cn/reactnative/modules/update/NativeCore.java +9 -1
- package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +32 -60
- package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +8 -23
- package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +42 -80
- package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +22 -39
- package/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +176 -299
- package/android/src/main/java/cn/reactnative/modules/update/Threads.java +35 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +242 -174
- package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +1 -1
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +75 -17
- package/android/src/main/java/cn/reactnative/modules/update/{UpdateModuleImpl.java → UpdateModule.java} +113 -95
- package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +16 -19
- package/android/src/oldarch/cn/reactnative/modules/update/NativePushySpec.java +69 -0
- package/cpp/android_jni/archive_limits_jni.cpp +83 -0
- package/cpp/android_jni/install_record_jni.cpp +89 -0
- package/cpp/android_jni/jni_log.h +31 -0
- package/cpp/android_jni/jni_natives.h +40 -0
- package/cpp/android_jni/jni_registration.cpp +133 -0
- package/cpp/android_jni/jni_util.h +160 -0
- package/cpp/android_jni/update_core_android.cpp +450 -0
- package/cpp/android_jni/update_flow_jni.cpp +133 -0
- package/{android/jni → cpp/hpatch}/hpatch.c +5 -5
- package/{android/jni → cpp/hpatch}/hpatch.h +4 -1
- package/cpp/patch_core/archive_limits.cpp +194 -0
- package/cpp/patch_core/archive_limits.h +99 -10
- package/cpp/patch_core/archive_patch_core.cpp +96 -12
- package/cpp/patch_core/archive_patch_core.h +49 -0
- package/cpp/patch_core/digest.h +5 -4
- package/cpp/patch_core/error_codes.h +5 -3
- package/cpp/patch_core/install_record.cpp +430 -0
- package/cpp/patch_core/install_record.h +163 -9
- package/cpp/patch_core/patch_core.cpp +62 -16
- package/cpp/patch_core/patch_core.h +27 -7
- package/cpp/patch_core/patch_manifest.cpp +201 -0
- package/cpp/patch_core/patch_manifest.h +74 -0
- package/cpp/patch_core/state_ops.h +3 -3
- package/cpp/update_flow_core/flow_json.cpp +82 -8
- package/cpp/update_flow_core/flow_json.h +21 -0
- package/cpp/update_flow_core/native_config.cpp +456 -0
- package/cpp/update_flow_core/native_config.h +84 -0
- package/cpp/update_flow_core/native_round.cpp +326 -0
- package/cpp/update_flow_core/native_round.h +90 -0
- package/cpp/update_flow_core/update_flow_core.h +2 -2
- package/harmony/hvigor-plugin.ts +11 -19
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +17 -26
- package/harmony/pushy/src/main/cpp/PushyPackage.h +2 -2
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +55 -94
- package/harmony/pushy/src/main/cpp/pushy.cpp +694 -517
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +48 -72
- package/harmony/pushy/src/main/ets/DownloadTask.ts +164 -404
- package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +1 -3
- package/harmony/pushy/src/main/ets/ErrorCodes.ts +1 -14
- package/harmony/pushy/src/main/ets/EventHub.ts +1 -1
- package/harmony/pushy/src/main/ets/InstallRecord.ts +52 -151
- package/harmony/pushy/src/main/ets/Logger.ts +20 -34
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +116 -42
- package/harmony/pushy/src/main/ets/PushyConfiguration.ts +8 -85
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +11 -25
- package/harmony/pushy/src/main/ets/SaveFile.ts +99 -18
- package/harmony/pushy/src/main/ets/SyncCoordinator.ts +136 -335
- package/harmony/pushy/src/main/ets/Texts.ts +0 -4
- package/harmony/pushy/src/main/ets/UpdateContext.ts +106 -119
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy+Launch.mm +185 -0
- package/ios/RCTPushy/RCTPushy.h +12 -0
- package/ios/RCTPushy/RCTPushy.mm +87 -2634
- package/ios/RCTPushy/RCTPushyCommon.h +34 -0
- package/ios/RCTPushy/RCTPushyConfiguration.h +0 -8
- package/ios/RCTPushy/RCTPushyConfiguration.mm +40 -121
- package/ios/RCTPushy/RCTPushyDownloader.mm +40 -37
- package/ios/RCTPushy/RCTPushyInstallRecord.mm +61 -0
- package/ios/RCTPushy/RCTPushyInstaller.mm +689 -0
- package/ios/RCTPushy/RCTPushyInternal.h +144 -0
- package/ios/RCTPushy/RCTPushyOrchestrator.mm +1001 -0
- package/ios/RCTPushy/RCTPushyState.mm +314 -0
- package/package.json +25 -23
- package/react-native-update.podspec +73 -82
- package/src/client.ts +361 -451
- package/src/context.ts +1 -1
- package/src/core.ts +35 -8
- package/src/endpoint.ts +0 -2
- package/src/error.ts +13 -32
- package/src/i18n.ts +18 -83
- package/src/locales/en.ts +0 -4
- package/src/locales/zh.ts +0 -4
- package/src/provider.tsx +60 -53
- package/src/telemetry.ts +0 -13
- package/src/type.ts +0 -15
- package/src/updateFlowCore.ts +116 -10
- package/src/utils.ts +49 -42
- package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +0 -6
- package/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java +0 -104
- package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +0 -37
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +0 -121
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +0 -125
- package/cpp/patch_core/jni_natives.h +0 -27
- package/cpp/patch_core/jni_registration.cpp +0 -103
- package/cpp/patch_core/jni_util.h +0 -60
- package/cpp/patch_core/patch_core_android.cpp +0 -97
- package/cpp/patch_core/update_core_android.cpp +0 -485
- package/cpp/update_flow_core/update_flow_jni.cpp +0 -150
|
@@ -6,10 +6,10 @@ import androidx.annotation.Nullable;
|
|
|
6
6
|
import java.io.IOException;
|
|
7
7
|
import java.nio.charset.Charset;
|
|
8
8
|
import java.nio.charset.StandardCharsets;
|
|
9
|
-
import java.util.HashSet;
|
|
10
9
|
import java.util.concurrent.CountDownLatch;
|
|
11
10
|
import java.util.concurrent.TimeUnit;
|
|
12
11
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
12
|
+
import java.util.concurrent.atomic.AtomicReference;
|
|
13
13
|
import okhttp3.MediaType;
|
|
14
14
|
import okhttp3.OkHttpClient;
|
|
15
15
|
import okhttp3.Request;
|
|
@@ -31,19 +31,18 @@ import org.json.JSONObject;
|
|
|
31
31
|
* version blacklisting.
|
|
32
32
|
*/
|
|
33
33
|
final class SyncCoordinator {
|
|
34
|
-
// Version-info
|
|
34
|
+
// Version-info flag read by JS (metadata.ts), stored encoded.
|
|
35
35
|
private static final String INFO_CRASH_HOLD = Texts.reveal("3905f5c2a6b96d56212a19");
|
|
36
|
-
private static final String INFO_FORCE_BOOT = Texts.reveal("3c18e6d2aba9674a360d19ead5a695");
|
|
37
36
|
|
|
38
37
|
static final String KEY_CONFIG = "nativeConfig";
|
|
39
38
|
// Raw response cache for the JS side to reuse (§10.3), scoped to the
|
|
40
|
-
// exact logical request and native config that produced it.
|
|
39
|
+
// exact logical request and native config that produced it. Stored in
|
|
40
|
+
// its own SharedPreferences file (UpdateContext.getNativeCheckCache).
|
|
41
41
|
static final String KEY_RESP_CACHE = "nativeCheckResp";
|
|
42
42
|
// Set when a round starts, cleared when it ends (§11.4). Residue on the
|
|
43
43
|
// next launch means the previous process died mid-round (a crash rescue
|
|
44
44
|
// was truncated): that launch resumes immediately instead of waiting 5s.
|
|
45
45
|
static final String KEY_ROUND_INCOMPLETE = "nativeCheckIncomplete";
|
|
46
|
-
private static final int MAX_QUERY_HTTP_ATTEMPTS = 8;
|
|
47
46
|
private static final long DOWNLOAD_PHASE_TIMEOUT_SECONDS = 600;
|
|
48
47
|
// The check response (and a remote queryUrls list) is a small JSON
|
|
49
48
|
// document; anything bigger is a broken or hijacked endpoint.
|
|
@@ -85,26 +84,16 @@ final class SyncCoordinator {
|
|
|
85
84
|
|
|
86
85
|
/** Blocking only on the host API's worker; never call on the UI thread. */
|
|
87
86
|
static BundlePreparationResult prepareBundle(UpdateContext context) throws InterruptedException {
|
|
88
|
-
if (
|
|
87
|
+
if (BuildConfig.DEBUG) {
|
|
89
88
|
return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "debug");
|
|
90
89
|
}
|
|
91
90
|
if (!nativeReady || sContext != context || !context.getIsUsingBundleUrl()) {
|
|
92
91
|
return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "not_initialized");
|
|
93
92
|
}
|
|
94
93
|
String configJson = context.getKv(KEY_CONFIG);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
JSONObject config = new JSONObject(configJson);
|
|
100
|
-
if (config.optBoolean("disabled", false)) {
|
|
101
|
-
return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "disabled");
|
|
102
|
-
}
|
|
103
|
-
if (config.optString("appKey", "").isEmpty()) {
|
|
104
|
-
return BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config");
|
|
105
|
-
}
|
|
106
|
-
} catch (JSONException e) {
|
|
107
|
-
return BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config");
|
|
94
|
+
BundlePreparationResult rejection = configRejection(configJson);
|
|
95
|
+
if (rejection != null) {
|
|
96
|
+
return rejection;
|
|
108
97
|
}
|
|
109
98
|
startRound(0);
|
|
110
99
|
if (!roundStarted.get()) {
|
|
@@ -112,7 +101,8 @@ final class SyncCoordinator {
|
|
|
112
101
|
}
|
|
113
102
|
roundDone.await();
|
|
114
103
|
if (roundConfigGeneration != UpdateContext.getNativeConfigGeneration()
|
|
115
|
-
|| !
|
|
104
|
+
|| !FlowBridge.configsEquivalent(configJson, roundConfigJson)
|
|
105
|
+
|| !FlowBridge.configsEquivalent(configJson, context.getKv(KEY_CONFIG))) {
|
|
116
106
|
return BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "config_changed");
|
|
117
107
|
}
|
|
118
108
|
if (roundGeneration != UpdateContext.getResetGeneration()) {
|
|
@@ -127,21 +117,49 @@ final class SyncCoordinator {
|
|
|
127
117
|
}
|
|
128
118
|
|
|
129
119
|
/**
|
|
130
|
-
* True when JS already obtained a valid response in this process for
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
120
|
+
* True when JS already obtained a valid response in this process for a
|
|
121
|
+
* config equivalent (FlowBridge.configsEquivalent, contract C1) to
|
|
122
|
+
* the one the native round would use: the delayed round is then a
|
|
123
|
+
* duplicate request. JS and a host may serialize the same config
|
|
124
|
+
* differently, so exact string equality would almost never match. Only
|
|
125
|
+
* the scheduled round consults this — the crash-rescue path still runs,
|
|
126
|
+
* JS is dead by then.
|
|
134
127
|
*/
|
|
135
128
|
private static boolean hasJsRound(UpdateContext context) {
|
|
136
129
|
String jsConfig = sJsCompletedConfig;
|
|
137
|
-
return jsConfig != null
|
|
130
|
+
return jsConfig != null
|
|
131
|
+
&& FlowBridge.configsEquivalent(jsConfig, context.getKv(KEY_CONFIG));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The shared gate (FlowBridge.runnableConfig, update_flow_core) every
|
|
136
|
+
* entry point (prepareBundle, the automatic round's gate, the round
|
|
137
|
+
* itself) applies to the persisted config, so they cannot disagree on
|
|
138
|
+
* what is runnable: null when a round may run, otherwise the result —
|
|
139
|
+
* absent is skipped/not_configured, a non-object failed/invalid_config, a
|
|
140
|
+
* truthy disabled skipped/disabled, a blank appKey failed/invalid_config.
|
|
141
|
+
*/
|
|
142
|
+
@Nullable
|
|
143
|
+
static BundlePreparationResult configRejection(@Nullable String json) {
|
|
144
|
+
try {
|
|
145
|
+
String result = FlowBridge.runnableConfig(json);
|
|
146
|
+
if (result == null) {
|
|
147
|
+
return BundlePreparationResult.of(BundlePreparationResult.FAILED, "internal_error");
|
|
148
|
+
}
|
|
149
|
+
JSONObject verdict = new JSONObject(result);
|
|
150
|
+
return verdict.has("status")
|
|
151
|
+
? BundlePreparationResult.of(verdict.getString("status"), verdict.getString("reason"))
|
|
152
|
+
: null;
|
|
153
|
+
} catch (JSONException e) {
|
|
154
|
+
return BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config");
|
|
155
|
+
}
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
private SyncCoordinator() {
|
|
141
159
|
}
|
|
142
160
|
|
|
143
161
|
static void schedule(final UpdateContext context, final String launchRolledBackVersion) {
|
|
144
|
-
if (
|
|
162
|
+
if (BuildConfig.DEBUG) {
|
|
145
163
|
return;
|
|
146
164
|
}
|
|
147
165
|
if (!scheduled.compareAndSet(false, true)) {
|
|
@@ -155,60 +173,35 @@ final class SyncCoordinator {
|
|
|
155
173
|
if (context.getKv(KEY_CONFIG) != null) {
|
|
156
174
|
CrashHold.install();
|
|
157
175
|
}
|
|
158
|
-
Thread
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"native sync skipped: JS already queried in this process");
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
startRound(0);
|
|
176
|
-
} catch (Throwable e) {
|
|
177
|
-
// The rescue path must never take the app down with it.
|
|
178
|
-
Log.w(Texts.LOG_TAG, "native sync failed: " + e);
|
|
176
|
+
Threads.startDaemon("pushy-native-sync", Thread.MIN_PRIORITY + 1, () -> {
|
|
177
|
+
try {
|
|
178
|
+
// Keep the check away from the cold-start critical path
|
|
179
|
+
// (§7 R5) — unless the previous process died mid-round,
|
|
180
|
+
// in which case every launch second counts (§11.4).
|
|
181
|
+
if (context.getKv(KEY_ROUND_INCOMPLETE) == null) {
|
|
182
|
+
Thread.sleep(5000);
|
|
183
|
+
}
|
|
184
|
+
if (hasJsRound(context)) {
|
|
185
|
+
// Not consuming the round: a later crash rescue may
|
|
186
|
+
// still need it.
|
|
187
|
+
Log.i(Texts.LOG_TAG,
|
|
188
|
+
"native sync skipped: JS already queried in this process");
|
|
189
|
+
return;
|
|
179
190
|
}
|
|
191
|
+
startRound(0);
|
|
192
|
+
} catch (Throwable e) {
|
|
193
|
+
// The rescue path must never take the app down with it.
|
|
194
|
+
Log.w(Texts.LOG_TAG, "native sync failed: " + e);
|
|
180
195
|
}
|
|
181
|
-
}
|
|
182
|
-
thread.setPriority(Thread.MIN_PRIORITY + 1);
|
|
183
|
-
thread.setDaemon(true);
|
|
184
|
-
thread.start();
|
|
196
|
+
});
|
|
185
197
|
}
|
|
186
198
|
|
|
187
199
|
static boolean isRoundInFlight() {
|
|
188
200
|
return roundStarted.get() && !roundCompleted;
|
|
189
201
|
}
|
|
190
202
|
|
|
191
|
-
/**
|
|
192
|
-
* Runs the process's single round on the calling thread if nobody has
|
|
193
|
-
* started it yet. deadlineNanos > 0 (crash rescue) caps every HTTP call
|
|
194
|
-
* and download phase to the remaining budget.
|
|
195
|
-
*/
|
|
196
203
|
private static boolean hasRunnableConfig(UpdateContext context) {
|
|
197
|
-
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
try {
|
|
201
|
-
String json = context.getKv(KEY_CONFIG);
|
|
202
|
-
if (json == null) {
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
JSONObject config = new JSONObject(json);
|
|
206
|
-
return !config.optBoolean("disabled", false)
|
|
207
|
-
&& config.opt("appKey") instanceof String
|
|
208
|
-
&& !config.getString("appKey").trim().isEmpty();
|
|
209
|
-
} catch (JSONException e) {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
204
|
+
return context != null && configRejection(context.getKv(KEY_CONFIG)) == null;
|
|
212
205
|
}
|
|
213
206
|
|
|
214
207
|
static void onConfigured(UpdateContext context) {
|
|
@@ -217,6 +210,11 @@ final class SyncCoordinator {
|
|
|
217
210
|
}
|
|
218
211
|
}
|
|
219
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Runs the process's single round on the calling thread if nobody has
|
|
215
|
+
* started it yet. deadlineNanos > 0 (crash rescue) caps every HTTP call
|
|
216
|
+
* and download phase to the remaining budget.
|
|
217
|
+
*/
|
|
220
218
|
private static void startRound(long deadlineNanos) {
|
|
221
219
|
// An automatic check before first-run provisioning must not consume
|
|
222
220
|
// the process's only round. Hosts may configure later in this launch.
|
|
@@ -310,24 +308,9 @@ final class SyncCoordinator {
|
|
|
310
308
|
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "check_failed");
|
|
311
309
|
String configJson = context.getKv(KEY_CONFIG);
|
|
312
310
|
roundConfigJson = configJson;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
JSONObject config;
|
|
318
|
-
try {
|
|
319
|
-
config = new JSONObject(configJson);
|
|
320
|
-
} catch (JSONException e) {
|
|
321
|
-
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config");
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
if (config.optBoolean("disabled", false)) {
|
|
325
|
-
roundResult = BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "disabled");
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
String appKey = config.optString("appKey", "");
|
|
329
|
-
if (appKey.isEmpty()) {
|
|
330
|
-
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config");
|
|
311
|
+
BundlePreparationResult rejection = configRejection(configJson);
|
|
312
|
+
if (rejection != null) {
|
|
313
|
+
roundResult = rejection;
|
|
331
314
|
return;
|
|
332
315
|
}
|
|
333
316
|
// Keep the existing interrupted-round breadcrumb and reset generation.
|
|
@@ -337,11 +320,15 @@ final class SyncCoordinator {
|
|
|
337
320
|
}
|
|
338
321
|
try {
|
|
339
322
|
runConfiguredRound(
|
|
340
|
-
context, launchRolledBackVersion, deadlineNanos,
|
|
341
|
-
resetGeneration, configJson, config, appKey);
|
|
323
|
+
context, launchRolledBackVersion, deadlineNanos, resetGeneration, configJson);
|
|
342
324
|
} finally {
|
|
325
|
+
// A committed round already cleared the breadcrumb in its own
|
|
326
|
+
// commit (UpdateContext.commitSyncResult); only rounds that ended
|
|
327
|
+
// without committing need the extra write.
|
|
343
328
|
try {
|
|
344
|
-
context.
|
|
329
|
+
if (context.getKv(KEY_ROUND_INCOMPLETE) != null) {
|
|
330
|
+
context.removeKv(KEY_ROUND_INCOMPLETE);
|
|
331
|
+
}
|
|
345
332
|
} catch (IllegalStateException ignored) {
|
|
346
333
|
}
|
|
347
334
|
}
|
|
@@ -352,64 +339,37 @@ final class SyncCoordinator {
|
|
|
352
339
|
String launchRolledBackVersion,
|
|
353
340
|
long deadlineNanos,
|
|
354
341
|
long resetGeneration,
|
|
355
|
-
String configJson
|
|
356
|
-
JSONObject config,
|
|
357
|
-
String appKey
|
|
342
|
+
String configJson
|
|
358
343
|
) throws JSONException {
|
|
359
|
-
String packageVersion = config.optString(
|
|
360
|
-
"packageVersion", context.getPackageVersion());
|
|
361
|
-
if (packageVersion.isEmpty()) {
|
|
362
|
-
packageVersion = context.getPackageVersion();
|
|
363
|
-
}
|
|
364
|
-
|
|
365
344
|
String currentVersion = context.getCurrentVersion();
|
|
345
|
+
// This launch as the shared core sees it (native_round.h). Absent
|
|
346
|
+
// values (null) are left out.
|
|
347
|
+
JSONObject env = new JSONObject();
|
|
348
|
+
env.put("packageVersion", context.getPackageVersion());
|
|
349
|
+
// The JS constant's form, so JS can reuse the response cache.
|
|
350
|
+
env.put("currentVersion", currentVersion == null ? JSONObject.NULL : currentVersion);
|
|
351
|
+
env.put("uuid", context.getKv("uuid"));
|
|
366
352
|
// Snapshot captured on the launch path before getConstants consumes
|
|
367
353
|
// the one-shot rollback marker. Reading SharedPreferences here, five
|
|
368
354
|
// seconds later, would lose the guard and could forceBoot the version
|
|
369
355
|
// that this very launch just rolled back.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (uuid == null) {
|
|
373
|
-
uuid = "";
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
JSONObject identity = new JSONObject();
|
|
377
|
-
identity.put("packageVersion", packageVersion);
|
|
378
|
-
identity.put(
|
|
379
|
-
"currentVersion",
|
|
380
|
-
currentVersion == null ? JSONObject.NULL : currentVersion
|
|
381
|
-
);
|
|
382
|
-
identity.put("uuid", uuid);
|
|
383
|
-
if (rolledBackVersion != null) {
|
|
384
|
-
identity.put("rolledBackVersion", rolledBackVersion);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
JSONObject cInfo = new JSONObject();
|
|
388
|
-
cInfo.put("rnu", config.optString("rnu", ""));
|
|
389
|
-
cInfo.put("rn", config.optString("rn", ""));
|
|
356
|
+
env.put("rolledBackVersion", launchRolledBackVersion);
|
|
357
|
+
env.put("buildTime", context.getBuildTime());
|
|
390
358
|
// React Native's Platform.Version is the Android SDK integer; use the
|
|
391
359
|
// same value so this request can be fingerprinted against the JS one.
|
|
392
|
-
|
|
393
|
-
|
|
360
|
+
env.put("os", "android " + Build.VERSION.SDK_INT);
|
|
361
|
+
env.put("bundleHash", context.computeBundleHash());
|
|
362
|
+
String envJson = env.toString();
|
|
394
363
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
input.put(
|
|
398
|
-
"currentVersion",
|
|
399
|
-
currentVersion == null ? JSONObject.NULL : currentVersion
|
|
400
|
-
);
|
|
401
|
-
input.put("buildTime", context.getBuildTime());
|
|
402
|
-
input.put("cInfo", cInfo);
|
|
403
|
-
input.put("supportedDiffVersion", NativeCore.supportedDiffVersion());
|
|
404
|
-
input.put("bundleHash", context.computeBundleHash());
|
|
405
|
-
|
|
406
|
-
String body = FlowBridge.buildRequestBody(input.toString());
|
|
407
|
-
if (body == null) {
|
|
364
|
+
String preparedJson = FlowBridge.prepareRound(configJson, envJson, Math.random());
|
|
365
|
+
if (preparedJson == null) {
|
|
408
366
|
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_request");
|
|
409
367
|
return;
|
|
410
368
|
}
|
|
411
|
-
|
|
412
|
-
String
|
|
369
|
+
JSONObject prepared = new JSONObject(preparedJson);
|
|
370
|
+
String body = prepared.getString("body");
|
|
371
|
+
String responseText =
|
|
372
|
+
runQueryRequest(prepared.getJSONObject("query").toString(), body, deadlineNanos);
|
|
413
373
|
if (responseText == null) {
|
|
414
374
|
Log.i(Texts.LOG_TAG,
|
|
415
375
|
"native sync: no endpoint reachable, giving up until next launch");
|
|
@@ -417,19 +377,18 @@ final class SyncCoordinator {
|
|
|
417
377
|
}
|
|
418
378
|
// Cache freshness is anchored to when the server response arrived,
|
|
419
379
|
// not to when a potentially long download/patch/activation finished.
|
|
420
|
-
final long responseAtSeconds = System.currentTimeMillis() / 1000;
|
|
421
|
-
|
|
422
380
|
String decisionJson = FlowBridge.handleResponse(
|
|
423
|
-
responseText,
|
|
381
|
+
responseText, configJson, envJson, body, System.currentTimeMillis() / 1000);
|
|
424
382
|
if (decisionJson == null) {
|
|
425
383
|
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_response");
|
|
426
384
|
return;
|
|
427
385
|
}
|
|
428
386
|
JSONObject decision = new JSONObject(decisionJson);
|
|
387
|
+
// Built once; every commit below caches it for JS.
|
|
388
|
+
final String responseCacheJson = decision.getString("cache");
|
|
429
389
|
if (!"download".equals(decision.optString("action"))) {
|
|
430
390
|
boolean committed = context.commitSyncResult(
|
|
431
|
-
resetGeneration, null, null, false,
|
|
432
|
-
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
|
|
391
|
+
resetGeneration, null, null, false, responseCacheJson);
|
|
433
392
|
roundResult = committed
|
|
434
393
|
? BundlePreparationResult.of(BundlePreparationResult.NO_UPDATE, decision.optString("reason"))
|
|
435
394
|
: BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "reset");
|
|
@@ -446,47 +405,29 @@ final class SyncCoordinator {
|
|
|
446
405
|
boolean downloaded = context.hasCompletedVersion(hash);
|
|
447
406
|
if (!downloaded) {
|
|
448
407
|
downloaded = performAttempts(
|
|
449
|
-
|
|
450
|
-
|
|
408
|
+
decision.optJSONArray("attempts"), deadlineNanos,
|
|
409
|
+
downloadStarter(context, hash, currentVersion));
|
|
451
410
|
}
|
|
452
411
|
if (!downloaded) {
|
|
453
412
|
// The native attempt has finished, so JS may safely reuse the
|
|
454
413
|
// response and retry through its own strategy chain.
|
|
455
414
|
boolean committed = context.commitSyncResult(
|
|
456
|
-
resetGeneration, null, null, false,
|
|
457
|
-
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
|
|
415
|
+
resetGeneration, null, null, false, responseCacheJson);
|
|
458
416
|
roundResult = BundlePreparationResult.of(
|
|
459
417
|
committed ? BundlePreparationResult.FAILED : BundlePreparationResult.CANCELLED,
|
|
460
418
|
committed ? "download_failed" : "reset");
|
|
461
419
|
return;
|
|
462
420
|
}
|
|
463
421
|
|
|
464
|
-
// Version info (mirroring the JS side's setLocalHashInfo
|
|
465
|
-
//
|
|
466
|
-
// see
|
|
467
|
-
|
|
468
|
-
JSONObject
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
for (String key : new String[] {"name", "description", "metaInfo"}) {
|
|
472
|
-
Object value = info.opt(key);
|
|
473
|
-
if (value instanceof String) {
|
|
474
|
-
hashInfo.put(key, value);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
// A forceBoot activation is the brick-rescue path: mark it in the
|
|
478
|
-
// persisted info so JS can report force_boot_rescue when this
|
|
479
|
-
// version survives to markSuccess. Only the server-sent directive
|
|
480
|
-
// counts — a silent-strategy activation is ordinary delivery.
|
|
481
|
-
JSONObject infoConfig = info.optJSONObject("config");
|
|
482
|
-
if (infoConfig != null && infoConfig.optBoolean("forceBoot", false)) {
|
|
483
|
-
hashInfo.put(INFO_FORCE_BOOT, true);
|
|
484
|
-
}
|
|
485
|
-
if (crashHoldActive) {
|
|
486
|
-
hashInfo.put(INFO_CRASH_HOLD, true);
|
|
487
|
-
}
|
|
488
|
-
hashInfoJson = hashInfo.toString();
|
|
422
|
+
// Version info (mirroring the JS side's setLocalHashInfo, the
|
|
423
|
+
// forceBoot rescue marked by the core), the activation and the
|
|
424
|
+
// response cache all land in one atomic commit — see
|
|
425
|
+
// UpdateContext.commitSyncResult.
|
|
426
|
+
JSONObject hashInfo = decision.getJSONObject("hashInfo");
|
|
427
|
+
if (crashHoldActive) {
|
|
428
|
+
hashInfo.put(INFO_CRASH_HOLD, true);
|
|
489
429
|
}
|
|
430
|
+
String hashInfoJson = hashInfo.toString();
|
|
490
431
|
// Silent strategies or a server-marked forceBoot version (per-version
|
|
491
432
|
// remote override — the brick rescue) activate for the next launch;
|
|
492
433
|
// otherwise activation stays with the JS side. Unless a crash is
|
|
@@ -500,7 +441,7 @@ final class SyncCoordinator {
|
|
|
500
441
|
hash,
|
|
501
442
|
hashInfoJson,
|
|
502
443
|
activate,
|
|
503
|
-
|
|
444
|
+
responseCacheJson);
|
|
504
445
|
} catch (Exception e) {
|
|
505
446
|
Log.w(Texts.LOG_TAG, "native sync: commit failed: " + e);
|
|
506
447
|
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "commit_failed");
|
|
@@ -525,21 +466,7 @@ final class SyncCoordinator {
|
|
|
525
466
|
: BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "reset");
|
|
526
467
|
}
|
|
527
468
|
|
|
528
|
-
private static
|
|
529
|
-
String configJson,
|
|
530
|
-
String requestBody,
|
|
531
|
-
String responseText,
|
|
532
|
-
long responseAtSeconds
|
|
533
|
-
) throws JSONException {
|
|
534
|
-
JSONObject cacheEntry = new JSONObject();
|
|
535
|
-
cacheEntry.put("ts", responseAtSeconds);
|
|
536
|
-
cacheEntry.put("body", responseText);
|
|
537
|
-
cacheEntry.put("request", requestBody);
|
|
538
|
-
cacheEntry.put("config", configJson);
|
|
539
|
-
return cacheEntry.toString();
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
private static final OkHttpClient httpClient = new OkHttpClient.Builder()
|
|
469
|
+
private static final OkHttpClient httpClient = HttpClients.base().newBuilder()
|
|
543
470
|
.connectTimeout(10, TimeUnit.SECONDS)
|
|
544
471
|
.readTimeout(10, TimeUnit.SECONDS)
|
|
545
472
|
.callTimeout(15, TimeUnit.SECONDS)
|
|
@@ -553,25 +480,13 @@ final class SyncCoordinator {
|
|
|
553
480
|
builder.post(RequestBody.create(
|
|
554
481
|
postBody, MediaType.parse("application/json; charset=utf-8")));
|
|
555
482
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
// handler's hold window.
|
|
560
|
-
long remainingMillis = TimeUnit.NANOSECONDS.toMillis(
|
|
561
|
-
deadlineNanos - System.nanoTime());
|
|
562
|
-
if (remainingMillis <= 0) {
|
|
563
|
-
return null;
|
|
564
|
-
}
|
|
565
|
-
if (remainingMillis < 15000) {
|
|
566
|
-
client = httpClient.newBuilder()
|
|
567
|
-
.callTimeout(remainingMillis, TimeUnit.MILLISECONDS)
|
|
568
|
-
.build();
|
|
569
|
-
}
|
|
570
|
-
}
|
|
483
|
+
// Crash-rescue budget: never let a single request outlive the
|
|
484
|
+
// handler's hold window.
|
|
485
|
+
OkHttpClient client = HttpClients.withDeadline(httpClient, deadlineNanos);
|
|
571
486
|
try (Response response = client.newCall(builder.build()).execute()) {
|
|
572
487
|
// Same rule as the artifact download: an https endpoint that
|
|
573
488
|
// redirects to plaintext http is a failed endpoint.
|
|
574
|
-
|
|
489
|
+
HttpClients.rejectProtocolDowngrade(url, response);
|
|
575
490
|
ResponseBody body = response.body();
|
|
576
491
|
if (!response.isSuccessful() || body == null) {
|
|
577
492
|
return null;
|
|
@@ -609,89 +524,32 @@ final class SyncCoordinator {
|
|
|
609
524
|
return source.readString(charset == null ? StandardCharsets.UTF_8 : charset);
|
|
610
525
|
}
|
|
611
526
|
|
|
612
|
-
// Shared schema rule (update_flow_core::IsValidResponse): a 200 with
|
|
613
|
-
// `{"error": ...}` is a failed endpoint, not a verdict, and must not stop
|
|
614
|
-
// the endpoint fallback.
|
|
615
|
-
private static boolean isValidResponse(String responseText) {
|
|
616
|
-
return responseText != null && FlowBridge.isValidResponse(responseText);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
527
|
/**
|
|
620
|
-
* Sequential fallback over the
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
528
|
+
* Sequential fallback over the endpoints (§5.1), one request at a time
|
|
529
|
+
* with its own timeout, as the shared core walks it
|
|
530
|
+
* (FlowBridge.nextQueryStep: order, list discovery, https-only remote
|
|
531
|
+
* bases, budget, response validity). No hedged race on purpose — this
|
|
624
532
|
* path is latency-insensitive.
|
|
625
533
|
*/
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
try {
|
|
634
|
-
ordered = orderedJson == null ? new JSONArray() : new JSONArray(orderedJson);
|
|
635
|
-
} catch (JSONException e) {
|
|
636
|
-
return null;
|
|
637
|
-
}
|
|
638
|
-
HashSet<String> tried = new HashSet<>();
|
|
639
|
-
int httpAttempts = 0;
|
|
640
|
-
for (int i = 0; i < ordered.length(); i++) {
|
|
641
|
-
String base = HttpUtils.normalizeEndpointBase(ordered.optString(i, ""));
|
|
642
|
-
if (base.isEmpty() || !tried.add(base)) {
|
|
643
|
-
continue;
|
|
644
|
-
}
|
|
645
|
-
if (httpAttempts++ >= MAX_QUERY_HTTP_ATTEMPTS) {
|
|
534
|
+
@Nullable
|
|
535
|
+
private static String runQueryRequest(String state, String body, long deadlineNanos)
|
|
536
|
+
throws JSONException {
|
|
537
|
+
String text = null;
|
|
538
|
+
while (true) {
|
|
539
|
+
state = FlowBridge.nextQueryStep(state, text);
|
|
540
|
+
if (state == null) {
|
|
646
541
|
return null;
|
|
647
542
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
return response;
|
|
543
|
+
JSONObject step = new JSONObject(state);
|
|
544
|
+
if (step.optBoolean("found")) {
|
|
545
|
+
return text;
|
|
652
546
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (queryUrls == null) {
|
|
656
|
-
return null;
|
|
657
|
-
}
|
|
658
|
-
for (int i = 0; i < queryUrls.length(); i++) {
|
|
659
|
-
String listUrl = queryUrls.optString(i, "");
|
|
660
|
-
if (listUrl.isEmpty()) {
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
if (httpAttempts++ >= MAX_QUERY_HTTP_ATTEMPTS) {
|
|
547
|
+
String url = step.optString("url", "");
|
|
548
|
+
if (url.isEmpty()) {
|
|
664
549
|
return null;
|
|
665
550
|
}
|
|
666
|
-
|
|
667
|
-
if (listText == null) {
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
JSONArray remote;
|
|
671
|
-
try {
|
|
672
|
-
remote = new JSONArray(listText);
|
|
673
|
-
} catch (JSONException e) {
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
|
-
for (int j = 0; j < remote.length(); j++) {
|
|
677
|
-
String base = HttpUtils.normalizeEndpointBase(remote.optString(j, ""));
|
|
678
|
-
if (base.isEmpty() || tried.contains(base)) {
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
if (httpAttempts++ >= MAX_QUERY_HTTP_ATTEMPTS) {
|
|
682
|
-
return null;
|
|
683
|
-
}
|
|
684
|
-
tried.add(base);
|
|
685
|
-
String response = httpRequest(
|
|
686
|
-
base + HttpUtils.QUERY_PATH + appKey, body, deadlineNanos);
|
|
687
|
-
if (isValidResponse(response)) {
|
|
688
|
-
return response;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
// One successfully fetched remote list is enough.
|
|
692
|
-
break;
|
|
551
|
+
text = httpRequest(url, step.optBoolean("post") ? body : null, deadlineNanos);
|
|
693
552
|
}
|
|
694
|
-
return null;
|
|
695
553
|
}
|
|
696
554
|
|
|
697
555
|
private static long capToHoldBudget(long phaseDeadlineNanos, long holdDeadlineNanos) {
|
|
@@ -701,9 +559,35 @@ final class SyncCoordinator {
|
|
|
701
559
|
return Math.min(phaseDeadlineNanos, holdDeadlineNanos);
|
|
702
560
|
}
|
|
703
561
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
562
|
+
/** Queues one download attempt; null when it was rejected up front (the listener was told). */
|
|
563
|
+
interface DownloadStarter {
|
|
564
|
+
@Nullable
|
|
565
|
+
DownloadTaskParams start(
|
|
566
|
+
String type, String url, UpdateContext.DownloadFileListener listener, long deadlineNanos);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private static DownloadStarter downloadStarter(
|
|
570
|
+
final UpdateContext context, final String hash, final String originHash
|
|
571
|
+
) {
|
|
572
|
+
return (type, url, listener, deadlineNanos) -> {
|
|
573
|
+
if ("diff".equals(type)) {
|
|
574
|
+
return context.downloadPatchFromPpk(url, hash, originHash, listener, deadlineNanos);
|
|
575
|
+
}
|
|
576
|
+
if ("pdiff".equals(type)) {
|
|
577
|
+
return context.downloadPatchFromApk(url, hash, listener, deadlineNanos);
|
|
578
|
+
}
|
|
579
|
+
return context.downloadFullUpdate(url, hash, listener, deadlineNanos);
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Strategies in the core's order (diff, pdiff, full), each over its
|
|
585
|
+
* mirrors, already planned for this device (FlowBridge.handleResponse:
|
|
586
|
+
* no diff without a running version, no cleartext URL under an
|
|
587
|
+
* https-only config).
|
|
588
|
+
*/
|
|
589
|
+
static boolean performAttempts(
|
|
590
|
+
@Nullable JSONArray attempts, long holdDeadlineNanos, DownloadStarter starter
|
|
707
591
|
) {
|
|
708
592
|
if (attempts == null) {
|
|
709
593
|
return false;
|
|
@@ -718,10 +602,6 @@ final class SyncCoordinator {
|
|
|
718
602
|
continue;
|
|
719
603
|
}
|
|
720
604
|
String type = attempt.optString("type");
|
|
721
|
-
if ("diff".equals(type) && (originHash == null || originHash.isEmpty())) {
|
|
722
|
-
// diff patches from the running version; none is running.
|
|
723
|
-
continue;
|
|
724
|
-
}
|
|
725
605
|
final boolean isFullAttempt = !"diff".equals(type) && !"pdiff".equals(type);
|
|
726
606
|
if (isFullAttempt && fullDeadlineNanos == 0) {
|
|
727
607
|
// Incremental failures must not consume the last-resort full
|
|
@@ -739,9 +619,6 @@ final class SyncCoordinator {
|
|
|
739
619
|
}
|
|
740
620
|
for (int j = 0; j < urls.length(); j++) {
|
|
741
621
|
String url = urls.optString(j, "");
|
|
742
|
-
if (url.isEmpty()) {
|
|
743
|
-
continue;
|
|
744
|
-
}
|
|
745
622
|
// Check before enqueueing: once the phase budget is gone we
|
|
746
623
|
// must not launch an orphan download that outlives the round.
|
|
747
624
|
long remainingNanos = deadlineNanos - System.nanoTime();
|
|
@@ -753,6 +630,7 @@ final class SyncCoordinator {
|
|
|
753
630
|
}
|
|
754
631
|
final CountDownLatch latch = new CountDownLatch(1);
|
|
755
632
|
final AtomicBoolean succeeded = new AtomicBoolean(false);
|
|
633
|
+
final AtomicReference<Throwable> failure = new AtomicReference<>();
|
|
756
634
|
final String attemptType = type;
|
|
757
635
|
UpdateContext.DownloadFileListener listener =
|
|
758
636
|
new UpdateContext.DownloadFileListener() {
|
|
@@ -766,20 +644,11 @@ final class SyncCoordinator {
|
|
|
766
644
|
public void onDownloadFailed(Throwable error) {
|
|
767
645
|
Log.i(Texts.LOG_TAG, "native sync: " + attemptType
|
|
768
646
|
+ " attempt failed: " + error);
|
|
647
|
+
failure.set(error);
|
|
769
648
|
latch.countDown();
|
|
770
649
|
}
|
|
771
650
|
};
|
|
772
|
-
DownloadTaskParams task;
|
|
773
|
-
if ("diff".equals(type)) {
|
|
774
|
-
task = context.downloadPatchFromPpk(
|
|
775
|
-
url, hash, originHash, listener, deadlineNanos);
|
|
776
|
-
} else if ("pdiff".equals(type)) {
|
|
777
|
-
task = context.downloadPatchFromApk(
|
|
778
|
-
url, hash, listener, deadlineNanos);
|
|
779
|
-
} else {
|
|
780
|
-
task = context.downloadFullUpdate(
|
|
781
|
-
url, hash, listener, deadlineNanos);
|
|
782
|
-
}
|
|
651
|
+
DownloadTaskParams task = starter.start(type, url, listener, deadlineNanos);
|
|
783
652
|
try {
|
|
784
653
|
if (!latch.await(remainingNanos, TimeUnit.NANOSECONDS)) {
|
|
785
654
|
Log.w(Texts.LOG_TAG,
|
|
@@ -806,6 +675,14 @@ final class SyncCoordinator {
|
|
|
806
675
|
if (succeeded.get()) {
|
|
807
676
|
return true;
|
|
808
677
|
}
|
|
678
|
+
Throwable error = failure.get();
|
|
679
|
+
if (error != null
|
|
680
|
+
&& !ErrorCodes.isMirrorRetryable(ErrorCodes.forDownloadFailure(error))) {
|
|
681
|
+
// The bytes arrived but could not be applied: every
|
|
682
|
+
// mirror serves the same artifact, so move on to the
|
|
683
|
+
// next strategy instead of downloading it again.
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
809
686
|
}
|
|
810
687
|
}
|
|
811
688
|
return false;
|