From 03363cab96517bce2a22d2a40ce2ee8e5147bf8c Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Wed, 13 Mar 2019 16:50:13 +0800 Subject: [PATCH 1/6] Jumping to source when clicking stack trace link in debug console Signed-off-by: Jinbo Wang --- .../core/adapter/DebugAdapterContext.java | 11 +++ .../core/adapter/IDebugAdapterContext.java | 4 + .../debug/core/adapter/ProcessConsole.java | 85 ++++++++++++++---- .../ConfigurationDoneRequestHandler.java | 12 ++- .../core/adapter/handler/ILaunchDelegate.java | 6 +- .../adapter/handler/LaunchRequestHandler.java | 86 ++++++++++++++++++- .../handler/LaunchWithDebuggingDelegate.java | 36 +------- .../LaunchWithoutDebuggingDelegate.java | 40 ++------- .../handler/StackTraceRequestHandler.java | 13 ++- .../core/protocol/AbstractProtocolServer.java | 2 + .../java/debug/core/protocol/Events.java | 32 +++++++ 11 files changed, 235 insertions(+), 92 deletions(-) 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 646ce01c5..1a0a46997 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 @@ -46,6 +46,7 @@ public class DebugAdapterContext implements IDebugAdapterContext { private StepFilters stepFilters; private Path classpathJar = null; private Path argsfile = null; + private ProcessConsole debuggeeProcessConsole; private IdCollection sourceReferences = new IdCollection<>(); private RecyclableObjectPool recyclableIdPool = new RecyclableObjectPool<>(); @@ -291,4 +292,14 @@ public Path getArgsfile() { public IExceptionManager getExceptionManager() { return this.exceptionManager; } + + @Override + public ProcessConsole getDebuggeeProcessConsole() { + return this.debuggeeProcessConsole; + } + + @Override + public void setDebuggeeProcessConsole(ProcessConsole processConsole) { + this.debuggeeProcessConsole = processConsole; + } } 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 27837808b..868104efe 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 @@ -124,4 +124,8 @@ public interface IDebugAdapterContext { Path getArgsfile(); IExceptionManager getExceptionManager(); + + ProcessConsole getDebuggeeProcessConsole(); + + void setDebuggeeProcessConsole(ProcessConsole processConsole); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java index de066bcaa..694bc9b98 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java @@ -17,19 +17,27 @@ import java.io.InputStreamReader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import io.reactivex.disposables.Disposable; import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; public class ProcessConsole { private Process process; private String name; private Charset encoding; - private PublishSubject stdoutSubject = PublishSubject.create(); - private PublishSubject stderrSubject = PublishSubject.create(); + private PublishSubject outputSubject = PublishSubject.create(); private Thread stdoutThread = null; private Thread stderrThread = null; + private AtomicInteger exit = new AtomicInteger(0); + private Disposable consumer = null; + /** + * constructor. + */ public ProcessConsole(Process process) { this(process, "Process", StandardCharsets.UTF_8); } @@ -55,7 +63,7 @@ public ProcessConsole(Process process, String name, Charset encoding) { public void start() { this.stdoutThread = new Thread(this.name + " Stdout Handler") { public void run() { - monitor(process.getInputStream(), stdoutSubject); + monitor(process.getInputStream(), MessageType.STDOUT); } }; stdoutThread.setDaemon(true); @@ -63,7 +71,7 @@ public void run() { this.stderrThread = new Thread(this.name + " Stderr Handler") { public void run() { - monitor(process.getErrorStream(), stderrSubject); + monitor(process.getErrorStream(), MessageType.STDERR); } }; stderrThread.setDaemon(true); @@ -85,35 +93,76 @@ public void stop() { } } - public void onStdout(Consumer callback) { - stdoutSubject.subscribe(callback); - } - - public void onStderr(Consumer callback) { - stderrSubject.subscribe(callback); + /** + * Register a callback to consume the stdout/stderr message from the process. + */ + public void onData(Consumer callback) { + this.consumer = outputSubject.observeOn(Schedulers.newThread()).subscribe(callback); } - private void monitor(InputStream input, PublishSubject subject) { + private void monitor(InputStream input, MessageType type) { BufferedReader reader = new BufferedReader(new InputStreamReader(input, encoding)); final int BUFFERSIZE = 4096; char[] buffer = new char[BUFFERSIZE]; while (true) { try { if (Thread.interrupted()) { - subject.onComplete(); - return; + break; } int read = reader.read(buffer, 0, BUFFERSIZE); if (read == -1) { - subject.onComplete(); - return; + break; } - subject.onNext(new String(buffer, 0, read)); + outputSubject.onNext(new Message(new String(buffer, 0, read), type)); } catch (IOException e) { - subject.onError(e); - return; + break; } } + + if (exit.addAndGet(1) == 2) { + outputSubject.onComplete(); + } + } + + /** + * Wait for the process's stdout/stderr message is fully consumed by the debug adapter. + */ + public void waitFor() { + // Give the debug adapter additional 3 seconds to handle the process io data. + waitFor(3); + } + + /** + * Wait for the process's stdout/stderr message is fully consumed by the debug adapter. + * @param timeoutSeconds - The maximum waiting time + */ + public void waitFor(int timeoutSeconds) { + int retry = timeoutSeconds * 2; + while (retry-- > 0) { + if (this.consumer != null && this.consumer.isDisposed()) { + break; + } + + try { + TimeUnit.MILLISECONDS.sleep(500); + } catch (InterruptedException e) { + // do nothing. + } + } + } + + public static class Message { + public String output; + public MessageType type; + + public Message(String output, MessageType type) { + this.output = output; + this.type = type; + } + } + + public static enum MessageType { + STDOUT, STDERR } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java index 977690d28..de201dff0 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java @@ -27,6 +27,7 @@ import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; import com.microsoft.java.debug.core.adapter.IEvaluationProvider; +import com.microsoft.java.debug.core.adapter.ProcessConsole; import com.microsoft.java.debug.core.protocol.Events; import com.microsoft.java.debug.core.protocol.Messages.Response; import com.microsoft.java.debug.core.protocol.Requests.Arguments; @@ -80,13 +81,15 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, context.getProtocolServer().sendEvent(new Events.ExitedEvent(0)); } else if (event instanceof VMDisconnectEvent) { context.setVmTerminated(); - context.getProtocolServer().sendEvent(new Events.TerminatedEvent()); // Terminate eventHub thread. try { debugSession.getEventHub().close(); } catch (Exception e) { // do nothing. } + + waitForDebuggeeConsole(context); + context.getProtocolServer().sendEvent(new Events.TerminatedEvent()); } else if (event instanceof ThreadStartEvent) { ThreadReference startThread = ((ThreadStartEvent) event).thread(); Events.ThreadEvent threadEvent = new Events.ThreadEvent("started", startThread.uniqueID()); @@ -119,4 +122,11 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, UsageDataSession.recordEvent(event); } } + + private void waitForDebuggeeConsole(IDebugAdapterContext context) { + ProcessConsole console = context.getDebuggeeProcessConsole(); + if (console != null) { + console.waitFor(); + } + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ILaunchDelegate.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ILaunchDelegate.java index edb865a78..f9e9fa119 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ILaunchDelegate.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ILaunchDelegate.java @@ -11,11 +11,14 @@ package com.microsoft.java.debug.core.adapter.handler; +import java.io.IOException; import java.util.concurrent.CompletableFuture; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; import com.microsoft.java.debug.core.protocol.Messages.Response; import com.microsoft.java.debug.core.protocol.Requests.LaunchArguments; +import com.sun.jdi.connect.IllegalConnectorArgumentsException; +import com.sun.jdi.connect.VMStartException; public interface ILaunchDelegate { void postLaunch(LaunchArguments launchArguments, IDebugAdapterContext context); @@ -24,6 +27,7 @@ public interface ILaunchDelegate { CompletableFuture launchInTerminal(LaunchArguments launchArguments, Response response, IDebugAdapterContext context); - CompletableFuture launchInternally(LaunchArguments launchArguments, Response response, IDebugAdapterContext context); + Process launchInternalDebuggeeProcess(LaunchArguments launchArguments, IDebugAdapterContext context) + throws IOException, IllegalConnectorArgumentsException, VMStartException; } 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 cdfca7fbf..8eb5b71d7 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 @@ -26,11 +26,14 @@ import java.util.concurrent.CompletableFuture; 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.microsoft.java.debug.core.Configuration; +import com.microsoft.java.debug.core.DebugException; import com.microsoft.java.debug.core.DebugSettings; import com.microsoft.java.debug.core.DebugUtility; import com.microsoft.java.debug.core.adapter.AdapterUtils; @@ -38,12 +41,18 @@ import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; import com.microsoft.java.debug.core.adapter.LaunchMode; +import com.microsoft.java.debug.core.adapter.ProcessConsole; +import com.microsoft.java.debug.core.adapter.ProcessConsole.MessageType; +import com.microsoft.java.debug.core.protocol.Events.OutputEvent; 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.ShortenApproach; +import com.microsoft.java.debug.core.protocol.Types; +import com.sun.jdi.connect.IllegalConnectorArgumentsException; +import com.sun.jdi.connect.VMStartException; public class LaunchRequestHandler implements IDebugRequestHandler { protected static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); @@ -179,10 +188,85 @@ protected CompletableFuture launch(LaunchArguments launchArguments, Re && (launchArguments.console == CONSOLE.integratedTerminal || launchArguments.console == CONSOLE.externalTerminal)) { return activeLaunchHandler.launchInTerminal(launchArguments, response, context); } else { - return activeLaunchHandler.launchInternally(launchArguments, response, context); + return launchInternally(launchArguments, response, context); } } + protected CompletableFuture launchInternally(LaunchArguments launchArguments, Response response, IDebugAdapterContext context) { + CompletableFuture resultFuture = new CompletableFuture<>(); + + try { + Process debuggeeProcess = activeLaunchHandler.launchInternalDebuggeeProcess(launchArguments, context); + context.setDebuggeeProcess(debuggeeProcess); + String[] lastIncompleteLine = new String[] { + null, + null + }; + Pattern stacktracePattern = Pattern.compile("\\s+at\\s+(([\\w$]+\\.)*[\\w$]+)\\(([\\w-$]+\\.java:\\d+)\\)"); + ProcessConsole debuggeeConsole = new ProcessConsole(debuggeeProcess, "Debuggee", context.getDebuggeeEncoding()); + context.setDebuggeeProcessConsole(debuggeeConsole); + debuggeeConsole.onData((message) -> { + // When DA receives a new OutputEvent, it just shows that on Debug Console and doesn't affect the DA's dispatching workflow. + // That means the debugger can send OutputEvent to DA at any time. + OutputEvent.Category eventCategory = message.type == MessageType.STDOUT ? OutputEvent.Category.stdout : OutputEvent.Category.stderr; + int type = message.type.ordinal(); + String[] lines = message.output.split("\n"); + boolean endWithLF = message.output.endsWith("\n"); + String unsent = ""; + for (int i = 0; i < lines.length; i++) { + String toSend = (i < lines.length - 1 || endWithLF) ? lines[i] + "\n" : lines[i]; + String revisedLine = lines[i]; + if (lastIncompleteLine[type] != null) { + revisedLine = lastIncompleteLine[type] + revisedLine; + lastIncompleteLine[type] = null; + } + + Matcher matcher = stacktracePattern.matcher(revisedLine); + if (matcher.find()) { + if (StringUtils.isNotEmpty(unsent)) { + context.getProtocolServer().sendEvent(new OutputEvent(eventCategory, unsent)); + unsent = ""; + } + + String methodField = matcher.group(1); + String locationField = matcher.group(matcher.groupCount()); + + String fullyQualifiedName = methodField.substring(0, methodField.lastIndexOf(".")); + String packageName = fullyQualifiedName.substring(0, fullyQualifiedName.lastIndexOf(".")); + String[] locations = locationField.split(":"); + String sourceName = locations[0]; + int lineNumber = Integer.parseInt(locations[1]); + String sourcePath = StringUtils.isBlank(packageName) ? sourceName + : packageName.replace('.', File.separatorChar) + File.separatorChar + sourceName; + Types.Source source = StackTraceRequestHandler.convertDebuggerSourceToClient(fullyQualifiedName, sourceName, sourcePath, context); + context.getProtocolServer().sendEvent(new OutputEvent(eventCategory, toSend, source, lineNumber)); + } else { + unsent += toSend; + } + + if (i == lines.length - 1 && StringUtils.isNotEmpty(unsent)) { + context.getProtocolServer().sendEvent(new OutputEvent(eventCategory, unsent)); + if (!endWithLF) { + lastIncompleteLine[type] = toSend; + } + } + } + }); + + debuggeeConsole.start(); + resultFuture.complete(response); + } catch (IOException | IllegalConnectorArgumentsException | VMStartException e) { + resultFuture.completeExceptionally( + new DebugException( + String.format("Failed to launch debuggee VM. Reason: %s", e.toString()), + ErrorCode.LAUNCH_FAILURE.getId() + ) + ); + } + + return resultFuture; + } + protected static String[] constructEnvironmentVariables(LaunchArguments launchArguments) { String[] envVars = null; if (launchArguments.env != null && !launchArguments.env.isEmpty()) { diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithDebuggingDelegate.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithDebuggingDelegate.java index 807e579ff..7e18c7489 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithDebuggingDelegate.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithDebuggingDelegate.java @@ -37,7 +37,6 @@ import com.microsoft.java.debug.core.adapter.IHotCodeReplaceProvider; import com.microsoft.java.debug.core.adapter.ISourceLookUpProvider; 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; @@ -169,7 +168,8 @@ public CompletableFuture launchInTerminal(LaunchArguments launchArgume return resultFuture; } - private Process launchInternalDebuggeeProcess(LaunchArguments launchArguments, IDebugAdapterContext context) + @Override + public Process launchInternalDebuggeeProcess(LaunchArguments launchArguments, IDebugAdapterContext context) throws IOException, IllegalConnectorArgumentsException, VMStartException { IVirtualMachineManagerProvider vmProvider = context.getProvider(IVirtualMachineManagerProvider.class); @@ -188,38 +188,6 @@ private Process launchInternalDebuggeeProcess(LaunchArguments launchArguments, I return debugSession.process(); } - @Override - public CompletableFuture launchInternally(LaunchArguments launchArguments, Response response, IDebugAdapterContext context) { - CompletableFuture resultFuture = new CompletableFuture<>(); - - try { - Process debugeeProcess = launchInternalDebuggeeProcess(launchArguments, context); - - ProcessConsole debuggeeConsole = new ProcessConsole(debugeeProcess, "Debuggee", context.getDebuggeeEncoding()); - debuggeeConsole.onStdout((output) -> { - // When DA receives a new OutputEvent, it just shows that on Debug Console and doesn't affect the DA's dispatching workflow. - // That means the debugger can send OutputEvent to DA at any time. - context.getProtocolServer().sendEvent(Events.OutputEvent.createStdoutOutput(output)); - }); - - debuggeeConsole.onStderr((err) -> { - context.getProtocolServer().sendEvent(Events.OutputEvent.createStderrOutput(err)); - }); - debuggeeConsole.start(); - - resultFuture.complete(response); - } catch (IOException | IllegalConnectorArgumentsException | VMStartException e) { - resultFuture.completeExceptionally( - new DebugException( - String.format("Failed to launch debuggee VM. Reason: %s", e.toString()), - ErrorCode.LAUNCH_FAILURE.getId() - ) - ); - } - - return resultFuture; - } - @Override public void postLaunch(LaunchArguments launchArguments, IDebugAdapterContext context) { Map options = new HashMap<>(); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java index 7d5ab8c18..436d22928 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java @@ -41,7 +41,8 @@ public class LaunchWithoutDebuggingDelegate implements ILaunchDelegate { protected static final String TERMINAL_TITLE = "Java Process Console"; protected static final long RUNINTERMINAL_TIMEOUT = 10 * 1000; - private Process launchInternalDebuggeeProcess(LaunchArguments launchArguments, IDebugAdapterContext context) + @Override + public Process launchInternalDebuggeeProcess(LaunchArguments launchArguments, IDebugAdapterContext context) throws IOException, IllegalConnectorArgumentsException, VMStartException { String[] cmds = LaunchRequestHandler.constructLaunchCommands(launchArguments, false, null); File workingDir = null; @@ -58,6 +59,10 @@ public void run() { logger.warning(String.format("Current thread is interrupted. Reason: %s", ignore.toString())); debuggeeProcess.destroy(); } finally { + ProcessConsole console = context.getDebuggeeProcessConsole(); + if (console != null) { + console.waitFor(); + } context.getProtocolServer().sendEvent(new Events.TerminatedEvent()); } } @@ -66,39 +71,6 @@ public void run() { return debuggeeProcess; } - @Override - public CompletableFuture launchInternally(LaunchArguments launchArguments, Response response, IDebugAdapterContext context) { - CompletableFuture resultFuture = new CompletableFuture<>(); - - try { - Process debuggeeProcess = launchInternalDebuggeeProcess(launchArguments, context); - context.setDebuggeeProcess(debuggeeProcess); - - ProcessConsole debuggeeConsole = new ProcessConsole(debuggeeProcess, "Debuggee", context.getDebuggeeEncoding()); - debuggeeConsole.onStdout((output) -> { - // When DA receives a new OutputEvent, it just shows that on Debug Console and doesn't affect the DA's dispatching workflow. - // That means the debugger can send OutputEvent to DA at any time. - context.getProtocolServer().sendEvent(Events.OutputEvent.createStdoutOutput(output)); - }); - - debuggeeConsole.onStderr((err) -> { - context.getProtocolServer().sendEvent(Events.OutputEvent.createStderrOutput(err)); - }); - debuggeeConsole.start(); - - resultFuture.complete(response); - } catch (IOException | IllegalConnectorArgumentsException | VMStartException e) { - resultFuture.completeExceptionally( - new DebugException( - String.format("Failed to launch debuggee VM. Reason: %s", e.toString()), - ErrorCode.LAUNCH_FAILURE.getId() - ) - ); - } - - return resultFuture; - } - @Override public void postLaunch(LaunchArguments launchArguments, IDebugAdapterContext context) { // For NO_DEBUG launch mode, the debugger does not respond to requests like diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java index 1947eecde..c548c69a9 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java @@ -123,10 +123,17 @@ private Types.Source convertDebuggerSourceToClient(Location location, IDebugAdap relativeSourcePath = enclosingType.replace('.', File.separatorChar) + ".java"; } - final String finalRelativeSourcePath = relativeSourcePath; + return convertDebuggerSourceToClient(fullyQualifiedName, sourceName, relativeSourcePath, context); + } + + /** + * Find the source mapping for the specified source file name. + */ + public static Types.Source convertDebuggerSourceToClient(String fullyQualifiedName, String sourceName, String sourcePath, IDebugAdapterContext context) + throws URISyntaxException { // use a lru cache for better performance String uri = context.getSourceLookupCache().computeIfAbsent(fullyQualifiedName, key -> { - String fromProvider = context.getProvider(ISourceLookUpProvider.class).getSourceFileURI(key, finalRelativeSourcePath); + String fromProvider = context.getProvider(ISourceLookUpProvider.class).getSourceFileURI(key, sourcePath); // avoid return null which will cause the compute function executed again return StringUtils.isBlank(fromProvider) ? "" : fromProvider; }); @@ -146,7 +153,7 @@ private Types.Source convertDebuggerSourceToClient(Location location, IDebugAdap } } else { // If the source lookup engine cannot find the source file, then lookup it in the source directories specified by user. - String absoluteSourcepath = AdapterUtils.sourceLookup(context.getSourcePaths(), relativeSourcePath); + String absoluteSourcepath = AdapterUtils.sourceLookup(context.getSourcePaths(), sourcePath); if (absoluteSourcepath != null) { return new Types.Source(sourceName, absoluteSourcepath, 0); } else { diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/AbstractProtocolServer.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/AbstractProtocolServer.java index 1cbe7d52d..9bec3c228 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/AbstractProtocolServer.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/AbstractProtocolServer.java @@ -100,6 +100,8 @@ public void run() { } catch (IOException e) { logger.log(Level.SEVERE, String.format("Read data from io exception: %s", e.toString()), e); } + + requestSubject.onComplete(); } /** diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Events.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Events.java index 0dea97b6f..e58a7f7fe 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Events.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Events.java @@ -11,6 +11,8 @@ package com.microsoft.java.debug.core.protocol; +import com.microsoft.java.debug.core.protocol.Types.Source; + /** * The event types defined by VSCode Debug Protocol. */ @@ -136,6 +138,11 @@ public enum Category { public Category category; public String output; + public int variablesReference; + public Source source; + public int line; + public int column; + public Object data; /** * Constructor. @@ -146,6 +153,17 @@ public OutputEvent(Category category, String output) { this.output = output; } + /** + * Constructor. + */ + public OutputEvent(Category category, String output, Source source, int line) { + super("output"); + this.category = category; + this.output = output; + this.source = source; + this.line = line; + } + public static OutputEvent createConsoleOutput(String output) { return new OutputEvent(Category.console, output); } @@ -154,10 +172,24 @@ public static OutputEvent createStdoutOutput(String output) { return new OutputEvent(Category.stdout, output); } + /** + * Constructor an stdout output event with source info. + */ + public static OutputEvent createStdoutOutput(String output, Source source, int line) { + return new OutputEvent(Category.stdout, output, source, line); + } + public static OutputEvent createStderrOutput(String output) { return new OutputEvent(Category.stderr, output); } + /** + * Constructor an stderr output event with source info. + */ + public static OutputEvent createStderrOutput(String output, Source source, int line) { + return new OutputEvent(Category.stderr, output, source, line); + } + public static OutputEvent createTelemetryOutput(String output) { return new OutputEvent(Category.telemetry, output); } From 6efda76f27de255d743077dbf9a94030f5d7d6e4 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Mon, 18 Mar 2019 11:03:10 +0800 Subject: [PATCH 2/6] Address the review comments Signed-off-by: Jinbo Wang --- .../core/adapter/DebugAdapterContext.java | 11 -- .../core/adapter/IDebugAdapterContext.java | 4 - .../core/adapter/JavaStackTraceConsole.java | 105 ++++++++++++++++++ .../debug/core/adapter/ProcessConsole.java | 84 +++----------- .../ConfigurationDoneRequestHandler.java | 12 +- .../core/adapter/handler/ILaunchDelegate.java | 2 +- .../adapter/handler/LaunchRequestHandler.java | 98 +++------------- .../handler/LaunchWithDebuggingDelegate.java | 2 +- .../LaunchWithoutDebuggingDelegate.java | 7 +- .../handler/StackTraceRequestHandler.java | 8 +- 10 files changed, 149 insertions(+), 184 deletions(-) create mode 100644 com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/JavaStackTraceConsole.java 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 1a0a46997..646ce01c5 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 @@ -46,7 +46,6 @@ public class DebugAdapterContext implements IDebugAdapterContext { private StepFilters stepFilters; private Path classpathJar = null; private Path argsfile = null; - private ProcessConsole debuggeeProcessConsole; private IdCollection sourceReferences = new IdCollection<>(); private RecyclableObjectPool recyclableIdPool = new RecyclableObjectPool<>(); @@ -292,14 +291,4 @@ public Path getArgsfile() { public IExceptionManager getExceptionManager() { return this.exceptionManager; } - - @Override - public ProcessConsole getDebuggeeProcessConsole() { - return this.debuggeeProcessConsole; - } - - @Override - public void setDebuggeeProcessConsole(ProcessConsole processConsole) { - this.debuggeeProcessConsole = processConsole; - } } 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 868104efe..27837808b 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 @@ -124,8 +124,4 @@ public interface IDebugAdapterContext { Path getArgsfile(); IExceptionManager getExceptionManager(); - - ProcessConsole getDebuggeeProcessConsole(); - - void setDebuggeeProcessConsole(ProcessConsole processConsole); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/JavaStackTraceConsole.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/JavaStackTraceConsole.java new file mode 100644 index 000000000..54e4d1e29 --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/JavaStackTraceConsole.java @@ -0,0 +1,105 @@ +/******************************************************************************* +* Copyright (c) 2017 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package com.microsoft.java.debug.core.adapter; + +import java.io.File; +import java.net.URISyntaxException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; + +import com.microsoft.java.debug.core.adapter.handler.StackTraceRequestHandler; +import com.microsoft.java.debug.core.protocol.Events.OutputEvent; +import com.microsoft.java.debug.core.protocol.Types; + +public class JavaStackTraceConsole { + private static final Pattern stacktracePattern = Pattern.compile("\\s+at\\s+(([\\w$]+\\.)*[\\w$]+)\\(([\\w-$]+\\.java:\\d+)\\)"); + private ProcessConsole console; + private IDebugAdapterContext context; + + public JavaStackTraceConsole(ProcessConsole console, IDebugAdapterContext context) { + this.console = console; + this.context = context; + } + + /** + * Start listening on the process console. + */ + public void start() { + String[] lastIncompleteLines = new String[] { + null, + null + }; + this.console.onStdout((message) -> { + lastIncompleteLines[0] = process(message, lastIncompleteLines[0], OutputEvent.Category.stdout); + }); + + this.console.onStderr((message) -> { + lastIncompleteLines[1] = process(message, lastIncompleteLines[1], OutputEvent.Category.stderr); + }); + + this.console.start(); + } + + private String process(String message, String lastIncompleteLine, OutputEvent.Category category) { + String[] lines = message.split("\n"); + boolean endWithLF = message.endsWith("\n"); + String unsent = ""; + for (int i = 0; i < lines.length; i++) { + String toSend = (i < lines.length - 1 || endWithLF) ? lines[i] + "\n" : lines[i]; + String revisedLine = lines[i]; + if (lastIncompleteLine != null) { + revisedLine = lastIncompleteLine + revisedLine; + lastIncompleteLine = null; + } + + Matcher matcher = stacktracePattern.matcher(revisedLine); + if (matcher.find()) { + if (StringUtils.isNotEmpty(unsent)) { + context.getProtocolServer().sendEvent(new OutputEvent(category, unsent)); + unsent = ""; + } + + String methodField = matcher.group(1); + String locationField = matcher.group(matcher.groupCount()); + + String fullyQualifiedName = methodField.substring(0, methodField.lastIndexOf(".")); + String packageName = fullyQualifiedName.substring(0, fullyQualifiedName.lastIndexOf(".")); + String[] locations = locationField.split(":"); + String sourceName = locations[0]; + int lineNumber = Integer.parseInt(locations[1]); + String sourcePath = StringUtils.isBlank(packageName) ? sourceName + : packageName.replace('.', File.separatorChar) + File.separatorChar + sourceName; + Types.Source source = null; + try { + source = StackTraceRequestHandler.convertDebuggerSourceToClient(fullyQualifiedName, sourceName, sourcePath, context); + } catch (URISyntaxException e) { + // do nothing. + } + + context.getProtocolServer().sendEvent(new OutputEvent(category, toSend, source, lineNumber)); + } else { + unsent += toSend; + } + + if (i == lines.length - 1 && StringUtils.isNotEmpty(unsent)) { + context.getProtocolServer().sendEvent(new OutputEvent(category, unsent)); + if (!endWithLF) { + lastIncompleteLine = toSend; + } + } + } + + return lastIncompleteLine; + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java index 694bc9b98..ad9990e6c 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java @@ -17,27 +17,20 @@ import java.io.InputStreamReader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import io.reactivex.disposables.Disposable; import io.reactivex.functions.Consumer; -import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; public class ProcessConsole { private Process process; private String name; private Charset encoding; - private PublishSubject outputSubject = PublishSubject.create(); + private PublishSubject stdoutSubject = PublishSubject.create(); + private PublishSubject stderrSubject = PublishSubject.create(); private Thread stdoutThread = null; private Thread stderrThread = null; - private AtomicInteger exit = new AtomicInteger(0); - private Disposable consumer = null; - /** - * constructor. - */ public ProcessConsole(Process process) { this(process, "Process", StandardCharsets.UTF_8); } @@ -63,7 +56,7 @@ public ProcessConsole(Process process, String name, Charset encoding) { public void start() { this.stdoutThread = new Thread(this.name + " Stdout Handler") { public void run() { - monitor(process.getInputStream(), MessageType.STDOUT); + monitor(process.getInputStream(), stdoutSubject); } }; stdoutThread.setDaemon(true); @@ -71,7 +64,7 @@ public void run() { this.stderrThread = new Thread(this.name + " Stderr Handler") { public void run() { - monitor(process.getErrorStream(), MessageType.STDERR); + monitor(process.getErrorStream(), stderrSubject); } }; stderrThread.setDaemon(true); @@ -93,76 +86,35 @@ public void stop() { } } - /** - * Register a callback to consume the stdout/stderr message from the process. - */ - public void onData(Consumer callback) { - this.consumer = outputSubject.observeOn(Schedulers.newThread()).subscribe(callback); + public Disposable onStdout(Consumer callback) { + return stdoutSubject.subscribe(callback); } - private void monitor(InputStream input, MessageType type) { + public Disposable onStderr(Consumer callback) { + return stderrSubject.subscribe(callback); + } + + private void monitor(InputStream input, PublishSubject subject) { BufferedReader reader = new BufferedReader(new InputStreamReader(input, encoding)); final int BUFFERSIZE = 4096; char[] buffer = new char[BUFFERSIZE]; while (true) { try { if (Thread.interrupted()) { - break; + subject.onComplete(); + return; } int read = reader.read(buffer, 0, BUFFERSIZE); if (read == -1) { - break; + subject.onComplete(); + return; } - outputSubject.onNext(new Message(new String(buffer, 0, read), type)); + subject.onNext(new String(buffer, 0, read)); } catch (IOException e) { - break; + subject.onError(e); + return; } } - - if (exit.addAndGet(1) == 2) { - outputSubject.onComplete(); - } - } - - /** - * Wait for the process's stdout/stderr message is fully consumed by the debug adapter. - */ - public void waitFor() { - // Give the debug adapter additional 3 seconds to handle the process io data. - waitFor(3); - } - - /** - * Wait for the process's stdout/stderr message is fully consumed by the debug adapter. - * @param timeoutSeconds - The maximum waiting time - */ - public void waitFor(int timeoutSeconds) { - int retry = timeoutSeconds * 2; - while (retry-- > 0) { - if (this.consumer != null && this.consumer.isDisposed()) { - break; - } - - try { - TimeUnit.MILLISECONDS.sleep(500); - } catch (InterruptedException e) { - // do nothing. - } - } - } - - public static class Message { - public String output; - public MessageType type; - - public Message(String output, MessageType type) { - this.output = output; - this.type = type; - } - } - - public static enum MessageType { - STDOUT, STDERR } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java index de201dff0..977690d28 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java @@ -27,7 +27,6 @@ import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; import com.microsoft.java.debug.core.adapter.IEvaluationProvider; -import com.microsoft.java.debug.core.adapter.ProcessConsole; import com.microsoft.java.debug.core.protocol.Events; import com.microsoft.java.debug.core.protocol.Messages.Response; import com.microsoft.java.debug.core.protocol.Requests.Arguments; @@ -81,15 +80,13 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, context.getProtocolServer().sendEvent(new Events.ExitedEvent(0)); } else if (event instanceof VMDisconnectEvent) { context.setVmTerminated(); + context.getProtocolServer().sendEvent(new Events.TerminatedEvent()); // Terminate eventHub thread. try { debugSession.getEventHub().close(); } catch (Exception e) { // do nothing. } - - waitForDebuggeeConsole(context); - context.getProtocolServer().sendEvent(new Events.TerminatedEvent()); } else if (event instanceof ThreadStartEvent) { ThreadReference startThread = ((ThreadStartEvent) event).thread(); Events.ThreadEvent threadEvent = new Events.ThreadEvent("started", startThread.uniqueID()); @@ -122,11 +119,4 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, UsageDataSession.recordEvent(event); } } - - private void waitForDebuggeeConsole(IDebugAdapterContext context) { - ProcessConsole console = context.getDebuggeeProcessConsole(); - if (console != null) { - console.waitFor(); - } - } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ILaunchDelegate.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ILaunchDelegate.java index f9e9fa119..ac12a09d5 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ILaunchDelegate.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ILaunchDelegate.java @@ -27,7 +27,7 @@ public interface ILaunchDelegate { CompletableFuture launchInTerminal(LaunchArguments launchArguments, Response response, IDebugAdapterContext context); - Process launchInternalDebuggeeProcess(LaunchArguments launchArguments, IDebugAdapterContext context) + Process launch(LaunchArguments launchArguments, IDebugAdapterContext context) throws IOException, IllegalConnectorArgumentsException, VMStartException; } 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 8eb5b71d7..60af1ed3e 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 @@ -26,8 +26,6 @@ import java.util.concurrent.CompletableFuture; 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; @@ -40,17 +38,15 @@ import com.microsoft.java.debug.core.adapter.ErrorCode; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; +import com.microsoft.java.debug.core.adapter.JavaStackTraceConsole; import com.microsoft.java.debug.core.adapter.LaunchMode; import com.microsoft.java.debug.core.adapter.ProcessConsole; -import com.microsoft.java.debug.core.adapter.ProcessConsole.MessageType; -import com.microsoft.java.debug.core.protocol.Events.OutputEvent; 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.ShortenApproach; -import com.microsoft.java.debug.core.protocol.Types; import com.sun.jdi.connect.IllegalConnectorArgumentsException; import com.sun.jdi.connect.VMStartException; @@ -188,83 +184,25 @@ protected CompletableFuture launch(LaunchArguments launchArguments, Re && (launchArguments.console == CONSOLE.integratedTerminal || launchArguments.console == CONSOLE.externalTerminal)) { return activeLaunchHandler.launchInTerminal(launchArguments, response, context); } else { - return launchInternally(launchArguments, response, context); - } - } - - protected CompletableFuture launchInternally(LaunchArguments launchArguments, Response response, IDebugAdapterContext context) { - CompletableFuture resultFuture = new CompletableFuture<>(); - - try { - Process debuggeeProcess = activeLaunchHandler.launchInternalDebuggeeProcess(launchArguments, context); - context.setDebuggeeProcess(debuggeeProcess); - String[] lastIncompleteLine = new String[] { - null, - null - }; - Pattern stacktracePattern = Pattern.compile("\\s+at\\s+(([\\w$]+\\.)*[\\w$]+)\\(([\\w-$]+\\.java:\\d+)\\)"); - ProcessConsole debuggeeConsole = new ProcessConsole(debuggeeProcess, "Debuggee", context.getDebuggeeEncoding()); - context.setDebuggeeProcessConsole(debuggeeConsole); - debuggeeConsole.onData((message) -> { - // When DA receives a new OutputEvent, it just shows that on Debug Console and doesn't affect the DA's dispatching workflow. - // That means the debugger can send OutputEvent to DA at any time. - OutputEvent.Category eventCategory = message.type == MessageType.STDOUT ? OutputEvent.Category.stdout : OutputEvent.Category.stderr; - int type = message.type.ordinal(); - String[] lines = message.output.split("\n"); - boolean endWithLF = message.output.endsWith("\n"); - String unsent = ""; - for (int i = 0; i < lines.length; i++) { - String toSend = (i < lines.length - 1 || endWithLF) ? lines[i] + "\n" : lines[i]; - String revisedLine = lines[i]; - if (lastIncompleteLine[type] != null) { - revisedLine = lastIncompleteLine[type] + revisedLine; - lastIncompleteLine[type] = null; - } - - Matcher matcher = stacktracePattern.matcher(revisedLine); - if (matcher.find()) { - if (StringUtils.isNotEmpty(unsent)) { - context.getProtocolServer().sendEvent(new OutputEvent(eventCategory, unsent)); - unsent = ""; - } - - String methodField = matcher.group(1); - String locationField = matcher.group(matcher.groupCount()); - - String fullyQualifiedName = methodField.substring(0, methodField.lastIndexOf(".")); - String packageName = fullyQualifiedName.substring(0, fullyQualifiedName.lastIndexOf(".")); - String[] locations = locationField.split(":"); - String sourceName = locations[0]; - int lineNumber = Integer.parseInt(locations[1]); - String sourcePath = StringUtils.isBlank(packageName) ? sourceName - : packageName.replace('.', File.separatorChar) + File.separatorChar + sourceName; - Types.Source source = StackTraceRequestHandler.convertDebuggerSourceToClient(fullyQualifiedName, sourceName, sourcePath, context); - context.getProtocolServer().sendEvent(new OutputEvent(eventCategory, toSend, source, lineNumber)); - } else { - unsent += toSend; - } - - if (i == lines.length - 1 && StringUtils.isNotEmpty(unsent)) { - context.getProtocolServer().sendEvent(new OutputEvent(eventCategory, unsent)); - if (!endWithLF) { - lastIncompleteLine[type] = toSend; - } - } - } - }); + CompletableFuture resultFuture = new CompletableFuture<>(); + try { + Process debuggeeProcess = activeLaunchHandler.launch(launchArguments, context); + context.setDebuggeeProcess(debuggeeProcess); + ProcessConsole debuggeeConsole = new ProcessConsole(debuggeeProcess, "Debuggee", context.getDebuggeeEncoding()); + JavaStackTraceConsole stackTraceConsole = new JavaStackTraceConsole(debuggeeConsole, context); + stackTraceConsole.start(); + resultFuture.complete(response); + } catch (IOException | IllegalConnectorArgumentsException | VMStartException e) { + resultFuture.completeExceptionally( + new DebugException( + String.format("Failed to launch debuggee VM. Reason: %s", e.toString()), + ErrorCode.LAUNCH_FAILURE.getId() + ) + ); + } - debuggeeConsole.start(); - resultFuture.complete(response); - } catch (IOException | IllegalConnectorArgumentsException | VMStartException e) { - resultFuture.completeExceptionally( - new DebugException( - String.format("Failed to launch debuggee VM. Reason: %s", e.toString()), - ErrorCode.LAUNCH_FAILURE.getId() - ) - ); + return resultFuture; } - - return resultFuture; } protected static String[] constructEnvironmentVariables(LaunchArguments launchArguments) { diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithDebuggingDelegate.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithDebuggingDelegate.java index 7e18c7489..0f690773a 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithDebuggingDelegate.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithDebuggingDelegate.java @@ -169,7 +169,7 @@ public CompletableFuture launchInTerminal(LaunchArguments launchArgume } @Override - public Process launchInternalDebuggeeProcess(LaunchArguments launchArguments, IDebugAdapterContext context) + public Process launch(LaunchArguments launchArguments, IDebugAdapterContext context) throws IOException, IllegalConnectorArgumentsException, VMStartException { IVirtualMachineManagerProvider vmProvider = context.getProvider(IVirtualMachineManagerProvider.class); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java index 436d22928..b43bf0529 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java @@ -24,7 +24,6 @@ import com.microsoft.java.debug.core.DebugException; import com.microsoft.java.debug.core.adapter.ErrorCode; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; -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; @@ -42,7 +41,7 @@ public class LaunchWithoutDebuggingDelegate implements ILaunchDelegate { protected static final long RUNINTERMINAL_TIMEOUT = 10 * 1000; @Override - public Process launchInternalDebuggeeProcess(LaunchArguments launchArguments, IDebugAdapterContext context) + public Process launch(LaunchArguments launchArguments, IDebugAdapterContext context) throws IOException, IllegalConnectorArgumentsException, VMStartException { String[] cmds = LaunchRequestHandler.constructLaunchCommands(launchArguments, false, null); File workingDir = null; @@ -59,10 +58,6 @@ public void run() { logger.warning(String.format("Current thread is interrupted. Reason: %s", ignore.toString())); debuggeeProcess.destroy(); } finally { - ProcessConsole console = context.getDebuggeeProcessConsole(); - if (console != null) { - console.waitFor(); - } context.getProtocolServer().sendEvent(new Events.TerminatedEvent()); } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java index c548c69a9..089a55b9d 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java @@ -129,11 +129,11 @@ private Types.Source convertDebuggerSourceToClient(Location location, IDebugAdap /** * Find the source mapping for the specified source file name. */ - public static Types.Source convertDebuggerSourceToClient(String fullyQualifiedName, String sourceName, String sourcePath, IDebugAdapterContext context) - throws URISyntaxException { + public static Types.Source convertDebuggerSourceToClient(String fullyQualifiedName, String sourceName, String relativeSourcePath, + IDebugAdapterContext context) throws URISyntaxException { // use a lru cache for better performance String uri = context.getSourceLookupCache().computeIfAbsent(fullyQualifiedName, key -> { - String fromProvider = context.getProvider(ISourceLookUpProvider.class).getSourceFileURI(key, sourcePath); + String fromProvider = context.getProvider(ISourceLookUpProvider.class).getSourceFileURI(key, relativeSourcePath); // avoid return null which will cause the compute function executed again return StringUtils.isBlank(fromProvider) ? "" : fromProvider; }); @@ -153,7 +153,7 @@ public static Types.Source convertDebuggerSourceToClient(String fullyQualifiedNa } } else { // If the source lookup engine cannot find the source file, then lookup it in the source directories specified by user. - String absoluteSourcepath = AdapterUtils.sourceLookup(context.getSourcePaths(), sourcePath); + String absoluteSourcepath = AdapterUtils.sourceLookup(context.getSourcePaths(), relativeSourcePath); if (absoluteSourcepath != null) { return new Types.Source(sourceName, absoluteSourcepath, 0); } else { From 98ec22beff2e4deb5ee67c3c2818c08abaa7b046 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Mon, 18 Mar 2019 11:05:31 +0800 Subject: [PATCH 3/6] Update license year Signed-off-by: Jinbo Wang --- .../java/debug/core/adapter/JavaStackTraceConsole.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/JavaStackTraceConsole.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/JavaStackTraceConsole.java index 54e4d1e29..7d39f3c4f 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/JavaStackTraceConsole.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/JavaStackTraceConsole.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2019 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at From 750c3a3dab82fc42ccb56f947b5a5e31b9a7eb50 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Wed, 3 Apr 2019 10:48:42 +0800 Subject: [PATCH 4/6] Use Observable to wrap ProcessConsole to pipeline style Signed-off-by: Jinbo Wang --- .../core/adapter/JavaStackTraceConsole.java | 105 ----------- .../debug/core/adapter/ProcessConsole.java | 167 +++++++++++------- .../adapter/handler/LaunchRequestHandler.java | 68 +++++-- 3 files changed, 159 insertions(+), 181 deletions(-) delete mode 100644 com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/JavaStackTraceConsole.java diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/JavaStackTraceConsole.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/JavaStackTraceConsole.java deleted file mode 100644 index 7d39f3c4f..000000000 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/JavaStackTraceConsole.java +++ /dev/null @@ -1,105 +0,0 @@ -/******************************************************************************* -* Copyright (c) 2019 Microsoft Corporation and others. -* All rights reserved. This program and the accompanying materials -* are made available under the terms of the Eclipse Public License v1.0 -* which accompanies this distribution, and is available at -* http://www.eclipse.org/legal/epl-v10.html -* -* Contributors: -* Microsoft Corporation - initial API and implementation -*******************************************************************************/ - -package com.microsoft.java.debug.core.adapter; - -import java.io.File; -import java.net.URISyntaxException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.commons.lang3.StringUtils; - -import com.microsoft.java.debug.core.adapter.handler.StackTraceRequestHandler; -import com.microsoft.java.debug.core.protocol.Events.OutputEvent; -import com.microsoft.java.debug.core.protocol.Types; - -public class JavaStackTraceConsole { - private static final Pattern stacktracePattern = Pattern.compile("\\s+at\\s+(([\\w$]+\\.)*[\\w$]+)\\(([\\w-$]+\\.java:\\d+)\\)"); - private ProcessConsole console; - private IDebugAdapterContext context; - - public JavaStackTraceConsole(ProcessConsole console, IDebugAdapterContext context) { - this.console = console; - this.context = context; - } - - /** - * Start listening on the process console. - */ - public void start() { - String[] lastIncompleteLines = new String[] { - null, - null - }; - this.console.onStdout((message) -> { - lastIncompleteLines[0] = process(message, lastIncompleteLines[0], OutputEvent.Category.stdout); - }); - - this.console.onStderr((message) -> { - lastIncompleteLines[1] = process(message, lastIncompleteLines[1], OutputEvent.Category.stderr); - }); - - this.console.start(); - } - - private String process(String message, String lastIncompleteLine, OutputEvent.Category category) { - String[] lines = message.split("\n"); - boolean endWithLF = message.endsWith("\n"); - String unsent = ""; - for (int i = 0; i < lines.length; i++) { - String toSend = (i < lines.length - 1 || endWithLF) ? lines[i] + "\n" : lines[i]; - String revisedLine = lines[i]; - if (lastIncompleteLine != null) { - revisedLine = lastIncompleteLine + revisedLine; - lastIncompleteLine = null; - } - - Matcher matcher = stacktracePattern.matcher(revisedLine); - if (matcher.find()) { - if (StringUtils.isNotEmpty(unsent)) { - context.getProtocolServer().sendEvent(new OutputEvent(category, unsent)); - unsent = ""; - } - - String methodField = matcher.group(1); - String locationField = matcher.group(matcher.groupCount()); - - String fullyQualifiedName = methodField.substring(0, methodField.lastIndexOf(".")); - String packageName = fullyQualifiedName.substring(0, fullyQualifiedName.lastIndexOf(".")); - String[] locations = locationField.split(":"); - String sourceName = locations[0]; - int lineNumber = Integer.parseInt(locations[1]); - String sourcePath = StringUtils.isBlank(packageName) ? sourceName - : packageName.replace('.', File.separatorChar) + File.separatorChar + sourceName; - Types.Source source = null; - try { - source = StackTraceRequestHandler.convertDebuggerSourceToClient(fullyQualifiedName, sourceName, sourcePath, context); - } catch (URISyntaxException e) { - // do nothing. - } - - context.getProtocolServer().sendEvent(new OutputEvent(category, toSend, source, lineNumber)); - } else { - unsent += toSend; - } - - if (i == lines.length - 1 && StringUtils.isNotEmpty(unsent)) { - context.getProtocolServer().sendEvent(new OutputEvent(category, unsent)); - if (!endWithLF) { - lastIncompleteLine = toSend; - } - } - } - - return lastIncompleteLine; - } -} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java index ad9990e6c..5e37b4b4b 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2019 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -17,19 +17,18 @@ import java.io.InputStreamReader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; +import com.microsoft.java.debug.core.protocol.Events.OutputEvent.Category; + +import io.reactivex.Observable; +import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; public class ProcessConsole { - private Process process; - private String name; - private Charset encoding; - private PublishSubject stdoutSubject = PublishSubject.create(); - private PublishSubject stderrSubject = PublishSubject.create(); - private Thread stdoutThread = null; - private Thread stderrThread = null; + private InputStreamObservable stdoutStream; + private InputStreamObservable stderrStream; + private Observable observable = null; public ProcessConsole(Process process) { this(process, "Process", StandardCharsets.UTF_8); @@ -45,76 +44,126 @@ public ProcessConsole(Process process) { * the process encoding format */ public ProcessConsole(Process process, String name, Charset encoding) { - this.process = process; - this.name = name; - this.encoding = encoding; + this.stdoutStream = new InputStreamObservable(name + " Stdout Handler", process.getInputStream(), encoding); + this.stderrStream = new InputStreamObservable(name + "Stderr Handler", process.getErrorStream(), encoding); + Observable stdout = this.stdoutStream.messages().map((message) -> new ConsoleMessage(message, Category.stdout)); + Observable stderr = this.stderrStream.messages().map((message) -> new ConsoleMessage(message, Category.stderr)); + this.observable = Observable.mergeArrayDelayError(stdout, stderr).observeOn(Schedulers.newThread()); } /** - * Start two separate threads to monitor the messages from stdout and stderr streams of the target process. + * Start monitoring the process stdout/stderr streams. */ public void start() { - this.stdoutThread = new Thread(this.name + " Stdout Handler") { - public void run() { - monitor(process.getInputStream(), stdoutSubject); - } - }; - stdoutThread.setDaemon(true); - stdoutThread.start(); - - this.stderrThread = new Thread(this.name + " Stderr Handler") { - public void run() { - monitor(process.getErrorStream(), stderrSubject); - } - }; - stderrThread.setDaemon(true); - stderrThread.start(); + stdoutStream.start(); + stderrStream.start(); } /** - * Stop the process console handlers. + * Stop monitoring the process console. */ public void stop() { - if (this.stdoutThread != null) { - this.stdoutThread.interrupt(); - this.stdoutThread = null; - } + stdoutStream.stop(); + stderrStream.stop(); + } - if (this.stderrThread != null) { - this.stderrThread.interrupt(); - this.stderrThread = null; - } + public Observable messages() { + return observable; } - public Disposable onStdout(Consumer callback) { - return stdoutSubject.subscribe(callback); + public Observable stdoutMessages() { + return this.messages().filter((message) -> message.category == Category.stdout); } - public Disposable onStderr(Consumer callback) { - return stderrSubject.subscribe(callback); + public Observable stderrMessages() { + return this.messages().filter((message) -> message.category == Category.stderr); } - private void monitor(InputStream input, PublishSubject subject) { - BufferedReader reader = new BufferedReader(new InputStreamReader(input, encoding)); - final int BUFFERSIZE = 4096; - char[] buffer = new char[BUFFERSIZE]; - while (true) { - try { - if (Thread.interrupted()) { - subject.onComplete(); - return; + /** + * Split the stdio message to lines, and return them as a new Observable. + */ + public Observable lineMessages() { + return this.messages().map((message) -> { + String[] lines = message.output.split("(?<=\n)"); + return Stream.of(lines).map((line) -> new ConsoleMessage(line, message.category)).toArray(ConsoleMessage[]::new); + }).concatMap((lines) -> Observable.fromArray(lines)); + } + + public static class InputStreamObservable { + private PublishSubject rxSubject = PublishSubject.create(); + private String name; + private InputStream inputStream; + private Charset encoding; + private Thread loopingThread; + + /** + * Constructor. + */ + public InputStreamObservable(String name, InputStream inputStream, Charset encoding) { + this.name = name; + this.inputStream = inputStream; + this.encoding = encoding; + } + + /** + * Starts the stream. + */ + public void start() { + loopingThread = new Thread(name) { + public void run() { + monitor(inputStream, rxSubject); } - int read = reader.read(buffer, 0, BUFFERSIZE); - if (read == -1) { - subject.onComplete(); + }; + loopingThread.setDaemon(true); + loopingThread.start(); + } + + /** + * Stops the stream. + */ + public void stop() { + if (loopingThread != null) { + loopingThread.interrupt(); + loopingThread = null; + } + } + + private void monitor(InputStream input, PublishSubject subject) { + BufferedReader reader = new BufferedReader(new InputStreamReader(input, encoding)); + final int BUFFERSIZE = 4096; + char[] buffer = new char[BUFFERSIZE]; + while (true) { + try { + if (Thread.interrupted()) { + subject.onComplete(); + return; + } + int read = reader.read(buffer, 0, BUFFERSIZE); + if (read == -1) { + subject.onComplete(); + return; + } + + subject.onNext(new String(buffer, 0, read)); + } catch (IOException e) { + subject.onError(e); return; } - - subject.onNext(new String(buffer, 0, read)); - } catch (IOException e) { - subject.onError(e); - return; } } + + public Observable messages() { + return rxSubject; + } + } + + public static class ConsoleMessage { + public String output; + public Category category; + + public ConsoleMessage(String message, Category category) { + this.output = message; + this.category = category; + } } } 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 60af1ed3e..46093708f 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 @@ -14,6 +14,7 @@ import java.io.File; import java.io.IOException; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -26,6 +27,8 @@ import java.util.concurrent.CompletableFuture; 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; @@ -38,15 +41,17 @@ import com.microsoft.java.debug.core.adapter.ErrorCode; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; -import com.microsoft.java.debug.core.adapter.JavaStackTraceConsole; import com.microsoft.java.debug.core.adapter.LaunchMode; import com.microsoft.java.debug.core.adapter.ProcessConsole; +import com.microsoft.java.debug.core.protocol.Events.OutputEvent; +import com.microsoft.java.debug.core.protocol.Events.OutputEvent.Category; 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.ShortenApproach; +import com.microsoft.java.debug.core.protocol.Types; import com.sun.jdi.connect.IllegalConnectorArgumentsException; import com.sun.jdi.connect.VMStartException; @@ -183,26 +188,55 @@ protected CompletableFuture launch(LaunchArguments launchArguments, Re if (context.supportsRunInTerminalRequest() && (launchArguments.console == CONSOLE.integratedTerminal || launchArguments.console == CONSOLE.externalTerminal)) { return activeLaunchHandler.launchInTerminal(launchArguments, response, context); - } else { - CompletableFuture resultFuture = new CompletableFuture<>(); + } + + CompletableFuture resultFuture = new CompletableFuture<>(); + try { + Process debuggeeProcess = activeLaunchHandler.launch(launchArguments, context); + context.setDebuggeeProcess(debuggeeProcess); + ProcessConsole debuggeeConsole = new ProcessConsole(debuggeeProcess, "Debuggee", context.getDebuggeeEncoding()); + debuggeeConsole.lineMessages() + .map((message) -> convertToOutputEvent(message.output, message.category, context)) + .subscribe((event) -> context.getProtocolServer().sendEvent(event)); + debuggeeConsole.start(); + resultFuture.complete(response); + } catch (IOException | IllegalConnectorArgumentsException | VMStartException e) { + resultFuture.completeExceptionally( + new DebugException( + String.format("Failed to launch debuggee VM. Reason: %s", e.toString()), + ErrorCode.LAUNCH_FAILURE.getId() + ) + ); + } + + return resultFuture; + } + + private static final Pattern STACKTRACE_PATTERN = Pattern.compile("\\s+at\\s+(([\\w$]+\\.)*[\\w$]+)\\(([\\w-$]+\\.java:\\d+)\\)"); + + private static OutputEvent convertToOutputEvent(String message, Category category, IDebugAdapterContext context) { + Matcher matcher = STACKTRACE_PATTERN.matcher(message); + if (matcher.find()) { + String methodField = matcher.group(1); + String locationField = matcher.group(matcher.groupCount()); + String fullyQualifiedName = methodField.substring(0, methodField.lastIndexOf(".")); + String packageName = fullyQualifiedName.lastIndexOf(".") > -1 ? fullyQualifiedName.substring(0, fullyQualifiedName.lastIndexOf(".")) : ""; + String[] locations = locationField.split(":"); + String sourceName = locations[0]; + int lineNumber = Integer.parseInt(locations[1]); + String sourcePath = StringUtils.isBlank(packageName) ? sourceName + : packageName.replace('.', File.separatorChar) + File.separatorChar + sourceName; + Types.Source source = null; try { - Process debuggeeProcess = activeLaunchHandler.launch(launchArguments, context); - context.setDebuggeeProcess(debuggeeProcess); - ProcessConsole debuggeeConsole = new ProcessConsole(debuggeeProcess, "Debuggee", context.getDebuggeeEncoding()); - JavaStackTraceConsole stackTraceConsole = new JavaStackTraceConsole(debuggeeConsole, context); - stackTraceConsole.start(); - resultFuture.complete(response); - } catch (IOException | IllegalConnectorArgumentsException | VMStartException e) { - resultFuture.completeExceptionally( - new DebugException( - String.format("Failed to launch debuggee VM. Reason: %s", e.toString()), - ErrorCode.LAUNCH_FAILURE.getId() - ) - ); + source = StackTraceRequestHandler.convertDebuggerSourceToClient(fullyQualifiedName, sourceName, sourcePath, context); + } catch (URISyntaxException e) { + // do nothing. } - return resultFuture; + return new OutputEvent(category, message, source, lineNumber); } + + return new OutputEvent(category, message); } protected static String[] constructEnvironmentVariables(LaunchArguments launchArguments) { From b2231d3efc923036f84a38d26e5f3c9b5e0df8ed Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Wed, 3 Apr 2019 13:37:14 +0800 Subject: [PATCH 5/6] Wait for the Debuggee Console messages to be completely consumed before sending TerminatedEvent Signed-off-by: Jinbo Wang --- .../ConfigurationDoneRequestHandler.java | 9 +---- .../adapter/handler/LaunchRequestHandler.java | 39 ++++++++++++++++++- .../LaunchWithoutDebuggingDelegate.java | 7 +++- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java index 977690d28..cb460ea7f 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java @@ -79,14 +79,7 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, context.setVmTerminated(); context.getProtocolServer().sendEvent(new Events.ExitedEvent(0)); } else if (event instanceof VMDisconnectEvent) { - context.setVmTerminated(); - context.getProtocolServer().sendEvent(new Events.TerminatedEvent()); - // Terminate eventHub thread. - try { - debugSession.getEventHub().close(); - } catch (Exception e) { - // do nothing. - } + // ignore since LaunchRequestHandler has already handled. } else if (event instanceof ThreadStartEvent) { ThreadReference startThread = ((ThreadStartEvent) event).thread(); Events.ThreadEvent threadEvent = new Events.ThreadEvent("started", startThread.uniqueID()); 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 46093708f..1767d5670 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 @@ -25,6 +25,9 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; @@ -37,12 +40,14 @@ import com.microsoft.java.debug.core.DebugException; import com.microsoft.java.debug.core.DebugSettings; import com.microsoft.java.debug.core.DebugUtility; +import com.microsoft.java.debug.core.IDebugSession; import com.microsoft.java.debug.core.adapter.AdapterUtils; import com.microsoft.java.debug.core.adapter.ErrorCode; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; import com.microsoft.java.debug.core.adapter.LaunchMode; import com.microsoft.java.debug.core.adapter.ProcessConsole; +import com.microsoft.java.debug.core.protocol.Events; import com.microsoft.java.debug.core.protocol.Events.OutputEvent; import com.microsoft.java.debug.core.protocol.Events.OutputEvent.Category; import com.microsoft.java.debug.core.protocol.Messages.Response; @@ -54,11 +59,13 @@ import com.microsoft.java.debug.core.protocol.Types; import com.sun.jdi.connect.IllegalConnectorArgumentsException; import com.sun.jdi.connect.VMStartException; +import com.sun.jdi.event.VMDisconnectEvent; public class LaunchRequestHandler implements IDebugRequestHandler { protected static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); protected static final long RUNINTERMINAL_TIMEOUT = 10 * 1000; protected ILaunchDelegate activeLaunchHandler; + private CompletableFuture waitForDebuggeeConsole = new CompletableFuture<>(); @Override public List getTargetCommands() { @@ -68,7 +75,7 @@ public List getTargetCommands() { @Override public CompletableFuture handle(Command command, Arguments arguments, Response response, IDebugAdapterContext context) { LaunchArguments launchArguments = (LaunchArguments) arguments; - activeLaunchHandler = launchArguments.noDebug ? new LaunchWithoutDebuggingDelegate() : new LaunchWithDebuggingDelegate(); + activeLaunchHandler = launchArguments.noDebug ? new LaunchWithoutDebuggingDelegate(this) : new LaunchWithDebuggingDelegate(); return handleLaunchCommand(arguments, response, context); } @@ -134,10 +141,39 @@ protected CompletableFuture handleLaunchCommand(Arguments arguments, R if (res.success) { activeLaunchHandler.postLaunch(launchArguments, context); } + + IDebugSession debugSession = context.getDebugSession(); + if (debugSession != null) { + debugSession.getEventHub().events() + .filter((debugEvent) -> debugEvent.event instanceof VMDisconnectEvent) + .subscribe((debugEvent) -> { + context.setVmTerminated(); + // Terminate eventHub thread. + try { + debugSession.getEventHub().close(); + } catch (Exception e) { + // do nothing. + } + + handleTerminatedEvent(context); + }); + } return CompletableFuture.completedFuture(res); }); } + protected void handleTerminatedEvent(IDebugAdapterContext context) { + CompletableFuture.runAsync(() -> { + try { + waitForDebuggeeConsole.get(5, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + // do nothing. + } + + context.getProtocolServer().sendEvent(new Events.TerminatedEvent()); + }); + } + /** * Construct the Java command lines based on the given launch arguments. * @param launchArguments - The launch arguments @@ -197,6 +233,7 @@ protected CompletableFuture launch(LaunchArguments launchArguments, Re ProcessConsole debuggeeConsole = new ProcessConsole(debuggeeProcess, "Debuggee", context.getDebuggeeEncoding()); debuggeeConsole.lineMessages() .map((message) -> convertToOutputEvent(message.output, message.category, context)) + .doFinally(() -> waitForDebuggeeConsole.complete(true)) .subscribe((event) -> context.getProtocolServer().sendEvent(event)); debuggeeConsole.start(); resultFuture.complete(response); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java index b43bf0529..5a0b8a771 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java @@ -39,6 +39,11 @@ public class LaunchWithoutDebuggingDelegate implements ILaunchDelegate { protected static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); protected static final String TERMINAL_TITLE = "Java Process Console"; protected static final long RUNINTERMINAL_TIMEOUT = 10 * 1000; + private LaunchRequestHandler handler; + + public LaunchWithoutDebuggingDelegate(LaunchRequestHandler handler) { + this.handler = handler; + } @Override public Process launch(LaunchArguments launchArguments, IDebugAdapterContext context) @@ -58,7 +63,7 @@ public void run() { logger.warning(String.format("Current thread is interrupted. Reason: %s", ignore.toString())); debuggeeProcess.destroy(); } finally { - context.getProtocolServer().sendEvent(new Events.TerminatedEvent()); + handler.handleTerminatedEvent(context); } } }.start(); From 72457c78500f4afba64ea627e7374a1b3b8dcc94 Mon Sep 17 00:00:00 2001 From: Jinbo Wang Date: Thu, 11 Apr 2019 16:58:48 +0800 Subject: [PATCH 6/6] Address review comments Signed-off-by: Jinbo Wang --- .../java/debug/core/adapter/ProcessConsole.java | 4 ++-- .../debug/core/adapter/handler/LaunchRequestHandler.java | 3 ++- .../adapter/handler/LaunchWithoutDebuggingDelegate.java | 9 +++++---- .../com/microsoft/java/debug/core/protocol/Events.java | 8 ++++---- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java index 5e37b4b4b..6b384c628 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java @@ -45,14 +45,14 @@ public ProcessConsole(Process process) { */ public ProcessConsole(Process process, String name, Charset encoding) { this.stdoutStream = new InputStreamObservable(name + " Stdout Handler", process.getInputStream(), encoding); - this.stderrStream = new InputStreamObservable(name + "Stderr Handler", process.getErrorStream(), encoding); + this.stderrStream = new InputStreamObservable(name + " Stderr Handler", process.getErrorStream(), encoding); Observable stdout = this.stdoutStream.messages().map((message) -> new ConsoleMessage(message, Category.stdout)); Observable stderr = this.stderrStream.messages().map((message) -> new ConsoleMessage(message, Category.stderr)); this.observable = Observable.mergeArrayDelayError(stdout, stderr).observeOn(Schedulers.newThread()); } /** - * Start monitoring the process stdout/stderr streams. + * Start monitoring the stdout/stderr streams of the target process. */ public void start() { stdoutStream.start(); 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 1767d5670..5e07e21c7 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 @@ -75,7 +75,8 @@ public List getTargetCommands() { @Override public CompletableFuture handle(Command command, Arguments arguments, Response response, IDebugAdapterContext context) { LaunchArguments launchArguments = (LaunchArguments) arguments; - activeLaunchHandler = launchArguments.noDebug ? new LaunchWithoutDebuggingDelegate(this) : new LaunchWithDebuggingDelegate(); + activeLaunchHandler = launchArguments.noDebug ? new LaunchWithoutDebuggingDelegate((daContext) -> handleTerminatedEvent(daContext)) + : new LaunchWithDebuggingDelegate(); return handleLaunchCommand(arguments, response, context); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java index 5a0b8a771..804ee58ca 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java @@ -17,6 +17,7 @@ import java.nio.file.Paths; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.function.Consumer; import java.util.logging.Logger; import com.google.gson.JsonObject; @@ -39,10 +40,10 @@ public class LaunchWithoutDebuggingDelegate implements ILaunchDelegate { protected static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); protected static final String TERMINAL_TITLE = "Java Process Console"; protected static final long RUNINTERMINAL_TIMEOUT = 10 * 1000; - private LaunchRequestHandler handler; + private Consumer terminateHandler; - public LaunchWithoutDebuggingDelegate(LaunchRequestHandler handler) { - this.handler = handler; + public LaunchWithoutDebuggingDelegate(Consumer terminateHandler) { + this.terminateHandler = terminateHandler; } @Override @@ -63,7 +64,7 @@ public void run() { logger.warning(String.format("Current thread is interrupted. Reason: %s", ignore.toString())); debuggeeProcess.destroy(); } finally { - handler.handleTerminatedEvent(context); + terminateHandler.accept(context); } } }.start(); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Events.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Events.java index e58a7f7fe..ea18f183c 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Events.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Events.java @@ -173,9 +173,9 @@ public static OutputEvent createStdoutOutput(String output) { } /** - * Constructor an stdout output event with source info. + * Construct an stdout output event with source info. */ - public static OutputEvent createStdoutOutput(String output, Source source, int line) { + public static OutputEvent createStdoutOutputWithSource(String output, Source source, int line) { return new OutputEvent(Category.stdout, output, source, line); } @@ -184,9 +184,9 @@ public static OutputEvent createStderrOutput(String output) { } /** - * Constructor an stderr output event with source info. + * Construct an stderr output event with source info. */ - public static OutputEvent createStderrOutput(String output, Source source, int line) { + public static OutputEvent createStderrOutputWithSource(String output, Source source, int line) { return new OutputEvent(Category.stderr, output, source, line); }