@bravemobile/react-native-code-push 13.1.0-beta.4 → 13.1.0-beta.5
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/CodePush.podspec +5 -0
- package/README.md +15 -145
- package/android/app/src/main/java/com/microsoft/codepush/react/ArchiveAttemptLog.java +181 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/{BinaryPatchResult.java → ArchiveRestoreResult.java} +17 -7
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +20 -20
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +20 -1
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushErrorCode.java +86 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushHttpException.java +29 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushIncompleteDownloadException.java +17 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +12 -7
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +143 -43
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +31 -31
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushErrorCodeTest.java +90 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +159 -19
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +304 -15
- package/cli/commands/createHistoryCommand/createReleaseHistory.test.ts +133 -0
- package/cli/commands/createHistoryCommand/createReleaseHistory.ts +3 -11
- package/cli/commands/releaseCommand/addToReleaseHistory.ts +3 -11
- package/cli/commands/releaseCommand/release.test.ts +3 -3
- package/cli/commands/updateHistoryCommand/updateReleaseHistory.ts +3 -11
- package/cli/dist/commands/createHistoryCommand/createReleaseHistory.js +2 -8
- package/cli/dist/commands/createHistoryCommand/createReleaseHistory.test.js +95 -0
- package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +2 -8
- package/cli/dist/commands/releaseCommand/release.test.js +3 -3
- package/cli/dist/commands/updateHistoryCommand/updateReleaseHistory.js +2 -8
- package/cli/dist/functions/stageReleaseHistoryFile.js +28 -0
- package/cli/functions/stageReleaseHistoryFile.ts +38 -0
- package/ios/CodePush/CodePush.h +6 -3
- package/ios/CodePush/CodePush.mm +10 -10
- package/ios/CodePush/CodePushBinaryPatch.h +18 -8
- package/ios/CodePush/CodePushBinaryPatch.m +23 -22
- package/ios/CodePush/CodePushDownloadHandler.m +2 -1
- package/ios/CodePush/CodePushErrorUtils.m +55 -0
- package/ios/CodePush/CodePushPackage.m +242 -71
- package/package.json +2 -1
- package/src/CodePush.js +28 -18
- package/src/CodePush.test.js +110 -55
- package/src/package-mixins.js +19 -10
- package/typings/react-native-code-push.d.ts +110 -29
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +0 -92
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
1
|
import type { CliConfigInterface } from "../../../typings/react-native-code-push.d.ts";
|
|
2
|
+
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
4
3
|
|
|
5
4
|
export async function updateReleaseHistory(
|
|
6
5
|
appVersion: string,
|
|
@@ -23,15 +22,8 @@ export async function updateReleaseHistory(
|
|
|
23
22
|
if (typeof rollout === "number") updateInfo.rollout = rollout;
|
|
24
23
|
|
|
25
24
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(releaseHistory, null, 2));
|
|
30
|
-
fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(releaseHistory));
|
|
31
|
-
|
|
32
|
-
await setReleaseHistory(binaryVersion, JSON_FILE_PATH, releaseHistory, platform, identifier)
|
|
33
|
-
|
|
34
|
-
fs.unlinkSync(JSON_FILE_PATH);
|
|
25
|
+
await stageReleaseHistoryFile(binaryVersion, releaseHistory, platform, (jsonFilePath) =>
|
|
26
|
+
setReleaseHistory(binaryVersion, jsonFilePath, releaseHistory, platform, identifier));
|
|
35
27
|
} catch (error) {
|
|
36
28
|
console.error('Error occurred while updating history:', error);
|
|
37
29
|
process.exit(1)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from "path";
|
|
1
|
+
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
3
2
|
export async function createReleaseHistory(targetVersion, setReleaseHistory, platform, identifier) {
|
|
4
3
|
const BINARY_RELEASE = {
|
|
5
4
|
enabled: true,
|
|
@@ -11,12 +10,7 @@ export async function createReleaseHistory(targetVersion, setReleaseHistory, pla
|
|
|
11
10
|
[targetVersion]: BINARY_RELEASE
|
|
12
11
|
};
|
|
13
12
|
try {
|
|
14
|
-
|
|
15
|
-
const JSON_FILE_PATH = path.resolve(process.cwd(), JSON_FILE_NAME);
|
|
16
|
-
console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(INITIAL_HISTORY, null, 2));
|
|
17
|
-
fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(INITIAL_HISTORY));
|
|
18
|
-
await setReleaseHistory(targetVersion, JSON_FILE_PATH, INITIAL_HISTORY, platform, identifier);
|
|
19
|
-
fs.unlinkSync(JSON_FILE_PATH);
|
|
13
|
+
await stageReleaseHistoryFile(targetVersion, INITIAL_HISTORY, platform, (jsonFilePath) => setReleaseHistory(targetVersion, jsonFilePath, INITIAL_HISTORY, platform, identifier));
|
|
20
14
|
}
|
|
21
15
|
catch (error) {
|
|
22
16
|
console.error('Error occurred while creating new history:', error);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
5
|
+
import { createReleaseHistory } from "./createReleaseHistory.js";
|
|
6
|
+
import { updateReleaseHistory } from "../updateHistoryCommand/updateReleaseHistory.js";
|
|
7
|
+
/**
|
|
8
|
+
* Covers where the release history is staged before the config is handed it.
|
|
9
|
+
*
|
|
10
|
+
* The config receives a path rather than the history itself, so the command has to put the
|
|
11
|
+
* file somewhere first. Two releases of one app for different platforms are the same binary
|
|
12
|
+
* version, so a staging path derived from that version alone is the same path for both, and
|
|
13
|
+
* they overwrite each other's contents before either has read its own.
|
|
14
|
+
*/
|
|
15
|
+
const BINARY_VERSION = "1.0.0";
|
|
16
|
+
let workingDir;
|
|
17
|
+
let previousCwd;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
previousCwd = process.cwd();
|
|
20
|
+
workingDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-history-staging-"));
|
|
21
|
+
process.chdir(workingDir);
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
process.chdir(previousCwd);
|
|
25
|
+
fs.rmSync(workingDir, { recursive: true, force: true });
|
|
26
|
+
jest.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
function stagedPath(platform) {
|
|
29
|
+
return path.join(workingDir, "codepush-release-history", platform, `${BINARY_VERSION}.json`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Turns the command's own exit into a throw, so a staging failure fails the test instead
|
|
33
|
+
* of taking the worker down with it.
|
|
34
|
+
*/
|
|
35
|
+
function failOnExit() {
|
|
36
|
+
jest.spyOn(process, "exit").mockImplementation((code) => {
|
|
37
|
+
throw new Error(`the command exited with code ${String(code)}`);
|
|
38
|
+
});
|
|
39
|
+
jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
40
|
+
jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
41
|
+
}
|
|
42
|
+
describe("staging the release history a config is handed", () => {
|
|
43
|
+
it("gives each platform writing at the same time a file of its own", async () => {
|
|
44
|
+
failOnExit();
|
|
45
|
+
const staged = {};
|
|
46
|
+
const setReleaseHistory = async (_binaryVersion, jsonFilePath, _releaseInfo, platform) => {
|
|
47
|
+
// Yields once, which is all it takes for the other platform to reach its own
|
|
48
|
+
// write and its own clean-up while this one still holds only a path.
|
|
49
|
+
await Promise.resolve();
|
|
50
|
+
staged[platform] = { jsonFilePath, contents: fs.readFileSync(jsonFilePath, "utf8") };
|
|
51
|
+
};
|
|
52
|
+
await Promise.all([
|
|
53
|
+
createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840"),
|
|
54
|
+
createReleaseHistory(BINARY_VERSION, setReleaseHistory, "android", "RN0840"),
|
|
55
|
+
]);
|
|
56
|
+
expect(staged.ios.jsonFilePath).not.toBe(staged.android.jsonFilePath);
|
|
57
|
+
});
|
|
58
|
+
it("hands each platform the history that platform released", async () => {
|
|
59
|
+
failOnExit();
|
|
60
|
+
const released = {
|
|
61
|
+
ios: JSON.stringify({ [BINARY_VERSION]: { enabled: true, mandatory: false, downloadUrl: "", packageHash: "" }, "1.0.1": { enabled: true, mandatory: true, downloadUrl: "ios-url", packageHash: "ios-hash" } }),
|
|
62
|
+
android: JSON.stringify({ [BINARY_VERSION]: { enabled: true, mandatory: false, downloadUrl: "", packageHash: "" }, "1.0.1": { enabled: true, mandatory: true, downloadUrl: "android-url", packageHash: "android-hash" } }),
|
|
63
|
+
};
|
|
64
|
+
const staged = {};
|
|
65
|
+
const getReleaseHistory = async (_binaryVersion, platform) => JSON.parse(released[platform]);
|
|
66
|
+
const setReleaseHistory = async (_binaryVersion, jsonFilePath, _releaseInfo, platform) => {
|
|
67
|
+
await Promise.resolve();
|
|
68
|
+
staged[platform] = fs.readFileSync(jsonFilePath, "utf8");
|
|
69
|
+
};
|
|
70
|
+
await Promise.all([
|
|
71
|
+
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "ios", "RN0840", undefined, false, undefined),
|
|
72
|
+
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "android", "RN0840", undefined, false, undefined),
|
|
73
|
+
]);
|
|
74
|
+
expect(staged.ios).toContain("ios-url");
|
|
75
|
+
expect(staged.ios).not.toContain("android-url");
|
|
76
|
+
expect(staged.android).toContain("android-url");
|
|
77
|
+
expect(staged.android).not.toContain("ios-url");
|
|
78
|
+
});
|
|
79
|
+
it("takes the file away once the config has stored the history", async () => {
|
|
80
|
+
failOnExit();
|
|
81
|
+
const setReleaseHistory = async () => undefined;
|
|
82
|
+
await createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840");
|
|
83
|
+
expect(fs.existsSync(stagedPath("ios"))).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
it("leaves the file behind when the config could not store the history", async () => {
|
|
86
|
+
failOnExit();
|
|
87
|
+
const setReleaseHistory = async () => {
|
|
88
|
+
throw new Error("the storage backend rejected the history");
|
|
89
|
+
};
|
|
90
|
+
await expect(createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840")).rejects.toThrow("the command exited with code 1");
|
|
91
|
+
// What the release tried to store is worth reading back, so a failed release keeps
|
|
92
|
+
// its history at a path that is the same on every run.
|
|
93
|
+
expect(Object.keys(JSON.parse(fs.readFileSync(stagedPath("ios"), "utf8")))).toEqual([BINARY_VERSION]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import fs from "fs";
|
|
1
|
+
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
3
2
|
export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownloadUrl, binaryPatchDownloadUrl, packageHash, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout, diffPackages) {
|
|
4
3
|
const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
|
|
5
4
|
const updateInfo = releaseHistory[appVersion];
|
|
@@ -28,12 +27,7 @@ export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownl
|
|
|
28
27
|
newReleaseHistory[appVersion].rollout = rollout;
|
|
29
28
|
}
|
|
30
29
|
try {
|
|
31
|
-
|
|
32
|
-
const JSON_FILE_PATH = path.resolve(process.cwd(), JSON_FILE_NAME);
|
|
33
|
-
console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(newReleaseHistory, null, 2));
|
|
34
|
-
fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(newReleaseHistory));
|
|
35
|
-
await setReleaseHistory(binaryVersion, JSON_FILE_PATH, newReleaseHistory, platform, identifier);
|
|
36
|
-
fs.unlinkSync(JSON_FILE_PATH);
|
|
30
|
+
await stageReleaseHistoryFile(binaryVersion, newReleaseHistory, platform, (jsonFilePath) => setReleaseHistory(binaryVersion, jsonFilePath, newReleaseHistory, platform, identifier));
|
|
37
31
|
}
|
|
38
32
|
catch (error) {
|
|
39
33
|
console.error('Error occurred while updating history:', error);
|
|
@@ -119,9 +119,9 @@ beforeEach(() => {
|
|
|
119
119
|
});
|
|
120
120
|
afterEach(() => {
|
|
121
121
|
jest.restoreAllMocks();
|
|
122
|
-
// addToReleaseHistory writes its JSON
|
|
123
|
-
// when the history was stored successfully.
|
|
124
|
-
fs.rmSync(path.resolve(process.cwd(),
|
|
122
|
+
// addToReleaseHistory writes its JSON under the directory it was invoked in, and only
|
|
123
|
+
// removes it when the history was stored successfully.
|
|
124
|
+
fs.rmSync(path.resolve(process.cwd(), "codepush-release-history"), { recursive: true, force: true });
|
|
125
125
|
});
|
|
126
126
|
describe("release without --binary-bundle-path", () => {
|
|
127
127
|
it("keeps existing three-argument uploaders callable", async () => {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from "path";
|
|
1
|
+
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
3
2
|
export async function updateReleaseHistory(appVersion, binaryVersion, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout) {
|
|
4
3
|
const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
|
|
5
4
|
const updateInfo = releaseHistory[appVersion];
|
|
@@ -12,12 +11,7 @@ export async function updateReleaseHistory(appVersion, binaryVersion, getRelease
|
|
|
12
11
|
if (typeof rollout === "number")
|
|
13
12
|
updateInfo.rollout = rollout;
|
|
14
13
|
try {
|
|
15
|
-
|
|
16
|
-
const JSON_FILE_PATH = path.resolve(process.cwd(), JSON_FILE_NAME);
|
|
17
|
-
console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(releaseHistory, null, 2));
|
|
18
|
-
fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(releaseHistory));
|
|
19
|
-
await setReleaseHistory(binaryVersion, JSON_FILE_PATH, releaseHistory, platform, identifier);
|
|
20
|
-
fs.unlinkSync(JSON_FILE_PATH);
|
|
14
|
+
await stageReleaseHistoryFile(binaryVersion, releaseHistory, platform, (jsonFilePath) => setReleaseHistory(binaryVersion, jsonFilePath, releaseHistory, platform, identifier));
|
|
21
15
|
}
|
|
22
16
|
catch (error) {
|
|
23
17
|
console.error('Error occurred while updating history:', error);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const STAGING_DIR_NAME = 'codepush-release-history';
|
|
4
|
+
/**
|
|
5
|
+
* Writes the release history to a file for the config to read, and takes it away again
|
|
6
|
+
* once the config has stored it.
|
|
7
|
+
*
|
|
8
|
+
* Written under a directory of the platform's own rather than straight into the directory
|
|
9
|
+
* the command ran in. The name follows from the binary version alone, so two commands
|
|
10
|
+
* releasing the same version of one app for different platforms shared one path: whichever
|
|
11
|
+
* finished first took the file away while the other was still about to read it, and before
|
|
12
|
+
* that each had overwritten the other's contents. The name itself is kept as it was,
|
|
13
|
+
* because the name is part of what the config is handed.
|
|
14
|
+
*
|
|
15
|
+
* A file whose history was never stored is left where it was written, so a release that
|
|
16
|
+
* failed can be read back from a path that is the same on every run.
|
|
17
|
+
*/
|
|
18
|
+
export async function stageReleaseHistoryFile(binaryVersion, releaseHistory, platform, store) {
|
|
19
|
+
const jsonFileName = `${binaryVersion}.json`;
|
|
20
|
+
const stagingDir = path.resolve(process.cwd(), STAGING_DIR_NAME, platform);
|
|
21
|
+
const jsonFilePath = path.join(stagingDir, jsonFileName);
|
|
22
|
+
fs.mkdirSync(stagingDir, { recursive: true });
|
|
23
|
+
console.log(`log: creating JSON file... ("${jsonFileName}")\n`, JSON.stringify(releaseHistory, null, 2));
|
|
24
|
+
fs.writeFileSync(jsonFilePath, JSON.stringify(releaseHistory));
|
|
25
|
+
const stored = await store(jsonFilePath);
|
|
26
|
+
fs.rmSync(jsonFilePath, { force: true });
|
|
27
|
+
return stored;
|
|
28
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type { ReleaseHistoryInterface } from "../../typings/react-native-code-push.d.ts";
|
|
4
|
+
|
|
5
|
+
const STAGING_DIR_NAME = 'codepush-release-history';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Writes the release history to a file for the config to read, and takes it away again
|
|
9
|
+
* once the config has stored it.
|
|
10
|
+
*
|
|
11
|
+
* Written under a directory of the platform's own rather than straight into the directory
|
|
12
|
+
* the command ran in. The name follows from the binary version alone, so two commands
|
|
13
|
+
* releasing the same version of one app for different platforms shared one path: whichever
|
|
14
|
+
* finished first took the file away while the other was still about to read it, and before
|
|
15
|
+
* that each had overwritten the other's contents. The name itself is kept as it was,
|
|
16
|
+
* because the name is part of what the config is handed.
|
|
17
|
+
*
|
|
18
|
+
* A file whose history was never stored is left where it was written, so a release that
|
|
19
|
+
* failed can be read back from a path that is the same on every run.
|
|
20
|
+
*/
|
|
21
|
+
export async function stageReleaseHistoryFile<T>(
|
|
22
|
+
binaryVersion: string,
|
|
23
|
+
releaseHistory: ReleaseHistoryInterface,
|
|
24
|
+
platform: 'ios' | 'android',
|
|
25
|
+
store: (jsonFilePath: string) => Promise<T>,
|
|
26
|
+
): Promise<T> {
|
|
27
|
+
const jsonFileName = `${binaryVersion}.json`;
|
|
28
|
+
const stagingDir = path.resolve(process.cwd(), STAGING_DIR_NAME, platform);
|
|
29
|
+
const jsonFilePath = path.join(stagingDir, jsonFileName);
|
|
30
|
+
|
|
31
|
+
fs.mkdirSync(stagingDir, { recursive: true });
|
|
32
|
+
console.log(`log: creating JSON file... ("${jsonFileName}")\n`, JSON.stringify(releaseHistory, null, 2));
|
|
33
|
+
fs.writeFileSync(jsonFilePath, JSON.stringify(releaseHistory));
|
|
34
|
+
|
|
35
|
+
const stored = await store(jsonFilePath);
|
|
36
|
+
fs.rmSync(jsonFilePath, { force: true });
|
|
37
|
+
return stored;
|
|
38
|
+
}
|
package/ios/CodePush/CodePush.h
CHANGED
|
@@ -157,22 +157,25 @@ failCallback:(void (^)(NSError *err))failCallback;
|
|
|
157
157
|
@interface CodePushErrorUtils : NSObject
|
|
158
158
|
|
|
159
159
|
+ (NSError *)errorWithMessage:(NSString *)errorMessage;
|
|
160
|
+
+ (NSError *)errorWithMessage:(NSString *)errorMessage httpStatusCode:(NSInteger)statusCode;
|
|
160
161
|
+ (BOOL)isCodePushError:(NSError *)error;
|
|
162
|
+
+ (BOOL)isNetworkFailure:(NSError *)error;
|
|
163
|
+
+ (BOOL)isHttpStatusError:(NSError *)error;
|
|
161
164
|
|
|
162
165
|
@end
|
|
163
166
|
|
|
164
167
|
@interface CodePushPackage : NSObject
|
|
165
168
|
|
|
166
169
|
/*
|
|
167
|
-
* doneCallback is given what the
|
|
168
|
-
*
|
|
170
|
+
* doneCallback is given what the attempts at installing the update from its update
|
|
171
|
+
* archives ended in, or nil when the update had no archive to attempt. The result
|
|
169
172
|
* describes the download and not the update: it is never part of the update's metadata.
|
|
170
173
|
*/
|
|
171
174
|
+ (void)downloadPackage:(NSDictionary *)updatePackage
|
|
172
175
|
expectedBundleFileName:(NSString *)expectedBundleFileName
|
|
173
176
|
operationQueue:(dispatch_queue_t)operationQueue
|
|
174
177
|
progressCallback:(void (^)(long long, long long))progressCallback
|
|
175
|
-
doneCallback:(void (^)(NSDictionary *
|
|
178
|
+
doneCallback:(void (^)(NSDictionary *updateArchiveResult))doneCallback
|
|
176
179
|
failCallback:(void (^)(NSError *err))failCallback;
|
|
177
180
|
|
|
178
181
|
+ (NSDictionary *)getCurrentPackage:(NSError **)error;
|
package/ios/CodePush/CodePush.mm
CHANGED
|
@@ -63,7 +63,7 @@ static NSString *const PackageIsPendingKey = @"isPending";
|
|
|
63
63
|
|
|
64
64
|
// Not one of those keys: the field a download that tried a binary patch reports its outcome
|
|
65
65
|
// under, on the package it resolves with and on no package it stores.
|
|
66
|
-
static NSString *const
|
|
66
|
+
static NSString *const UpdateArchiveResultKey = @"updateArchiveResult";
|
|
67
67
|
|
|
68
68
|
#pragma mark - Static variables
|
|
69
69
|
|
|
@@ -740,8 +740,8 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
|
|
|
740
740
|
// The download is progressing forward
|
|
741
741
|
progressCallback:^(long long expectedContentLength, long long receivedContentLength) {
|
|
742
742
|
// Received bytes only run backwards when a new download starts a stream of its
|
|
743
|
-
// own - the
|
|
744
|
-
// from the finished
|
|
743
|
+
// own - the next archive after a binary patch fallback. The pause left over
|
|
744
|
+
// from the finished download would swallow that whole stream, so frame
|
|
745
745
|
// observation resumes here and the throttle forgets the previous stream.
|
|
746
746
|
if (notifyProgress && receivedContentLength < _latestReceivedConentLength) {
|
|
747
747
|
self.paused = NO;
|
|
@@ -767,21 +767,21 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
|
|
|
767
767
|
}
|
|
768
768
|
}
|
|
769
769
|
// The download completed
|
|
770
|
-
doneCallback:^(NSDictionary *
|
|
770
|
+
doneCallback:^(NSDictionary *updateArchiveResult) {
|
|
771
771
|
NSError *err;
|
|
772
772
|
NSDictionary *newPackage = [CodePushPackage getPackage:mutableUpdatePackage[PackageHashKey] error:&err];
|
|
773
773
|
|
|
774
774
|
if (err) {
|
|
775
|
-
return reject([NSString stringWithFormat: @"%
|
|
775
|
+
return reject([NSString stringWithFormat: @"%ld", (long)err.code], err.localizedDescription, err);
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
-
if (
|
|
778
|
+
if (updateArchiveResult) {
|
|
779
779
|
// The result describes this download rather than the update, so it is put on
|
|
780
780
|
// what this call resolves with and nowhere else: the metadata that was
|
|
781
781
|
// written for the update knows nothing about it, and whoever asked for the
|
|
782
782
|
// download decides what the result is worth.
|
|
783
783
|
NSMutableDictionary *downloadedPackage = [newPackage mutableCopy];
|
|
784
|
-
downloadedPackage[
|
|
784
|
+
downloadedPackage[UpdateArchiveResultKey] = updateArchiveResult;
|
|
785
785
|
return resolve(downloadedPackage);
|
|
786
786
|
}
|
|
787
787
|
|
|
@@ -796,7 +796,7 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
|
|
|
796
796
|
// Stop observing frame updates if the download fails.
|
|
797
797
|
_didUpdateProgress = NO;
|
|
798
798
|
self.paused = YES;
|
|
799
|
-
reject([NSString stringWithFormat: @"%
|
|
799
|
+
reject([NSString stringWithFormat: @"%ld", (long)err.code], err.localizedDescription, err);
|
|
800
800
|
}];
|
|
801
801
|
}
|
|
802
802
|
|
|
@@ -876,7 +876,7 @@ RCT_EXPORT_METHOD(getUpdateMetadata:(double)updateStateValue
|
|
|
876
876
|
NSMutableDictionary *package = [[CodePushPackage getCurrentPackage:&error] mutableCopy];
|
|
877
877
|
|
|
878
878
|
if (error) {
|
|
879
|
-
return reject([NSString stringWithFormat: @"%
|
|
879
|
+
return reject([NSString stringWithFormat: @"%ld", (long)error.code], error.localizedDescription, error);
|
|
880
880
|
} else if (package == nil) {
|
|
881
881
|
// The app hasn't downloaded any CodePush updates yet,
|
|
882
882
|
// so we simply return nil regardless if the user
|
|
@@ -930,7 +930,7 @@ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage
|
|
|
930
930
|
error:&error];
|
|
931
931
|
|
|
932
932
|
if (error) {
|
|
933
|
-
reject([NSString stringWithFormat: @"%
|
|
933
|
+
reject([NSString stringWithFormat: @"%ld", (long)error.code], error.localizedDescription, error);
|
|
934
934
|
} else {
|
|
935
935
|
[self savePendingUpdate:updatePackage[PackageHashKey]
|
|
936
936
|
isLoading:NO];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#import <Foundation/Foundation.h>
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
* Why an update that was downloaded as
|
|
4
|
+
* Why an update that was downloaded as one of its archives has to be downloaded in full
|
|
5
5
|
* instead.
|
|
6
6
|
*
|
|
7
7
|
* A failed restore is not an error the user ever hears about: it is the signal to
|
|
@@ -11,22 +11,32 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/** The bundle inside the app binary could not be opened or read. */
|
|
14
|
-
extern NSString *const
|
|
14
|
+
extern NSString *const CodePushArchiveFallbackReasonBaseBundleUnavailable;
|
|
15
15
|
|
|
16
16
|
/** The bundle inside the app binary is not the one the patch was computed against. */
|
|
17
|
-
extern NSString *const
|
|
17
|
+
extern NSString *const CodePushArchiveFallbackReasonBaseHashMismatch;
|
|
18
18
|
|
|
19
19
|
/** The manifest is missing, malformed, points outside the archive, or asks for too much. */
|
|
20
|
-
extern NSString *const
|
|
20
|
+
extern NSString *const CodePushArchiveFallbackReasonInvalidManifest;
|
|
21
21
|
|
|
22
22
|
/** The patch was produced by a format or a codec this client cannot apply. */
|
|
23
|
-
extern NSString *const
|
|
23
|
+
extern NSString *const CodePushArchiveFallbackReasonUnsupportedFormat;
|
|
24
24
|
|
|
25
25
|
/** The applier refused the patch, or the restored bundle could not be written. */
|
|
26
|
-
extern NSString *const
|
|
26
|
+
extern NSString *const CodePushArchiveFallbackReasonPatchApplyFailed;
|
|
27
27
|
|
|
28
28
|
/** The restored bundle is not the one the manifest promised. */
|
|
29
|
-
extern NSString *const
|
|
29
|
+
extern NSString *const CodePushArchiveFallbackReasonTargetVerificationFailed;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The asset diff could not be merged with the installed update it was built against.
|
|
33
|
+
*
|
|
34
|
+
* Reported by the caller too, from the merge that follows the restore: the diff archive
|
|
35
|
+
* only carries what the installed update does not, so this covers reading the installed
|
|
36
|
+
* update's files, applying the deletion manifest, and everything else that stitches the
|
|
37
|
+
* two back together.
|
|
38
|
+
*/
|
|
39
|
+
extern NSString *const CodePushArchiveFallbackReasonAssetMergeFailed;
|
|
30
40
|
|
|
31
41
|
/**
|
|
32
42
|
* The update restored from the patch did not pass the checks every update passes before
|
|
@@ -35,7 +45,7 @@ extern NSString *const CodePushBinaryPatchReasonTargetVerificationFailed;
|
|
|
35
45
|
* This one is reported by the caller rather than by the applier: by the time it applies,
|
|
36
46
|
* the applier has already handed back a bundle it was happy with.
|
|
37
47
|
*/
|
|
38
|
-
extern NSString *const
|
|
48
|
+
extern NSString *const CodePushArchiveFallbackReasonPackageVerificationFailed;
|
|
39
49
|
|
|
40
50
|
/**
|
|
41
51
|
* Rebuilds the JS bundle of an update that was downloaded as a binary patch archive.
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
#include "binarypatch_zstd_decompressor.h"
|
|
15
15
|
#include "libHDiffPatch/HPatch/patch.h"
|
|
16
16
|
|
|
17
|
-
NSString *const
|
|
18
|
-
NSString *const
|
|
19
|
-
NSString *const
|
|
20
|
-
NSString *const
|
|
21
|
-
NSString *const
|
|
22
|
-
NSString *const
|
|
23
|
-
NSString *const
|
|
17
|
+
NSString *const CodePushArchiveFallbackReasonBaseBundleUnavailable = @"base_bundle_unavailable";
|
|
18
|
+
NSString *const CodePushArchiveFallbackReasonBaseHashMismatch = @"base_hash_mismatch";
|
|
19
|
+
NSString *const CodePushArchiveFallbackReasonInvalidManifest = @"invalid_manifest";
|
|
20
|
+
NSString *const CodePushArchiveFallbackReasonUnsupportedFormat = @"unsupported_format";
|
|
21
|
+
NSString *const CodePushArchiveFallbackReasonPatchApplyFailed = @"patch_apply_failed";
|
|
22
|
+
NSString *const CodePushArchiveFallbackReasonTargetVerificationFailed = @"target_verification_failed";
|
|
23
|
+
NSString *const CodePushArchiveFallbackReasonAssetMergeFailed = @"asset_merge_failed";
|
|
24
|
+
NSString *const CodePushArchiveFallbackReasonPackageVerificationFailed = @"package_verification_failed";
|
|
24
25
|
|
|
25
26
|
#pragma mark - Private constants
|
|
26
27
|
|
|
@@ -205,7 +206,7 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
205
206
|
NSString *contentsFolderPath = [self contentsFolderInUnzippedFolder:unzippedFolderPath];
|
|
206
207
|
NSString *manifestFilePath = [contentsFolderPath stringByAppendingPathComponent:BinaryPatchManifestFileName];
|
|
207
208
|
if (![self isFileAtPath:manifestFilePath]) {
|
|
208
|
-
return [self failWithReason:
|
|
209
|
+
return [self failWithReason:CodePushArchiveFallbackReasonInvalidManifest
|
|
209
210
|
outFailureReason:failureReason];
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -219,13 +220,13 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
219
220
|
: nil;
|
|
220
221
|
if (![manifest isKindOfClass:[NSDictionary class]]) {
|
|
221
222
|
CPLog(@"The binary patch manifest could not be read: %@", error);
|
|
222
|
-
return [self failWithReason:
|
|
223
|
+
return [self failWithReason:CodePushArchiveFallbackReasonInvalidManifest
|
|
223
224
|
outFailureReason:failureReason];
|
|
224
225
|
}
|
|
225
226
|
|
|
226
227
|
if ([[self numberInManifest:manifest forKey:BinaryPatchFormatVersionKey] integerValue] != BinaryPatchFormatVersion
|
|
227
228
|
|| ![BinaryPatchAlgorithm isEqualToString:[self stringInManifest:manifest forKey:BinaryPatchAlgorithmKey]]) {
|
|
228
|
-
return [self failWithReason:
|
|
229
|
+
return [self failWithReason:CodePushArchiveFallbackReasonUnsupportedFormat
|
|
229
230
|
outFailureReason:failureReason];
|
|
230
231
|
}
|
|
231
232
|
|
|
@@ -239,7 +240,7 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
239
240
|
if (targetBundleFilePath == nil || patchFilePath == nil || ![self isFileAtPath:patchFilePath]
|
|
240
241
|
|| baseBundleHash.length == 0 || targetBundleHash.length == 0
|
|
241
242
|
|| targetBundleSize <= 0 || targetBundleSize > BinaryPatchMaxTargetBundleSize) {
|
|
242
|
-
return [self failWithReason:
|
|
243
|
+
return [self failWithReason:CodePushArchiveFallbackReasonInvalidManifest
|
|
243
244
|
outFailureReason:failureReason];
|
|
244
245
|
}
|
|
245
246
|
|
|
@@ -250,7 +251,7 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
250
251
|
attributes:nil
|
|
251
252
|
error:&error]) {
|
|
252
253
|
CPLog(@"Unable to create the binary patch working directory at %@: %@", workingFolderPath, error);
|
|
253
|
-
return [self failWithReason:
|
|
254
|
+
return [self failWithReason:CodePushArchiveFallbackReasonPatchApplyFailed
|
|
254
255
|
outFailureReason:failureReason];
|
|
255
256
|
}
|
|
256
257
|
|
|
@@ -296,13 +297,13 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
296
297
|
error:nil][NSFileSystemFreeSize];
|
|
297
298
|
if (freeSize != nil && freeSize.longLongValue < targetBundleSize) {
|
|
298
299
|
CPLog(@"Not enough free space to restore a %lld byte bundle.", targetBundleSize);
|
|
299
|
-
return
|
|
300
|
+
return CodePushArchiveFallbackReasonPatchApplyFailed;
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
NSString *baseBundlePath = [baseBundleURL path];
|
|
303
304
|
if (baseBundlePath == nil) {
|
|
304
305
|
CPLog(@"The app binary carries no JS bundle to patch against.");
|
|
305
|
-
return
|
|
306
|
+
return CodePushArchiveFallbackReasonBaseBundleUnavailable;
|
|
306
307
|
}
|
|
307
308
|
|
|
308
309
|
NSError *error = nil;
|
|
@@ -318,10 +319,10 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
318
319
|
error:&error];
|
|
319
320
|
if (baseBundle == nil) {
|
|
320
321
|
CPLog(@"Unable to read the JS bundle inside the app binary: %@", error);
|
|
321
|
-
return
|
|
322
|
+
return CodePushArchiveFallbackReasonBaseBundleUnavailable;
|
|
322
323
|
}
|
|
323
324
|
if (![baseBundleHash isEqualToString:[CodePushUpdateUtils computeHashForData:baseBundle]]) {
|
|
324
|
-
return
|
|
325
|
+
return CodePushArchiveFallbackReasonBaseHashMismatch;
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
__attribute__((objc_precise_lifetime)) NSData *patch = [NSData dataWithContentsOfFile:patchFilePath
|
|
@@ -329,7 +330,7 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
329
330
|
error:&error];
|
|
330
331
|
if (patch == nil) {
|
|
331
332
|
CPLog(@"Unable to read the binary patch: %@", error);
|
|
332
|
-
return
|
|
333
|
+
return CodePushArchiveFallbackReasonPatchApplyFailed;
|
|
333
334
|
}
|
|
334
335
|
|
|
335
336
|
NSString *restoredBundleFilePath = [workingFolderPath stringByAppendingPathComponent:BinaryPatchTargetFileName];
|
|
@@ -342,20 +343,20 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
342
343
|
if (applyResult != CodePushBinaryPatchApplyResultOK) {
|
|
343
344
|
CPLog(@"The binary patch applier returned %ld.", (long)applyResult);
|
|
344
345
|
return applyResult == CodePushBinaryPatchApplyResultUnsupportedCompression
|
|
345
|
-
?
|
|
346
|
-
:
|
|
346
|
+
? CodePushArchiveFallbackReasonUnsupportedFormat
|
|
347
|
+
: CodePushArchiveFallbackReasonPatchApplyFailed;
|
|
347
348
|
}
|
|
348
349
|
|
|
349
350
|
NSDictionary *restoredBundleAttributes = [fileManager attributesOfItemAtPath:restoredBundleFilePath
|
|
350
351
|
error:&error];
|
|
351
352
|
if (restoredBundleAttributes == nil || (long long)[restoredBundleAttributes fileSize] != targetBundleSize) {
|
|
352
353
|
CPLog(@"The restored bundle is not %lld bytes long: %@", targetBundleSize, error);
|
|
353
|
-
return
|
|
354
|
+
return CodePushArchiveFallbackReasonTargetVerificationFailed;
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
NSString *restoredBundleHash = [CodePushUpdateUtils computeHashForFileAtPath:restoredBundleFilePath];
|
|
357
358
|
if (restoredBundleHash == nil || ![targetBundleHash isEqualToString:restoredBundleHash]) {
|
|
358
|
-
return
|
|
359
|
+
return CodePushArchiveFallbackReasonTargetVerificationFailed;
|
|
359
360
|
}
|
|
360
361
|
|
|
361
362
|
if (![self moveItemAtPath:restoredBundleFilePath toPath:targetBundleFilePath]
|
|
@@ -365,7 +366,7 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
365
366
|
// that follows empties the unzipped archive before it replaces it, which is what
|
|
366
367
|
// clears them.
|
|
367
368
|
CPLog(@"Unable to put the restored bundle in place of the patch: %@", error);
|
|
368
|
-
return
|
|
369
|
+
return CodePushArchiveFallbackReasonPatchApplyFailed;
|
|
369
370
|
}
|
|
370
371
|
|
|
371
372
|
return nil;
|
|
@@ -54,7 +54,8 @@ failCallback:(void (^)(NSError *err))failCallback {
|
|
|
54
54
|
if (statusCode >= 400) {
|
|
55
55
|
[self.outputFileStream close];
|
|
56
56
|
[connection cancel];
|
|
57
|
-
NSError *err = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat: @"Received %ld response from %@", (long)statusCode, self.downloadUrl]
|
|
57
|
+
NSError *err = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat: @"Received %ld response from %@", (long)statusCode, self.downloadUrl]
|
|
58
|
+
httpStatusCode:statusCode];
|
|
58
59
|
self.failCallback(err);
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
static NSString *const CodePushErrorDomain = @"CodePushError";
|
|
6
6
|
static const int CodePushErrorCode = -1;
|
|
7
|
+
/*
|
|
8
|
+
* The status a server answered a download with, on the error raised for it.
|
|
9
|
+
*
|
|
10
|
+
* Carried in the user info rather than in the error code, because the code is what JS reads
|
|
11
|
+
* an error by and every error of this domain has always been -1 there.
|
|
12
|
+
*/
|
|
13
|
+
static NSString *const CodePushHttpStatusCodeKey = @"CodePushHttpStatusCode";
|
|
7
14
|
|
|
8
15
|
+ (NSError *)errorWithMessage:(NSString *)errorMessage
|
|
9
16
|
{
|
|
@@ -12,9 +19,57 @@ static const int CodePushErrorCode = -1;
|
|
|
12
19
|
userInfo:@{ NSLocalizedDescriptionKey: NSLocalizedString(errorMessage, nil) }];
|
|
13
20
|
}
|
|
14
21
|
|
|
22
|
+
+ (NSError *)errorWithMessage:(NSString *)errorMessage httpStatusCode:(NSInteger)statusCode
|
|
23
|
+
{
|
|
24
|
+
return [NSError errorWithDomain:CodePushErrorDomain
|
|
25
|
+
code:CodePushErrorCode
|
|
26
|
+
userInfo:@{ NSLocalizedDescriptionKey: NSLocalizedString(errorMessage, nil),
|
|
27
|
+
CodePushHttpStatusCodeKey: @(statusCode) }];
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
+ (BOOL)isCodePushError:(NSError *)err
|
|
16
31
|
{
|
|
17
32
|
return err != nil && [CodePushErrorDomain isEqualToString:err.domain];
|
|
18
33
|
}
|
|
19
34
|
|
|
35
|
+
/*
|
|
36
|
+
* Whether the download failed because the server answered it with a status rather than with
|
|
37
|
+
* a body to install.
|
|
38
|
+
*/
|
|
39
|
+
+ (BOOL)isHttpStatusError:(NSError *)err
|
|
40
|
+
{
|
|
41
|
+
return [self isCodePushError:err] && err.userInfo[CodePushHttpStatusCodeKey] != nil;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* Whether the request failed because the network did not carry it.
|
|
46
|
+
*
|
|
47
|
+
* A server that answered is not this, however it answered: the connection worked, and
|
|
48
|
+
* asking a different URL over it is worth doing. A connection that never opened or that
|
|
49
|
+
* dropped is, and every URL behind it is equally out of reach.
|
|
50
|
+
*
|
|
51
|
+
* Named from the codes rather than the domain, because `NSURLErrorDomain` also covers a
|
|
52
|
+
* URL that was malformed or a scheme that is not supported - failures of the request
|
|
53
|
+
* rather than of the network under it.
|
|
54
|
+
*/
|
|
55
|
+
+ (BOOL)isNetworkFailure:(NSError *)err
|
|
56
|
+
{
|
|
57
|
+
if (err == nil || ![NSURLErrorDomain isEqualToString:err.domain]) {
|
|
58
|
+
return NO;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
switch (err.code) {
|
|
62
|
+
case NSURLErrorTimedOut:
|
|
63
|
+
case NSURLErrorCannotFindHost:
|
|
64
|
+
case NSURLErrorCannotConnectToHost:
|
|
65
|
+
case NSURLErrorNetworkConnectionLost:
|
|
66
|
+
case NSURLErrorDNSLookupFailed:
|
|
67
|
+
case NSURLErrorNotConnectedToInternet:
|
|
68
|
+
case NSURLErrorSecureConnectionFailed:
|
|
69
|
+
return YES;
|
|
70
|
+
default:
|
|
71
|
+
return NO;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
20
75
|
@end
|