diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchActivator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchActivator.java index 40eea70e9..cd40c5ecd 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchActivator.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchActivator.java @@ -48,7 +48,7 @@ public class BootLaunchActivator extends AbstractUIPlugin { myStore.setValue("cglib.breakpoint.warning.disabled", true); } - launchManager.addLaunchListener(TestJarLaunchListener.getSingletonInstance()); + TestJarSupport.start(); } private void setPreference(String plugin, String key, boolean value) { @@ -74,8 +74,7 @@ public class BootLaunchActivator extends AbstractUIPlugin { if (workspaceListener!=null) { workspaceListener.dispose(); } - ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); - launchManager.removeLaunchListener(TestJarLaunchListener.getSingletonInstance()); + TestJarSupport.stop(); super.stop(context); } diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/TestJarLaunchListener.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/TestJarLaunchListener.java index 3380b626c..989850c9b 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/TestJarLaunchListener.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/TestJarLaunchListener.java @@ -44,9 +44,7 @@ import org.springsource.ide.eclipse.commons.livexp.util.Log; import com.google.gson.reflect.TypeToken; @SuppressWarnings("restriction") -public class TestJarLaunchListener implements ILaunchesListener2 { - - private static TestJarLaunchListener instance; +class TestJarLaunchListener implements ILaunchesListener2 { private static final Pattern TESTJAR_PATTERN = Pattern.compile("^spring-boot-testjars-\\d+\\.\\d+\\.\\d+(.*)?.jar$"); @@ -54,13 +52,6 @@ public class TestJarLaunchListener implements ILaunchesListener2 { public record ExecutableProject(String name, String uri, String gav, String mainClass, Collection classpath) {} - public static TestJarLaunchListener getSingletonInstance() { - if (instance == null) { - instance = new TestJarLaunchListener(); - } - return instance; - } - public void launchRemoved(ILaunch launch) { clearTestJarWorkspaceProjectFiles(launch.getLaunchConfiguration()); } @@ -207,4 +198,12 @@ public class TestJarLaunchListener implements ILaunchesListener2 { } } + void clearTestJarArtifactEnvKeyFromLaunches(ILaunchManager launchManager) { + for (ILaunch l : launchManager.getLaunches()) { + if (!l.isTerminated() && l.getLaunchConfiguration() != null) { + clearTestJarWorkspaceProjectFiles(l.getLaunchConfiguration()); + } + } + } + } diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/TestJarSupport.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/TestJarSupport.java new file mode 100644 index 000000000..1b7831da6 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/TestJarSupport.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.eclipse.boot.launch; + +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.springframework.ide.eclipse.boot.core.BootActivator; +import org.springframework.ide.eclipse.boot.core.BootPreferences; + +public class TestJarSupport { + + private static final TestJarLaunchListener LAUNCHES_LISTENER = new TestJarLaunchListener(); + + private static final IPropertyChangeListener PREFERENCE_LISTENER = propertyChange -> { + if (BootPreferences.PREF_BOOT_TESTJARS_LAUNCH_SUPPORT.equals(propertyChange.getProperty())) { + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + if (BootActivator.getDefault().getPreferenceStore().getBoolean(BootPreferences.PREF_BOOT_TESTJARS_LAUNCH_SUPPORT)) { + // TestJars support ON + launchManager.addLaunchListener(LAUNCHES_LISTENER); + } else { + // TestJars support OFF + launchManager.removeLaunchListener(LAUNCHES_LISTENER); + LAUNCHES_LISTENER.clearTestJarArtifactEnvKeyFromLaunches(launchManager); + } + } + }; + + public static void start() { + if (BootActivator.getDefault().getPreferenceStore().getBoolean(BootPreferences.PREF_BOOT_TESTJARS_LAUNCH_SUPPORT)) { + BootActivator.getDefault().getPreferenceStore().addPropertyChangeListener(PREFERENCE_LISTENER); + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + launchManager.addLaunchListener(LAUNCHES_LISTENER); + } + + } + + public static void stop() { + if (BootActivator.getDefault().getPreferenceStore().getBoolean(BootPreferences.PREF_BOOT_TESTJARS_LAUNCH_SUPPORT)) { + BootActivator.getDefault().getPreferenceStore().removePropertyChangeListener(PREFERENCE_LISTENER); + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + launchManager.removeLaunchListener(LAUNCHES_LISTENER); + LAUNCHES_LISTENER.clearTestJarArtifactEnvKeyFromLaunches(launchManager); + } + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot/src/org/springframework/ide/eclipse/boot/core/BootPreferences.java b/eclipse-extensions/org.springframework.ide.eclipse.boot/src/org/springframework/ide/eclipse/boot/core/BootPreferences.java index a91c358cd..68be24ed4 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot/src/org/springframework/ide/eclipse/boot/core/BootPreferences.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot/src/org/springframework/ide/eclipse/boot/core/BootPreferences.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015, 2017 Pivotal, Inc. + * Copyright (c) 2015, 2024 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -44,6 +44,9 @@ public class BootPreferences implements IPreferenceChangeListener { public static final String PREF_BOOT_FAST_STARTUP_JVM_ARGS = "org.springframework.ide.eclipse.boot.FastStartupJvmArgs"; public static final String BOOT_FAST_STARTUP_DEFAULT_JVM_ARGS = "-XX:TieredStopAtLevel=1"; + public static final String PREF_BOOT_TESTJARS_LAUNCH_SUPPORT = "org.springframework.ide.eclipse.boot.TestJarsLaunch"; + + // Boot Preference Page ID public static final String BOOT_PREFERENCE_PAGE_ID = "org.springframework.ide.eclipse.boot.ui.preferences.BootPreferencePage"; diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot/src/org/springframework/ide/eclipse/boot/ui/preferences/BootPreferencePage.java b/eclipse-extensions/org.springframework.ide.eclipse.boot/src/org/springframework/ide/eclipse/boot/ui/preferences/BootPreferencePage.java index 1940d77e3..ac5c2f714 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot/src/org/springframework/ide/eclipse/boot/ui/preferences/BootPreferencePage.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot/src/org/springframework/ide/eclipse/boot/ui/preferences/BootPreferencePage.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015, 2023 Pivotal, Inc. + * Copyright (c) 2015, 2024 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -15,6 +15,7 @@ import static org.springframework.ide.eclipse.boot.core.BootPreferences.PREF_BOO import static org.springframework.ide.eclipse.boot.core.BootPreferences.PREF_BOOT_FAST_STARTUP_REMIND_MESSAGE; import static org.springframework.ide.eclipse.boot.core.BootPreferences.PREF_BOOT_THIN_WRAPPER; import static org.springframework.ide.eclipse.boot.core.BootPreferences.PREF_IGNORE_SILENT_EXIT; +import static org.springframework.ide.eclipse.boot.core.BootPreferences.PREF_BOOT_TESTJARS_LAUNCH_SUPPORT; import org.eclipse.debug.internal.ui.preferences.BooleanFieldEditor2; import org.eclipse.jface.layout.GridDataFactory; @@ -71,6 +72,8 @@ public class BootPreferencePage extends FieldEditorPreferencePage implements IWo thinLauncher.setErrorMessage("Thin launcher must be an existing file"); setTooltip(thinLauncherComposite, thinLauncher, "Thin boot launcher jar to use in Spring Boot Launch configuration (when that option is enabled in the launch config)"); addField(thinLauncher); + + addField(new BooleanFieldEditor2(PREF_BOOT_TESTJARS_LAUNCH_SUPPORT, "TestJars Support", SWT.CHECK, launchGroup)); } diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot/src/org/springframework/ide/eclipse/boot/ui/preferences/PreferencesInitializer.java b/eclipse-extensions/org.springframework.ide.eclipse.boot/src/org/springframework/ide/eclipse/boot/ui/preferences/PreferencesInitializer.java index 9b300ab0c..6ce574317 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot/src/org/springframework/ide/eclipse/boot/ui/preferences/PreferencesInitializer.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot/src/org/springframework/ide/eclipse/boot/ui/preferences/PreferencesInitializer.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2017 Pivotal, Inc. + * Copyright (c) 2016, 2024 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -17,6 +17,7 @@ import static org.springframework.ide.eclipse.boot.core.BootPreferences.PREF_BOO import static org.springframework.ide.eclipse.boot.core.BootPreferences.PREF_BOOT_FAST_STARTUP_REMIND_MESSAGE; import static org.springframework.ide.eclipse.boot.core.BootPreferences.PREF_BOOT_FAST_STARTUP_JVM_ARGS; import static org.springframework.ide.eclipse.boot.core.BootPreferences.BOOT_FAST_STARTUP_DEFAULT_JVM_ARGS; +import static org.springframework.ide.eclipse.boot.core.BootPreferences.PREF_BOOT_TESTJARS_LAUNCH_SUPPORT; import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer; import org.eclipse.jface.preference.IPreferenceStore; @@ -25,7 +26,7 @@ import org.springsource.ide.eclipse.commons.core.preferences.StsProperties; /** * Initializer of default values for the Spring Boot support - * + * * @author Alex Boyko * */ @@ -34,7 +35,7 @@ public class PreferencesInitializer extends AbstractPreferenceInitializer { @Override public void initializeDefaultPreferences() { IPreferenceStore store = BootActivator.getDefault().getPreferenceStore(); - + /* * Note that line below cannot be moved to BootPreferencePage static * init method because the class has BooleanFieldEditor2 import that in @@ -42,13 +43,15 @@ public class PreferencesInitializer extends AbstractPreferenceInitializer { * because workbench may not be started in the case of a unit test */ store.setDefault(PREF_IGNORE_SILENT_EXIT, DEFAULT_PREF_IGNORE_SILENT_EXIT); - + store.setDefault(PREF_INITIALIZR_URL, StsProperties.getInstance().get("spring.initializr.json.url")); - + store.setDefault(PREF_BOOT_FAST_STARTUP_DEFAULT, true); store.setDefault(PREF_BOOT_FAST_STARTUP_REMIND_MESSAGE, true); store.setDefault(PREF_BOOT_FAST_STARTUP_JVM_ARGS, BOOT_FAST_STARTUP_DEFAULT_JVM_ARGS); + store.setDefault(BOOT_FAST_STARTUP_DEFAULT_JVM_ARGS, DEFAULT_PREF_IGNORE_SILENT_EXIT); + store.setDefault(PREF_BOOT_TESTJARS_LAUNCH_SUPPORT, true); } - + } diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts index e25d523fc..d0c4895ba 100644 --- a/vscode-extensions/vscode-spring-boot/lib/Main.ts +++ b/vscode-extensions/vscode-spring-boot/lib/Main.ts @@ -19,6 +19,7 @@ import { ExtensionAPI } from "./api"; import {registerClasspathService} from "@pivotal-tools/commons-vscode/lib/classpath"; import {registerJavaDataService} from "@pivotal-tools/commons-vscode/lib/java-data"; import * as setLogLevelUi from './set-log-levels-ui'; +import { startTestJarSupport } from "./test-jar-launch"; const PROPERTIES_LANGUAGE_ID = "spring-boot-properties"; const YAML_LANGUAGE_ID = "spring-boot-properties-yaml"; @@ -137,6 +138,9 @@ export function activate(context: ExtensionContext): Thenable { highlightCodeLensSettingKey: 'boot-java.highlight-codelens.on' }; + // Register launch config contributior to java debug launch to be able to connect to JMX + context.subscriptions.push(startDebugSupport()); + return commons.activate(options, context).then(client => { commands.registerCommand('vscode-spring-boot.ls.start', () => client.start().then(() => { // Boot LS is fully started @@ -146,8 +150,9 @@ export function activate(context: ExtensionContext): Thenable { // Force classpath listener to be enabled. Boot LS can only be launched iff classpath is available and there Spring-Boot on the classpath somewhere. commands.executeCommand('sts.vscode-spring-boot.enableClasspathListening', true); - // Register launch config contributior to java debug launch to be able to connect to JMX - context.subscriptions.push(startDebugSupport()); + // Register TestJars launch support + context.subscriptions.push(startTestJarSupport()); + })); commands.registerCommand('vscode-spring-boot.ls.stop', () => client.stop()); liveHoverUi.activate(client, options, context); diff --git a/vscode-extensions/vscode-spring-boot/lib/debug-config-provider.ts b/vscode-extensions/vscode-spring-boot/lib/debug-config-provider.ts index 3a9cf42e6..ad4f4e279 100644 --- a/vscode-extensions/vscode-spring-boot/lib/debug-config-provider.ts +++ b/vscode-extensions/vscode-spring-boot/lib/debug-config-provider.ts @@ -1,21 +1,17 @@ import { debug, - window, commands, - workspace, CancellationToken, DebugConfiguration, DebugConfigurationProvider, WorkspaceFolder, DebugConfigurationProviderTriggerKind, - DebugSession, DebugSessionCustomEvent, - Disposable + Disposable, + ProviderResult } from "vscode"; import * as path from "path"; import psList from 'ps-list'; -import * as fs from "fs"; -import { tmpdir } from "os"; -import { randomUUID } from "crypto"; +import { ListenablePreferenceSetting } from "@pivotal-tools/commons-vscode/lib/launch-util"; const JMX_VM_ARG = '-Dspring.jmx.enabled=' const ACTUATOR_JMX_EXPOSURE_ARG = '-Dmanagement.endpoints.jmx.exposure.include=' @@ -23,21 +19,11 @@ const ADMIN_VM_ARG = '-Dspring.application.admin.enabled=' const BOOT_PROJECT_ARG = '-Dspring.boot.project.name='; const RMI_HOSTNAME = '-Djava.rmi.server.hostname=localhost'; -const ENV_TESTJAR_ARTIFACT_PREFIX = "TESTJARS_ARTIFACT_"; - -const TEST_RUNNER_MAIN_CLASSES = [ +export const TEST_RUNNER_MAIN_CLASSES = [ 'org.eclipse.jdt.internal.junit.runner.RemoteTestRunner', 'com.microsoft.java.test.runner.Launcher' ]; -interface ExecutableBootProject { - name: string; - uri: string; - mainClass: string; - classpath: string[]; - gav: string; -} - interface ProcessEvent { type: string; processId: number; @@ -46,34 +32,9 @@ interface ProcessEvent { class SpringBootDebugConfigProvider implements DebugConfigurationProvider { - async resolveDebugConfigurationWithSubstitutedVariables(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise { - // TestJar launch support - if (TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isTestJarsOnClasspath(debugConfiguration)) { - const projects = await commands.executeCommand("sts/spring-boot/executableBootProjects") as ExecutableBootProject[]; - let env = debugConfiguration.env; - if (!env) { - env = {}; - debugConfiguration.env = env; - } - const projectsWithErrors: ExecutableBootProject[] = []; - // Create all project classparth data files and add env vars for workspace projects - await Promise.all(projects.map(async p => { - const envName = this.createEnvVarName(p); - if (!env[envName]) { - try { - env[envName] = await this.createFile(p); - } catch (error) { - projectsWithErrors.push(p); - } - } - })); - if (projectsWithErrors.length > 0) { - const projectStr = projectsWithErrors.map(p => `'${p.name}'`); - window.showWarningMessage(`TestJar Support: Could not provide data for workspace projects: ${projectStr}`); - } - } + resolveDebugConfigurationWithSubstitutedVariables(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { // Running app live hovers support - if (isAutoConnectOn() && !TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isActuatorOnClasspath(debugConfiguration)) { + if (!TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isActuatorOnClasspath(debugConfiguration)) { if (debugConfiguration.vmArgs) { if (debugConfiguration.vmArgs.indexOf(JMX_VM_ARG) < 0) { debugConfiguration.vmArgs += ` ${JMX_VM_ARG}true`; @@ -97,40 +58,46 @@ class SpringBootDebugConfigProvider implements DebugConfigurationProvider { return debugConfiguration; } - private createEnvVarName(project: ExecutableBootProject) { - return `${ENV_TESTJAR_ARTIFACT_PREFIX}${project.gav.replace(/:/g, "_")}`; - } +} - private async createFile(project: ExecutableBootProject) { - const filePath = path.join(tmpdir(), `${project.gav.replace(/:/g, "_")}-${randomUUID()}`); - await fs.writeFile(filePath, `# the main class to invoke\nmain=${project.mainClass}\n# the classpath to use delimited by the OS specific delimiters\nclasspath=${project.classpath.join(path.delimiter)}`, function(err) { - if(err) { - throw Error(); +export function hookListenerToBooleanPreference(setting: string, listenerCreator: () => Disposable): Disposable { + const listenableSetting = new ListenablePreferenceSetting(setting); + let listener: Disposable | undefined = listenableSetting.value ? listenerCreator() : undefined; + listenableSetting.onDidChangeValue(() => { + if (listenableSetting.value) { + if (!listener) { + listener = listenerCreator(); } - }); - return filePath; - } + } else { + if (listener) { + listener.dispose(); + listener = undefined; + } + } + }); + return { + dispose: () => { + if (listener) { + listener.dispose(); + } + listenableSetting.dispose(); + } + }; } export function startDebugSupport(): Disposable { - return Disposable.from( - debug.onDidReceiveDebugSessionCustomEvent(handleCustomDebugEvent), - debug.onDidTerminateDebugSession(cleanupDebugSession), - debug.registerDebugConfigurationProvider('java', new SpringBootDebugConfigProvider(), DebugConfigurationProviderTriggerKind.Initial), - new Disposable(() => cleanupDebugSession(debug.activeDebugSession)) // If VSCode is shutdown then clean active debug session if it satidfies conditions + return hookListenerToBooleanPreference( + 'boot-java.live-information.automatic-connection.on', + () => Disposable.from( + debug.onDidReceiveDebugSessionCustomEvent(handleCustomDebugEvent), + debug.registerDebugConfigurationProvider('java', new SpringBootDebugConfigProvider(), DebugConfigurationProviderTriggerKind.Initial) + ) ); } -async function cleanupDebugSession(session: DebugSession) { - // Handle termination of a Boot app with TestJars on the classpath - if (session.type === 'java' && TEST_RUNNER_MAIN_CLASSES.includes(session.configuration.mainClass) && isTestJarsOnClasspath(session.configuration) && session.configuration.env) { - await Promise.all(Object.keys(session.configuration.env).filter(k => k.startsWith(ENV_TESTJAR_ARTIFACT_PREFIX)).map(k => fs.rm(session.configuration.env[k], () => {}))); - } -} - async function handleCustomDebugEvent(e: DebugSessionCustomEvent): Promise { - if (isAutoConnectOn() && e.session?.type === 'java' && e?.body?.type === 'processid') { + if (e.session?.type === 'java' && e?.body?.type === 'processid') { const debugConfiguration: DebugConfiguration = e.session.configuration; if (canConnect(debugConfiguration)) { setTimeout(async () => { @@ -172,26 +139,6 @@ function isActuatorJarFile(f: string): boolean { return false; } -function isTestJarsOnClasspath(debugConfiguration: DebugConfiguration): boolean { - if (Array.isArray(debugConfiguration.classPaths)) { - return !!debugConfiguration.classPaths.find(isTestJarFile); - } - return false; - -} - -function isTestJarFile(f: string): boolean { - const fileName = path.basename(f || ""); - if (/^spring-boot-testjars-\d+\.\d+\.\d+(.*)?.jar$/.test(fileName)) { - return true; - } - return false; -} - -function isAutoConnectOn(): boolean { - return workspace.getConfiguration().get("boot-java.live-information.automatic-connection.on", true); -} - function canConnect(debugConfiguration: DebugConfiguration): boolean { if (!TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isActuatorOnClasspath(debugConfiguration)) { return debugConfiguration.vmArgs diff --git a/vscode-extensions/vscode-spring-boot/lib/test-jar-launch.ts b/vscode-extensions/vscode-spring-boot/lib/test-jar-launch.ts new file mode 100644 index 000000000..87c7af5e6 --- /dev/null +++ b/vscode-extensions/vscode-spring-boot/lib/test-jar-launch.ts @@ -0,0 +1,100 @@ +import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, DebugConfigurationProviderTriggerKind, DebugSession, Disposable, WorkspaceFolder, commands, debug, window } from "vscode"; +import { TEST_RUNNER_MAIN_CLASSES, hookListenerToBooleanPreference } from "./debug-config-provider"; +import path from "path"; +import { tmpdir } from "os"; +import { randomUUID } from "crypto"; +import * as fs from "fs"; + +const ENV_TESTJAR_ARTIFACT_PREFIX = "TESTJARS_ARTIFACT_"; + +interface ExecutableBootProject { + name: string; + uri: string; + mainClass: string; + classpath: string[]; + gav: string; +} + +class TestJarDebugConfigProvider implements DebugConfigurationProvider { + + async resolveDebugConfigurationWithSubstitutedVariables(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise { + // TestJar launch support + if (TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isTestJarsOnClasspath(debugConfiguration)) { + const projects = await commands.executeCommand("sts/spring-boot/executableBootProjects") as ExecutableBootProject[]; + let env = debugConfiguration.env; + if (!env) { + env = {}; + debugConfiguration.env = env; + } + const projectsWithErrors: ExecutableBootProject[] = []; + // Create all project classparth data files and add env vars for workspace projects + await Promise.all(projects.map(async p => { + const envName = this.createEnvVarName(p); + if (!env[envName]) { + try { + env[envName] = await this.createFile(p); + } catch (error) { + projectsWithErrors.push(p); + } + } + })); + if (projectsWithErrors.length > 0) { + const projectStr = projectsWithErrors.map(p => `'${p.name}'`); + window.showWarningMessage(`TestJar Support: Could not provide data for workspace projects: ${projectStr}`); + } + } + return debugConfiguration; + } + + private createEnvVarName(project: ExecutableBootProject) { + return `${ENV_TESTJAR_ARTIFACT_PREFIX}${project.gav.replace(/:/g, "_")}`; + } + + private async createFile(project: ExecutableBootProject) { + const filePath = path.join(tmpdir(), `${project.gav.replace(/:/g, "_")}-${randomUUID()}`); + await fs.writeFile(filePath, `# the main class to invoke\nmain=${project.mainClass}\n# the classpath to use delimited by the OS specific delimiters\nclasspath=${project.classpath.join(path.delimiter)}`, function(err) { + if(err) { + throw Error(); + } + }); + return filePath; + } + +} + +export function startTestJarSupport(): Disposable { + return hookListenerToBooleanPreference( + 'boot-java-vscode-only.test-jars', + () => Disposable.from( + debug.onDidTerminateDebugSession(cleanupDebugSession), + debug.registerDebugConfigurationProvider('java', new TestJarDebugConfigProvider(), DebugConfigurationProviderTriggerKind.Initial), + new Disposable(() => cleanupDebugSession(debug.activeDebugSession)) // If VSCode is shutdown then clean active debug session if it satisfies conditions + ) + ); +} + +async function cleanupDebugSession(session: DebugSession) { + // Handle termination of a Boot app with TestJars on the classpath + if (session.type === 'java' && TEST_RUNNER_MAIN_CLASSES.includes(session.configuration.mainClass) && isTestJarsOnClasspath(session.configuration) && session.configuration.env) { + await Promise.all(Object.keys(session.configuration.env).filter(k => k.startsWith(ENV_TESTJAR_ARTIFACT_PREFIX)).map(k => fs.rm(session.configuration.env[k], () => {}))); + } +} + +function isTestJarsOnClasspath(debugConfiguration: DebugConfiguration): boolean { + if (Array.isArray(debugConfiguration.classPaths)) { + return !!debugConfiguration.classPaths.find(isTestJarFile); + } + return false; + +} + +function isTestJarFile(f: string): boolean { + const fileName = path.basename(f || ""); + if (/^spring-boot-testjars-\d+\.\d+\.\d+(.*)?.jar$/.test(fileName)) { + return true; + } + return false; +} + + + diff --git a/vscode-extensions/vscode-spring-boot/package.json b/vscode-extensions/vscode-spring-boot/package.json index b84da8887..4b6cddab9 100644 --- a/vscode-extensions/vscode-spring-boot/package.json +++ b/vscode-extensions/vscode-spring-boot/package.json @@ -228,6 +228,11 @@ ] }, "description": "Array of jmx urls pointing to remote spring boot applications to poll for live hover information. A typical url looks something like this: `service:jmx:rmi://localhost:9111/jndi/rmi://localhost:9111/jmxrmi`" + }, + "boot-java-vscode-only.test-jars": { + "type": "boolean", + "default": true, + "description": "Enable/Disable Spring Boot TestJars launch environment variables" } } },