Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
98 changes: 98 additions & 0 deletions bundled/scripts/noConfigScripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 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 `javadebug` 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 `javadebug` command.

## Usage

### Basic Usage

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

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

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

### Maven Projects

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

### Gradle Projects

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

### With Arguments

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

### Spring Boot

```bash
javadebug -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 `javadebug`, the wrapper script temporarily sets `JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0`
2. The wrapper launches the Java process with JDWP enabled
3. JVM starts and outputs: "Listening for transport dt_socket at address: 12345"
4. The wrapper captures the JDWP port from this output
5. The port is written to a communication file
6. 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 `javadebug` command (not plain `java`)
2. Check that the `javadebug` command is available: `which javadebug` (Unix) or `Get-Command javadebug` (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 `javadebug` 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/javadebug
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 javadebug 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/javadebug.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 javadebug 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/javadebug.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 javadebug 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/javadebug.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 javadebug 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
99 changes: 99 additions & 0 deletions bundled/scripts/noConfigScripts/jdwp-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/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;

if (!isDebugEnabled) {
// No debugging, just run java normally
const javaHome = process.env.JAVA_HOME;
const javaCmd = javaHome ? path.join(javaHome, 'bin', 'java') : 'java';
const child = spawn(javaCmd, process.argv.slice(2), {
stdio: 'inherit',
shell: false
});
child.on('exit', (code) => process.exit(code || 0));
} else {
// Debugging enabled, capture JDWP port
const javaHome = process.env.JAVA_HOME;
const javaCmd = javaHome ? path.join(javaHome, 'bin', 'java') : 'java';

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