Add symbolication support to DevServerHelper
Reviewed By: javache Differential Revision: D4929829 fbshipit-source-id: 6babdb868d27c1b0da0332cc6aee38502f35704f
This commit is contained in:
parent
57b0039ce1
commit
102f990861
|
@ -13,7 +13,10 @@ import javax.annotation.Nullable;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -27,9 +30,11 @@ import android.os.Handler;
|
|||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.network.OkHttpCallUtil;
|
||||
import com.facebook.react.devsupport.interfaces.PackagerStatusCallback;
|
||||
import com.facebook.react.devsupport.interfaces.StackFrame;
|
||||
import com.facebook.react.modules.systeminfo.AndroidInfoHelpers;
|
||||
import com.facebook.react.packagerconnection.FileIoHandler;
|
||||
import com.facebook.react.packagerconnection.JSPackagerClient;
|
||||
|
@ -38,14 +43,17 @@ import com.facebook.react.packagerconnection.NotificationOnlyHandler;
|
|||
import com.facebook.react.packagerconnection.RequestOnlyHandler;
|
||||
import com.facebook.react.packagerconnection.Responder;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.ConnectionPool;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.Buffer;
|
||||
|
@ -79,6 +87,7 @@ public class DevServerHelper {
|
|||
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 SYMBOLICATE_URL_FORMAT = "http://%s/symbolicate";
|
||||
|
||||
private static final String PACKAGER_OK_STATUS = "packager-status:running";
|
||||
|
||||
|
@ -102,6 +111,10 @@ public class DevServerHelper {
|
|||
void onPokeSamplingProfilerCommand(@Nullable final Responder responder);
|
||||
}
|
||||
|
||||
public interface SymbolicationListener {
|
||||
void onSymbolicationComplete(@Nullable Iterable<StackFrame> stackFrames);
|
||||
}
|
||||
|
||||
private final DevInternalSettings mSettings;
|
||||
private final OkHttpClient mClient;
|
||||
private final Handler mRestartOnChangePollingHandler;
|
||||
|
@ -154,7 +167,10 @@ public class DevServerHelper {
|
|||
});
|
||||
handlers.putAll(new FileIoHandler().handlers());
|
||||
|
||||
mPackagerClient = new JSPackagerClient("devserverhelper", mSettings.getPackagerConnectionSettings(), handlers);
|
||||
mPackagerClient = new JSPackagerClient(
|
||||
"devserverhelper",
|
||||
mSettings.getPackagerConnectionSettings(),
|
||||
handlers);
|
||||
mPackagerClient.init();
|
||||
|
||||
return null;
|
||||
|
@ -209,17 +225,72 @@ public class DevServerHelper {
|
|||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
public void symbolicateStackTrace(
|
||||
Iterable<StackFrame> stackFrames,
|
||||
final SymbolicationListener listener) {
|
||||
try {
|
||||
final String symbolicateURL = createSymbolicateURL(
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost());
|
||||
final JSONArray jsonStackFrames = new JSONArray();
|
||||
for (final StackFrame stackFrame : stackFrames) {
|
||||
jsonStackFrames.put(new JSONObject(
|
||||
MapBuilder.of(
|
||||
"file", stackFrame.getFile(),
|
||||
"methodName", stackFrame.getMethod(),
|
||||
"lineNumber", stackFrame.getLine(),
|
||||
"column", stackFrame.getColumn())));
|
||||
}
|
||||
final Request request = new Request.Builder()
|
||||
.url(symbolicateURL)
|
||||
.post(RequestBody.create(
|
||||
MediaType.parse("application/json"),
|
||||
new JSONObject().put("stack", jsonStackFrames).toString()))
|
||||
.build();
|
||||
Call symbolicateCall = Assertions.assertNotNull(mClient.newCall(request));
|
||||
symbolicateCall.enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Got IOException when attempting symbolicate stack trace: " + e.getMessage());
|
||||
listener.onSymbolicationComplete(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, final Response response) throws IOException {
|
||||
try {
|
||||
listener.onSymbolicationComplete(Arrays.asList(
|
||||
StackTraceHelper.convertJsStackTrace(new JSONObject(
|
||||
response.body().string()).getJSONArray("stack"))));
|
||||
} catch (JSONException exception) {
|
||||
listener.onSymbolicationComplete(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (JSONException e) {
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Got JSONException when attempting symbolicate stack trace: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Intent action for reloading the JS */
|
||||
public static String getReloadAppAction(Context context) {
|
||||
return context.getPackageName() + RELOAD_APP_ACTION_SUFFIX;
|
||||
}
|
||||
|
||||
public String getWebsocketProxyURL() {
|
||||
return String.format(Locale.US, WEBSOCKET_PROXY_URL_FORMAT, mSettings.getPackagerConnectionSettings().getDebugServerHost());
|
||||
return String.format(
|
||||
Locale.US,
|
||||
WEBSOCKET_PROXY_URL_FORMAT,
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost());
|
||||
}
|
||||
|
||||
public String getHeapCaptureUploadUrl() {
|
||||
return String.format(Locale.US, HEAP_CAPTURE_UPLOAD_URL_FORMAT, mSettings.getPackagerConnectionSettings().getDebugServerHost());
|
||||
return String.format(
|
||||
Locale.US,
|
||||
HEAP_CAPTURE_UPLOAD_URL_FORMAT,
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost());
|
||||
}
|
||||
|
||||
public String getInspectorDeviceUrl() {
|
||||
|
@ -258,7 +329,12 @@ public class DevServerHelper {
|
|||
return mSettings.isHotModuleReplacementEnabled();
|
||||
}
|
||||
|
||||
private static String createBundleURL(String host, String jsModulePath, boolean devMode, boolean hmr, boolean jsMinify) {
|
||||
private static String createBundleURL(
|
||||
String host,
|
||||
String jsModulePath,
|
||||
boolean devMode,
|
||||
boolean hmr,
|
||||
boolean jsMinify) {
|
||||
return String.format(Locale.US, BUNDLE_URL_FORMAT, host, jsModulePath, devMode, hmr, jsMinify);
|
||||
}
|
||||
|
||||
|
@ -266,6 +342,10 @@ public class DevServerHelper {
|
|||
return String.format(Locale.US, RESOURCE_URL_FORMAT, host, resourcePath);
|
||||
}
|
||||
|
||||
private static String createSymbolicateURL(String host) {
|
||||
return String.format(Locale.US, SYMBOLICATE_URL_FORMAT, host);
|
||||
}
|
||||
|
||||
public String getDevServerBundleURL(final String jsModulePath) {
|
||||
return createBundleURL(
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost(),
|
||||
|
@ -317,10 +397,15 @@ public class DevServerHelper {
|
|||
Matcher match = regex.matcher(contentType);
|
||||
if (match.find()) {
|
||||
String boundary = match.group(1);
|
||||
MultipartStreamReader bodyReader = new MultipartStreamReader(response.body().source(), boundary);
|
||||
MultipartStreamReader bodyReader = new MultipartStreamReader(
|
||||
response.body().source(),
|
||||
boundary);
|
||||
boolean completed = bodyReader.readAllParts(new MultipartStreamReader.ChunkCallback() {
|
||||
@Override
|
||||
public void execute(Map<String, String> headers, Buffer body, boolean finished) throws IOException {
|
||||
public void execute(
|
||||
Map<String, String> headers,
|
||||
Buffer body,
|
||||
boolean finished) throws IOException {
|
||||
// This will get executed for every chunk of the multipart response. The last chunk
|
||||
// (finished = true) will be the JS bundle, the other ones will be progress events
|
||||
// encoded as JSON.
|
||||
|
@ -332,7 +417,8 @@ public class DevServerHelper {
|
|||
}
|
||||
processBundleResult(url, status, body, outputFile, callback);
|
||||
} else {
|
||||
if (!headers.containsKey("Content-Type") || !headers.get("Content-Type").equals("application/json")) {
|
||||
if (!headers.containsKey("Content-Type") ||
|
||||
!headers.get("Content-Type").equals("application/json")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
@ -358,12 +444,21 @@ public class DevServerHelper {
|
|||
});
|
||||
if (!completed) {
|
||||
callback.onFailure(new DebugServerException(
|
||||
"Error while reading multipart response.\n\nResponse code: " + response.code() + "\n\n" +
|
||||
"URL: " + call.request().url().toString() + "\n\n"));
|
||||
"Error while reading multipart response.\n\nResponse code: " +
|
||||
response.code() + "\n\n" + "URL: " + call.request().url().toString() +
|
||||
"\n\n"));
|
||||
}
|
||||
} else {
|
||||
// In case the server doesn't support multipart/mixed responses, fallback to normal download.
|
||||
processBundleResult(url, response.code(), Okio.buffer(response.body().source()), outputFile, callback);
|
||||
/**
|
||||
* In case the server doesn't support multipart/mixed responses,
|
||||
* fallback to normal download.
|
||||
*/
|
||||
processBundleResult(
|
||||
url,
|
||||
response.code(),
|
||||
Okio.buffer(response.body().source()),
|
||||
outputFile,
|
||||
callback);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -383,8 +478,12 @@ public class DevServerHelper {
|
|||
callback.onFailure(debugServerException);
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("The development server returned response error code: ").append(statusCode).append("\n\n")
|
||||
.append("URL: ").append(url).append("\n\n")
|
||||
sb.append("The development server returned response error code: ")
|
||||
.append(statusCode)
|
||||
.append("\n\n")
|
||||
.append("URL: ")
|
||||
.append(url)
|
||||
.append("\n\n")
|
||||
.append("Body:\n")
|
||||
.append(bodyString);
|
||||
callback.onFailure(new DebugServerException(sb.toString()));
|
||||
|
@ -412,7 +511,8 @@ public class DevServerHelper {
|
|||
}
|
||||
|
||||
public void isPackagerRunning(final PackagerStatusCallback callback) {
|
||||
String statusURL = createPackagerStatusURL(mSettings.getPackagerConnectionSettings().getDebugServerHost());
|
||||
String statusURL = createPackagerStatusURL(
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost());
|
||||
Request request = new Request.Builder()
|
||||
.url(statusURL)
|
||||
.build();
|
||||
|
@ -532,11 +632,17 @@ public class DevServerHelper {
|
|||
}
|
||||
|
||||
private String createOnChangeEndpointUrl() {
|
||||
return String.format(Locale.US, ONCHANGE_ENDPOINT_URL_FORMAT, mSettings.getPackagerConnectionSettings().getDebugServerHost());
|
||||
return String.format(
|
||||
Locale.US,
|
||||
ONCHANGE_ENDPOINT_URL_FORMAT,
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost());
|
||||
}
|
||||
|
||||
private String createLaunchJSDevtoolsCommandUrl() {
|
||||
return String.format(Locale.US, LAUNCH_JS_DEVTOOLS_COMMAND_URL_FORMAT, mSettings.getPackagerConnectionSettings().getDebugServerHost());
|
||||
return String.format(
|
||||
Locale.US,
|
||||
LAUNCH_JS_DEVTOOLS_COMMAND_URL_FORMAT,
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost());
|
||||
}
|
||||
|
||||
public void launchJSDevtools() {
|
||||
|
@ -558,18 +664,37 @@ public class DevServerHelper {
|
|||
}
|
||||
|
||||
public String getSourceMapUrl(String mainModuleName) {
|
||||
return String.format(Locale.US, SOURCE_MAP_URL_FORMAT, mSettings.getPackagerConnectionSettings().getDebugServerHost(), mainModuleName, getDevMode(), getHMR(), getJSMinifyMode());
|
||||
return String.format(
|
||||
Locale.US,
|
||||
SOURCE_MAP_URL_FORMAT,
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost(),
|
||||
mainModuleName,
|
||||
getDevMode(),
|
||||
getHMR(),
|
||||
getJSMinifyMode());
|
||||
}
|
||||
|
||||
public String getSourceUrl(String mainModuleName) {
|
||||
return String.format(Locale.US, BUNDLE_URL_FORMAT, mSettings.getPackagerConnectionSettings().getDebugServerHost(), mainModuleName, getDevMode(), getHMR(), getJSMinifyMode());
|
||||
return String.format(
|
||||
Locale.US,
|
||||
BUNDLE_URL_FORMAT,
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost(),
|
||||
mainModuleName,
|
||||
getDevMode(),
|
||||
getHMR(),
|
||||
getJSMinifyMode());
|
||||
}
|
||||
|
||||
public String getJSBundleURLForRemoteDebugging(String mainModuleName) {
|
||||
// The host IP we use when connecting to the JS bundle server from the emulator is not the
|
||||
// same as the one needed to connect to the same server from the JavaScript proxy running on the
|
||||
// host itself.
|
||||
return createBundleURL(getHostForJSProxy(), mainModuleName, getDevMode(), getHMR(), getJSMinifyMode());
|
||||
return createBundleURL(
|
||||
getHostForJSProxy(),
|
||||
mainModuleName,
|
||||
getDevMode(),
|
||||
getHMR(),
|
||||
getJSMinifyMode());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -581,7 +706,9 @@ public class DevServerHelper {
|
|||
public @Nullable File downloadBundleResourceFromUrlSync(
|
||||
final String resourcePath,
|
||||
final File outputFile) {
|
||||
final String resourceURL = createResourceURL(mSettings.getPackagerConnectionSettings().getDebugServerHost(), resourcePath);
|
||||
final String resourceURL = createResourceURL(
|
||||
mSettings.getPackagerConnectionSettings().getDebugServerHost(),
|
||||
resourcePath);
|
||||
final Request request = new Request.Builder()
|
||||
.url(resourceURL)
|
||||
.build();
|
||||
|
|
|
@ -12,11 +12,17 @@ package com.facebook.react.devsupport;
|
|||
import javax.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.devsupport.interfaces.StackFrame;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Helper class converting JS and Java stack traces into arrays of {@link StackFrame} objects.
|
||||
*/
|
||||
|
@ -25,6 +31,9 @@ public class StackTraceHelper {
|
|||
public static final java.lang.String COLUMN_KEY = "column";
|
||||
public static final java.lang.String LINE_NUMBER_KEY = "lineNumber";
|
||||
|
||||
private static final Pattern STACK_FRAME_PATTERN = Pattern.compile(
|
||||
"^(?:(.*?)@)?(.*?)\\:([0-9]+)\\:([0-9]+)$");
|
||||
|
||||
/**
|
||||
* Represents a generic entry in a stack trace, be it originally from JS or Java.
|
||||
*/
|
||||
|
@ -117,6 +126,56 @@ public class StackTraceHelper {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a JavaScript stack trace (see {@code parseErrorStack} JS module) to an array of
|
||||
* {@link StackFrame}s.
|
||||
*/
|
||||
public static StackFrame[] convertJsStackTrace(JSONArray stack) {
|
||||
int size = stack != null ? stack.length() : 0;
|
||||
StackFrame[] result = new StackFrame[size];
|
||||
try {
|
||||
for (int i = 0; i < size; i++) {
|
||||
JSONObject frame = stack.getJSONObject(i);
|
||||
String methodName = frame.getString("methodName");
|
||||
String fileName = frame.getString("file");
|
||||
int lineNumber = -1;
|
||||
if (frame.has(LINE_NUMBER_KEY) && !frame.isNull(LINE_NUMBER_KEY)) {
|
||||
lineNumber = frame.getInt(LINE_NUMBER_KEY);
|
||||
}
|
||||
int columnNumber = -1;
|
||||
if (frame.has(COLUMN_KEY) && !frame.isNull(COLUMN_KEY)) {
|
||||
columnNumber = frame.getInt(COLUMN_KEY);
|
||||
}
|
||||
result[i] = new StackFrameImpl(fileName, methodName, lineNumber, columnNumber);
|
||||
}
|
||||
} catch (JSONException exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a JavaScript stack trace to an array of {@link StackFrame}s.
|
||||
*/
|
||||
public static StackFrame[] convertJsStackTrace(String stack) {
|
||||
String[] stackTrace = stack.split("\n");
|
||||
StackFrame[] result = new StackFrame[stackTrace.length];
|
||||
for (int i = 0; i < stackTrace.length; ++i) {
|
||||
Matcher matcher = STACK_FRAME_PATTERN.matcher(stackTrace[i]);
|
||||
if (!matcher.find()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unexpected stack frame format: " + stackTrace[i]);
|
||||
}
|
||||
|
||||
result[i] = new StackFrameImpl(
|
||||
matcher.group(2),
|
||||
matcher.group(1) == null ? "(unknown)" : matcher.group(1),
|
||||
Integer.parseInt(matcher.group(3)),
|
||||
Integer.parseInt(matcher.group(4)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a {@link Throwable} to an array of {@link StackFrame}s.
|
||||
*/
|
||||
|
|
|
@ -22,6 +22,7 @@ rn_robolectric_test(
|
|||
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/devsupport:devsupport"),
|
||||
react_native_target("java/com/facebook/react/devsupport:interfaces"),
|
||||
react_native_tests_target("java/com/facebook/react/bridge:testhelpers"),
|
||||
],
|
||||
)
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* 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.devsupport;
|
||||
|
||||
import com.facebook.react.devsupport.interfaces.StackFrame;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class StackTraceHelperTest {
|
||||
|
||||
@Test
|
||||
public void testParseStackFrameWithMethod() {
|
||||
final StackFrame frame = StackTraceHelper.convertJsStackTrace(
|
||||
"render@Test.bundle:1:2000")[0];
|
||||
assertThat(frame.getMethod()).isEqualTo("render");
|
||||
assertThat(frame.getFileName()).isEqualTo("Test.bundle");
|
||||
assertThat(frame.getLine()).isEqualTo(1);
|
||||
assertThat(frame.getColumn()).isEqualTo(2000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseStackFrameWithoutMethod() {
|
||||
final StackFrame frame = StackTraceHelper.convertJsStackTrace(
|
||||
"Test.bundle:1:2000")[0];
|
||||
assertThat(frame.getMethod()).isEqualTo("(unknown)");
|
||||
assertThat(frame.getFileName()).isEqualTo("Test.bundle");
|
||||
assertThat(frame.getLine()).isEqualTo(1);
|
||||
assertThat(frame.getColumn()).isEqualTo(2000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseStackFrameWithInvalidFrame() {
|
||||
try {
|
||||
StackTraceHelper.convertJsStackTrace("Test.bundle:ten:twenty");
|
||||
failBecauseExceptionWasNotThrown(IllegalArgumentException.class);
|
||||
} catch (Exception e) {
|
||||
assertThat(e).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue