Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cn.reactnative.modules.update;

/**
* Stable, machine-readable error codes used as the promise rejection code so
* the JS layer and user loggers can aggregate errors across platforms.
*
* MUST stay in sync with cpp/patch_core/error_codes.h (the single source of
* truth) and src/error.ts (UpdateErrorCode). Messages are free-form; only the
* codes are part of the contract.
*/
final class ErrorCodes {
static final String INVALID_OPTIONS = "INVALID_OPTIONS";
static final String DOWNLOAD_FAILED = "DOWNLOAD_FAILED";
static final String PATCH_FAILED = "PATCH_FAILED";
static final String FILE_OPERATION_FAILED = "FILE_OPERATION_FAILED";
static final String SWITCH_VERSION_FAILED = "SWITCH_VERSION_FAILED";
static final String MARK_SUCCESS_FAILED = "MARK_SUCCESS_FAILED";
static final String RESTART_FAILED = "RESTART_FAILED";
static final String INVALID_HASH_INFO = "INVALID_HASH_INFO";
static final String UNSUPPORTED_PLATFORM = "UNSUPPORTED_PLATFORM";

private ErrorCodes() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ private StateSerialRunner() {

static void run(
@Nullable final Promise promise,
final String errorCode,
final String operationName,
final Operation operation
) {
Expand All @@ -53,7 +54,7 @@ public void run() {
operation.run();
} catch (Throwable error) {
if (promise != null) {
promise.reject(operationName + " failed", error);
promise.reject(errorCode, operationName + " failed", error);
} else {
Log.e(UpdateContext.TAG, operationName + " failed", error);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ private UiThreadRunner() {

static void run(
@Nullable final Promise promise,
final String errorCode,
final String operationName,
final Operation operation
) {
Expand All @@ -25,7 +26,7 @@ public void run() {
operation.run();
} catch (Throwable error) {
if (promise != null) {
promise.reject(operationName + " failed", error);
promise.reject(errorCode, operationName + " failed", error);
} else {
Log.e(UpdateContext.TAG, operationName + " failed", error);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,10 @@ private void applyState(SharedPreferences.Editor editor, StateCoreResult state)
}

private void persistEditor(SharedPreferences.Editor editor, String reason) {
if (!editor.commit() && DEBUG) {
Log.w(TAG, "Failed to persist update state for " + reason);
// A lost state write can mean a missed rollback or a version switch
// that silently never happens, so this must be visible in release too.
if (!editor.commit()) {
Log.e(TAG, "Failed to persist update state for " + reason);
}
}

Expand Down Expand Up @@ -367,6 +369,13 @@ public String getBundleUrl(String defaultAssetsUrl) {
ignoreRollback,
true
);
if (launchState.didRollback) {
// The crash-protection rollback: the new version never called
// markSuccess. Keep this visible in release logs.
Log.e(TAG, "Version " + currentState.currentVersion
+ " was not marked as successful, rolling back to "
+ launchState.currentVersion);
}
if (launchState.didRollback || launchState.consumedFirstTime) {
SharedPreferences.Editor editor = sp.edit();
applyState(editor, launchState);
Expand Down Expand Up @@ -411,6 +420,8 @@ private String rollBack() {
false,
false
);
Log.e(TAG, "Rolling back version " + currentState.currentVersion
+ " to " + nextState.currentVersion);
SharedPreferences.Editor editor = sp.edit();
applyState(editor, nextState);
persistEditor(editor, "rollback");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void onDownloadCompleted(DownloadTaskParams params) {

@Override
public void onDownloadFailed(Throwable error) {
promise.reject(error);
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
}
});
}
Expand All @@ -64,7 +64,7 @@ public void onDownloadCompleted(DownloadTaskParams params) {

@Override
public void onDownloadFailed(Throwable error) {
promise.reject(error);
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
}
});
}
Expand All @@ -84,7 +84,7 @@ public void onDownloadCompleted(DownloadTaskParams params) {

@Override
public void onDownloadFailed(Throwable error) {
promise.reject(error);
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
}
});
}
Expand All @@ -107,11 +107,11 @@ public void onDownloadCompleted(DownloadTaskParams params) {

@Override
public void onDownloadFailed(Throwable error) {
promise.reject(error);
promise.reject(ErrorCodes.DOWNLOAD_FAILED, error);
}
});
} catch (Exception e) {
promise.reject("downloadPatchFromPpk failed: " + e.getMessage());
promise.reject(ErrorCodes.INVALID_OPTIONS, "downloadPatchFromPpk failed: " + e.getMessage(), e);
}
}

Expand All @@ -130,7 +130,7 @@ public static void restartApp(
@Nullable final String hash,
final Promise promise
) {
UiThreadRunner.run(promise, "restartApp", new UiThreadRunner.Operation() {
UiThreadRunner.run(promise, ErrorCodes.RESTART_FAILED, "restartApp", new UiThreadRunner.Operation() {
@Override
public void run() throws Throwable {
ReactReloadManager.restartApp(updateContext, reactContext, hash);
Expand All @@ -149,7 +149,7 @@ public static void setNeedUpdate(
final Promise promise
) {
final String hash = options.getString("hash");
StateSerialRunner.run(promise, "switchVersionLater", new StateSerialRunner.Operation() {
StateSerialRunner.run(promise, ErrorCodes.SWITCH_VERSION_FAILED, "switchVersionLater", new StateSerialRunner.Operation() {
@Override
public void run() {
setNeedUpdateInternal(updateContext, hash);
Expand All @@ -160,7 +160,7 @@ public void run() {

public static void setNeedUpdate(final UpdateContext updateContext, final ReadableMap options) {
final String hash = options.getString("hash");
StateSerialRunner.run(null, "switchVersionLater", new StateSerialRunner.Operation() {
StateSerialRunner.run(null, ErrorCodes.SWITCH_VERSION_FAILED, "switchVersionLater", new StateSerialRunner.Operation() {
@Override
public void run() {
setNeedUpdateInternal(updateContext, hash);
Expand All @@ -173,7 +173,7 @@ private static void markSuccessInternal(UpdateContext updateContext) {
}

public static void markSuccess(final UpdateContext updateContext, final Promise promise) {
StateSerialRunner.run(promise, "markSuccess", new StateSerialRunner.Operation() {
StateSerialRunner.run(promise, ErrorCodes.MARK_SUCCESS_FAILED, "markSuccess", new StateSerialRunner.Operation() {
@Override
public void run() {
markSuccessInternal(updateContext);
Expand All @@ -183,7 +183,7 @@ public void run() {
}

public static void markSuccess(final UpdateContext updateContext) {
StateSerialRunner.run(null, "markSuccess", new StateSerialRunner.Operation() {
StateSerialRunner.run(null, ErrorCodes.MARK_SUCCESS_FAILED, "markSuccess", new StateSerialRunner.Operation() {
@Override
public void run() {
markSuccessInternal(updateContext);
Expand All @@ -200,7 +200,7 @@ public static void setUuid(
final String uuid,
final Promise promise
) {
StateSerialRunner.run(promise, "setUuid", new StateSerialRunner.Operation() {
StateSerialRunner.run(promise, ErrorCodes.FILE_OPERATION_FAILED, "setUuid", new StateSerialRunner.Operation() {
@Override
public void run() {
setUuidInternal(updateContext, uuid);
Expand All @@ -210,7 +210,7 @@ public void run() {
}

public static void setUuid(final UpdateContext updateContext, final String uuid) {
StateSerialRunner.run(null, "setUuid", new StateSerialRunner.Operation() {
StateSerialRunner.run(null, ErrorCodes.FILE_OPERATION_FAILED, "setUuid", new StateSerialRunner.Operation() {
@Override
public void run() {
setUuidInternal(updateContext, uuid);
Expand All @@ -235,7 +235,7 @@ public static void setLocalHashInfo(
final String info,
final Promise promise
) {
StateSerialRunner.run(promise, "setLocalHashInfo", new StateSerialRunner.Operation() {
StateSerialRunner.run(promise, ErrorCodes.INVALID_HASH_INFO, "setLocalHashInfo", new StateSerialRunner.Operation() {
@Override
public void run() {
setLocalHashInfoInternal(updateContext, hash, info);
Expand All @@ -249,7 +249,7 @@ public static void setLocalHashInfo(
final String hash,
final String info
) {
StateSerialRunner.run(null, "setLocalHashInfo", new StateSerialRunner.Operation() {
StateSerialRunner.run(null, ErrorCodes.INVALID_HASH_INFO, "setLocalHashInfo", new StateSerialRunner.Operation() {
@Override
public void run() {
setLocalHashInfoInternal(updateContext, hash, info);
Expand All @@ -264,7 +264,7 @@ public static void getLocalHashInfo(
) {
String value = updateContext.getKv("hash_" + hash);
if (!isValidHashInfo(value)) {
promise.reject("getLocalHashInfo failed: invalid json string");
promise.reject(ErrorCodes.INVALID_HASH_INFO, "getLocalHashInfo failed: invalid json string");
return;
}

Expand Down
43 changes: 43 additions & 0 deletions cpp/patch_core/error_codes.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#ifndef PUSHY_PATCH_CORE_ERROR_CODES_H_
#define PUSHY_PATCH_CORE_ERROR_CODES_H_

// Single source of truth for the stable, machine-readable error codes shared
// by every platform. Native modules reject promises with one of these codes
// so the JS layer (src/error.ts UpdateErrorCode) and user loggers can
// aggregate errors across platforms and locales.
//
// Mirrors that cannot include this header MUST stay in sync by hand:
// - src/error.ts (UpdateErrorCode union, JS layer)
// - android/.../ErrorCodes.java (Java constants)
// iOS (RCTPushy.mm) includes this header directly.
//
// Human-readable messages are NOT part of this contract: they may differ per
// platform and locale. Only the codes are stable.

namespace pushy {
namespace error_codes {

// Method options missing or malformed (blank hash/url, wrong types).
constexpr const char* kInvalidOptions = "INVALID_OPTIONS";
// Native download failed (network error, bad HTTP status, truncated body).
constexpr const char* kDownloadFailed = "DOWNLOAD_FAILED";
// Unzip or hdiff patch application failed.
constexpr const char* kPatchFailed = "PATCH_FAILED";
// Local file or state persistence operation failed.
constexpr const char* kFileOperationFailed = "FILE_OPERATION_FAILED";
// switchVersion / setNeedUpdate state transition failed.
constexpr const char* kSwitchVersionFailed = "SWITCH_VERSION_FAILED";
// markSuccess state transition failed.
constexpr const char* kMarkSuccessFailed = "MARK_SUCCESS_FAILED";
// reloadUpdate / restartApp failed.
constexpr const char* kRestartFailed = "RESTART_FAILED";
// Stored or provided hash info is not a valid JSON object.
constexpr const char* kInvalidHashInfo = "INVALID_HASH_INFO";
// The method is not supported on this platform (e.g. downloadAndInstallApk
// outside Android).
constexpr const char* kUnsupportedPlatform = "UNSUPPORTED_PLATFORM";

} // namespace error_codes
} // namespace pushy

#endif // PUSHY_PATCH_CORE_ERROR_CODES_H_
22 changes: 20 additions & 2 deletions harmony/pushy/src/main/ets/UpdateContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,13 +468,22 @@ export class UpdateContext {
public getBundleUrl() {
UpdateContext.isUsingBundleUrl = true;
this.trace('getBundleUrl:enter');
const stateBeforeLaunch = this.getStateSnapshot();
const launchState = NativePatchCore.runStateCore(
STATE_OP_RESOLVE_LAUNCH,
this.getStateSnapshot(),
stateBeforeLaunch,
'',
UpdateContext.ignoreRollback,
true,
);
if (launchState.didRollback) {
// The crash-protection rollback: the new version never called
// markSuccess. Keep this visible in release logs.
console.error(
`Version ${stateBeforeLaunch.currentVersion} was not marked as successful,` +
` rolled back to ${launchState.currentVersion}`,
);
}
if (launchState.didRollback || launchState.consumedFirstTime) {
this.persistState(launchState, {
markFirstLoadMarker: launchState.consumedFirstTime,
Expand All @@ -490,7 +499,12 @@ export class UpdateContext {
);

let version = launchState.loadVersion || '';
while (version) {
// Guard the rollback chain against cycles: a corrupted state returning an
// already-visited version would otherwise spin this loop forever during
// startup (Android has the same guard).
const visitedVersions = new Set<string>();
while (version && !visitedVersions.has(version)) {
visitedVersions.add(version);
const bundleFile = this.getBundlePath(version);
try {
if (!fileIo.accessSync(bundleFile)) {
Expand All @@ -514,7 +528,11 @@ export class UpdateContext {
}

private rollBack(): string {
const stateBefore = this.getStateSnapshot();
const nextState = this.runStateOperation(STATE_OP_ROLLBACK);
console.error(
`Rolling back version ${stateBefore.currentVersion} to ${nextState.currentVersion}`,
);
return nextState.currentVersion || '';
}

Expand Down
Loading
Loading