Split JSPackagerWebSocketClient into JSPackagerClient and ReconnectingWebSocket

Reviewed By: cwdick

Differential Revision: D4495054

fbshipit-source-id: 49c634dde789317270c711370dfbdc82f70f44cd
This commit is contained in:
Lukas Piatkowski
2017-02-14 09:37:28 -08:00
committed by Facebook Github Bot
parent 0ea47424ff
commit 47616d84d8
6 changed files with 324 additions and 223 deletions

View File

@@ -13,7 +13,9 @@ import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import android.content.Context;
@@ -28,7 +30,8 @@ import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.network.OkHttpCallUtil;
import com.facebook.react.devsupport.interfaces.PackagerStatusCallback;
import com.facebook.react.modules.systeminfo.AndroidInfoHelpers;
import com.facebook.react.packagerconnection.JSPackagerWebSocketClient;
import com.facebook.react.packagerconnection.JSPackagerClient;
import com.facebook.react.packagerconnection.ReconnectingWebSocket;
import okhttp3.Call;
import okhttp3.Callback;
@@ -86,7 +89,7 @@ public class DevServerHelper {
public interface PackagerCommandListener {
void onPackagerReloadCommand();
void onCaptureHeapCommand();
void onPokeSamplingProfilerCommand(@Nullable final JSPackagerWebSocketClient.WebSocketSender webSocket);
void onPokeSamplingProfilerCommand(@Nullable final ReconnectingWebSocket.WebSocketSender webSocket);
}
private final DevInternalSettings mSettings;
@@ -94,7 +97,7 @@ public class DevServerHelper {
private final Handler mRestartOnChangePollingHandler;
private boolean mOnChangePollingEnabled;
private @Nullable JSPackagerWebSocketClient mPackagerConnection;
private @Nullable JSPackagerClient mPackagerClient;
private @Nullable InspectorPackagerConnection mInspectorPackagerConnection;
private @Nullable OkHttpClient mOnChangePollingClient;
private @Nullable OnServerContentChangeListener mOnServerContentChangeListener;
@@ -112,32 +115,40 @@ public class DevServerHelper {
}
public void openPackagerConnection(final PackagerCommandListener commandListener) {
if (mPackagerConnection != null) {
if (mPackagerClient != null) {
FLog.w(ReactConstants.TAG, "Packager connection already open, nooping.");
return;
}
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
mPackagerConnection = new JSPackagerWebSocketClient(getPackagerConnectionURL(),
new JSPackagerWebSocketClient.JSPackagerCallback() {
@Override
public void onMessage(
@Nullable JSPackagerWebSocketClient.WebSocketSender webSocket,
String target,
String action) {
if (commandListener != null && "bridge".equals(target)) {
if ("reload".equals(action)) {
commandListener.onPackagerReloadCommand();
} else if ("captureHeap".equals(action)) {
commandListener.onCaptureHeapCommand();
} else if ("pokeSamplingProfiler".equals(action)) {
commandListener.onPokeSamplingProfilerCommand(webSocket);
}
}
}
});
mPackagerConnection.connect();
Map<String, JSPackagerClient.RequestHandler> handlers =
new HashMap<String, JSPackagerClient.RequestHandler>();
handlers.put("reload", new JSPackagerClient.RequestHandler() {
@Override
public void onNotification(
@Nullable ReconnectingWebSocket.WebSocketSender webSocket) {
commandListener.onPackagerReloadCommand();
}
});
handlers.put("captureHeap", new JSPackagerClient.RequestHandler() {
@Override
public void onNotification(
@Nullable ReconnectingWebSocket.WebSocketSender webSocket) {
commandListener.onCaptureHeapCommand();
}
});
handlers.put("pokeSamplingProfiler", new JSPackagerClient.RequestHandler() {
@Override
public void onNotification(
@Nullable ReconnectingWebSocket.WebSocketSender webSocket) {
commandListener.onPokeSamplingProfilerCommand(webSocket);
}
});
mPackagerClient = new JSPackagerClient(getPackagerConnectionURL(), handlers);
mPackagerClient.init();
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
@@ -147,9 +158,9 @@ public class DevServerHelper {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if (mPackagerConnection != null) {
mPackagerConnection.closeQuietly();
mPackagerConnection = null;
if (mPackagerClient != null) {
mPackagerClient.close();
mPackagerClient = null;
}
return null;
}

View File

@@ -56,7 +56,7 @@ import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.devsupport.interfaces.PackagerStatusCallback;
import com.facebook.react.devsupport.interfaces.StackFrame;
import com.facebook.react.modules.debug.interfaces.DeveloperSettings;
import com.facebook.react.packagerconnection.JSPackagerWebSocketClient;
import com.facebook.react.packagerconnection.ReconnectingWebSocket;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
@@ -694,7 +694,7 @@ public class DevSupportManagerImpl implements
@Override
public void onPokeSamplingProfilerCommand(
@Nullable final JSPackagerWebSocketClient.WebSocketSender webSocket) {
@Nullable final ReconnectingWebSocket.WebSocketSender webSocket) {
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -710,7 +710,7 @@ public class DevSupportManagerImpl implements
}
private void handlePokeSamplingProfiler(
@Nullable JSPackagerWebSocketClient.WebSocketSender webSocket) {
@Nullable ReconnectingWebSocket.WebSocketSender webSocket) {
try {
List<String> pokeResults = JSCSamplingProfiler.poke(60000);
for (String result : pokeResults) {

View File

@@ -0,0 +1,97 @@
/**
* 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.packagerconnection;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Map;
import android.util.JsonReader;
import android.util.JsonToken;
import com.facebook.common.logging.FLog;
import okhttp3.ResponseBody;
import okhttp3.ws.WebSocket;
/**
* A client for packager that uses WebSocket connection.
*/
final public class JSPackagerClient implements ReconnectingWebSocket.MessageCallback {
private static final String TAG = JSPackagerClient.class.getSimpleName();
public interface RequestHandler {
public void onNotification(@Nullable ReconnectingWebSocket.WebSocketSender webSocket);
}
private ReconnectingWebSocket mWebSocket;
private Map<String, RequestHandler> mRequestHandlers;
public JSPackagerClient(String url, Map<String, RequestHandler> requestHandlers) {
super();
mWebSocket = new ReconnectingWebSocket(url, this);
mRequestHandlers = requestHandlers;
}
public void init() {
mWebSocket.connect();
}
public void close() {
mWebSocket.closeQuietly();
}
@Override
public void onMessage(@Nullable ReconnectingWebSocket.WebSocketSender webSocket, ResponseBody response) {
if (response.contentType() != WebSocket.TEXT) {
FLog.w(TAG, "Websocket received unexpected message with payload of type " + response.contentType());
return;
}
try {
JsonReader reader = new JsonReader(response.charStream());
Integer version = null;
String target = null;
String action = null;
reader.beginObject();
while (reader.hasNext()) {
String field = reader.nextName();
if (JsonToken.NULL == reader.peek()) {
reader.skipValue();
continue;
}
if ("version".equals(field)) {
version = reader.nextInt();
} else if ("target".equals(field)) {
target = reader.nextString();
} else if ("action".equals(field)) {
action = reader.nextString();
}
}
reader.close();
if (version == null || target == null || action == null || version != 1) {
return;
}
if ("bridge".equals(target) && mRequestHandlers.containsKey(action)) {
mRequestHandlers.get(action).onNotification(webSocket);
}
} catch (IOException e) {
FLog.e(TAG, "Parsing response message from websocket failed", e);
} finally {
response.close();
}
}
}

View File

@@ -15,8 +15,6 @@ import java.util.concurrent.TimeUnit;
import android.os.Handler;
import android.os.Looper;
import android.util.JsonReader;
import android.util.JsonToken;
import com.facebook.common.logging.FLog;
@@ -31,19 +29,14 @@ import okhttp3.ws.WebSocketListener;
import okio.Buffer;
/**
* A wrapper around WebSocketClient that recognizes packager's message format.
* A wrapper around WebSocketClient that reconnects automatically
*/
public class JSPackagerWebSocketClient implements WebSocketListener {
private static final String TAG = "JSPackagerWebSocketClient";
final public class ReconnectingWebSocket implements WebSocketListener {
private static final String TAG = ReconnectingWebSocket.class.getSimpleName();
private static final int RECONNECT_DELAY_MS = 2000;
private final String mUrl;
private final Handler mHandler;
private boolean mClosed = false;
private boolean mSuppressConnectionErrors;
final public class WebSocketSender {
static public class WebSocketSender {
private WebSocket mWebSocket;
public WebSocketSender(WebSocket webSocket) {
@@ -55,14 +48,18 @@ public class JSPackagerWebSocketClient implements WebSocketListener {
}
}
public interface JSPackagerCallback {
void onMessage(@Nullable WebSocketSender webSocket, String target, String action);
public interface MessageCallback {
void onMessage(@Nullable WebSocketSender webSocket, ResponseBody message);
}
private final String mUrl;
private final Handler mHandler;
private boolean mClosed = false;
private boolean mSuppressConnectionErrors;
private @Nullable WebSocket mWebSocket;
private @Nullable JSPackagerCallback mCallback;
private @Nullable MessageCallback mCallback;
public JSPackagerWebSocketClient(String url, JSPackagerCallback callback) {
public ReconnectingWebSocket(String url, MessageCallback callback) {
super();
mUrl = url;
mCallback = callback;
@@ -73,33 +70,40 @@ public class JSPackagerWebSocketClient implements WebSocketListener {
if (mClosed) {
throw new IllegalStateException("Can't connect closed client");
}
OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read
.build();
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 synchronized void delayedReconnect() {
// check that we haven't been closed in the meantime
if (!mClosed) {
connect();
}
}
private void reconnect() {
if (mClosed) {
throw new IllegalStateException("Can't reconnect closed client");
}
if (!mSuppressConnectionErrors) {
FLog.w(TAG, "Couldn't connect to packager, will silently retry");
FLog.w(TAG, "Couldn't connect to \"" + mUrl + "\", will silently retry");
mSuppressConnectionErrors = true;
}
mHandler.postDelayed(
new Runnable() {
@Override
public void run() {
// check that we haven't been closed in the meantime
if (!mClosed) {
connect();
}
delayedReconnect();
}
},
RECONNECT_DELAY_MS);
@@ -108,6 +112,7 @@ public class JSPackagerWebSocketClient implements WebSocketListener {
public void closeQuietly() {
mClosed = true;
closeWebSocketQuietly();
mCallback = null;
}
private void closeWebSocketQuietly() {
@@ -121,65 +126,19 @@ public class JSPackagerWebSocketClient implements WebSocketListener {
}
}
private void triggerMessageCallback(String target, String action) {
if (mCallback != null) {
WebSocketSender webSocketSender = mWebSocket == null
? null
: new WebSocketSender(mWebSocket);
mCallback.onMessage(webSocketSender, target, action);
}
private void abort(String message, Throwable cause) {
FLog.e(TAG, "Error occurred, shutting down websocket connection: " + message, cause);
closeWebSocketQuietly();
}
@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;
}
try {
JsonReader reader = new JsonReader(response.charStream());
Integer version = null;
String target = null;
String action = null;
reader.beginObject();
while (reader.hasNext()) {
String field = reader.nextName();
if (JsonToken.NULL == reader.peek()) {
reader.skipValue();
continue;
}
if ("version".equals(field)) {
version = reader.nextInt();
} else if ("target".equals(field)) {
target = reader.nextString();
} else if ("action".equals(field)) {
action = reader.nextString();
}
}
reader.close();
if (version == null || version != 1) {
return;
}
if (target == null || action == null) {
return;
}
triggerMessageCallback(target, action);
} catch (IOException e) {
abort("Parsing response message from websocket failed", e);
} finally {
response.close();
}
public synchronized void onOpen(WebSocket webSocket, Response response) {
mWebSocket = webSocket;
mSuppressConnectionErrors = false;
}
@Override
public void onFailure(IOException e, Response response) {
public synchronized void onFailure(IOException e, Response response) {
if (mWebSocket != null) {
abort("Websocket exception", e);
}
@@ -189,26 +148,23 @@ public class JSPackagerWebSocketClient implements WebSocketListener {
}
@Override
public void onOpen(WebSocket webSocket, Response response) {
mWebSocket = webSocket;
mSuppressConnectionErrors = false;
public synchronized void onMessage(ResponseBody message) {
if (mCallback != null) {
WebSocketSender webSocketSender = mWebSocket == null
? null
: new WebSocketSender(mWebSocket);
mCallback.onMessage(webSocketSender, message);
}
}
@Override
public void onClose(int code, String reason) {
public synchronized void onPong(Buffer payload) { }
@Override
public synchronized void onClose(int code, String reason) {
mWebSocket = null;
if (!mClosed) {
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);
closeWebSocketQuietly();
}
}