Ver Fonte

debugger draft

Ranides Atterwim há 5 anos atrás
pai
commit
807cf747a3

+ 70 - 0
assira.debugger/pom.xml

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>assira.project</artifactId>
+        <groupId>net.ranides</groupId>
+        <version>2.3.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>assira.debugger</artifactId>
+
+    <properties>
+        <assira.junit.debug>false</assira.junit.debug>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>${maven.compiler.source}</source>
+                    <target>${maven.compiler.target}</target>
+                    <parameters>true</parameters>
+                    <compilerArgs>
+                        <argument>-Xlint:unchecked</argument>
+                        <argument>-Xlint:removal</argument>
+                        <argument>--add-modules</argument>
+                        <argument>jdk.jdi</argument>
+                    </compilerArgs>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>net.ranides</groupId>
+            <artifactId>assira</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>net.ranides</groupId>
+            <artifactId>assira.junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>com.sun</groupId>-->
+<!--            <artifactId>tools</artifactId>-->
+<!--            <version>1.8</version>-->
+<!--            <scope>system</scope>-->
+<!--            <systemPath>${java.home}/../lib/tools.jar</systemPath>-->
+<!--        </dependency>-->
+    </dependencies>
+
+</project>

+ 292 - 0
assira.debugger/src/main/java/net/ranides/demo/debugger/SimpleDebugger.java

@@ -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;
+    }
+
+}

+ 71 - 0
assira.debugger/src/test/java/net/ranides/demo/debugger/AppDebugger.java

@@ -0,0 +1,71 @@
+package net.ranides.demo.debugger;
+
+import com.sun.jdi.VMDisconnectedException;
+import com.sun.jdi.event.LocatableEvent;
+import com.sun.jdi.request.BreakpointRequest;
+
+import java.util.Optional;
+
+public class AppDebugger {
+
+    private static final String EXAMPLE = "net.ranides.demo.debugger.AppObservable$ExampleObject@19";
+
+    public static void main(String[] args) {
+
+        try (var debugger = new SimpleDebugger()) {
+            var example = debugger.connect("localhost", "5151").locate(EXAMPLE);
+
+            System.out.println(" - debug session...");
+
+            // EXAMPLE: short syntax: method call at "location"
+            var valueE = (Double) example.call("methodNum", new Object[]{"3", "7", 3.0});
+            System.out.printf(" - methodNum = '%s'%n", valueE);
+
+
+            // EXAMPLE: short syntax: variable access at "location"
+            var valueF = (Integer) example.variable("counter");
+            System.out.printf(" - counter = '%s'%n", valueF);
+
+
+            // EXAMPLE: short syntax: invoke method inside remote context
+            var valueA = example.eval(context -> {
+                var counter = (Integer) context.variable("counter");
+                return (Integer) context.call("methodInt", new Object[]{6, counter});
+            });
+            System.out.printf(" - methodInt = '%s'%n", valueA);
+
+
+            // EXAMPLE: manually create breakpoint & process debug events
+            BreakpointRequest breakpoint = debugger.breakpoint(example);
+            int[] result = {0};
+            int[] repeat = {0};
+
+            debugger.loop(context -> {
+                if (!context.matches(example)) {
+                    System.out.printf(" * ignore event: %s%n", context.event());
+                    return Optional.empty();
+                }
+
+                Integer counter = (Integer) context.variable("counter");
+                System.out.printf(" * counter = %d%n", counter);
+
+                result[0] += counter;
+
+                if (repeat[0]++ > 10) {
+                    breakpoint.setEnabled(false);
+                    context.stop();
+                    return Optional.of("calculated");
+                }
+                return Optional.empty();
+            });
+            System.out.printf(" - result = %d%n", result[0]);
+
+        } catch (VMDisconnectedException e) {
+            System.out.println("Virtual Machine is disconnected.");
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            System.out.println("Disconnected.");
+        }
+    }
+}

+ 40 - 0
assira.debugger/src/test/java/net/ranides/demo/debugger/AppObservable.java

@@ -0,0 +1,40 @@
+package net.ranides.demo.debugger;
+
+public class AppObservable {
+
+    /**
+     * Something
+     *
+     * @param args
+     * @throws InterruptedException sometimes
+     */
+    public static void main(String[] args) throws InterruptedException {
+        new ExampleObject().run();
+    }
+
+    public static class ExampleObject {
+
+        public void run() throws InterruptedException {
+            for (int counter = 1; ; counter++) {
+                System.out.printf("%03d ", counter % 1000);
+                if (counter % 20 == 0) {
+                    System.out.printf("%n");
+                }
+                Thread.sleep(500);
+            }
+        }
+
+        public int methodInt(int a, int b) {
+            return a * b;
+        }
+
+        public String methodStr(String a, String b) {
+            return a + "*" + b + "=" + (Integer.parseInt(a) * Integer.parseInt(b));
+        }
+
+        public double methodNum(String a, String b, double c) {
+            return (Integer.parseInt(a) + Integer.parseInt(b)) / c;
+        }
+    }
+
+}

+ 1 - 0
pom.xml

@@ -64,6 +64,7 @@
                 <module>assira.jdk11</module>
                 <module>assira.lambda</module>
                 <module>assira.benchmark</module>
+                <module>assira.debugger</module>
             </modules>
         </profile>
     </profiles>