org.teavm.debugging.Debugger Maven / Gradle / Ivy
/*
* Copyright 2014 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.debugging;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.LinkedBlockingQueue;
import org.teavm.debugging.information.*;
import org.teavm.debugging.javascript.*;
import org.teavm.model.MethodReference;
/**
*
* @author Alexey Andreev
*/
public class Debugger {
private static final Object dummyObject = new Object();
private ConcurrentMap listeners = new ConcurrentHashMap<>();
private JavaScriptDebugger javaScriptDebugger;
private DebugInformationProvider debugInformationProvider;
private BlockingQueue temporaryBreakpoints = new LinkedBlockingQueue<>();
private ConcurrentMap debugInformationMap = new ConcurrentHashMap<>();
private ConcurrentMap> debugInformationFileMap =
new ConcurrentHashMap<>();
private ConcurrentMap scriptMap = new ConcurrentHashMap<>();
ConcurrentMap breakpointMap = new ConcurrentHashMap<>();
ConcurrentMap breakpoints = new ConcurrentHashMap<>();
private volatile CallFrame[] callStack;
public Debugger(JavaScriptDebugger javaScriptDebugger, DebugInformationProvider debugInformationProvider) {
this.javaScriptDebugger = javaScriptDebugger;
this.debugInformationProvider = debugInformationProvider;
javaScriptDebugger.addListener(javaScriptListener);
}
public JavaScriptDebugger getJavaScriptDebugger() {
return javaScriptDebugger;
}
public void addListener(DebuggerListener listener) {
listeners.put(listener, dummyObject);
}
public void removeListener(DebuggerListener listener) {
listeners.remove(listener);
}
public void suspend() {
javaScriptDebugger.suspend();
}
public void resume() {
javaScriptDebugger.resume();
}
public void stepInto() {
step(true);
}
public void stepOut() {
javaScriptDebugger.stepOut();
}
public void stepOver() {
step(false);
}
private void jsStep(boolean enterMethod) {
if (enterMethod) {
javaScriptDebugger.stepInto();
} else {
javaScriptDebugger.stepOver();
}
}
private void step(boolean enterMethod) {
CallFrame[] callStack = getCallStack();
if (callStack == null || callStack.length == 0) {
jsStep(enterMethod);
return;
}
CallFrame recentFrame = callStack[0];
if (recentFrame.getLocation() == null || recentFrame.getLocation().getFileName() == null
|| recentFrame.getLocation().getLine() < 0) {
jsStep(enterMethod);
return;
}
Set successors = new HashSet<>();
for (CallFrame frame : callStack) {
boolean exits;
String script = frame.getOriginalLocation().getScript();
DebugInformation debugInfo = debugInformationMap.get(script);
if (frame.getLocation() != null && frame.getLocation().getFileName() != null
&& frame.getLocation().getLine() >= 0 && debugInfo != null) {
exits = addFollowing(debugInfo, frame.getLocation(), script, new HashSet<>(), successors);
if (enterMethod) {
CallSiteSuccessorFinder successorFinder = new CallSiteSuccessorFinder(debugInfo, script,
successors);
DebuggerCallSite[] callSites = debugInfo.getCallSites(frame.getLocation());
for (DebuggerCallSite callSite : callSites) {
callSite.acceptVisitor(successorFinder);
}
}
} else {
exits = true;
}
if (!exits) {
break;
}
enterMethod = true;
}
for (JavaScriptLocation successor : successors) {
temporaryBreakpoints.add(javaScriptDebugger.createBreakpoint(successor));
}
javaScriptDebugger.resume();
}
private static class CallSiteSuccessorFinder implements DebuggerCallSiteVisitor {
private DebugInformation debugInfo;
private String script;
Set locations;
public CallSiteSuccessorFinder(DebugInformation debugInfo, String script, Set locations) {
this.debugInfo = debugInfo;
this.script = script;
this.locations = locations;
}
@Override
public void visit(DebuggerVirtualCallSite callSite) {
for (MethodReference potentialMethod : debugInfo.getOverridingMethods(callSite.getMethod())) {
for (GeneratedLocation loc : debugInfo.getMethodEntrances(potentialMethod)) {
loc = debugInfo.getStatementLocation(loc);
locations.add(new JavaScriptLocation(script, loc.getLine(), loc.getColumn()));
}
}
}
@Override
public void visit(DebuggerStaticCallSite callSite) {
for (GeneratedLocation loc : debugInfo.getMethodEntrances(callSite.getMethod())) {
loc = debugInfo.getStatementLocation(loc);
locations.add(new JavaScriptLocation(script, loc.getLine(), loc.getColumn()));
}
}
}
private boolean addFollowing(DebugInformation debugInfo, SourceLocation location, String script,
Set visited, Set successors) {
if (!visited.add(location)) {
return false;
}
SourceLocation[] following = debugInfo.getFollowingLines(location);
boolean exits = false;
if (following != null) {
for (SourceLocation successor : following) {
if (successor == null) {
exits = true;
} else {
Collection genLocations = debugInfo.getGeneratedLocations(successor);
if (!genLocations.isEmpty()) {
for (GeneratedLocation loc : genLocations) {
loc = debugInfo.getStatementLocation(loc);
successors.add(new JavaScriptLocation(script, loc.getLine(), loc.getColumn()));
}
} else {
exits |= addFollowing(debugInfo, successor, script, visited, successors);
}
}
}
}
return exits;
}
private List debugInformationBySource(String sourceFile) {
Map list = debugInformationFileMap.get(sourceFile);
return list != null ? new ArrayList<>(list.keySet()) : Collections.emptyList();
}
public void continueToLocation(SourceLocation location) {
continueToLocation(location.getFileName(), location.getLine());
}
public void continueToLocation(String fileName, int line) {
if (!javaScriptDebugger.isSuspended()) {
return;
}
for (DebugInformation debugInformation : debugInformationBySource(fileName)) {
Collection locations = debugInformation.getGeneratedLocations(fileName, line);
for (GeneratedLocation location : locations) {
JavaScriptLocation jsLocation = new JavaScriptLocation(scriptMap.get(debugInformation),
location.getLine(), location.getColumn());
JavaScriptBreakpoint jsBreakpoint = javaScriptDebugger.createBreakpoint(jsLocation);
if (jsBreakpoint != null) {
temporaryBreakpoints.add(jsBreakpoint);
}
}
}
javaScriptDebugger.resume();
}
public boolean isSuspended() {
return javaScriptDebugger.isSuspended();
}
public Breakpoint createBreakpoint(String file, int line) {
return createBreakpoint(new SourceLocation(file, line));
}
public Breakpoint createBreakpoint(SourceLocation location) {
synchronized (breakpointMap) {
Breakpoint breakpoint = new Breakpoint(this, location);
breakpoints.put(breakpoint, dummyObject);
updateInternalBreakpoints(breakpoint);
updateBreakpointStatus(breakpoint, false);
return breakpoint;
}
}
public Set getBreakpoints() {
return new HashSet<>(breakpoints.keySet());
}
void updateInternalBreakpoints(Breakpoint breakpoint) {
if (breakpoint.isDestroyed()) {
return;
}
for (JavaScriptBreakpoint jsBreakpoint : breakpoint.jsBreakpoints) {
breakpointMap.remove(jsBreakpoint);
jsBreakpoint.destroy();
}
List jsBreakpoints = new ArrayList<>();
SourceLocation location = breakpoint.getLocation();
for (DebugInformation debugInformation : debugInformationBySource(location.getFileName())) {
Collection locations = debugInformation.getGeneratedLocations(location);
for (GeneratedLocation genLocation : locations) {
JavaScriptLocation jsLocation = new JavaScriptLocation(scriptMap.get(debugInformation),
genLocation.getLine(), genLocation.getColumn());
JavaScriptBreakpoint jsBreakpoint = javaScriptDebugger.createBreakpoint(jsLocation);
jsBreakpoints.add(jsBreakpoint);
breakpointMap.put(jsBreakpoint, breakpoint);
}
}
breakpoint.jsBreakpoints = jsBreakpoints;
}
private DebuggerListener[] getListeners() {
return listeners.keySet().toArray(new DebuggerListener[0]);
}
void updateBreakpointStatus(Breakpoint breakpoint, boolean fireEvent) {
boolean valid = false;
for (JavaScriptBreakpoint jsBreakpoint : breakpoint.jsBreakpoints) {
if (jsBreakpoint.isValid()) {
valid = true;
}
}
if (breakpoint.valid != valid) {
breakpoint.valid = valid;
if (fireEvent) {
for (DebuggerListener listener : getListeners()) {
listener.breakpointStatusChanged(breakpoint);
}
}
}
}
public CallFrame[] getCallStack() {
if (!isSuspended()) {
return null;
}
if (callStack == null) {
// TODO: with inlining enabled we can have several JVM methods compiled into one JavaScript function
// so we must consider this case.
List frames = new ArrayList<>();
boolean wasEmpty = false;
for (JavaScriptCallFrame jsFrame : javaScriptDebugger.getCallStack()) {
DebugInformation debugInformation = debugInformationMap.get(jsFrame.getLocation().getScript());
SourceLocation loc;
if (debugInformation != null) {
loc = debugInformation.getSourceLocation(jsFrame.getLocation().getLine(),
jsFrame.getLocation().getColumn());
} else {
loc = null;
}
boolean empty = loc == null || (loc.getFileName() == null && loc.getLine() < 0);
MethodReference method = !empty ? debugInformation.getMethodAt(jsFrame.getLocation().getLine(),
jsFrame.getLocation().getColumn()) : null;
if (!empty || !wasEmpty) {
VariableMap vars = new VariableMap(jsFrame.getVariables(), this, jsFrame.getLocation());
frames.add(new CallFrame(this, jsFrame, loc, method, vars));
}
wasEmpty = empty;
}
callStack = frames.toArray(new CallFrame[0]);
}
return callStack.clone();
}
private void addScript(String name) {
if (debugInformationMap.containsKey(name)) {
updateBreakpoints();
return;
}
DebugInformation debugInfo = debugInformationProvider.getDebugInformation(name);
if (debugInfo == null) {
updateBreakpoints();
return;
}
if (debugInformationMap.putIfAbsent(name, debugInfo) != null) {
updateBreakpoints();
return;
}
for (String sourceFile : debugInfo.getFilesNames()) {
ConcurrentMap list = debugInformationFileMap.get(sourceFile);
if (list == null) {
list = new ConcurrentHashMap<>();
ConcurrentMap existing = debugInformationFileMap.putIfAbsent(
sourceFile, list);
if (existing != null) {
list = existing;
}
}
list.put(debugInfo, dummyObject);
}
scriptMap.put(debugInfo, name);
updateBreakpoints();
}
private void updateBreakpoints() {
synchronized (breakpointMap) {
for (Breakpoint breakpoint : breakpoints.keySet()) {
updateInternalBreakpoints(breakpoint);
updateBreakpointStatus(breakpoint, true);
}
}
}
public boolean isAttached() {
return javaScriptDebugger.isAttached();
}
public void detach() {
javaScriptDebugger.detach();
}
void destroyBreakpoint(Breakpoint breakpoint) {
for (JavaScriptBreakpoint jsBreakpoint : breakpoint.jsBreakpoints) {
jsBreakpoint.destroy();
breakpointMap.remove(jsBreakpoint);
}
breakpoint.jsBreakpoints = new ArrayList<>();
breakpoints.remove(breakpoint);
}
private void fireResumed() {
List temporaryBreakpoints = new ArrayList<>();
this.temporaryBreakpoints.drainTo(temporaryBreakpoints);
for (JavaScriptBreakpoint jsBreakpoint : temporaryBreakpoints) {
jsBreakpoint.destroy();
}
for (DebuggerListener listener : getListeners()) {
listener.resumed();
}
}
private void fireAttached() {
synchronized (breakpointMap) {
for (Breakpoint breakpoint : breakpoints.keySet()) {
updateInternalBreakpoints(breakpoint);
updateBreakpointStatus(breakpoint, false);
}
}
for (DebuggerListener listener : getListeners()) {
listener.attached();
}
}
private void fireDetached() {
for (Breakpoint breakpoint : breakpoints.keySet()) {
updateBreakpointStatus(breakpoint, false);
}
for (DebuggerListener listener : getListeners()) {
listener.detached();
}
}
private void fireBreakpointChanged(JavaScriptBreakpoint jsBreakpoint) {
Breakpoint breakpoint = breakpointMap.get(jsBreakpoint);
if (breakpoint != null) {
updateBreakpointStatus(breakpoint, true);
}
}
String[] mapVariable(String variable, JavaScriptLocation location) {
DebugInformation debugInfo = debugInformationMap.get(location.getScript());
if (debugInfo == null) {
return new String[0];
}
return debugInfo.getVariableMeaningAt(location.getLine(), location.getColumn(), variable);
}
String mapField(String className, String jsField) {
for (DebugInformation debugInfo : debugInformationMap.values()) {
String meaning = debugInfo.getFieldMeaning(className, jsField);
if (meaning != null) {
return meaning;
}
}
return null;
}
private JavaScriptDebuggerListener javaScriptListener = new JavaScriptDebuggerListener() {
@Override
public void resumed() {
fireResumed();
}
@Override
public void paused() {
callStack = null;
for (DebuggerListener listener : getListeners()) {
listener.paused();
}
}
@Override
public void scriptAdded(String name) {
addScript(name);
}
@Override
public void attached() {
fireAttached();
}
@Override
public void detached() {
fireDetached();
}
@Override
public void breakpointChanged(JavaScriptBreakpoint jsBreakpoint) {
fireBreakpointChanged(jsBreakpoint);
}
};
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy