From 67785755408feba1618dc827e59db6d7973592e5 Mon Sep 17 00:00:00 2001 From: wenytang-ms <75360946+wenytang-ms@users.noreply.github.com> Date: Mon, 26 Oct 2026 03:04:07 +0000 Subject: [PATCH 1/2] Add DAP terminate request support --- .../java/debug/core/adapter/DebugAdapter.java | 2 + .../handler/DebuggeeProcessHelper.java | 33 +++++ ...connectRequestWithoutDebuggingHandler.java | 12 +- .../handler/InitializeRequestHandler.java | 1 + .../handler/TerminateRequestHandler.java | 46 ++++++ .../java/debug/core/protocol/Requests.java | 5 + .../java/debug/core/protocol/Types.java | 1 + .../TerminateRequestHandlerJdiTest.java | 83 +++++++++++ .../handler/TerminateRequestHandlerTest.java | 137 ++++++++++++++++++ .../src/main/java/ShutdownHookTest.java | 23 +++ 10 files changed, 333 insertions(+), 10 deletions(-) create mode 100644 com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/DebuggeeProcessHelper.java create mode 100644 com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandler.java create mode 100644 com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerJdiTest.java create mode 100644 com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerTest.java create mode 100644 com.microsoft.java.debug.test/project/5.shutdownhook/src/main/java/ShutdownHookTest.java diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapter.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapter.java index 8f595151f..ac2a24bf9 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapter.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapter.java @@ -47,6 +47,7 @@ import com.microsoft.java.debug.core.adapter.handler.StackTraceRequestHandler; import com.microsoft.java.debug.core.adapter.handler.StepInTargetsRequestHandler; import com.microsoft.java.debug.core.adapter.handler.StepRequestHandler; +import com.microsoft.java.debug.core.adapter.handler.TerminateRequestHandler; import com.microsoft.java.debug.core.adapter.handler.ThreadsRequestHandler; import com.microsoft.java.debug.core.adapter.handler.VariablesRequestHandler; import com.microsoft.java.debug.core.protocol.IProtocolServer; @@ -107,6 +108,7 @@ protected void initialize() { // When there are multiple handlers registered for the same request, follow the rule "first register, first execute". registerHandler(new InitializeRequestHandler()); registerHandler(new LaunchRequestHandler()); + registerHandler(new TerminateRequestHandler()); // DEBUG mode only registerHandlerForDebug(new AttachRequestHandler()); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/DebuggeeProcessHelper.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/DebuggeeProcessHelper.java new file mode 100644 index 000000000..e8478fdd3 --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/DebuggeeProcessHelper.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2026 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.handler; + +import java.util.Optional; + +import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; + +final class DebuggeeProcessHelper { + private DebuggeeProcessHelper() { + } + + static void terminate(IDebugAdapterContext context) { + Process debuggeeProcess = context.getDebuggeeProcess(); + if (debuggeeProcess != null) { + debuggeeProcess.destroy(); + } else if (context.getProcessId() > 0) { + Optional debuggeeHandle = ProcessHandle.of(context.getProcessId()); + if (debuggeeHandle.isPresent()) { + debuggeeHandle.get().destroy(); + } + } + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/DisconnectRequestWithoutDebuggingHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/DisconnectRequestWithoutDebuggingHandler.java index 59d750bcd..ae52709fe 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/DisconnectRequestWithoutDebuggingHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/DisconnectRequestWithoutDebuggingHandler.java @@ -11,8 +11,6 @@ package com.microsoft.java.debug.core.adapter.handler; -import java.util.Optional; - 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.Arguments; @@ -24,14 +22,8 @@ public class DisconnectRequestWithoutDebuggingHandler extends AbstractDisconnect @Override public void destroyDebugSession(Command command, Arguments arguments, Response response, IDebugAdapterContext context) { DisconnectArguments disconnectArguments = (DisconnectArguments) arguments; - Process debuggeeProcess = context.getDebuggeeProcess(); - if (debuggeeProcess != null && disconnectArguments.terminateDebuggee) { - debuggeeProcess.destroy(); - } else if (context.getProcessId() > 0 && disconnectArguments.terminateDebuggee) { - Optional debuggeeHandle = ProcessHandle.of(context.getProcessId()); - if (debuggeeHandle.isPresent()) { - debuggeeHandle.get().destroy(); - } + if (disconnectArguments.terminateDebuggee) { + DebuggeeProcessHelper.terminate(context); } } } 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 6b9245166..4c8ba1660 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 @@ -50,6 +50,7 @@ public CompletableFuture handle(Requests.Command command, Req caps.supportsHitConditionalBreakpoints = true; caps.supportsConditionalBreakpoints = true; caps.supportsSetVariable = true; + caps.supportsTerminateRequest = true; caps.supportTerminateDebuggee = true; caps.supportsCompletionsRequest = true; caps.supportsRestartFrame = true; diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandler.java new file mode 100644 index 000000000..2eff521cb --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandler.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2026 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.handler; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.java.debug.core.IDebugSession; +import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; +import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; +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.Command; + +public class TerminateRequestHandler implements IDebugRequestHandler { + @Override + public List getTargetCommands() { + return Arrays.asList(Command.TERMINATE); + } + + @Override + public CompletableFuture handle(Command command, Arguments arguments, Response response, + IDebugAdapterContext context) { + if (context.isAttached()) { + return CompletableFuture.completedFuture(response); + } + + IDebugSession debugSession = context.getDebugSession(); + if (debugSession != null) { + debugSession.terminate(); + } else { + DebuggeeProcessHelper.terminate(context); + } + return CompletableFuture.completedFuture(response); + } +} 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 1129d230e..d247cd7a0 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 @@ -227,6 +227,10 @@ public static class DisconnectArguments extends Arguments { public boolean restart; } + public static class TerminateArguments extends Arguments { + public boolean restart; + } + public static class ConfigurationDoneArguments extends Arguments { } @@ -440,6 +444,7 @@ public static enum Command { LAUNCH("launch", LaunchArguments.class), ATTACH("attach", AttachArguments.class), DISCONNECT("disconnect", DisconnectArguments.class), + TERMINATE("terminate", TerminateArguments.class), CONFIGURATIONDONE("configurationDone", ConfigurationDoneArguments.class), NEXT("next", NextArguments.class), CONTINUE("continue", ContinueArguments.class), diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Types.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Types.java index 9d32d4783..8e62481c5 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Types.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Types.java @@ -419,6 +419,7 @@ public static class Capabilities { public boolean supportsRestartFrame; public boolean supportsSetVariable; public boolean supportsRestartRequest; + public boolean supportsTerminateRequest; public boolean supportTerminateDebuggee; public boolean supportsDelayedStackTraceLoading; public boolean supportsLogPoints; diff --git a/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerJdiTest.java b/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerJdiTest.java new file mode 100644 index 000000000..7d52c6476 --- /dev/null +++ b/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerJdiTest.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2026 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.handler; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Locale; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.Assume; +import org.junit.Test; + +import com.microsoft.java.debug.core.DebugSessionFactory; +import com.microsoft.java.debug.core.IBreakpoint; +import com.microsoft.java.debug.core.IDebugSession; +import com.microsoft.java.debug.core.adapter.DebugAdapterContext; +import com.microsoft.java.debug.core.adapter.ProviderContext; +import com.microsoft.java.debug.core.protocol.Messages.Response; +import com.microsoft.java.debug.core.protocol.Requests.Command; +import com.microsoft.java.debug.core.protocol.Requests.TerminateArguments; +import com.sun.jdi.event.BreakpointEvent; + +import io.reactivex.disposables.Disposable; + +public class TerminateRequestHandlerJdiTest { + private static final String PROJECT_NAME = "5.shutdownhook"; + private static final int READY_LINE = 15; + private static final int SHUTDOWN_HOOK_LINE = 20; + + @Test + public void terminateShouldKeepDebuggerAttachedForShutdownHookBreakpoint() throws Exception { + Assume.assumeFalse(System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win")); + + IDebugSession debugSession = DebugSessionFactory.getDebugSession(PROJECT_NAME, "ShutdownHookTest"); + CountDownLatch readyBreakpoint = new CountDownLatch(1); + CountDownLatch shutdownHookBreakpoint = new CountDownLatch(1); + IBreakpoint ready = debugSession.createBreakpoint("ShutdownHookTest", READY_LINE, 0, null, null); + IBreakpoint shutdownHook = debugSession.createBreakpoint("ShutdownHookTest", SHUTDOWN_HOOK_LINE, 0, null, null); + Disposable subscription = debugSession.getEventHub().breakpointEvents().subscribe(debugEvent -> { + int line = ((BreakpointEvent) debugEvent.event).location().lineNumber(); + if (line == READY_LINE) { + debugEvent.shouldResume = false; + readyBreakpoint.countDown(); + } else if (line == SHUTDOWN_HOOK_LINE) { + debugEvent.shouldResume = false; + shutdownHookBreakpoint.countDown(); + } + }); + + try { + ready.install(); + shutdownHook.install(); + debugSession.start(); + debugSession.resume(); + assertTrue("The debuggee did not reach the ready breakpoint.", + readyBreakpoint.await(10, TimeUnit.SECONDS)); + + DebugAdapterContext context = new DebugAdapterContext(null, new ProviderContext()); + context.setDebugSession(debugSession); + new TerminateRequestHandler() + .handle(Command.TERMINATE, new TerminateArguments(), new Response(), context).join(); + + assertFalse(context.isVmTerminated()); + assertTrue("The shutdown hook breakpoint was not hit.", + shutdownHookBreakpoint.await(10, TimeUnit.SECONDS)); + debugSession.resume(); + } finally { + subscription.dispose(); + DebugSessionFactory.shutdownDebugSession(PROJECT_NAME); + } + } +} diff --git a/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerTest.java b/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerTest.java new file mode 100644 index 000000000..2385cd19e --- /dev/null +++ b/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerTest.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) 2026 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.handler; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.TimeUnit; + +import org.easymock.EasyMockSupport; +import org.junit.Test; + +import com.microsoft.java.debug.core.IDebugSession; +import com.microsoft.java.debug.core.adapter.DebugAdapterContext; +import com.microsoft.java.debug.core.adapter.ProviderContext; +import com.microsoft.java.debug.core.protocol.Messages.Response; +import com.microsoft.java.debug.core.protocol.Requests.Command; +import com.microsoft.java.debug.core.protocol.Requests.InitializeArguments; +import com.microsoft.java.debug.core.protocol.Requests.TerminateArguments; +import com.microsoft.java.debug.core.protocol.Types.Capabilities; + +public class TerminateRequestHandlerTest extends EasyMockSupport { + @Test + public void terminateLaunchShouldKeepDebugSessionActive() { + IDebugSession debugSession = createMock(IDebugSession.class); + debugSession.terminate(); + replayAll(); + + DebugAdapterContext context = new DebugAdapterContext(null, new ProviderContext()); + context.setDebugSession(debugSession); + Response response = new Response(); + + Response result = new TerminateRequestHandler() + .handle(Command.TERMINATE, new TerminateArguments(), response, context).join(); + + assertSame(response, result); + assertFalse(context.isVmTerminated()); + verifyAll(); + } + + @Test + public void terminateAttachShouldBeNoOp() { + IDebugSession debugSession = createMock(IDebugSession.class); + replayAll(); + + DebugAdapterContext context = new DebugAdapterContext(null, new ProviderContext()); + context.setDebugSession(debugSession); + context.setAttached(true); + + new TerminateRequestHandler() + .handle(Command.TERMINATE, new TerminateArguments(), new Response(), context).join(); + + assertFalse(context.isVmTerminated()); + verifyAll(); + } + + @Test + public void terminateNoDebugShouldDestroyTrackedProcess() { + RecordingProcess process = new RecordingProcess(); + DebugAdapterContext context = new DebugAdapterContext(null, new ProviderContext()); + context.setDebuggeeProcess(process); + + new TerminateRequestHandler() + .handle(Command.TERMINATE, new TerminateArguments(), new Response(), context).join(); + + assertTrue(process.isDestroyed()); + assertFalse(context.isVmTerminated()); + } + + @Test + public void initializeShouldAdvertiseTerminateRequest() { + DebugAdapterContext context = new DebugAdapterContext(null, new ProviderContext()); + Response response = new Response(); + + new InitializeRequestHandler() + .handle(Command.INITIALIZE, new InitializeArguments(), response, context).join(); + + assertTrue(((Capabilities) response.body).supportsTerminateRequest); + } + + private static class RecordingProcess extends Process { + private boolean destroyed; + + boolean isDestroyed() { + return destroyed; + } + + @Override + public OutputStream getOutputStream() { + return new ByteArrayOutputStream(); + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(new byte[0]); + } + + @Override + public InputStream getErrorStream() { + return new ByteArrayInputStream(new byte[0]); + } + + @Override + public int waitFor() { + return 0; + } + + @Override + public boolean waitFor(long timeout, TimeUnit unit) { + return true; + } + + @Override + public int exitValue() { + return 0; + } + + @Override + public void destroy() { + destroyed = true; + } + } +} diff --git a/com.microsoft.java.debug.test/project/5.shutdownhook/src/main/java/ShutdownHookTest.java b/com.microsoft.java.debug.test/project/5.shutdownhook/src/main/java/ShutdownHookTest.java new file mode 100644 index 000000000..81b8df287 --- /dev/null +++ b/com.microsoft.java.debug.test/project/5.shutdownhook/src/main/java/ShutdownHookTest.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2026 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 + *******************************************************************************/ + +public class ShutdownHookTest { + public static void main(String[] args) throws Exception { + Runtime.getRuntime().addShutdownHook(new Thread(ShutdownHookTest::onShutdown)); + System.out.println("ready"); + Thread.sleep(Long.MAX_VALUE); + } + + private static void onShutdown() { + int hookReached = 1; + System.out.println(hookReached); + } +} From bf9ef74a595309f2818cdc54ae7fa20a7a84b6bd Mon Sep 17 00:00:00 2001 From: wenytang-ms <75360946+wenytang-ms@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:08:29 +0000 Subject: [PATCH 2/2] Resume suspended VM before terminate --- .../handler/TerminateRequestHandler.java | 1 + .../handler/ThreadsRequestHandler.java | 14 ++- .../TerminateRequestHandlerJdiTest.java | 106 +++++++++++++++--- .../handler/TerminateRequestHandlerTest.java | 95 ++++++++++++++-- 4 files changed, 190 insertions(+), 26 deletions(-) diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandler.java index 2eff521cb..f3786fc31 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandler.java @@ -37,6 +37,7 @@ public CompletableFuture handle(Command command, Arguments arguments, IDebugSession debugSession = context.getDebugSession(); if (debugSession != null) { + ThreadsRequestHandler.resumeAllThreads(context); debugSession.terminate(); } else { DebuggeeProcessHelper.terminate(context); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ThreadsRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ThreadsRequestHandler.java index 5c4770391..c72f0b043 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ThreadsRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ThreadsRequestHandler.java @@ -181,14 +181,22 @@ private CompletableFuture resume(Requests.ContinueArguments arguments, } private CompletableFuture resumeAll(Requests.ThreadOperationArguments arguments, Response response, IDebugAdapterContext context) { + resumeAllThreads(context, new Events.ContinuedEvent(arguments.threadId, true)); + return CompletableFuture.completedFuture(response); + } + + static void resumeAllThreads(IDebugAdapterContext context) { + resumeAllThreads(context, new Events.ContinuedEvent(true)); + } + + private static void resumeAllThreads(IDebugAdapterContext context, Events.ContinuedEvent continuedEvent) { context.getStepResultManager().removeAllMethodResults(); context.getExceptionManager().removeAllExceptions(); context.getThreadCache().clearAllThreadStoppedState(); resumeVM(context); - context.getProtocolServer().sendEvent(new Events.ContinuedEvent(arguments.threadId, true)); + context.getProtocolServer().sendEvent(continuedEvent); context.getStackFrameManager().clearStackFrames(); context.getRecyclableIdPool().removeAllObjects(); - return CompletableFuture.completedFuture(response); } private CompletableFuture resumeOthers(Requests.ThreadOperationArguments arguments, Response response, IDebugAdapterContext context) { @@ -252,7 +260,7 @@ public static void checkThreadRunningAndRecycleIds(ThreadReference thread, IDebu } } - private void resumeVM(IDebugAdapterContext context) { + private static void resumeVM(IDebugAdapterContext context) { List visibleThreads = context.getThreadCache().visibleThreads(context); context.getThreadCache().clearEventThread(); diff --git a/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerJdiTest.java b/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerJdiTest.java index 7d52c6476..ed00e72db 100644 --- a/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerJdiTest.java +++ b/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerJdiTest.java @@ -12,20 +12,26 @@ package com.microsoft.java.debug.core.adapter.handler; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.util.Locale; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.Assume; import org.junit.Test; +import com.microsoft.java.debug.core.DebugSettings; import com.microsoft.java.debug.core.DebugSessionFactory; import com.microsoft.java.debug.core.IBreakpoint; import com.microsoft.java.debug.core.IDebugSession; import com.microsoft.java.debug.core.adapter.DebugAdapterContext; import com.microsoft.java.debug.core.adapter.ProviderContext; +import com.microsoft.java.debug.core.protocol.Events.DebugEvent; +import com.microsoft.java.debug.core.protocol.IProtocolServer; +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.Command; import com.microsoft.java.debug.core.protocol.Requests.TerminateArguments; @@ -42,23 +48,31 @@ public class TerminateRequestHandlerJdiTest { public void terminateShouldKeepDebuggerAttachedForShutdownHookBreakpoint() throws Exception { Assume.assumeFalse(System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win")); - IDebugSession debugSession = DebugSessionFactory.getDebugSession(PROJECT_NAME, "ShutdownHookTest"); + boolean suspendAllThreads = DebugSettings.getCurrent().suspendAllThreads; + DebugSettings.getCurrent().suspendAllThreads = true; + IDebugSession debugSession = null; + Process debuggeeProcess = null; + Disposable subscription = null; CountDownLatch readyBreakpoint = new CountDownLatch(1); CountDownLatch shutdownHookBreakpoint = new CountDownLatch(1); - IBreakpoint ready = debugSession.createBreakpoint("ShutdownHookTest", READY_LINE, 0, null, null); - IBreakpoint shutdownHook = debugSession.createBreakpoint("ShutdownHookTest", SHUTDOWN_HOOK_LINE, 0, null, null); - Disposable subscription = debugSession.getEventHub().breakpointEvents().subscribe(debugEvent -> { - int line = ((BreakpointEvent) debugEvent.event).location().lineNumber(); - if (line == READY_LINE) { - debugEvent.shouldResume = false; - readyBreakpoint.countDown(); - } else if (line == SHUTDOWN_HOOK_LINE) { - debugEvent.shouldResume = false; - shutdownHookBreakpoint.countDown(); - } - }); try { + debugSession = DebugSessionFactory.getDebugSession(PROJECT_NAME, "ShutdownHookTest"); + assertNotNull(debugSession); + debuggeeProcess = debugSession.process(); + IBreakpoint ready = debugSession.createBreakpoint("ShutdownHookTest", READY_LINE, 0, null, null); + IBreakpoint shutdownHook = debugSession.createBreakpoint("ShutdownHookTest", SHUTDOWN_HOOK_LINE, 0, null, null); + subscription = debugSession.getEventHub().breakpointEvents().subscribe(debugEvent -> { + int line = ((BreakpointEvent) debugEvent.event).location().lineNumber(); + if (line == READY_LINE) { + debugEvent.shouldResume = false; + readyBreakpoint.countDown(); + } else if (line == SHUTDOWN_HOOK_LINE) { + debugEvent.shouldResume = false; + shutdownHookBreakpoint.countDown(); + } + }); + ready.install(); shutdownHook.install(); debugSession.start(); @@ -66,7 +80,7 @@ public void terminateShouldKeepDebuggerAttachedForShutdownHookBreakpoint() throw assertTrue("The debuggee did not reach the ready breakpoint.", readyBreakpoint.await(10, TimeUnit.SECONDS)); - DebugAdapterContext context = new DebugAdapterContext(null, new ProviderContext()); + DebugAdapterContext context = new DebugAdapterContext(new NoopProtocolServer(), new ProviderContext()); context.setDebugSession(debugSession); new TerminateRequestHandler() .handle(Command.TERMINATE, new TerminateArguments(), new Response(), context).join(); @@ -74,10 +88,72 @@ public void terminateShouldKeepDebuggerAttachedForShutdownHookBreakpoint() throw assertFalse(context.isVmTerminated()); assertTrue("The shutdown hook breakpoint was not hit.", shutdownHookBreakpoint.await(10, TimeUnit.SECONDS)); - debugSession.resume(); } finally { + try { + dispose(subscription); + terminate(debuggeeProcess); + shutdownDebugSession(); + } finally { + DebugSettings.getCurrent().suspendAllThreads = suspendAllThreads; + } + } + } + + private static void dispose(Disposable subscription) { + if (subscription == null) { + return; + } + try { subscription.dispose(); + } catch (RuntimeException ex) { + System.err.println("Failed to dispose the breakpoint subscription: " + ex.getMessage()); + } + } + + private static void terminate(Process debuggeeProcess) { + if (debuggeeProcess == null) { + return; + } + try { + if (debuggeeProcess.isAlive()) { + debuggeeProcess.destroyForcibly(); + } + if (!debuggeeProcess.waitFor(5, TimeUnit.SECONDS)) { + System.err.println("The shutdown-hook test process did not exit within five seconds."); + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + System.err.println("Interrupted while waiting for the shutdown-hook test process to exit."); + } catch (RuntimeException ex) { + System.err.println("Failed to terminate the shutdown-hook test process: " + ex.getMessage()); + } + } + + private static void shutdownDebugSession() { + try { DebugSessionFactory.shutdownDebugSession(PROJECT_NAME); + } catch (Exception ex) { + System.err.println("Failed to shut down the test debug session: " + ex.getMessage()); + } + } + + private static class NoopProtocolServer implements IProtocolServer { + @Override + public CompletableFuture sendRequest(Request request) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture sendRequest(Request request, long timeout) { + return CompletableFuture.completedFuture(null); + } + + @Override + public void sendEvent(DebugEvent event) { + } + + @Override + public void sendResponse(Response response) { } } } diff --git a/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerTest.java b/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerTest.java index 2385cd19e..badc49b2c 100644 --- a/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerTest.java +++ b/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/handler/TerminateRequestHandlerTest.java @@ -11,7 +11,11 @@ package com.microsoft.java.debug.core.adapter.handler; +import static org.easymock.EasyMock.capture; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -19,37 +23,92 @@ import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; +import java.util.Collections; +import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.easymock.Capture; +import org.easymock.EasyMock; import org.easymock.EasyMockSupport; +import org.easymock.IMocksControl; import org.junit.Test; import com.microsoft.java.debug.core.IDebugSession; import com.microsoft.java.debug.core.adapter.DebugAdapterContext; +import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; +import com.microsoft.java.debug.core.adapter.IExceptionManager; +import com.microsoft.java.debug.core.adapter.IStackFrameManager; +import com.microsoft.java.debug.core.adapter.IStepResultManager; import com.microsoft.java.debug.core.adapter.ProviderContext; +import com.microsoft.java.debug.core.adapter.RecyclableObjectPool; +import com.microsoft.java.debug.core.adapter.ThreadCache; +import com.microsoft.java.debug.core.protocol.Events; +import com.microsoft.java.debug.core.protocol.IProtocolServer; import com.microsoft.java.debug.core.protocol.Messages.Response; import com.microsoft.java.debug.core.protocol.Requests.Command; import com.microsoft.java.debug.core.protocol.Requests.InitializeArguments; import com.microsoft.java.debug.core.protocol.Requests.TerminateArguments; import com.microsoft.java.debug.core.protocol.Types.Capabilities; +import com.sun.jdi.ThreadReference; +import com.sun.jdi.VirtualMachine; public class TerminateRequestHandlerTest extends EasyMockSupport { @Test - public void terminateLaunchShouldKeepDebugSessionActive() { - IDebugSession debugSession = createMock(IDebugSession.class); + public void terminateLaunchShouldResumeAndClearStateBeforeTermination() { + IMocksControl control = EasyMock.createStrictControl(); + IDebugAdapterContext context = control.createMock(IDebugAdapterContext.class); + IDebugSession debugSession = control.createMock(IDebugSession.class); + IStepResultManager stepResultManager = control.createMock(IStepResultManager.class); + IExceptionManager exceptionManager = control.createMock(IExceptionManager.class); + RecordingThreadCache threadCache = new RecordingThreadCache(); + IProtocolServer protocolServer = control.createMock(IProtocolServer.class); + IStackFrameManager stackFrameManager = control.createMock(IStackFrameManager.class); + RecyclableObjectPool recyclableIdPool = new RecyclableObjectPool<>(); + int recyclableId = recyclableIdPool.addObject(1L, new Object()); + VirtualMachine vm = control.createMock(VirtualMachine.class); + Capture event = EasyMock.newCapture(); + AtomicBoolean vmResumed = new AtomicBoolean(); + + expect(context.isAttached()).andReturn(false); + expect(context.getDebugSession()).andReturn(debugSession); + expect(context.getStepResultManager()).andReturn(stepResultManager); + stepResultManager.removeAllMethodResults(); + expect(context.getExceptionManager()).andReturn(exceptionManager); + exceptionManager.removeAllExceptions(); + expect(context.getThreadCache()).andReturn(threadCache); + expect(context.getThreadCache()).andReturn(threadCache); + expect(context.getThreadCache()).andReturn(threadCache); + expect(context.getDebugSession()).andReturn(debugSession); + expect(debugSession.getVM()).andReturn(vm); + vm.resume(); + expectLastCall().andAnswer(() -> { + vmResumed.set(true); + return null; + }); + expect(context.getProtocolServer()).andReturn(protocolServer); + protocolServer.sendEvent(capture(event)); + expect(context.getStackFrameManager()).andReturn(stackFrameManager); + stackFrameManager.clearStackFrames(); + expect(context.getRecyclableIdPool()).andReturn(recyclableIdPool); debugSession.terminate(); - replayAll(); + expectLastCall().andAnswer(() -> { + assertTrue("The VM must be resumed before it is terminated.", vmResumed.get()); + assertTrue("Stopped-thread state must be cleared before termination.", threadCache.stoppedStateCleared); + assertTrue("Event-thread state must be cleared before termination.", threadCache.eventThreadsCleared); + return null; + }); + control.replay(); - DebugAdapterContext context = new DebugAdapterContext(null, new ProviderContext()); - context.setDebugSession(debugSession); Response response = new Response(); - Response result = new TerminateRequestHandler() .handle(Command.TERMINATE, new TerminateArguments(), response, context).join(); assertSame(response, result); - assertFalse(context.isVmTerminated()); - verifyAll(); + assertTrue(event.getValue() instanceof Events.ContinuedEvent); + assertTrue(((Events.ContinuedEvent) event.getValue()).allThreadsContinued); + assertNull(recyclableIdPool.getObjectById(recyclableId)); + control.verify(); } @Test @@ -134,4 +193,24 @@ public void destroy() { destroyed = true; } } + + private static class RecordingThreadCache extends ThreadCache { + private boolean stoppedStateCleared; + private boolean eventThreadsCleared; + + @Override + public void clearAllThreadStoppedState() { + stoppedStateCleared = true; + } + + @Override + public List visibleThreads(IDebugAdapterContext context) { + return Collections.emptyList(); + } + + @Override + public void clearEventThread() { + eventThreadsCleared = true; + } + } }