org.chromium.debug.core/src/org/chromium/debug/core/model/BreakpointSynchronizer.java
package org.chromium.debug.core.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.chromium.debug.core.ChromiumDebugPlugin;
import org.chromium.debug.core.ChromiumSourceDirector;
import org.chromium.sdk.Breakpoint;
import org.chromium.sdk.CallbackSemaphore;
import org.chromium.sdk.JavascriptVm;
import org.chromium.sdk.SyncCallback;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IBreakpointManager;
import org.eclipse.debug.core.model.IBreakpoint;
/**
* A class responsible for comparing breakpoints in workspace and on remote VM and synchronizing
* them in both directions. {@link Direction#RESET_REMOTE} allows several synchronization
* jobs to different VMs.
*/
public class BreakpointSynchronizer {
private final JavascriptVm javascriptVm;
private final BreakpointMap.InTargetMap breakpointInTargetMap;
private final ChromiumSourceDirector sourceDirector;
private final BreakpointHelper breakpointHelper;
private final String debugModelId;
public BreakpointSynchronizer(JavascriptVm javascriptVm,
BreakpointMap.InTargetMap breakpointInTargetMap,
ChromiumSourceDirector sourceDirector, BreakpointHelper breakpointHelper,
String debugModelId) {
this.javascriptVm = javascriptVm;
this.breakpointInTargetMap = breakpointInTargetMap;
this.sourceDirector = sourceDirector;
this.breakpointHelper = breakpointHelper;
this.debugModelId = debugModelId;
}
/**
* Describes a direction the breakpoint synchronization should be performed in.
*/
public enum Direction {
/**
* All breakpoints in remote VM/VMs are cleared/updated/created to conform to breakpoints in
* Eclipse workspace.
*/
RESET_REMOTE,
/**
* All breakpoints in local workspace are cleared/updated/created to conform to breakpoints in
* remote VM (not applicable for multiple VMs).
*/
RESET_LOCAL,
/**
* Breakpoints are created locally or remotely or tied together so that every breakpoint
* has a counterpart on other side.
*/
MERGE
}
/**
* Additional interface used by {@link BreakpointSynchronizer}.
*/
public interface BreakpointHelper {
/**
* Create breakpoint on remote VM (asynchronously) and link it to uiBreakpoint.
*/
void createBreakpointOnRemote(ChromiumLineBreakpoint uiBreakpoint,
VmResourceId vmResourceId,
CreateCallback createCallback, SyncCallback syncCallback);
interface CreateCallback {
void failure(Exception ex);
void success();
}
}
public interface Callback {
void onDone(IStatus status);
}
/**
* The main entry method of the class. Asynchronously performs synchronization job.
* TODO(peter.rybin): consider some end-of-job notification for this method and possibly locks
*/
public void syncBreakpoints(Direction direction, Callback callback) {
ReportBuilder reportBuilder = new ReportBuilder(direction);
StatusBuilder statusBuilder = new StatusBuilder(callback, reportBuilder);
statusBuilder.plan();
Exception ex = null;
try {
syncBreakpointsImpl(direction, statusBuilder);
} catch (RuntimeException e) {
ex = e;
} finally {
statusBuilder.done(ex);
}
}
private void syncBreakpointsImpl(final Direction direction, final StatusBuilder statusBuilder) {
// Collect the remote breakpoints.
Collection<? extends Breakpoint> sdkBreakpoints = readSdkBreakpoints(javascriptVm);
// Collect all local breakpoints.
Set<ChromiumLineBreakpoint> uiBreakpoints = getUiBreakpoints();
List<Breakpoint> sdkBreakpoints2 = new ArrayList<Breakpoint>(sdkBreakpoints.size());
if (direction != Direction.MERGE) {
breakpointInTargetMap.clear();
}
// Throw away all already linked breakpoints and put remaining into sdkBreakpoints2 list.
for (Breakpoint sdkBreakpoint : sdkBreakpoints) {
ChromiumLineBreakpoint uiBreakpoint = breakpointInTargetMap.getUiBreakpoint(sdkBreakpoint);
if (uiBreakpoint == null) {
// No mapping. Schedule for further processing.
sdkBreakpoints2.add(sdkBreakpoint);
} else {
// There is a live mapping. This set should also contain this breakpoint.
uiBreakpoints.remove(uiBreakpoint);
statusBuilder.getReportBuilder().increment(ReportBuilder.Property.LINKED);
}
}
// Sort all breakpoints by (script_name, line_number).
SortedBreakpoints<ChromiumLineBreakpoint> sortedUiBreakpoints =
sortBreakpoints(uiBreakpoints, uiBreakpointHandler);
SortedBreakpoints<Breakpoint> sortedSdkBreakpoints =
sortBreakpoints(sdkBreakpoints2, sdkBreakpointHandler);
BreakpointMerger breakpointMerger = new BreakpointMerger(direction, breakpointInTargetMap);
// Find all unlinked breakpoints on both sides.
mergeBreakpoints(breakpointMerger, sortedUiBreakpoints, sortedSdkBreakpoints);
List<Breakpoint> sdkBreakpointsToDelete;
List<Breakpoint> sdkBreakpointsToCreate;
List<ChromiumLineBreakpoint> uiBreakpointsToDelete;
List<ChromiumLineBreakpoint> uiBreakpointsToCreate;
// Plan actions for all breakpoints without pair.
if (direction == Direction.RESET_REMOTE) {
sdkBreakpointsToDelete = breakpointMerger.getMissingSdk();
sdkBreakpointsToCreate = Collections.emptyList();
} else {
sdkBreakpointsToCreate = breakpointMerger.getMissingSdk();
sdkBreakpointsToDelete = Collections.emptyList();
}
if (direction == Direction.RESET_LOCAL) {
uiBreakpointsToDelete = breakpointMerger.getMissingUi();
uiBreakpointsToCreate = Collections.emptyList();
} else {
uiBreakpointsToCreate = breakpointMerger.getMissingUi();
uiBreakpointsToDelete = Collections.emptyList();
}
// First delete everything, then create (we may need to re-create some breakpoints, so order
// is significant).
deteleBreakpoints(sdkBreakpointsToDelete, uiBreakpointsToDelete, statusBuilder);
createBreakpoints(sdkBreakpointsToCreate, uiBreakpointsToCreate, statusBuilder);
}
private void deteleBreakpoints(List<Breakpoint> sdkBreakpointsToDelete,
List<ChromiumLineBreakpoint> uiBreakpointsToDelete, final StatusBuilder statusBuilder) {
for (Breakpoint sdkBreakpoint : sdkBreakpointsToDelete) {
final PlannedTaskHelper deleteTaskHelper = new PlannedTaskHelper(statusBuilder);
JavascriptVm.BreakpointCallback callback = new JavascriptVm.BreakpointCallback() {
public void failure(String errorMessage) {
deleteTaskHelper.setException(new Exception(errorMessage));
}
public void success(Breakpoint breakpoint) {
statusBuilder.getReportBuilder().increment(ReportBuilder.Property.DELETED_ON_REMOTE);
}
};
sdkBreakpoint.clear(callback, deleteTaskHelper);
}
for (ChromiumLineBreakpoint uiBreakpoint : uiBreakpointsToDelete) {
ChromiumLineBreakpoint.getIgnoreList().add(uiBreakpoint);
try {
try {
uiBreakpoint.delete();
} catch (CoreException e) {
throw new RuntimeException(e);
}
} finally {
ChromiumLineBreakpoint.getIgnoreList().remove(uiBreakpoint);
}
statusBuilder.getReportBuilder().increment(ReportBuilder.Property.DELETED_LOCALLY);
}
}
private void createBreakpoints(List<Breakpoint> sdkBreakpointsToCreate,
List<ChromiumLineBreakpoint> uiBreakpointsToCreate, final StatusBuilder statusBuilder) {
IBreakpointManager breakpointManager = DebugPlugin.getDefault().getBreakpointManager();
for (Breakpoint sdkBreakpoint : sdkBreakpointsToCreate) {
Object sourceElement = sourceDirector.getSourceElement(sdkBreakpoint);
if (sourceElement instanceof IFile == false) {
continue;
}
// We do not actually support working files for scripts with offset.
int script_line_offset = 0;
IFile resource = (IFile) sourceElement;
ChromiumLineBreakpoint uiBreakpoint;
try {
uiBreakpoint = ChromiumLineBreakpoint.Helper.createLocal(sdkBreakpoint, breakpointManager,
resource, script_line_offset, debugModelId);
breakpointInTargetMap.add(sdkBreakpoint, uiBreakpoint);
} catch (CoreException e) {
throw new RuntimeException(e);
}
statusBuilder.getReportBuilder().increment(ReportBuilder.Property.CREATED_LOCALLY);
}
for (ChromiumLineBreakpoint uiBreakpoint : uiBreakpointsToCreate) {
VmResourceId vmResourceId = uiBreakpointHandler.getVmResourceId(uiBreakpoint);
if (vmResourceId == null) {
// Actually we should not get here, because getScript call succeeded before.
continue;
}
final PlannedTaskHelper createTaskHelper = new PlannedTaskHelper(statusBuilder);
BreakpointHelper.CreateCallback createCallback = new BreakpointHelper.CreateCallback() {
public void success() {
statusBuilder.getReportBuilder().increment(ReportBuilder.Property.CREATED_ON_REMOTE);
}
public void failure(Exception ex) {
createTaskHelper.setException(ex);
}
};
breakpointHelper.createBreakpointOnRemote(uiBreakpoint, vmResourceId, createCallback,
createTaskHelper);
}
}
private static class BreakpointMerger extends Merger<ChromiumLineBreakpoint, Breakpoint> {
private final Direction direction;
private final List<ChromiumLineBreakpoint> missingUi = new ArrayList<ChromiumLineBreakpoint>();
private final List<Breakpoint> missingSdk = new ArrayList<Breakpoint>();
private final BreakpointMap.InTargetMap breakpointMap;
BreakpointMerger(Direction direction, BreakpointMap.InTargetMap breakpointMap) {
this.direction = direction;
this.breakpointMap = breakpointMap;
}
@Override
void both(ChromiumLineBreakpoint v1, Breakpoint v2) {
if (direction == Direction.MERGE) {
breakpointMap.add(v2, v1);
} else {
onlyFirst(v1);
onlySecond(v2);
}
}
@Override
void onlyFirst(ChromiumLineBreakpoint v1) {
missingUi.add(v1);
}
@Override
void onlySecond(Breakpoint v2) {
missingSdk.add(v2);
}
List<ChromiumLineBreakpoint> getMissingUi() {
return missingUi;
}
List<Breakpoint> getMissingSdk() {
return missingSdk;
}
}
/**
* A class responsible for creating a summary status of synchronization operation. The status
* is created once all asynchronous jobs have finished. Each job first registers itself
* via {@link #plan()} method and
* later reports its result via {@link #done(Exception)} method.
* When the last job is reporting its finishing, the status gets built and sent to
* {@link #callback}. If no exceptions were registered,
* status contains text report from {@link ReportBuilder}.
*/
private static class StatusBuilder {
private final Callback callback;
private int plannedNumber = 0;
private final List<Exception> exceptions = new ArrayList<Exception>(0);
private boolean alreadyReported = false;
private final ReportBuilder reportBuilder;
StatusBuilder(Callback callback, ReportBuilder reportBuilder) {
this.callback = callback;
this.reportBuilder = reportBuilder;
}
ReportBuilder getReportBuilder() {
return reportBuilder;
}
public synchronized void plan() {
if (alreadyReported) {
throw new IllegalStateException();
}
plannedNumber++;
}
public void done(Exception ex) {
boolean timeToReport = doneImpl(ex);
if (timeToReport) {
reportResult();
}
}
private synchronized boolean doneImpl(Exception ex) {
if (ex != null) {
exceptions.add(ex);
}
plannedNumber--;
if (plannedNumber == 0) {
if (!alreadyReported) {
alreadyReported = true;
return true;
}
}
return false;
}
private void reportResult() {
IStatus status;
if (exceptions.isEmpty()) {
status = new Status(IStatus.OK, ChromiumDebugPlugin.PLUGIN_ID,
"Breakpoint synchronization done: " + reportBuilder.build(), null); //$NON-NLS-1$
} else {
IStatus[] subStatuses = new IStatus[exceptions.size()];
for (int i = 0 ; i < subStatuses.length; i++) {
subStatuses[i] = new Status(IStatus.ERROR, ChromiumDebugPlugin.PLUGIN_ID,
exceptions.get(i).getMessage(), exceptions.get(i));
}
status = new MultiStatus(ChromiumDebugPlugin.PLUGIN_ID, IStatus.ERROR, subStatuses,
"Breakpoint synchronization errors", null); //$NON-NLS-1$
}
if (callback != null) {
callback.onDone(status);
}
}
}
private static class PlannedTaskHelper implements SyncCallback {
private final StatusBuilder statusBuilder;
private volatile Exception exception = null;
PlannedTaskHelper(StatusBuilder statusBuilder) {
this.statusBuilder = statusBuilder;
statusBuilder.plan();
}
public void callbackDone(RuntimeException e) {
if (e != null) {
exception = e;
}
statusBuilder.done(exception);
}
void setException(Exception ex) {
exception = ex;
}
}
/**
* A class that contains several conunters.
*/
private static class ReportBuilder {
enum Property {
LINKED,
CREATED_LOCALLY,
DELETED_LOCALLY,
CREATED_ON_REMOTE,
DELETED_ON_REMOTE;
String getVisibleName() {
return toString();
}
}
private final Direction direction;
private final Map<Property, AtomicInteger> counters;
ReportBuilder(Direction direction) {
this.direction = direction;
counters = new EnumMap<Property, AtomicInteger>(Property.class);
for (Property property : Property.class.getEnumConstants()) {
counters.put(property, new AtomicInteger(0));
}
}
public void increment(Property property) {
counters.get(property).addAndGet(1);
}
public String build() {
StringBuilder builder = new StringBuilder();
builder.append("direction=").append(direction); //$NON-NLS-1$
for (Map.Entry<Property, AtomicInteger> en : counters.entrySet()) {
int number = en.getValue().get();
if (number == 0) {
continue;
}
builder.append(" ").append(en.getKey().getVisibleName()); //$NON-NLS-1$
builder.append("=").append(number); //$NON-NLS-1$
}
return builder.toString();
}
}
/**
* A handler for properties of breakpoint type B that helps reading them.
*/
private static abstract class PropertyHandler<B> {
/** @return vm resource name or null */
abstract VmResourceId getVmResourceId(B breakpoint);
/** @return 0-based number */
abstract long getLineNumber(B breakpoint);
}
private final PropertyHandler<ChromiumLineBreakpoint> uiBreakpointHandler =
new PropertyHandler<ChromiumLineBreakpoint>() {
@Override
long getLineNumber(ChromiumLineBreakpoint chromiumLineBreakpoint) {
int lineNumber;
try {
// TODO(peter.rybin): Consider supporting inline scripts here.
return chromiumLineBreakpoint.getLineNumber() - 1;
} catch (CoreException e) {
throw new RuntimeException(e);
}
}
@Override
VmResourceId getVmResourceId(ChromiumLineBreakpoint chromiumLineBreakpoint) {
IMarker marker = chromiumLineBreakpoint.getMarker();
if (marker == null) {
return null;
}
IResource resource = marker.getResource();
if (resource instanceof IFile == false) {
return null;
}
IFile file = (IFile) resource;
try {
return sourceDirector.getReverseSourceLookup().findVmResource(file);
} catch (CoreException e) {
throw new RuntimeException("Failed to read script name from breakpoint", e); //$NON-NLS-1$
}
}
};
private static final PropertyHandler<Breakpoint> sdkBreakpointHandler =
new PropertyHandler<Breakpoint>() {
@Override
long getLineNumber(Breakpoint breakpoint) {
return breakpoint.getLineNumber();
}
@Override
VmResourceId getVmResourceId(Breakpoint breakpoint) {
if (breakpoint.getType() == Breakpoint.Type.SCRIPT_NAME) {
return VmResourceId.forName(breakpoint.getScriptName());
} else {
Long scriptId = breakpoint.getScriptId();
if (scriptId == null) {
return null;
}
return VmResourceId.forId(scriptId);
}
}
};
/**
* A helping structure that holds field of complicated type.
*/
private static class SortedBreakpoints<B> {
final Map<VmResourceId, Map<Long, B>> data;
SortedBreakpoints(Map<VmResourceId, Map<Long, B>> data) {
this.data = data;
}
}
/**
* Put all breakpoints into map script_name -> line_number -> breakpoint.
*/
private static <B> SortedBreakpoints<B> sortBreakpoints(Collection<? extends B> breakpoints,
PropertyHandler<B> handler) {
Map<VmResourceId, Map<Long, B>> result = new HashMap<VmResourceId, Map<Long, B>>();
for (B breakpoint : breakpoints) {
VmResourceId vmResourceId = handler.getVmResourceId(breakpoint);
if (vmResourceId == null) {
continue;
}
Map<Long, B> subMap = result.get(vmResourceId);
if (subMap == null) {
subMap = new HashMap<Long, B>(3);
result.put(vmResourceId, subMap);
}
long line = handler.getLineNumber(breakpoint);
// For simplicity we ignore multiple breakpoints on the same line.
subMap.put(line, breakpoint);
}
return new SortedBreakpoints<B>(result);
}
/**
* A class that implements merge operation for a particular complete/incomplete pair of values.
*/
private static abstract class Merger<V1, V2> {
abstract void onlyFirst(V1 v1);
abstract void onlySecond(V2 v2);
abstract void both(V1 v1, V2 v2);
}
/**
* Merges values of 2 maps.
* @param map2 must implement {@link Map#remove} method.
*/
private static <K, V1, V2> void mergeMaps(Map<K, V1> map1, Map<K, V2> map2,
Merger<V1, V2> merger) {
for (Map.Entry<K, V1> en : map1.entrySet()) {
V2 v2 = map2.remove(en.getKey());
if (v2 == null) {
merger.onlyFirst(en.getValue());
} else {
merger.both(en.getValue(), v2);
}
}
for (V2 v2 : map2.values()) {
merger.onlySecond(v2);
}
}
private static <B1, B2> void mergeBreakpoints(final Merger<B1, B2> perBreakpointMerger,
SortedBreakpoints<B1> side1, SortedBreakpoints<B2> side2) {
Merger<Map<Long, B1>, Map<Long, B2>> perScriptMerger =
new Merger<Map<Long,B1>, Map<Long,B2>>() {
@Override
void both(Map<Long, B1> v1, Map<Long, B2> v2) {
mergeMaps(v1, v2, perBreakpointMerger);
}
@Override
void onlyFirst(Map<Long, B1> v1) {
mergeMaps(v1, Collections.<Long, B2>emptyMap(), perBreakpointMerger);
}
@Override
void onlySecond(Map<Long, B2> v2) {
mergeMaps(Collections.<Long, B1>emptyMap(), v2, perBreakpointMerger);
}
};
mergeMaps(side1.data, side2.data, perScriptMerger);
}
private static Collection<? extends Breakpoint> readSdkBreakpoints(JavascriptVm javascriptVm) {
class CallbackImpl implements JavascriptVm.ListBreakpointsCallback {
public void failure(Exception exception) {
problem = exception;
}
public void success(Collection<? extends Breakpoint> breakpoints) {
result = breakpoints;
}
Collection<? extends Breakpoint> getResult() {
if (problem != null) {
throw new RuntimeException("Failed to synchronize breakpoints", problem); //$NON-NLS-1$
}
return result;
}
Exception problem = null;
Collection<? extends Breakpoint> result = null;
}
CallbackImpl callback = new CallbackImpl();
CallbackSemaphore callbackSemaphore = new CallbackSemaphore();
javascriptVm.listBreakpoints(callback, callbackSemaphore);
boolean res = callbackSemaphore.tryAcquireDefault();
if (!res) {
throw new RuntimeException("Timeout"); //$NON-NLS-1$
}
return callback.getResult();
}
// We need this method to return Set for future purposes.
private Set<ChromiumLineBreakpoint> getUiBreakpoints() {
IBreakpointManager breakpointManager = DebugPlugin.getDefault().getBreakpointManager();
Set<ChromiumLineBreakpoint> result = new HashSet<ChromiumLineBreakpoint>();
for (IBreakpoint breakpoint: breakpointManager.getBreakpoints(debugModelId)) {
if (breakpoint instanceof ChromiumLineBreakpoint == false) {
continue;
}
ChromiumLineBreakpoint chromiumLineBreakpoint = (ChromiumLineBreakpoint) breakpoint;
result.add(chromiumLineBreakpoint);
}
return result;
}
public static class ProtocolNotSupportedOnRemote extends Exception {
ProtocolNotSupportedOnRemote() {
}
ProtocolNotSupportedOnRemote(String message, Throwable cause) {
super(message, cause);
}
ProtocolNotSupportedOnRemote(String message) {
super(message);
}
ProtocolNotSupportedOnRemote(Throwable cause) {
super(cause);
}
}
}