inspector/src/main/java/com/taobao/weex/devtools/inspector/network/NetworkEventReporterImpl.java
/*
* Copyright (c) 2014-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.taobao.weex.devtools.inspector.network;
import android.os.SystemClock;
import android.util.Log;
import com.taobao.weex.devtools.common.Utf8Charset;
import com.taobao.weex.devtools.inspector.console.CLog;
import com.taobao.weex.devtools.inspector.protocol.module.Console;
import com.taobao.weex.devtools.inspector.protocol.module.Network;
import com.taobao.weex.devtools.inspector.protocol.module.Page;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Implementation of {@link NetworkEventReporter} which allows callers to inform the WeexInspector
* system of network traffic. Callers can safely eagerly access this class and store a
* reference if they wish. When WebKit Inspector clients are connected, the internal
* implementation will be automatically wired up to them.
*/
public class NetworkEventReporterImpl implements NetworkEventReporter {
private final AtomicInteger mNextRequestId = new AtomicInteger(0);
@Nullable
private ResourceTypeHelper mResourceTypeHelper;
private static NetworkEventReporter sInstance;
private static boolean enabled = false;
private NetworkEventReporterImpl() {
}
public static void setEnabled(boolean isEnabled) {
enabled = isEnabled;
}
/**
* Static accessor allowing callers to easily hook into the WebKit Inspector system without
* creating dependencies on the main WeexInspector initialization code path.
*/
public static synchronized NetworkEventReporter get() {
if (sInstance == null) {
sInstance = new NetworkEventReporterImpl();
}
return sInstance;
}
@Override
public boolean isEnabled() {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
return enabled && (peerManager != null);
}
@Nullable
private NetworkPeerManager getPeerManagerIfEnabled() {
NetworkPeerManager peerManager = NetworkPeerManager.getInstanceOrNull();
if (peerManager != null && peerManager.hasRegisteredPeers()) {
return peerManager;
}
return null;
}
@Override
public void requestWillBeSent(InspectorRequest request) {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
if (peerManager != null) {
Network.Request requestJSON = new Network.Request();
requestJSON.url = request.url();
requestJSON.method = request.method();
requestJSON.headers = formatHeadersAsJSON(request);
requestJSON.postData = readBodyAsString(peerManager, request);
// Hack to use the initiator of SCRIPT to generate a fake call stack that includes
// the request's "friendly" name.
String requestFriendlyName = request.friendlyName();
Integer requestPriority = request.friendlyNameExtra();
Network.Initiator initiatorJSON = new Network.Initiator();
initiatorJSON.type = "parser";
initiatorJSON.url = requestFriendlyName;
initiatorJSON.stack = new ArrayList<Console.CallFrame>();
initiatorJSON.stack.add(new Console.CallFrame(requestFriendlyName,
requestFriendlyName,
requestPriority != null ? requestPriority : 0 /* lineNumber */,
0 /* columnNumber */));
Network.RequestWillBeSentParams params = new Network.RequestWillBeSentParams();
params.requestId = request.id();
params.frameId = "1";
params.loaderId = "1";
params.documentURL = request.url();
Log.v("requestWillBeSent", "params.documentURL " + params.documentURL);
params.request = requestJSON;
params.timestamp = stethoNow() / 1000.0;
params.initiator = initiatorJSON;
params.redirectResponse = null;
// Type is now required as of at least WebKit Inspector rev @188492. If you don't send
// it, Chrome will refuse to draw the row in the Network tab until the response is
// received (providing the type). This delay is very noticable on slow networks.
params.type = Page.ResourceType.OTHER;
peerManager.sendNotificationToPeers("Network.requestWillBeSent", params);
}
}
@Nullable
private static String readBodyAsString(
NetworkPeerManager peerManager,
InspectorRequest request) {
try {
byte[] body = request.body();
if (body != null) {
return new String(body, Utf8Charset.INSTANCE);
}
} catch (IOException | OutOfMemoryError e) {
CLog.writeToConsole(
peerManager,
Console.MessageLevel.WARNING,
Console.MessageSource.NETWORK,
"Could not reproduce POST body: " + e);
}
return null;
}
@Override
public void responseHeadersReceived(InspectorResponse response) {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
if (peerManager != null) {
Network.Response responseJSON = new Network.Response();
responseJSON.url = response.url();
responseJSON.status = response.statusCode();
responseJSON.statusText = response.reasonPhrase();
responseJSON.headers = formatHeadersAsJSON(response);
String contentType = getContentType(response);
responseJSON.mimeType = contentType != null ?
getResourceTypeHelper().stripContentExtras(contentType) :
"application/octet-stream";
responseJSON.connectionReused = response.connectionReused();
responseJSON.connectionId = response.connectionId();
responseJSON.fromDiskCache = response.fromDiskCache();
try {
if (NetworkEventReporterManager.getParam("reportTiming", true)) {
if (response instanceof TimingInspectorResponse) {
Timing timing = ((TimingInspectorResponse) response).resourceTiming();
responseJSON.timing = createTimingFrom(timing);
}
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Network.ResponseReceivedParams receivedParams = new Network.ResponseReceivedParams();
receivedParams.requestId = response.requestId();
receivedParams.frameId = "1";
receivedParams.loaderId = "1";
receivedParams.timestamp = stethoNow() / 1000.0;
receivedParams.response = responseJSON;
AsyncPrettyPrinter asyncPrettyPrinter =
initAsyncPrettyPrinterForResponse(response, peerManager);
receivedParams.type =
determineResourceType(asyncPrettyPrinter, contentType, getResourceTypeHelper());
peerManager.sendNotificationToPeers("Network.responseReceived", receivedParams);
}
}
@Nullable
private static AsyncPrettyPrinter initAsyncPrettyPrinterForResponse(
InspectorResponse response,
NetworkPeerManager peerManager) {
AsyncPrettyPrinterRegistry registry = peerManager.getAsyncPrettyPrinterRegistry();
AsyncPrettyPrinter asyncPrettyPrinter = createPrettyPrinterForResponse(response, registry);
if (asyncPrettyPrinter != null) {
peerManager.getResponseBodyFileManager().associateAsyncPrettyPrinterWithId(
response.requestId(),
asyncPrettyPrinter);
}
return asyncPrettyPrinter;
}
private static Page.ResourceType determineResourceType(
AsyncPrettyPrinter asyncPrettyPrinter,
String contentType,
ResourceTypeHelper resourceTypeHelper) {
Log.v("determineResourceType", "determineResourceType : " + contentType);
if (asyncPrettyPrinter != null) {
return asyncPrettyPrinter.getPrettifiedType().getResourceType();
} else {
return contentType != null ?
resourceTypeHelper.determineResourceType(contentType) :
Page.ResourceType.OTHER;
}
}
//@VisibleForTesting
@Nullable
static AsyncPrettyPrinter createPrettyPrinterForResponse(
InspectorResponse response,
@Nullable AsyncPrettyPrinterRegistry registry) {
if (registry != null) {
for (int i = 0, count = response.headerCount(); i < count; i++) {
AsyncPrettyPrinterFactory factory = registry.lookup(response.headerName(i));
if (factory != null) {
AsyncPrettyPrinter asyncPrettyPrinter = factory.getInstance(
response.headerName(i),
response.headerValue(i));
return asyncPrettyPrinter;
}
}
}
return null;
}
@Override
public InputStream interpretResponseStream(
String requestId,
@Nullable String contentType,
@Nullable String contentEncoding,
@Nullable InputStream availableInputStream,
ResponseHandler responseHandler) {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
if (peerManager != null) {
if (availableInputStream == null) {
responseHandler.onEOF();
return null;
}
Page.ResourceType resourceType =
contentType != null ?
getResourceTypeHelper().determineResourceType(contentType) :
null;
// There's this weird logic at play that only knows how to base64 decode certain kinds of
// resources.
boolean base64Encode = false;
if (resourceType != null && resourceType == Page.ResourceType.IMAGE) {
base64Encode = true;
}
try {
OutputStream fileOutputStream =
peerManager.getResponseBodyFileManager().openResponseBodyFile(
requestId,
base64Encode);
return DecompressionHelper.teeInputWithDecompression(
peerManager,
requestId,
availableInputStream,
fileOutputStream,
contentEncoding,
responseHandler);
} catch (IOException e) {
CLog.writeToConsole(
peerManager,
Console.MessageLevel.ERROR,
Console.MessageSource.NETWORK,
"Error writing response body data for request #" + requestId);
}
}
return availableInputStream;
}
@Override
public void httpExchangeFailed(String requestId, String errorText) {
loadingFailed(requestId, errorText);
}
@Override
public void responseReadFinished(String requestId) {
loadingFinished(requestId);
}
private void loadingFinished(String requestId) {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
if (peerManager != null) {
Network.LoadingFinishedParams finishedParams = new Network.LoadingFinishedParams();
finishedParams.requestId = requestId;
finishedParams.timestamp = stethoNow() / 1000.0;
peerManager.sendNotificationToPeers("Network.loadingFinished", finishedParams);
}
}
@Override
public void responseReadFailed(String requestId, String errorText) {
loadingFailed(requestId, errorText);
}
private void loadingFailed(String requestId, String errorText) {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
if (peerManager != null) {
Network.LoadingFailedParams failedParams = new Network.LoadingFailedParams();
failedParams.requestId = requestId;
failedParams.timestamp = stethoNow() / 1000.0;
failedParams.errorText = errorText;
failedParams.type = Page.ResourceType.OTHER;
peerManager.sendNotificationToPeers("Network.loadingFailed", failedParams);
}
}
@Override
public void dataSent(
String requestId,
int dataLength,
int encodedDataLength) {
// The inspector protocol only gives us the dataReceived event, but we can happily combine
// upstream and downstream data into this to visualize the real size of the request, not
// strictly the size of the "content" as reported in the UI.
dataReceived(requestId, dataLength, encodedDataLength);
}
@Override
public void dataReceived(
String requestId,
int dataLength,
int encodedDataLength) {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
if (peerManager != null) {
Network.DataReceivedParams dataReceivedParams = new Network.DataReceivedParams();
dataReceivedParams.requestId = requestId;
dataReceivedParams.timestamp = stethoNow() / 1000.0;
dataReceivedParams.dataLength = dataLength;
dataReceivedParams.encodedDataLength = encodedDataLength;
peerManager.sendNotificationToPeers("Network.dataReceived", dataReceivedParams);
}
}
@Override
public String nextRequestId() {
return String.valueOf(mNextRequestId.getAndIncrement());
}
@Nullable
private String getContentType(InspectorHeaders headers) {
// This may need to change in the future depending on how cumbersome header simulation
// is for the various hooks we expose.
return headers.firstHeaderValue("Content-Type");
}
@Override
public void webSocketCreated(String requestId, String url) {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
if (peerManager != null) {
Network.WebSocketCreatedParams params = new Network.WebSocketCreatedParams();
params.requestId = requestId;
params.url = url;
peerManager.sendNotificationToPeers("Network.webSocketCreated", params);
}
}
@Override
public void webSocketClosed(String requestId) {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
if (peerManager != null) {
Network.WebSocketClosedParams params = new Network.WebSocketClosedParams();
params.requestId = requestId;
params.timestamp = stethoNow() / 1000.0;
peerManager.sendNotificationToPeers("Network.webSocketClosed", params);
}
}
@Override
public void webSocketWillSendHandshakeRequest(InspectorWebSocketRequest request) {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
if (peerManager != null) {
Network.WebSocketWillSendHandshakeRequestParams params =
new Network.WebSocketWillSendHandshakeRequestParams();
params.requestId = request.id();
params.timestamp = stethoNow() / 1000.0;
params.wallTime = System.currentTimeMillis() / 1000.0;
Network.WebSocketRequest requestJSON = new Network.WebSocketRequest();
requestJSON.headers = formatHeadersAsJSON(request);
params.request = requestJSON;
peerManager.sendNotificationToPeers("Network.webSocketWillSendHandshakeRequest", params);
}
}
@Override
public void webSocketHandshakeResponseReceived(InspectorWebSocketResponse response) {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
if (peerManager != null) {
Network.WebSocketHandshakeResponseReceivedParams params =
new Network.WebSocketHandshakeResponseReceivedParams();
params.requestId = response.requestId();
params.timestamp = stethoNow() / 1000.0;
Network.WebSocketResponse responseJSON = new Network.WebSocketResponse();
responseJSON.headers = formatHeadersAsJSON(response);
responseJSON.headersText = null;
if (response.requestHeaders() != null) {
responseJSON.requestHeaders = formatHeadersAsJSON(response.requestHeaders());
responseJSON.requestHeadersText = null;
}
responseJSON.status = response.statusCode();
responseJSON.statusText = response.reasonPhrase();
params.response = responseJSON;
peerManager.sendNotificationToPeers("Network.webSocketHandshakeResponseReceived", params);
}
}
@Override
public void webSocketFrameSent(InspectorWebSocketFrame frame) {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
if (peerManager != null) {
Network.WebSocketFrameSentParams params = new Network.WebSocketFrameSentParams();
params.requestId = frame.requestId();
params.timestamp = stethoNow() / 1000.0;
params.response = convertFrame(frame);
peerManager.sendNotificationToPeers("Network.webSocketFrameSent", params);
}
}
@Override
public void webSocketFrameReceived(InspectorWebSocketFrame frame) {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
if (peerManager != null) {
Network.WebSocketFrameReceivedParams params = new Network.WebSocketFrameReceivedParams();
params.requestId = frame.requestId();
params.timestamp = stethoNow() / 1000.0;
params.response = convertFrame(frame);
peerManager.sendNotificationToPeers("Network.webSocketFrameReceived", params);
}
}
private static Network.WebSocketFrame convertFrame(InspectorWebSocketFrame in) {
Network.WebSocketFrame out = new Network.WebSocketFrame();
out.opcode = in.opcode();
out.mask = in.mask();
out.payloadData = in.payloadData();
return out;
}
@Override
public void webSocketFrameError(String requestId, String errorMessage) {
NetworkPeerManager peerManager = getPeerManagerIfEnabled();
if (peerManager != null) {
Network.WebSocketFrameErrorParams params = new Network.WebSocketFrameErrorParams();
params.requestId = requestId;
params.timestamp = stethoNow() / 1000.0;
params.errorMessage = errorMessage;
peerManager.sendNotificationToPeers("Network.webSocketFrameError", params);
}
}
private static JSONObject formatHeadersAsJSON(InspectorHeaders headers) {
JSONObject json = new JSONObject();
for (int i = 0; i < headers.headerCount(); i++) {
String name = headers.headerName(i);
String value = headers.headerValue(i);
try {
if (json.has(name)) {
// Multiple headers are separated with a new line.
json.put(name, json.getString(name) + "\n" + value);
} else {
json.put(name, value);
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
return json;
}
@Nonnull
private ResourceTypeHelper getResourceTypeHelper() {
if (mResourceTypeHelper == null) {
mResourceTypeHelper = new ResourceTypeHelper();
}
return mResourceTypeHelper;
}
private static long stethoNow() {
return SystemClock.elapsedRealtime();
}
private Network.ResourceTiming createTimingFrom(Timing timing) {
Network.ResourceTiming resourceTiming = new Network.ResourceTiming();
resourceTiming.connectEnd = timing.connectEnd;
resourceTiming.connectStart = timing.connectStart;
resourceTiming.dnsEnd = timing.dnsEnd;
resourceTiming.dnsStart = timing.dnsStart;
resourceTiming.proxyEnd = timing.proxyEnd;
resourceTiming.proxyStart = timing.proxyStart;
resourceTiming.sslEnd = timing.sslEnd;
resourceTiming.sslStart = timing.sslStart;
resourceTiming.sendEnd = timing.sendEnd;
resourceTiming.sendStart = timing.sendStart;
resourceTiming.requestTime = timing.requestTime;
resourceTiming.receiveHeadersEnd = timing.receiveHeadersEnd;
return resourceTiming;
}
}