2015-03-23 20:28:42 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2015-03-19 16:55:40 +00:00
|
|
|
|
|
|
|
#import "RCTWebSocketExecutor.h"
|
|
|
|
|
2016-11-23 15:47:52 +00:00
|
|
|
#import <React/RCTAssert.h>
|
|
|
|
#import <React/RCTBridge.h>
|
|
|
|
#import <React/RCTConvert.h>
|
|
|
|
#import <React/RCTDefines.h>
|
|
|
|
#import <React/RCTLog.h>
|
|
|
|
#import <React/RCTUtils.h>
|
|
|
|
|
2015-05-14 16:28:09 +00:00
|
|
|
#import "RCTSRWebSocket.h"
|
2015-03-19 16:55:40 +00:00
|
|
|
|
2016-11-21 15:02:58 +00:00
|
|
|
#if RCT_DEV // Debug executors are only supported in dev mode
|
|
|
|
|
2015-11-14 18:25:00 +00:00
|
|
|
typedef void (^RCTWSMessageCallback)(NSError *error, NSDictionary<NSString *, id> *reply);
|
2015-05-14 16:28:09 +00:00
|
|
|
|
|
|
|
@interface RCTWebSocketExecutor () <RCTSRWebSocketDelegate>
|
2015-03-19 16:55:40 +00:00
|
|
|
|
|
|
|
@end
|
|
|
|
|
2015-04-11 22:08:00 +00:00
|
|
|
@implementation RCTWebSocketExecutor
|
|
|
|
{
|
2015-05-14 16:28:09 +00:00
|
|
|
RCTSRWebSocket *_socket;
|
2015-04-11 22:08:00 +00:00
|
|
|
dispatch_queue_t _jsQueue;
|
2015-11-14 18:25:00 +00:00
|
|
|
NSMutableDictionary<NSNumber *, RCTWSMessageCallback> *_callbacks;
|
2015-03-19 16:55:40 +00:00
|
|
|
dispatch_semaphore_t _socketOpenSemaphore;
|
2015-11-14 18:25:00 +00:00
|
|
|
NSMutableDictionary<NSString *, NSString *> *_injectedObjects;
|
2015-06-09 22:42:10 +00:00
|
|
|
NSURL *_url;
|
2016-11-21 15:02:58 +00:00
|
|
|
NSError *_setupError;
|
2015-03-19 16:55:40 +00:00
|
|
|
}
|
|
|
|
|
2015-06-09 22:42:10 +00:00
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
|
2016-06-13 20:15:30 +00:00
|
|
|
@synthesize bridge = _bridge;
|
|
|
|
|
2015-04-11 22:08:00 +00:00
|
|
|
- (instancetype)initWithURL:(NSURL *)URL
|
2015-03-19 16:55:40 +00:00
|
|
|
{
|
2015-06-15 14:53:45 +00:00
|
|
|
RCTAssertParam(URL);
|
|
|
|
|
2015-11-25 11:09:00 +00:00
|
|
|
if ((self = [self init])) {
|
2015-06-09 22:42:10 +00:00
|
|
|
_url = URL;
|
2015-03-19 16:55:40 +00:00
|
|
|
}
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2015-06-09 22:42:10 +00:00
|
|
|
- (void)setUp
|
|
|
|
{
|
2015-11-25 11:09:00 +00:00
|
|
|
if (!_url) {
|
2017-03-22 12:27:58 +00:00
|
|
|
NSInteger port = [[[_bridge bundleURL] port] integerValue] ?: 8081;
|
|
|
|
NSString *host = [[_bridge bundleURL] host] ?: @"localhost";
|
2016-06-13 20:15:30 +00:00
|
|
|
NSString *URLString = [NSString stringWithFormat:@"http://%@:%zd/debugger-proxy?role=client", host, port];
|
2015-11-25 11:09:00 +00:00
|
|
|
_url = [RCTConvert NSURL:URLString];
|
|
|
|
}
|
|
|
|
|
2016-04-01 14:01:51 +00:00
|
|
|
_jsQueue = dispatch_queue_create("com.facebook.react.WebSocketExecutor", DISPATCH_QUEUE_SERIAL);
|
2015-06-09 22:42:10 +00:00
|
|
|
_socket = [[RCTSRWebSocket alloc] initWithURL:_url];
|
|
|
|
_socket.delegate = self;
|
2015-11-14 18:25:00 +00:00
|
|
|
_callbacks = [NSMutableDictionary new];
|
2015-08-17 14:35:34 +00:00
|
|
|
_injectedObjects = [NSMutableDictionary new];
|
2015-06-09 22:42:10 +00:00
|
|
|
[_socket setDelegateDispatchQueue:_jsQueue];
|
|
|
|
|
2016-04-07 20:14:39 +00:00
|
|
|
NSURL *startDevToolsURL = [NSURL URLWithString:@"/launch-js-devtools" relativeToURL:_url];
|
2015-06-09 22:42:10 +00:00
|
|
|
|
2016-09-27 13:19:45 +00:00
|
|
|
NSURLSession *session = [NSURLSession sharedSession];
|
|
|
|
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:[NSURLRequest requestWithURL:startDevToolsURL]
|
|
|
|
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){}];
|
|
|
|
[dataTask resume];
|
2015-06-09 22:42:10 +00:00
|
|
|
if (![self connectToProxy]) {
|
|
|
|
[self invalidate];
|
2016-11-21 15:02:58 +00:00
|
|
|
NSString *error = [NSString stringWithFormat:@"Connection to %@ timed out. Are you "
|
|
|
|
"running node proxy? If you are running on the device, check if "
|
|
|
|
"you have the right IP address in `RCTWebSocketExecutor.m`.", _url];
|
|
|
|
_setupError = RCTErrorWithMessage(error);
|
|
|
|
RCTFatal(_setupError);
|
2015-06-09 22:42:10 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
NSInteger retries = 3;
|
|
|
|
BOOL runtimeIsReady = [self prepareJSRuntime];
|
|
|
|
while (!runtimeIsReady && retries > 0) {
|
|
|
|
runtimeIsReady = [self prepareJSRuntime];
|
|
|
|
retries--;
|
|
|
|
}
|
|
|
|
if (!runtimeIsReady) {
|
|
|
|
[self invalidate];
|
2016-11-21 15:02:58 +00:00
|
|
|
NSString *error = @"Runtime is not ready for debugging.\n "
|
|
|
|
"- Make sure Packager server is running.\n"
|
|
|
|
"- Make sure the JavaScript Debugger is running and not paused on a "
|
|
|
|
"breakpoint or exception and try reloading again.";
|
|
|
|
_setupError = RCTErrorWithMessage(error);
|
|
|
|
RCTFatal(_setupError);
|
2015-06-09 22:42:10 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-19 16:55:40 +00:00
|
|
|
- (BOOL)connectToProxy
|
|
|
|
{
|
|
|
|
_socketOpenSemaphore = dispatch_semaphore_create(0);
|
|
|
|
[_socket open];
|
2016-04-20 00:12:24 +00:00
|
|
|
long connected = dispatch_semaphore_wait(_socketOpenSemaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 10));
|
2016-11-21 15:02:58 +00:00
|
|
|
return connected == 0 && _socket.readyState == RCTSR_OPEN;
|
2015-03-19 16:55:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)prepareJSRuntime
|
|
|
|
{
|
|
|
|
__block NSError *initError;
|
|
|
|
dispatch_semaphore_t s = dispatch_semaphore_create(0);
|
2016-11-21 15:02:58 +00:00
|
|
|
[self sendMessage:@{@"method": @"prepareJSRuntime"} onReply:^(NSError *error, NSDictionary<NSString *, id> *reply) {
|
2015-03-19 16:55:40 +00:00
|
|
|
initError = error;
|
|
|
|
dispatch_semaphore_signal(s);
|
|
|
|
}];
|
|
|
|
long runtimeIsReady = dispatch_semaphore_wait(s, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC));
|
2016-11-21 15:02:58 +00:00
|
|
|
if (initError) {
|
|
|
|
RCTLogInfo(@"Websocket runtime setup failed: %@", initError);
|
|
|
|
}
|
2015-03-19 16:55:40 +00:00
|
|
|
return runtimeIsReady == 0 && initError == nil;
|
|
|
|
}
|
|
|
|
|
2015-05-14 16:28:09 +00:00
|
|
|
- (void)webSocket:(RCTSRWebSocket *)webSocket didReceiveMessage:(id)message
|
2015-03-19 16:55:40 +00:00
|
|
|
{
|
|
|
|
NSError *error = nil;
|
2015-11-14 18:25:00 +00:00
|
|
|
NSDictionary<NSString *, id> *reply = RCTJSONParse(message, &error);
|
2015-08-21 17:15:04 +00:00
|
|
|
NSNumber *messageID = reply[@"replyID"];
|
|
|
|
RCTWSMessageCallback callback = _callbacks[messageID];
|
|
|
|
if (callback) {
|
|
|
|
callback(error, reply);
|
2016-12-08 01:11:57 +00:00
|
|
|
[_callbacks removeObjectForKey:messageID];
|
2015-03-19 16:55:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-14 16:28:09 +00:00
|
|
|
- (void)webSocketDidOpen:(RCTSRWebSocket *)webSocket
|
2015-03-19 16:55:40 +00:00
|
|
|
{
|
|
|
|
dispatch_semaphore_signal(_socketOpenSemaphore);
|
|
|
|
}
|
|
|
|
|
2015-05-14 16:28:09 +00:00
|
|
|
- (void)webSocket:(RCTSRWebSocket *)webSocket didFailWithError:(NSError *)error
|
2015-03-19 16:55:40 +00:00
|
|
|
{
|
2015-12-23 21:31:44 +00:00
|
|
|
dispatch_semaphore_signal(_socketOpenSemaphore);
|
2016-11-21 15:02:58 +00:00
|
|
|
RCTLogInfo(@"WebSocket connection failed with error %@", error);
|
2015-03-19 16:55:40 +00:00
|
|
|
}
|
|
|
|
|
2016-11-21 15:02:58 +00:00
|
|
|
- (void)sendMessage:(NSDictionary<NSString *, id> *)message onReply:(RCTWSMessageCallback)callback
|
2015-03-19 16:55:40 +00:00
|
|
|
{
|
|
|
|
static NSUInteger lastID = 10000;
|
|
|
|
|
2016-11-21 15:02:58 +00:00
|
|
|
if (_setupError) {
|
|
|
|
callback(_setupError, nil);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-04-11 22:08:00 +00:00
|
|
|
dispatch_async(_jsQueue, ^{
|
2015-03-19 16:55:40 +00:00
|
|
|
if (!self.valid) {
|
2017-06-12 10:31:44 +00:00
|
|
|
callback(RCTErrorWithMessage(@"Runtime is not ready for debugging. Make sure Packager server is running."), nil);
|
2015-03-19 16:55:40 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-04-11 22:08:00 +00:00
|
|
|
NSNumber *expectedID = @(lastID++);
|
2016-07-07 19:36:56 +00:00
|
|
|
self->_callbacks[expectedID] = [callback copy];
|
2015-11-14 18:25:00 +00:00
|
|
|
NSMutableDictionary<NSString *, id> *messageWithID = [message mutableCopy];
|
2015-04-11 22:08:00 +00:00
|
|
|
messageWithID[@"id"] = expectedID;
|
2016-07-07 19:36:56 +00:00
|
|
|
[self->_socket send:RCTJSONStringify(messageWithID, NULL)];
|
2015-04-11 22:08:00 +00:00
|
|
|
});
|
2015-03-19 16:55:40 +00:00
|
|
|
}
|
|
|
|
|
2015-10-16 15:10:25 +00:00
|
|
|
- (void)executeApplicationScript:(NSData *)script sourceURL:(NSURL *)URL onComplete:(RCTJavaScriptCompleteBlock)onComplete
|
2015-03-19 16:55:40 +00:00
|
|
|
{
|
2017-06-22 15:17:03 +00:00
|
|
|
// Hack: the bridge transitions out of loading state as soon as this method returns, which prevents us
|
|
|
|
// from completely invalidating the bridge and preventing an endless barage of RCTLog.logIfNoNativeHook
|
|
|
|
// calls if the JS execution environment is broken. We therefore block this thread until this message has returned.
|
|
|
|
dispatch_semaphore_t scriptSem = dispatch_semaphore_create(0);
|
|
|
|
|
2015-11-14 18:25:00 +00:00
|
|
|
NSDictionary<NSString *, id> *message = @{
|
2015-07-28 22:48:46 +00:00
|
|
|
@"method": @"executeApplicationScript",
|
2015-08-24 10:14:33 +00:00
|
|
|
@"url": RCTNullIfNil(URL.absoluteString),
|
2015-07-28 22:48:46 +00:00
|
|
|
@"inject": _injectedObjects,
|
|
|
|
};
|
2017-06-12 10:31:44 +00:00
|
|
|
[self sendMessage:message onReply:^(NSError *socketError, NSDictionary<NSString *, id> *reply) {
|
|
|
|
if (socketError) {
|
|
|
|
onComplete(socketError);
|
|
|
|
} else {
|
|
|
|
NSString *error = reply[@"error"];
|
|
|
|
onComplete(error ? RCTErrorWithMessage(error) : nil);
|
|
|
|
}
|
2017-06-22 15:17:03 +00:00
|
|
|
dispatch_semaphore_signal(scriptSem);
|
2015-03-19 16:55:40 +00:00
|
|
|
}];
|
2017-06-22 15:17:03 +00:00
|
|
|
|
|
|
|
dispatch_semaphore_wait(scriptSem, DISPATCH_TIME_FOREVER);
|
2015-03-19 16:55:40 +00:00
|
|
|
}
|
|
|
|
|
2015-12-08 23:57:34 +00:00
|
|
|
- (void)flushedQueue:(RCTJavaScriptCallback)onComplete
|
|
|
|
{
|
|
|
|
[self _executeJSCall:@"flushedQueue" arguments:@[] callback:onComplete];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)callFunctionOnModule:(NSString *)module
|
|
|
|
method:(NSString *)method
|
|
|
|
arguments:(NSArray *)args
|
|
|
|
callback:(RCTJavaScriptCallback)onComplete
|
|
|
|
{
|
|
|
|
[self _executeJSCall:@"callFunctionReturnFlushedQueue" arguments:@[module, method, args] callback:onComplete];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)invokeCallbackID:(NSNumber *)cbID
|
|
|
|
arguments:(NSArray *)args
|
|
|
|
callback:(RCTJavaScriptCallback)onComplete
|
|
|
|
{
|
|
|
|
[self _executeJSCall:@"invokeCallbackAndReturnFlushedQueue" arguments:@[cbID, args] callback:onComplete];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)_executeJSCall:(NSString *)method arguments:(NSArray *)arguments callback:(RCTJavaScriptCallback)onComplete
|
2015-03-19 16:55:40 +00:00
|
|
|
{
|
|
|
|
RCTAssert(onComplete != nil, @"callback was missing for exec JS call");
|
2015-11-14 18:25:00 +00:00
|
|
|
NSDictionary<NSString *, id> *message = @{
|
2015-12-08 23:57:34 +00:00
|
|
|
@"method": method,
|
2015-04-11 22:08:00 +00:00
|
|
|
@"arguments": arguments
|
|
|
|
};
|
2016-11-21 15:02:58 +00:00
|
|
|
[self sendMessage:message onReply:^(NSError *socketError, NSDictionary<NSString *, id> *reply) {
|
2015-03-19 16:55:40 +00:00
|
|
|
if (socketError) {
|
|
|
|
onComplete(nil, socketError);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-06-12 10:31:44 +00:00
|
|
|
NSError *jsonError;
|
|
|
|
id result = RCTJSONParse(reply[@"result"], &jsonError);
|
|
|
|
NSString *error = reply[@"error"];
|
|
|
|
onComplete(result, error ? RCTErrorWithMessage(error) : jsonError);
|
2015-03-19 16:55:40 +00:00
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)injectJSONText:(NSString *)script asGlobalObjectNamed:(NSString *)objectName callback:(RCTJavaScriptCompleteBlock)onComplete
|
|
|
|
{
|
2015-04-11 22:08:00 +00:00
|
|
|
dispatch_async(_jsQueue, ^{
|
2016-07-07 19:36:56 +00:00
|
|
|
self->_injectedObjects[objectName] = script;
|
2015-03-19 16:55:40 +00:00
|
|
|
onComplete(nil);
|
2015-04-11 22:08:00 +00:00
|
|
|
});
|
2015-03-19 16:55:40 +00:00
|
|
|
}
|
|
|
|
|
2015-04-22 14:03:55 +00:00
|
|
|
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
|
2015-06-13 00:01:27 +00:00
|
|
|
{
|
2016-06-06 14:57:55 +00:00
|
|
|
RCTExecuteOnMainQueue(block);
|
2015-06-13 00:01:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block
|
2015-04-22 14:03:55 +00:00
|
|
|
{
|
|
|
|
dispatch_async(dispatch_get_main_queue(), block);
|
|
|
|
}
|
|
|
|
|
2015-03-19 16:55:40 +00:00
|
|
|
- (void)invalidate
|
|
|
|
{
|
|
|
|
_socket.delegate = nil;
|
|
|
|
[_socket closeWithCode:1000 reason:@"Invalidated"];
|
|
|
|
_socket = nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)isValid
|
|
|
|
{
|
2015-05-14 16:28:09 +00:00
|
|
|
return _socket != nil && _socket.readyState == RCTSR_OPEN;
|
2015-03-19 16:55:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)dealloc
|
|
|
|
{
|
|
|
|
RCTAssert(!self.valid, @"-invalidate must be called before -dealloc");
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
2015-04-21 16:48:29 +00:00
|
|
|
|
|
|
|
#endif
|