react-native-update 10.49.0 → 10.51.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/BUNDLEHASH_MIGRATION.md +22 -2
- 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/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/BundledResourceCopier.java +42 -9
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -22
- 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/PatchFailedException.java +16 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +196 -42
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +56 -3
- 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/patch_core/digest.cpp +53 -0
- package/cpp/patch_core/digest.h +18 -0
- package/cpp/patch_core/patch_core.cpp +17 -1
- package/cpp/patch_core/patch_core.h +8 -0
- package/cpp/patch_core/tests/patch_core_test.cpp +99 -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 +150 -1
- package/harmony/pushy/src/main/ets/DownloadTask.ts +133 -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 +16 -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 +152 -52
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +840 -68
- 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 +439 -122
- package/src/endpoint.ts +105 -80
- package/src/error.ts +4 -0
- package/src/locales/en.ts +4 -0
- package/src/locales/zh.ts +4 -0
- package/src/provider.tsx +74 -9
- package/src/telemetry.ts +8 -0
- package/src/type.ts +31 -0
- package/src/updateFlowCore.ts +320 -0
- package/src/utils.ts +14 -12
- package/src/isInRollout.ts +0 -86
- 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) {
|
|
@@ -139,6 +299,25 @@ static BOOL PushyStringIsBlank(NSString *value) {
|
|
|
139
299
|
return [[value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] == 0;
|
|
140
300
|
}
|
|
141
301
|
|
|
302
|
+
// Server-provided identifiers (hash/originHash) are used as child names under
|
|
303
|
+
// the download root; anything that could resolve outside of it (path
|
|
304
|
+
// separators, "..", ".") must be rejected before touching the filesystem.
|
|
305
|
+
static BOOL PushyIsSafePathComponent(NSString *value) {
|
|
306
|
+
if (PushyStringIsBlank(value)) {
|
|
307
|
+
return NO;
|
|
308
|
+
}
|
|
309
|
+
if ([value isEqualToString:@"."] || [value isEqualToString:@".."]) {
|
|
310
|
+
return NO;
|
|
311
|
+
}
|
|
312
|
+
if ([value containsString:@"/"] || [value containsString:@"\\"]) {
|
|
313
|
+
return NO;
|
|
314
|
+
}
|
|
315
|
+
if ([value rangeOfString:@"\0"].location != NSNotFound) {
|
|
316
|
+
return NO;
|
|
317
|
+
}
|
|
318
|
+
return YES;
|
|
319
|
+
}
|
|
320
|
+
|
|
142
321
|
static void PushyRejectError(RCTPromiseRejectBlock reject, NSError *error) {
|
|
143
322
|
// Prefer the stable cross-platform code (error_codes.h); fall back to the
|
|
144
323
|
// numeric NSError code for system errors that were not classified.
|
|
@@ -162,15 +341,28 @@ static pushy::patch::PatchManifest PushyPatchManifestFromJson(NSDictionary *json
|
|
|
162
341
|
pushy::patch::PatchManifest manifest;
|
|
163
342
|
|
|
164
343
|
NSDictionary *copies = json[@"copies"];
|
|
344
|
+
// Content checksum per copy target ("copiesCrc", pdiff manifests from
|
|
345
|
+
// CLI >= 2.21.2). The patch core verifies the copy source against it and
|
|
346
|
+
// fails the patch on mismatch, so the JS strategy chain falls back to the
|
|
347
|
+
// full package instead of copying drifted bytes from a rebuilt binary.
|
|
348
|
+
NSDictionary *copiesCrc = json[@"copiesCrc"];
|
|
349
|
+
if (![copiesCrc isKindOfClass:[NSDictionary class]]) {
|
|
350
|
+
copiesCrc = nil;
|
|
351
|
+
}
|
|
165
352
|
for (NSString *to in copies) {
|
|
166
353
|
NSString *from = copies[to];
|
|
167
354
|
if (from.length <= 0) {
|
|
168
355
|
from = to;
|
|
169
356
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
357
|
+
pushy::patch::CopyOperation operation;
|
|
358
|
+
operation.from = PushyToStdString(from);
|
|
359
|
+
operation.to = PushyToStdString(to);
|
|
360
|
+
NSNumber *expectedCrc = copiesCrc[to];
|
|
361
|
+
if ([expectedCrc isKindOfClass:[NSNumber class]]) {
|
|
362
|
+
operation.has_expected_crc = true;
|
|
363
|
+
operation.expected_crc = (uint32_t)[expectedCrc unsignedLongLongValue];
|
|
364
|
+
}
|
|
365
|
+
manifest.copies.push_back(operation);
|
|
174
366
|
}
|
|
175
367
|
|
|
176
368
|
NSDictionary *deletes = json[@"deletes"];
|
|
@@ -223,6 +415,21 @@ static void PushyApplyStateToDefaults(NSUserDefaults *defaults, const pushy::sta
|
|
|
223
415
|
PushyFromStdString(state.rolled_back_version));
|
|
224
416
|
}
|
|
225
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
|
+
|
|
226
433
|
@interface RCTPushy ()
|
|
227
434
|
- (void)downloadUpdate:(PushyType)type
|
|
228
435
|
options:(NSDictionary *)options
|
|
@@ -248,11 +455,66 @@ static void PushyApplyStateToDefaults(NSUserDefaults *defaults, const pushy::sta
|
|
|
248
455
|
callback:(void (^)(NSError *error))callback;
|
|
249
456
|
- (BOOL)switchVersion:(NSString *)hash error:(NSError **)error;
|
|
250
457
|
- (BOOL)ensureDirectoryExistsAtPath:(NSString *)path;
|
|
458
|
+
+ (void)excludeFromBackup:(NSString *)path;
|
|
251
459
|
- (void)unzipFileAtPath:(NSString *)path
|
|
252
460
|
toDestination:(NSString *)destination
|
|
253
461
|
completionHandler:(void (^)(NSError *error))completionHandler;
|
|
462
|
+
+ (NSString *)downloadDir;
|
|
463
|
+
+ (NSURL *)binaryBundleURL;
|
|
464
|
+
+ (NSString *)packageVersion;
|
|
465
|
+
+ (NSString *)buildTime;
|
|
466
|
+
@end
|
|
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;
|
|
254
482
|
@end
|
|
255
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
|
+
|
|
256
518
|
@implementation RCTPushy {
|
|
257
519
|
dispatch_queue_t _fileQueue;
|
|
258
520
|
bool hasListeners;
|
|
@@ -263,8 +525,10 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
263
525
|
+ (NSURL *)bundleURL
|
|
264
526
|
{
|
|
265
527
|
__block NSURL *resolvedURL = nil;
|
|
266
|
-
|
|
267
|
-
|
|
528
|
+
__block NSString *launchRolledBackVersion = nil;
|
|
529
|
+
@try {
|
|
530
|
+
PushyWithStateLock(^{
|
|
531
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
268
532
|
|
|
269
533
|
NSString *curPackageVersion = [RCTPushy packageVersion];
|
|
270
534
|
NSString *curBuildTime = [RCTPushy buildTime];
|
|
@@ -318,7 +582,7 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
318
582
|
if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:NULL]) {
|
|
319
583
|
pushyLaunchVersion = loadVersion;
|
|
320
584
|
resolvedURL = [NSURL fileURLWithPath:bundlePath];
|
|
321
|
-
|
|
585
|
+
break;
|
|
322
586
|
} else {
|
|
323
587
|
RCTLogError(@"RCTPushy -- bundle version %@ not found, rolling back", loadVersion);
|
|
324
588
|
state = pushy::state::Rollback(state);
|
|
@@ -327,9 +591,18 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
327
591
|
}
|
|
328
592
|
}
|
|
329
593
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
+
}
|
|
333
606
|
}
|
|
334
607
|
|
|
335
608
|
+ (NSString *) rollback {
|
|
@@ -419,6 +692,36 @@ RCT_EXPORT_METHOD(setUuid:(NSString *)uuid resolver:(RCTPromiseResolveBlock)res
|
|
|
419
692
|
resolve(@true);
|
|
420
693
|
}
|
|
421
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
|
+
|
|
422
725
|
RCT_EXPORT_METHOD(setLocalHashInfo:(NSString *)hash
|
|
423
726
|
value:(NSString *)value resolver:(RCTPromiseResolveBlock)resolve
|
|
424
727
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
@@ -532,37 +835,7 @@ RCT_EXPORT_METHOD(getBundleHash:(RCTPromiseResolveBlock)resolve
|
|
|
532
835
|
resolve(@"");
|
|
533
836
|
#else
|
|
534
837
|
dispatch_async(_fileQueue, ^{
|
|
535
|
-
|
|
536
|
-
if (path == nil) {
|
|
537
|
-
resolve(@"");
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
NSDictionary *attributes =
|
|
541
|
-
[[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
|
|
542
|
-
if (attributes == nil) {
|
|
543
|
-
resolve(@"");
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
NSString *cacheKey = [NSString stringWithFormat:@"%@|%llu|%.0f",
|
|
547
|
-
[RCTPushy packageVersion],
|
|
548
|
-
attributes.fileSize,
|
|
549
|
-
[attributes.fileModificationDate timeIntervalSince1970]];
|
|
550
|
-
|
|
551
|
-
NSUserDefaults *defaults = PushyDefaults();
|
|
552
|
-
NSString *cached = [defaults stringForKey:keyBundleHashCache];
|
|
553
|
-
NSString *cachedPrefix = [cacheKey stringByAppendingString:@"|"];
|
|
554
|
-
if ([cached hasPrefix:cachedPrefix]) {
|
|
555
|
-
resolve([cached substringFromIndex:cachedPrefix.length]);
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
NSString *hash = PushyFromStdString(
|
|
560
|
-
pushy::digest::Sha256File(PushyToStdString(path))) ?: @"";
|
|
561
|
-
if (hash.length > 0) {
|
|
562
|
-
[defaults setObject:[cachedPrefix stringByAppendingString:hash]
|
|
563
|
-
forKey:keyBundleHashCache];
|
|
564
|
-
}
|
|
565
|
-
resolve(hash);
|
|
838
|
+
resolve(PushyBundleHashSync());
|
|
566
839
|
});
|
|
567
840
|
#endif
|
|
568
841
|
}
|
|
@@ -600,6 +873,10 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
600
873
|
// for gray release bucketing and must not change on reset.
|
|
601
874
|
__block NSString *keepVersion = nil;
|
|
602
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);
|
|
603
880
|
NSUserDefaults *defaults = PushyDefaults();
|
|
604
881
|
keepVersion = pushyLaunchVersion;
|
|
605
882
|
|
|
@@ -619,6 +896,9 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
619
896
|
}
|
|
620
897
|
[defaults removeObjectForKey:keyFirstLoadMarked];
|
|
621
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];
|
|
622
902
|
ignoreRollback = false;
|
|
623
903
|
});
|
|
624
904
|
|
|
@@ -694,12 +974,12 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
694
974
|
NSString *updateUrl = PushyOptionString(options, @"updateUrl");
|
|
695
975
|
NSString *hash = PushyOptionString(options, @"hash");
|
|
696
976
|
|
|
697
|
-
if (PushyStringIsBlank(updateUrl) ||
|
|
977
|
+
if (PushyStringIsBlank(updateUrl) || !PushyIsSafePathComponent(hash)) {
|
|
698
978
|
callback(PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
|
|
699
979
|
return;
|
|
700
980
|
}
|
|
701
981
|
NSString *originHash = PushyOptionString(options, @"originHash");
|
|
702
|
-
if (type == PushyTypePatchFromPpk &&
|
|
982
|
+
if (type == PushyTypePatchFromPpk && !PushyIsSafePathComponent(originHash)) {
|
|
703
983
|
callback(PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS));
|
|
704
984
|
return;
|
|
705
985
|
}
|
|
@@ -711,31 +991,85 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
711
991
|
return;
|
|
712
992
|
}
|
|
713
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
|
+
|
|
714
1032
|
NSString *zipFilePath = [dir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@%@",hash, [self zipExtension:type]]];
|
|
715
1033
|
|
|
716
1034
|
// On failure, remove the partial version directory like Android/Harmony
|
|
717
1035
|
// do: a half-unzipped/half-patched dir leaks disk and could later be
|
|
718
1036
|
// mistaken for a complete version. hash is validated non-blank above, so
|
|
719
1037
|
// this can never resolve to the download root itself.
|
|
720
|
-
NSString *unzipDir = [dir stringByAppendingPathComponent:hash];
|
|
721
1038
|
void (^completion)(NSError *) = ^(NSError *error) {
|
|
722
|
-
|
|
723
|
-
|
|
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) {
|
|
724
1056
|
[[NSFileManager defaultManager] removeItemAtPath:unzipDir error:nil];
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
|
|
1057
|
+
}
|
|
1058
|
+
PushyFinishDownload(hash, finalError);
|
|
1059
|
+
});
|
|
728
1060
|
};
|
|
729
1061
|
|
|
730
1062
|
RCTLogInfo(@"RCTPushy -- download file %@", updateUrl);
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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);
|
|
739
1073
|
} completionHandler:^(NSString *path, NSError *error) {
|
|
740
1074
|
if (error != nil) {
|
|
741
1075
|
completion(error);
|
|
@@ -893,7 +1227,7 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
893
1227
|
|
|
894
1228
|
- (BOOL)switchVersion:(NSString *)hash error:(NSError **)error
|
|
895
1229
|
{
|
|
896
|
-
if (
|
|
1230
|
+
if (!PushyIsSafePathComponent(hash)) {
|
|
897
1231
|
if (error != NULL) {
|
|
898
1232
|
*error = PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS);
|
|
899
1233
|
}
|
|
@@ -901,14 +1235,7 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
901
1235
|
}
|
|
902
1236
|
|
|
903
1237
|
PushyWithStateLock(^{
|
|
904
|
-
|
|
905
|
-
pushy::state::State next = pushy::state::SwitchVersion(
|
|
906
|
-
PushyStateFromDefaults(defaults),
|
|
907
|
-
PushyToStdString(hash)
|
|
908
|
-
);
|
|
909
|
-
PushyApplyStateToDefaults(defaults, next);
|
|
910
|
-
// Re-enable first-load consumption and rollback checks for the newly selected bundle.
|
|
911
|
-
ignoreRollback = false;
|
|
1238
|
+
PushySwitchVersionLocked(hash);
|
|
912
1239
|
});
|
|
913
1240
|
return YES;
|
|
914
1241
|
}
|
|
@@ -922,6 +1249,10 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
922
1249
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
923
1250
|
BOOL isDirectory = NO;
|
|
924
1251
|
if ([fileManager fileExistsAtPath:path isDirectory:&isDirectory]) {
|
|
1252
|
+
if (isDirectory) {
|
|
1253
|
+
// Directories created by older versions never got the flag.
|
|
1254
|
+
[RCTPushy excludeFromBackup:path];
|
|
1255
|
+
}
|
|
925
1256
|
return isDirectory;
|
|
926
1257
|
}
|
|
927
1258
|
|
|
@@ -933,10 +1264,27 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
933
1264
|
if (!success && error != nil) {
|
|
934
1265
|
RCTLogWarn(@"Pushy create directory error: %@", error.localizedDescription);
|
|
935
1266
|
}
|
|
1267
|
+
if (success) {
|
|
1268
|
+
[RCTPushy excludeFromBackup:path];
|
|
1269
|
+
}
|
|
936
1270
|
|
|
937
1271
|
return success;
|
|
938
1272
|
}
|
|
939
1273
|
|
|
1274
|
+
// Everything under rctpushy is re-downloadable, and Application Support is
|
|
1275
|
+
// backed up to iCloud by default — Apple requires such content to be
|
|
1276
|
+
// excluded from backups.
|
|
1277
|
+
+ (void)excludeFromBackup:(NSString *)path
|
|
1278
|
+
{
|
|
1279
|
+
NSURL *url = [NSURL fileURLWithPath:path isDirectory:YES];
|
|
1280
|
+
NSError *error = nil;
|
|
1281
|
+
if (![url setResourceValue:@YES
|
|
1282
|
+
forKey:NSURLIsExcludedFromBackupKey
|
|
1283
|
+
error:&error]) {
|
|
1284
|
+
RCTLogWarn(@"Pushy exclude from backup error: %@", error.localizedDescription);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
940
1288
|
- (void)unzipFileAtPath:(NSString *)path
|
|
941
1289
|
toDestination:(NSString *)destination
|
|
942
1290
|
completionHandler:(void (^)(NSError *error))completionHandler
|
|
@@ -1056,3 +1404,427 @@ RCT_EXPORT_METHOD(resetToPackagedBundle:(RCTPromiseResolveBlock)resolve
|
|
|
1056
1404
|
#endif
|
|
1057
1405
|
|
|
1058
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
|