From 994f3d4a63732d31bd678ee8ecae2a5c144f1398 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Sat, 11 Oct 2025 14:17:23 +0800 Subject: [PATCH 1/9] feat: no config debug --- bundled/scripts/noConfigScripts/README.md | 94 +++++++++ bundled/scripts/noConfigScripts/TESTING.md | 186 ++++++++++++++++++ bundled/scripts/noConfigScripts/javadebug | 12 ++ bundled/scripts/noConfigScripts/javadebug.bat | 9 + .../scripts/noConfigScripts/javadebug.fish | 12 ++ bundled/scripts/noConfigScripts/javadebug.ps1 | 11 ++ .../scripts/noConfigScripts/jdwp-wrapper.js | 111 +++++++++++ src/extension.ts | 9 + src/noConfigDebugInit.ts | 139 +++++++++++++ 9 files changed, 583 insertions(+) create mode 100644 bundled/scripts/noConfigScripts/README.md create mode 100644 bundled/scripts/noConfigScripts/TESTING.md create mode 100644 bundled/scripts/noConfigScripts/javadebug create mode 100644 bundled/scripts/noConfigScripts/javadebug.bat create mode 100644 bundled/scripts/noConfigScripts/javadebug.fish create mode 100644 bundled/scripts/noConfigScripts/javadebug.ps1 create mode 100644 bundled/scripts/noConfigScripts/jdwp-wrapper.js create mode 100644 src/noConfigDebugInit.ts diff --git a/bundled/scripts/noConfigScripts/README.md b/bundled/scripts/noConfigScripts/README.md new file mode 100644 index 00000000..9b512661 --- /dev/null +++ b/bundled/scripts/noConfigScripts/README.md @@ -0,0 +1,94 @@ +# 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: + +- `JAVA_TOOL_OPTIONS`: Configured with JDWP to enable debugging on a random port +- `VSCODE_JDWP_ADAPTER_ENDPOINTS`: Path to a communication file for port exchange +- `PATH`: Includes the `javadebug` command wrapper + +## 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. The extension sets `JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0,quiet=y` +2. When you run `javadebug`, it wraps the Java process +3. The wrapper captures the JDWP port from JVM output: "Listening for transport dt_socket at address: 12345" +4. The port is written to a communication file +5. 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 +2. Check that JAVA_TOOL_OPTIONS is set in your terminal +3. Verify the terminal was opened AFTER the extension activated + +### Node.js Not Found + +The wrapper script requires Node.js to be installed and available in PATH. + +## Limitations + +- Requires Node.js to be installed +- Only works in terminals opened within VS Code +- Cannot debug applications that override JAVA_TOOL_OPTIONS + +## 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) diff --git a/bundled/scripts/noConfigScripts/TESTING.md b/bundled/scripts/noConfigScripts/TESTING.md new file mode 100644 index 00000000..5f9c8ff5 --- /dev/null +++ b/bundled/scripts/noConfigScripts/TESTING.md @@ -0,0 +1,186 @@ +# Testing Java No-Config Debug + +## Quick Test + +1. **Create a simple Java file** (`HelloWorld.java`): + +```java +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Starting application..."); + + String message = "Hello, World!"; + System.out.println(message); // Set a breakpoint here + + for (int i = 0; i < 3; i++) { + System.out.println("Count: " + i); + } + + System.out.println("Application finished."); + } +} +``` + +2. **Compile it**: +```bash +javac HelloWorld.java +``` + +3. **Set a breakpoint** on the line with `System.out.println(message);` + +4. **Run with javadebug**: +```bash +javadebug HelloWorld +``` + +5. **Result**: + - The debugger should automatically attach + - Execution should pause at your breakpoint + - You can inspect variables, step through code, etc. + +## Advanced Test - With Arguments + +Create `EchoArgs.java`: + +```java +public class EchoArgs { + public static void main(String[] args) { + System.out.println("Arguments received: " + args.length); + + for (int i = 0; i < args.length; i++) { + System.out.println("Arg " + i + ": " + args[i]); // Breakpoint here + } + } +} +``` + +Compile and run: +```bash +javac EchoArgs.java +javadebug EchoArgs arg1 arg2 "arg with spaces" +``` + +## Test with JAR + +Create a JAR with manifest: + +```bash +# Compile +javac HelloWorld.java + +# Create manifest +echo "Main-Class: HelloWorld" > manifest.txt + +# Create JAR +jar cfm hello.jar manifest.txt HelloWorld.class + +# Debug the JAR +javadebug -jar hello.jar +``` + +## Test Terminal History + +One of the key benefits is easy parameter modification: + +```bash +# First run +javadebug EchoArgs test1 test2 + +# Press ↑ to recall, modify and run again +javadebug EchoArgs different parameters + +# Press ↑ again, modify again +javadebug EchoArgs yet another test +``` + +This is much faster than editing launch.json each time! + +## Verify Environment Variables + +In your VS Code terminal, check that environment variables are set: + +**Unix/Linux/macOS**: +```bash +echo $JAVA_TOOL_OPTIONS +echo $VSCODE_JDWP_ADAPTER_ENDPOINTS +which javadebug +``` + +**Windows (PowerShell)**: +```powershell +$env:JAVA_TOOL_OPTIONS +$env:VSCODE_JDWP_ADAPTER_ENDPOINTS +Get-Command javadebug +``` + +**Windows (CMD)**: +```cmd +echo %JAVA_TOOL_OPTIONS% +echo %VSCODE_JDWP_ADAPTER_ENDPOINTS% +where javadebug +``` + +## Expected Output + +When you run `javadebug`, you should see something like: + +``` +Picked up JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0,quiet=y +[Java Debug] Captured JDWP port: 54321 +[Java Debug] Wrote endpoint file: C:\...\endpoint-abc123.txt +Starting application... +``` + +Then VS Code should show: +- Debug toolbar appears +- Breakpoint icon turns solid red (was hollow) +- Execution pauses at your breakpoint + +## Common Issues + +### Issue: "javadebug: command not found" + +**Solution**: +- Close and reopen your terminal +- The extension must be activated first +- Check that the extension is installed and enabled + +### Issue: Debugger doesn't attach + +**Solution**: +- Check the Debug Console for errors +- Verify JAVA_TOOL_OPTIONS is set +- Try setting a breakpoint before running + +### Issue: "Address already in use" + +**Solution**: +- Stop any existing debug sessions +- Wait a few seconds and try again +- The port from a previous session might still be bound + +## Comparison with Traditional Debugging + +### Traditional Way (with launch.json): + +1. Create `.vscode/launch.json` +2. Configure: +```json +{ + "type": "java", + "name": "Debug HelloWorld", + "request": "launch", + "mainClass": "HelloWorld", + "args": ["arg1", "arg2"] +} +``` +3. Press F5 +4. To change args: Edit launch.json, save, press F5 + +### No-Config Way: + +1. Set breakpoint +2. Run: `javadebug HelloWorld arg1 arg2` +3. To change args: Press ↑, edit command, press Enter + +**Much faster! 🚀** diff --git a/bundled/scripts/noConfigScripts/javadebug b/bundled/scripts/noConfigScripts/javadebug new file mode 100644 index 00000000..650a71cd --- /dev/null +++ b/bundled/scripts/noConfigScripts/javadebug @@ -0,0 +1,12 @@ +#!/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 + +# 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" "$@" diff --git a/bundled/scripts/noConfigScripts/javadebug.bat b/bundled/scripts/noConfigScripts/javadebug.bat new file mode 100644 index 00000000..2ade288e --- /dev/null +++ b/bundled/scripts/noConfigScripts/javadebug.bat @@ -0,0 +1,9 @@ +@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 Use Node.js wrapper to capture JDWP port +node "%~dp0jdwp-wrapper.js" %* diff --git a/bundled/scripts/noConfigScripts/javadebug.fish b/bundled/scripts/noConfigScripts/javadebug.fish new file mode 100644 index 00000000..223a5d3f --- /dev/null +++ b/bundled/scripts/noConfigScripts/javadebug.fish @@ -0,0 +1,12 @@ +#!/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 + +# 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 diff --git a/bundled/scripts/noConfigScripts/javadebug.ps1 b/bundled/scripts/noConfigScripts/javadebug.ps1 new file mode 100644 index 00000000..b3da0f8e --- /dev/null +++ b/bundled/scripts/noConfigScripts/javadebug.ps1 @@ -0,0 +1,11 @@ +# 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 + +# 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 diff --git a/bundled/scripts/noConfigScripts/jdwp-wrapper.js b/bundled/scripts/noConfigScripts/jdwp-wrapper.js new file mode 100644 index 00000000..754b491d --- /dev/null +++ b/bundled/scripts/noConfigScripts/jdwp-wrapper.js @@ -0,0 +1,111 @@ +#!/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+)/; + + // Monitor stdout for JDWP port + child.stdout.on('data', (data) => { + const output = data.toString(); + process.stdout.write(data); + + if (!portCaptured) { + const match = output.match(jdwpPortRegex); + if (match && match[1]) { + const port = parseInt(match[1], 10); + 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 stderr + child.stderr.on('data', (data) => { + const output = data.toString(); + process.stderr.write(data); + + // JDWP message might appear on stderr + if (!portCaptured) { + const match = output.match(jdwpPortRegex); + if (match && match[1]) { + const port = parseInt(match[1], 10); + console.log(`[Java Debug] Captured JDWP port: ${port}`); + + 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}`); + } + } + } + }); + + child.on('exit', (code) => process.exit(code || 0)); + child.on('error', (err) => { + console.error(`[Java Debug] Failed to start java: ${err}`); + process.exit(1); + }); +} diff --git a/src/extension.ts b/src/extension.ts index 6d8c2623..2609b27b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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"; @@ -31,6 +32,14 @@ import { promisify } from "util"; export async function activate(context: vscode.ExtensionContext): Promise { 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); } diff --git a/src/noConfigDebugInit.ts b/src/noConfigDebugInit.ts new file mode 100644 index 00000000..56f2cf72 --- /dev/null +++ b/src/noConfigDebugInit.ts @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as fs from 'fs'; +import * as path from 'path'; +import * as crypto from 'crypto'; +import * as vscode from 'vscode'; + +/** + * Registers the configuration-less debugging setup for the extension. + * + * This function sets up environment variables and a file system watcher to + * facilitate debugging without requiring a pre-configured launch.json file. + * + * @param envVarCollection - The collection of environment variables to be modified. + * @param extPath - The path to the extension directory. + * + * Environment Variables: + * - `VSCODE_JDWP_ADAPTER_ENDPOINTS`: Path to the file containing the debugger adapter endpoint. + * - `JAVA_TOOL_OPTIONS`: JDWP configuration for automatic debugging. + * - `PATH`: Appends the path to the noConfigScripts directory. + */ +export async function registerNoConfigDebug( + envVarCollection: vscode.EnvironmentVariableCollection, + extPath: string, +): Promise { + const collection = envVarCollection; + + // create a temp directory for the noConfigDebugAdapterEndpoints + // file path format: extPath/.noConfigDebugAdapterEndpoints/endpoint-stableWorkspaceHash.txt + let workspaceString = vscode.workspace.workspaceFile?.fsPath; + if (!workspaceString) { + workspaceString = vscode.workspace.workspaceFolders?.map((e) => e.uri.fsPath).join(';'); + } + if (!workspaceString) { + console.error('[Java Debug] No workspace folder found'); + return Promise.resolve(new vscode.Disposable(() => {})); + } + + // create a stable hash for the workspace folder, reduce terminal variable churn + const hash = crypto.createHash('sha256'); + hash.update(workspaceString.toString()); + const stableWorkspaceHash = hash.digest('hex').slice(0, 16); + + const tempDirPath = path.join(extPath, '.noConfigDebugAdapterEndpoints'); + const tempFilePath = path.join(tempDirPath, `endpoint-${stableWorkspaceHash}.txt`); + + // create the temp directory if it doesn't exist + if (!fs.existsSync(tempDirPath)) { + fs.mkdirSync(tempDirPath, { recursive: true }); + } else { + // remove endpoint file in the temp directory if it exists + if (fs.existsSync(tempFilePath)) { + fs.unlinkSync(tempFilePath); + } + } + + // clear the env var collection to remove any existing env vars + collection.clear(); + + // Add env vars for VSCODE_JDWP_ADAPTER_ENDPOINTS and JAVA_TOOL_OPTIONS + collection.replace('VSCODE_JDWP_ADAPTER_ENDPOINTS', tempFilePath); + + // Configure JDWP to listen on a random port and suspend until debugger attaches + // quiet=y prevents the "Listening for transport..." message from appearing in terminal + collection.replace('JAVA_TOOL_OPTIONS', + '-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0,quiet=y'); + + const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts'); + const pathSeparator = process.platform === 'win32' ? ';' : ':'; + + // Check if the current PATH already ends with a path separator to avoid double separators + const currentPath = process.env.PATH || ''; + const needsSeparator = currentPath.length > 0 && !currentPath.endsWith(pathSeparator); + const pathValueToAppend = needsSeparator ? `${pathSeparator}${noConfigScriptsDir}` : noConfigScriptsDir; + + collection.append('PATH', pathValueToAppend); + + // create file system watcher for the debuggerAdapterEndpointFolder for when the communication port is written + const fileSystemWatcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(tempDirPath, '**/*.txt') + ); + + const fileCreationEvent = fileSystemWatcher.onDidCreate(async (uri) => { + console.log('[Java Debug] No-config debug session detected'); + + const filePath = uri.fsPath; + fs.readFile(filePath, (err, data) => { + if (err) { + console.error(`[Java Debug] Error reading endpoint file: ${err}`); + return; + } + try { + // parse the client port + const dataParse = data.toString(); + const jsonData = JSON.parse(dataParse); + const clientPort = jsonData.client?.port; + console.log(`[Java Debug] Parsed JDWP port: ${clientPort}`); + + const options: vscode.DebugSessionOptions = { + noDebug: false, + }; + + // start debug session with the client port + vscode.debug.startDebugging( + undefined, + { + type: 'java', + request: 'attach', + name: 'Attach to Java (No-Config)', + hostName: 'localhost', + port: clientPort, + }, + options, + ).then( + (started) => { + if (started) { + console.log('[Java Debug] Successfully started no-config debug session'); + } else { + console.error('[Java Debug] Error starting debug session, session not started.'); + } + }, + (error) => { + console.error(`[Java Debug] Error starting debug session: ${error}`); + }, + ); + } catch (parseErr) { + console.error(`[Java Debug] Error parsing JSON: ${parseErr}`); + } + }); + }); + + return Promise.resolve( + new vscode.Disposable(() => { + fileSystemWatcher.dispose(); + fileCreationEvent.dispose(); + }), + ); +} From e1ab7c05f6d49dd1cff35d5082ee0f6e99408257 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 15 Oct 2025 11:01:28 +0800 Subject: [PATCH 2/9] feat: no config debug support --- bundled/scripts/noConfigScripts/README.md | 24 +++--- bundled/scripts/noConfigScripts/javadebug | 4 + bundled/scripts/noConfigScripts/javadebug.bat | 4 + bundled/scripts/noConfigScripts/javadebug.ps1 | 4 + .../scripts/noConfigScripts/jdwp-wrapper.js | 86 ++++++++----------- src/noConfigDebugInit.ts | 80 ++++++++++++++--- 6 files changed, 133 insertions(+), 69 deletions(-) diff --git a/bundled/scripts/noConfigScripts/README.md b/bundled/scripts/noConfigScripts/README.md index 9b512661..a67e1eec 100644 --- a/bundled/scripts/noConfigScripts/README.md +++ b/bundled/scripts/noConfigScripts/README.md @@ -6,10 +6,11 @@ This feature enables configuration-less debugging for Java applications, similar When you open a terminal in VS Code with this extension installed, the following environment variables are automatically set: -- `JAVA_TOOL_OPTIONS`: Configured with JDWP to enable debugging on a random port - `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 @@ -60,11 +61,12 @@ javadebug -jar myapp.jar --spring.profiles.active=dev ## How It Works Internally -1. The extension sets `JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0,quiet=y` -2. When you run `javadebug`, it wraps the Java process -3. The wrapper captures the JDWP port from JVM output: "Listening for transport dt_socket at address: 12345" -4. The port is written to a communication file -5. VS Code's file watcher detects the file and automatically starts an attach debug session +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 @@ -74,9 +76,10 @@ If you see "Address already in use", another Java debug session is running. Term ### No Breakpoints Hit -1. Ensure you're running with `javadebug` command -2. Check that JAVA_TOOL_OPTIONS is set in your terminal +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 @@ -84,9 +87,10 @@ The wrapper script requires Node.js to be installed and available in PATH. ## Limitations -- Requires Node.js to be installed +- Requires Node.js to be installed and available in PATH - Only works in terminals opened within VS Code -- Cannot debug applications that override JAVA_TOOL_OPTIONS +- Requires using the `javadebug` command instead of `java` +- The Java process will suspend (hang) until the debugger attaches ## See Also diff --git a/bundled/scripts/noConfigScripts/javadebug b/bundled/scripts/noConfigScripts/javadebug index 650a71cd..e2e5ba2c 100644 --- a/bundled/scripts/noConfigScripts/javadebug +++ b/bundled/scripts/noConfigScripts/javadebug @@ -5,6 +5,10 @@ # 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 )" diff --git a/bundled/scripts/noConfigScripts/javadebug.bat b/bundled/scripts/noConfigScripts/javadebug.bat index 2ade288e..7f0f7b93 100644 --- a/bundled/scripts/noConfigScripts/javadebug.bat +++ b/bundled/scripts/noConfigScripts/javadebug.bat @@ -5,5 +5,9 @@ REM This script intercepts java commands and automatically enables JDWP debuggin 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" %* diff --git a/bundled/scripts/noConfigScripts/javadebug.ps1 b/bundled/scripts/noConfigScripts/javadebug.ps1 index b3da0f8e..af050709 100644 --- a/bundled/scripts/noConfigScripts/javadebug.ps1 +++ b/bundled/scripts/noConfigScripts/javadebug.ps1 @@ -4,6 +4,10 @@ # 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 diff --git a/bundled/scripts/noConfigScripts/jdwp-wrapper.js b/bundled/scripts/noConfigScripts/jdwp-wrapper.js index 754b491d..a4f1c07d 100644 --- a/bundled/scripts/noConfigScripts/jdwp-wrapper.js +++ b/bundled/scripts/noConfigScripts/jdwp-wrapper.js @@ -43,64 +43,52 @@ if (!isDebugEnabled) { 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); - - if (!portCaptured) { - const match = output.match(jdwpPortRegex); - if (match && match[1]) { - const port = parseInt(match[1], 10); - 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}`); - } - } - } + capturePort(output); }); - // Monitor stderr + // Monitor stderr for JDWP port (it might appear on stderr) child.stderr.on('data', (data) => { const output = data.toString(); process.stderr.write(data); - - // JDWP message might appear on stderr - if (!portCaptured) { - const match = output.match(jdwpPortRegex); - if (match && match[1]) { - const port = parseInt(match[1], 10); - console.log(`[Java Debug] Captured JDWP port: ${port}`); - - 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}`); - } - } - } + capturePort(output); }); child.on('exit', (code) => process.exit(code || 0)); diff --git a/src/noConfigDebugInit.ts b/src/noConfigDebugInit.ts index 56f2cf72..e0389e53 100644 --- a/src/noConfigDebugInit.ts +++ b/src/noConfigDebugInit.ts @@ -49,22 +49,22 @@ export async function registerNoConfigDebug( if (!fs.existsSync(tempDirPath)) { fs.mkdirSync(tempDirPath, { recursive: true }); } else { - // remove endpoint file in the temp directory if it exists + // remove endpoint file in the temp directory if it exists (async to avoid blocking) if (fs.existsSync(tempFilePath)) { - fs.unlinkSync(tempFilePath); + fs.promises.unlink(tempFilePath).catch((err) => { + console.error(`[Java Debug] Failed to cleanup old endpoint file: ${err}`); + }); } } // clear the env var collection to remove any existing env vars collection.clear(); - // Add env vars for VSCODE_JDWP_ADAPTER_ENDPOINTS and JAVA_TOOL_OPTIONS + // Add env var for VSCODE_JDWP_ADAPTER_ENDPOINTS + // Note: We do NOT set JAVA_TOOL_OPTIONS globally to avoid affecting all Java processes + // (javac, maven, gradle, language server, etc.). Instead, JAVA_TOOL_OPTIONS is set + // only in the javadebug wrapper scripts (javadebug.ps1, javadebug.bat, javadebug) collection.replace('VSCODE_JDWP_ADAPTER_ENDPOINTS', tempFilePath); - - // Configure JDWP to listen on a random port and suspend until debugger attaches - // quiet=y prevents the "Listening for transport..." message from appearing in terminal - collection.replace('JAVA_TOOL_OPTIONS', - '-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0,quiet=y'); const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts'); const pathSeparator = process.platform === 'win32' ? ';' : ':'; @@ -81,10 +81,19 @@ export async function registerNoConfigDebug( new vscode.RelativePattern(tempDirPath, '**/*.txt') ); - const fileCreationEvent = fileSystemWatcher.onDidCreate(async (uri) => { + // Track active debug sessions to prevent duplicates + const activeDebugSessions = new Set(); + + // Handle both file creation and modification to support multiple runs + const handleEndpointFile = async (uri: vscode.Uri) => { console.log('[Java Debug] No-config debug session detected'); const filePath = uri.fsPath; + + // Add a small delay to ensure file is fully written + // File system events can fire before write is complete + await new Promise(resolve => setTimeout(resolve, 100)); + fs.readFile(filePath, (err, data) => { if (err) { console.error(`[Java Debug] Error reading endpoint file: ${err}`); @@ -94,8 +103,31 @@ export async function registerNoConfigDebug( // parse the client port const dataParse = data.toString(); const jsonData = JSON.parse(dataParse); - const clientPort = jsonData.client?.port; + + // Validate JSON structure + if (!jsonData || typeof jsonData !== 'object' || !jsonData.client) { + console.error(`[Java Debug] Invalid endpoint file format: ${dataParse}`); + return; + } + + const clientPort = jsonData.client.port; + + // Validate port number + if (!clientPort || typeof clientPort !== 'number' || clientPort < 1 || clientPort > 65535) { + console.error(`[Java Debug] Invalid port number: ${clientPort}`); + return; + } + + // Check if we already have an active session for this port + if (activeDebugSessions.has(clientPort)) { + console.log(`[Java Debug] Debug session already active for port ${clientPort}, skipping`); + return; + } + console.log(`[Java Debug] Parsed JDWP port: ${clientPort}`); + + // Mark this port as active + activeDebugSessions.add(clientPort); const options: vscode.DebugSessionOptions = { noDebug: false, @@ -116,24 +148,52 @@ export async function registerNoConfigDebug( (started) => { if (started) { console.log('[Java Debug] Successfully started no-config debug session'); + // Clean up the endpoint file after successful debug session start (async) + if (fs.existsSync(filePath)) { + fs.promises.unlink(filePath).then(() => { + console.log('[Java Debug] Cleaned up endpoint file'); + }).catch((cleanupErr) => { + console.error(`[Java Debug] Failed to cleanup endpoint file: ${cleanupErr}`); + }); + } } else { console.error('[Java Debug] Error starting debug session, session not started.'); + // Remove from active sessions on failure + activeDebugSessions.delete(clientPort); } }, (error) => { console.error(`[Java Debug] Error starting debug session: ${error}`); + // Remove from active sessions on error + activeDebugSessions.delete(clientPort); }, ); } catch (parseErr) { console.error(`[Java Debug] Error parsing JSON: ${parseErr}`); } }); + }; + + // Listen for both file creation and modification events + const fileCreationEvent = fileSystemWatcher.onDidCreate(handleEndpointFile); + const fileChangeEvent = fileSystemWatcher.onDidChange(handleEndpointFile); + + // Clean up active sessions when debug session ends + const debugSessionEndListener = vscode.debug.onDidTerminateDebugSession((session) => { + if (session.name === 'Attach to Java (No-Config)' && session.configuration.port) { + const port = session.configuration.port; + activeDebugSessions.delete(port); + console.log(`[Java Debug] Debug session ended for port ${port}`); + } }); return Promise.resolve( new vscode.Disposable(() => { fileSystemWatcher.dispose(); fileCreationEvent.dispose(); + fileChangeEvent.dispose(); + debugSessionEndListener.dispose(); + activeDebugSessions.clear(); }), ); } From 99b8c372ec02bb803d55f34f4d92125b2411a264 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 15 Oct 2025 11:03:18 +0800 Subject: [PATCH 3/9] feat: update --- bundled/scripts/noConfigScripts/javadebug.fish | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bundled/scripts/noConfigScripts/javadebug.fish b/bundled/scripts/noConfigScripts/javadebug.fish index 223a5d3f..9d07d659 100644 --- a/bundled/scripts/noConfigScripts/javadebug.fish +++ b/bundled/scripts/noConfigScripts/javadebug.fish @@ -5,6 +5,10 @@ # 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)) From 67d099b7a5808b3a7b5624961f24d86de205cce7 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Wed, 15 Oct 2025 11:04:59 +0800 Subject: [PATCH 4/9] ci: remove useless file --- bundled/scripts/noConfigScripts/TESTING.md | 186 --------------------- 1 file changed, 186 deletions(-) delete mode 100644 bundled/scripts/noConfigScripts/TESTING.md diff --git a/bundled/scripts/noConfigScripts/TESTING.md b/bundled/scripts/noConfigScripts/TESTING.md deleted file mode 100644 index 5f9c8ff5..00000000 --- a/bundled/scripts/noConfigScripts/TESTING.md +++ /dev/null @@ -1,186 +0,0 @@ -# Testing Java No-Config Debug - -## Quick Test - -1. **Create a simple Java file** (`HelloWorld.java`): - -```java -public class HelloWorld { - public static void main(String[] args) { - System.out.println("Starting application..."); - - String message = "Hello, World!"; - System.out.println(message); // Set a breakpoint here - - for (int i = 0; i < 3; i++) { - System.out.println("Count: " + i); - } - - System.out.println("Application finished."); - } -} -``` - -2. **Compile it**: -```bash -javac HelloWorld.java -``` - -3. **Set a breakpoint** on the line with `System.out.println(message);` - -4. **Run with javadebug**: -```bash -javadebug HelloWorld -``` - -5. **Result**: - - The debugger should automatically attach - - Execution should pause at your breakpoint - - You can inspect variables, step through code, etc. - -## Advanced Test - With Arguments - -Create `EchoArgs.java`: - -```java -public class EchoArgs { - public static void main(String[] args) { - System.out.println("Arguments received: " + args.length); - - for (int i = 0; i < args.length; i++) { - System.out.println("Arg " + i + ": " + args[i]); // Breakpoint here - } - } -} -``` - -Compile and run: -```bash -javac EchoArgs.java -javadebug EchoArgs arg1 arg2 "arg with spaces" -``` - -## Test with JAR - -Create a JAR with manifest: - -```bash -# Compile -javac HelloWorld.java - -# Create manifest -echo "Main-Class: HelloWorld" > manifest.txt - -# Create JAR -jar cfm hello.jar manifest.txt HelloWorld.class - -# Debug the JAR -javadebug -jar hello.jar -``` - -## Test Terminal History - -One of the key benefits is easy parameter modification: - -```bash -# First run -javadebug EchoArgs test1 test2 - -# Press ↑ to recall, modify and run again -javadebug EchoArgs different parameters - -# Press ↑ again, modify again -javadebug EchoArgs yet another test -``` - -This is much faster than editing launch.json each time! - -## Verify Environment Variables - -In your VS Code terminal, check that environment variables are set: - -**Unix/Linux/macOS**: -```bash -echo $JAVA_TOOL_OPTIONS -echo $VSCODE_JDWP_ADAPTER_ENDPOINTS -which javadebug -``` - -**Windows (PowerShell)**: -```powershell -$env:JAVA_TOOL_OPTIONS -$env:VSCODE_JDWP_ADAPTER_ENDPOINTS -Get-Command javadebug -``` - -**Windows (CMD)**: -```cmd -echo %JAVA_TOOL_OPTIONS% -echo %VSCODE_JDWP_ADAPTER_ENDPOINTS% -where javadebug -``` - -## Expected Output - -When you run `javadebug`, you should see something like: - -``` -Picked up JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0,quiet=y -[Java Debug] Captured JDWP port: 54321 -[Java Debug] Wrote endpoint file: C:\...\endpoint-abc123.txt -Starting application... -``` - -Then VS Code should show: -- Debug toolbar appears -- Breakpoint icon turns solid red (was hollow) -- Execution pauses at your breakpoint - -## Common Issues - -### Issue: "javadebug: command not found" - -**Solution**: -- Close and reopen your terminal -- The extension must be activated first -- Check that the extension is installed and enabled - -### Issue: Debugger doesn't attach - -**Solution**: -- Check the Debug Console for errors -- Verify JAVA_TOOL_OPTIONS is set -- Try setting a breakpoint before running - -### Issue: "Address already in use" - -**Solution**: -- Stop any existing debug sessions -- Wait a few seconds and try again -- The port from a previous session might still be bound - -## Comparison with Traditional Debugging - -### Traditional Way (with launch.json): - -1. Create `.vscode/launch.json` -2. Configure: -```json -{ - "type": "java", - "name": "Debug HelloWorld", - "request": "launch", - "mainClass": "HelloWorld", - "args": ["arg1", "arg2"] -} -``` -3. Press F5 -4. To change args: Edit launch.json, save, press F5 - -### No-Config Way: - -1. Set breakpoint -2. Run: `javadebug HelloWorld arg1 arg2` -3. To change args: Press ↑, edit command, press Enter - -**Much faster! 🚀** From e85ba89ea20d0b05d628df5ad1fe8b8343c6b814 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 15 Oct 2025 13:47:50 +0800 Subject: [PATCH 5/9] fix: fix lint error --- src/extension.ts | 4 +- src/noConfigDebugInit.ts | 94 +++++++++++++++++++++++++++------------- 2 files changed, 66 insertions(+), 32 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 2609b27b..867b2c02 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -32,14 +32,14 @@ import { promisify } from "util"; export async function activate(context: vscode.ExtensionContext): Promise { 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); } diff --git a/src/noConfigDebugInit.ts b/src/noConfigDebugInit.ts index e0389e53..1c382e5d 100644 --- a/src/noConfigDebugInit.ts +++ b/src/noConfigDebugInit.ts @@ -6,6 +6,8 @@ import * as path from 'path'; import * as crypto from 'crypto'; import * as vscode from 'vscode'; +import { sendInfo, sendError } from "vscode-extension-telemetry-wrapper"; + /** * Registers the configuration-less debugging setup for the extension. * @@ -33,8 +35,12 @@ export async function registerNoConfigDebug( workspaceString = vscode.workspace.workspaceFolders?.map((e) => e.uri.fsPath).join(';'); } if (!workspaceString) { - console.error('[Java Debug] No workspace folder found'); - return Promise.resolve(new vscode.Disposable(() => {})); + const error: Error = { + name: "NoConfigDebugError", + message: '[Java Debug] No workspace folder found', + }; + sendError(error); + return Promise.resolve(new vscode.Disposable(() => { })); } // create a stable hash for the workspace folder, reduce terminal variable churn @@ -52,7 +58,11 @@ export async function registerNoConfigDebug( // remove endpoint file in the temp directory if it exists (async to avoid blocking) if (fs.existsSync(tempFilePath)) { fs.promises.unlink(tempFilePath).catch((err) => { - console.error(`[Java Debug] Failed to cleanup old endpoint file: ${err}`); + const error: Error = { + name: "NoConfigDebugError", + message: `[Java Debug] Failed to cleanup old endpoint file: ${err}`, + }; + sendError(error); }); } } @@ -80,52 +90,60 @@ export async function registerNoConfigDebug( const fileSystemWatcher = vscode.workspace.createFileSystemWatcher( new vscode.RelativePattern(tempDirPath, '**/*.txt') ); - + // Track active debug sessions to prevent duplicates const activeDebugSessions = new Set(); - + // Handle both file creation and modification to support multiple runs const handleEndpointFile = async (uri: vscode.Uri) => { - console.log('[Java Debug] No-config debug session detected'); - const filePath = uri.fsPath; - + // Add a small delay to ensure file is fully written // File system events can fire before write is complete await new Promise(resolve => setTimeout(resolve, 100)); - + fs.readFile(filePath, (err, data) => { if (err) { - console.error(`[Java Debug] Error reading endpoint file: ${err}`); + const error: Error = { + name: "NoConfigDebugError", + message: `[Java Debug] No-config debug failed: file_read_error - ${err}`, + }; + sendError(error); return; } try { // parse the client port const dataParse = data.toString(); const jsonData = JSON.parse(dataParse); - + // Validate JSON structure if (!jsonData || typeof jsonData !== 'object' || !jsonData.client) { - console.error(`[Java Debug] Invalid endpoint file format: ${dataParse}`); + const error: Error = { + name: "NoConfigDebugError", + message: `[Java Debug] No-config debug failed: invalid_format - ${dataParse}`, + }; + sendError(error); return; } - + const clientPort = jsonData.client.port; - + // Validate port number if (!clientPort || typeof clientPort !== 'number' || clientPort < 1 || clientPort > 65535) { - console.error(`[Java Debug] Invalid port number: ${clientPort}`); + const error: Error = { + name: "NoConfigDebugError", + message: `[Java Debug] No-config debug failed: invalid_port - ${clientPort}`, + }; + sendError(error); return; } - + // Check if we already have an active session for this port if (activeDebugSessions.has(clientPort)) { - console.log(`[Java Debug] Debug session already active for port ${clientPort}, skipping`); + // Skip duplicate session silently - this is expected behavior return; } - - console.log(`[Java Debug] Parsed JDWP port: ${clientPort}`); - + // Mark this port as active activeDebugSessions.add(clientPort); @@ -147,29 +165,45 @@ export async function registerNoConfigDebug( ).then( (started) => { if (started) { - console.log('[Java Debug] Successfully started no-config debug session'); + // Send telemetry only on successful session start with port info + sendInfo('', { message: '[Java Debug] No-config debug session started', port: clientPort }); // Clean up the endpoint file after successful debug session start (async) if (fs.existsSync(filePath)) { - fs.promises.unlink(filePath).then(() => { - console.log('[Java Debug] Cleaned up endpoint file'); - }).catch((cleanupErr) => { - console.error(`[Java Debug] Failed to cleanup endpoint file: ${cleanupErr}`); + fs.promises.unlink(filePath).catch((cleanupErr) => { + // Cleanup failure is non-critical, just log for debugging + const error: Error = { + name: "NoConfigDebugError", + message: `[Java Debug] No-config debug failed: cleanup_error - ${cleanupErr}`, + }; + sendError(error); }); } } else { - console.error('[Java Debug] Error starting debug session, session not started.'); + const error: Error = { + name: "NoConfigDebugError", + message: `[Java Debug] No-config debug failed: attach_failed - port ${clientPort}`, + }; + sendError(error); // Remove from active sessions on failure activeDebugSessions.delete(clientPort); } }, (error) => { - console.error(`[Java Debug] Error starting debug session: ${error}`); + const attachError: Error = { + name: "NoConfigDebugError", + message: `[Java Debug] No-config debug failed: attach_error - port ${clientPort} - ${error}`, + }; + sendError(attachError); // Remove from active sessions on error activeDebugSessions.delete(clientPort); }, ); } catch (parseErr) { - console.error(`[Java Debug] Error parsing JSON: ${parseErr}`); + const error: Error = { + name: "NoConfigDebugError", + message: `[Java Debug] No-config debug failed: parse_error - ${parseErr}`, + }; + sendError(error); } }); }; @@ -177,13 +211,13 @@ export async function registerNoConfigDebug( // Listen for both file creation and modification events const fileCreationEvent = fileSystemWatcher.onDidCreate(handleEndpointFile); const fileChangeEvent = fileSystemWatcher.onDidChange(handleEndpointFile); - + // Clean up active sessions when debug session ends const debugSessionEndListener = vscode.debug.onDidTerminateDebugSession((session) => { if (session.name === 'Attach to Java (No-Config)' && session.configuration.port) { const port = session.configuration.port; activeDebugSessions.delete(port); - console.log(`[Java Debug] Debug session ended for port ${port}`); + // Session end is normal operation, no telemetry needed } }); From 3288011446289c1398b7e9596e1e58a61de316e3 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 15 Oct 2025 14:37:19 +0800 Subject: [PATCH 6/9] fix: update name as debugjava --- bundled/scripts/noConfigScripts/README.md | 22 +++++++++---------- .../noConfigScripts/{javadebug => debugjava} | 2 +- .../{javadebug.bat => debugjava.bat} | 2 +- .../{javadebug.fish => debugjava.fish} | 2 +- .../{javadebug.ps1 => debugjava.ps1} | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) rename bundled/scripts/noConfigScripts/{javadebug => debugjava} (92%) rename bundled/scripts/noConfigScripts/{javadebug.bat => debugjava.bat} (90%) rename bundled/scripts/noConfigScripts/{javadebug.fish => debugjava.fish} (91%) rename bundled/scripts/noConfigScripts/{javadebug.ps1 => debugjava.ps1} (92%) diff --git a/bundled/scripts/noConfigScripts/README.md b/bundled/scripts/noConfigScripts/README.md index a67e1eec..bdab8956 100644 --- a/bundled/scripts/noConfigScripts/README.md +++ b/bundled/scripts/noConfigScripts/README.md @@ -7,9 +7,9 @@ This feature enables configuration-less debugging for Java applications, similar 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 +- `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 `javadebug` command. +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 @@ -22,7 +22,7 @@ java -cp . com.example.Main Simply run: ```bash -javadebug -cp . com.example.Main +debugjava -cp . com.example.Main ``` The debugger will automatically attach, and breakpoints will work without any launch.json configuration! @@ -30,25 +30,25 @@ The debugger will automatically attach, and breakpoints will work without any la ### Maven Projects ```bash -javadebug -jar target/myapp.jar +debugjava -jar target/myapp.jar ``` ### Gradle Projects ```bash -javadebug -jar build/libs/myapp.jar +debugjava -jar build/libs/myapp.jar ``` ### With Arguments ```bash -javadebug -cp . com.example.Main arg1 arg2 --flag=value +debugjava -cp . com.example.Main arg1 arg2 --flag=value ``` ### Spring Boot ```bash -javadebug -jar myapp.jar --spring.profiles.active=dev +debugjava -jar myapp.jar --spring.profiles.active=dev ``` ## Advantages @@ -61,7 +61,7 @@ javadebug -jar myapp.jar --spring.profiles.active=dev ## 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` +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 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 @@ -76,8 +76,8 @@ If you see "Address already in use", another Java debug session is running. Term ### 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) +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 @@ -89,7 +89,7 @@ The wrapper script requires Node.js to be installed and available in PATH. - 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` +- Requires using the `debugjava` command instead of `java` - The Java process will suspend (hang) until the debugger attaches ## See Also diff --git a/bundled/scripts/noConfigScripts/javadebug b/bundled/scripts/noConfigScripts/debugjava similarity index 92% rename from bundled/scripts/noConfigScripts/javadebug rename to bundled/scripts/noConfigScripts/debugjava index e2e5ba2c..e0861d16 100644 --- a/bundled/scripts/noConfigScripts/javadebug +++ b/bundled/scripts/noConfigScripts/debugjava @@ -5,7 +5,7 @@ # 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 +# 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" diff --git a/bundled/scripts/noConfigScripts/javadebug.bat b/bundled/scripts/noConfigScripts/debugjava.bat similarity index 90% rename from bundled/scripts/noConfigScripts/javadebug.bat rename to bundled/scripts/noConfigScripts/debugjava.bat index 7f0f7b93..828c656f 100644 --- a/bundled/scripts/noConfigScripts/javadebug.bat +++ b/bundled/scripts/noConfigScripts/debugjava.bat @@ -5,7 +5,7 @@ REM This script intercepts java commands and automatically enables JDWP debuggin 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 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 diff --git a/bundled/scripts/noConfigScripts/javadebug.fish b/bundled/scripts/noConfigScripts/debugjava.fish similarity index 91% rename from bundled/scripts/noConfigScripts/javadebug.fish rename to bundled/scripts/noConfigScripts/debugjava.fish index 9d07d659..b745bc18 100644 --- a/bundled/scripts/noConfigScripts/javadebug.fish +++ b/bundled/scripts/noConfigScripts/debugjava.fish @@ -5,7 +5,7 @@ # 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 +# 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" diff --git a/bundled/scripts/noConfigScripts/javadebug.ps1 b/bundled/scripts/noConfigScripts/debugjava.ps1 similarity index 92% rename from bundled/scripts/noConfigScripts/javadebug.ps1 rename to bundled/scripts/noConfigScripts/debugjava.ps1 index af050709..931c0cad 100644 --- a/bundled/scripts/noConfigScripts/javadebug.ps1 +++ b/bundled/scripts/noConfigScripts/debugjava.ps1 @@ -4,7 +4,7 @@ # 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 +# 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" From 2cab98cc8b2a9c0d64bc5ec44e0287ea6a55b8e8 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 15 Oct 2025 14:37:51 +0800 Subject: [PATCH 7/9] fix: update name as debugjava --- src/noConfigDebugInit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/noConfigDebugInit.ts b/src/noConfigDebugInit.ts index 1c382e5d..50c92642 100644 --- a/src/noConfigDebugInit.ts +++ b/src/noConfigDebugInit.ts @@ -73,7 +73,7 @@ export async function registerNoConfigDebug( // Add env var for VSCODE_JDWP_ADAPTER_ENDPOINTS // Note: We do NOT set JAVA_TOOL_OPTIONS globally to avoid affecting all Java processes // (javac, maven, gradle, language server, etc.). Instead, JAVA_TOOL_OPTIONS is set - // only in the javadebug wrapper scripts (javadebug.ps1, javadebug.bat, javadebug) + // only in the debugjava wrapper scripts (debugjava.ps1, debugjava.bat, debugjava) collection.replace('VSCODE_JDWP_ADAPTER_ENDPOINTS', tempFilePath); const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts'); From b144c6cb909adc83e3027b9d576a757980309876 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 15 Oct 2025 15:30:25 +0800 Subject: [PATCH 8/9] fix: resolve comments in the pr --- .../scripts/noConfigScripts/jdwp-wrapper.js | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/bundled/scripts/noConfigScripts/jdwp-wrapper.js b/bundled/scripts/noConfigScripts/jdwp-wrapper.js index a4f1c07d..5da84271 100644 --- a/bundled/scripts/noConfigScripts/jdwp-wrapper.js +++ b/bundled/scripts/noConfigScripts/jdwp-wrapper.js @@ -21,20 +21,46 @@ 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() { + const javaHome = process.env.JAVA_HOME; + + // Try JAVA_HOME first + 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 PATH.`); + } + + // Fall back to 'java' in PATH + return 'java'; +} + +const javaCmd = getJavaCommand(); + 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)); + 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 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 From d60dd80da4e8e5ea00f2c9b44cf79d84e512b8c2 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Fri, 17 Oct 2025 14:02:40 +0800 Subject: [PATCH 9/9] feat: java home set --- bundled/scripts/noConfigScripts/README.md | 14 +++++++++----- bundled/scripts/noConfigScripts/jdwp-wrapper.js | 13 +++++++++---- src/noConfigDebugInit.ts | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/bundled/scripts/noConfigScripts/README.md b/bundled/scripts/noConfigScripts/README.md index bdab8956..021f0553 100644 --- a/bundled/scripts/noConfigScripts/README.md +++ b/bundled/scripts/noConfigScripts/README.md @@ -62,11 +62,15 @@ debugjava -jar myapp.jar --spring.profiles.active=dev ## 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 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 +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 diff --git a/bundled/scripts/noConfigScripts/jdwp-wrapper.js b/bundled/scripts/noConfigScripts/jdwp-wrapper.js index 5da84271..de8a4d92 100644 --- a/bundled/scripts/noConfigScripts/jdwp-wrapper.js +++ b/bundled/scripts/noConfigScripts/jdwp-wrapper.js @@ -23,9 +23,8 @@ 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; - - // Try JAVA_HOME first if (javaHome) { const javaPath = path.join(javaHome, 'bin', 'java'); const javaPathExe = process.platform === 'win32' ? `${javaPath}.exe` : javaPath; @@ -38,10 +37,16 @@ function getJavaCommand() { return javaPath; } - console.warn(`[Java Debug] JAVA_HOME is set to '${javaHome}', but java command not found there. Falling back to PATH.`); + 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; } - // Fall back to 'java' in PATH + // Priority 3: Fall back to 'java' in PATH return 'java'; } diff --git a/src/noConfigDebugInit.ts b/src/noConfigDebugInit.ts index 50c92642..b0da4f01 100644 --- a/src/noConfigDebugInit.ts +++ b/src/noConfigDebugInit.ts @@ -7,6 +7,7 @@ import * as crypto from 'crypto'; import * as vscode from 'vscode'; import { sendInfo, sendError } from "vscode-extension-telemetry-wrapper"; +import { getJavaHome } from "./utility"; /** * Registers the configuration-less debugging setup for the extension. @@ -76,6 +77,19 @@ export async function registerNoConfigDebug( // only in the debugjava wrapper scripts (debugjava.ps1, debugjava.bat, debugjava) collection.replace('VSCODE_JDWP_ADAPTER_ENDPOINTS', tempFilePath); + // Try to get Java executable from Java Language Server + // This ensures we use the same Java version as the project is compiled with + try { + const javaHome = await getJavaHome(); + if (javaHome) { + const javaExec = path.join(javaHome, 'bin', 'java'); + collection.replace('VSCODE_JAVA_EXEC', javaExec); + } + } catch (error) { + // If we can't get Java from Language Server, that's okay + // The wrapper script will fall back to JAVA_HOME or PATH + } + const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts'); const pathSeparator = process.platform === 'win32' ? ';' : ':';