The adapter has since moved out of this repository: its source is the Java Dev Pack's
adapter/. Paths below are as they stood when this plan was carried out, and are left that way on purpose.
Status: DONE — shipped and verified end-to-end in the app UI. The
tools/java-dap module (7cb615e) compiles against the real
com.microsoft.java:com.microsoft.java.debug.core:0.53.1; the fat jar is
published as GitHub release java-dap-v1; and a full UI-first debug session on
the Odin2 (rebuilt app, v1.3.2) passed: set a gutter breakpoint in a .java
file → run-config Debug → javac compile → adapter launch/attach → Paused at
the breakpoint with current-line highlight → Continue re-hit the breakpoint on
every loop iteration (output streamed: i=1..5, done total=30) → clean
terminate (disconnected). Desktop and in-proot spikes also pass. Leg 1 (stdio)
- leg 2 (JDWP dt_socket under proot) both confirmed. v1 limitation: no
expression
evaluate/hover (long-press variable inspect returns nothing for Java —IEvaluationProvideris a no-op stub); variables show via the scopes/variables panel.
Three JVM/native adapters were investigated for "make it fully work under proot":
- netcoredbg (.NET) — dead-end. The debuggee launch fails under proot with
0x80070490(Element not found); a ptrace-based engine can't nest inside proot's ptrace sandbox (same class as gdb/lldb). Not fixable without moving off proot. Standalonedotnet App.dllruns fine — only debugging breaks. - js-debug (Node) — dead-end. It's TCP-only; the app→adapter loopback
connect()never completes (leg 1, see below). It cannot speak stdio, so there's no way around the broken leg. - java-debug (JVM) — viable, because the JVM adapter can speak DAP over stdio, which sidesteps the one broken loopback leg.
- Leg 1 — app JVM → adapter (proot child). This is what empirically fails
(the js-debug 12 s
TcpTransport.connecttimeout). Eliminated by using stdio pipes — already proven for debugpy / lldb-dap / netcoredbg viaProcessTransport. - Leg 2 — adapter → debuggee JVM (JDWP
dt_socket). This is proot-internal (child↔child), never touches the app JVM. debugpy already relies on exactly this class of proot-internal loopback and fully works on-device, so it is very likely fine when pinned to IPv4.
Strategy: DAP over stdio (leg 1 gone), JDWP dt_socket pinned to
127.0.0.1 + -Djava.net.preferIPv4Stack=true (leg 2 minimized).
Rank 1 (chosen): a small custom stdio adapter on com.microsoft.java.debug.core
(0.53.1). Ship a shaded fat jar jcode-java-dap.jar = java-debug-core + a
~200–400-line launcher dev.blamspot.jcode.javadap.Main:
new ProtocolServer(System.in, System.out, providerContext).run();providerContext (IProviderContext) must register 5 providers — getProvider
throws if one is missing:
IVirtualMachineManagerProvider→com.sun.jdi.Bootstrap.virtualMachineManager()(trivial passthrough).ISourceLookUpProvider→ the only substantive custom code. Map source file ↔ FQN using the file'spackage+ every top-level type declared (not just the public one), resolved against the configuredsourcePaths, and implementgetBreakpointLocations(). Bugs here surface as silently unverified breakpoints.IEvaluationProvider,IHotCodeReplaceProvider,ICompletionsProvider→ unsupported stubs.
With console=internalConsole, java-debug-core self-spawns the debuggee JVM
(JDI LaunchingConnector = com.sun.tools.jdi.SunCommandLineLauncher) with
-agentlib:jdwp and attaches — no Eclipse JDT LS needed.
Rank 2 (rejected): the "official" java-debug via eclipse.jdt.ls + TCP.
Returns a TCP port → forces the broken leg-1 TCP connect, and drags in full
jdt.ls (heavy, wants a workspace model, hostile to plain javac files).
Rank 3 (fallback): hand-rolled com.sun.jdi ↔ DAP stdio adapter. Only if
core's handler registration proves too entangled.
core/distro/.../DebugEngineModels.kt—java-debugentry replaced:dapAdapter=true,transport="stdio",debugType="java",adapterCommand="java -Djava.net.preferIPv4Stack=true -cp \"$HOME/java-dap/jcode-java-dap.jar\" dev.blamspot.jcode.javadap.Main",requiredSdks=["jdk"], Java-only (.java). FlippingdapAdapterfalse→true makes the fail-fast block atDebugController.kt:90no longer trip.app/.../debug/DebugController.kt— new"java" ->branch inprepareLaunchcallingprepareJava(), which: detectsmainClass(package +public static void main(String[])), computes the source root by stripping the package path,javac -g -encoding UTF-8 -d /tmp/jcode-java-classesover the tree, and returns a stdioLaunchPlan(tcpPort=null) withmainClass,classPaths,sourcePaths,vmArgs=-Djava.net.preferIPv4Stack=true.
Resulting DAP launch request:
{
"type": "java", "request": "launch", "name": "JCode Debug",
"cwd": "<distroCwd>", "console": "internalConsole", "stopOnEntry": false,
"mainClass": "com.example.Program",
"classPaths": ["/tmp/jcode-java-classes"],
"sourcePaths": ["/…/src"],
"vmArgs": "-Djava.net.preferIPv4Stack=true",
"args": ""
}Concrete mainClass + classPaths are mandatory — $Auto/$Runtime/empty
resolution lives in the JDT plugin we don't ship — which is exactly what
prepareJava produces.
— DONE (7cb615e). Note: the Maven groupId istools/java-dapGradle modulecom.microsoft.java(not.debug); all five providers are mandatory (SetBreakpointsRequestHandler.initialize()subscribes toIHotCodeReplaceProvider.getEventHub()in theDebugAdapterconstructor);--add-modules jdk.jdiat compile time.De-risk with a desktop JVM spike— DONE, PASS. A HelloWorld breakpoint over stdio binds (verified:true) and hits; call stack, live variables, loop re-hits oncontinue, output streaming, clean terminate all work — via both classpath andjava -jar jcode-java-dap.jar. The trailing RxJava VMDisconnect stack trace is java-debug-core's benign teardown noise (fires afterterminated). The main engineering unknown (core handlers + providers outside the JDT plugin) is resolved.On-device leg-2 verification— DONE, PASS (Odin2, ubuntu-24.04 guest, openjdk-21-jdk-headless). The same stdio spike run inside proot against the real fat jar hit the breakpoint (HelloWorld.main:5,total=0/i=1), re-hit across loop iterations oncontinue, streamed output, and exited cleanly. The adapter→debuggee JDWPdt_socketloopback works under proot — no attach-mode fallback needed. Leg 1 (stdio) + leg 2 (JDWP) both confirmed on-device.Publish the shaded jar as a GitHub release asset— DONE. Published asjava-dap-v1on blamspotdev/j-code-android; the cataloginstallCommandURL resolves and the downloaded jar's sha256 matches the built artifact.Rebuild the app + final UI-first debug test— DONE, PASS (see Status). The signed release APK (matching keystore,install -r, runtime preserved) was verified with a live breakpoint-pause + loop-debug session in the UI.
- No
evaluate/hover for Java (IEvaluationProviderno-op stub): the editor long-press variable-inspect popup returns nothing for.java. Variables are available via scopes/variables. A real evaluation provider is a future item. - java-debug's
requiredSdks=["jdk"]pulls in thejdktoolchain entry, which also downloads ktlint (~50 MB) for Kotlin linting — unnecessary for Java debugging and slow on throttled links. Consider a leaner JDK-only requirement (or make the ktlint step non-fatal) so the Install button completes quickly. (The adapter itself only needsjava/javacon PATH.) - Kotlin (
.kt) support: deferred (kotlinc class-name mapping differs).
- Leg-2 loopback — the one that could still block it. Leg 1 is eliminated by
stdio. Leg 2 is proot-internal
dt_socket; debugpy proves this class works on-device, and IPv4-pinning both JVMs is the mitigation. Residual risk: java-debug's connector could bind a host/IPv6 thatpreferIPv4Stackdoesn't fully override, reproducing the js-debug stall. Fallback: attach mode — JCode spawns the debuggee withaddress=127.0.0.1:<port>,server=y,suspend=yand the adapter sends a DAPattachto that literal port. Same source-lookup provider, so it's a transport fallback, not a redesign. Must be verified on-device with a HelloWorld breakpoint before calling it done. - java-debug-core standalone handler registration — see spike above.
- Kotlin (
.kt) — kotlinc class-name mapping (…Kt, synthetic/inline names) breaks the naive source→FQN provider; kotlinc isn't in thejdkentry. Java-only for v1; Kotlin is a follow-up. evaluate/hover —IEvaluationProvideris JDT-coupled; the editor's long-press inspect returns null for Java in v1. Acceptable; document it.- Two JVMs under proot — memory pressure; consider a debuggee
-Xmxcap (analogous to the .NET GC cap inprepareDotnet) if devices OOM.
- The
jdkSDK catalog entry (catalog.yaml:60) is fine — it uses forward slashes (/tmp/ktlint). An earlier research pass claimed a\tmp\ktlintbackslash bug; that was a misread. No catalog.yaml change is needed. - No
extraPathis needed —openjdk-21-jdk-headlessputsjava/javacon the default PATHspawnDapProcessalready sets. Headless is sufficient: it keepsjdk.jdi(com.sun.jdi) and the jdwp agent; only AWT/GUI is dropped. A bare JRE would not work.