org.chromium.sdk/src/org/chromium/sdk/internal/tools/v8/ChromeDevToolSessionManager.java
changeset 2 e4420d2515f1
equal deleted inserted replaced
1:ef76fc2ac88c 2:e4420d2515f1
       
     1 // Copyright (c) 2009 The Chromium Authors. All rights reserved.
       
     2 // Use of this source code is governed by a BSD-style license that can be
       
     3 // found in the LICENSE file.
       
     4 
       
     5 package org.chromium.sdk.internal.tools.v8;
       
     6 
       
     7 import java.util.EnumSet;
       
     8 import java.util.Set;
       
     9 import java.util.concurrent.Semaphore;
       
    10 import java.util.concurrent.TimeUnit;
       
    11 import java.util.concurrent.atomic.AtomicReference;
       
    12 import java.util.logging.Level;
       
    13 import java.util.logging.Logger;
       
    14 
       
    15 import org.chromium.sdk.DebugEventListener;
       
    16 import org.chromium.sdk.TabDebugEventListener;
       
    17 import org.chromium.sdk.internal.BrowserImpl;
       
    18 import org.chromium.sdk.internal.BrowserTabImpl;
       
    19 import org.chromium.sdk.internal.DebugSession;
       
    20 import org.chromium.sdk.internal.DebugSessionManager;
       
    21 import org.chromium.sdk.internal.JsonUtil;
       
    22 import org.chromium.sdk.internal.Result;
       
    23 import org.chromium.sdk.internal.V8ContextFilter;
       
    24 import org.chromium.sdk.internal.protocol.data.ContextData;
       
    25 import org.chromium.sdk.internal.protocol.data.ContextHandle;
       
    26 import org.chromium.sdk.internal.protocolparser.JsonProtocolParseException;
       
    27 import org.chromium.sdk.internal.tools.ChromeDevToolsProtocol;
       
    28 import org.chromium.sdk.internal.tools.ToolHandler;
       
    29 import org.chromium.sdk.internal.tools.ToolOutput;
       
    30 import org.chromium.sdk.internal.tools.v8.request.DebuggerMessage;
       
    31 import org.chromium.sdk.internal.transport.Message;
       
    32 import org.json.simple.JSONObject;
       
    33 import org.json.simple.parser.ParseException;
       
    34 
       
    35 /**
       
    36  * Handles the interaction with the "V8Debugger" tool.
       
    37  */
       
    38 public class ChromeDevToolSessionManager implements DebugSessionManager {
       
    39 
       
    40   /**
       
    41    * This exception is thrown whenever the handler could not get a tab
       
    42    * attachment result from the debugged browser.
       
    43    */
       
    44   public static class AttachmentFailureException extends Exception {
       
    45 
       
    46     private static final long serialVersionUID = 1L;
       
    47 
       
    48     public AttachmentFailureException() {
       
    49     }
       
    50 
       
    51     public AttachmentFailureException(String message, Throwable cause) {
       
    52       super(message, cause);
       
    53     }
       
    54   }
       
    55 
       
    56   /**
       
    57    * An interface to run callbacks in response to V8 debugger commands that do
       
    58    * not have associated JSON payloads.
       
    59    */
       
    60   public interface ResultAwareCallback {
       
    61 
       
    62     /**
       
    63      * This method is invoked whenever a response to a V8 command is received.
       
    64      *
       
    65      * @param result of the command
       
    66      */
       
    67     void resultReceived(Result result);
       
    68   }
       
    69 
       
    70   /** The class logger. */
       
    71   private static final Logger LOGGER =
       
    72       Logger.getLogger(ChromeDevToolSessionManager.class.getName());
       
    73 
       
    74   private static final V8ContextFilter CONTEXT_FILTER = new V8ContextFilter() {
       
    75     public boolean isContextOurs(ContextHandle contextHandle) {
       
    76       Object data = contextHandle.data();
       
    77       if (data == null) {
       
    78         return false;
       
    79       }
       
    80 
       
    81       final boolean skipSketchCode = true;
       
    82       if (skipSketchCode) {
       
    83         // We do not actually have a context id to compare with. So we shouldn't waste time
       
    84         // on parsing until we have this id.
       
    85         return true;
       
    86       }
       
    87 
       
    88       long scriptContextId;
       
    89       if (data instanceof String) {
       
    90         String stringData = (String) data;
       
    91         // we should parse string and check context id. It should have the format "type,id".
       
    92       } else if (data instanceof JSONObject) {
       
    93         JSONObject dataObject = (JSONObject) data;
       
    94         ContextData contextData;
       
    95         try {
       
    96           contextData = V8ProtocolUtil.getV8Parser().parse(dataObject, ContextData.class);
       
    97         } catch (JsonProtocolParseException e) {
       
    98           throw new RuntimeException(e);
       
    99         }
       
   100         scriptContextId = contextData.value();
       
   101       }
       
   102       //TODO(peter.rybin): Here we are probably supposed to compare it with our context id.
       
   103       return true;
       
   104     }
       
   105   };
       
   106 
       
   107   /** The host BrowserTabImpl instance. */
       
   108   private final BrowserTabImpl browserTabImpl;
       
   109 
       
   110   private final ToolOutput toolOutput;
       
   111 
       
   112   /** The debug context for this handler. */
       
   113   private final DebugSession debugSession;
       
   114 
       
   115   private final AtomicReference<AttachState> attachState =
       
   116     new AtomicReference<AttachState>(null);
       
   117 
       
   118   private final AtomicReference<ResultAwareCallback> attachCallback =
       
   119       new AtomicReference<ResultAwareCallback>(null);
       
   120 
       
   121   private final AtomicReference<ResultAwareCallback> detachCallback =
       
   122       new AtomicReference<ResultAwareCallback>(null);
       
   123 
       
   124   /**
       
   125    * A no-op JavaScript to evaluate.
       
   126    */
       
   127   public static final String JAVASCRIPT_VOID = "javascript:void(0);";
       
   128 
       
   129   public ChromeDevToolSessionManager(BrowserTabImpl browserTabImpl, ToolOutput toolOutput) {
       
   130     this.browserTabImpl = browserTabImpl;
       
   131     this.toolOutput = toolOutput;
       
   132     V8CommandOutputImpl v8MessageOutput = new V8CommandOutputImpl(toolOutput);
       
   133     this.debugSession = new DebugSession(this, CONTEXT_FILTER, v8MessageOutput);
       
   134   }
       
   135 
       
   136   public DebugSession getDebugSession() {
       
   137     return debugSession;
       
   138   }
       
   139 
       
   140   public DebugEventListener getDebugEventListener() {
       
   141     return browserTabImpl.getDebugEventListener();
       
   142   }
       
   143 
       
   144   private TabDebugEventListener getTabDebugEventListener() {
       
   145     return browserTabImpl.getTabDebugEventListener();
       
   146   }
       
   147 
       
   148   private void handleChromeDevToolMessage(final Message message) {
       
   149     JSONObject json;
       
   150     try {
       
   151       json = JsonUtil.jsonObjectFromJson(message.getContent());
       
   152     } catch (ParseException e) {
       
   153       LOGGER.log(Level.SEVERE, "Invalid JSON received: {0}", message.getContent());
       
   154       return;
       
   155     }
       
   156     String commandString = JsonUtil.getAsString(json, ChromeDevToolsProtocol.COMMAND.key);
       
   157     DebuggerToolCommand command = DebuggerToolCommand.forName(commandString);
       
   158     if (command != null) {
       
   159       switch (command) {
       
   160         case ATTACH:
       
   161           processAttach(json);
       
   162           break;
       
   163         case DETACH:
       
   164           processDetach(json);
       
   165           break;
       
   166         case DEBUGGER_COMMAND:
       
   167           processDebuggerCommand(json);
       
   168           break;
       
   169         case NAVIGATED:
       
   170           processNavigated(json);
       
   171           break;
       
   172         case CLOSED:
       
   173           processClosed(json);
       
   174           break;
       
   175       }
       
   176       return;
       
   177     }
       
   178     throw new IllegalArgumentException("Invalid command: " + commandString);
       
   179   }
       
   180 
       
   181   public void onDebuggerDetached() {
       
   182     // ignore
       
   183   }
       
   184 
       
   185   /**
       
   186    * Disconnects tab from connections. Can safely be called several times. This sends EOS
       
   187    * message and unregisters tab from browser.
       
   188    * Should be called from UI, may block.
       
   189    */
       
   190   public void cutTheLineMyself() {
       
   191     toolHandler.cutTheLine();
       
   192   }
       
   193   /**
       
   194    * This method is sure to be called only once.
       
   195    */
       
   196   private void handleEos() {
       
   197     // We overwrite other values; nobody else should become unhappy, all expecters
       
   198     // are in handle* methods and they are not going to get their results
       
   199     attachState.set(AttachState.DISCONNECTED);
       
   200     browserTabImpl.handleEosFromToolService();
       
   201     debugSession.getV8CommandProcessor().processEos();
       
   202 
       
   203     DebugEventListener debugEventListener = getDebugEventListener();
       
   204     if (debugEventListener != null) {
       
   205       debugEventListener.disconnected();
       
   206     }
       
   207   }
       
   208 
       
   209   private AttachState getAttachState() {
       
   210     return attachState.get();
       
   211   }
       
   212 
       
   213   /**
       
   214    * This method is for UI -- pretty low precision of result type.
       
   215    * @return whether the handler is attached to a tab
       
   216    */
       
   217   public boolean isAttachedForUi() {
       
   218     return STATES_CALLED_ATTACHED.contains(getAttachState());
       
   219   }
       
   220 
       
   221   /**
       
   222    * Attaches the remote debugger to the associated browser tab.
       
   223    *
       
   224    * @return the attachment result
       
   225    * @throws AttachmentFailureException whenever the handler could not connect
       
   226    *         to the browser
       
   227    */
       
   228   public Result attachToTab() throws AttachmentFailureException {
       
   229     boolean res = attachState.compareAndSet(null, AttachState.ATTACHING);
       
   230     if (!res) {
       
   231       throw new AttachmentFailureException("Illegal state", null);
       
   232     }
       
   233 
       
   234     String command = V8DebuggerToolMessageFactory.attach();
       
   235     Result attachResult = sendSimpleCommandSync(attachCallback, command);
       
   236 
       
   237     debugSession.startCommunication();
       
   238 
       
   239     return attachResult;
       
   240   }
       
   241 
       
   242   /**
       
   243    * Detaches the remote debugger from the associated browser tab.
       
   244    * Should be called from UI thread.
       
   245    * @return the detachment result
       
   246    */
       
   247   public Result detachFromTab() {
       
   248     if (attachState.get() != AttachState.NORMAL) {
       
   249       toolHandler.onDebuggerDetached();
       
   250       return Result.ILLEGAL_TAB_STATE;
       
   251     }
       
   252 
       
   253     String command = V8DebuggerToolMessageFactory.detach();
       
   254     Result result;
       
   255     try {
       
   256       result = sendSimpleCommandSync(detachCallback, command);
       
   257     } catch (AttachmentFailureException e) {
       
   258       result = null;
       
   259     } finally {
       
   260       // Make sure line is cut
       
   261       cutTheLineMyself();
       
   262     }
       
   263 
       
   264     return result;
       
   265   }
       
   266 
       
   267   private Result sendSimpleCommandSync(AtomicReference<ResultAwareCallback> callbackReference,
       
   268       String command) throws AttachmentFailureException {
       
   269     final Semaphore sem = new Semaphore(0);
       
   270     final Result[] output = new Result[1];
       
   271     ResultAwareCallback callback = new ResultAwareCallback() {
       
   272       public void resultReceived(Result result) {
       
   273         output[0] = result;
       
   274         sem.release();
       
   275       }
       
   276     };
       
   277 
       
   278     boolean res = callbackReference.compareAndSet(null, callback);
       
   279     if (!res) {
       
   280       throw new IllegalStateException("Callback is already set");
       
   281     }
       
   282 
       
   283     boolean completed;
       
   284     try {
       
   285       toolOutput.send(command);
       
   286 
       
   287       try {
       
   288         completed = sem.tryAcquire(BrowserImpl.OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
       
   289       } catch (InterruptedException e) {
       
   290         throw new RuntimeException(e);
       
   291       }
       
   292     } finally {
       
   293       // Make sure we do not leave our callback behind us.
       
   294       callbackReference.compareAndSet(callback, null);
       
   295     }
       
   296 
       
   297     // If the command fails, notify the caller.
       
   298     if (!completed) {
       
   299       throw new AttachmentFailureException("Timed out", null);
       
   300     }
       
   301 
       
   302     return output[0];
       
   303   }
       
   304 
       
   305   public ToolHandler getToolHandler() {
       
   306     return toolHandler;
       
   307   }
       
   308 
       
   309   private final ToolHandlerImpl toolHandler = new ToolHandlerImpl();
       
   310 
       
   311   private class ToolHandlerImpl implements ToolHandler {
       
   312     private volatile boolean isLineCut = false;
       
   313     private boolean alreadyHasEos = false;
       
   314 
       
   315     /**
       
   316      * Here we call methods that are normally are being invoked from Connection Dispatch
       
   317      * thread. So we compete for "this" as synchronization monitor with that thread.
       
   318      * We should be careful, cause theoretically it may cause a deadlock.
       
   319      */
       
   320     void cutTheLine() {
       
   321       // First mark ourselves as "cut off" to stop other threads,
       
   322       // then start waiting on synchronized.
       
   323       isLineCut = true;
       
   324       synchronized (this) {
       
   325         sendEos();
       
   326       }
       
   327     }
       
   328 
       
   329     public synchronized void handleMessage(Message message) {
       
   330       if (isLineCut) {
       
   331         return;
       
   332       }
       
   333       handleChromeDevToolMessage(message);
       
   334     }
       
   335 
       
   336     public synchronized void handleEos() {
       
   337       if (isLineCut) {
       
   338         return;
       
   339       }
       
   340       sendEos();
       
   341     }
       
   342     private synchronized void sendEos() {
       
   343       if (alreadyHasEos) {
       
   344         return;
       
   345       }
       
   346       alreadyHasEos = true;
       
   347       ChromeDevToolSessionManager.this.handleEos();
       
   348     }
       
   349 
       
   350     public void onDebuggerDetached() {
       
   351       // ignore
       
   352     }
       
   353   }
       
   354 
       
   355   private void processClosed(JSONObject json) {
       
   356     notifyCallback(detachCallback, Result.OK);
       
   357     getTabDebugEventListener().closed();
       
   358     cutTheLineMyself();
       
   359   }
       
   360 
       
   361   /**
       
   362    * This method is invoked from synchronized code sections. It checks if there is a callback
       
   363    * provided in {@code callbackReference}. Sets callback to null.
       
   364    *
       
   365    * @param callbackReference reference which may hold callback
       
   366    * @param result to notify the callback with
       
   367    */
       
   368   private void notifyCallback(AtomicReference<ResultAwareCallback> callbackReference,
       
   369       Result result) {
       
   370     ResultAwareCallback callback = callbackReference.getAndSet(null);
       
   371     if (callback != null) {
       
   372       try {
       
   373         callback.resultReceived(result);
       
   374       } catch (Exception e) {
       
   375         LOGGER.log(Level.WARNING, "Exception in the callback", e);
       
   376       }
       
   377     }
       
   378   }
       
   379 
       
   380   private void processAttach(JSONObject json) {
       
   381     Long resultValue = JsonUtil.getAsLong(json, ChromeDevToolsProtocol.RESULT.key);
       
   382     Result result = Result.forCode(resultValue.intValue());
       
   383     // Message destination equals context.getTabId()
       
   384     if (result == Result.OK) {
       
   385       boolean res = attachState.compareAndSet(AttachState.ATTACHING, AttachState.NORMAL);
       
   386       if (!res) {
       
   387         throw new IllegalStateException();
       
   388       }
       
   389     } else {
       
   390       if (result == null) {
       
   391         result = Result.DEBUGGER_ERROR;
       
   392       }
       
   393     }
       
   394     notifyCallback(attachCallback, result);
       
   395   }
       
   396 
       
   397   private void processDetach(JSONObject json) {
       
   398     Long resultValue = JsonUtil.getAsLong(json, ChromeDevToolsProtocol.RESULT.key);
       
   399     Result result = Result.forCode(resultValue.intValue());
       
   400     if (result == Result.OK) {
       
   401       // ignore result, we may already be in DISCONNECTED state
       
   402       attachState.compareAndSet(AttachState.DETACHING, AttachState.DETACHED);
       
   403     } else {
       
   404       if (result == null) {
       
   405         result = Result.DEBUGGER_ERROR;
       
   406       }
       
   407     }
       
   408     notifyCallback(detachCallback, result);
       
   409   }
       
   410 
       
   411   private void processDebuggerCommand(JSONObject json) {
       
   412     JSONObject v8Json = JsonUtil.getAsJSON(json, ChromeDevToolsProtocol.DATA.key);
       
   413     V8CommandProcessor.checkNull(v8Json, "'data' field not found");
       
   414     debugSession.getV8CommandProcessor().processIncomingJson(v8Json);
       
   415   }
       
   416 
       
   417   private void processNavigated(JSONObject json) {
       
   418     String newUrl = JsonUtil.getAsString(json, ChromeDevToolsProtocol.DATA.key);
       
   419     debugSession.navigated();
       
   420     getTabDebugEventListener().navigated(newUrl);
       
   421   }
       
   422 
       
   423   public static class V8CommandOutputImpl implements V8CommandOutput {
       
   424     private final ToolOutput toolOutput;
       
   425 
       
   426     public V8CommandOutputImpl(ToolOutput toolOutput) {
       
   427       this.toolOutput = toolOutput;
       
   428     }
       
   429 
       
   430     public void send(DebuggerMessage debuggerMessage, boolean isImmediate) {
       
   431       toolOutput.send(
       
   432           V8DebuggerToolMessageFactory.debuggerCommand(
       
   433               JsonUtil.streamAwareToJson(debuggerMessage)));
       
   434       if (isImmediate) {
       
   435         toolOutput.send(
       
   436             V8DebuggerToolMessageFactory.evaluateJavascript(JAVASCRIPT_VOID));
       
   437       }
       
   438     }
       
   439   }
       
   440 
       
   441   private static class V8DebuggerToolMessageFactory {
       
   442 
       
   443     static String attach() {
       
   444       return createDebuggerMessage(DebuggerToolCommand.ATTACH, null);
       
   445     }
       
   446 
       
   447     static String detach() {
       
   448       return createDebuggerMessage(DebuggerToolCommand.DETACH, null);
       
   449     }
       
   450 
       
   451     public static String debuggerCommand(String json) {
       
   452       return createDebuggerMessage(DebuggerToolCommand.DEBUGGER_COMMAND, json);
       
   453     }
       
   454 
       
   455     public static String evaluateJavascript(String javascript) {
       
   456       return createDebuggerMessage(DebuggerToolCommand.EVALUATE_JAVASCRIPT,
       
   457           JsonUtil.quoteString(javascript));
       
   458     }
       
   459 
       
   460     private static String createDebuggerMessage(
       
   461         DebuggerToolCommand command, String dataField) {
       
   462       StringBuilder sb = new StringBuilder("{\"command\":\"");
       
   463       sb.append(command.commandName).append('"');
       
   464       if (dataField != null) {
       
   465         sb.append(",\"data\":").append(dataField);
       
   466       }
       
   467       sb.append('}');
       
   468       return sb.toString();
       
   469     }
       
   470   }
       
   471 
       
   472   private enum AttachState {
       
   473     ATTACHING, NORMAL, DETACHING, DETACHED, DISCONNECTED
       
   474   }
       
   475 
       
   476   private static final Set<AttachState> STATES_CALLED_ATTACHED = EnumSet.of(AttachState.NORMAL);
       
   477 }