react-native/React/Modules/RCTDevSettings.mm
Eloy Durán 19caaba1d5 Fixes current CI failures and allows tree shaking of native dev support code.
Summary:
* The dev support code moved into a `DevSupport` subspec, meaning that only if the subspec is specified in the user’s Podfile will the packager client, dev menu, etc be included. This is mainly done through checks for header availability.

  It also improves the weird situation where you had to specify the `RCTWebSocket` subspec if you wanted to be able to use the packager client during development.

* I removed hardcoding the release version in the podspec on release, because the podspec still relies on `package.json` when evaluating, so there’s no real point in not also getting the version number from there. This should remove any requirement to perform maintenance of the OSS release script regarding the podspec.
Closes https://github.com/facebook/react-native/pull/12602

Differential Revision: D4621021

Pulled By: ericvicenti

fbshipit-source-id: 6c208371fc40ea607809a6ab05dd3714ed9980cf
2017-02-27 14:04:58 -08:00

548 lines
16 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 "JSCSamplingProfiler.h"
#import "RCTBridge+Private.h"
#import "RCTBridgeModule.h"
#import "RCTEventDispatcher.h"
#import "RCTLog.h"
#import "RCTProfile.h"
#import "RCTUtils.h"
NSString *const kRCTDevSettingProfilingEnabled = @"profilingEnabled";
NSString *const kRCTDevSettingHotLoadingEnabled = @"hotLoadingEnabled";
NSString *const kRCTDevSettingLiveReloadEnabled = @"liveReloadEnabled";
NSString *const kRCTDevSettingIsInspectorShown = @"showInspector";
NSString *const kRCTDevSettingIsDebuggingRemotely = @"isDebuggingRemotely";
NSString *const kRCTDevSettingWebsocketExecutorName = @"websocket-executor-name";
NSString *const kRCTDevSettingExecutorOverrideClass = @"executor-override";
NSString *const kRCTDevSettingShakeToShowDevMenu = @"shakeToShow";
NSString *const kRCTDevSettingIsPerfMonitorShown = @"RCTPerfMonitorKey";
NSString *const kRCTDevSettingIsJSCProfilingEnabled = @"RCTJSCProfilerEnabled";
NSString *const kRCTDevSettingStartSamplingProfilerOnLaunch = @"startSamplingProfilerOnLaunch";
NSString *const kRCTDevSettingsUserDefaultsKey = @"RCTDevMenu";
#if RCT_DEV
#if __has_include("RCTPackagerClient.h")
#import "RCTPackagerClient.h"
#import "RCTReloadPackagerMethod.h"
#import "RCTSamplingProfilerPackagerMethod.h"
#endif
@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;
}
@property (nonatomic, strong) Class executorClass;
@property (nonatomic, readwrite, strong) id<RCTDevSettingsDataSource> dataSource;
@end
@implementation RCTDevSettings
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
- (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];
[self connectPackager];
});
}
return self;
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (void)invalidate
{
[_liveReloadUpdateTask cancel];
[[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)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.jsContext.JSGlobalContextRef);
}
RCT_EXPORT_METHOD(reload)
{
[_bridge reload];
}
- (NSString *)websocketExecutorName
{
// This value is passed as a command-line argument, so fall back to reading from NSUserDefaults directly
return [[NSUserDefaults standardUserDefaults] stringForKey:kRCTDevSettingWebsocketExecutorName];
}
- (void)setIsShakeToShowDevMenuEnabled:(BOOL)isShakeToShowDevMenuEnabled
{
[self _updateSettingWithValue:@(isShakeToShowDevMenuEnabled) 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];
if (executorOverride) {
self.executorClass = NSClassFromString(executorOverride);
} 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)
{
[self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingHotLoadingEnabled];
[self _hotLoadingSettingDidChange];
}
- (BOOL)isHotLoadingEnabled
{
return [[self settingForKey:kRCTDevSettingHotLoadingEnabled] boolValue];
}
- (void)_hotLoadingSettingDidChange
{
BOOL hotLoadingEnabled = self.isHotLoadingAvailable && self.isHotLoadingEnabled;
if (RCTGetURLQueryParam(_bridge.bundleURL, @"hot").boolValue != hotLoadingEnabled) {
_bridge.bundleURL = RCTURLByReplacingQueryParam(_bridge.bundleURL, @"hot",
hotLoadingEnabled ? @"true" : nil);
[_bridge reload];
}
}
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
{
JSContext *context = _bridge.jsContext;
JSGlobalContextRef globalContext = context.JSGlobalContextRef;
// JSPokeSamplingProfiler() toggles the profiling process
JSValueRef jsResult = JSC_JSPokeSamplingProfiler(globalContext);
if (JSC_JSValueGetType(globalContext, jsResult) != kJSTypeNull) {
NSString *results = [[JSC_JSValue(globalContext) valueWithJSValueRef:jsResult inContext:context] toObject];
JSCSamplingProfiler *profilerModule = [_bridge moduleForClass:[JSCSamplingProfiler 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)setIsJSCProfilingEnabled:(BOOL)isJSCProfilingEnabled
{
[self _updateSettingWithValue:@(isJSCProfilingEnabled) forKey:kRCTDevSettingIsJSCProfilingEnabled];
}
- (BOOL)isJSCProfilingEnabled
{
return [[self settingForKey:kRCTDevSettingIsJSCProfilingEnabled] 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];
}
}
#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 _hotLoadingSettingDidChange];
[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];
});
}
#pragma mark - RCTWebSocketObserver
- (NSURL *)packagerURL
{
#if !__has_include("RCTWebSocketObserver.h")
return nil;
#else
NSString *host = [_bridge.bundleURL host];
NSString *scheme = [_bridge.bundleURL scheme];
if (!host) {
host = @"localhost";
scheme = @"http";
}
NSNumber *port = [_bridge.bundleURL port];
if (!port) {
port = @8081; // Packager default port
}
return [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@:%@/message?role=ios-rn-rctdevmenu", scheme, host, port]];
#endif
}
// TODO: Move non-UI logic into separate RCTDevSettings module
- (void)connectPackager
{
RCTAssertMainQueue();
NSURL *url = [self packagerURL];
if (!url) {
return;
}
#if __has_include("RCTPackagerClient.h")
// The jsPackagerClient is a static map that holds different packager clients per the packagerURL
// In case many instances of DevMenu are created, the latest instance that use the same URL as
// previous instances will override given packager client's method handlers
static NSMutableDictionary<NSString *, RCTPackagerClient *> *jsPackagerClients = nil;
if (jsPackagerClients == nil) {
jsPackagerClients = [NSMutableDictionary new];
}
NSString *key = [url absoluteString];
RCTPackagerClient *packagerClient = jsPackagerClients[key];
if (!packagerClient) {
packagerClient = [[RCTPackagerClient alloc] initWithURL:url];
jsPackagerClients[key] = packagerClient;
} else {
[packagerClient stop];
}
[packagerClient addHandler:[[RCTReloadPackagerMethod alloc] initWithBridge:_bridge]
forMethod:@"reload"];
[packagerClient addHandler:[[RCTSamplingProfilerPackagerMethod alloc] initWithBridge:_bridge]
forMethod:@"pokeSamplingProfiler"];
[packagerClient start];
#endif
}
@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