react-native-update 10.45.0 → 10.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +24 -0
- package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +2 -1
- package/android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java +2 -1
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +13 -2
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +15 -15
- package/cpp/patch_core/error_codes.h +43 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +20 -2
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +83 -23
- package/ios/RCTPushy/RCTPushyDownloader.mm +22 -1
- package/package.json +2 -1
- package/src/client.ts +197 -27
- package/src/core.ts +3 -1
- package/src/endpoint.ts +4 -2
- package/src/error.ts +68 -0
- package/src/index.ts +3 -0
- package/src/locales/en.ts +5 -1
- package/src/locales/zh.ts +3 -1
- package/src/provider.tsx +50 -24
- package/src/telemetry.ts +83 -0
- package/src/type.ts +12 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package cn.reactnative.modules.update;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stable, machine-readable error codes used as the promise rejection code so
|
|
5
|
+
* the JS layer and user loggers can aggregate errors across platforms.
|
|
6
|
+
*
|
|
7
|
+
* MUST stay in sync with cpp/patch_core/error_codes.h (the single source of
|
|
8
|
+
* truth) and src/error.ts (UpdateErrorCode). Messages are free-form; only the
|
|
9
|
+
* codes are part of the contract.
|
|
10
|
+
*/
|
|
11
|
+
final class ErrorCodes {
|
|
12
|
+
static final String INVALID_OPTIONS = "INVALID_OPTIONS";
|
|
13
|
+
static final String DOWNLOAD_FAILED = "DOWNLOAD_FAILED";
|
|
14
|
+
static final String PATCH_FAILED = "PATCH_FAILED";
|
|
15
|
+
static final String FILE_OPERATION_FAILED = "FILE_OPERATION_FAILED";
|
|
16
|
+
static final String SWITCH_VERSION_FAILED = "SWITCH_VERSION_FAILED";
|
|
17
|
+
static final String MARK_SUCCESS_FAILED = "MARK_SUCCESS_FAILED";
|
|
18
|
+
static final String RESTART_FAILED = "RESTART_FAILED";
|
|
19
|
+
static final String INVALID_HASH_INFO = "INVALID_HASH_INFO";
|
|
20
|
+
static final String UNSUPPORTED_PLATFORM = "UNSUPPORTED_PLATFORM";
|
|
21
|
+
|
|
22
|
+
private ErrorCodes() {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -43,6 +43,7 @@ final class StateSerialRunner {
|
|
|
43
43
|
|
|
44
44
|
static void run(
|
|
45
45
|
@Nullable final Promise promise,
|
|
46
|
+
final String errorCode,
|
|
46
47
|
final String operationName,
|
|
47
48
|
final Operation operation
|
|
48
49
|
) {
|
|
@@ -53,7 +54,7 @@ final class StateSerialRunner {
|
|
|
53
54
|
operation.run();
|
|
54
55
|
} catch (Throwable error) {
|
|
55
56
|
if (promise != null) {
|
|
56
|
-
promise.reject(operationName + " failed", error);
|
|
57
|
+
promise.reject(errorCode, operationName + " failed", error);
|
|
57
58
|
} else {
|
|
58
59
|
Log.e(UpdateContext.TAG, operationName + " failed", error);
|
|
59
60
|
}
|
|
@@ -15,6 +15,7 @@ final class UiThreadRunner {
|
|
|
15
15
|
|
|
16
16
|
static void run(
|
|
17
17
|
@Nullable final Promise promise,
|
|
18
|
+
final String errorCode,
|
|
18
19
|
final String operationName,
|
|
19
20
|
final Operation operation
|
|
20
21
|
) {
|
|
@@ -25,7 +26,7 @@ final class UiThreadRunner {
|
|
|
25
26
|
operation.run();
|
|
26
27
|
} catch (Throwable error) {
|
|
27
28
|
if (promise != null) {
|
|
28
|
-
promise.reject(operationName + " failed", error);
|
|
29
|
+
promise.reject(errorCode, operationName + " failed", error);
|
|
29
30
|
} else {
|
|
30
31
|
Log.e(UpdateContext.TAG, operationName + " failed", error);
|
|
31
32
|
}
|
|
@@ -207,8 +207,10 @@ public class UpdateContext {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
private void persistEditor(SharedPreferences.Editor editor, String reason) {
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
// A lost state write can mean a missed rollback or a version switch
|
|
211
|
+
// that silently never happens, so this must be visible in release too.
|
|
212
|
+
if (!editor.commit()) {
|
|
213
|
+
Log.e(TAG, "Failed to persist update state for " + reason);
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
216
|
|
|
@@ -367,6 +369,13 @@ public class UpdateContext {
|
|
|
367
369
|
ignoreRollback,
|
|
368
370
|
true
|
|
369
371
|
);
|
|
372
|
+
if (launchState.didRollback) {
|
|
373
|
+
// The crash-protection rollback: the new version never called
|
|
374
|
+
// markSuccess. Keep this visible in release logs.
|
|
375
|
+
Log.e(TAG, "Version " + currentState.currentVersion
|
|
376
|
+
+ " was not marked as successful, rolling back to "
|
|
377
|
+
+ launchState.currentVersion);
|
|
378
|
+
}
|
|
370
379
|
if (launchState.didRollback || launchState.consumedFirstTime) {
|
|
371
380
|
SharedPreferences.Editor editor = sp.edit();
|
|
372
381
|
applyState(editor, launchState);
|
|
@@ -411,6 +420,8 @@ public class UpdateContext {
|
|
|
411
420
|
false,
|
|
412
421
|
false
|
|
413
422
|
);
|
|
423
|
+
Log.e(TAG, "Rolling back version " + currentState.currentVersion
|
|
424
|
+
+ " to " + nextState.currentVersion);
|
|
414
425
|
SharedPreferences.Editor editor = sp.edit();
|
|
415
426
|
applyState(editor, nextState);
|
|
416
427
|
persistEditor(editor, "rollback");
|
|
@@ -41,7 +41,7 @@ public class UpdateModuleImpl {
|
|
|
41
41
|
|
|
42
42
|
@Override
|
|
43
43
|
public void onDownloadFailed(Throwable error) {
|
|
44
|
-
promise.reject(error);
|
|
44
|
+
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
}
|
|
@@ -64,7 +64,7 @@ public class UpdateModuleImpl {
|
|
|
64
64
|
|
|
65
65
|
@Override
|
|
66
66
|
public void onDownloadFailed(Throwable error) {
|
|
67
|
-
promise.reject(error);
|
|
67
|
+
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
}
|
|
@@ -84,7 +84,7 @@ public class UpdateModuleImpl {
|
|
|
84
84
|
|
|
85
85
|
@Override
|
|
86
86
|
public void onDownloadFailed(Throwable error) {
|
|
87
|
-
promise.reject(error);
|
|
87
|
+
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
90
|
}
|
|
@@ -107,11 +107,11 @@ public class UpdateModuleImpl {
|
|
|
107
107
|
|
|
108
108
|
@Override
|
|
109
109
|
public void onDownloadFailed(Throwable error) {
|
|
110
|
-
promise.reject(error);
|
|
110
|
+
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
} catch (Exception e) {
|
|
114
|
-
promise.reject("downloadPatchFromPpk failed: " + e.getMessage());
|
|
114
|
+
promise.reject(ErrorCodes.INVALID_OPTIONS, "downloadPatchFromPpk failed: " + e.getMessage(), e);
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -130,7 +130,7 @@ public class UpdateModuleImpl {
|
|
|
130
130
|
@Nullable final String hash,
|
|
131
131
|
final Promise promise
|
|
132
132
|
) {
|
|
133
|
-
UiThreadRunner.run(promise, "restartApp", new UiThreadRunner.Operation() {
|
|
133
|
+
UiThreadRunner.run(promise, ErrorCodes.RESTART_FAILED, "restartApp", new UiThreadRunner.Operation() {
|
|
134
134
|
@Override
|
|
135
135
|
public void run() throws Throwable {
|
|
136
136
|
ReactReloadManager.restartApp(updateContext, reactContext, hash);
|
|
@@ -149,7 +149,7 @@ public class UpdateModuleImpl {
|
|
|
149
149
|
final Promise promise
|
|
150
150
|
) {
|
|
151
151
|
final String hash = options.getString("hash");
|
|
152
|
-
StateSerialRunner.run(promise, "switchVersionLater", new StateSerialRunner.Operation() {
|
|
152
|
+
StateSerialRunner.run(promise, ErrorCodes.SWITCH_VERSION_FAILED, "switchVersionLater", new StateSerialRunner.Operation() {
|
|
153
153
|
@Override
|
|
154
154
|
public void run() {
|
|
155
155
|
setNeedUpdateInternal(updateContext, hash);
|
|
@@ -160,7 +160,7 @@ public class UpdateModuleImpl {
|
|
|
160
160
|
|
|
161
161
|
public static void setNeedUpdate(final UpdateContext updateContext, final ReadableMap options) {
|
|
162
162
|
final String hash = options.getString("hash");
|
|
163
|
-
StateSerialRunner.run(null, "switchVersionLater", new StateSerialRunner.Operation() {
|
|
163
|
+
StateSerialRunner.run(null, ErrorCodes.SWITCH_VERSION_FAILED, "switchVersionLater", new StateSerialRunner.Operation() {
|
|
164
164
|
@Override
|
|
165
165
|
public void run() {
|
|
166
166
|
setNeedUpdateInternal(updateContext, hash);
|
|
@@ -173,7 +173,7 @@ public class UpdateModuleImpl {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
public static void markSuccess(final UpdateContext updateContext, final Promise promise) {
|
|
176
|
-
StateSerialRunner.run(promise, "markSuccess", new StateSerialRunner.Operation() {
|
|
176
|
+
StateSerialRunner.run(promise, ErrorCodes.MARK_SUCCESS_FAILED, "markSuccess", new StateSerialRunner.Operation() {
|
|
177
177
|
@Override
|
|
178
178
|
public void run() {
|
|
179
179
|
markSuccessInternal(updateContext);
|
|
@@ -183,7 +183,7 @@ public class UpdateModuleImpl {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
public static void markSuccess(final UpdateContext updateContext) {
|
|
186
|
-
StateSerialRunner.run(null, "markSuccess", new StateSerialRunner.Operation() {
|
|
186
|
+
StateSerialRunner.run(null, ErrorCodes.MARK_SUCCESS_FAILED, "markSuccess", new StateSerialRunner.Operation() {
|
|
187
187
|
@Override
|
|
188
188
|
public void run() {
|
|
189
189
|
markSuccessInternal(updateContext);
|
|
@@ -200,7 +200,7 @@ public class UpdateModuleImpl {
|
|
|
200
200
|
final String uuid,
|
|
201
201
|
final Promise promise
|
|
202
202
|
) {
|
|
203
|
-
StateSerialRunner.run(promise, "setUuid", new StateSerialRunner.Operation() {
|
|
203
|
+
StateSerialRunner.run(promise, ErrorCodes.FILE_OPERATION_FAILED, "setUuid", new StateSerialRunner.Operation() {
|
|
204
204
|
@Override
|
|
205
205
|
public void run() {
|
|
206
206
|
setUuidInternal(updateContext, uuid);
|
|
@@ -210,7 +210,7 @@ public class UpdateModuleImpl {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
public static void setUuid(final UpdateContext updateContext, final String uuid) {
|
|
213
|
-
StateSerialRunner.run(null, "setUuid", new StateSerialRunner.Operation() {
|
|
213
|
+
StateSerialRunner.run(null, ErrorCodes.FILE_OPERATION_FAILED, "setUuid", new StateSerialRunner.Operation() {
|
|
214
214
|
@Override
|
|
215
215
|
public void run() {
|
|
216
216
|
setUuidInternal(updateContext, uuid);
|
|
@@ -235,7 +235,7 @@ public class UpdateModuleImpl {
|
|
|
235
235
|
final String info,
|
|
236
236
|
final Promise promise
|
|
237
237
|
) {
|
|
238
|
-
StateSerialRunner.run(promise, "setLocalHashInfo", new StateSerialRunner.Operation() {
|
|
238
|
+
StateSerialRunner.run(promise, ErrorCodes.INVALID_HASH_INFO, "setLocalHashInfo", new StateSerialRunner.Operation() {
|
|
239
239
|
@Override
|
|
240
240
|
public void run() {
|
|
241
241
|
setLocalHashInfoInternal(updateContext, hash, info);
|
|
@@ -249,7 +249,7 @@ public class UpdateModuleImpl {
|
|
|
249
249
|
final String hash,
|
|
250
250
|
final String info
|
|
251
251
|
) {
|
|
252
|
-
StateSerialRunner.run(null, "setLocalHashInfo", new StateSerialRunner.Operation() {
|
|
252
|
+
StateSerialRunner.run(null, ErrorCodes.INVALID_HASH_INFO, "setLocalHashInfo", new StateSerialRunner.Operation() {
|
|
253
253
|
@Override
|
|
254
254
|
public void run() {
|
|
255
255
|
setLocalHashInfoInternal(updateContext, hash, info);
|
|
@@ -264,7 +264,7 @@ public class UpdateModuleImpl {
|
|
|
264
264
|
) {
|
|
265
265
|
String value = updateContext.getKv("hash_" + hash);
|
|
266
266
|
if (!isValidHashInfo(value)) {
|
|
267
|
-
promise.reject("getLocalHashInfo failed: invalid json string");
|
|
267
|
+
promise.reject(ErrorCodes.INVALID_HASH_INFO, "getLocalHashInfo failed: invalid json string");
|
|
268
268
|
return;
|
|
269
269
|
}
|
|
270
270
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#ifndef PUSHY_PATCH_CORE_ERROR_CODES_H_
|
|
2
|
+
#define PUSHY_PATCH_CORE_ERROR_CODES_H_
|
|
3
|
+
|
|
4
|
+
// Single source of truth for the stable, machine-readable error codes shared
|
|
5
|
+
// by every platform. Native modules reject promises with one of these codes
|
|
6
|
+
// so the JS layer (src/error.ts UpdateErrorCode) and user loggers can
|
|
7
|
+
// aggregate errors across platforms and locales.
|
|
8
|
+
//
|
|
9
|
+
// Mirrors that cannot include this header MUST stay in sync by hand:
|
|
10
|
+
// - src/error.ts (UpdateErrorCode union, JS layer)
|
|
11
|
+
// - android/.../ErrorCodes.java (Java constants)
|
|
12
|
+
// iOS (RCTPushy.mm) includes this header directly.
|
|
13
|
+
//
|
|
14
|
+
// Human-readable messages are NOT part of this contract: they may differ per
|
|
15
|
+
// platform and locale. Only the codes are stable.
|
|
16
|
+
|
|
17
|
+
namespace pushy {
|
|
18
|
+
namespace error_codes {
|
|
19
|
+
|
|
20
|
+
// Method options missing or malformed (blank hash/url, wrong types).
|
|
21
|
+
constexpr const char* kInvalidOptions = "INVALID_OPTIONS";
|
|
22
|
+
// Native download failed (network error, bad HTTP status, truncated body).
|
|
23
|
+
constexpr const char* kDownloadFailed = "DOWNLOAD_FAILED";
|
|
24
|
+
// Unzip or hdiff patch application failed.
|
|
25
|
+
constexpr const char* kPatchFailed = "PATCH_FAILED";
|
|
26
|
+
// Local file or state persistence operation failed.
|
|
27
|
+
constexpr const char* kFileOperationFailed = "FILE_OPERATION_FAILED";
|
|
28
|
+
// switchVersion / setNeedUpdate state transition failed.
|
|
29
|
+
constexpr const char* kSwitchVersionFailed = "SWITCH_VERSION_FAILED";
|
|
30
|
+
// markSuccess state transition failed.
|
|
31
|
+
constexpr const char* kMarkSuccessFailed = "MARK_SUCCESS_FAILED";
|
|
32
|
+
// reloadUpdate / restartApp failed.
|
|
33
|
+
constexpr const char* kRestartFailed = "RESTART_FAILED";
|
|
34
|
+
// Stored or provided hash info is not a valid JSON object.
|
|
35
|
+
constexpr const char* kInvalidHashInfo = "INVALID_HASH_INFO";
|
|
36
|
+
// The method is not supported on this platform (e.g. downloadAndInstallApk
|
|
37
|
+
// outside Android).
|
|
38
|
+
constexpr const char* kUnsupportedPlatform = "UNSUPPORTED_PLATFORM";
|
|
39
|
+
|
|
40
|
+
} // namespace error_codes
|
|
41
|
+
} // namespace pushy
|
|
42
|
+
|
|
43
|
+
#endif // PUSHY_PATCH_CORE_ERROR_CODES_H_
|
|
@@ -468,13 +468,22 @@ export class UpdateContext {
|
|
|
468
468
|
public getBundleUrl() {
|
|
469
469
|
UpdateContext.isUsingBundleUrl = true;
|
|
470
470
|
this.trace('getBundleUrl:enter');
|
|
471
|
+
const stateBeforeLaunch = this.getStateSnapshot();
|
|
471
472
|
const launchState = NativePatchCore.runStateCore(
|
|
472
473
|
STATE_OP_RESOLVE_LAUNCH,
|
|
473
|
-
|
|
474
|
+
stateBeforeLaunch,
|
|
474
475
|
'',
|
|
475
476
|
UpdateContext.ignoreRollback,
|
|
476
477
|
true,
|
|
477
478
|
);
|
|
479
|
+
if (launchState.didRollback) {
|
|
480
|
+
// The crash-protection rollback: the new version never called
|
|
481
|
+
// markSuccess. Keep this visible in release logs.
|
|
482
|
+
console.error(
|
|
483
|
+
`Version ${stateBeforeLaunch.currentVersion} was not marked as successful,` +
|
|
484
|
+
` rolled back to ${launchState.currentVersion}`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
478
487
|
if (launchState.didRollback || launchState.consumedFirstTime) {
|
|
479
488
|
this.persistState(launchState, {
|
|
480
489
|
markFirstLoadMarker: launchState.consumedFirstTime,
|
|
@@ -490,7 +499,12 @@ export class UpdateContext {
|
|
|
490
499
|
);
|
|
491
500
|
|
|
492
501
|
let version = launchState.loadVersion || '';
|
|
493
|
-
|
|
502
|
+
// Guard the rollback chain against cycles: a corrupted state returning an
|
|
503
|
+
// already-visited version would otherwise spin this loop forever during
|
|
504
|
+
// startup (Android has the same guard).
|
|
505
|
+
const visitedVersions = new Set<string>();
|
|
506
|
+
while (version && !visitedVersions.has(version)) {
|
|
507
|
+
visitedVersions.add(version);
|
|
494
508
|
const bundleFile = this.getBundlePath(version);
|
|
495
509
|
try {
|
|
496
510
|
if (!fileIo.accessSync(bundleFile)) {
|
|
@@ -514,7 +528,11 @@ export class UpdateContext {
|
|
|
514
528
|
}
|
|
515
529
|
|
|
516
530
|
private rollBack(): string {
|
|
531
|
+
const stateBefore = this.getStateSnapshot();
|
|
517
532
|
const nextState = this.runStateOperation(STATE_OP_ROLLBACK);
|
|
533
|
+
console.error(
|
|
534
|
+
`Rolling back version ${stateBefore.currentVersion} to ${nextState.currentVersion}`,
|
|
535
|
+
);
|
|
518
536
|
return nextState.currentVersion || '';
|
|
519
537
|
}
|
|
520
538
|
|
package/harmony/pushy.har
CHANGED
|
Binary file
|
package/ios/RCTPushy/RCTPushy.mm
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#import "RCTPushyDownloader.h"
|
|
3
3
|
#import "ZipArchive.h"
|
|
4
4
|
#include "../../cpp/patch_core/archive_patch_core.h"
|
|
5
|
+
#include "../../cpp/patch_core/error_codes.h"
|
|
5
6
|
#include "../../cpp/patch_core/patch_core.h"
|
|
6
7
|
#include "../../cpp/patch_core/state_core.h"
|
|
7
8
|
|
|
@@ -37,9 +38,15 @@ static NSString * const BUNDLE_FILE_NAME = @"index.bundlejs";
|
|
|
37
38
|
static NSString * const SOURCE_PATCH_NAME = @"__diff.json";
|
|
38
39
|
static NSString * const BUNDLE_PATCH_NAME = @"index.bundlejs.patch";
|
|
39
40
|
|
|
40
|
-
// error def
|
|
41
|
+
// error def — messages are human-readable; the stable cross-platform codes
|
|
42
|
+
// live in cpp/patch_core/error_codes.h and travel in PushyErrorCodeKey.
|
|
41
43
|
static NSString * const ERROR_OPTIONS = @"options error";
|
|
42
44
|
static NSString * const ERROR_FILE_OPERATION = @"file operation error";
|
|
45
|
+
static NSString * const PushyErrorCodeKey = @"PushyErrorCode";
|
|
46
|
+
|
|
47
|
+
static NSString *PushyCode(const char *code) {
|
|
48
|
+
return [NSString stringWithUTF8String:code];
|
|
49
|
+
}
|
|
43
50
|
|
|
44
51
|
// event def
|
|
45
52
|
static NSString * const EVENT_PROGRESS_DOWNLOAD = @"RCTPushyDownloadProgress";
|
|
@@ -80,7 +87,10 @@ static std::string PushyToStdString(NSString *value) {
|
|
|
80
87
|
static NSError *PushyNSErrorFromStatus(const pushy::patch::Status &status) {
|
|
81
88
|
return [NSError errorWithDomain:PushyErrorDomain
|
|
82
89
|
code:-1
|
|
83
|
-
userInfo:@{
|
|
90
|
+
userInfo:@{
|
|
91
|
+
NSLocalizedDescriptionKey: [NSString stringWithUTF8String:status.message.c_str()],
|
|
92
|
+
PushyErrorCodeKey: PushyCode(pushy::error_codes::kPatchFailed),
|
|
93
|
+
}];
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
static NSUserDefaults *PushyDefaults(void) {
|
|
@@ -118,21 +128,24 @@ static BOOL PushyStringIsBlank(NSString *value) {
|
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
static void PushyRejectError(RCTPromiseRejectBlock reject, NSError *error) {
|
|
121
|
-
|
|
131
|
+
// Prefer the stable cross-platform code (error_codes.h); fall back to the
|
|
132
|
+
// numeric NSError code for system errors that were not classified.
|
|
133
|
+
NSString *code = error.userInfo[PushyErrorCodeKey];
|
|
134
|
+
if (code == nil) {
|
|
135
|
+
code = [NSString stringWithFormat:@"%ld", (long)error.code];
|
|
136
|
+
}
|
|
137
|
+
reject(code, error.localizedDescription, error);
|
|
122
138
|
}
|
|
123
139
|
|
|
124
|
-
static NSError *
|
|
140
|
+
static NSError *PushyErrorWithCode(const char *code, NSString *message) {
|
|
125
141
|
return [NSError errorWithDomain:PushyErrorDomain
|
|
126
142
|
code:-1
|
|
127
143
|
userInfo:@{
|
|
128
144
|
NSLocalizedDescriptionKey: message ?: @"unknown error",
|
|
145
|
+
PushyErrorCodeKey: PushyCode(code),
|
|
129
146
|
}];
|
|
130
147
|
}
|
|
131
148
|
|
|
132
|
-
static void PushyRejectMessage(RCTPromiseRejectBlock reject, NSString *message) {
|
|
133
|
-
PushyRejectError(reject, PushyErrorWithMessage(message));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
149
|
static pushy::patch::PatchManifest PushyPatchManifestFromJson(NSDictionary *json) {
|
|
137
150
|
pushy::patch::PatchManifest manifest;
|
|
138
151
|
|
|
@@ -257,6 +270,7 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
257
270
|
}
|
|
258
271
|
|
|
259
272
|
if (!state.current_version.empty()) {
|
|
273
|
+
std::string const versionBeforeLaunch = state.current_version;
|
|
260
274
|
pushy::state::LaunchDecision decision = pushy::state::ResolveLaunchState(
|
|
261
275
|
state,
|
|
262
276
|
ignoreRollback.load(),
|
|
@@ -264,6 +278,13 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
264
278
|
);
|
|
265
279
|
state = decision.state;
|
|
266
280
|
|
|
281
|
+
if (decision.did_rollback) {
|
|
282
|
+
// The crash-protection rollback: the new version never called
|
|
283
|
+
// markSuccess. Keep this visible in release logs.
|
|
284
|
+
RCTLogWarn(@"RCTPushy -- version %@ was not marked as successful, rolled back to %@",
|
|
285
|
+
PushyFromStdString(versionBeforeLaunch),
|
|
286
|
+
PushyFromStdString(state.current_version));
|
|
287
|
+
}
|
|
267
288
|
if (decision.did_rollback || decision.consumed_first_time) {
|
|
268
289
|
PushyApplyStateToDefaults(defaults, state);
|
|
269
290
|
}
|
|
@@ -275,13 +296,18 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
275
296
|
|
|
276
297
|
NSString *loadVersion = PushyFromStdString(decision.load_version);
|
|
277
298
|
NSString *downloadDir = [RCTPushy downloadDir];
|
|
278
|
-
|
|
299
|
+
// Guard the rollback chain against cycles: a corrupted state
|
|
300
|
+
// returning an already-visited version would otherwise spin this
|
|
301
|
+
// loop forever during startup (Android has the same guard).
|
|
302
|
+
NSMutableSet<NSString *> *visitedVersions = [NSMutableSet set];
|
|
303
|
+
while (loadVersion.length && ![visitedVersions containsObject:loadVersion]) {
|
|
304
|
+
[visitedVersions addObject:loadVersion];
|
|
279
305
|
NSString *bundlePath = [[downloadDir stringByAppendingPathComponent:loadVersion] stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
|
280
306
|
if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:NULL]) {
|
|
281
307
|
resolvedURL = [NSURL fileURLWithPath:bundlePath];
|
|
282
308
|
return;
|
|
283
309
|
} else {
|
|
284
|
-
RCTLogError(@"RCTPushy -- bundle version %@ not found", loadVersion);
|
|
310
|
+
RCTLogError(@"RCTPushy -- bundle version %@ not found, rolling back", loadVersion);
|
|
285
311
|
state = pushy::state::Rollback(state);
|
|
286
312
|
PushyApplyStateToDefaults(defaults, state);
|
|
287
313
|
loadVersion = PushyFromStdString(state.current_version);
|
|
@@ -359,7 +385,7 @@ RCT_EXPORT_METHOD(setUuid:(NSString *)uuid resolver:(RCTPromiseResolveBlock)res
|
|
|
359
385
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
360
386
|
{
|
|
361
387
|
if (PushyStringIsBlank(uuid)) {
|
|
362
|
-
PushyRejectError(reject,
|
|
388
|
+
PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
|
|
363
389
|
return;
|
|
364
390
|
}
|
|
365
391
|
|
|
@@ -373,7 +399,7 @@ RCT_EXPORT_METHOD(setLocalHashInfo:(NSString *)hash
|
|
|
373
399
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
374
400
|
{
|
|
375
401
|
if (PushyStringIsBlank(hash) || PushyStringIsBlank(value)) {
|
|
376
|
-
|
|
402
|
+
PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
|
|
377
403
|
return;
|
|
378
404
|
}
|
|
379
405
|
|
|
@@ -386,7 +412,9 @@ RCT_EXPORT_METHOD(setLocalHashInfo:(NSString *)hash
|
|
|
386
412
|
|
|
387
413
|
resolve(@true);
|
|
388
414
|
} else {
|
|
389
|
-
PushyRejectError(reject,
|
|
415
|
+
PushyRejectError(reject, PushyErrorWithCode(
|
|
416
|
+
pushy::error_codes::kInvalidHashInfo,
|
|
417
|
+
error != nil ? error.localizedDescription : @"invalid json string"));
|
|
390
418
|
}
|
|
391
419
|
}
|
|
392
420
|
|
|
@@ -421,6 +449,15 @@ RCT_EXPORT_METHOD(downloadPatchFromPpk:(NSDictionary *)options
|
|
|
421
449
|
[self downloadUpdate:PushyTypePatchFromPpk options:options resolver:resolve rejecter:reject];
|
|
422
450
|
}
|
|
423
451
|
|
|
452
|
+
RCT_EXPORT_METHOD(downloadAndInstallApk:(NSDictionary *)options
|
|
453
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
454
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
455
|
+
{
|
|
456
|
+
PushyRejectError(reject, PushyErrorWithCode(
|
|
457
|
+
pushy::error_codes::kUnsupportedPlatform,
|
|
458
|
+
@"downloadAndInstallApk is only supported on Android"));
|
|
459
|
+
}
|
|
460
|
+
|
|
424
461
|
RCT_EXPORT_METHOD(setNeedUpdate:(NSDictionary *)options
|
|
425
462
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
426
463
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
@@ -502,6 +539,12 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
502
539
|
{
|
|
503
540
|
[self performUpdate:type options:options callback:^(NSError *error) {
|
|
504
541
|
if (error != nil) {
|
|
542
|
+
if (error.userInfo[PushyErrorCodeKey] == nil) {
|
|
543
|
+
// Unclassified (system/network) errors from the download
|
|
544
|
+
// pipeline; keep the original message.
|
|
545
|
+
error = PushyErrorWithCode(pushy::error_codes::kDownloadFailed,
|
|
546
|
+
error.localizedDescription);
|
|
547
|
+
}
|
|
505
548
|
PushyRejectError(reject, error);
|
|
506
549
|
return;
|
|
507
550
|
}
|
|
@@ -527,24 +570,38 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
527
570
|
NSString *hash = PushyOptionString(options, @"hash");
|
|
528
571
|
|
|
529
572
|
if (PushyStringIsBlank(updateUrl) || PushyStringIsBlank(hash)) {
|
|
530
|
-
callback(
|
|
573
|
+
callback(PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
|
|
531
574
|
return;
|
|
532
575
|
}
|
|
533
576
|
NSString *originHash = PushyOptionString(options, @"originHash");
|
|
534
577
|
if (type == PushyTypePatchFromPpk && PushyStringIsBlank(originHash)) {
|
|
535
|
-
callback(
|
|
578
|
+
callback(PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
|
|
536
579
|
return;
|
|
537
580
|
}
|
|
538
581
|
|
|
539
582
|
NSString *dir = [RCTPushy downloadDir];
|
|
540
583
|
BOOL success = [self ensureDirectoryExistsAtPath:dir];
|
|
541
584
|
if (!success) {
|
|
542
|
-
callback(
|
|
585
|
+
callback(PushyErrorWithCode(pushy::error_codes::kFileOperationFailed, ERROR_FILE_OPERATION));
|
|
543
586
|
return;
|
|
544
587
|
}
|
|
545
588
|
|
|
546
589
|
NSString *zipFilePath = [dir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@%@",hash, [self zipExtension:type]]];
|
|
547
590
|
|
|
591
|
+
// On failure, remove the partial version directory like Android/Harmony
|
|
592
|
+
// do: a half-unzipped/half-patched dir leaks disk and could later be
|
|
593
|
+
// mistaken for a complete version. hash is validated non-blank above, so
|
|
594
|
+
// this can never resolve to the download root itself.
|
|
595
|
+
NSString *unzipDir = [dir stringByAppendingPathComponent:hash];
|
|
596
|
+
void (^completion)(NSError *) = ^(NSError *error) {
|
|
597
|
+
if (error != nil) {
|
|
598
|
+
dispatch_async(self->_fileQueue, ^{
|
|
599
|
+
[[NSFileManager defaultManager] removeItemAtPath:unzipDir error:nil];
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
callback(error);
|
|
603
|
+
};
|
|
604
|
+
|
|
548
605
|
RCTLogInfo(@"RCTPushy -- download file %@", updateUrl);
|
|
549
606
|
[RCTPushyDownloader download:updateUrl savePath:zipFilePath progressHandler:^(long long receivedBytes, long long totalBytes) {
|
|
550
607
|
if (self->hasListeners) {
|
|
@@ -556,14 +613,14 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
556
613
|
}
|
|
557
614
|
} completionHandler:^(NSString *path, NSError *error) {
|
|
558
615
|
if (error != nil) {
|
|
559
|
-
|
|
616
|
+
completion(error);
|
|
560
617
|
return;
|
|
561
618
|
}
|
|
562
619
|
[self unzipDownloadedPackage:zipFilePath
|
|
563
620
|
hash:hash
|
|
564
621
|
type:type
|
|
565
622
|
originHash:originHash
|
|
566
|
-
callback:
|
|
623
|
+
callback:completion];
|
|
567
624
|
}];
|
|
568
625
|
}
|
|
569
626
|
|
|
@@ -629,18 +686,21 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
629
686
|
NSString *destination = [unzipDir stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
|
630
687
|
NSData *data = [NSData dataWithContentsOfFile:sourcePatch];
|
|
631
688
|
if (data == nil) {
|
|
632
|
-
callback(
|
|
689
|
+
callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed, @"missing patch manifest"));
|
|
633
690
|
return;
|
|
634
691
|
}
|
|
635
692
|
|
|
636
693
|
NSError *error = nil;
|
|
637
694
|
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
|
|
638
695
|
if (error != nil) {
|
|
639
|
-
|
|
696
|
+
// Classify as a patch failure like the sibling manifest branches;
|
|
697
|
+
// unclassified errors would otherwise be tagged DOWNLOAD_FAILED by the
|
|
698
|
+
// downloadUpdate fallback even though the download itself succeeded.
|
|
699
|
+
callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed, error.localizedDescription));
|
|
640
700
|
return;
|
|
641
701
|
}
|
|
642
702
|
if (![jsonObject isKindOfClass:[NSDictionary class]]) {
|
|
643
|
-
callback(
|
|
703
|
+
callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed, @"invalid patch manifest"));
|
|
644
704
|
return;
|
|
645
705
|
}
|
|
646
706
|
NSDictionary *json = (NSDictionary *)jsonObject;
|
|
@@ -695,7 +755,7 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
695
755
|
{
|
|
696
756
|
if (PushyStringIsBlank(hash)) {
|
|
697
757
|
if (error != NULL) {
|
|
698
|
-
*error =
|
|
758
|
+
*error = PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS);
|
|
699
759
|
}
|
|
700
760
|
return NO;
|
|
701
761
|
}
|
|
@@ -758,7 +818,7 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
758
818
|
|
|
759
819
|
NSError *unzipError = error;
|
|
760
820
|
if (!succeeded && unzipError == nil) {
|
|
761
|
-
unzipError =
|
|
821
|
+
unzipError = PushyErrorWithCode(pushy::error_codes::kPatchFailed, @"unzip failed");
|
|
762
822
|
}
|
|
763
823
|
completionHandler(unzipError);
|
|
764
824
|
}];
|
|
@@ -113,7 +113,8 @@ didFinishDownloadingToURL:(NSURL *)location
|
|
|
113
113
|
// before touching savePath so an existing valid package is not destroyed.
|
|
114
114
|
NSURLResponse *response = downloadTask.response;
|
|
115
115
|
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
|
116
|
-
|
|
116
|
+
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
|
117
|
+
NSInteger statusCode = httpResponse.statusCode;
|
|
117
118
|
if (statusCode < 200 || statusCode >= 300) {
|
|
118
119
|
self.fileError = [NSError errorWithDomain:RCTPushyDownloaderErrorDomain
|
|
119
120
|
code:statusCode
|
|
@@ -122,6 +123,26 @@ didFinishDownloadingToURL:(NSURL *)location
|
|
|
122
123
|
}];
|
|
123
124
|
return;
|
|
124
125
|
}
|
|
126
|
+
|
|
127
|
+
// Reject truncated transfers like Android/Harmony do. Skip the check
|
|
128
|
+
// for encoded responses (e.g. gzip): NSURLSession decompresses them
|
|
129
|
+
// transparently, so the on-disk size legitimately differs from the
|
|
130
|
+
// Content-Length of the encoded body.
|
|
131
|
+
NSString *contentEncoding = [httpResponse.allHeaderFields[@"Content-Encoding"] lowercaseString];
|
|
132
|
+
BOOL isEncodedBody = contentEncoding.length > 0 && ![contentEncoding isEqualToString:@"identity"];
|
|
133
|
+
long long expectedLength = httpResponse.expectedContentLength;
|
|
134
|
+
if (!isEncodedBody && expectedLength > 0) {
|
|
135
|
+
NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:location.path
|
|
136
|
+
error:nil][NSFileSize];
|
|
137
|
+
if (fileSize != nil && fileSize.longLongValue != expectedLength) {
|
|
138
|
+
self.fileError = [NSError errorWithDomain:RCTPushyDownloaderErrorDomain
|
|
139
|
+
code:-1
|
|
140
|
+
userInfo:@{
|
|
141
|
+
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"download incomplete: expected %lld bytes, got %lld", expectedLength, fileSize.longLongValue],
|
|
142
|
+
}];
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
125
146
|
}
|
|
126
147
|
|
|
127
148
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-update",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.47.0",
|
|
4
4
|
"description": "react-native hot update",
|
|
5
5
|
"main": "src/index",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"test": "bun test src/__tests__",
|
|
16
16
|
"test:patch-core": "./scripts/test-patch-core.sh",
|
|
17
17
|
"build:harmony-har": "node scripts/build-harmony-har.js",
|
|
18
|
+
"build:harmony-e2e": "bash Example/e2etest/scripts/build-harmony-e2e.sh",
|
|
18
19
|
"build:so": "bun submodule && bash scripts/build-android-so.sh",
|
|
19
20
|
"build:ios-debug": "cd Example/e2etest && bun && detox build --configuration ios.sim.debug",
|
|
20
21
|
"build:ios-release": "cd Example/e2etest && bun && detox build --configuration ios.sim.release",
|