// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.sdk.internal.wip; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.EnumMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.chromium.sdk.Breakpoint; import org.chromium.sdk.BreakpointTypeExtension; import org.chromium.sdk.BrowserTab; import org.chromium.sdk.CallbackSemaphore; import org.chromium.sdk.FunctionScopeExtension; import org.chromium.sdk.IgnoreCountBreakpointExtension; import org.chromium.sdk.RelayOk; import org.chromium.sdk.RestartFrameExtension; import org.chromium.sdk.Script; import org.chromium.sdk.SyncCallback; import org.chromium.sdk.TabDebugEventListener; import org.chromium.sdk.Version; import org.chromium.sdk.internal.JsonUtil; import org.chromium.sdk.internal.websocket.WsConnection; import org.chromium.sdk.internal.wip.protocol.input.WipCommandResponse.Success; import org.chromium.sdk.internal.wip.protocol.output.WipParams; import org.chromium.sdk.internal.wip.protocol.output.debugger.PauseParams; import org.chromium.sdk.internal.wip.protocol.output.debugger.SetBreakpointsActiveParams; import org.chromium.sdk.internal.wip.protocol.output.debugger.SetPauseOnExceptionsParams; import org.chromium.sdk.util.GenericCallback; import org.chromium.sdk.util.MethodIsBlockingException; import org.chromium.sdk.util.RelaySyncCallback; import org.chromium.sdk.util.SignalRelay; import org.chromium.sdk.util.SignalRelay.AlreadySignalledException; import org.chromium.sdk.wip.EvaluateToMappingExtension; import org.chromium.sdk.wip.PermanentRemoteValueMapping; import org.chromium.sdk.wip.WipBrowser; import org.chromium.sdk.wip.WipBrowserTab; import org.chromium.sdk.wip.WipJavascriptVm; import org.json.simple.JSONObject; import org.json.simple.parser.ParseException; /** * {@link BrowserTab} implementation that attaches to remote tab via WebInspector * protocol (WIP). */ public class WipTabImpl implements WipBrowserTab, WipJavascriptVm { private static final Logger LOGGER = Logger.getLogger(WipTabImpl.class.getName()); private final WsConnection socket; private final WipBrowserImpl browserImpl; private final TabDebugEventListener tabListener; private final WipCommandProcessor commandProcessor; private final WipScriptManager scriptManager = new WipScriptManager(this); private final WipBreakpointManager breakpointManager = new WipBreakpointManager(this); private final WipContextBuilder contextBuilder = new WipContextBuilder(this); private final WipFrameManager frameManager = new WipFrameManager(this); private final VmState vmState = new VmState(); private final SignalRelay<Void> closeSignalRelay; private volatile String url; public WipTabImpl(WsConnection socket, WipBrowserImpl browserImpl, TabDebugEventListener tabListener, String preliminaryUrl) throws IOException { this.socket = socket; this.browserImpl = browserImpl; this.tabListener = tabListener; this.url = preliminaryUrl; this.closeSignalRelay = SignalRelay.create(new SignalRelay.Callback<Void>() { @Override public void onSignal(Void signal, Exception cause) { WipTabImpl.this.tabListener.closed(); } }); try { closeSignalRelay.bind(socket.getCloser(), null, null); } catch (AlreadySignalledException e) { throw new IOException("Connection is closed", e); } commandProcessor = new WipCommandProcessor(this, socket); WsConnection.Listener socketListener = new WsConnection.Listener() { @Override public void textMessageRecieved(String text) { JSONObject json; try { json = JsonUtil.jsonObjectFromJson(text); } catch (ParseException e) { throw new RuntimeException(e); } commandProcessor.acceptResponse(json); } @Override public void errorMessage(Exception ex) { LOGGER.log(Level.SEVERE, "WebSocket protocol error", ex); } @Override public void eofMessage() { commandProcessor.processEos(); } }; socket.startListening(socketListener); init(); } private void init() { SyncCallback syncCallback = new SyncCallback() { @Override public void callbackDone(RuntimeException e) { // This statement suits sync callback more rather than a regular callback: // it's safe enough and we prefer to execute it event if the command failed. scriptManager.endPopulateScriptMode(); } }; commandProcessor.send( new org.chromium.sdk.internal.wip.protocol.output.debugger.EnableParams(), null, syncCallback); commandProcessor.send( new org.chromium.sdk.internal.wip.protocol.output.page.EnableParams(), null, null); frameManager.readFrames(); } void updateUrl(String url, boolean silent) { this.url = url; if (silent) { return; } scriptManager.pageReloaded(); breakpointManager.clearNonProvisionalBreakpoints(); WipTabImpl.this.tabListener.navigated(this.url); contextBuilder.getEvaluateHack().pageReloaded(); } WipScriptManager getScriptManager() { return scriptManager; } WipBreakpointManager getBreakpointManager() { return breakpointManager; } @Override public boolean detach() { closeSignalRelay.sendSignal(null, null); return true; } @Override public boolean isAttached() { return !closeSignalRelay.isSignalled(); } @Override public PermanentRemoteValueMapping createPermanentValueMapping(String id) { return new PermanentRemoteValueMappingImpl(this, id); } @Override public RelayOk enableBreakpoints(Boolean enabled, GenericCallback<Boolean> callback, SyncCallback syncCallback) { return updateVmVariable(enabled, VmState.BREAKPOINTS_ACTIVE, callback, syncCallback); } @Override public RelayOk setBreakOnException(ExceptionCatchMode catchMode, GenericCallback<ExceptionCatchMode> callback, SyncCallback syncCallback) { VmState.Variable<ExceptionCatchMode> variable = VmState.BREAK_ON_EXCEPTION; return updateVmVariable(catchMode, variable, callback, syncCallback); } /** * Updates locally saved variables state and send request to remote. If user only calls * the method to learn the current value, request is sent anyway, to keep responses in sequence. * @return */ private <T> RelayOk updateVmVariable(T value, VmState.Variable<T> variable, final GenericCallback<T> callback, SyncCallback syncCallback) { synchronized (vmState) { final T newValue; if (value == null) { newValue = variable.getValue(vmState); } else { variable.setValue(vmState, value); newValue = value; } WipParams params = variable.createRequestParams(vmState); WipCommandCallback wrappedCallback; if (callback == null) { wrappedCallback = null; } else { wrappedCallback = new WipCommandCallback.Default() { @Override protected void onSuccess(Success success) { callback.success(newValue); } @Override protected void onError(String message) { callback.failure(new Exception(message)); } }; } return commandProcessor.send(params, wrappedCallback, syncCallback); } } @Override public Version getVersion() { // TODO(peter.rybin): support it. return new Version(Arrays.asList(0, 0), " <Unknown V8 version>"); } @Override public BreakpointTypeExtension getBreakpointTypeExtension() { return WipBreakpointImpl.TYPE_EXTENSION; } @Override public IgnoreCountBreakpointExtension getIgnoreCountBreakpointExtension() { return WipBreakpointImpl.getIgnoreCountBreakpointExtensionImpl(); } @Override public EvaluateToMappingExtension getEvaluateWithDestinationMappingExtension() { return WipEvaluateContextBase.EVALUATE_TO_MAPPING_EXTENSION; } @Override public RestartFrameExtension getRestartFrameExtension() { return null; } @Override public FunctionScopeExtension getFunctionScopeExtension() { return null; } @Override public void getScripts(final ScriptsCallback callback) throws MethodIsBlockingException { final CallbackSemaphore callbackSemaphore = new CallbackSemaphore(); GenericCallback<Collection<Script>> innerCallback; if (callback == null) { innerCallback = null; } else { innerCallback = new GenericCallback<Collection<Script>>() { @Override public void success(Collection<Script> value) { callback.success(value); } @Override public void failure(Exception exception) { callback.failure(exception.getMessage()); } }; } RelayOk relayOk = scriptManager.getScripts(innerCallback, callbackSemaphore); callbackSemaphore.acquireDefault(relayOk); } @Override public RelayOk setBreakpoint(Breakpoint.Target target, int line, int column, boolean enabled, String condition, BreakpointCallback callback, SyncCallback syncCallback) { return breakpointManager.setBreakpoint(target, line, column, enabled, condition, callback, syncCallback); } @Override public void suspend(final SuspendCallback callback) { PauseParams params = new PauseParams(); WipCommandCallback wrappedCallback; if (callback == null) { wrappedCallback = null; } else { wrappedCallback = new WipCommandCallback.Default() { @Override protected void onSuccess(Success success) { callback.success(); } @Override protected void onError(String message) { callback.failure(new Exception(message)); } }; } commandProcessor.send(params, wrappedCallback, null); } @Override public RelayOk listBreakpoints(ListBreakpointsCallback callback, SyncCallback syncCallback) { if (callback != null) { callback.success(breakpointManager.getAllBreakpoints()); } return RelaySyncCallback.finish(syncCallback); } @Override public WipBrowser getBrowser() { return browserImpl; } @Override public WipJavascriptVm getJavascriptVm() { return this; } @Override public String getUrl() { return url; } public TabDebugEventListener getDebugListener() { return this.tabListener; } public WsConnection getWsSocket() { return this.socket; } WipContextBuilder getContextBuilder() { return contextBuilder; } WipCommandProcessor getCommandProcessor() { return commandProcessor; } WipFrameManager getFrameManager() { return frameManager; } TabDebugEventListener getTabListener() { return tabListener; } /** * Saves currently set VM parameters. Default values must correspond to those * of WebInspector protocol. */ private static class VmState { // TODO: get protocol declare this default value explicitly. private static final boolean DEFAULT_BREAKPOINTS_ACTIVE = true; // TODO: get protocol declare this default value explicitly. private static final ExceptionCatchMode DEFAULT_CATCH_MODE = ExceptionCatchMode.NONE; boolean breakpointsActive = DEFAULT_BREAKPOINTS_ACTIVE; // TODO: do we know default value? ExceptionCatchMode breakOnExceptionMode = DEFAULT_CATCH_MODE; static abstract class Variable<T> { abstract T getValue(VmState vmState); abstract void setValue(VmState vmState, T value); abstract WipParams createRequestParams(VmState vmState); } static final Variable<Boolean> BREAKPOINTS_ACTIVE = new Variable<Boolean>() { @Override Boolean getValue(VmState vmState) { return vmState.breakpointsActive; } @Override void setValue(VmState vmState, Boolean value) { vmState.breakpointsActive = value; } @Override WipParams createRequestParams(VmState vmState) { return new SetBreakpointsActiveParams(vmState.breakpointsActive); } }; static final Variable<ExceptionCatchMode> BREAK_ON_EXCEPTION = new Variable<ExceptionCatchMode>() { @Override ExceptionCatchMode getValue(VmState vmState) { return vmState.breakOnExceptionMode; } @Override void setValue(VmState vmState, ExceptionCatchMode value) { vmState.breakOnExceptionMode = value; } @Override WipParams createRequestParams(VmState vmState) { return vmState.createPauseOnExceptionRequest(); } }; private SetPauseOnExceptionsParams createPauseOnExceptionRequest() { SetPauseOnExceptionsParams.State state = SDK_TO_WIP_CATCH_MODE.get(breakOnExceptionMode); return new SetPauseOnExceptionsParams(state); } private static Map<ExceptionCatchMode, SetPauseOnExceptionsParams.State> SDK_TO_WIP_CATCH_MODE; static { SDK_TO_WIP_CATCH_MODE = new EnumMap<ExceptionCatchMode, SetPauseOnExceptionsParams.State>( ExceptionCatchMode.class); SDK_TO_WIP_CATCH_MODE.put(ExceptionCatchMode.ALL, SetPauseOnExceptionsParams.State.ALL); SDK_TO_WIP_CATCH_MODE.put(ExceptionCatchMode.UNCAUGHT, SetPauseOnExceptionsParams.State.UNCAUGHT); SDK_TO_WIP_CATCH_MODE.put(ExceptionCatchMode.NONE, SetPauseOnExceptionsParams.State.NONE); assert SDK_TO_WIP_CATCH_MODE.size() == ExceptionCatchMode.values().length; } } }