Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions bundled/scripts/noConfigScripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Java No-Config Debug

This feature enables configuration-less debugging for Java applications, similar to the JavaScript Debug Terminal in VS Code.

## How It Works

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

- `VSCODE_JDWP_ADAPTER_ENDPOINTS`: Path to a communication file for port exchange
- `PATH`: Includes the `debugjava` command wrapper

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.

## Usage

### Basic Usage

Instead of running:
```bash
java -cp . com.example.Main
```

Simply run:
```bash
debugjava -cp . com.example.Main
```

The debugger will automatically attach, and breakpoints will work without any launch.json configuration!

### Maven Projects

```bash
debugjava -jar target/myapp.jar
```

### Gradle Projects

```bash
debugjava -jar build/libs/myapp.jar
```

### With Arguments

```bash
debugjava -cp . com.example.Main arg1 arg2 --flag=value
```

### Spring Boot

```bash
debugjava -jar myapp.jar --spring.profiles.active=dev
```

## Advantages

1. **No Configuration Required**: No need to create or maintain launch.json
2. **Rapid Prototyping**: Perfect for quick debugging sessions
3. **Script Debugging**: Debug applications launched by complex shell scripts
4. **Environment Consistency**: Inherits all terminal environment variables
5. **Parameter Flexibility**: Easy to change arguments using terminal history (↑ key)

## How It Works Internally

1. When you run `debugjava`, the wrapper script temporarily sets `JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0`
2. The wrapper determines which Java executable to use (priority order):
- First: `JAVA_HOME/bin/java` if JAVA_HOME environment variable is set (user's explicit choice)
- Second: `VSCODE_JAVA_EXEC` environment variable (Java path from VS Code's Java Language Server)
- Third: `java` command from system PATH
3. The wrapper launches the Java process with JDWP enabled
4. JVM starts and outputs: "Listening for transport dt_socket at address: 12345"
5. The wrapper captures the JDWP port from this output
6. The port is written to a communication file
7. VS Code's file watcher detects the file and automatically starts an attach debug session

## Troubleshooting

### Port Already in Use

If you see "Address already in use", another Java debug session is running. Terminate it first.

### No Breakpoints Hit

1. Ensure you're running with `debugjava` command (not plain `java`)
2. Check that the `debugjava` command is available: `which debugjava` (Unix) or `Get-Command debugjava` (PowerShell)
3. Verify the terminal was opened AFTER the extension activated
4. Check the Debug Console for error messages

### Node.js Not Found

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

## Limitations

- Requires Node.js to be installed and available in PATH
- Only works in terminals opened within VS Code
- Requires using the `debugjava` command instead of `java`
- The Java process will suspend (hang) until the debugger attaches

## See Also

- [Debugger for Java Documentation](https://github.com/microsoft/vscode-java-debug)
- [JDWP Documentation](https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/jdwp-spec.html)
16 changes: 16 additions & 0 deletions bundled/scripts/noConfigScripts/debugjava
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
# Java No-Config Debug Wrapper Script for Unix/Linux/macOS
# This script intercepts java commands and automatically enables JDWP debugging

# Export the endpoint file path for JDWP port communication
export JDWP_ADAPTER_ENDPOINTS=$VSCODE_JDWP_ADAPTER_ENDPOINTS

# Set JDWP options only for this debugjava invocation
# This overrides the global JAVA_TOOL_OPTIONS to avoid affecting other Java processes
export JAVA_TOOL_OPTIONS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0"

# Get the directory of this script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# Use Node.js wrapper to capture JDWP port
exec node "$SCRIPT_DIR/jdwp-wrapper.js" "$@"
13 changes: 13 additions & 0 deletions bundled/scripts/noConfigScripts/debugjava.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@echo off
REM Java No-Config Debug Wrapper Script for Windows
REM This script intercepts java commands and automatically enables JDWP debugging

REM Export the endpoint file path for JDWP port communication
set JDWP_ADAPTER_ENDPOINTS=%VSCODE_JDWP_ADAPTER_ENDPOINTS%

REM Set JDWP options only for this debugjava invocation
REM This overrides the global JAVA_TOOL_OPTIONS to avoid affecting other Java processes
set JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0

REM Use Node.js wrapper to capture JDWP port
node "%~dp0jdwp-wrapper.js" %*
16 changes: 16 additions & 0 deletions bundled/scripts/noConfigScripts/debugjava.fish
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env fish
# Java No-Config Debug Wrapper Script for Fish Shell
# This script intercepts java commands and automatically enables JDWP debugging

# Export the endpoint file path for JDWP port communication
set -x JDWP_ADAPTER_ENDPOINTS $VSCODE_JDWP_ADAPTER_ENDPOINTS

# Set JDWP options only for this debugjava invocation
# This overrides the global JAVA_TOOL_OPTIONS to avoid affecting other Java processes
set -x JAVA_TOOL_OPTIONS "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0"

# Get the directory of this script
set script_dir (dirname (status -f))

# Use Node.js wrapper to capture JDWP port
exec node "$script_dir/jdwp-wrapper.js" $argv
15 changes: 15 additions & 0 deletions bundled/scripts/noConfigScripts/debugjava.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Java No-Config Debug Wrapper Script for PowerShell
# This script intercepts java commands and automatically enables JDWP debugging

# Export the endpoint file path for JDWP port communication
$env:JDWP_ADAPTER_ENDPOINTS = $env:VSCODE_JDWP_ADAPTER_ENDPOINTS

# Set JDWP options only for this debugjava invocation
# This overrides the global JAVA_TOOL_OPTIONS to avoid affecting other Java processes
$env:JAVA_TOOL_OPTIONS = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0"

# Get the directory of this script
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path

# Use Node.js wrapper to capture JDWP port
& node (Join-Path $scriptDir "jdwp-wrapper.js") $args
130 changes: 130 additions & 0 deletions bundled/scripts/noConfigScripts/jdwp-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env node
/**
* JDWP Port Listener and Communication Wrapper
*
* This script wraps Java process execution and captures the JDWP port
* from the JVM output, then writes it to the endpoint file for VS Code
* to pick up and attach the debugger.
*
* JDWP Output Format:
* "Listening for transport dt_socket at address: 12345"
*/

const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');

// Get environment variables
const endpointFile = process.env.JDWP_ADAPTER_ENDPOINTS || process.env.VSCODE_JDWP_ADAPTER_ENDPOINTS;
const javaToolOptions = process.env.JAVA_TOOL_OPTIONS || '';

// Check if debugging is enabled
const isDebugEnabled = javaToolOptions.includes('jdwp') && endpointFile;

// Helper function to find java command
function getJavaCommand() {
// Priority 1: Try JAVA_HOME environment variable first (user's explicit choice)
const javaHome = process.env.JAVA_HOME;
if (javaHome) {
const javaPath = path.join(javaHome, 'bin', 'java');
const javaPathExe = process.platform === 'win32' ? `${javaPath}.exe` : javaPath;

// Check if the file exists
if (fs.existsSync(javaPathExe)) {
return javaPath;
}
if (fs.existsSync(javaPath)) {
return javaPath;
}

console.warn(`[Java Debug] JAVA_HOME is set to '${javaHome}', but java command not found there. Falling back to VS Code's Java.`);
}

// Priority 2: Use VSCODE_JAVA_EXEC if provided by VS Code (from Java Language Server)
const vscodeJavaExec = process.env.VSCODE_JAVA_EXEC;
if (vscodeJavaExec && fs.existsSync(vscodeJavaExec)) {
return vscodeJavaExec;
}

// Priority 3: Fall back to 'java' in PATH
return 'java';
}

const javaCmd = getJavaCommand();

if (!isDebugEnabled) {
// No debugging, just run java normally
const child = spawn(javaCmd, process.argv.slice(2), {
stdio: 'inherit',
shell: false
});
child.on('exit', (code) => process.exit(code || 0));
child.on('error', (err) => {
console.error(`[Java Debug] Failed to start java: ${err.message}`);
console.error(`[Java Debug] Make sure Java is installed and either JAVA_HOME is set correctly or 'java' is in your PATH.`);
process.exit(1);
});
} else {
// Debugging enabled, capture JDWP port
const child = spawn(javaCmd, process.argv.slice(2), {
stdio: ['inherit', 'pipe', 'pipe'],
shell: false
});

let portCaptured = false;
const jdwpPortRegex = /Listening for transport dt_socket at address:\s*(\d+)/;

// Shared function to capture JDWP port from output
const capturePort = (output) => {
if (portCaptured) return;

const match = output.match(jdwpPortRegex);
if (match && match[1]) {
const port = parseInt(match[1], 10);

// Validate port range
if (port < 1 || port > 65535) {
console.error(`[Java Debug] Invalid port number: ${port}`);
return;
}

console.log(`[Java Debug] Captured JDWP port: ${port}`);

// Write port to endpoint file
const endpointData = JSON.stringify({
client: {
host: 'localhost',
port: port
}
});

try {
fs.writeFileSync(endpointFile, endpointData, 'utf8');
console.log(`[Java Debug] Wrote endpoint file: ${endpointFile}`);
portCaptured = true;
} catch (err) {
console.error(`[Java Debug] Failed to write endpoint file: ${err}`);
}
}
};

// Monitor stdout for JDWP port
child.stdout.on('data', (data) => {
const output = data.toString();
process.stdout.write(data);
capturePort(output);
});

// Monitor stderr for JDWP port (it might appear on stderr)
child.stderr.on('data', (data) => {
const output = data.toString();
process.stderr.write(data);
capturePort(output);
});

child.on('exit', (code) => process.exit(code || 0));
child.on('error', (err) => {
console.error(`[Java Debug] Failed to start java: ${err}`);
process.exit(1);
});
}
9 changes: 9 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { HCR_EVENT, JAVA_LANGID, TELEMETRY_EVENT, USER_NOTIFICATION_EVENT } from
import { NotificationBar } from "./customWidget";
import { initializeCodeLensProvider, startDebugging } from "./debugCodeLensProvider";
import { initExpService } from "./experimentationService";
import { registerNoConfigDebug } from "./noConfigDebugInit";
import { handleHotCodeReplaceCustomEvent, initializeHotCodeReplace, NO_BUTTON, YES_BUTTON } from "./hotCodeReplace";
import { JavaDebugAdapterDescriptorFactory } from "./javaDebugAdapterDescriptorFactory";
import { JavaInlineValuesProvider } from "./JavaInlineValueProvider";
Expand All @@ -31,6 +32,14 @@ import { promisify } from "util";
export async function activate(context: vscode.ExtensionContext): Promise<any> {
await initializeFromJsonFile(context.asAbsolutePath("./package.json"));
await initExpService(context);

// Register No-Config Debug functionality
const noConfigDisposable = await registerNoConfigDebug(
context.environmentVariableCollection,
context.extensionPath
);
context.subscriptions.push(noConfigDisposable);

return instrumentOperation("activation", initializeExtension)(context);
}

Expand Down
Loading