diff --git a/build.gradle b/build.gradle index 07b2df0e..1f3ce8f9 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ subprojects { dependencies { implementation 'org.slf4j:slf4j-api:2.0.9' - implementation 'org.slf4j:slf4j-simple:2.0.9' + implementation 'ch.qos.logback:logback-classic:1.4.11' testImplementation platform('org.junit:junit-bom:5.10.0') testImplementation 'org.junit.jupiter:junit-jupiter' @@ -37,7 +37,8 @@ subprojects { group 'distribution' from (configurations.runtimeClasspath) { include 'slf4j-api-*.jar' - include 'slf4j-simple-*.jar' + include 'logback-classic-*.jar' + include 'logback-core-*.jar' } into '../vscode-extension/server' } diff --git a/vscode-extension/client/src/debug-adapter.js b/vscode-extension/client/src/debug-adapter.js new file mode 100644 index 00000000..baedb8a2 --- /dev/null +++ b/vscode-extension/client/src/debug-adapter.js @@ -0,0 +1,50 @@ + +import { join } from "node:path"; +import { ExtensionContext, window, workspace, debug, DebugAdapterExecutable } from "vscode"; +import { SimpleLogger } from "./simple-logger"; + +/** + * @param {string} javaBin + * @param {ExtensionContext} context + */ +export async function activateDebugAdapter(javaBin, context) { + const logChannel = window.createOutputChannel('ZenScript Debug Adapter', "log"); + const logger = new SimpleLogger(logChannel); + logger.info('Initializing Debug Adapter'); + const config = workspace.getConfiguration(); + // start debug adapter protocol + const classpath = join(__dirname, '..', '..', 'server', '*'); + let dapDebug = '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006,quiet=y'; + const dapMain = 'raylras.zen.dap.debugserver.StandardIOLauncher'; + const dapArgs = ['-cp', classpath]; + if (config.get('zenscript.debugAdapter.debug')) { + logger.info(`Language server is running in debug mode.`); + if (config.get('zenscript.debugAdapter.suspend')) { + dapDebug = dapDebug.replace(/suspend=n/, "suspend=y"); + } + logger.info(`DAP Debug arguments: ${dapDebug}`); + dapArgs.push(dapDebug); + } + const isSuspendDAP = dapDebug.indexOf("suspend=y") > -1; + dapArgs.push('-Dfile.encoding=UTF-8'); + dapArgs.push(dapMain); + const dapOptions = { + env: process.env + }; + context.subscriptions.push(debug.registerDebugAdapterDescriptorFactory('zenscript', { + createDebugAdapterDescriptor(session, executable) { + logger.info('Starting ZensScript Debug Adapter server'); + if (isSuspendDAP) { + logger.info('Waiting for debugger attachment for DAP...'); + } + return new DebugAdapterExecutable(javaBin, dapArgs, dapOptions); + } + })); + + + debug.onDidReceiveDebugSessionCustomEvent((e) => { + if(e.event === "outputLog") { + logChannel.appendLine(e.body); + } + }); +} diff --git a/vscode-extension/client/src/extension.js b/vscode-extension/client/src/extension.js index 5ce1fb6e..8c1c712a 100644 --- a/vscode-extension/client/src/extension.js +++ b/vscode-extension/client/src/extension.js @@ -2,6 +2,7 @@ import { ExtensionContext, window } from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; import LocateJavaHome from "@viperproject/locate-java-home"; import { activateLanguageServer } from "./language-server" +import { activateDebugAdapter } from "./debug-adapter" /** @type {LanguageClient} */ let languageClient = undefined; @@ -15,7 +16,12 @@ export async function activate(context) { window.showErrorMessage("No valid Java environment found, please install Java 17 or later"); } else { const javaBin = javaHomes[0].executables.java; - languageClient = await activateLanguageServer(javaBin); + + activateDebugAdapter(javaBin, context); + activateLanguageServer(javaBin) + .then(client => { + languageClient = client; + }); } }); } diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json index 392e9b1e..125c2a8b 100644 --- a/vscode-extension/package-lock.json +++ b/vscode-extension/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@viperproject/locate-java-home": "^1.1.13", + "@vscode/debugadapter": "^1.63.0", "dayjs": "^1.11.9", "vscode-languageclient": "^9.0.0" }, @@ -228,6 +229,22 @@ "locate-java-home": "bin/locate-java-home" } }, + "node_modules/@vscode/debugadapter": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.63.0.tgz", + "integrity": "sha512-d2eAnYCZkKJ0C28gT93KPi5YXFav8VyagcxkJ94LZ8qqgJ27+2ct26MOHGYKB8L25ZdDs8A4YmyJO32J0afhNQ==", + "dependencies": { + "@vscode/debugprotocol": "1.63.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@vscode/debugprotocol": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.63.0.tgz", + "integrity": "sha512-7gewwv69pA7gcJUhtJsru5YN7E1AwwnlBrF5mJY4R/NGInOUqOYOWHlqQwG+4AXn0nXWbcn26MHgaGI9Q26SqA==" + }, "node_modules/@vscode/vsce": { "version": "2.21.0", "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.21.0.tgz", diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 90f91074..6526cee1 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -20,7 +20,8 @@ "vscode": "^1.81.0" }, "activationEvents": [ - "onLanguage:zenscript" + "onLanguage:zenscript", + "onDebugResolve:zenscript" ], "icon": "./icon/zs.webp", "main": "./client/out/extension.js", @@ -45,6 +46,11 @@ "path": "./language/ZenScript.tmLanguage.json" } ], + "breakpoints": [ + { + "language": "zenscript" + } + ], "configuration": { "title": "ZenScript Language Server", "properties": { @@ -58,16 +64,114 @@ "order": 1, "type": "boolean", "default": false, - "description": "Suspend to wait for the debugger to attach" + "description": "Suspend to wait for the debugger to attach for language server" }, - "zenscript.languageServer.javaHome": { + "zenscript.debugAdapter.debug": { "order": 2, + "type": "boolean", + "default": false, + "description": "Enable/disable debug mode for debug adapter" + }, + "zenscript.debugAdapter.suspend": { + "order": 3, + "type": "boolean", + "default": false, + "description": "Suspend to wait for the debugger to attach for debug adapter" + }, + "zenscript.languageServer.javaHome": { + "order": 4, "type": "string", "default": null, "description": "Path of java home" } } - } + }, + "debuggers": [ + { + "type": "zenscript", + "label": "ZenScript", + "configurationAttributes": { + "attach": { + "required": [ + "scriptRoot", + "hostName", + "port" + ], + "properties": { + "scriptRoot": { + "type": "string", + "description": "The path of 'scripts' folder", + "default": "${workspaceFolder}/scripts" + }, + "hostName": { + "type": "string", + "description": "The host name of your running JVM.", + "default": "localhost" + }, + "port": { + "type": "number", + "description": "The port number of your running JVM.", + "default": 8000 + } + } + }, + "launch": { + "required": [ + "scriptRoot" + ], + "properties": { + "scriptRoot": { + "type": "string", + "description": "The path of 'scripts' folder", + "default": "${workspaceFolder}/scripts" + }, + "javaExecutable": { + "type": "string", + "description": "The java to launch minecraft, if not specified, try to resolve from probezs environment" + }, + "launchInTerminal": { + "type": "boolean", + "description": "Is process launch in vscode integrated terminal, false to launch it as subprocess", + "default": true + } + } + } + }, + "configurationSnippets": [ + + { + "label": "ZenScript: Launch Minecraft", + "description": "Launch a new minecraft instance and start debug", + "body": { + "type": "zenscript", + "request": "launch", + "name": "ZenScript Launch", + "scriptRoot": "^\"\\${workspaceFolder}/scripts\"" + } + }, + { + "label": "ZenScript: Attach VM", + "description": "Attaches a debugger to a running JVM", + "body": { + "type": "zenscript", + "request": "attach", + "name": "ZenScript Attach", + "scriptRoot": "^\"\\${workspaceFolder}/scripts\"", + "hostName": "localhost", + "port": 8000 + } + } + ], + "initialConfigurations": [ + { + "type": "zenscript", + "request": "launch", + "name": "ZenScript Launch", + "scriptRoot": "${workspaceFolder}/scripts" + } + ] + } + ] }, "scripts": { "package": "vsce package", @@ -78,6 +182,7 @@ }, "dependencies": { "@viperproject/locate-java-home": "^1.1.13", + "@vscode/debugadapter": "^1.63.0", "dayjs": "^1.11.9", "vscode-languageclient": "^9.0.0" }, diff --git a/zenscript-code-model/src/main/java/raylras/zen/model/CompilationEnvironment.java b/zenscript-code-model/src/main/java/raylras/zen/model/CompilationEnvironment.java index 2e08baa1..42ccb83a 100644 --- a/zenscript-code-model/src/main/java/raylras/zen/model/CompilationEnvironment.java +++ b/zenscript-code-model/src/main/java/raylras/zen/model/CompilationEnvironment.java @@ -10,7 +10,6 @@ import raylras.zen.model.type.Types; import raylras.zen.util.PathUtil; -import java.nio.file.FileSystems; import java.nio.file.Path; import java.util.Collection; import java.util.HashMap; @@ -22,7 +21,6 @@ public class CompilationEnvironment { public static final String DEFAULT_ROOT_DIRECTORY = "scripts"; - public static final String DEFAULT_GENERATED_DIRECTORY = "generated"; private final Path root; private final Path generatedRoot; @@ -136,11 +134,7 @@ public String toString() { } private static Path resolveGeneratedRoot(CompilationEnvironment env) { - return FileSystems.getDefault() - .getPath(System.getProperty("user.home")) - .resolve(".probezs") - .resolve(PathUtil.toHash(env.getRoot())) - .resolve(DEFAULT_GENERATED_DIRECTORY); + return PathUtil.resolveGeneratedRoot(env.getRoot()); } } diff --git a/zenscript-code-model/src/main/java/raylras/zen/util/PathUtil.java b/zenscript-code-model/src/main/java/raylras/zen/util/PathUtil.java index eeba161d..712500a3 100644 --- a/zenscript-code-model/src/main/java/raylras/zen/util/PathUtil.java +++ b/zenscript-code-model/src/main/java/raylras/zen/util/PathUtil.java @@ -4,6 +4,7 @@ import java.math.BigInteger; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -12,6 +13,7 @@ public final class PathUtil { + public static final String DEFAULT_GENERATED_DIRECTORY = "generated"; private PathUtil() {} public static Path toPath(String uri) { @@ -75,4 +77,13 @@ public static String toHash(Path path) { } } + + public static Path resolveGeneratedRoot(Path envRoot) { + return FileSystems.getDefault() + .getPath(System.getProperty("user.home")) + .resolve(".probezs") + .resolve(PathUtil.toHash(envRoot)) + .resolve(DEFAULT_GENERATED_DIRECTORY); + } + } diff --git a/zenscript-code-model/src/main/java/raylras/zen/util/Position.java b/zenscript-code-model/src/main/java/raylras/zen/util/Position.java index 56dd317c..8cf07adb 100644 --- a/zenscript-code-model/src/main/java/raylras/zen/util/Position.java +++ b/zenscript-code-model/src/main/java/raylras/zen/util/Position.java @@ -24,4 +24,5 @@ public String toString() { return "(" + line + ":" + column + ')'; } + } diff --git a/zenscript-code-model/src/main/resources/simplelogger.properties b/zenscript-code-model/src/main/resources/simplelogger.properties deleted file mode 100644 index d0cfd814..00000000 --- a/zenscript-code-model/src/main/resources/simplelogger.properties +++ /dev/null @@ -1,38 +0,0 @@ -# SLF4J's SimpleLogger configuration file -# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. - -# Default logging detail level for all instances of SimpleLogger. -# Must be one of ("trace", "debug", "info", "warn", "error" or "off"). -# If not specified, defaults to "info". -org.slf4j.simpleLogger.defaultLogLevel=debug - -# Should the level string be output in brackets? -# Defaults to false. -org.slf4j.simpleLogger.levelInBrackets=true - -# Logging detail level for a SimpleLogger instance named "xxxxx". -# Must be one of ("trace", "debug", "info", "warn", "error" or "off"). -# If not specified, the default logging detail level is used. -#org.slf4j.simpleLogger.log.xxxxx= - -# Set to true if you want the current date and time to be included in output messages. -# Default is false, and will output the number of milliseconds elapsed since startup. -org.slf4j.simpleLogger.showDateTime=true - -# The date and time format to be used in the output messages. -# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. -# If the format is not specified or is invalid, the default format is used. -# The default format is yyyy-MM-dd HH:mm:ss:SSS Z. -org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss.SSS - -# Set to true if you want to output the current thread name. -# Defaults to true. -org.slf4j.simpleLogger.showThreadName=false - -# Set to true if you want the Logger instance name to be included in output messages. -# Defaults to true. -org.slf4j.simpleLogger.showLogName=true - -# Set to true if you want the last component of the name to be included in output messages. -# Defaults to false. -org.slf4j.simpleLogger.showShortLogName=true diff --git a/zenscript-debug-adapter/build.gradle b/zenscript-debug-adapter/build.gradle index 99622334..b83c4d1a 100644 --- a/zenscript-debug-adapter/build.gradle +++ b/zenscript-debug-adapter/build.gradle @@ -2,11 +2,20 @@ group 'raylras.zen.dap' dependencies { implementation project(':zenscript-code-model') + implementation 'org.eclipse.lsp4j:org.eclipse.lsp4j.debug:0.21.1' + implementation 'io.reactivex.rxjava3:rxjava:3.1.7' +} + +test { + useJUnitPlatform() } distDeps { from (configurations.runtimeClasspath) { - include 'org.eclipse.lsp4j-*.jar' + include 'org.eclipse.lsp4j.debug-*.jar' + include 'org.eclipse.lsp4j.jsonrpc.debug-*.jar' + include 'reactive-streams-*.jar' + include 'rxjava-*.jar' } into '../vscode-extension/server' } diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/DAPException.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/DAPException.java new file mode 100644 index 00000000..c8c59f81 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/DAPException.java @@ -0,0 +1,23 @@ +package raylras.zen.dap; + +public class DAPException extends RuntimeException { + public DAPException() { + super(); + } + + public DAPException(String message) { + super(message); + } + + public DAPException(String message, Throwable cause) { + super(message, cause); + } + + public DAPException(Throwable cause) { + super(cause); + } + + protected DAPException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/DAPLogAppender.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/DAPLogAppender.java new file mode 100644 index 00000000..b38882a7 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/DAPLogAppender.java @@ -0,0 +1,38 @@ +package raylras.zen.dap; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import ch.qos.logback.core.Layout; +import org.eclipse.lsp4j.debug.OutputEventArguments; +import org.eclipse.lsp4j.debug.OutputEventArgumentsCategory; +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; +import raylras.zen.dap.debugserver.IZenDebugProtocolClient; + +import java.lang.ref.WeakReference; + +public class DAPLogAppender extends AppenderBase { + private final WeakReference client; + + private Layout layout; + + public void setLayout(Layout layout) { + this.layout = layout; + } + + public DAPLogAppender(IZenDebugProtocolClient client) { + this.client = new WeakReference<>(client); + } + + @Override + protected void append(ILoggingEvent event) { + IZenDebugProtocolClient client = this.client.get(); + if(layout != null && client != null) { + String s = layout.doLayout(event); + client.outputLog(s); + + + } + } + + +} \ No newline at end of file diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/DAPPositions.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/DAPPositions.java new file mode 100644 index 00000000..da259afd --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/DAPPositions.java @@ -0,0 +1,93 @@ +package raylras.zen.dap; + + +import com.sun.jdi.Location; +import org.eclipse.lsp4j.debug.Breakpoint; +import org.eclipse.lsp4j.debug.SourceBreakpoint; +import raylras.zen.dap.debugserver.DebugAdapterContext; +import raylras.zen.util.Position; + +public final class DAPPositions { + + + public static void fillDAPBreakpoint(Breakpoint breakpoint, Position position, DebugAdapterContext context) { + breakpoint.setLine(toDAPLine(position, context)); + if (position.column() >= 0) { + breakpoint.setColumn(toDAPColumn(position, context)); + } + } + + public static Position fromDAPSourceBreakpoint(SourceBreakpoint sourceBreakpoint, DebugAdapterContext context) { + int line = fromDAPLine(sourceBreakpoint.getLine(), context); + int column = -1; + if (sourceBreakpoint.getColumn() != null) { + column = fromDAPColumn(sourceBreakpoint.getColumn(), context); + } + return Position.of(line, column); + } + + public static int fromDAPLine(int dapLine, DebugAdapterContext context) { + if (context.isLineStartAt1()) { + return dapLine - 1; + } + return dapLine; + } + + public static int fromDAPColumn(int dapColumn, DebugAdapterContext context) { + if (context.isColumnStartAt1()) { + return dapColumn - 1; + } + return dapColumn; + } + + public static int toDAPLine(int zeroStartLine, DebugAdapterContext context) { + if (context.isLineStartAt1()) { + return zeroStartLine + 1; + } + return zeroStartLine; + } + + public static int toDAPColumn(int zeroStartColumn, DebugAdapterContext context) { + if (context.isColumnStartAt1()) { + return zeroStartColumn + 1; + } + return zeroStartColumn; + } + + public static int toDAPLine(Position position, DebugAdapterContext context) { + return toDAPLine(position.line(), context); + } + + public static int toDAPColumn(Position position, DebugAdapterContext context) { + return toDAPColumn(position.column(), context); + } + + public static int toJDILine(int zeroStartColumn) { + return zeroStartColumn + 1; + } + + public static int toJDIColumn(int zeroStartColumn) { + return zeroStartColumn + 1; + } + public static int fromJDILine(int jdiLine) { + return jdiLine - 1; + } + + public static int fromJDIColumn(int jdiColumn) { + return jdiColumn - 1; + } + + public static int fromJDILine(Location location) { + return fromJDILine(location.lineNumber()); + } + + + public static int toJDILine(Position position) { + return toJDILine(position.line()); + } + + public static int toJDIColumn(Position position) { + return toJDILine(position.column()); + } + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/DebugAdapterContext.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/DebugAdapterContext.java new file mode 100644 index 00000000..aa53d3eb --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/DebugAdapterContext.java @@ -0,0 +1,99 @@ +package raylras.zen.dap.debugserver; + +import raylras.zen.dap.debugserver.breakpoint.BreakpointManager; +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; +import raylras.zen.dap.debugserver.runtime.StackFrameManager; +import raylras.zen.dap.debugserver.runtime.StepState; +import raylras.zen.dap.debugserver.runtime.ThreadManager; + +import java.nio.file.Path; + +public class DebugAdapterContext{ + + private boolean lineStartAt1; + private boolean columnStartAt1; + private boolean initialized; + private DebugSession debugSession; + private IZenDebugProtocolClient client; + + private final BreakpointManager breakpointManager = new BreakpointManager(); + private final StackFrameManager stackFrameManager = new StackFrameManager(); + private final ThreadManager threadManager = new ThreadManager(); + + public DebugObjectManager getDebugObjectManager() { + return debugObjectManager; + } + + private final DebugObjectManager debugObjectManager = new DebugObjectManager(threadManager); + private StepState pendingStep = null; + + private Path scriptRootPath; + + public Path getScriptRootPath() { + return scriptRootPath; + } + + public void setScriptRootPath(Path scriptRootPath) { + this.scriptRootPath = scriptRootPath; + } + + public boolean isInitialized() { + return initialized; + } + + public void setInitialized(boolean initialized) { + this.initialized = initialized; + } + + public DebugSession getDebugSession() { + return debugSession; + } + + public void setDebugSession(DebugSession debugSession) { + this.debugSession = debugSession; + } + + public BreakpointManager getBreakpointManager() { + return breakpointManager; + } + + public IZenDebugProtocolClient getClient() { + return client; + } + + public void setClient(IZenDebugProtocolClient client) { + this.client = client; + } + + public ThreadManager getThreadManager() { + return threadManager; + } + + public boolean isLineStartAt1() { + return lineStartAt1; + } + + public void setLineStartAt1(boolean lineStartAt1) { + this.lineStartAt1 = lineStartAt1; + } + + public boolean isColumnStartAt1() { + return columnStartAt1; + } + + public void setColumnStartAt1(boolean columnStartAt1) { + this.columnStartAt1 = columnStartAt1; + } + + public StackFrameManager getStackFrameManager() { + return stackFrameManager; + } + + public StepState getPendingStep() { + return pendingStep; + } + + public void setPendingStep(StepState pendingStep) { + this.pendingStep = pendingStep; + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/DebugSession.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/DebugSession.java new file mode 100644 index 00000000..929f5948 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/DebugSession.java @@ -0,0 +1,35 @@ +package raylras.zen.dap.debugserver; + +import com.sun.jdi.VirtualMachine; +import com.sun.jdi.request.EventRequest; +import com.sun.jdi.request.ThreadDeathRequest; +import com.sun.jdi.request.ThreadStartRequest; +import raylras.zen.dap.event.EventHub; + +public class DebugSession { + private final VirtualMachine virtualMachine; + private final EventHub eventHub = new EventHub(); + + public DebugSession(VirtualMachine virtualMachine) { + this.virtualMachine = virtualMachine; + } + + public EventHub eventHub() { + return eventHub; + } + + public VirtualMachine getVM() { + return virtualMachine; + } + + + public void start() { + ThreadStartRequest threadStartRequest = virtualMachine.eventRequestManager().createThreadStartRequest(); + ThreadDeathRequest threadDeathRequest = virtualMachine.eventRequestManager().createThreadDeathRequest(); + threadStartRequest.setSuspendPolicy(EventRequest.SUSPEND_NONE); + threadDeathRequest.setSuspendPolicy(EventRequest.SUSPEND_NONE); + threadStartRequest.enable(); + threadDeathRequest.enable(); + eventHub.start(virtualMachine); + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/IZenDebugProtocolClient.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/IZenDebugProtocolClient.java new file mode 100644 index 00000000..f6fff607 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/IZenDebugProtocolClient.java @@ -0,0 +1,11 @@ +package raylras.zen.dap.debugserver; + +import org.eclipse.lsp4j.debug.ContinuedEventArguments; +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; + +public interface IZenDebugProtocolClient extends IDebugProtocolClient { + @JsonNotification + default void outputLog(String args) { + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/StandardIOLauncher.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/StandardIOLauncher.java new file mode 100644 index 00000000..0b0b19f4 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/StandardIOLauncher.java @@ -0,0 +1,98 @@ +package raylras.zen.dap.debugserver; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.PatternLayout; +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; +import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.debug.DebugLauncher; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; +import org.slf4j.LoggerFactory; +import raylras.zen.dap.DAPException; +import raylras.zen.dap.DAPLogAppender; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class StandardIOLauncher { + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ZenDebugAdapter.class); + + + public static void main(String[] args) { + start(); + } + + public static void start() { + ZenDebugAdapter debugAdapter = new ZenDebugAdapter(); + ExecutorService threads = Executors.newSingleThreadExecutor(); + + Launcher serverLauncher = new DebugLauncher.Builder() + .setLocalService(debugAdapter) + .setRemoteInterface(IZenDebugProtocolClient.class) + .setInput(System.in) + .setOutput(System.out) + .setExecutorService(threads) + .setExceptionHandler(StandardIOLauncher::handleException) + .create(); + + IZenDebugProtocolClient client = serverLauncher.getRemoteProxy(); + initializeLogger(client); + debugAdapter.connect(client); + serverLauncher.startListening(); + } + + private static void initializeLogger(IZenDebugProtocolClient client) { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + loggerContext.reset(); + Logger rootLogger = loggerContext.getLogger("ROOT"); + PatternLayout layout = new PatternLayout(); + layout.setContext(loggerContext); + layout.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"); + layout.start(); + DAPLogAppender dapLogAppender = new DAPLogAppender(client); + dapLogAppender.setLayout(layout); + dapLogAppender.setContext(loggerContext); + dapLogAppender.start(); + rootLogger.addAppender(dapLogAppender); + } + + private static ResponseError handleException(Throwable throwable) { + if (throwable instanceof CompletionException || throwable instanceof InvocationTargetException && throwable.getCause() != null) { + return handleException(throwable.getCause()); + } + + if (throwable instanceof DAPException) { + + return new ResponseError( + 1, + throwable.getMessage(), + throwable + ); + } + + if (throwable instanceof ResponseErrorException responseError) { + return responseError.getResponseError(); + } + + + logger.error("Internal Error", throwable); + ResponseError error = new ResponseError(); + error.setMessage("Internal Error"); + error.setCode(ResponseErrorCode.InternalError); + ByteArrayOutputStream stackTrace = new ByteArrayOutputStream(); + PrintWriter stackTraceWriter = new PrintWriter(stackTrace); + throwable.printStackTrace(stackTraceWriter); + stackTraceWriter.flush(); + error.setData(stackTrace.toString()); + return error; + } + +} + diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/ZenDebugAdapter.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/ZenDebugAdapter.java new file mode 100644 index 00000000..7ec2bf4e --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/ZenDebugAdapter.java @@ -0,0 +1,246 @@ +package raylras.zen.dap.debugserver; + +import com.sun.jdi.IncompatibleThreadStateException; +import com.sun.jdi.VMDisconnectedException; +import com.sun.jdi.VirtualMachine; +import org.eclipse.lsp4j.debug.*; +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import raylras.zen.dap.debugserver.handler.*; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class ZenDebugAdapter implements IDebugProtocolServer { + private final DebugAdapterContext context = new DebugAdapterContext(); + private static final Logger logger = LoggerFactory.getLogger(ZenDebugAdapter.class); + + @Override + public CompletableFuture initialize(InitializeRequestArguments args) { + Capabilities capabilities = new Capabilities(); + capabilities.setSupportsConfigurationDoneRequest(true); + capabilities.setSupportsSingleThreadExecutionRequests(true); + capabilities.setSupportsSteppingGranularity(true); + capabilities.setSupportsDelayedStackTraceLoading(true); + capabilities.setSupportsTerminateRequest(true); + + capabilities.setSupportsStepBack(false); + capabilities.setSupportsInstructionBreakpoints(false); + capabilities.setSupportsTerminateThreadsRequest(false); + capabilities.setSupportSuspendDebuggee(false); + + context.setLineStartAt1(args.getLinesStartAt1()); + context.setColumnStartAt1(args.getColumnsStartAt1()); + context.setInitialized(true); + + return CompletableFuture.completedFuture(capabilities); + } + + @Override + public CompletableFuture configurationDone(ConfigurationDoneArguments args) { + ConfigurationDoneHandler.handle(context); + context.getDebugSession().getVM().resume(); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture launch(Map args) { + DebugStartHandler.LaunchArgument attachArgument = DebugStartHandler.parseLaunchArgs(args); + DebugStartHandler.handleLaunch(attachArgument, context).thenAccept(succeed -> { + if (succeed) { + // notify client that we have started jvm and wait for breakpoints + context.getClient().initialized(); + } else { + context.getClient().terminated(new TerminatedEventArguments()); + } + }); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture attach(Map args) { + + DebugStartHandler.AttachArgument attachArgument = DebugStartHandler.parseAttachArgs(args); + boolean succeed = DebugStartHandler.handleAttach(attachArgument, context); + if (succeed) { + // notify client that we have started jvm and wait for breakpoints + context.getClient().initialized(); + } else { + context.getClient().terminated(new TerminatedEventArguments()); + } + + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture disconnect(DisconnectArguments args) { + DebugSession debugSession = context.getDebugSession(); + if (debugSession == null) { + return CompletableFuture.completedFuture(null); + } + VirtualMachine vm = debugSession.getVM(); + try { + if (args.getTerminateDebuggee() != null && args.getTerminateDebuggee()) { + vm.exit(0); + } else { + vm.dispose(); + } + } catch (VMDisconnectedException ignored) { + + } + + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture terminate(TerminateArguments args) { + DebugSession debugSession = context.getDebugSession(); + if (debugSession != null) { + try { + debugSession.getVM().exit(0); + } catch (VMDisconnectedException ignored) { + + } + } + return CompletableFuture.completedFuture(null); + } + + public void connect(IZenDebugProtocolClient remoteProxy) { + context.setClient(remoteProxy); + } + + + @Override + public CompletableFuture breakpointLocations(BreakpointLocationsArguments args) { + return IDebugProtocolServer.super.breakpointLocations(args); + } + + @Override + public CompletableFuture setBreakpoints(SetBreakpointsArguments args) { + SetBreakpointsResponse response = new SetBreakpointsResponse(); + Breakpoint[] breakpoints = SetBreakpointsHandler.setBreakpoints(context, args); + response.setBreakpoints(breakpoints); + return CompletableFuture.completedFuture(response); + } + + + @Override + public CompletableFuture setFunctionBreakpoints(SetFunctionBreakpointsArguments args) { + return IDebugProtocolServer.super.setFunctionBreakpoints(args); + } + + @Override + public CompletableFuture setExceptionBreakpoints(SetExceptionBreakpointsArguments args) { + // TODO + return CompletableFuture.completedFuture(new SetExceptionBreakpointsResponse()); + } + + @Override + public CompletableFuture dataBreakpointInfo(DataBreakpointInfoArguments args) { + return IDebugProtocolServer.super.dataBreakpointInfo(args); + } + + @Override + public CompletableFuture setDataBreakpoints(SetDataBreakpointsArguments args) { + return IDebugProtocolServer.super.setDataBreakpoints(args); + } + + @Override + public CompletableFuture threads() { + ThreadsResponse response = ThreadListHandler.visibleThreads(context); + return CompletableFuture.completedFuture(response); + } + + + @Override + public CompletableFuture stackTrace(StackTraceArguments args) { + StackTraceResponse stackTrace; + try { + stackTrace = StackTraceHandler.getStackTrace(context, args); + } catch (IncompatibleThreadStateException e) { + logger.error("Could not get stack trace of thread {}", args.getThreadId(), e); + return CompletableFuture.completedFuture(new StackTraceResponse()); + } + + return CompletableFuture.completedFuture(stackTrace); + } + + @Override + public CompletableFuture scopes(ScopesArguments args) { + ScopesResponse response = new ScopesResponse(); + List scopes = ScopeHandler.scopes(args.getFrameId(), context); + response.setScopes(scopes.toArray(new Scope[0])); + return CompletableFuture.completedFuture(response); + } + + @Override + public CompletableFuture variables(VariablesArguments args) { + VariablesResponse response = new VariablesResponse(); + List variables = VariablesHandler.variables(args, context); + response.setVariables(variables.toArray(new Variable[0])); + return CompletableFuture.completedFuture(response); + } + + @Override + public CompletableFuture evaluate(EvaluateArguments args) { + return IDebugProtocolServer.super.evaluate(args); + } + + @Override + public CompletableFuture continue_(ContinueArguments args) { + ContinueResponse response = new ContinueResponse(); + boolean allContinued = DebugJumpHandler.continue_(args, context); + response.setAllThreadsContinued(allContinued); + return CompletableFuture.completedFuture(response); + } + + @Override + public CompletableFuture source(SourceArguments args) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture next(NextArguments args) { + DebugJumpHandler.next(args, context); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture stepIn(StepInArguments args) { + DebugJumpHandler.stepIn(args, context); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture stepOut(StepOutArguments args) { + DebugJumpHandler.stepOut(args, context); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture goto_(GotoArguments args) { +// DebugJumpHandler.goto_(args, context); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture pause(PauseArguments args) { + DebugJumpHandler.pause(args.getThreadId(), context); + return CompletableFuture.completedFuture(null); + } + + + @Override + public CompletableFuture stepInTargets(StepInTargetsArguments args) { + return IDebugProtocolServer.super.stepInTargets(args); + } + + @Override + public CompletableFuture gotoTargets(GotoTargetsArguments args) { + return IDebugProtocolServer.super.gotoTargets(args); + } + + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/breakpoint/Breakpoint.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/breakpoint/Breakpoint.java new file mode 100644 index 00000000..51f0170a --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/breakpoint/Breakpoint.java @@ -0,0 +1,141 @@ +package raylras.zen.dap.debugserver.breakpoint; + +import com.sun.jdi.VMDisconnectedException; +import com.sun.jdi.VirtualMachine; +import com.sun.jdi.event.ClassPrepareEvent; +import com.sun.jdi.request.BreakpointRequest; +import com.sun.jdi.request.EventRequest; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import raylras.zen.dap.DAPPositions; +import raylras.zen.dap.event.EventHub; +import raylras.zen.util.Position; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import static raylras.zen.dap.jdi.ObservableUtils.safeFilter; + +public class Breakpoint { + private static final Logger logger = LoggerFactory.getLogger(Breakpoint.class); + private final String sourcePath; + + private Position position; + private boolean isVerified = false; + private final VirtualMachine vm; + private final EventHub eventHub; + private int id; + + + public Breakpoint(Position position, String sourcePath, VirtualMachine vm, EventHub eventHub) { + this.position = position; + this.sourcePath = sourcePath; + this.vm = vm; + this.eventHub = eventHub; + } + + private final List requests = Collections.synchronizedList(new ArrayList<>()); + private final List subscriptions = new ArrayList<>(); + + public Position getPosition() { + return position; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public List getRequests() { + return requests; + } + + public List getSubscriptions() { + return subscriptions; + } + + public String getSourcePath() { + return sourcePath; + } + + public boolean isVerified() { + return isVerified; + } + + public boolean hasRequest(EventRequest request) { + return getRequests().stream().anyMatch(it -> Objects.equals(request, it)); + } + + public CompletableFuture install() { + logger.info("Start install breakpoint at {}({}:{})", this.sourcePath, this.position.line(), this.position.column()); + CompletableFuture future = new CompletableFuture<>(); + int jdiLineNumber = DAPPositions.toJDILine(position); + Disposable subscribe = Observable.merge( + Observable.fromIterable(vm.allClasses()), + eventHub.classPrepareEvents() + .map(it -> (ClassPrepareEvent) it.getEvent()) + .map(ClassPrepareEvent::referenceType) + ) + .filter(safeFilter(it -> Objects.equals(sourcePath, it.sourceName()))) + .doOnComplete(() -> { + logger.warn("Stopped observing breakpoint at {}({}:{})", this.sourcePath, this.position.line(), this.position.column()); + }) + .doOnNext(it -> { + logger.info("Loading breakpoints for {}({}) ...", it.name(), sourcePath); + }) + .flatMapMaybe(referenceType -> Observable.fromIterable(referenceType.methods()) + .flatMap(method -> Observable.fromSupplier(() -> method.locationsOfLine(jdiLineNumber)) + .flatMap(Observable::fromIterable) + .onErrorComplete() + ) + .filter(location -> requests.stream().filter(it -> it instanceof BreakpointRequest) + .map(it -> (BreakpointRequest) it) + .map(BreakpointRequest::location) + .noneMatch(it -> Objects.equals(location, it)) + ) + .map(it -> { + BreakpointRequest breakpointRequest = vm.eventRequestManager().createBreakpointRequest(it); + breakpointRequest.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); + return breakpointRequest; + }) + .doOnNext(it -> { + it.enable(); + requests.add(it); + }) + .count() + .filter(it -> it > 0) + .doOnSuccess(it -> { + logger.info("Added {} breakpoints at {}({})", it, this.sourcePath, this.position.line()); + this.isVerified = true; + future.complete(this); + }) + ) + .doOnError(e -> { + logger.error("Exception occurred when installing breakpoint at {}({}:{})", this.sourcePath, this.position.line(), this.position.column(), e); + }) + .subscribe(); + subscriptions.add(subscribe); + + return future; + } + + + public void close() { + try { + vm.eventRequestManager().deleteEventRequests(getRequests()); + } catch (VMDisconnectedException ex) { + // ignore since removing breakpoints is meaningless when JVM is terminated. + } + getSubscriptions().forEach(Disposable::dispose); + requests.clear(); + subscriptions.clear(); + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/breakpoint/BreakpointManager.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/breakpoint/BreakpointManager.java new file mode 100644 index 00000000..cc41476e --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/breakpoint/BreakpointManager.java @@ -0,0 +1,125 @@ +package raylras.zen.dap.debugserver.breakpoint; + +import com.sun.jdi.VirtualMachine; +import com.sun.jdi.request.ClassPrepareRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import raylras.zen.util.Position; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +public class BreakpointManager { + private static final Logger logger = LoggerFactory.getLogger(BreakpointManager.class); + /** + * A collection of breakpoints registered with this manager. + */ + private final List breakpoints = Collections.synchronizedList(new ArrayList<>(10)); + private final Map> sourceToBreakpoints = new HashMap<>(); + private final AtomicInteger nextBreakpointId = new AtomicInteger(1); + + /** + * Constructor. + */ + public BreakpointManager() { + } + + public List findBreakpointsAt(String sourceName, int line) { + HashMap positionBreakpointHashMap = sourceToBreakpoints.get(sourceName); + if (positionBreakpointHashMap == null) { + return Collections.emptyList(); + } + + return positionBreakpointHashMap.entrySet().stream() + .filter(it -> it.getKey().line() == line) + .map(Map.Entry::getValue) + .toList(); + } + + public List setBreakpoints(String source, List breakpoints, boolean sourceModified) { + List result = new ArrayList<>(); + HashMap breakpointMap = this.sourceToBreakpoints.get(source); + // When source file is modified, delete all previously added breakpoints. + if (sourceModified && breakpointMap != null) { + for (Breakpoint bp : breakpointMap.values()) { + try { + // Destroy the breakpoint on the debugee VM. + bp.close(); + } catch (Exception e) { + logger.error("Remove breakpoint exception: ", e); + } + this.breakpoints.remove(bp); + } + this.sourceToBreakpoints.put(source, null); + breakpointMap = null; + } + if (breakpointMap == null) { + breakpointMap = new HashMap<>(); + this.sourceToBreakpoints.put(source, breakpointMap); + } + + // Compute the breakpoints that are newly added. + List toAdd = new ArrayList<>(); + List visitedBreakpoints = new ArrayList<>(); + for (Breakpoint breakpoint : breakpoints) { + Breakpoint existed = breakpointMap.get(breakpoint.getPosition()); + if (existed != null) { + result.add(existed); + visitedBreakpoints.add(existed.hashCode()); + continue; + } else { + result.add(breakpoint); + } + toAdd.add(breakpoint); + } + + // Compute the breakpoints that are no longer listed. + List toRemove = new ArrayList<>(); + for (Breakpoint breakpoint : breakpointMap.values()) { + if (!visitedBreakpoints.contains(breakpoint.hashCode())) { + toRemove.add(breakpoint); + } + } + + removeBreakpointsInternally(source, toRemove); + addBreakpointsInternally(source, toAdd); + + return result; + } + + + private void addBreakpointsInternally(String source, List breakpoints) { + Map breakpointMap = this.sourceToBreakpoints.computeIfAbsent(source, k -> new HashMap<>()); + + if (breakpoints != null && !breakpoints.isEmpty()) { + for (Breakpoint breakpoint : breakpoints) { + breakpoint.setId(this.nextBreakpointId.getAndIncrement()); + this.breakpoints.add(breakpoint); + breakpointMap.put(breakpoint.getPosition(), breakpoint); + } + } + } + + /** + * Removes the specified breakpoints from breakpoint manager. + */ + private void removeBreakpointsInternally(String source, List breakpoints) { + Map breakpointMap = this.sourceToBreakpoints.get(source); + if (breakpointMap == null || breakpointMap.isEmpty() || breakpoints.isEmpty()) { + return; + } + + for (Breakpoint breakpoint : breakpoints) { + if (this.breakpoints.contains(breakpoint)) { + try { + // Destroy the breakpoint on the debugee VM. + breakpoint.close(); + this.breakpoints.remove(breakpoint); + breakpointMap.remove(breakpoint.getPosition()); + } catch (Exception e) { + logger.error("Remove breakpoint exception", e); + } + } + } + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/ConfigurationDoneHandler.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/ConfigurationDoneHandler.java new file mode 100644 index 00000000..5dc563c3 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/ConfigurationDoneHandler.java @@ -0,0 +1,89 @@ +package raylras.zen.dap.debugserver.handler; + +import com.sun.jdi.event.*; +import io.reactivex.rxjava3.disposables.Disposable; +import org.eclipse.lsp4j.debug.ExitedEventArguments; +import org.eclipse.lsp4j.debug.TerminatedEventArguments; +import org.eclipse.lsp4j.debug.ThreadEventArguments; +import org.eclipse.lsp4j.debug.ThreadEventArgumentsReason; +import raylras.zen.dap.debugserver.DebugAdapterContext; +import raylras.zen.dap.debugserver.DebugSession; + +public class ConfigurationDoneHandler { + + + public static void handle(DebugAdapterContext context) { + DebugSession session = context.getDebugSession(); + + if (session == null) { + // the vm is not started + TerminatedEventArguments terminatedEventArguments = new TerminatedEventArguments(); + terminatedEventArguments.setRestart(false); + context.getClient().terminated(terminatedEventArguments); + return; + } + Disposable subscribe = session.eventHub().allEvents().subscribe(jdiEvent -> { + Event event = jdiEvent.getEvent(); + + if (event instanceof VMStartEvent vmStartEvent) { + handleVMStart(vmStartEvent); + } else if (event instanceof VMDeathEvent vmDeathEvent) { + handleVMDeath(vmDeathEvent, context); + } else if (event instanceof VMDisconnectEvent vmDisconnectEvent) { + handleVMDisconnect(vmDisconnectEvent, context); + } else if (event instanceof ThreadStartEvent threadStartEvent) { + handleThreadStart(threadStartEvent, context); + } else if (event instanceof ThreadDeathEvent threadDeathEvent) { + handleThreadDeath(threadDeathEvent, context); + } else if (event instanceof ExceptionEvent exceptionEvent) { + handleException(exceptionEvent); + } + + }); + + + } + + private static void handleException(ExceptionEvent exceptionEvent) { + } + + private static void handleThreadDeath(ThreadDeathEvent threadDeathEvent, DebugAdapterContext context) { + context.getStackFrameManager().removeByThread(threadDeathEvent.thread().uniqueID()); + context.getDebugObjectManager().removeByThread(threadDeathEvent.thread().uniqueID()); + int id = context.getThreadManager().threadStopped(threadDeathEvent.thread()); + ThreadEventArguments threadEventArguments = new ThreadEventArguments(); + threadEventArguments.setThreadId(id); + threadEventArguments.setReason(ThreadEventArgumentsReason.EXITED); + context.getClient().thread(threadEventArguments); + } + + private static void handleThreadStart(ThreadStartEvent threadStartEvent, DebugAdapterContext context) { + int id = context.getThreadManager().threadStarted(threadStartEvent.thread()); + ThreadEventArguments threadEventArguments = new ThreadEventArguments(); + threadEventArguments.setThreadId(id); + threadEventArguments.setReason(ThreadEventArgumentsReason.STARTED); + context.getClient().thread(threadEventArguments); + } + + private static void handleVMDisconnect(VMDisconnectEvent vmDisconnectEvent, DebugAdapterContext context) { + + TerminatedEventArguments terminatedEventArguments = new TerminatedEventArguments(); + terminatedEventArguments.setRestart(false); + context.getClient().terminated(terminatedEventArguments); + } + + private static void handleVMDeath(VMDeathEvent vmDeathEvent, DebugAdapterContext context) { + ExitedEventArguments exitedEventArguments = new ExitedEventArguments(); + try { + exitedEventArguments.setExitCode(vmDeathEvent.virtualMachine().process().exitValue()); + } catch (Exception ignored) { + + } + context.getClient().exited(exitedEventArguments); + } + + private static void handleVMStart(VMStartEvent vmStartEvent) { + } + + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/DebugJumpHandler.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/DebugJumpHandler.java new file mode 100644 index 00000000..ea083055 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/DebugJumpHandler.java @@ -0,0 +1,123 @@ +package raylras.zen.dap.debugserver.handler; + +import com.sun.jdi.IncompatibleThreadStateException; +import com.sun.jdi.Location; +import com.sun.jdi.StackFrame; +import com.sun.jdi.ThreadReference; +import org.eclipse.lsp4j.debug.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import raylras.zen.dap.debugserver.DebugAdapterContext; +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; +import raylras.zen.dap.debugserver.runtime.StackFrameManager; +import raylras.zen.dap.debugserver.runtime.StepState; +import raylras.zen.dap.debugserver.runtime.ThreadManager; + + +public final class DebugJumpHandler { + private static final Logger logger = LoggerFactory.getLogger(DebugJumpHandler.class); + + + public static boolean continue_(ContinueArguments args, DebugAdapterContext context) { + return doResume(args.getSingleThread() != null && args.getSingleThread(), args.getThreadId(), context); + } + + public static void pause(int threadId, DebugAdapterContext context) { + ThreadReference threadReference = context.getThreadManager().getById(threadId); + if (threadReference == null) { + logger.warn("Failed to pause, could not find running thread with id: {}", threadId); + return; + } + boolean succeed = context.getThreadManager().pauseThread(threadReference); + if (succeed) {; + StoppedEventArguments arguments = new StoppedEventArguments(); + arguments.setReason(StoppedEventArgumentsReason.PAUSE); + arguments.setThreadId(threadId); + context.getClient().stopped(arguments); + } else { + logger.warn("Failed to pause thread: {}, is is already paused!", threadReference.name()); + } + } + + private static boolean doResume(boolean singleThread, Integer threadId, DebugAdapterContext context) { + ThreadManager threadManager = context.getThreadManager(); + StackFrameManager stackFrameManager = context.getStackFrameManager(); + DebugObjectManager debugObjectManager = context.getDebugObjectManager(); + if (singleThread) { + ThreadReference threadReference = context.getThreadManager().getById(threadId); + if (threadReference == null) { + logger.warn("Failed to resume, could not find running thread with id: {}", threadId); + return false; + } + stackFrameManager.removeByThread(threadReference.uniqueID()); + debugObjectManager.removeByThread(threadReference.uniqueID()); + + boolean resumed = threadManager.resumeThread(threadReference); + if (!resumed) { + logger.warn("Failed to resume thread: {}", threadReference.name()); + } + return false; + } + boolean allResumed = true; + stackFrameManager.reset(); + debugObjectManager.reset(); + for (ThreadReference threadReference : context.getThreadManager().pausedThreads()) { + + boolean resumed = threadManager.resumeThread(threadReference); + if (!resumed) { + logger.warn("Failed to resume thread: {}", threadReference.name()); + allResumed = false; + } + } + return allResumed; + } + + private static void doStep(Boolean singleThread, int threadId, DebugAdapterContext context, StepState.Kind kind, boolean isLine) { + StepState pendingStep = context.getPendingStep(); + if (pendingStep != null) { + pendingStep.close(context); + } + ThreadReference threadReference = context.getThreadManager().getById(threadId); + Location location = null; + try { + StackFrame frame = threadReference.frame(0); + location = frame.location(); + } catch (IncompatibleThreadStateException ignored) { + } + + pendingStep = new StepState(kind, isLine, location); + pendingStep.configure(context, threadReference); + pendingStep.install(context); + context.setPendingStep(pendingStep); + doResume(singleThread != null && singleThread, threadId, context); + } + + public static void next(NextArguments args, DebugAdapterContext context) { + boolean isLine = args.getGranularity() != SteppingGranularity.STATEMENT; + doStep(args.getSingleThread(), args.getThreadId(), context, StepState.Kind.STEP_OVER, isLine); + } + + public static void stepIn(StepInArguments args, DebugAdapterContext context) { + boolean isLine = args.getGranularity() != SteppingGranularity.STATEMENT; + doStep(args.getSingleThread(), args.getThreadId(), context, StepState.Kind.STEP_IN, isLine); + } + + public static void stepOut(StepOutArguments args, DebugAdapterContext context) { + boolean isLine = args.getGranularity() != SteppingGranularity.STATEMENT; + doStep(args.getSingleThread(), args.getThreadId(), context, StepState.Kind.STEP_OUT, isLine); + } + + public static void goto_(GotoArguments args) { + + } + + + public static void stepInTargets(StepInTargetsArguments args) { + + } + + public static void gotoTargets(GotoTargetsArguments args) { + + } + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/DebugStartHandler.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/DebugStartHandler.java new file mode 100644 index 00000000..429bf11b --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/DebugStartHandler.java @@ -0,0 +1,262 @@ +package raylras.zen.dap.debugserver.handler; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.sun.jdi.VirtualMachine; +import com.sun.jdi.connect.Connector; +import com.sun.jdi.connect.IllegalConnectorArgumentsException; +import com.sun.jdi.connect.ListeningConnector; +import com.sun.jdi.request.EventRequest; +import com.sun.jdi.request.VMDeathRequest; +import org.eclipse.lsp4j.debug.RunInTerminalRequestArguments; +import org.eclipse.lsp4j.debug.RunInTerminalRequestArgumentsKind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import raylras.zen.dap.debugserver.DebugAdapterContext; +import raylras.zen.dap.debugserver.DebugSession; +import raylras.zen.dap.jdi.JDILauncher; +import raylras.zen.util.PathUtil; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class DebugStartHandler { + + private static final Logger logger = LoggerFactory.getLogger(DebugStartHandler.class); + + + public record AttachArgument( + String hostName, + int port, + Path scriptRoot + ) { + } + + public record LaunchArgument( + Path scriptRoot, + Path javaExecutable, + boolean launchInTerminal + ) { + } + + public static AttachArgument parseAttachArgs(Map args) { + + String hostName = (String) args.get("hostName"); + int port = (int) ((double) args.get("port")); + String scriptRoot = (String) args.get("scriptRoot"); + + return new AttachArgument(hostName, port, Path.of(scriptRoot)); + } + + + public static LaunchArgument parseLaunchArgs(Map args) { + + String scriptRoot = (String) args.get("scriptRoot"); + String javaExecutable = (String) args.get("javaExecutable"); + Path scriptRootPath = Path.of(scriptRoot); + Object terminal = args.get("launchInTerminal"); + boolean inTerminal = !(terminal instanceof Boolean && !((boolean) terminal)); + if (javaExecutable == null) { + return new LaunchArgument(scriptRootPath, null, inTerminal); + } + return new LaunchArgument(scriptRootPath, Path.of(javaExecutable), inTerminal); + } + + public static boolean handleAttach(AttachArgument attach, DebugAdapterContext context) { + try { + logger.info("trying to attach to vm {}:{}", attach.hostName, attach.port); + + DebugSession debugSession = JDILauncher.attach(attach.hostName, attach.port, 100); + + afterStart(attach.scriptRoot, context, debugSession); + return true; + } catch (Exception e) { + logger.error("failed to attach to vm", e); + return false; + } + } + + private static void afterStart(Path scriptRoot, DebugAdapterContext context, DebugSession debugSession) { + VMDeathRequest vmDeathRequest = debugSession.getVM().eventRequestManager().createVMDeathRequest(); + vmDeathRequest.setSuspendPolicy(EventRequest.SUSPEND_NONE); + vmDeathRequest.setEnabled(true); + + context.setScriptRootPath(scriptRoot); + context.setDebugSession(debugSession); + + debugSession.start(); + SetBreakpointsHandler.registerBreakpointHandler(context); + } + + private static final int ATTACH_TERMINAL_TIMEOUT = 20 * 1000; + + + public static CompletableFuture handleLaunch(LaunchArgument launchArgument, DebugAdapterContext context) { + + ListeningConnector connector = JDILauncher.getListeningConnector(); + Map listenArguments = JDILauncher.getListenArguments(connector, ATTACH_TERMINAL_TIMEOUT); + String[] commands; + try { + String address = connector.startListening(listenArguments); + commands = constructLaunchCommands(launchArgument, address); + } catch (IOException | IllegalConnectorArgumentsException e) { + logger.error("failed to construct launch commands", e); + return CompletableFuture.completedFuture(false); + } + logger.info("trying to attach to launch minecraft with command: {}", String.join(" ", commands)); + CompletableFuture future = new CompletableFuture<>(); + + if (launchArgument.launchInTerminal) { + String[] scriptCommands = generateStartupScript(commands); + if (scriptCommands == null) { + return CompletableFuture.completedFuture(false); + } + RunInTerminalRequestArguments runArgs = new RunInTerminalRequestArguments(); + runArgs.setArgs(scriptCommands); + runArgs.setKind(RunInTerminalRequestArgumentsKind.INTEGRATED); + runArgs.setTitle("Minecraft"); + runArgs.setArgsCanBeInterpretedByShell(false); + runArgs.setCwd(launchArgument.scriptRoot.resolve("..").normalize().toString()); + + context.getClient().runInTerminal(runArgs).whenCompleteAsync((response, e) -> { + if (e != null) { + logger.error("Failed to launch minecraft", e); + future.complete(false); + try { + connector.stopListening(listenArguments); + } catch (Exception ignored) { + } + } + + try { + VirtualMachine vm = connector.accept(listenArguments); + afterStart(launchArgument.scriptRoot, context, new DebugSession(vm)); + future.complete(true); + } catch (Exception ex) { + logger.error("Failed to launch minecraft", ex); + future.complete(false); + } + + }); + } else { + CompletableFuture.runAsync(() -> { + try { + Runtime.getRuntime().exec(commands, new String[0], launchArgument.scriptRoot.resolve("..").toFile()); + try { + VirtualMachine vm = connector.accept(listenArguments); + afterStart(launchArgument.scriptRoot, context, new DebugSession(vm)); + future.complete(true); + } catch (Exception ex) { + logger.error("Failed to launch minecraft", ex); + future.complete(false); + } + } catch (IOException e) { + logger.error("Failed to launch minecraft", e); + future.complete(false); + } + }); + + } + + return future; + } + + + public static String[] generateStartupScript(String[] args) { + try { + + List outCommand = new ArrayList<>(); + boolean isWindows = System.getProperty("os.name").startsWith("Windows"); + + File scriptFile = File.createTempFile("ZenScriptDAPLaunchMinecraft", isWindows ? ".bat" : ".sh"); + scriptFile.deleteOnExit(); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(scriptFile))) { + if (isWindows) { + writer.write("@echo off"); + + } else { + writer.write("#!/bin/bash"); + } + writer.newLine(); + for (int i = 0; i < args.length; i++) { + if (i != 0) { + writer.write(" "); + } + writer.write(args[i]); + } + } + + if (isWindows) { + // Windows + outCommand.add("cmd.exe"); + outCommand.add("/c"); + } else { + outCommand.add("bash"); + } + outCommand.add(scriptFile.getAbsolutePath()); + return outCommand.toArray(new String[0]); + } catch (IOException e) { + logger.error("Failed to generate launch script", e); + return null; + } + } + + private static final Gson GSON = new GsonBuilder() + .create(); + + /** + * Construct the Java command lines based on the given launch arguments. + * + * @param address - the debug port + * @return the command arrays + */ + public static String[] constructLaunchCommands(LaunchArgument launchArgument, String address) throws IOException { + List launchCmds = new ArrayList<>(); + + Path path = PathUtil.resolveGeneratedRoot(launchArgument.scriptRoot).resolve("env.json"); + + Map args = GSON.fromJson(Files.newBufferedReader(path), new TypeToken<>() { + }); + + String javaPath = (String) args.get("javaPath"); + String classpath = (String) args.get("classpath"); + List jvmFlags = (List) args.get("jvmFlags"); + List launchArgs = (List) args.get("launchArgs"); + + if (launchArgument.javaExecutable != null && Files.exists(launchArgument.javaExecutable)) { + launchCmds.add(launchArgument.javaExecutable.toString()); + } else { + launchCmds.add(Paths.get(javaPath, "bin", "java").toString()); + } + + launchCmds.add(String.format("-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=%s", address)); + for (String jvmFlag : jvmFlags) { + if (jvmFlag.matches("-agentlib:jdwp=.*")) { + continue; + } + launchCmds.add(jvmFlag); + } + + launchCmds.add("-cp"); + launchCmds.add(classpath); + + launchCmds.add("net.minecraft.launchwrapper.Launch"); + + launchCmds.add("--tweakClass"); + launchCmds.add("net.minecraftforge.fml.common.launcher.FMLTweaker"); + launchCmds.addAll(launchArgs); + + return launchCmds.toArray(new String[0]); + } + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/ScopeHandler.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/ScopeHandler.java new file mode 100644 index 00000000..1e0d2f7f --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/ScopeHandler.java @@ -0,0 +1,94 @@ +package raylras.zen.dap.debugserver.handler; + +import com.sun.jdi.*; +import org.eclipse.lsp4j.debug.Scope; +import org.eclipse.lsp4j.debug.ScopePresentationHint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import raylras.zen.dap.debugserver.DebugAdapterContext; +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; +import raylras.zen.dap.debugserver.runtime.StackFrameManager; +import raylras.zen.dap.debugserver.variable.VariableProxy; +import raylras.zen.dap.debugserver.variable.VariableProxyFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ScopeHandler { + private static final Logger logger = LoggerFactory.getLogger(ScopeHandler.class); + + public static List scopes(int frameId, DebugAdapterContext context) { + DebugObjectManager debugObjectManager = context.getDebugObjectManager(); + StackFrameManager stackFrameManager = context.getStackFrameManager(); + StackFrame stackFrame = stackFrameManager.getById(frameId); + if (stackFrame == null) { + logger.warn("trying to fetch scope of invalid stack frame {}", frameId); + return Collections.emptyList(); + } + ThreadReference thread = stackFrame.thread(); + + VariableProxyFactory factory = debugObjectManager.getVariableFactory(); + + List scopes = new ArrayList<>(); + + + List localVariables = new ArrayList<>(); + List argumentVariables = new ArrayList<>(); + + try { + ObjectReference thisObject = stackFrame.thisObject(); + if (thisObject != null) { + VariableProxy proxy = factory.createValueProxy("this", thisObject, thread); + localVariables.add(proxy); + } + } catch (Exception e) { + logger.error("failed to get this of " + frameId, e); + } + try { + for (LocalVariable visibleVariable : stackFrame.visibleVariables()) { + Value value = stackFrame.getValue(visibleVariable); + if (value == null) { + logger.error("failed to get local variables of " + visibleVariable.name()); + continue; + } + VariableProxy proxy = factory.createValueProxy(visibleVariable.name(), value, thread); + if (visibleVariable.isArgument()) { + argumentVariables.add(proxy); + } else { + localVariables.add(proxy); + } + } + } catch (AbsentInformationException ignored) { + logger.warn("the stack frame '{}' does not have local variable info.", frameId); + } catch (Exception e) { + logger.error("failed to get local variables of " + frameId, e); + } + + // locals + Scope locals = new Scope(); + locals.setName("Locals"); + locals.setPresentationHint(ScopePresentationHint.LOCALS); + + VariableProxy localScope = factory.createScope("Locals", localVariables, thread); + locals.setVariablesReference(debugObjectManager.getId(localScope)); + scopes.add(locals); + + // arguments + Scope arguments = new Scope(); + arguments.setName("Arguments"); + arguments.setPresentationHint(ScopePresentationHint.ARGUMENTS); + + VariableProxy argumentScope = factory.createScope("Arguments", argumentVariables, thread); + arguments.setVariablesReference(debugObjectManager.getId(argumentScope)); + scopes.add(arguments); + + + + + return scopes; + + } + + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/SetBreakpointsHandler.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/SetBreakpointsHandler.java new file mode 100644 index 00000000..b6eaee9c --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/SetBreakpointsHandler.java @@ -0,0 +1,122 @@ +package raylras.zen.dap.debugserver.handler; + +import com.sun.jdi.Location; +import com.sun.jdi.ThreadReference; +import com.sun.jdi.event.BreakpointEvent; +import com.sun.jdi.event.StepEvent; +import com.sun.jdi.request.ClassPrepareRequest; +import com.sun.jdi.request.EventRequest; +import io.reactivex.rxjava3.disposables.Disposable; +import org.eclipse.lsp4j.debug.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import raylras.zen.dap.DAPPositions; +import raylras.zen.dap.debugserver.DebugAdapterContext; +import raylras.zen.dap.debugserver.DebugSession; +import raylras.zen.dap.debugserver.ZenDebugAdapter; +import raylras.zen.dap.debugserver.breakpoint.Breakpoint; +import raylras.zen.util.Position; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class SetBreakpointsHandler { + + private static final Logger logger = LoggerFactory.getLogger(ZenDebugAdapter.class); + + public static org.eclipse.lsp4j.debug.Breakpoint[] setBreakpoints(DebugAdapterContext context, SetBreakpointsArguments arguments) { + if (context.getDebugSession() == null) { + logger.error("Debug session not started! failed to set breakpoints"); + return new org.eclipse.lsp4j.debug.Breakpoint[0]; + } + Path source = Path.of(arguments.getSource().getPath()); + if (!source.startsWith(context.getScriptRootPath())) { + logger.error("The source file {} is not belong to current script root: {}", source, context.getScriptRootPath()); + return new org.eclipse.lsp4j.debug.Breakpoint[0]; + } + + String sourceName = context.getScriptRootPath().relativize(source).toString(); + List toAdds = fromDAPBreakpoints(sourceName, arguments.getBreakpoints(), context); + List added = context.getBreakpointManager() + .setBreakpoints(sourceName, toAdds, arguments.getSourceModified()); + + for (Breakpoint breakpoint : added) { + breakpoint.install().thenAccept(it -> { + BreakpointEventArguments breakpointEventArguments = new BreakpointEventArguments(); + breakpointEventArguments.setReason(BreakpointEventArgumentsReason.CHANGED); + breakpointEventArguments.setBreakpoint(toDAPBreakpoint(breakpoint, arguments.getSource(), context)); + context.getClient().breakpoint(breakpointEventArguments); + }); + } + + return toDAPBreakpoints(added, arguments.getSource(), context); + } + + private static List fromDAPBreakpoints(String sourceName, SourceBreakpoint[] sourceBreakpoints, DebugAdapterContext context) { + List results = new ArrayList<>(sourceBreakpoints.length); + + for (SourceBreakpoint sourceBreakpoint : sourceBreakpoints) { + Breakpoint breakpoint = new Breakpoint(DAPPositions.fromDAPSourceBreakpoint(sourceBreakpoint, context), sourceName, context.getDebugSession().getVM(), context.getDebugSession().eventHub()); + results.add(breakpoint); + } + + return results; + } + + private static org.eclipse.lsp4j.debug.Breakpoint toDAPBreakpoint(Breakpoint breakpoint, Source source, DebugAdapterContext context) { + org.eclipse.lsp4j.debug.Breakpoint result = new org.eclipse.lsp4j.debug.Breakpoint(); + result.setId(breakpoint.getId()); + DAPPositions.fillDAPBreakpoint(result, breakpoint.getPosition(), context); + result.setVerified(breakpoint.isVerified()); + result.setSource(source); + return result; + } + + private static org.eclipse.lsp4j.debug.Breakpoint[] toDAPBreakpoints(List breakpoints, Source source, DebugAdapterContext context) { + org.eclipse.lsp4j.debug.Breakpoint[] results = new org.eclipse.lsp4j.debug.Breakpoint[breakpoints.size()]; + + for (int i = 0; i < breakpoints.size(); i++) { + results[i] = toDAPBreakpoint(breakpoints.get(i), source, context); + } + + return results; + } + + + public static void registerBreakpointHandler(DebugAdapterContext context) { + DebugSession debugSession = context.getDebugSession(); + if (debugSession == null) { + return; + } + + ClassPrepareRequest classPrepareRequest = debugSession.getVM().eventRequestManager().createClassPrepareRequest(); + if (debugSession.getVM().canUseSourceNameFilters()) { + classPrepareRequest.addSourceNameFilter("*.zs"); + } + classPrepareRequest.enable(); + + Disposable subscribe = debugSession.eventHub() + .breakpointEvents() + // if there are step event at the same location of breakpoint, skip it. + .filter(it -> it.getEventSet().size() == 1 || it.getEventSet().stream().anyMatch(t -> t instanceof StepEvent)) + .subscribe(debugEvent -> { + BreakpointEvent event = (BreakpointEvent) debugEvent.getEvent(); + ThreadReference bpThread = event.thread(); + // TODO: do not pause when eval expressions + + StoppedEventArguments arguments = new StoppedEventArguments(); + arguments.setText("Breakpoint"); + arguments.setThreadId(context.getThreadManager().getThreadId(bpThread)); + arguments.setReason(StoppedEventArgumentsReason.BREAKPOINT); + context.getClient().stopped(arguments); + context.getThreadManager().threadPaused(bpThread); + debugEvent.setResume(false); + + + }); + + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/StackTraceHandler.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/StackTraceHandler.java new file mode 100644 index 00000000..e40e3720 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/StackTraceHandler.java @@ -0,0 +1,52 @@ +package raylras.zen.dap.debugserver.handler; + +import com.sun.jdi.IncompatibleThreadStateException; +import com.sun.jdi.StackFrame; +import com.sun.jdi.ThreadReference; +import org.eclipse.lsp4j.debug.StackTraceArguments; +import org.eclipse.lsp4j.debug.StackTraceResponse; +import raylras.zen.dap.debugserver.DebugAdapterContext; + +import java.util.ArrayList; +import java.util.List; + +public final class StackTraceHandler { + + + public static StackTraceResponse getStackTrace(DebugAdapterContext context, StackTraceArguments args) throws IncompatibleThreadStateException { + + StackTraceResponse stackTraceResponse = new StackTraceResponse(); + ThreadReference runningThread = context.getThreadManager().getById(args.getThreadId()); + + int start = 0; + if (args.getStartFrame() != null) { + start = args.getStartFrame(); + } + + if (start < 0 || runningThread == null) { + stackTraceResponse.setTotalFrames(0); + stackTraceResponse.setStackFrames(new org.eclipse.lsp4j.debug.StackFrame[0]); + return stackTraceResponse; + } + + int end = runningThread.frameCount(); + if (args.getLevels() != null && args.getLevels() > 0) { + end = Math.min(args.getLevels() + start, end); + } + + List stackFrames = new ArrayList<>(end - start); + for (int i = start; i < end; i++) { + StackFrame frame = runningThread.frame(i); + + org.eclipse.lsp4j.debug.StackFrame dapFrame = context.getStackFrameManager().toDAPStackFrame(frame, context); + stackFrames.add(dapFrame); + } + + stackTraceResponse.setStackFrames(stackFrames.toArray(org.eclipse.lsp4j.debug.StackFrame[]::new)); + stackTraceResponse.setTotalFrames(runningThread.frameCount()); + + return stackTraceResponse; + } + + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/ThreadListHandler.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/ThreadListHandler.java new file mode 100644 index 00000000..3623f373 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/ThreadListHandler.java @@ -0,0 +1,41 @@ +package raylras.zen.dap.debugserver.handler; + +import com.sun.jdi.ObjectCollectedException; +import com.sun.jdi.ThreadReference; +import com.sun.jdi.VMDisconnectedException; +import org.eclipse.lsp4j.debug.Thread; +import org.eclipse.lsp4j.debug.ThreadsResponse; +import raylras.zen.dap.debugserver.DebugAdapterContext; + +import java.util.ArrayList; + +public final class ThreadListHandler { + + + public static ThreadsResponse visibleThreads(DebugAdapterContext context) { + ArrayList threads = new ArrayList<>(); + try { + + context.getThreadManager().reloadThreads(context.getDebugSession().getVM()); + for (ThreadReference threadReference : context.getThreadManager().allThreads()) { + + + String jdiName = threadReference.name(); + String name = jdiName.isBlank() ? String.valueOf(threadReference.uniqueID()) : jdiName; + Thread thread = new Thread(); + thread.setId(context.getThreadManager().tryGetId(threadReference)); + thread.setName("Thread [" + name + "]"); + threads.add(thread); + } + } catch (ObjectCollectedException | VMDisconnectedException ex) { + // allThreads may throw VMDisconnectedException when VM terminates and thread.name() may throw ObjectCollectedException + // when the thread is exiting. + } + ThreadsResponse response = new ThreadsResponse(); + response.setThreads(threads.toArray(Thread[]::new)); + return response; + } + + + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/VariablesHandler.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/VariablesHandler.java new file mode 100644 index 00000000..e4979c7e --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/handler/VariablesHandler.java @@ -0,0 +1,57 @@ +package raylras.zen.dap.debugserver.handler; + +import org.eclipse.lsp4j.debug.Variable; +import org.eclipse.lsp4j.debug.VariablePresentationHint; +import org.eclipse.lsp4j.debug.VariablePresentationHintKind; +import org.eclipse.lsp4j.debug.VariablesArguments; +import raylras.zen.dap.debugserver.DebugAdapterContext; +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; +import raylras.zen.dap.debugserver.variable.LazyProxy; +import raylras.zen.dap.debugserver.variable.VariableProxy; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + + +public class VariablesHandler { + + + public static List variables(VariablesArguments args, DebugAdapterContext context) { + DebugObjectManager debugObjectManager = context.getDebugObjectManager(); + VariableProxy proxy = debugObjectManager.getById(args.getVariablesReference()); + if (proxy == null) { + return Collections.emptyList(); + } + + List children = proxy.getChildren(debugObjectManager); + + return children.stream().map(it -> toDAPVariable(it, debugObjectManager)).collect(Collectors.toList()); + } + + + private static Variable toDAPVariable(VariableProxy proxy, DebugObjectManager manager) { + int id = manager.getId(proxy); + Variable variable = new Variable(); + + variable.setVariablesReference(id); + variable.setName(proxy.getName()); +// variable.setEvaluateName(proxy.getEvaluateName()); + variable.setType(proxy.getType()); + variable.setValue(proxy.getValue(manager)); + + VariablePresentationHint presentationHint = new VariablePresentationHint(); + + if (proxy.isVirtual()) { + presentationHint.setKind(VariablePresentationHintKind.VIRTUAL); + } + if (proxy instanceof LazyProxy) { + presentationHint.setLazy(true); + } + variable.setPresentationHint(presentationHint); + + return variable; + } + + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/DebugAPIAdapter.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/DebugAPIAdapter.java new file mode 100644 index 00000000..70982816 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/DebugAPIAdapter.java @@ -0,0 +1,51 @@ +package raylras.zen.dap.debugserver.runtime; + +import com.sun.jdi.*; + +import java.util.Collections; + +public class DebugAPIAdapter { + + public static ClassType findClass(VirtualMachine vm, String fqn) { + return (ClassType) vm.classesByName(fqn).get(0); + } + + public static ArrayReference iterableToArray(ObjectReference iterable, ThreadReference threadReference) { + ClassType adapter = findClass(iterable.virtualMachine(), "youyihj.probezs.util.DebugAPIAdapter"); + Method iterableToArray = adapter.methodsByName("iterableToArray").get(0); + + try { + return (ArrayReference) adapter.invokeMethod(threadReference, iterableToArray, Collections.singletonList(iterable), ClassType.INVOKE_SINGLE_THREADED); + } catch (Exception e) { + return null; + } + } + + public static String[] memberSignatures(ReferenceType referenceType, ThreadReference threadReference) { + ClassType adapter = findClass(referenceType.virtualMachine(), "youyihj.probezs.util.DebugAPIAdapter"); + Method iterableToArray = adapter.methodsByName("memberSignatures").get(0); + + try { + ArrayReference arrayReference = (ArrayReference) adapter.invokeMethod(threadReference, iterableToArray, Collections.singletonList(referenceType.classObject()), ClassType.INVOKE_SINGLE_THREADED); + int length = arrayReference.length(); + String[] result = new String[length]; + for (int i = 0; i < length; i++) { + result[i] = ((StringReference) arrayReference.getValue(i)).value(); + } + return result; + } catch (Exception e) { + return new String[0]; + } + } + + public static String toString(ObjectReference objectReference, ThreadReference threadReference) { + ClassType object = findClass(objectReference.virtualMachine(), "java.lang.Object"); + Method toString = objectReference.referenceType().methodsByName("toString").get(0); + try { + StringReference str = (StringReference) objectReference.invokeMethod(threadReference, toString, Collections.emptyList(), ClassType.INVOKE_SINGLE_THREADED); + return str.value(); + } catch (Exception ignored) { + return null; + } + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/DebugObjectManager.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/DebugObjectManager.java new file mode 100644 index 00000000..e17f91cc --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/DebugObjectManager.java @@ -0,0 +1,75 @@ +package raylras.zen.dap.debugserver.runtime; + +import com.sun.jdi.ThreadReference; +import raylras.zen.dap.debugserver.variable.VariableProxy; +import raylras.zen.dap.debugserver.variable.VariableProxyFactory; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class DebugObjectManager { + private final VariableProxyFactory factory = new VariableProxyFactory(this); + private final ObjectIdMap debugObjectIds = new ObjectIdMap<>(); + private final Map> debugObjectsByThread = new ConcurrentHashMap<>(); + private final Map debugObjectThread = new ConcurrentHashMap<>(); + private final ThreadManager threadManager; + + public DebugObjectManager(ThreadManager threadManager) { + this.threadManager = threadManager; + } + + + public ThreadReference getOwnerThread(VariableProxy proxy) { + int id = getId(proxy); + long threadId = debugObjectThread.get(id); + return threadManager.getByUniqueId(threadId); + } + + public VariableProxyFactory getVariableFactory() { + return factory; + } + + + public void reset() { + debugObjectIds.reset(); + debugObjectsByThread.clear(); + debugObjectThread.clear(); + } + + public void removeByThread(long threadUUID) { + Set toRemove = debugObjectsByThread.remove(threadUUID); + if (toRemove == null || toRemove.isEmpty()) { + return; + } + if (debugObjectsByThread.isEmpty()) { + reset(); + return; + } + for (Integer id : toRemove) { + debugObjectIds.removeById(id); + debugObjectThread.remove(id); + } + + } + + public T put(T obj, ThreadReference thread) { + int id = debugObjectIds.getOrPut(obj); + + if (id >= 0) { + debugObjectsByThread.computeIfAbsent(thread.uniqueID(), it -> new HashSet<>()).add(id); + debugObjectThread.put(id, thread.uniqueID()); + } + + return obj; + } + + public int getId(VariableProxy obj) { + return debugObjectIds.tryGet(obj); + } + + public VariableProxy getById(int id) { + return debugObjectIds.getById(id); + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/ObjectIdMap.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/ObjectIdMap.java new file mode 100644 index 00000000..946f5d64 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/ObjectIdMap.java @@ -0,0 +1,89 @@ +package raylras.zen.dap.debugserver.runtime; + +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class ObjectIdMap { + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private final HashMap reversedMap = new HashMap<>(); + private final HashMap idMap = new HashMap<>(); + private final AtomicInteger nextId = new AtomicInteger(1); + + + public T getById(int id) { + try { + lock.readLock().lock(); + return idMap.get(id); + } finally { + lock.readLock().unlock(); + } + } + + public int tryGet(T obj) { + try { + lock.readLock().lock(); + Integer existing = reversedMap.get(obj); + if (existing != null) { + return existing; + } + return -1; + } finally { + lock.readLock().unlock(); + } + } + + public int getOrPut(T obj) { + int existing = tryGet(obj); + if (existing >= 0) { + return existing; + } + try { + lock.writeLock().lock(); + int id = nextId.incrementAndGet(); + reversedMap.put(obj, id); + idMap.put(id, obj); + return id; + } finally { + lock.writeLock().unlock(); + } + } + + public int remove(T obj) { + try { + lock.writeLock().lock(); + Integer id = reversedMap.remove(obj); + if (id == null) { + return -1; + } + idMap.remove(id); + return id; + } finally { + lock.writeLock().unlock(); + } + } + + public T removeById(int id) { + try { + lock.writeLock().lock(); + T obj = idMap.remove(id); + reversedMap.remove(obj); + return obj; + } finally { + lock.writeLock().unlock(); + } + } + + public void reset() { + try { + lock.writeLock().lock(); + this.idMap.clear(); + this.reversedMap.clear(); + this.nextId.set(1); + } finally { + lock.writeLock().unlock(); + } + } + + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/StackFrameManager.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/StackFrameManager.java new file mode 100644 index 00000000..f8eb4d75 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/StackFrameManager.java @@ -0,0 +1,148 @@ +package raylras.zen.dap.debugserver.runtime; + +import com.sun.jdi.AbsentInformationException; +import com.sun.jdi.Location; +import com.sun.jdi.Method; +import com.sun.jdi.StackFrame; +import com.sun.jdi.request.BreakpointRequest; +import org.eclipse.lsp4j.debug.Source; +import org.eclipse.lsp4j.debug.SourcePresentationHint; +import org.eclipse.lsp4j.debug.StackFramePresentationHint; +import raylras.zen.dap.DAPPositions; +import raylras.zen.dap.debugserver.DebugAdapterContext; +import raylras.zen.dap.debugserver.breakpoint.Breakpoint; + +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class StackFrameManager { + + private final ObjectIdMap stackFrameIds = new ObjectIdMap<>(); + private final Map> stackFramesByThread = new ConcurrentHashMap<>(); + + + public void reset() { + stackFrameIds.reset(); + stackFramesByThread.clear(); + } + + public void removeByThread(long threadUUID) { + Set toRemove = stackFramesByThread.remove(threadUUID); + if (toRemove == null || toRemove.isEmpty()) { + return; + } + if (stackFramesByThread.isEmpty()) { + reset(); + return; + } + for (Integer id : toRemove) { + stackFrameIds.removeById(id); + } + + } + + public int stackFrameId(StackFrame stackFrame) { + int id = stackFrameIds.getOrPut(stackFrame); + + if (id >= 0) { + stackFramesByThread.computeIfAbsent(stackFrame.thread().uniqueID(), it -> new HashSet<>()).add(id); + } + + return id; + } + + public StackFrame getById(int id) { + return stackFrameIds.getById(id); + } + + + public org.eclipse.lsp4j.debug.StackFrame toDAPStackFrame(StackFrame jdiFrame, DebugAdapterContext context) { + + int id = stackFrameId(jdiFrame); + + org.eclipse.lsp4j.debug.StackFrame stackFrame = new org.eclipse.lsp4j.debug.StackFrame(); + stackFrame.setId(id); + Location location = jdiFrame.location(); + + // name + Method method = location.method(); + if (method.isNative()) { + stackFrame.setName("[Native Method]"); + stackFrame.setPresentationHint(StackFramePresentationHint.SUBTLE); + Source source = new Source(); + source.setName("[Native Code]"); + source.setPresentationHint(SourcePresentationHint.DEEMPHASIZE); + stackFrame.setSource(source); + } else { + // TODO: pretty print it + stackFrame.setName(method.name()); + + + boolean isZsFile = false; + // source + Source source = new Source(); + try { + String sourceName = location.sourceName(); + if (sourceName.endsWith(".zs")) { + isZsFile = true; + source.setName(sourceName); + String sourcePath = location.sourcePath(); + Path fullPath = context.getScriptRootPath().resolve(sourcePath).normalize(); + source.setPath(fullPath.toUri().toString()); + } else { + if (sourceName.endsWith(".java")) { + source.setName(sourceName); + stackFrame.setLine(location.lineNumber()); + try { + source.setPath(location.sourcePath()); + } catch (AbsentInformationException ignored) { + } + } else { + source.setName("[Unknown Source]"); + } + stackFrame.setPresentationHint(StackFramePresentationHint.SUBTLE); + source.setPresentationHint(SourcePresentationHint.DEEMPHASIZE); + } + } catch (AbsentInformationException ignored) { + source.setName("[Unknown Source]"); + stackFrame.setPresentationHint(StackFramePresentationHint.SUBTLE); + source.setPresentationHint(SourcePresentationHint.DEEMPHASIZE); + } + stackFrame.setSource(source); + + + if (isZsFile) { + // range + try { + int line = DAPPositions.fromJDILine(location); + stackFrame.setLine(DAPPositions.toDAPLine(line, context)); + + int column = 0; + + List breakpoints = context.getBreakpointManager().findBreakpointsAt(location.sourceName(), line); + + Optional availableBreakpoint = breakpoints.stream().filter(breakpoint -> breakpoint.getRequests() + .stream() + .filter(it -> it instanceof BreakpointRequest) + .map(it -> (BreakpointRequest) it) + .map(BreakpointRequest::location) + .map(it -> Objects.equals(location, it)) + .findAny().isPresent() + ).findAny(); + if (availableBreakpoint.isPresent()) { + column = availableBreakpoint.get().getPosition().column(); + } + stackFrame.setColumn(DAPPositions.toDAPColumn(column, context)); + } catch (AbsentInformationException ignored) { + } + } + } + + return stackFrame; + + + } + + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/StepState.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/StepState.java new file mode 100644 index 00000000..dd30fed6 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/StepState.java @@ -0,0 +1,163 @@ +package raylras.zen.dap.debugserver.runtime; + +import com.sun.jdi.*; +import com.sun.jdi.event.*; +import com.sun.jdi.request.EventRequest; +import com.sun.jdi.request.StepRequest; +import io.reactivex.rxjava3.disposables.Disposable; +import org.eclipse.lsp4j.debug.StoppedEventArguments; +import org.eclipse.lsp4j.debug.StoppedEventArgumentsReason; +import raylras.zen.dap.debugserver.DebugAdapterContext; +import raylras.zen.dap.event.EventHub; + +public class StepState { + + + public enum Kind { + STEP_IN(StepRequest.STEP_INTO), + STEP_OUT(StepRequest.STEP_OUT), + STEP_OVER(StepRequest.STEP_OVER); + + public final int depth; + + Kind(int depth) { + this.depth = depth; + } + + + } + + public StepState(Kind kind, boolean isLine, Location startLocation) { + this.kind = kind; + this.isLine = isLine; + if (startLocation != null) { + this.startLine = startLocation.lineNumber(); + this.startIndex = startLocation.codeIndex(); + this.startMethod = startLocation.method(); + } + } + + + private final Kind kind; + private final boolean isLine; + + private int startLine = 0; + private long startIndex = -1; + private Method startMethod = null; + + private EventRequest stepRequest; + private Disposable eventSubscription; + private long threadId; + + + private void createStepRequest(DebugAdapterContext context, ThreadReference threadReference) { + this.stepRequest = context.getDebugSession().getVM().eventRequestManager().createStepRequest(threadReference, isLine ? StepRequest.STEP_LINE : StepRequest.STEP_MIN, this.kind.depth); + + this.stepRequest.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); + this.stepRequest.enable(); + } + + public void configure(DebugAdapterContext context, ThreadReference threadReference) { + if (this.stepRequest != null && this.threadId != threadReference.uniqueID()) { + this.stepRequest.disable(); + context.getDebugSession().getVM().eventRequestManager().deleteEventRequest(stepRequest); + this.stepRequest = null; + } + if (this.stepRequest == null) { + createStepRequest(context, threadReference); + } + this.threadId = threadReference.uniqueID(); + + } + + public void close(DebugAdapterContext context) { + if (this.eventSubscription != null) { + this.eventSubscription.dispose(); + this.eventSubscription = null; + } + this.stepRequest.disable(); + context.getDebugSession().getVM().eventRequestManager().deleteEventRequest(stepRequest); + context.setPendingStep(null); + } + + public void install(DebugAdapterContext context) { + + EventHub eventHub = context.getDebugSession().eventHub(); + this.eventSubscription = eventHub.allEvents() + .filter(it -> it.getEvent() instanceof BreakpointEvent || it.getEvent() instanceof ExceptionEvent || it.getEvent() instanceof StepEvent) + .subscribe(event -> { + + Event jdiEvent = event.getEvent(); + + if (!(jdiEvent instanceof StepEvent stepEvent)) { + // cancel step if meet a breakpoint + long threadId = ((LocatableEvent) jdiEvent).thread().uniqueID(); + if (threadId == this.threadId && this.stepRequest != null) { + close(context); + } + return; + } + + if (canStepAt(stepEvent.location()) && !isSameLocation(stepEvent.location())) { + StoppedEventArguments arguments = new StoppedEventArguments(); + arguments.setThreadId(context.getThreadManager().getThreadId(stepEvent.thread())); + arguments.setReason(StoppedEventArgumentsReason.STEP); + context.getClient().stopped(arguments); + context.getThreadManager().threadPaused(stepEvent.thread()); + event.setResume(false); + close(context); + return; + } + + if (!isOutOfStep(stepEvent.thread())) { + this.configure(context, stepEvent.thread()); + } else { + close(context); + } + event.setResume(true); + + }); + } + + private boolean canStepAt(Location location) { + try { + String sourceName = location.sourceName(); + return sourceName.endsWith(".zs"); + } catch (AbsentInformationException ignored) { + return false; + } + } + + private boolean isSameLocation(Location location) { + if (location.method() != startMethod) { + return false; + } + if (isLine) { + return location.lineNumber() == startLine; + } + return location.codeIndex() == startIndex; + } + + + private static boolean isOutOfStep(ThreadReference threadReference) { + try { + return threadReference.frames().stream().noneMatch(it -> { + Location location = it.location(); + if (location == null) { + return false; + } + if (location.lineNumber() == 0) { + return false; + } + try { + return location.sourceName().endsWith(".zs"); + } catch (AbsentInformationException ignored) { + return false; + } + }); + } catch (IncompatibleThreadStateException e) { + return true; + } + } + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/ThreadManager.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/ThreadManager.java new file mode 100644 index 00000000..d250c449 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/ThreadManager.java @@ -0,0 +1,104 @@ +package raylras.zen.dap.debugserver.runtime; + +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ThreadReference; +import com.sun.jdi.VirtualMachine; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class ThreadManager { + + private final ConcurrentHashMap cachedThreads = new ConcurrentHashMap<>(); + private final ObjectIdMap threadIdMap = new ObjectIdMap<>(); + + private final Set pausedThreads = Collections.synchronizedSet(new HashSet<>()); + + + public Collection allThreads() { + return cachedThreads.values(); + } + + public Collection pausedThreads() { + return pausedThreads.stream().map(cachedThreads::get).filter(Objects::nonNull).toList(); + } + + public int getThreadId(ThreadReference thread) { + return threadIdMap.tryGet(thread.uniqueID()); + } + + + public int threadStarted(ThreadReference threadReference) { + int id = threadIdMap.getOrPut(threadReference.uniqueID()); + cachedThreads.put(threadReference.uniqueID(), threadReference); + return id; + } + + public void threadPaused(ThreadReference threadReference) { + pausedThreads.add(threadReference.uniqueID()); + } + + public void threadResumed(ThreadReference threadReference) { + pausedThreads.remove(threadReference.uniqueID()); + } + + public int threadStopped(ThreadReference threadReference) { + cachedThreads.remove(threadReference.uniqueID()); + pausedThreads.remove(threadReference.uniqueID()); + return threadIdMap.remove(threadReference.uniqueID()); + } + + public int tryGetId(ThreadReference reference) { + return threadIdMap.tryGet(reference.uniqueID()); + } + + public boolean resumeThread(ThreadReference reference) { + if (reference == null || !reference.isSuspended()) { + return false; + } + threadResumed(reference); + reference.resume(); + return true; + } + + public boolean pauseThread(ThreadReference reference) { + if (reference == null || reference.isSuspended()) { + return false; + } + reference.suspend(); + threadPaused(reference); + return true; + } + + + public ThreadReference getById(int threadId) { + Long id = threadIdMap.getById(threadId); + return cachedThreads.get(id); + } + + public ThreadReference getByUniqueId(long uniqueId) { + return cachedThreads.get(uniqueId); + } + + public void reloadThreads(VirtualMachine vm) { + List threadReferences = vm.allThreads(); + + Set currentIds = threadReferences.stream().map(ObjectReference::uniqueID).collect(Collectors.toSet()); + + for (Long threadUniqueId : cachedThreads.keySet()) { + if (!currentIds.contains(threadUniqueId)) { + threadIdMap.remove(threadUniqueId); + pausedThreads.remove(threadUniqueId); + } + } + + cachedThreads.clear(); + + for (ThreadReference threadReference : threadReferences) { + threadStarted(threadReference); + } + + } + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/VariableFormatter.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/VariableFormatter.java new file mode 100644 index 00000000..b79069c3 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/runtime/VariableFormatter.java @@ -0,0 +1,15 @@ +package raylras.zen.dap.debugserver.runtime; + +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ThreadReference; +import com.sun.jdi.Value; + +public class VariableFormatter { + + public static String format(Value value, ThreadReference threadReference) { + if (value instanceof ObjectReference objectReference) { + return DebugAPIAdapter.toString(objectReference, threadReference); + } + return value.toString(); + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ArrayView.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ArrayView.java new file mode 100644 index 00000000..b28e4739 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ArrayView.java @@ -0,0 +1,71 @@ +package raylras.zen.dap.debugserver.variable; + +import com.sun.jdi.ArrayReference; +import com.sun.jdi.ThreadReference; +import com.sun.jdi.Value; +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; +import raylras.zen.dap.debugserver.runtime.VariableFormatter; + +import java.util.ArrayList; +import java.util.List; + +public class ArrayView implements VariableProxy { + private final ArrayReference ref; + private final String name; + private boolean virtual = false; + + protected ArrayView(String name, ArrayReference ref) { + this.ref = ref; + this.name = name; + } + + @Override + public List getChildren(DebugObjectManager manager) { + int length = ref.length(); + List variables = new ArrayList<>(length); + ThreadReference ownerThread = manager.getOwnerThread(this); + for (int i = 0; i < length; i++) { + VariableProxy proxy = manager.getVariableFactory().createValueProxy(String.valueOf(i), ref.getValue(i), ownerThread); + variables.add(proxy); + } + return variables; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue(DebugObjectManager manager) { + ThreadReference ownerThread = manager.getOwnerThread(this); + StringBuilder builder = new StringBuilder(); + builder.append("("); + builder.append(ref.length()); + builder.append(") "); + builder.append("["); + for (int i = 0; i < ref.length(); i++) { + if (i != 0) { + builder.append(", "); + } + if (i > 10) { + builder.append("..."); + break; + } + Value value = ref.getValue(i); + builder.append(VariableFormatter.format(value, ownerThread)); + } + builder.append("]"); + + return builder.toString(); + } + + @Override + public boolean isVirtual() { + return virtual; + } + + public void setVirtual(boolean virtual) { + this.virtual = virtual; + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ErrorVariableProxy.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ErrorVariableProxy.java new file mode 100644 index 00000000..46968764 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ErrorVariableProxy.java @@ -0,0 +1,31 @@ +package raylras.zen.dap.debugserver.variable; + +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; + +import java.util.Collections; +import java.util.List; + +public class ErrorVariableProxy implements VariableProxy{ + private final String name; + private final String text; + + public ErrorVariableProxy(String name, String text) { + this.name = name; + this.text = text; + } + + @Override + public List getChildren(DebugObjectManager manager) { + return Collections.emptyList(); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue(DebugObjectManager manager) { + return "<" + text + ">"; + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/JavaFieldView.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/JavaFieldView.java new file mode 100644 index 00000000..6983f2f4 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/JavaFieldView.java @@ -0,0 +1,56 @@ +package raylras.zen.dap.debugserver.variable; + +import com.sun.jdi.*; +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; + +import java.util.ArrayList; +import java.util.List; + +public class JavaFieldView implements VariableProxy { + private final ObjectReference data; + private final String name; + + protected JavaFieldView(String name, ObjectReference data) { + this.data = data; + this.name = name; + } + + public boolean isEmpty() { + return data.referenceType().visibleFields().isEmpty(); + } + + @Override + public List getChildren(DebugObjectManager manager) { + List variables = new ArrayList<>(); + ReferenceType referenceType = data.referenceType(); + ThreadReference ownerThread = manager.getOwnerThread(this); + for (Field field : data.referenceType().visibleFields()) { + Value value; + if (field.isStatic()) { + value = referenceType.getValue(field); + } else { + value = data.getValue(field); + } + VariableProxy proxy = manager.getVariableFactory().createValueProxy(field.name(), value, ownerThread); + variables.add(proxy); + } + return variables; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isVirtual() { + return true; + } + + + @Override + public String getValue(DebugObjectManager manager) { + return ""; + } + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/LazyProxy.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/LazyProxy.java new file mode 100644 index 00000000..48740af9 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/LazyProxy.java @@ -0,0 +1,33 @@ +package raylras.zen.dap.debugserver.variable; + +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +public class LazyProxy implements VariableProxy { + private final Supplier compute; + private final String name; + + protected LazyProxy(String name, Supplier compute) { + this.compute = compute; + this.name = name; + } + + @Override + public List getChildren(DebugObjectManager manager) { + VariableProxy variableProxy = compute.get(); + return Collections.singletonList(variableProxy); + } + @Override + public String getName() { + return name; + } + + + @Override + public String getValue(DebugObjectManager manager) { + return ""; + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ListView.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ListView.java new file mode 100644 index 00000000..a29ccd27 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ListView.java @@ -0,0 +1,91 @@ +package raylras.zen.dap.debugserver.variable; + +import com.sun.jdi.*; +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; +import raylras.zen.dap.debugserver.runtime.VariableFormatter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ListView implements VariableProxy { + private final ObjectReference ref; + private final String name; + + protected ListView(String name, ObjectReference ref) { + this.name = name; + this.ref = ref; + } + + @Override + public List getChildren(DebugObjectManager manager) { + ReferenceType type = ref.referenceType(); + ThreadReference thread = manager.getOwnerThread(this); + Method sizeMethod = type.methodsByName("size", "()I").get(0); + int length; + try { + length = ((IntegerValue) ref.invokeMethod(thread, sizeMethod, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED)).value(); + } catch (Exception ignored) { + VariableProxy error = manager.getVariableFactory().createError(name, "failed to evaluate", thread); + return Collections.singletonList(error); + } + List variables = new ArrayList<>(length); + Method getMethod = type.methodsByName("get").get(0); + VirtualMachine virtualMachine = ref.virtualMachine(); + for (int i = 0; i < length; i++) { + try { + Value value = ref.invokeMethod(thread, getMethod, Collections.singletonList(virtualMachine.mirrorOf(i)), ObjectReference.INVOKE_SINGLE_THREADED); + VariableProxy proxy = manager.getVariableFactory().createValueProxy(String.valueOf(i), value, thread); + variables.add(proxy); + } catch (Exception ignored) { + VariableProxy error = manager.getVariableFactory().createError(String.valueOf(i), "failed to evaluate", thread); + variables.add(error); + } + } + return variables; + } + + @Override + public String getName() { + return name; + } + + + @Override + public String getValue(DebugObjectManager manager) { + StringBuilder builder = new StringBuilder(); + ReferenceType type = ref.referenceType(); + ThreadReference thread = manager.getOwnerThread(this); + Method sizeMethod = type.methodsByName("size", "()I").get(0); + int length; + try { + length = ((IntegerValue) ref.invokeMethod(thread, sizeMethod, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED)).value(); + } catch (Exception ignored) { + return ""; + } + VirtualMachine virtualMachine = ref.virtualMachine(); + builder.append("["); + for (int i = 0; i < length; i++) { + if (i != 0) { + builder.append(", "); + } + if (i > 10) { + builder.append("..."); + break; + } + Method getMethod = type.methodsByName("get").get(0); + try { + Value value = ref.invokeMethod(thread, getMethod, Collections.singletonList(virtualMachine.mirrorOf(i)), ObjectReference.INVOKE_SINGLE_THREADED); + builder.append(VariableFormatter.format(value, thread)); + } catch (Exception ignored) { + + builder.append(""); + } + } + builder.append("]"); + + return builder.toString(); + } + + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/MapEntryProxy.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/MapEntryProxy.java new file mode 100644 index 00000000..4d493795 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/MapEntryProxy.java @@ -0,0 +1,65 @@ +package raylras.zen.dap.debugserver.variable; + +import com.sun.jdi.*; +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; +import raylras.zen.dap.debugserver.runtime.VariableFormatter; + +import java.util.Collections; +import java.util.List; + +public class MapEntryProxy implements VariableProxy { + private final ObjectReference ref; + private final String name; + + protected MapEntryProxy(String name, ObjectReference ref) { + this.ref = ref; + this.name = name; + } + + @Override + public List getChildren(DebugObjectManager manager) { + ReferenceType referenceType = ref.referenceType(); + ThreadReference thread = manager.getOwnerThread(this); + try { + Method getKey = referenceType.methodsByName("getKey").get(0); + Method getValue = referenceType.methodsByName("getValue").get(0); + + Value key = ref.invokeMethod(thread, getKey, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED); + Value value = ref.invokeMethod(thread, getValue, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED); + + return List.of( + manager.getVariableFactory().createValueProxy("key", key, thread), + manager.getVariableFactory().createValueProxy("value", value, thread) + ); + } catch (Exception e) { + VariableProxy error = manager.getVariableFactory().createError(name, "failed to evaluate", thread); + return Collections.singletonList(error); + } + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue(DebugObjectManager manager) { + try { + ReferenceType referenceType = ref.referenceType(); + ThreadReference thread = manager.getOwnerThread(this); + Method getKey = referenceType.methodsByName("getKey").get(0); + Method getValue = referenceType.methodsByName("getValue").get(0); + + Value key = ref.invokeMethod(thread, getKey, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED); + Value value = ref.invokeMethod(thread, getValue, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED); + return VariableFormatter.format(key, thread) + " --> " + VariableFormatter.format(value, thread); + } catch (Exception e) { + return ""; + } + } + + @Override + public boolean isVirtual() { + return true; + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/MapView.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/MapView.java new file mode 100644 index 00000000..6421d9ce --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/MapView.java @@ -0,0 +1,57 @@ +package raylras.zen.dap.debugserver.variable; + +import com.sun.jdi.*; +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MapView implements VariableProxy { + private final ObjectReference ref; + private final String name; + + protected MapView(String name, ObjectReference ref) { + this.ref = ref; + this.name = name; + } + + @Override + public List getChildren(DebugObjectManager manager) { + ThreadReference thread = manager.getOwnerThread(this); + try { + ReferenceType mapType = ref.referenceType(); + Method entrySetMethod = mapType.methodsByName("entrySet", "()Ljava/util/Set").get(0); + ObjectReference entrySet = (ObjectReference) ref.invokeMethod(thread, entrySetMethod, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED); + + ReferenceType entrySetType = entrySet.referenceType(); + Method toArrayMethod = entrySetType.methodsByName("toArray", "()[Ljava/lang/Object").get(0); + ArrayReference entryArray = (ArrayReference) entrySet.invokeMethod(thread, toArrayMethod, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED); + int length = entryArray.length(); + List variables = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + VariableProxy proxy = manager.getVariableFactory().createMapEntryProxy(String.valueOf(i), (ObjectReference) entryArray.getValue(i), thread); + variables.add(proxy); + } + return variables; + } catch (Exception ignored) { + VariableProxy error = manager.getVariableFactory().createError(name, "failed to evaluate", thread); + return Collections.singletonList(error); + } + } + @Override + public String getName() { + return name; + } + + + @Override + public boolean isVirtual() { + return true; + } + + @Override + public String getValue(DebugObjectManager manager) { + return "Map"; + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ObjectProxy.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ObjectProxy.java new file mode 100644 index 00000000..972dbc1d --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ObjectProxy.java @@ -0,0 +1,112 @@ +package raylras.zen.dap.debugserver.variable; + +import com.sun.jdi.*; +import raylras.zen.dap.debugserver.runtime.DebugAPIAdapter; +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; +import raylras.zen.dap.debugserver.runtime.VariableFormatter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ObjectProxy implements VariableProxy { + private final ObjectReference ref; + private final String name; + + protected ObjectProxy(String name, ObjectReference objectReference) { + this.ref = objectReference; + this.name = name; + } + + + @Override + public List getChildren(DebugObjectManager manager) { + List result = new ArrayList<>(); + VariableProxyFactory factory = manager.getVariableFactory(); + ThreadReference ownerThread = manager.getOwnerThread(this); + + ReferenceType referenceType = ref.referenceType(); + + if (referenceType instanceof ClassType classType) { + ClassType current = classType; + while (current != null && !"java.lang.Object".equals(current.name())) { + collectMembers(manager, current, ownerThread, factory, result); + current = current.superclass(); + } + + for (InterfaceType interfaceType : classType.allInterfaces()) { + collectMembers(manager, interfaceType, ownerThread, factory, result); + } + + } + + if (factory.isIterable(referenceType)) { + VariableProxy lazyIterable = factory.createLazy("[[Enumerate Items]]", () -> { + ArrayReference arrayReference = DebugAPIAdapter.iterableToArray(ref, ownerThread); + ArrayView arrayProxy = factory.createArrayProxy("[[Enumerate Items]]", arrayReference, ownerThread); + arrayProxy.setVirtual(true); + return arrayProxy; + }, ownerThread); + result.add(lazyIterable); + } + + JavaFieldView fieldView = factory.createFieldView("[[Java Fields]]", ref, ownerThread); + if (!fieldView.isEmpty()) { + result.add(fieldView); + } + + return result; + } + + private void collectMembers(DebugObjectManager manager, ReferenceType referenceType, ThreadReference ownerThread, VariableProxyFactory factory, List result) { + String[] members = DebugAPIAdapter.memberSignatures(referenceType, ownerThread); + for (String member : members) { + String[] split = member.split(":"); + + if (split.length < 2) { + continue; + } + String name = split[0]; + + try { + + if (split.length == 2) { + String fieldName = split[1]; + Field field = referenceType.fieldByName(fieldName); + Value value = ref.getValue(field); + VariableProxy proxy = factory.createValueProxy(name, value, ownerThread); + result.add(proxy); + } else { + String methodName = split[1]; + String methodSignature = split[2]; + + try { + Method method = referenceType.methodsByName(methodName, methodSignature).get(0); + Value value = ref.invokeMethod(ownerThread, method, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED); + VariableProxy proxy = factory.createValueProxy(name, value, ownerThread); + result.add(proxy); + } catch (Exception e) { + VariableProxy error = manager.getVariableFactory().createError(name, "failed to evaluate: " + name + ":" + e.getMessage(), ownerThread); + result.add(error); + } + } + } catch (Exception e) { + VariableProxy err = manager.getVariableFactory().createError(name, "failed to evaluate: " + e.getMessage(), ownerThread); + result.add(err); + } + } + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue(DebugObjectManager manager) { + ThreadReference ownerThread = manager.getOwnerThread(this); + return VariableFormatter.format(ref, ownerThread); + } + + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/PrimitiveValueProxy.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/PrimitiveValueProxy.java new file mode 100644 index 00000000..1d83923c --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/PrimitiveValueProxy.java @@ -0,0 +1,35 @@ +package raylras.zen.dap.debugserver.variable; + +import com.sun.jdi.Value; +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; + +import java.util.Collections; +import java.util.List; + +public class PrimitiveValueProxy implements VariableProxy { + private final Value value; + private final String name; + + protected PrimitiveValueProxy(String name, Value value) { + this.name = name; + this.value = value; + } + + @Override + public List getChildren(DebugObjectManager manager) { + return Collections.emptyList(); + } + + + @Override + public String getName() { + return name; + } + + @Override + public String getValue(DebugObjectManager manager) { + return value.toString(); + } + + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ScopeProxy.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ScopeProxy.java new file mode 100644 index 00000000..a90291ac --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/ScopeProxy.java @@ -0,0 +1,31 @@ +package raylras.zen.dap.debugserver.variable; + +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; + +import java.util.List; + +public class ScopeProxy implements VariableProxy { + private final List variables; + private final String name; + + protected ScopeProxy(String name, List variables) { + this.variables = variables; + this.name = name; + } + + @Override + public List getChildren(DebugObjectManager manager) { + return variables; + } + + + @Override + public String getName() { + return name; + } + + @Override + public String getValue(DebugObjectManager manager) { + return name; + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/VariableProxy.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/VariableProxy.java new file mode 100644 index 00000000..a73f2d1f --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/VariableProxy.java @@ -0,0 +1,21 @@ +package raylras.zen.dap.debugserver.variable; + +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; + +import java.util.List; + +public interface VariableProxy { + + List getChildren(DebugObjectManager manager); + + + String getName(); + String getValue(DebugObjectManager manager); + default String getType() { + return null; + } + + default boolean isVirtual() { + return false; + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/VariableProxyFactory.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/VariableProxyFactory.java new file mode 100644 index 00000000..3a63dac8 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/debugserver/variable/VariableProxyFactory.java @@ -0,0 +1,129 @@ +package raylras.zen.dap.debugserver.variable; + +import com.sun.jdi.*; +import raylras.zen.dap.debugserver.runtime.DebugObjectManager; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +public class VariableProxyFactory { + + private final DebugObjectManager manager; + + public VariableProxyFactory(DebugObjectManager manager) { + this.manager = manager; + } + + public boolean isList(Type type) { + if (!(type instanceof ClassType classType)) { + return false; + } + return classType.allInterfaces().stream().anyMatch(it -> "java.util.List".equals(it.name())); + } + + public boolean isMap(Type type) { + + if (!(type instanceof ClassType classType)) { + return false; + } + return classType.allInterfaces().stream().anyMatch(it -> "java.util.Map".equals(it.name())); + } + + public boolean isIterable(Type type) { + if (!(type instanceof ClassType classType)) { + return false; + } + return classType.allInterfaces().stream().anyMatch(it -> "java.lang.Iterable".equals(it.name())); + } + + public VariableProxy createValueProxy(String name, Value value, ThreadReference ownerThread) { + if (value instanceof PrimitiveValue || value instanceof StringReference) { + return createPrimitiveProxy(name, value, ownerThread); + } + if (value instanceof ArrayReference arrayReference) { + return createArrayProxy(name, arrayReference, ownerThread); + } + if(value == null) { + return createNull(name, ownerThread); + } + + Type type = value.type(); + if (isList(type)) { + return createListProxy(name, (ObjectReference) value, ownerThread); + } + if (isMap(type)) { + return createMapProxy(name, (ObjectReference) value, ownerThread); + } + return createObject(name, value, ownerThread); + + } + + private VariableProxy createObject(String name, Value value, ThreadReference ownerThread) { + if (!(value instanceof ObjectReference objectReference)) { + return createNull(name, ownerThread); + } + return manager.put(new ObjectProxy(name, objectReference), ownerThread); + } + + public VariableProxy createNull(String name, ThreadReference ownerThread) { + + VariableProxy variableProxy = new VariableProxy() { + @Override + public List getChildren(DebugObjectManager manager) { + return Collections.emptyList(); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue(DebugObjectManager manager) { + return "null"; + } + }; + return variableProxy; + } + + public VariableProxy createPrimitiveProxy(String name, Value primitiveValue, ThreadReference ownerThread) { + + return new PrimitiveValueProxy(name, primitiveValue); + } + + public JavaFieldView createFieldView(String name, ObjectReference data, ThreadReference ownerThread) { + return manager.put(new JavaFieldView(name, data), ownerThread); + } + + public VariableProxy createMapEntryProxy(String name, ObjectReference ref, ThreadReference ownerThread) { + + return manager.put(new MapEntryProxy(name, ref), ownerThread); + } + + public ArrayView createArrayProxy(String name, ArrayReference ref, ThreadReference ownerThread) { + return manager.put(new ArrayView(name, ref), ownerThread); + } + + public VariableProxy createListProxy(String name, ObjectReference ref, ThreadReference ownerThread) { + return manager.put(new ListView(name, ref), ownerThread); + } + + public VariableProxy createMapProxy(String name, ObjectReference ref, ThreadReference ownerThread) { + return manager.put(new MapView(name, ref), ownerThread); + } + + public VariableProxy createLazy(String name, Supplier supplier, ThreadReference ownerThread) { + return manager.put(new LazyProxy(name, supplier), ownerThread); + } + + public VariableProxy createScope(String name, List proxyList, ThreadReference ownerThread) { + return manager.put(new ScopeProxy(name, proxyList), ownerThread); + } + + public VariableProxy createError(String name, String reason, ThreadReference ownerThread) { + + return new ErrorVariableProxy(name, reason); + } + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/event/EventHub.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/event/EventHub.java new file mode 100644 index 00000000..92e221e1 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/event/EventHub.java @@ -0,0 +1,85 @@ +package raylras.zen.dap.event; + +import com.sun.jdi.VMDisconnectedException; +import com.sun.jdi.VirtualMachine; +import com.sun.jdi.event.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.subjects.PublishSubject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class EventHub { + + private static final Logger logger = LoggerFactory.getLogger(EventHub.class); + private final PublishSubject subject = PublishSubject.create(); + + + private Thread workingThread = null; + private boolean isClosed = false; + + + public Observable allEvents() { + return subject; + } + + public Observable classPrepareEvents() { + return subject.filter(it -> it.getEvent() instanceof ClassPrepareEvent); + } + + public Observable breakpointEvents() { + return subject.filter(it -> it.getEvent() instanceof BreakpointEvent); + } + + public void start(VirtualMachine vm) { + if (isClosed) { + throw new IllegalStateException("This event hub is already closed."); + } + + workingThread = new Thread(() -> { + EventQueue queue = vm.eventQueue(); + while (true) { + try { + if (Thread.interrupted()) { + subject.onComplete(); + return; + } + + EventSet set = queue.remove(); + + boolean shouldResume = true; + for (Event event : set) { + JDIEvent dbgEvent = new JDIEvent(event, set); + subject.onNext(dbgEvent); + if (!dbgEvent.shouldResume()) { + try { + logger.info("Paused at JDI Event: {}", event); + } catch (VMDisconnectedException e) { + // do nothing + } + } + shouldResume &= dbgEvent.shouldResume(); + } + + if (shouldResume) { + set.resume(); + } + } catch (InterruptedException | VMDisconnectedException e) { + isClosed = true; + subject.onComplete(); + return; + } + } + }, "Event Hub"); + + workingThread.start(); + } + + public void close() { + if (isClosed) { + return; + } + workingThread.interrupt(); + workingThread = null; + isClosed = true; + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/event/JDIEvent.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/event/JDIEvent.java new file mode 100644 index 00000000..5387a3d1 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/event/JDIEvent.java @@ -0,0 +1,32 @@ +package raylras.zen.dap.event; + +import com.sun.jdi.event.Event; +import com.sun.jdi.event.EventSet; + +public class JDIEvent { + + private final Event event; + private final EventSet eventSet; + private boolean shouldResume = true; + + public JDIEvent(Event event, EventSet eventSet) { + this.event = event; + this.eventSet = eventSet; + } + + public Event getEvent() { + return event; + } + + public EventSet getEventSet() { + return eventSet; + } + + public boolean shouldResume() { + return shouldResume; + } + + public void setResume(boolean shouldResume) { + this.shouldResume = shouldResume; + } +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/jdi/JDILauncher.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/jdi/JDILauncher.java new file mode 100644 index 00000000..b3fa9f71 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/jdi/JDILauncher.java @@ -0,0 +1,74 @@ +package raylras.zen.dap.jdi; + +import com.sun.jdi.Bootstrap; +import com.sun.jdi.VirtualMachineManager; +import com.sun.jdi.connect.AttachingConnector; +import com.sun.jdi.connect.Connector; +import com.sun.jdi.connect.IllegalConnectorArgumentsException; +import com.sun.jdi.connect.ListeningConnector; +import raylras.zen.dap.debugserver.DebugSession; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public final class JDILauncher { + + public static final String HOME = "home"; + public static final String OPTIONS = "options"; + public static final String MAIN = "main"; + public static final String SUSPEND = "suspend"; + public static final String QUOTE = "quote"; + public static final String EXEC = "vmexec"; + public static final String CWD = "cwd"; + public static final String ENV = "env"; + public static final String HOSTNAME = "hostname"; + public static final String PORT = "port"; + public static final String TIMEOUT = "timeout"; + + + private static final VirtualMachineManager vmManager = Bootstrap.virtualMachineManager(); + + /** + * Attach to an existing debuggee VM. + * + * @param hostName the machine where the debuggee VM is launched on + * @param port the debug port that the debuggee VM exposed + * @param attachTimeout the timeout when attaching to the debuggee VM + * @return an instance of IDebugSession + * @throws IOException when unable to attach. + * @throws IllegalConnectorArgumentsException when one of the connector arguments is invalid. + */ + public static DebugSession attach(String hostName, int port, int attachTimeout) + throws IOException, IllegalConnectorArgumentsException { + List connectors = vmManager.attachingConnectors(); + AttachingConnector connector = connectors.get(0); + // in JDK 10, the first AttachingConnector is not the one we want + final String SUN_ATTACH_CONNECTOR = "com.sun.tools.jdi.SocketAttachingConnector"; + for (AttachingConnector con : connectors) { + if (con.getClass().getName().equals(SUN_ATTACH_CONNECTOR)) { + connector = con; + break; + } + } + Map arguments = connector.defaultArguments(); + arguments.get(HOSTNAME).setValue(hostName); + arguments.get(PORT).setValue(String.valueOf(port)); + arguments.get(TIMEOUT).setValue(String.valueOf(attachTimeout)); + return new DebugSession(connector.attach(arguments)); + } + + + public static ListeningConnector getListeningConnector() { + List connectors = vmManager.listeningConnectors(); + return connectors.get(0); + } + public static Map getListenArguments(ListeningConnector connector, int listenTimeout) { + Map arguments = connector.defaultArguments(); + arguments.get(TIMEOUT).setValue(String.valueOf(listenTimeout)); + return arguments; + + } + + +} diff --git a/zenscript-debug-adapter/src/main/java/raylras/zen/dap/jdi/ObservableUtils.java b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/jdi/ObservableUtils.java new file mode 100644 index 00000000..578e2356 --- /dev/null +++ b/zenscript-debug-adapter/src/main/java/raylras/zen/dap/jdi/ObservableUtils.java @@ -0,0 +1,16 @@ +package raylras.zen.dap.jdi; + +import io.reactivex.rxjava3.functions.Predicate; + +public class ObservableUtils { + + public static Predicate safeFilter(Predicate predicate) { + return (t) -> { + try { + return predicate.test(t); + } catch (Throwable ignored) { + return false; + } + }; + } +} diff --git a/zenscript-language-server/src/main/java/raylras/zen/lsp/ZenLanguageServer.java b/zenscript-language-server/src/main/java/raylras/zen/lsp/ZenLanguageServer.java index bf1a650d..fecdc40c 100644 --- a/zenscript-language-server/src/main/java/raylras/zen/lsp/ZenLanguageServer.java +++ b/zenscript-language-server/src/main/java/raylras/zen/lsp/ZenLanguageServer.java @@ -38,7 +38,6 @@ public void connect(LanguageClient client) { public CompletableFuture initialize(InitializeParams params) { service.initializeWorkspaces(params.getWorkspaceFolders()); L10N.setLocale(params.getLocale()); - ServerCapabilities capabilities = new ServerCapabilities(); capabilities.setTextDocumentSync(TextDocumentSyncKind.Full); capabilities.setCompletionProvider(new CompletionOptions(true, List.of(".", ":"))); diff --git a/zenscript-language-server/src/main/resources/logback.xml b/zenscript-language-server/src/main/resources/logback.xml new file mode 100644 index 00000000..62d01481 --- /dev/null +++ b/zenscript-language-server/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + System.err + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file