// 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 static org.chromium.sdk.util.BasicUtil.containsKeySafe; import static org.chromium.sdk.util.BasicUtil.getSafe; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import org.chromium.sdk.DebugEventListener; import org.chromium.sdk.JavascriptVm; import org.chromium.sdk.RelayOk; import org.chromium.sdk.Script; import org.chromium.sdk.SyncCallback; import org.chromium.sdk.internal.ScriptBase; import org.chromium.sdk.internal.wip.protocol.input.debugger.GetScriptSourceData; import org.chromium.sdk.internal.wip.protocol.input.debugger.ScriptParsedEventData; import org.chromium.sdk.internal.wip.protocol.output.debugger.GetScriptSourceParams; import org.chromium.sdk.util.AsyncFuture; import org.chromium.sdk.util.AsyncFuture.Callback; import org.chromium.sdk.util.AsyncFutureMerger; import org.chromium.sdk.util.AsyncFutureRef; import org.chromium.sdk.util.GenericCallback; import org.chromium.sdk.util.RelaySyncCallback; /** * Keeps all current scripts for the debug session and handles script source loading. */ class WipScriptManager { private final WipTabImpl tabImpl; // Access must be synchronized. private final Map<String, ScriptData> scriptIdToData = new HashMap<String, ScriptData>(); /** * A future for script pre-load operation. User may call {@link #getScripts} at any time, * but we return result only once we have loaded all pre-existing scripts. */ private final AsyncFutureRef<Void> scriptsPreloaded; /** Accessed from Dispatch thread only. */ private ScriptPopulateMode populateMode = new ScriptPopulateMode(); WipScriptManager(WipTabImpl tabImpl) { this.tabImpl = tabImpl; this.scriptsPreloaded = populateMode.createAndInitMasterFuture(); } WipTabImpl getTabImpl() { return tabImpl; } // Run command in dispatch thread so that no scripts event could happen in the meantime. // TODO: make sure we do not return those scripts that are reported compiled but not loaded yet. RelayOk getScripts(final GenericCallback<Collection<Script>> callback, SyncCallback syncCallback) { // Async command chain here, wrap syncCallback to guaranteed calling. RelaySyncCallback relay = new RelaySyncCallback(syncCallback); // Guard for the step one. final RelaySyncCallback.Guard guardOne = relay.newGuard(); // Chain commands are in the reverse order. // Wait for script pre-load operation and return scripts. final AsyncFuture.Callback<Void> futureCallback = new AsyncFuture.Callback<Void>() { @Override public void done(Void res) { if (callback != null) { callback.success(getCurrentScripts()); } } }; // Start everything in dispatch thread (otherwise user may be called from this thread). Runnable mainRunnable = new Runnable() { @Override public void run() { RelayOk relayOk = scriptsPreloaded.getAsync(futureCallback, guardOne.getRelay().getUserSyncCallback()); guardOne.discharge(relayOk); } }; return tabImpl.getCommandProcessor().runInDispatchThread(mainRunnable, guardOne.asSyncCallback()); } Script getScript(String scriptId) { ScriptData data; synchronized (scriptIdToData) { data = getSafe(scriptIdToData, scriptId); } if (data == null) { return null; } if (!data.sourceLoadedFuture.isDone()) { return null; } return data.scriptImpl; } private Collection<Script> getCurrentScripts() { synchronized (scriptIdToData) { List<Script> list = new ArrayList<Script>(scriptIdToData.size()); for (ScriptData data : scriptIdToData.values()) { if (data.sourceLoadedFuture.isDone()) { list.add(data.scriptImpl); } } return list; } } public void scriptIsReportedParsed(ScriptParsedEventData data) { final String sourceID = data.scriptId(); String url = data.url(); if (url.isEmpty()) { url = null; } ScriptBase.Descriptor<String> descriptor = new ScriptBase.Descriptor<String>(Script.Type.NORMAL, sourceID, url, (int) data.startLine(), (int) data.startColumn(), -1); final WipScriptImpl script = new WipScriptImpl(this, descriptor); final ScriptData scriptData = new ScriptData(script); synchronized (scriptIdToData) { if (containsKeySafe(scriptIdToData, sourceID)) { throw new IllegalStateException("Already has script with id " + sourceID); } scriptIdToData.put(sourceID, scriptData); } scriptData.sourceLoadedFuture.initializeRunning(new SourceLoadOperation(script, sourceID)); final ScriptPopulateMode populateModeSaved = populateMode; AsyncFuture.Callback<Boolean> callback; SyncCallback syncCallback; if (populateModeSaved == null) { callback = new AsyncFuture.Callback<Boolean>() { @Override public void done(Boolean res) { tabImpl.getTabListener().getDebugEventListener().scriptLoaded(script); } }; syncCallback = null; } else { populateModeSaved.anotherSourceToWait(); callback = new AsyncFuture.Callback<Boolean>() { @Override public void done(Boolean res) { populateModeSaved.sourceLoaded(res); } }; syncCallback = new SyncCallback() { @Override public void callbackDone(RuntimeException e) { populateModeSaved.sourceLoadedSync(e); } }; } scriptData.sourceLoadedFuture.getAsync(callback, syncCallback); } /** * Asynchronously loads script source. */ private final class SourceLoadOperation implements AsyncFuture.Operation<Boolean> { private final WipScriptImpl script; private final String sourceID; private SourceLoadOperation(WipScriptImpl script, String sourceID) { this.script = script; this.sourceID = sourceID; } @Override public RelayOk start(final Callback<Boolean> operationCallback, SyncCallback syncCallback) { GenericCallback<GetScriptSourceData> commandCallback = new GenericCallback<GetScriptSourceData>() { @Override public void success(GetScriptSourceData data) { String source = data.scriptSource(); script.setSource(source); operationCallback.done(true); } @Override public void failure(Exception exception) { throw new RuntimeException(exception); } }; GetScriptSourceParams params = new GetScriptSourceParams(sourceID); return tabImpl.getCommandProcessor().send(params, commandCallback, syncCallback); } } private class ScriptData { final WipScriptImpl scriptImpl; final AsyncFutureRef<Boolean> sourceLoadedFuture = new AsyncFutureRef<Boolean>(); ScriptData(WipScriptImpl scriptImpl) { this.scriptImpl = scriptImpl; } } /** * Asynchronously loads all script sources that will be referenced from a new debug context * (from its stack frames). * Must be called from Dispatch thread. */ RelayOk loadScriptSourcesAsync(Set<String> ids, ScriptSourceLoadCallback callback, SyncCallback syncCallback) { Queue<ScriptData> scripts = new ArrayDeque<ScriptData>(ids.size()); Map<String, WipScriptImpl> result = new HashMap<String, WipScriptImpl>(ids.size()); synchronized (scriptIdToData) { for (String id : ids) { ScriptData data = getSafe(scriptIdToData, id); if (data == null) { // We probably got id of internal script (usually happens when we suspend on breakpoint // thrown from internals). result.put(id, null); continue; } result.put(id, data.scriptImpl); if (!data.sourceLoadedFuture.isDone()) { scripts.add(data); } } } // Start a chain of asynchronous operations. // Make sure we call this sync callback sooner or later. RelaySyncCallback relay = new RelaySyncCallback(syncCallback); return loadNextScript(scripts, result, callback, relay); } interface ScriptSourceLoadCallback { void done(Map<String, WipScriptImpl> loadedScripts); } static String convertAlienSourceId(Object sourceIdObj) { if (sourceIdObj instanceof String == false) { throw new IllegalArgumentException("Script id must be string"); } return (String) sourceIdObj; } // TODO: scripts are loaded in-series; make load parallel instead (to wait less). private RelayOk loadNextScript(final Queue<ScriptData> scripts, final Map<String, WipScriptImpl> result, final ScriptSourceLoadCallback callback, final RelaySyncCallback relay) { final ScriptData data = scripts.poll(); if (data == null) { // Terminate the chain of asynchronous loads and pass a result to the callback. RelayOk relayOk; if (callback != null) { callback.done(result); } return relay.finish(); } // Create a guard for the case that we fail before issuing next #loadNextScript() call. final RelaySyncCallback.Guard guard = relay.newGuard(); AsyncFuture.Callback<Boolean> futureCallback = new AsyncFuture.Callback<Boolean>() { @Override public void done(Boolean res) { RelayOk relayOk = loadNextScript(scripts, result, callback, relay); // We successfully relayed responsibility for operationDestructable to next async call, // discharge guard. guard.discharge(relayOk); } }; // The async operation will call a guard even if something failed within the AsyncFuture. return data.sourceLoadedFuture.getAsync(futureCallback, guard.asSyncCallback()); } public void pageReloaded() { synchronized (scriptIdToData) { scriptIdToData.clear(); } } void endPopulateScriptMode() { populateMode.endMode(); populateMode = null; } /** * Right after initialization we come into 'populate scripts' mode, when back-end * reports about all pre-existing scripts as if they have just been parsed. * <p> * We treat this notifications differently: all these scripts must be returned from * {@link JavascriptVm#getScripts} from the beginning and only truly new scripts get * reported via {@link DebugEventListener#scriptLoaded}. * <p> * This means that until 'populate mode' ends (and all sources are loaded), * {@link JavascriptVm#getScripts} call blocks. */ private static class ScriptPopulateMode { /** * Future for script preload operation. It completes when all pre-exising scripts * are fully loaded (with sources). The operation result value is an array of * source loading success/failure flags. */ private final AsyncFutureMerger<Boolean> populateAndLoadSourcesFuture = new AsyncFutureMerger<Boolean>(); /** * Reports that 'populate script' mode is finished. However we may still be waiting for * the corresponding script sources. */ void endMode() { populateAndLoadSourcesFuture.subOperationDone(null); populateAndLoadSourcesFuture.subOperationDoneSync(null); } /** * We learned about another pre-existing script. Now we have to wait for its source. */ void anotherSourceToWait() { populateAndLoadSourcesFuture.addSubOperation(); } void sourceLoaded(Boolean result) { populateAndLoadSourcesFuture.subOperationDone(result); } /** * Additional method that completes {@link #sourceLoaded} and used to be compatible with * {@link SyncCallback} paradigm. */ void sourceLoadedSync(RuntimeException e) { populateAndLoadSourcesFuture.subOperationDoneSync(e); } /** * Creates a 'master operation' future that hides the complex result value * of {@link #populateAndLoadSourcesFuture}. It hides it from Java GC also, * so the {@link ArrayList} gets collected once operation is finished. */ AsyncFutureRef<Void> createAndInitMasterFuture() { AsyncFutureRef<Void> asyncFutureRef = new AsyncFutureRef<Void>(); asyncFutureRef.initializeRunning(new AsyncFuture.Operation<Void>() { @Override public RelayOk start(final Callback<Void> callback, SyncCallback syncCallback) { AsyncFuture<?> innerFuture = populateAndLoadSourcesFuture.getFuture(); return innerFuture.getAsync(new Callback<Object>() { @Override public void done(Object res) { callback.done(null); } }, syncCallback); } }); return asyncFutureRef; } } }