From 4cad384f9fe254da6641e80e174b84806fe40029 Mon Sep 17 00:00:00 2001 From: Nipuna Ranasinghe Date: Fri, 13 Dec 2024 14:57:58 +0530 Subject: [PATCH] Improve fast-run LS command to support user env variables --- .../command/executors/RunExecutor.java | 121 +++++++++++++---- .../workspace/BallerinaWorkspaceManager.java | 124 ++++++++++++------ 2 files changed, 175 insertions(+), 70 deletions(-) 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 fcc24d3a2d4d..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,10 +16,13 @@ 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; @@ -28,11 +31,17 @@ 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. @@ -43,52 +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 { - RunContext.Builder builder = new RunContext.Builder(extractPath(context)); - builder.withProgramArgs(extractProgramArgs(context)); - int debugPort = extractDebugArgs(context); - if (debugPort >= 0) { - builder.withDebugPort(debugPort); - } - // TODO: handle env vars - - RunContext runContext = builder.build(); - Optional processOpt = context.workspace().run(runContext); + 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().getFirst().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 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 context.getArguments().stream() - .filter(commandArg -> commandArg.key().equals("debugPort")) - .map(commandArg -> commandArg.value().getAsInt()) - .findAny() + return getCommandArgWithName(context, ARG_DEBUG_PORT) + .map(CommandArgument::value) + .map(JsonPrimitive::getAsInt) .orElse(-1); } - private static List extractProgramArgs(ExecuteCommandContext context) { - List args = new ArrayList<>(); - if (context.getArguments().size() <= 2) { - return args; - } - context.getArguments().get(2).value().getAsJsonArray().iterator() - .forEachRemaining(arg -> args.add(arg.getAsString())); + 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()); + } - return args; + 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) { @@ -111,6 +176,6 @@ private static void listenOutput(ExtendedLanguageClient client, Supplier run(RunContext context) throws IOException { - Path projectRoot = projectRoot(context.balSourcePath()); - Optional projectPairOpt = projectContext(projectRoot); - 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)); - if (context.debugPort() > 0) { - commands.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:" + context.debugPort()); - } - commands.add("-cp"); - commands.add(getAllClassPaths(jarResolver)); - commands.add(initClassName); - commands.addAll(context.programArgs()); + + 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); @@ -658,6 +670,29 @@ public Optional run(RunContext context) throws IOException { } } + private List prepareExecutionCommands(RunContext context, Module executableModule, JarResolver jarResolver) { + List commands = new ArrayList<>(); + commands.add(JAVA_COMMAND); + commands.add(HEAP_DUMP_FLAG); + commands.add(HEAP_DUMP_PATH_FLAG + USER_DIR); + if (context.debugPort() > 0) { + commands.add(DEBUG_SOCKET_CONFIG + context.debugPort()); + } + + commands.add("-cp"); + commands.add(getAllClassPaths(jarResolver)); + + String initClassName = JarResolver.getQualifiedClassName( + executableModule.packageInstance().packageOrg().toString(), + executableModule.packageInstance().packageName().toString(), + executableModule.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(); @@ -675,6 +710,11 @@ 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) { Optional projectPairOpt = projectContext(projectRoot(filePath)); @@ -1348,7 +1388,7 @@ public void didClose(Path filePath, DidCloseTextDocumentParams params) { } } - // ============================================================================================================== // +// ============================================================================================================== // private Path computeProjectRoot(Path path) { return computeProjectKindAndProjectRoot(path).getRight();