IDE support for TestJars: maven projects in the workspace only
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
import * as OS from "os";
|
||||
import * as VSCode from 'vscode';
|
||||
import { workspace } from 'vscode';
|
||||
import {
|
||||
commands,
|
||||
window,
|
||||
workspace,
|
||||
ExtensionContext,
|
||||
Uri
|
||||
} from 'vscode';
|
||||
|
||||
import * as commons from '@pivotal-tools/commons-vscode';
|
||||
import * as liveHoverUi from './live-hover-connect-ui';
|
||||
@@ -24,7 +29,7 @@ const FACTORIES_LANGUAGE_ID = "spring-factories";
|
||||
const STOP_ASKING = "Stop Asking";
|
||||
|
||||
/** Called when extension is activated */
|
||||
export function activate(context: VSCode.ExtensionContext): Thenable<ExtensionAPI> {
|
||||
export function activate(context: ExtensionContext): Thenable<ExtensionAPI> {
|
||||
|
||||
// registerPipelineGenerator(context);
|
||||
let options : commons.ActivatorOptions = {
|
||||
@@ -36,14 +41,14 @@ export function activate(context: VSCode.ExtensionContext): Thenable<ExtensionAP
|
||||
vmArgs: [
|
||||
'-XX:+HeapDumpOnOutOfMemoryError'
|
||||
],
|
||||
checkjvm: (context: VSCode.ExtensionContext, jvm: commons.JVM) => {
|
||||
checkjvm: (context: ExtensionContext, jvm: commons.JVM) => {
|
||||
let version = jvm.getMajorVersion();
|
||||
if (version < 17) {
|
||||
throw Error(`Spring Tools Language Server requires Java 17 or higher to be launched. Current Java version is ${version}`);
|
||||
}
|
||||
|
||||
if (!jvm.isJdk()) {
|
||||
VSCode.window.showWarningMessage(
|
||||
window.showWarningMessage(
|
||||
'JAVA_HOME or PATH environment variable seems to point to a JRE. A JDK is required, hence Boot Hints are unavailable.',
|
||||
STOP_ASKING).then(selection => {
|
||||
if (selection === STOP_ASKING) {
|
||||
@@ -58,7 +63,7 @@ export function activate(context: VSCode.ExtensionContext): Thenable<ExtensionAP
|
||||
mainClass: 'org.springframework.ide.vscode.boot.app.BootLanguageServerBootApp',
|
||||
configFileName: 'application.properties'
|
||||
},
|
||||
workspaceOptions: VSCode.workspace.getConfiguration("spring-boot.ls"),
|
||||
workspaceOptions: workspace.getConfiguration("spring-boot.ls"),
|
||||
clientOptions: {
|
||||
markdown: {
|
||||
isTrusted: true
|
||||
@@ -88,7 +93,7 @@ export function activate(context: VSCode.ExtensionContext): Thenable<ExtensionAP
|
||||
}
|
||||
return uri.toString();
|
||||
},
|
||||
protocol2Code: uri => VSCode.Uri.parse(uri)
|
||||
protocol2Code: uri => Uri.parse(uri)
|
||||
},
|
||||
// See PT-158992999 as to why a scheme is added to the document selector
|
||||
// documentSelector: [ PROPERTIES_LANGUAGE_ID, YAML_LANGUAGE_ID, JAVA_LANGUAGE_ID ],
|
||||
@@ -132,34 +137,34 @@ export function activate(context: VSCode.ExtensionContext): Thenable<ExtensionAP
|
||||
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 => {
|
||||
VSCode.commands.registerCommand('vscode-spring-boot.ls.start', () => client.start().then(() => {
|
||||
commands.registerCommand('vscode-spring-boot.ls.start', () => client.start().then(() => {
|
||||
// Boot LS is fully started
|
||||
registerClasspathService(client);
|
||||
registerJavaDataService(client);
|
||||
|
||||
// Force classpath listener to be enabled. Boot LS can only be launched iff classpath is available and there Spring-Boot on the classpath somewhere.
|
||||
VSCode.commands.executeCommand('sts.vscode-spring-boot.enableClasspathListening', true);
|
||||
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());
|
||||
}));
|
||||
VSCode.commands.registerCommand('vscode-spring-boot.ls.stop', () => client.stop());
|
||||
commands.registerCommand('vscode-spring-boot.ls.stop', () => client.stop());
|
||||
liveHoverUi.activate(client, options, context);
|
||||
rewrite.activate(client, options, context);
|
||||
setLogLevelUi.activate(client, options, context);
|
||||
|
||||
VSCode.commands.registerCommand('vscode-spring-boot.spring.modulith.metadata.refresh', async () => {
|
||||
const modulithProjects = await VSCode.commands.executeCommand('sts/modulith/projects');
|
||||
commands.registerCommand('vscode-spring-boot.spring.modulith.metadata.refresh', async () => {
|
||||
const modulithProjects = await commands.executeCommand('sts/modulith/projects');
|
||||
const projectNames = Object.keys(modulithProjects);
|
||||
if (projectNames.length === 0) {
|
||||
VSCode.window.showErrorMessage('No Spring Modulith projects found');
|
||||
window.showErrorMessage('No Spring Modulith projects found');
|
||||
} else {
|
||||
const projectName = projectNames.length === 1 ? projectNames[0] : await VSCode.window.showQuickPick(
|
||||
const projectName = projectNames.length === 1 ? projectNames[0] : await window.showQuickPick(
|
||||
projectNames,
|
||||
{ placeHolder: "Select the target project." },
|
||||
);
|
||||
VSCode.commands.executeCommand('sts/modulith/metadata/refresh', modulithProjects[projectName]);
|
||||
commands.executeCommand('sts/modulith/metadata/refresh', modulithProjects[projectName]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, ProviderResult, WorkspaceFolder } from "vscode";
|
||||
import { debug,
|
||||
window,
|
||||
commands,
|
||||
workspace,
|
||||
CancellationToken,
|
||||
DebugConfiguration,
|
||||
DebugConfigurationProvider,
|
||||
WorkspaceFolder,
|
||||
DebugConfigurationProviderTriggerKind,
|
||||
DebugSession,
|
||||
DebugSessionCustomEvent,
|
||||
Disposable
|
||||
} from "vscode";
|
||||
import * as path from "path";
|
||||
import * as VSCode from "vscode";
|
||||
import { Disposable } from "vscode";
|
||||
import psList from 'ps-list';
|
||||
import { ListenablePreferenceSetting } from "@pivotal-tools/commons-vscode/lib/launch-util";
|
||||
import * as fs from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
const JMX_VM_ARG = '-Dspring.jmx.enabled='
|
||||
const ACTUATOR_JMX_EXPOSURE_ARG = '-Dmanagement.endpoints.jmx.exposure.include='
|
||||
@@ -11,15 +23,57 @@ 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 = [
|
||||
'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;
|
||||
shellProcessId: number
|
||||
}
|
||||
|
||||
class SpringBootDebugConfigProvider implements DebugConfigurationProvider {
|
||||
|
||||
resolveDebugConfigurationWithSubstitutedVariables(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult<DebugConfiguration> {
|
||||
if (!TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isActuatorOnClasspath(debugConfiguration)) {
|
||||
async resolveDebugConfigurationWithSubstitutedVariables(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise<DebugConfiguration> {
|
||||
// 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}`);
|
||||
}
|
||||
}
|
||||
// Running app live hovers support
|
||||
if (isAutoConnectOn() && !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`;
|
||||
@@ -43,58 +97,46 @@ class SpringBootDebugConfigProvider implements DebugConfigurationProvider {
|
||||
return debugConfiguration;
|
||||
}
|
||||
|
||||
}
|
||||
private createEnvVarName(project: ExecutableBootProject) {
|
||||
return `${ENV_TESTJAR_ARTIFACT_PREFIX}${project.gav.replace(/:/g, "_")}`;
|
||||
}
|
||||
|
||||
interface ProcessEvent {
|
||||
type: string;
|
||||
processId: number;
|
||||
shellProcessId: number
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
function hookListenerToBooleanPreference(setting: string, listenerCreator: () => Disposable): Disposable {
|
||||
const listenableSetting = new ListenablePreferenceSetting<boolean>(setting);
|
||||
let listener: Disposable | undefined = listenableSetting.value ? listenerCreator() : undefined;
|
||||
listenableSetting.onDidChangeValue(() => {
|
||||
if (listenableSetting.value) {
|
||||
if (!listener) {
|
||||
listener = listenerCreator();
|
||||
}
|
||||
} else {
|
||||
if (listener) {
|
||||
listener.dispose();
|
||||
listener = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
if (listener) {
|
||||
listener.dispose();
|
||||
}
|
||||
listenableSetting.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function startDebugSupport(): Disposable {
|
||||
return hookListenerToBooleanPreference(
|
||||
'boot-java.live-information.automatic-connection.on',
|
||||
() => Disposable.from(
|
||||
VSCode.debug.onDidReceiveDebugSessionCustomEvent(handleCustomDebugEvent),
|
||||
VSCode.debug.registerDebugConfigurationProvider('java', new SpringBootDebugConfigProvider(), VSCode.DebugConfigurationProviderTriggerKind.Initial)
|
||||
)
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
async function handleCustomDebugEvent(e: VSCode.DebugSessionCustomEvent): Promise<void> {
|
||||
if (e.session?.type === 'java' && e?.body?.type === 'processid') {
|
||||
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<void> {
|
||||
if (isAutoConnectOn() && e.session?.type === 'java' && e?.body?.type === 'processid') {
|
||||
const debugConfiguration: DebugConfiguration = e.session.configuration;
|
||||
if (canConnect(debugConfiguration)) {
|
||||
setTimeout(async () => {
|
||||
const pid = await getAppPid(e.body as ProcessEvent);
|
||||
const processKey = pid.toString();
|
||||
VSCode.commands.executeCommand('sts/livedata/connect', { processKey });
|
||||
commands.executeCommand('sts/livedata/connect', { processKey });
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
@@ -130,6 +172,26 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user