diff --git a/core/src/main/java/org/teavm/common/CompletablePromise.java b/core/src/main/java/org/teavm/common/CompletablePromise.java new file mode 100644 index 000000000..a2522efbf --- /dev/null +++ b/core/src/main/java/org/teavm/common/CompletablePromise.java @@ -0,0 +1,31 @@ +/* + * Copyright 2018 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.common; + +public class CompletablePromise extends Promise { + public CompletablePromise() { + } + + @Override + public void complete(T value) { + super.complete(value); + } + + @Override + public void completeWithError(Throwable e) { + super.completeWithError(e); + } +} diff --git a/core/src/main/java/org/teavm/common/Promise.java b/core/src/main/java/org/teavm/common/Promise.java new file mode 100644 index 000000000..c86cf6dbb --- /dev/null +++ b/core/src/main/java/org/teavm/common/Promise.java @@ -0,0 +1,319 @@ +/* + * Copyright 2018 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.common; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +public class Promise { + public static final Promise VOID = Promise.of(null); + + private T value; + private Promise promise; + private Throwable error; + private State state = State.PENDING; + private List> thenList; + private List catchList; + + Promise() { + } + + public static Promise of(T value) { + Promise promise = new Promise<>(); + promise.complete(value); + return promise; + } + + public static Promise error(Throwable e) { + Promise promise = new Promise<>(); + promise.completeWithError(e); + return promise; + } + + public static Promise allVoid(Collection> promises) { + if (promises.isEmpty()) { + return Promise.VOID; + } + AllVoidFunction all = new AllVoidFunction(promises.size()); + + for (Promise promise : promises) { + promise.then(all.thenF).catchError(all.catchF); + } + + return all.result; + } + + + public static Promise> all(Collection> promises) { + if (promises.isEmpty()) { + return Promise.of(Collections.emptyList()); + } + AllFunction all = new AllFunction<>(promises.size()); + + int i = 0; + for (Promise promise : promises) { + promise.then(all.thenF(i++)).catchError(all.catchF); + } + + return all.result; + } + + static class AllVoidFunction { + Promise result = new Promise<>(); + int count; + boolean error; + + AllVoidFunction(int count) { + this.result = result; + this.count = count; + } + + Function thenF = v -> { + if (!error && --count == 0) { + result.complete(null); + } + return null; + }; + + Function catchF = e -> { + if (!error) { + error = true; + result.completeWithError(e); + } + return null; + }; + } + + + static class AllFunction { + Promise> result = new Promise<>(); + List list = new ArrayList<>(); + int count; + boolean error; + + AllFunction(int count) { + this.result = result; + this.count = count; + list.addAll(Collections.nCopies(count, null)); + } + + Function thenF(int index) { + return v -> { + if (!error) { + list.set(index, v); + if (--count == 0) { + result.complete(list); + } + } + return null; + }; + }; + + Function catchF = e -> { + if (!error) { + error = true; + result.completeWithError(e); + } + return null; + }; + } + + public Promise then(Function f) { + Promise result = new Promise<>(); + if (state == State.PENDING || state == State.WAITING_PROMISE) { + if (thenList == null) { + thenList = new ArrayList<>(); + thenList.add(new Then<>(f, result, false)); + } + } else { + passValue(f, result); + } + return result; + } + + public Promise thenVoid(Consumer f) { + return then(r -> { + f.accept(r); + return null; + }); + } + + public Promise thenAsync(Function> f) { + Promise result = new Promise<>(); + if (state == State.PENDING || state == State.WAITING_PROMISE) { + if (thenList == null) { + thenList = new ArrayList<>(); + thenList.add(new Then<>(f, result, true)); + } + } else if (state == State.COMPLETED) { + passValueAsync(f, result); + } + return result; + } + + public Promise catchError(Function f) { + Promise result = new Promise<>(); + if (state == State.PENDING || state == State.WAITING_PROMISE) { + if (catchList == null) { + catchList = new ArrayList<>(); + catchList.add(new Catch(f, result)); + } + } else if (state == State.ERRORED) { + passError(f, result); + } + return result; + } + + + public Promise catchVoid(Consumer f) { + return catchError(e -> { + f.accept(e); + return null; + }); + } + + void passValue(Function f, Promise target) { + if (state == State.COMPLETED) { + S next; + try { + next = f.apply(value); + } catch (Throwable e) { + target.completeWithError(e); + return; + } + target.complete(next); + } else { + target.completeWithError(error); + } + } + + void passValueAsync(Function> f, Promise target) { + if (state == State.COMPLETED) { + target.completeAsync(f.apply(value)); + } else { + target.completeWithError(error); + } + } + + void passError(Function f, Promise target) { + S next; + try { + next = f.apply(error); + } catch (Throwable e) { + target.completeWithError(e); + return; + } + target.complete(next); + } + + void complete(T value) { + if (state != State.PENDING) { + throw new IllegalStateException("Already completed"); + } + completeImpl(value); + } + + void completeAsync(Promise value) { + if (state != State.PENDING) { + throw new IllegalStateException("Already completed"); + } + state = State.WAITING_PROMISE; + + value + .then(result -> { + completeImpl(result); + return null; + }) + .catchError(e -> { + completeWithErrorImpl(e); + return null; + }); + } + + private void completeImpl(T value) { + state = State.COMPLETED; + this.value = value; + + if (thenList != null) { + List> list = thenList; + thenList = null; + for (Then then : list) { + if (then.promise) { + passValueAsync((Function>) then.f, (Promise) then.target); + } else { + passValue(then.f, (Promise) then.target); + } + } + } + catchList = null; + } + + void completeWithError(Throwable e) { + if (state != State.PENDING) { + throw new IllegalStateException("Already completed"); + } + completeWithErrorImpl(e); + } + + void completeWithErrorImpl(Throwable e) { + state = State.ERRORED; + this.error = e; + + if (catchList != null) { + List list = catchList; + thenList = null; + for (Catch c : list) { + passError(c.f, (Promise) c.target); + } + } else { + e.printStackTrace(); + } + thenList = null; + } + + enum State { + PENDING, + WAITING_PROMISE, + COMPLETED, + ERRORED + } + + static class Then { + Function f; + Promise target; + boolean promise; + + Then(Function f, Promise target, boolean promise) { + this.f = f; + this.target = target; + this.promise = promise; + } + } + + static class Catch { + Function f; + Promise target; + + Catch(Function f, Promise target) { + this.f = f; + this.target = target; + } + } +} diff --git a/core/src/main/java/org/teavm/debugging/CallFrame.java b/core/src/main/java/org/teavm/debugging/CallFrame.java index f9afe60f1..3b46ada82 100644 --- a/core/src/main/java/org/teavm/debugging/CallFrame.java +++ b/core/src/main/java/org/teavm/debugging/CallFrame.java @@ -15,8 +15,9 @@ */ package org.teavm.debugging; -import java.util.Collections; import java.util.Map; +import org.teavm.common.Promise; +import org.teavm.debugging.information.DebugInformation; import org.teavm.debugging.information.SourceLocation; import org.teavm.debugging.javascript.JavaScriptCallFrame; import org.teavm.debugging.javascript.JavaScriptLocation; @@ -27,15 +28,16 @@ public class CallFrame { private JavaScriptCallFrame originalCallFrame; private SourceLocation location; private MethodReference method; - private Map variables; + private Promise> variables; + private DebugInformation debugInformation; CallFrame(Debugger debugger, JavaScriptCallFrame originalFrame, SourceLocation location, MethodReference method, - Map variables) { + DebugInformation debugInformation) { this.debugger = debugger; this.originalCallFrame = originalFrame; this.location = location; this.method = method; - this.variables = Collections.unmodifiableMap(variables); + this.debugInformation = debugInformation; } public Debugger getDebugger() { @@ -58,7 +60,10 @@ public class CallFrame { return method; } - public Map getVariables() { + public Promise> getVariables() { + if (variables == null) { + variables = debugger.createVariables(originalCallFrame, debugInformation); + } return variables; } } diff --git a/core/src/main/java/org/teavm/debugging/Debugger.java b/core/src/main/java/org/teavm/debugging/Debugger.java index 779b25d0a..1f92e36f6 100644 --- a/core/src/main/java/org/teavm/debugging/Debugger.java +++ b/core/src/main/java/org/teavm/debugging/Debugger.java @@ -15,28 +15,45 @@ */ 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 java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +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; +import org.teavm.debugging.information.GeneratedLocation; +import org.teavm.debugging.information.SourceLocation; +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.JavaScriptLocation; +import org.teavm.debugging.javascript.JavaScriptVariable; import org.teavm.model.MethodReference; public class Debugger { - private static final Object dummyObject = new Object(); - private ConcurrentMap listeners = new ConcurrentHashMap<>(); + private Set listeners = new LinkedHashSet<>(); 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<>(); - private final ConcurrentMap breakpointMap = new ConcurrentHashMap<>(); - private final ConcurrentMap breakpoints = new ConcurrentHashMap<>(); - private volatile CallFrame[] callStack; + private List temporaryBreakpoints = new ArrayList<>(); + private Map debugInformationMap = new HashMap<>(); + private Map> debugInformationFileMap = 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<>(); public Debugger(JavaScriptDebugger javaScriptDebugger, DebugInformationProvider debugInformationProvider) { this.javaScriptDebugger = javaScriptDebugger; @@ -49,52 +66,46 @@ public class Debugger { } public void addListener(DebuggerListener listener) { - listeners.put(listener, dummyObject); + listeners.add(listener); } public void removeListener(DebuggerListener listener) { listeners.remove(listener); } - public void suspend() { - javaScriptDebugger.suspend(); + public Promise suspend() { + return javaScriptDebugger.suspend(); } - public void resume() { - javaScriptDebugger.resume(); + public Promise resume() { + return javaScriptDebugger.resume(); } - public void stepInto() { - step(true); + public Promise stepInto() { + return step(true); } - public void stepOut() { - javaScriptDebugger.stepOut(); + public Promise stepOut() { + return javaScriptDebugger.stepOut(); } - public void stepOver() { - step(false); + public Promise stepOver() { + return step(false); } - private void jsStep(boolean enterMethod) { - if (enterMethod) { - javaScriptDebugger.stepInto(); - } else { - javaScriptDebugger.stepOver(); - } + private Promise jsStep(boolean enterMethod) { + return enterMethod ? javaScriptDebugger.stepInto() : javaScriptDebugger.stepOver(); } - private void step(boolean enterMethod) { + private Promise step(boolean enterMethod) { CallFrame[] callStack = getCallStack(); if (callStack == null || callStack.length == 0) { - jsStep(enterMethod); - return; + return jsStep(enterMethod); } CallFrame recentFrame = callStack[0]; if (recentFrame.getLocation() == null || recentFrame.getLocation().getFileName() == null || recentFrame.getLocation().getLine() < 0) { - jsStep(enterMethod); - return; + return jsStep(enterMethod); } Set successors = new HashSet<>(); for (CallFrame frame : callStack) { @@ -120,10 +131,13 @@ public class Debugger { } enterMethod = true; } + + List> jsBreakpointPromises = new ArrayList<>(); for (JavaScriptLocation successor : successors) { - temporaryBreakpoints.add(javaScriptDebugger.createBreakpoint(successor)); + jsBreakpointPromises.add(javaScriptDebugger.createBreakpoint(successor) + .thenVoid(temporaryBreakpoints::add)); } - javaScriptDebugger.resume(); + return Promise.allVoid(jsBreakpointPromises).thenAsync(v -> javaScriptDebugger.resume()); } static class CallSiteSuccessorFinder implements DebuggerCallSiteVisitor { @@ -184,37 +198,36 @@ public class Debugger { } private List debugInformationBySource(String sourceFile) { - Map list = debugInformationFileMap.get(sourceFile); - return list != null ? new ArrayList<>(list.keySet()) : Collections.emptyList(); + Set list = debugInformationFileMap.get(sourceFile); + return list != null ? new ArrayList<>(list) : Collections.emptyList(); } - public void continueToLocation(SourceLocation location) { - continueToLocation(location.getFileName(), location.getLine()); + public Promise continueToLocation(SourceLocation location) { + return continueToLocation(location.getFileName(), location.getLine()); } - public void continueToLocation(String fileName, int line) { + public Promise continueToLocation(String fileName, int line) { if (!javaScriptDebugger.isSuspended()) { - return; + return Promise.VOID; } + + List> promises = new ArrayList<>(); 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); - } + promises.add(javaScriptDebugger.createBreakpoint(jsLocation).thenVoid(temporaryBreakpoints::add)); } } - javaScriptDebugger.resume(); + return Promise.allVoid(promises).thenAsync(v -> javaScriptDebugger.resume()); } public boolean isSuspended() { return javaScriptDebugger.isSuspended(); } - public Breakpoint createBreakpoint(String file, int line) { + public Promise createBreakpoint(String file, int line) { return createBreakpoint(new SourceLocation(file, line)); } @@ -222,28 +235,30 @@ public class Debugger { return debugInformationFileMap.keySet(); } - public Breakpoint createBreakpoint(SourceLocation location) { - synchronized (breakpointMap) { - Breakpoint breakpoint = new Breakpoint(this, location); - breakpoints.put(breakpoint, dummyObject); - updateInternalBreakpoints(breakpoint); + public Promise createBreakpoint(SourceLocation location) { + Breakpoint breakpoint = new Breakpoint(this, location); + breakpoints.add(breakpoint); + return updateInternalBreakpoints(breakpoint).then(v -> { updateBreakpointStatus(breakpoint, false); return breakpoint; - } + }); } - public Set getBreakpoints() { - return new HashSet<>(breakpoints.keySet()); + public Set getBreakpoints() { + return readonlyBreakpoints; } - private void updateInternalBreakpoints(Breakpoint breakpoint) { + private Promise updateInternalBreakpoints(Breakpoint breakpoint) { if (breakpoint.isDestroyed()) { - return; + return Promise.VOID; } + + List> promises = new ArrayList<>(); for (JavaScriptBreakpoint jsBreakpoint : breakpoint.jsBreakpoints) { breakpointMap.remove(jsBreakpoint); - jsBreakpoint.destroy(); + promises.add(jsBreakpoint.destroy()); } + List jsBreakpoints = new ArrayList<>(); SourceLocation location = breakpoint.getLocation(); for (DebugInformation debugInformation : debugInformationBySource(location.getFileName())) { @@ -251,16 +266,19 @@ public class Debugger { 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); + 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.keySet().toArray(new DebuggerListener[0]); + return listeners.toArray(new DebuggerListener[0]); } private void updateBreakpointStatus(Breakpoint breakpoint, boolean fireEvent) { @@ -302,9 +320,7 @@ public class Debugger { MethodReference method = !empty ? debugInformation.getMethodAt(jsFrame.getLocation().getLine(), jsFrame.getLocation().getColumn()) : null; if (!empty || !wasEmpty) { - VariableMap vars = new VariableMap(jsFrame.getVariables(), this, debugInformation, - jsFrame.getLocation()); - frames.add(new CallFrame(this, jsFrame, loc, method, vars)); + frames.add(new CallFrame(this, jsFrame, loc, method, debugInformation)); } wasEmpty = empty; } @@ -313,42 +329,56 @@ public class Debugger { return callStack.clone(); } + Promise> createVariables(JavaScriptCallFrame jsFrame, DebugInformation debugInformation) { + return jsFrame.getVariables().then(jsVariables -> { + Map vars = new HashMap<>(); + for (Map.Entry entry : jsVariables.entrySet()) { + JavaScriptVariable jsVar = entry.getValue(); + String[] names = mapVariable(entry.getKey(), jsFrame.getLocation()); + Value value = new Value(this, debugInformation, jsVar.getValue()); + for (String name : names) { + if (name == null) { + name = "js:" + jsVar.getName(); + } + vars.put(name, new Variable(name, value)); + } + } + return Collections.unmodifiableMap(vars); + }); + } + private void addScript(String name) { + if (!name.isEmpty()) { + scriptNames.add(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; } + debugInformationMap.put(name, debugInfo); for (String sourceFile : debugInfo.getFilesNames()) { - ConcurrentMap list = debugInformationFileMap.get(sourceFile); + Set list = debugInformationFileMap.get(sourceFile); if (list == null) { - list = new ConcurrentHashMap<>(); - ConcurrentMap existing = debugInformationFileMap.putIfAbsent( - sourceFile, list); - if (existing != null) { - list = existing; - } + list = new HashSet<>(); + debugInformationFileMap.put(sourceFile, list); } - list.put(debugInfo, dummyObject); + list.add(debugInfo); } scriptMap.put(debugInfo, name); updateBreakpoints(); } + public Set getScriptNames() { + return scriptNames; + } + private void updateBreakpoints() { - synchronized (breakpointMap) { - for (Breakpoint breakpoint : breakpoints.keySet()) { - updateInternalBreakpoints(breakpoint); - updateBreakpointStatus(breakpoint, true); - } + for (Breakpoint breakpoint : breakpoints) { + updateInternalBreakpoints(breakpoint).thenVoid(v -> updateBreakpointStatus(breakpoint, true)); } } @@ -370,22 +400,33 @@ public class Debugger { } 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); + private void firePaused(JavaScriptBreakpoint breakpoint) { + List temporaryBreakpoints = new ArrayList<>(this.temporaryBreakpoints); + this.temporaryBreakpoints.clear(); + List> promises = new ArrayList<>(); + for (JavaScriptBreakpoint jsBreakpoint : temporaryBreakpoints) { + promises.add(jsBreakpoint.destroy()); + } + callStack = null; + Promise.allVoid(promises).thenVoid(v -> { + Breakpoint javaBreakpoint = null; + if (breakpoint != null && !temporaryBreakpoints.contains(breakpoint)) { + javaBreakpoint = breakpointMap.get(breakpoint); } + for (DebuggerListener listener : getListeners()) { + listener.paused(javaBreakpoint); + } + }); + } + + private void fireAttached() { + for (Breakpoint breakpoint : breakpoints) { + updateInternalBreakpoints(breakpoint).thenVoid(v -> updateBreakpointStatus(breakpoint, false)); } for (DebuggerListener listener : getListeners()) { listener.attached(); @@ -393,7 +434,7 @@ public class Debugger { } private void fireDetached() { - for (Breakpoint breakpoint : breakpoints.keySet()) { + for (Breakpoint breakpoint : breakpoints) { updateBreakpointStatus(breakpoint, false); } for (DebuggerListener listener : getListeners()) { @@ -434,14 +475,7 @@ public class Debugger { @Override public void paused(JavaScriptBreakpoint breakpoint) { - callStack = null; - Breakpoint javaBreakpoint = null; - if (breakpoint != null && !temporaryBreakpoints.contains(breakpoint)) { - javaBreakpoint = breakpointMap.get(breakpoint); - } - for (DebuggerListener listener : getListeners()) { - listener.paused(javaBreakpoint); - } + firePaused(breakpoint); } @Override diff --git a/core/src/main/java/org/teavm/debugging/PropertyMap.java b/core/src/main/java/org/teavm/debugging/PropertyMap.java deleted file mode 100644 index 4e15f3ab0..000000000 --- a/core/src/main/java/org/teavm/debugging/PropertyMap.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.AbstractMap; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import org.teavm.debugging.information.DebugInformation; -import org.teavm.debugging.javascript.JavaScriptVariable; - -class PropertyMap extends AbstractMap { - private String className; - private AtomicReference> backingMap = new AtomicReference<>(); - private Map jsVariables; - private Debugger debugger; - private DebugInformation debugInformation; - - public PropertyMap(String className, Map jsVariables, Debugger debugger, - DebugInformation debugInformation) { - this.className = className; - this.jsVariables = jsVariables; - this.debugger = debugger; - this.debugInformation = debugInformation; - } - - @Override - public int size() { - updateBackingMap(); - return backingMap.get().size(); - } - - @Override - public Variable get(Object key) { - updateBackingMap(); - return backingMap.get().get(key); - } - - @Override - public Set> entrySet() { - updateBackingMap(); - return backingMap.get().entrySet(); - } - - private void updateBackingMap() { - if (backingMap.get() != null) { - return; - } - Map vars = new HashMap<>(); - for (Map.Entry entry : jsVariables.entrySet()) { - JavaScriptVariable jsVar = entry.getValue(); - String name; - if (className.endsWith("[]")) { - if (entry.getKey().equals("data")) { - name = entry.getKey(); - } else { - continue; - } - } else if (isNumeric(entry.getKey())) { - name = entry.getKey(); - } else { - name = debugger.mapField(className, entry.getKey()); - if (name == null) { - continue; - } - } - Value value = new Value(debugger, debugInformation, jsVar.getValue()); - vars.put(name, new Variable(name, value)); - } - backingMap.compareAndSet(null, vars); - } - - private boolean isNumeric(String str) { - for (int i = 0; i < str.length(); ++i) { - char c = str.charAt(i); - if (c < '0' || c > '9') { - return false; - } - } - return true; - } -} diff --git a/core/src/main/java/org/teavm/debugging/Value.java b/core/src/main/java/org/teavm/debugging/Value.java index 2ee0330f9..93e1d9f84 100644 --- a/core/src/main/java/org/teavm/debugging/Value.java +++ b/core/src/main/java/org/teavm/debugging/Value.java @@ -15,16 +15,19 @@ */ package org.teavm.debugging; +import java.util.HashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; +import org.teavm.common.Promise; import org.teavm.debugging.information.DebugInformation; import org.teavm.debugging.javascript.JavaScriptValue; +import org.teavm.debugging.javascript.JavaScriptVariable; public class Value { private Debugger debugger; private DebugInformation debugInformation; private JavaScriptValue jsValue; - private AtomicReference properties = new AtomicReference<>(); + private Promise> properties; + private Promise type; Value(Debugger debugger, DebugInformation debugInformation, JavaScriptValue jsValue) { this.debugger = debugger; @@ -32,30 +35,66 @@ public class Value { this.jsValue = jsValue; } - public String getRepresentation() { + private static boolean isNumeric(String str) { + for (int i = 0; i < str.length(); ++i) { + char c = str.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return true; + } + + public Promise getRepresentation() { return jsValue.getRepresentation(); } - public String getType() { - String className = jsValue.getClassName(); - if (className.startsWith("a/")) { - className = className.substring(2); - String javaClassName = debugInformation.getClassNameByJsName(className); - if (javaClassName != null) { - className = javaClassName; - } - } else if (className.startsWith("@")) { - className = className.substring(1); + public Promise getType() { + if (type == null) { + type = jsValue.getClassName().then(className -> { + if (className.startsWith("a/")) { + className = className.substring(2); + String javaClassName = debugInformation.getClassNameByJsName(className); + if (javaClassName != null) { + className = javaClassName; + } + } + return className; + }); } - return className; + return type; } - public Map getProperties() { - if (properties.get() == null) { - properties.compareAndSet(null, new PropertyMap(jsValue.getClassName(), jsValue.getProperties(), debugger, - debugInformation)); + public Promise> getProperties() { + if (properties == null) { + properties = jsValue.getProperties().thenAsync(jsVariables -> { + return jsValue.getClassName().then(className -> { + Map vars = new HashMap<>(); + for (Map.Entry entry : jsVariables.entrySet()) { + JavaScriptVariable jsVar = entry.getValue(); + String name; + if (className.endsWith("[]")) { + if (entry.getKey().equals("data")) { + name = entry.getKey(); + } else { + continue; + } + } else if (isNumeric(entry.getKey())) { + name = entry.getKey(); + } else { + name = debugger.mapField(className, entry.getKey()); + if (name == null) { + continue; + } + } + Value value = new Value(debugger, debugInformation, jsVar.getValue()); + vars.put(name, new Variable(name, value)); + } + return vars; + }); + }); } - return properties.get(); + return properties; } public boolean hasInnerStructure() { diff --git a/core/src/main/java/org/teavm/debugging/VariableMap.java b/core/src/main/java/org/teavm/debugging/VariableMap.java deleted file mode 100644 index e3e65944f..000000000 --- a/core/src/main/java/org/teavm/debugging/VariableMap.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.AbstractMap; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import org.teavm.debugging.information.DebugInformation; -import org.teavm.debugging.javascript.JavaScriptLocation; -import org.teavm.debugging.javascript.JavaScriptVariable; - -class VariableMap extends AbstractMap { - private AtomicReference> backingMap = new AtomicReference<>(); - private Map jsVariables; - private Debugger debugger; - private DebugInformation debugInformation; - private JavaScriptLocation location; - - public VariableMap(Map jsVariables, Debugger debugger, - DebugInformation debugInformation, JavaScriptLocation location) { - this.jsVariables = jsVariables; - this.debugger = debugger; - this.debugInformation = debugInformation; - this.location = location; - } - - @Override - public Set> entrySet() { - updateBackingMap(); - return backingMap.get().entrySet(); - } - - @Override - public Variable get(Object key) { - updateBackingMap(); - return backingMap.get().get(key); - } - - @Override - public int size() { - updateBackingMap(); - return backingMap.get().size(); - } - - private void updateBackingMap() { - if (backingMap.get() != null) { - return; - } - Map vars = new HashMap<>(); - for (Map.Entry entry : jsVariables.entrySet()) { - JavaScriptVariable jsVar = entry.getValue(); - String[] names = debugger.mapVariable(entry.getKey(), location); - Value value = new Value(debugger, debugInformation, jsVar.getValue()); - for (String name : names) { - if (name == null) { - name = "js:" + jsVar.getName(); - } - vars.put(name, new Variable(name, value)); - } - } - backingMap.compareAndSet(null, vars); - } -} diff --git a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptBreakpoint.java b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptBreakpoint.java index 9b73552bc..9b8f17398 100644 --- a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptBreakpoint.java +++ b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptBreakpoint.java @@ -15,10 +15,12 @@ */ package org.teavm.debugging.javascript; +import org.teavm.common.Promise; + public interface JavaScriptBreakpoint { JavaScriptLocation getLocation(); boolean isValid(); - void destroy(); + Promise destroy(); } diff --git a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptCallFrame.java b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptCallFrame.java index 141f46d15..c27c1d14c 100644 --- a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptCallFrame.java +++ b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptCallFrame.java @@ -16,13 +16,14 @@ package org.teavm.debugging.javascript; import java.util.Map; +import org.teavm.common.Promise; public interface JavaScriptCallFrame { JavaScriptDebugger getDebugger(); JavaScriptLocation getLocation(); - Map getVariables(); + Promise> getVariables(); JavaScriptValue getThisVariable(); 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 f03406950..2ab3ea8c1 100644 --- a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptDebugger.java +++ b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptDebugger.java @@ -15,22 +15,24 @@ */ package org.teavm.debugging.javascript; +import org.teavm.common.Promise; + public interface JavaScriptDebugger { void addListener(JavaScriptDebuggerListener listener); void removeListener(JavaScriptDebuggerListener listener); - void suspend(); + Promise suspend(); - void resume(); + Promise resume(); - void stepInto(); + Promise stepInto(); - void stepOut(); + Promise stepOut(); - void stepOver(); + Promise stepOver(); - void continueToLocation(JavaScriptLocation location); + Promise continueToLocation(JavaScriptLocation location); boolean isSuspended(); @@ -40,5 +42,5 @@ public interface JavaScriptDebugger { JavaScriptCallFrame[] getCallStack(); - JavaScriptBreakpoint createBreakpoint(JavaScriptLocation location); + Promise createBreakpoint(JavaScriptLocation location); } diff --git a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptValue.java b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptValue.java index eb7e3b42f..5ead7ec78 100644 --- a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptValue.java +++ b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptValue.java @@ -16,13 +16,14 @@ package org.teavm.debugging.javascript; import java.util.Map; +import org.teavm.common.Promise; public interface JavaScriptValue { - String getRepresentation(); + Promise getRepresentation(); - String getClassName(); + Promise getClassName(); - Map getProperties(); + Promise> getProperties(); boolean hasInnerStructure(); diff --git a/samples/benchmark/src/main/java/org/teavm/samples/benchmark/shared/Scene.java b/samples/benchmark/src/main/java/org/teavm/samples/benchmark/shared/Scene.java index 239039612..de098dbe3 100644 --- a/samples/benchmark/src/main/java/org/teavm/samples/benchmark/shared/Scene.java +++ b/samples/benchmark/src/main/java/org/teavm/samples/benchmark/shared/Scene.java @@ -26,7 +26,8 @@ public class Scene { private Body axis; private Body reel; private long lastCalculated; - private long startTime; + private long relativeTime; + private boolean hasUnfinishedComputations; public Scene() { world = new World(new Vec2(0, -9.8f)); @@ -35,7 +36,6 @@ public class Scene { joinReelToAxis(); initBalls(); lastCalculated = System.currentTimeMillis(); - startTime = lastCalculated; } private void initAxis() { @@ -133,18 +133,27 @@ public class Scene { public void calculate() { long currentTime = System.currentTimeMillis(); - int timeToCalculate = (int) (currentTime - lastCalculated); - long relativeTime = currentTime - startTime; + long timeToCalculate = currentTime - lastCalculated; + int count = 5; while (timeToCalculate > 10) { int period = (int) ((relativeTime + 5000) / 10000); reel.applyTorque(period % 2 == 0 ? 8f : -8f); world.step(0.01f, 20, 40); lastCalculated += 10; timeToCalculate -= 10; + relativeTime += 10; + if (count-- == 0) { + hasUnfinishedComputations = true; + return; + } } + hasUnfinishedComputations = false; } public int timeUntilNextStep() { + if (hasUnfinishedComputations) { + return 0; + } return (int) Math.max(0, lastCalculated + 10 - System.currentTimeMillis()); } diff --git a/samples/benchmark/src/main/java/org/teavm/samples/benchmark/teavm/BenchmarkStarter.java b/samples/benchmark/src/main/java/org/teavm/samples/benchmark/teavm/BenchmarkStarter.java index 78b207c0e..bfdc4ef11 100644 --- a/samples/benchmark/src/main/java/org/teavm/samples/benchmark/teavm/BenchmarkStarter.java +++ b/samples/benchmark/src/main/java/org/teavm/samples/benchmark/teavm/BenchmarkStarter.java @@ -44,7 +44,6 @@ public final class BenchmarkStarter { private static double timeSpentCalculating; private static double totalTime; - private BenchmarkStarter() { } 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 7b0778edb..6b96b92f4 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 @@ -17,20 +17,21 @@ package org.teavm.chromerdp; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.NullNode; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; -import java.util.concurrent.CompletableFuture; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.teavm.chromerdp.data.CallArgumentDTO; @@ -54,32 +55,41 @@ import org.teavm.chromerdp.messages.ScriptParsedNotification; import org.teavm.chromerdp.messages.SetBreakpointCommand; import org.teavm.chromerdp.messages.SetBreakpointResponse; import org.teavm.chromerdp.messages.SuspendedNotification; +import org.teavm.common.CompletablePromise; +import org.teavm.common.Promise; 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.JavaScriptLocation; +import org.teavm.debugging.javascript.JavaScriptVariable; public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeConsumer { private static final Logger logger = LoggerFactory.getLogger(ChromeRDPDebugger.class); - private static final Object dummy = new Object(); - private ChromeRDPExchange exchange; - private ConcurrentMap listeners = new ConcurrentHashMap<>(); - private ConcurrentMap breakpointLocationMap = new ConcurrentHashMap<>(); - private ConcurrentMap breakpoints = new ConcurrentHashMap<>(); - private ConcurrentMap breakpointsByChromeId = new ConcurrentHashMap<>(); + private static final Promise> EMPTY_SCOPE = + Promise.of(Collections.emptyMap()); + private volatile ChromeRDPExchange exchange; + private Set listeners = new LinkedHashSet<>(); + private Map breakpointLocationMap = new HashMap<>(); + private Set breakpoints = new LinkedHashSet<>(); + private Map breakpointsByChromeId = new HashMap<>(); private volatile RDPCallFrame[] callStack = new RDPCallFrame[0]; - private ConcurrentMap scripts = new ConcurrentHashMap<>(); - private ConcurrentMap scriptIds = new ConcurrentHashMap<>(); - private boolean suspended; + private Map scripts = new HashMap<>(); + private Map scriptIds = new HashMap<>(); + private volatile boolean suspended; private ObjectMapper mapper = new ObjectMapper(); private ConcurrentMap> responseHandlers = new ConcurrentHashMap<>(); - private ConcurrentMap> futures = new ConcurrentHashMap<>(); + private ConcurrentMap> promises = new ConcurrentHashMap<>(); private AtomicInteger messageIdGenerator = new AtomicInteger(); - private Lock breakpointLock = new ReentrantLock(); private List getListeners() { - return new ArrayList<>(listeners.keySet()); + return new ArrayList<>(listeners); + } + + private Executor executor; + + public ChromeRDPDebugger(Executor executor) { + this.executor = executor; } @Override @@ -92,8 +102,8 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC } this.exchange = exchange; if (exchange != null) { - for (RDPBreakpoint breakpoint : breakpoints.keySet().toArray(new RDPBreakpoint[0])) { - updateBreakpoint(breakpoint); + for (RDPBreakpoint breakpoint : breakpoints.toArray(new RDPBreakpoint[0])) { + updateBreakpoint(breakpoint.nativeBreakpoint); } for (JavaScriptDebuggerListener listener : getListeners()) { listener.attached(); @@ -110,70 +120,77 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC } } - private void injectFunctions(int contextId) { - callMethod("Runtime.enable", void.class, null); - - CompileScriptCommand compileParams = new CompileScriptCommand(); - compileParams.expression = "$dbg_class = function(obj) { return typeof obj === 'object' && obj != null " - + "? obj.__teavm_class__() : null };"; - compileParams.sourceURL = "file://fake"; - compileParams.persistScript = true; - compileParams.executionContextId = contextId; - CompileScriptResponse response = callMethod("Runtime.compileScript", CompileScriptResponse.class, - compileParams); - - RunScriptCommand runParams = new RunScriptCommand(); - runParams.scriptId = response.scriptId; - callMethod("Runtime.runScript", void.class, runParams); + private Promise injectFunctions(int contextId) { + return callMethodAsync("Runtime.enable", void.class, null) + .thenAsync(v -> { + CompileScriptCommand compileParams = new CompileScriptCommand(); + compileParams.expression = "$dbg_class = function(obj) { return typeof obj === 'object' " + + "&& obj !== null && '__teavm_class__' in obj ? obj.__teavm_class__() : null; };\n" + + "$dbg_repr = function(obj) { return typeof obj === 'object' " + + "&& obj !== null && 'toString' in obj ? obj.toString() : null; }\n"; + compileParams.sourceURL = "file://fake"; + compileParams.persistScript = true; + compileParams.executionContextId = contextId; + return callMethodAsync("Runtime.compileScript", CompileScriptResponse.class, compileParams); + }) + .thenAsync(response -> { + RunScriptCommand runParams = new RunScriptCommand(); + runParams.scriptId = response.scriptId; + return callMethodAsync("Runtime.runScript", void.class, runParams); + }); } - private ChromeRDPExchangeListener exchangeListener = this::receiveMessage; + private ChromeRDPExchangeListener exchangeListener = messageText -> { + callInExecutor(() -> receiveMessage(messageText) + .catchError(e -> { + logger.error("Error handling message", e); + return null; + })); + }; - private void receiveMessage(String messageText) { - new Thread(() -> { - try { - JsonNode jsonMessage = mapper.readTree(messageText); - if (jsonMessage.has("id")) { - Response response = mapper.reader(Response.class).readValue(jsonMessage); - if (response.getError() != null) { - if (logger.isWarnEnabled()) { - logger.warn("Error message #{} received from browser: {}", jsonMessage.get("id"), - response.getError().toString()); - } - } - CompletableFuture future = futures.remove(response.getId()); - try { - responseHandlers.remove(response.getId()).received(response.getResult(), future); - } catch (RuntimeException e) { - logger.warn("Error processing message ${}", response.getId(), e); - future.completeExceptionally(e); - } - } else { - Message message = mapper.readerFor(Message.class).readValue(messageText); - if (message.getMethod() == null) { - return; - } - switch (message.getMethod()) { - case "Debugger.paused": - firePaused(parseJson(SuspendedNotification.class, message.getParams())); - break; - case "Debugger.resumed": - fireResumed(); - break; - case "Debugger.scriptParsed": - scriptParsed(parseJson(ScriptParsedNotification.class, message.getParams())); - break; + private Promise receiveMessage(String messageText) { + try { + JsonNode jsonMessage = mapper.readTree(messageText); + if (jsonMessage.has("id")) { + Response response = mapper.readerFor(Response.class).readValue(jsonMessage); + if (response.getError() != null) { + if (logger.isWarnEnabled()) { + logger.warn("Error message #{} received from browser: {}", jsonMessage.get("id"), + response.getError().toString()); } } - } catch (Exception e) { - if (logger.isErrorEnabled()) { - logger.error("Error receiving message from Google Chrome", e); + CompletablePromise promise = promises.remove(response.getId()); + try { + responseHandlers.remove(response.getId()).received(response.getResult(), promise); + } catch (RuntimeException e) { + logger.warn("Error processing message ${}", response.getId(), e); + promise.completeWithError(e); } + return Promise.VOID; + } else { + Message message = mapper.readerFor(Message.class).readValue(messageText); + if (message.getMethod() == null) { + return Promise.VOID; + } + switch (message.getMethod()) { + case "Debugger.paused": + return firePaused(parseJson(SuspendedNotification.class, message.getParams())); + case "Debugger.resumed": + return fireResumed(); + case "Debugger.scriptParsed": + return scriptParsed(parseJson(ScriptParsedNotification.class, message.getParams())); + } + return Promise.VOID; } - }).start(); + } catch (Exception e) { + if (logger.isErrorEnabled()) { + logger.error("Error receiving message from Google Chrome", e); + } + return Promise.VOID; + } } - private synchronized void firePaused(SuspendedNotification params) { + private Promise firePaused(SuspendedNotification params) { suspended = true; CallFrameDTO[] callFrameDTOs = params.getCallFrames(); RDPCallFrame[] callStack = new RDPCallFrame[callFrameDTOs.length]; @@ -182,41 +199,48 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC } this.callStack = callStack; - RDPBreakpoint breakpoint = null; + RDPNativeBreakpoint nativeBreakpoint = null; if (params.getHitBreakpoints() != null && !params.getHitBreakpoints().isEmpty()) { - breakpoint = breakpointsByChromeId.get(params.getHitBreakpoints().get(0)); + nativeBreakpoint = breakpointsByChromeId.get(params.getHitBreakpoints().get(0)); } + RDPBreakpoint breakpoint = !nativeBreakpoint.breakpoints.isEmpty() + ? nativeBreakpoint.breakpoints.iterator().next() + : null; for (JavaScriptDebuggerListener listener : getListeners()) { listener.paused(breakpoint); } + + return Promise.VOID; } - private synchronized void fireResumed() { + private Promise fireResumed() { suspended = false; callStack = null; for (JavaScriptDebuggerListener listener : getListeners()) { listener.resumed(); } + + return Promise.VOID; } - private synchronized void scriptParsed(ScriptParsedNotification params) { + private Promise scriptParsed(ScriptParsedNotification params) { if (scripts.putIfAbsent(params.getScriptId(), params.getUrl()) != null) { - return; + return Promise.VOID; } if (params.getUrl().equals("file://fake")) { - return; + return Promise.VOID; } scriptIds.put(params.getUrl(), params.getScriptId()); for (JavaScriptDebuggerListener listener : getListeners()) { listener.scriptAdded(params.getUrl()); } - injectFunctions(params.getExecutionContextId()); + return injectFunctions(params.getExecutionContextId()); } @Override public void addListener(JavaScriptDebuggerListener listener) { - listeners.put(listener, dummy); + listeners.add(listener); } @Override @@ -225,35 +249,35 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC } @Override - public void suspend() { - callMethod("Debugger.pause", void.class, null); + public Promise suspend() { + return callMethodAsync("Debugger.pause", void.class, null); } @Override - public void resume() { - callMethod("Debugger.resume", void.class, null); + public Promise resume() { + return callMethodAsync("Debugger.resume", void.class, null); } @Override - public void stepInto() { - callMethod("Debugger.stepInto", void.class, null); + public Promise stepInto() { + return callMethodAsync("Debugger.stepInto", void.class, null); } @Override - public void stepOut() { - callMethod("Debugger.stepOut", void.class, null); + public Promise stepOut() { + return callMethodAsync("Debugger.stepOut", void.class, null); } @Override - public void stepOver() { - callMethod("Debugger.stepOver", void.class, null); + public Promise stepOver() { + return callMethodAsync("Debugger.stepOver", void.class, null); } @Override - public void continueToLocation(JavaScriptLocation location) { + public Promise continueToLocation(JavaScriptLocation location) { ContinueToLocationCommand params = new ContinueToLocationCommand(); params.setLocation(unmap(location)); - callMethod("Debugger.continueToLocation", void.class, params); + return callMethodAsync("Debugger.continueToLocation", void.class, params); } @Override @@ -283,70 +307,75 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC } @Override - public JavaScriptBreakpoint createBreakpoint(JavaScriptLocation location) { - RDPBreakpoint breakpoint; + public Promise createBreakpoint(JavaScriptLocation location) { + RDPBreakpoint breakpoint = new RDPBreakpoint(this); + breakpoint.nativeBreakpoint = lockNativeBreakpoint(location, breakpoint); + CompletablePromise result = new CompletablePromise<>(); + breakpoints.add(breakpoint); + breakpoint.nativeBreakpoint.initPromise.thenVoid(v -> result.complete(breakpoint)); + return result; + } - breakpointLock.lock(); - try { - breakpoint = breakpointLocationMap.get(location); - if (breakpoint == null) { - breakpoint = new RDPBreakpoint(this, location); - breakpointLocationMap.put(location, breakpoint); - updateBreakpoint(breakpoint); - } - breakpoint.referenceCount.incrementAndGet(); - breakpoints.put(breakpoint, dummy); - } finally { - breakpointLock.unlock(); + Promise destroyBreakpoint(RDPBreakpoint breakpoint) { + if (breakpoint.nativeBreakpoint == null) { + return Promise.VOID; + } + RDPNativeBreakpoint nativeBreakpoint = breakpoint.nativeBreakpoint; + breakpoint.nativeBreakpoint = null; + nativeBreakpoint.breakpoints.remove(breakpoint); + return releaseNativeBreakpoint(nativeBreakpoint, breakpoint); + } + + private RDPNativeBreakpoint lockNativeBreakpoint(JavaScriptLocation location, RDPBreakpoint bp) { + RDPNativeBreakpoint breakpoint; + + breakpoint = breakpointLocationMap.get(location); + if (breakpoint != null) { + breakpoint.breakpoints.add(bp); + return breakpoint; } + breakpoint = new RDPNativeBreakpoint(this, location); + breakpoint.breakpoints.add(bp); + breakpointLocationMap.put(location, breakpoint); + RDPNativeBreakpoint finalBreakpoint = breakpoint; + breakpoint.initPromise = updateBreakpoint(breakpoint).then(v -> { + checkBreakpoint(finalBreakpoint); + return null; + }); return breakpoint; } - void destroyBreakpoint(RDPBreakpoint breakpoint) { - if (breakpoint.referenceCount.decrementAndGet() > 0) { - return; + private Promise releaseNativeBreakpoint(RDPNativeBreakpoint breakpoint, RDPBreakpoint bp) { + breakpoint.breakpoints.remove(bp); + return checkBreakpoint(breakpoint); + } + + private Promise checkBreakpoint(RDPNativeBreakpoint breakpoint) { + if (!breakpoint.breakpoints.isEmpty()) { + return Promise.VOID; } - breakpointLock.lock(); - try { - if (breakpoint.referenceCount.get() > 0) { - return; - } + if (breakpointLocationMap.get(breakpoint.getLocation()) == breakpoint) { breakpointLocationMap.remove(breakpoint.getLocation()); - breakpoints.remove(breakpoint); - - if (breakpoint.chromeId == null) { - synchronized (breakpoint.updateMonitor) { - while (breakpoint.updating.get()) { - try { - breakpoint.updateMonitor.wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - } - } - } - - if (breakpoint.chromeId != null) { + } + if (breakpoint.destroyPromise == null) { + breakpoint.destroyPromise = breakpoint.initPromise.thenAsync(v -> { breakpointsByChromeId.remove(breakpoint.chromeId); if (logger.isInfoEnabled()) { logger.info("Removing breakpoint at {}", breakpoint.getLocation()); } RemoveBreakpointCommand params = new RemoveBreakpointCommand(); params.setBreakpointId(breakpoint.chromeId); - callMethod("Debugger.removeBreakpoint", void.class, params); - } + return callMethodAsync("Debugger.removeBreakpoint", void.class, params); + }); breakpoint.debugger = null; - breakpoint.chromeId = null; - } finally { - breakpointLock.unlock(); } + return breakpoint.destroyPromise; } - private void updateBreakpoint(final RDPBreakpoint breakpoint) { + private Promise updateBreakpoint(RDPNativeBreakpoint breakpoint) { if (breakpoint.chromeId != null) { - return; + return Promise.VOID; } SetBreakpointCommand params = new SetBreakpointCommand(); params.setLocation(unmap(breakpoint.getLocation())); @@ -355,46 +384,63 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC logger.info("Setting breakpoint at {}", breakpoint.getLocation()); } - breakpoint.updating.set(true); - try { - SetBreakpointResponse response = callMethod("Debugger.setBreakpoint", SetBreakpointResponse.class, params); - if (response != null) { - breakpoint.chromeId = response.getBreakpointId(); - if (breakpoint.chromeId != null) { - breakpointsByChromeId.put(breakpoint.chromeId, breakpoint); + return callMethodAsync("Debugger.setBreakpoint", SetBreakpointResponse.class, params) + .thenVoid(response -> { + if (response != null) { + breakpoint.chromeId = response.getBreakpointId(); + if (breakpoint.chromeId != null) { + breakpointsByChromeId.put(breakpoint.chromeId, breakpoint); + } + } else { + if (logger.isWarnEnabled()) { + logger.warn("Error setting breakpoint at {}", breakpoint.getLocation()); + } + breakpoint.chromeId = null; } - } else { - if (logger.isWarnEnabled()) { - logger.warn("Error setting breakpoint at {}", breakpoint.getLocation()); - } - breakpoint.chromeId = null; - } - } finally { - synchronized (breakpoint.updateMonitor) { - breakpoint.updating.set(false); - breakpoint.updateMonitor.notifyAll(); - } - } - for (JavaScriptDebuggerListener listener : getListeners()) { - listener.breakpointChanged(breakpoint); - } + for (RDPBreakpoint bp : breakpoint.breakpoints) { + for (JavaScriptDebuggerListener listener : getListeners()) { + listener.breakpointChanged(bp); + } + } + }); } - List getScope(String scopeId) { + Promise> getScope(String scopeId) { GetPropertiesCommand params = new GetPropertiesCommand(); params.setObjectId(scopeId); params.setOwnProperties(true); - GetPropertiesResponse response = callMethod("Runtime.getProperties", GetPropertiesResponse.class, params); - if (response == null) { - return Collections.emptyList(); - } + return callMethodAsync("Runtime.getProperties", GetPropertiesResponse.class, params) + .thenAsync(response -> { + if (response == null) { + return Promise.of(Collections.emptyList()); + } - return parseProperties(response.getResult()); + PropertyDescriptorDTO proto = Arrays.asList(response.getResult()).stream() + .filter(p -> p.getName().equals("__proto__")) + .findAny() + .orElse(null); + if (proto == null || proto.getValue() == null || proto.getValue().getObjectId() == null) { + return Promise.of(parseProperties(scopeId, response.getResult(), null)); + } + + GetPropertiesCommand protoParams = new GetPropertiesCommand(); + protoParams.setObjectId(proto.getValue().getObjectId()); + protoParams.setOwnProperties(false); + + return callMethodAsync("Runtime.getProperties", GetPropertiesResponse.class, protoParams) + .then(protoProperties -> { + PropertyDescriptorDTO[] getters = Arrays.asList(protoProperties.getResult()).stream() + .filter(p -> p.getGetter() != null && p.getValue() == null + && !p.getName().equals("__proto__")) + .toArray(PropertyDescriptorDTO[]::new); + return parseProperties(scopeId, response.getResult(), getters); + }); + }); } - String getClassName(String objectId) { + Promise getClassName(String objectId) { CallFunctionCommand params = new CallFunctionCommand(); CallArgumentDTO arg = new CallArgumentDTO(); arg.setObjectId(objectId); @@ -402,12 +448,14 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC params.setArguments(new CallArgumentDTO[] { arg }); params.setFunctionDeclaration("$dbg_class"); - CallFunctionResponse response = callMethod("Runtime.callFunctionOn", CallFunctionResponse.class, params); - RemoteObjectDTO result = response != null ? response.getResult() : null; - return result.getValue() != null ? result.getValue().textValue() : null; + return callMethodAsync("Runtime.callFunctionOn", CallFunctionResponse.class, params) + .then(response -> { + RemoteObjectDTO result = response != null ? response.getResult() : null; + return result.getValue() != null ? result.getValue().textValue() : null; + }); } - String getRepresentation(String objectId) { + Promise getRepresentation(String objectId) { CallFunctionCommand params = new CallFunctionCommand(); CallArgumentDTO arg = new CallArgumentDTO(); arg.setObjectId(objectId); @@ -415,42 +463,81 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC params.setArguments(new CallArgumentDTO[] { arg }); params.setFunctionDeclaration("$dbg_repr"); - CallFunctionResponse response = callMethod("Runtime.callFunctionOn", CallFunctionResponse.class, params); - RemoteObjectDTO result = response != null ? response.getResult() : null; - return result.getValue() != null ? result.getValue().textValue() : null; + return callMethodAsync("Runtime.callFunctionOn", CallFunctionResponse.class, params) + .then(response -> { + RemoteObjectDTO result = response != null ? response.getResult() : null; + return result.getValue() != null ? result.getValue().textValue() : null; + }); } - private List parseProperties(PropertyDescriptorDTO[] properties) { + private List parseProperties(String scopeId, PropertyDescriptorDTO[] properties, + PropertyDescriptorDTO[] getters) { List variables = new ArrayList<>(); if (properties != null) { for (PropertyDescriptorDTO property : properties) { RemoteObjectDTO remoteValue = property.getValue(); + RemoteObjectDTO getter = property.getGetter(); RDPValue value; if (remoteValue != null && remoteValue.getType() != null) { - switch (remoteValue.getType()) { - case "undefined": - value = new RDPValue(this, "undefined", "undefined", null, false); - break; - case "object": - case "function": - value = new RDPValue(this, null, remoteValue.getType(), remoteValue.getObjectId(), true); - break; - default: - value = new RDPValue(this, remoteValue.getValue().asText(), remoteValue.getType(), - remoteValue.getObjectId(), false); - break; - } + value = mapValue(remoteValue); + } else if (getter != null && getter.getObjectId() != null) { + value = mapValue(getter); } else { - value = new RDPValue(this, "null", "null", "null", false); + value = new RDPValue(this, "null", "null", null, false); } RDPLocalVariable var = new RDPLocalVariable(property.getName(), value); variables.add(var); } } + if (getters != null) { + for (PropertyDescriptorDTO property : getters) { + RDPValue value = new RDPValue(this, "", "@Function", scopeId, true); + value.getter = property.getGetter(); + RDPLocalVariable var = new RDPLocalVariable(property.getName(), value); + variables.add(var); + } + } return variables; } + Promise invokeGetter(String functionId, String objectId) { + CallFunctionCommand params = new CallFunctionCommand(); + params.setObjectId(functionId); + + CallArgumentDTO functionArg = new CallArgumentDTO(); + functionArg.setObjectId(functionId); + CallArgumentDTO arg = new CallArgumentDTO(); + arg.setObjectId(objectId); + + params.setArguments(new CallArgumentDTO[] { arg }); + params.setFunctionDeclaration("Function.prototype.call"); + + return callMethodAsync("Runtime.callFunctionOn", CallFunctionResponse.class, params) + .then(response -> { + RemoteObjectDTO result = response != null ? response.getResult() : null; + return result.getValue() != null ? mapValue(result) : null; + }); + } + + RDPValue mapValue(RemoteObjectDTO remoteValue) { + switch (remoteValue.getType()) { + case "undefined": + return new RDPValue(this, "undefined", "undefined", null, false); + case "object": + case "function": + if (remoteValue.getValue() instanceof NullNode) { + return new RDPValue(this, "null", "null", null, false); + } else { + return new RDPValue(this, null, remoteValue.getType(), remoteValue.getObjectId(), + true); + } + default: + return new RDPValue(this, remoteValue.getValue().asText(), remoteValue.getType(), + remoteValue.getObjectId(), false); + } + } + private T parseJson(Class type, JsonNode node) throws IOException { return mapper.readerFor(type).readValue(node); } @@ -485,7 +572,7 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC break; } } - return new RDPCallFrame(this, dto.getCallFrameId(), map(dto.getLocation()), new RDPScope(this, scopeId), + return new RDPCallFrame(this, dto.getCallFrameId(), map(dto.getLocation()), scopeId, thisObject, closure); } @@ -501,9 +588,9 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC return dto; } - private R callMethod(String method, Class returnType, Object params) { + private Promise callMethodAsync(String method, Class returnType, Object params) { if (exchange == null) { - return null; + return Promise.of(null); } Message message = new Message(); message.setId(messageIdGenerator.incrementAndGet()); @@ -512,7 +599,8 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC message.setParams(mapper.valueToTree(params)); } - CompletableFuture sync = setResponseHandler(message.getId(), (node, out) -> { + sendMessage(message); + return setResponseHandler(message.getId(), (JsonNode node, CompletablePromise out) -> { if (node == null) { out.complete(null); } else { @@ -520,41 +608,38 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC out.complete(response); } }); - sendMessage(message); - try { - return read(sync); - } catch (InterruptedException e) { - return null; - } catch (TimeoutException e) { - logger.warn("Chrome debug protocol: timed out", e); - return null; - } } @SuppressWarnings("unchecked") - private CompletableFuture setResponseHandler(int messageId, ResponseHandler handler) { - CompletableFuture future = new CompletableFuture<>(); - futures.put(messageId, (CompletableFuture) future); + private Promise setResponseHandler(int messageId, ResponseHandler handler) { + CompletablePromise promise = new CompletablePromise<>(); + promises.put(messageId, (CompletablePromise) promise); responseHandlers.put(messageId, (ResponseHandler) handler); - return future; + return promise; } interface ResponseHandler { - void received(JsonNode node, CompletableFuture out) throws IOException; + void received(JsonNode node, CompletablePromise out) throws IOException; } - private static T read(Future future) throws InterruptedException, TimeoutException { - try { - return future.get(1500, TimeUnit.MILLISECONDS); - } catch (ExecutionException e) { - Throwable cause = e.getCause(); - if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } else if (cause instanceof Error) { - throw (Error) cause; - } else { - throw new RuntimeException(cause); - } + Promise> createScope(String id) { + if (id == null) { + return EMPTY_SCOPE; } + return getScope(id).then(scope -> { + Map newBackingMap = new HashMap<>(); + for (RDPLocalVariable variable : scope) { + newBackingMap.put(variable.getName(), variable); + } + return Collections.unmodifiableMap(newBackingMap); + }); + } + + private Promise callInExecutor(Supplier> f) { + CompletablePromise result = new CompletablePromise<>(); + executor.execute(() -> { + f.get().thenVoid(result::complete).catchVoid(result::completeWithError); + }); + return result; } } 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 07815bc69..a7b025b04 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 @@ -23,14 +23,19 @@ import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.WeakHashMap; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; import java.util.stream.Collectors; +import org.teavm.common.Promise; import org.teavm.debugging.Breakpoint; import org.teavm.debugging.CallFrame; import org.teavm.debugging.Debugger; 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.JavaScriptVariable; public final class ChromeRDPRunner { private ChromeRDPServer server; @@ -38,14 +43,12 @@ public final class ChromeRDPRunner { private Map breakpointIds = new WeakHashMap<>(); private int currentFrame; private int breakpointIdGen; - private volatile Runnable attachListener; - private volatile Runnable suspendListener; - private volatile Runnable resumeListener; + BlockingQueue queue = new LinkedBlockingQueue<>(); private ChromeRDPRunner() { server = new ChromeRDPServer(); server.setPort(2357); - ChromeRDPDebugger jsDebugger = new ChromeRDPDebugger(); + ChromeRDPDebugger jsDebugger = new ChromeRDPDebugger(queue::offer); server.setExchangeConsumer(jsDebugger); new Thread(server::start).start(); @@ -61,9 +64,6 @@ public final class ChromeRDPRunner { private DebuggerListener listener = new DebuggerListener() { @Override public void resumed() { - if (resumeListener != null) { - resumeListener.run(); - } } @Override @@ -77,9 +77,6 @@ public final class ChromeRDPRunner { System.out.println("Breakpoint #" + breakpointIds.get(breakpoint) + " hit"); } currentFrame = 0; - if (suspendListener != null) { - suspendListener.run(); - } } @Override @@ -88,9 +85,6 @@ public final class ChromeRDPRunner { @Override public void attached() { - if (attachListener != null) { - attachListener.run(); - } } @Override @@ -98,7 +92,7 @@ public final class ChromeRDPRunner { } }; - public static void main(String[] args) throws IOException { + public static void main(String[] args) { ChromeRDPRunner runner = new ChromeRDPRunner(); try { runner.acceptInput(); @@ -107,145 +101,206 @@ public final class ChromeRDPRunner { } } - public void acceptInput() throws IOException, InterruptedException { - if (!debugger.isAttached()) { + public void acceptInput() throws InterruptedException { + boolean wasAttached = debugger.isAttached(); + if (!wasAttached) { System.out.println("Waiting for remote process to attach..."); - CountDownLatch latch = new CountDownLatch(1); - attachListener = latch::countDown; - if (!debugger.isAttached()) { - try { - latch.await(); - } catch (InterruptedException e) { - return; - } - } - attachListener = null; - System.out.println("Attached"); } + while (true) { + queue.take().run(); + if (debugger.isAttached() && !wasAttached) { + wasAttached = true; + System.out.println("Attached"); + new Thread(() -> { + try { + stdinThread(); + } catch (IOException e) { + e.printStackTrace(); + } + }).start(); + } else if (!debugger.isAttached() && wasAttached) { + break; + } + } + + queue.offer(() -> { + debugger.detach(); + server.stop(); + }); + } + + private void stdinThread() throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); - loop: while (true) { + while (true) { System.out.print("> "); String line = reader.readLine(); if (line == null) { break; } - line = line.trim(); - String[] parts = Arrays.stream(line.split(" +")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .toArray(String[]::new); - if (parts.length == 0) { - continue; - } - - switch (parts[0]) { - case "suspend": - if (debugger.isSuspended()) { - System.out.println("Suspend command is only available when program is running"); - } else { - CountDownLatch latch = new CountDownLatch(1); - suspendListener = latch::countDown; - debugger.suspend(); - latch.await(); - suspendListener = null; - } + BlockingQueue callbackQueue = new ArrayBlockingQueue<>(1); + queue.add(() -> { + processSingleCommand(line).then(r -> callbackQueue.offer(r)).catchError(e -> { + e.printStackTrace(); + return true; + }); + }); + try { + if (!callbackQueue.take()) { break; - - case "detach": - break loop; - - case "continue": - case "cont": - case "c": - suspended(parts, resumeCommand); - break; - - case "breakpoint": - case "break": - case "br": - case "bp": - breakpointCommand.execute(parts); - break; - - case "backtrace": - case "bt": - suspended(parts, backtraceCommand); - break; - - case "frame": - case "fr": - case "f": - suspended(parts, frameCommand); - break; - - case "step": - case "s": - suspended(parts, stepCommand); - break; - - case "next": - case "n": - suspended(parts, nextCommand); - break; - - case "out": - case "o": - suspended(parts, outCommand); - break; - - case "info": - suspended(parts, infoCommand); - break; - - default: - System.out.println("Unknown command"); + } + } catch (InterruptedException e) { + break; } } - - debugger.detach(); - server.stop(); } - private void suspended(String[] arguments, Command command) throws InterruptedException { + private Promise processSingleCommand(String line) { + line = line.trim(); + String[] parts = Arrays.stream(line.split(" +")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toArray(String[]::new); + if (parts.length == 0) { + return Promise.of(true); + } + + switch (parts[0]) { + case "suspend": + if (debugger.isSuspended()) { + System.out.println("Suspend command is only available when program is running"); + return Promise.of(true); + } else { + return debugger.suspend().then(v -> true); + } + + case "detach": + return Promise.of(false); + + case "continue": + case "cont": + case "c": + return suspended(parts, resumeCommand); + + case "breakpoint": + case "break": + case "br": + case "bp": + return breakpointCommand.execute(parts).then(v -> true); + + case "backtrace": + case "bt": + return suspended(parts, backtraceCommand); + + case "frame": + case "fr": + case "f": + return suspended(parts, frameCommand); + + case "step": + case "s": + return suspended(parts, stepCommand); + + case "next": + case "n": + return suspended(parts, nextCommand); + + case "out": + case "o": + return suspended(parts, outCommand); + + case "info": + return suspended(parts, infoCommand); + + case "print": + case "p": + return suspended(parts, printCommand); + + default: + System.out.println("Unknown command"); + return Promise.of(true); + } + } + + private Promise suspended(String[] arguments, Command command) { if (!debugger.isSuspended()) { System.out.println("This command is only available when remote process is suspended"); - return; + return Promise.of(true); } - command.execute(arguments); + return command.execute(arguments).then(v -> true); } - private Command resumeCommand = args -> { - CountDownLatch latch = new CountDownLatch(1); - resumeListener = latch::countDown; - debugger.resume(); - latch.await(); - resumeListener = null; - }; + private Command resumeCommand = args -> debugger.resume(); private Command breakpointCommand = args -> { - if (args.length != 3) { + if (args.length != 3 && args.length != 3) { System.out.println("Expected 2 arguments"); - return; + return Promise.VOID; + } + + if (args.length == 4) { + return tryResolveJsBreakpoint(args[1], Integer.parseInt(args[2]), Integer.parseInt(args[3])); } String[] fileNames = resolveFileName(args[1]); if (fileNames.length == 0) { - System.out.println("Unknown file: " + args[1]); - return; + return tryResolveJsBreakpoint(args[1], Integer.parseInt(args[2]), + args.length == 3 ? 1 : Integer.parseInt(args[3])); } else if (fileNames.length > 1) { System.out.println("Ambiguous file name: " + args[1] + ". Possible names are: " + Arrays.toString(fileNames)); - return; + return Promise.VOID; } - Breakpoint bp = debugger.createBreakpoint(fileNames[0], Integer.parseInt(args[2])); - int id = breakpointIdGen++; - breakpointIds.put(bp, id); - System.out.println("Breakpoint #" + id + " was set at " + bp.getLocation()); + return debugger.createBreakpoint(fileNames[0], Integer.parseInt(args[2])).thenVoid(bp -> { + int id = breakpointIdGen++; + breakpointIds.put(bp, id); + System.out.println("Breakpoint #" + id + " was set at " + bp.getLocation()); + }); }; + private Promise tryResolveJsBreakpoint(String fileName, int lineNumber, int columnNumber) { + String[] fileNames = resolveJsFileName(fileName); + if (fileNames.length == 0) { + System.out.println("Unknown file: " + fileName); + return Promise.VOID; + } else if (fileNames.length > 1) { + System.out.println("Ambiguous file name: " + fileName + ". Possible names are: " + + Arrays.toString(fileNames)); + return Promise.VOID; + } + + JavaScriptLocation location = new JavaScriptLocation(fileNames[0], lineNumber - 1, columnNumber - 1); + return debugger.getJavaScriptDebugger().createBreakpoint(location).thenVoid(bp -> { + System.out.println("Native breakpoint was set at " + bp.getLocation()); + }); + } + + 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 String[] resolveFileName(String fileName) { if (debugger.getSourceFiles().contains(fileName)) { return new String[] { fileName }; @@ -294,20 +349,22 @@ public final class ChromeRDPRunner { } System.out.println(sb.toString()); } + return Promise.VOID; }; private Command frameCommand = args -> { if (args.length != 2) { System.out.println("Expected 1 argument"); - return; + return Promise.VOID; } int index = Integer.parseInt(args[1]); int max = debugger.getCallStack().length - 1; if (index < 0 || index > max) { System.out.println("Given frame index is outside of valid range 0.." + max); - return; + return Promise.VOID; } currentFrame = index; + return Promise.VOID; }; private Command stepCommand = args -> debugger.stepInto(); @@ -319,7 +376,7 @@ public final class ChromeRDPRunner { private Command infoCommand = args -> { if (args.length != 2) { System.out.println("Expected 1 argument"); - return; + return Promise.VOID; } switch (args[1]) { @@ -331,25 +388,119 @@ public final class ChromeRDPRunner { int id = breakpointIds.get(breakpoint); System.out.println(" #" + id + ": " + breakpoint.getLocation()); } - break; + return Promise.VOID; } case "variables": { CallFrame frame = debugger.getCallStack()[currentFrame]; - for (Variable var : frame.getVariables().values().stream() - .sorted(Comparator.comparing(Variable::getName)) - .collect(Collectors.toList())) { - System.out.println(" " + var.getName() + ": " + var.getValue().getType()); - } - break; + return printScope(frame.getVariables()); } default: System.out.println("Invalid argument"); + return Promise.VOID; } }; + private Command printCommand = args -> { + if (args.length != 2) { + System.out.println("Expected 1 argument"); + return Promise.VOID; + } + + String[] path = args[1].split("\\."); + return followPath(path, 0, debugger.getCallStack()[currentFrame].getVariables()); + }; + + private Promise followPath(String[] path, int index, Promise> scope) { + String elem = path[index]; + return scope.thenAsync(map -> { + Variable var = map.get(elem); + if (var != null) { + if (index == path.length - 1) { + return variableToString(var) + .thenVoid(str -> System.out.println(str)) + .thenAsync(v -> var.getValue().getType().thenAsync(type -> type.startsWith("@") + ? printJsScope(var.getValue().getOriginalValue().getProperties()) + : printScope(var.getValue().getProperties()))); + } else { + return var.getValue().getType().thenAsync(type -> type.startsWith("@") + ? followJsPath(path, index + 1, var.getValue().getOriginalValue().getProperties()) + : followPath(path, index + 1, var.getValue().getProperties())); + } + } else { + System.out.println("Invalid path specified"); + return Promise.VOID; + } + }); + } + + private Promise followJsPath(String[] path, int index, + Promise> scope) { + String elem = path[index]; + return scope.thenAsync(map -> { + JavaScriptVariable var = map.get(elem); + if (var != null) { + if (index == path.length - 1) { + return jsVariableToString(var) + .thenVoid(str -> System.out.println(str)) + .thenAsync(v -> printJsScope(var.getValue().getProperties())); + } else { + return followJsPath(path, index + 1, var.getValue().getProperties()); + } + } else { + System.out.println("Invalid path specified"); + return Promise.VOID; + } + }); + } + + private Promise printScope(Promise> scope) { + return scope + .then(vars -> vars.values()) + .then(vars -> vars.stream() + .sorted(Comparator.comparing(Variable::getName)) + .map(this::variableToString) + .collect(Collectors.toList()) + ) + .thenAsync(Promise::all) + .thenVoid(vars -> { + for (String var : vars) { + System.out.println(" " + var); + } + }); + } + + private Promise printJsScope(Promise> scope) { + return scope + .then(vars -> vars.values()) + .then(vars -> vars.stream() + .sorted(Comparator.comparing(JavaScriptVariable::getName)) + .map(this::jsVariableToString) + .collect(Collectors.toList()) + ) + .thenAsync(Promise::all) + .thenVoid(vars -> { + for (String var : vars) { + System.out.println(" " + var); + } + }); + } + + private Promise variableToString(Variable variable) { + return variable.getValue().getType() + .thenAsync(type -> variable.getValue().getRepresentation() + .then(repr -> variable.getName() + ": " + type + " (" + repr + ")")); + } + + + private Promise jsVariableToString(JavaScriptVariable variable) { + return variable.getValue().getClassName() + .thenAsync(type -> variable.getValue().getRepresentation() + .then(repr -> variable.getName() + ": " + type + " (" + repr + ")")); + } + private interface Command { - void execute(String[] args) throws InterruptedException; + Promise execute(String[] args); } } diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPBreakpoint.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPBreakpoint.java index be1cde013..18eb08f97 100644 --- a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPBreakpoint.java +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPBreakpoint.java @@ -15,38 +15,35 @@ */ package org.teavm.chromerdp; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; +import org.teavm.common.Promise; import org.teavm.debugging.javascript.JavaScriptBreakpoint; import org.teavm.debugging.javascript.JavaScriptLocation; class RDPBreakpoint implements JavaScriptBreakpoint { - volatile String chromeId; ChromeRDPDebugger debugger; - private JavaScriptLocation location; - AtomicInteger referenceCount = new AtomicInteger(); - final Object updateMonitor = new Object(); - AtomicBoolean updating = new AtomicBoolean(true); + RDPNativeBreakpoint nativeBreakpoint; + Promise destroyPromise; - RDPBreakpoint(ChromeRDPDebugger debugger, JavaScriptLocation location) { + RDPBreakpoint(ChromeRDPDebugger debugger) { this.debugger = debugger; - this.location = location; } @Override public JavaScriptLocation getLocation() { - return location; + return nativeBreakpoint.getLocation(); } @Override - public void destroy() { - if (debugger != null) { - debugger.destroyBreakpoint(this); + public Promise destroy() { + if (destroyPromise == null) { + destroyPromise = debugger.destroyBreakpoint(this); + debugger = null; } + return destroyPromise; } @Override public boolean isValid() { - return chromeId != null && debugger != null && debugger.isAttached(); + return nativeBreakpoint != null && nativeBreakpoint.isValid(); } } diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPCallFrame.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPCallFrame.java index ca4eccaf8..84b330e6c 100644 --- a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPCallFrame.java +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPCallFrame.java @@ -15,25 +15,29 @@ */ package org.teavm.chromerdp; -import java.util.Collections; import java.util.Map; -import org.teavm.debugging.javascript.*; +import org.teavm.common.Promise; +import org.teavm.debugging.javascript.JavaScriptCallFrame; +import org.teavm.debugging.javascript.JavaScriptDebugger; +import org.teavm.debugging.javascript.JavaScriptLocation; +import org.teavm.debugging.javascript.JavaScriptValue; +import org.teavm.debugging.javascript.JavaScriptVariable; class RDPCallFrame implements JavaScriptCallFrame { - private JavaScriptDebugger debugger; + private ChromeRDPDebugger debugger; private String chromeId; private JavaScriptLocation location; - private Map variables; + private Promise> variables; private JavaScriptValue thisObject; private JavaScriptValue closure; + private String scopeId; - RDPCallFrame(JavaScriptDebugger debugger, String chromeId, JavaScriptLocation location, - Map variables, JavaScriptValue thisObject, - JavaScriptValue closure) { + RDPCallFrame(ChromeRDPDebugger debugger, String chromeId, JavaScriptLocation location, String scopeId, + JavaScriptValue thisObject, JavaScriptValue closure) { this.debugger = debugger; this.chromeId = chromeId; this.location = location; - this.variables = Collections.unmodifiableMap(variables); + this.scopeId = scopeId; this.thisObject = thisObject; this.closure = closure; } @@ -48,7 +52,10 @@ class RDPCallFrame implements JavaScriptCallFrame { } @Override - public Map getVariables() { + public Promise> getVariables() { + if (variables == null) { + variables = debugger.createScope(scopeId); + } return variables; } diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPNativeBreakpoint.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPNativeBreakpoint.java new file mode 100644 index 000000000..38d388aad --- /dev/null +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPNativeBreakpoint.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 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.LinkedHashSet; +import java.util.Set; +import org.teavm.common.Promise; +import org.teavm.debugging.javascript.JavaScriptLocation; + +class RDPNativeBreakpoint { + volatile String chromeId; + ChromeRDPDebugger debugger; + private JavaScriptLocation location; + Promise initPromise; + Promise destroyPromise; + Set breakpoints = new LinkedHashSet<>(); + + RDPNativeBreakpoint(ChromeRDPDebugger debugger, JavaScriptLocation location) { + this.debugger = debugger; + this.location = location; + } + + public JavaScriptLocation getLocation() { + return location; + } + + public boolean isValid() { + return chromeId != null && debugger != null && debugger.isAttached(); + } +} diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPScope.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPScope.java deleted file mode 100644 index 6680ea0a6..000000000 --- a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPScope.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.chromerdp; - -import java.util.AbstractMap; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; - -class RDPScope extends AbstractMap { - private AtomicReference> backingMap = new AtomicReference<>(); - private ChromeRDPDebugger debugger; - private String id; - - RDPScope(ChromeRDPDebugger debugger, String id) { - this.debugger = debugger; - this.id = id; - } - - @Override - public Set> entrySet() { - initBackingMap(); - return backingMap.get().entrySet(); - } - - @Override - public int size() { - initBackingMap(); - return backingMap.get().size(); - } - - @Override - public RDPLocalVariable get(Object key) { - initBackingMap(); - return backingMap.get().get(key); - } - - private void initBackingMap() { - if (backingMap.get() != null) { - return; - } - Map newBackingMap = new HashMap<>(); - if (id != null) { - for (RDPLocalVariable variable : debugger.getScope(id)) { - newBackingMap.put(variable.getName(), variable); - } - } - backingMap.compareAndSet(null, newBackingMap); - } -} diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPValue.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPValue.java index 54af2fdf0..61f493842 100644 --- a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPValue.java +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPValue.java @@ -15,56 +15,77 @@ */ package org.teavm.chromerdp; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; +import org.teavm.chromerdp.data.RemoteObjectDTO; +import org.teavm.common.Promise; import org.teavm.debugging.javascript.JavaScriptValue; import org.teavm.debugging.javascript.JavaScriptVariable; class RDPValue implements JavaScriptValue { - private AtomicReference representation = new AtomicReference<>(); - private AtomicReference className = new AtomicReference<>(); - private String typeName; private ChromeRDPDebugger debugger; private String objectId; - private Map properties; + private Promise> properties; private boolean innerStructure; + private Promise className; + private Promise representation; + private final String defaultRepresentation; + private final String typeName; + RemoteObjectDTO getter; RDPValue(ChromeRDPDebugger debugger, String representation, String typeName, String objectId, boolean innerStructure) { - this.representation.set(representation == null && objectId == null ? "" : representation); - this.typeName = typeName; this.debugger = debugger; this.objectId = objectId; this.innerStructure = innerStructure; - properties = objectId != null ? new RDPScope(debugger, objectId) : Collections.emptyMap(); + this.typeName = typeName; + defaultRepresentation = representation; } @Override - public String getRepresentation() { - if (representation.get() == null) { - representation.compareAndSet(null, debugger.getRepresentation(objectId)); - } - return representation.get(); - } - - @Override - public String getClassName() { - if (className.get() == null) { + public Promise getRepresentation() { + if (representation == null) { if (objectId != null) { - String computedClassName = debugger.getClassName(objectId); - className.compareAndSet(null, computedClassName != null ? computedClassName : "@Object"); + representation = defaultRepresentation != null + ? Promise.of(defaultRepresentation) + : debugger.getRepresentation(objectId); } else { - className.compareAndSet(null, "@" + typeName); + representation = Promise.of(defaultRepresentation != null ? defaultRepresentation : ""); } } - return className.get(); + return representation; } - @SuppressWarnings("unchecked") @Override - public Map getProperties() { - return (Map) properties; + public Promise getClassName() { + if (className == null) { + if (objectId == null) { + className = Promise.of("@" + typeName); + } else { + className = debugger.getClassName(objectId).then(c -> c != null ? c : "@Object"); + } + } + return className; + } + + @Override + public Promise> getProperties() { + if (properties == null) { + if (getter == null) { + properties = debugger.createScope(objectId); + } else { + properties = debugger.invokeGetter(getter.getObjectId(), objectId).then(value -> { + if (value == null) { + value = new RDPValue(debugger, "null", "null", null, false); + } + Map map = new HashMap<>(); + map.put("", new RDPLocalVariable("", value)); + map.put("", new RDPLocalVariable("", debugger.mapValue(getter))); + return map; + }); + } + } + return properties; } @Override diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/data/PropertyDescriptorDTO.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/data/PropertyDescriptorDTO.java index 28f157431..66edb1784 100644 --- a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/data/PropertyDescriptorDTO.java +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/data/PropertyDescriptorDTO.java @@ -16,11 +16,13 @@ package org.teavm.chromerdp.data; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public class PropertyDescriptorDTO { private String name; private RemoteObjectDTO value; + private RemoteObjectDTO getter; public String getName() { return name; @@ -37,4 +39,14 @@ public class PropertyDescriptorDTO { public void setValue(RemoteObjectDTO value) { this.value = value; } + + @JsonProperty("get") + public RemoteObjectDTO getGetter() { + return getter; + } + + @JsonProperty("get") + public void setGetter(RemoteObjectDTO getter) { + this.getter = getter; + } } diff --git a/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMDebugProcess.java b/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMDebugProcess.java index c9f8ffc9a..02650afe4 100644 --- a/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMDebugProcess.java +++ b/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMDebugProcess.java @@ -16,9 +16,10 @@ package org.teavm.idea.debug; import com.intellij.debugger.ui.breakpoints.JavaLineBreakpointType; -import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.ui.ExecutionConsole; import com.intellij.icons.AllIcons; +import com.intellij.openapi.application.Application; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.extensions.ExtensionPoint; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.util.Key; @@ -105,7 +106,8 @@ public class TeaVMDebugProcess extends XDebugProcess { private Debugger initDebugger() { debugServer = new ChromeRDPServer(); debugServer.setPort(port); - ChromeRDPDebugger chromeDebugger = new ChromeRDPDebugger(); + Application application = ApplicationManager.getApplication(); + ChromeRDPDebugger chromeDebugger = new ChromeRDPDebugger(application::invokeLater); debugServer.setExchangeConsumer(chromeDebugger); editorsProvider = new TeaVMDebuggerEditorsProvider(); diff --git a/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMLineBreakpointHandler.java b/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMLineBreakpointHandler.java index 4c6374559..4842ce4e3 100644 --- a/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMLineBreakpointHandler.java +++ b/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMLineBreakpointHandler.java @@ -38,7 +38,6 @@ public class TeaVMLineBreakpointHandler> extends XB private ProjectFileIndex fileIndex; private TeaVMDebugProcess debugProcess; - @SuppressWarnings("unchecked") public TeaVMLineBreakpointHandler(Class> breakpointType, Project project, Debugger innerDebugger, TeaVMDebugProcess debugProcess) { super(breakpointType); @@ -63,10 +62,11 @@ public class TeaVMLineBreakpointHandler> extends XB return; } - Breakpoint innerBreakpoint = innerDebugger.createBreakpoint(path, breakpoint.getLine() + 1); - breakpoint.putUserData(TeaVMDebugProcess.INNER_BREAKPOINT_KEY, innerBreakpoint); - debugProcess.breakpointMap.put(innerBreakpoint, breakpoint); - debugProcess.updateBreakpointStatus(innerBreakpoint); + innerDebugger.createBreakpoint(path, breakpoint.getLine() + 1).thenVoid(innerBreakpoint -> { + breakpoint.putUserData(TeaVMDebugProcess.INNER_BREAKPOINT_KEY, innerBreakpoint); + debugProcess.breakpointMap.put(innerBreakpoint, breakpoint); + debugProcess.updateBreakpointStatus(innerBreakpoint); + }); } @Nullable diff --git a/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMOriginalValue.java b/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMOriginalValue.java index decb51416..ff53ed65e 100644 --- a/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMOriginalValue.java +++ b/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMOriginalValue.java @@ -21,10 +21,10 @@ import com.intellij.xdebugger.frame.XNamedValue; import com.intellij.xdebugger.frame.XValueChildrenList; import com.intellij.xdebugger.frame.XValueNode; import com.intellij.xdebugger.frame.XValuePlace; +import java.util.stream.Collectors; import javax.swing.Icon; import org.jetbrains.annotations.NotNull; import org.teavm.debugging.javascript.JavaScriptValue; -import org.teavm.debugging.javascript.JavaScriptVariable; public class TeaVMOriginalValue extends XNamedValue { private boolean root; @@ -39,20 +39,25 @@ public class TeaVMOriginalValue extends XNamedValue { @Override public void computePresentation(@NotNull XValueNode node, @NotNull XValuePlace place) { Icon icon = root ? PlatformIcons.VARIABLE_ICON : PlatformIcons.FIELD_ICON; - String representation = innerValue.getRepresentation(); - if (representation == null) { - representation = "null"; - } - node.setPresentation(icon, innerValue.getClassName(), representation, !innerValue.getProperties().isEmpty()); + innerValue.getRepresentation().thenVoid(representation -> { + innerValue.getClassName().thenVoid(className -> { + String nonNullRepr = representation != null ? representation : "null"; + node.setPresentation(icon, className.substring(1), nonNullRepr, innerValue.hasInnerStructure()); + }); + }); } - @Override public void computeChildren(@NotNull XCompositeNode node) { - XValueChildrenList children = new XValueChildrenList(); - for (JavaScriptVariable variable : innerValue.getProperties().values()) { - children.add(new TeaVMOriginalValue(variable.getName(), false, variable.getValue())); - } - node.addChildren(children, true); + innerValue.getProperties().then(properties -> properties.values().stream() + .map(variable -> new TeaVMOriginalValue(variable.getName(), false, variable.getValue())) + .collect(Collectors.toList())) + .thenVoid(values -> { + XValueChildrenList children = new XValueChildrenList(); + for (TeaVMOriginalValue value : values) { + children.add(value); + } + node.addChildren(children, true); + }); } } diff --git a/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMStackFrame.java b/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMStackFrame.java index 7b0318ceb..09becb44e 100644 --- a/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMStackFrame.java +++ b/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMStackFrame.java @@ -23,8 +23,11 @@ import com.intellij.xdebugger.frame.XCompositeNode; import com.intellij.xdebugger.frame.XNamedValue; import com.intellij.xdebugger.frame.XStackFrame; import com.intellij.xdebugger.frame.XValueChildrenList; +import java.util.Map; +import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.teavm.common.Promise; import org.teavm.debugging.CallFrame; import org.teavm.debugging.Value; import org.teavm.debugging.Variable; @@ -74,16 +77,33 @@ class TeaVMStackFrame extends XStackFrame { @Override public void computeChildren(@NotNull XCompositeNode node) { - XValueChildrenList children = new XValueChildrenList(); - for (Variable variable : innerFrame.getVariables().values()) { - children.add(createValueNode(variable.getName(), true, variable.getValue())); - } - node.addChildren(children, true); + computeChildrenImpl(node, innerFrame.getVariables(), true); } - static XNamedValue createValueNode(String name, boolean root, Value value) { - return !value.getType().startsWith("@") + static void computeChildrenImpl(XCompositeNode node, Promise> variablesPromise, + boolean root) { + variablesPromise.then(variables -> variables.values() + .stream() + .map(var -> createValueNode(var.getName(), root, var.getValue())) + .collect(Collectors.toList())) + .thenAsync(Promise::all) + .thenVoid(values -> { + XValueChildrenList children = new XValueChildrenList(); + for (XNamedValue value : values) { + children.add(value); + } + node.addChildren(children, true); + }) + .catchError(e -> { + node.setErrorMessage("Error occurred calculating scope: " + e.getMessage()); + e.printStackTrace(); + return null; + }); + } + + static Promise createValueNode(String name, boolean root, Value value) { + return value.getType().then(type -> !type.startsWith("@") ? new TeaVMValue(name, root, value) - : new TeaVMOriginalValue(name, root, value.getOriginalValue()); + : new TeaVMOriginalValue(name, root, value.getOriginalValue())); } } diff --git a/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMValue.java b/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMValue.java index e01d64e9d..9711d0d4e 100644 --- a/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMValue.java +++ b/tools/idea/plugin/src/main/java/org/teavm/idea/debug/TeaVMValue.java @@ -18,13 +18,15 @@ package org.teavm.idea.debug; import com.intellij.util.PlatformIcons; import com.intellij.xdebugger.frame.XCompositeNode; import com.intellij.xdebugger.frame.XNamedValue; -import com.intellij.xdebugger.frame.XValueChildrenList; import com.intellij.xdebugger.frame.XValueNode; import com.intellij.xdebugger.frame.XValuePlace; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Objects; import javax.swing.Icon; import org.jetbrains.annotations.NotNull; +import org.teavm.common.Promise; import org.teavm.debugging.Value; import org.teavm.debugging.Variable; @@ -41,41 +43,58 @@ public class TeaVMValue extends XNamedValue { @Override public void computePresentation(@NotNull XValueNode node, @NotNull XValuePlace place) { Icon icon = root ? PlatformIcons.VARIABLE_ICON : PlatformIcons.FIELD_ICON; - String representation = innerValue.getRepresentation(); - if (representation == null) { - representation = "null"; - } - if (Objects.equals(innerValue.getType(), "java.lang.String")) { - representation = getStringRepresentation(); - } - node.setPresentation(icon, innerValue.getType(), representation, !innerValue.getProperties().isEmpty()); + innerValue.getRepresentation() + .then(representation -> representation != null ? representation : "null") + .thenVoid(representation -> { + innerValue.getType().thenVoid(type -> { + if (Objects.equals(type, "java.lang.String")) { + getStringRepresentation().thenVoid(str -> node.setPresentation(icon, type, str, true)); + } else { + node.setPresentation(icon, type, representation, innerValue.hasInnerStructure()); + } + }); + }); + } - private String getStringRepresentation() { - Variable charactersProperty = innerValue.getProperties().get("characters"); - if (charactersProperty != null) { - Variable dataProperty = charactersProperty.getValue().getProperties().get("data"); - if (dataProperty != null) { - Value dataValue = dataProperty.getValue(); - int[] indexes = dataValue.getProperties().keySet().stream() - .filter(t -> isDigits(t)) - .mapToInt(Integer::parseInt) - .toArray(); - int maxIndex = Math.min(Arrays.stream(indexes).max().orElse(-1) + 1, 256); - char[] chars = new char[maxIndex]; - for (int i = 0; i < maxIndex; ++i) { - Variable charProperty = dataValue.getProperties().get(Integer.toString(i)); - if (charProperty != null) { - String charRepr = charProperty.getValue().getRepresentation(); - if (isDigits(charRepr)) { - chars[i] = (char) Integer.parseInt(charRepr); + private Promise getStringRepresentation() { + return innerValue.getProperties().thenAsync(properties -> { + Variable charactersProperty = properties.get("characters"); + if (charactersProperty == null) { + return errorString(); + } + return charactersProperty.getValue().getProperties().thenAsync(charsProperties -> { + Variable dataProperty = charsProperties.get("data"); + if (dataProperty == null) { + return errorString(); + } + return dataProperty.getValue().getProperties().thenAsync(dataValueProperties -> { + int[] indexes = dataValueProperties.keySet().stream() + .filter(t -> isDigits(t)) + .mapToInt(Integer::parseInt) + .toArray(); + int maxIndex = Math.min(Arrays.stream(indexes).max().orElse(-1) + 1, 256); + char[] chars = new char[maxIndex]; + List> promises = new ArrayList<>(); + for (int i = 0; i < maxIndex; ++i) { + Variable charProperty = dataValueProperties.get(Integer.toString(i)); + if (charProperty != null) { + int index = i; + promises.add(charProperty.getValue().getRepresentation().thenVoid(charRepr -> { + if (isDigits(charRepr)) { + chars[index] = (char) Integer.parseInt(charRepr); + } + })); } } - } - return new String(chars); - } - } - return ""; + return Promise.allVoid(promises).thenAsync(v -> Promise.of(new String(chars))); + }); + }); + }); + } + + private Promise errorString() { + return Promise.of(""); } private static boolean isDigits(String str) { @@ -90,10 +109,6 @@ public class TeaVMValue extends XNamedValue { @Override public void computeChildren(@NotNull XCompositeNode node) { - XValueChildrenList children = new XValueChildrenList(); - for (Variable variable : innerValue.getProperties().values()) { - children.add(TeaVMStackFrame.createValueNode(variable.getName(), false, variable.getValue())); - } - node.addChildren(children, true); + TeaVMStackFrame.computeChildrenImpl(node, innerValue.getProperties(), false); } }