// 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;
}
}
}