From a2715f2c79c99132c8dac488304491b4a944c8df Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Tue, 29 Nov 2022 21:30:41 +0100 Subject: [PATCH] Wasm: working on Chrome RDP debugger --- .../backend/wasm/debug/info/LineInfo.java | 8 +- .../wasm/debug/parser/DebugInfoParser.java | 3 + .../wasm/debug/parser/DebugLinesParser.java | 8 +- .../java/org/teavm/common/CollectionUtil.java | 2 +- .../java/org/teavm/debugging/CallFrame.java | 7 +- .../teavm/debugging/DebugInfoReaderImpl.java | 76 +++++ .../java/org/teavm/debugging/Debugger.java | 317 +++++++++++++----- .../javascript/JavaScriptDebugger.java | 5 +- .../JavaScriptDebuggerListener.java | 2 +- .../javascript/JavaScriptLanguage.java | 22 ++ .../javascript/JavaScriptLocation.java | 6 +- .../javascript/JavaScriptScript.java | 28 ++ .../chromerdp/BaseChromeRDPDebugger.java | 2 + .../teavm/chromerdp/ChromeRDPDebugger.java | 60 +++- .../org/teavm/chromerdp/ChromeRDPRunner.java | 27 +- .../org/teavm/chromerdp/ChromeRDPScript.java | 54 +++ .../chromerdp/WasmChromeRDPDebugger.java | 127 ------- .../teavm/chromerdp/WasmChromeRDPRunner.java | 55 --- 18 files changed, 497 insertions(+), 312 deletions(-) create mode 100644 core/src/main/java/org/teavm/debugging/DebugInfoReaderImpl.java create mode 100644 core/src/main/java/org/teavm/debugging/javascript/JavaScriptLanguage.java create mode 100644 core/src/main/java/org/teavm/debugging/javascript/JavaScriptScript.java create mode 100644 tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPScript.java delete mode 100644 tools/chrome-rdp/src/main/java/org/teavm/chromerdp/WasmChromeRDPDebugger.java delete mode 100644 tools/chrome-rdp/src/main/java/org/teavm/chromerdp/WasmChromeRDPRunner.java diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfo.java b/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfo.java index 3ad0975b6..18c624f67 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfo.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfo.java @@ -22,14 +22,20 @@ import java.util.List; import org.teavm.common.CollectionUtil; public class LineInfo { + private int offset; private LineInfoSequence[] sequences; private List 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 sequences() { return sequenceList; } diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugInfoParser.java b/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugInfoParser.java index dfc9dcca6..14a05ea99 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugInfoParser.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugInfoParser.java @@ -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); } }); diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugLinesParser.java b/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugLinesParser.java index 6b525364b..124a690fd 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugLinesParser.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugLinesParser.java @@ -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; diff --git a/core/src/main/java/org/teavm/common/CollectionUtil.java b/core/src/main/java/org/teavm/common/CollectionUtil.java index 1c1691f5d..647f9d37d 100644 --- a/core/src/main/java/org/teavm/common/CollectionUtil.java +++ b/core/src/main/java/org/teavm/common/CollectionUtil.java @@ -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) { diff --git a/core/src/main/java/org/teavm/debugging/CallFrame.java b/core/src/main/java/org/teavm/debugging/CallFrame.java index 3b46ada82..2bfc261d5 100644 --- a/core/src/main/java/org/teavm/debugging/CallFrame.java +++ b/core/src/main/java/org/teavm/debugging/CallFrame.java @@ -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> getVariables() { if (variables == null) { - variables = debugger.createVariables(originalCallFrame, debugInformation); + if (debugInformation != null) { + variables = debugger.createVariables(originalCallFrame, debugInformation); + } else { + variables = Promise.of(Collections.emptyMap()); + } } return variables; } diff --git a/core/src/main/java/org/teavm/debugging/DebugInfoReaderImpl.java b/core/src/main/java/org/teavm/debugging/DebugInfoReaderImpl.java new file mode 100644 index 000000000..8e7777d24 --- /dev/null +++ b/core/src/main/java/org/teavm/debugging/DebugInfoReaderImpl.java @@ -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 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 skip(int amount) { + promise = new CompletablePromise<>(); + count = amount; + return promise; + } + + @Override + public Promise 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); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/org/teavm/debugging/Debugger.java b/core/src/main/java/org/teavm/debugging/Debugger.java index ee77157ac..6b9201cae 100644 --- a/core/src/main/java/org/teavm/debugging/Debugger.java +++ b/core/src/main/java/org/teavm/debugging/Debugger.java @@ -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 listeners = new LinkedHashSet<>(); private JavaScriptDebugger javaScriptDebugger; private DebugInformationProvider debugInformationProvider; private List temporaryBreakpoints = new ArrayList<>(); - private Map debugInformationMap = new HashMap<>(); + private Map debugInformationMap = new HashMap<>(); private Map> debugInformationFileMap = new HashMap<>(); - private Map scriptMap = new HashMap<>(); + private Map wasmLineInfoMap = new HashMap<>(); + private Map wasmScriptMap = new HashMap<>(); + private Map> wasmInfoFileMap = new HashMap<>(); + private Map scriptMap = new HashMap<>(); private Map breakpointMap = new HashMap<>(); private Set breakpoints = new LinkedHashSet<>(); private Set readonlyBreakpoints = Collections.unmodifiableSet(breakpoints); private CallFrame[] callStack; private Set scriptNames = new LinkedHashSet<>(); + private Set allSourceFiles = new LinkedHashSet<>(); public Debugger(JavaScriptDebugger javaScriptDebugger, DebugInformationProvider debugInformationProvider) { this.javaScriptDebugger = javaScriptDebugger; @@ -98,61 +107,81 @@ public class Debugger { } private Promise 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 successors = new HashSet<>(); + var successors = new HashSet(); boolean first = true; - 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); + 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; } - } else { - exits = true; - } - if (!exits) { - break; } enterMethod = false; - if (!first && frame.getLocation() != null) { - for (GeneratedLocation location : debugInfo.getGeneratedLocations(frame.getLocation())) { - successors.add(new JavaScriptLocation(script, location.getLine(), location.getColumn())); - } - } first = false; } - List> jsBreakpointPromises = new ArrayList<>(); - for (JavaScriptLocation successor : successors) { + var jsBreakpointPromises = new ArrayList>(); + 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 successors) { + var debugInfo = debugInformationMap.get(script); + boolean exits; + 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) { + var successorFinder = new CallSiteSuccessorFinder(debugInfo, script, successors); + var callSites = debugInfo.getCallSites(frame.getLocation()); + for (var callSite : callSites) { + callSite.acceptVisitor(successorFinder); + } + } + } else { + exits = true; + } + if (!exits) { + return false; + } + if (!first && frame.getLocation() != null) { + for (var location : debugInfo.getGeneratedLocations(frame.getLocation())) { + successors.add(new JavaScriptLocation(script, location.getLine(), location.getColumn())); + } + } + return false; + } + static class CallSiteSuccessorFinder implements DebuggerCallSiteVisitor { private DebugInformation debugInfo; - private String script; + private JavaScriptScript script; Set locations; - CallSiteSuccessorFinder(DebugInformation debugInfo, String script, Set locations) { + CallSiteSuccessorFinder(DebugInformation debugInfo, JavaScriptScript script, + Set 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 visited, Set successors) { if (!visited.add(location)) { return false; @@ -205,7 +234,12 @@ public class Debugger { } private List debugInformationBySource(String sourceFile) { - Set list = debugInformationFileMap.get(sourceFile); + var list = debugInformationFileMap.get(sourceFile); + return list != null ? new ArrayList<>(list) : Collections.emptyList(); + } + + private List 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 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 getSourceFiles() { - return debugInformationFileMap.keySet(); + return allSourceFiles; } public Promise 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> promises = new ArrayList<>(); - for (JavaScriptBreakpoint jsBreakpoint : breakpoint.jsBreakpoints) { + var promises = new ArrayList>(); + for (var jsBreakpoint : breakpoint.jsBreakpoints) { breakpointMap.remove(jsBreakpoint); promises.add(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), + var jsBreakpoints = new ArrayList(); + 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 frames = new ArrayList<>(); + var 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; + for (var jsFrame : javaScriptDebugger.getCallStack()) { + List 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) { - frames.add(new CallFrame(this, jsFrame, loc, method, debugInformation)); + 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 = locWithMethod.empty; } - wasEmpty = 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 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 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(); + 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> createVariables(JavaScriptCallFrame jsFrame, DebugInformation debugInformation) { return jsFrame.getVariables().then(jsVariables -> { Map 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 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; - } - DebugInformation debugInfo = debugInformationProvider.getDebugInformation(name); + promise.thenVoid(v -> updateBreakpoints()); + } + + private Promise 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 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 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 getScriptNames() { @@ -488,8 +647,8 @@ public class Debugger { } @Override - public void scriptAdded(String name) { - addScript(name); + public void scriptAdded(JavaScriptScript script) { + addScript(script); } @Override diff --git a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptDebugger.java b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptDebugger.java index 2ab3ea8c1..aa3f5f6a8 100644 --- a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptDebugger.java +++ b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptDebugger.java @@ -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 stepOver(); - Promise continueToLocation(JavaScriptLocation location); - boolean isSuspended(); boolean isAttached(); @@ -43,4 +42,6 @@ public interface JavaScriptDebugger { JavaScriptCallFrame[] getCallStack(); Promise createBreakpoint(JavaScriptLocation location); + + Map getScripts(); } diff --git a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptDebuggerListener.java b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptDebuggerListener.java index b5a79f039..04329497d 100644 --- a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptDebuggerListener.java +++ b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptDebuggerListener.java @@ -26,5 +26,5 @@ public interface JavaScriptDebuggerListener { void breakpointChanged(JavaScriptBreakpoint breakpoint); - void scriptAdded(String name); + void scriptAdded(JavaScriptScript script); } diff --git a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptLanguage.java b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptLanguage.java new file mode 100644 index 000000000..ea0e826fe --- /dev/null +++ b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptLanguage.java @@ -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 +} diff --git a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptLocation.java b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptLocation.java index cca9e9b71..c41c170e0 100644 --- a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptLocation.java +++ b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptLocation.java @@ -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; } diff --git a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptScript.java b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptScript.java new file mode 100644 index 000000000..9fe5acaf7 --- /dev/null +++ b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptScript.java @@ -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 getSource(); +} diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/BaseChromeRDPDebugger.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/BaseChromeRDPDebugger.java index 7cfba0711..8d82b2835 100644 --- a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/BaseChromeRDPDebugger.java +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/BaseChromeRDPDebugger.java @@ -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); diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPDebugger.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPDebugger.java index c70205687..b7aec2f0a 100644 --- a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPDebugger.java +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPDebugger.java @@ -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 breakpoints = new LinkedHashSet<>(); private Map breakpointsByChromeId = new HashMap<>(); private volatile RDPCallFrame[] callStack = new RDPCallFrame[0]; - private Map scripts = new HashMap<>(); - private Map scriptIds = new HashMap<>(); + private Map scripts = new LinkedHashMap<>(); + private Map readonlyScripts = Collections.unmodifiableMap(scripts); private volatile boolean suspended; private Promise runtimeEnabledPromise; @@ -162,17 +166,37 @@ 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); } - return injectFunctions(params.getExecutionContextId()); + if (language == JavaScriptLanguage.JS) { + return injectFunctions(params.getExecutionContextId()); + } + return Promise.VOID; + } + + @Override + public Map getScripts() { + return readonlyScripts; } @Override @@ -210,13 +234,6 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri return callMethodAsync("Debugger.stepOver", void.class, null); } - @Override - public Promise 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 getScriptSource(String id) { + var callArgs = new GetScriptSourceCommand(); + callArgs.scriptId = id; + return callMethodAsync("Debugger.getScriptSource", ScriptSource.class, callArgs) + .then(source -> source.bytecode); + } } diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPRunner.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPRunner.java index 317aef323..217237ada 100644 --- a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPRunner.java +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPRunner.java @@ -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 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) { diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPScript.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPScript.java new file mode 100644 index 000000000..f36ecc365 --- /dev/null +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPScript.java @@ -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 getSource() { + return debugger.getScriptSource(id); + } +} diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/WasmChromeRDPDebugger.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/WasmChromeRDPDebugger.java deleted file mode 100644 index c35ccfc1c..000000000 --- a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/WasmChromeRDPDebugger.java +++ /dev/null @@ -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 handleMessage(Message message) throws IOException { - switch (message.getMethod()) { - case "Debugger.scriptParsed": - return scriptParsed(parseJson(ScriptParsedNotification.class, message.getParams())); - } - return Promise.VOID; - } - - private Promise 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 promise; - private byte[] target; - private int offset; - private int count; - - DebugInfoReaderImpl(byte[] data) { - this.data = data; - } - - @Override - public Promise skip(int amount) { - promise = new CompletablePromise<>(); - count = amount; - return promise; - } - - @Override - public Promise 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); - } - } - } -} diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/WasmChromeRDPRunner.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/WasmChromeRDPRunner.java deleted file mode 100644 index 7240c1ac6..000000000 --- a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/WasmChromeRDPRunner.java +++ /dev/null @@ -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 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(); - } - } -}