org.teavm.chromerdp.ChromeRDPRunner Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of teavm-chrome-rdp Show documentation
Show all versions of teavm-chrome-rdp Show documentation
TeaVM debugging backend for Google Chrome RDP
/*
* 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.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.JavaScriptScript;
import org.teavm.debugging.javascript.JavaScriptVariable;
public final class ChromeRDPRunner {
private ChromeRDPServer server;
private Debugger debugger;
private Map breakpointIds = new WeakHashMap<>();
private int currentFrame;
private int breakpointIdGen;
BlockingQueue queue = new LinkedBlockingQueue<>();
private ChromeRDPRunner() {
server = new ChromeRDPServer();
server.setPort(2357);
ChromeRDPDebugger jsDebugger = new ChromeRDPDebugger(queue::offer);
server.setExchangeConsumer(jsDebugger);
new Thread(server::start).start();
debugger = new Debugger(jsDebugger, new URLDebugInformationProvider(""));
debugger.addListener(listener);
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.err.println("Uncaught exception in thread " + t);
e.printStackTrace();
});
}
private DebuggerListener listener = new DebuggerListener() {
@Override
public void resumed() {
}
@Override
public void paused(Breakpoint breakpoint) {
CallFrame[] stack = debugger.getCallStack();
if (stack.length > 0) {
System.out.println();
System.out.println("Suspended at " + stack[0].getLocation());
}
if (breakpoint != null) {
System.out.println("Breakpoint #" + breakpointIds.get(breakpoint) + " hit");
}
currentFrame = 0;
}
@Override
public void breakpointStatusChanged(Breakpoint breakpoint) {
}
@Override
public void attached() {
}
@Override
public void detached() {
queue.offer(() -> { });
}
};
public static void main(String[] args) {
ChromeRDPRunner runner = new ChromeRDPRunner();
try {
runner.acceptInput();
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
public void acceptInput() throws InterruptedException {
boolean wasAttached = debugger.isAttached();
if (!wasAttached) {
System.out.println("Waiting for remote process to attach...");
}
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) {
System.out.println("Detached");
break;
}
}
queue.offer(() -> {
debugger.detach();
server.stop();
});
}
private void stdinThread() throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.print("> ");
String line = reader.readLine();
if (line == null) {
break;
}
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;
}
} catch (InterruptedException e) {
break;
}
}
}
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 Promise.of(true);
}
return command.execute(arguments).then(v -> true);
}
private Command resumeCommand = args -> debugger.resume();
private Command breakpointCommand = args -> {
if (args.length < 2 || args.length > 4) {
System.out.println("Expected 2 arguments");
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) {
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 Promise.VOID;
}
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) {
var 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 JavaScriptScript[] resolveJsFileName(String fileName) {
return new JavaScriptScript[0];
}
private String[] resolveFileName(String fileName) {
if (debugger.getSourceFiles().contains(fileName)) {
return new String[] { fileName };
}
String[] result = debugger.getSourceFiles().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 static boolean isPrecededByPathSeparator(String actualName, String specifiedName) {
if (actualName.length() < specifiedName.length() + 1) {
return false;
}
char c = actualName.charAt(actualName.length() - specifiedName.length() - 1);
return c == '/' || c == '\\';
}
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());
}
return Promise.VOID;
};
private Command frameCommand = args -> {
if (args.length != 2) {
System.out.println("Expected 1 argument");
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 Promise.VOID;
}
currentFrame = index;
return Promise.VOID;
};
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 Promise.VOID;
}
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());
}
return Promise.VOID;
}
case "variables": {
CallFrame frame = debugger.getCallStack()[currentFrame];
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