react-native-update 10.56.1 → 10.57.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/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +102 -11
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateConfig.java +99 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateResult.java +51 -0
- package/android/src/main/java/cn/reactnative/modules/update/PushyNativeUpdate.java +127 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +35 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +1 -1
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +121 -4
- package/harmony/pushy/src/main/ets/NativeUpdateConfig.ts +102 -0
- package/harmony/pushy/src/main/ets/NativeUpdateResult.ts +32 -0
- package/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets +15 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +1 -1
- package/harmony/pushy/src/main/ets/UpdateContext.ts +25 -0
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.h +24 -0
- package/ios/RCTPushy/RCTPushy.mm +191 -12
- package/ios/RCTPushy/RCTPushyNativeConfig.h +5 -0
- package/ios/RCTPushy/RCTPushyNativeConfig.mm +103 -0
- package/package.json +1 -1
- package/src/NativePushy.ts +3 -2
- package/src/client.ts +8 -0
- package/src/type.ts +7 -0
package/ios/RCTPushy/RCTPushy.mm
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#import "RCTPushy.h"
|
|
2
|
+
#import "RCTPushyNativeConfig.h"
|
|
2
3
|
#import "RCTPushyDownloader.h"
|
|
3
4
|
#import "ZipArchive.h"
|
|
4
5
|
#include "../../cpp/patch_core/archive_limits.h"
|
|
@@ -36,6 +37,13 @@
|
|
|
36
37
|
#include <atomic>
|
|
37
38
|
#include <sys/stat.h>
|
|
38
39
|
|
|
40
|
+
// Immutable host-facing snapshot; activated always means NEXT launch.
|
|
41
|
+
static NSDictionary *PushyHostResult(NSString *status, NSString *reason,
|
|
42
|
+
NSString *hash, BOOL activated) {
|
|
43
|
+
return @{@"status": status, @"reason": reason ?: @"",
|
|
44
|
+
@"hash": hash ?: @"", @"activated": @(activated)};
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
static NSString *const keyPushyInfo = @"REACTNATIVECN_PUSHY_INFO_KEY";
|
|
40
48
|
// Binary identity (SyncBinaryVersion input). Prefixed like every other key:
|
|
41
49
|
// the unprefixed names collided with generic host-app/SDK defaults, and a
|
|
@@ -720,6 +728,9 @@ static void PushySwitchVersionLocked(NSString *hash) {
|
|
|
720
728
|
// bundle — this is what lets a bricked hot update be replaced on the next
|
|
721
729
|
// launch. Decisions come from cpp/update_flow_core; this class is IO glue.
|
|
722
730
|
@interface RCTPushyOrchestrator : NSObject
|
|
731
|
+
+ (void)persistConfiguration:(NSString *)config;
|
|
732
|
+
+ (BOOL)hasRunnableConfig;
|
|
733
|
+
+ (NSDictionary *)checkAndUpdate;
|
|
723
734
|
+ (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion;
|
|
724
735
|
+ (void)markJsCheckCompleted:(NSString *)config;
|
|
725
736
|
+ (void)startRoundWithDeadline:(NSTimeInterval)deadlineUptime;
|
|
@@ -761,6 +772,14 @@ static NSTimeInterval pushyProcessAnchorUptime = 0;
|
|
|
761
772
|
// (markJsCheckCompleted). Process-scoped by design. Guarded by
|
|
762
773
|
// @synchronized (RCTPushyOrchestrator class).
|
|
763
774
|
static NSString *pushyJsCompletedConfig = nil;
|
|
775
|
+
// The group supplements (rather than consumes) the crash-rescue semaphore.
|
|
776
|
+
static dispatch_group_t pushyHostRoundGroup;
|
|
777
|
+
static std::atomic<bool> pushyNativeCheckReady{false};
|
|
778
|
+
static NSDictionary *pushyHostRoundResult = nil;
|
|
779
|
+
static NSString *pushyHostRoundConfig = nil;
|
|
780
|
+
static uint64_t pushyHostRoundGeneration = 0;
|
|
781
|
+
static std::atomic<uint64_t> pushyNativeConfigGeneration{0};
|
|
782
|
+
static uint64_t pushyHostRoundConfigGeneration = 0;
|
|
764
783
|
|
|
765
784
|
static const NSTimeInterval kPushyRescueTriggerUptime = 60;
|
|
766
785
|
static const NSTimeInterval kPushyRescueBudgetBackgroundThread = 10;
|
|
@@ -1008,6 +1027,59 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
1008
1027
|
return currentVersion;
|
|
1009
1028
|
}
|
|
1010
1029
|
|
|
1030
|
+
+ (void)configure:(NSDictionary<NSString *, id> *)options
|
|
1031
|
+
completion:(RCTPushyNativeConfigurationCompletion)completion
|
|
1032
|
+
{
|
|
1033
|
+
NSError *validationError = nil;
|
|
1034
|
+
// Snapshot nested mutable caller values before crossing a queue boundary.
|
|
1035
|
+
NSString *config = RCTPushyNormalizeNativeConfig(options, &validationError);
|
|
1036
|
+
static dispatch_queue_t configQueue;
|
|
1037
|
+
static dispatch_once_t once;
|
|
1038
|
+
dispatch_once(&once, ^{
|
|
1039
|
+
configQueue = dispatch_queue_create("cn.reactnative.pushy.host-config", DISPATCH_QUEUE_SERIAL);
|
|
1040
|
+
});
|
|
1041
|
+
dispatch_async(configQueue, ^{
|
|
1042
|
+
NSError *failure = validationError;
|
|
1043
|
+
if (config != nil) {
|
|
1044
|
+
@try {
|
|
1045
|
+
[RCTPushyOrchestrator persistConfiguration:config];
|
|
1046
|
+
} @catch (NSException *exception) {
|
|
1047
|
+
failure = PushyErrorWithCode(pushy::error_codes::kFileOperationFailed,
|
|
1048
|
+
exception.reason ?: @"Native configuration failed");
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (completion != nil) {
|
|
1052
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1053
|
+
completion(failure);
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
+ (void)checkAndUpdateWithCompletion:(RCTPushyNativeUpdateCompletion)completion
|
|
1060
|
+
{
|
|
1061
|
+
static dispatch_queue_t hostQueue;
|
|
1062
|
+
static dispatch_once_t once;
|
|
1063
|
+
dispatch_once(&once, ^{
|
|
1064
|
+
hostQueue = dispatch_queue_create("cn.reactnative.pushy.host-check", DISPATCH_QUEUE_SERIAL);
|
|
1065
|
+
});
|
|
1066
|
+
dispatch_async(hostQueue, ^{
|
|
1067
|
+
NSDictionary *result;
|
|
1068
|
+
@try {
|
|
1069
|
+
result = [RCTPushyOrchestrator checkAndUpdate];
|
|
1070
|
+
} @catch (NSException *exception) {
|
|
1071
|
+
RCTLogWarn(@"RCTPushy -- native host check failed: %@", exception.reason);
|
|
1072
|
+
result = PushyHostResult(@"failed", @"internal_error", nil, NO);
|
|
1073
|
+
}
|
|
1074
|
+
if (completion != nil) {
|
|
1075
|
+
NSDictionary *snapshot = [result copy];
|
|
1076
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1077
|
+
completion(snapshot);
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1011
1083
|
+ (BOOL)requiresMainQueueSetup
|
|
1012
1084
|
{
|
|
1013
1085
|
return NO;
|
|
@@ -1115,8 +1187,13 @@ RCT_EXPORT_METHOD(syncNativeConfig:(NSString *)config
|
|
|
1115
1187
|
error != nil ? error.localizedDescription : ERROR_OPTIONS));
|
|
1116
1188
|
return;
|
|
1117
1189
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1190
|
+
@try {
|
|
1191
|
+
[RCTPushyOrchestrator persistConfiguration:config];
|
|
1192
|
+
resolve(@true);
|
|
1193
|
+
} @catch (NSException *exception) {
|
|
1194
|
+
PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kFileOperationFailed,
|
|
1195
|
+
exception.reason ?: @"Native configuration failed"));
|
|
1196
|
+
}
|
|
1120
1197
|
}
|
|
1121
1198
|
|
|
1122
1199
|
RCT_EXPORT_METHOD(getNativeCheckCache:(RCTPromiseResolveBlock)resolve
|
|
@@ -2138,6 +2215,39 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2138
2215
|
|
|
2139
2216
|
@implementation RCTPushyOrchestrator
|
|
2140
2217
|
|
|
2218
|
+
+ (void)persistConfiguration:(NSString *)config {
|
|
2219
|
+
PushyWithStateLock(^{
|
|
2220
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
2221
|
+
if ([defaults stringForKey:keyUuid].length == 0) {
|
|
2222
|
+
[defaults setObject:[NSUUID UUID].UUIDString forKey:keyUuid];
|
|
2223
|
+
}
|
|
2224
|
+
if ([[defaults stringForKey:keyNativeConfig] isEqualToString:config]) {
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
// The same generation protects reset and replacement of the request
|
|
2228
|
+
// identity/policy, including a late crash-rescue activation.
|
|
2229
|
+
pushyResetGeneration.fetch_add(1);
|
|
2230
|
+
pushyNativeConfigGeneration.fetch_add(1);
|
|
2231
|
+
[defaults setObject:config forKey:keyNativeConfig];
|
|
2232
|
+
[defaults removeObjectForKey:keyNativeCheckCache];
|
|
2233
|
+
[self markJsCheckCompleted:nil];
|
|
2234
|
+
});
|
|
2235
|
+
if (pushyNativeCheckReady.load() && [self hasRunnableConfig]) {
|
|
2236
|
+
PushyInstallCrashRescueHandler();
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
+ (BOOL)hasRunnableConfig {
|
|
2241
|
+
NSString *json = [PushyDefaults() stringForKey:keyNativeConfig];
|
|
2242
|
+
if (json.length == 0) {
|
|
2243
|
+
return NO;
|
|
2244
|
+
}
|
|
2245
|
+
bool ok = false;
|
|
2246
|
+
flowjson::Value config = flowjson::Parse(PushyToStdString(json), &ok);
|
|
2247
|
+
return ok && config.IsObject() && !config.Get("disabled").Truthy()
|
|
2248
|
+
&& !config.Get("appKey").AsString().empty();
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2141
2251
|
+ (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion {
|
|
2142
2252
|
#if !DEBUG
|
|
2143
2253
|
// Once per process; a few seconds of delay keeps the check away from the
|
|
@@ -2147,8 +2257,11 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2147
2257
|
static dispatch_once_t onceToken;
|
|
2148
2258
|
dispatch_once(&onceToken, ^{
|
|
2149
2259
|
pushyRoundDone = dispatch_semaphore_create(0);
|
|
2260
|
+
pushyHostRoundGroup = dispatch_group_create();
|
|
2261
|
+
dispatch_group_enter(pushyHostRoundGroup);
|
|
2150
2262
|
pushyProcessAnchorUptime = PushyMonotonicNow();
|
|
2151
2263
|
pushyLaunchRolledBackForRescue = [launchRolledBackVersion copy];
|
|
2264
|
+
pushyNativeCheckReady.store(true);
|
|
2152
2265
|
NSUserDefaults *defaults = PushyDefaults();
|
|
2153
2266
|
// The crash-hold rescue shares the orchestrator's rollout gate: no
|
|
2154
2267
|
// persisted config, no handler (§11.3).
|
|
@@ -2171,6 +2284,49 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2171
2284
|
#endif
|
|
2172
2285
|
}
|
|
2173
2286
|
|
|
2287
|
+
+ (NSDictionary *)checkAndUpdate {
|
|
2288
|
+
#if DEBUG
|
|
2289
|
+
return PushyHostResult(@"skipped", @"debug", nil, NO);
|
|
2290
|
+
#else
|
|
2291
|
+
if (!pushyNativeCheckReady.load()) {
|
|
2292
|
+
return PushyHostResult(@"skipped", @"not_initialized", nil, NO);
|
|
2293
|
+
}
|
|
2294
|
+
NSString *configJson = [PushyDefaults() stringForKey:keyNativeConfig];
|
|
2295
|
+
if (configJson.length == 0) {
|
|
2296
|
+
return PushyHostResult(@"skipped", @"not_configured", nil, NO);
|
|
2297
|
+
}
|
|
2298
|
+
id config = [NSJSONSerialization JSONObjectWithData:
|
|
2299
|
+
[configJson dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
|
|
2300
|
+
if (![config isKindOfClass:NSDictionary.class]) {
|
|
2301
|
+
return PushyHostResult(@"failed", @"invalid_config", nil, NO);
|
|
2302
|
+
}
|
|
2303
|
+
id disabled = config[@"disabled"];
|
|
2304
|
+
if ([disabled respondsToSelector:@selector(boolValue)] && [disabled boolValue]) {
|
|
2305
|
+
return PushyHostResult(@"skipped", @"disabled", nil, NO);
|
|
2306
|
+
}
|
|
2307
|
+
id appKey = config[@"appKey"];
|
|
2308
|
+
if (![appKey isKindOfClass:NSString.class] || [appKey length] == 0) {
|
|
2309
|
+
return PushyHostResult(@"failed", @"invalid_config", nil, NO);
|
|
2310
|
+
}
|
|
2311
|
+
[self startRoundWithDeadline:0];
|
|
2312
|
+
// A group is broadcast-style. Sharing the rescue semaphore would let one
|
|
2313
|
+
// waiter consume the only signal and leave the other waiting forever.
|
|
2314
|
+
if (!pushyRoundStarted.load()) {
|
|
2315
|
+
return PushyHostResult(@"skipped", @"config_changed", nil, NO);
|
|
2316
|
+
}
|
|
2317
|
+
dispatch_group_wait(pushyHostRoundGroup, DISPATCH_TIME_FOREVER);
|
|
2318
|
+
if (pushyHostRoundConfigGeneration != pushyNativeConfigGeneration.load()
|
|
2319
|
+
|| ![configJson isEqualToString:pushyHostRoundConfig]
|
|
2320
|
+
|| ![configJson isEqualToString:[PushyDefaults() stringForKey:keyNativeConfig]]) {
|
|
2321
|
+
return PushyHostResult(@"cancelled", @"config_changed", nil, NO);
|
|
2322
|
+
}
|
|
2323
|
+
if (pushyHostRoundGeneration != pushyResetGeneration.load()) {
|
|
2324
|
+
return PushyHostResult(@"cancelled", @"reset", nil, NO);
|
|
2325
|
+
}
|
|
2326
|
+
return pushyHostRoundResult ?: PushyHostResult(@"failed", @"internal_error", nil, NO);
|
|
2327
|
+
#endif
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2174
2330
|
+ (void)markJsCheckCompleted:(NSString *)config {
|
|
2175
2331
|
@synchronized (RCTPushyOrchestrator.class) {
|
|
2176
2332
|
pushyJsCompletedConfig = [config copy];
|
|
@@ -2197,6 +2353,10 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2197
2353
|
// started it yet. deadlineUptime > 0 (crash rescue) caps every HTTP call and
|
|
2198
2354
|
// download phase to the remaining budget.
|
|
2199
2355
|
+ (void)startRoundWithDeadline:(NSTimeInterval)deadlineUptime {
|
|
2356
|
+
// Missing/disabled configuration is a preflight skip, not a used round.
|
|
2357
|
+
if (![self hasRunnableConfig]) {
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2200
2360
|
bool expected = false;
|
|
2201
2361
|
if (!pushyRoundStarted.compare_exchange_strong(expected, true)) {
|
|
2202
2362
|
return;
|
|
@@ -2206,9 +2366,11 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2206
2366
|
} @catch (NSException *exception) {
|
|
2207
2367
|
// The rescue path must never take the app down with it.
|
|
2208
2368
|
RCTLogWarn(@"RCTPushy -- native check crashed: %@", exception.reason);
|
|
2369
|
+
pushyHostRoundResult = PushyHostResult(@"failed", @"internal_error", nil, NO);
|
|
2209
2370
|
} @finally {
|
|
2210
2371
|
pushyRoundCompleted.store(true);
|
|
2211
2372
|
dispatch_semaphore_signal(pushyRoundDone);
|
|
2373
|
+
dispatch_group_leave(pushyHostRoundGroup);
|
|
2212
2374
|
}
|
|
2213
2375
|
}
|
|
2214
2376
|
|
|
@@ -2220,7 +2382,7 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2220
2382
|
+ (void)runRescueWithDeadline:(NSTimeInterval)deadlineUptime {
|
|
2221
2383
|
pushyCrashRescueActive.store(true);
|
|
2222
2384
|
[self startRoundWithDeadline:deadlineUptime];
|
|
2223
|
-
if (!pushyRoundCompleted.load()) {
|
|
2385
|
+
if (pushyRoundStarted.load() && !pushyRoundCompleted.load()) {
|
|
2224
2386
|
NSTimeInterval remaining = deadlineUptime - PushyMonotonicNow();
|
|
2225
2387
|
if (remaining > 0) {
|
|
2226
2388
|
dispatch_semaphore_wait(pushyRoundDone, dispatch_time(DISPATCH_TIME_NOW,
|
|
@@ -2292,27 +2454,33 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2292
2454
|
}
|
|
2293
2455
|
|
|
2294
2456
|
+ (void)runOnce:(NSString *)launchRolledBackVersion deadline:(NSTimeInterval)deadlineUptime {
|
|
2295
|
-
// Sampled before any IO: resetToPackagedBundle bumps it, and a reset that
|
|
2296
|
-
// lands while this round is running must win over the round's decision.
|
|
2297
2457
|
const uint64_t resetGeneration = pushyResetGeneration.load();
|
|
2458
|
+
pushyHostRoundGeneration = resetGeneration;
|
|
2459
|
+
pushyHostRoundConfigGeneration = pushyNativeConfigGeneration.load();
|
|
2460
|
+
pushyHostRoundResult = PushyHostResult(@"failed", @"check_failed", nil, NO);
|
|
2298
2461
|
NSUserDefaults *defaults = PushyDefaults();
|
|
2299
2462
|
NSString *configJson = [defaults stringForKey:keyNativeConfig];
|
|
2463
|
+
pushyHostRoundConfig = [configJson copy];
|
|
2300
2464
|
if (configJson.length == 0) {
|
|
2301
|
-
|
|
2302
|
-
// native check silently does not run — this is the rollout gate.
|
|
2465
|
+
pushyHostRoundResult = PushyHostResult(@"skipped", @"not_configured", nil, NO);
|
|
2303
2466
|
return;
|
|
2304
2467
|
}
|
|
2305
2468
|
bool ok = false;
|
|
2306
2469
|
flowjson::Value config = flowjson::Parse(PushyToStdString(configJson), &ok);
|
|
2307
|
-
if (!ok || !config.IsObject()
|
|
2470
|
+
if (!ok || !config.IsObject()) {
|
|
2471
|
+
pushyHostRoundResult = PushyHostResult(@"failed", @"invalid_config", nil, NO);
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
if (config.Get("disabled").Truthy()) {
|
|
2475
|
+
pushyHostRoundResult = PushyHostResult(@"skipped", @"disabled", nil, NO);
|
|
2308
2476
|
return;
|
|
2309
2477
|
}
|
|
2310
2478
|
NSString *appKey = PushyFromStdString(config.Get("appKey").AsString());
|
|
2311
2479
|
if (appKey.length == 0) {
|
|
2480
|
+
pushyHostRoundResult = PushyHostResult(@"failed", @"invalid_config", nil, NO);
|
|
2312
2481
|
return;
|
|
2313
2482
|
}
|
|
2314
|
-
//
|
|
2315
|
-
// next launch reads to skip its 5s delay if we die mid-round (§11.4).
|
|
2483
|
+
// Preserve the interrupted-round breadcrumb and reset-safe atomic commit.
|
|
2316
2484
|
[defaults setObject:@YES forKey:keyNativeCheckIncomplete];
|
|
2317
2485
|
@try {
|
|
2318
2486
|
[self runConfiguredRound:config
|
|
@@ -2388,6 +2556,7 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2388
2556
|
NSString *body = [NSString stringWithUTF8String:bodyJson.c_str()];
|
|
2389
2557
|
if (body == nil) {
|
|
2390
2558
|
RCTLogWarn(@"RCTPushy -- native check: request body is not valid UTF-8");
|
|
2559
|
+
pushyHostRoundResult = PushyHostResult(@"failed", @"invalid_request", nil, NO);
|
|
2391
2560
|
return;
|
|
2392
2561
|
}
|
|
2393
2562
|
|
|
@@ -2405,19 +2574,23 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2405
2574
|
PushyToStdString(responseText), identity, false,
|
|
2406
2575
|
config.Get("afterDownload").AsString());
|
|
2407
2576
|
if (decision.Get("action").AsString() != "download") {
|
|
2408
|
-
[self commitRoundWithGeneration:resetGeneration
|
|
2577
|
+
BOOL committed = [self commitRoundWithGeneration:resetGeneration
|
|
2409
2578
|
hashInfo:nil
|
|
2410
2579
|
activate:nil
|
|
2411
2580
|
responseText:responseText
|
|
2412
2581
|
request:body
|
|
2413
2582
|
config:configJson
|
|
2414
2583
|
responseAt:responseAtSeconds];
|
|
2584
|
+
pushyHostRoundResult = committed
|
|
2585
|
+
? PushyHostResult(@"noUpdate", PushyFromStdString(decision.Get("reason").AsString()), nil, NO)
|
|
2586
|
+
: PushyHostResult(@"cancelled", @"reset", nil, NO);
|
|
2415
2587
|
RCTLogInfo(@"RCTPushy -- native check: nothing to do (%s)",
|
|
2416
2588
|
decision.Get("reason").AsString().c_str());
|
|
2417
2589
|
return;
|
|
2418
2590
|
}
|
|
2419
2591
|
NSString *hash = PushyFromStdString(decision.Get("hash").AsString());
|
|
2420
2592
|
if (!PushyIsSafePathComponent(hash)) {
|
|
2593
|
+
pushyHostRoundResult = PushyHostResult(@"failed", @"invalid_response", nil, NO);
|
|
2421
2594
|
return;
|
|
2422
2595
|
}
|
|
2423
2596
|
|
|
@@ -2430,13 +2603,16 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2430
2603
|
deadline:deadlineUptime];
|
|
2431
2604
|
}
|
|
2432
2605
|
if (!downloaded) {
|
|
2433
|
-
[self commitRoundWithGeneration:resetGeneration
|
|
2606
|
+
BOOL committed = [self commitRoundWithGeneration:resetGeneration
|
|
2434
2607
|
hashInfo:nil
|
|
2435
2608
|
activate:nil
|
|
2436
2609
|
responseText:responseText
|
|
2437
2610
|
request:body
|
|
2438
2611
|
config:configJson
|
|
2439
2612
|
responseAt:responseAtSeconds];
|
|
2613
|
+
pushyHostRoundResult = committed
|
|
2614
|
+
? PushyHostResult(@"failed", @"download_failed", nil, NO)
|
|
2615
|
+
: PushyHostResult(@"cancelled", @"reset", nil, NO);
|
|
2440
2616
|
return;
|
|
2441
2617
|
}
|
|
2442
2618
|
|
|
@@ -2488,6 +2664,9 @@ static BOOL PushyIsValidCheckResponse(NSString *responseText) {
|
|
|
2488
2664
|
}
|
|
2489
2665
|
RCTLogInfo(@"RCTPushy -- native check: downloaded %@, activation left to JS", hash);
|
|
2490
2666
|
}
|
|
2667
|
+
pushyHostRoundResult = committed
|
|
2668
|
+
? PushyHostResult(@"downloaded", @"", hash, activate)
|
|
2669
|
+
: PushyHostResult(@"cancelled", @"reset", nil, NO);
|
|
2491
2670
|
}
|
|
2492
2671
|
|
|
2493
2672
|
// Everything a round persists — version info, the activation, the response
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#import "RCTPushyNativeConfig.h"
|
|
2
|
+
|
|
3
|
+
static void PushyConfigInvalid(NSString *message) {
|
|
4
|
+
@throw [NSException exceptionWithName:NSInvalidArgumentException
|
|
5
|
+
reason:[@"Invalid native configuration: " stringByAppendingString:message]
|
|
6
|
+
userInfo:nil];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
static NSString *PushyConfigString(id value, NSString *name, BOOL allowEmpty) {
|
|
10
|
+
if (![value isKindOfClass:NSString.class]
|
|
11
|
+
|| (!allowEmpty && [[value stringByTrimmingCharactersInSet:
|
|
12
|
+
NSCharacterSet.whitespaceAndNewlineCharacterSet] length] == 0)) {
|
|
13
|
+
PushyConfigInvalid([NSString stringWithFormat:@"%@ must be a string%@",
|
|
14
|
+
name, allowEmpty ? @"" : @" and must not be blank"]);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static NSArray *PushyConfigUrls(id value, NSString *name, BOOL base) {
|
|
20
|
+
if (![value isKindOfClass:NSArray.class] || (base && [value count] == 0)) {
|
|
21
|
+
PushyConfigInvalid([NSString stringWithFormat:@"%@ must be %@ array",
|
|
22
|
+
name, base ? @"a non-empty" : @"an"]);
|
|
23
|
+
}
|
|
24
|
+
NSMutableArray *result = [NSMutableArray array];
|
|
25
|
+
NSRegularExpression *authority = [NSRegularExpression regularExpressionWithPattern:
|
|
26
|
+
@"^https?://(\\[[0-9a-fA-F:]+\\]|[a-zA-Z0-9.-]+)(:[0-9]+)?([/?#]|$)"
|
|
27
|
+
options:0 error:nil];
|
|
28
|
+
for (id item in value) {
|
|
29
|
+
NSString *url = [PushyConfigString(item, name, NO) stringByTrimmingCharactersInSet:
|
|
30
|
+
NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
|
31
|
+
if ([authority firstMatchInString:url options:0 range:NSMakeRange(0, url.length)] == nil
|
|
32
|
+
|| [url rangeOfCharacterFromSet:NSCharacterSet.whitespaceAndNewlineCharacterSet].location != NSNotFound
|
|
33
|
+
|| [url containsString:@"\\"]
|
|
34
|
+
|| (base && ([url containsString:@"?"] || [url containsString:@"#"]))) {
|
|
35
|
+
PushyConfigInvalid([NSString stringWithFormat:
|
|
36
|
+
@"%@ requires absolute HTTP(S) URLs without credentials%@",
|
|
37
|
+
name, base ? @", queries or fragments" : @""]);
|
|
38
|
+
}
|
|
39
|
+
if (base) {
|
|
40
|
+
while ([url hasSuffix:@"/"]) {
|
|
41
|
+
url = [url substringToIndex:url.length - 1];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (![result containsObject:url]) {
|
|
45
|
+
[result addObject:url];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
NSString *RCTPushyNormalizeNativeConfig(NSDictionary *options, NSError **error) {
|
|
52
|
+
@try {
|
|
53
|
+
if (![options isKindOfClass:NSDictionary.class]) {
|
|
54
|
+
PushyConfigInvalid(@"expected an object");
|
|
55
|
+
}
|
|
56
|
+
NSArray *keys = @[@"appKey", @"endpoints", @"queryUrls", @"afterDownload",
|
|
57
|
+
@"disabled", @"packageVersion", @"rnu", @"rn"];
|
|
58
|
+
for (id key in options) {
|
|
59
|
+
if (![keys containsObject:key]) {
|
|
60
|
+
PushyConfigInvalid([NSString stringWithFormat:@"unknown option %@", key]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
NSString *appKey = PushyConfigString(options[@"appKey"], @"appKey", NO);
|
|
64
|
+
BOOL customEndpoints = options[@"endpoints"] != nil;
|
|
65
|
+
NSArray *endpoints = PushyConfigUrls(customEndpoints ? options[@"endpoints"]
|
|
66
|
+
: @[@"https://update.react-native.cn/api", @"https://update.reactnative.cn/api"],
|
|
67
|
+
@"endpoints", YES);
|
|
68
|
+
NSArray *queryUrls = PushyConfigUrls(options[@"queryUrls"] ?: (customEndpoints ? @[]
|
|
69
|
+
: @[@"https://gitee.com/sunnylqm/react-native-pushy/raw/master/endpoints.json",
|
|
70
|
+
@"https://cdn.jsdelivr.net/gh/reactnativecn/react-native-update@master/endpoints.json"]),
|
|
71
|
+
@"queryUrls", NO);
|
|
72
|
+
NSString *afterDownload = options[@"afterDownload"]
|
|
73
|
+
? PushyConfigString(options[@"afterDownload"], @"afterDownload", NO) : @"none";
|
|
74
|
+
if (![@[@"none", @"setNeedUpdate"] containsObject:afterDownload]) {
|
|
75
|
+
PushyConfigInvalid(@"afterDownload must be none or setNeedUpdate");
|
|
76
|
+
}
|
|
77
|
+
id disabled = options[@"disabled"] ?: @NO;
|
|
78
|
+
if (![disabled isKindOfClass:NSNumber.class] || CFGetTypeID((__bridge CFTypeRef)disabled) != CFBooleanGetTypeID()) {
|
|
79
|
+
PushyConfigInvalid(@"disabled must be a boolean");
|
|
80
|
+
}
|
|
81
|
+
NSMutableDictionary *result = [@{
|
|
82
|
+
@"appKey": appKey, @"endpoints": endpoints, @"queryUrls": queryUrls,
|
|
83
|
+
@"afterDownload": afterDownload, @"disabled": disabled,
|
|
84
|
+
@"rnu": options[@"rnu"] ? PushyConfigString(options[@"rnu"], @"rnu", YES) : @"",
|
|
85
|
+
@"rn": options[@"rn"] ? PushyConfigString(options[@"rn"], @"rn", YES) : @""
|
|
86
|
+
} mutableCopy];
|
|
87
|
+
if (options[@"packageVersion"] != nil) {
|
|
88
|
+
result[@"packageVersion"] = PushyConfigString(options[@"packageVersion"], @"packageVersion", NO);
|
|
89
|
+
}
|
|
90
|
+
// Stable ordering makes repeated identical configure calls idempotent.
|
|
91
|
+
NSData *data = [NSJSONSerialization dataWithJSONObject:result
|
|
92
|
+
options:NSJSONWritingSortedKeys error:error];
|
|
93
|
+
return data == nil ? nil : [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
94
|
+
} @catch (NSException *exception) {
|
|
95
|
+
if (error != NULL) {
|
|
96
|
+
*error = [NSError errorWithDomain:@"cn.reactnative.pushy" code:1 userInfo:@{
|
|
97
|
+
NSLocalizedDescriptionKey: exception.reason ?: @"Invalid native configuration",
|
|
98
|
+
@"PushyErrorCode": @"INVALID_OPTIONS"
|
|
99
|
+
}];
|
|
100
|
+
}
|
|
101
|
+
return nil;
|
|
102
|
+
}
|
|
103
|
+
}
|
package/package.json
CHANGED
package/src/NativePushy.ts
CHANGED
|
@@ -26,8 +26,9 @@ export interface Spec extends TurboModule {
|
|
|
26
26
|
* Persist the config subset the native cold-start update check consumes
|
|
27
27
|
* (appKey, endpoints, afterDownload policy; NATIVE_CHECKUPDATE_DESIGN
|
|
28
28
|
* §10.1). Stored as a raw JSON string, parsed natively on read. JS is the
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* default config source. Native hosts may also call configure(); select
|
|
30
|
+
* nativeConfigSource: 'native' in JS to leave host configuration untouched.
|
|
31
|
+
* A native side without persisted config skips its check.
|
|
31
32
|
*/
|
|
32
33
|
syncNativeConfig(config: string): Promise<void>;
|
|
33
34
|
/**
|
package/src/client.ts
CHANGED
|
@@ -420,6 +420,10 @@ export class Pushy {
|
|
|
420
420
|
};
|
|
421
421
|
|
|
422
422
|
private flushNativeConfig = () => {
|
|
423
|
+
if (this.options.nativeConfigSource === 'native') {
|
|
424
|
+
this.pendingNativeConfigJson = undefined;
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
423
427
|
if (this.nativeConfigSyncInFlight) {
|
|
424
428
|
return;
|
|
425
429
|
}
|
|
@@ -480,6 +484,10 @@ export class Pushy {
|
|
|
480
484
|
};
|
|
481
485
|
|
|
482
486
|
private syncNativeConfig = () => {
|
|
487
|
+
if (this.options.nativeConfigSource === 'native') {
|
|
488
|
+
this.pendingNativeConfigJson = undefined;
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
483
491
|
if (
|
|
484
492
|
Platform.OS === 'web' ||
|
|
485
493
|
typeof PushyModule.syncNativeConfig !== 'function'
|
package/src/type.ts
CHANGED
|
@@ -228,6 +228,13 @@ export interface ClientOptions {
|
|
|
228
228
|
* per-version forceBoot directive may.
|
|
229
229
|
*/
|
|
230
230
|
disableNativeCheck?: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* Owner of persisted native update configuration. Default: 'javascript'.
|
|
233
|
+
* Use 'native' when configure() is called by the host: JS will not overwrite
|
|
234
|
+
* that configuration. This does not configure or disable JS checks itself;
|
|
235
|
+
* keep appKey/server consistent and use checkStrategy to control JS checks.
|
|
236
|
+
*/
|
|
237
|
+
nativeConfigSource?: 'javascript' | 'native';
|
|
231
238
|
}
|
|
232
239
|
|
|
233
240
|
export interface UpdateTestPayload {
|