/* * 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.facebook.stetho.inspector; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import android.util.Log; import com.facebook.stetho.common.LogRedirector; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.jsonrpc.JsonRpcException; import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer; import com.facebook.stetho.inspector.jsonrpc.PendingRequest; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcError; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcRequest; import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcResponse; import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain; import com.facebook.stetho.json.ObjectMapper; import com.facebook.stetho.websocket.CloseCodes; import com.facebook.stetho.websocket.SimpleEndpoint; import com.facebook.stetho.websocket.SimpleSession; import org.json.JSONException; import org.json.JSONObject; /** * Implements a limited version of the Chrome Debugger WebSocket protocol (using JSON-RPC 2.0). * The most up-to-date documentation can be found in the Blink source code: * <a href="https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/Source/devtools/protocol.json&q=protocol.json&sq=package:chromium&type=cs">protocol.json</a> */ public class ChromeDevtoolsServer implements SimpleEndpoint { private static final String TAG = "ChromeDevtoolsServer"; public static final String PATH = "/inspector"; private final ObjectMapper mObjectMapper; private final MethodDispatcher mMethodDispatcher; private final Map<SimpleSession, JsonRpcPeer> mPeers = Collections.synchronizedMap( new HashMap<SimpleSession, JsonRpcPeer>()); public ChromeDevtoolsServer(Iterable<ChromeDevtoolsDomain> domainModules) { mObjectMapper = new ObjectMapper(); mMethodDispatcher = new MethodDispatcher(mObjectMapper, domainModules); } @Override public void onOpen(SimpleSession session) { LogRedirector.d(TAG, "onOpen"); mPeers.put(session, new JsonRpcPeer(mObjectMapper, session)); } @Override public void onClose(SimpleSession session, int code, String reasonPhrase) { LogRedirector.d(TAG, "onClose: reason=" + code + " " + reasonPhrase); JsonRpcPeer peer = mPeers.remove(session); if (peer != null) { peer.invokeDisconnectReceivers(); } } @Override public void onMessage(SimpleSession session, byte[] message, int messageLen) { LogRedirector.d(TAG, "Ignoring binary message of length " + messageLen); } @Override public void onMessage(SimpleSession session, String message) { if (LogRedirector.isLoggable(TAG, Log.VERBOSE)) { LogRedirector.v(TAG, "onMessage: message=" + message); } try { JsonRpcPeer peer = mPeers.get(session); Util.throwIfNull(peer); handleRemoteMessage(peer, message); } catch (IOException e) { if (LogRedirector.isLoggable(TAG, Log.VERBOSE)) { LogRedirector.v(TAG, "Unexpected I/O exception processing message: " + e); } closeSafely(session, CloseCodes.UNEXPECTED_CONDITION, e.getClass().getSimpleName()); } catch (MessageHandlingException e) { LogRedirector.i(TAG, "Message could not be processed by implementation: " + e); closeSafely(session, CloseCodes.UNEXPECTED_CONDITION, e.getClass().getSimpleName()); } catch (JSONException e) { LogRedirector.v(TAG, "Unexpected JSON exception processing message", e); closeSafely(session, CloseCodes.UNEXPECTED_CONDITION, e.getClass().getSimpleName()); } } private void closeSafely(SimpleSession session, int code, String reasonPhrase) { session.close(code, reasonPhrase); } private void handleRemoteMessage(JsonRpcPeer peer, String message) throws IOException, MessageHandlingException, JSONException { // Parse as a generic JSONObject first since we don't know if this is a request or response. JSONObject messageNode = new JSONObject(message); if (messageNode.has("method")) { handleRemoteRequest(peer, messageNode); } else if (messageNode.has("result")) { handleRemoteResponse(peer, messageNode); } else { throw new MessageHandlingException("Improper JSON-RPC message: " + message); } } private void handleRemoteRequest(JsonRpcPeer peer, JSONObject requestNode) throws MessageHandlingException { JsonRpcRequest request; request = mObjectMapper.convertValue( requestNode, JsonRpcRequest.class); JSONObject result = null; JSONObject error = null; try { result = mMethodDispatcher.dispatch(peer, request.method, request.params); } catch (JsonRpcException e) { logDispatchException(e); error = mObjectMapper.convertValue(e.getErrorMessage(), JSONObject.class); } if (request.id != null) { JsonRpcResponse response = new JsonRpcResponse(); response.id = request.id; response.result = result; response.error = error; JSONObject jsonObject = mObjectMapper.convertValue(response, JSONObject.class); String responseString; try { responseString = jsonObject.toString(); } catch (OutOfMemoryError e) { // JSONStringer can cause an OOM when the Json to handle is too big. response.result = null; response.error = mObjectMapper.convertValue(e.getMessage(), JSONObject.class); jsonObject = mObjectMapper.convertValue(response, JSONObject.class); responseString = jsonObject.toString(); } peer.getWebSocket().sendText(responseString); } } private static void logDispatchException(JsonRpcException e) { JsonRpcError errorMessage = e.getErrorMessage(); switch (errorMessage.code) { case METHOD_NOT_FOUND: LogRedirector.d(TAG, "Method not implemented: " + errorMessage.message); break; default: LogRedirector.w(TAG, "Error processing remote message", e); } } private void handleRemoteResponse(JsonRpcPeer peer, JSONObject responseNode) throws MismatchedResponseException { JsonRpcResponse response = mObjectMapper.convertValue( responseNode, JsonRpcResponse.class); PendingRequest pendingRequest = peer.getAndRemovePendingRequest(response.id); if (pendingRequest == null) { throw new MismatchedResponseException(response.id); } if (pendingRequest.callback != null) { pendingRequest.callback.onResponse(peer, response); } } @Override public void onError(SimpleSession session, Throwable ex) { LogRedirector.e(TAG, "onError: ex=" + ex.toString()); } }