react-native-update 10.55.0 → 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 +406 -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 +244 -86
- 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
|
@@ -14,8 +14,15 @@
|
|
|
14
14
|
|
|
15
15
|
#import <UIKit/UIKit.h>
|
|
16
16
|
|
|
17
|
+
// use_frameworks! 构建里 React 头文件只有 <React/…> 框架形式可见,引号形式的
|
|
18
|
+
// __has_include 会探测失败;漏掉这里会静默编入 [self.bridge reload] 兜底,而
|
|
19
|
+
// bridgeless 下 bridge 是 nil/代理,reload 变成无声 no-op(切版本后不重启)。
|
|
17
20
|
#if __has_include("RCTReloadCommand.h")
|
|
18
21
|
#import "RCTReloadCommand.h"
|
|
22
|
+
#define PUSHY_HAS_RELOAD_COMMAND 1
|
|
23
|
+
#elif __has_include(<React/RCTReloadCommand.h>)
|
|
24
|
+
#import <React/RCTReloadCommand.h>
|
|
25
|
+
#define PUSHY_HAS_RELOAD_COMMAND 1
|
|
19
26
|
#endif
|
|
20
27
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
21
28
|
#import "RCTPushySpec.h"
|
|
@@ -23,13 +30,24 @@
|
|
|
23
30
|
|
|
24
31
|
#import <React/RCTConvert.h>
|
|
25
32
|
#import <React/RCTLog.h>
|
|
33
|
+
#import <objc/runtime.h>
|
|
26
34
|
#import <os/lock.h>
|
|
27
35
|
|
|
28
36
|
#include <atomic>
|
|
37
|
+
#include <sys/stat.h>
|
|
29
38
|
|
|
30
39
|
static NSString *const keyPushyInfo = @"REACTNATIVECN_PUSHY_INFO_KEY";
|
|
31
|
-
|
|
32
|
-
|
|
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";
|
|
33
51
|
static NSString *const paramLastVersion = @"lastVersion";
|
|
34
52
|
static NSString *const paramCurrentVersion = @"currentVersion";
|
|
35
53
|
static NSString *const paramIsFirstTime = @"isFirstTime";
|
|
@@ -168,46 +186,32 @@ static NSMutableDictionary<NSString *, NSString *> *PushyArtifactDigests(void) {
|
|
|
168
186
|
|
|
169
187
|
static NSError *PushyErrorWithCode(const char *code, NSString *message);
|
|
170
188
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Free space on the volume holding `path` (walks up to an existing ancestor);
|
|
178
|
-
// -1 when unknown.
|
|
179
|
-
static long long PushyFreeDiskSpaceForPath(NSString *path) {
|
|
180
|
-
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
181
|
-
NSString *probe = path;
|
|
182
|
-
while (probe.length > 1 && ![fileManager fileExistsAtPath:probe]) {
|
|
183
|
-
probe = [probe stringByDeletingLastPathComponent];
|
|
184
|
-
}
|
|
185
|
-
NSDictionary *attributes = [fileManager attributesOfFileSystemForPath:probe error:nil];
|
|
186
|
-
NSNumber *free = attributes[NSFileSystemFreeSize];
|
|
187
|
-
return free == nil ? -1 : free.longLongValue;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// nil when the volume can take `bytesToWrite` plus the safety margin (or the
|
|
191
|
-
// 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.
|
|
192
193
|
static NSError *PushyEnsureFreeSpace(NSString *path, long long bytesToWrite) {
|
|
193
|
-
|
|
194
|
-
if (
|
|
194
|
+
NSString *shortfall = RCTPushyFreeSpaceShortfall(path, bytesToWrite);
|
|
195
|
+
if (shortfall == nil) {
|
|
195
196
|
return nil;
|
|
196
197
|
}
|
|
197
|
-
|
|
198
|
-
if (free < needed) {
|
|
199
|
-
return PushyErrorWithCode(
|
|
200
|
-
pushy::error_codes::kPatchFailed,
|
|
201
|
-
[NSString stringWithFormat:@"insufficient disk space: need %lld bytes, have %lld",
|
|
202
|
-
needed, free]);
|
|
203
|
-
}
|
|
204
|
-
return nil;
|
|
198
|
+
return PushyErrorWithCode(pushy::error_codes::kPatchFailed, shortfall);
|
|
205
199
|
}
|
|
206
200
|
|
|
207
201
|
// SSZipArchive delegate enforcing cpp/patch_core/archive_limits.h while the
|
|
208
|
-
// archive is walked
|
|
209
|
-
// ratio
|
|
210
|
-
//
|
|
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.
|
|
211
215
|
@interface PushyUnzipGuard : NSObject <SSZipArchiveDelegate>
|
|
212
216
|
@property (nonatomic, copy) NSString *violation;
|
|
213
217
|
@property (nonatomic, assign) long long totalUncompressed;
|
|
@@ -229,6 +233,13 @@ static NSError *PushyEnsureFreeSpace(NSString *path, long long bytesToWrite) {
|
|
|
229
233
|
if (self.violation != nil) {
|
|
230
234
|
return NO;
|
|
231
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
|
+
}
|
|
232
243
|
long long size = (long long)fileInfo.uncompressed_size;
|
|
233
244
|
long long compressed = (long long)fileInfo.compressed_size;
|
|
234
245
|
if (size > pushy::archive_limits::kMaxEntryBytes) {
|
|
@@ -251,6 +262,21 @@ static NSError *PushyEnsureFreeSpace(NSString *path, long long bytesToWrite) {
|
|
|
251
262
|
return YES;
|
|
252
263
|
}
|
|
253
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
|
+
|
|
254
280
|
@end
|
|
255
281
|
|
|
256
282
|
static NSError *PushyDownloadDeadlineExpiredError(void) {
|
|
@@ -267,10 +293,13 @@ typedef NS_ENUM(NSInteger, PushyType) {
|
|
|
267
293
|
PushyTypeFullDownload = 1,
|
|
268
294
|
PushyTypePatchFromPackage = 2,
|
|
269
295
|
PushyTypePatchFromPpk = 3,
|
|
270
|
-
//TASK_TYPE_PLAIN_DOWNLOAD=4?
|
|
271
296
|
};
|
|
272
297
|
|
|
273
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};
|
|
274
303
|
// Bumped by resetToPackagedBundle. The cold-start check runs for minutes and
|
|
275
304
|
// may already hold a decision when the app resets to the packaged bundle; it
|
|
276
305
|
// samples this counter and abandons activation (and its response cache) when
|
|
@@ -412,6 +441,11 @@ static void PushyFinishDownload(NSString *hash, NSError *error) {
|
|
|
412
441
|
static os_unfair_lock pushyStateLock = OS_UNFAIR_LOCK_INIT;
|
|
413
442
|
|
|
414
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
|
|
415
449
|
os_unfair_lock_lock(&pushyStateLock);
|
|
416
450
|
@try {
|
|
417
451
|
block();
|
|
@@ -508,46 +542,87 @@ static NSError *PushyErrorWithCode(const char *code, NSString *message) {
|
|
|
508
542
|
}];
|
|
509
543
|
}
|
|
510
544
|
|
|
511
|
-
static
|
|
512
|
-
|
|
545
|
+
static BOOL PushyJsonIsAbsent(id value) {
|
|
546
|
+
return value == nil || [value isKindOfClass:[NSNull class]];
|
|
547
|
+
}
|
|
513
548
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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;
|
|
527
562
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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);
|
|
535
590
|
}
|
|
536
|
-
manifest.copies.push_back(operation);
|
|
537
591
|
}
|
|
538
592
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
+
}
|
|
542
608
|
}
|
|
609
|
+
return YES;
|
|
610
|
+
}
|
|
543
611
|
|
|
544
|
-
|
|
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];
|
|
545
618
|
}
|
|
546
619
|
|
|
547
620
|
static pushy::state::State PushyStateFromDefaults(NSUserDefaults *defaults) {
|
|
548
621
|
pushy::state::State state;
|
|
549
|
-
state.package_version = PushyToStdString(
|
|
550
|
-
|
|
622
|
+
state.package_version = PushyToStdString(
|
|
623
|
+
PushyBinaryIdentityValue(defaults, paramPackageVersion, legacyParamPackageVersion));
|
|
624
|
+
state.build_time = PushyToStdString(
|
|
625
|
+
PushyBinaryIdentityValue(defaults, paramBuildTime, legacyParamBuildTime));
|
|
551
626
|
NSDictionary *pushyInfo = [defaults dictionaryForKey:keyPushyInfo];
|
|
552
627
|
if (pushyInfo != nil) {
|
|
553
628
|
state.current_version = PushyToStdString(pushyInfo[paramCurrentVersion]);
|
|
@@ -563,6 +638,10 @@ static pushy::state::State PushyStateFromDefaults(NSUserDefaults *defaults) {
|
|
|
563
638
|
static void PushyApplyStateToDefaults(NSUserDefaults *defaults, const pushy::state::State &state) {
|
|
564
639
|
PushySetNullableString(defaults, paramPackageVersion, PushyFromStdString(state.package_version));
|
|
565
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];
|
|
566
645
|
|
|
567
646
|
BOOL hasPushyInfo = !state.current_version.empty() || !state.last_version.empty() || state.first_time || !state.first_time_ok;
|
|
568
647
|
if (hasPushyInfo) {
|
|
@@ -609,7 +688,7 @@ static void PushySwitchVersionLocked(NSString *hash) {
|
|
|
609
688
|
- (void)performUpdate:(PushyType)type
|
|
610
689
|
options:(NSDictionary *)options
|
|
611
690
|
callback:(void (^)(NSError *error))callback;
|
|
612
|
-
- (
|
|
691
|
+
- (NSError *)reloadBridgeWithReason:(NSString *)reason;
|
|
613
692
|
- (void)unzipDownloadedPackage:(NSString *)zipFilePath
|
|
614
693
|
hash:(NSString *)hash
|
|
615
694
|
type:(PushyType)type
|
|
@@ -755,10 +834,34 @@ static void PushyInstallCrashRescueHandler(void) {
|
|
|
755
834
|
});
|
|
756
835
|
}
|
|
757
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
|
+
|
|
758
858
|
// Shared by the getBundleHash RCT method and the native cold-start check.
|
|
759
|
-
// Returns @"" when unknown
|
|
760
|
-
//
|
|
761
|
-
|
|
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) {
|
|
762
865
|
NSString *path = [[RCTPushy binaryBundleURL] path];
|
|
763
866
|
if (path == nil) {
|
|
764
867
|
return @"";
|
|
@@ -774,19 +877,33 @@ static NSString *PushyBundleHashSync(void) {
|
|
|
774
877
|
[attributes.fileModificationDate timeIntervalSince1970]];
|
|
775
878
|
|
|
776
879
|
NSUserDefaults *defaults = PushyDefaults();
|
|
777
|
-
NSString *cached = [defaults stringForKey:keyBundleHashCache];
|
|
778
880
|
NSString *cachedPrefix = [cacheKey stringByAppendingString:@"|"];
|
|
779
|
-
|
|
780
|
-
|
|
881
|
+
NSString *cached = PushyCachedBundleHash(defaults, cachedPrefix);
|
|
882
|
+
if (cached != nil) {
|
|
883
|
+
return cached;
|
|
884
|
+
}
|
|
885
|
+
if (!computeIfMissing) {
|
|
886
|
+
return @"";
|
|
781
887
|
}
|
|
782
888
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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];
|
|
788
906
|
}
|
|
789
|
-
return hash;
|
|
790
907
|
}
|
|
791
908
|
|
|
792
909
|
@implementation RCTPushy {
|
|
@@ -798,6 +915,7 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
798
915
|
|
|
799
916
|
+ (NSURL *)bundleURL
|
|
800
917
|
{
|
|
918
|
+
pushyIsUsingBundleUrl = true;
|
|
801
919
|
__block NSURL *resolvedURL = nil;
|
|
802
920
|
__block NSString *launchRolledBackVersion = nil;
|
|
803
921
|
@try {
|
|
@@ -907,6 +1025,7 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
907
1025
|
ret[@"rolledBackVersion"] = [defaults objectForKey:keyRolledBackMarked];
|
|
908
1026
|
ret[@"isFirstTime"] = [defaults objectForKey:keyFirstLoadMarked];
|
|
909
1027
|
ret[@"uuid"] = [defaults objectForKey:keyUuid];
|
|
1028
|
+
ret[@"isUsingBundleUrl"] = @(pushyIsUsingBundleUrl.load());
|
|
910
1029
|
// 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道),
|
|
911
1030
|
// JS 随 checkUpdate 以 diffV 上报,服务端按能力门控下发
|
|
912
1031
|
ret[@"supportedDiffVersion"] = @(pushy::hbc::kSupportedDiffVersion);
|
|
@@ -1105,14 +1224,22 @@ RCT_EXPORT_METHOD(reloadUpdate:(NSDictionary *)options
|
|
|
1105
1224
|
return;
|
|
1106
1225
|
}
|
|
1107
1226
|
|
|
1108
|
-
[self reloadBridgeWithReason:@"pushy reloadUpdate"];
|
|
1227
|
+
NSError *reloadError = [self reloadBridgeWithReason:@"pushy reloadUpdate"];
|
|
1228
|
+
if (reloadError != nil) {
|
|
1229
|
+
PushyRejectError(reject, reloadError);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1109
1232
|
resolve(@true);
|
|
1110
1233
|
}
|
|
1111
1234
|
|
|
1112
1235
|
RCT_EXPORT_METHOD(restartApp:(RCTPromiseResolveBlock)resolve
|
|
1113
1236
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
1114
1237
|
{
|
|
1115
|
-
[self reloadBridgeWithReason:@"pushy restartApp"];
|
|
1238
|
+
NSError *reloadError = [self reloadBridgeWithReason:@"pushy restartApp"];
|
|
1239
|
+
if (reloadError != nil) {
|
|
1240
|
+
PushyRejectError(reject, reloadError);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1116
1243
|
resolve(@true);
|
|
1117
1244
|
}
|
|
1118
1245
|
|
|
@@ -1131,7 +1258,7 @@ RCT_EXPORT_METHOD(getBundleHash:(RCTPromiseResolveBlock)resolve
|
|
|
1131
1258
|
resolve(@"");
|
|
1132
1259
|
#else
|
|
1133
1260
|
dispatch_async(_fileQueue, ^{
|
|
1134
|
-
resolve(PushyBundleHashSync());
|
|
1261
|
+
resolve(PushyBundleHashSync(YES));
|
|
1135
1262
|
});
|
|
1136
1263
|
#endif
|
|
1137
1264
|
}
|
|
@@ -1143,15 +1270,23 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
1143
1270
|
resolve(@true);
|
|
1144
1271
|
#else
|
|
1145
1272
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
+
}
|
|
1155
1290
|
|
|
1156
1291
|
[self clearInvalidFiles];
|
|
1157
1292
|
resolve(@true);
|
|
@@ -1168,51 +1303,63 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1168
1303
|
// asset loads). Only the client uuid survives — it identifies the install
|
|
1169
1304
|
// for gray release bucketing and must not change on reset.
|
|
1170
1305
|
__block NSString *keepVersion = nil;
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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);
|
|
1187
1323
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1324
|
+
for (NSString *key in [defaults dictionaryRepresentation].allKeys) {
|
|
1325
|
+
if ([key hasPrefix:keyHashInfo]) {
|
|
1326
|
+
[defaults removeObjectForKey:key];
|
|
1327
|
+
}
|
|
1191
1328
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
})
|
|
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
|
+
}
|
|
1200
1341
|
|
|
1201
1342
|
dispatch_async(_fileQueue, ^{
|
|
1202
1343
|
// maxAgeDays=0: remove every downloaded entry except the running
|
|
1203
|
-
// 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.
|
|
1204
1349
|
pushy::patch::Status status = pushy::patch::CleanupOldEntries(
|
|
1205
1350
|
PushyToStdString([RCTPushy downloadDir]),
|
|
1206
|
-
PushyToStdString(keepVersion),
|
|
1207
|
-
"",
|
|
1351
|
+
std::vector<std::string>{PushyToStdString(keepVersion)},
|
|
1208
1352
|
0
|
|
1209
1353
|
);
|
|
1210
1354
|
if (!status.ok) {
|
|
1211
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;
|
|
1212
1360
|
}
|
|
1361
|
+
resolve(@true);
|
|
1213
1362
|
});
|
|
1214
|
-
|
|
1215
|
-
resolve(@true);
|
|
1216
1363
|
}
|
|
1217
1364
|
|
|
1218
1365
|
|
|
@@ -1253,16 +1400,35 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1253
1400
|
}];
|
|
1254
1401
|
}
|
|
1255
1402
|
|
|
1256
|
-
|
|
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
|
|
1257
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
|
+
}
|
|
1258
1427
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1259
|
-
|
|
1260
|
-
RCTReloadCommandSetBundleURL([[self class] bundleURL]);
|
|
1261
|
-
RCTTriggerReloadCommandListeners(reason);
|
|
1262
|
-
#else
|
|
1263
|
-
[self.bridge reload];
|
|
1264
|
-
#endif
|
|
1428
|
+
[bridge reload];
|
|
1265
1429
|
});
|
|
1430
|
+
return nil;
|
|
1431
|
+
#endif
|
|
1266
1432
|
}
|
|
1267
1433
|
|
|
1268
1434
|
- (void)performUpdate:(PushyType)type options:(NSDictionary *)options callback:(void (^)(NSError *error))callback
|
|
@@ -1444,7 +1610,14 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1444
1610
|
callback(error);
|
|
1445
1611
|
return;
|
|
1446
1612
|
}
|
|
1447
|
-
|
|
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
|
+
}
|
|
1448
1621
|
});
|
|
1449
1622
|
}];
|
|
1450
1623
|
}
|
|
@@ -1490,7 +1663,7 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1490
1663
|
NSString *bundlePatch = [unzipDir stringByAppendingPathComponent:BUNDLE_PATCH_NAME];
|
|
1491
1664
|
|
|
1492
1665
|
NSString *destination = [unzipDir stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
|
1493
|
-
long long manifestBytes =
|
|
1666
|
+
long long manifestBytes = RCTPushyFileSize(sourcePatch);
|
|
1494
1667
|
if (manifestBytes > pushy::archive_limits::kMaxManifestBytes) {
|
|
1495
1668
|
callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed,
|
|
1496
1669
|
[NSString stringWithFormat:@"patch manifest too large: %lld bytes", manifestBytes]));
|
|
@@ -1516,6 +1689,12 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1516
1689
|
return;
|
|
1517
1690
|
}
|
|
1518
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
|
+
}
|
|
1519
1698
|
|
|
1520
1699
|
std::vector<std::string> entryNames;
|
|
1521
1700
|
if ([[NSFileManager defaultManager] fileExistsAtPath:sourcePatch isDirectory:NULL]) {
|
|
@@ -1530,7 +1709,7 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1530
1709
|
type == PushyTypePatchFromPackage
|
|
1531
1710
|
? pushy::archive_patch::ArchivePatchType::kPatchFromPackage
|
|
1532
1711
|
: pushy::archive_patch::ArchivePatchType::kPatchFromPpk,
|
|
1533
|
-
|
|
1712
|
+
manifest,
|
|
1534
1713
|
entryNames,
|
|
1535
1714
|
&plan
|
|
1536
1715
|
);
|
|
@@ -1683,11 +1862,12 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1683
1862
|
}
|
|
1684
1863
|
|
|
1685
1864
|
// Resource caps before the first byte is extracted
|
|
1686
|
-
// (cpp/patch_core/archive_limits.h).
|
|
1687
|
-
//
|
|
1688
|
-
//
|
|
1689
|
-
// entry by
|
|
1690
|
-
|
|
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);
|
|
1691
1871
|
NSError *preflight = nil;
|
|
1692
1872
|
if (archiveBytes > pushy::archive_limits::kMaxArchiveBytes) {
|
|
1693
1873
|
preflight = PushyErrorWithCode(pushy::error_codes::kPatchFailed,
|
|
@@ -1697,7 +1877,7 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1697
1877
|
}
|
|
1698
1878
|
if (preflight != nil) {
|
|
1699
1879
|
[fileManager removeItemAtPath:path error:nil];
|
|
1700
|
-
[fileManager removeItemAtPath:
|
|
1880
|
+
[fileManager removeItemAtPath:RCTPushyResumeSidecarPath(path) error:nil];
|
|
1701
1881
|
if (completionHandler != nil) {
|
|
1702
1882
|
completionHandler(preflight);
|
|
1703
1883
|
}
|
|
@@ -1705,9 +1885,11 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1705
1885
|
}
|
|
1706
1886
|
|
|
1707
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.
|
|
1708
1890
|
[SSZipArchive unzipFileAtPath:path
|
|
1709
1891
|
toDestination:destination
|
|
1710
|
-
preserveAttributes:
|
|
1892
|
+
preserveAttributes:NO
|
|
1711
1893
|
overwrite:YES
|
|
1712
1894
|
nestedZipLevel:0
|
|
1713
1895
|
password:nil
|
|
@@ -1719,8 +1901,7 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1719
1901
|
// The resume sidecar dies with its archive (§11.4): whether the
|
|
1720
1902
|
// unzip consumed it or classified it as poisoned, nothing must
|
|
1721
1903
|
// survive to vouch for bytes that are gone.
|
|
1722
|
-
[fileManager removeItemAtPath:
|
|
1723
|
-
error:nil];
|
|
1904
|
+
[fileManager removeItemAtPath:RCTPushyResumeSidecarPath(archivePath) error:nil];
|
|
1724
1905
|
if (completionHandler == nil) {
|
|
1725
1906
|
return;
|
|
1726
1907
|
}
|
|
@@ -1730,16 +1911,6 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1730
1911
|
unzipError = PushyErrorWithCode(pushy::error_codes::kPatchFailed, guard.violation);
|
|
1731
1912
|
} else if (!succeeded && unzipError == nil) {
|
|
1732
1913
|
unzipError = PushyErrorWithCode(pushy::error_codes::kPatchFailed, @"unzip failed");
|
|
1733
|
-
} else if (succeeded && unzipError == nil
|
|
1734
|
-
&& [fileManager fileExistsAtPath:
|
|
1735
|
-
[destination stringByAppendingPathComponent:VERSION_COMPLETE_FILE_NAME]]) {
|
|
1736
|
-
// The completion marker is written by this SDK only, after the
|
|
1737
|
-
// whole install succeeded. An archive that ships its own would
|
|
1738
|
-
// be trusted as a finished install even if the patch step
|
|
1739
|
-
// that follows fails — reject the package outright (the
|
|
1740
|
-
// caller removes the half-unpacked directory).
|
|
1741
|
-
unzipError = PushyErrorWithCode(pushy::error_codes::kPatchFailed,
|
|
1742
|
-
@"archive contains reserved entry " VERSION_COMPLETE_FILE_NAME_LITERAL);
|
|
1743
1914
|
} else if (unzipError != nil && unzipError.userInfo[PushyErrorCodeKey] == nil) {
|
|
1744
1915
|
// SSZipArchive's own NSError (corrupt zip, bad magic, ...) has
|
|
1745
1916
|
// no stable code; without one, downloadUpdate's fallback would
|
|
@@ -1759,14 +1930,23 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1759
1930
|
// Snapshot the state under the lock, but run the (slow) filesystem
|
|
1760
1931
|
// cleanup outside of it so state operations are not blocked.
|
|
1761
1932
|
__block pushy::state::State state;
|
|
1933
|
+
__block NSString *launchVersion = nil;
|
|
1762
1934
|
PushyWithStateLock(^{
|
|
1763
1935
|
state = PushyStateFromDefaults(PushyDefaults());
|
|
1936
|
+
launchVersion = pushyLaunchVersion;
|
|
1764
1937
|
});
|
|
1765
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.
|
|
1766
1943
|
pushy::patch::Status status = pushy::patch::CleanupOldEntries(
|
|
1767
1944
|
PushyToStdString(downloadDir),
|
|
1768
|
-
|
|
1769
|
-
|
|
1945
|
+
std::vector<std::string>{
|
|
1946
|
+
state.current_version,
|
|
1947
|
+
state.last_version,
|
|
1948
|
+
PushyToStdString(launchVersion),
|
|
1949
|
+
},
|
|
1770
1950
|
3
|
|
1771
1951
|
);
|
|
1772
1952
|
if (!status.ok) {
|
|
@@ -1778,15 +1958,14 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1778
1958
|
- (NSString *)zipExtension:(PushyType)type
|
|
1779
1959
|
{
|
|
1780
1960
|
switch (type) {
|
|
1781
|
-
case PushyTypeFullDownload:
|
|
1782
|
-
return @".ppk";
|
|
1783
1961
|
case PushyTypePatchFromPackage:
|
|
1784
1962
|
return @".ipa.patch";
|
|
1785
1963
|
case PushyTypePatchFromPpk:
|
|
1786
1964
|
return @".ppk.patch";
|
|
1787
|
-
|
|
1788
|
-
|
|
1965
|
+
case PushyTypeFullDownload:
|
|
1966
|
+
break;
|
|
1789
1967
|
}
|
|
1968
|
+
return @".ppk";
|
|
1790
1969
|
}
|
|
1791
1970
|
|
|
1792
1971
|
+ (NSString *)downloadDir
|
|
@@ -1820,9 +1999,30 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1820
1999
|
static NSString *buildTime;
|
|
1821
2000
|
static dispatch_once_t onceToken;
|
|
1822
2001
|
dispatch_once(&onceToken, ^{
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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;
|
|
1826
2026
|
});
|
|
1827
2027
|
return buildTime;
|
|
1828
2028
|
#endif
|
|
@@ -1840,6 +2040,34 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1840
2040
|
|
|
1841
2041
|
#pragma mark - native cold-start check orchestration
|
|
1842
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
|
+
|
|
1843
2071
|
// Blocking JSON HTTP round-trip on the orchestrator's utility thread. Returns
|
|
1844
2072
|
// the response body on 2xx, nil on any failure. The semaphore timeout is a
|
|
1845
2073
|
// backstop over the request's own timeoutInterval.
|
|
@@ -1859,25 +2087,30 @@ static NSString *PushyHttpRequest(NSString *urlString, NSString *method,
|
|
|
1859
2087
|
}
|
|
1860
2088
|
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
1861
2089
|
__block NSString *result = nil;
|
|
1862
|
-
|
|
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
|
|
1863
2098
|
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
1864
2099
|
NSHTTPURLResponse *httpResponse =
|
|
1865
2100
|
[response isKindOfClass:[NSHTTPURLResponse class]]
|
|
1866
2101
|
? (NSHTTPURLResponse *)response
|
|
1867
2102
|
: nil;
|
|
1868
2103
|
NSInteger status = httpResponse.statusCode;
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
// artifact download.
|
|
1872
|
-
BOOL downgraded = [[url.scheme lowercaseString] isEqualToString:@"https"]
|
|
1873
|
-
&& [[httpResponse.URL.scheme lowercaseString] isEqualToString:@"http"];
|
|
1874
|
-
if (error == nil && httpResponse != nil && !downgraded && status >= 200 &&
|
|
1875
|
-
status < 300 && data != nil) {
|
|
2104
|
+
if (error == nil && httpResponse != nil && status >= 200 && status < 300
|
|
2105
|
+
&& data != nil) {
|
|
1876
2106
|
result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
1877
2107
|
}
|
|
1878
2108
|
dispatch_semaphore_signal(sem);
|
|
1879
2109
|
}];
|
|
1880
2110
|
[task resume];
|
|
2111
|
+
// The session retains its delegate until invalidated: let this one task
|
|
2112
|
+
// settle, then release everything.
|
|
2113
|
+
[session finishTasksAndInvalidate];
|
|
1881
2114
|
if (dispatch_semaphore_wait(
|
|
1882
2115
|
sem, dispatch_time(DISPATCH_TIME_NOW,
|
|
1883
2116
|
(int64_t)((timeout + 5) * NSEC_PER_SEC))) != 0) {
|
|
@@ -2144,8 +2377,11 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2144
2377
|
input.Set("cInfo", cInfo);
|
|
2145
2378
|
input.Set("supportedDiffVersion",
|
|
2146
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.
|
|
2147
2382
|
input.Set("bundleHash",
|
|
2148
|
-
flowjson::Value::String(PushyToStdString(
|
|
2383
|
+
flowjson::Value::String(PushyToStdString(
|
|
2384
|
+
PushyBundleHashSync(!pushyCrashRescueActive.load()))));
|
|
2149
2385
|
|
|
2150
2386
|
std::string bodyJson =
|
|
2151
2387
|
flowjson::Stringify(updateflow::BuildCheckRequestBody(input));
|