Persistent websocket connection from Android to Packager

Reviewed By: astreet

Differential Revision: D3447685

fbshipit-source-id: 0e4e3fb02b84b9b15c2c798c0e4c89ff6fd1665c
This commit is contained in:
Alex Kotliarskyi 2016-06-22 11:22:19 -07:00 committed by Facebook Github Bot 0
parent b3886652ab
commit adcb9491bd
4 changed files with 200 additions and 2 deletions

View File

@ -0,0 +1,164 @@
/**
* 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.bridge;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
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 final String mUrl;
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;
}
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() {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
connect();
}
}, 2000);
}
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) {
abort("Websocket exception", e);
reconnect();
}
@Override
public void onOpen(WebSocket webSocket, Response response) {
mWebSocket = webSocket;
}
@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

@ -9,6 +9,7 @@ android_library(
react_native_dep('third-party/java/infer-annotations:infer-annotations'), react_native_dep('third-party/java/infer-annotations:infer-annotations'),
react_native_dep('third-party/java/jsr-305:jsr-305'), 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'),
react_native_dep('third-party/java/okhttp:okhttp3-ws'),
react_native_dep('third-party/java/okio:okio'), react_native_dep('third-party/java/okio:okio'),
react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/bridge:bridge'),
react_native_target('java/com/facebook/react/common:common'), react_native_target('java/com/facebook/react/common:common'),

View File

@ -15,6 +15,7 @@ import android.text.TextUtils;
import com.facebook.common.logging.FLog; import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.JSPackagerWebSocketClient;
import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.ReactConstants; import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.network.OkHttpCallUtil; import com.facebook.react.common.network.OkHttpCallUtil;
@ -59,6 +60,7 @@ public class DevServerHelper {
private static final String ONCHANGE_ENDPOINT_URL_FORMAT = private static final String ONCHANGE_ENDPOINT_URL_FORMAT =
"http://%s/onchange"; "http://%s/onchange";
private static final String WEBSOCKET_PROXY_URL_FORMAT = "ws://%s/debugger-proxy?role=client"; private static final String WEBSOCKET_PROXY_URL_FORMAT = "ws://%s/debugger-proxy?role=client";
private static final String PACKAGER_CONNECTION_URL_FORMAT = "ws://%s/message?role=shell";
private static final String PACKAGER_STATUS_URL_FORMAT = "http://%s/status"; private static final String PACKAGER_STATUS_URL_FORMAT = "http://%s/status";
private static final String PACKAGER_OK_STATUS = "packager-status:running"; private static final String PACKAGER_OK_STATUS = "packager-status:running";
@ -76,12 +78,17 @@ public class DevServerHelper {
void onServerContentChanged(); void onServerContentChanged();
} }
public interface PackagerCommandListener {
void onReload();
}
public interface PackagerStatusCallback { public interface PackagerStatusCallback {
void onPackagerStatusFetched(boolean packagerIsRunning); void onPackagerStatusFetched(boolean packagerIsRunning);
} }
private final DevInternalSettings mSettings; private final DevInternalSettings mSettings;
private final OkHttpClient mClient; private final OkHttpClient mClient;
private final JSPackagerWebSocketClient mPackagerConnection;
private final Handler mRestartOnChangePollingHandler; private final Handler mRestartOnChangePollingHandler;
private boolean mOnChangePollingEnabled; private boolean mOnChangePollingEnabled;
@ -89,7 +96,7 @@ public class DevServerHelper {
private @Nullable OnServerContentChangeListener mOnServerContentChangeListener; private @Nullable OnServerContentChangeListener mOnServerContentChangeListener;
private @Nullable Call mDownloadBundleFromURLCall; private @Nullable Call mDownloadBundleFromURLCall;
public DevServerHelper(DevInternalSettings settings) { public DevServerHelper(DevInternalSettings settings, final PackagerCommandListener commandListener) {
mSettings = settings; mSettings = settings;
mClient = new OkHttpClient.Builder() mClient = new OkHttpClient.Builder()
.connectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) .connectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
@ -98,6 +105,16 @@ public class DevServerHelper {
.build(); .build();
mRestartOnChangePollingHandler = new Handler(); mRestartOnChangePollingHandler = new Handler();
mPackagerConnection = new JSPackagerWebSocketClient(getPackagerConnectionURL(),
new JSPackagerWebSocketClient.JSPackagerCallback() {
@Override
public void onMessage(String target, String action) {
if (commandListener != null && "bridge".equals(target) && "reload".equals(action)) {
commandListener.onReload();
}
}
});
mPackagerConnection.connect();
} }
/** Intent action for reloading the JS */ /** Intent action for reloading the JS */
@ -109,6 +126,10 @@ public class DevServerHelper {
return String.format(Locale.US, WEBSOCKET_PROXY_URL_FORMAT, getDebugServerHost()); return String.format(Locale.US, WEBSOCKET_PROXY_URL_FORMAT, getDebugServerHost());
} }
private String getPackagerConnectionURL() {
return String.format(Locale.US, PACKAGER_CONNECTION_URL_FORMAT, getDebugServerHost());
}
/** /**
* @return the host to use when connecting to the bundle server from the host itself. * @return the host to use when connecting to the bundle server from the host itself.
*/ */

View File

@ -135,7 +135,19 @@ public class DevSupportManagerImpl implements DevSupportManager {
mApplicationContext = applicationContext; mApplicationContext = applicationContext;
mJSAppBundleName = packagerPathForJSBundleName; mJSAppBundleName = packagerPathForJSBundleName;
mDevSettings = new DevInternalSettings(applicationContext, this); mDevSettings = new DevInternalSettings(applicationContext, this);
mDevServerHelper = new DevServerHelper(mDevSettings); mDevServerHelper = new DevServerHelper(
mDevSettings,
new DevServerHelper.PackagerCommandListener() {
@Override
public void onReload() {
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
handleReloadJS();
}
});
}
});
// Prepare shake gesture detector (will be started/stopped from #reload) // Prepare shake gesture detector (will be started/stopped from #reload)
mShakeDetector = new ShakeDetector(new ShakeDetector.ShakeListener() { mShakeDetector = new ShakeDetector(new ShakeDetector.ShakeListener() {