import { debug, commands, CancellationToken, DebugConfiguration, DebugConfigurationProvider, WorkspaceFolder, DebugConfigurationProviderTriggerKind, DebugSessionCustomEvent, Disposable, workspace, DebugSession } from "vscode"; import * as path from "path"; import psList from 'ps-list'; import { ListenablePreferenceSetting } from "@pivotal-tools/commons-vscode/lib/launch-util"; import { getPortPromise } from "portfinder"; import { RemoteBootApp } from "./live-hover-connect-ui"; const JMX_VM_ARG = '-Dspring.jmx.enabled='; const ACTUATOR_JMX_EXPOSURE_ARG = '-Dmanagement.endpoints.jmx.exposure.include='; 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 JMX_PORT_ARG = '-Dcom.sun.management.jmxremote.port='; export const TEST_RUNNER_MAIN_CLASSES = [ 'org.eclipse.jdt.internal.junit.runner.RemoteTestRunner', 'com.microsoft.java.test.runner.Launcher' ]; interface ProcessEvent { type: string; processId: number; shellProcessId: number } class SpringBootDebugConfigProvider implements DebugConfigurationProvider { async resolveDebugConfigurationWithSubstitutedVariables(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken) { // Running app live hovers support if (!TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isActuatorOnClasspath(debugConfiguration)) { if (typeof debugConfiguration.vmArgs === 'string') { if (debugConfiguration.vmArgs.indexOf(JMX_VM_ARG) < 0) { debugConfiguration.vmArgs += ` ${JMX_VM_ARG}true`; } if (debugConfiguration.vmArgs.indexOf(ACTUATOR_JMX_EXPOSURE_ARG) < 0) { debugConfiguration.vmArgs += ` ${ACTUATOR_JMX_EXPOSURE_ARG}*`; } if (debugConfiguration.vmArgs.indexOf(ADMIN_VM_ARG) < 0) { debugConfiguration.vmArgs += ` ${ADMIN_VM_ARG}true`; } if (debugConfiguration.vmArgs.indexOf(BOOT_PROJECT_ARG) < 0) { debugConfiguration.vmArgs += ` ${BOOT_PROJECT_ARG}${debugConfiguration.projectName}`; } if (debugConfiguration.vmArgs.indexOf(RMI_HOSTNAME) < 0) { debugConfiguration.vmArgs += ` ${RMI_HOSTNAME}`; } if (debugConfiguration.vmArgs.indexOf(JMX_PORT_ARG) < 0) { debugConfiguration.vmArgs += ` ${JMX_PORT_ARG}${await getPortPromise({ startPort: 10000 })} -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false` } } else { debugConfiguration.vmArgs = `${JMX_VM_ARG}true ${ACTUATOR_JMX_EXPOSURE_ARG}* ${ADMIN_VM_ARG}true ${BOOT_PROJECT_ARG}${debugConfiguration.projectName} ${RMI_HOSTNAME} ${JMX_PORT_ARG}${await getPortPromise({ startPort: 10000 })} -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false`; } } return debugConfiguration; } } 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(); } } else { if (listener) { listener.dispose(); listener = undefined; } } }); return { dispose: () => { if (listener) { listener.dispose(); } listenableSetting.dispose(); } }; } export function startDebugSupport(): Disposable { return Disposable.from( hookListenerToBooleanPreference( 'boot-java.live-information.automatic-connection.on', () => debug.registerDebugConfigurationProvider('java', new SpringBootDebugConfigProvider(), DebugConfigurationProviderTriggerKind.Initial) ), debug.onDidReceiveDebugSessionCustomEvent(handleCustomDebugEvent), debug.onDidTerminateDebugSession(handleTerminateDebugSession) ); } async function handleCustomDebugEvent(e: DebugSessionCustomEvent): Promise { if (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 vmArgs = debugConfiguration.vmArgs as string; let idx = vmArgs.indexOf(JMX_PORT_ARG) + JMX_PORT_ARG.length; let jmxPort = ""; for (; idx < vmArgs.length && vmArgs.charAt(idx) <= "9" && vmArgs.charAt(idx) >= "0"; idx++) { jmxPort += vmArgs.charAt(idx); } await commands.executeCommand("vscode-spring-boot.live.activate", { host: "127.0.0.1", port: null, urlScheme: "http", jmxurl: `service:jmx:rmi:///jndi/rmi://127.0.0.1:${jmxPort}/jmxrmi`, manualConnect: true, processId: pid.toString(), processName: debugConfiguration.mainClass, projectName: debugConfiguration.projectName } as RemoteBootApp); if (workspace.getConfiguration("boot-java.live-information.automatic-connection").get("on")) { await commands.executeCommand("vscode-spring-boot.live.show.active"); } }, 500); } } } function handleTerminateDebugSession(session: DebugSession) { commands.executeCommand('vscode-spring-boot.live.deactivate'); } async function getAppPid(e: ProcessEvent): Promise { if (e.processId && e.processId > 0) { return e.processId; } else if (e.shellProcessId) { const processes = await psList(); const appProcess = processes.find(p => p.ppid === e.shellProcessId); if (appProcess) { return appProcess.pid; } throw Error(`No child process found for parent shell process with pid = ${e.shellProcessId}`); } else { throw Error('No pid or parent shell process id available'); } } function isActuatorOnClasspath(debugConfiguration: DebugConfiguration): boolean { if (Array.isArray(debugConfiguration.classPaths)) { return !!debugConfiguration.classPaths.find(isActuatorJarFile); } return false; } function isActuatorJarFile(f: string): boolean { const fileName = path.basename(f || ""); if (/^spring-boot-actuator-\d+\.\d+\.\d+(.*)?.jar$/.test(fileName)) { return true; } return false; } function canConnect(debugConfiguration: DebugConfiguration): boolean { if (!TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isActuatorOnClasspath(debugConfiguration)) { return typeof debugConfiguration.vmArgs === 'string' && debugConfiguration.vmArgs.indexOf(`${JMX_VM_ARG}true`) >= 0 && debugConfiguration.vmArgs.indexOf(JMX_PORT_ARG) >= 0 && debugConfiguration.vmArgs.indexOf(`${ADMIN_VM_ARG}true`) >= 0 } return false; }