Add/Connect remote process command

This commit is contained in:
BoykoAlex
2022-03-09 13:45:20 -05:00
parent 6578446a74
commit 48fa9b61e5
9 changed files with 614 additions and 69 deletions

View File

@@ -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<String, RemoteBootAppData[]> allRemoteApps = new HashMap<>();
final Gson gson = new Gson();
server.onCommand("sts/livedata/remoteConnect", params -> {
List<Object> 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<RemoteBootAppData> 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);

View File

@@ -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<ICompletionEngine> 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));

View File

@@ -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)));

View File

@@ -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<String, String> tags) throws IOException;
Map<?, ?> getStartup() throws IOException;
}

View File

@@ -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<String, JsonElement> 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<String, String> tags) throws IOException {
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromPath("/metrics/http.server.requests");
if (tags != null) {
for (Entry<String, String> 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;
}
}

View File

@@ -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<SpringProcessConnectorRemote> remoteProcessConnectors;
public SpringProcessCommandHandler(SimpleLanguageServer server, SpringProcessConnectorService connectorService,
SpringProcessConnectorLocal localProcessConnector, SpringProcessConnectorRemote remoteProcessConnector) {
SpringProcessConnectorLocal localProcessConnector, Collection<SpringProcessConnectorRemote> 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()]));
}

View File

@@ -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 + "]";
}
}

View File

@@ -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<RemoteBootAppData, String> 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<RemoteBootAppData> 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() {

View File

@@ -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<String, String> 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<LiveRequestMapping> result = new ArrayList<>();
Iterator<String> 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<String> 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;
}
}