diff --git a/Libraries/WebSocket/RCTWebSocketManager.h b/Libraries/WebSocket/RCTWebSocketManager.h new file mode 100644 index 000000000..debc3f767 --- /dev/null +++ b/Libraries/WebSocket/RCTWebSocketManager.h @@ -0,0 +1,19 @@ +/** + * 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 "RCTDefines.h" + +#if RCT_DEV // Only supported in dev mode + +#import "RCTWebSocketProxy.h" + +@interface RCTWebSocketManager : NSObject +@end + +#endif diff --git a/Libraries/WebSocket/RCTWebSocketManager.m b/Libraries/WebSocket/RCTWebSocketManager.m new file mode 100644 index 000000000..7288dda09 --- /dev/null +++ b/Libraries/WebSocket/RCTWebSocketManager.m @@ -0,0 +1,143 @@ +/** + * 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 "RCTDefines.h" + +#if RCT_DEV // Only supported in dev mode + +#import "RCTWebSocketManager.h" + +#import "RCTConvert.h" +#import "RCTLog.h" +#import "RCTUtils.h" +#import "RCTSRWebSocket.h" + +#pragma mark - RCTWebSocketObserver + +@interface RCTWebSocketObserver : NSObject + +@property (nonatomic, strong) RCTSRWebSocket *socket; +@property (nonatomic, weak) id delegate; +@property (nonatomic, strong) dispatch_semaphore_t socketOpenSemaphore; + +- (instancetype)initWithURL:(NSURL *)url delegate:(id)delegate; + +@end + +@implementation RCTWebSocketObserver + +- (instancetype)initWithURL:(NSURL *)url delegate:(id)delegate +{ + if ((self = [self init])) { + _socket = [[RCTSRWebSocket alloc] initWithURL:url]; + _socket.delegate = self; + + _delegate = delegate; +} + return self; +} + +- (void)start +{ + _socketOpenSemaphore = dispatch_semaphore_create(0); + [_socket open]; + dispatch_semaphore_wait(_socketOpenSemaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 2)); +} + +- (void)stop +{ + _socket.delegate = nil; + [_socket closeWithCode:1000 reason:@"Invalidated"]; + _socket = nil; +} + +- (void)webSocket:(RCTSRWebSocket *)webSocket didReceiveMessage:(id)message +{ + if (_delegate) { + NSError *error = nil; + NSDictionary *msg = RCTJSONParse(message, &error); + + if (!error) { + [_delegate socketProxy:[RCTWebSocketManager sharedInstance] didReceiveMessage:msg]; + } else { + RCTLogError(@"WebSocketManager failed to parse message with error %@\n\n%@\n", error, message); + } + } +} + +- (void)webSocketDidOpen:(RCTSRWebSocket *)webSocket +{ + dispatch_semaphore_signal(_socketOpenSemaphore); +} + +- (void)webSocket:(RCTSRWebSocket *)webSocket didFailWithError:(NSError *)error +{ + dispatch_semaphore_signal(_socketOpenSemaphore); + dispatch_async(dispatch_get_main_queue(), ^{ + // Give the setUp method an opportunity to report an error first + RCTLogError(@"WebSocket connection failed with error %@", error); + }); +} + +@end + +#pragma mark - RCTWebSocketManager + +@interface RCTWebSocketManager() + +@property (nonatomic, strong) NSMutableDictionary *sockets; +@property (nonatomic, strong) dispatch_queue_t queue; + +@end + +@implementation RCTWebSocketManager + ++ (instancetype)sharedInstance +{ + static RCTWebSocketManager *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [self new]; + }); + + return sharedInstance; +} + +- (void)setDelegate:(id)delegate forURL:(NSURL *)url +{ + NSString *key = [url absoluteString]; + RCTWebSocketObserver *observer = _sockets[key]; + + if (observer) { + if (!delegate) { + [observer stop]; + [_sockets removeObjectForKey:key]; + } else { + observer.delegate = delegate; + } + } else { + RCTWebSocketObserver *newObserver = [[RCTWebSocketObserver alloc] initWithURL:url delegate:delegate]; + [newObserver start]; + _sockets[key] = newObserver; + } +} + +- (instancetype)init +{ + if ((self = [super init])) { + _sockets = [NSMutableDictionary new]; + _queue = dispatch_queue_create("com.facebook.React.WebSocketManager", DISPATCH_QUEUE_SERIAL); + } + return self; +} + +@end + +#endif diff --git a/React/Base/RCTWebSocketProxy.h b/React/Base/RCTWebSocketProxy.h new file mode 100644 index 000000000..e823c3c8f --- /dev/null +++ b/React/Base/RCTWebSocketProxy.h @@ -0,0 +1,26 @@ +/** + * 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 "RCTDefines.h" + +#if RCT_DEV // Only supported in dev mode + +#import "RCTWebSocketProxyDelegate.h" + +@protocol RCTWebSocketProxy + ++ (instancetype)sharedInstance; + +- (void)setDelegate:(id)delegate forURL:(NSURL *)url; + +- (instancetype) init __attribute__((unavailable("init not available, call sharedInstance instead"))); + +@end + +#endif diff --git a/React/Base/RCTWebSocketProxyDelegate.h b/React/Base/RCTWebSocketProxyDelegate.h new file mode 100644 index 000000000..f668bf161 --- /dev/null +++ b/React/Base/RCTWebSocketProxyDelegate.h @@ -0,0 +1,20 @@ +/** + * 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 "RCTDefines.h" + +#if RCT_DEV // Only supported in dev mode + +@protocol RCTWebSocketProxy; + +@protocol RCTWebSocketProxyDelegate +- (void)socketProxy:(id)sender didReceiveMessage:(NSDictionary *)message; +@end + +#endif diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index c3a47cb27..69773d26c 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -19,6 +19,7 @@ #import "RCTRootView.h" #import "RCTSourceCode.h" #import "RCTUtils.h" +#import "RCTWebSocketProxy.h" #if RCT_DEV @@ -117,7 +118,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) @end -@interface RCTDevMenu () +@interface RCTDevMenu () @property (nonatomic, strong) Class executorClass; @@ -194,6 +195,7 @@ RCT_EXPORT_MODULE() // Delay setup until after Bridge init dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf updateSettings:_settings]; + [weakSelf connectPackager]; }); #if TARGET_IPHONE_SIMULATOR @@ -228,6 +230,54 @@ RCT_EXPORT_MODULE() return self; } +- (NSURL *)packagerURL +{ + NSString *host = [_bridge.bundleURL host]; + if (!host) { + return nil; + } + + NSString *scheme = [_bridge.bundleURL scheme]; + NSNumber *port = [_bridge.bundleURL port]; + return [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@:%@/packager-proxy?role=client", scheme, host, port]]; +} + +// TODO: Move non-UI logic into separate RCTDevSettings module +- (void)connectPackager +{ + Class webSocketManagerClass = NSClassFromString(@"RCTWebSocketManager"); + id webSocketManager = (id )[webSocketManagerClass sharedInstance]; + NSURL *url = [self packagerURL]; + if (url) { + [webSocketManager setDelegate:self forURL:url]; + } +} + +- (BOOL)isSupportedVersion:(NSNumber *)version +{ + NSArray *const kSupportedVersions = @[ @1 ]; + return [kSupportedVersions containsObject:version]; +} + +- (void)socketProxy:(id)sender didReceiveMessage:(NSDictionary *)message +{ + if ([self isSupportedVersion:message[@"version"]]) { + [self processTarget:message[@"target"] action:message[@"action"] options:message[@"options"]]; + } +} + +- (void)processTarget:(NSString *)target action:(NSString *)action options:(NSDictionary *)options +{ + if ([target isEqualToString:@"bridge"]) { + if ([action isEqualToString:@"reload"]) { + if ([options[@"debug"] boolValue]) { + _bridge.executorClass = NSClassFromString(@"RCTWebSocketExecutor"); + } + [_bridge reload]; + } + } +} + - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue();