@bravemobile/react-native-code-push 13.1.0-beta.3 → 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 +20 -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 +55 -13
- package/cli/commands/releaseCommand/release.ts +39 -6
- 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.js +23 -8
- package/cli/dist/commands/releaseCommand/release.test.js +37 -9
- package/cli/dist/commands/updateHistoryCommand/updateReleaseHistory.js +2 -8
- package/cli/dist/functions/resolveAssetDiffBases.js +7 -2
- package/cli/dist/functions/resolveAssetDiffBases.test.js +26 -12
- package/cli/dist/functions/stageReleaseHistoryFile.js +28 -0
- package/cli/functions/resolveAssetDiffBases.test.ts +28 -14
- package/cli/functions/resolveAssetDiffBases.ts +8 -1
- 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 +152 -41
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +0 -92
package/CodePush.podspec
CHANGED
|
@@ -36,6 +36,11 @@ Pod::Spec.new do |s|
|
|
|
36
36
|
]
|
|
37
37
|
s.public_header_files = ['ios/CodePush/CodePush.h']
|
|
38
38
|
|
|
39
|
+
s.test_spec 'Tests' do |test_spec|
|
|
40
|
+
test_spec.source_files = 'ios/CodePushTests/**/*.{h,m}'
|
|
41
|
+
test_spec.resources = 'cli/fixtures/binary-patch/*'
|
|
42
|
+
end
|
|
43
|
+
|
|
39
44
|
binary_patch_header_search_paths = '"$(PODS_TARGET_SRCROOT)/cpp/binarypatch" "$(PODS_TARGET_SRCROOT)/cpp/binarypatch/vendor/HDiffPatch" "$(PODS_TARGET_SRCROOT)/cpp/binarypatch/vendor/zstd"'
|
|
40
45
|
# ZSTD_DISABLE_ASM: the assembly fast path is intentionally not vendored.
|
|
41
46
|
# _IS_USED_MULTITHREAD=0: patches are applied on the thread that downloads them.
|
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- **No API Server Needed** – Use static hosting solutions (e.g., AWS S3) without maintaining additional API servers.
|
|
6
6
|
- **Familiar API** – Built on `microsoft/react-native-code-push`, with the native module migrated to TurboModule.
|
|
7
7
|
- **Flexible Deployment** – Implement your own release workflow, giving you complete control over the deployment process.
|
|
8
|
+
- **Diff Updates** – Publish [binary patch and asset diff releases](docs/diff-updates.md) so a client downloads the difference from what it already holds, not the whole bundle.
|
|
8
9
|
|
|
9
10
|
### 🚀 New Architecture support
|
|
10
11
|
|
|
@@ -292,76 +293,12 @@ export default CodePush({
|
|
|
292
293
|
|
|
293
294
|
#### 4-1. Telemetry Callbacks
|
|
294
295
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
- **onDownloadStart:** Triggered when the bundle download begins.
|
|
299
|
-
- **onDownloadSuccess:** Triggered when the bundle download completes successfully.
|
|
300
|
-
- **onSyncError:** Triggered when an unknown error occurs during the update process. (`CodePush.SyncStatus.UNKNOWN_ERROR` status)
|
|
301
|
-
- **onBinaryPatchResult:** Triggered when an update that was published with a binary patch has been downloaded, with the release label and how the patch went. Unlike the callbacks above, this one can also be passed to a single `sync()` call - see below.
|
|
296
|
+
`CodePushOptions` takes optional callbacks that report what an update did - when it was
|
|
297
|
+
downloaded, whether it ran or was rolled back, and which archive it came from. They are
|
|
298
|
+
purely for observation.
|
|
302
299
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
is there to be observed and nothing more. The library neither stores it nor sends it anywhere - an app that wants it in
|
|
306
|
-
its telemetry sends it itself. A callback that throws never fails the update - the error is only logged to the console.
|
|
307
|
-
A fallback also shows in `downloadProgressCallback`: the full download reports a second progress stream that counts
|
|
308
|
-
`receivedBytes` from zero again, against its own, larger `totalBytes`.
|
|
309
|
-
|
|
310
|
-
Register `onBinaryPatchResult` on `CodePush({ ... })` like the callbacks above and it covers every sync, whatever the `checkFrequency` is
|
|
311
|
-
and including the `CodePush.sync()` calls you make yourself.
|
|
312
|
-
|
|
313
|
-
```typescript
|
|
314
|
-
export default CodePush({
|
|
315
|
-
checkFrequency: CodePush.CheckFrequency.MANUAL, // or something else
|
|
316
|
-
releaseHistoryFetcher: releaseHistoryFetcher,
|
|
317
|
-
onBinaryPatchResult: (label, result) => {
|
|
318
|
-
// Send it to your own telemetry, if you want it there.
|
|
319
|
-
},
|
|
320
|
-
})(MyApp);
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
Pass it to a `sync()` call to override the registered one for that call - to tag the result of one particular sync,
|
|
324
|
-
for instance.
|
|
325
|
-
|
|
326
|
-
```typescript
|
|
327
|
-
CodePush.sync({
|
|
328
|
-
onBinaryPatchResult: (label, result) => {
|
|
329
|
-
// Send it to your own telemetry, if you want it there.
|
|
330
|
-
},
|
|
331
|
-
});
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
#### 4-2. Asset Diff Archives
|
|
336
|
-
|
|
337
|
-
A release published with a binary patch can carry **asset diff archives** as well - one per
|
|
338
|
-
recently released version. A diff archive holds the patch of the JS bundle, only the assets
|
|
339
|
-
that version does not already have, and a manifest of the files it has to drop. The client
|
|
340
|
-
copies the update it already has installed, applies those, and ends up holding exactly the
|
|
341
|
-
contents of the full archive.
|
|
342
|
-
|
|
343
|
-
So one release has a full archive, a patch archive and up to `--diff-base-count` diff
|
|
344
|
-
archives, and a client downloads one of them per attempt: the diff archive built against
|
|
345
|
-
the update it is running, when the release has one, and the patch archive when it is
|
|
346
|
-
running the bundle in the binary or an update this release was not diffed against.
|
|
347
|
-
|
|
348
|
-
Diff archives are published only when all three of these hold:
|
|
349
|
-
- the release is a binary patch release (`release --binary-bundle-path`),
|
|
350
|
-
- `code-push.config.ts` implements `bundleDownloader`, so the CLI can fetch the earlier releases to diff against,
|
|
351
|
-
- `--diff-base-count` is greater than `0` (it defaults to `3`).
|
|
352
|
-
|
|
353
|
-
```ts
|
|
354
|
-
bundleDownloader: async (downloadUrl) => {
|
|
355
|
-
const downloadedFilePath = path.join(os.tmpdir(), path.basename(new URL(downloadUrl).pathname));
|
|
356
|
-
// fetch the archive from your storage (S3, GCS, ...) to downloadedFilePath
|
|
357
|
-
return { downloadedFilePath };
|
|
358
|
-
},
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
Rebuilding the update from a diff archive falls back the way a patch does: the update is
|
|
362
|
-
downloaded in full instead, and `onBinaryPatchResult` reports it as
|
|
363
|
-
`{ status: "fallback", fallbackReason: "package_verification_failed" }`. See
|
|
364
|
-
[Asset diff archives](cli/README.md#asset-diff-archives) for what the release publishes.
|
|
300
|
+
See [Telemetry callbacks](docs/telemetry-callbacks.md) for what each one receives and how to
|
|
301
|
+
register them.
|
|
365
302
|
|
|
366
303
|
|
|
367
304
|
### 5. Configure the CLI Tool
|
|
@@ -372,7 +309,7 @@ downloaded in full instead, and `onBinaryPatchResult` reports it as
|
|
|
372
309
|
**(1) Create a `code-push.config.ts` file in the root directory of your project.**
|
|
373
310
|
|
|
374
311
|
Then, implement three functions to upload the bundle file and create/update the release history,
|
|
375
|
-
plus an optional fourth one - `bundleDownloader` - if you publish [asset diff archives](#
|
|
312
|
+
plus an optional fourth one - `bundleDownloader` - if you publish [asset diff archives](docs/diff-updates.md#asset-diff-archives).
|
|
376
313
|
The CLI tool uses these functions to release CodePush updates and manage releases.
|
|
377
314
|
(These functions are not used at runtime by the library.)
|
|
378
315
|
|
|
@@ -389,6 +326,7 @@ const Config: CliConfigInterface = {
|
|
|
389
326
|
source: string,
|
|
390
327
|
platform: "ios" | "android",
|
|
391
328
|
identifier,
|
|
329
|
+
artifact,
|
|
392
330
|
): Promise<{downloadUrl: string}> => {
|
|
393
331
|
// ...
|
|
394
332
|
},
|
|
@@ -413,7 +351,7 @@ const Config: CliConfigInterface = {
|
|
|
413
351
|
|
|
414
352
|
// Only needed to publish asset diff archives (see "4-2. Asset Diff Archives").
|
|
415
353
|
// bundleDownloader: async (
|
|
416
|
-
//
|
|
354
|
+
// archive,
|
|
417
355
|
// platform: "ios" | "android",
|
|
418
356
|
// identifier,
|
|
419
357
|
// ): Promise<{downloadedFilePath: string}> => {
|
|
@@ -427,6 +365,9 @@ module.exports = Config;
|
|
|
427
365
|
|
|
428
366
|
**`bundleUploader`**
|
|
429
367
|
- Implements a function to upload the bundle file.
|
|
368
|
+
- The optional `artifact` argument identifies the full bundle, binary patch, or asset diff
|
|
369
|
+
archive. The `release` command always provides it, including the target binary version and
|
|
370
|
+
the package hashes needed to construct a storage key without parsing a file name.
|
|
430
371
|
- The `downloadUrl` returned by this function is recorded in `ReleaseHistoryInterface` data
|
|
431
372
|
and is used by the library runtime to download the bundle file from this URL.
|
|
432
373
|
- Used in the following cases:
|
|
@@ -474,82 +415,16 @@ module.exports = Config;
|
|
|
474
415
|
```
|
|
475
416
|
|
|
476
417
|
|
|
477
|
-
### 6.
|
|
478
|
-
|
|
479
|
-
Only needed if you want to release **binary patch updates** (`release --binary-bundle-path`).
|
|
480
|
-
A binary patch is the difference between the update and the JS bundle that is already
|
|
481
|
-
inside the installed app, so releasing one means holding on to that bundle: the exact
|
|
482
|
-
bytes the build you shipped to the store embedded.
|
|
483
|
-
|
|
484
|
-
The library ships a hook for each platform that copies the freshly compiled bundle out of
|
|
485
|
-
the build, together with a `binary-patch-base.json` record describing it.
|
|
418
|
+
### 6. Diff Updates (Optional)
|
|
486
419
|
|
|
487
|
-
|
|
420
|
+
In place of the full archive, a release can offer a binary patch computed against the JS
|
|
421
|
+
bundle inside the app binary. A release that carries one can also carry an asset diff, for a
|
|
422
|
+
client running one of the recent updates the release was diffed against. Both need a build
|
|
423
|
+
hook that copies the JS bundle out of your store build; asset diffs also need a
|
|
424
|
+
`bundleDownloader` in `code-push.config.ts`.
|
|
488
425
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
apply plugin: "com.facebook.react"
|
|
492
|
-
|
|
493
|
-
react {
|
|
494
|
-
// ...
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
apply from: "../../node_modules/@bravemobile/react-native-code-push/android/codepush-export.gradle"
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
The line can go anywhere in the file (if you use `ext.codePushExportDir`, set it before the
|
|
501
|
-
`apply from:` line). Every variant that bundles JS then exports to
|
|
502
|
-
`android/app/build/codepush/embedded-bundle/<variant>/` after it is bundled. Pass
|
|
503
|
-
`-PcodePushExportDir=<path>` (or set `ext.codePushExportDir`) to export somewhere else; the
|
|
504
|
-
`<variant>` directory is appended either way.
|
|
505
|
-
|
|
506
|
-
**iOS** - call the export script at the end of the **"Bundle React Native code and images"**
|
|
507
|
-
build phase. In Xcode, open that phase
|
|
508
|
-
in your app target's **Build Phases** tab and append the last line below to its script:
|
|
509
|
-
|
|
510
|
-
```bash
|
|
511
|
-
/bin/sh -c "\"$WITH_ENVIRONMENT\" \"$REACT_NATIVE_XCODE\""
|
|
512
|
-
|
|
513
|
-
"$SRCROOT/../node_modules/@bravemobile/react-native-code-push/scripts/export-embedded-bundle.sh"
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
The export lands in `$BUILD_DIR/codepush/embedded-bundle/$CONFIGURATION-$PLATFORM_NAME/`.
|
|
517
|
-
Set the `CODEPUSH_EXPORT_DIR` environment variable to export somewhere else; the
|
|
518
|
-
`$CONFIGURATION-$PLATFORM_NAME` directory is appended either way.
|
|
519
|
-
|
|
520
|
-
**Archive the export per binary release.** Whatever pipeline builds your store binary
|
|
521
|
-
should keep that build's export somewhere durable, organized by binary version, so a later
|
|
522
|
-
release can fetch the bundle that matches the binary it targets:
|
|
523
|
-
|
|
524
|
-
```bash
|
|
525
|
-
# Android, after ./gradlew :app:assembleRelease
|
|
526
|
-
aws s3 cp --recursive \
|
|
527
|
-
android/app/build/codepush/embedded-bundle/release \
|
|
528
|
-
"s3://your-bucket/binaries/android/$BINARY_VERSION/"
|
|
529
|
-
|
|
530
|
-
# iOS - point the export at a path the pipeline knows, since $BUILD_DIR only exists inside the build
|
|
531
|
-
export CODEPUSH_EXPORT_DIR="$PWD/codepush-export"
|
|
532
|
-
xcodebuild -workspace ios/YourApp.xcworkspace -scheme YourApp -configuration Release archive # ...
|
|
533
|
-
aws s3 cp --recursive \
|
|
534
|
-
"$CODEPUSH_EXPORT_DIR/Release-iphoneos" \
|
|
535
|
-
"s3://your-bucket/binaries/ios/$BINARY_VERSION/"
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
To release a patch later, download the export and pass the bundle's path to
|
|
539
|
-
`--binary-bundle-path`:
|
|
540
|
-
|
|
541
|
-
```bash
|
|
542
|
-
aws s3 cp --recursive "s3://your-bucket/binaries/android/1.0.0/" ./binary/
|
|
543
|
-
npx code-push release -b 1.0.0 -v 1.0.1 -p android \
|
|
544
|
-
--binary-bundle-path ./binary/index.android.bundle
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
`release` also uses the record to verify the base bundle it was handed - see
|
|
548
|
-
[Verifying the base bundle](cli/README.md#verifying-the-base-bundle) for details.
|
|
549
|
-
|
|
550
|
-
> [!NOTE]
|
|
551
|
-
> Applying these hooks automatically through the Expo config plugin (`app.plugin.js`) is
|
|
552
|
-
> not implemented yet.
|
|
426
|
+
See [Diff updates](docs/diff-updates.md) for the hooks, what to keep per binary version, and
|
|
427
|
+
how a client picks an archive.
|
|
553
428
|
|
|
554
429
|
|
|
555
430
|
## 🚀 CLI Tool Usage
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
package com.microsoft.codepush.react;
|
|
2
|
+
|
|
3
|
+
import org.json.JSONArray;
|
|
4
|
+
import org.json.JSONObject;
|
|
5
|
+
|
|
6
|
+
import java.util.ArrayList;
|
|
7
|
+
import java.util.List;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The record of installing an update from its update archives: which archives were tried,
|
|
11
|
+
* whether the update was installed from one of them, why the full archive had to be
|
|
12
|
+
* downloaded when it was not, and how long the work took.
|
|
13
|
+
*
|
|
14
|
+
* The record exists for an app that wants to judge an archive rollout with its own telemetry,
|
|
15
|
+
* so it travels to whoever asked for the download and nowhere else. It is not part of the
|
|
16
|
+
* update's metadata, it is never written to disk, and nothing here sends it anywhere.
|
|
17
|
+
*/
|
|
18
|
+
class ArchiveAttemptLog {
|
|
19
|
+
|
|
20
|
+
static final String ARCHIVE_ASSET_DIFF = "asset-diff";
|
|
21
|
+
static final String ARCHIVE_BINARY_PATCH = "binary-patch";
|
|
22
|
+
|
|
23
|
+
private static final String APPLY_DURATION_MS_KEY = "applyDurationMs";
|
|
24
|
+
private static final String ARCHIVE_KEY = "archive";
|
|
25
|
+
private static final String ATTEMPTS_KEY = "attempts";
|
|
26
|
+
private static final String DURATION_MS_KEY = "durationMs";
|
|
27
|
+
private static final String FALLBACK_REASON_KEY = "fallbackReason";
|
|
28
|
+
private static final String STATUS_KEY = "status";
|
|
29
|
+
private static final String TOTAL_DURATION_MS_KEY = "totalDurationMs";
|
|
30
|
+
|
|
31
|
+
private static final String STATUS_APPLIED = "applied";
|
|
32
|
+
private static final String STATUS_FALLBACK = "fallback";
|
|
33
|
+
|
|
34
|
+
/** One archive being tried, timed from the moment it was started. */
|
|
35
|
+
private static class Attempt {
|
|
36
|
+
final String archive;
|
|
37
|
+
final long startTimeMs = System.currentTimeMillis();
|
|
38
|
+
|
|
39
|
+
/** How long the applier took, once it has restored the bundle. */
|
|
40
|
+
long applyDurationMs = -1;
|
|
41
|
+
|
|
42
|
+
boolean fellBack;
|
|
43
|
+
String fallbackReason;
|
|
44
|
+
long durationMs = -1;
|
|
45
|
+
|
|
46
|
+
/** Whether the server answered this archive's URL with a status instead of the archive. */
|
|
47
|
+
boolean wasNotServed;
|
|
48
|
+
|
|
49
|
+
Attempt(String archive) {
|
|
50
|
+
this.archive = archive;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private final List<Attempt> mAttempts = new ArrayList<>();
|
|
55
|
+
|
|
56
|
+
/** When the first archive was started, which is where a fallback's total time runs from. */
|
|
57
|
+
private long mFirstAttemptStartTimeMs = -1;
|
|
58
|
+
private long mTotalDurationMs = -1;
|
|
59
|
+
|
|
60
|
+
/** Starts trying one archive. Everything recorded next is about this archive. */
|
|
61
|
+
void beginAttempt(String archive) {
|
|
62
|
+
if (mFirstAttemptStartTimeMs < 0) {
|
|
63
|
+
mFirstAttemptStartTimeMs = System.currentTimeMillis();
|
|
64
|
+
}
|
|
65
|
+
mAttempts.add(new Attempt(archive));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The applier restored the bundle. Whether the update installs is decided by the checks
|
|
70
|
+
* that follow, so this is not yet the attempt succeeding.
|
|
71
|
+
*/
|
|
72
|
+
void recordBundleRestored(long applyDurationMs) {
|
|
73
|
+
current().applyDurationMs = applyDurationMs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether the current archive's attempt got as far as restoring the bundle. A failure
|
|
78
|
+
* after that point is on the asset side of the archive, which the archives do not share -
|
|
79
|
+
* unlike the bundle patch, which they carry byte for byte.
|
|
80
|
+
*/
|
|
81
|
+
boolean currentAttemptRestoredBundle() {
|
|
82
|
+
return current().applyDurationMs >= 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether the current archive never arrived, because the server answered its URL with a
|
|
87
|
+
* status rather than with the archive.
|
|
88
|
+
*
|
|
89
|
+
* A verdict on one URL, and the archives are at URLs of their own: a release whose diff
|
|
90
|
+
* has been cleaned up still has its patch archive. This is the one failure before the
|
|
91
|
+
* bundle is restored that says nothing about the archives left to try.
|
|
92
|
+
*/
|
|
93
|
+
boolean currentAttemptWasNotServed() {
|
|
94
|
+
return current().wasNotServed;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The attempt at the current archive ended in one of the reasons the appliers report.
|
|
99
|
+
*
|
|
100
|
+
* The attempt is timed here rather than when it is read, because what happens next is
|
|
101
|
+
* another archive being downloaded and that is not time this attempt spent.
|
|
102
|
+
*/
|
|
103
|
+
void recordFallback(String failureReason) {
|
|
104
|
+
Attempt attempt = current();
|
|
105
|
+
attempt.fellBack = true;
|
|
106
|
+
attempt.fallbackReason = failureReason;
|
|
107
|
+
long now = System.currentTimeMillis();
|
|
108
|
+
attempt.durationMs = now - attempt.startTimeMs;
|
|
109
|
+
mTotalDurationMs = now - mFirstAttemptStartTimeMs;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The attempt ended in an error rather than in a verdict of its own.
|
|
114
|
+
*
|
|
115
|
+
* An error raised after the bundle was restored is the restored update failing the checks
|
|
116
|
+
* that every update passes before it is installed. Before that point the appliers have no
|
|
117
|
+
* word for what happened - the archive not being downloadable, say - and inventing one
|
|
118
|
+
* here would put a value on the wire that no platform reports, so the fallback is
|
|
119
|
+
* reported without a reason.
|
|
120
|
+
*/
|
|
121
|
+
void recordFallbackAfterError(Throwable error) {
|
|
122
|
+
current().wasNotServed = CodePushErrorCode.HTTP.equals(CodePushErrorCode.of(error));
|
|
123
|
+
recordFallback(currentAttemptRestoredBundle() ? ArchiveRestoreResult.REASON_PACKAGE_VERIFICATION_FAILED : null);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The archive being tried, for the log lines that name which one failed. */
|
|
127
|
+
String currentArchive() {
|
|
128
|
+
return current().archive;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private Attempt current() {
|
|
132
|
+
return mAttempts.get(mAttempts.size() - 1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* What the attempts ended in, as the app reads it.
|
|
137
|
+
*
|
|
138
|
+
* The result times the whole patch path: from the first archive starting to download to
|
|
139
|
+
* the last attempt being finished with, whichever way it ended. The attempts array retells
|
|
140
|
+
* the archives one by one, each timed over its own try and carrying the time its applier
|
|
141
|
+
* took when it got as far as restoring the bundle.
|
|
142
|
+
*/
|
|
143
|
+
JSONObject result() {
|
|
144
|
+
Attempt last = current();
|
|
145
|
+
boolean applied = !last.fellBack;
|
|
146
|
+
long now = System.currentTimeMillis();
|
|
147
|
+
|
|
148
|
+
JSONObject result = new JSONObject();
|
|
149
|
+
CodePushUtils.setJSONValueForKey(result, STATUS_KEY, applied ? STATUS_APPLIED : STATUS_FALLBACK);
|
|
150
|
+
CodePushUtils.setJSONValueForKey(result, ARCHIVE_KEY, last.archive);
|
|
151
|
+
if (last.fallbackReason != null) {
|
|
152
|
+
CodePushUtils.setJSONValueForKey(result, FALLBACK_REASON_KEY, last.fallbackReason);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// A path that was given up on was timed when it was, before the full download it
|
|
156
|
+
// must not include. One that installed is timed here, because nothing else marks
|
|
157
|
+
// the moment it finished - and mTotalDurationMs cannot stand in for it, because an
|
|
158
|
+
// earlier attempt that fell back froze it partway through the path.
|
|
159
|
+
CodePushUtils.setJSONValueForKey(result, TOTAL_DURATION_MS_KEY,
|
|
160
|
+
applied ? now - mFirstAttemptStartTimeMs : mTotalDurationMs);
|
|
161
|
+
|
|
162
|
+
JSONArray attempts = new JSONArray();
|
|
163
|
+
for (Attempt attempt : mAttempts) {
|
|
164
|
+
JSONObject entry = new JSONObject();
|
|
165
|
+
CodePushUtils.setJSONValueForKey(entry, ARCHIVE_KEY, attempt.archive);
|
|
166
|
+
if (attempt.fallbackReason != null) {
|
|
167
|
+
CodePushUtils.setJSONValueForKey(entry, FALLBACK_REASON_KEY, attempt.fallbackReason);
|
|
168
|
+
}
|
|
169
|
+
CodePushUtils.setJSONValueForKey(entry, DURATION_MS_KEY,
|
|
170
|
+
attempt.fellBack ? attempt.durationMs : now - attempt.startTimeMs);
|
|
171
|
+
// An attempt that never restored the bundle has no apply to report.
|
|
172
|
+
if (attempt.applyDurationMs >= 0) {
|
|
173
|
+
CodePushUtils.setJSONValueForKey(entry, APPLY_DURATION_MS_KEY, attempt.applyDurationMs);
|
|
174
|
+
}
|
|
175
|
+
attempts.put(entry);
|
|
176
|
+
}
|
|
177
|
+
CodePushUtils.setJSONValueForKey(result, ATTEMPTS_KEY, attempts);
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
package com.microsoft.codepush.react;
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Outcome of restoring an update from its
|
|
4
|
+
* Outcome of restoring an update from one of its update archives.
|
|
5
5
|
*
|
|
6
6
|
* A failed result is not an error the user hears about: it is the signal to download the
|
|
7
7
|
* update's full archive instead. The reason strings are the vocabulary the appliers of
|
|
8
8
|
* every platform report, and the logs a rollout is judged from are read for exactly these
|
|
9
9
|
* words, so they must not be reworded.
|
|
10
10
|
*/
|
|
11
|
-
public class
|
|
11
|
+
public class ArchiveRestoreResult {
|
|
12
12
|
|
|
13
13
|
/** The bundle inside the app binary could not be opened or read. */
|
|
14
14
|
public static final String REASON_BASE_BUNDLE_UNAVAILABLE = "base_bundle_unavailable";
|
|
@@ -28,6 +28,16 @@ public class BinaryPatchResult {
|
|
|
28
28
|
/** The restored bundle is not the one the manifest promised. */
|
|
29
29
|
public static final String REASON_TARGET_VERIFICATION_FAILED = "target_verification_failed";
|
|
30
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
|
+
public static final String REASON_ASSET_MERGE_FAILED = "asset_merge_failed";
|
|
40
|
+
|
|
31
41
|
/**
|
|
32
42
|
* The update restored from the patch did not pass the checks every update passes before
|
|
33
43
|
* it is installed - the folder hash above all.
|
|
@@ -40,17 +50,17 @@ public class BinaryPatchResult {
|
|
|
40
50
|
private final boolean mSucceeded;
|
|
41
51
|
private final String mFailureReason;
|
|
42
52
|
|
|
43
|
-
private
|
|
53
|
+
private ArchiveRestoreResult(boolean succeeded, String failureReason) {
|
|
44
54
|
mSucceeded = succeeded;
|
|
45
55
|
mFailureReason = failureReason;
|
|
46
56
|
}
|
|
47
57
|
|
|
48
|
-
public static
|
|
49
|
-
return new
|
|
58
|
+
public static ArchiveRestoreResult success() {
|
|
59
|
+
return new ArchiveRestoreResult(true, null);
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
public static
|
|
53
|
-
return new
|
|
62
|
+
public static ArchiveRestoreResult failure(String failureReason) {
|
|
63
|
+
return new ArchiveRestoreResult(false, failureReason);
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
public boolean succeeded() {
|
|
@@ -26,7 +26,7 @@ import java.io.OutputStream;
|
|
|
26
26
|
* hashed before the patch is applied and the restored bundle is hashed afterwards, and
|
|
27
27
|
* the restored bytes only reach the update contents once both checks have passed.
|
|
28
28
|
*
|
|
29
|
-
* Every failure is reported as a {@link
|
|
29
|
+
* Every failure is reported as a {@link ArchiveRestoreResult}, never as an exception: the
|
|
30
30
|
* caller answers all of them the same way, by downloading the full archive instead.
|
|
31
31
|
*/
|
|
32
32
|
public class CodePushBinaryPatch {
|
|
@@ -74,11 +74,11 @@ public class CodePushBinaryPatch {
|
|
|
74
74
|
* and after the attempt so an interrupted run leaves nothing
|
|
75
75
|
* @param baseBundleFileName name of the JS bundle inside the app binary
|
|
76
76
|
*/
|
|
77
|
-
public
|
|
77
|
+
public ArchiveRestoreResult restoreBundle(String unzippedFolderPath, String workingFolderPath, String baseBundleFileName) {
|
|
78
78
|
File contentsFolder = resolveContentsFolder(new File(unzippedFolderPath));
|
|
79
79
|
File manifestFile = new File(contentsFolder, CodePushConstants.BINARY_PATCH_MANIFEST_FILE_NAME);
|
|
80
80
|
if (!manifestFile.isFile()) {
|
|
81
|
-
return
|
|
81
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_INVALID_MANIFEST);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
JSONObject manifest;
|
|
@@ -86,12 +86,12 @@ public class CodePushBinaryPatch {
|
|
|
86
86
|
manifest = CodePushUtils.getJsonObjectFromFile(manifestFile.getAbsolutePath());
|
|
87
87
|
} catch (IOException | CodePushMalformedDataException e) {
|
|
88
88
|
CodePushUtils.log(e);
|
|
89
|
-
return
|
|
89
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_INVALID_MANIFEST);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
if (manifest.optInt(CodePushConstants.BINARY_PATCH_FORMAT_VERSION_KEY, -1) != CodePushConstants.BINARY_PATCH_FORMAT_VERSION
|
|
93
93
|
|| !CodePushConstants.BINARY_PATCH_ALGORITHM.equals(manifest.optString(CodePushConstants.BINARY_PATCH_ALGORITHM_KEY, null))) {
|
|
94
|
-
return
|
|
94
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_UNSUPPORTED_FORMAT);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
File targetBundleFile = resolveInsideFolder(contentsFolder, manifest.optString(CodePushConstants.BINARY_PATCH_BUNDLE_PATH_KEY, null));
|
|
@@ -102,7 +102,7 @@ public class CodePushBinaryPatch {
|
|
|
102
102
|
if (targetBundleFile == null || patchFile == null || !patchFile.isFile()
|
|
103
103
|
|| isNullOrEmpty(baseBundleHash) || isNullOrEmpty(targetBundleHash)
|
|
104
104
|
|| targetBundleSize <= 0 || targetBundleSize > CodePushConstants.BINARY_PATCH_MAX_TARGET_BUNDLE_SIZE) {
|
|
105
|
-
return
|
|
105
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_INVALID_MANIFEST);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
// An earlier attempt that was killed while patching leaves its restored bundle behind.
|
|
@@ -110,13 +110,13 @@ public class CodePushBinaryPatch {
|
|
|
110
110
|
File workingFolder = new File(workingFolderPath);
|
|
111
111
|
if (!workingFolder.mkdirs()) {
|
|
112
112
|
CodePushUtils.log("Unable to create the binary patch working directory at " + workingFolderPath);
|
|
113
|
-
return
|
|
113
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_PATCH_APPLY_FAILED);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
try {
|
|
117
117
|
if (workingFolder.getUsableSpace() < targetBundleSize) {
|
|
118
118
|
CodePushUtils.log("Not enough free space to restore a " + targetBundleSize + " byte bundle.");
|
|
119
|
-
return
|
|
119
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_PATCH_APPLY_FAILED);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
byte[] baseBundle;
|
|
@@ -124,13 +124,13 @@ public class CodePushBinaryPatch {
|
|
|
124
124
|
baseBundle = mBaseBundleProvider.readBaseBundle(baseBundleFileName);
|
|
125
125
|
} catch (Exception e) {
|
|
126
126
|
CodePushUtils.log(e);
|
|
127
|
-
return
|
|
127
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_BASE_BUNDLE_UNAVAILABLE);
|
|
128
128
|
}
|
|
129
129
|
if (baseBundle == null) {
|
|
130
|
-
return
|
|
130
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_BASE_BUNDLE_UNAVAILABLE);
|
|
131
131
|
}
|
|
132
132
|
if (!baseBundleHash.equals(CodePushUpdateUtils.computeHashForBytes(baseBundle))) {
|
|
133
|
-
return
|
|
133
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_BASE_HASH_MISMATCH);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
byte[] patch;
|
|
@@ -138,38 +138,38 @@ public class CodePushBinaryPatch {
|
|
|
138
138
|
patch = readFile(patchFile);
|
|
139
139
|
} catch (IOException e) {
|
|
140
140
|
CodePushUtils.log(e);
|
|
141
|
-
return
|
|
141
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_PATCH_APPLY_FAILED);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
File restoredBundleFile = new File(workingFolder, CodePushConstants.BINARY_PATCH_TARGET_FILE_NAME);
|
|
145
145
|
int resultCode = mPatchApplier.apply(baseBundle, patch, restoredBundleFile.getAbsolutePath(), targetBundleSize);
|
|
146
146
|
if (resultCode != PatchApplier.RESULT_OK) {
|
|
147
147
|
CodePushUtils.log("The binary patch applier returned " + resultCode + ".");
|
|
148
|
-
return
|
|
148
|
+
return ArchiveRestoreResult.failure(reasonForResultCode(resultCode));
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
if (restoredBundleFile.length() != targetBundleSize) {
|
|
152
|
-
return
|
|
152
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_TARGET_VERIFICATION_FAILED);
|
|
153
153
|
}
|
|
154
154
|
String restoredBundleHash;
|
|
155
155
|
try {
|
|
156
156
|
restoredBundleHash = CodePushUpdateUtils.computeHashForFile(restoredBundleFile);
|
|
157
157
|
} catch (IOException e) {
|
|
158
158
|
CodePushUtils.log(e);
|
|
159
|
-
return
|
|
159
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_TARGET_VERIFICATION_FAILED);
|
|
160
160
|
}
|
|
161
161
|
if (!targetBundleHash.equals(restoredBundleHash)) {
|
|
162
|
-
return
|
|
162
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_TARGET_VERIFICATION_FAILED);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
if (!moveFile(restoredBundleFile, targetBundleFile) || !patchFile.delete() || !manifestFile.delete()) {
|
|
166
166
|
// The contents are half restored, so they must not be installed. The full
|
|
167
167
|
// archive that follows unzips over them, which is what clears them.
|
|
168
168
|
CodePushUtils.log("Unable to put the restored bundle in place of the patch.");
|
|
169
|
-
return
|
|
169
|
+
return ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_PATCH_APPLY_FAILED);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
return
|
|
172
|
+
return ArchiveRestoreResult.success();
|
|
173
173
|
} finally {
|
|
174
174
|
FileUtils.deleteDirectoryAtPath(workingFolderPath);
|
|
175
175
|
}
|
|
@@ -234,8 +234,8 @@ public class CodePushBinaryPatch {
|
|
|
234
234
|
|
|
235
235
|
private static String reasonForResultCode(int resultCode) {
|
|
236
236
|
return resultCode == PatchApplier.RESULT_UNSUPPORTED_COMPRESSION
|
|
237
|
-
?
|
|
238
|
-
:
|
|
237
|
+
? ArchiveRestoreResult.REASON_UNSUPPORTED_FORMAT
|
|
238
|
+
: ArchiveRestoreResult.REASON_PATCH_APPLY_FAILED;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
/** Rename, falling back to a copy for the case where the two paths are on different volumes. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.microsoft.codepush.react;
|
|
2
2
|
|
|
3
3
|
public class CodePushConstants {
|
|
4
|
+
public static final String ASSET_DIFF_DOWNLOAD_URL_KEY = "assetDiffDownloadUrl";
|
|
4
5
|
public static final String ASSETS_BUNDLE_PREFIX = "assets://";
|
|
5
6
|
// The binary patch values below are the client half of the contract the CLI writes
|
|
6
7
|
// into a patch archive. The CLI is TypeScript, so its constants cannot be imported
|
|
@@ -23,7 +24,7 @@ public class CodePushConstants {
|
|
|
23
24
|
public static final long BINARY_PATCH_MAX_TARGET_BUNDLE_SIZE = 128L * 1024 * 1024;
|
|
24
25
|
// Not part of the CLI's contract: the field a download that tried a patch reports its
|
|
25
26
|
// outcome under, on the package it resolves with and on no package it stores.
|
|
26
|
-
public static final String
|
|
27
|
+
public static final String UPDATE_ARCHIVE_RESULT_KEY = "updateArchiveResult";
|
|
27
28
|
public static final String BINARY_PATCH_TARGET_BUNDLE_HASH_KEY = "targetBundleHash";
|
|
28
29
|
public static final String BINARY_PATCH_TARGET_BUNDLE_SIZE_KEY = "targetBundleSize";
|
|
29
30
|
public static final String BINARY_PATCH_TARGET_FILE_NAME = "target.bundle";
|
|
@@ -35,6 +36,24 @@ public class CodePushConstants {
|
|
|
35
36
|
public static final String DEFAULT_JS_BUNDLE_NAME = "index.android.bundle";
|
|
36
37
|
public static final String DIFF_MANIFEST_FILE_NAME = "hotcodepush.json";
|
|
37
38
|
public static final int DOWNLOAD_BUFFER_SIZE = 1024 * 256;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* How long a download waits for the server to answer at all.
|
|
42
|
+
*
|
|
43
|
+
* Kept well above any real handshake, because the only thing this has to catch is a
|
|
44
|
+
* host that will never answer - a captive portal, a dead route, a network that went
|
|
45
|
+
* away between the update check and the download.
|
|
46
|
+
*/
|
|
47
|
+
public static final int DOWNLOAD_CONNECT_TIMEOUT_IN_MS = 10 * 1000;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* How long a download waits for the next bytes of a response already flowing.
|
|
51
|
+
*
|
|
52
|
+
* This bounds silence, not the download: a slow connection that keeps delivering is
|
|
53
|
+
* never cut off, however long the whole archive takes. A connection that delivers
|
|
54
|
+
* nothing for this long is one the operating system has stopped reporting as dead.
|
|
55
|
+
*/
|
|
56
|
+
public static final int DOWNLOAD_READ_TIMEOUT_IN_MS = 30 * 1000;
|
|
38
57
|
public static final String DOWNLOAD_FILE_NAME = "download.zip";
|
|
39
58
|
public static final String DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress";
|
|
40
59
|
public static final String DOWNLOAD_URL_KEY = "downloadUrl";
|