576 lines
17 KiB
Plaintext
576 lines
17 KiB
Plaintext
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import "RCTDevSettings.h"
|
|
|
|
#import <objc/runtime.h>
|
|
|
|
#import <JavaScriptCore/JavaScriptCore.h>
|
|
|
|
#import <jschelpers/JavaScriptCore.h>
|
|
|
|
#import "RCTBridge+Private.h"
|
|
#import "RCTBridgeModule.h"
|
|
#import "RCTEventDispatcher.h"
|
|
#import "RCTJSCSamplingProfiler.h"
|
|
#import "RCTLog.h"
|
|
#import "RCTPackagerClient.h"
|
|
#import "RCTProfile.h"
|
|
#import "RCTUtils.h"
|
|
|
|
static NSString *const kRCTDevSettingProfilingEnabled = @"profilingEnabled";
|
|
static NSString *const kRCTDevSettingHotLoadingEnabled = @"hotLoadingEnabled";
|
|
static NSString *const kRCTDevSettingLiveReloadEnabled = @"liveReloadEnabled";
|
|
static NSString *const kRCTDevSettingIsInspectorShown = @"showInspector";
|
|
static NSString *const kRCTDevSettingIsDebuggingRemotely = @"isDebuggingRemotely";
|
|
static NSString *const kRCTDevSettingExecutorOverrideClass = @"executor-override";
|
|
static NSString *const kRCTDevSettingShakeToShowDevMenu = @"shakeToShow";
|
|
static NSString *const kRCTDevSettingIsPerfMonitorShown = @"RCTPerfMonitorKey";
|
|
static NSString *const kRCTDevSettingStartSamplingProfilerOnLaunch = @"startSamplingProfilerOnLaunch";
|
|
|
|
static NSString *const kRCTDevSettingsUserDefaultsKey = @"RCTDevMenu";
|
|
|
|
#if ENABLE_PACKAGER_CONNECTION
|
|
#import "RCTPackagerConnection.h"
|
|
#endif
|
|
|
|
#if RCT_ENABLE_INSPECTOR
|
|
#import "RCTInspectorDevServerHelper.h"
|
|
#import <jschelpers/JSCWrapper.h>
|
|
#endif
|
|
|
|
#if RCT_DEV
|
|
|
|
@interface RCTDevSettingsUserDefaultsDataSource : NSObject <RCTDevSettingsDataSource>
|
|
|
|
@end
|
|
|
|
@implementation RCTDevSettingsUserDefaultsDataSource {
|
|
NSMutableDictionary *_settings;
|
|
NSUserDefaults *_userDefaults;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
return [self initWithDefaultValues:nil];
|
|
}
|
|
|
|
- (instancetype)initWithDefaultValues:(NSDictionary *)defaultValues
|
|
{
|
|
if (self = [super init]) {
|
|
_userDefaults = [NSUserDefaults standardUserDefaults];
|
|
if (defaultValues) {
|
|
[self _reloadWithDefaults:defaultValues];
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)updateSettingWithValue:(id)value forKey:(NSString *)key
|
|
{
|
|
RCTAssert((key != nil), @"%@", [NSString stringWithFormat:@"%@: Tried to update nil key", [self class]]);
|
|
|
|
id currentValue = [self settingForKey:key];
|
|
if (currentValue == value || [currentValue isEqual:value]) {
|
|
return;
|
|
}
|
|
if (value) {
|
|
_settings[key] = value;
|
|
} else {
|
|
[_settings removeObjectForKey:key];
|
|
}
|
|
[_userDefaults setObject:_settings forKey:kRCTDevSettingsUserDefaultsKey];
|
|
}
|
|
|
|
- (id)settingForKey:(NSString *)key
|
|
{
|
|
return _settings[key];
|
|
}
|
|
|
|
- (void)_reloadWithDefaults:(NSDictionary *)defaultValues
|
|
{
|
|
NSDictionary *existingSettings = [_userDefaults objectForKey:kRCTDevSettingsUserDefaultsKey];
|
|
_settings = existingSettings ? [existingSettings mutableCopy] : [NSMutableDictionary dictionary];
|
|
for (NSString *key in [defaultValues keyEnumerator]) {
|
|
if (!_settings[key]) {
|
|
_settings[key] = defaultValues[key];
|
|
}
|
|
}
|
|
[_userDefaults setObject:_settings forKey:kRCTDevSettingsUserDefaultsKey];
|
|
}
|
|
|
|
@end
|
|
|
|
@interface RCTDevSettings () <RCTBridgeModule, RCTInvalidating>
|
|
{
|
|
NSURLSessionDataTask *_liveReloadUpdateTask;
|
|
NSURL *_liveReloadURL;
|
|
BOOL _isJSLoaded;
|
|
#if ENABLE_PACKAGER_CONNECTION
|
|
RCTHandlerToken _reloadToken;
|
|
RCTHandlerToken _pokeSamplingProfilerToken;
|
|
#endif
|
|
}
|
|
|
|
@property (nonatomic, strong) Class executorClass;
|
|
@property (nonatomic, readwrite, strong) id<RCTDevSettingsDataSource> dataSource;
|
|
|
|
@end
|
|
|
|
@implementation RCTDevSettings
|
|
|
|
@synthesize bridge = _bridge;
|
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
+ (BOOL)requiresMainQueueSetup
|
|
{
|
|
return YES; // RCT_DEV-only
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
// default behavior is to use NSUserDefaults
|
|
NSDictionary *defaultValues = @{
|
|
kRCTDevSettingShakeToShowDevMenu: @YES,
|
|
};
|
|
RCTDevSettingsUserDefaultsDataSource *dataSource = [[RCTDevSettingsUserDefaultsDataSource alloc] initWithDefaultValues:defaultValues];
|
|
return [self initWithDataSource:dataSource];
|
|
}
|
|
|
|
- (instancetype)initWithDataSource:(id<RCTDevSettingsDataSource>)dataSource
|
|
{
|
|
if (self = [super init]) {
|
|
_dataSource = dataSource;
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(jsLoaded:)
|
|
name:RCTJavaScriptDidLoadNotification
|
|
object:nil];
|
|
|
|
// Delay setup until after Bridge init
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self _synchronizeAllSettings];
|
|
});
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)setBridge:(RCTBridge *)bridge
|
|
{
|
|
RCTAssert(_bridge == nil, @"RCTDevSettings module should not be reused");
|
|
_bridge = bridge;
|
|
|
|
#if ENABLE_PACKAGER_CONNECTION
|
|
RCTBridge *__weak weakBridge = bridge;
|
|
_reloadToken =
|
|
[[RCTPackagerConnection sharedPackagerConnection]
|
|
addNotificationHandler:^(id params) {
|
|
if (params != (id)kCFNull && [params[@"debug"] boolValue]) {
|
|
weakBridge.executorClass = objc_lookUpClass("RCTWebSocketExecutor");
|
|
}
|
|
[weakBridge reload];
|
|
}
|
|
queue:dispatch_get_main_queue()
|
|
forMethod:@"reload"];
|
|
|
|
_pokeSamplingProfilerToken =
|
|
[[RCTPackagerConnection sharedPackagerConnection]
|
|
addRequestHandler:^(NSDictionary<NSString *, id> *params, RCTPackagerClientResponder *responder) {
|
|
pokeSamplingProfiler(weakBridge, responder);
|
|
}
|
|
queue:dispatch_get_main_queue()
|
|
forMethod:@"pokeSamplingProfiler"];
|
|
#endif
|
|
|
|
#if RCT_ENABLE_INSPECTOR
|
|
// we need this dispatch back to the main thread because even though this
|
|
// is executed on the main thread, at this point the bridge is not yet
|
|
// finished with its initialisation. But it does finish by the time it
|
|
// relinquishes control of the main thread, so only queue on the JS thread
|
|
// after the current main thread operation is done.
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[bridge dispatchBlock:^{
|
|
[RCTInspectorDevServerHelper connectForContext:bridge.jsContextRef
|
|
withBundleURL:bridge.bundleURL];
|
|
} queue:RCTJSThread];
|
|
});
|
|
#endif
|
|
}
|
|
|
|
static void pokeSamplingProfiler(RCTBridge *const bridge, RCTPackagerClientResponder *const responder)
|
|
{
|
|
if (!bridge) {
|
|
[responder respondWithError:@"The bridge is nil. Try again."];
|
|
return;
|
|
}
|
|
|
|
JSGlobalContextRef globalContext = bridge.jsContextRef;
|
|
if (!JSC_JSSamplingProfilerEnabled(globalContext)) {
|
|
[responder respondWithError:@"The JSSamplingProfiler is disabled. See 'iOS specific setup' section here https://fburl.com/u4lw7xeq for some help"];
|
|
return;
|
|
}
|
|
|
|
// JSPokeSamplingProfiler() toggles the profiling process
|
|
JSValueRef jsResult = JSC_JSPokeSamplingProfiler(globalContext);
|
|
if (JSC_JSValueGetType(globalContext, jsResult) == kJSTypeNull) {
|
|
[responder respondWithResult:@"started"];
|
|
} else {
|
|
JSContext *context = [JSC_JSContext(globalContext) contextWithJSGlobalContextRef:globalContext];
|
|
NSString *results = [[JSC_JSValue(globalContext) valueWithJSValueRef:jsResult inContext:context] toObject];
|
|
[responder respondWithResult:results];
|
|
}
|
|
}
|
|
|
|
- (dispatch_queue_t)methodQueue
|
|
{
|
|
return dispatch_get_main_queue();
|
|
}
|
|
|
|
- (void)invalidate
|
|
{
|
|
[_liveReloadUpdateTask cancel];
|
|
#if ENABLE_PACKAGER_CONNECTION
|
|
[[RCTPackagerConnection sharedPackagerConnection] removeHandler:_reloadToken];
|
|
[[RCTPackagerConnection sharedPackagerConnection] removeHandler:_pokeSamplingProfilerToken];
|
|
#endif
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (void)_updateSettingWithValue:(id)value forKey:(NSString *)key
|
|
{
|
|
[_dataSource updateSettingWithValue:value forKey:key];
|
|
}
|
|
|
|
- (id)settingForKey:(NSString *)key
|
|
{
|
|
return [_dataSource settingForKey:key];
|
|
}
|
|
|
|
- (BOOL)isNuclideDebuggingAvailable
|
|
{
|
|
#if RCT_ENABLE_INSPECTOR
|
|
return facebook::react::isCustomJSCPtr(_bridge.jsContextRef);
|
|
#else
|
|
return false;
|
|
#endif //RCT_ENABLE_INSPECTOR
|
|
}
|
|
|
|
- (BOOL)isRemoteDebuggingAvailable
|
|
{
|
|
Class jsDebuggingExecutorClass = objc_lookUpClass("RCTWebSocketExecutor");
|
|
return (jsDebuggingExecutorClass != nil);
|
|
}
|
|
|
|
- (BOOL)isHotLoadingAvailable
|
|
{
|
|
return _bridge.bundleURL && !_bridge.bundleURL.fileURL; // Only works when running from server
|
|
}
|
|
|
|
- (BOOL)isLiveReloadAvailable
|
|
{
|
|
return (_liveReloadURL != nil);
|
|
}
|
|
|
|
- (BOOL)isJSCSamplingProfilerAvailable
|
|
{
|
|
return JSC_JSSamplingProfilerEnabled(_bridge.jsContextRef);
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(reload)
|
|
{
|
|
[_bridge reload];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setIsShakeToShowDevMenuEnabled:(BOOL)enabled)
|
|
{
|
|
[self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingShakeToShowDevMenu];
|
|
}
|
|
|
|
- (BOOL)isShakeToShowDevMenuEnabled
|
|
{
|
|
return [[self settingForKey:kRCTDevSettingShakeToShowDevMenu] boolValue];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setIsDebuggingRemotely:(BOOL)enabled)
|
|
{
|
|
[self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingIsDebuggingRemotely];
|
|
[self _remoteDebugSettingDidChange];
|
|
}
|
|
|
|
- (BOOL)isDebuggingRemotely
|
|
{
|
|
return [[self settingForKey:kRCTDevSettingIsDebuggingRemotely] boolValue];
|
|
}
|
|
|
|
- (void)_remoteDebugSettingDidChange
|
|
{
|
|
// This value is passed as a command-line argument, so fall back to reading from NSUserDefaults directly
|
|
NSString *executorOverride = [[NSUserDefaults standardUserDefaults] stringForKey:kRCTDevSettingExecutorOverrideClass];
|
|
Class executorOverrideClass = executorOverride ? NSClassFromString(executorOverride) : nil;
|
|
if (executorOverrideClass) {
|
|
self.executorClass = executorOverrideClass;
|
|
} else {
|
|
BOOL enabled = self.isRemoteDebuggingAvailable && self.isDebuggingRemotely;
|
|
self.executorClass = enabled ? objc_getClass("RCTWebSocketExecutor") : nil;
|
|
}
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setProfilingEnabled:(BOOL)enabled)
|
|
{
|
|
[self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingProfilingEnabled];
|
|
[self _profilingSettingDidChange];
|
|
}
|
|
|
|
- (BOOL)isProfilingEnabled
|
|
{
|
|
return [[self settingForKey:kRCTDevSettingProfilingEnabled] boolValue];
|
|
}
|
|
|
|
- (void)_profilingSettingDidChange
|
|
{
|
|
BOOL enabled = self.isProfilingEnabled;
|
|
if (_liveReloadURL && enabled != RCTProfileIsProfiling()) {
|
|
if (enabled) {
|
|
[_bridge startProfiling];
|
|
} else {
|
|
[_bridge stopProfiling:^(NSData *logData) {
|
|
RCTProfileSendResult(self->_bridge, @"systrace", logData);
|
|
}];
|
|
}
|
|
}
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setLiveReloadEnabled:(BOOL)enabled)
|
|
{
|
|
[self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingLiveReloadEnabled];
|
|
[self _liveReloadSettingDidChange];
|
|
}
|
|
|
|
- (BOOL)isLiveReloadEnabled
|
|
{
|
|
return [[self settingForKey:kRCTDevSettingLiveReloadEnabled] boolValue];
|
|
}
|
|
|
|
- (void)_liveReloadSettingDidChange
|
|
{
|
|
BOOL liveReloadEnabled = (self.isLiveReloadAvailable && self.isLiveReloadEnabled);
|
|
if (liveReloadEnabled) {
|
|
[self _pollForLiveReload];
|
|
} else {
|
|
[_liveReloadUpdateTask cancel];
|
|
_liveReloadUpdateTask = nil;
|
|
}
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setHotLoadingEnabled:(BOOL)enabled)
|
|
{
|
|
if (self.isHotLoadingEnabled != enabled) {
|
|
[self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingHotLoadingEnabled];
|
|
[_bridge reload];
|
|
}
|
|
}
|
|
|
|
- (BOOL)isHotLoadingEnabled
|
|
{
|
|
return [[self settingForKey:kRCTDevSettingHotLoadingEnabled] boolValue];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(toggleElementInspector)
|
|
{
|
|
BOOL value = [[self settingForKey:kRCTDevSettingIsInspectorShown] boolValue];
|
|
[self _updateSettingWithValue:@(!value) forKey:kRCTDevSettingIsInspectorShown];
|
|
|
|
if (_isJSLoaded) {
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[self.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
|
|
#pragma clang diagnostic pop
|
|
}
|
|
}
|
|
|
|
- (void)toggleJSCSamplingProfiler
|
|
{
|
|
JSGlobalContextRef globalContext = _bridge.jsContextRef;
|
|
// JSPokeSamplingProfiler() toggles the profiling process
|
|
JSValueRef jsResult = JSC_JSPokeSamplingProfiler(globalContext);
|
|
|
|
if (JSC_JSValueGetType(globalContext, jsResult) != kJSTypeNull) {
|
|
JSContext *context = [JSC_JSContext(globalContext) contextWithJSGlobalContextRef:globalContext];
|
|
NSString *results = [[JSC_JSValue(globalContext) valueWithJSValueRef:jsResult inContext:context] toObject];
|
|
RCTJSCSamplingProfiler *profilerModule = [_bridge moduleForClass:[RCTJSCSamplingProfiler class]];
|
|
[profilerModule operationCompletedWithResults:results];
|
|
}
|
|
}
|
|
|
|
- (BOOL)isElementInspectorShown
|
|
{
|
|
return [[self settingForKey:kRCTDevSettingIsInspectorShown] boolValue];
|
|
}
|
|
|
|
- (void)setIsPerfMonitorShown:(BOOL)isPerfMonitorShown
|
|
{
|
|
[self _updateSettingWithValue:@(isPerfMonitorShown) forKey:kRCTDevSettingIsPerfMonitorShown];
|
|
}
|
|
|
|
- (BOOL)isPerfMonitorShown
|
|
{
|
|
return [[self settingForKey:kRCTDevSettingIsPerfMonitorShown] boolValue];
|
|
}
|
|
|
|
- (void)setStartSamplingProfilerOnLaunch:(BOOL)startSamplingProfilerOnLaunch
|
|
{
|
|
[self _updateSettingWithValue:@(startSamplingProfilerOnLaunch) forKey:kRCTDevSettingStartSamplingProfilerOnLaunch];
|
|
}
|
|
|
|
- (BOOL)startSamplingProfilerOnLaunch
|
|
{
|
|
return [[self settingForKey:kRCTDevSettingStartSamplingProfilerOnLaunch] boolValue];
|
|
}
|
|
|
|
- (void)setExecutorClass:(Class)executorClass
|
|
{
|
|
_executorClass = executorClass;
|
|
if (_bridge.executorClass != executorClass) {
|
|
|
|
// TODO (6929129): we can remove this special case test once we have better
|
|
// support for custom executors in the dev menu. But right now this is
|
|
// needed to prevent overriding a custom executor with the default if a
|
|
// custom executor has been set directly on the bridge
|
|
if (executorClass == Nil &&
|
|
_bridge.executorClass != objc_lookUpClass("RCTWebSocketExecutor")) {
|
|
return;
|
|
}
|
|
|
|
_bridge.executorClass = executorClass;
|
|
[_bridge reload];
|
|
}
|
|
}
|
|
|
|
#if RCT_DEV
|
|
|
|
- (void)addHandler:(id<RCTPackagerClientMethod>)handler forPackagerMethod:(NSString *)name
|
|
{
|
|
#if ENABLE_PACKAGER_CONNECTION
|
|
[[RCTPackagerConnection sharedPackagerConnection] addHandler:handler forMethod:name];
|
|
#endif
|
|
}
|
|
|
|
#endif
|
|
|
|
#pragma mark - Internal
|
|
|
|
/**
|
|
* Query the data source for all possible settings and make sure we're doing the right
|
|
* thing for the state of each setting.
|
|
*/
|
|
- (void)_synchronizeAllSettings
|
|
{
|
|
[self _liveReloadSettingDidChange];
|
|
[self _remoteDebugSettingDidChange];
|
|
[self _profilingSettingDidChange];
|
|
}
|
|
|
|
- (void)_pollForLiveReload
|
|
{
|
|
if (!_isJSLoaded || ![[self settingForKey:kRCTDevSettingLiveReloadEnabled] boolValue] || !_liveReloadURL) {
|
|
return;
|
|
}
|
|
|
|
if (_liveReloadUpdateTask) {
|
|
return;
|
|
}
|
|
|
|
__weak RCTDevSettings *weakSelf = self;
|
|
_liveReloadUpdateTask = [[NSURLSession sharedSession] dataTaskWithURL:_liveReloadURL completionHandler:
|
|
^(__unused NSData *data, NSURLResponse *response, NSError *error) {
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
__strong RCTDevSettings *strongSelf = weakSelf;
|
|
if (strongSelf && [[strongSelf settingForKey:kRCTDevSettingLiveReloadEnabled] boolValue]) {
|
|
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
|
|
if (!error && HTTPResponse.statusCode == 205) {
|
|
[strongSelf reload];
|
|
} else {
|
|
if (error.code != NSURLErrorCancelled) {
|
|
strongSelf->_liveReloadUpdateTask = nil;
|
|
[strongSelf _pollForLiveReload];
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
}];
|
|
|
|
[_liveReloadUpdateTask resume];
|
|
}
|
|
|
|
- (void)jsLoaded:(NSNotification *)notification
|
|
{
|
|
if (notification.userInfo[@"bridge"] != _bridge) {
|
|
return;
|
|
}
|
|
|
|
_isJSLoaded = YES;
|
|
|
|
// Check if live reloading is available
|
|
NSURL *scriptURL = _bridge.bundleURL;
|
|
if (![scriptURL isFileURL]) {
|
|
// Live reloading is disabled when running from bundled JS file
|
|
_liveReloadURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:scriptURL];
|
|
} else {
|
|
_liveReloadURL = nil;
|
|
}
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// update state again after the bridge has finished loading
|
|
[self _synchronizeAllSettings];
|
|
|
|
// Inspector can only be shown after JS has loaded
|
|
if ([self isElementInspectorShown]) {
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[self.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
|
|
#pragma clang diagnostic pop
|
|
}
|
|
});
|
|
}
|
|
|
|
@end
|
|
|
|
#else // #if RCT_DEV
|
|
|
|
@implementation RCTDevSettings
|
|
|
|
- (instancetype)initWithDataSource:(id<RCTDevSettingsDataSource>)dataSource { return [super init]; }
|
|
- (BOOL)isHotLoadingAvailable { return NO; }
|
|
- (BOOL)isLiveReloadAvailable { return NO; }
|
|
- (BOOL)isRemoteDebuggingAvailable { return NO; }
|
|
- (id)settingForKey:(NSString *)key { return nil; }
|
|
- (void)reload {}
|
|
- (void)toggleElementInspector {}
|
|
- (void)toggleJSCSamplingProfiler {}
|
|
|
|
@end
|
|
|
|
#endif
|
|
|
|
@implementation RCTBridge (RCTDevSettings)
|
|
|
|
- (RCTDevSettings *)devSettings
|
|
{
|
|
#if RCT_DEV
|
|
return [self moduleForClass:[RCTDevSettings class]];
|
|
#else
|
|
return nil;
|
|
#endif
|
|
}
|
|
|
|
@end
|