Skip to content

Commit 25afffa

Browse files
authored
Merge branch 'main' into wenyt/codeowner
2 parents 42a2209 + 295079a commit 25afffa

File tree

8 files changed

+548
-0
lines changed

8 files changed

+548
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Java No-Config Debug
2+
3+
This feature enables configuration-less debugging for Java applications, similar to the JavaScript Debug Terminal in VS Code.
4+
5+
## How It Works
6+
7+
When you open a terminal in VS Code with this extension installed, the following environment variables are automatically set:
8+
9+
- `VSCODE_JDWP_ADAPTER_ENDPOINTS`: Path to a communication file for port exchange
10+
- `PATH`: Includes the `debugjava` command wrapper
11+
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 `debugjava` command.
13+
14+
## Usage
15+
16+
### Basic Usage
17+
18+
Instead of running:
19+
```bash
20+
java -cp . com.example.Main
21+
```
22+
23+
Simply run:
24+
```bash
25+
debugjava -cp . com.example.Main
26+
```
27+
28+
The debugger will automatically attach, and breakpoints will work without any launch.json configuration!
29+
30+
### Maven Projects
31+
32+
```bash
33+
debugjava -jar target/myapp.jar
34+
```
35+
36+
### Gradle Projects
37+
38+
```bash
39+
debugjava -jar build/libs/myapp.jar
40+
```
41+
42+
### With Arguments
43+
44+
```bash
45+
debugjava -cp . com.example.Main arg1 arg2 --flag=value
46+
```
47+
48+
### Spring Boot
49+
50+
```bash
51+
debugjava -jar myapp.jar --spring.profiles.active=dev
52+
```
53+
54+
## Advantages
55+
56+
1. **No Configuration Required**: No need to create or maintain launch.json
57+
2. **Rapid Prototyping**: Perfect for quick debugging sessions
58+
3. **Script Debugging**: Debug applications launched by complex shell scripts
59+
4. **Environment Consistency**: Inherits all terminal environment variables
60+
5. **Parameter Flexibility**: Easy to change arguments using terminal history (↑ key)
61+
62+
## How It Works Internally
63+
64+
1. When you run `debugjava`, the wrapper script temporarily sets `JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0`
65+
2. The wrapper determines which Java executable to use (priority order):
66+
- First: `JAVA_HOME/bin/java` if JAVA_HOME environment variable is set (user's explicit choice)
67+
- Second: `VSCODE_JAVA_EXEC` environment variable (Java path from VS Code's Java Language Server)
68+
- Third: `java` command from system PATH
69+
3. The wrapper launches the Java process with JDWP enabled
70+
4. JVM starts and outputs: "Listening for transport dt_socket at address: 12345"
71+
5. The wrapper captures the JDWP port from this output
72+
6. The port is written to a communication file
73+
7. VS Code's file watcher detects the file and automatically starts an attach debug session
74+
75+
## Troubleshooting
76+
77+
### Port Already in Use
78+
79+
If you see "Address already in use", another Java debug session is running. Terminate it first.
80+
81+
### No Breakpoints Hit
82+
83+
1. Ensure you're running with `debugjava` command (not plain `java`)
84+
2. Check that the `debugjava` command is available: `which debugjava` (Unix) or `Get-Command debugjava` (PowerShell)
85+
3. Verify the terminal was opened AFTER the extension activated
86+
4. Check the Debug Console for error messages
87+
88+
### Node.js Not Found
89+
90+
The wrapper script requires Node.js to be installed and available in PATH.
91+
92+
## Limitations
93+
94+
- Requires Node.js to be installed and available in PATH
95+
- Only works in terminals opened within VS Code
96+
- Requires using the `debugjava` command instead of `java`
97+
- The Java process will suspend (hang) until the debugger attaches
98+
99+
## See Also
100+
101+
- [Debugger for Java Documentation](https://github.com/microsoft/vscode-java-debug)
102+
- [JDWP Documentation](https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/jdwp-spec.html)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/bash
2+
# Java No-Config Debug Wrapper Script for Unix/Linux/macOS
3+
# This script intercepts java commands and automatically enables JDWP debugging
4+
5+
# Export the endpoint file path for JDWP port communication
6+
export JDWP_ADAPTER_ENDPOINTS=$VSCODE_JDWP_ADAPTER_ENDPOINTS
7+
8+
# Set JDWP options only for this debugjava 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+
12+
# Get the directory of this script
13+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
14+
15+
# Use Node.js wrapper to capture JDWP port
16+
exec node "$SCRIPT_DIR/jdwp-wrapper.js" "$@"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@echo off
2+
REM Java No-Config Debug Wrapper Script for Windows
3+
REM This script intercepts java commands and automatically enables JDWP debugging
4+
5+
REM Export the endpoint file path for JDWP port communication
6+
set JDWP_ADAPTER_ENDPOINTS=%VSCODE_JDWP_ADAPTER_ENDPOINTS%
7+
8+
REM Set JDWP options only for this debugjava 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+
12+
REM Use Node.js wrapper to capture JDWP port
13+
node "%~dp0jdwp-wrapper.js" %*
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env fish
2+
# Java No-Config Debug Wrapper Script for Fish Shell
3+
# This script intercepts java commands and automatically enables JDWP debugging
4+
5+
# Export the endpoint file path for JDWP port communication
6+
set -x JDWP_ADAPTER_ENDPOINTS $VSCODE_JDWP_ADAPTER_ENDPOINTS
7+
8+
# Set JDWP options only for this debugjava invocation
9+
# This overrides the global JAVA_TOOL_OPTIONS to avoid affecting other Java processes
10+
set -x JAVA_TOOL_OPTIONS "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0"
11+
12+
# Get the directory of this script
13+
set script_dir (dirname (status -f))
14+
15+
# Use Node.js wrapper to capture JDWP port
16+
exec node "$script_dir/jdwp-wrapper.js" $argv
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Java No-Config Debug Wrapper Script for PowerShell
2+
# This script intercepts java commands and automatically enables JDWP debugging
3+
4+
# Export the endpoint file path for JDWP port communication
5+
$env:JDWP_ADAPTER_ENDPOINTS = $env:VSCODE_JDWP_ADAPTER_ENDPOINTS
6+
7+
# Set JDWP options only for this debugjava 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+
11+
# Get the directory of this script
12+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
13+
14+
# Use Node.js wrapper to capture JDWP port
15+
& node (Join-Path $scriptDir "jdwp-wrapper.js") $args
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env node
2+
/**
3+
* JDWP Port Listener and Communication Wrapper
4+
*
5+
* This script wraps Java process execution and captures the JDWP port
6+
* from the JVM output, then writes it to the endpoint file for VS Code
7+
* to pick up and attach the debugger.
8+
*
9+
* JDWP Output Format:
10+
* "Listening for transport dt_socket at address: 12345"
11+
*/
12+
13+
const { spawn } = require('child_process');
14+
const fs = require('fs');
15+
const path = require('path');
16+
17+
// Get environment variables
18+
const endpointFile = process.env.JDWP_ADAPTER_ENDPOINTS || process.env.VSCODE_JDWP_ADAPTER_ENDPOINTS;
19+
const javaToolOptions = process.env.JAVA_TOOL_OPTIONS || '';
20+
21+
// Check if debugging is enabled
22+
const isDebugEnabled = javaToolOptions.includes('jdwp') && endpointFile;
23+
24+
// Helper function to find java command
25+
function getJavaCommand() {
26+
// Priority 1: Try JAVA_HOME environment variable first (user's explicit choice)
27+
const javaHome = process.env.JAVA_HOME;
28+
if (javaHome) {
29+
const javaPath = path.join(javaHome, 'bin', 'java');
30+
const javaPathExe = process.platform === 'win32' ? `${javaPath}.exe` : javaPath;
31+
32+
// Check if the file exists
33+
if (fs.existsSync(javaPathExe)) {
34+
return javaPath;
35+
}
36+
if (fs.existsSync(javaPath)) {
37+
return javaPath;
38+
}
39+
40+
console.warn(`[Java Debug] JAVA_HOME is set to '${javaHome}', but java command not found there. Falling back to VS Code's Java.`);
41+
}
42+
43+
// Priority 2: Use VSCODE_JAVA_EXEC if provided by VS Code (from Java Language Server)
44+
const vscodeJavaExec = process.env.VSCODE_JAVA_EXEC;
45+
if (vscodeJavaExec && fs.existsSync(vscodeJavaExec)) {
46+
return vscodeJavaExec;
47+
}
48+
49+
// Priority 3: Fall back to 'java' in PATH
50+
return 'java';
51+
}
52+
53+
const javaCmd = getJavaCommand();
54+
55+
if (!isDebugEnabled) {
56+
// No debugging, just run java normally
57+
const child = spawn(javaCmd, process.argv.slice(2), {
58+
stdio: 'inherit',
59+
shell: false
60+
});
61+
child.on('exit', (code) => process.exit(code || 0));
62+
child.on('error', (err) => {
63+
console.error(`[Java Debug] Failed to start java: ${err.message}`);
64+
console.error(`[Java Debug] Make sure Java is installed and either JAVA_HOME is set correctly or 'java' is in your PATH.`);
65+
process.exit(1);
66+
});
67+
} else {
68+
// Debugging enabled, capture JDWP port
69+
const child = spawn(javaCmd, process.argv.slice(2), {
70+
stdio: ['inherit', 'pipe', 'pipe'],
71+
shell: false
72+
});
73+
74+
let portCaptured = false;
75+
const jdwpPortRegex = /Listening for transport dt_socket at address:\s*(\d+)/;
76+
77+
// Shared function to capture JDWP port from output
78+
const capturePort = (output) => {
79+
if (portCaptured) return;
80+
81+
const match = output.match(jdwpPortRegex);
82+
if (match && match[1]) {
83+
const port = parseInt(match[1], 10);
84+
85+
// Validate port range
86+
if (port < 1 || port > 65535) {
87+
console.error(`[Java Debug] Invalid port number: ${port}`);
88+
return;
89+
}
90+
91+
console.log(`[Java Debug] Captured JDWP port: ${port}`);
92+
93+
// Write port to endpoint file
94+
const endpointData = JSON.stringify({
95+
client: {
96+
host: 'localhost',
97+
port: port
98+
}
99+
});
100+
101+
try {
102+
fs.writeFileSync(endpointFile, endpointData, 'utf8');
103+
console.log(`[Java Debug] Wrote endpoint file: ${endpointFile}`);
104+
portCaptured = true;
105+
} catch (err) {
106+
console.error(`[Java Debug] Failed to write endpoint file: ${err}`);
107+
}
108+
}
109+
};
110+
111+
// Monitor stdout for JDWP port
112+
child.stdout.on('data', (data) => {
113+
const output = data.toString();
114+
process.stdout.write(data);
115+
capturePort(output);
116+
});
117+
118+
// Monitor stderr for JDWP port (it might appear on stderr)
119+
child.stderr.on('data', (data) => {
120+
const output = data.toString();
121+
process.stderr.write(data);
122+
capturePort(output);
123+
});
124+
125+
child.on('exit', (code) => process.exit(code || 0));
126+
child.on('error', (err) => {
127+
console.error(`[Java Debug] Failed to start java: ${err}`);
128+
process.exit(1);
129+
});
130+
}

src/extension.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { HCR_EVENT, JAVA_LANGID, TELEMETRY_EVENT, USER_NOTIFICATION_EVENT } from
1313
import { NotificationBar } from "./customWidget";
1414
import { initializeCodeLensProvider, startDebugging } from "./debugCodeLensProvider";
1515
import { initExpService } from "./experimentationService";
16+
import { registerNoConfigDebug } from "./noConfigDebugInit";
1617
import { handleHotCodeReplaceCustomEvent, initializeHotCodeReplace, NO_BUTTON, YES_BUTTON } from "./hotCodeReplace";
1718
import { JavaDebugAdapterDescriptorFactory } from "./javaDebugAdapterDescriptorFactory";
1819
import { JavaInlineValuesProvider } from "./JavaInlineValueProvider";
@@ -31,6 +32,14 @@ import { promisify } from "util";
3132
export async function activate(context: vscode.ExtensionContext): Promise<any> {
3233
await initializeFromJsonFile(context.asAbsolutePath("./package.json"));
3334
await initExpService(context);
35+
36+
// Register No-Config Debug functionality
37+
const noConfigDisposable = await registerNoConfigDebug(
38+
context.environmentVariableCollection,
39+
context.extensionPath
40+
);
41+
context.subscriptions.push(noConfigDisposable);
42+
3443
return instrumentOperation("activation", initializeExtension)(context);
3544
}
3645

0 commit comments

Comments
 (0)