/* * Copyright (c) 2012, the Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.dart.tools.debug.core.webkit; import com.google.dart.tools.debug.core.DartDebugCorePlugin; import de.roderick.weberknecht.WebSocket; import de.roderick.weberknecht.WebSocketEventHandler; import de.roderick.weberknecht.WebSocketException; import de.roderick.weberknecht.WebSocketMessage; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * A class to connect to and communicate with a Webkit Inspection Protocol server. * * <pre> WebkitConnection connection = new WebkitConnection(ChromiumConnector.getWebSocketURLFor(port, 1)); connection.addConnectionListener(new WebkitConnectionListener() { @Override public void connectionClosed(WebkitConnection connection) { System.out.println("connection closed"); } }); connection.connect(); // add a console listener connection.getConsole().addConsoleListener(new ConsoleListener() { public void messageAdded(String message) { System.out.println("message added: " + message); } public void messageRepeatCountUpdated(int count) { } public void messagesCleared() { System.out.println("messages cleared"); } }); // enable console events connection.getConsole().enable(); // navigate to cheese.com connection.getPage().navigate("http://www.cheese.com"); </pre> * * @see http://code.google.com/chrome/devtools/docs/protocol/tot/index.html * @see http://trac.webkit.org/browser/trunk/Source/WebCore/inspector/Inspector.json */ public class WebkitConnection { public static interface WebkitConnectionListener { public void connectionClosed(WebkitConnection connection); } static interface Callback { public void handleResult(JSONObject result) throws JSONException; } static interface NotificationHandler { public void handleNotification(String method, JSONObject params) throws JSONException; } private URI webSocketUri; private String host; private int port; private String webSocketFile; private WebSocket websocket; private boolean connected; private WebkitConsole console; private WebkitDebugger debugger; private WebkitPage page; private WebkitRuntime runtime; private WebkitCSS css; private WebkitDom dom; private WebkitDomDebugger domDebugger; private WebkitWorker worker; private WebkitNetwork network; private WebkitObservatory observatory; private int requestId = 0; private Map<String, NotificationHandler> notificationHandlers = new HashMap<String, NotificationHandler>(); private Map<Integer, Callback> callbackMap = new HashMap<Integer, Callback>(); private List<WebkitConnectionListener> connectionListeners = new ArrayList<WebkitConnectionListener>(); public WebkitConnection(String host, int port, String webSocketFile) { this.host = host; this.port = port; this.webSocketFile = webSocketFile; } public WebkitConnection(URI webSocketUri) { this.webSocketUri = webSocketUri; } /** * A copy constructor for DartiumDebugTarget. * * @param connection */ public WebkitConnection(WebkitConnection connection) { this.host = connection.host; this.port = connection.port; this.webSocketFile = connection.webSocketFile; this.webSocketUri = connection.webSocketUri; } public void addConnectionListener(WebkitConnectionListener listener) { connectionListeners.add(listener); } public void close() throws IOException { if (websocket != null) { try { websocket.close(); } catch (WebSocketException exception) { throw new IOException(exception); } finally { websocket = null; } } } public void connect() throws IOException { try { if (webSocketUri != null) { websocket = new WebSocket(webSocketUri); } else { websocket = new WebSocket(host, port, webSocketFile); } // Register Event Handlers websocket.setEventHandler(new WebSocketEventHandler() { @Override public void onClose() { websocket = null; notifyClosed(); } @Override public void onMessage(WebSocketMessage message) { processWebSocketMessage(message); } @Override public void onOpen() { connected = true; } @Override public void onPing() { // nothing to do } @Override public void onPong() { // nothing to do } }); websocket.connect(); } catch (WebSocketException exception) { throw new IOException(exception); } catch (Throwable exception) { // Defensively catch any programming errors from the weberknecht library. throw new IOException(exception); } } public WebkitConsole getConsole() { if (console == null) { console = new WebkitConsole(this); } return console; } public WebkitCSS getCSS() { if (css == null) { css = new WebkitCSS(this); } return css; } public WebkitDebugger getDebugger() { if (debugger == null) { debugger = new WebkitDebugger(this); } return debugger; } public WebkitDom getDom() { if (dom == null) { dom = new WebkitDom(this); } return dom; } public WebkitDomDebugger getDomDebugger() { if (domDebugger == null) { domDebugger = new WebkitDomDebugger(this); } return domDebugger; } public WebkitNetwork getNetwork() { if (network == null) { network = new WebkitNetwork(this); } return network; } public WebkitObservatory getObservatory() { if (observatory == null) { observatory = new WebkitObservatory(this); } return observatory; } public WebkitPage getPage() { if (page == null) { page = new WebkitPage(this); } return page; } public WebkitRuntime getRuntime() { if (runtime == null) { runtime = new WebkitRuntime(this); } return runtime; } public URI getWebSocketUri() { if (webSocketUri != null) { return webSocketUri; } else { try { return new URI("ws", null, host, port, webSocketFile, null, null); } catch (URISyntaxException e) { return null; } } } public WebkitWorker getWorker() { if (worker == null) { worker = new WebkitWorker(this); } return worker; } public boolean isConnected() { return websocket != null && connected; } public void removeConnectionListener(WebkitConnectionListener listener) { connectionListeners.remove(listener); } protected void notifyClosed() { for (WebkitConnectionListener listener : connectionListeners) { listener.connectionClosed(this); } // Clean up the callbackMap on termination. List<Callback> callbacks = new ArrayList<Callback>(callbackMap.values()); for (Callback callback : callbacks) { try { callback.handleResult(WebkitResult.createJsonErrorResult("connection termination")); } catch (JSONException e) { } } callbackMap.clear(); } protected void processWebSocketMessage(WebSocketMessage message) { final int MAX_PRINT_LENGTH = 2000; try { String text = message.getText(); if (text.length() > MAX_PRINT_LENGTH) { DartDebugCorePlugin.log("<== " + text.substring(0, MAX_PRINT_LENGTH) + "..."); DartDebugCorePlugin.log("<== (long line: " + text.length() + " chars)"); } else { DartDebugCorePlugin.log("<== " + text); } JSONObject object = new JSONObject(text); if (object.has("id")) { processResponse(object); } else { processNotification(object); } } catch (JSONException exception) { DartDebugCorePlugin.logError(exception); } } protected void registerNotificationHandler(String prefix, NotificationHandler handler) { notificationHandlers.put(prefix, handler); } protected void sendRequest(JSONObject request) throws IOException, JSONException { sendRequest(request, null); } protected void sendRequest(JSONObject request, Callback callback) throws IOException, JSONException { if (!isConnected()) { throw new IOException("connection terminated"); } int id = 0; try { synchronized (this) { id = getNextRequestId(); request.put("id", id); if (callback != null) { callbackMap.put(id, callback); } } DartDebugCorePlugin.log("==> " + request); websocket.send(request.toString()); } catch (WebSocketException exception) { if (callback != null) { synchronized (this) { callbackMap.remove(id); } } throw new IOException(exception); } } private int getNextRequestId() { return ++requestId; } private void processNotification(JSONObject object) throws JSONException { // Two notifications we receive but don't do anything with: // "Profiler.resetProfiles", "CSS.mediaQueryResultChanged" final String[] ignoreDomains = {"Profiler.", "Inspector."}; if (object.has("method")) { String method = object.getString("method"); String prefix = method; int index = prefix.indexOf('.'); if (index != -1) { prefix = prefix.substring(0, index + 1); } NotificationHandler handler = notificationHandlers.get(prefix); if (handler != null) { if (object.has("params")) { handler.handleNotification(method, object.getJSONObject("params")); } else { handler.handleNotification(method, null); } } else { for (String domain : ignoreDomains) { if (domain.equals(prefix)) { return; } } DartDebugCorePlugin.logInfo("no handler for notification: " + object); } } } private void processResponse(JSONObject result) throws JSONException { try { int id = result.optInt("id", -1); Callback callback; synchronized (this) { callback = callbackMap.remove(id); } if (callback != null) { callback.handleResult(result); } else if (result.has("error")) { // If we get an error back, and nobody was listening for the result, then log it. WebkitResult<?> webkitResult = WebkitResult.createFrom(result); DartDebugCorePlugin.logInfo("Error from command id " + id + ": " + webkitResult.getError()); } } catch (Throwable exception) { DartDebugCorePlugin.logError(exception); } } }