Repository navigation
Stop detaches the debugger before shutdown hooks run, so JVM shutdown cannot be debugged #1720
Description
Since #1274 / microsoft/java-debug#579, pressing Stop on a launched Java program (with "console": "internalConsole") sends SIGTERM through vm.process().destroy(), so shutdown hooks now run. However, the debugger detaches immediately afterwards, so breakpoints in shutdown hooks, @PreDestroy methods, Spring SmartLifecycle.stop(), etc. are never hit. As a result, the shutdown path of an application cannot be debugged from VS Code.
This is not a duplicate of #1274, #1585 or #1705. Those issues are about whether the JVM is terminated and whether the hooks run at all. This one is about the debug session staying attached while they run.
Environment
- Operating System: Linux Mint 22.1
- JDK version: 21.0.7
- Visual Studio Code version: 1.143.0
- Java extension version: 1.59.2026102108 (pre-release)
- Java Debugger extension version: 0.60.0
Steps To Reproduce
- Create a program:
public class App { public static void main(String[] args) throws Exception { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("hook start"); // breakpoint here System.out.println("hook end"); })); Thread.sleep(Long.MAX_VALUE); } }
- Launch it with a
launchconfiguration with"console": "internalConsole". - Set a breakpoint on the
hook startline. - Press Stop.
Current Result
The debug session ends immediately. The hook runs (hook start / hook end may or may not reach the Debug Console), but the breakpoint is not hit.
Expected Result
The first Stop asks the JVM to exit gracefully and keeps the debugger attached, so breakpoints in shutdown code are hit. The session ends when the JVM exits (VMDeath/VMDisconnect). A second Stop forcibly kills the JVM, as it does now. IntelliJ IDEA and Eclipse behave this way.
Additional Informations
Cause: DisconnectRequestHandler.destroyDebugSession does terminate() and then detach() in a finally block. Process.destroy() only sends the signal and does not wait for the process, so the debugger is gone before the hooks start. (Before microsoft/java-debug#626 the order was reversed explicitly.)
DAP already supports this two-step behaviour through the terminate request. If the adapter reports supportsTerminateRequest: true, VS Code sends terminate on the first Stop and disconnect on the next one. A possible implementation:
- report
supportsTerminateRequestin theinitializeresponse; - handle
terminatewithvm.process().destroy()(when aProcessis available) and do not detach; the existing VMDeath/VMDisconnect handling then sendsterminatedand ends the session; - keep the current
disconnectbehaviour (forced termination) for the second Stop and for restart.
I have checked this approach locally with a small extension. It uses a DebugAdapterTracker to add supportsTerminateRequest, sends SIGTERM to the PID from the processid event, and lets the JVM exit on its own. With it, breakpoints in shutdown hooks are hit, and the session closes when the JVM exits.
With "console": "integratedTerminal" the adapter has no Process and falls back to vm.exit(0), so the hooks do not run at all. That case may need the PID from the runInTerminal response, and could be handled separately.
Activity
issuelens commentedon Oct 22, 2026
Thanks for the detailed reproduction and analysis. The current debug-adapter path does call
terminate()and then immediately callsdetach()in afinallyblock. Whenterminate()usesProcess.destroy(), graceful JVM shutdown can begin while the debugger connection is disposed before shutdown-hook breakpoints are reached.We did not find a matching duplicate. #1274, #1585, and #1705 are related history, but they concern whether shutdown hooks run or whether Stop terminates the JVM; this report is specifically about remaining attached while shutdown code executes.
As a limited workaround, launch in a terminal and send Ctrl+C there instead of using Stop; this can let the JVM handle the signal while the debug session remains attached. The proposed two-stage DAP
terminate/disconnectapproach is relevant for maintainer evaluation, with terminal-launched processes likely needing separate handling.