Persistent websocket connection from Android to Packager
Reviewed By: astreet Differential Revision: D3447685 fbshipit-source-id: 0e4e3fb02b84b9b15c2c798c0e4c89ff6fd1665c
This commit is contained in:
parent
b3886652ab
commit
adcb9491bd
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'),
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in New Issue