Skip to content

Commit 45e949f

Browse files
committed
fix: avoid activating Java during debug configuration discovery
1 parent e25d477 commit 45e949f

9 files changed

Lines changed: 521 additions & 81 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ The debugger will automatically attach. See [No-Config Debug Documentation](bund
6262

6363
No-Config Debug is enabled by default. To disable the terminal integration and the AI `debug_java_application` tool, set `"java.debug.settings.enableNoConfigDebug": false`, reload VS Code, and recreate existing terminals. Standard Java launch/attach debugging, including F5 and Run/Debug CodeLens, remains available.
6464

65+
Preparing the terminal integration does not start or wait for the Java language extension. The wrapper uses `JAVA_HOME`, then `VSCODE_JAVA_EXEC` when available, then `java` on `PATH`. If Java support activates later, recreate an existing terminal to use the newly discovered Java executable.
66+
6567
## AI-Assisted Debugging
6668

6769
When using GitHub Copilot Chat, you can now ask AI to help you debug Java applications! The extension provides a Language Model Tool that enables natural language debugging:

‎bundled/scripts/noConfigScripts/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ When you open a terminal in VS Code with this extension installed, the following
1111

1212
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.
1313

14+
Terminal setup does not activate or wait for Language Support for Java. Opening the debug configuration picker in a non-Java workspace therefore does not start Java just to discover its executable. If Java support is already active, its tooling JDK is contributed as `VSCODE_JAVA_EXEC`; otherwise, the integration observes activation in the background and adds it when available. Observation stops after activation or when the integration is disposed.
15+
16+
The wrapper keeps its selection order: `JAVA_HOME`, then `VSCODE_JAVA_EXEC`, then `java` on `PATH`. An existing cached `VSCODE_JAVA_EXEC` is retained while Java support is unavailable. Terminals created before Java executable discovery must be recreated to receive the new value. Actual Java launch/attach still requires Java support and may activate it.
17+
1418
## Disabling No-Config Debug
1519

1620
No-Config Debug is enabled by default. To opt out for all workspaces or just the current workspace, add this to the corresponding VS Code settings:

‎src/extension.ts‎

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import { initializeThreadOperations } from "./threadOperations";
3131
import * as utility from "./utility";
3232
import { registerBreakpointCommands } from "./breakpointCommands";
3333
import { registerVariableMenuCommands } from "./variableMenu";
34-
import { promisify } from "util";
3534

3635
export async function activate(context: vscode.ExtensionContext): Promise<any> {
3736
await initializeFromJsonFile(context.asAbsolutePath("./package.json"));
@@ -97,7 +96,7 @@ function initializeExtension(_operationId: string, context: vscode.ExtensionCont
9796
initializeHotCodeReplace(context);
9897
initializeCodeLensProvider(context);
9998
initializeThreadOperations(context);
100-
subscribeToJavaExtensionEvents();
99+
subscribeToJavaExtensionEvents(context);
101100

102101
context.subscriptions.push(vscode.languages.registerInlineValuesProvider("java", new JavaInlineValuesProvider()));
103102
return {
@@ -110,8 +109,6 @@ export async function deactivate() {
110109
await disposeTelemetryWrapper();
111110
}
112111

113-
const delay = promisify(setTimeout);
114-
115112
/**
116113
* Register Language Model Tools after Java Language Server is ready.
117114
* The debug tools depend on JDT.LS for compilation, classpath resolution,
@@ -157,22 +154,11 @@ async function registerLanguageModelToolsWhenReady(context: vscode.ExtensionCont
157154
}
158155
}
159156

160-
async function subscribeToJavaExtensionEvents(): Promise<void> {
161-
const javaExt = vscode.extensions.getExtension("redhat.java");
162-
if (!javaExt) {
163-
return;
164-
}
165-
166-
// wait javaExt to activate
167-
const timeout = 30 * 60 * 1000; // wait 30 min at most
168-
let count = 0;
169-
while (!javaExt.isActive && count < timeout) {
170-
await delay(1000);
171-
count += 1000;
172-
}
173-
174-
if (javaExt.isActive) {
175-
javaExt.exports?.onDidSourceInvalidate?.((event: any) => {
157+
function subscribeToJavaExtensionEvents(context: vscode.ExtensionContext): void {
158+
context.subscriptions.push(utility.onDidActivateJavaExtension((api: {
159+
onDidSourceInvalidate?: vscode.Event<{ affectedRootPaths?: string[] }>;
160+
} | undefined) => {
161+
const subscription = api?.onDidSourceInvalidate?.((event) => {
176162
if (event?.affectedRootPaths?.length) {
177163
const activeDebugSession = vscode.debug.activeDebugSession;
178164
if (activeDebugSession?.type === "java") {
@@ -182,7 +168,10 @@ async function subscribeToJavaExtensionEvents(): Promise<void> {
182168
}
183169
}
184170
});
185-
}
171+
if (subscription) {
172+
context.subscriptions.push(subscription);
173+
}
174+
}));
186175
}
187176

188177
function registerDebugEventListener(context: vscode.ExtensionContext) {

‎src/noConfigDebugInit.ts‎

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as path from 'path';
66
import * as vscode from 'vscode';
77

88
import { sendInfo, sendError } from "vscode-extension-telemetry-wrapper";
9-
import { getJavaHome } from "./utility";
9+
import { onDidActivateJavaExtension } from "./utility";
1010
import { buildNoConfigPathAppendValue } from "./pathUtil";
1111
import { applyAppendIfChanged, applyReplaceIfChanged } from "./envVarSync";
1212

@@ -237,7 +237,7 @@ export async function registerNoConfigDebug(
237237
});
238238
};
239239

240-
// Listen before publishing the endpoint or awaiting Java/script setup.
240+
// Listen before publishing the endpoint or awaiting script setup.
241241
// Terminals surviving a reload may already have the stable endpoint path.
242242
const fileCreationEvent = fileSystemWatcher.onDidCreate(handleEndpointFile);
243243
const fileChangeEvent = fileSystemWatcher.onDidChange(handleEndpointFile);
@@ -267,21 +267,6 @@ export async function registerNoConfigDebug(
267267
// only in the debugjava wrapper scripts (debugjava.ps1, debugjava.bat, debugjava)
268268
applyReplaceIfChanged(collection, 'VSCODE_JDWP_ADAPTER_ENDPOINTS', tempFilePath);
269269

270-
// Try to get Java executable from Java Language Server
271-
// This ensures we use the same Java version as the project is compiled with.
272-
// If detection fails or returns nothing, we deliberately keep any previously
273-
// set VSCODE_JAVA_EXEC to avoid churn from transient startup failures.
274-
try {
275-
const javaHome = await getJavaHome();
276-
if (javaHome) {
277-
const javaExec = path.join(javaHome, 'bin', 'java');
278-
applyReplaceIfChanged(collection, 'VSCODE_JAVA_EXEC', javaExec);
279-
}
280-
} catch (error) {
281-
// If we can't get Java from Language Server, that's okay
282-
// The wrapper script will fall back to JAVA_HOME or PATH
283-
}
284-
285270
const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts');
286271
const debugJavaScriptPath = path.join(noConfigScriptsDir, "debugjava");
287272
try {
@@ -294,9 +279,16 @@ export async function registerNoConfigDebug(
294279
sendError(error);
295280
}
296281
applyAppendIfChanged(collection, 'PATH', buildNoConfigPathAppendValue(noConfigScriptsDir));
282+
const javaExecutableSync = onDidActivateJavaExtension((api: { javaRequirement?: { java_home?: string } } | undefined) => {
283+
const javaHome = api?.javaRequirement?.java_home;
284+
if (javaHome) {
285+
applyReplaceIfChanged(collection, "VSCODE_JAVA_EXEC", path.join(javaHome, "bin", "java"));
286+
}
287+
});
297288

298289
return Promise.resolve(
299290
new vscode.Disposable(() => {
291+
javaExecutableSync.dispose();
300292
fileSystemWatcher.dispose();
301293
fileCreationEvent.dispose();
302294
fileChangeEvent.dispose();

‎src/stackTraceLinkProvider.ts‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
// Licensed under the MIT license.
33

44
import { CancellationToken, commands, DocumentLink, DocumentLinkProvider, DocumentSelector,
5-
env, ExtensionContext, languages, Position, ProviderResult, Range, TextDocument, Uri,
5+
env, Event, ExtensionContext, languages, Position, ProviderResult, Range, TextDocument, Uri,
66
window, workspace } from "vscode";
77
import { instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-telemetry-wrapper";
88
import { resolveSourceUri } from "./languageServerPlugin";
99
import { parseJavaStackFrame } from "./stackFrameParser";
10-
import { getJavaExtensionAPI, isJavaExtEnabled, ServerMode } from "./utility";
10+
import { onDidActivateJavaExtension, ServerMode } from "./utility";
1111

1212
const ANALYZE_STACK_TRACE_COMMAND = "java.debug.analyzeStackTrace";
1313
const NAVIGATE_TO_STACK_FRAME_COMMAND = "_java.debug.navigateToStackFrame";
@@ -177,16 +177,14 @@ export function registerStackTraceLinkProvider(context: ExtensionContext): void
177177
}
178178

179179
function registerLinkProviderWhenReady(context: ExtensionContext): void {
180-
// Without the Java language server, frames cannot be resolved to source - nothing to linkify.
181-
if (!isJavaExtEnabled()) {
182-
return;
183-
}
184-
185180
const doRegister = () => context.subscriptions.push(
186181
languages.registerDocumentLinkProvider(STACK_TRACE_DOCUMENT_SELECTOR, new JavaStackTraceLinkProvider()),
187182
);
188183

189-
getJavaExtensionAPI().then((api) => {
184+
context.subscriptions.push(onDidActivateJavaExtension((api: {
185+
serverMode: string;
186+
onDidServerModeChange: Event<string>;
187+
} | undefined) => {
190188
if (!api) {
191189
return;
192190
}
@@ -205,5 +203,5 @@ function registerLinkProviderWhenReady(context: ExtensionContext): void {
205203
// Already in Standard mode.
206204
doRegister();
207205
}
208-
});
206+
}));
209207
}

‎src/utility.ts‎

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,19 +177,69 @@ export function getJavaExtensionAPI(progressReporter?: IProgressReporter): Thena
177177
throw new JavaExtensionNotEnabledError("VS Code Java Extension is not enabled.");
178178
}
179179

180-
return new Promise<any>(async (resolve) => {
181-
progressReporter?.getCancellationToken().onCancellationRequested(() => {
182-
resolve(undefined);
183-
});
180+
const token = progressReporter?.getCancellationToken();
181+
if (token?.isCancellationRequested) {
182+
return Promise.resolve(undefined);
183+
}
184184

185-
resolve(await extension.activate());
186-
});
185+
const activation = extension.activate();
186+
if (!token) {
187+
return activation;
188+
}
189+
190+
let listener: vscode.Disposable | undefined;
191+
return Promise.race([
192+
activation,
193+
new Promise<undefined>((resolve) => {
194+
listener = token.onCancellationRequested(() => resolve(undefined));
195+
}),
196+
]).finally(() => listener?.dispose());
187197
}
188198

189199
export function getJavaExtension(): vscode.Extension<any> | undefined {
190200
return vscode.extensions.getExtension(JAVA_EXTENSION_ID);
191201
}
192202

203+
export function onDidActivateJavaExtension<T>(listener: (api: T) => void): vscode.Disposable {
204+
let disposed = false;
205+
let timer: NodeJS.Timeout | undefined;
206+
const update = (): boolean => {
207+
if (disposed) {
208+
return true;
209+
}
210+
try {
211+
const extension = vscode.extensions.getExtension<T>(JAVA_EXTENSION_ID);
212+
if (!extension?.isActive) {
213+
return false;
214+
}
215+
listener(extension.exports);
216+
} catch {
217+
sendError({
218+
name: "JavaExtensionError",
219+
message: "Could not initialize integration with the active Java extension.",
220+
});
221+
}
222+
return true;
223+
};
224+
225+
// VS Code has no extension-activation event. Observe without activating Java,
226+
// including when the first Java file is opened much later in this workspace.
227+
if (!update()) {
228+
timer = setInterval(() => {
229+
if (update()) {
230+
clearInterval(timer);
231+
}
232+
}, 1000);
233+
timer.unref();
234+
}
235+
return new vscode.Disposable(() => {
236+
disposed = true;
237+
if (timer) {
238+
clearInterval(timer);
239+
}
240+
});
241+
}
242+
193243
export function isJavaExtEnabled(): boolean {
194244
const javaExt = vscode.extensions.getExtension(JAVA_EXTENSION_ID);
195245
return !!javaExt;

0 commit comments

Comments
 (0)