Move JSPackagerWebSocketClient and JSDebuggerWebSocketClient to react/devsupport module

Summary: This move lets us remove the dependency on okhttp3 from react/bridge. The classes I moved are all strictly related to dev support features and don't need to go into the core bridge module that gets shipped to production.

Reviewed By: AaaChiuuu

Differential Revision: D3698977

fbshipit-source-id: 6ba1517377061690cef289a544ec1622122cef85
This commit is contained in:
Don Yu
2016-08-11 09:47:37 -07:00
committed by Facebook Github Bot 9
parent 6e60b1763c
commit 031fe4d797
7 changed files with 6 additions and 10 deletions

View File

@@ -7,6 +7,7 @@ android_library(
deps = [
react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'),
react_native_dep('third-party/java/infer-annotations:infer-annotations'),
react_native_dep('third-party/java/jackson:core'),
react_native_dep('third-party/java/jsr-305:jsr-305'),
react_native_dep('third-party/java/okhttp:okhttp3'),
react_native_dep('third-party/java/okhttp:okhttp3-ws'),

View File

@@ -15,7 +15,6 @@ import android.text.TextUtils;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.JSPackagerWebSocketClient;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.network.OkHttpCallUtil;

View File

@@ -45,7 +45,6 @@ import com.facebook.react.bridge.JavaJSExecutor;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WebsocketJavaScriptExecutor;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.ShakeDetector;
import com.facebook.react.common.futures.SimpleSettableFuture;

View File

@@ -0,0 +1,265 @@
/**
* 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.
*/
package com.facebook.react.devsupport;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.TimeUnit;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.ws.WebSocket;
import okhttp3.ws.WebSocketCall;
import okhttp3.ws.WebSocketListener;
import okio.Buffer;
/**
* A wrapper around WebSocketClient that recognizes RN debugging message format.
*/
public class JSDebuggerWebSocketClient implements WebSocketListener {
private static final String TAG = "JSDebuggerWebSocketClient";
private static final JsonFactory mJsonFactory = new JsonFactory();
public interface JSDebuggerCallback {
void onSuccess(@Nullable String response);
void onFailure(Throwable cause);
}
private @Nullable WebSocket mWebSocket;
private @Nullable OkHttpClient mHttpClient;
private @Nullable JSDebuggerCallback mConnectCallback;
private final AtomicInteger mRequestID = new AtomicInteger();
private final ConcurrentHashMap<Integer, JSDebuggerCallback> mCallbacks =
new ConcurrentHashMap<>();
public void connect(String url, JSDebuggerCallback callback) {
if (mHttpClient != null) {
throw new IllegalStateException("JSDebuggerWebSocketClient is already initialized.");
}
mConnectCallback = callback;
mHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read
.build();
Request request = new Request.Builder().url(url).build();
WebSocketCall call = WebSocketCall.create(mHttpClient, request);
call.enqueue(this);
}
/**
* Creates the next JSON message to send to remote JS executor, with request ID pre-filled in.
*/
private JsonGenerator startMessageObject(int requestID) throws IOException {
JsonGenerator jg = mJsonFactory.createGenerator(new StringWriter());
jg.writeStartObject();
jg.writeNumberField("id", requestID);
return jg;
}
/**
* Takes in a JsonGenerator created by {@link #startMessageObject} and returns the stringified
* JSON
*/
private String endMessageObject(JsonGenerator jg) throws IOException {
jg.writeEndObject();
jg.flush();
return ((StringWriter) jg.getOutputTarget()).getBuffer().toString();
}
public void prepareJSRuntime(JSDebuggerCallback callback) {
int requestID = mRequestID.getAndIncrement();
mCallbacks.put(requestID, callback);
try {
JsonGenerator jg = startMessageObject(requestID);
jg.writeStringField("method", "prepareJSRuntime");
sendMessage(requestID, endMessageObject(jg));
} catch (IOException e) {
triggerRequestFailure(requestID, e);
}
}
public void loadApplicationScript(
String sourceURL,
HashMap<String, String> injectedObjects,
JSDebuggerCallback callback) {
int requestID = mRequestID.getAndIncrement();
mCallbacks.put(requestID, callback);
try {
JsonGenerator jg = startMessageObject(requestID);
jg.writeStringField("method", "executeApplicationScript");
jg.writeStringField("url", sourceURL);
jg.writeObjectFieldStart("inject");
for (String key : injectedObjects.keySet()) {
jg.writeObjectField(key, injectedObjects.get(key));
}
jg.writeEndObject();
sendMessage(requestID, endMessageObject(jg));
} catch (IOException e) {
triggerRequestFailure(requestID, e);
}
}
public void executeJSCall(
String methodName,
String jsonArgsArray,
JSDebuggerCallback callback) {
int requestID = mRequestID.getAndIncrement();
mCallbacks.put(requestID, callback);
try {
JsonGenerator jg = startMessageObject(requestID);
jg.writeStringField("method", methodName);
jg.writeFieldName("arguments");
jg.writeRawValue(jsonArgsArray);
sendMessage(requestID, endMessageObject(jg));
} catch (IOException e) {
triggerRequestFailure(requestID, e);
}
}
public void closeQuietly() {
if (mWebSocket != null) {
try {
mWebSocket.close(1000, "End of session");
} catch (IOException e) {
// swallow, no need to handle it here
}
mWebSocket = null;
}
}
private void sendMessage(int requestID, String message) {
if (mWebSocket == null) {
triggerRequestFailure(
requestID,
new IllegalStateException("WebSocket connection no longer valid"));
return;
}
try {
mWebSocket.sendMessage(RequestBody.create(WebSocket.TEXT, message));
} catch (IOException e) {
triggerRequestFailure(requestID, e);
}
}
private void triggerRequestFailure(int requestID, Throwable cause) {
JSDebuggerCallback callback = mCallbacks.get(requestID);
if (callback != null) {
mCallbacks.remove(requestID);
callback.onFailure(cause);
}
}
private void triggerRequestSuccess(int requestID, @Nullable String response) {
JSDebuggerCallback callback = mCallbacks.get(requestID);
if (callback != null) {
mCallbacks.remove(requestID);
callback.onSuccess(response);
}
}
@Override
public void onMessage(ResponseBody response) throws IOException {
if (response.contentType() != WebSocket.TEXT) {
FLog.w(TAG, "Websocket received unexpected message with payload of type " + response.contentType());
return;
}
String message = null;
try {
message = response.source().readUtf8();
} finally {
response.close();
}
Integer replyID = null;
try {
JsonParser parser = new JsonFactory().createParser(message);
String result = null;
while (parser.nextToken() != JsonToken.END_OBJECT) {
String field = parser.getCurrentName();
if ("replyID".equals(field)) {
parser.nextToken();
replyID = parser.getIntValue();
} else if ("result".equals(field)) {
parser.nextToken();
result = parser.getText();
}
}
if (replyID != null) {
triggerRequestSuccess(replyID, result);
}
} catch (IOException e) {
if (replyID != null) {
triggerRequestFailure(replyID, e);
} else {
abort("Parsing response message from websocket failed", e);
}
}
}
@Override
public void onFailure(IOException e, Response response) {
abort("Websocket exception", e);
}
@Override
public void onOpen(WebSocket webSocket, Response response) {
mWebSocket = webSocket;
Assertions.assertNotNull(mConnectCallback).onSuccess(null);
mConnectCallback = null;
}
@Override
public void onClose(int code, String reason) {
mWebSocket = null;
}
@Override
public void onPong(Buffer payload) {
// ignore
}
private void abort(String message, Throwable cause) {
FLog.e(TAG, "Error occurred, shutting down websocket connection: " + message, cause);
closeQuietly();
// Trigger failure callbacks
if (mConnectCallback != null) {
mConnectCallback.onFailure(cause);
mConnectCallback = null;
}
for (JSDebuggerCallback callback : mCallbacks.values()) {
callback.onFailure(cause);
}
mCallbacks.clear();
}
}

View File

@@ -0,0 +1,178 @@
/**
* 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.
*/
package com.facebook.react.devsupport;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import android.os.Handler;
import android.os.Looper;
import com.facebook.common.logging.FLog;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.ws.WebSocket;
import okhttp3.ws.WebSocketCall;
import okhttp3.ws.WebSocketListener;
import okio.Buffer;
/**
* A wrapper around WebSocketClient that recognizes packager's message format.
*/
public class JSPackagerWebSocketClient implements WebSocketListener {
private static final String TAG = "JSPackagerWebSocketClient";
private static final int RECONNECT_DELAY_MS = 2000;
private final String mUrl;
private final Handler mHandler;
private boolean mSuppressConnectionErrors;
public interface JSPackagerCallback {
void onMessage(String target, String action);
}
private @Nullable WebSocket mWebSocket;
private @Nullable JSPackagerCallback mCallback;
public JSPackagerWebSocketClient(String url, JSPackagerCallback callback) {
super();
mUrl = url;
mCallback = callback;
mHandler = new Handler(Looper.getMainLooper());
}
public void connect() {
OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read
.build();
Request request = new Request.Builder().url(mUrl).build();
WebSocketCall call = WebSocketCall.create(httpClient, request);
call.enqueue(this);
}
private void reconnect() {
if (!mSuppressConnectionErrors) {
FLog.w(TAG, "Couldn't connect to packager, will silently retry");
mSuppressConnectionErrors = true;
}
mHandler.postDelayed(
new Runnable() {
@Override
public void run() {
connect();
}
}, RECONNECT_DELAY_MS);
}
public void closeQuietly() {
if (mWebSocket != null) {
try {
mWebSocket.close(1000, "End of session");
} catch (IOException e) {
// swallow, no need to handle it here
}
mWebSocket = null;
}
}
private void triggerMessageCallback(String target, String action) {
if (mCallback != null) {
mCallback.onMessage(target, action);
}
}
@Override
public void onMessage(ResponseBody response) throws IOException {
if (response.contentType() != WebSocket.TEXT) {
FLog.w(TAG, "Websocket received unexpected message with payload of type " + response.contentType());
return;
}
String message = null;
try {
message = response.source().readUtf8();
} finally {
response.close();
}
try {
JsonParser parser = new JsonFactory().createParser(message);
Integer version = null;
String target = null;
String action = null;
while (parser.nextToken() != JsonToken.END_OBJECT) {
String field = parser.getCurrentName();
if ("version".equals(field)) {
parser.nextToken();
version = parser.getIntValue();
} else if ("target".equals(field)) {
parser.nextToken();
target = parser.getText();
} else if ("action".equals(field)) {
parser.nextToken();
action = parser.getText();
}
}
if (version != 1) {
return;
}
if (target == null || action == null) {
return;
}
triggerMessageCallback(target, action);
} catch (IOException e) {
abort("Parsing response message from websocket failed", e);
}
}
@Override
public void onFailure(IOException e, Response response) {
if (mWebSocket != null) {
abort("Websocket exception", e);
}
reconnect();
}
@Override
public void onOpen(WebSocket webSocket, Response response) {
mWebSocket = webSocket;
mSuppressConnectionErrors = false;
}
@Override
public void onClose(int code, String reason) {
mWebSocket = null;
reconnect();
}
@Override
public void onPong(Buffer payload) {
// ignore
}
private void abort(String message, Throwable cause) {
FLog.e(TAG, "Error occurred, shutting down websocket connection: " + message, cause);
closeQuietly();
}
}

View File

@@ -0,0 +1,197 @@
/**
* 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.
*/
package com.facebook.react.devsupport;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import android.os.Handler;
import android.os.Looper;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.JavaJSExecutor;
/**
* Executes JS remotely via the react nodejs server as a proxy to a browser on the host machine.
*/
public class WebsocketJavaScriptExecutor implements JavaJSExecutor {
private static final long CONNECT_TIMEOUT_MS = 5000;
private static final int CONNECT_RETRY_COUNT = 3;
public interface JSExecutorConnectCallback {
void onSuccess();
void onFailure(Throwable cause);
}
public static class WebsocketExecutorTimeoutException extends Exception {
public WebsocketExecutorTimeoutException(String message) {
super(message);
}
}
private static class JSExecutorCallbackFuture implements
JSDebuggerWebSocketClient.JSDebuggerCallback {
private final Semaphore mSemaphore = new Semaphore(0);
private @Nullable Throwable mCause;
private @Nullable String mResponse;
@Override
public void onSuccess(@Nullable String response) {
mResponse = response;
mSemaphore.release();
}
@Override
public void onFailure(Throwable cause) {
mCause = cause;
mSemaphore.release();
}
/**
* Call only once per object instance!
*/
public @Nullable String get() throws Throwable {
mSemaphore.acquire();
if (mCause != null) {
throw mCause;
}
return mResponse;
}
}
final private HashMap<String, String> mInjectedObjects = new HashMap<>();
private @Nullable JSDebuggerWebSocketClient mWebSocketClient;
public void connect(final String webSocketServerUrl, final JSExecutorConnectCallback callback) {
final AtomicInteger retryCount = new AtomicInteger(CONNECT_RETRY_COUNT);
final JSExecutorConnectCallback retryProxyCallback = new JSExecutorConnectCallback() {
@Override
public void onSuccess() {
callback.onSuccess();
}
@Override
public void onFailure(Throwable cause) {
if (retryCount.decrementAndGet() <= 0) {
callback.onFailure(cause);
} else {
connectInternal(webSocketServerUrl, this);
}
}
};
connectInternal(webSocketServerUrl, retryProxyCallback);
}
private void connectInternal(
String webSocketServerUrl,
final JSExecutorConnectCallback callback) {
final JSDebuggerWebSocketClient client = new JSDebuggerWebSocketClient();
final Handler timeoutHandler = new Handler(Looper.getMainLooper());
client.connect(
webSocketServerUrl, new JSDebuggerWebSocketClient.JSDebuggerCallback() {
// It's possible that both callbacks can fire on an error so make sure we only
// dispatch results once to our callback.
private boolean didSendResult = false;
@Override
public void onSuccess(@Nullable String response) {
client.prepareJSRuntime(
new JSDebuggerWebSocketClient.JSDebuggerCallback() {
@Override
public void onSuccess(@Nullable String response) {
timeoutHandler.removeCallbacksAndMessages(null);
mWebSocketClient = client;
if (!didSendResult) {
callback.onSuccess();
didSendResult = true;
}
}
@Override
public void onFailure(Throwable cause) {
timeoutHandler.removeCallbacksAndMessages(null);
if (!didSendResult) {
callback.onFailure(cause);
didSendResult = true;
}
}
});
}
@Override
public void onFailure(Throwable cause) {
timeoutHandler.removeCallbacksAndMessages(null);
if (!didSendResult) {
callback.onFailure(cause);
didSendResult = true;
}
}
});
timeoutHandler.postDelayed(
new Runnable() {
@Override
public void run() {
client.closeQuietly();
callback.onFailure(
new WebsocketExecutorTimeoutException(
"Timeout while connecting to remote debugger"));
}
},
CONNECT_TIMEOUT_MS);
}
@Override
public void close() {
if (mWebSocketClient != null) {
mWebSocketClient.closeQuietly();
}
}
@Override
public void loadApplicationScript(String sourceURL)
throws ProxyExecutorException {
JSExecutorCallbackFuture callback = new JSExecutorCallbackFuture();
Assertions.assertNotNull(mWebSocketClient).loadApplicationScript(
sourceURL,
mInjectedObjects,
callback);
try {
callback.get();
} catch (Throwable cause) {
throw new ProxyExecutorException(cause);
}
}
@Override
public @Nullable String executeJSCall(String methodName, String jsonArgsArray)
throws ProxyExecutorException {
JSExecutorCallbackFuture callback = new JSExecutorCallbackFuture();
Assertions.assertNotNull(mWebSocketClient).executeJSCall(
methodName,
jsonArgsArray,
callback);
try {
return callback.get();
} catch (Throwable cause) {
throw new ProxyExecutorException(cause);
}
}
@Override
public void setGlobalVariable(String propertyName, String jsonEncodedValue) {
// Store and use in the next loadApplicationScript() call.
mInjectedObjects.put(propertyName, jsonEncodedValue);
}
}