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 dbf789d8c..fbaee548f 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 @@ -19,10 +19,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -30,14 +31,30 @@ import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.teavm.chromerdp.data.*; -import org.teavm.chromerdp.messages.*; -import org.teavm.debugging.javascript.*; +import org.teavm.chromerdp.data.CallArgumentDTO; +import org.teavm.chromerdp.data.CallFrameDTO; +import org.teavm.chromerdp.data.LocationDTO; +import org.teavm.chromerdp.data.Message; +import org.teavm.chromerdp.data.PropertyDescriptorDTO; +import org.teavm.chromerdp.data.RemoteObjectDTO; +import org.teavm.chromerdp.data.Response; +import org.teavm.chromerdp.data.ScopeDTO; +import org.teavm.chromerdp.messages.CallFunctionCommand; +import org.teavm.chromerdp.messages.CallFunctionResponse; +import org.teavm.chromerdp.messages.ContinueToLocationCommand; +import org.teavm.chromerdp.messages.GetPropertiesCommand; +import org.teavm.chromerdp.messages.GetPropertiesResponse; +import org.teavm.chromerdp.messages.RemoveBreakpointCommand; +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.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; -/** - * - * @author Alexey Andreev - */ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeConsumer { private static final Logger logger = LoggerFactory.getLogger(ChromeRDPDebugger.class); private static final Object dummy = new Object(); @@ -50,7 +67,8 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC private ConcurrentMap scriptIds = new ConcurrentHashMap<>(); private boolean suspended; private ObjectMapper mapper = new ObjectMapper(); - private ConcurrentMap responseHandlers = new ConcurrentHashMap<>(); + private ConcurrentMap> responseHandlers = new ConcurrentHashMap<>(); + private ConcurrentMap> futures = new ConcurrentHashMap<>(); private AtomicInteger messageIdGenerator = new AtomicInteger(); private Lock breakpointLock = new ReentrantLock(); @@ -89,43 +107,42 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC private ChromeRDPExchangeListener exchangeListener = messageText -> receiveMessage(messageText); private void receiveMessage(final String messageText) { - new Thread() { - @Override public void run() { - 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()); - } - } - responseHandlers.remove(response.getId()).received(response.getResult()); - } else { - Message message = mapper.reader(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; + 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()); } } - } catch (Exception e) { - if (logger.isErrorEnabled()) { - logger.error("Error receiving message from Google Chrome", e); + CompletableFuture future = futures.remove(response.getId()); + responseHandlers.remove(response.getId()).received(response.getResult(), future); + } else { + Message message = mapper.reader(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; } } + } catch (Exception e) { + if (logger.isErrorEnabled()) { + logger.error("Error receiving message from Google Chrome", e); + } } - }.start(); + }).start(); } private synchronized void firePaused(SuspendedNotification params) { @@ -328,7 +345,7 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC if (logger.isInfoEnabled()) { logger.info("Setting breakpoint at {}, message id is ", breakpoint.getLocation(), message.getId()); } - ResponseHandler handler = node -> { + setResponseHandler(message.getId(), (node, out) -> { if (node != null) { SetBreakpointResponse response = mapper.reader(SetBreakpointResponse.class).readValue(node); breakpoint.chromeId = response.getBreakpointId(); @@ -342,8 +359,7 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC for (JavaScriptDebuggerListener listener : getListeners()) { listener.breakpointChanged(breakpoint); } - }; - responseHandlers.put(message.getId(), handler); + }); sendMessage(message); } @@ -358,14 +374,18 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC params.setObjectId(scopeId); params.setOwnProperties(true); message.setParams(mapper.valueToTree(params)); - final BlockingQueue> sync = new LinkedTransferQueue<>(); - responseHandlers.put(message.getId(), node -> { - GetPropertiesResponse response = mapper.reader(GetPropertiesResponse.class).readValue(node); - sync.add(parseProperties(response.getResult())); + CompletableFuture> sync = setResponseHandler(message.getId(), (node, out) -> { + if (node == null) { + out.complete(Collections.emptyList()); + } else { + GetPropertiesResponse response = mapper.reader(GetPropertiesResponse.class).readValue(node); + out.complete(parseProperties(response.getResult())); + } }); sendMessage(message); + try { - return sync.take(); + return read(sync); } catch (InterruptedException e) { return Collections.emptyList(); } @@ -385,19 +405,19 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC params.setArguments(new CallArgumentDTO[] { arg }); params.setFunctionDeclaration("$dbg_class"); message.setParams(mapper.valueToTree(params)); - final BlockingQueue sync = new LinkedTransferQueue<>(); - responseHandlers.put(message.getId(), node -> { + + CompletableFuture sync = setResponseHandler(message.getId(), (node, out) -> { if (node == null) { - sync.add(""); + out.complete(""); } else { CallFunctionResponse response = mapper.reader(CallFunctionResponse.class).readValue(node); RemoteObjectDTO result = response.getResult(); - sync.add(result.getValue() != null ? result.getValue().getTextValue() : ""); + out.complete(result.getValue() != null ? result.getValue().getTextValue() : ""); } }); sendMessage(message); try { - String result = sync.take(); + String result = read(sync); return result.isEmpty() ? null : result; } catch (InterruptedException e) { return null; @@ -418,20 +438,19 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC params.setArguments(new CallArgumentDTO[] { arg }); params.setFunctionDeclaration("$dbg_repr"); message.setParams(mapper.valueToTree(params)); - final BlockingQueue sync = new LinkedTransferQueue<>(); - responseHandlers.put(message.getId(), node -> { + CompletableFuture sync = setResponseHandler(message.getId(), (node, out) -> { if (node == null) { - sync.add(new RepresentationWrapper(null)); + out.complete(new RepresentationWrapper(null)); } else { CallFunctionResponse response = mapper.reader(CallFunctionResponse.class).readValue(node); RemoteObjectDTO result = response.getResult(); - sync.add(new RepresentationWrapper(result.getValue() != null + out.complete(new RepresentationWrapper(result.getValue() != null ? result.getValue().getTextValue() : null)); } }); sendMessage(message); try { - RepresentationWrapper result = sync.take(); + RepresentationWrapper result = read(sync); return result.repr; } catch (InterruptedException e) { return null; @@ -528,7 +547,30 @@ public class ChromeRDPDebugger implements JavaScriptDebugger, ChromeRDPExchangeC return dto; } - interface ResponseHandler { - void received(JsonNode node) throws IOException; + @SuppressWarnings("unchecked") + private CompletableFuture setResponseHandler(int messageId, ResponseHandler handler) { + CompletableFuture future = new CompletableFuture<>(); + futures.put(messageId, (CompletableFuture) future); + responseHandlers.put(messageId, (ResponseHandler) handler); + return future; + } + + interface ResponseHandler { + void received(JsonNode node, CompletableFuture out) throws IOException; + } + + private static T read(Future future) throws InterruptedException { + try { + return future.get(); + } 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); + } + } } } diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPException.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPException.java new file mode 100644 index 000000000..439bf10f0 --- /dev/null +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPException.java @@ -0,0 +1,22 @@ +/* + * Copyright 2016 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; + +public class ChromeRDPException extends RuntimeException { + public ChromeRDPException(String message) { + super(message); + } +} 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 new file mode 100644 index 000000000..4a00eb463 --- /dev/null +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPRunner.java @@ -0,0 +1,304 @@ +/* + * Copyright 2016 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.chromerdp; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.stream.Collectors; +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; + +public final class ChromeRDPRunner { + private ChromeRDPServer server; + private Debugger debugger; + private Map breakpointIds = new WeakHashMap<>(); + private int currentFrame; + private int breakpointIdGen; + private volatile Runnable attachListener; + private volatile Runnable suspendListener; + private volatile Runnable resumeListener; + + private ChromeRDPRunner() { + server = new ChromeRDPServer(); + server.setPort(2357); + ChromeRDPDebugger jsDebugger = new ChromeRDPDebugger(); + server.setExchangeConsumer(jsDebugger); + + new Thread(server::start).start(); + debugger = new Debugger(jsDebugger, new URLDebugInformationProvider("")); + debugger.addListener(listener); + } + + private DebuggerListener listener = new DebuggerListener() { + @Override + public void resumed() { + if (resumeListener != null) { + resumeListener.run(); + } + } + + @Override + public void paused() { + CallFrame[] stack = debugger.getCallStack(); + if (stack.length > 0) { + System.out.println(); + System.out.println("Suspended at " + stack[0].getLocation()); + } + currentFrame = 0; + if (suspendListener != null) { + suspendListener.run(); + } + } + + @Override + public void breakpointStatusChanged(Breakpoint breakpoint) { + } + + @Override + public void attached() { + if (attachListener != null) { + attachListener.run(); + } + } + + @Override + public void detached() { + } + }; + + public static void main(String[] args) throws IOException { + ChromeRDPRunner runner = new ChromeRDPRunner(); + try { + runner.acceptInput(); + } catch (InterruptedException e) { + System.out.println("Interrupted"); + } + } + + public void acceptInput() throws IOException, InterruptedException { + if (!debugger.isAttached()) { + 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"); + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + loop: 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; + } + 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"); + } + } + + debugger.detach(); + server.stop(); + } + + private void suspended(String[] arguments, Command command) throws InterruptedException { + if (!debugger.isSuspended()) { + System.out.println("This command is only available when remote process is suspended"); + return; + } + command.execute(arguments); + } + + private Command resumeCommand = args -> { + CountDownLatch latch = new CountDownLatch(1); + resumeListener = latch::countDown; + debugger.resume(); + latch.await(); + resumeListener = null; + }; + + private Command breakpointCommand = args -> { + if (args.length != 3) { + System.out.println("Expected 2 arguments"); + return; + } + Breakpoint bp = debugger.createBreakpoint(args[1], Integer.parseInt(args[2])); + int id = breakpointIdGen++; + breakpointIds.put(bp, id); + System.out.println("Breakpoint #" + id + " was set at " + bp.getLocation()); + }; + + private Command backtraceCommand = args -> { + CallFrame[] callStack = debugger.getCallStack(); + for (int i = 0; i < callStack.length; ++i) { + StringBuilder sb = new StringBuilder(i == currentFrame ? " -> " : " "); + sb.append("#").append(i).append(": "); + CallFrame frame = callStack[i]; + if (frame.getMethod() != null) { + sb.append(frame.getMethod().getClassName()).append('.').append(frame.getMethod().getName()); + } else { + sb.append("[unknown method]"); + } + if (frame.getLocation() != null) { + sb.append('(').append(frame.getLocation()).append(')'); + } + System.out.println(sb.toString()); + } + }; + + private Command frameCommand = args -> { + if (args.length != 2) { + System.out.println("Expected 1 argument"); + return; + } + 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; + } + currentFrame = index; + }; + + private Command stepCommand = args -> debugger.stepInto(); + + private Command nextCommand = args -> debugger.stepOver(); + + private Command outCommand = args -> debugger.stepOut(); + + private Command infoCommand = args -> { + if (args.length != 2) { + System.out.println("Expected 1 argument"); + return; + } + + switch (args[1]) { + case "breakpoints": { + List sortedBreakpoints = debugger.getBreakpoints().stream() + .sorted(Comparator.comparing(breakpointIds::get)) + .collect(Collectors.toList()); + for (Breakpoint breakpoint : sortedBreakpoints) { + int id = breakpointIds.get(breakpoint); + System.out.println(" #" + id + ": " + breakpoint.getLocation()); + } + break; + } + + 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; + } + + default: + System.out.println("Invalid argument"); + } + }; + + private interface Command { + void execute(String[] args) throws InterruptedException; + } +} diff --git a/tools/idea/src/main/java/org/teavm/idea/debug/TeaVMDebugProcess.java b/tools/idea/src/main/java/org/teavm/idea/debug/TeaVMDebugProcess.java index f02343d2e..452725f4c 100644 --- a/tools/idea/src/main/java/org/teavm/idea/debug/TeaVMDebugProcess.java +++ b/tools/idea/src/main/java/org/teavm/idea/debug/TeaVMDebugProcess.java @@ -44,7 +44,6 @@ public class TeaVMDebugProcess extends XDebugProcess { innerDebugger.addListener(new DebuggerListener() { @Override public void resumed() { - getSession().resume(); } @Override diff --git a/tools/idea/src/main/java/org/teavm/idea/debug/TeaVMLineBreakpointHandler.java b/tools/idea/src/main/java/org/teavm/idea/debug/TeaVMLineBreakpointHandler.java index e5732fbfb..317644e51 100644 --- a/tools/idea/src/main/java/org/teavm/idea/debug/TeaVMLineBreakpointHandler.java +++ b/tools/idea/src/main/java/org/teavm/idea/debug/TeaVMLineBreakpointHandler.java @@ -61,7 +61,7 @@ public class TeaVMLineBreakpointHandler extends XBreakpointHandler