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:
Alexander Blom 2016-11-15 08:55:38 -08:00 коммит произвёл Facebook Github Bot
Родитель 655fe2796a
Коммит 18184a83f1
5 изменённых файлов: 362 добавлений и 3 удалений

Просмотреть файл

@ -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;
}
}
}