|
@@ -0,0 +1,292 @@
|
|
|
|
|
+package net.ranides.demo.debugger;
|
|
|
|
|
+
|
|
|
|
|
+import com.sun.jdi.*;
|
|
|
|
|
+import com.sun.jdi.connect.AttachingConnector;
|
|
|
|
|
+import com.sun.jdi.connect.Connector;
|
|
|
|
|
+import com.sun.jdi.event.BreakpointEvent;
|
|
|
|
|
+import com.sun.jdi.event.Event;
|
|
|
|
|
+import com.sun.jdi.event.EventSet;
|
|
|
|
|
+import com.sun.jdi.event.LocatableEvent;
|
|
|
|
|
+import com.sun.jdi.request.BreakpointRequest;
|
|
|
|
|
+import lombok.AccessLevel;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.Arrays;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+
|
|
|
|
|
+public class SimpleDebugger implements AutoCloseable {
|
|
|
|
|
+
|
|
|
|
|
+ private VirtualMachine vm;
|
|
|
|
|
+
|
|
|
|
|
+ public SimpleDebugger connect(String port) throws Exception {
|
|
|
|
|
+ return connect("localhost", port);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public SimpleDebugger connect(String host, String port) throws Exception {
|
|
|
|
|
+ AttachingConnector connector = Bootstrap.virtualMachineManager()
|
|
|
|
|
+ .attachingConnectors()
|
|
|
|
|
+ .stream()
|
|
|
|
|
+ .filter(v -> v.name().equals("com.sun.jdi.SocketAttach"))
|
|
|
|
|
+ .findFirst()
|
|
|
|
|
+ .orElseThrow(() -> new UnsupportedOperationException("No SocketAttachingConnector found."));
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Connector.Argument> args = connector.defaultArguments();
|
|
|
|
|
+ args.get("hostname").setValue(host);
|
|
|
|
|
+ args.get("port").setValue(port);
|
|
|
|
|
+
|
|
|
|
|
+ this.vm = connector.attach(args);
|
|
|
|
|
+
|
|
|
|
|
+ return this;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public RemoteLocation locate(String classWithLine) {
|
|
|
|
|
+ String[] parts = classWithLine.split("@");
|
|
|
|
|
+ return new RemoteLocation(parts[0], Integer.parseInt(parts[1]));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public RemoteLocation locate(String className, int line) {
|
|
|
|
|
+ return new RemoteLocation(className, line);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public BreakpointRequest breakpoint(RemoteLocation remote) throws Exception {
|
|
|
|
|
+ ClassType type = vm.classesByName(remote.className)
|
|
|
|
|
+ .stream()
|
|
|
|
|
+ .filter(ClassType.class::isInstance)
|
|
|
|
|
+ .map(ClassType.class::cast)
|
|
|
|
|
+ .findAny()
|
|
|
|
|
+ .orElseThrow(() -> new IllegalArgumentException("Unknown class: " + remote.className));
|
|
|
|
|
+
|
|
|
|
|
+ Location location = type.locationsOfLine(remote.line).get(0);
|
|
|
|
|
+ BreakpointRequest breakpoint = vm.eventRequestManager().createBreakpointRequest(location);
|
|
|
|
|
+ breakpoint.enable();
|
|
|
|
|
+ return breakpoint;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public <T> T loop(LoopHandler<T> handler) throws Exception {
|
|
|
|
|
+ EventSet eventSet;
|
|
|
|
|
+ while ((eventSet = vm.eventQueue().remove()) != null) {
|
|
|
|
|
+ for (Event event : eventSet) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ LoopContext context = new LoopContext(event);
|
|
|
|
|
+ T result = handler.handle(context);
|
|
|
|
|
+ if(context.stop) {
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (RuntimeException error) {
|
|
|
|
|
+ throw error;
|
|
|
|
|
+ } catch (Exception error) {
|
|
|
|
|
+ throw new RuntimeException(error);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ vm.resume();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new IllegalStateException("Abnormal termination");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Object variable0(LocatableEvent event, String name) throws Exception {
|
|
|
|
|
+ StackFrame frame = event.thread().frame(0);
|
|
|
|
|
+ LocalVariable variable = frame.visibleVariableByName(name);
|
|
|
|
|
+ if(variable == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("Unknown variable: " + name);
|
|
|
|
|
+ }
|
|
|
|
|
+ Value mirror = frame.getValue(variable);
|
|
|
|
|
+ return fromMirror(mirror);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Object call0(LocatableEvent event, String methodName, Object[] arguments) throws Exception {
|
|
|
|
|
+ List<Value> input = Arrays.stream(arguments).map(this::mirrorOf).collect(Collectors.toList());
|
|
|
|
|
+ Value out = call0(event, methodName, input);
|
|
|
|
|
+ return fromMirror(out);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Value call0(LocatableEvent event, String methodName, List<? extends Value> arguments) throws Exception {
|
|
|
|
|
+ ThreadReference thread = event.thread();
|
|
|
|
|
+ ObjectReference that = thread.frame(0).thisObject();
|
|
|
|
|
+ List<Method> methods = that.referenceType().methodsByName(methodName);
|
|
|
|
|
+ if(methods.isEmpty()) {
|
|
|
|
|
+ throw new IllegalArgumentException("Unknown method: " + methodName);
|
|
|
|
|
+ }
|
|
|
|
|
+ return that.invokeMethod(thread, methods.get(0), arguments, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void close() {
|
|
|
|
|
+ vm.dispose();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Value mirrorOf(Object value) {
|
|
|
|
|
+ if (value instanceof String) {
|
|
|
|
|
+ return vm.mirrorOf((String) value);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof Integer) {
|
|
|
|
|
+ return vm.mirrorOf((Integer) value);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof Short) {
|
|
|
|
|
+ return vm.mirrorOf((Short) value);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof Byte) {
|
|
|
|
|
+ return vm.mirrorOf((Byte) value);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof Long) {
|
|
|
|
|
+ return vm.mirrorOf((Long) value);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof Float) {
|
|
|
|
|
+ return vm.mirrorOf((Float) value);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof Double) {
|
|
|
|
|
+ return vm.mirrorOf((Double) value);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof Boolean) {
|
|
|
|
|
+ return vm.mirrorOf((Boolean) value);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof Character) {
|
|
|
|
|
+ return vm.mirrorOf((Character) value);
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new IllegalArgumentException("Not supported value: " + value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Object fromMirror(Value value) {
|
|
|
|
|
+ if (value instanceof StringReference) {
|
|
|
|
|
+ return ((StringReference) value).value();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof IntegerValue) {
|
|
|
|
|
+ return ((IntegerValue) value).value();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof ShortValue) {
|
|
|
|
|
+ return ((ShortValue) value).value();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof ByteValue) {
|
|
|
|
|
+ return ((ByteValue) value).value();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof LongValue) {
|
|
|
|
|
+ return ((LongValue) value).value();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof FloatValue) {
|
|
|
|
|
+ return ((FloatValue) value).value();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof DoubleValue) {
|
|
|
|
|
+ return ((DoubleValue) value).value();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof BooleanValue) {
|
|
|
|
|
+ return ((BooleanValue) value).value();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof CharValue) {
|
|
|
|
|
+ return ((CharValue) value).value();
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new IllegalArgumentException("Not supported value: " + value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public class RemoteLocation {
|
|
|
|
|
+ private final String className;
|
|
|
|
|
+ private final int line;
|
|
|
|
|
+
|
|
|
|
|
+ public RemoteLocation(String className, int line) {
|
|
|
|
|
+ this.className = className;
|
|
|
|
|
+ this.line = line;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public boolean matches(LocatableEvent event) {
|
|
|
|
|
+ var location = event.location();
|
|
|
|
|
+ return location.declaringType().name().equals(className) && location.lineNumber() == line;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Object call(String name, Object[] arguments) throws Exception {
|
|
|
|
|
+ return eval(context -> context.call(name, arguments));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Object variable(String name) throws Exception {
|
|
|
|
|
+ return eval(context -> context.variable(name));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public <T> T eval(EvalHandler<T> handler) throws Exception {
|
|
|
|
|
+ T none = null;
|
|
|
|
|
+ BreakpointRequest breakpoint = SimpleDebugger.this.breakpoint(this);
|
|
|
|
|
+
|
|
|
|
|
+ return loop(context -> {
|
|
|
|
|
+ if(context.event instanceof BreakpointEvent) {
|
|
|
|
|
+ BreakpointEvent bp = (BreakpointEvent) context.event;
|
|
|
|
|
+ if(bp.request() == breakpoint) {
|
|
|
|
|
+ breakpoint.setEnabled(false);
|
|
|
|
|
+ context.stop();
|
|
|
|
|
+ return handler.handle(new EvalContext(bp));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // that was not "our" breakpoint, return anything to the loop & continue
|
|
|
|
|
+ return none;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public class LoopContext {
|
|
|
|
|
+ private final Event event;
|
|
|
|
|
+ private boolean stop = false;
|
|
|
|
|
+
|
|
|
|
|
+ public LoopContext(Event event) {
|
|
|
|
|
+ this.event = event;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Event event() {
|
|
|
|
|
+ return event;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void stop() {
|
|
|
|
|
+ this.stop = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public boolean isLocatable() {
|
|
|
|
|
+ return event instanceof LocatableEvent;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public boolean matches(RemoteLocation location) throws Exception {
|
|
|
|
|
+ return isLocatable() && location.matches((LocatableEvent)event);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Object variable(String name) throws Exception {
|
|
|
|
|
+ if(!isLocatable()) {
|
|
|
|
|
+ throw new IllegalStateException("Can't access variable from unlocatable event: " + event);
|
|
|
|
|
+ }
|
|
|
|
|
+ return variable0((LocatableEvent)event, name);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Object call(String methodName, Object[] arguments) throws Exception {
|
|
|
|
|
+ if(!isLocatable()) {
|
|
|
|
|
+ throw new IllegalStateException("Can't invoke method from unlocatable event: " + event);
|
|
|
|
|
+ }
|
|
|
|
|
+ return call0((LocatableEvent)event, methodName, arguments);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
|
|
|
|
+ public class EvalContext {
|
|
|
|
|
+
|
|
|
|
|
+ private final LocatableEvent event;
|
|
|
|
|
+
|
|
|
|
|
+ public LocatableEvent event() {
|
|
|
|
|
+ return event;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public boolean matches(RemoteLocation location) throws Exception {
|
|
|
|
|
+ return location.matches(event);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Object variable(String name) throws Exception {
|
|
|
|
|
+ return variable0(event, name);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Object call(String methodName, Object[] arguments) throws Exception {
|
|
|
|
|
+ return call0(event, methodName, arguments);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public interface LoopHandler<T> {
|
|
|
|
|
+ T handle(LoopContext context) throws Exception;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public interface EvalHandler<T> {
|
|
|
|
|
+ T handle(EvalContext context) throws Exception;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|