diff --git a/language-server/modules/langserver-commons/src/main/java/org/ballerinalang/langserver/commons/workspace/RunContext.java b/language-server/modules/langserver-commons/src/main/java/org/ballerinalang/langserver/commons/workspace/RunContext.java new file mode 100644 index 000000000000..b041880bc5ba --- /dev/null +++ b/language-server/modules/langserver-commons/src/main/java/org/ballerinalang/langserver/commons/workspace/RunContext.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020, WSO2 LLC. (http://www.wso2.org). + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.ballerinalang.langserver.commons.workspace; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Represents the context required to run a Ballerina program using the LS workspace manager. + * + * @param balSourcePath Ballerina source file path to run + * @param programArgs program arguments to run the program + * @param env environment variables to be added to the program + * @param debugPort debug port to be used for debugging (if available) + * @since 2201.11.0 + */ +public record RunContext(Path balSourcePath, List programArgs, Map env, Integer debugPort) { + + public static class Builder { + + private final Path sourcePath; + private List programArgs = new ArrayList<>(); + private Map env = Map.of(); + private int debugPort = -1; + + public Builder(Path sourcePath) { + this.sourcePath = sourcePath; + } + + public Builder withProgramArgs(List programArgs) { + this.programArgs = programArgs; + return this; + } + + public Builder withEnv(Map env) { + this.env = env; + return this; + } + + public Builder withDebugPort(int debugPort) { + this.debugPort = debugPort; + return this; + } + + public RunContext build() { + return new RunContext(sourcePath, programArgs, env, debugPort); + } + } +} diff --git a/language-server/modules/langserver-commons/src/main/java/org/ballerinalang/langserver/commons/workspace/WorkspaceManager.java b/language-server/modules/langserver-commons/src/main/java/org/ballerinalang/langserver/commons/workspace/WorkspaceManager.java index 156c8e92b57c..1a6206fb9c01 100644 --- a/language-server/modules/langserver-commons/src/main/java/org/ballerinalang/langserver/commons/workspace/WorkspaceManager.java +++ b/language-server/modules/langserver-commons/src/main/java/org/ballerinalang/langserver/commons/workspace/WorkspaceManager.java @@ -239,12 +239,13 @@ public interface WorkspaceManager { /** * Compiles and runs the project of the given file path. Run happens in a separate process. - * @param filePath Path that belongs to the project to be run. + * + * @param runContext context related to the project to be run. * @return Process created by running the project. Empty if failed due to non process related issues. * @throws IOException If failed to start the process. * @since 2201.6.0 */ - Optional run(Path filePath, List mainFuncArgs) throws IOException; + Optional run(RunContext runContext) throws IOException; /** * Stop a running process started with {@link #run}. diff --git a/language-server/modules/langserver-core/src/main/java/org/ballerinalang/langserver/command/executors/RunExecutor.java b/language-server/modules/langserver-core/src/main/java/org/ballerinalang/langserver/command/executors/RunExecutor.java index 31222bda88df..00ff5b017bc3 100644 --- a/language-server/modules/langserver-core/src/main/java/org/ballerinalang/langserver/command/executors/RunExecutor.java +++ b/language-server/modules/langserver-core/src/main/java/org/ballerinalang/langserver/command/executors/RunExecutor.java @@ -16,22 +16,32 @@ package org.ballerinalang.langserver.command.executors; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import org.ballerinalang.annotation.JavaSPIService; import org.ballerinalang.langserver.commons.ExecuteCommandContext; import org.ballerinalang.langserver.commons.client.ExtendedLanguageClient; +import org.ballerinalang.langserver.commons.command.CommandArgument; import org.ballerinalang.langserver.commons.command.LSCommandExecutorException; import org.ballerinalang.langserver.commons.command.spi.LSCommandExecutor; +import org.ballerinalang.langserver.commons.workspace.RunContext; import org.eclipse.lsp4j.LogTraceParams; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; -import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; /** * Command executor for running a Ballerina file. Each project at most has a single instance running at a time. @@ -42,36 +52,108 @@ @JavaSPIService("org.ballerinalang.langserver.commons.command.spi.LSCommandExecutor") public class RunExecutor implements LSCommandExecutor { + private static final String RUN_COMMAND = "RUN"; + + // commands arg names + private static final String ARG_PATH = "path"; + private static final String ARG_PROGRAM_ARGS = "programArgs"; + private static final String ARG_ENV = "env"; + private static final String ARG_DEBUG_PORT = "debugPort"; + + // output channels + private static final String ERROR_CHANNEL = "err"; + private static final String OUT_CHANNEL = "out"; + @Override public Boolean execute(ExecuteCommandContext context) throws LSCommandExecutorException { try { - Optional processOpt = context.workspace().run(extractPath(context), - extractMainFunctionArgs(context)); + RunContext workspaceRunContext = getWorkspaceRunContext(context); + Optional processOpt = context.workspace().run(workspaceRunContext); if (processOpt.isEmpty()) { return false; } Process process = processOpt.get(); - listenOutputAsync(context.getLanguageClient(), process::getInputStream, "out"); - listenOutputAsync(context.getLanguageClient(), process::getErrorStream, "err"); + listenOutputAsync(context.getLanguageClient(), process::getInputStream, OUT_CHANNEL); + listenOutputAsync(context.getLanguageClient(), process::getErrorStream, ERROR_CHANNEL); return true; } catch (IOException e) { + LogTraceParams error = new LogTraceParams("Error while running the program in fast-run mode: " + + e.getMessage(), ERROR_CHANNEL); + context.getLanguageClient().logTrace(error); + throw new LSCommandExecutorException(e); + } catch (Exception e) { + LogTraceParams error = new LogTraceParams("Unexpected error while executing the fast-run: " + + e.getMessage(), ERROR_CHANNEL); + context.getLanguageClient().logTrace(error); throw new LSCommandExecutorException(e); } } - private static Path extractPath(ExecuteCommandContext context) { - return Path.of(context.getArguments().get(0).value().getAsString()); + private RunContext getWorkspaceRunContext(ExecuteCommandContext context) { + RunContext.Builder builder = new RunContext.Builder(extractPath(context)); + builder.withProgramArgs(extractProgramArgs(context)); + builder.withEnv(extractEnvVariables(context)); + builder.withDebugPort(extractDebugArgs(context)); + + return builder.build(); } - private static List extractMainFunctionArgs(ExecuteCommandContext context) { - List args = new ArrayList<>(); - if (context.getArguments().size() == 1) { - return args; - } - context.getArguments().get(1).value().getAsJsonArray().iterator().forEachRemaining(arg -> { - args.add(arg.getAsString()); - }); - return args; + private Path extractPath(ExecuteCommandContext context) { + return getCommandArgWithName(context, ARG_PATH) + .map(CommandArgument::value) + .map(JsonPrimitive::getAsString) + .map(pathStr -> { + try { + Path path = Path.of(pathStr); + if (!Files.exists(path)) { + throw new IllegalArgumentException("Specified path does not exist: " + pathStr); + } + return path; + } catch (InvalidPathException e) { + throw new IllegalArgumentException("Invalid path: " + pathStr, e); + } + }) + .orElseThrow(() -> new IllegalArgumentException("Path argument is required")); + } + + private int extractDebugArgs(ExecuteCommandContext context) { + return getCommandArgWithName(context, ARG_DEBUG_PORT) + .map(CommandArgument::value) + .map(JsonPrimitive::getAsInt) + .orElse(-1); + } + + private List extractProgramArgs(ExecuteCommandContext context) { + return getCommandArgWithName(context, ARG_PROGRAM_ARGS) + .map(arg -> arg.value().getAsJsonArray()) + .map(jsonArray -> StreamSupport.stream(jsonArray.spliterator(), false) + .filter(JsonElement::isJsonPrimitive) + .map(JsonElement::getAsJsonPrimitive) + .filter(JsonPrimitive::isString) + .map(JsonPrimitive::getAsString) + .collect(Collectors.toList())) + .orElse(Collections.emptyList()); + } + + private Map extractEnvVariables(ExecuteCommandContext context) { + return getCommandArgWithName(context, ARG_ENV) + .map(CommandArgument::value) + .map(jsonObject -> { + Map envMap = new HashMap<>(); + for (Map.Entry entry : jsonObject.entrySet()) { + if (entry.getValue().isJsonPrimitive() && entry.getValue().getAsJsonPrimitive().isString()) { + envMap.put(entry.getKey(), entry.getValue().getAsString()); + } + } + return Collections.unmodifiableMap(envMap); + }) + .orElse(Map.of()); + } + + private static Optional getCommandArgWithName(ExecuteCommandContext context, String name) { + return context.getArguments().stream() + .filter(commandArg -> commandArg.key().equals(name)) + .findAny(); } public void listenOutputAsync(ExtendedLanguageClient client, Supplier getInputStream, String channel) { @@ -94,6 +176,6 @@ private static void listenOutput(ExtendedLanguageClient client, Supplier run(Path filePath, List mainFuncArgs) throws IOException { - Optional projectPairOpt = projectContext(projectRoot(filePath)); - if (projectPairOpt.isEmpty()) { - String msg = "Run command execution aborted because project is not loaded"; - UserErrorException e = new UserErrorException(msg); - clientLogger.logError(LSContextOperation.WS_EXEC_CMD, msg, e, null, (Position) null); + public Optional run(RunContext executionContext) throws IOException { + Path projectRoot = projectRoot(executionContext.balSourcePath()); + Optional projectContext = validateProjectContext(projectRoot); + if (projectContext.isEmpty()) { return Optional.empty(); } - ProjectContext projectContext = projectPairOpt.get(); - if (!stopProject(projectContext)) { - String msg = "Run command execution aborted because couldn't stop the previous run"; - UserErrorException e = new UserErrorException(msg); - clientLogger.logError(LSContextOperation.WS_EXEC_CMD, msg, e, null, (Position) null); + + if (!prepareProjectForExecution(projectContext.get())) { return Optional.empty(); } + return executeProject(projectContext.get(), executionContext); + } + + private Optional validateProjectContext(Path projectRoot) { + Optional projectContextOpt = projectContext(projectRoot); + if (projectContextOpt.isEmpty()) { + logError("Run command execution aborted because project is not loaded"); + return Optional.empty(); + } + + return projectContextOpt; + } + + private boolean prepareProjectForExecution(ProjectContext projectContext) { + // stop previous project run + if (!stopProject(projectContext)) { + logError("Run command execution aborted because couldn't stop the previous run"); + return false; + } + Project project = projectContext.project(); - Package pkg = project.currentPackage(); - Module executableModule = pkg.getDefaultModule(); Optional packageCompilation = waitAndGetPackageCompilation(project.sourceRoot(), true); if (packageCompilation.isEmpty()) { - return Optional.empty(); + logError("Run command execution aborted because package compilation failed"); + return false; } + + // check for compilation errors JBallerinaBackend jBallerinaBackend = execBackend(projectContext, packageCompilation.get()); Collection diagnostics = jBallerinaBackend.diagnosticResult().diagnostics(false); if (diagnostics.stream().anyMatch(BallerinaWorkspaceManager::isError)) { - String msg = "Run command execution aborted due to compilation errors: " + diagnostics; - UserErrorException e = new UserErrorException(msg); - clientLogger.logError(LSContextOperation.WS_EXEC_CMD, msg, e, null, (Position) null); - return Optional.empty(); + logError("Run command execution aborted due to compilation errors: " + diagnostics); + return false; } + + return true; + } + + private Optional executeProject(ProjectContext projectContext, RunContext context) throws IOException { + Project project = projectContext.project(); + Package pkg = project.currentPackage(); + Module executableModule = pkg.getDefaultModule(); + JBallerinaBackend jBallerinaBackend = execBackend(projectContext, pkg.getCompilation()); JarResolver jarResolver = jBallerinaBackend.jarResolver(); - String initClassName = JarResolver.getQualifiedClassName( - executableModule.packageInstance().packageOrg().toString(), - executableModule.packageInstance().packageName().toString(), - executableModule.packageInstance().packageVersion().toString(), - MODULE_INIT_CLASS_NAME); - List commands = new ArrayList<>(); - commands.add(System.getProperty("java.command")); - commands.add("-XX:+HeapDumpOnOutOfMemoryError"); - commands.add("-XX:HeapDumpPath=" + System.getProperty(USER_DIR)); - commands.add("-cp"); - commands.add(getAllClassPaths(jarResolver)); - commands.add(initClassName); - commands.addAll(mainFuncArgs); + + List commands = prepareExecutionCommands(context, executableModule, jarResolver); ProcessBuilder pb = new ProcessBuilder(commands); + pb.environment().putAll(context.env()); Lock lock = projectContext.lockAndGet(); try { Optional existing = projectContext.process(); if (existing.isPresent()) { - // We just removed this in above `stopProject`. This means there is a parallel command running. - String msg = "Run command execution aborted because another run is in progress"; - UserErrorException e = new UserErrorException(msg); - clientLogger.logError(LSContextOperation.WS_EXEC_CMD, msg, e, null, (Position) null); + logError("Run command execution aborted because another run is in progress"); return Optional.empty(); } + Process ps = pb.start(); projectContext.setProcess(ps); return Optional.of(ps); @@ -653,6 +671,29 @@ public Optional run(Path filePath, List mainFuncArgs) throws IO } } + private List prepareExecutionCommands(RunContext context, Module module, JarResolver jarResolver) { + List commands = new ArrayList<>(); + commands.add(System.getProperty(JAVA_COMMAND)); + commands.add(HEAP_DUMP_FLAG); + commands.add(HEAP_DUMP_PATH_FLAG + USER_DIR); + if (context.debugPort() > 0) { + commands.add(DEBUG_ARGS + context.debugPort()); + } + + commands.add("-cp"); + commands.add(getAllClassPaths(jarResolver)); + + String initClassName = JarResolver.getQualifiedClassName( + module.packageInstance().packageOrg().toString(), + module.packageInstance().packageName().toString(), + module.packageInstance().packageVersion().toString(), + MODULE_INIT_CLASS_NAME + ); + commands.add(initClassName); + commands.addAll(context.programArgs()); + return commands; + } + private static JBallerinaBackend execBackend(ProjectContext projectContext, PackageCompilation packageCompilation) { Lock lock = projectContext.lockAndGet(); @@ -670,6 +711,10 @@ private static JBallerinaBackend execBackend(ProjectContext projectContext, } } + private void logError(String message) { + UserErrorException e = new UserErrorException(message); + clientLogger.logError(LSContextOperation.WS_EXEC_CMD, message, e, null, (Position) null); + } @Override public boolean stop(Path filePath) { @@ -1344,7 +1389,7 @@ public void didClose(Path filePath, DidCloseTextDocumentParams params) { } } - // ============================================================================================================== // +// ============================================================================================================== // private Path computeProjectRoot(Path path) { return computeProjectKindAndProjectRoot(path).getRight(); diff --git a/misc/debug-adapter/modules/debug-adapter-core/spotbugs-exclude.xml b/misc/debug-adapter/modules/debug-adapter-core/spotbugs-exclude.xml index 7e528799f807..b541c72b23f8 100644 --- a/misc/debug-adapter/modules/debug-adapter-core/spotbugs-exclude.xml +++ b/misc/debug-adapter/modules/debug-adapter-core/spotbugs-exclude.xml @@ -61,7 +61,16 @@ - + + + + + + + + + + diff --git a/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/BallerinaExtendedDebugServer.java b/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/BallerinaExtendedDebugServer.java new file mode 100644 index 000000000000..e44f512d42cd --- /dev/null +++ b/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/BallerinaExtendedDebugServer.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://wso2.com). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballerinalang.debugadapter; + +import org.eclipse.lsp4j.debug.OutputEventArguments; +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer; +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; + +import java.util.concurrent.CompletableFuture; + +/** + * Extended debug server interface for Ballerina debug adapter. + * + * @since 2201.11.0 + */ +public interface BallerinaExtendedDebugServer extends IDebugProtocolServer { + + /** + * Custom request to forward the program output from the remote VM (initiated by the debug client) to the debug + * server. This extension is specifically designed for fast-run mode, enabling the debug adapter to forward the + * program output to the client. + * + * @param arguments the output event arguments + * @return a CompletableFuture representing the completion of the request + */ + @JsonRequest + CompletableFuture output(OutputEventArguments arguments); +} diff --git a/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/DebugExecutionManager.java b/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/DebugExecutionManager.java index c918e5fd186c..f741ca9b754a 100755 --- a/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/DebugExecutionManager.java +++ b/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/DebugExecutionManager.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.util.Map; +import java.util.Objects; import java.util.Optional; /** @@ -72,27 +73,58 @@ public String getRemoteVMAddress() { * Attaches to an existing JVM using an SocketAttachingConnector and returns the attached VM instance. */ public VirtualMachine attach(String hostName, int port) throws IOException, IllegalConnectorArgumentsException { - AttachingConnector socketAttachingConnector = Bootstrap.virtualMachineManager().attachingConnectors().stream() + // Default retry configuration + return attach(hostName, port, 50, 100); // 100 attempts, 50ms between attempts + } + + public VirtualMachine attach(String hostName, int port, long retryIntervalMs, int maxAttempts) + throws IOException, IllegalConnectorArgumentsException { + + AttachingConnector attachingConnector = Bootstrap.virtualMachineManager().attachingConnectors().stream() .filter(ac -> ac.name().equals(SOCKET_CONNECTOR_NAME)) .findFirst() .orElseThrow(() -> new RuntimeException("Unable to locate SocketAttachingConnector")); - Map connectorArgs = socketAttachingConnector.defaultArguments(); - if (!hostName.isEmpty()) { - connectorArgs.get(CONNECTOR_ARGS_HOST).setValue(hostName); - } + hostName = Objects.isNull(hostName) || hostName.isBlank() ? LOCAL_HOST : hostName; + Map connectorArgs = attachingConnector.defaultArguments(); + connectorArgs.get(CONNECTOR_ARGS_HOST).setValue(hostName); connectorArgs.get(CONNECTOR_ARGS_PORT).setValue(String.valueOf(port)); - LOGGER.info(String.format("Debugger is attaching to: %s:%d", hostName, port)); - attachedVm = socketAttachingConnector.attach(connectorArgs); - this.host = !hostName.isEmpty() ? hostName : LOCAL_HOST; - this.port = port; + return attachWithRetries(attachingConnector, connectorArgs, retryIntervalMs, maxAttempts); + } + + private VirtualMachine attachWithRetries(AttachingConnector connector, Map args, + long retryIntervalMs, int maxAttempts) + throws IOException, IllegalConnectorArgumentsException { + + for (int attempt = 1; attempt <= maxAttempts; attempt++) { + try { + attachedVm = connector.attach(args); + this.host = args.get(CONNECTOR_ARGS_HOST).value(); + this.port = Integer.parseInt(args.get(CONNECTOR_ARGS_PORT).value()); + if (server.getClientConfigHolder().getKind() == ClientConfigHolder.ClientConfigKind.ATTACH_CONFIG) { + server.getOutputLogger().sendDebugServerOutput( + String.format("Connected to the target VM, address: '%s:%s'", host, port) + ); + } - // Todo - enable for launch-mode after implementing debug server client logger - if (server.getClientConfigHolder().getKind() == ClientConfigHolder.ClientConfigKind.ATTACH_CONFIG) { - server.getOutputLogger().sendDebugServerOutput((String.format("Connected to the target VM, address: " + - "'%s:%s'", host, port))); + return attachedVm; + } catch (IOException e) { + LOGGER.debug(String.format("Attach attempt %d/%d failed: %s", attempt, maxAttempts, e.getMessage())); + try { + Thread.sleep(retryIntervalMs); + } catch (InterruptedException ignored) { + // ignored + } + } } - return attachedVm; + + throw new IOException("Failed to attach to the target VM."); + } + + public void reset() { + attachedVm = null; + host = null; + port = null; } } diff --git a/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/DebugOutputLogger.java b/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/DebugOutputLogger.java index 45c9345f269e..d8ebaea4c45d 100644 --- a/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/DebugOutputLogger.java +++ b/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/DebugOutputLogger.java @@ -87,7 +87,7 @@ public void sendConsoleOutput(String output) { } /** - * Processes messages which are generated by the debug server itself and should treated as console outputs. + * Processes messages which are generated by the debug server itself and should be treated as console outputs. * * @param output string output produced in the debugger/target VM. */ diff --git a/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/JBallerinaDebugServer.java b/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/JBallerinaDebugServer.java index 5f2503c3e3e1..3ab74fd6d4ff 100755 --- a/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/JBallerinaDebugServer.java +++ b/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/JBallerinaDebugServer.java @@ -74,6 +74,8 @@ import org.eclipse.lsp4j.debug.ExitedEventArguments; import org.eclipse.lsp4j.debug.InitializeRequestArguments; import org.eclipse.lsp4j.debug.NextArguments; +import org.eclipse.lsp4j.debug.OutputEventArguments; +import org.eclipse.lsp4j.debug.OutputEventArgumentsCategory; import org.eclipse.lsp4j.debug.PauseArguments; import org.eclipse.lsp4j.debug.RestartArguments; import org.eclipse.lsp4j.debug.RunInTerminalRequestArguments; @@ -100,7 +102,6 @@ import org.eclipse.lsp4j.debug.VariablesArguments; import org.eclipse.lsp4j.debug.VariablesResponse; import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; -import org.eclipse.lsp4j.debug.services.IDebugProtocolServer; import org.eclipse.lsp4j.jsonrpc.Endpoint; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint; @@ -120,6 +121,7 @@ import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -137,13 +139,14 @@ import static org.ballerinalang.debugadapter.utils.PackageUtils.getQualifiedClassName; import static org.ballerinalang.debugadapter.utils.ServerUtils.isBalStackFrame; import static org.ballerinalang.debugadapter.utils.ServerUtils.isBalStrand; +import static org.ballerinalang.debugadapter.utils.ServerUtils.isFastRunEnabled; import static org.ballerinalang.debugadapter.utils.ServerUtils.isNoDebugMode; import static org.ballerinalang.debugadapter.utils.ServerUtils.toBalBreakpoint; /** * JBallerina debug server implementation. */ -public class JBallerinaDebugServer implements IDebugProtocolServer { +public class JBallerinaDebugServer implements BallerinaExtendedDebugServer { private IDebugProtocolClient client; private ClientConfigHolder clientConfigHolder; @@ -268,43 +271,39 @@ public CompletableFuture configurationDone(ConfigurationDoneArguments args @Override public CompletableFuture launch(Map args) { - try { - clientConfigHolder = new ClientLaunchConfigHolder(args); - launchDebuggeeProgram(); - return CompletableFuture.completedFuture(null); - } catch (Exception e) { - outputLogger.sendErrorOutput("Failed to launch the Ballerina program due to: " + e.getMessage()); - return CompletableFuture.failedFuture(e); - } + return CompletableFuture.supplyAsync(() -> { + try { + clientConfigHolder = new ClientLaunchConfigHolder(args); + launchDebuggeeProgram(); + return null; + } catch (Exception e) { + outputLogger.sendErrorOutput("Failed to launch the Ballerina program due to: " + e.getMessage()); + throw new CompletionException(e); + } + }); } @Override public CompletableFuture attach(Map args) { - try { - clientConfigHolder = new ClientAttachConfigHolder(args); - context.setDebugMode(ExecutionContext.DebugMode.ATTACH); - Project sourceProject = context.getProjectCache().getProject(Path.of(clientConfigHolder.getSourcePath())); - context.setSourceProject(sourceProject); - ClientAttachConfigHolder configHolder = (ClientAttachConfigHolder) clientConfigHolder; - - String hostName = configHolder.getHostName().orElse(""); - int portName = configHolder.getDebuggePort(); - attachToRemoteVM(hostName, portName); - return CompletableFuture.completedFuture(null); - } catch (Exception e) { - String host = ((ClientAttachConfigHolder) clientConfigHolder).getHostName().orElse(LOCAL_HOST); - String portName; + return CompletableFuture.supplyAsync(() -> { try { - portName = Integer.toString(clientConfigHolder.getDebuggePort()); - } catch (ClientConfigurationException clientConfigurationException) { - portName = VALUE_UNKNOWN; + clientConfigHolder = new ClientAttachConfigHolder(args); + context.setDebugMode(ExecutionContext.DebugMode.ATTACH); + Project srcProject = context.getProjectCache().getProject(Path.of(clientConfigHolder.getSourcePath())); + context.setSourceProject(srcProject); + ClientAttachConfigHolder configHolder = (ClientAttachConfigHolder) clientConfigHolder; + + String hostName = configHolder.getHostName().orElse(""); + int portName = configHolder.getDebuggePort(); + attachToRemoteVM(hostName, portName); + return null; + } catch (Exception e) { + String errorMessage = getAttachmentErrorMessage(e); + outputLogger.sendErrorOutput(errorMessage); + terminateDebugSession(context.getDebuggeeVM() != null, false); + throw new CompletionException(e); } - LOGGER.error(e.getMessage()); - outputLogger.sendErrorOutput(String.format("Failed to attach to the target VM address: '%s:%s' due to: %s", - host, portName, e.getMessage())); - terminateDebugSession(context.getDebuggeeVM() != null, false); - return CompletableFuture.failedFuture(e); - } + }); } @Override @@ -471,15 +470,26 @@ private void launchDebuggeeProgram() throws Exception { Project sourceProject = context.getProjectCache().getProject(Path.of(clientConfigHolder.getSourcePath())); context.setSourceProject(sourceProject); String sourceProjectRoot = context.getSourceProjectRoot(); - BProgramRunner programRunner = context.getSourceProject() instanceof SingleFileProject ? - new BSingleFileRunner((ClientLaunchConfigHolder) clientConfigHolder, sourceProjectRoot) : - new BPackageRunner((ClientLaunchConfigHolder) clientConfigHolder, sourceProjectRoot); - - if (context.getSupportsRunInTerminalRequest() && clientConfigHolder.getRunInTerminalKind() != null) { - launchInTerminal(programRunner); + // if the debug server runs in fast-run mode, send a notification to the client to re-run the remote program and + // re-attach to the new VM. + if (isFastRunEnabled(context)) { + int port = ServerUtils.findFreePort(); + outputLogger.sendDebugServerOutput("Waiting for the debug process to start...%s%s" + .formatted(System.lineSeparator(), System.lineSeparator())); + ServerUtils.sendFastRunNotification(context, port); + attachToRemoteVM(LOCAL_HOST, port); } else { - context.setLaunchedProcess(programRunner.start()); - startListeningToProgramOutput(); + BProgramRunner programRunner = context.getSourceProject() instanceof SingleFileProject ? + new BSingleFileRunner((ClientLaunchConfigHolder) clientConfigHolder, sourceProjectRoot) : + new BPackageRunner((ClientLaunchConfigHolder) clientConfigHolder, sourceProjectRoot); + + if (context.getSupportsRunInTerminalRequest() && clientConfigHolder.getRunInTerminalKind() != null) { + launchInTerminal(programRunner); + } else { + Process debuggeeProcess = programRunner.start(); + context.setLaunchedProcess(debuggeeProcess); + startListeningToProgramOutput(); + } } } @@ -605,6 +615,17 @@ public CompletableFuture terminate(TerminateArguments args) { return CompletableFuture.completedFuture(null); } + @Override + public CompletableFuture output(OutputEventArguments arguments) { + switch (arguments.getCategory()) { + case OutputEventArgumentsCategory.STDOUT -> outputLogger.sendProgramOutput(arguments.getOutput()); + case OutputEventArgumentsCategory.STDERR -> outputLogger.sendErrorOutput(arguments.getOutput()); + default -> { + } + } + return CompletableFuture.completedFuture(null); + } + @Override public CompletableFuture runInTerminal(RunInTerminalRequestArguments args) { Endpoint endPoint = new GenericEndpoint(context.getClient()); @@ -615,7 +636,7 @@ public CompletableFuture runInTerminal(RunInTerminalReque while (context.getDebuggeeVM() == null && tryCounter < 10) { try { JDIUtils.sleepMillis(3000); - attachToRemoteVM("", clientConfigHolder.getDebuggePort()); + attachToRemoteVM(LOCAL_HOST, clientConfigHolder.getDebuggePort()); } catch (IOException ignored) { tryCounter++; } catch (IllegalConnectorArgumentsException | ClientConfigurationException e) { @@ -872,11 +893,11 @@ private void startListeningToProgramOutput() { try (BufferedReader inputStream = context.getInputStream()) { String line; - outputLogger.sendDebugServerOutput("Waiting for debug process to start..." + System.lineSeparator() - + System.lineSeparator()); + outputLogger.sendDebugServerOutput("Waiting for the debug process to start...%s%s" + .formatted(System.lineSeparator(), System.lineSeparator())); while ((line = inputStream.readLine()) != null) { if (line.contains("Listening for transport dt_socket")) { - attachToRemoteVM("", clientConfigHolder.getDebuggePort()); + attachToRemoteVM(LOCAL_HOST, clientConfigHolder.getDebuggePort()); } else if (context.getDebuggeeVM() == null && line.contains(COMPILATION_ERROR_MESSAGE)) { terminateDebugSession(false, true); } @@ -1167,6 +1188,18 @@ private EvaluateResponse constructEvaluateResponse(EvaluateArguments args, BVari return response; } + private String getAttachmentErrorMessage(Exception e) { + String host = ((ClientAttachConfigHolder) clientConfigHolder).getHostName().orElse(LOCAL_HOST); + String portName; + try { + portName = Integer.toString(clientConfigHolder.getDebuggePort()); + } catch (ClientConfigurationException clientConfigurationException) { + portName = VALUE_UNKNOWN; + } + return String.format("Failed to attach to the target VM address: '%s:%s' due to: %s", + host, portName, e.getMessage()); + } + /** * Clears the suspended state information. */ @@ -1188,6 +1221,7 @@ private void clearSuspendedState() { private void resetServer() { Optional.ofNullable(eventProcessor).ifPresent(JDIEventProcessor::reset); Optional.ofNullable(outputLogger).ifPresent(DebugOutputLogger::reset); + Optional.ofNullable(executionManager).ifPresent(DebugExecutionManager::reset); terminateDebuggee(); clearSuspendedState(); context.reset(); diff --git a/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/config/ClientConfigHolder.java b/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/config/ClientConfigHolder.java index c2d294585858..73b118cc7861 100644 --- a/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/config/ClientConfigHolder.java +++ b/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/config/ClientConfigHolder.java @@ -41,6 +41,7 @@ public class ClientConfigHolder { private static final String ARG_CAPABILITIES = "capabilities"; private static final String ARG_SUPPORT_READONLY_EDITOR = "supportsReadOnlyEditors"; private static final String ARG_SUPPORT_BP_VERIFICATION = "supportsBreakpointVerification"; + private static final String ARG_SUPPORT_FAST_RUN = "supportsFastRun"; private static final String ARG_TERMINAL_KIND = "terminal"; private static final String INTEGRATED_TERMINAL_KIND = "INTEGRATED"; private static final String EXTERNAL_TERMINAL_KIND = "EXTERNAL"; @@ -98,6 +99,15 @@ public Optional getExtendedCapabilities() { extendedClientCapabilities.setSupportsBreakpointVerification(false); } + Object fastRunConfig = capabilities.get(ARG_SUPPORT_FAST_RUN); + if (fastRunConfig instanceof Boolean b) { + extendedClientCapabilities.setSupportsFastRun(b); + } else if (fastRunConfig instanceof String s) { + extendedClientCapabilities.setSupportsFastRun(Boolean.parseBoolean(s)); + } else { + extendedClientCapabilities.setSupportsFastRun(false); + } + return Optional.ofNullable(extendedClientCapabilities); } @@ -133,6 +143,7 @@ public static class ExtendedClientCapabilities { private boolean supportsReadOnlyEditors = false; private boolean supportsBreakpointVerification = false; + private boolean supportsFastRun = false; public boolean supportsReadOnlyEditors() { return supportsReadOnlyEditors; @@ -149,5 +160,13 @@ public boolean supportsBreakpointVerification() { public void setSupportsBreakpointVerification(boolean supportsBreakpointVerification) { this.supportsBreakpointVerification = supportsBreakpointVerification; } + + public boolean supportsFastRun() { + return supportsFastRun; + } + + public void setSupportsFastRun(boolean supportsFastRun) { + this.supportsFastRun = supportsFastRun; + } } } diff --git a/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/utils/ServerUtils.java b/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/utils/ServerUtils.java index 6efda07cec62..f3634ce9c8a0 100644 --- a/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/utils/ServerUtils.java +++ b/misc/debug-adapter/modules/debug-adapter-core/src/main/java/org/ballerinalang/debugadapter/utils/ServerUtils.java @@ -23,8 +23,13 @@ import org.eclipse.lsp4j.debug.Source; import org.eclipse.lsp4j.debug.SourceBreakpoint; import org.eclipse.lsp4j.debug.StackFrame; +import org.eclipse.lsp4j.jsonrpc.Endpoint; +import org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint; +import java.net.ServerSocket; +import java.util.Map; import java.util.Objects; +import java.util.Optional; import static org.ballerinalang.debugadapter.utils.PackageUtils.BAL_FILE_EXT; @@ -35,6 +40,8 @@ */ public class ServerUtils { + private static final String FAST_RUN_NOTIFICATION_NAME = "startFastRun"; + /** * Checks whether the debug server should run in no-debug mode. * @@ -42,8 +49,30 @@ public class ServerUtils { * @return true if the debug mode is no-debug mode */ public static boolean isNoDebugMode(ExecutionContext context) { - ClientConfigHolder confHolder = context.getAdapter().getClientConfigHolder(); - return confHolder instanceof ClientLaunchConfigHolder launchConfigHolder && launchConfigHolder.isNoDebugMode(); + try { + ClientConfigHolder confHolder = context.getAdapter().getClientConfigHolder(); + return confHolder instanceof ClientLaunchConfigHolder launchConfHolder && launchConfHolder.isNoDebugMode(); + } catch (Exception e) { + return false; + } + } + + /** + * Checks whether the fast-run mode is enabled in the connected debug client. + * + * @param context debug context + * @return true if the debug mode is fast-run mode + */ + public static boolean isFastRunEnabled(ExecutionContext context) { + try { + Optional extendedCapabilities = + context.getAdapter().getClientConfigHolder().getExtendedCapabilities(); + return extendedCapabilities + .map(ClientConfigHolder.ExtendedClientCapabilities::supportsFastRun) + .orElse(false); + } catch (Exception e) { + return false; + } } /** @@ -119,4 +148,45 @@ public static boolean supportsBreakpointVerification(ExecutionContext context) { .map(ClientConfigHolder.ExtendedClientCapabilities::supportsBreakpointVerification) .orElse(false); } + + /** + * Sends a custom notification to the debug client to trigger a fast-run. + * + * @param context debug context + * @param port port number + */ + public static void sendFastRunNotification(ExecutionContext context, int port) { + Endpoint endPoint = new GenericEndpoint(context.getClient()); + ClientConfigHolder configs = context.getAdapter().getClientConfigHolder(); + Map envVarMap = ((ClientLaunchConfigHolder) configs).getEnv().orElse(Map.of()); + String[] programArgs = ((ClientLaunchConfigHolder) configs).getProgramArgs().toArray(new String[0]); + + FastRunArgs args = new FastRunArgs(port, envVarMap, programArgs); + endPoint.notify(FAST_RUN_NOTIFICATION_NAME, args); + } + + /** + * Finds an available port. + * + * @return available port number. + */ + public static int findFreePort() { + try (ServerSocket socket = new ServerSocket(0)) { + socket.setReuseAddress(true); + return socket.getLocalPort(); + } catch (Exception e) { + throw new IllegalStateException("Could not find a free TCP/IP port to start debugging", e); + } + } + + /** + * Represents the arguments of the 'startFastRun' notification. + * + * @param debugPort debug port number + * @param env environment variables + * @param programArgs program arguments + */ + public record FastRunArgs(int debugPort, Map env, String[] programArgs) { + + } } diff --git a/tests/jballerina-debugger-integration-test/src/test/java/org/ballerinalang/debugger/test/adapter/DebugOutputTest.java b/tests/jballerina-debugger-integration-test/src/test/java/org/ballerinalang/debugger/test/adapter/DebugOutputTest.java index 5cce2c720b67..f98bcf1acbc3 100644 --- a/tests/jballerina-debugger-integration-test/src/test/java/org/ballerinalang/debugger/test/adapter/DebugOutputTest.java +++ b/tests/jballerina-debugger-integration-test/src/test/java/org/ballerinalang/debugger/test/adapter/DebugOutputTest.java @@ -62,7 +62,7 @@ public void testProgramOutput() throws BallerinaTestException { // Validates initial debug outputs which uses `CONSOLE` category. Assert.assertEquals(outputs.get(0).getRight().getCategory(), OutputEventArgumentsCategory.CONSOLE); - Assert.assertEquals(outputs.get(0).getLeft(), "Waiting for debug process to start..." + + Assert.assertEquals(outputs.get(0).getLeft(), "Waiting for the debug process to start..." + System.lineSeparator() + System.lineSeparator()); Assert.assertEquals(outputs.get(1).getRight().getCategory(), OutputEventArgumentsCategory.CONSOLE);