[DAP] Server remains listening but does not accept subsequent connections after debugger disconnect
[DAP] Server remains listening but does not accept subsequent connections after debugger disconnect
Summary
When debugging JavaScript embedded in a long-running Java service, the first GraalVM DAP connection works. After ending that debugger session while leaving the host JVM running, another DAP connection establishes TCP successfully but never becomes operational. The DAP listening socket remains open, although the threads that accepted and handled the original connection have exited.
Environment
- Host JVM: Oracle GraalVM 25.3.4.1+1.1, Java 25.0.4.1 LTS, Java HotSpot(TM) 64-Bit Server VM, build
25.0.4.1+1-LTS-jvmci-25.3-b22. - Polyglot tools:
org.graalvm.tools:dap-tool:25.3.4.1, brought in throughorg.graalvm.polyglot:dap:25.3.4.1. - OS: Linux x86_64 (Ubuntu).
- Client: IntelliJ IDEA, a
GraalVM DAPdebug configuration connecting to localhost:4711. - Application: a long-running Java/Spring service embedding GraalJS, with a shared
org.graalvm.polyglot.Engine. - Java debugging and the DAP client are separate debug sessions in IDEA.
- JVM options relevant to debugging:
-Dpolyglot.dap=4711
-Dpolyglot.dap.Suspend=false
-Dpolyglot.inspect=9229
-Dpolyglot.inspect.Suspend=falseThe Inspector options were also enabled during the observation. This has not yet been retested with Inspector disabled or against a newer snapshot.
The shared Engine is constructed as follows:
Engine engine = Engine.newBuilder()
.allowExperimentalOptions(true)
.build();Steps to reproduce
These steps describe the observed application scenario; a standalone minimal reproducer has not yet been validated.
- Start a long-running Java application embedding GraalJS with a shared Engine and the DAP options above. Initialize the guest language so the DAP server is listening.
- Attach a DAP debugger to localhost:4711 and use the first session.
- End the DAP session in the IDE, leaving the host Java application and its Engine running.
- Start the same DAP attach configuration again, without restarting the application.
Expected behavior
For an attach/detach workflow, a subsequent debugger should be able to attach to the still-running application.
If reconnecting is intentionally unsupported, the server should at least close its listening socket when it stops accepting connections, so subsequent clients fail promptly rather than establishing TCP and waiting indefinitely.
Actual behavior and runtime evidence
- The host service remains running.
- The new IDE DAP session appears active but does not initialize successfully.
ss -ltnpshows localhost:4711 still inLISTEN, owned by the host JVM, withRecv-Q = 1and backlog 50.ss -tnpshows the new client connection inESTAB; the server side has 370 unread bytes and is not associated with an accepted process file descriptor in the output. This is consistent with a connection waiting in the listener's accept queue.- A
jcmd <pid> Thread.printdump contains neitherDAP client connection threadnorDAP request handler.
The TCP observations and absence of DAP threads indicate that the new connection has reached the listening socket but no application thread is accepting it.
Source-level findings
Inspected the source JAR shipped with dap-tool:25.3.4.1, specifically com.oracle.truffle.tools.dap.server.DebugProtocolServerImpl:
start(ServerSocket)(around lines 646–698) uses aOneTimeExecutorand invokesserverSocket.accept()exactly once. There is no accept loop. Once the first connection's listener finishes, the client socket is closed and the task returns, leaving the server socket open.- The server socket is closed by a callback registered with
onDispose(...). disconnect(...)(around lines 264–286) setsdisposed = true, closes the debugger session, and disposes the execution context, but does not run those server-disposal callbacks.dispose()(around lines 288–303) immediately returns ifdisposedis already true. This appears to prevent later disposal from running the registered server-socket cleanup after a protocol disconnect.
These findings suggest two related lifecycle problems: accepting only one client and leaving the listener open after disconnect. A reconnect fix likely needs fresh per-session debugger state as well as another accept; simply adding an accept loop may not be sufficient.
The exact DAP disconnect payload from the IDE was not captured, so the disconnect() cleanup path above is source analysis rather than a verified protocol trace.
Impact
The DAP debugger can only be used once per application run in this scenario. Restarting the IDE's DAP configuration alone does not restore debugging. The open listener makes the failure look like a hung connection rather than an unavailable debugger.
Is a single DAP client connection per Engine intentional? If so, could the listener be closed reliably on disconnect, and is there a supported way to re-enable DAP for an existing long-running Engine?
Source: oracle/graal