/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "RCTDevSettings.h" #import #import "RCTBridge+Private.h" #import "RCTBridgeModule.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTProfile.h" #import "RCTUtils.h" static NSString *const kRCTDevSettingProfilingEnabled = @"profilingEnabled"; static NSString *const kRCTDevSettingHotLoadingEnabled = @"hotLoadingEnabled"; 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 kRCTDevSettingsUserDefaultsKey = @"RCTDevMenu"; #if ENABLE_PACKAGER_CONNECTION #import "RCTPackagerClient.h" #import "RCTPackagerConnection.h" #endif #if RCT_ENABLE_INSPECTOR #import "RCTInspectorDevServerHelper.h" #endif #if RCT_DEV_MENU @interface RCTDevSettingsUserDefaultsDataSource : NSObject @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 () { BOOL _isJSLoaded; #if ENABLE_PACKAGER_CONNECTION RCTHandlerToken _reloadToken; #endif } @property (nonatomic, strong) Class executorClass; @property (nonatomic, readwrite, strong) id 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, kRCTDevSettingHotLoadingEnabled : @YES, }; RCTDevSettingsUserDefaultsDataSource *dataSource = [[RCTDevSettingsUserDefaultsDataSource alloc] initWithDefaultValues:defaultValues]; return [self initWithDataSource:dataSource]; } - (instancetype)initWithDataSource:(id)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"]; #endif #if RCT_ENABLE_INSPECTOR && !TARGET_OS_UIKITFORMAC // 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 connectWithBundleURL:bridge.bundleURL]; } queue:RCTJSThread]; }); #endif } - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } - (void)invalidate { #if ENABLE_PACKAGER_CONNECTION [[RCTPackagerConnection sharedPackagerConnection] removeHandler:_reloadToken]; #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 _bridge.isInspectable; #else return false; #endif // RCT_ENABLE_INSPECTOR } - (BOOL)isRemoteDebuggingAvailable { if (RCTTurboModuleEnabled()) { return NO; } Class jsDebuggingExecutorClass = objc_lookUpClass("RCTWebSocketExecutor"); return (jsDebuggingExecutorClass != nil); } - (BOOL)isHotLoadingAvailable { return _bridge.bundleURL && !_bridge.bundleURL.fileURL; // Only works when running from server } 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 (self.isHotLoadingAvailable && enabled != RCTProfileIsProfiling()) { if (enabled) { [_bridge startProfiling]; } else { [_bridge stopProfiling:^(NSData *logData) { RCTProfileSendResult(self->_bridge, @"systrace", logData); }]; } } } RCT_EXPORT_METHOD(setHotLoadingEnabled : (BOOL)enabled) { if (self.isHotLoadingEnabled != enabled) { [self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingHotLoadingEnabled]; if (_isJSLoaded) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if (enabled) { [_bridge enqueueJSCall:@"HMRClient" method:@"enable" args:@[] completion:NULL]; } else { [_bridge enqueueJSCall:@"HMRClient" method:@"disable" args:@[] completion:NULL]; } #pragma clang diagnostic pop } } } - (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 } } - (BOOL)isElementInspectorShown { return [[self settingForKey:kRCTDevSettingIsInspectorShown] boolValue]; } - (void)setIsPerfMonitorShown:(BOOL)isPerfMonitorShown { [self _updateSettingWithValue:@(isPerfMonitorShown) forKey:kRCTDevSettingIsPerfMonitorShown]; } - (BOOL)isPerfMonitorShown { return [[self settingForKey:kRCTDevSettingIsPerfMonitorShown] 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_MENU - (void)addHandler:(id)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 _remoteDebugSettingDidChange]; [self _profilingSettingDidChange]; } - (void)jsLoaded:(NSNotification *)notification { if (notification.userInfo[@"bridge"] != _bridge) { return; } _isJSLoaded = YES; 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)dataSource { return [super init]; } - (BOOL)isHotLoadingAvailable { return NO; } - (BOOL)isRemoteDebuggingAvailable { return NO; } - (id)settingForKey:(NSString *)key { return nil; } - (void)reload { } - (void)toggleElementInspector { } @end #endif @implementation RCTBridge (RCTDevSettings) - (RCTDevSettings *)devSettings { #if RCT_DEV return [self moduleForClass:[RCTDevSettings class]]; #else return nil; #endif } @end