From 48fa9b61e54559d2337a7b1c77f7c2bc28f94eb2 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 9 Mar 2022 13:45:20 -0500 Subject: [PATCH] Add/Connect remote process command --- .../boot/app/BootLanguageServerBootApp.java | 58 +++- .../app/BootLanguageServerInitializer.java | 7 +- .../BootJavaLanguageServerComponents.java | 45 +-- .../java/livehover/v2/ActuatorConnection.java | 34 +++ .../livehover/v2/HttpActuatorConnection.java | 97 +++++++ .../v2/SpringProcessCommandHandler.java | 38 +-- .../v2/SpringProcessConnectorOverHttp.java | 98 +++++++ .../v2/SpringProcessConnectorRemote.java | 34 +-- ...pringProcessLiveDataExtractorOverHttp.java | 272 ++++++++++++++++++ 9 files changed, 614 insertions(+), 69 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/ActuatorConnection.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/HttpActuatorConnection.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverHttp.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverHttp.java 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 211b8dc1b..2b8ac063d 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, 2021 Pivotal, Inc. + * Copyright (c) 2018, 2022 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,7 +11,12 @@ package org.springframework.ide.vscode.boot.app; import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Qualifier; @@ -34,6 +39,9 @@ import org.springframework.ide.vscode.boot.java.links.JavaServerElementLocationP import org.springframework.ide.vscode.boot.java.links.JdtJavaDocumentUriProvider; import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; import org.springframework.ide.vscode.boot.java.links.SourceLinks; +import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessConnectorRemote; +import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessConnectorRemote.RemoteBootAppData; +import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessConnectorService; import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessLiveDataProvider; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.java.utils.SymbolCache; @@ -77,6 +85,8 @@ import org.springframework.ide.vscode.languageserver.starter.LanguageServerRunne import org.yaml.snakeyaml.Yaml; import com.google.common.collect.ImmutableList; +import com.google.gson.Gson; +import com.google.gson.JsonElement; import reactor.core.publisher.Hooks; @@ -118,7 +128,53 @@ public class BootLanguageServerBootApp { SpringProcessLiveDataProvider liveDataProvider() { return new SpringProcessLiveDataProvider(); } + + @Bean + SpringProcessConnectorService processConnectorService(SimpleLanguageServer server, SpringProcessLiveDataProvider liveDataProvider) { + return new SpringProcessConnectorService(server, liveDataProvider); + } + + @Bean + SpringProcessConnectorRemote remoteAppsFromSettingsConnector(SimpleLanguageServer server, SpringProcessConnectorService liveDataService) { + SpringProcessConnectorRemote bean = new SpringProcessConnectorRemote(server, liveDataService); + server.getWorkspaceService().onDidChangeConfiguraton(settings -> { + RemoteBootAppData[] appData = settings.getAs(RemoteBootAppData[].class, "boot-java", "remote-apps"); + if (appData == null) { + //Avoid NPE + appData = new RemoteBootAppData[0]; + } + bean.updateApps(appData); + }); + return bean; + } + + @Bean + SpringProcessConnectorRemote remoteAppsFromCommandsConnector(SimpleLanguageServer server, SpringProcessConnectorService liveDataService) { + SpringProcessConnectorRemote bean = new SpringProcessConnectorRemote(server, liveDataService); + final Map allRemoteApps = new HashMap<>(); + final Gson gson = new Gson(); + server.onCommand("sts/livedata/remoteConnect", params -> { + List args = params.getArguments(); + String owner = ((JsonElement) args.get(0)).getAsString(); + RemoteBootAppData[] data = gson.fromJson((JsonElement) args.get(1), RemoteBootAppData[].class); + if (data.length > 0) { + allRemoteApps.put(owner, data); + } else { + allRemoteApps.remove(owner); + } + List all = new ArrayList<>(); + for (RemoteBootAppData[] remoteBootAppData : allRemoteApps.values()) { + for (RemoteBootAppData r : remoteBootAppData) { + all.add(r); + } + } + bean.updateApps(all.toArray(new RemoteBootAppData[all.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/app/BootLanguageServerInitializer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java index ffe42d682..b7771e87d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018, 2021 Pivotal, Inc. + * Copyright (c) 2018, 2022 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,7 +11,6 @@ package org.springframework.ide.vscode.boot.app; import java.util.List; -import java.util.Optional; import org.eclipse.lsp4j.MessageType; import org.slf4j.Logger; @@ -19,6 +18,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; import org.springframework.ide.vscode.boot.java.links.JavaElementLocationProvider; import org.springframework.ide.vscode.boot.java.links.SourceLinks; @@ -62,6 +62,7 @@ public class BootLanguageServerInitializer implements InitializingBean { @Autowired YamlAssistContextProvider yamlAssistContextProvider; @Autowired SymbolCache symbolCache; @Autowired SpringProcessLiveDataProvider liveDataProvider; + @Autowired ApplicationContext appContext; @Autowired BootJavaConfig config; @Autowired SpringSymbolIndex springIndexer; @Autowired(required = false) List completionEngines; @@ -93,7 +94,7 @@ public class BootLanguageServerInitializer implements InitializingBean { // some server intialization code. Migrate that code and get rid of the ComposableLanguageServer class CompositeLanguageServerComponents.Builder builder = new CompositeLanguageServerComponents.Builder(); builder.add(new BootPropertiesLanguageServerComponents(server, params, javaElementLocationProvider, parser, yamlStructureProvider, yamlAssistContextProvider, sourceLinks)); - builder.add(new BootJavaLanguageServerComponents(server, params, sourceLinks, cuCache, adHocProperties, symbolCache, liveDataProvider, config, springIndexer)); + builder.add(new BootJavaLanguageServerComponents(appContext)); builder.add(new SpringXMLLanguageServerComponents(server, springIndexer, params, config)); components = builder.build(server); params.projectObserver.addListener(reconcileOpenDocumentsForProjectChange(server, components, params.projectFinder)); 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 aa3b62bf1..482ba1d8d 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, 2020 Pivotal, Inc. + * Copyright (c) 2016, 2022 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,18 +18,16 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import org.eclipse.lsp4j.CompletionItemKind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; import org.springframework.ide.vscode.boot.app.BootJavaConfig; import org.springframework.ide.vscode.boot.app.BootLanguageServerParams; import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyAwareLookup; import org.springframework.ide.vscode.boot.java.autowired.AutowiredHoverProvider; import org.springframework.ide.vscode.boot.java.conditionals.ConditionalsLiveHoverProvider; -import org.springframework.ide.vscode.boot.java.data.DataRepositoryCompletionProcessor; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeLensEngine; -import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine; import org.springframework.ide.vscode.boot.java.handlers.BootJavaDocumentHighlightEngine; import org.springframework.ide.vscode.boot.java.handlers.BootJavaDocumentSymbolHandler; import org.springframework.ide.vscode.boot.java.handlers.BootJavaHoverProvider; @@ -37,7 +35,6 @@ import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine import org.springframework.ide.vscode.boot.java.handlers.BootJavaReferencesHandler; import org.springframework.ide.vscode.boot.java.handlers.BootJavaWorkspaceSymbolHandler; import org.springframework.ide.vscode.boot.java.handlers.CodeLensProvider; -import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider; import org.springframework.ide.vscode.boot.java.handlers.HighlightProvider; import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider; @@ -56,19 +53,11 @@ import org.springframework.ide.vscode.boot.java.requestmapping.LiveAppURLSymbolP import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingHoverProvider; import org.springframework.ide.vscode.boot.java.requestmapping.WebfluxHandlerCodeLensProvider; import org.springframework.ide.vscode.boot.java.requestmapping.WebfluxRouteHighlightProdivder; -import org.springframework.ide.vscode.boot.java.scope.ScopeCompletionProcessor; -import org.springframework.ide.vscode.boot.java.snippets.JavaSnippet; -import org.springframework.ide.vscode.boot.java.snippets.JavaSnippetContext; -import org.springframework.ide.vscode.boot.java.snippets.JavaSnippetManager; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.java.utils.SpringLiveChangeDetectionWatchdog; -import org.springframework.ide.vscode.boot.java.utils.SymbolCache; -import org.springframework.ide.vscode.boot.java.value.ValueCompletionProcessor; import org.springframework.ide.vscode.boot.java.value.ValueHoverProvider; import org.springframework.ide.vscode.boot.java.value.ValuePropertyReferencesProvider; -import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexProvider; import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider; -import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine; import org.springframework.ide.vscode.commons.languageserver.composable.LanguageServerComponents; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; @@ -76,14 +65,12 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcil import org.springframework.ide.vscode.commons.languageserver.util.CodeLensHandler; import org.springframework.ide.vscode.commons.languageserver.util.DocumentHighlightHandler; import org.springframework.ide.vscode.commons.languageserver.util.HoverHandler; -import org.springframework.ide.vscode.commons.languageserver.util.LspClient; import org.springframework.ide.vscode.commons.languageserver.util.ReferencesHandler; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.languageserver.util.SimpleWorkspaceService; import org.springframework.ide.vscode.commons.util.text.LanguageId; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; /** @@ -124,22 +111,14 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent private SpringProcessTracker liveProcessTracker; public BootJavaLanguageServerComponents( - SimpleLanguageServer server, - BootLanguageServerParams serverParams, - SourceLinks sourceLinks, - CompilationUnitCache cuCache, - ProjectBasedPropertyIndexProvider adHocIndexProvider, - SymbolCache symbolCache, - SpringProcessLiveDataProvider liveDataProvider, - BootJavaConfig config, - SpringSymbolIndex indexer + ApplicationContext appContext ) { - this.server = server; - this.serverParams = serverParams; + this.server = appContext.getBean(SimpleLanguageServer.class); + this.serverParams = appContext.getBean(BootLanguageServerParams.class); projectFinder = serverParams.projectFinder; projectObserver = serverParams.projectObserver; - this.cuCache = cuCache; + this.cuCache = appContext.getBean(CompilationUnitCache.class); propertyIndexProvider = serverParams.indexProvider; @@ -154,29 +133,29 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent // // central live data components (to coordinate live data flow) - liveDataService = new SpringProcessConnectorService(server, liveDataProvider); + liveDataService = appContext.getBean(SpringProcessConnectorService.class); // connect the live data provider with the hovers (for data extraction and live updates) + SpringProcessLiveDataProvider liveDataProvider = appContext.getBean(SpringProcessLiveDataProvider.class); + SourceLinks sourceLinks = appContext.getBean(SourceLinks.class); hoverProvider = createHoverHandler(projectFinder, sourceLinks, liveDataProvider); new SpringProcessLiveHoverUpdater(server, hoverProvider, projectFinder, liveDataProvider); // deal with locally running processes and their connections SpringProcessConnectorLocal liveDataLocalProcessConnector = new SpringProcessConnectorLocal(liveDataService, projectObserver); - // deal with configured remote connections - SpringProcessConnectorRemote liveDataRemoteProcessConnector = new SpringProcessConnectorRemote(server, liveDataService); - // create and handle commands - new SpringProcessCommandHandler(server, liveDataService, liveDataLocalProcessConnector, liveDataRemoteProcessConnector); + 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())); // // // - + SpringSymbolIndex indexer = appContext.getBean(SpringSymbolIndex.class); documents.onDocumentSymbol(new BootJavaDocumentSymbolHandler(this, indexer)); workspaceService.onWorkspaceSymbol(new BootJavaWorkspaceSymbolHandler(indexer, new LiveAppURLSymbolProvider(liveDataProvider))); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/ActuatorConnection.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/ActuatorConnection.java new file mode 100644 index 000000000..ab464a1ea --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/ActuatorConnection.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2022 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.livehover.v2; + +import java.io.IOException; +import java.util.Map; +import java.util.Properties; + +public interface ActuatorConnection { + + String getEnvironment(); + + String getProcessID(); + + Properties getSystemProperties(); + + String getConditionalsReport() throws IOException; + + String getRequestMappings() throws IOException; + + String getBeans() throws IOException; + + String getMetrics(String metric, Map tags) throws IOException; + + Map getStartup() throws IOException; +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/HttpActuatorConnection.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/HttpActuatorConnection.java new file mode 100644 index 000000000..8be5f9129 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/HttpActuatorConnection.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 2022 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.livehover.v2; + +import java.io.IOException; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +public class HttpActuatorConnection implements ActuatorConnection { + + private Gson gson; + private RestTemplate restTemplate; + + public HttpActuatorConnection(String actuatorUrl) { + this.restTemplate = new RestTemplateBuilder().rootUri(actuatorUrl).build(); + this.gson = new Gson(); + } + + @Override + public String getEnvironment() { + return restTemplate.getForObject("/env", String.class); + } + + @Override + public String getProcessID() { + return getSystemProperties().getProperty("PID"); + } + + @Override + public Properties getSystemProperties() { + JsonObject json = gson.fromJson(getEnvironment(), JsonObject.class); + JsonArray propertySources = json.getAsJsonArray("propertySources"); + for (JsonElement jsonElement : propertySources) { + JsonObject obj = jsonElement.getAsJsonObject(); + if ("systemProperties".equals(obj.get("name").getAsString())) { + JsonElement props = obj.get("properties"); + Properties p = new Properties(); + for (Entry entry : props.getAsJsonObject().entrySet()) { + p.put(entry.getKey(), entry.getValue().getAsJsonObject().get("value").getAsString()); + } + return p; + } + } + return null; + } + + @Override + public String getConditionalsReport() throws IOException { + return restTemplate.getForObject("/conditions", String.class); + } + + @Override + public String getRequestMappings() throws IOException { + return restTemplate.getForObject("/mappings", String.class); + } + + @Override + public String getBeans() throws IOException { + return restTemplate.getForObject("/beans", String.class); + } + + @Override + public String getMetrics(String metric, Map tags) throws IOException { + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromPath("/metrics/http.server.requests"); + if (tags != null) { + for (Entry e : tags.entrySet()) { + uriBuilder.queryParam("tag", e.getKey() + ":" + e.getValue()); + } + } + String url = uriBuilder.encode().toUriString(); + return restTemplate.getForObject(url, String.class); + } + + @Override + public Map getStartup() throws IOException { + return null; + } + +} 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 d91d00847..76b60bbf3 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, 2020 Pivotal, Inc. + * Copyright (c) 2019, 2022 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.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -40,13 +41,13 @@ public class SpringProcessCommandHandler { private final SpringProcessConnectorService connectorService; private final SpringProcessConnectorLocal localProcessConnector; - private final SpringProcessConnectorRemote remoteProcessConnector; + private final Collection remoteProcessConnectors; public SpringProcessCommandHandler(SimpleLanguageServer server, SpringProcessConnectorService connectorService, - SpringProcessConnectorLocal localProcessConnector, SpringProcessConnectorRemote remoteProcessConnector) { + SpringProcessConnectorLocal localProcessConnector, Collection remoteProcessConnectors) { this.connectorService = connectorService; this.localProcessConnector = localProcessConnector; - this.remoteProcessConnector = remoteProcessConnector; + this.remoteProcessConnectors = remoteProcessConnectors; server.onCommand(COMMAND_LIST_PROCESSES, (params) -> { return getProcessCommands(); @@ -85,12 +86,14 @@ public class SpringProcessCommandHandler { } // try remote processes - RemoteBootAppData[] remoteProcesses = remoteProcessConnector.getProcesses(); - for (RemoteBootAppData remoteProcess : remoteProcesses) { - String key = SpringProcessConnectorRemote.getProcessKey(remoteProcess); - if (processKey.equals(key)) { - remoteProcessConnector.connectProcess(remoteProcess); - return CompletableFuture.completedFuture(null); + for (SpringProcessConnectorRemote remoteProcessConnector : remoteProcessConnectors) { + RemoteBootAppData[] remoteProcesses = remoteProcessConnector.getProcesses(); + for (RemoteBootAppData remoteProcess : remoteProcesses) { + String key = SpringProcessConnectorRemote.getProcessKey(remoteProcess); + if (processKey.equals(key)) { + remoteProcessConnector.connectProcess(remoteProcess); + return CompletableFuture.completedFuture(null); + } } } } @@ -146,14 +149,17 @@ public class SpringProcessCommandHandler { } // other available remote processes - 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 (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())); + } } } + 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/SpringProcessConnectorOverHttp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverHttp.java new file mode 100644 index 000000000..644a92b4a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverHttp.java @@ -0,0 +1,98 @@ +package org.springframework.ide.vscode.boot.java.livehover.v2; + +public class SpringProcessConnectorOverHttp implements SpringProcessConnector { + + private final String processKey; + private final String actuatorUrl; + private final String urlScheme; + private final String port; + private final String projectName; + + // not final, might be updated with data from JMX process, if not initially set + private String processID; + private String processName; + private String host; + + private HttpActuatorConnection actuatorConnection; + + + public SpringProcessConnectorOverHttp(String processKey, String actuatorUrl, + String urlScheme, String processID, String processName, String projectName, String host, String port) { + + this.processKey = processKey; + + this.actuatorUrl = actuatorUrl; + this.urlScheme = urlScheme; + this.processID = processID; + this.processName = processName; + this.projectName = projectName; + this.host = host; + this.port = port; + } + + @Override + public String getProcessKey() { + return processKey; + } + + @Override + public void connect() throws Exception { + actuatorConnection = new HttpActuatorConnection(actuatorUrl); + } + + @Override + public SpringProcessLiveData refresh(SpringProcessLiveData currentData) throws Exception { + if (actuatorConnection != null) { + SpringProcessLiveData liveData = new SpringProcessLiveDataExtractorOverHttp().retrieveLiveData(actuatorConnection, processID, processName, urlScheme, host, null, port, currentData); + + if (this.processID == null) { + this.processID = liveData.getProcessID(); + } + + if (this.processName == null) { + this.processName = liveData.getProcessName(); + } + + if (liveData != null && liveData.getBeans() != null && !liveData.getBeans().isEmpty()) { + return liveData; + } + } + + throw new Exception("no live data received, lets try again"); + } + + @Override + public void disconnect() throws Exception { + actuatorConnection = null; + } + + @Override + public void addConnectorChangeListener(SpringProcessConnectionChangeListener listener) { + // Useless in the case of Http connection + } + + @Override + public void removeConnectorChangeListener(SpringProcessConnectionChangeListener listener) { + // Useless in the case of Http connection + } + + @Override + public String getProjectName() { + return projectName; + } + + @Override + public String getProcessId() { + return processID; + } + + @Override + public String getProcessName() { + return processName; + } + + @Override + public String toString() { + return "SpringProcessConnectorOverHttp [actuatorURL=" + actuatorUrl + "]"; + } +} 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 f567b4925..372d84140 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, 2020 Pivotal, Inc. + * Copyright (c) 2019, 2022 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 @@ -20,7 +20,6 @@ import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ide.vscode.commons.languageserver.util.Settings; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.util.StringUtils; @@ -169,28 +168,26 @@ public class SpringProcessConnectorRemote { private static Logger logger = LoggerFactory.getLogger(SpringProcessConnectorRemote.class); /** - * We keep the remote app instances in a Map indexed by the json daya. This allows us to + * We keep the remote app instances in a Map indexed by the json data. This allows us to * return the same instance(s) repeatedly as long as the data does not change. */ private final Map remoteAppInstances; + private final SpringProcessConnectorService processConnectorService; public SpringProcessConnectorRemote(SimpleLanguageServer server, SpringProcessConnectorService processConnector) { this.processConnectorService = processConnector; this.remoteAppInstances = new HashMap<>(); - - server.getWorkspaceService().onDidChangeConfiguraton(this::handleSettings); } - - private synchronized void handleSettings(Settings settings) { + + /** + * Replaces existing remote app data map with the new remote app data. Empty array of appData will result in no remote apps. + * @param appData new remote app data + */ + final public synchronized void updateApps(RemoteBootAppData[] appData) { logger.info("updating settings for remote processses to track - start"); - RemoteBootAppData[] appData = settings.getAs(RemoteBootAppData[].class, "boot-java", "remote-apps"); - if (appData == null) { - //Avoid NPE - appData = new RemoteBootAppData[0]; - } - + // remove outdated remote apps Set newAppData = new HashSet<>(Arrays.asList(appData)); @@ -221,7 +218,7 @@ public class SpringProcessConnectorRemote { logger.info("updating settings for remote processses to track - done"); } - + public static String getProcessName(RemoteBootAppData appData) { if (StringUtils.hasText(appData.getProcessName())) { return appData.getProcessName(); @@ -247,8 +244,13 @@ public class SpringProcessConnectorRemote { String urlScheme = remoteProcess.getUrlScheme(); // boolean keepChecking = _appData.isKeepChecking(); - SpringProcessConnectorOverJMX connector = new SpringProcessConnectorOverJMX(processKey, jmxURL, urlScheme, processID, processName, null, host, port); - processConnectorService.connectProcess(processKey, connector); + if (jmxURL.startsWith("http")) { + SpringProcessConnectorOverHttp connector = new SpringProcessConnectorOverHttp(processKey, jmxURL, urlScheme, processID, processName, urlScheme, host, port); + processConnectorService.connectProcess(processKey, connector); + } else { + SpringProcessConnectorOverJMX connector = new SpringProcessConnectorOverJMX(processKey, jmxURL, urlScheme, processID, processName, null, host, port); + processConnectorService.connectProcess(processKey, connector); + } } public RemoteBootAppData[] getProcesses() { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverHttp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverHttp.java new file mode 100644 index 000000000..1936e1817 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverHttp.java @@ -0,0 +1,272 @@ +/******************************************************************************* + * Copyright (c) 2022 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.livehover.v2; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableList; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public class SpringProcessLiveDataExtractorOverHttp { + + private static final Logger log = LoggerFactory.getLogger(SpringProcessLiveDataExtractorOverJMX.class); + + // NOTE: Gson-based serialisation replaces the old Jackson ObjectMapper. Not sure if this makes a difference in the long run, but to retain the same output that Jackson Object Mapper + // was generating during serialisation, some configuration in Gson is required, as the default behaviour of Gson is different than Object Mapper. + // Namely: Object Mapper does not escape Html, whereas Gson does by default (for example + // '=' in Gson appears as '\u003d') + private final Gson gson = new GsonBuilder() + .disableHtmlEscaping() + .create(); + + /** + * @param processID if null, will be determined searching existing mbeans for that information (for remote processes via platform beans runtime name) + * @param processName if null, will be determined searching existing mbeans for that information (for remote processes infering the java command from the system properties) + * @param urlScheme should always be != null + * @param host should always be != null + * @param contextPath if null, will be determined searching existing mbeans for that information (for local processes) + * @param port if null, will be determined searching existing mbeans for that information (for local processes) + * @param currentData currently stored live data + */ + public SpringProcessLiveData retrieveLiveData(ActuatorConnection connection, String processID, String processName, + String urlScheme, String host, String contextPath, String port, SpringProcessLiveData currentData) { + + try { + + String environment = connection.getEnvironment(); + String[] activeProfiles = getActiveProfiles(environment); + + LiveProperties properties = getProperties(environment); + + if (processID == null) { + processID = connection.getProcessID(); + } + + if (processName == null) { + Properties systemProperties = connection.getSystemProperties(); + if (systemProperties != null) { + String javaCommand = getJavaCommand(systemProperties); + processName = getProcessName(javaCommand); + } + } + + LiveConditional[] conditionals = getConditionals(connection, processID, processName); + LiveRequestMapping[] requestMappings = getRequestMappings(connection); + LiveBeansModel beans = getBeans(connection); + LiveMetricsModel metrics = getMetrics(connection); + StartupMetricsModel startup = getStartupMetrics(connection, currentData == null ? null : currentData.getStartupMetrics()); + + if (contextPath == null) { + contextPath = getContextPath(environment); + } + +// if (port == null) { +// port = getPort(connection, environment); +// } + + return new SpringProcessLiveData( + processName, + processID, + contextPath, + urlScheme, + port, + host, + beans, + activeProfiles, + requestMappings, + conditionals, + properties, + metrics, + startup); + } + catch (Exception e) { + log.error("error reading live data from: " + processID + " - " + processName, e); + } + + return null; + } + + private LiveMetricsModel getMetrics(ActuatorConnection connection) { + + return new LiveMetricsModel() { + + @Override + public RequestMappingMetrics getRequestMappingMetrics(String[] paths, String[] requestMethods) { + try { + if (paths.length == 0) { + return null; + } + Map tags = new HashMap<>(); + tags.put("uri", String.join(",", paths)); + if (requestMethods.length > 0) { + tags.put("method", String.join(",", requestMethods)); + } + + String metricsData = connection.getMetrics("http.server.requests", tags); + + return RequestMappingMetrics.parse(metricsData); + } catch (IOException e) { + // ignore + } catch (Exception e) { + log.error("", e); + } + return null; + } + + }; + } + + private StartupMetricsModel getStartupMetrics(ActuatorConnection connection, StartupMetricsModel currentStartup) { + if (currentStartup != null) { + return currentStartup; + } + try { + Map r = connection.getStartup(); + if (r != null) { + return StartupMetricsModel.parse(r); + } + } catch (IOException e) { + // ignore + } catch (Exception e) { + log.error("", e); + } + return null; + } + + public String getProcessName(String command) throws Exception { + if (command != null) { + int space = command.indexOf(' '); + if (space >= 0) { + command = command.substring(0, space); + } + command = command.trim(); + if (!"".equals(command)) { + return command; + } + } + return "Unknown"; + } + + public String getJavaCommand(Properties systemProperties) { + return (String) systemProperties.get("sun.java.command"); + } + + public LiveBeansModel getBeans(ActuatorConnection connection) { + try { + String json = connection.getBeans(); + + if (json instanceof String) { + return LiveBeansModel.parse((String) json); + } else { + return LiveBeansModel.parse(gson.toJson(json)); + } + } catch (IOException e) { + // ignore + } catch (Exception e) { + log.error("Error parsing beans", e); + } + return LiveBeansModel.builder().build(); + } + + public LiveRequestMapping[] getRequestMappings(ActuatorConnection connection) throws Exception { + try { + String mappings = connection.getRequestMappings(); + return parseRequestMappingsJson(mappings, "2.x"); + } catch (IOException e) { + //ignore.. app stopped + } + return null; + } + + private LiveRequestMapping[] parseRequestMappingsJson(String json, String bootVersion) { + JSONObject obj = new JSONObject(json); + if (bootVersion.equals("2.x")) { + return LiveRequestMappingBoot2xParser.parse(obj); + } else { //1.x + List result = new ArrayList<>(); + Iterator keys = obj.keys(); + while (keys.hasNext()) { + String rawKey = keys.next(); + JSONObject value = obj.getJSONObject(rawKey); + result.add(new LiveRequestMappingBoot1xRequestMapping(rawKey, value)); + } + return (LiveRequestMapping[]) result.toArray(new LiveRequestMapping[result.size()]); + } + } + + public LiveConditional[] getConditionals(ActuatorConnection connection, String processId, String processName) { + try { + String report = connection.getConditionalsReport(); + return LiveConditionalParser.parse(report, processId, processName); + } catch (IOException e) { + //ignore. Happens a lot when apps are stopped while we try to talk to them. + } + return null; + } + + public String[] getActiveProfiles(String environment) { + try { + if (environment != null) { + JSONObject env = new JSONObject(environment); + + Object _profiles = env.opt("activeProfiles"); //Boot 2.0 + if (_profiles == null) { + _profiles = env.opt("profiles"); //Boot 1.5 + } + + if (_profiles instanceof JSONArray) { + JSONArray profiles = (JSONArray) _profiles; + ImmutableList.Builder list = ImmutableList.builder(); + for (Object object : profiles) { + if (object instanceof String) { + list.add((String) object); + } + } + return list.build().toArray(new String[0]); + } + } + } catch (Exception e) { + log.error("error resolving profiles from env", e); + } + return null; + } + + public LiveProperties getProperties(String environment) throws Exception { + + try { + if (environment != null) { + return LivePropertiesJsonParser.parseProperties(environment); + } + } catch (Exception e) { + log.error("error resolving live properties from environment endpoint", e); + } + return null; + } + + + + public String getContextPath(String environment) throws Exception { + return environment != null ? LiveContextPathUtil.getContextPath("2.x", environment) : null; + } + +}