Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ProcessHandle> debuggeeHandle = ProcessHandle.of(context.getProcessId());
if (debuggeeHandle.isPresent()) {
debuggeeHandle.get().destroy();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ProcessHandle> debuggeeHandle = ProcessHandle.of(context.getProcessId());
if (debuggeeHandle.isPresent()) {
debuggeeHandle.get().destroy();
}
if (disconnectArguments.terminateDebuggee) {
DebuggeeProcessHelper.terminate(context);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public CompletableFuture<Messages.Response> 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*******************************************************************************
* 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<Command> getTargetCommands() {
return Arrays.asList(Command.TERMINATE);
}

@Override
public CompletableFuture<Response> handle(Command command, Arguments arguments, Response response,
IDebugAdapterContext context) {
if (context.isAttached()) {
return CompletableFuture.completedFuture(response);
}

IDebugSession debugSession = context.getDebugSession();
if (debugSession != null) {
ThreadsRequestHandler.resumeAllThreads(context);
debugSession.terminate();
Comment thread
wenytang-ms marked this conversation as resolved.
} else {
DebuggeeProcessHelper.terminate(context);
}
return CompletableFuture.completedFuture(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,22 @@ private CompletableFuture<Response> resume(Requests.ContinueArguments arguments,
}

private CompletableFuture<Response> 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<Response> resumeOthers(Requests.ThreadOperationArguments arguments, Response response, IDebugAdapterContext context) {
Expand Down Expand Up @@ -252,7 +260,7 @@ public static void checkThreadRunningAndRecycleIds(ThreadReference thread, IDebu
}
}

private void resumeVM(IDebugAdapterContext context) {
private static void resumeVM(IDebugAdapterContext context) {
List<ThreadReference> visibleThreads = context.getThreadCache().visibleThreads(context);
context.getThreadCache().clearEventThread();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

}
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*******************************************************************************
* 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.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;
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"));

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);

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();
debugSession.resume();
assertTrue("The debuggee did not reach the ready breakpoint.",
readyBreakpoint.await(10, TimeUnit.SECONDS));

DebugAdapterContext context = new DebugAdapterContext(new NoopProtocolServer(), 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));
} 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<Response> sendRequest(Request request) {
return CompletableFuture.completedFuture(null);
}

@Override
public CompletableFuture<Response> sendRequest(Request request, long timeout) {
return CompletableFuture.completedFuture(null);
}

@Override
public void sendEvent(DebugEvent event) {
}

@Override
public void sendResponse(Response response) {
}
}
}
Loading
Loading