Make Android connect to the inspector proxy
Summary: Maintains a single persistent connection to the packager for the inspector. It supports getting the available pages and connecting to them. Reviewed By: foghina Differential Revision: D4088690 fbshipit-source-id: 0c445225f5a3de573b199e7868c8693b78f45729
This commit is contained in:
Родитель
655fe2796a
Коммит
18184a83f1
|
@ -3,10 +3,13 @@
|
|||
package com.facebook.react.bridge;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.jni.HybridData;
|
||||
import com.facebook.proguard.annotations.DoNotStrip;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
|
||||
@DoNotStrip
|
||||
public class Inspector {
|
||||
|
@ -17,11 +20,21 @@ public class Inspector {
|
|||
private final HybridData mHybridData;
|
||||
|
||||
public static List<Page> getPages() {
|
||||
return Arrays.asList(instance().getPagesNative());
|
||||
try {
|
||||
return Arrays.asList(instance().getPagesNative());
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
FLog.e(ReactConstants.TAG, "Inspector doesn't work in open source yet", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public static LocalConnection connect(int pageId, RemoteConnection remote) {
|
||||
return instance().connectNative(pageId, remote);
|
||||
try {
|
||||
return instance().connectNative(pageId, remote);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
FLog.e(ReactConstants.TAG, "Inspector doesn't work in open source yet", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static native Inspector instance();
|
||||
|
|
|
@ -64,6 +64,7 @@ public class DevServerHelper {
|
|||
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 HEAP_CAPTURE_UPLOAD_URL_FORMAT = "http://%s/jscheapcaptureupload";
|
||||
private static final String INSPECTOR_DEVICE_URL_FORMAT = "http://%s/inspector/device?name=%s";
|
||||
|
||||
private static final String PACKAGER_OK_STATUS = "packager-status:running";
|
||||
|
||||
|
@ -94,6 +95,7 @@ public class DevServerHelper {
|
|||
|
||||
private boolean mOnChangePollingEnabled;
|
||||
private @Nullable JSPackagerWebSocketClient mPackagerConnection;
|
||||
private @Nullable InspectorPackagerConnection mInspectorPackagerConnection;
|
||||
private @Nullable OkHttpClient mOnChangePollingClient;
|
||||
private @Nullable OnServerContentChangeListener mOnServerContentChangeListener;
|
||||
private @Nullable Call mDownloadBundleFromURLCall;
|
||||
|
@ -145,7 +147,35 @@ public class DevServerHelper {
|
|||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
/** Intent action for reloading the JS */
|
||||
public void openInspectorConnection() {
|
||||
if (mInspectorPackagerConnection != null) {
|
||||
FLog.w(ReactConstants.TAG, "Inspector connection already open, nooping.");
|
||||
return;
|
||||
}
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
mInspectorPackagerConnection = new InspectorPackagerConnection(getInspectorDeviceUrl());
|
||||
mInspectorPackagerConnection.connect();
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
public void closeInspectorConnection() {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
if (mInspectorPackagerConnection != null) {
|
||||
mInspectorPackagerConnection.closeQuietly();
|
||||
mInspectorPackagerConnection = null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
/** Intent action for reloading the JS */
|
||||
public static String getReloadAppAction(Context context) {
|
||||
return context.getPackageName() + RELOAD_APP_ACTION_SUFFIX;
|
||||
}
|
||||
|
@ -162,6 +192,14 @@ public class DevServerHelper {
|
|||
return String.format(Locale.US, HEAP_CAPTURE_UPLOAD_URL_FORMAT, getDebugServerHost());
|
||||
}
|
||||
|
||||
public String getInspectorDeviceUrl() {
|
||||
return String.format(
|
||||
Locale.US,
|
||||
INSPECTOR_DEVICE_URL_FORMAT,
|
||||
getDebugServerHost(),
|
||||
AndroidInfoHelpers.getFriendlyDeviceName());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the host to use when connecting to the bundle server from the host itself.
|
||||
*/
|
||||
|
|
|
@ -821,6 +821,7 @@ public class DevSupportManagerImpl implements DevSupportManager, PackagerCommand
|
|||
}
|
||||
|
||||
mDevServerHelper.openPackagerConnection(this);
|
||||
mDevServerHelper.openInspectorConnection();
|
||||
if (mDevSettings.isReloadOnJSChangeEnabled()) {
|
||||
mDevServerHelper.startPollingOnChangeEndpoint(
|
||||
new DevServerHelper.OnServerContentChangeListener() {
|
||||
|
@ -861,6 +862,7 @@ public class DevSupportManagerImpl implements DevSupportManager, PackagerCommand
|
|||
}
|
||||
|
||||
mDevServerHelper.closePackagerConnection();
|
||||
mDevServerHelper.closeInspectorConnection();
|
||||
mDevServerHelper.stopPollingOnChangeEndpoint();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,295 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.react.devsupport;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.react.bridge.Inspector;
|
||||
|
||||
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;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class InspectorPackagerConnection {
|
||||
private static final String TAG = "InspectorPackagerConnection";
|
||||
|
||||
private final Connection mConnection;
|
||||
private final Map<String, Inspector.LocalConnection> mInspectorConnections;
|
||||
|
||||
public InspectorPackagerConnection(String url) {
|
||||
mConnection = new Connection(url);
|
||||
mInspectorConnections = new HashMap<>();
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
mConnection.connect();
|
||||
}
|
||||
|
||||
public void closeQuietly() {
|
||||
mConnection.close();
|
||||
}
|
||||
|
||||
void handleProxyMessage(JSONObject message)
|
||||
throws JSONException, IOException {
|
||||
String event = message.getString("event");
|
||||
switch (event) {
|
||||
case "getPages":
|
||||
sendEvent("getPages", getPages());
|
||||
break;
|
||||
case "wrappedEvent":
|
||||
handleWrappedEvent(message.getJSONObject("payload"));
|
||||
break;
|
||||
case "connect":
|
||||
handleConnect(message.getJSONObject("payload"));
|
||||
break;
|
||||
case "disconnect":
|
||||
handleDisconnect(message.getJSONObject("payload"));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown event: " + event);
|
||||
}
|
||||
}
|
||||
|
||||
void closeAllConnections() {
|
||||
for (Map.Entry<String, Inspector.LocalConnection> entry : mInspectorConnections.entrySet()) {
|
||||
entry.getValue().disconnect();
|
||||
}
|
||||
mInspectorConnections.clear();
|
||||
}
|
||||
|
||||
private void handleConnect(JSONObject payload) throws JSONException, IOException {
|
||||
final String pageId = payload.getString("pageId");
|
||||
Inspector.LocalConnection inspectorConnection = mInspectorConnections.remove(pageId);
|
||||
if (inspectorConnection != null) {
|
||||
throw new IllegalStateException("Already connected: " + pageId);
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Use strings for id's too
|
||||
inspectorConnection = Inspector.connect(Integer.parseInt(pageId), new Inspector.RemoteConnection() {
|
||||
@Override
|
||||
public void onMessage(String message) {
|
||||
try {
|
||||
sendWrappedEvent(pageId, message);
|
||||
} catch (IOException | JSONException e) {
|
||||
FLog.w(TAG, "Couldn't send event to packager", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect() {
|
||||
try {
|
||||
mInspectorConnections.remove(pageId);
|
||||
sendEvent("disconnect", makePageIdPayload(pageId));
|
||||
} catch (IOException | JSONException e) {
|
||||
FLog.w(TAG, "Couldn't send event to packager", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
mInspectorConnections.put(pageId, inspectorConnection);
|
||||
} catch (Exception e) {
|
||||
FLog.w(TAG, "Failed to open page: " + pageId, e);
|
||||
sendEvent("disconnect", makePageIdPayload(pageId));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDisconnect(JSONObject payload) throws JSONException {
|
||||
final String pageId = payload.getString("pageId");
|
||||
Inspector.LocalConnection inspectorConnection = mInspectorConnections.remove(pageId);
|
||||
if (inspectorConnection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
inspectorConnection.disconnect();
|
||||
}
|
||||
|
||||
private void handleWrappedEvent(JSONObject payload) throws JSONException, IOException {
|
||||
final String pageId = payload.getString("pageId");
|
||||
String wrappedEvent = payload.getString("wrappedEvent");
|
||||
Inspector.LocalConnection inspectorConnection = mInspectorConnections.get(pageId);
|
||||
if (inspectorConnection == null) {
|
||||
throw new IllegalStateException("Not connected: " + pageId);
|
||||
}
|
||||
inspectorConnection.sendMessage(wrappedEvent);
|
||||
}
|
||||
|
||||
private JSONArray getPages() throws JSONException {
|
||||
List<Inspector.Page> pages = Inspector.getPages();
|
||||
JSONArray array = new JSONArray();
|
||||
for (Inspector.Page page : pages) {
|
||||
JSONObject jsonPage = new JSONObject();
|
||||
jsonPage.put("id", String.valueOf(page.getId()));
|
||||
jsonPage.put("title", page.getTitle());
|
||||
array.put(jsonPage);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
private void sendWrappedEvent(String pageId, String message) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("pageId", pageId);
|
||||
payload.put("wrappedEvent", message);
|
||||
sendEvent("wrappedEvent", payload);
|
||||
}
|
||||
|
||||
private void sendEvent(String name, Object payload)
|
||||
throws JSONException, IOException {
|
||||
JSONObject jsonMessage = new JSONObject();
|
||||
jsonMessage.put("event", name);
|
||||
jsonMessage.put("payload", payload);
|
||||
mConnection.send(jsonMessage);
|
||||
}
|
||||
|
||||
private JSONObject makePageIdPayload(String pageId) throws JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("pageId", pageId);
|
||||
return payload;
|
||||
}
|
||||
|
||||
private class Connection implements WebSocketListener {
|
||||
private static final int RECONNECT_DELAY_MS = 2000;
|
||||
|
||||
private final String mUrl;
|
||||
|
||||
private @Nullable WebSocket mWebSocket;
|
||||
private final Handler mHandler;
|
||||
private boolean mClosed;
|
||||
private boolean mSuppressConnectionErrors;
|
||||
|
||||
public Connection(String url) {
|
||||
mUrl = url;
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket, Response response) {
|
||||
mWebSocket = webSocket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(IOException e, Response response) {
|
||||
if (mWebSocket != null) {
|
||||
abort("Websocket exception", e);
|
||||
}
|
||||
if (!mClosed) {
|
||||
reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(ResponseBody message) throws IOException {
|
||||
try {
|
||||
handleProxyMessage(new JSONObject(message.string()));
|
||||
} catch (JSONException e) {
|
||||
throw new IOException(e);
|
||||
} finally {
|
||||
message.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPong(Buffer payload) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(int code, String reason) {
|
||||
mWebSocket = null;
|
||||
closeAllConnections();
|
||||
if (!mClosed) {
|
||||
reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
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();
|
||||
|
||||
Request request = new Request.Builder().url(mUrl).build();
|
||||
WebSocketCall call = WebSocketCall.create(httpClient, request);
|
||||
call.enqueue(this);
|
||||
}
|
||||
|
||||
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");
|
||||
mSuppressConnectionErrors = true;
|
||||
}
|
||||
mHandler.postDelayed(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// check that we haven't been closed in the meantime
|
||||
if (!mClosed) {
|
||||
connect();
|
||||
}
|
||||
}
|
||||
},
|
||||
RECONNECT_DELAY_MS);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
mClosed = true;
|
||||
if (mWebSocket != null) {
|
||||
try {
|
||||
mWebSocket.close(1000, "End of session");
|
||||
} catch (IOException e) {
|
||||
// swallow, no need to handle it here
|
||||
}
|
||||
mWebSocket = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void send(JSONObject object) throws IOException {
|
||||
if (mWebSocket == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mWebSocket.sendMessage(RequestBody.create(WebSocket.TEXT, object.toString()));
|
||||
}
|
||||
|
||||
private void abort(String message, Throwable cause) {
|
||||
FLog.e(TAG, "Error occurred, shutting down websocket connection: " + message, cause);
|
||||
closeAllConnections();
|
||||
closeWebSocketQuietly();
|
||||
}
|
||||
|
||||
private void closeWebSocketQuietly() {
|
||||
if (mWebSocket != null) {
|
||||
try {
|
||||
mWebSocket.close(1000, "End of session");
|
||||
} catch (IOException e) {
|
||||
// swallow, no need to handle it here
|
||||
}
|
||||
mWebSocket = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.react.modules.systeminfo;
|
||||
|
||||
import android.os.Build;
|
||||
|
@ -30,4 +32,13 @@ public class AndroidInfoHelpers {
|
|||
|
||||
return DEVICE_LOCALHOST;
|
||||
}
|
||||
|
||||
public static String getFriendlyDeviceName() {
|
||||
if (isRunningOnGenymotion()) {
|
||||
// Genymotion already has a friendly name by default
|
||||
return Build.MODEL;
|
||||
} else {
|
||||
return Build.MODEL + " - " + Build.VERSION.RELEASE + " - API " + Build.VERSION.SDK_INT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче