react-native-update 10.55.1 → 10.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-CN.md +35 -0
- package/README.md +37 -0
- package/android/build.gradle +53 -27
- package/android/jni/Application.mk +6 -1
- package/android/jni/hpatch.c +59 -4
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/proguard.pro +70 -14
- package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +15 -8
- package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +1 -0
- package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +1 -2
- package/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +25 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -34
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +29 -0
- package/android/src/main/java/cn/reactnative/modules/update/FileOperationException.java +15 -0
- package/android/src/main/java/cn/reactnative/modules/update/HttpUtils.java +59 -0
- package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +27 -1
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +57 -16
- package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +38 -46
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +279 -183
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +14 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +222 -215
- package/android/src/main/java/cn/reactnative/modules/update/UpdatePackage.java +5 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +23 -25
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +29 -29
- package/cpp/patch_core/archive_limits.h +6 -0
- package/cpp/patch_core/jni_util.h +6 -2
- package/cpp/patch_core/patch_core.cpp +237 -84
- package/cpp/patch_core/patch_core.h +11 -0
- package/cpp/patch_core/patch_core_android.cpp +49 -33
- package/cpp/patch_core/update_core_android.cpp +134 -103
- package/cpp/update_flow_core/flow_json.cpp +150 -17
- package/cpp/update_flow_core/flow_json.h +13 -1
- package/cpp/update_flow_core/update_flow_core.cpp +78 -8
- package/cpp/update_flow_core/update_flow_core.h +9 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +44 -25
- package/harmony/hvigor-plugin.ts +168 -21
- package/harmony/pushy/src/README.md +74 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +13 -3
- package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +11 -5
- package/harmony/pushy/src/main/cpp/pushy.cpp +103 -18
- package/harmony/pushy/src/main/ets/ArchiveLimits.ts +61 -5
- package/harmony/pushy/src/main/ets/DownloadTask.ts +255 -166
- package/harmony/pushy/src/main/ets/ErrorCodes.ts +75 -0
- package/harmony/pushy/src/main/ets/EventHub.ts +22 -20
- package/harmony/pushy/src/main/ets/InstallRecord.ts +79 -21
- package/harmony/pushy/src/main/ets/Logger.ts +42 -16
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +98 -17
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +10 -3
- package/harmony/pushy/src/main/ets/PathUtils.ts +9 -3
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +139 -53
- package/harmony/pushy/src/main/ets/UpdateContext.ts +269 -159
- package/harmony/pushy/src/main/module.json5 +5 -1
- package/harmony/pushy.har +0 -0
- package/ios/PrivacyInfo.xcprivacy +79 -0
- package/ios/RCTPushy/RCTPushy.mm +399 -170
- package/ios/RCTPushy/RCTPushyDownloader.h +14 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +136 -11
- package/package.json +14 -24
- package/react-native-update.podspec +36 -3
- package/src/client.ts +236 -82
- package/src/context.ts +40 -27
- package/src/core.ts +41 -16
- package/src/endpoint.ts +2 -1
- package/src/error.ts +49 -11
- package/src/index.ts +14 -0
- package/src/locales/en.ts +1 -23
- package/src/locales/zh.ts +1 -22
- package/src/provider.tsx +159 -111
- package/src/type.ts +0 -2
- package/src/updateFlowCore.ts +10 -2
- package/src/utils.ts +61 -4
- package/android/src/main/java/cn/reactnative/modules/update/ReactNativeHostHandler.java +0 -13
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +0 -45
package/ios/RCTPushy/RCTPushy.mm
CHANGED
|
@@ -30,13 +30,24 @@
|
|
|
30
30
|
|
|
31
31
|
#import <React/RCTConvert.h>
|
|
32
32
|
#import <React/RCTLog.h>
|
|
33
|
+
#import <objc/runtime.h>
|
|
33
34
|
#import <os/lock.h>
|
|
34
35
|
|
|
35
36
|
#include <atomic>
|
|
37
|
+
#include <sys/stat.h>
|
|
36
38
|
|
|
37
39
|
static NSString *const keyPushyInfo = @"REACTNATIVECN_PUSHY_INFO_KEY";
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
// Binary identity (SyncBinaryVersion input). Prefixed like every other key:
|
|
41
|
+
// the unprefixed names collided with generic host-app/SDK defaults, and a
|
|
42
|
+
// foreign value there made every launch look like a rebuilt binary and wipe
|
|
43
|
+
// the installed update.
|
|
44
|
+
static NSString *const paramPackageVersion = @"REACTNATIVECN_PUSHY_PACKAGEVERSION";
|
|
45
|
+
static NSString *const paramBuildTime = @"REACTNATIVECN_PUSHY_BUILDTIME";
|
|
46
|
+
// The unprefixed names earlier SDKs wrote. Read only as a fallback until the
|
|
47
|
+
// first state write migrates them (PushyStateFromDefaults /
|
|
48
|
+
// PushyApplyStateToDefaults).
|
|
49
|
+
static NSString *const legacyParamPackageVersion = @"packageVersion";
|
|
50
|
+
static NSString *const legacyParamBuildTime = @"buildTime";
|
|
40
51
|
static NSString *const paramLastVersion = @"lastVersion";
|
|
41
52
|
static NSString *const paramCurrentVersion = @"currentVersion";
|
|
42
53
|
static NSString *const paramIsFirstTime = @"isFirstTime";
|
|
@@ -175,46 +186,32 @@ static NSMutableDictionary<NSString *, NSString *> *PushyArtifactDigests(void) {
|
|
|
175
186
|
|
|
176
187
|
static NSError *PushyErrorWithCode(const char *code, NSString *message);
|
|
177
188
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Free space on the volume holding `path` (walks up to an existing ancestor);
|
|
185
|
-
// -1 when unknown.
|
|
186
|
-
static long long PushyFreeDiskSpaceForPath(NSString *path) {
|
|
187
|
-
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
188
|
-
NSString *probe = path;
|
|
189
|
-
while (probe.length > 1 && ![fileManager fileExistsAtPath:probe]) {
|
|
190
|
-
probe = [probe stringByDeletingLastPathComponent];
|
|
191
|
-
}
|
|
192
|
-
NSDictionary *attributes = [fileManager attributesOfFileSystemForPath:probe error:nil];
|
|
193
|
-
NSNumber *free = attributes[NSFileSystemFreeSize];
|
|
194
|
-
return free == nil ? -1 : free.longLongValue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// nil when the volume can take `bytesToWrite` plus the safety margin (or the
|
|
198
|
-
// free space is unknown); an error otherwise.
|
|
189
|
+
// nil when the volume holding `path` can take `bytesToWrite` plus the safety
|
|
190
|
+
// margin (or the free space is unknown); a PATCH_FAILED error otherwise. The
|
|
191
|
+
// probe is the downloader's (RCTPushyFreeSpaceShortfall), which applies the
|
|
192
|
+
// same rule to the announced Content-Length before the first byte lands.
|
|
199
193
|
static NSError *PushyEnsureFreeSpace(NSString *path, long long bytesToWrite) {
|
|
200
|
-
|
|
201
|
-
if (
|
|
194
|
+
NSString *shortfall = RCTPushyFreeSpaceShortfall(path, bytesToWrite);
|
|
195
|
+
if (shortfall == nil) {
|
|
202
196
|
return nil;
|
|
203
197
|
}
|
|
204
|
-
|
|
205
|
-
if (free < needed) {
|
|
206
|
-
return PushyErrorWithCode(
|
|
207
|
-
pushy::error_codes::kPatchFailed,
|
|
208
|
-
[NSString stringWithFormat:@"insufficient disk space: need %lld bytes, have %lld",
|
|
209
|
-
needed, free]);
|
|
210
|
-
}
|
|
211
|
-
return nil;
|
|
198
|
+
return PushyErrorWithCode(pushy::error_codes::kPatchFailed, shortfall);
|
|
212
199
|
}
|
|
213
200
|
|
|
214
201
|
// SSZipArchive delegate enforcing cpp/patch_core/archive_limits.h while the
|
|
215
|
-
// archive is walked
|
|
216
|
-
// ratio
|
|
217
|
-
//
|
|
202
|
+
// archive is walked (entry count, per-entry size, total size, compression
|
|
203
|
+
// ratio) plus two entry-shape rules Android's java.util.zip gets for free:
|
|
204
|
+
// - no symlinks: SSZipArchive materialises them with any target regardless
|
|
205
|
+
// of preserveAttributes, so `assets -> ../../Documents` followed by
|
|
206
|
+
// `assets/x` would write outside the version directory;
|
|
207
|
+
// - no ".pushy-" reserved names at any depth: the completion marker is
|
|
208
|
+
// written by this SDK only, after the whole install succeeded. An archive
|
|
209
|
+
// shipping its own would be trusted as a finished install even if the
|
|
210
|
+
// patch step that follows fails.
|
|
211
|
+
// A NO from zipArchiveShouldUnzipFileAtIndex: cancels the whole unzip (the
|
|
212
|
+
// 2.x contract the podspec pins), and the completion handler turns the
|
|
213
|
+
// recorded violation into a hard PATCH_FAILED; the caller then discards the
|
|
214
|
+
// staging directory.
|
|
218
215
|
@interface PushyUnzipGuard : NSObject <SSZipArchiveDelegate>
|
|
219
216
|
@property (nonatomic, copy) NSString *violation;
|
|
220
217
|
@property (nonatomic, assign) long long totalUncompressed;
|
|
@@ -236,6 +233,13 @@ static NSError *PushyEnsureFreeSpace(NSString *path, long long bytesToWrite) {
|
|
|
236
233
|
if (self.violation != nil) {
|
|
237
234
|
return NO;
|
|
238
235
|
}
|
|
236
|
+
// Unix mode bits travel in the high 16 bits of the external attributes
|
|
237
|
+
// (the same field SSZipArchive keys its own symlink branch on).
|
|
238
|
+
if (((fileInfo.external_fa >> 16) & S_IFMT) == S_IFLNK) {
|
|
239
|
+
self.violation = [NSString stringWithFormat:@"archive entry #%ld is a symlink",
|
|
240
|
+
(long)fileIndex];
|
|
241
|
+
return NO;
|
|
242
|
+
}
|
|
239
243
|
long long size = (long long)fileInfo.uncompressed_size;
|
|
240
244
|
long long compressed = (long long)fileInfo.compressed_size;
|
|
241
245
|
if (size > pushy::archive_limits::kMaxEntryBytes) {
|
|
@@ -258,6 +262,21 @@ static NSError *PushyEnsureFreeSpace(NSString *path, long long bytesToWrite) {
|
|
|
258
262
|
return YES;
|
|
259
263
|
}
|
|
260
264
|
|
|
265
|
+
// The entry name is only exposed once the entry is on disk (shouldUnzip
|
|
266
|
+
// runs before the name is read). A reserved name is flagged here; the next
|
|
267
|
+
// shouldUnzip refuses (cancelling the unzip) or, for the last entry, the
|
|
268
|
+
// completion handler picks the violation up. Either way the caller drops
|
|
269
|
+
// the whole staging directory, so the written file never survives.
|
|
270
|
+
- (void)zipArchiveDidUnzipFileAtIndex:(NSInteger)fileIndex
|
|
271
|
+
totalFiles:(NSInteger)totalFiles
|
|
272
|
+
archivePath:(NSString *)archivePath
|
|
273
|
+
unzippedFilePath:(NSString *)unzippedFilePath {
|
|
274
|
+
NSString *name = unzippedFilePath.lastPathComponent;
|
|
275
|
+
if (self.violation == nil && [name hasPrefix:@".pushy-"]) {
|
|
276
|
+
self.violation = [NSString stringWithFormat:@"archive contains reserved entry %@", name];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
261
280
|
@end
|
|
262
281
|
|
|
263
282
|
static NSError *PushyDownloadDeadlineExpiredError(void) {
|
|
@@ -274,10 +293,13 @@ typedef NS_ENUM(NSInteger, PushyType) {
|
|
|
274
293
|
PushyTypeFullDownload = 1,
|
|
275
294
|
PushyTypePatchFromPackage = 2,
|
|
276
295
|
PushyTypePatchFromPpk = 3,
|
|
277
|
-
//TASK_TYPE_PLAIN_DOWNLOAD=4?
|
|
278
296
|
};
|
|
279
297
|
|
|
280
298
|
static std::atomic<bool> ignoreRollback{false};
|
|
299
|
+
// Whether the host asked +bundleURL for the bundle to load (the
|
|
300
|
+
// isUsingBundleUrl constant, mirroring Android/Harmony). NO means the app
|
|
301
|
+
// runs a bundle this SDK never resolved, so updates cannot take effect.
|
|
302
|
+
static std::atomic<bool> pushyIsUsingBundleUrl{false};
|
|
281
303
|
// Bumped by resetToPackagedBundle. The cold-start check runs for minutes and
|
|
282
304
|
// may already hold a decision when the app resets to the packaged bundle; it
|
|
283
305
|
// samples this counter and abandons activation (and its response cache) when
|
|
@@ -419,6 +441,11 @@ static void PushyFinishDownload(NSString *hash, NSError *error) {
|
|
|
419
441
|
static os_unfair_lock pushyStateLock = OS_UNFAIR_LOCK_INIT;
|
|
420
442
|
|
|
421
443
|
static void PushyWithStateLock(void (NS_NOESCAPE ^block)(void)) {
|
|
444
|
+
#if DEBUG
|
|
445
|
+
// Not re-entrant: a nested acquisition deadlocks in release, so catch it
|
|
446
|
+
// where it is written.
|
|
447
|
+
os_unfair_lock_assert_not_owner(&pushyStateLock);
|
|
448
|
+
#endif
|
|
422
449
|
os_unfair_lock_lock(&pushyStateLock);
|
|
423
450
|
@try {
|
|
424
451
|
block();
|
|
@@ -515,46 +542,87 @@ static NSError *PushyErrorWithCode(const char *code, NSString *message) {
|
|
|
515
542
|
}];
|
|
516
543
|
}
|
|
517
544
|
|
|
518
|
-
static
|
|
519
|
-
|
|
545
|
+
static BOOL PushyJsonIsAbsent(id value) {
|
|
546
|
+
return value == nil || [value isKindOfClass:[NSNull class]];
|
|
547
|
+
}
|
|
520
548
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
549
|
+
// Builds the patch-core manifest from a parsed __diff.json. Every field is
|
|
550
|
+
// type-checked: this runs inside a GCD block on _fileQueue, where the
|
|
551
|
+
// unrecognized-selector exception a `copies` array or a numeric `from` would
|
|
552
|
+
// raise takes the whole app down (and trips the crash rescue) instead of
|
|
553
|
+
// failing the patch. NO with `reason` set for a malformed manifest.
|
|
554
|
+
static BOOL PushyPatchManifestFromJson(NSDictionary *json,
|
|
555
|
+
pushy::patch::PatchManifest *manifest,
|
|
556
|
+
NSString **reason) {
|
|
557
|
+
id copies = json[@"copies"];
|
|
558
|
+
if (!PushyJsonIsAbsent(copies)) {
|
|
559
|
+
if (![copies isKindOfClass:[NSDictionary class]]) {
|
|
560
|
+
*reason = @"patch manifest: copies is not an object";
|
|
561
|
+
return NO;
|
|
534
562
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
563
|
+
// Content checksum per copy target ("copiesCrc", pdiff manifests
|
|
564
|
+
// from CLI >= 2.21.2). The patch core verifies the copy source
|
|
565
|
+
// against it and fails the patch on mismatch, so the JS strategy
|
|
566
|
+
// chain falls back to the full package instead of copying drifted
|
|
567
|
+
// bytes from a rebuilt binary.
|
|
568
|
+
NSDictionary *copiesCrc = json[@"copiesCrc"];
|
|
569
|
+
if (![copiesCrc isKindOfClass:[NSDictionary class]]) {
|
|
570
|
+
copiesCrc = nil;
|
|
571
|
+
}
|
|
572
|
+
for (id to in (NSDictionary *)copies) {
|
|
573
|
+
id from = ((NSDictionary *)copies)[to];
|
|
574
|
+
if (![to isKindOfClass:[NSString class]] || ![from isKindOfClass:[NSString class]]) {
|
|
575
|
+
*reason = @"patch manifest: copies entry is not a string";
|
|
576
|
+
return NO;
|
|
577
|
+
}
|
|
578
|
+
if ([from length] == 0) {
|
|
579
|
+
from = to;
|
|
580
|
+
}
|
|
581
|
+
pushy::patch::CopyOperation operation;
|
|
582
|
+
operation.from = PushyToStdString(from);
|
|
583
|
+
operation.to = PushyToStdString(to);
|
|
584
|
+
NSNumber *expectedCrc = copiesCrc[to];
|
|
585
|
+
if ([expectedCrc isKindOfClass:[NSNumber class]]) {
|
|
586
|
+
operation.has_expected_crc = true;
|
|
587
|
+
operation.expected_crc = (uint32_t)[expectedCrc unsignedLongLongValue];
|
|
588
|
+
}
|
|
589
|
+
manifest->copies.push_back(operation);
|
|
542
590
|
}
|
|
543
|
-
manifest.copies.push_back(operation);
|
|
544
591
|
}
|
|
545
592
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
593
|
+
// The CLI emits `deletes` as an object keyed by path (Android reads its
|
|
594
|
+
// keys); a plain array of paths is accepted as well, like Harmony.
|
|
595
|
+
id deletes = json[@"deletes"];
|
|
596
|
+
if (!PushyJsonIsAbsent(deletes)) {
|
|
597
|
+
if (![deletes isKindOfClass:[NSDictionary class]] && ![deletes isKindOfClass:[NSArray class]]) {
|
|
598
|
+
*reason = @"patch manifest: deletes is not an object or array";
|
|
599
|
+
return NO;
|
|
600
|
+
}
|
|
601
|
+
for (id path in deletes) {
|
|
602
|
+
if (![path isKindOfClass:[NSString class]]) {
|
|
603
|
+
*reason = @"patch manifest: deletes entry is not a string";
|
|
604
|
+
return NO;
|
|
605
|
+
}
|
|
606
|
+
manifest->deletes.push_back(PushyToStdString(path));
|
|
607
|
+
}
|
|
549
608
|
}
|
|
609
|
+
return YES;
|
|
610
|
+
}
|
|
550
611
|
|
|
551
|
-
|
|
612
|
+
// Binary identity under the prefixed key, or — until the first state write
|
|
613
|
+
// migrates it — the value an earlier SDK left under the unprefixed one, so an
|
|
614
|
+
// installed update survives the SDK upgrade instead of reading as a rebuilt
|
|
615
|
+
// binary.
|
|
616
|
+
static NSString *PushyBinaryIdentityValue(NSUserDefaults *defaults, NSString *key, NSString *legacyKey) {
|
|
617
|
+
return [defaults stringForKey:key] ?: [defaults stringForKey:legacyKey];
|
|
552
618
|
}
|
|
553
619
|
|
|
554
620
|
static pushy::state::State PushyStateFromDefaults(NSUserDefaults *defaults) {
|
|
555
621
|
pushy::state::State state;
|
|
556
|
-
state.package_version = PushyToStdString(
|
|
557
|
-
|
|
622
|
+
state.package_version = PushyToStdString(
|
|
623
|
+
PushyBinaryIdentityValue(defaults, paramPackageVersion, legacyParamPackageVersion));
|
|
624
|
+
state.build_time = PushyToStdString(
|
|
625
|
+
PushyBinaryIdentityValue(defaults, paramBuildTime, legacyParamBuildTime));
|
|
558
626
|
NSDictionary *pushyInfo = [defaults dictionaryForKey:keyPushyInfo];
|
|
559
627
|
if (pushyInfo != nil) {
|
|
560
628
|
state.current_version = PushyToStdString(pushyInfo[paramCurrentVersion]);
|
|
@@ -570,6 +638,10 @@ static pushy::state::State PushyStateFromDefaults(NSUserDefaults *defaults) {
|
|
|
570
638
|
static void PushyApplyStateToDefaults(NSUserDefaults *defaults, const pushy::state::State &state) {
|
|
571
639
|
PushySetNullableString(defaults, paramPackageVersion, PushyFromStdString(state.package_version));
|
|
572
640
|
PushySetNullableString(defaults, paramBuildTime, PushyFromStdString(state.build_time));
|
|
641
|
+
// One-time migration: once the prefixed keys hold the identity, the
|
|
642
|
+
// legacy ones are retired so a host-app write there is never read again.
|
|
643
|
+
[defaults removeObjectForKey:legacyParamPackageVersion];
|
|
644
|
+
[defaults removeObjectForKey:legacyParamBuildTime];
|
|
573
645
|
|
|
574
646
|
BOOL hasPushyInfo = !state.current_version.empty() || !state.last_version.empty() || state.first_time || !state.first_time_ok;
|
|
575
647
|
if (hasPushyInfo) {
|
|
@@ -616,7 +688,7 @@ static void PushySwitchVersionLocked(NSString *hash) {
|
|
|
616
688
|
- (void)performUpdate:(PushyType)type
|
|
617
689
|
options:(NSDictionary *)options
|
|
618
690
|
callback:(void (^)(NSError *error))callback;
|
|
619
|
-
- (
|
|
691
|
+
- (NSError *)reloadBridgeWithReason:(NSString *)reason;
|
|
620
692
|
- (void)unzipDownloadedPackage:(NSString *)zipFilePath
|
|
621
693
|
hash:(NSString *)hash
|
|
622
694
|
type:(PushyType)type
|
|
@@ -762,10 +834,34 @@ static void PushyInstallCrashRescueHandler(void) {
|
|
|
762
834
|
});
|
|
763
835
|
}
|
|
764
836
|
|
|
837
|
+
// Single-flight for the digest in PushyBundleHashSync: the delayed cold-start
|
|
838
|
+
// round (utility queue) and JS getBundleHash (_fileQueue) can both miss the
|
|
839
|
+
// cache right after an install; the second caller waits for the first and
|
|
840
|
+
// then reads its cache entry instead of hashing the same multi-MB file twice.
|
|
841
|
+
static NSLock *PushyBundleHashLock(void) {
|
|
842
|
+
static NSLock *lock;
|
|
843
|
+
static dispatch_once_t once;
|
|
844
|
+
dispatch_once(&once, ^{
|
|
845
|
+
lock = [NSLock new];
|
|
846
|
+
});
|
|
847
|
+
return lock;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
static NSString *PushyCachedBundleHash(NSUserDefaults *defaults, NSString *cachedPrefix) {
|
|
851
|
+
NSString *cached = [defaults stringForKey:keyBundleHashCache];
|
|
852
|
+
if ([cached hasPrefix:cachedPrefix]) {
|
|
853
|
+
return [cached substringFromIndex:cachedPrefix.length];
|
|
854
|
+
}
|
|
855
|
+
return nil;
|
|
856
|
+
}
|
|
857
|
+
|
|
765
858
|
// Shared by the getBundleHash RCT method and the native cold-start check.
|
|
766
|
-
// Returns @"" when unknown
|
|
767
|
-
//
|
|
768
|
-
|
|
859
|
+
// Returns @"" when unknown. With computeIfMissing the sha256 of the embedded
|
|
860
|
+
// bundle is taken on a cache miss (once per install; blocking — call off the
|
|
861
|
+
// main thread). Without it only the cache is consulted: a crash rescue on
|
|
862
|
+
// the main thread has a 3.5s budget in total, and hashing a large bundle on
|
|
863
|
+
// a fresh install would spend all of it before the first request.
|
|
864
|
+
static NSString *PushyBundleHashSync(BOOL computeIfMissing) {
|
|
769
865
|
NSString *path = [[RCTPushy binaryBundleURL] path];
|
|
770
866
|
if (path == nil) {
|
|
771
867
|
return @"";
|
|
@@ -781,19 +877,33 @@ static NSString *PushyBundleHashSync(void) {
|
|
|
781
877
|
[attributes.fileModificationDate timeIntervalSince1970]];
|
|
782
878
|
|
|
783
879
|
NSUserDefaults *defaults = PushyDefaults();
|
|
784
|
-
NSString *cached = [defaults stringForKey:keyBundleHashCache];
|
|
785
880
|
NSString *cachedPrefix = [cacheKey stringByAppendingString:@"|"];
|
|
786
|
-
|
|
787
|
-
|
|
881
|
+
NSString *cached = PushyCachedBundleHash(defaults, cachedPrefix);
|
|
882
|
+
if (cached != nil) {
|
|
883
|
+
return cached;
|
|
884
|
+
}
|
|
885
|
+
if (!computeIfMissing) {
|
|
886
|
+
return @"";
|
|
788
887
|
}
|
|
789
888
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
889
|
+
NSLock *lock = PushyBundleHashLock();
|
|
890
|
+
[lock lock];
|
|
891
|
+
@try {
|
|
892
|
+
// The previous holder may have filled the cache while we waited.
|
|
893
|
+
cached = PushyCachedBundleHash(defaults, cachedPrefix);
|
|
894
|
+
if (cached != nil) {
|
|
895
|
+
return cached;
|
|
896
|
+
}
|
|
897
|
+
NSString *hash = PushyFromStdString(
|
|
898
|
+
pushy::digest::Sha256File(PushyToStdString(path))) ?: @"";
|
|
899
|
+
if (hash.length > 0) {
|
|
900
|
+
[defaults setObject:[cachedPrefix stringByAppendingString:hash]
|
|
901
|
+
forKey:keyBundleHashCache];
|
|
902
|
+
}
|
|
903
|
+
return hash;
|
|
904
|
+
} @finally {
|
|
905
|
+
[lock unlock];
|
|
795
906
|
}
|
|
796
|
-
return hash;
|
|
797
907
|
}
|
|
798
908
|
|
|
799
909
|
@implementation RCTPushy {
|
|
@@ -805,6 +915,7 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
805
915
|
|
|
806
916
|
+ (NSURL *)bundleURL
|
|
807
917
|
{
|
|
918
|
+
pushyIsUsingBundleUrl = true;
|
|
808
919
|
__block NSURL *resolvedURL = nil;
|
|
809
920
|
__block NSString *launchRolledBackVersion = nil;
|
|
810
921
|
@try {
|
|
@@ -914,6 +1025,7 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
914
1025
|
ret[@"rolledBackVersion"] = [defaults objectForKey:keyRolledBackMarked];
|
|
915
1026
|
ret[@"isFirstTime"] = [defaults objectForKey:keyFirstLoadMarked];
|
|
916
1027
|
ret[@"uuid"] = [defaults objectForKey:keyUuid];
|
|
1028
|
+
ret[@"isUsingBundleUrl"] = @(pushyIsUsingBundleUrl.load());
|
|
917
1029
|
// 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道),
|
|
918
1030
|
// JS 随 checkUpdate 以 diffV 上报,服务端按能力门控下发
|
|
919
1031
|
ret[@"supportedDiffVersion"] = @(pushy::hbc::kSupportedDiffVersion);
|
|
@@ -1112,14 +1224,22 @@ RCT_EXPORT_METHOD(reloadUpdate:(NSDictionary *)options
|
|
|
1112
1224
|
return;
|
|
1113
1225
|
}
|
|
1114
1226
|
|
|
1115
|
-
[self reloadBridgeWithReason:@"pushy reloadUpdate"];
|
|
1227
|
+
NSError *reloadError = [self reloadBridgeWithReason:@"pushy reloadUpdate"];
|
|
1228
|
+
if (reloadError != nil) {
|
|
1229
|
+
PushyRejectError(reject, reloadError);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1116
1232
|
resolve(@true);
|
|
1117
1233
|
}
|
|
1118
1234
|
|
|
1119
1235
|
RCT_EXPORT_METHOD(restartApp:(RCTPromiseResolveBlock)resolve
|
|
1120
1236
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
1121
1237
|
{
|
|
1122
|
-
[self reloadBridgeWithReason:@"pushy restartApp"];
|
|
1238
|
+
NSError *reloadError = [self reloadBridgeWithReason:@"pushy restartApp"];
|
|
1239
|
+
if (reloadError != nil) {
|
|
1240
|
+
PushyRejectError(reject, reloadError);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1123
1243
|
resolve(@true);
|
|
1124
1244
|
}
|
|
1125
1245
|
|
|
@@ -1138,7 +1258,7 @@ RCT_EXPORT_METHOD(getBundleHash:(RCTPromiseResolveBlock)resolve
|
|
|
1138
1258
|
resolve(@"");
|
|
1139
1259
|
#else
|
|
1140
1260
|
dispatch_async(_fileQueue, ^{
|
|
1141
|
-
resolve(PushyBundleHashSync());
|
|
1261
|
+
resolve(PushyBundleHashSync(YES));
|
|
1142
1262
|
});
|
|
1143
1263
|
#endif
|
|
1144
1264
|
}
|
|
@@ -1150,15 +1270,23 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
1150
1270
|
resolve(@true);
|
|
1151
1271
|
#else
|
|
1152
1272
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1273
|
+
// Failures surface as MARK_SUCCESS_FAILED like on Android, so per-code
|
|
1274
|
+
// telemetry is not blind to this transition on iOS.
|
|
1275
|
+
@try {
|
|
1276
|
+
PushyWithStateLock(^{
|
|
1277
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
1278
|
+
pushy::state::MarkSuccessResult result =
|
|
1279
|
+
pushy::state::MarkSuccess(PushyStateFromDefaults(defaults));
|
|
1280
|
+
if (!result.stale_version_to_delete.empty()) {
|
|
1281
|
+
[defaults removeObjectForKey:PushyHashInfoKey(PushyFromStdString(result.stale_version_to_delete))];
|
|
1282
|
+
}
|
|
1283
|
+
PushyApplyStateToDefaults(defaults, result.state);
|
|
1284
|
+
});
|
|
1285
|
+
} @catch (NSException *exception) {
|
|
1286
|
+
PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kMarkSuccessFailed,
|
|
1287
|
+
exception.reason ?: @"markSuccess failed"));
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1162
1290
|
|
|
1163
1291
|
[self clearInvalidFiles];
|
|
1164
1292
|
resolve(@true);
|
|
@@ -1175,51 +1303,63 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1175
1303
|
// asset loads). Only the client uuid survives — it identifies the install
|
|
1176
1304
|
// for gray release bucketing and must not change on reset.
|
|
1177
1305
|
__block NSString *keepVersion = nil;
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1306
|
+
@try {
|
|
1307
|
+
PushyWithStateLock(^{
|
|
1308
|
+
// Invalidate any in-flight cold-start round before clearing state, so
|
|
1309
|
+
// a round that commits under this same lock afterwards always sees the
|
|
1310
|
+
// new generation and drops its result.
|
|
1311
|
+
pushyResetGeneration.fetch_add(1);
|
|
1312
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
1313
|
+
keepVersion = pushyLaunchVersion;
|
|
1314
|
+
|
|
1315
|
+
// A default-constructed State is exactly the reset state (no current /
|
|
1316
|
+
// last version, first_time=false, first_time_ok=true); keep the binary
|
|
1317
|
+
// identity so the next launch does not re-trigger the package-updated
|
|
1318
|
+
// sync path.
|
|
1319
|
+
pushy::state::State state;
|
|
1320
|
+
state.package_version = PushyToStdString([RCTPushy packageVersion]);
|
|
1321
|
+
state.build_time = PushyToStdString([RCTPushy buildTime]);
|
|
1322
|
+
PushyApplyStateToDefaults(defaults, state);
|
|
1194
1323
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1324
|
+
for (NSString *key in [defaults dictionaryRepresentation].allKeys) {
|
|
1325
|
+
if ([key hasPrefix:keyHashInfo]) {
|
|
1326
|
+
[defaults removeObjectForKey:key];
|
|
1327
|
+
}
|
|
1198
1328
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
})
|
|
1329
|
+
[defaults removeObjectForKey:keyFirstLoadMarked];
|
|
1330
|
+
[defaults removeObjectForKey:KeyPackageUpdatedMarked];
|
|
1331
|
+
// A cached response still advertises the version this reset just
|
|
1332
|
+
// removed; dropping it stops the JS side from reusing that answer.
|
|
1333
|
+
[defaults removeObjectForKey:keyNativeCheckCache];
|
|
1334
|
+
ignoreRollback = false;
|
|
1335
|
+
});
|
|
1336
|
+
} @catch (NSException *exception) {
|
|
1337
|
+
PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kResetFailed,
|
|
1338
|
+
exception.reason ?: @"reset failed"));
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1207
1341
|
|
|
1208
1342
|
dispatch_async(_fileQueue, ^{
|
|
1209
1343
|
// maxAgeDays=0: remove every downloaded entry except the running
|
|
1210
|
-
// version's directory (cleaned up by the next regular cleanup).
|
|
1344
|
+
// version's directory (cleaned up by the next regular cleanup). The
|
|
1345
|
+
// state was just wiped, so that is the only name left to keep. The
|
|
1346
|
+
// promise settles here, after the cleanup, so a failed wipe reaches
|
|
1347
|
+
// JS as RESET_FAILED (Android parity) instead of a warning nobody
|
|
1348
|
+
// aggregates.
|
|
1211
1349
|
pushy::patch::Status status = pushy::patch::CleanupOldEntries(
|
|
1212
1350
|
PushyToStdString([RCTPushy downloadDir]),
|
|
1213
|
-
PushyToStdString(keepVersion),
|
|
1214
|
-
"",
|
|
1351
|
+
std::vector<std::string>{PushyToStdString(keepVersion)},
|
|
1215
1352
|
0
|
|
1216
1353
|
);
|
|
1217
1354
|
if (!status.ok) {
|
|
1218
1355
|
RCTLogWarn(@"Pushy reset cleanup error: %s", status.message.c_str());
|
|
1356
|
+
PushyRejectError(reject, PushyErrorWithCode(
|
|
1357
|
+
pushy::error_codes::kResetFailed,
|
|
1358
|
+
[NSString stringWithUTF8String:status.message.c_str()] ?: @"reset cleanup failed"));
|
|
1359
|
+
return;
|
|
1219
1360
|
}
|
|
1361
|
+
resolve(@true);
|
|
1220
1362
|
});
|
|
1221
|
-
|
|
1222
|
-
resolve(@true);
|
|
1223
1363
|
}
|
|
1224
1364
|
|
|
1225
1365
|
|
|
@@ -1260,16 +1400,35 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1260
1400
|
}];
|
|
1261
1401
|
}
|
|
1262
1402
|
|
|
1263
|
-
|
|
1403
|
+
// Triggers a JS reload; returns nil when one was scheduled, a RESTART_FAILED
|
|
1404
|
+
// error when this host cannot restart. React Native re-resolves the bundle
|
|
1405
|
+
// URL itself on reload (RCTHost asks its bundleURLProvider, RCTBridge.setUp
|
|
1406
|
+
// re-asks sourceURLForBridge:), so +bundleURL is deliberately NOT called
|
|
1407
|
+
// here: it consumes one-shot launch state (first_time, the first-load mark,
|
|
1408
|
+
// ignoreRollback), and consuming it for a reload that never happens would
|
|
1409
|
+
// make the next cold start roll the freshly switched version back.
|
|
1410
|
+
- (NSError *)reloadBridgeWithReason:(NSString *)reason
|
|
1264
1411
|
{
|
|
1412
|
+
#if PUSHY_HAS_RELOAD_COMMAND
|
|
1413
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1414
|
+
RCTTriggerReloadCommandListeners(reason);
|
|
1415
|
+
});
|
|
1416
|
+
return nil;
|
|
1417
|
+
#else
|
|
1418
|
+
// No RCTReloadCommand in this React Native: only the legacy bridge can
|
|
1419
|
+
// reload. Under bridgeless the module's bridge is nil or an RCTBridgeProxy
|
|
1420
|
+
// whose -reload is a logged no-op (object_getClass avoids messaging the
|
|
1421
|
+
// proxy) — report that instead of resolving a restart that never happens.
|
|
1422
|
+
RCTBridge *bridge = self.bridge;
|
|
1423
|
+
if (bridge == nil || object_getClass(bridge) == NSClassFromString(@"RCTBridgeProxy")) {
|
|
1424
|
+
return PushyErrorWithCode(pushy::error_codes::kRestartFailed,
|
|
1425
|
+
@"no bridge available to reload (bridgeless host without RCTReloadCommand)");
|
|
1426
|
+
}
|
|
1265
1427
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1266
|
-
|
|
1267
|
-
RCTReloadCommandSetBundleURL([[self class] bundleURL]);
|
|
1268
|
-
RCTTriggerReloadCommandListeners(reason);
|
|
1269
|
-
#else
|
|
1270
|
-
[self.bridge reload];
|
|
1271
|
-
#endif
|
|
1428
|
+
[bridge reload];
|
|
1272
1429
|
});
|
|
1430
|
+
return nil;
|
|
1431
|
+
#endif
|
|
1273
1432
|
}
|
|
1274
1433
|
|
|
1275
1434
|
- (void)performUpdate:(PushyType)type options:(NSDictionary *)options callback:(void (^)(NSError *error))callback
|
|
@@ -1451,7 +1610,14 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1451
1610
|
callback(error);
|
|
1452
1611
|
return;
|
|
1453
1612
|
}
|
|
1454
|
-
|
|
1613
|
+
@try {
|
|
1614
|
+
[self finishDownloadedPackage:hash type:type originHash:originHash callback:callback];
|
|
1615
|
+
} @catch (NSException *exception) {
|
|
1616
|
+
// An uncaught exception in a GCD block is fatal (and would
|
|
1617
|
+
// trip the crash rescue); a bad package is a PATCH_FAILED.
|
|
1618
|
+
callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed,
|
|
1619
|
+
exception.reason ?: @"patch failed"));
|
|
1620
|
+
}
|
|
1455
1621
|
});
|
|
1456
1622
|
}];
|
|
1457
1623
|
}
|
|
@@ -1497,7 +1663,7 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1497
1663
|
NSString *bundlePatch = [unzipDir stringByAppendingPathComponent:BUNDLE_PATCH_NAME];
|
|
1498
1664
|
|
|
1499
1665
|
NSString *destination = [unzipDir stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
|
1500
|
-
long long manifestBytes =
|
|
1666
|
+
long long manifestBytes = RCTPushyFileSize(sourcePatch);
|
|
1501
1667
|
if (manifestBytes > pushy::archive_limits::kMaxManifestBytes) {
|
|
1502
1668
|
callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed,
|
|
1503
1669
|
[NSString stringWithFormat:@"patch manifest too large: %lld bytes", manifestBytes]));
|
|
@@ -1523,6 +1689,12 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1523
1689
|
return;
|
|
1524
1690
|
}
|
|
1525
1691
|
NSDictionary *json = (NSDictionary *)jsonObject;
|
|
1692
|
+
pushy::patch::PatchManifest manifest;
|
|
1693
|
+
NSString *manifestReason = nil;
|
|
1694
|
+
if (!PushyPatchManifestFromJson(json, &manifest, &manifestReason)) {
|
|
1695
|
+
callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed, manifestReason));
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1526
1698
|
|
|
1527
1699
|
std::vector<std::string> entryNames;
|
|
1528
1700
|
if ([[NSFileManager defaultManager] fileExistsAtPath:sourcePatch isDirectory:NULL]) {
|
|
@@ -1537,7 +1709,7 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1537
1709
|
type == PushyTypePatchFromPackage
|
|
1538
1710
|
? pushy::archive_patch::ArchivePatchType::kPatchFromPackage
|
|
1539
1711
|
: pushy::archive_patch::ArchivePatchType::kPatchFromPpk,
|
|
1540
|
-
|
|
1712
|
+
manifest,
|
|
1541
1713
|
entryNames,
|
|
1542
1714
|
&plan
|
|
1543
1715
|
);
|
|
@@ -1690,11 +1862,12 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1690
1862
|
}
|
|
1691
1863
|
|
|
1692
1864
|
// Resource caps before the first byte is extracted
|
|
1693
|
-
// (cpp/patch_core/archive_limits.h).
|
|
1694
|
-
//
|
|
1695
|
-
//
|
|
1696
|
-
// entry by
|
|
1697
|
-
|
|
1865
|
+
// (cpp/patch_core/archive_limits.h). SSZipArchive only exposes the
|
|
1866
|
+
// entry count up front (zipArchiveWillUnzipArchiveAtPath:zipInfo:),
|
|
1867
|
+
// not the uncompressed total, so the disk check uses a 2x-archive
|
|
1868
|
+
// heuristic; the delegate below enforces the exact caps entry by
|
|
1869
|
+
// entry.
|
|
1870
|
+
long long archiveBytes = RCTPushyFileSize(path);
|
|
1698
1871
|
NSError *preflight = nil;
|
|
1699
1872
|
if (archiveBytes > pushy::archive_limits::kMaxArchiveBytes) {
|
|
1700
1873
|
preflight = PushyErrorWithCode(pushy::error_codes::kPatchFailed,
|
|
@@ -1704,7 +1877,7 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1704
1877
|
}
|
|
1705
1878
|
if (preflight != nil) {
|
|
1706
1879
|
[fileManager removeItemAtPath:path error:nil];
|
|
1707
|
-
[fileManager removeItemAtPath:
|
|
1880
|
+
[fileManager removeItemAtPath:RCTPushyResumeSidecarPath(path) error:nil];
|
|
1708
1881
|
if (completionHandler != nil) {
|
|
1709
1882
|
completionHandler(preflight);
|
|
1710
1883
|
}
|
|
@@ -1712,9 +1885,11 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1712
1885
|
}
|
|
1713
1886
|
|
|
1714
1887
|
PushyUnzipGuard *guard = [PushyUnzipGuard new];
|
|
1888
|
+
// preserveAttributes:NO — the archive's mode/mtime bits have no use
|
|
1889
|
+
// here and are one less thing a hostile package controls.
|
|
1715
1890
|
[SSZipArchive unzipFileAtPath:path
|
|
1716
1891
|
toDestination:destination
|
|
1717
|
-
preserveAttributes:
|
|
1892
|
+
preserveAttributes:NO
|
|
1718
1893
|
overwrite:YES
|
|
1719
1894
|
nestedZipLevel:0
|
|
1720
1895
|
password:nil
|
|
@@ -1726,8 +1901,7 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1726
1901
|
// The resume sidecar dies with its archive (§11.4): whether the
|
|
1727
1902
|
// unzip consumed it or classified it as poisoned, nothing must
|
|
1728
1903
|
// survive to vouch for bytes that are gone.
|
|
1729
|
-
[fileManager removeItemAtPath:
|
|
1730
|
-
error:nil];
|
|
1904
|
+
[fileManager removeItemAtPath:RCTPushyResumeSidecarPath(archivePath) error:nil];
|
|
1731
1905
|
if (completionHandler == nil) {
|
|
1732
1906
|
return;
|
|
1733
1907
|
}
|
|
@@ -1737,16 +1911,6 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1737
1911
|
unzipError = PushyErrorWithCode(pushy::error_codes::kPatchFailed, guard.violation);
|
|
1738
1912
|
} else if (!succeeded && unzipError == nil) {
|
|
1739
1913
|
unzipError = PushyErrorWithCode(pushy::error_codes::kPatchFailed, @"unzip failed");
|
|
1740
|
-
} else if (succeeded && unzipError == nil
|
|
1741
|
-
&& [fileManager fileExistsAtPath:
|
|
1742
|
-
[destination stringByAppendingPathComponent:VERSION_COMPLETE_FILE_NAME]]) {
|
|
1743
|
-
// The completion marker is written by this SDK only, after the
|
|
1744
|
-
// whole install succeeded. An archive that ships its own would
|
|
1745
|
-
// be trusted as a finished install even if the patch step
|
|
1746
|
-
// that follows fails — reject the package outright (the
|
|
1747
|
-
// caller removes the half-unpacked directory).
|
|
1748
|
-
unzipError = PushyErrorWithCode(pushy::error_codes::kPatchFailed,
|
|
1749
|
-
@"archive contains reserved entry " VERSION_COMPLETE_FILE_NAME_LITERAL);
|
|
1750
1914
|
} else if (unzipError != nil && unzipError.userInfo[PushyErrorCodeKey] == nil) {
|
|
1751
1915
|
// SSZipArchive's own NSError (corrupt zip, bad magic, ...) has
|
|
1752
1916
|
// no stable code; without one, downloadUpdate's fallback would
|
|
@@ -1766,14 +1930,23 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1766
1930
|
// Snapshot the state under the lock, but run the (slow) filesystem
|
|
1767
1931
|
// cleanup outside of it so state operations are not blocked.
|
|
1768
1932
|
__block pushy::state::State state;
|
|
1933
|
+
__block NSString *launchVersion = nil;
|
|
1769
1934
|
PushyWithStateLock(^{
|
|
1770
1935
|
state = PushyStateFromDefaults(PushyDefaults());
|
|
1936
|
+
launchVersion = pushyLaunchVersion;
|
|
1771
1937
|
});
|
|
1772
1938
|
NSString *downloadDir = [RCTPushy downloadDir];
|
|
1939
|
+
// Keep the persisted current/last versions AND the version this
|
|
1940
|
+
// process booted from: two switches without a restart would otherwise
|
|
1941
|
+
// evict the running bundle while its on-demand assets are still
|
|
1942
|
+
// being served.
|
|
1773
1943
|
pushy::patch::Status status = pushy::patch::CleanupOldEntries(
|
|
1774
1944
|
PushyToStdString(downloadDir),
|
|
1775
|
-
|
|
1776
|
-
|
|
1945
|
+
std::vector<std::string>{
|
|
1946
|
+
state.current_version,
|
|
1947
|
+
state.last_version,
|
|
1948
|
+
PushyToStdString(launchVersion),
|
|
1949
|
+
},
|
|
1777
1950
|
3
|
|
1778
1951
|
);
|
|
1779
1952
|
if (!status.ok) {
|
|
@@ -1785,15 +1958,14 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1785
1958
|
- (NSString *)zipExtension:(PushyType)type
|
|
1786
1959
|
{
|
|
1787
1960
|
switch (type) {
|
|
1788
|
-
case PushyTypeFullDownload:
|
|
1789
|
-
return @".ppk";
|
|
1790
1961
|
case PushyTypePatchFromPackage:
|
|
1791
1962
|
return @".ipa.patch";
|
|
1792
1963
|
case PushyTypePatchFromPpk:
|
|
1793
1964
|
return @".ppk.patch";
|
|
1794
|
-
|
|
1795
|
-
|
|
1965
|
+
case PushyTypeFullDownload:
|
|
1966
|
+
break;
|
|
1796
1967
|
}
|
|
1968
|
+
return @".ppk";
|
|
1797
1969
|
}
|
|
1798
1970
|
|
|
1799
1971
|
+ (NSString *)downloadDir
|
|
@@ -1827,9 +1999,30 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1827
1999
|
static NSString *buildTime;
|
|
1828
2000
|
static dispatch_once_t onceToken;
|
|
1829
2001
|
dispatch_once(&onceToken, ^{
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
2002
|
+
// The podspec's s.resource lands in the app bundle for a static
|
|
2003
|
+
// library but inside this pod's own framework bundle under
|
|
2004
|
+
// use_frameworks!; look in both.
|
|
2005
|
+
NSString *stampPath =
|
|
2006
|
+
[[NSBundle mainBundle] pathForResource:@"pushy_build_time" ofType:@"txt"]
|
|
2007
|
+
?: [[NSBundle bundleForClass:[RCTPushy class]] pathForResource:@"pushy_build_time"
|
|
2008
|
+
ofType:@"txt"];
|
|
2009
|
+
NSString *stamp = stampPath == nil ? nil
|
|
2010
|
+
: [[NSString stringWithContentsOfFile:stampPath encoding:NSUTF8StringEncoding error:nil]
|
|
2011
|
+
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
2012
|
+
if (stamp.length == 0) {
|
|
2013
|
+
// Never empty: with "" SyncBinaryVersion would miss a rebuilt
|
|
2014
|
+
// binary that kept its CFBundleShortVersionString, and a stale
|
|
2015
|
+
// hot update would outlive an App Store update. Every build
|
|
2016
|
+
// rewrites Info.plist, so its mtime (integer seconds) stands in.
|
|
2017
|
+
RCTLogWarn(@"RCTPushy -- pushy_build_time.txt %@; using the Info.plist mtime as buildTime",
|
|
2018
|
+
stampPath == nil ? @"not found in the main or framework bundle" : @"is empty");
|
|
2019
|
+
NSString *infoPlist =
|
|
2020
|
+
[[NSBundle mainBundle].bundlePath stringByAppendingPathComponent:@"Info.plist"];
|
|
2021
|
+
NSDate *modified = [[NSFileManager defaultManager] attributesOfItemAtPath:infoPlist
|
|
2022
|
+
error:nil].fileModificationDate;
|
|
2023
|
+
stamp = [NSString stringWithFormat:@"%lld", (long long)[modified timeIntervalSince1970]];
|
|
2024
|
+
}
|
|
2025
|
+
buildTime = stamp;
|
|
1833
2026
|
});
|
|
1834
2027
|
return buildTime;
|
|
1835
2028
|
#endif
|
|
@@ -1847,6 +2040,34 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1847
2040
|
|
|
1848
2041
|
#pragma mark - native cold-start check orchestration
|
|
1849
2042
|
|
|
2043
|
+
// Refuses an https→http downgrade before the redirected request — and with
|
|
2044
|
+
// it the POST body (appKey, uuid, versions) — leaves the device. The shared
|
|
2045
|
+
// session would follow first and only the response could be rejected; ATS
|
|
2046
|
+
// blocks plaintext by default, this covers apps that opted out with
|
|
2047
|
+
// NSAllowsArbitraryLoads. Same rule as RCTPushyDownloader. Returning nil
|
|
2048
|
+
// delivers the 3xx itself as the final response, which the status check in
|
|
2049
|
+
// PushyHttpRequest then treats as a failed endpoint.
|
|
2050
|
+
@interface PushyCheckRequestRedirectGuard : NSObject <NSURLSessionTaskDelegate>
|
|
2051
|
+
@end
|
|
2052
|
+
|
|
2053
|
+
@implementation PushyCheckRequestRedirectGuard
|
|
2054
|
+
|
|
2055
|
+
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
|
|
2056
|
+
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
|
|
2057
|
+
newRequest:(NSURLRequest *)request
|
|
2058
|
+
completionHandler:(void (^)(NSURLRequest *))completionHandler
|
|
2059
|
+
{
|
|
2060
|
+
NSString *originalScheme = [task.originalRequest.URL.scheme lowercaseString];
|
|
2061
|
+
NSString *nextScheme = [request.URL.scheme lowercaseString];
|
|
2062
|
+
if ([originalScheme isEqualToString:@"https"] && [nextScheme isEqualToString:@"http"]) {
|
|
2063
|
+
completionHandler(nil);
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
completionHandler(request);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
@end
|
|
2070
|
+
|
|
1850
2071
|
// Blocking JSON HTTP round-trip on the orchestrator's utility thread. Returns
|
|
1851
2072
|
// the response body on 2xx, nil on any failure. The semaphore timeout is a
|
|
1852
2073
|
// backstop over the request's own timeoutInterval.
|
|
@@ -1866,25 +2087,30 @@ static NSString *PushyHttpRequest(NSString *urlString, NSString *method,
|
|
|
1866
2087
|
}
|
|
1867
2088
|
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
1868
2089
|
__block NSString *result = nil;
|
|
1869
|
-
|
|
2090
|
+
// A delegate session (not the shared one) so the redirect guard sees the
|
|
2091
|
+
// 3xx before it is followed; the completion handler still receives the
|
|
2092
|
+
// final response.
|
|
2093
|
+
NSURLSession *session = [NSURLSession
|
|
2094
|
+
sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
|
|
2095
|
+
delegate:[PushyCheckRequestRedirectGuard new]
|
|
2096
|
+
delegateQueue:nil];
|
|
2097
|
+
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
|
|
1870
2098
|
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
1871
2099
|
NSHTTPURLResponse *httpResponse =
|
|
1872
2100
|
[response isKindOfClass:[NSHTTPURLResponse class]]
|
|
1873
2101
|
? (NSHTTPURLResponse *)response
|
|
1874
2102
|
: nil;
|
|
1875
2103
|
NSInteger status = httpResponse.statusCode;
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
// artifact download.
|
|
1879
|
-
BOOL downgraded = [[url.scheme lowercaseString] isEqualToString:@"https"]
|
|
1880
|
-
&& [[httpResponse.URL.scheme lowercaseString] isEqualToString:@"http"];
|
|
1881
|
-
if (error == nil && httpResponse != nil && !downgraded && status >= 200 &&
|
|
1882
|
-
status < 300 && data != nil) {
|
|
2104
|
+
if (error == nil && httpResponse != nil && status >= 200 && status < 300
|
|
2105
|
+
&& data != nil) {
|
|
1883
2106
|
result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
1884
2107
|
}
|
|
1885
2108
|
dispatch_semaphore_signal(sem);
|
|
1886
2109
|
}];
|
|
1887
2110
|
[task resume];
|
|
2111
|
+
// The session retains its delegate until invalidated: let this one task
|
|
2112
|
+
// settle, then release everything.
|
|
2113
|
+
[session finishTasksAndInvalidate];
|
|
1888
2114
|
if (dispatch_semaphore_wait(
|
|
1889
2115
|
sem, dispatch_time(DISPATCH_TIME_NOW,
|
|
1890
2116
|
(int64_t)((timeout + 5) * NSEC_PER_SEC))) != 0) {
|
|
@@ -2151,8 +2377,11 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2151
2377
|
input.Set("cInfo", cInfo);
|
|
2152
2378
|
input.Set("supportedDiffVersion",
|
|
2153
2379
|
flowjson::Value::Number(pushy::hbc::kSupportedDiffVersion));
|
|
2380
|
+
// Cache-only while a crash is being held (§11.3 budget); computed (and
|
|
2381
|
+
// shared with JS getBundleHash) on the ordinary delayed round.
|
|
2154
2382
|
input.Set("bundleHash",
|
|
2155
|
-
flowjson::Value::String(PushyToStdString(
|
|
2383
|
+
flowjson::Value::String(PushyToStdString(
|
|
2384
|
+
PushyBundleHashSync(!pushyCrashRescueActive.load()))));
|
|
2156
2385
|
|
|
2157
2386
|
std::string bodyJson =
|
|
2158
2387
|
flowjson::Stringify(updateflow::BuildCheckRequestBody(input));
|