diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.classpath b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.classpath index 1db08c6b4..8d8612144 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.classpath +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.classpath @@ -1,6 +1,6 @@ - + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.settings/org.eclipse.jdt.core.prefs index c430614c3..b993611e7 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.settings/org.eclipse.jdt.core.prefs +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/.settings/org.eclipse.jdt.core.prefs @@ -9,8 +9,8 @@ org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nul org.eclipse.jdt.core.compiler.annotation.nullable.secondary= org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.compliance=11 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 org.eclipse.jdt.core.compiler.problem.APILeak=warning org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning @@ -109,4 +109,4 @@ org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.release=enabled -org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.source=17 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/light-bulb-off.svg b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/light-bulb-off.svg new file mode 100644 index 000000000..4045ecfba --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/light-bulb-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/light-bulb-on.svg b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/light-bulb-on.svg new file mode 100644 index 000000000..6913af9bc --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/icons-src/light-bulb-on.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 b0ac55a70..bbc01b6d2 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015, 2022 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 @@ -10,7 +10,6 @@ *******************************************************************************/ package org.springframework.ide.eclipse.boot.dash; -import java.time.Duration; import java.util.Arrays; import java.util.Map; @@ -23,6 +22,7 @@ import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.model.IProcess; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.resource.ImageRegistry; import org.eclipse.ui.plugin.AbstractUIPlugin; @@ -36,15 +36,12 @@ import org.springframework.ide.eclipse.boot.dash.model.BootDashModelContext; import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel; import org.springframework.ide.eclipse.boot.dash.model.DefaultBootDashModelContext; import org.springframework.ide.eclipse.boot.dash.model.LocalBootDashModel; -import org.springframework.ide.eclipse.boot.dash.model.RunState; import org.springframework.ide.eclipse.boot.dash.model.RunTargets; import org.springframework.ide.eclipse.boot.dash.util.LaunchConfRunStateTracker; import org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.RunStateListener; import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; import org.springsource.ide.eclipse.commons.livexp.util.Log; -import reactor.core.publisher.Mono; - /** * The activator class controls the plug-in life cycle */ @@ -195,35 +192,46 @@ public class BootDashActivator extends AbstractUIPlugin { @Override public void stateChanged(ILaunchConfiguration owner) { - if (BootLaunchConfigurationDelegate.getEnableJmx(owner) && BootLaunchConfigurationDelegate.getAutoConnect(owner)) { + if (BootLaunchConfigurationDelegate.getEnableJmx(owner)) { IJavaProject project = JavaCore.create(BootLaunchConfigurationDelegate.getProject(owner)); try { if (project != null && Arrays.stream(project.getResolvedClasspath(true)).anyMatch(BootPropertyTester::isActuatorJar)) { LocalBootDashModel localModel = (LocalBootDashModel) getModel().getSectionByTargetId(RunTargets.LOCAL.getId()); LaunchConfRunStateTracker tracker = localModel.getLaunchConfRunStateTracker(); - RunState state = tracker.getState(owner); - if (state == RunState.RUNNING || state == RunState.DEBUGGING) { + switch (tracker.getState(owner)) { + case RUNNING: + case DEBUGGING: for (ILaunch l : DebugPlugin.getDefault().getLaunchManager().getLaunches()) { if (l.getLaunchConfiguration() == owner) { for (IProcess p : l.getProcesses()) { String pid = p.getAttribute(IProcess.ATTR_PROCESS_ID); if (pid != null) { - CommandInfo cmd = new CommandInfo("sts/livedata/connect", - Map.of("processKey", pid)); - - // The delay seems to be necessary for the moment. Although the process is created VM attach API may not work at early stages of the process run. - // "VirtualMachine.list()" call may not list the newly created process which means the process is gone and triggers disconnect. - // If lifecycle management is enabled the ready state seem to be a great indicator of a boot process fully started. - // TODO: explore health endpoint perhaps instead of ready state under Admin endpoint. -// Flux.fromIterable(servers).flatMap(s -> Mono.delay(Duration.ofMillis(500)).then(s.executeCommand(cmd))).subscribe(); - Mono.delay(Duration.ofMillis(500)).then(LiveProcessCommandsExecutor.getDefault().executeCommand(cmd)).subscribe(); + CommandInfo cmd = new CommandInfo("sts/livedata/localAdd", Map.of( + "host", "127.0.0.1", + "port", l.getAttribute(BootLaunchConfigurationDelegate.JMX_PORT), + "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() + )); + LiveProcessCommandsExecutor.getDefault().executeCommand(cmd).subscribe(); } } } } + 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(); + } + } + default: } - } } catch (Exception e) { getLog().error("Failed to connect to Boot app", e); diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/CommandInfo.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/CommandInfo.java index 68623dbd5..1de003f60 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/CommandInfo.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/CommandInfo.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Pivotal Software, Inc. + * Copyright (c) 2019, 2024 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 @@ -16,9 +16,9 @@ import java.util.Map.Entry; public class CommandInfo { public final String command; - public final Map info; + public final Map info; - public CommandInfo(String command, Map info) { + public CommandInfo(String command, Map info) { super(); this.command = command; this.info = info; @@ -27,7 +27,7 @@ public class CommandInfo { @Override public String toString() { StringBuilder s = new StringBuilder("CommandInfo("+command+", {\n"); - for (Entry e : info.entrySet()) { + for (Entry e : info.entrySet()) { s.append(" "+e.getKey() +": "+e.getValue()+"\n"); } s.append("})"); diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/DefaultLiveProcessCommandExecutor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/DefaultLiveProcessCommandExecutor.java index f612b0702..40dba06d7 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/DefaultLiveProcessCommandExecutor.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/DefaultLiveProcessCommandExecutor.java @@ -57,4 +57,12 @@ public final class DefaultLiveProcessCommandExecutor implements LiveProcessComma ))).thenAccept(l -> {})); } + @Override + public Mono executeCommand(String command, Object... params) { + return Mono.fromFuture(BootLsCommandUtils.getLanguageServers(command).collectAll(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams( + command, + ImmutableList.of(params) + ))).thenAccept(l -> {})); + } + } \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveDataConnectionManagementActions.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveDataConnectionManagementActions.java index 0f8a8a702..4e97eadf0 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveDataConnectionManagementActions.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveDataConnectionManagementActions.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2023 Pivotal Software, Inc. + * Copyright (c) 2019, 2024 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 @@ -77,15 +77,24 @@ public class LiveDataConnectionManagementActions extends AbstractDisposable impl public ExecuteCommandAction(CommandInfo commandInfo) { super(params); this.commandInfo = commandInfo; - String command = commandInfo.command; - int lastSlash = command.lastIndexOf("/"); - String humanReadableCommand = command.substring(lastSlash+1); - humanReadableCommand = humanReadableCommand.substring(0, 1).toUpperCase() + humanReadableCommand.substring(1); - this.projectName = commandInfo.info.get("projectName"); - label = humanReadableCommand + " "+commandInfo.info.get("label"); + this.projectName = (String) commandInfo.info.get("projectName"); + label = computeLabel(commandInfo); this.setText(label); } + private String computeLabel(CommandInfo commandInfo) { + switch (commandInfo.command) { + case "sts/livedata/connect": + return "Show Live Data from: %s".formatted(commandInfo.info.get("label")); + case "sts/livedata/refresh": + return "Refresh Live Data from: %s".formatted(commandInfo.info.get("label")); + case "sts/livedata/disconnect": + return "Hide Live Data from: %s".formatted(commandInfo.info.get("label")); + default: + } + return (String) commandInfo.info.get("label"); + } + public String getProjectName() { return projectName; } @@ -115,7 +124,7 @@ public class LiveDataConnectionManagementActions extends AbstractDisposable impl } public String getProcessId() { - return commandInfo.info.get("processId"); + return (String) commandInfo.info.get("processId"); } } diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveProcessCommandsExecutor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveProcessCommandsExecutor.java index 16c6e1784..4f19ec4d9 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveProcessCommandsExecutor.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/LiveProcessCommandsExecutor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2023 Pivotal Software, Inc. + * Copyright (c) 2019, 2024 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 @@ -17,6 +17,7 @@ public interface LiveProcessCommandsExecutor { Flux listCommands(); Mono executeCommand(CommandInfo cmd); + Mono executeCommand(String command, Object... params); static LiveProcessCommandsExecutor getDefault() { return new DefaultLiveProcessCommandExecutor(); diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/Constants.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/Constants.java index 5d177cb8d..7bc8745b0 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/Constants.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/Constants.java @@ -17,6 +17,7 @@ public class Constants { public static final String PLUGIN_ID = "org.springframework.tooling.boot.ls"; + public static final String PREF_LIVE_INFORMATION_ALL_JVM_PROCESSES = "boot-java.live-information.all-local-java-processes"; public static final String PREF_LIVE_INFORMATION_FETCH_DATA_RETRY_MAX_NO = "boot-java.live-information.fetch-data.max-retries"; public static final String PREF_LIVE_INFORMATION_FETCH_DATA_RETRY_DELAY_IN_SECONDS = "boot-java.live-information.fetch-data.retry-delay-in-seconds"; diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java index c97921f53..be861a3cb 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java @@ -192,6 +192,7 @@ public class DelegatingStreamConnectionProvider implements StreamConnectionProvi liveInformationFetchData.put("retry-delay-in-seconds", preferenceStore.getInt(Constants.PREF_LIVE_INFORMATION_FETCH_DATA_RETRY_DELAY_IN_SECONDS)); liveInformation.put("fetch-data", liveInformationFetchData); + liveInformation.put("all-local-java-processes", preferenceStore.getBoolean(Constants.PREF_LIVE_INFORMATION_ALL_JVM_PROCESSES)); supportXML.put("on", preferenceStore.getBoolean(Constants.PREF_SUPPORT_SPRING_XML_CONFIGS)); supportXML.put("scan-folders", preferenceStore.getString(Constants.PREF_XML_CONFIGS_SCAN_FOLDERS)); diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/LiveProcessCommandElement.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/LiveProcessCommandElement.java index 694f4d347..1a9555494 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/LiveProcessCommandElement.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/LiveProcessCommandElement.java @@ -53,13 +53,13 @@ public class LiveProcessCommandElement extends QuickAccessElement { @Override public String getLabel() { if (COMMAND_REFRESH.equals(action)) { - return "Refresh Live Data for: " + label; + return "Refresh Live Data from: " + label; } else if (COMMAND_CONNECT.equals(action)) { - return "Show live data for: " + label; + return "Show Live Data from: " + label; } else if (COMMAND_DISCONNECT.equals(action)) { - return "Disconnect live data from: " + label; + return "Hide Live Data from: " + label; } else { // error case diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/prefs/LiveInformationPreferencePage.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/prefs/LiveInformationPreferencePage.java index c55eda901..9f6fedb22 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/prefs/LiveInformationPreferencePage.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/prefs/LiveInformationPreferencePage.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 VMware, Inc. + * Copyright (c) 2023, 2024 VMware, 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 @@ -81,6 +81,7 @@ public class LiveInformationPreferencePage extends FieldEditorPreferencePage imp Composite fieldEditorParent = getFieldEditorParent(); + addField(new BooleanFieldEditor(Constants.PREF_LIVE_INFORMATION_ALL_JVM_PROCESSES, "Show all local JVM processes when connecting to Live Spring Boot App processes", fieldEditorParent)); addField(new StringFieldEditor(Constants.PREF_LIVE_INFORMATION_FETCH_DATA_RETRY_MAX_NO, "Live Information - Max number of retries (before giving up)", fieldEditorParent)); addField(new StringFieldEditor(Constants.PREF_LIVE_INFORMATION_FETCH_DATA_RETRY_DELAY_IN_SECONDS, "Live Information - Delay between retries in seconds", fieldEditorParent)); diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/prefs/PrefsInitializer.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/prefs/PrefsInitializer.java index 391c91bae..df744d304 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/prefs/PrefsInitializer.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/prefs/PrefsInitializer.java @@ -62,6 +62,8 @@ public class PrefsInitializer extends AbstractPreferenceInitializer { preferenceStore.setDefault(Constants.PREF_MODULITH, true); + preferenceStore.setDefault(Constants.PREF_LIVE_INFORMATION_ALL_JVM_PROCESSES, false); + } } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ShowMessageException.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ShowMessageException.java index c859b0061..141d6e4f3 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ShowMessageException.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ShowMessageException.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 @@ -19,21 +19,21 @@ public class ShowMessageException extends RuntimeException { private static final long serialVersionUID = 1L; public final MessageParams message; - public ShowMessageException(MessageParams message, Exception cause) { + public ShowMessageException(MessageParams message, Throwable cause) { super(message.getMessage(), cause); this.message = message; } - public static ShowMessageException error(String message, Exception cause) { + public static ShowMessageException error(String message, Throwable cause) { return create(MessageType.Error, message, cause); } - public static ShowMessageException warning(String message, Exception cause) { + public static ShowMessageException warning(String message, Throwable cause) { return create(MessageType.Warning, message, cause); } - private static ShowMessageException create(MessageType warning, String message, Exception cause) { + private static ShowMessageException create(MessageType warning, String message, Throwable cause) { MessageParams m = new MessageParams(); m.setMessage(message); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaConfig.java index 169cebe21..3eb04c27b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaConfig.java @@ -96,6 +96,11 @@ public class BootJavaConfig implements InitializingBean { return enabled != null && enabled.booleanValue(); } + public boolean isShowingAllJvmProcesses() { + Boolean isAll = settings.getBoolean("boot-java", "live-information", "all-local-java-processes"); + return isAll != null && isAll.booleanValue(); + } + public String[] xmlBeansFoldersToScan() { String foldersStr = settings.getString("boot-java", "support-spring-xml-config", "scan-folders"); if (foldersStr != null) { 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 93fcc220d..b9dd50474 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018, 2023 Pivotal, Inc. + * Copyright (c) 2018, 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 @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import org.eclipse.lsp4j.CodeActionKind; import org.eclipse.lsp4j.CodeActionOptions; @@ -200,6 +201,33 @@ public class BootLanguageServerBootApp { return bean; } + @Bean + SpringProcessConnectorRemote localAppsFromCommandsConnector(SimpleLanguageServer server, SpringProcessConnectorService liveDataService) { + SpringProcessConnectorRemote bean = new SpringProcessConnectorRemote(server, liveDataService); + final Map localApps = new HashMap<>(); + final Gson gson = new Gson(); + server.onCommand("sts/livedata/localAdd", params -> { + 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); + } + bean.updateApps(localApps.values().toArray(new RemoteBootAppData[localApps.size()])); + return CompletableFuture.completedFuture(null); + } + }); + 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()); + if (!removedApps.isEmpty()) { + bean.updateApps(localApps.values().toArray(new RemoteBootAppData[localApps.size()])); + } + return CompletableFuture.completedFuture(null); + } + }); + return bean; + } + @ConditionalOnMissingClass("org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness") @Bean AdHocSpringPropertyIndexProvider adHocProperties(BootLanguageServerParams params, FileObserver fileObserver, DocumentEventListenerManager documentEvents) { return new AdHocSpringPropertyIndexProvider(params.projectFinder, params.projectObserver, fileObserver, documentEvents); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java index 783b6932f..fbec4e29f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2023 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 @@ -143,14 +143,15 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent hoverProvider = createHoverHandler(projectFinder, sourceLinks, liveDataProvider); new SpringProcessLiveHoverUpdater(server, hoverProvider, projectFinder, liveDataProvider); + BootJavaConfig config = appContext.getBean(BootJavaConfig.class); + // deal with locally running processes and their connections - SpringProcessConnectorLocal liveDataLocalProcessConnector = new SpringProcessConnectorLocal(liveDataService, projectObserver); + SpringProcessConnectorLocal liveDataLocalProcessConnector = new SpringProcessConnectorLocal(liveDataService, projectObserver, config); // create and handle commands new SpringProcessCommandHandler(server, liveDataService, liveDataLocalProcessConnector, appContext.getBeansOfType(SpringProcessConnectorRemote.class).values()); // track locally running processes and automatically connect to them if configured to do so - BootJavaConfig config = appContext.getBean(BootJavaConfig.class); liveProcessTracker = new SpringProcessTracker(liveDataLocalProcessConnector, Duration.ofMillis(config.getLiveInformationAutomaticTrackingDelay())); // diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessCommandHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessCommandHandler.java index ddfea9957..848ef76a0 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessCommandHandler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessCommandHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2022 Pivotal, Inc. + * Copyright (c) 2019, 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 @@ -115,20 +115,19 @@ public class SpringProcessCommandHandler { }); } - private CompletableFuture connect(ExecuteCommandParams params) { + private CompletableFuture connect(ExecuteCommandParams params) { String processKey = getProcessKey(params); if (processKey != null) { // try local processes - if (SpringProcessConnectorLocal.isAvailable()) { + if (localProcessConnector.isAvailable()) { // Try cached processes. SpringProcessDescriptor[] processes = localProcessConnector .getProcesses(false, SpringProcessStatus.REGULAR, SpringProcessStatus.AUTO_CONNECT); for (SpringProcessDescriptor process : processes) { if (process.getProcessKey().equals(processKey)) { - localProcessConnector.connectProcess(process); - return CompletableFuture.completedFuture(null); + return localProcessConnector.connectProcess(process); } } @@ -136,8 +135,7 @@ public class SpringProcessCommandHandler { .getProcesses(true, SpringProcessStatus.REGULAR, SpringProcessStatus.AUTO_CONNECT); for (SpringProcessDescriptor process : processes) { if (process.getProcessKey().equals(processKey)) { - localProcessConnector.connectProcess(process); - return CompletableFuture.completedFuture(null); + return localProcessConnector.connectProcess(process); } } @@ -149,8 +147,7 @@ public class SpringProcessCommandHandler { for (RemoteBootAppData remoteProcess : remoteProcesses) { String key = SpringProcessConnectorRemote.getProcessKey(remoteProcess); if (processKey.equals(key)) { - remoteProcessConnector.connectProcess(remoteProcess); - return CompletableFuture.completedFuture(null); + return remoteProcessConnector.connectProcess(remoteProcess); } } } @@ -158,26 +155,26 @@ public class SpringProcessCommandHandler { return CompletableFuture.completedFuture(null); } - - private CompletableFuture refresh(ExecuteCommandParams params) { + + private CompletableFuture refresh(ExecuteCommandParams params) { SpringProcessParams springProcessParams = new SpringProcessParams(); springProcessParams.setProcessKey(getProcessKey(params)); springProcessParams.setEndpoint(getArgumentByKey(params, "endpoint")); if (springProcessParams.getProcessKey() != null) { - connectorService.refreshProcess(springProcessParams); + return connectorService.refreshProcess(springProcessParams); } return CompletableFuture.completedFuture(null); } - private CompletableFuture refreshMetrics(ExecuteCommandParams params) { + private CompletableFuture refreshMetrics(ExecuteCommandParams params) { SpringProcessParams springProcessParams = new SpringProcessParams(); springProcessParams.setProcessKey(getProcessKey(params)); springProcessParams.setEndpoint(getArgumentByKey(params, "endpoint")); springProcessParams.setMetricName(getArgumentByKey(params, "metricName")); springProcessParams.setTags(getArgumentByKey(params, "tags")); // Convert tags to a map if (springProcessParams.getProcessKey() != null) { - connectorService.refreshProcess(springProcessParams); + return connectorService.refreshProcess(springProcessParams); } return CompletableFuture.completedFuture(null); @@ -206,33 +203,54 @@ public class SpringProcessCommandHandler { result.add(new LiveProcessCommand(COMMAND_DISCONNECT, processKey, label, process.getProjectName(), process.getProcessId())); alreadyConnected.add(processKey); } + + + // collect available remote process. Some of them might be local processes, make a note of these too + List remoteProcessCommands = new ArrayList<>(); + Map localProcessCommands = new HashMap<>(); + for (SpringProcessConnectorRemote remoteProcessConnector : remoteProcessConnectors) { + RemoteBootAppData[] remoteProcesses = remoteProcessConnector.getProcesses(); + for (RemoteBootAppData remoteProcess : remoteProcesses) { + String processKey = SpringProcessConnectorRemote.getProcessKey(remoteProcess); + boolean isLocal = remoteProcess.getProcessID() != null && ("localhost".equals(remoteProcess.getHost()) || "127.0.0.1".equals(remoteProcess.getHost())); + if (alreadyConnected.contains(processKey)) { + alreadyConnected.add(SpringProcessConnectorService.getProcessKey(remoteProcess.getProcessID(), remoteProcess.getProcessName())); + } else { + String label = createLabel(remoteProcess.getProcessID(), SpringProcessConnectorRemote.getProcessName(remoteProcess)); + LiveProcessCommand command = new LiveProcessCommand(COMMAND_CONNECT, processKey, label, remoteProcess.getProjectName(), remoteProcess.getProcessID()); + if (isLocal) { + // Local add these later while checking for local boot processes + localProcessCommands.put(remoteProcess.getProcessID(), command); + } else { + // Keep these to be added at the end + remoteProcessCommands.add(command); + } + } + } + } // other available local processes - if (SpringProcessConnectorLocal.isAvailable()) { + if (localProcessConnector.isAvailable()) { SpringProcessDescriptor[] localProcesses = localProcessConnector.getProcesses(true, SpringProcessStatus.REGULAR, SpringProcessStatus.AUTO_CONNECT); for (SpringProcessDescriptor localProcess : localProcesses) { String processKey = localProcess.getProcessKey(); if (!alreadyConnected.contains(processKey)) { - String label = createLabel(localProcess.getProcessID(), localProcess.getProcessName()); - - LiveProcessCommand command = new LiveProcessCommand(COMMAND_CONNECT, processKey, label, localProcess.getProjectName(), null); + LiveProcessCommand command = localProcessCommands.remove(localProcess.getProcessID()); + if (command == null) { + String label = createLabel(localProcess.getProcessID(), localProcess.getProcessName()); + command = new LiveProcessCommand(COMMAND_CONNECT, processKey, label, localProcess.getProjectName(), null); + } result.add(command); } } } // other available remote processes - for (SpringProcessConnectorRemote remoteProcessConnector : remoteProcessConnectors) { - RemoteBootAppData[] remoteProcesses = remoteProcessConnector.getProcesses(); - for (RemoteBootAppData remoteProcess : remoteProcesses) { - String processKey = SpringProcessConnectorRemote.getProcessKey(remoteProcess); - if (!alreadyConnected.contains(processKey)) { - String label = createLabel(remoteProcess.getProcessID(), SpringProcessConnectorRemote.getProcessName(remoteProcess)); - result.add(new LiveProcessCommand(COMMAND_CONNECT, processKey, label, null, remoteProcess.getProcessID())); - } - } + for (LiveProcessCommand command : localProcessCommands.values()) { + result.add(command); } - + result.addAll(remoteProcessCommands); + log.debug("getProcessCommands => {}", result); return CompletableFuture.completedFuture((Object[]) result.toArray(new Object[result.size()])); } 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 d7899cbcd..bf7489fec 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2022 Pivotal, Inc. + * Copyright (c) 2019, 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 @@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.app.BootJavaConfig; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.SpringProjectUtil; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; @@ -36,7 +37,6 @@ import com.sun.tools.attach.VirtualMachineDescriptor; /** * @author Martin Lippert */ -@SuppressWarnings("restriction") public class SpringProcessConnectorLocal { private static final Logger log = LoggerFactory.getLogger(SpringProcessConnectorLocal.class); @@ -52,8 +52,11 @@ public class SpringProcessConnectorLocal { private boolean projectsChanged; + final private BootJavaConfig config; + - public SpringProcessConnectorLocal(SpringProcessConnectorService processConnector, ProjectObserver projectObserver) { + public SpringProcessConnectorLocal(SpringProcessConnectorService processConnector, ProjectObserver projectObserver, BootJavaConfig config) { + this.config = config; this.projects = new ConcurrentHashMap<>(); this.processes = Collections.synchronizedSet(new HashSet<>()); this.statusUpdateThreadPool = Executors.newFixedThreadPool(10); @@ -91,14 +94,17 @@ public class SpringProcessConnectorLocal { * checks whether this class can operate normally or not - it is recommended to check this before calling out to this class * (if the attach to VirtualMachine library is not around, this class cannot really do anything and will throw exceptions) */ - public static boolean isAvailable() { - try { - Class vmClass = VirtualMachine.class; - return vmClass != null; - } - catch (NoClassDefFoundError e) { - return false; + public boolean isAvailable() { + if (config.isShowingAllJvmProcesses()) { + try { + Class vmClass = VirtualMachine.class; + return vmClass != null; + } + catch (NoClassDefFoundError e) { + return false; + } } + return false; } public boolean isLocalProcess(String processKey) { @@ -186,7 +192,7 @@ public class SpringProcessConnectorLocal { } } - public void connectProcess(SpringProcessDescriptor descriptor) { + public CompletableFuture connectProcess(SpringProcessDescriptor descriptor) { VirtualMachine vm = null; VirtualMachineDescriptor vmDescriptor = descriptor.getVm(); @@ -205,6 +211,7 @@ public class SpringProcessConnectorLocal { jmxAddress = vm.startLocalManagementAgent(); } catch (Exception e) { log.error("Error starting local management agent", e); + return CompletableFuture.failedFuture(e); } } @@ -216,11 +223,13 @@ public class SpringProcessConnectorLocal { SpringProcessConnectorOverJMX connector = new SpringProcessConnectorOverJMX(ProcessType.LOCAL, descriptor.getProcessKey(), jmxAddress, urlScheme, processID, processName, descriptor.getProjectName(), null, null); - this.processConnectorService.connectProcess(descriptor.getProcessKey(), connector); + return this.processConnectorService.connectProcess(descriptor.getProcessKey(), connector); } + return CompletableFuture.failedFuture(new Exception("No JMX URL available!")); } catch (Exception e) { log.error("exception while connecting to jvm process", e); + return CompletableFuture.failedFuture(e); } finally { if (vm != null) { 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 544560cea..493757470 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, 2022 Pivotal, Inc. + * Copyright (c) 2019, 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 @@ -18,6 +18,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CompletableFuture; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,6 +44,7 @@ public class SpringProcessConnectorRemote { private String processId; private String processName; + private String projectName; public String getJmxurl() { return jmxurl; @@ -100,11 +102,15 @@ public class SpringProcessConnectorRemote { this.processName = processName; } + public String getProjectName() { + return projectName; + } + @Override public String toString() { return "RemoteBootAppData [jmxurl=" + jmxurl + ", host=" + host + ", urlScheme=" + urlScheme + ", port=" + port + ", manualConnect=" + manualConnect + ", keepChecking=" + keepChecking + ", processId=" - + processId + ", processName=" + processName + "]"; + + processId + ", processName=" + processName + ". projectName=" + projectName + "]"; } public void setManualConnection(boolean manualConnect) { @@ -117,7 +123,7 @@ public class SpringProcessConnectorRemote { @Override public int hashCode() { - return Objects.hash(host, jmxurl, keepChecking, manualConnect, port, processId, processName, urlScheme); + return Objects.hash(host, jmxurl, keepChecking, manualConnect, port, processId, processName, urlScheme, projectName); } @Override @@ -132,7 +138,8 @@ public class SpringProcessConnectorRemote { return Objects.equals(host, other.host) && Objects.equals(jmxurl, other.jmxurl) && keepChecking == other.keepChecking && manualConnect == other.manualConnect && Objects.equals(port, other.port) && Objects.equals(processId, other.processId) - && Objects.equals(processName, other.processName) && Objects.equals(urlScheme, other.urlScheme); + && Objects.equals(processName, other.processName) && Objects.equals(urlScheme, other.urlScheme) + && Objects.equals(projectName, other.getProcessName()); } } @@ -205,10 +212,10 @@ public class SpringProcessConnectorRemote { } public static String getProcessKey(RemoteBootAppData appData) { - return "remote process - " + appData.getJmxurl(); + return appData.getJmxurl(); } - public void connectProcess(RemoteBootAppData remoteProcess) { + public CompletableFuture connectProcess(RemoteBootAppData remoteProcess) { String processKey = getProcessKey(remoteProcess); String processID = remoteProcess.getProcessID(); String processName = getProcessName(remoteProcess); @@ -216,14 +223,15 @@ public class SpringProcessConnectorRemote { String host = remoteProcess.getHost(); String port = remoteProcess.getPort(); String urlScheme = remoteProcess.getUrlScheme(); + String projectName = remoteProcess.getProjectName(); // boolean keepChecking = _appData.isKeepChecking(); if (jmxURL.startsWith("http")) { - SpringProcessConnectorOverHttp connector = new SpringProcessConnectorOverHttp(ProcessType.REMOTE, processKey, jmxURL, urlScheme, processID, processName, urlScheme, host, port); - processConnectorService.connectProcess(processKey, connector); + SpringProcessConnectorOverHttp connector = new SpringProcessConnectorOverHttp(ProcessType.REMOTE, processKey, jmxURL, urlScheme, processID, processName, projectName, host, port); + return processConnectorService.connectProcess(processKey, connector); } else { - SpringProcessConnectorOverJMX connector = new SpringProcessConnectorOverJMX(ProcessType.REMOTE, processKey, jmxURL, urlScheme, processID, processName, null, host, port); - processConnectorService.connectProcess(processKey, connector); + SpringProcessConnectorOverJMX connector = new SpringProcessConnectorOverJMX(ProcessType.REMOTE, processKey, jmxURL, urlScheme, processID, processName, projectName, host, port); + return processConnectorService.connectProcess(processKey, connector); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorService.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorService.java index 044280b2c..87f04a35e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorService.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorService.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2023 Pivotal, Inc. + * Copyright (c) 2019, 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 @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.boot.java.livehover.v2; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; @@ -87,7 +88,7 @@ public class SpringProcessConnectorService { this.retryDelayInSeconds = retryDelayInSeconds; } - public void connectProcess(String processKey, SpringProcessConnector connector) { + public CompletableFuture connectProcess(String processKey, SpringProcessConnector connector) { log.info("connect to process: " + processKey); this.connectors.put(processKey, connector); @@ -95,17 +96,12 @@ public class SpringProcessConnectorService { connector.addConnectorChangeListener(connectorListener); - try { - final IndefiniteProgressTask progressTask = getProgressTask( - "spring-process-connector-service-connect-" + processKey, "Connect", null); - scheduleConnect(progressTask, processKey, connector, 0, TimeUnit.SECONDS, 0); - } - catch (Exception e) { - log.error("error connecting to " + processKey, e); - } + final IndefiniteProgressTask progressTask = getProgressTask( + "spring-process-connector-service-connect-" + processKey, "Connect", null); + return scheduleConnect(progressTask, processKey, connector, 0, TimeUnit.SECONDS, 0); } - public void refreshProcess(SpringProcessParams springProcessParams) { + public CompletableFuture refreshProcess(SpringProcessParams springProcessParams) { log.info("refresh process: " + springProcessParams.getProcessKey()); SpringProcessConnector connector = this.connectors.get(springProcessParams.getProcessKey()); @@ -113,8 +109,9 @@ public class SpringProcessConnectorService { final IndefiniteProgressTask progressTask = getProgressTask( "spring-process-connector-service-refresh-data-" + springProcessParams.getProcessKey(), "Refresh", null); - scheduleRefresh(progressTask, springProcessParams, connector, 0, TimeUnit.SECONDS, 0); + return scheduleRefresh(progressTask, springProcessParams, connector, 0, TimeUnit.SECONDS, 0); } + return CompletableFuture.completedFuture(null); } public SpringProcessLiveData getLiveData(String processKey) { @@ -161,35 +158,35 @@ public class SpringProcessConnectorService { return processID; } - private void scheduleConnect(IndefiniteProgressTask progressTask, String processKey, SpringProcessConnector connector, long delay, TimeUnit unit, int retryNo) { + private CompletableFuture scheduleConnect(IndefiniteProgressTask progressTask, String processKey, SpringProcessConnector connector, long delay, TimeUnit unit, int retryNo) { String progressMessage = "Connecting to process: " + processKey + " - retry no: " + retryNo; log.info(progressMessage); - - this.scheduler.schedule(() -> { + return CompletableFuture.runAsync(() -> { try { progressTask.progressEvent(progressMessage); connector.connect(); - progressTask.done(); - - refreshProcess(new SpringProcessParams(processKey, "", "", "")); + progressTask.done(); } catch (Exception e) { - log.info("problem occured during process connect", e); - - if (retryNo < maxRetryCount && isKnownProcessKey(processKey)) { - scheduleConnect(progressTask, processKey, connector, retryDelayInSeconds, TimeUnit.SECONDS, retryNo + 1); - } else { - progressTask.done(); - - // Send message to client if maximum retries reached on error - if (isKnownProcessKey(processKey)) { - diagnosticService.diagnosticEvent(ShowMessageException - .error("Failed to connect to process " + processKey + " after retries: " + retryNo, e)); - } - } + throw new CompletionException(e); } - }, delay, unit); + }, CompletableFuture.delayedExecutor(delay, unit, scheduler)).thenCompose(v -> refreshProcess(new SpringProcessParams(processKey, "", "", ""))).exceptionallyCompose(e -> { + log.info("problem occured during process connect", e); + + if (retryNo < maxRetryCount && isKnownProcessKey(processKey)) { + return scheduleConnect(progressTask, processKey, connector, retryDelayInSeconds, TimeUnit.SECONDS, retryNo + 1); + } else { + progressTask.done(); + + // Send message to client if maximum retries reached on error + if (isKnownProcessKey(processKey)) { + diagnosticService.diagnosticEvent(ShowMessageException + .error("Failed to connect to process " + processKey + " after retries: " + retryNo, e)); + } + return CompletableFuture.completedStage(null); + } + }); } private void scheduleDisconnect(IndefiniteProgressTask progressTask, String processKey, SpringProcessConnector connector, long delay, TimeUnit unit, int retryNo) { @@ -220,16 +217,14 @@ public class SpringProcessConnectorService { }, delay, unit); } - private void scheduleRefresh(IndefiniteProgressTask progressTask, SpringProcessParams springProcessParams, SpringProcessConnector connector, long delay, TimeUnit unit, int retryNo) { + private CompletableFuture scheduleRefresh(IndefiniteProgressTask progressTask, SpringProcessParams springProcessParams, SpringProcessConnector connector, long delay, TimeUnit unit, int retryNo) { String processKey = springProcessParams.getProcessKey(); String endpoint = springProcessParams.getEndpoint(); String metricName = springProcessParams.getMetricName(); String progressMessage = "Refreshing data from Spring process: " + processKey + " - retry no: " + retryNo; log.info(progressMessage); - - this.scheduler.schedule(() -> { - + return CompletableFuture.runAsync(() -> { try { progressTask.progressEvent(progressMessage); if(METRICS.equals(endpoint) && (MEMORY.equals(metricName))) { @@ -268,28 +263,32 @@ public class SpringProcessConnectorService { progressTask.done(); } catch (Exception e) { - - log.info("problem occured during process live data refresh", e); + throw new CompletionException(e); + } + }, CompletableFuture.delayedExecutor(delay, unit, scheduler)).exceptionallyCompose(e -> { + log.info("problem occured during process live data refresh", e); + + if (retryNo < maxRetryCount && isKnownProcessKey(processKey)) { + return scheduleRefresh(progressTask, springProcessParams, connector, retryDelayInSeconds, TimeUnit.SECONDS, + retryNo + 1); + } + else { + progressTask.done(); - if (retryNo < maxRetryCount && isKnownProcessKey(processKey)) { - scheduleRefresh(progressTask, springProcessParams, connector, retryDelayInSeconds, TimeUnit.SECONDS, - retryNo + 1); - } - else { - progressTask.done(); - - // Send message to client if maximum retries reached on error - if (isKnownProcessKey(processKey)) { - diagnosticService.diagnosticEvent(ShowMessageException - .error("Failed to refresh live data from process " + processKey + " after retries: " + retryNo, e)); - - if (!connectedSuccess.containsKey(connector.getProcessKey())) { - disconnectProcess(processKey); - } + // Send message to client if maximum retries reached on error + if (isKnownProcessKey(processKey)) { + diagnosticService.diagnosticEvent(ShowMessageException + .error("Failed to refresh live data from process " + processKey + " after retries: " + retryNo, e)); + + if (!connectedSuccess.containsKey(connector.getProcessKey())) { + disconnectProcess(processKey); } } + return CompletableFuture.completedFuture(null); } - }, delay, unit); + + }); + } private IndefiniteProgressTask getProgressTask(String prefixId, String title, String message) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessTracker.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessTracker.java index 39d94d084..c19f67785 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessTracker.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessTracker.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019 Pivotal, Inc. + * Copyright (c) 2019, 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 @@ -31,7 +31,6 @@ public class SpringProcessTracker { private static final Logger log = LoggerFactory.getLogger(SpringProcessTracker.class); private final SpringProcessConnectorLocal localProcessConnector; - private final boolean isConnectorAvailable; private boolean automaticTrackingEnabled; private Duration POLLING_INTERVAL; @@ -43,9 +42,7 @@ public class SpringProcessTracker { this.localProcessConnector = localProcessConnector; this.POLLING_INTERVAL = pollingInterval != null ? pollingInterval : Duration.ofMillis(BootJavaConfig.LIVE_INFORMATION_AUTOMATIC_TRACKING_DELAY_DEFAULT); this.automaticTrackingEnabled = false; - this.processesAlreadySeen = new HashSet<>(); - - this.isConnectorAvailable = SpringProcessConnectorLocal.isAvailable(); + this.processesAlreadySeen = new HashSet<>(); } public synchronized void setTrackingEnabled(boolean trackingEnabled) { @@ -74,8 +71,8 @@ public class SpringProcessTracker { } public synchronized void start() { - if (!isConnectorAvailable) { - log.error("virtual machine connector library not available, no automatic local process tracking possible"); + if (!localProcessConnector.isAvailable()) { + log.error("No automatic local process tracking possible"); return; } diff --git a/vscode-extensions/vscode-spring-boot/images/light-bulb-off.svg b/vscode-extensions/vscode-spring-boot/images/light-bulb-off.svg new file mode 100644 index 000000000..4045ecfba --- /dev/null +++ b/vscode-extensions/vscode-spring-boot/images/light-bulb-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode-extensions/vscode-spring-boot/images/light-bulb-on.svg b/vscode-extensions/vscode-spring-boot/images/light-bulb-on.svg new file mode 100644 index 000000000..6913af9bc --- /dev/null +++ b/vscode-extensions/vscode-spring-boot/images/light-bulb-on.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 ad4f4e279..6275fb569 100644 --- a/vscode-extensions/vscode-spring-boot/lib/debug-config-provider.ts +++ b/vscode-extensions/vscode-spring-boot/lib/debug-config-provider.ts @@ -7,18 +7,23 @@ import { debug, DebugConfigurationProviderTriggerKind, DebugSessionCustomEvent, Disposable, - ProviderResult + 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 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' @@ -30,12 +35,13 @@ interface ProcessEvent { shellProcessId: number } + class SpringBootDebugConfigProvider implements DebugConfigurationProvider { - resolveDebugConfigurationWithSubstitutedVariables(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { + 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 (debugConfiguration.vmArgs) { + if (typeof debugConfiguration.vmArgs === 'string') { if (debugConfiguration.vmArgs.indexOf(JMX_VM_ARG) < 0) { debugConfiguration.vmArgs += ` ${JMX_VM_ARG}true`; } @@ -51,8 +57,15 @@ class SpringBootDebugConfigProvider implements DebugConfigurationProvider { 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}`; + 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; @@ -87,12 +100,13 @@ export function hookListenerToBooleanPreference(setting: string, listenerCreator } export function startDebugSupport(): Disposable { - return hookListenerToBooleanPreference( - 'boot-java.live-information.automatic-connection.on', - () => Disposable.from( - debug.onDidReceiveDebugSessionCustomEvent(handleCustomDebugEvent), - debug.registerDebugConfigurationProvider('java', new SpringBootDebugConfigProvider(), DebugConfigurationProviderTriggerKind.Initial) - ) + return Disposable.from( + hookListenerToBooleanPreference( + 'boot-java.live-information.automatic-connection.on', + () => debug.registerDebugConfigurationProvider('java', new SpringBootDebugConfigProvider(), DebugConfigurationProviderTriggerKind.Initial) + ), + debug.onDidReceiveDebugSessionCustomEvent(handleCustomDebugEvent), + debug.onDidTerminateDebugSession(handleTerminateDebugSession) ); } @@ -102,13 +116,34 @@ async function handleCustomDebugEvent(e: DebugSessionCustomEvent): Promise if (canConnect(debugConfiguration)) { setTimeout(async () => { const pid = await getAppPid(e.body as ProcessEvent); - const processKey = pid.toString(); - commands.executeCommand('sts/livedata/connect', { processKey }); + 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; @@ -141,8 +176,9 @@ function isActuatorJarFile(f: string): boolean { function canConnect(debugConfiguration: DebugConfiguration): boolean { if (!TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isActuatorOnClasspath(debugConfiguration)) { - return debugConfiguration.vmArgs + 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 39b78ff7e..b74728c23 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 @@ -1,8 +1,7 @@ -'use strict'; - import * as VSCode from 'vscode'; import { LanguageClient } from "vscode-languageclient/node"; import { ActivatorOptions } from '@pivotal-tools/commons-vscode'; +import { commands, window } from 'vscode'; interface ProcessCommandInfo { processKey : string; @@ -11,30 +10,74 @@ interface ProcessCommandInfo { projectName: string; } +export interface RemoteBootApp { + jmxurl: string; + host: string; + urlScheme: "https" | "http"; + port: string; + manualConnect: boolean; + keepChecking?: boolean; + processId: string; + processName: string; + projectName?: string; +} + +type BootAppState = "none" | "connecting" | "connected" | "disconnecting" | "disconnected"; + +let activeBootApp: RemoteBootApp | undefined; +let state: BootAppState + async function liveHoverConnectHandler() { //sts.vscode-spring-boot.codeAction const processData : ProcessCommandInfo[] = await VSCode.commands.executeCommand('sts/livedata/listProcesses'); const choiceMap = new Map(); const choices : string[] = []; processData.forEach(p => { - const slash = p.action.lastIndexOf('/'); - if (slash>=0) { - var actionLabel = p.action.substring(slash+1); - actionLabel = actionLabel.substring(0, 1).toUpperCase() + actionLabel.substring(1); - const choiceLabel = actionLabel + " " + p.label; - choiceMap.set(choiceLabel, p); - choices.push(choiceLabel); + let actionLabel = ""; + switch (p.action) { + case "sts/livedata/connect": + actionLabel = "Show" + break; + case "sts/livedata/refresh": + actionLabel = "Refresh"; + break; + case "sts/livedata/disconnect": + actionLabel = "Hide"; + break; } + const choiceLabel = actionLabel + " Live Data from: " + p.label; + choiceMap.set(choiceLabel, p); + choices.push(choiceLabel); }); if (choices) { const picked = await VSCode.window.showQuickPick(choices); if (picked) { const chosen = choiceMap.get(picked); - await VSCode.commands.executeCommand(chosen.action, chosen); + if (activeBootApp?.jmxurl === chosen.processKey) { + switch (chosen.action) { + case "sts/livedata/connect": + await commands.executeCommand('vscode-spring-boot.live.show.active'); + break; + case "sts/livedata/disconnect": + await commands.executeCommand('vscode-spring-boot.live.hide.active'); + break; + default: + await VSCode.commands.executeCommand(chosen.action, chosen); + } + } else { + await VSCode.commands.executeCommand(chosen.action, chosen); + } } } } +async function updateBootAppState(newState: BootAppState) { + if (newState !== state) { + state = newState; + commands.executeCommand('setContext', 'vscode-spring-boot.active-app-state', state); + } +} + /** Called when extension is activated */ export function activate( client: LanguageClient, @@ -42,12 +85,58 @@ export function activate( context: VSCode.ExtensionContext ) { context.subscriptions.push( - VSCode.commands.registerCommand('vscode-spring-boot.live-hover.connect', () => { + + commands.registerCommand('vscode-spring-boot.live-hover.connect', () => { if (client.isRunning()) { return liveHoverConnectHandler(); } else { - VSCode.window.showErrorMessage("No Spring Boot project found. Action is only available for Spring Boot Projects"); + window.showErrorMessage("No Spring Boot project found. Action is only available for Spring Boot Projects"); } - }) + }), + + commands.registerCommand("vscode-spring-boot.live.activate", async appData => { + activeBootApp = appData; + await commands.executeCommand('sts/livedata/localAdd', activeBootApp); + updateBootAppState("disconnected"); + }), + + commands.registerCommand("vscode-spring-boot.live.deactivate", async () => { + await commands.executeCommand('sts/livedata/localRemove', activeBootApp.jmxurl); + activeBootApp = undefined; + updateBootAppState("none"); + }), + + commands.registerCommand("vscode-spring-boot.live.show.active", async () => { + try { + updateBootAppState("connecting"); + await commands.executeCommand('sts/livedata/connect', { + processKey: activeBootApp.jmxurl + }); + updateBootAppState("connected"); + } catch (error) { + updateBootAppState("disconnected"); + throw error; + } + }), + + commands.registerCommand("vscode-spring-boot.live.refresh.active", async () => { + await commands.executeCommand('sts/livedata/refresh', { + processKey: activeBootApp.jmxurl + }); + }), + + commands.registerCommand("vscode-spring-boot.live.hide.active", async () => { + try { + updateBootAppState("disconnecting"); + await commands.executeCommand('sts/livedata/disconnect', { + processKey: activeBootApp.jmxurl + }); + updateBootAppState("disconnected"); + } catch (error) { + updateBootAppState("connected"); + throw error; + } + }), + ); } diff --git a/vscode-extensions/vscode-spring-boot/package.json b/vscode-extensions/vscode-spring-boot/package.json index 13f67e130..bc164aa14 100644 --- a/vscode-extensions/vscode-spring-boot/package.json +++ b/vscode-extensions/vscode-spring-boot/package.json @@ -124,13 +124,42 @@ { "command": "vscode-spring-boot.yaml-to-props", "when": "false" + }, + { + "command": "vscode-spring-boot.live.show.active", + "when": "false" + }, + { + "command": "vscode-spring-boot.live.hide.active", + "when": "false" + }, + { + "command": "vscode-spring-boot.live.refresh.active", + "when": "false" + } + ], + "debug/toolBar": [ + { + "when": "inDebugMode && debugType == 'java' && (vscode-spring-boot.active-app-state == 'disconnected' || vscode-spring-boot.active-app-state == 'connecting')", + "command": "vscode-spring-boot.live.show.active", + "group": "Spring Boot" + }, + { + "when": "inDebugMode && debugType == 'java' && vscode-spring-boot.active-app-state == 'connected'", + "command": "vscode-spring-boot.live.refresh.active", + "group": "Spring Boot" + }, + { + "when": "inDebugMode && debugType == 'java' && (vscode-spring-boot.active-app-state == 'connected' || vscode-spring-boot.active-app-state == 'disconnecting')", + "command": "vscode-spring-boot.live.hide.active", + "group": "Spring Boot" } ] }, "commands": [ { "command": "vscode-spring-boot.live-hover.connect", - "title": "Manage Live Spring Boot Process Connections", + "title": "Show/Refresh/Hide Live Data from Spring Boot Processes", "category": "Spring Boot" }, { @@ -180,6 +209,27 @@ "command": "vscode-spring-boot.yaml-to-props", "title": "Convert .yaml to .properties", "category": "Spring Boot" + }, + { + "command": "vscode-spring-boot.live.show.active", + "title": "Show Live Data from launched Spring Boot App", + "enablement": "vscode-spring-boot.active-app-state == 'disconnected'", + "icon": "./images/light-bulb-off.svg", + "category": "Spring Boot" + }, + { + "command": "vscode-spring-boot.live.hide.active", + "title": "Hide Live Data from launched Spring Boot App", + "enablement": "vscode-spring-boot.active-app-state == 'connected'", + "icon": "./images/light-bulb-on.svg", + "category": "Spring Boot" + }, + { + "command": "vscode-spring-boot.live.refresh.active", + "title": "Refresh Live Data from launched Spring Boot App", + "enablement": "vscode-spring-boot.active-app-state == 'connected'", + "icon": "$(refresh)", + "category": "Spring Boot" } ], "configuration": [ @@ -198,6 +248,11 @@ "default": true, "description": "Live Information - Automatic addition of JVM arguments enabling JMX and Process Connection via JMX Enabled" }, + "boot-java.live-information.all-local-java-processes": { + "type": "boolean", + "description": "Show all local JVM processes when connecting to Live Spring Boot App processes", + "default": false + }, "boot-java.live-information.fetch-data.max-retries": { "type": "number", "default": 10, @@ -1099,6 +1154,7 @@ "vsce-pre-release-package": "vsce package --pre-release" }, "dependencies": { + "portfinder": "1.0.32", "@pivotal-tools/commons-vscode": "file:../commons-vscode/pivotal-tools-commons-vscode-0.2.4.tgz", "ps-list": "^7.2.0", "vscode-languageclient": "^9.0.1"