Skip to content

Stop detaches the debugger before shutdown hooks run, so JVM shutdown cannot be debugged #1720

Open

Description

@danilaslynko

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
  1. 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);
        }
    }
  2. Launch it with a launch configuration with "console": "internalConsole".
  3. Set a breakpoint on the hook start line.
  4. 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 supportsTerminateRequest in the initialize response;
  • handle terminate with vm.process().destroy() (when a Process is available) and do not detach; the existing VMDeath/VMDisconnect handling then sends terminated and ends the session;
  • keep the current disconnect behaviour (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

  1. issuelens commented on Oct 22, 2026

    @issuelens

    Thanks for the detailed reproduction and analysis. The current debug-adapter path does call terminate() and then immediately calls detach() in a finally block. When terminate() uses Process.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/disconnect approach is relevant for maintainer evaluation, with terminal-launched processes likely needing separate handling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions