Skip to content

Commit e1ab7c0

Browse files
committed
feat: no config debug support
1 parent 994f3d4 commit e1ab7c0

File tree

6 files changed

+133
-69
lines changed

6 files changed

+133
-69
lines changed

bundled/scripts/noConfigScripts/README.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ This feature enables configuration-less debugging for Java applications, similar
66

77
When you open a terminal in VS Code with this extension installed, the following environment variables are automatically set:
88

9-
- `JAVA_TOOL_OPTIONS`: Configured with JDWP to enable debugging on a random port
109
- `VSCODE_JDWP_ADAPTER_ENDPOINTS`: Path to a communication file for port exchange
1110
- `PATH`: Includes the `javadebug` command wrapper
1211

12+
Note: `JAVA_TOOL_OPTIONS` is NOT set globally to avoid affecting other Java tools (javac, maven, gradle). Instead, it's set only when you run the `javadebug` command.
13+
1314
## Usage
1415

1516
### Basic Usage
@@ -60,11 +61,12 @@ javadebug -jar myapp.jar --spring.profiles.active=dev
6061

6162
## How It Works Internally
6263

63-
1. The extension sets `JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0,quiet=y`
64-
2. When you run `javadebug`, it wraps the Java process
65-
3. The wrapper captures the JDWP port from JVM output: "Listening for transport dt_socket at address: 12345"
66-
4. The port is written to a communication file
67-
5. VS Code's file watcher detects the file and automatically starts an attach debug session
64+
1. When you run `javadebug`, the wrapper script temporarily sets `JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0`
65+
2. The wrapper launches the Java process with JDWP enabled
66+
3. JVM starts and outputs: "Listening for transport dt_socket at address: 12345"
67+
4. The wrapper captures the JDWP port from this output
68+
5. The port is written to a communication file
69+
6. VS Code's file watcher detects the file and automatically starts an attach debug session
6870

6971
## Troubleshooting
7072

@@ -74,19 +76,21 @@ If you see "Address already in use", another Java debug session is running. Term
7476

7577
### No Breakpoints Hit
7678

77-
1. Ensure you're running with `javadebug` command
78-
2. Check that JAVA_TOOL_OPTIONS is set in your terminal
79+
1. Ensure you're running with `javadebug` command (not plain `java`)
80+
2. Check that the `javadebug` command is available: `which javadebug` (Unix) or `Get-Command javadebug` (PowerShell)
7981
3. Verify the terminal was opened AFTER the extension activated
82+
4. Check the Debug Console for error messages
8083

8184
### Node.js Not Found
8285

8386
The wrapper script requires Node.js to be installed and available in PATH.
8487

8588
## Limitations
8689

87-
- Requires Node.js to be installed
90+
- Requires Node.js to be installed and available in PATH
8891
- Only works in terminals opened within VS Code
89-
- Cannot debug applications that override JAVA_TOOL_OPTIONS
92+
- Requires using the `javadebug` command instead of `java`
93+
- The Java process will suspend (hang) until the debugger attaches
9094

9195
## See Also
9296

bundled/scripts/noConfigScripts/javadebug

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
# Export the endpoint file path for JDWP port communication
66
export JDWP_ADAPTER_ENDPOINTS=$VSCODE_JDWP_ADAPTER_ENDPOINTS
77

8+
# Set JDWP options only for this javadebug invocation
9+
# This overrides the global JAVA_TOOL_OPTIONS to avoid affecting other Java processes
10+
export JAVA_TOOL_OPTIONS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0"
11+
812
# Get the directory of this script
913
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
1014

bundled/scripts/noConfigScripts/javadebug.bat

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,9 @@ REM This script intercepts java commands and automatically enables JDWP debuggin
55
REM Export the endpoint file path for JDWP port communication
66
set JDWP_ADAPTER_ENDPOINTS=%VSCODE_JDWP_ADAPTER_ENDPOINTS%
77

8+
REM Set JDWP options only for this javadebug invocation
9+
REM This overrides the global JAVA_TOOL_OPTIONS to avoid affecting other Java processes
10+
set JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0
11+
812
REM Use Node.js wrapper to capture JDWP port
913
node "%~dp0jdwp-wrapper.js" %*

bundled/scripts/noConfigScripts/javadebug.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
# Export the endpoint file path for JDWP port communication
55
$env:JDWP_ADAPTER_ENDPOINTS = $env:VSCODE_JDWP_ADAPTER_ENDPOINTS
66

7+
# Set JDWP options only for this javadebug invocation
8+
# This overrides the global JAVA_TOOL_OPTIONS to avoid affecting other Java processes
9+
$env:JAVA_TOOL_OPTIONS = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0"
10+
711
# Get the directory of this script
812
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
913

bundled/scripts/noConfigScripts/jdwp-wrapper.js

Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -43,64 +43,52 @@ if (!isDebugEnabled) {
4343
let portCaptured = false;
4444
const jdwpPortRegex = /Listening for transport dt_socket at address:\s*(\d+)/;
4545

46+
// Shared function to capture JDWP port from output
47+
const capturePort = (output) => {
48+
if (portCaptured) return;
49+
50+
const match = output.match(jdwpPortRegex);
51+
if (match && match[1]) {
52+
const port = parseInt(match[1], 10);
53+
54+
// Validate port range
55+
if (port < 1 || port > 65535) {
56+
console.error(`[Java Debug] Invalid port number: ${port}`);
57+
return;
58+
}
59+
60+
console.log(`[Java Debug] Captured JDWP port: ${port}`);
61+
62+
// Write port to endpoint file
63+
const endpointData = JSON.stringify({
64+
client: {
65+
host: 'localhost',
66+
port: port
67+
}
68+
});
69+
70+
try {
71+
fs.writeFileSync(endpointFile, endpointData, 'utf8');
72+
console.log(`[Java Debug] Wrote endpoint file: ${endpointFile}`);
73+
portCaptured = true;
74+
} catch (err) {
75+
console.error(`[Java Debug] Failed to write endpoint file: ${err}`);
76+
}
77+
}
78+
};
79+
4680
// Monitor stdout for JDWP port
4781
child.stdout.on('data', (data) => {
4882
const output = data.toString();
4983
process.stdout.write(data);
50-
51-
if (!portCaptured) {
52-
const match = output.match(jdwpPortRegex);
53-
if (match && match[1]) {
54-
const port = parseInt(match[1], 10);
55-
console.log(`[Java Debug] Captured JDWP port: ${port}`);
56-
57-
// Write port to endpoint file
58-
const endpointData = JSON.stringify({
59-
client: {
60-
host: 'localhost',
61-
port: port
62-
}
63-
});
64-
65-
try {
66-
fs.writeFileSync(endpointFile, endpointData, 'utf8');
67-
console.log(`[Java Debug] Wrote endpoint file: ${endpointFile}`);
68-
portCaptured = true;
69-
} catch (err) {
70-
console.error(`[Java Debug] Failed to write endpoint file: ${err}`);
71-
}
72-
}
73-
}
84+
capturePort(output);
7485
});
7586

76-
// Monitor stderr
87+
// Monitor stderr for JDWP port (it might appear on stderr)
7788
child.stderr.on('data', (data) => {
7889
const output = data.toString();
7990
process.stderr.write(data);
80-
81-
// JDWP message might appear on stderr
82-
if (!portCaptured) {
83-
const match = output.match(jdwpPortRegex);
84-
if (match && match[1]) {
85-
const port = parseInt(match[1], 10);
86-
console.log(`[Java Debug] Captured JDWP port: ${port}`);
87-
88-
const endpointData = JSON.stringify({
89-
client: {
90-
host: 'localhost',
91-
port: port
92-
}
93-
});
94-
95-
try {
96-
fs.writeFileSync(endpointFile, endpointData, 'utf8');
97-
console.log(`[Java Debug] Wrote endpoint file: ${endpointFile}`);
98-
portCaptured = true;
99-
} catch (err) {
100-
console.error(`[Java Debug] Failed to write endpoint file: ${err}`);
101-
}
102-
}
103-
}
91+
capturePort(output);
10492
});
10593

10694
child.on('exit', (code) => process.exit(code || 0));

src/noConfigDebugInit.ts

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,22 @@ export async function registerNoConfigDebug(
4949
if (!fs.existsSync(tempDirPath)) {
5050
fs.mkdirSync(tempDirPath, { recursive: true });
5151
} else {
52-
// remove endpoint file in the temp directory if it exists
52+
// remove endpoint file in the temp directory if it exists (async to avoid blocking)
5353
if (fs.existsSync(tempFilePath)) {
54-
fs.unlinkSync(tempFilePath);
54+
fs.promises.unlink(tempFilePath).catch((err) => {
55+
console.error(`[Java Debug] Failed to cleanup old endpoint file: ${err}`);
56+
});
5557
}
5658
}
5759

5860
// clear the env var collection to remove any existing env vars
5961
collection.clear();
6062

61-
// Add env vars for VSCODE_JDWP_ADAPTER_ENDPOINTS and JAVA_TOOL_OPTIONS
63+
// Add env var for VSCODE_JDWP_ADAPTER_ENDPOINTS
64+
// Note: We do NOT set JAVA_TOOL_OPTIONS globally to avoid affecting all Java processes
65+
// (javac, maven, gradle, language server, etc.). Instead, JAVA_TOOL_OPTIONS is set
66+
// only in the javadebug wrapper scripts (javadebug.ps1, javadebug.bat, javadebug)
6267
collection.replace('VSCODE_JDWP_ADAPTER_ENDPOINTS', tempFilePath);
63-
64-
// Configure JDWP to listen on a random port and suspend until debugger attaches
65-
// quiet=y prevents the "Listening for transport..." message from appearing in terminal
66-
collection.replace('JAVA_TOOL_OPTIONS',
67-
'-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0,quiet=y');
6868

6969
const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts');
7070
const pathSeparator = process.platform === 'win32' ? ';' : ':';
@@ -81,10 +81,19 @@ export async function registerNoConfigDebug(
8181
new vscode.RelativePattern(tempDirPath, '**/*.txt')
8282
);
8383

84-
const fileCreationEvent = fileSystemWatcher.onDidCreate(async (uri) => {
84+
// Track active debug sessions to prevent duplicates
85+
const activeDebugSessions = new Set<number>();
86+
87+
// Handle both file creation and modification to support multiple runs
88+
const handleEndpointFile = async (uri: vscode.Uri) => {
8589
console.log('[Java Debug] No-config debug session detected');
8690

8791
const filePath = uri.fsPath;
92+
93+
// Add a small delay to ensure file is fully written
94+
// File system events can fire before write is complete
95+
await new Promise(resolve => setTimeout(resolve, 100));
96+
8897
fs.readFile(filePath, (err, data) => {
8998
if (err) {
9099
console.error(`[Java Debug] Error reading endpoint file: ${err}`);
@@ -94,8 +103,31 @@ export async function registerNoConfigDebug(
94103
// parse the client port
95104
const dataParse = data.toString();
96105
const jsonData = JSON.parse(dataParse);
97-
const clientPort = jsonData.client?.port;
106+
107+
// Validate JSON structure
108+
if (!jsonData || typeof jsonData !== 'object' || !jsonData.client) {
109+
console.error(`[Java Debug] Invalid endpoint file format: ${dataParse}`);
110+
return;
111+
}
112+
113+
const clientPort = jsonData.client.port;
114+
115+
// Validate port number
116+
if (!clientPort || typeof clientPort !== 'number' || clientPort < 1 || clientPort > 65535) {
117+
console.error(`[Java Debug] Invalid port number: ${clientPort}`);
118+
return;
119+
}
120+
121+
// Check if we already have an active session for this port
122+
if (activeDebugSessions.has(clientPort)) {
123+
console.log(`[Java Debug] Debug session already active for port ${clientPort}, skipping`);
124+
return;
125+
}
126+
98127
console.log(`[Java Debug] Parsed JDWP port: ${clientPort}`);
128+
129+
// Mark this port as active
130+
activeDebugSessions.add(clientPort);
99131

100132
const options: vscode.DebugSessionOptions = {
101133
noDebug: false,
@@ -116,24 +148,52 @@ export async function registerNoConfigDebug(
116148
(started) => {
117149
if (started) {
118150
console.log('[Java Debug] Successfully started no-config debug session');
151+
// Clean up the endpoint file after successful debug session start (async)
152+
if (fs.existsSync(filePath)) {
153+
fs.promises.unlink(filePath).then(() => {
154+
console.log('[Java Debug] Cleaned up endpoint file');
155+
}).catch((cleanupErr) => {
156+
console.error(`[Java Debug] Failed to cleanup endpoint file: ${cleanupErr}`);
157+
});
158+
}
119159
} else {
120160
console.error('[Java Debug] Error starting debug session, session not started.');
161+
// Remove from active sessions on failure
162+
activeDebugSessions.delete(clientPort);
121163
}
122164
},
123165
(error) => {
124166
console.error(`[Java Debug] Error starting debug session: ${error}`);
167+
// Remove from active sessions on error
168+
activeDebugSessions.delete(clientPort);
125169
},
126170
);
127171
} catch (parseErr) {
128172
console.error(`[Java Debug] Error parsing JSON: ${parseErr}`);
129173
}
130174
});
175+
};
176+
177+
// Listen for both file creation and modification events
178+
const fileCreationEvent = fileSystemWatcher.onDidCreate(handleEndpointFile);
179+
const fileChangeEvent = fileSystemWatcher.onDidChange(handleEndpointFile);
180+
181+
// Clean up active sessions when debug session ends
182+
const debugSessionEndListener = vscode.debug.onDidTerminateDebugSession((session) => {
183+
if (session.name === 'Attach to Java (No-Config)' && session.configuration.port) {
184+
const port = session.configuration.port;
185+
activeDebugSessions.delete(port);
186+
console.log(`[Java Debug] Debug session ended for port ${port}`);
187+
}
131188
});
132189

133190
return Promise.resolve(
134191
new vscode.Disposable(() => {
135192
fileSystemWatcher.dispose();
136193
fileCreationEvent.dispose();
194+
fileChangeEvent.dispose();
195+
debugSessionEndListener.dispose();
196+
activeDebugSessions.clear();
137197
}),
138198
);
139199
}

0 commit comments

Comments
 (0)