Wasm: working on Chrome RDP debugger

This commit is contained in:
Alexey Andreev 2022-11-29 21:30:41 +01:00
parent 9d3927e196
commit a2715f2c79
18 changed files with 497 additions and 312 deletions

View File

@ -22,14 +22,20 @@ import java.util.List;
import org.teavm.common.CollectionUtil;
public class LineInfo {
private int offset;
private LineInfoSequence[] sequences;
private List<? extends LineInfoSequence> sequenceList;
public LineInfo(LineInfoSequence[] sequences) {
public LineInfo(int offset, LineInfoSequence[] sequences) {
this.offset = offset;
this.sequences = sequences.clone();
sequenceList = Collections.unmodifiableList(Arrays.asList(this.sequences));
}
public int offset() {
return offset;
}
public List<? extends LineInfoSequence> sequences() {
return sequenceList;
}

View File

@ -93,6 +93,9 @@ public class DebugInfoParser {
if (sectionCode == 0) {
return parseSection(pos + sectionSize);
} else {
if (sectionCode == 3) {
lines.setOffset(pos);
}
return skip(sectionSize, "Error skipping section " + sectionCode + " of size " + sectionSize);
}
});

View File

@ -42,6 +42,7 @@ public class DebugLinesParser extends DebugSectionParser {
private int address;
private MethodInfo currentMethod;
private int sequenceStartAddress;
private int offset;
public DebugLinesParser(
DebugFileParser files,
@ -56,8 +57,13 @@ public class DebugLinesParser extends DebugSectionParser {
return lineInfo;
}
public void setOffset(int offset) {
this.offset = offset;
}
@Override
protected void doParse() {
address = offset;
while (ptr < data.length) {
var cmd = data[ptr++] & 0xFF;
switch (cmd) {
@ -85,7 +91,7 @@ public class DebugLinesParser extends DebugSectionParser {
break;
}
}
lineInfo = new LineInfo(sequences.toArray(new LineInfoSequence[0]));
lineInfo = new LineInfo(offset, sequences.toArray(new LineInfoSequence[0]));
sequences = null;
commands = null;
stateStack = null;

View File

@ -35,7 +35,7 @@ public final class CollectionUtil {
while (true) {
var i = (l + u) / 2;
var t = keyExtractor.apply(list.get(i));
var cmp = comparator.compare(t, key);
var cmp = comparator.compare(key, t);
if (cmp == 0) {
return i;
} else if (cmp > 0) {

View File

@ -15,6 +15,7 @@
*/
package org.teavm.debugging;
import java.util.Collections;
import java.util.Map;
import org.teavm.common.Promise;
import org.teavm.debugging.information.DebugInformation;
@ -62,7 +63,11 @@ public class CallFrame {
public Promise<Map<String, Variable>> getVariables() {
if (variables == null) {
if (debugInformation != null) {
variables = debugger.createVariables(originalCallFrame, debugInformation);
} else {
variables = Promise.of(Collections.emptyMap());
}
}
return variables;
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2022 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 org.teavm.backend.wasm.debug.parser.DebugInfoParser;
import org.teavm.backend.wasm.debug.parser.DebugInfoReader;
import org.teavm.common.CompletablePromise;
import org.teavm.common.Promise;
class DebugInfoReaderImpl implements DebugInfoReader {
private byte[] data;
private int ptr;
private CompletablePromise<Integer> promise;
private byte[] target;
private int offset;
private int count;
DebugInfoReaderImpl(byte[] data) {
this.data = data;
}
DebugInfoParser read() {
var debugInfoParser = new DebugInfoParser(this);
Promise.runNow(() -> {
debugInfoParser.parse().catchVoid(Throwable::printStackTrace);
complete();
});
return debugInfoParser;
}
@Override
public Promise<Integer> skip(int amount) {
promise = new CompletablePromise<>();
count = amount;
return promise;
}
@Override
public Promise<Integer> read(byte[] buffer, int offset, int count) {
promise = new CompletablePromise<>();
this.target = buffer;
this.offset = offset;
this.count = count;
return promise;
}
private void complete() {
while (promise != null) {
var p = promise;
count = Math.min(count, data.length - ptr);
promise = null;
if (target != null) {
System.arraycopy(data, ptr, target, offset, count);
target = null;
}
ptr += count;
if (count == 0) {
count = -1;
}
p.complete(count);
}
}
}

View File

@ -16,6 +16,7 @@
package org.teavm.debugging;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@ -24,10 +25,12 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.teavm.backend.wasm.debug.info.LineInfo;
import org.teavm.backend.wasm.debug.info.LineInfoFileCommand;
import org.teavm.backend.wasm.debug.info.MethodInfo;
import org.teavm.common.Promise;
import org.teavm.debugging.information.DebugInformation;
import org.teavm.debugging.information.DebugInformationProvider;
import org.teavm.debugging.information.DebuggerCallSite;
import org.teavm.debugging.information.DebuggerCallSiteVisitor;
import org.teavm.debugging.information.DebuggerStaticCallSite;
import org.teavm.debugging.information.DebuggerVirtualCallSite;
@ -38,22 +41,28 @@ import org.teavm.debugging.javascript.JavaScriptCallFrame;
import org.teavm.debugging.javascript.JavaScriptDebugger;
import org.teavm.debugging.javascript.JavaScriptDebuggerListener;
import org.teavm.debugging.javascript.JavaScriptLocation;
import org.teavm.debugging.javascript.JavaScriptScript;
import org.teavm.debugging.javascript.JavaScriptVariable;
import org.teavm.model.MethodReference;
import org.teavm.model.ValueType;
public class Debugger {
private Set<DebuggerListener> listeners = new LinkedHashSet<>();
private JavaScriptDebugger javaScriptDebugger;
private DebugInformationProvider debugInformationProvider;
private List<JavaScriptBreakpoint> temporaryBreakpoints = new ArrayList<>();
private Map<String, DebugInformation> debugInformationMap = new HashMap<>();
private Map<JavaScriptScript, DebugInformation> debugInformationMap = new HashMap<>();
private Map<String, Set<DebugInformation>> debugInformationFileMap = new HashMap<>();
private Map<DebugInformation, String> scriptMap = new HashMap<>();
private Map<JavaScriptScript, LineInfo> wasmLineInfoMap = new HashMap<>();
private Map<LineInfo, JavaScriptScript> wasmScriptMap = new HashMap<>();
private Map<String, Set<LineInfo>> wasmInfoFileMap = new HashMap<>();
private Map<DebugInformation, JavaScriptScript> scriptMap = new HashMap<>();
private Map<JavaScriptBreakpoint, Breakpoint> breakpointMap = new HashMap<>();
private Set<Breakpoint> breakpoints = new LinkedHashSet<>();
private Set<? extends Breakpoint> readonlyBreakpoints = Collections.unmodifiableSet(breakpoints);
private CallFrame[] callStack;
private Set<String> scriptNames = new LinkedHashSet<>();
private Set<String> allSourceFiles = new LinkedHashSet<>();
public Debugger(JavaScriptDebugger javaScriptDebugger, DebugInformationProvider debugInformationProvider) {
this.javaScriptDebugger = javaScriptDebugger;
@ -98,29 +107,57 @@ public class Debugger {
}
private Promise<Void> step(boolean enterMethod) {
CallFrame[] callStack = getCallStack();
var callStack = getCallStack();
if (callStack == null || callStack.length == 0) {
return jsStep(enterMethod);
}
CallFrame recentFrame = callStack[0];
var recentFrame = callStack[0];
if (recentFrame.getLocation() == null || recentFrame.getLocation().getFileName() == null
|| recentFrame.getLocation().getLine() < 0) {
return jsStep(enterMethod);
}
Set<JavaScriptLocation> successors = new HashSet<>();
var successors = new HashSet<JavaScriptLocation>();
boolean first = true;
for (CallFrame frame : callStack) {
loop:
for (var frame : callStack) {
var script = frame.getOriginalLocation().getScript();
switch (script.getLanguage()) {
case JS:
if (!addJsBreakpoints(frame, script, enterMethod, first, successors)) {
break loop;
}
break;
case WASM: {
var info = wasmLineInfoMap.get(script);
if (info != null) {
return enterMethod ? javaScriptDebugger.stepInto() : javaScriptDebugger.stepOver();
}
break;
}
}
enterMethod = false;
first = false;
}
var jsBreakpointPromises = new ArrayList<Promise<Void>>();
for (var successor : successors) {
jsBreakpointPromises.add(javaScriptDebugger.createBreakpoint(successor)
.thenVoid(temporaryBreakpoints::add));
}
return Promise.allVoid(jsBreakpointPromises).thenAsync(v -> javaScriptDebugger.resume());
}
private boolean addJsBreakpoints(CallFrame frame, JavaScriptScript script, boolean enterMethod, boolean first,
Set<JavaScriptLocation> successors) {
var debugInfo = debugInformationMap.get(script);
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) {
var successorFinder = new CallSiteSuccessorFinder(debugInfo, script, successors);
var callSites = debugInfo.getCallSites(frame.getLocation());
for (var callSite : callSites) {
callSite.acceptVisitor(successorFinder);
}
}
@ -128,31 +165,23 @@ public class Debugger {
exits = true;
}
if (!exits) {
break;
return false;
}
enterMethod = false;
if (!first && frame.getLocation() != null) {
for (GeneratedLocation location : debugInfo.getGeneratedLocations(frame.getLocation())) {
for (var location : debugInfo.getGeneratedLocations(frame.getLocation())) {
successors.add(new JavaScriptLocation(script, location.getLine(), location.getColumn()));
}
}
first = false;
}
List<Promise<Void>> jsBreakpointPromises = new ArrayList<>();
for (JavaScriptLocation successor : successors) {
jsBreakpointPromises.add(javaScriptDebugger.createBreakpoint(successor)
.thenVoid(temporaryBreakpoints::add));
}
return Promise.allVoid(jsBreakpointPromises).thenAsync(v -> javaScriptDebugger.resume());
return false;
}
static class CallSiteSuccessorFinder implements DebuggerCallSiteVisitor {
private DebugInformation debugInfo;
private String script;
private JavaScriptScript script;
Set<JavaScriptLocation> locations;
CallSiteSuccessorFinder(DebugInformation debugInfo, String script, Set<JavaScriptLocation> locations) {
CallSiteSuccessorFinder(DebugInformation debugInfo, JavaScriptScript script,
Set<JavaScriptLocation> locations) {
this.debugInfo = debugInfo;
this.script = script;
this.locations = locations;
@ -177,7 +206,7 @@ public class Debugger {
}
}
private boolean addFollowing(DebugInformation debugInfo, SourceLocation location, String script,
private boolean addFollowing(DebugInformation debugInfo, SourceLocation location, JavaScriptScript script,
Set<SourceLocation> visited, Set<JavaScriptLocation> successors) {
if (!visited.add(location)) {
return false;
@ -205,7 +234,12 @@ public class Debugger {
}
private List<DebugInformation> debugInformationBySource(String sourceFile) {
Set<DebugInformation> list = debugInformationFileMap.get(sourceFile);
var list = debugInformationFileMap.get(sourceFile);
return list != null ? new ArrayList<>(list) : Collections.emptyList();
}
private List<LineInfo> wasmLineInfoBySource(String sourceFile) {
var list = wasmInfoFileMap.get(sourceFile);
return list != null ? new ArrayList<>(list) : Collections.emptyList();
}
@ -222,7 +256,7 @@ public class Debugger {
for (DebugInformation debugInformation : debugInformationBySource(fileName)) {
Collection<GeneratedLocation> locations = debugInformation.getGeneratedLocations(fileName, line);
for (GeneratedLocation location : locations) {
JavaScriptLocation jsLocation = new JavaScriptLocation(scriptMap.get(debugInformation),
var jsLocation = new JavaScriptLocation(scriptMap.get(debugInformation),
location.getLine(), location.getColumn());
promises.add(javaScriptDebugger.createBreakpoint(jsLocation).thenVoid(temporaryBreakpoints::add));
}
@ -239,11 +273,11 @@ public class Debugger {
}
public Collection<? extends String> getSourceFiles() {
return debugInformationFileMap.keySet();
return allSourceFiles;
}
public Promise<Breakpoint> createBreakpoint(SourceLocation location) {
Breakpoint breakpoint = new Breakpoint(this, location);
var breakpoint = new Breakpoint(this, location);
breakpoints.add(breakpoint);
return updateInternalBreakpoints(breakpoint).then(v -> {
updateBreakpointStatus(breakpoint, false);
@ -260,18 +294,18 @@ public class Debugger {
return Promise.VOID;
}
List<Promise<Void>> promises = new ArrayList<>();
for (JavaScriptBreakpoint jsBreakpoint : breakpoint.jsBreakpoints) {
var promises = new ArrayList<Promise<Void>>();
for (var jsBreakpoint : breakpoint.jsBreakpoints) {
breakpointMap.remove(jsBreakpoint);
promises.add(jsBreakpoint.destroy());
}
List<JavaScriptBreakpoint> jsBreakpoints = new ArrayList<>();
SourceLocation location = breakpoint.getLocation();
for (DebugInformation debugInformation : debugInformationBySource(location.getFileName())) {
Collection<GeneratedLocation> locations = debugInformation.getGeneratedLocations(location);
for (GeneratedLocation genLocation : locations) {
JavaScriptLocation jsLocation = new JavaScriptLocation(scriptMap.get(debugInformation),
var jsBreakpoints = new ArrayList<JavaScriptBreakpoint>();
var location = breakpoint.getLocation();
for (var debugInformation : debugInformationBySource(location.getFileName())) {
var locations = debugInformation.getGeneratedLocations(location);
for (var genLocation : locations) {
var jsLocation = new JavaScriptLocation(scriptMap.get(debugInformation),
genLocation.getLine(), genLocation.getColumn());
promises.add(javaScriptDebugger.createBreakpoint(jsLocation).thenVoid(jsBreakpoint -> {
jsBreakpoints.add(jsBreakpoint);
@ -279,11 +313,27 @@ public class Debugger {
}));
}
}
for (var wasmLineInfo : wasmLineInfoBySource(location.getFileName())) {
for (var sequence : wasmLineInfo.sequences()) {
for (var loc : sequence.unpack().locations()) {
if (loc.location().line() == location.getLine()
&& loc.location().file().fullName().equals(location.getFileName())) {
var jsLocation = new JavaScriptLocation(wasmScriptMap.get(wasmLineInfo),
0, loc.address());
promises.add(javaScriptDebugger.createBreakpoint(jsLocation).thenVoid(jsBreakpoint -> {
jsBreakpoints.add(jsBreakpoint);
breakpointMap.put(jsBreakpoint, breakpoint);
}));
}
}
}
}
breakpoint.jsBreakpoints = jsBreakpoints;
return Promise.allVoid(promises);
}
private DebuggerListener[] getListeners() {
return listeners.toArray(new DebuggerListener[0]);
}
@ -312,32 +362,98 @@ public class Debugger {
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<CallFrame> frames = new ArrayList<>();
var frames = new ArrayList<CallFrame>();
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;
for (var jsFrame : javaScriptDebugger.getCallStack()) {
List<SourceLocationWithMethod> locations;
DebugInformation debugInformation = null;
switch (jsFrame.getLocation().getScript().getLanguage()) {
case JS:
debugInformation = debugInformationMap.get(jsFrame.getLocation().getScript());
locations = mapJsFrames(jsFrame, debugInformation);
break;
case WASM:
locations = mapWasmFrames(jsFrame);
break;
default:
locations = Collections.emptyList();
break;
}
boolean empty = loc == null || (loc.getFileName() == null && loc.getLine() < 0);
MethodReference method = !empty && debugInformation != null
? debugInformation.getMethodAt(jsFrame.getLocation().getLine(),
jsFrame.getLocation().getColumn())
: null;
if (!empty || !wasEmpty) {
for (var locWithMethod : locations) {
var loc = locWithMethod.loc;
var method = locWithMethod.method;
if (!locWithMethod.empty || !wasEmpty) {
frames.add(new CallFrame(this, jsFrame, loc, method, debugInformation));
}
wasEmpty = empty;
wasEmpty = locWithMethod.empty;
}
}
callStack = frames.toArray(new CallFrame[0]);
}
return callStack.clone();
}
private static class SourceLocationWithMethod {
private final boolean empty;
private final SourceLocation loc;
private final MethodReference method;
SourceLocationWithMethod(boolean empty, SourceLocation loc, MethodReference method) {
this.empty = empty;
this.loc = loc;
this.method = method;
}
}
private List<SourceLocationWithMethod> mapJsFrames(JavaScriptCallFrame frame,
DebugInformation debugInformation) {
SourceLocation loc;
if (debugInformation != null) {
loc = debugInformation.getSourceLocation(frame.getLocation().getLine(),
frame.getLocation().getColumn());
} else {
loc = null;
}
boolean empty = loc == null || (loc.getFileName() == null && loc.getLine() < 0);
var method = !empty && debugInformation != null
? debugInformation.getMethodAt(frame.getLocation().getLine(), frame.getLocation().getColumn())
: null;
return Collections.singletonList(new SourceLocationWithMethod(empty, loc, method));
}
private List<SourceLocationWithMethod> mapWasmFrames(JavaScriptCallFrame frame) {
var lineInfo = wasmLineInfoMap.get(frame.getLocation().getScript());
if (lineInfo == null) {
return Collections.emptyList();
}
var sequence = lineInfo.find(frame.getLocation().getColumn());
if (sequence == null) {
return Collections.emptyList();
}
var instructionLocation = sequence.unpack().find(frame.getLocation().getColumn());
if (instructionLocation == null) {
return Collections.emptyList();
}
var location = instructionLocation.location();
var result = new ArrayList<SourceLocationWithMethod>();
while (true) {
var loc = new SourceLocation(location.file().fullName(), location.line());
var inlining = location.inlining();
var method = inlining != null ? inlining.method() : sequence.method();
result.add(new SourceLocationWithMethod(false, loc, getMethodReference(method)));
if (inlining == null) {
break;
}
location = inlining.location();
}
return result;
}
private MethodReference getMethodReference(MethodInfo methodInfo) {
return new MethodReference(methodInfo.cls().fullName(), methodInfo.name(), ValueType.VOID);
}
Promise<Map<String, Variable>> createVariables(JavaScriptCallFrame jsFrame, DebugInformation debugInformation) {
return jsFrame.getVariables().then(jsVariables -> {
Map<String, Variable> vars = new HashMap<>();
@ -356,29 +472,72 @@ public class Debugger {
});
}
private void addScript(String name) {
if (!name.isEmpty()) {
scriptNames.add(name);
private void addScript(JavaScriptScript script) {
Promise<Void> promise;
switch (script.getLanguage()) {
case JS:
promise = addJavaScriptScript(script);
break;
case WASM:
promise = addWasmScript(script);
break;
default:
promise = Promise.VOID;
break;
}
if (debugInformationMap.containsKey(name)) {
updateBreakpoints();
return;
promise.thenVoid(v -> updateBreakpoints());
}
DebugInformation debugInfo = debugInformationProvider.getDebugInformation(name);
private Promise<Void> addJavaScriptScript(JavaScriptScript script) {
var debugInfo = debugInformationProvider.getDebugInformation(script.getUrl());
if (debugInfo == null) {
return;
return Promise.VOID;
}
debugInformationMap.put(name, debugInfo);
for (String sourceFile : debugInfo.getFilesNames()) {
Set<DebugInformation> list = debugInformationFileMap.get(sourceFile);
debugInformationMap.put(script, debugInfo);
for (var sourceFile : debugInfo.getFilesNames()) {
var list = debugInformationFileMap.get(sourceFile);
if (list == null) {
list = new HashSet<>();
debugInformationFileMap.put(sourceFile, list);
allSourceFiles.add(sourceFile);
}
list.add(debugInfo);
}
scriptMap.put(debugInfo, name);
updateBreakpoints();
scriptMap.put(debugInfo, script);
return Promise.VOID;
}
private Promise<Void> addWasmScript(JavaScriptScript script) {
return script.getSource().thenVoid(source -> {
if (source == null) {
return;
}
var decoder = Base64.getDecoder();
var reader = new DebugInfoReaderImpl(decoder.decode(source));
var parser = reader.read();
if (parser.getLineInfo() != null) {
wasmLineInfoMap.put(script, parser.getLineInfo());
wasmScriptMap.put(parser.getLineInfo(), script);
for (var sequence : parser.getLineInfo().sequences()) {
for (var command : sequence.commands()) {
if (command instanceof LineInfoFileCommand) {
addWasmInfoFile(((LineInfoFileCommand) command).file().fullName(),
parser.getLineInfo());
}
}
}
}
});
}
private void addWasmInfoFile(String sourceFile, LineInfo wasmLineInfo) {
var list = wasmInfoFileMap.get(sourceFile);
if (list == null) {
list = new HashSet<>();
wasmInfoFileMap.put(sourceFile, list);
}
list.add(wasmLineInfo);
allSourceFiles.add(sourceFile);
}
public Set<? extends String> getScriptNames() {
@ -488,8 +647,8 @@ public class Debugger {
}
@Override
public void scriptAdded(String name) {
addScript(name);
public void scriptAdded(JavaScriptScript script) {
addScript(script);
}
@Override

View File

@ -15,6 +15,7 @@
*/
package org.teavm.debugging.javascript;
import java.util.Map;
import org.teavm.common.Promise;
public interface JavaScriptDebugger {
@ -32,8 +33,6 @@ public interface JavaScriptDebugger {
Promise<Void> stepOver();
Promise<Void> continueToLocation(JavaScriptLocation location);
boolean isSuspended();
boolean isAttached();
@ -43,4 +42,6 @@ public interface JavaScriptDebugger {
JavaScriptCallFrame[] getCallStack();
Promise<JavaScriptBreakpoint> createBreakpoint(JavaScriptLocation location);
Map<? extends String, ? extends JavaScriptScript> getScripts();
}

View File

@ -26,5 +26,5 @@ public interface JavaScriptDebuggerListener {
void breakpointChanged(JavaScriptBreakpoint breakpoint);
void scriptAdded(String name);
void scriptAdded(JavaScriptScript script);
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2022 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.javascript;
public enum JavaScriptLanguage {
JS,
WASM,
UNKNOWN
}

View File

@ -18,17 +18,17 @@ package org.teavm.debugging.javascript;
import java.util.Objects;
public class JavaScriptLocation {
private String script;
private JavaScriptScript script;
private int line;
private int column;
public JavaScriptLocation(String script, int line, int column) {
public JavaScriptLocation(JavaScriptScript script, int line, int column) {
this.script = script;
this.line = line;
this.column = column;
}
public String getScript() {
public JavaScriptScript getScript() {
return script;
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2022 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.javascript;
import org.teavm.common.Promise;
public interface JavaScriptScript {
String getId();
JavaScriptLanguage getLanguage();
String getUrl();
Promise<String> getSource();
}

View File

@ -169,6 +169,8 @@ public abstract class BaseChromeRDPDebugger implements ChromeRDPExchangeConsumer
message.setMethod(method);
if (params != null) {
message.setParams(mapper.valueToTree(params));
} else {
message.setParams(mapper.createObjectNode());
}
sendMessage(message);

View File

@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@ -37,12 +38,13 @@ import org.teavm.chromerdp.messages.CallFunctionCommand;
import org.teavm.chromerdp.messages.CallFunctionResponse;
import org.teavm.chromerdp.messages.CompileScriptCommand;
import org.teavm.chromerdp.messages.CompileScriptResponse;
import org.teavm.chromerdp.messages.ContinueToLocationCommand;
import org.teavm.chromerdp.messages.GetPropertiesCommand;
import org.teavm.chromerdp.messages.GetPropertiesResponse;
import org.teavm.chromerdp.messages.GetScriptSourceCommand;
import org.teavm.chromerdp.messages.RemoveBreakpointCommand;
import org.teavm.chromerdp.messages.RunScriptCommand;
import org.teavm.chromerdp.messages.ScriptParsedNotification;
import org.teavm.chromerdp.messages.ScriptSource;
import org.teavm.chromerdp.messages.SetBreakpointCommand;
import org.teavm.chromerdp.messages.SetBreakpointResponse;
import org.teavm.chromerdp.messages.SuspendedNotification;
@ -52,7 +54,9 @@ import org.teavm.debugging.javascript.JavaScriptBreakpoint;
import org.teavm.debugging.javascript.JavaScriptCallFrame;
import org.teavm.debugging.javascript.JavaScriptDebugger;
import org.teavm.debugging.javascript.JavaScriptDebuggerListener;
import org.teavm.debugging.javascript.JavaScriptLanguage;
import org.teavm.debugging.javascript.JavaScriptLocation;
import org.teavm.debugging.javascript.JavaScriptScript;
import org.teavm.debugging.javascript.JavaScriptVariable;
public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScriptDebugger {
@ -62,8 +66,8 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri
private Set<RDPBreakpoint> breakpoints = new LinkedHashSet<>();
private Map<String, RDPNativeBreakpoint> breakpointsByChromeId = new HashMap<>();
private volatile RDPCallFrame[] callStack = new RDPCallFrame[0];
private Map<String, String> scripts = new HashMap<>();
private Map<String, String> scriptIds = new HashMap<>();
private Map<String, ChromeRDPScript> scripts = new LinkedHashMap<>();
private Map<String, JavaScriptScript> readonlyScripts = Collections.unmodifiableMap(scripts);
private volatile boolean suspended;
private Promise<Void> runtimeEnabledPromise;
@ -162,18 +166,38 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri
if (params.getUrl() == null) {
return Promise.VOID;
}
if (scripts.putIfAbsent(params.getScriptId(), params.getUrl()) != null) {
return Promise.VOID;
var language = JavaScriptLanguage.JS;
if (params.getScriptLanguage() != null) {
switch (params.getScriptLanguage()) {
case "WebAssembly":
language = JavaScriptLanguage.WASM;
break;
case "JavaScript":
language = JavaScriptLanguage.JS;
break;
default:
language = JavaScriptLanguage.UNKNOWN;
break;
}
}
var script = new ChromeRDPScript(this, params.getScriptId(), language, params.getUrl());
scripts.put(script.getId(), script);
if (params.getUrl().equals("file://fake")) {
return Promise.VOID;
}
scriptIds.put(params.getUrl(), params.getScriptId());
for (JavaScriptDebuggerListener listener : getListeners()) {
listener.scriptAdded(params.getUrl());
for (var listener : getListeners()) {
listener.scriptAdded(script);
}
if (language == JavaScriptLanguage.JS) {
return injectFunctions(params.getExecutionContextId());
}
return Promise.VOID;
}
@Override
public Map<? extends String, ? extends JavaScriptScript> getScripts() {
return readonlyScripts;
}
@Override
public void addListener(JavaScriptDebuggerListener listener) {
@ -210,13 +234,6 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri
return callMethodAsync("Debugger.stepOver", void.class, null);
}
@Override
public Promise<Void> continueToLocation(JavaScriptLocation location) {
ContinueToLocationCommand params = new ContinueToLocationCommand();
params.setLocation(unmap(location));
return callMethodAsync("Debugger.continueToLocation", void.class, params);
}
@Override
public boolean isSuspended() {
return isAttached() && suspended;
@ -492,8 +509,8 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri
}
private LocationDTO unmap(JavaScriptLocation location) {
LocationDTO dto = new LocationDTO();
dto.setScriptId(scriptIds.get(location.getScript()));
var dto = new LocationDTO();
dto.setScriptId(location.getScript().getId());
dto.setLineNumber(location.getLine());
dto.setColumnNumber(location.getColumn());
return dto;
@ -511,4 +528,11 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri
return Collections.unmodifiableMap(newBackingMap);
});
}
Promise<String> getScriptSource(String id) {
var callArgs = new GetScriptSourceCommand();
callArgs.scriptId = id;
return callMethodAsync("Debugger.getScriptSource", ScriptSource.class, callArgs)
.then(source -> source.bytecode);
}
}

View File

@ -35,6 +35,7 @@ import org.teavm.debugging.DebuggerListener;
import org.teavm.debugging.Variable;
import org.teavm.debugging.information.URLDebugInformationProvider;
import org.teavm.debugging.javascript.JavaScriptLocation;
import org.teavm.debugging.javascript.JavaScriptScript;
import org.teavm.debugging.javascript.JavaScriptVariable;
public final class ChromeRDPRunner {
@ -261,7 +262,7 @@ public final class ChromeRDPRunner {
};
private Promise<Void> tryResolveJsBreakpoint(String fileName, int lineNumber, int columnNumber) {
String[] fileNames = resolveJsFileName(fileName);
var fileNames = resolveJsFileName(fileName);
if (fileNames.length == 0) {
System.out.println("Unknown file: " + fileName);
return Promise.VOID;
@ -277,28 +278,8 @@ public final class ChromeRDPRunner {
});
}
private String[] resolveJsFileName(String fileName) {
if (debugger.getScriptNames().contains(fileName)) {
return new String[] { fileName };
}
String[] result = debugger.getScriptNames().stream()
.filter(f -> f.endsWith(fileName) && isPrecededByPathSeparator(f, fileName))
.toArray(String[]::new);
if (result.length == 1) {
return result;
}
return debugger.getSourceFiles().stream()
.filter(f -> {
int index = f.lastIndexOf('.');
if (index <= 0) {
return false;
}
String nameWithoutExt = f.substring(0, index);
return nameWithoutExt.endsWith(fileName) && isPrecededByPathSeparator(nameWithoutExt, fileName);
})
.toArray(String[]::new);
private JavaScriptScript[] resolveJsFileName(String fileName) {
return new JavaScriptScript[0];
}
private String[] resolveFileName(String fileName) {

View File

@ -0,0 +1,54 @@
/*
* Copyright 2022 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.chromerdp;
import org.teavm.common.Promise;
import org.teavm.debugging.javascript.JavaScriptLanguage;
import org.teavm.debugging.javascript.JavaScriptScript;
class ChromeRDPScript implements JavaScriptScript {
private ChromeRDPDebugger debugger;
private String id;
private JavaScriptLanguage language;
private String url;
ChromeRDPScript(ChromeRDPDebugger debugger, String id, JavaScriptLanguage language, String url) {
this.debugger = debugger;
this.id = id;
this.language = language;
this.url = url;
}
@Override
public String getId() {
return id;
}
@Override
public JavaScriptLanguage getLanguage() {
return language;
}
@Override
public String getUrl() {
return url;
}
@Override
public Promise<String> getSource() {
return debugger.getScriptSource(id);
}
}

View File

@ -1,127 +0,0 @@
/*
* Copyright 2022 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.chromerdp;
import java.io.IOException;
import java.util.Base64;
import java.util.Objects;
import java.util.concurrent.Executor;
import org.teavm.backend.wasm.debug.parser.DebugInfoParser;
import org.teavm.backend.wasm.debug.parser.DebugInfoReader;
import org.teavm.chromerdp.data.Message;
import org.teavm.chromerdp.messages.GetScriptSourceCommand;
import org.teavm.chromerdp.messages.ScriptParsedNotification;
import org.teavm.chromerdp.messages.ScriptSource;
import org.teavm.common.CompletablePromise;
import org.teavm.common.Promise;
public class WasmChromeRDPDebugger extends BaseChromeRDPDebugger {
public WasmChromeRDPDebugger(Executor executor) {
super(executor);
}
@Override
protected void onAttach() {
}
@Override
protected void onDetach() {
}
@Override
protected Promise<Void> handleMessage(Message message) throws IOException {
switch (message.getMethod()) {
case "Debugger.scriptParsed":
return scriptParsed(parseJson(ScriptParsedNotification.class, message.getParams()));
}
return Promise.VOID;
}
private Promise<Void> scriptParsed(ScriptParsedNotification params) {
if (Objects.equals(params.getScriptLanguage(), "WebAssembly")) {
var callArgs = new GetScriptSourceCommand();
callArgs.scriptId = params.getScriptId();
return callMethodAsync("Debugger.getScriptSource", ScriptSource.class, callArgs)
.thenVoid(source -> parseWasm(source, params.getUrl()));
}
return Promise.VOID;
}
private void parseWasm(ScriptSource source, String url) {
if (source.bytecode == null) {
return;
}
var bytes = Base64.getDecoder().decode(source.bytecode);
var reader = new DebugInfoReaderImpl(bytes);
var debugInfoParser = new DebugInfoParser(reader);
Promise.runNow(() -> {
debugInfoParser.parse().catchVoid(Throwable::printStackTrace);
reader.complete();
});
var lineInfo = debugInfoParser.getLineInfo();
if (lineInfo != null) {
System.out.println("Debug information found in script: " + url);
}
}
private static class DebugInfoReaderImpl implements DebugInfoReader {
private byte[] data;
private int ptr;
private CompletablePromise<Integer> promise;
private byte[] target;
private int offset;
private int count;
DebugInfoReaderImpl(byte[] data) {
this.data = data;
}
@Override
public Promise<Integer> skip(int amount) {
promise = new CompletablePromise<>();
count = amount;
return promise;
}
@Override
public Promise<Integer> read(byte[] buffer, int offset, int count) {
promise = new CompletablePromise<>();
this.target = buffer;
this.offset = offset;
this.count = count;
return promise;
}
private void complete() {
while (promise != null) {
var p = promise;
count = Math.min(count, data.length - ptr);
promise = null;
if (target != null) {
System.arraycopy(data, ptr, target, offset, count);
target = null;
}
ptr += count;
if (count == 0) {
count = -1;
}
p.complete(count);
}
}
}
}

View File

@ -1,55 +0,0 @@
/*
* Copyright 2022 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.chromerdp;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class WasmChromeRDPRunner {
private ChromeRDPServer server;
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
private WasmChromeRDPRunner() {
server = new ChromeRDPServer();
server.setPort(2357);
var wasmDebugger = new WasmChromeRDPDebugger(queue::offer);
server.setExchangeConsumer(wasmDebugger);
new Thread(server::start).start();
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.err.println("Uncaught exception in thread " + t);
e.printStackTrace();
});
}
public static void main(String[] args) {
var runner = new WasmChromeRDPRunner();
try {
while (true) {
runner.run();
}
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
public void run() throws InterruptedException {
while (true) {
queue.take().run();
}
}
}