/** * 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 #import #import #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 #endif #if RCT_DEV @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 () { 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 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)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 *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)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)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