From 115939764139d4a5190022248e1af38eef064a6f Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Wed, 29 Nov 2017 19:10:44 +0800 Subject: [PATCH 1/2] Provide launchInTerminal feature to support console input Signed-off-by: Jinbo Wang --- .../java/debug/core/DebugException.java | 20 ++ .../java/debug/core/DebugSession.java | 2 +- .../core/adapter/DebugAdapterContext.java | 11 + .../java/debug/core/adapter/ErrorCode.java | 23 +- .../core/adapter/IDebugAdapterContext.java | 4 + .../debug/core/adapter/ProtocolServer.java | 29 ++- .../handler/InitializeRequestHandler.java | 1 + .../adapter/handler/LaunchRequestHandler.java | 224 ++++++++++++++++-- .../java/debug/core/protocol/Requests.java | 109 ++++++++- .../java/debug/core/protocol/Responses.java | 8 + 10 files changed, 391 insertions(+), 40 deletions(-) diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugException.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugException.java index eae0291b7..6ee7fbccd 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugException.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugException.java @@ -13,6 +13,7 @@ public class DebugException extends Exception { private static final long serialVersionUID = 1L; + private int errorCode; public DebugException() { super(); @@ -29,4 +30,23 @@ public DebugException(String message, Throwable cause) { public DebugException(Throwable cause) { super(cause); } + + public DebugException(String message, int errorCode) { + super(message); + this.errorCode = errorCode; + } + + public DebugException(String message, Throwable cause, int errorCode) { + super(message, cause); + this.errorCode = errorCode; + } + + public DebugException(Throwable cause, int errorCode) { + super(cause); + this.errorCode = errorCode; + } + + public int getErrorCode() { + return this.errorCode; + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java index 7d5b5cc56..5595ed9f8 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java @@ -71,7 +71,7 @@ public void detach() { @Override public void terminate() { - if (vm.process().isAlive()) { + if (vm.process() == null || vm.process().isAlive()) { vm.exit(0); } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java index ecfbf5340..99bc29a71 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java @@ -31,6 +31,7 @@ public class DebugAdapterContext implements IDebugAdapterContext { private boolean debuggerPathsAreUri = true; private boolean clientLinesStartAt1 = true; private boolean clientPathsAreUri = false; + private boolean supportsRunInTerminalRequest; private boolean isAttached = false; private String[] sourcePaths; private Charset debuggeeEncoding; @@ -107,6 +108,16 @@ public void setClientPathsAreUri(boolean clientPathsAreUri) { this.clientPathsAreUri = clientPathsAreUri; } + @Override + public void setSupportsRunInTerminalRequest(boolean supportsRunInTerminalRequest) { + this.supportsRunInTerminalRequest = supportsRunInTerminalRequest; + } + + @Override + public boolean supportsRunInTerminalRequest() { + return supportsRunInTerminalRequest; + } + @Override public boolean isAttached() { return isAttached; diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ErrorCode.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ErrorCode.java index bffeb6272..ea1792ad3 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ErrorCode.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ErrorCode.java @@ -11,6 +11,8 @@ package com.microsoft.java.debug.core.adapter; +import java.util.Arrays; + public enum ErrorCode { UNKNOWN_FAILURE(1000), UNRECOGNIZED_REQUEST_FAILURE(1001), @@ -25,7 +27,8 @@ public enum ErrorCode { EVALUATE_FAILURE(1010), EMPTY_DEBUG_SESSION(1011), INVALID_ENCODING(1012), - VM_TERMINATED(1013); + VM_TERMINATED(1013), + LAUNCH_IN_TERMINAL_FAILURE(1014); private int id; @@ -36,4 +39,22 @@ public enum ErrorCode { public int getId() { return id; } + + /** + * Get the corresponding ErrorCode type by the error code id. + * If the error code is not defined in the enum type, return ErrorCode.UNKNOWN_FAILURE. + * @param id + * the error code id. + * @return the ErrorCode type. + */ + public static ErrorCode parse(int id) { + ErrorCode[] found = Arrays.stream(ErrorCode.values()).filter(code -> { + return code.getId() == id; + }).toArray(ErrorCode[]::new); + + if (found.length > 0) { + return found[0]; + } + return ErrorCode.UNKNOWN_FAILURE; + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java index 142b7af90..4451ad25f 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java @@ -53,6 +53,10 @@ public interface IDebugAdapterContext { void setClientPathsAreUri(boolean clientPathsAreUri); + void setSupportsRunInTerminalRequest(boolean supportsRunInTerminalRequest); + + boolean supportsRunInTerminalRequest(); + boolean isAttached(); void setAttached(boolean attached); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProtocolServer.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProtocolServer.java index 24296dd1c..fc01aa6ad 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProtocolServer.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProtocolServer.java @@ -14,13 +14,16 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.logging.Level; import java.util.logging.Logger; import com.microsoft.java.debug.core.Configuration; +import com.microsoft.java.debug.core.DebugException; import com.microsoft.java.debug.core.UsageDataSession; import com.microsoft.java.debug.core.protocol.AbstractProtocolServer; import com.microsoft.java.debug.core.protocol.Messages; +import com.sun.jdi.VMDisconnectedException; public class ProtocolServer extends AbstractProtocolServer { private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); @@ -74,20 +77,36 @@ public CompletableFuture sendRequest(Messages.Request request @Override protected void dispatchRequest(Messages.Request request) { usageDataSession.recordRequest(request); - this.debugAdapter.dispatchRequest(request).whenComplete((response, ex) -> { + this.debugAdapter.dispatchRequest(request).thenAccept((response) -> { if (response != null) { sendResponse(response); - } else if (ex != null) { - logger.log(Level.SEVERE, String.format("DebugSession dispatch exception: %s", ex.toString()), ex); + } else { + logger.log(Level.SEVERE, "The request dispatcher should not return null response."); + response = new Messages.Response(request.seq, request.command); sendResponse(AdapterUtils.setErrorResponse(response, ErrorCode.UNKNOWN_FAILURE, + "The request dispatcher should not return null response.")); + } + }).exceptionally((ex) -> { + Messages.Response response = new Messages.Response(request.seq, request.command); + if (ex instanceof CompletionException && ex.getCause() != null) { + ex = ex.getCause(); + } + + if (ex instanceof VMDisconnectedException) { + // mark it success to avoid reporting error on VSCode. + response.success = true; + sendResponse(response); + } else if (ex instanceof DebugException) { + sendResponse(AdapterUtils.setErrorResponse(response, + ErrorCode.parse(((DebugException) ex).getErrorCode()), ex.getMessage() != null ? ex.getMessage() : ex.toString())); } else { - logger.log(Level.SEVERE, "The request dispatcher should not return null response."); sendResponse(AdapterUtils.setErrorResponse(response, ErrorCode.UNKNOWN_FAILURE, - "The request dispatcher should not return null response.")); + ex.getMessage() != null ? ex.getMessage() : ex.toString())); } + return null; }); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InitializeRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InitializeRequestHandler.java index f1f64e1e7..b702bb824 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InitializeRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InitializeRequestHandler.java @@ -42,6 +42,7 @@ public CompletableFuture handle(Requests.Command command, Req context.setClientPathsAreUri(false); } } + context.setSupportsRunInTerminalRequest(initializeArguments.supportsRunInTerminalRequest); Types.Capabilities caps = new Types.Capabilities(); caps.supportsConfigurationDoneRequest = true; diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchRequestHandler.java index 0affc7636..af6f62e54 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchRequestHandler.java @@ -22,12 +22,19 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import com.google.gson.JsonObject; import com.microsoft.java.debug.core.Configuration; +import com.microsoft.java.debug.core.DebugException; +import com.microsoft.java.debug.core.DebugSession; import com.microsoft.java.debug.core.DebugUtility; import com.microsoft.java.debug.core.IDebugSession; import com.microsoft.java.debug.core.adapter.AdapterUtils; @@ -39,15 +46,25 @@ import com.microsoft.java.debug.core.adapter.IVirtualMachineManagerProvider; import com.microsoft.java.debug.core.adapter.ProcessConsole; import com.microsoft.java.debug.core.protocol.Events; +import com.microsoft.java.debug.core.protocol.JsonUtils; +import com.microsoft.java.debug.core.protocol.Messages.Request; import com.microsoft.java.debug.core.protocol.Messages.Response; import com.microsoft.java.debug.core.protocol.Requests.Arguments; +import com.microsoft.java.debug.core.protocol.Requests.CONSOLE; import com.microsoft.java.debug.core.protocol.Requests.Command; import com.microsoft.java.debug.core.protocol.Requests.LaunchArguments; +import com.microsoft.java.debug.core.protocol.Requests.RunInTerminalRequestArguments; +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.connect.VMStartException; public class LaunchRequestHandler implements IDebugRequestHandler { private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); + private static final long RUNINTERMINAL_TIMEOUT = 10 * 1000; + private static final int ACCEPT_TIMEOUT = 10 * 1000; + private static final String TERMINAL_TITLE = "Java Debug Console"; @Override public List getTargetCommands() { @@ -65,6 +82,8 @@ public CompletableFuture handle(Command command, Arguments arguments, context.setAttached(false); context.setSourcePaths(launchArguments.sourcePaths); + context.setVmStopOnEntry(launchArguments.stopOnEntry); + context.setMainClass(parseMainClassWithoutModuleName(launchArguments.mainClass)); if (StringUtils.isBlank(launchArguments.encoding)) { context.setDebuggeeEncoding(StandardCharsets.UTF_8); @@ -77,7 +96,6 @@ public CompletableFuture handle(Command command, Arguments arguments, context.setDebuggeeEncoding(Charset.forName(launchArguments.encoding)); } - if (StringUtils.isBlank(launchArguments.vmArgs)) { launchArguments.vmArgs = String.format("-Dfile.encoding=%s", context.getDebuggeeEncoding().name()); } else { @@ -85,9 +103,130 @@ public CompletableFuture handle(Command command, Arguments arguments, launchArguments.vmArgs = String.format("%s -Dfile.encoding=%s", launchArguments.vmArgs, context.getDebuggeeEncoding().name()); } + return launch(launchArguments, response, context).thenCompose(res -> { + if (res.success) { + ISourceLookUpProvider sourceProvider = context.getProvider(ISourceLookUpProvider.class); + Map options = new HashMap<>(); + options.put(Constants.DEBUGGEE_ENCODING, context.getDebuggeeEncoding()); + if (launchArguments.projectName != null) { + options.put(Constants.PROJECTNAME, launchArguments.projectName); + } + sourceProvider.initialize(context.getDebugSession(), options); + + // Send an InitializedEvent to indicate that the debugger is ready to accept configuration requests + // (e.g. SetBreakpointsRequest, SetExceptionBreakpointsRequest). + context.getProtocolServer().sendEvent(new Events.InitializedEvent()); + } + return CompletableFuture.completedFuture(res); + }); + } + + private CompletableFuture launch(LaunchArguments launchArguments, Response response, IDebugAdapterContext context) { + logger.info("Trying to launch Java Program with options:\n" + String.format("main-class: %s\n", launchArguments.mainClass) + + String.format("args: %s\n", launchArguments.args) + + String.format("module-path: %s\n", StringUtils.join(launchArguments.modulePaths, File.pathSeparator)) + + String.format("class-path: %s\n", StringUtils.join(launchArguments.classPaths, File.pathSeparator)) + + String.format("vmArgs: %s", launchArguments.vmArgs)); + + if (context.supportsRunInTerminalRequest() + && (launchArguments.console == CONSOLE.integratedTerminal || launchArguments.console == CONSOLE.externalTerminal)) { + return launchInTerminal(launchArguments, response, context); + } else { + return launchInternally(launchArguments, response, context); + } + } + + private CompletableFuture launchInTerminal(LaunchArguments launchArguments, Response response, IDebugAdapterContext context) { + CompletableFuture resultFuture = new CompletableFuture<>(); IVirtualMachineManagerProvider vmProvider = context.getProvider(IVirtualMachineManagerProvider.class); + final String launchInTerminalErrorFormat = "Failed to launch debuggee in terminal. Reason: %s"; + + try { + List connectors = vmProvider.getVirtualMachineManager().listeningConnectors(); + ListeningConnector listenConnector = connectors.get(0); + Map args = listenConnector.defaultArguments(); + ((Connector.IntegerArgument) args.get("timeout")).setValue(ACCEPT_TIMEOUT); + String address = listenConnector.startListening(args); + + String[] cmds = constructLaunchCommands(launchArguments, false, address); + RunInTerminalRequestArguments requestArgs = null; + if (launchArguments.console == CONSOLE.integratedTerminal) { + requestArgs = RunInTerminalRequestArguments.createIntegratedTerminal( + cmds, + launchArguments.cwd, + launchArguments.env, + TERMINAL_TITLE); + } else { + requestArgs = RunInTerminalRequestArguments.createExternalTerminal( + cmds, + launchArguments.cwd, + launchArguments.env, + TERMINAL_TITLE); + } + Request request = new Request(Command.RUNINTERMINAL.getName(), + (JsonObject) JsonUtils.toJsonTree(requestArgs, RunInTerminalRequestArguments.class)); + + // Notes: In windows (reference to https://support.microsoft.com/en-us/help/830473/command-prompt-cmd--exe-command-line-string-limitation), + // when launching the program in cmd.exe, if the command line length exceed the threshold value (8191 characters), + // it will be automatically truncated so that launching in terminal failed. Especially, for maven project, the class path contains + // the local .m2 repository path, it may exceed the limit. + context.getProtocolServer().sendRequest(request, RUNINTERMINAL_TIMEOUT) + .whenComplete((runResponse, ex) -> { + if (runResponse != null) { + if (runResponse.success) { + try { + VirtualMachine vm = listenConnector.accept(args); + context.setDebugSession(new DebugSession(vm)); + logger.info("Launching debuggee in terminal console succeeded."); + resultFuture.complete(response); + } catch (IOException | IllegalConnectorArgumentsException e) { + logger.log(Level.SEVERE, String.format(launchInTerminalErrorFormat, e.toString())); + resultFuture.completeExceptionally( + new DebugException( + String.format(launchInTerminalErrorFormat, e.toString()), + ErrorCode.LAUNCH_IN_TERMINAL_FAILURE.getId() + ) + ); + } + } else { + logger.log(Level.SEVERE, String.format(launchInTerminalErrorFormat, runResponse.message)); + resultFuture.completeExceptionally( + new DebugException( + String.format(launchInTerminalErrorFormat, runResponse.message), + ErrorCode.LAUNCH_IN_TERMINAL_FAILURE.getId() + ) + ); + } + } else { + if (ex instanceof CompletionException && ex.getCause() != null) { + ex = ex.getCause(); + } + String errorMessage = String.format(launchInTerminalErrorFormat, ex != null ? ex.toString() : "Null response"); + logger.log(Level.SEVERE, errorMessage); + resultFuture.completeExceptionally( + new DebugException( + String.format(launchInTerminalErrorFormat, errorMessage), + ErrorCode.LAUNCH_IN_TERMINAL_FAILURE.getId() + ) + ); + } + }); + } catch (IOException | IllegalConnectorArgumentsException e) { + logger.log(Level.SEVERE, String.format(launchInTerminalErrorFormat, e.toString())); + resultFuture.completeExceptionally( + new DebugException( + String.format(launchInTerminalErrorFormat, e.toString()), + ErrorCode.LAUNCH_IN_TERMINAL_FAILURE.getId() + ) + ); + } + + return resultFuture; + } + private CompletableFuture launchInternally(LaunchArguments launchArguments, Response response, IDebugAdapterContext context) { + CompletableFuture resultFuture = new CompletableFuture<>(); // Append environment to native environment. String[] envVars = null; @@ -114,14 +253,7 @@ public CompletableFuture handle(Command command, Arguments arguments, } try { - StringBuilder launchLogs = new StringBuilder(); - launchLogs.append("Trying to launch Java Program with options:\n"); - launchLogs.append(String.format("main-class: %s\n", launchArguments.mainClass)); - launchLogs.append(String.format("args: %s\n", launchArguments.args)); - launchLogs.append(String.format("module-path: %s\n", StringUtils.join(launchArguments.modulePaths, File.pathSeparator))); - launchLogs.append(String.format("class-path: %s\n", StringUtils.join(launchArguments.classPaths, File.pathSeparator))); - launchLogs.append(String.format("vmArgs: %s", launchArguments.vmArgs)); - logger.info(launchLogs.toString()); + IVirtualMachineManagerProvider vmProvider = context.getProvider(IVirtualMachineManagerProvider.class); IDebugSession debugSession = DebugUtility.launch( vmProvider.getVirtualMachineManager(), @@ -133,8 +265,6 @@ public CompletableFuture handle(Command command, Arguments arguments, launchArguments.cwd, envVars); context.setDebugSession(debugSession); - context.setVmStopOnEntry(launchArguments.stopOnEntry); - context.setMainClass(parseMainClassWithoutModuleName(launchArguments.mainClass)); logger.info("Launching debuggee VM succeeded."); @@ -149,23 +279,19 @@ public CompletableFuture handle(Command command, Arguments arguments, context.getProtocolServer().sendEvent(Events.OutputEvent.createStderrOutput(err)); }); debuggeeConsole.start(); - } catch (IOException | IllegalConnectorArgumentsException | VMStartException e) { - return AdapterUtils.createAsyncErrorResponse(response, ErrorCode.LAUNCH_FAILURE, - String.format("Failed to launch debuggee VM. Reason: %s", e.toString())); - } - ISourceLookUpProvider sourceProvider = context.getProvider(ISourceLookUpProvider.class); - Map options = new HashMap<>(); - options.put(Constants.DEBUGGEE_ENCODING, context.getDebuggeeEncoding()); - if (launchArguments.projectName != null) { - options.put(Constants.PROJECTNAME, launchArguments.projectName); + resultFuture.complete(response); + } catch (IOException | IllegalConnectorArgumentsException | VMStartException e) { + logger.log(Level.SEVERE, String.format("Failed to launch debuggee VM. Reason: %s", e.toString())); + resultFuture.completeExceptionally( + new DebugException( + String.format("Failed to launch debuggee VM. Reason: %s", e.toString()), + ErrorCode.LAUNCH_FAILURE.getId() + ) + ); } - sourceProvider.initialize(context.getDebugSession(), options); - // Send an InitializedEvent to indicate that the debugger is ready to accept configuration requests - // (e.g. SetBreakpointsRequest, SetExceptionBreakpointsRequest). - context.getProtocolServer().sendEvent(new Events.InitializedEvent()); - return CompletableFuture.completedFuture(response); + return resultFuture; } private static String parseMainClassWithoutModuleName(String mainClass) { @@ -173,4 +299,52 @@ private static String parseMainClassWithoutModuleName(String mainClass) { return mainClass.substring(index + 1); } + private String[] constructLaunchCommands(LaunchArguments launchArguments, boolean serverMode, String address) { + String slash = System.getProperty("file.separator"); + + List launchCmds = new ArrayList<>(); + launchCmds.add(System.getProperty("java.home") + slash + "bin" + slash + "java"); + launchCmds.add(String.format("-agentlib:jdwp=transport=dt_socket,server=%s,suspend=y,address=%s", (serverMode ? "y" : "n"), address)); + if (StringUtils.isNotBlank(launchArguments.vmArgs)) { + launchCmds.addAll(parseArguments(launchArguments.vmArgs)); + } + if (ArrayUtils.isNotEmpty(launchArguments.modulePaths)) { + launchCmds.add("--module-path"); + launchCmds.add(String.join(File.pathSeparator, launchArguments.modulePaths)); + } + if (ArrayUtils.isNotEmpty(launchArguments.classPaths)) { + launchCmds.add("-cp"); + launchCmds.add(String.join(File.pathSeparator, launchArguments.classPaths)); + } + // For java 9 project, should specify "-m $MainClass". + String[] mainClasses = launchArguments.mainClass.split("/"); + if (ArrayUtils.isNotEmpty(launchArguments.modulePaths) || mainClasses.length == 2) { + launchCmds.add("-m"); + } + launchCmds.add(launchArguments.mainClass); + if (StringUtils.isNotBlank(launchArguments.args)) { + launchCmds.addAll(parseArguments(launchArguments.args)); + } + return launchCmds.toArray(new String[0]); + } + + /** + * Parses the given command line into separate arguments that can be passed + * to Runtime.getRuntime().exec(cmdArray). + * + * @param args command line as a single string. + * @return the arguments array. + */ + private static List parseArguments(String cmdStr) { + List list = new ArrayList(); + // The legal arguments are + // 1. token starting with something other than quote " and followed by zero or more non-space characters + // 2. a quote " followed by whatever, until another quote " + Matcher m = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(cmdStr); + while (m.find()) { + String arg = m.group(1).replaceAll("^\"|\"$", ""); // Remove surrounding quotes. + list.add(arg); + } + return list; + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java index 733d8c018..6f194965e 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java @@ -38,32 +38,119 @@ public static class InitializeArguments extends Arguments { public boolean supportsRunInTerminalRequest; } - public static class LaunchArguments extends Arguments { + public static class LaunchBaseArguments extends Arguments { public String type; public String name; public String request; public String projectName; + public String[] sourcePaths = new String[0]; + } + + public static enum CONSOLE { + internalConsole, + integratedTerminal, + externalTerminal; + } + + public static class LaunchArguments extends LaunchBaseArguments { public String mainClass; public String args = ""; public String vmArgs = ""; public String encoding = ""; public String[] classPaths = new String[0]; public String[] modulePaths = new String[0]; - public String[] sourcePaths = new String[0]; public String cwd; public Map env; public boolean stopOnEntry; + public CONSOLE console = CONSOLE.internalConsole; } - public static class AttachArguments extends Arguments { - public String type; - public String name; - public String request; + public static class AttachArguments extends LaunchBaseArguments { public String hostName; public int port; public int timeout = 30000; // Default to 30s. - public String[] sourcePaths = new String[0]; - public String projectName; + } + + public static class RunInTerminalRequestArguments extends Arguments { + public String kind; // Supported kind should be "integrated" or "external". + public String title; + public String cwd; // required. + public String[] args; // required. + public Map env; + + private RunInTerminalRequestArguments() { + // do nothing. + } + + /** + * Create a RunInTerminalRequestArguments instance. + * @param cmds + * List of command arguments. The first arguments is the command to run. + * @param cwd + * Working directory of the command. + * @return the request arguments instance. + */ + public static RunInTerminalRequestArguments createIntegratedTerminal(String[] cmds, String cwd) { + RunInTerminalRequestArguments requestArgs = new RunInTerminalRequestArguments(); + requestArgs.args = cmds; + requestArgs.cwd = cwd; + requestArgs.kind = "integrated"; + return requestArgs; + } + + /** + * Create a RunInTerminalRequestArguments instance. + * @param cmds + * List of command arguments. The first arguments is the command to run. + * @param cwd + * Working directory of the command. + * @param env + * Environment key-value pairs that are added to the default environment. + * @param title + * Optional title of the terminal. + * @return the request arguments instance. + */ + public static RunInTerminalRequestArguments createIntegratedTerminal(String[] cmds, String cwd, Map env, String title) { + RunInTerminalRequestArguments requestArgs = createIntegratedTerminal(cmds, cwd); + requestArgs.env = env; + requestArgs.title = title; + return requestArgs; + } + + /** + * Create a RunInTerminalRequestArguments instance. + * @param cmds + * List of command arguments. The first arguments is the command to run. + * @param cwd + * Working directory of the command. + * @return the request arguments instance. + */ + public static RunInTerminalRequestArguments createExternalTerminal(String[] cmds, String cwd) { + RunInTerminalRequestArguments requestArgs = new RunInTerminalRequestArguments(); + requestArgs.args = cmds; + requestArgs.cwd = cwd; + requestArgs.kind = "external"; + return requestArgs; + } + + /** + * Create a RunInTerminalRequestArguments instance. + * @param cmds + * List of command arguments. The first arguments is the command to run. + * @param cwd + * Working directory of the command. + * @param env + * Environment key-value pairs that are added to the default environment. + * @param title + * Optional title of the terminal. + * @return the request arguments instance. + */ + public static RunInTerminalRequestArguments createExternalTerminal(String[] cmds, String cwd, Map env, String title) { + RunInTerminalRequestArguments requestArgs = createExternalTerminal(cmds, cwd); + requestArgs.env = env; + requestArgs.title = title; + return requestArgs; + } } public static class RestartArguments extends Arguments { @@ -178,6 +265,7 @@ public static enum Command { SETEXCEPTIONBREAKPOINTS("setExceptionBreakpoints", SetExceptionBreakpointsArguments.class), SETFUNCTIONBREAKPOINTS("setFunctionBreakpoints", SetFunctionBreakpointsArguments.class), EVALUATE("evaluate", EvaluateArguments.class), + RUNINTERMINAL("runInTerminal", RunInTerminalRequestArguments.class), UNSUPPORTED("", Arguments.class); private String command; @@ -188,6 +276,11 @@ public static enum Command { this.argumentType = argumentType; } + public String getName() { + return this.command; + } + + @Override public String toString() { return this.command; } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Responses.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Responses.java index f4d8bd723..cc0f64541 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Responses.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Responses.java @@ -34,6 +34,14 @@ public InitializeResponseBody(Types.Capabilities capabilities) { } } + public static class RunInTerminalResponseBody extends ResponseBody { + public int processId; + + public RunInTerminalResponseBody(int processId) { + this.processId = processId; + } + } + public static class ErrorResponseBody extends ResponseBody { public Types.Message error; From c0a54e4026090035de0289db79d0ddfd80e75632 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Wed, 29 Nov 2017 19:59:11 +0800 Subject: [PATCH 2/2] Use thenCompose to handle null response case --- .../debug/core/adapter/ProtocolServer.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProtocolServer.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProtocolServer.java index fc01aa6ad..423bfe996 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProtocolServer.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProtocolServer.java @@ -77,16 +77,17 @@ public CompletableFuture sendRequest(Messages.Request request @Override protected void dispatchRequest(Messages.Request request) { usageDataSession.recordRequest(request); - this.debugAdapter.dispatchRequest(request).thenAccept((response) -> { + this.debugAdapter.dispatchRequest(request).thenCompose((response) -> { + CompletableFuture future = new CompletableFuture<>(); if (response != null) { sendResponse(response); + future.complete(null); } else { logger.log(Level.SEVERE, "The request dispatcher should not return null response."); - response = new Messages.Response(request.seq, request.command); - sendResponse(AdapterUtils.setErrorResponse(response, - ErrorCode.UNKNOWN_FAILURE, - "The request dispatcher should not return null response.")); + future.completeExceptionally(new DebugException("The request dispatcher should not return null response.", + ErrorCode.UNKNOWN_FAILURE.getId())); } + return future; }).exceptionally((ex) -> { Messages.Response response = new Messages.Response(request.seq, request.command); if (ex instanceof CompletionException && ex.getCause() != null) { @@ -99,12 +100,12 @@ protected void dispatchRequest(Messages.Request request) { sendResponse(response); } else if (ex instanceof DebugException) { sendResponse(AdapterUtils.setErrorResponse(response, - ErrorCode.parse(((DebugException) ex).getErrorCode()), - ex.getMessage() != null ? ex.getMessage() : ex.toString())); + ErrorCode.parse(((DebugException) ex).getErrorCode()), + ex.getMessage() != null ? ex.getMessage() : ex.toString())); } else { sendResponse(AdapterUtils.setErrorResponse(response, - ErrorCode.UNKNOWN_FAILURE, - ex.getMessage() != null ? ex.getMessage() : ex.toString())); + ErrorCode.UNKNOWN_FAILURE, + ex.getMessage() != null ? ex.getMessage() : ex.toString())); } return null; });