Skip to content

Commit a2f6a40

Browse files
authored
Nix java debug plugin (#44)
1 parent 7004455 commit a2f6a40

5 files changed

Lines changed: 266 additions & 0 deletions

File tree

overlay.nix

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ in
2020
replbox = self.callPackage ./pkgs/replbox { };
2121

2222
jdt-language-server = self.callPackage ./pkgs/jdt-language-server { };
23+
java-debug = self.callPackage ./pkgs/java-debug {
24+
inherit jdt-language-server;
25+
};
26+
2327
rescript-language-server = self.callPackage ./pkgs/rescript-language-server { };
2428
nbcode = self.callPackage ./pkgs/nbcode { };
2529

pkgs/java-debug/debug-plugin.nix

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{ stdenv
2+
, maven
3+
, fetchFromGitHub
4+
, graalvm11-ce
5+
, callPackage
6+
}:
7+
let
8+
repository = callPackage ./repo.nix { };
9+
10+
version = "0.35.0";
11+
12+
in stdenv.mkDerivation {
13+
inherit version;
14+
name = "java-debug-plugin";
15+
16+
src = fetchFromGitHub {
17+
owner = "microsoft";
18+
repo = "java-debug";
19+
rev = "e6655ead412ceed5afa37b3781ed84e0b8ba425a";
20+
sha256 = "1syrzp8syisnd8fkj3lis5rv83chzj4gwm63ygib41c428yyw20a";
21+
};
22+
23+
buildInputs = [ maven graalvm11-ce ];
24+
buildPhase = ''
25+
# Maven tries to grab lockfiles in the repository, so it has to be writeable
26+
cp -a ${repository} ./repository
27+
chmod u+w -R ./repository
28+
${maven}/bin/mvn --offline -Dmaven.repo.local=./repository package
29+
'';
30+
31+
installPhase = ''
32+
mkdir -p $out/lib
33+
cp com.microsoft.java.debug.plugin/target/com.microsoft.java.debug.plugin-${version}.jar $out/lib/java-debug.jar
34+
'';
35+
}

pkgs/java-debug/default.nix

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{ stdenv
2+
, callPackage
3+
, makeWrapper
4+
, jdt-language-server
5+
}:
6+
let
7+
debug-plugin = callPackage ./debug-plugin.nix { };
8+
9+
in stdenv.mkDerivation {
10+
name = "java-debug";
11+
12+
unpackPhase = "true";
13+
dontBuild = true;
14+
15+
#"-Dcom.microsoft.java.debug.serverAddress=localhost:0"
16+
buildInputs = [ makeWrapper ];
17+
installPhase = ''
18+
mkdir -p $out/bin
19+
cp ${./java-dap} $out/bin/java-dap
20+
21+
makeWrapper $out/bin/java-dap $out/bin/java-debug \
22+
--add-flags --use-ephemeral-port \
23+
--add-flags --debug-plugin \
24+
--add-flags ${debug-plugin}/lib/java-debug.jar \
25+
--add-flags --language-server \
26+
--add-flags ${jdt-language-server}/bin/jdt-language-server
27+
'';
28+
}

pkgs/java-debug/java-dap

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/usr/bin/env python3
2+
"""Small wrapper to correctly initialize the Java DAP.
3+
4+
This launches the (normal) Java LSP and then tells it to initialize with the
5+
Java DAP plugin bundle. This causes the DAP plugin to bind to a TCP port, and
6+
once that's done, the communication with the DAP can start.
7+
8+
This is known to be flaky, so this retries until it succeeds.
9+
"""
10+
11+
import argparse
12+
import json
13+
import logging
14+
import os
15+
import signal
16+
import subprocess
17+
import sys
18+
import time
19+
20+
from typing import Any, IO, Dict, List, Mapping, Optional
21+
22+
23+
def _send_lsp_message(msg: Dict[str, Any], lsp: IO[bytes]) -> None:
24+
"""Sends one LSP message."""
25+
serialized_msg = json.dumps({
26+
'jsonrpc': '2.0',
27+
**msg,
28+
})
29+
payload = len(serialized_msg)
30+
lsp.write((f'Content-Length: {len(serialized_msg)}\r\n\r\n' +
31+
serialized_msg).encode('utf-8'))
32+
lsp.flush()
33+
34+
35+
def _receive_lsp_message(lsp: IO[bytes]) -> Optional[Dict[str, Any]]:
36+
"""Receives one LSP message."""
37+
headers = b''
38+
while not headers.endswith(b'\r\n\r\n'):
39+
byte = lsp.read(1)
40+
if len(byte) == 0:
41+
return None
42+
headers += byte
43+
content_length = 0
44+
for header in headers.strip().split(b'\r\n'):
45+
name, value = header.split(b':', maxsplit=2)
46+
if name.strip().lower() == b'content-length':
47+
content_length = int(value.strip())
48+
serialized = b''
49+
while content_length:
50+
chunk = lsp.read(content_length)
51+
if not chunk:
52+
raise Exception(f'short read: {serialized!r}')
53+
content_length -= len(chunk)
54+
serialized += chunk
55+
message: Dict[str, Any] = json.loads(serialized)
56+
return message
57+
58+
59+
def _run(use_ephemeral_port: bool, language_server: str, debug_plugin: str) -> bool:
60+
"""Attempts to start the DAP. Returns whether the caller should retry."""
61+
args = [language_server]
62+
if use_ephemeral_port:
63+
args.append('-Dcom.microsoft.java.debug.serverAddress=localhost:0')
64+
else:
65+
args.append('-Dcom.microsoft.java.debug.serverAddress=localhost:41010')
66+
67+
with subprocess.Popen(args,
68+
stdout=subprocess.PIPE,
69+
stdin=subprocess.PIPE,
70+
preexec_fn=os.setsid) as dap:
71+
try:
72+
_send_lsp_message(
73+
{
74+
'id': 1,
75+
'method': 'initialize',
76+
'params': {
77+
'processId': None,
78+
'initializationOptions': {
79+
'bundles': [
80+
debug_plugin,
81+
],
82+
},
83+
'trace': 'verbose',
84+
'capabilities': {},
85+
},
86+
}, dap.stdin)
87+
# Wait for the initialize message has been acknowledged.
88+
# This maximizes the probability of success.
89+
while True:
90+
message = _receive_lsp_message(dap.stdout)
91+
if not message:
92+
return True
93+
if message.get('method') == 'window/logMessage':
94+
print(message.get('params', {}).get('message'),
95+
file=sys.stderr)
96+
if message.get('id') == 1:
97+
break
98+
_send_lsp_message(
99+
{
100+
'id': 2,
101+
'method': 'workspace/executeCommand',
102+
'params': {
103+
'command': 'vscode.java.startDebugSession',
104+
},
105+
}, dap.stdin)
106+
# Wait for the reply. If the request errored out, exit early to
107+
# send a clear signal to the caller.
108+
while True:
109+
message = _receive_lsp_message(dap.stdout)
110+
if not message:
111+
return True
112+
if message.get('method') == 'window/logMessage':
113+
print(message.get('params', {}).get('message'),
114+
file=sys.stderr)
115+
if message.get('id') == 2:
116+
if 'error' in message:
117+
print(message['error'].get('message'), file=sys.stderr)
118+
# This happens often during the first launch before
119+
# things warm up.
120+
return True
121+
if use_ephemeral_port:
122+
with os.fdopen(3, 'w') as port_fd:
123+
port_fd.write(str(message['result']))
124+
break
125+
# If we reached this point, the LSP and DAP have both
126+
# successfully initialized.
127+
# Keep reading to drain the queue.
128+
while True:
129+
message = _receive_lsp_message(dap.stdout)
130+
if not message:
131+
break
132+
if message.get('method') == 'window/logMessage':
133+
print(message.get('params', {}).get('message'),
134+
file=sys.stderr)
135+
except Exception:
136+
logging.exception('failed')
137+
finally:
138+
pgrp = os.getpgid(dap.pid)
139+
os.killpg(pgrp, signal.SIGINT)
140+
return False
141+
142+
143+
def _main() -> None:
144+
parser = argparse.ArgumentParser(description=__doc__)
145+
# TODO: remove this flag and always use the ephemeral port.
146+
parser.add_argument(
147+
'--use-ephemeral-port',
148+
action='store_true',
149+
help='Use an ephemeral port and write the port number to fd 3')
150+
parser.add_argument(
151+
'--language-server',
152+
type=str,
153+
help='The language server to launch')
154+
parser.add_argument(
155+
'--debug-plugin',
156+
type=str,
157+
help='The path to the debug plugin')
158+
args = parser.parse_args()
159+
while True:
160+
retry = _run(args.use_ephemeral_port, args.language_server, args.debug_plugin)
161+
if not retry:
162+
break
163+
164+
165+
if __name__ == '__main__':
166+
_main()

pkgs/java-debug/repo.nix

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{ stdenv
2+
, maven
3+
, fetchFromGitHub
4+
, graalvm11-ce
5+
}:
6+
stdenv.mkDerivation {
7+
name = "java-debug-repo";
8+
src = fetchFromGitHub {
9+
owner = "microsoft";
10+
repo = "java-debug";
11+
rev = "e6655ead412ceed5afa37b3781ed84e0b8ba425a";
12+
sha256 = "1syrzp8syisnd8fkj3lis5rv83chzj4gwm63ygib41c428yyw20a";
13+
};
14+
15+
dontConfigure = true;
16+
buildInputs = [ maven graalvm11-ce ];
17+
buildPhase = "${maven}/bin/mvn -Dmaven.repo.local=$out package";
18+
19+
# keep only *.{pom,jar,sha1,nbm} and delete all ephemeral files with lastModified timestamps inside
20+
installPhase = ''
21+
echo $out
22+
23+
find $out -type f -name \*.lastUpdated -delete
24+
find $out -type f -name resolver-status.properties -delete
25+
find $out -type f -name _remote.repositories -delete
26+
'';
27+
28+
# don't do any fixup
29+
dontFixup = true;
30+
outputHashAlgo = "sha256";
31+
outputHashMode = "recursive";
32+
outputHash = "0lgqka9qlf9v685xrmb85rgrvwpi0sfqsx2z4zalgkqcsfhz8gb1";
33+
}

0 commit comments

Comments
 (0)