react-native-update 10.50.0 → 10.51.1
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/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
- package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
- package/README-CN.md +14 -1
- package/README.md +14 -1
- package/android/build.gradle +10 -0
- package/android/jni/Android.mk +5 -1
- 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/src/main/java/cn/reactnative/modules/update/DownloadTask.java +76 -21
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +160 -42
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +44 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/cpp/update_flow_core/flow_json.cpp +522 -0
- package/cpp/update_flow_core/flow_json.h +103 -0
- package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
- package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
- package/cpp/update_flow_core/update_flow_core.cpp +301 -0
- package/cpp/update_flow_core/update_flow_core.h +77 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +103 -1
- package/harmony/pushy/src/main/ets/DownloadTask.ts +48 -9
- package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
- package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +14 -0
- package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +148 -69
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +779 -61
- package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
- package/package.json +3 -1
- package/react-native-update.podspec +2 -0
- package/src/NativePushy.ts +14 -0
- package/src/client.ts +341 -118
- package/src/endpoint.ts +7 -29
- package/src/provider.tsx +38 -2
- package/src/type.ts +26 -0
- package/src/updateFlowCore.ts +320 -0
- package/src/utils.ts +1 -12
- package/src/isInRollout.ts +0 -87
- package/src/resolveCheckResult.ts +0 -30
package/ios/RCTPushy/RCTPushy.mm
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
#include "../../cpp/patch_core/error_codes.h"
|
|
8
8
|
#include "../../cpp/patch_core/patch_core.h"
|
|
9
9
|
#include "../../cpp/patch_core/state_core.h"
|
|
10
|
+
#include "../../cpp/update_flow_core/flow_json.h"
|
|
11
|
+
#include "../../cpp/update_flow_core/update_flow_core.h"
|
|
12
|
+
|
|
13
|
+
#import <UIKit/UIKit.h>
|
|
10
14
|
|
|
11
15
|
#if __has_include("RCTReloadCommand.h")
|
|
12
16
|
#import "RCTReloadCommand.h"
|
|
@@ -37,12 +41,19 @@ static NSString *const KeyPackageUpdatedMarked = @"REACTNATIVECN_PUSHY_ISPACKAGE
|
|
|
37
41
|
// installed binary (packageVersion + embedded bundle size + mtime). Recomputed
|
|
38
42
|
// only when the key changes, i.e. once per install.
|
|
39
43
|
static NSString *const keyBundleHashCache = @"REACTNATIVECN_PUSHY_BUNDLEHASH_KEY";
|
|
44
|
+
// Raw JSON persisted by JS (syncNativeConfig) for the native cold-start
|
|
45
|
+
// update check; parsed on read by the orchestrator. Absent = check disabled.
|
|
46
|
+
static NSString *const keyNativeConfig = @"REACTNATIVECN_PUSHY_NATIVE_CONFIG_KEY";
|
|
47
|
+
// Raw response cache written by the native cold-start check for the JS side
|
|
48
|
+
// to reuse (§10.3), scoped to the request and config that produced it.
|
|
49
|
+
static NSString *const keyNativeCheckCache = @"REACTNATIVECN_PUSHY_NATIVE_CHECK_RESP_KEY";
|
|
40
50
|
static NSString *const PushyErrorDomain = @"cn.reactnative.pushy";
|
|
41
51
|
|
|
42
52
|
// file def
|
|
43
53
|
static NSString * const BUNDLE_FILE_NAME = @"index.bundlejs";
|
|
44
54
|
static NSString * const SOURCE_PATCH_NAME = @"__diff.json";
|
|
45
55
|
static NSString * const BUNDLE_PATCH_NAME = @"index.bundlejs.patch";
|
|
56
|
+
static NSString * const VERSION_COMPLETE_FILE_NAME = @".pushy-complete";
|
|
46
57
|
|
|
47
58
|
// error def — messages are human-readable; the stable cross-platform codes
|
|
48
59
|
// live in cpp/patch_core/error_codes.h and travel in PushyErrorCodeKey.
|
|
@@ -60,6 +71,26 @@ static NSString * const PARAM_PROGRESS_HASH = @"hash";
|
|
|
60
71
|
static NSString * const PARAM_PROGRESS_RECEIVED = @"received";
|
|
61
72
|
static NSString * const PARAM_PROGRESS_TOTAL = @"total";
|
|
62
73
|
|
|
74
|
+
static NSTimeInterval PushyMonotonicNow(void) {
|
|
75
|
+
return [NSProcessInfo processInfo].systemUptime;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static BOOL PushyHasCompletedVersionAtPath(NSString *versionDir) {
|
|
79
|
+
NSString *bundlePath = [versionDir stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
|
80
|
+
NSString *markerPath = [versionDir stringByAppendingPathComponent:VERSION_COMPLETE_FILE_NAME];
|
|
81
|
+
return [[NSFileManager defaultManager] fileExistsAtPath:bundlePath]
|
|
82
|
+
&& [[NSFileManager defaultManager] fileExistsAtPath:markerPath];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static NSError *PushyDownloadDeadlineExpiredError(void) {
|
|
86
|
+
return [NSError errorWithDomain:PushyErrorDomain
|
|
87
|
+
code:-1
|
|
88
|
+
userInfo:@{
|
|
89
|
+
NSLocalizedDescriptionKey: @"download deadline expired before start",
|
|
90
|
+
PushyErrorCodeKey: PushyCode(pushy::error_codes::kDownloadFailed),
|
|
91
|
+
}];
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
|
|
64
95
|
typedef NS_ENUM(NSInteger, PushyType) {
|
|
65
96
|
PushyTypeFullDownload = 1,
|
|
@@ -69,6 +100,12 @@ typedef NS_ENUM(NSInteger, PushyType) {
|
|
|
69
100
|
};
|
|
70
101
|
|
|
71
102
|
static std::atomic<bool> ignoreRollback{false};
|
|
103
|
+
// Bumped by resetToPackagedBundle. The cold-start check runs for minutes and
|
|
104
|
+
// may already hold a decision when the app resets to the packaged bundle; it
|
|
105
|
+
// samples this counter and abandons activation (and its response cache) when
|
|
106
|
+
// the value moved, so an in-flight rescue can never resurrect the version the
|
|
107
|
+
// app just reset away from.
|
|
108
|
+
static std::atomic<uint64_t> pushyResetGeneration{0};
|
|
72
109
|
// The version whose bundle this process actually loaded (resolved in
|
|
73
110
|
// +bundleURL). resetToPackagedBundle must not delete its directory: update
|
|
74
111
|
// assets (images/fonts) are read from it on demand at runtime, so wiping it
|
|
@@ -76,6 +113,126 @@ static std::atomic<bool> ignoreRollback{false};
|
|
|
76
113
|
// has not loaded yet. Guarded by the state lock.
|
|
77
114
|
static NSString *pushyLaunchVersion = nil;
|
|
78
115
|
|
|
116
|
+
// JS and the bridge-free cold-start engine use different RCTPushy instances,
|
|
117
|
+
// but they must still share one download per target hash. Without this
|
|
118
|
+
// process-wide registry two NSURLSessionDownloadTasks race over the same
|
|
119
|
+
// archive path and the later unzip can delete the first task's valid output.
|
|
120
|
+
static NSMutableDictionary<NSString *, NSMutableDictionary *> *PushyInFlightDownloads(void) {
|
|
121
|
+
static NSMutableDictionary<NSString *, NSMutableDictionary *> *downloads;
|
|
122
|
+
static dispatch_once_t onceToken;
|
|
123
|
+
dispatch_once(&onceToken, ^{
|
|
124
|
+
downloads = [NSMutableDictionary dictionary];
|
|
125
|
+
});
|
|
126
|
+
return downloads;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
typedef NS_ENUM(NSInteger, PushyDownloadRegistration) {
|
|
130
|
+
PushyDownloadRegistrationOwner,
|
|
131
|
+
PushyDownloadRegistrationJoined,
|
|
132
|
+
PushyDownloadRegistrationDeferred,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
static PushyDownloadRegistration PushyRegisterDownload(
|
|
136
|
+
NSString *hash,
|
|
137
|
+
NSInteger type,
|
|
138
|
+
NSTimeInterval deadlineUptime,
|
|
139
|
+
void (^callback)(NSError *),
|
|
140
|
+
void (^progress)(long long, long long),
|
|
141
|
+
void (^deferredStart)(void)
|
|
142
|
+
) {
|
|
143
|
+
NSMutableDictionary *downloads = PushyInFlightDownloads();
|
|
144
|
+
@synchronized (downloads) {
|
|
145
|
+
NSMutableDictionary *entry = downloads[hash];
|
|
146
|
+
if (entry != nil) {
|
|
147
|
+
if ([entry[@"type"] integerValue] == type) {
|
|
148
|
+
NSTimeInterval ownerDeadline = [entry[@"deadlineUptime"] doubleValue];
|
|
149
|
+
// A caller with substantially more time must not inherit an
|
|
150
|
+
// owner's nearly-exhausted timeout: it observes the current
|
|
151
|
+
// transfer and restarts after the owner settles (the
|
|
152
|
+
// completion-marker preflight makes a successful owner free).
|
|
153
|
+
// The comparison is on remaining budget, not on the absolute
|
|
154
|
+
// deadline: a JS caller always computes now+600 a few seconds
|
|
155
|
+
// after the owner did, so a strict `>` would defer every
|
|
156
|
+
// second caller and turn the shared download back into
|
|
157
|
+
// serialized re-downloads. Only a genuinely starved owner
|
|
158
|
+
// (less than half the newcomer's budget left) defers.
|
|
159
|
+
const NSTimeInterval now = PushyMonotonicNow();
|
|
160
|
+
if (2 * (ownerDeadline - now) < (deadlineUptime - now)) {
|
|
161
|
+
if (progress != nil) {
|
|
162
|
+
[entry[@"progress"] addObject:[progress copy]];
|
|
163
|
+
}
|
|
164
|
+
[entry[@"deferred"] addObject:[deferredStart copy]];
|
|
165
|
+
return PushyDownloadRegistrationDeferred;
|
|
166
|
+
}
|
|
167
|
+
[entry[@"callbacks"] addObject:[callback copy]];
|
|
168
|
+
if (progress != nil) {
|
|
169
|
+
[entry[@"progress"] addObject:[progress copy]];
|
|
170
|
+
}
|
|
171
|
+
return PushyDownloadRegistrationJoined;
|
|
172
|
+
}
|
|
173
|
+
// A diff failure must not settle a joined full request. Queue the
|
|
174
|
+
// different artifact type behind the owner; once restarted it
|
|
175
|
+
// registers normally (and re-checks the completion marker).
|
|
176
|
+
if (progress != nil) {
|
|
177
|
+
// The artifact type differs, but it still installs the same
|
|
178
|
+
// target hash. Keep the waiting JS UI moving while its own
|
|
179
|
+
// transfer is queued behind the current owner.
|
|
180
|
+
[entry[@"progress"] addObject:[progress copy]];
|
|
181
|
+
}
|
|
182
|
+
[entry[@"deferred"] addObject:[deferredStart copy]];
|
|
183
|
+
return PushyDownloadRegistrationDeferred;
|
|
184
|
+
}
|
|
185
|
+
NSMutableArray *progressHandlers = [NSMutableArray array];
|
|
186
|
+
if (progress != nil) {
|
|
187
|
+
[progressHandlers addObject:[progress copy]];
|
|
188
|
+
}
|
|
189
|
+
downloads[hash] = [@{
|
|
190
|
+
@"type": @(type),
|
|
191
|
+
@"deadlineUptime": @(deadlineUptime),
|
|
192
|
+
@"callbacks": [NSMutableArray arrayWithObject:[callback copy]],
|
|
193
|
+
@"progress": progressHandlers,
|
|
194
|
+
@"deferred": [NSMutableArray array],
|
|
195
|
+
} mutableCopy];
|
|
196
|
+
return PushyDownloadRegistrationOwner;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static void PushyReportDownloadProgress(
|
|
201
|
+
NSString *hash, long long received, long long total
|
|
202
|
+
) {
|
|
203
|
+
NSMutableDictionary *downloads = PushyInFlightDownloads();
|
|
204
|
+
NSArray *handlers = nil;
|
|
205
|
+
@synchronized (downloads) {
|
|
206
|
+
handlers = [downloads[hash][@"progress"] copy];
|
|
207
|
+
}
|
|
208
|
+
for (id value in handlers) {
|
|
209
|
+
void (^handler)(long long, long long) =
|
|
210
|
+
(void (^)(long long, long long))value;
|
|
211
|
+
handler(received, total);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
static void PushyFinishDownload(NSString *hash, NSError *error) {
|
|
216
|
+
NSMutableDictionary *downloads = PushyInFlightDownloads();
|
|
217
|
+
NSArray *callbacks = nil;
|
|
218
|
+
NSArray *deferred = nil;
|
|
219
|
+
@synchronized (downloads) {
|
|
220
|
+
callbacks = [downloads[hash][@"callbacks"] copy];
|
|
221
|
+
deferred = [downloads[hash][@"deferred"] copy];
|
|
222
|
+
[downloads removeObjectForKey:hash];
|
|
223
|
+
}
|
|
224
|
+
// Establish the next (different-type) owner before waking the completed
|
|
225
|
+
// owner's callbacks; otherwise its strategy loop could race the waiter.
|
|
226
|
+
for (id value in deferred) {
|
|
227
|
+
void (^start)(void) = (void (^)(void))value;
|
|
228
|
+
start();
|
|
229
|
+
}
|
|
230
|
+
for (id value in callbacks) {
|
|
231
|
+
void (^callback)(NSError *) = (void (^)(NSError *))value;
|
|
232
|
+
callback(error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
79
236
|
// Serializes every read-modify-write of the persisted update state. The state
|
|
80
237
|
// machine itself is a pure function (state_core), but callers run on different
|
|
81
238
|
// threads (main thread bundleURL, module method queue, _fileQueue), so the
|
|
@@ -85,8 +242,11 @@ static os_unfair_lock pushyStateLock = OS_UNFAIR_LOCK_INIT;
|
|
|
85
242
|
|
|
86
243
|
static void PushyWithStateLock(void (NS_NOESCAPE ^block)(void)) {
|
|
87
244
|
os_unfair_lock_lock(&pushyStateLock);
|
|
88
|
-
|
|
89
|
-
|
|
245
|
+
@try {
|
|
246
|
+
block();
|
|
247
|
+
} @finally {
|
|
248
|
+
os_unfair_lock_unlock(&pushyStateLock);
|
|
249
|
+
}
|
|
90
250
|
}
|
|
91
251
|
|
|
92
252
|
static std::string PushyToStdString(NSString *value) {
|
|
@@ -255,6 +415,21 @@ static void PushyApplyStateToDefaults(NSUserDefaults *defaults, const pushy::sta
|
|
|
255
415
|
PushyFromStdString(state.rolled_back_version));
|
|
256
416
|
}
|
|
257
417
|
|
|
418
|
+
// Version switch without acquiring the state lock: the caller must already
|
|
419
|
+
// hold it. Lets the cold-start check commit its whole result (version info,
|
|
420
|
+
// switch, response cache) inside one lock acquisition, so resetToPackagedBundle
|
|
421
|
+
// can never interleave between the generation check and the writes.
|
|
422
|
+
static void PushySwitchVersionLocked(NSString *hash) {
|
|
423
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
424
|
+
pushy::state::State next = pushy::state::SwitchVersion(
|
|
425
|
+
PushyStateFromDefaults(defaults),
|
|
426
|
+
PushyToStdString(hash)
|
|
427
|
+
);
|
|
428
|
+
PushyApplyStateToDefaults(defaults, next);
|
|
429
|
+
// Re-enable first-load consumption and rollback checks for the newly selected bundle.
|
|
430
|
+
ignoreRollback = false;
|
|
431
|
+
}
|
|
432
|
+
|
|
258
433
|
@interface RCTPushy ()
|
|
259
434
|
- (void)downloadUpdate:(PushyType)type
|
|
260
435
|
options:(NSDictionary *)options
|
|
@@ -284,8 +459,62 @@ static void PushyApplyStateToDefaults(NSUserDefaults *defaults, const pushy::sta
|
|
|
284
459
|
- (void)unzipFileAtPath:(NSString *)path
|
|
285
460
|
toDestination:(NSString *)destination
|
|
286
461
|
completionHandler:(void (^)(NSError *error))completionHandler;
|
|
462
|
+
+ (NSString *)downloadDir;
|
|
463
|
+
+ (NSURL *)binaryBundleURL;
|
|
464
|
+
+ (NSString *)packageVersion;
|
|
465
|
+
+ (NSString *)buildTime;
|
|
287
466
|
@end
|
|
288
467
|
|
|
468
|
+
// Native cold-start update check (NATIVE_CHECKUPDATE_DESIGN §10): runs once
|
|
469
|
+
// per process, a few seconds after launch, entirely independent of the app
|
|
470
|
+
// bundle — this is what lets a bricked hot update be replaced on the next
|
|
471
|
+
// launch. Decisions come from cpp/update_flow_core; this class is IO glue.
|
|
472
|
+
@interface RCTPushyOrchestrator : NSObject
|
|
473
|
+
+ (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion;
|
|
474
|
+
+ (void)runOnce:(NSString *)launchRolledBackVersion;
|
|
475
|
+
+ (BOOL)commitRoundWithGeneration:(uint64_t)generation
|
|
476
|
+
hashInfo:(NSDictionary *)hashInfoEntry
|
|
477
|
+
activate:(NSString *)hashToActivate
|
|
478
|
+
responseText:(NSString *)responseText
|
|
479
|
+
request:(NSString *)requestBody
|
|
480
|
+
config:(NSString *)configJson
|
|
481
|
+
responseAt:(long long)responseAtSeconds;
|
|
482
|
+
@end
|
|
483
|
+
|
|
484
|
+
// Shared by the getBundleHash RCT method and the native cold-start check.
|
|
485
|
+
// Returns @"" when unknown; blocking (sha256 of the embedded bundle on first
|
|
486
|
+
// call per install, cached afterwards) — call off the main thread.
|
|
487
|
+
static NSString *PushyBundleHashSync(void) {
|
|
488
|
+
NSString *path = [[RCTPushy binaryBundleURL] path];
|
|
489
|
+
if (path == nil) {
|
|
490
|
+
return @"";
|
|
491
|
+
}
|
|
492
|
+
NSDictionary *attributes =
|
|
493
|
+
[[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
|
|
494
|
+
if (attributes == nil) {
|
|
495
|
+
return @"";
|
|
496
|
+
}
|
|
497
|
+
NSString *cacheKey = [NSString stringWithFormat:@"%@|%llu|%.0f",
|
|
498
|
+
[RCTPushy packageVersion],
|
|
499
|
+
attributes.fileSize,
|
|
500
|
+
[attributes.fileModificationDate timeIntervalSince1970]];
|
|
501
|
+
|
|
502
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
503
|
+
NSString *cached = [defaults stringForKey:keyBundleHashCache];
|
|
504
|
+
NSString *cachedPrefix = [cacheKey stringByAppendingString:@"|"];
|
|
505
|
+
if ([cached hasPrefix:cachedPrefix]) {
|
|
506
|
+
return [cached substringFromIndex:cachedPrefix.length];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
NSString *hash = PushyFromStdString(
|
|
510
|
+
pushy::digest::Sha256File(PushyToStdString(path))) ?: @"";
|
|
511
|
+
if (hash.length > 0) {
|
|
512
|
+
[defaults setObject:[cachedPrefix stringByAppendingString:hash]
|
|
513
|
+
forKey:keyBundleHashCache];
|
|
514
|
+
}
|
|
515
|
+
return hash;
|
|
516
|
+
}
|
|
517
|
+
|
|
289
518
|
@implementation RCTPushy {
|
|
290
519
|
dispatch_queue_t _fileQueue;
|
|
291
520
|
bool hasListeners;
|
|
@@ -296,8 +525,10 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
296
525
|
+ (NSURL *)bundleURL
|
|
297
526
|
{
|
|
298
527
|
__block NSURL *resolvedURL = nil;
|
|
299
|
-
|
|
300
|
-
|
|
528
|
+
__block NSString *launchRolledBackVersion = nil;
|
|
529
|
+
@try {
|
|
530
|
+
PushyWithStateLock(^{
|
|
531
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
301
532
|
|
|
302
533
|
NSString *curPackageVersion = [RCTPushy packageVersion];
|
|
303
534
|
NSString *curBuildTime = [RCTPushy buildTime];
|
|
@@ -351,7 +582,7 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
351
582
|
if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:NULL]) {
|
|
352
583
|
pushyLaunchVersion = loadVersion;
|
|
353
584
|
resolvedURL = [NSURL fileURLWithPath:bundlePath];
|
|
354
|
-
|
|
585
|
+
break;
|
|
355
586
|
} else {
|
|
356
587
|
RCTLogError(@"RCTPushy -- bundle version %@ not found, rolling back", loadVersion);
|
|
357
588
|
state = pushy::state::Rollback(state);
|
|
@@ -360,9 +591,18 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
360
591
|
}
|
|
361
592
|
}
|
|
362
593
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
594
|
+
// Capture before constantsToExport consumes this one-shot marker.
|
|
595
|
+
// The delayed native check must never forceBoot the version that this
|
|
596
|
+
// launch just rolled back.
|
|
597
|
+
launchRolledBackVersion = PushyFromStdString(state.rolled_back_version);
|
|
598
|
+
});
|
|
599
|
+
return resolvedURL ?: [RCTPushy binaryBundleURL];
|
|
600
|
+
} @finally {
|
|
601
|
+
// State corruption is exactly when the rescue path matters most. If
|
|
602
|
+
// resolution throws before a snapshot exists, nil safely omits only
|
|
603
|
+
// this launch's rollback guard instead of disabling the check.
|
|
604
|
+
[RCTPushyOrchestrator scheduleFromColdStart:launchRolledBackVersion];
|
|
605
|
+
}
|
|
366
606
|
}
|
|
367
607
|
|
|
368
608
|
+ (NSString *) rollback {
|
|
@@ -452,6 +692,36 @@ RCT_EXPORT_METHOD(setUuid:(NSString *)uuid resolver:(RCTPromiseResolveBlock)res
|
|
|
452
692
|
resolve(@true);
|
|
453
693
|
}
|
|
454
694
|
|
|
695
|
+
RCT_EXPORT_METHOD(syncNativeConfig:(NSString *)config
|
|
696
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
697
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
698
|
+
{
|
|
699
|
+
// Provisioning for the native cold-start check (NATIVE_CHECKUPDATE_DESIGN
|
|
700
|
+
// §10.1). Validate at write time: a corrupt config would otherwise
|
|
701
|
+
// silently disable the native check forever with no signal.
|
|
702
|
+
if (PushyStringIsBlank(config)) {
|
|
703
|
+
PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
NSData *data = [config dataUsingEncoding:NSUTF8StringEncoding];
|
|
707
|
+
NSError *error = nil;
|
|
708
|
+
id object = data == nil ? nil : [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
|
709
|
+
if (![object isKindOfClass:[NSDictionary class]]) {
|
|
710
|
+
PushyRejectError(reject, PushyErrorWithCode(
|
|
711
|
+
pushy::error_codes::kInvalidOptions,
|
|
712
|
+
error != nil ? error.localizedDescription : ERROR_OPTIONS));
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
[PushyDefaults() setObject:config forKey:keyNativeConfig];
|
|
716
|
+
resolve(@true);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
RCT_EXPORT_METHOD(getNativeCheckCache:(RCTPromiseResolveBlock)resolve
|
|
720
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
721
|
+
{
|
|
722
|
+
resolve([PushyDefaults() stringForKey:keyNativeCheckCache] ?: @"");
|
|
723
|
+
}
|
|
724
|
+
|
|
455
725
|
RCT_EXPORT_METHOD(setLocalHashInfo:(NSString *)hash
|
|
456
726
|
value:(NSString *)value resolver:(RCTPromiseResolveBlock)resolve
|
|
457
727
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
@@ -565,37 +835,7 @@ RCT_EXPORT_METHOD(getBundleHash:(RCTPromiseResolveBlock)resolve
|
|
|
565
835
|
resolve(@"");
|
|
566
836
|
#else
|
|
567
837
|
dispatch_async(_fileQueue, ^{
|
|
568
|
-
|
|
569
|
-
if (path == nil) {
|
|
570
|
-
resolve(@"");
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
NSDictionary *attributes =
|
|
574
|
-
[[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
|
|
575
|
-
if (attributes == nil) {
|
|
576
|
-
resolve(@"");
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
NSString *cacheKey = [NSString stringWithFormat:@"%@|%llu|%.0f",
|
|
580
|
-
[RCTPushy packageVersion],
|
|
581
|
-
attributes.fileSize,
|
|
582
|
-
[attributes.fileModificationDate timeIntervalSince1970]];
|
|
583
|
-
|
|
584
|
-
NSUserDefaults *defaults = PushyDefaults();
|
|
585
|
-
NSString *cached = [defaults stringForKey:keyBundleHashCache];
|
|
586
|
-
NSString *cachedPrefix = [cacheKey stringByAppendingString:@"|"];
|
|
587
|
-
if ([cached hasPrefix:cachedPrefix]) {
|
|
588
|
-
resolve([cached substringFromIndex:cachedPrefix.length]);
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
NSString *hash = PushyFromStdString(
|
|
593
|
-
pushy::digest::Sha256File(PushyToStdString(path))) ?: @"";
|
|
594
|
-
if (hash.length > 0) {
|
|
595
|
-
[defaults setObject:[cachedPrefix stringByAppendingString:hash]
|
|
596
|
-
forKey:keyBundleHashCache];
|
|
597
|
-
}
|
|
598
|
-
resolve(hash);
|
|
838
|
+
resolve(PushyBundleHashSync());
|
|
599
839
|
});
|
|
600
840
|
#endif
|
|
601
841
|
}
|
|
@@ -633,6 +873,10 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
633
873
|
// for gray release bucketing and must not change on reset.
|
|
634
874
|
__block NSString *keepVersion = nil;
|
|
635
875
|
PushyWithStateLock(^{
|
|
876
|
+
// Invalidate any in-flight cold-start round before clearing state, so
|
|
877
|
+
// a round that commits under this same lock afterwards always sees the
|
|
878
|
+
// new generation and drops its result.
|
|
879
|
+
pushyResetGeneration.fetch_add(1);
|
|
636
880
|
NSUserDefaults *defaults = PushyDefaults();
|
|
637
881
|
keepVersion = pushyLaunchVersion;
|
|
638
882
|
|
|
@@ -652,6 +896,9 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
652
896
|
}
|
|
653
897
|
[defaults removeObjectForKey:keyFirstLoadMarked];
|
|
654
898
|
[defaults removeObjectForKey:KeyPackageUpdatedMarked];
|
|
899
|
+
// A cached response still advertises the version this reset just
|
|
900
|
+
// removed; dropping it stops the JS side from reusing that answer.
|
|
901
|
+
[defaults removeObjectForKey:keyNativeCheckCache];
|
|
655
902
|
ignoreRollback = false;
|
|
656
903
|
});
|
|
657
904
|
|
|
@@ -744,31 +991,85 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
744
991
|
return;
|
|
745
992
|
}
|
|
746
993
|
|
|
994
|
+
NSString *unzipDir = [dir stringByAppendingPathComponent:hash];
|
|
995
|
+
if (PushyHasCompletedVersionAtPath(unzipDir)) {
|
|
996
|
+
callback(nil);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
NSTimeInterval deadlineUptime = PushyMonotonicNow() + 600;
|
|
1001
|
+
NSNumber *configuredDeadline = options[@"deadlineUptime"];
|
|
1002
|
+
if ([configuredDeadline isKindOfClass:[NSNumber class]]) {
|
|
1003
|
+
deadlineUptime = configuredDeadline.doubleValue;
|
|
1004
|
+
}
|
|
1005
|
+
if (deadlineUptime <= PushyMonotonicNow()) {
|
|
1006
|
+
callback(PushyDownloadDeadlineExpiredError());
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
void (^progress)(long long, long long) = ^(long long receivedBytes, long long totalBytes) {
|
|
1011
|
+
if (self->hasListeners) {
|
|
1012
|
+
[self sendEventWithName:EVENT_PROGRESS_DOWNLOAD body:@{
|
|
1013
|
+
PARAM_PROGRESS_HASH:hash,
|
|
1014
|
+
PARAM_PROGRESS_RECEIVED:@(receivedBytes),
|
|
1015
|
+
PARAM_PROGRESS_TOTAL:@(totalBytes),
|
|
1016
|
+
}];
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
void (^deferredStart)(void) = ^{
|
|
1020
|
+
[self performUpdate:type options:options callback:callback];
|
|
1021
|
+
};
|
|
1022
|
+
PushyDownloadRegistration registration = PushyRegisterDownload(
|
|
1023
|
+
hash, type, deadlineUptime, callback, progress, deferredStart);
|
|
1024
|
+
if (registration != PushyDownloadRegistrationOwner) {
|
|
1025
|
+
RCTLogInfo(
|
|
1026
|
+
@"RCTPushy -- %@ in-flight download for %@",
|
|
1027
|
+
registration == PushyDownloadRegistrationJoined ? @"join" : @"defer",
|
|
1028
|
+
hash);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
747
1032
|
NSString *zipFilePath = [dir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@%@",hash, [self zipExtension:type]]];
|
|
748
1033
|
|
|
749
1034
|
// On failure, remove the partial version directory like Android/Harmony
|
|
750
1035
|
// do: a half-unzipped/half-patched dir leaks disk and could later be
|
|
751
1036
|
// mistaken for a complete version. hash is validated non-blank above, so
|
|
752
1037
|
// this can never resolve to the download root itself.
|
|
753
|
-
NSString *unzipDir = [dir stringByAppendingPathComponent:hash];
|
|
754
1038
|
void (^completion)(NSError *) = ^(NSError *error) {
|
|
755
|
-
|
|
756
|
-
|
|
1039
|
+
// Settle every JS/native waiter only after cleanup or the atomic
|
|
1040
|
+
// completion marker write has run on the process-wide file queue.
|
|
1041
|
+
dispatch_async(self->_fileQueue, ^{
|
|
1042
|
+
NSError *finalError = error;
|
|
1043
|
+
if (finalError == nil) {
|
|
1044
|
+
NSString *marker = [unzipDir stringByAppendingPathComponent:VERSION_COMPLETE_FILE_NAME];
|
|
1045
|
+
NSError *markerError = nil;
|
|
1046
|
+
BOOL marked = [[NSData data] writeToFile:marker
|
|
1047
|
+
options:NSDataWritingAtomic
|
|
1048
|
+
error:&markerError];
|
|
1049
|
+
if (!marked) {
|
|
1050
|
+
finalError = markerError ?: PushyErrorWithCode(
|
|
1051
|
+
pushy::error_codes::kFileOperationFailed,
|
|
1052
|
+
@"failed to mark completed update");
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (finalError != nil) {
|
|
757
1056
|
[[NSFileManager defaultManager] removeItemAtPath:unzipDir error:nil];
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
|
|
1057
|
+
}
|
|
1058
|
+
PushyFinishDownload(hash, finalError);
|
|
1059
|
+
});
|
|
761
1060
|
};
|
|
762
1061
|
|
|
763
1062
|
RCTLogInfo(@"RCTPushy -- download file %@", updateUrl);
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1063
|
+
NSTimeInterval timeoutSeconds = deadlineUptime - PushyMonotonicNow();
|
|
1064
|
+
if (timeoutSeconds <= 0) {
|
|
1065
|
+
completion(PushyDownloadDeadlineExpiredError());
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
[RCTPushyDownloader download:updateUrl
|
|
1069
|
+
savePath:zipFilePath
|
|
1070
|
+
timeoutInterval:timeoutSeconds
|
|
1071
|
+
progressHandler:^(long long receivedBytes, long long totalBytes) {
|
|
1072
|
+
PushyReportDownloadProgress(hash, receivedBytes, totalBytes);
|
|
772
1073
|
} completionHandler:^(NSString *path, NSError *error) {
|
|
773
1074
|
if (error != nil) {
|
|
774
1075
|
completion(error);
|
|
@@ -934,14 +1235,7 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
934
1235
|
}
|
|
935
1236
|
|
|
936
1237
|
PushyWithStateLock(^{
|
|
937
|
-
|
|
938
|
-
pushy::state::State next = pushy::state::SwitchVersion(
|
|
939
|
-
PushyStateFromDefaults(defaults),
|
|
940
|
-
PushyToStdString(hash)
|
|
941
|
-
);
|
|
942
|
-
PushyApplyStateToDefaults(defaults, next);
|
|
943
|
-
// Re-enable first-load consumption and rollback checks for the newly selected bundle.
|
|
944
|
-
ignoreRollback = false;
|
|
1238
|
+
PushySwitchVersionLocked(hash);
|
|
945
1239
|
});
|
|
946
1240
|
return YES;
|
|
947
1241
|
}
|
|
@@ -1110,3 +1404,427 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1110
1404
|
#endif
|
|
1111
1405
|
|
|
1112
1406
|
@end
|
|
1407
|
+
|
|
1408
|
+
#pragma mark - native cold-start check orchestration
|
|
1409
|
+
|
|
1410
|
+
// Blocking JSON HTTP round-trip on the orchestrator's utility thread. Returns
|
|
1411
|
+
// the response body on 2xx, nil on any failure. The semaphore timeout is a
|
|
1412
|
+
// backstop over the request's own timeoutInterval.
|
|
1413
|
+
static NSString *PushyHttpRequest(NSString *urlString, NSString *method,
|
|
1414
|
+
NSString *body, NSTimeInterval timeout) {
|
|
1415
|
+
NSURL *url = [NSURL URLWithString:urlString];
|
|
1416
|
+
if (url == nil) {
|
|
1417
|
+
return nil;
|
|
1418
|
+
}
|
|
1419
|
+
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
|
1420
|
+
request.HTTPMethod = method;
|
|
1421
|
+
request.timeoutInterval = timeout;
|
|
1422
|
+
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
|
|
1423
|
+
if (body != nil) {
|
|
1424
|
+
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
|
1425
|
+
request.HTTPBody = [body dataUsingEncoding:NSUTF8StringEncoding];
|
|
1426
|
+
}
|
|
1427
|
+
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
1428
|
+
__block NSString *result = nil;
|
|
1429
|
+
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request
|
|
1430
|
+
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
1431
|
+
NSHTTPURLResponse *httpResponse =
|
|
1432
|
+
[response isKindOfClass:[NSHTTPURLResponse class]]
|
|
1433
|
+
? (NSHTTPURLResponse *)response
|
|
1434
|
+
: nil;
|
|
1435
|
+
NSInteger status = httpResponse.statusCode;
|
|
1436
|
+
if (error == nil && httpResponse != nil && status >= 200 &&
|
|
1437
|
+
status < 300 && data != nil) {
|
|
1438
|
+
result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
1439
|
+
}
|
|
1440
|
+
dispatch_semaphore_signal(sem);
|
|
1441
|
+
}];
|
|
1442
|
+
[task resume];
|
|
1443
|
+
if (dispatch_semaphore_wait(
|
|
1444
|
+
sem, dispatch_time(DISPATCH_TIME_NOW,
|
|
1445
|
+
(int64_t)((timeout + 5) * NSEC_PER_SEC))) != 0) {
|
|
1446
|
+
[task cancel];
|
|
1447
|
+
return nil;
|
|
1448
|
+
}
|
|
1449
|
+
return result;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
static NSString *PushyNormalizeEndpointBase(NSString *base) {
|
|
1453
|
+
while ([base hasSuffix:@"/"]) {
|
|
1454
|
+
base = [base substringToIndex:base.length - 1];
|
|
1455
|
+
}
|
|
1456
|
+
return base;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
1460
|
+
if (responseText == nil) {
|
|
1461
|
+
return NO;
|
|
1462
|
+
}
|
|
1463
|
+
bool ok = false;
|
|
1464
|
+
flowjson::Value parsed = flowjson::Parse(PushyToStdString(responseText), &ok);
|
|
1465
|
+
return ok && parsed.IsObject();
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
@implementation RCTPushyOrchestrator
|
|
1469
|
+
|
|
1470
|
+
+ (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion {
|
|
1471
|
+
#if !DEBUG
|
|
1472
|
+
// Once per process; a few seconds of delay keeps the check away from the
|
|
1473
|
+
// cold-start critical path (§7 R5) — its result targets the NEXT launch.
|
|
1474
|
+
static dispatch_once_t onceToken;
|
|
1475
|
+
dispatch_once(&onceToken, ^{
|
|
1476
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)),
|
|
1477
|
+
dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
1478
|
+
@try {
|
|
1479
|
+
[self runOnce:launchRolledBackVersion];
|
|
1480
|
+
} @catch (NSException *exception) {
|
|
1481
|
+
// The rescue path must never take the app down with it.
|
|
1482
|
+
RCTLogWarn(@"RCTPushy -- native check crashed: %@", exception.reason);
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
});
|
|
1486
|
+
#endif
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// A bare module instance drives the existing download/patch pipeline without
|
|
1490
|
+
// a bridge: the file queue is process-global and progress events are gated on
|
|
1491
|
+
// hasListeners (never set without a bridge).
|
|
1492
|
+
+ (RCTPushy *)engine {
|
|
1493
|
+
static RCTPushy *engine;
|
|
1494
|
+
static dispatch_once_t onceToken;
|
|
1495
|
+
dispatch_once(&onceToken, ^{
|
|
1496
|
+
engine = [[RCTPushy alloc] init];
|
|
1497
|
+
});
|
|
1498
|
+
return engine;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
+ (void)runOnce:(NSString *)launchRolledBackVersion {
|
|
1502
|
+
// Sampled before any IO: resetToPackagedBundle bumps it, and a reset that
|
|
1503
|
+
// lands while this round is running must win over the round's decision.
|
|
1504
|
+
const uint64_t resetGeneration = pushyResetGeneration.load();
|
|
1505
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
1506
|
+
NSString *configJson = [defaults stringForKey:keyNativeConfig];
|
|
1507
|
+
if (configJson.length == 0) {
|
|
1508
|
+
// No persisted config (old integration / first ever launch): the
|
|
1509
|
+
// native check silently does not run — this is the rollout gate.
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
bool ok = false;
|
|
1513
|
+
flowjson::Value config = flowjson::Parse(PushyToStdString(configJson), &ok);
|
|
1514
|
+
if (!ok || !config.IsObject() || config.Get("disabled").Truthy()) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
NSString *appKey = PushyFromStdString(config.Get("appKey").AsString());
|
|
1518
|
+
if (appKey.length == 0) {
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
NSString *packageVersion =
|
|
1522
|
+
PushyFromStdString(config.Get("packageVersion").AsString());
|
|
1523
|
+
if (packageVersion.length == 0) {
|
|
1524
|
+
packageVersion = [RCTPushy packageVersion];
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
__block NSString *currentVersion = nil;
|
|
1528
|
+
PushyWithStateLock(^{
|
|
1529
|
+
pushy::state::State state = PushyStateFromDefaults(PushyDefaults());
|
|
1530
|
+
currentVersion = PushyFromStdString(state.current_version);
|
|
1531
|
+
});
|
|
1532
|
+
NSString *rolledBackVersion = launchRolledBackVersion;
|
|
1533
|
+
NSString *uuid = [defaults stringForKey:keyUuid] ?: @"";
|
|
1534
|
+
|
|
1535
|
+
flowjson::Value identity = flowjson::Value::Object();
|
|
1536
|
+
identity.Set("packageVersion",
|
|
1537
|
+
flowjson::Value::String(PushyToStdString(packageVersion)));
|
|
1538
|
+
if (currentVersion != nil) {
|
|
1539
|
+
identity.Set("currentVersion",
|
|
1540
|
+
flowjson::Value::String(PushyToStdString(currentVersion)));
|
|
1541
|
+
}
|
|
1542
|
+
identity.Set("uuid", flowjson::Value::String(PushyToStdString(uuid)));
|
|
1543
|
+
if (rolledBackVersion != nil) {
|
|
1544
|
+
identity.Set("rolledBackVersion",
|
|
1545
|
+
flowjson::Value::String(PushyToStdString(rolledBackVersion)));
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
flowjson::Value cInfo = flowjson::Value::Object();
|
|
1549
|
+
cInfo.Set("rnu", config.Get("rnu"));
|
|
1550
|
+
cInfo.Set("rn", config.Get("rn"));
|
|
1551
|
+
cInfo.Set("os", flowjson::Value::String(PushyToStdString([NSString
|
|
1552
|
+
stringWithFormat:@"ios %@", [[UIDevice currentDevice] systemVersion]])));
|
|
1553
|
+
cInfo.Set("uuid", flowjson::Value::String(PushyToStdString(uuid)));
|
|
1554
|
+
|
|
1555
|
+
flowjson::Value input = flowjson::Value::Object();
|
|
1556
|
+
input.Set("packageVersion", identity.Get("packageVersion"));
|
|
1557
|
+
if (currentVersion != nil) {
|
|
1558
|
+
input.Set("currentVersion", identity.Get("currentVersion"));
|
|
1559
|
+
}
|
|
1560
|
+
input.Set("buildTime",
|
|
1561
|
+
flowjson::Value::String(PushyToStdString([RCTPushy buildTime])));
|
|
1562
|
+
input.Set("cInfo", cInfo);
|
|
1563
|
+
input.Set("supportedDiffVersion",
|
|
1564
|
+
flowjson::Value::Number(pushy::hbc::kSupportedDiffVersion));
|
|
1565
|
+
input.Set("bundleHash",
|
|
1566
|
+
flowjson::Value::String(PushyToStdString(PushyBundleHashSync())));
|
|
1567
|
+
|
|
1568
|
+
std::string bodyJson =
|
|
1569
|
+
flowjson::Stringify(updateflow::BuildCheckRequestBody(input));
|
|
1570
|
+
NSString *body = [NSString stringWithUTF8String:bodyJson.c_str()];
|
|
1571
|
+
if (body == nil) {
|
|
1572
|
+
RCTLogWarn(@"RCTPushy -- native check: request body is not valid UTF-8");
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
NSString *responseText = [self runCheckRequest:config appKey:appKey body:body];
|
|
1577
|
+
if (responseText == nil) {
|
|
1578
|
+
RCTLogInfo(@"RCTPushy -- native check: no endpoint reachable, giving up until next launch");
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
long long responseAtSeconds = (long long)[[NSDate date] timeIntervalSince1970];
|
|
1582
|
+
|
|
1583
|
+
flowjson::Value decision = updateflow::HandleCheckResponse(
|
|
1584
|
+
PushyToStdString(responseText), identity, false,
|
|
1585
|
+
config.Get("afterDownload").AsString());
|
|
1586
|
+
if (decision.Get("action").AsString() != "download") {
|
|
1587
|
+
[self commitRoundWithGeneration:resetGeneration
|
|
1588
|
+
hashInfo:nil
|
|
1589
|
+
activate:nil
|
|
1590
|
+
responseText:responseText
|
|
1591
|
+
request:body
|
|
1592
|
+
config:configJson
|
|
1593
|
+
responseAt:responseAtSeconds];
|
|
1594
|
+
RCTLogInfo(@"RCTPushy -- native check: nothing to do (%s)",
|
|
1595
|
+
decision.Get("reason").AsString().c_str());
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
NSString *hash = PushyFromStdString(decision.Get("hash").AsString());
|
|
1599
|
+
if (!PushyIsSafePathComponent(hash)) {
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
NSString *versionDir = [[RCTPushy downloadDir] stringByAppendingPathComponent:hash];
|
|
1604
|
+
BOOL downloaded = PushyHasCompletedVersionAtPath(versionDir);
|
|
1605
|
+
if (!downloaded) {
|
|
1606
|
+
downloaded = [self performAttempts:decision.Get("attempts")
|
|
1607
|
+
hash:hash
|
|
1608
|
+
originHash:currentVersion];
|
|
1609
|
+
}
|
|
1610
|
+
if (!downloaded) {
|
|
1611
|
+
[self commitRoundWithGeneration:resetGeneration
|
|
1612
|
+
hashInfo:nil
|
|
1613
|
+
activate:nil
|
|
1614
|
+
responseText:responseText
|
|
1615
|
+
request:body
|
|
1616
|
+
config:configJson
|
|
1617
|
+
responseAt:responseAtSeconds];
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// Persist name/description/metaInfo alongside the version, mirroring the
|
|
1622
|
+
// JS side's setLocalHashInfo after a successful download.
|
|
1623
|
+
const flowjson::Value &info = decision.Get("info");
|
|
1624
|
+
NSMutableDictionary *versionInfo = [NSMutableDictionary dictionary];
|
|
1625
|
+
for (const char *key : {"name", "description", "metaInfo"}) {
|
|
1626
|
+
if (info.Get(key).IsString()) {
|
|
1627
|
+
versionInfo[@(key)] = PushyFromStdString(info.Get(key).AsString()) ?: @"";
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
// Silent strategies or a server-marked forceBoot version (per-version
|
|
1631
|
+
// remote override — the brick rescue) activate for the next launch;
|
|
1632
|
+
// otherwise activation stays with the JS side (§6/§10.1).
|
|
1633
|
+
BOOL activate = decision.Get("activate").Truthy();
|
|
1634
|
+
BOOL committed = [self commitRoundWithGeneration:resetGeneration
|
|
1635
|
+
hashInfo:@{@"hash": hash, @"info": versionInfo}
|
|
1636
|
+
activate:activate ? hash : nil
|
|
1637
|
+
responseText:responseText
|
|
1638
|
+
request:body
|
|
1639
|
+
config:configJson
|
|
1640
|
+
responseAt:responseAtSeconds];
|
|
1641
|
+
if (!committed) {
|
|
1642
|
+
RCTLogInfo(@"RCTPushy -- native check: reset during round, dropping result");
|
|
1643
|
+
} else if (activate) {
|
|
1644
|
+
RCTLogInfo(@"RCTPushy -- native check: downloaded %@ and set for next launch", hash);
|
|
1645
|
+
} else {
|
|
1646
|
+
RCTLogInfo(@"RCTPushy -- native check: downloaded %@, activation left to JS", hash);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// Everything a round persists — version info, the activation, the response
|
|
1651
|
+
// cache — is written inside ONE state-lock acquisition that first re-checks the
|
|
1652
|
+
// reset generation. resetToPackagedBundle bumps that generation under the same
|
|
1653
|
+
// lock, so there is no compare-and-act window: either the whole round commits,
|
|
1654
|
+
// or the reset wins and none of it does.
|
|
1655
|
+
+ (BOOL)commitRoundWithGeneration:(uint64_t)generation
|
|
1656
|
+
hashInfo:(NSDictionary *)hashInfoEntry
|
|
1657
|
+
activate:(NSString *)hashToActivate
|
|
1658
|
+
responseText:(NSString *)responseText
|
|
1659
|
+
request:(NSString *)requestBody
|
|
1660
|
+
config:(NSString *)configJson
|
|
1661
|
+
responseAt:(long long)responseAtSeconds {
|
|
1662
|
+
NSDictionary *cacheEntry = @{
|
|
1663
|
+
@"ts": @(responseAtSeconds),
|
|
1664
|
+
@"body": responseText,
|
|
1665
|
+
@"request": requestBody,
|
|
1666
|
+
@"config": configJson,
|
|
1667
|
+
};
|
|
1668
|
+
NSData *cacheData = [NSJSONSerialization dataWithJSONObject:cacheEntry options:0 error:nil];
|
|
1669
|
+
__block BOOL committed = NO;
|
|
1670
|
+
PushyWithStateLock(^{
|
|
1671
|
+
if (pushyResetGeneration.load() != generation) {
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
1675
|
+
if (hashInfoEntry != nil) {
|
|
1676
|
+
NSData *infoData = [NSJSONSerialization dataWithJSONObject:hashInfoEntry[@"info"]
|
|
1677
|
+
options:0
|
|
1678
|
+
error:nil];
|
|
1679
|
+
if (infoData != nil) {
|
|
1680
|
+
[defaults setObject:[[NSString alloc] initWithData:infoData encoding:NSUTF8StringEncoding]
|
|
1681
|
+
forKey:PushyHashInfoKey(hashInfoEntry[@"hash"])];
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
if (hashToActivate != nil) {
|
|
1685
|
+
PushySwitchVersionLocked(hashToActivate);
|
|
1686
|
+
}
|
|
1687
|
+
if (cacheData != nil) {
|
|
1688
|
+
[defaults setObject:[[NSString alloc] initWithData:cacheData encoding:NSUTF8StringEncoding]
|
|
1689
|
+
forKey:keyNativeCheckCache];
|
|
1690
|
+
}
|
|
1691
|
+
committed = YES;
|
|
1692
|
+
});
|
|
1693
|
+
return committed;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// Sequential fallback over the ordered candidates (§5.1): one request at a
|
|
1697
|
+
// time with its own timeout; after the configured round fails, queryUrls
|
|
1698
|
+
// discovery merges remote candidates (excluding the already-tried) for one
|
|
1699
|
+
// more round. No hedged race on purpose — this path is latency-insensitive.
|
|
1700
|
+
+ (NSString *)runCheckRequest:(const flowjson::Value &)config
|
|
1701
|
+
appKey:(NSString *)appKey
|
|
1702
|
+
body:(NSString *)body {
|
|
1703
|
+
double sample = arc4random() / 4294967296.0;
|
|
1704
|
+
flowjson::Value ordered =
|
|
1705
|
+
updateflow::OrderEndpointCandidates(config.Get("endpoints"), sample);
|
|
1706
|
+
NSMutableSet<NSString *> *tried = [NSMutableSet set];
|
|
1707
|
+
const NSUInteger maxHttpAttempts = 8;
|
|
1708
|
+
NSUInteger httpAttempts = 0;
|
|
1709
|
+
for (const auto &endpoint : ordered.elements()) {
|
|
1710
|
+
NSString *base = PushyNormalizeEndpointBase(
|
|
1711
|
+
PushyFromStdString(endpoint.AsString()));
|
|
1712
|
+
if (base.length == 0 || [tried containsObject:base]) {
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
if (httpAttempts++ >= maxHttpAttempts) {
|
|
1716
|
+
return nil;
|
|
1717
|
+
}
|
|
1718
|
+
[tried addObject:base];
|
|
1719
|
+
NSString *response = PushyHttpRequest(
|
|
1720
|
+
[NSString stringWithFormat:@"%@/checkUpdate/%@", base, appKey],
|
|
1721
|
+
@"POST", body, 10);
|
|
1722
|
+
if (PushyIsValidCheckResponse(response)) {
|
|
1723
|
+
return response;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
for (const auto &queryUrl : config.Get("queryUrls").elements()) {
|
|
1727
|
+
NSString *listUrl = PushyFromStdString(queryUrl.AsString());
|
|
1728
|
+
if (listUrl == nil) {
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
if (httpAttempts++ >= maxHttpAttempts) {
|
|
1732
|
+
return nil;
|
|
1733
|
+
}
|
|
1734
|
+
NSString *listText = PushyHttpRequest(listUrl, @"GET", nil, 10);
|
|
1735
|
+
if (listText == nil) {
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
bool ok = false;
|
|
1739
|
+
flowjson::Value remote = flowjson::Parse(PushyToStdString(listText), &ok);
|
|
1740
|
+
if (!ok || !remote.IsArray()) {
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
for (const auto &endpoint : remote.elements()) {
|
|
1744
|
+
NSString *base = PushyNormalizeEndpointBase(
|
|
1745
|
+
PushyFromStdString(endpoint.AsString()));
|
|
1746
|
+
if (base.length == 0 || [tried containsObject:base]) {
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
if (httpAttempts++ >= maxHttpAttempts) {
|
|
1750
|
+
return nil;
|
|
1751
|
+
}
|
|
1752
|
+
[tried addObject:base];
|
|
1753
|
+
NSString *response = PushyHttpRequest(
|
|
1754
|
+
[NSString stringWithFormat:@"%@/checkUpdate/%@", base, appKey],
|
|
1755
|
+
@"POST", body, 10);
|
|
1756
|
+
if (PushyIsValidCheckResponse(response)) {
|
|
1757
|
+
return response;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
break; // one successfully fetched remote list is enough
|
|
1761
|
+
}
|
|
1762
|
+
return nil;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
+ (BOOL)performAttempts:(const flowjson::Value &)attempts
|
|
1766
|
+
hash:(NSString *)hash
|
|
1767
|
+
originHash:(NSString *)originHash {
|
|
1768
|
+
RCTPushy *engine = [self engine];
|
|
1769
|
+
NSTimeInterval incrementalDeadline = PushyMonotonicNow() + 600;
|
|
1770
|
+
NSTimeInterval fullDeadline = 0;
|
|
1771
|
+
for (const auto &attempt : attempts.elements()) {
|
|
1772
|
+
const std::string &type = attempt.Get("type").AsString();
|
|
1773
|
+
PushyType pushyType = type == "diff" ? PushyTypePatchFromPpk
|
|
1774
|
+
: type == "pdiff" ? PushyTypePatchFromPackage
|
|
1775
|
+
: PushyTypeFullDownload;
|
|
1776
|
+
if (pushyType == PushyTypePatchFromPpk && originHash.length == 0) {
|
|
1777
|
+
continue; // diff patches from the running version; none running
|
|
1778
|
+
}
|
|
1779
|
+
BOOL isFullAttempt = pushyType == PushyTypeFullDownload;
|
|
1780
|
+
if (isFullAttempt && fullDeadline == 0) {
|
|
1781
|
+
// diff/pdiff cannot starve the last-resort full download.
|
|
1782
|
+
fullDeadline = PushyMonotonicNow() + 600;
|
|
1783
|
+
}
|
|
1784
|
+
NSTimeInterval deadline = isFullAttempt ? fullDeadline : incrementalDeadline;
|
|
1785
|
+
for (const auto &urlValue : attempt.Get("urls").elements()) {
|
|
1786
|
+
NSString *url = PushyFromStdString(urlValue.AsString());
|
|
1787
|
+
if (url == nil) {
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
NSTimeInterval remaining = deadline - PushyMonotonicNow();
|
|
1791
|
+
if (remaining <= 0) {
|
|
1792
|
+
if (isFullAttempt) {
|
|
1793
|
+
return NO;
|
|
1794
|
+
}
|
|
1795
|
+
break;
|
|
1796
|
+
}
|
|
1797
|
+
NSMutableDictionary *options =
|
|
1798
|
+
[@{
|
|
1799
|
+
@"updateUrl": url,
|
|
1800
|
+
@"hash": hash,
|
|
1801
|
+
@"deadlineUptime": @(deadline),
|
|
1802
|
+
} mutableCopy];
|
|
1803
|
+
if (pushyType == PushyTypePatchFromPpk) {
|
|
1804
|
+
options[@"originHash"] = originHash;
|
|
1805
|
+
}
|
|
1806
|
+
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
1807
|
+
__block NSError *resultError = nil;
|
|
1808
|
+
[engine performUpdate:pushyType options:options callback:^(NSError *error) {
|
|
1809
|
+
resultError = error;
|
|
1810
|
+
dispatch_semaphore_signal(sem);
|
|
1811
|
+
}];
|
|
1812
|
+
if (dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW,
|
|
1813
|
+
(int64_t)(remaining * NSEC_PER_SEC))) != 0) {
|
|
1814
|
+
RCTLogWarn(@"RCTPushy -- native check: %s attempt timed out", type.c_str());
|
|
1815
|
+
if (isFullAttempt) {
|
|
1816
|
+
return NO;
|
|
1817
|
+
}
|
|
1818
|
+
break;
|
|
1819
|
+
}
|
|
1820
|
+
if (resultError == nil) {
|
|
1821
|
+
return YES;
|
|
1822
|
+
}
|
|
1823
|
+
RCTLogInfo(@"RCTPushy -- native check: %s attempt failed: %@",
|
|
1824
|
+
type.c_str(), resultError.localizedDescription);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
return NO;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
@end
|