From 6822ba5c9d5d4729fdb1e8df3d615aa37e64ec5e Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 14 Jul 2026 13:45:51 -0700 Subject: [PATCH 1/5] Use VM agent JMX URL for local processes. Remove jdk_tools bundle Signed-off-by: BoykoAlex --- .../feature.xml | 7 -- .../eclipse/boot/dash/BootDashActivator.java | 39 +++++-- ...stractLaunchConfigurationsDashElement.java | 103 ++++++++++++------ .../model/actuator/JMXActuatorClient.java | 6 +- .../BootLaunchConfigurationDelegate.java | 17 +-- ...CliServiceLaunchConfigurationDelegate.java | 33 +----- .../boot/launch/livebean/JmxBeanSupport.java | 16 ++- .../launch/process/BootProcessFactory.java | 15 ++- ...ringApplicationLifeCycleClientManager.java | 20 +++- .../category.xml | 1 - .../META-INF/MANIFEST.MF | 1 - .../commons/core/util/ProcessUtils.java | 52 ++++----- .../.classpath | 6 - .../.settings/org.eclipse.jdt.core.prefs | 8 -- .../.settings/org.eclipse.pde.prefs | 32 ------ .../META-INF/MANIFEST.MF | 9 -- .../about.html | 22 ---- .../build.properties | 5 - .../pom.xml | 37 ------- eclipse-language-servers/pom.xml | 1 - .../boot/app/BootLanguageServerBootApp.java | 2 +- .../java/livehover/v2/LocalJvmAttach.java | 64 +++++++++++ .../v2/SpringProcessConnectorLocal.java | 53 ++------- .../lib/debug-config-provider.ts | 31 ++---- .../lib/live-hover-connect-ui.ts | 22 +++- .../vscode-spring-boot/package.json | 1 - 26 files changed, 271 insertions(+), 332 deletions(-) delete mode 100644 eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/.classpath delete mode 100644 eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/.settings/org.eclipse.jdt.core.prefs delete mode 100644 eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/.settings/org.eclipse.pde.prefs delete mode 100644 eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/META-INF/MANIFEST.MF delete mode 100644 eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/about.html delete mode 100644 eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/build.properties delete mode 100644 eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/pom.xml create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/LocalJvmAttach.java diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/feature.xml b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/feature.xml index 1ceb868aa7..db3b1b2be7 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/feature.xml +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash.feature/feature.xml @@ -86,11 +86,4 @@ version="0.0.0" unpack="false"/> - - diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/BootDashActivator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/BootDashActivator.java index ab5718f9ef..926c969cd7 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/BootDashActivator.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/BootDashActivator.java @@ -11,6 +11,7 @@ package org.springframework.ide.eclipse.boot.dash; import java.util.Arrays; +import java.util.HashMap; import java.util.Map; import org.eclipse.core.net.proxy.IProxyService; @@ -206,15 +207,21 @@ public void stateChanged(ILaunchConfiguration owner) { for (IProcess p : l.getProcesses()) { String pid = p.getAttribute(IProcess.ATTR_PROCESS_ID); if (pid != null) { - CommandInfo cmd = new CommandInfo("sts/livedata/localAdd", Map.of( - "host", "127.0.0.1", - "urlScheme", "http", - "jmxurl", "service:jmx:rmi:///jndi/rmi://127.0.0.1:%s/jmxrmi".formatted(l.getAttribute(BootLaunchConfigurationDelegate.JMX_PORT)), - "manualConnect", !BootLaunchConfigurationDelegate.getAutoConnect(owner), - "processId", pid, - "processName", owner.getAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, p.getLabel()), - "projectName", project.getProject().getName() - )); + Map liveDataArgs = new HashMap<>(); + liveDataArgs.put("host", "127.0.0.1"); + liveDataArgs.put("urlScheme", "http"); + // Only a pinned, nonzero JMX port has a stable jmxurl. For the + // auto/dynamic port case no port is picked here; the language + // server attaches to 'processId' on-demand instead. + int jmxPort = BootLaunchConfigurationDelegate.getJMXPortAsInt(l); + if (jmxPort > 0) { + liveDataArgs.put("jmxurl", "service:jmx:rmi:///jndi/rmi://127.0.0.1:%s/jmxrmi".formatted(jmxPort)); + } + liveDataArgs.put("manualConnect", !BootLaunchConfigurationDelegate.getAutoConnect(owner)); + liveDataArgs.put("processId", pid); + liveDataArgs.put("processName", owner.getAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, p.getLabel())); + liveDataArgs.put("projectName", project.getProject().getName()); + CommandInfo cmd = new CommandInfo("sts/livedata/localAdd", liveDataArgs); LiveProcessCommandsExecutor.getDefault().executeCommand(cmd).subscribe(); } @@ -224,9 +231,17 @@ public void stateChanged(ILaunchConfiguration owner) { break; case INACTIVE: for (ILaunch l : DebugPlugin.getDefault().getLaunchManager().getLaunches()) { - if (l.getLaunchConfiguration() == owner && l.getAttribute(BootLaunchConfigurationDelegate.JMX_PORT) != null) { - String jmxUrl = "service:jmx:rmi:///jndi/rmi://127.0.0.1:%s/jmxrmi".formatted(l.getAttribute(BootLaunchConfigurationDelegate.JMX_PORT)); - LiveProcessCommandsExecutor.getDefault().executeCommand("sts/livedata/localRemove", jmxUrl).subscribe(); + if (l.getLaunchConfiguration() == owner) { + int jmxPort = BootLaunchConfigurationDelegate.getJMXPortAsInt(l); + // Must match the key used when the app was added above: jmxurl if a + // port was pinned, otherwise the process id (see PROCESS_ID, captured + // by BootProcessFactory for the auto/dynamic port case). + String processKey = jmxPort > 0 + ? "service:jmx:rmi:///jndi/rmi://127.0.0.1:%s/jmxrmi".formatted(jmxPort) + : l.getAttribute(BootLaunchConfigurationDelegate.PROCESS_ID); + if (processKey != null) { + LiveProcessCommandsExecutor.getDefault().executeCommand("sts/livedata/localRemove", processKey).subscribe(); + } } } default: diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AbstractLaunchConfigurationsDashElement.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AbstractLaunchConfigurationsDashElement.java index 86cbb68567..b1b5622ef0 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AbstractLaunchConfigurationsDashElement.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/AbstractLaunchConfigurationsDashElement.java @@ -50,6 +50,7 @@ import org.springsource.ide.eclipse.commons.core.pstore.IPropertyStore; import org.springsource.ide.eclipse.commons.core.pstore.PropertyStoreApi; import org.springsource.ide.eclipse.commons.core.pstore.PropertyStores; +import org.springsource.ide.eclipse.commons.core.util.ProcessUtils; import org.springsource.ide.eclipse.commons.frameworks.core.maintype.MainTypeFinder; import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression; import org.springsource.ide.eclipse.commons.livexp.core.DisposeListener; @@ -518,7 +519,30 @@ public String toString() { } protected ActuatorClient getActuatorClient() { - return JMXActuatorClient.forPort(getTypeLookup(), this::getJmxPort); + return JMXActuatorClient.forUrl(getTypeLookup(), this::getJmxUrl); + } + + /** + * JMX URL for this element's running process: a fixed jmxurl if a JMX port was pinned, + * otherwise attaches to the process id on-demand to resolve its local management agent's + * URL (see ProcessUtils.getLocalManagementAgentUrl). + */ + private String getJmxUrl() { + for (ILaunchConfiguration c : getLaunchConfigs()) { + for (ILaunch l : LaunchUtils.getLaunches(c)) { + if (!l.isTerminated()) { + int port = BootLaunchConfigurationDelegate.getJMXPortAsInt(l); + if (port>0) { + return JMXClient.createLocalJmxUrl(port); + } + String pid = l.getAttribute(BootLaunchConfigurationDelegate.PROCESS_ID); + if (pid!=null) { + return ProcessUtils.getLocalManagementAgentUrl(pid); + } + } + } + } + return null; } @Override @@ -602,13 +626,13 @@ public Failable> getLiveProfiles() { if (liveProfiles == null) { liveProfiles = PollingLiveExp.create(Failable.>error(MissingLiveInfoMessages.NOT_YET_COMPUTED), () -> { try { - String localJmxUrl = JMXClient.createLocalJmxUrl(getJmxPort()); - if (localJmxUrl == null) { + String processKey = getProcessKey(); + if (processKey == null) { return Failable.error(getBootDashModel().getRunTarget().getType().getMissingLiveInfoMessages().getMissingInfoMessage(getName(), "profiles")); } String[] o = BootLsCommandUtils.executeCommand(TypeToken.get(String[].class), "sts/livedata/get", Map.of( "endpoint", "profiles", - "processKey", localJmxUrl + "processKey", processKey )).get().orElse(new String[0]); liveProfiles.refreshOnce(); return Failable.of(ImmutableSet.copyOf(o)); @@ -632,18 +656,27 @@ public Failable> getLiveProfiles() { } } - private int getJmxPort() { + /** + * Key identifying this element's running process to the language server's live data + * connections: a jmxurl if a fixed JMX port was pinned, otherwise the process id (the + * language server attaches to it on-demand; see PROCESS_ID / SpringApplicationLifeCycleClientManager). + */ + private String getProcessKey() { for (ILaunchConfiguration c : getLaunchConfigs()) { for (ILaunch l : LaunchUtils.getLaunches(c)) { if (!l.isTerminated()) { int port = BootLaunchConfigurationDelegate.getJMXPortAsInt(l); if (port>0) { - return port; + return JMXClient.createLocalJmxUrl(port); + } + String pid = l.getAttribute(BootLaunchConfigurationDelegate.PROCESS_ID); + if (pid!=null) { + return pid; } } } } - return -1; + return null; } private int getLivePort(String propName) { @@ -659,35 +692,33 @@ private int getLivePort(String propName) { for (ILaunch l : BootLaunchUtils.getLaunches(conf)) { if (!l.isTerminated()) { debug("["+this.getName()+"] getLivePort("+propName+") found a launch"); - int jmxPort = BootLaunchConfigurationDelegate.getJMXPortAsInt(l); - debug("["+this.getName()+"] getLivePort("+propName+") jmxPort = "+jmxPort); - if (jmxPort>0) { - SpringApplicationLifeCycleClientManager cm = null; - try { - cm = new SpringApplicationLifeCycleClientManager(jmxPort); - SpringApplicationLifecycleClient c = cm.getLifeCycleClient(); - debug("["+this.getName()+"] getLivePort("+propName+") lifeCycleClient = "+c); - if (c!=null) { - //Just because lifecycle bean is ready does not mean that the port property has already been set. - //To avoid race condition we should wait here until the port is set (some apps aren't web apps and - //may never get a port set, so we shouldn't wait indefinitely!) - return RetryUtil.retry(100, 1000, () -> { - debug("["+this.getName()+"] getLivePort("+propName+") trying to get..."); - int port = c.getProperty(propName, -1); - debug("["+this.getName()+"] getLivePort("+propName+") port = "+ port); - if (port<=0) { - throw new IllegalStateException("port not (yet) set"); - } - return port; - }); - } - } catch (Exception e) { - debug(ExceptionUtil.getMessage(e)); - //most likely this just means the app isn't running so ignore - } finally { - if (cm!=null) { - cm.disposeClient(); - } + // Uses a pinned JMX port directly if one was set, otherwise attaches to + // the launch's process id on-demand (see SpringApplicationLifeCycleClientManager). + SpringApplicationLifeCycleClientManager cm = null; + try { + cm = new SpringApplicationLifeCycleClientManager(l); + SpringApplicationLifecycleClient c = cm.getLifeCycleClient(); + debug("["+this.getName()+"] getLivePort("+propName+") lifeCycleClient = "+c); + if (c!=null) { + //Just because lifecycle bean is ready does not mean that the port property has already been set. + //To avoid race condition we should wait here until the port is set (some apps aren't web apps and + //may never get a port set, so we shouldn't wait indefinitely!) + return RetryUtil.retry(100, 1000, () -> { + debug("["+this.getName()+"] getLivePort("+propName+") trying to get..."); + int port = c.getProperty(propName, -1); + debug("["+this.getName()+"] getLivePort("+propName+") port = "+ port); + if (port<=0) { + throw new IllegalStateException("port not (yet) set"); + } + return port; + }); + } + } catch (Exception e) { + debug(ExceptionUtil.getMessage(e)); + //most likely this just means the app isn't running so ignore + } finally { + if (cm!=null) { + cm.disposeClient(); } } } diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/JMXActuatorClient.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/JMXActuatorClient.java index 0ad8daf92d..78b296c0c0 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/JMXActuatorClient.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/model/actuator/JMXActuatorClient.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2019 Pivotal, Inc. + * Copyright (c) 2017, 2026 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 @@ -63,10 +63,6 @@ public OperationInfo(String objectName, String operationName, String version) { private JMXClient client = null; - public static JMXActuatorClient forPort(TypeLookup typeLookup, Supplier jmxPort) { - return new JMXActuatorClient(typeLookup, () -> JMXClient.createLocalJmxUrl(jmxPort.get())); - } - public static JMXActuatorClient forUrl(TypeLookup typeLookup, Supplier jmxUrl) { return new JMXActuatorClient(typeLookup, jmxUrl); } diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchConfigurationDelegate.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchConfigurationDelegate.java index 360d4194f5..0a43747dc1 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchConfigurationDelegate.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchConfigurationDelegate.java @@ -57,7 +57,6 @@ import org.springframework.ide.eclipse.boot.launch.livebean.JmxBeanSupport.Feature; import org.springframework.ide.eclipse.boot.launch.process.BootProcessFactory; import org.springframework.ide.eclipse.boot.launch.profiles.ProfileHistory; -import org.springframework.ide.eclipse.boot.launch.util.PortFinder; import org.springsource.ide.eclipse.commons.core.util.OsUtils; import org.springsource.ide.eclipse.commons.core.util.StringUtil; import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; @@ -236,16 +235,20 @@ public String getVMArguments(ILaunchConfiguration conf) } catch (Exception e) { //ignore: bad data in launch config. } - if (port==0) { - port = PortFinder.findFreePort(); //slightly better than calling JmxBeanSupport.randomPort() - } + // port==0 means 'auto': no port is picked here. The child JVM starts without any + // com.sun.management.jmxremote* args, and the JMX agent is started later + // on-demand via Attach API against the child's real PID (see BootProcessFactory / + // SpringApplicationLifeCycleClientManager). An explicitly pinned nonzero port + // keeps using the old fixed-port args. String[] enableLiveBeanArgs = DebugPlugin.parseArguments(JmxBeanSupport.jmxBeanVmArgs(port, enabled)); for (int i = 0; i < enableLiveBeanArgs.length; i++) { vmArgs.add(i, enableLiveBeanArgs[i]); } - ILaunch currentLaunch = CURRENT_LAUNCH.get(); - if (currentLaunch!=null) { - currentLaunch.setAttribute(JMX_PORT, ""+port); + if (port!=0) { + ILaunch currentLaunch = CURRENT_LAUNCH.get(); + if (currentLaunch!=null) { + currentLaunch.setAttribute(JMX_PORT, ""+port); + } } } // Fast startup VM args diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/CloudCliServiceLaunchConfigurationDelegate.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/CloudCliServiceLaunchConfigurationDelegate.java index e5bffe613c..2526fa1704 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/CloudCliServiceLaunchConfigurationDelegate.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/cli/CloudCliServiceLaunchConfigurationDelegate.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2020 Pivotal Software, Inc. + * Copyright (c) 2017, 2026 Pivotal Software, 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 @@ -41,7 +41,6 @@ import org.springframework.ide.eclipse.boot.util.version.Version; import org.springframework.ide.eclipse.boot.util.version.VersionParser; import org.springframework.ide.eclipse.boot.util.version.VersionRange; -import org.springsource.ide.eclipse.commons.core.util.ProcessUtils; import org.springsource.ide.eclipse.commons.livexp.util.Log; /** @@ -60,8 +59,6 @@ public class CloudCliServiceLaunchConfigurationDelegate extends BootCliLaunchCon public final static String ATTR_CLOUD_SERVICE_ID = "local-cloud-service-id"; private final static String PREF_DONT_SHOW_PLATFORM_WARNING = "org.springframework.ide.eclipse.boot.launch.cloud.cli.NotSupportedPlatform"; - private final static String PREF_DONT_SHOW_JRE_WARNING = "org.springframework.ide.eclipse.boot.launch.cloud.cli.JRE"; - private final static String PREF_DONT_SHOW_JDK_WARNING = "org.springframework.ide.eclipse.boot.launch.cloud.cli.JDK"; private List getCloudCliServiceLifeCycleVmArguments(ILaunchConfiguration configuration, int jmxPort) { List vmArgs = new ArrayList<>(); @@ -240,33 +237,7 @@ public IProcess newProcess(ILaunch launch, Process process, String label, Map { - MessageDialogWithToggle dialog = MessageDialogWithToggle.openWarning( - Display.getCurrent().getActiveShell(), "Cloud CLI Service Info Limitation", - "Cloud service process life-cycle data is limited and port data is unavailable because STS runnning on an old JDK version. Point STS to the latest JDK and restart it to have complete service process life-cycle and port data", - "Don't show this message again", - false, null, null); - store.setValue(PREF_DONT_SHOW_JDK_WARNING, dialog.getToggleState()); - }); - } - } - } catch (NoClassDefFoundError e) { - Log.warn(e); - if (!store.getBoolean(PREF_DONT_SHOW_JRE_WARNING)) { - PlatformUI.getWorkbench().getDisplay().asyncExec(() -> { - MessageDialogWithToggle dialog = MessageDialogWithToggle.openWarning( - Display.getCurrent().getActiveShell(), "Cloud CLI Service Info Limitation", - "Cloud service process life-cycle data is limited and port data is unavailable because STS is running on a JRE. Point it to a JDK and restart STS for complete service process life-cycle and port data", - "Don't show this message again", - false, null, null); - store.setValue(PREF_DONT_SHOW_JRE_WARNING, dialog.getToggleState()); - }); - } + pid = process.pid(); } catch (UnsupportedOperationException e) { Log.warn(e); if (!store.getBoolean(PREF_DONT_SHOW_PLATFORM_WARNING)) { diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/livebean/JmxBeanSupport.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/livebean/JmxBeanSupport.java index ae3b666b17..6ada24ac5e 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/livebean/JmxBeanSupport.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/livebean/JmxBeanSupport.java @@ -44,7 +44,8 @@ public static String jmxBeanVmArgs(String jmxPort, EnumSet enabled) { if (!enabled.isEmpty()) { //At least one feature enabled StringBuilder str = new StringBuilder(); - for (String a : enableJmxArgs(jmxPort)) { + String[] args = "0".equals(jmxPort) ? autoJmxArgs() : enableJmxArgs(jmxPort); + for (String a : args) { str.append(a+"\n"); } for (Feature feature : enabled) { @@ -80,6 +81,19 @@ public static String[] enableJmxArgs(String jmxPort) { }; } + /** + * VM args for the auto/dynamic port case (jmxPort == 0). No + * {@code com.sun.management.jmxremote*} args are emitted here: rather than picking a port + * up front, the JMX agent is started on-demand later via the Attach API against the + * child's real OS PID, once the child actually exists and can bind its own port. + */ + public static String[] autoJmxArgs() { + return new String[] { + "-Dspring.jmx.enabled=true", + "-Dmanagement.endpoints.jmx.exposure.include=*" + }; + } + public static final String JMX_PORT_PROP = "com.sun.management.jmxremote.port"; public static final String LAUNCH_CONFIG_TYPE_ID = BootLaunchConfigurationDelegate.TYPE_ID; public static int randomPort() { diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/process/BootProcessFactory.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/process/BootProcessFactory.java index 93b6368fbd..7f08bae951 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/process/BootProcessFactory.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/process/BootProcessFactory.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015 Pivotal, Inc. + * Copyright (c) 2015, 2026 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 @@ -22,7 +22,6 @@ import org.eclipse.debug.core.model.IProcess; import org.eclipse.debug.core.model.IStreamsProxy; import org.eclipse.debug.core.model.RuntimeProcess; -import org.springframework.ide.eclipse.boot.core.BootActivator; import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; import org.springframework.ide.eclipse.boot.launch.util.SpringApplicationLifeCycleClientManager; import org.springframework.ide.eclipse.boot.launch.util.SpringApplicationLifecycleClient; @@ -67,10 +66,17 @@ private static void debug(String string) { public IProcess newProcess(ILaunch launch, final Process process, final String label, Map attributes) { final int jmxPort = getJMXPort(launch); + if (jmxPort<=0) { + // Auto/dynamic JMX port case: no port was picked at launch time (see + // BootLaunchConfigurationDelegate.getVMArguments()). Capture the real child PID now + // that the process exists, so the JMX agent can be attached to on-demand later. + launch.setAttribute(BootLaunchConfigurationDelegate.PROCESS_ID, String.valueOf(process.pid())); + } + final boolean hasLifeCycleConnection = jmxPort>0 || launch.getAttribute(BootLaunchConfigurationDelegate.PROCESS_ID)!=null; final long timeout = getNiceTerminationTimeout(launch); RuntimeProcess rtProcess = new RuntimeProcess(launch, process, label, attributes) { - SpringApplicationLifeCycleClientManager clientMgr = new SpringApplicationLifeCycleClientManager(jmxPort); + SpringApplicationLifeCycleClientManager clientMgr = new SpringApplicationLifeCycleClientManager(launch); @Override public void terminate() throws DebugException { @@ -108,7 +114,6 @@ private boolean destroyForcibly() { m.invoke(process); return true; } catch (Exception e) { - BootActivator.log(e); //ignore... probably means we are not running on Java 8 VM and so we can't use 'destroyForcibly'. } return false; @@ -118,7 +123,7 @@ private boolean terminateNicely() { if (isTerminated()) { return true; } - if (jmxPort>0) { + if (hasLifeCycleConnection) { try { debug("Trying to terminate nicely: "+label); SpringApplicationLifecycleClient client = clientMgr.getLifeCycleClient(); diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/util/SpringApplicationLifeCycleClientManager.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/util/SpringApplicationLifeCycleClientManager.java index 02763d9d25..3b592366ea 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/util/SpringApplicationLifeCycleClientManager.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/util/SpringApplicationLifeCycleClientManager.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015 Pivotal, Inc. + * Copyright (c) 2015, 2026 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 @@ -18,6 +18,7 @@ import org.eclipse.core.runtime.Assert; import org.eclipse.debug.core.ILaunch; import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; +import org.springsource.ide.eclipse.commons.core.util.ProcessUtils; /** * Creates and manages an instance of {@link SpringApplicationLifecycleClient}. @@ -36,10 +37,23 @@ public SpringApplicationLifeCycleClientManager(Callable connection } /** - * Convenenience method, use ILaunch as the jmxPort provider. + * Convenenience method, use ILaunch as the connection provider. If a fixed JMX port was + * pinned for this launch, connect to it directly. Otherwise (the auto/dynamic port case), + * attach to the child process by its real OS PID and start its local management agent + * on-demand. */ public SpringApplicationLifeCycleClientManager(ILaunch l) { - this(() -> createJMXConnection(BootLaunchConfigurationDelegate.getJMXPortAsInt(l))); + this(() -> { + int port = BootLaunchConfigurationDelegate.getJMXPortAsInt(l); + if (port > 0) { + return createJMXConnection(port); + } + String pid = l.getAttribute(BootLaunchConfigurationDelegate.PROCESS_ID); + if (pid == null) { + throw new IllegalStateException("Neither a JMX port nor a process id is available for this launch"); + } + return ProcessUtils.createJMXConnector(pid); + }); } private static JMXConnector createJMXConnection(int port) { diff --git a/eclipse-language-servers/org.springframework.tooling.ls.integration.repository/category.xml b/eclipse-language-servers/org.springframework.tooling.ls.integration.repository/category.xml index 56971ba968..2506d3f50b 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.integration.repository/category.xml +++ b/eclipse-language-servers/org.springframework.tooling.ls.integration.repository/category.xml @@ -58,7 +58,6 @@ - diff --git a/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/META-INF/MANIFEST.MF b/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/META-INF/MANIFEST.MF index 6699cf8cea..87520fdd96 100644 --- a/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/META-INF/MANIFEST.MF +++ b/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/META-INF/MANIFEST.MF @@ -13,7 +13,6 @@ Require-Bundle: org.eclipse.core.resources, org.eclipse.wst.common.project.facet.core, org.eclipse.platform, org.springsource.ide.eclipse.commons.frameworks.core;bundle-version="3.5.0", - org.springsource.ide.eclipse.commons.jdk_tools;resolution:=optional, org.eclipse.text, org.springsource.ide.eclipse.commons.livexp, org.eclipse.debug.core, diff --git a/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/src/org/springsource/ide/eclipse/commons/core/util/ProcessUtils.java b/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/src/org/springsource/ide/eclipse/commons/core/util/ProcessUtils.java index e8ac185e48..0cdae3abf5 100644 --- a/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/src/org/springsource/ide/eclipse/commons/core/util/ProcessUtils.java +++ b/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/src/org/springsource/ide/eclipse/commons/core/util/ProcessUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Pivotal Software, Inc. + * Copyright (c) 2017, 2026 Pivotal Software, 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 @@ -11,7 +11,6 @@ package org.springsource.ide.eclipse.commons.core.util; import java.io.IOException; -import java.lang.reflect.Field; import java.util.List; import javax.management.remote.JMXConnector; @@ -30,27 +29,30 @@ * @author Alex Boyko * */ -@SuppressWarnings("restriction") public class ProcessUtils { /** - * Retrieves PID of a process - * @param p the process - * @return PID + * Attaches to a local process by its PID and starts (or reuses) its local JMX management + * agent on-demand, returning the JMX service URL the agent bound to. This avoids ever + * having to pick a JMX port before the target process exists: the port is only known once + * the target process has already bound it itself. + * @param pid the PID + * @return the JMX service URL of the process' local management agent, or {@code null} if + * no such process could be found or attached to */ - public static long getProcessID(Process p) { - long result = -1; - try { - Field f = p.getClass().getDeclaredField("pid"); - f.setAccessible(true); - result = f.getLong(p); - f.setAccessible(false); - } - catch (Exception ex) { - throw new UnsupportedOperationException("Process PID calculation not supported on the current platform", - ex); + public static String getLocalManagementAgentUrl(String pid) { + List vmds = VirtualMachine.list(); + VirtualMachineDescriptor vmd = vmds.stream().filter(descriptor -> descriptor.id().equals(pid)).findFirst().orElse(null); + if (vmd != null) { + try { + return VirtualMachine.attach(vmd).startLocalManagementAgent(); + } catch (AttachNotSupportedException e) { + CorePlugin.log(e); + } catch (IOException e) { + CorePlugin.log(e); + } } - return result; + return null; } /** @@ -59,17 +61,11 @@ public static long getProcessID(Process p) { * @return JMX connector */ public static JMXConnector createJMXConnector(String pid) { - List vmds = VirtualMachine.list(); - VirtualMachineDescriptor vmd = vmds.stream().filter(descriptor -> descriptor.id().equals(pid)).findFirst().orElse(null); - if (vmd != null) { + String agentUrl = getLocalManagementAgentUrl(pid); + if (agentUrl != null) { try { - String agentUrl = VirtualMachine.attach(vmd).startLocalManagementAgent(); - if (agentUrl != null) { - JMXServiceURL serviceUrl = new JMXServiceURL(agentUrl); - return JMXConnectorFactory.connect(serviceUrl, null); - } - } catch (AttachNotSupportedException e) { - CorePlugin.log(e); + JMXServiceURL serviceUrl = new JMXServiceURL(agentUrl); + return JMXConnectorFactory.connect(serviceUrl, null); } catch (IOException e) { CorePlugin.log(e); } diff --git a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/.classpath b/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/.classpath deleted file mode 100644 index 411911f82c..0000000000 --- a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/.classpath +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/.settings/org.eclipse.jdt.core.prefs b/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 9f6ece88bd..0000000000 --- a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,8 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 -org.eclipse.jdt.core.compiler.compliance=1.8 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.release=disabled -org.eclipse.jdt.core.compiler.source=1.8 diff --git a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/.settings/org.eclipse.pde.prefs b/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/.settings/org.eclipse.pde.prefs deleted file mode 100644 index 4cafe5101a..0000000000 --- a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/.settings/org.eclipse.pde.prefs +++ /dev/null @@ -1,32 +0,0 @@ -compilers.f.unresolved-features=1 -compilers.f.unresolved-plugins=1 -compilers.incompatible-environment=1 -compilers.p.build=1 -compilers.p.build.bin.includes=1 -compilers.p.build.encodings=2 -compilers.p.build.java.compiler=2 -compilers.p.build.java.compliance=1 -compilers.p.build.missing.output=2 -compilers.p.build.output.library=1 -compilers.p.build.source.library=1 -compilers.p.build.src.includes=1 -compilers.p.deprecated=1 -compilers.p.discouraged-class=1 -compilers.p.internal=1 -compilers.p.missing-packages=2 -compilers.p.missing-version-export-package=2 -compilers.p.missing-version-import-package=2 -compilers.p.missing-version-require-bundle=2 -compilers.p.no-required-att=0 -compilers.p.not-externalized-att=2 -compilers.p.unknown-attribute=1 -compilers.p.unknown-class=1 -compilers.p.unknown-element=1 -compilers.p.unknown-identifier=1 -compilers.p.unknown-resource=1 -compilers.p.unresolved-ex-points=0 -compilers.p.unresolved-import=2 -compilers.s.create-docs=false -compilers.s.doc-folder=doc -compilers.s.open-tags=1 -eclipse.preferences.version=1 diff --git a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/META-INF/MANIFEST.MF b/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/META-INF/MANIFEST.MF deleted file mode 100644 index b95a6010a2..0000000000 --- a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/META-INF/MANIFEST.MF +++ /dev/null @@ -1,9 +0,0 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: SpringSource Tool Suite (JDK Tools) -Bundle-SymbolicName: org.springsource.ide.eclipse.commons.jdk_tools;singleton:=true -Bundle-Version: 5.3.0.qualifier -Bundle-RequiredExecutionEnvironment: JavaSE-21 -Bundle-ClassPath: external:$java.home$/../lib/tools.jar -Export-Package: com.sun.tools.attach -Bundle-Vendor: Broadcom diff --git a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/about.html b/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/about.html deleted file mode 100644 index ae88621cc7..0000000000 --- a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/about.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - -About Eclipse Integration Commons - - - -

About Eclipse Integration Commons

-

June 30, 2015

-

Abstract

-

The Eclipse Integration Commons project provides components that help -other projects to implement Eclipse-based tooling.

-

License

-

Pivotal Software, Inc. (“Pivotal”) makes available all content -in this download ("Content"). Unless otherwise indicated below, the -Content is provided to you under the terms and conditions of the Eclipse -Public License Version 1.0 ("EPL"). A copy of the EPL is available in -the file called license.txt. For purposes of the EPL, "Program" will -mean the Content.

- - diff --git a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/build.properties b/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/build.properties deleted file mode 100644 index 2a306f232b..0000000000 --- a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/build.properties +++ /dev/null @@ -1,5 +0,0 @@ -source.. = src/ -output.. = bin/ -bin.includes = META-INF/,\ - about.html - diff --git a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/pom.xml b/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/pom.xml deleted file mode 100644 index 7bba9d2c03..0000000000 --- a/eclipse-language-servers/org.springsource.ide.eclipse.commons.jdk_tools/pom.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - 4.0.0 - - - org.springframework.boot.ide - org.springframework.boot.ide.servers - 5.3.0-SNAPSHOT - ../pom.xml - - - org.springsource.ide.eclipse.commons.jdk_tools - eclipse-plugin - org.springsource.ide.eclipse.commons.jdk_tools - - - - - org.eclipse.tycho - tycho-p2-plugin - ${tycho-version} - - - second-generate-p2-metadata - - p2-metadata - - verify - - - - - - - diff --git a/eclipse-language-servers/pom.xml b/eclipse-language-servers/pom.xml index 6ba2bfce08..d1690aec19 100644 --- a/eclipse-language-servers/pom.xml +++ b/eclipse-language-servers/pom.xml @@ -49,7 +49,6 @@ org.springsource.ide.eclipse.commons.frameworks.core org.springsource.ide.eclipse.commons.frameworks.test.util org.springsource.ide.eclipse.commons.frameworks.ui - org.springsource.ide.eclipse.commons.jdk_tools org.springsource.ide.eclipse.commons.livexp org.springsource.ide.eclipse.commons.tests.util org.springsource.ide.eclipse.commons.ui diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java index 45909658fa..e271dfeb03 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java @@ -234,7 +234,7 @@ SpringProcessConnectorRemote localAppsFromCommandsConnector(SimpleLanguageServer synchronized(localApps) { RemoteBootAppData[] newAdditions = params.getArguments().stream().map(a -> gson.fromJson((JsonElement) a, RemoteBootAppData.class)).toArray(RemoteBootAppData[]::new); for (RemoteBootAppData app : newAdditions) { - localApps.put(app.getJmxurl(), app); + localApps.put(SpringProcessConnectorRemote.getProcessKey(app), app); } bean.updateApps(localApps.values().toArray(new RemoteBootAppData[localApps.size()])); return CompletableFuture.completedFuture(null); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/LocalJvmAttach.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/LocalJvmAttach.java new file mode 100644 index 0000000000..ee2dc0625e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/LocalJvmAttach.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2026 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.vscode.boot.java.livehover.v2; + +import java.util.List; + +import com.sun.tools.attach.VirtualMachine; +import com.sun.tools.attach.VirtualMachineDescriptor; + +/** + * Attaches to a local JVM process by its OS PID and starts (or reuses) its local JMX + * management agent on-demand, returning the JMX service URL the agent bound to. + *

+ * This avoids picking a JMX port before the target JVM exists: the port is only known once + * the target JVM has already bound it itself. + * + * @author Alex Boyko + */ +class LocalJvmAttach { + + private static final String LOCAL_CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress"; + + private LocalJvmAttach() { + } + + static String startLocalManagementAgent(String pid) throws Exception { + List vmds = VirtualMachine.list(); + VirtualMachineDescriptor vmd = vmds.stream().filter(d -> d.id().equals(pid)).findFirst().orElse(null); + if (vmd == null) { + throw new IllegalStateException("No local JVM found for pid " + pid); + } + return startLocalManagementAgent(vmd); + } + + static String startLocalManagementAgent(VirtualMachineDescriptor vmd) throws Exception { + VirtualMachine vm = VirtualMachine.attach(vmd); + try { + String jmxAddress = null; + try { + jmxAddress = vm.getAgentProperties().getProperty(LOCAL_CONNECTOR_ADDRESS); + } catch (Exception e) { + //ignore + } + if (jmxAddress == null) { + jmxAddress = vm.startLocalManagementAgent(); + } + if (jmxAddress == null) { + throw new IllegalStateException("Could not start local management agent for pid " + vmd.id()); + } + return jmxAddress; + } finally { + vm.detach(); + } + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorLocal.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorLocal.java index 0a4843859f..a221642a26 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorLocal.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorLocal.java @@ -40,9 +40,7 @@ public class SpringProcessConnectorLocal { private static final Logger log = LoggerFactory.getLogger(SpringProcessConnectorLocal.class); - - private static final String LOCAL_CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress"; - + private final Map projects; private final Set processes; @@ -193,54 +191,23 @@ private void updateStatus(SpringProcessDescriptor[] processes) { } public CompletableFuture connectProcess(SpringProcessDescriptor descriptor) { - VirtualMachine vm = null; VirtualMachineDescriptor vmDescriptor = descriptor.getVm(); - + try { - String jmxAddress = null; - vm = VirtualMachine.attach(vmDescriptor); - - try { - jmxAddress = vm.getAgentProperties().getProperty(LOCAL_CONNECTOR_ADDRESS); - } catch (Exception e) { - //ignore - } - - if (jmxAddress == null) { - try { - jmxAddress = vm.startLocalManagementAgent(); - } catch (Exception e) { - log.error("Error starting local management agent", e); - return CompletableFuture.failedFuture(e); - } - } - - if (jmxAddress != null) { - String processID = getProcessID(vmDescriptor); - String processName = getProcessName(vmDescriptor); - String urlScheme = "http"; - - SpringProcessConnectorOverJMX connector = new SpringProcessConnectorOverJMX(ProcessType.LOCAL, - descriptor.getProcessKey(), jmxAddress, urlScheme, processID, processName, descriptor.getProjectName(), null, null); + String jmxAddress = LocalJvmAttach.startLocalManagementAgent(vmDescriptor); - return this.processConnectorService.connectProcess(descriptor.getProcessKey(), connector); - } - return CompletableFuture.failedFuture(new Exception("No JMX URL available!")); + String processID = getProcessID(vmDescriptor); + String processName = getProcessName(vmDescriptor); + + SpringProcessConnectorOverJMX connector = new SpringProcessConnectorOverJMX(ProcessType.LOCAL, + descriptor.getProcessKey(), jmxAddress, "http", processID, processName, descriptor.getProjectName(), null, null); + + return this.processConnectorService.connectProcess(descriptor.getProcessKey(), connector); } catch (Exception e) { log.error("exception while connecting to jvm process", e); return CompletableFuture.failedFuture(e); } - finally { - if (vm != null) { - try { - vm.detach(); - } - catch (Exception e) { - log.error("error detaching from vm: " + vmDescriptor.id(), e); - } - } - } } public boolean isKnownProcessKey(String processKey) { 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 a7e34ad38d..a9f6660e46 100644 --- a/vscode-extensions/vscode-spring-boot/lib/debug-config-provider.ts +++ b/vscode-extensions/vscode-spring-boot/lib/debug-config-provider.ts @@ -13,16 +13,12 @@ import { debug, 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', @@ -39,7 +35,9 @@ interface ProcessEvent { class SpringBootDebugConfigProvider implements DebugConfigurationProvider { async resolveDebugConfigurationWithSubstitutedVariables(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, _token?: CancellationToken) { - // Running app live hovers support + // Running app live hovers support. No JMX port is picked here: the language server + // attaches to the process id on-demand instead (see handleCustomDebugEvent), once the + // process actually exists and has bound its own local management agent port. if (!TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isActuatorOnClasspath(debugConfiguration)) { if (typeof debugConfiguration.vmArgs === 'string') { if (debugConfiguration.vmArgs.indexOf(JMX_VM_ARG) < 0) { @@ -54,18 +52,8 @@ class SpringBootDebugConfigProvider implements DebugConfigurationProvider { 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`; + debugConfiguration.vmArgs = `${JMX_VM_ARG}true ${ACTUATOR_JMX_EXPOSURE_ARG}* ${ADMIN_VM_ARG}true ${BOOT_PROJECT_ARG}${debugConfiguration.projectName}`; } } return debugConfiguration; @@ -116,17 +104,13 @@ async function handleCustomDebugEvent(e: DebugSessionCustomEvent): Promise 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); - } + // No jmxurl: the language server attaches to processId on-demand instead of + // connecting to a pre-picked port (see SpringProcessConnectorRemote on the LS side). 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`, + jmxurl: null, manualConnect: true, processId: pid.toString(), processName: debugConfiguration.mainClass, @@ -175,7 +159,6 @@ 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; diff --git a/vscode-extensions/vscode-spring-boot/lib/live-hover-connect-ui.ts b/vscode-extensions/vscode-spring-boot/lib/live-hover-connect-ui.ts index 811cf3b827..f60b8bed6b 100644 --- a/vscode-extensions/vscode-spring-boot/lib/live-hover-connect-ui.ts +++ b/vscode-extensions/vscode-spring-boot/lib/live-hover-connect-ui.ts @@ -15,7 +15,7 @@ interface ProcessCommandInfo { } export interface RemoteBootApp { - jmxurl: string; + jmxurl: string | null; host: string; urlScheme: "https" | "http"; port: string; @@ -26,6 +26,16 @@ export interface RemoteBootApp { projectName?: string; } +/** + * Most callers pin a fixed jmxurl, which is a stable and unique identifier on its own. The + * auto/dynamic JMX port case has no jmxurl since no port is picked by the extension; the + * process id is used as the key instead (mirrors SpringProcessConnectorRemote.getProcessKey + * on the language server side). + */ +function getProcessKey(appData: RemoteBootApp | undefined): string | undefined { + return appData?.jmxurl || appData?.processId; +} + export interface BootAppQuickPick extends QuickPickItem { commandInfo: ProcessCommandInfo; } @@ -94,7 +104,7 @@ async function liveHoverConnectHandler() { } async function executeLiveProcessAction(commandInfo: ProcessCommandInfo) { - if (activeBootApp?.jmxurl === commandInfo.processKey) { + if (getProcessKey(activeBootApp) === commandInfo.processKey) { switch (commandInfo.action) { case CONNECT_CMD: await commands.executeCommand('vscode-spring-boot.live.show.active'); @@ -140,7 +150,7 @@ export function activate( }), commands.registerCommand("vscode-spring-boot.live.deactivate", async () => { - await commands.executeCommand('sts/livedata/localRemove', activeBootApp.jmxurl); + await commands.executeCommand('sts/livedata/localRemove', getProcessKey(activeBootApp)); activeBootApp = undefined; updateBootAppState("none"); }), @@ -149,7 +159,7 @@ export function activate( try { updateBootAppState("connecting"); await commands.executeCommand(CONNECT_CMD, { - processKey: activeBootApp.jmxurl + processKey: getProcessKey(activeBootApp) }); updateBootAppState("connected"); } catch (error) { @@ -160,7 +170,7 @@ export function activate( commands.registerCommand("vscode-spring-boot.live.refresh.active", async () => { await commands.executeCommand(REFRESH_CMD, { - processKey: activeBootApp.jmxurl + processKey: getProcessKey(activeBootApp) }); }), @@ -168,7 +178,7 @@ export function activate( try { updateBootAppState("disconnecting"); await commands.executeCommand(DISCONNECT_CMD, { - processKey: activeBootApp.jmxurl + processKey: getProcessKey(activeBootApp) }); updateBootAppState("disconnected"); } catch (error) { diff --git a/vscode-extensions/vscode-spring-boot/package.json b/vscode-extensions/vscode-spring-boot/package.json index 54df904484..33ab1bb0dc 100644 --- a/vscode-extensions/vscode-spring-boot/package.json +++ b/vscode-extensions/vscode-spring-boot/package.json @@ -1614,7 +1614,6 @@ }, "dependencies": { "@pivotal-tools/commons-vscode": "file:../commons-vscode/pivotal-tools-commons-vscode-0.2.4.tgz", - "portfinder": "~1.0.38", "ps-list": "^7.2.0", "vscode-languageclient": "^9.0.1" }, From e9d16d6078b5b279ddb64d21a0abfd511677a994 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 14 Jul 2026 14:31:45 -0700 Subject: [PATCH 2/5] Missing change Signed-off-by: BoykoAlex --- .../v2/SpringProcessConnectorRemote.java | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorRemote.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorRemote.java index 3559f2d105..ec6f240a38 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorRemote.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorRemote.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2024 Pivotal, Inc. + * Copyright (c) 2019, 2026 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 @@ -218,8 +218,13 @@ else if (StringUtils.hasText(appData.getHost())) { } } + /** + * Most callers pin a fixed jmxurl, which is a stable and unique identifier on its own. The + * auto/dynamic JMX port case has no jmxurl since no port is picked by the IDE; the process + * id is used as the key instead. + */ public static String getProcessKey(RemoteBootAppData appData) { - return appData.getJmxurl(); + return StringUtils.hasText(appData.getJmxurl()) ? appData.getJmxurl() : appData.getProcessID(); } public CompletableFuture connectProcess(RemoteBootAppData remoteProcess) { @@ -232,8 +237,13 @@ public CompletableFuture connectProcess(RemoteBootAppData remoteProcess) { String urlScheme = remoteProcess.getUrlScheme(); String projectName = remoteProcess.getProjectName(); // boolean keepChecking = _appData.isKeepChecking(); - - if (jmxURL.startsWith("http")) { + + if (jmxURL == null) { + // Auto/dynamic JMX port case: no port was picked by the IDE. Attach to the real + // child process by its PID and let the JVM start its own local management agent + // on-demand. + return connectLocalProcessByPid(processKey, processID, processName, projectName, urlScheme); + } else if (jmxURL.startsWith("http")) { SpringProcessConnectorOverHttp connector = new SpringProcessConnectorOverHttp(processType, processKey, jmxURL, urlScheme, processID, processName, projectName, host, port); return processConnectorService.connectProcess(processKey, connector); } else { @@ -241,7 +251,21 @@ public CompletableFuture connectProcess(RemoteBootAppData remoteProcess) { return processConnectorService.connectProcess(processKey, connector); } } - + + private CompletableFuture connectLocalProcessByPid(String processKey, String processID, String processName, String projectName, String urlScheme) { + if (processID == null) { + return CompletableFuture.failedFuture(new IllegalStateException("No jmxurl or process id available for " + processKey)); + } + try { + String jmxAddress = LocalJvmAttach.startLocalManagementAgent(processID); + SpringProcessConnectorOverJMX connector = new SpringProcessConnectorOverJMX(processType, processKey, jmxAddress, urlScheme, processID, processName, projectName, null, null); + return processConnectorService.connectProcess(processKey, connector); + } catch (Exception e) { + logger.error("exception while attaching to local process by pid: " + processID, e); + return CompletableFuture.failedFuture(e); + } + } + public RemoteBootAppData[] getProcesses() { Set remoteApps = this.remoteAppInstances.keySet(); return (RemoteBootAppData[]) remoteApps.toArray(new RemoteBootAppData[remoteApps.size()]); From c3342ce4eaa10080e4f0f8176c73cbca5b5daefe Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 14 Jul 2026 14:38:08 -0700 Subject: [PATCH 3/5] Remove unused method Signed-off-by: BoykoAlex --- .../eclipse/commons/core/util/ProcessUtils.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/src/org/springsource/ide/eclipse/commons/core/util/ProcessUtils.java b/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/src/org/springsource/ide/eclipse/commons/core/util/ProcessUtils.java index 0cdae3abf5..1d23164a1f 100644 --- a/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/src/org/springsource/ide/eclipse/commons/core/util/ProcessUtils.java +++ b/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/src/org/springsource/ide/eclipse/commons/core/util/ProcessUtils.java @@ -73,18 +73,4 @@ public static JMXConnector createJMXConnector(String pid) { return null; } - /** - * Determines whether current JDK supports utilities defined in this class - * @return true for compatible JDK - */ - public static boolean isLatestJdkForTools() { - try { - return VirtualMachine.class.getDeclaredMethod("startLocalManagementAgent") != null; - } - catch (NoSuchMethodException | SecurityException e) { - // ignore - } - return false; - } - } From 4fb2ccb120c3f8d50a9aa28d72fe76f683a582d3 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 14 Jul 2026 14:58:14 -0700 Subject: [PATCH 4/5] Proper cleanup of VM once URL is known Signed-off-by: BoykoAlex --- .../ide/eclipse/commons/core/util/ProcessUtils.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/src/org/springsource/ide/eclipse/commons/core/util/ProcessUtils.java b/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/src/org/springsource/ide/eclipse/commons/core/util/ProcessUtils.java index 1d23164a1f..1a46651338 100644 --- a/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/src/org/springsource/ide/eclipse/commons/core/util/ProcessUtils.java +++ b/eclipse-language-servers/org.springsource.ide.eclipse.commons.core/src/org/springsource/ide/eclipse/commons/core/util/ProcessUtils.java @@ -45,7 +45,12 @@ public static String getLocalManagementAgentUrl(String pid) { VirtualMachineDescriptor vmd = vmds.stream().filter(descriptor -> descriptor.id().equals(pid)).findFirst().orElse(null); if (vmd != null) { try { - return VirtualMachine.attach(vmd).startLocalManagementAgent(); + VirtualMachine vm = VirtualMachine.attach(vmd); + try { + return vm.startLocalManagementAgent(); + } finally { + vm.detach(); + } } catch (AttachNotSupportedException e) { CorePlugin.log(e); } catch (IOException e) { From a79922648242f8b7a79526ec471272815c4ff64d Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 14 Jul 2026 16:22:36 -0700 Subject: [PATCH 5/5] Guards for no active boot app for VSCode Signed-off-by: BoykoAlex --- .../boot/app/BootLanguageServerBootApp.java | 8 +++++++- .../lib/live-hover-connect-ui.ts | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java index e271dfeb03..fe01b9deda 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -242,7 +243,12 @@ SpringProcessConnectorRemote localAppsFromCommandsConnector(SimpleLanguageServer }); server.onCommand("sts/livedata/localRemove", params -> { synchronized(localApps) { - List removedApps = params.getArguments().stream().map(o -> o instanceof JsonElement ? ((JsonElement) o).getAsString() : (String) o).map(localApps::remove).collect(Collectors.toList()); + List removedApps = params.getArguments().stream() + .map(o -> o instanceof JsonElement e ? (e.isJsonNull() ? null : e.getAsString()) : (String) o) + .filter(Objects::nonNull) + .map(localApps::remove) + .filter(Objects::nonNull) + .collect(Collectors.toList()); if (!removedApps.isEmpty()) { bean.updateApps(localApps.values().toArray(new RemoteBootAppData[localApps.size()])); } diff --git a/vscode-extensions/vscode-spring-boot/lib/live-hover-connect-ui.ts b/vscode-extensions/vscode-spring-boot/lib/live-hover-connect-ui.ts index f60b8bed6b..aa9f03deba 100644 --- a/vscode-extensions/vscode-spring-boot/lib/live-hover-connect-ui.ts +++ b/vscode-extensions/vscode-spring-boot/lib/live-hover-connect-ui.ts @@ -150,12 +150,19 @@ export function activate( }), commands.registerCommand("vscode-spring-boot.live.deactivate", async () => { - await commands.executeCommand('sts/livedata/localRemove', getProcessKey(activeBootApp)); - activeBootApp = undefined; + // Fires on every debug/run session termination, not just ones where live-hover was + // ever activated, so activeBootApp may be unset here. + if (activeBootApp) { + await commands.executeCommand('sts/livedata/localRemove', getProcessKey(activeBootApp)); + activeBootApp = undefined; + } updateBootAppState("none"); }), commands.registerCommand("vscode-spring-boot.live.show.active", async () => { + if (!activeBootApp) { + return; + } try { updateBootAppState("connecting"); await commands.executeCommand(CONNECT_CMD, { @@ -169,12 +176,18 @@ export function activate( }), commands.registerCommand("vscode-spring-boot.live.refresh.active", async () => { + if (!activeBootApp) { + return; + } await commands.executeCommand(REFRESH_CMD, { processKey: getProcessKey(activeBootApp) }); }), commands.registerCommand("vscode-spring-boot.live.hide.active", async () => { + if (!activeBootApp) { + return; + } try { updateBootAppState("disconnecting"); await commands.executeCommand(DISCONNECT_CMD, {