first steps towards detecting changes in live running apps across, at the moment across internal restarts via devtools

This commit is contained in:
Martin Lippert
2018-06-07 10:53:32 +02:00
parent cb645f3a34
commit 2353b43fa3
10 changed files with 577 additions and 12 deletions

View File

@@ -148,12 +148,25 @@ public class SpringBootApp {
}
private boolean isSpringBootAppClasspath() throws IOException {
Properties props = this.vm.getSystemProperties();
String classpath = (String) props.get("java.class.path");
String[] cpElements = getClasspath(classpath);
return contains(cpElements, "spring-boot");
return contains(getClasspath(), "spring-boot");
}
public String[] getClasspath() throws IOException {
Properties props = this.vm.getSystemProperties();
String classpath = (String) props.get("java.class.path");
String[] cpElements = splitClasspath(classpath);
return cpElements;
}
public String getJavaCommand() throws IOException {
Properties props = this.vm.getSystemProperties();
if (props.contains("sun.java.command")) {
return props.getProperty("sun.java.command");
}
else {
return null;
}
}
public boolean containsSystemProperty(Object key) throws IOException {
Properties props = this.vm.getSystemProperties();
@@ -340,7 +353,7 @@ public class SpringBootApp {
return false;
}
protected String[] getClasspath(String classpath) {
protected String[] splitClasspath(String classpath) {
List<String> classpathElements = new ArrayList<>();
if (classpath != null) {
StringTokenizer tokenizer = new StringTokenizer(classpath, File.pathSeparator);

View File

@@ -11,14 +11,19 @@
package org.springframework.ide.vscode.commons.boot.app.cli.livebean;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.StringUtil;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableSet;
/**
* @author Martin Lippert
@@ -210,4 +215,8 @@ public class LiveBeansModel {
return beansViaName.isEmpty(); //Assumes every bean has a name.
}
public Set<String> getBeanNames() {
return beansViaName.keySet();
}
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* Copyright (c) 2017, 2018 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
@@ -28,6 +28,11 @@ public class BootJavaConfig {
return enabled == null || enabled.booleanValue();
}
public boolean isChangeDetectionEnabled() {
Boolean enabled = settings.getBoolean("boot-java", "change-detection", "on");
return enabled == null || enabled.booleanValue();
}
public void handleConfigurationChange(Settings newConfig) {
Log.info("Settings received: "+newConfig);
this.settings = newConfig;

View File

@@ -56,6 +56,7 @@ 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.SpringIndexer;
import org.springframework.ide.vscode.boot.java.utils.SpringLiveChangeDetectionWatchdog;
import org.springframework.ide.vscode.boot.java.utils.SpringLiveHoverWatchdog;
import org.springframework.ide.vscode.boot.java.value.ValueCompletionProcessor;
import org.springframework.ide.vscode.boot.java.value.ValueHoverProvider;
@@ -95,6 +96,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
private final SpringIndexer indexer;
private final SpringPropertyIndexProvider propertyIndexProvider;
private final SpringLiveHoverWatchdog liveHoverWatchdog;
private final SpringLiveChangeDetectionWatchdog liveChangeDetectionWatchdog;
private final ProjectObserver projectObserver;
private final BootJavaConfig config;
private final CompilationUnitCache cuCache;
@@ -169,6 +171,8 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
// }
});
liveChangeDetectionWatchdog = new SpringLiveChangeDetectionWatchdog(this, server, serverParams.projectObserver, serverParams.runningAppProvider, projectFinder, serverParams.watchDogInterval);
codeLensHandler = createCodeLensEngine();
documents.onCodeLens(codeLensHandler);
@@ -177,11 +181,21 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
workspaceService.onDidChangeConfiguraton(settings -> {
config.handleConfigurationChange(settings);
// live hover watchdog
if (config.isBootHintsEnabled()) {
liveHoverWatchdog.enableHighlights();
} else {
liveHoverWatchdog.disableHighlights();
}
// live change detection watchdog
if (config.isChangeDetectionEnabled()) {
liveChangeDetectionWatchdog.enableHighlights();
}
else {
liveChangeDetectionWatchdog.disableHighlights();
}
});
server.onInitialize(this::initialize);
@@ -213,6 +227,8 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
private void initialized() {
this.indexer.serverInitialized();
this.liveChangeDetectionWatchdog.start();
// TODO: due to a missing message from lsp4e this "initialized" is not called in
// the LSP4E case
// if this gets fixed, the code should move here (from "initialize" above)
@@ -223,6 +239,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
private void shutdown() {
this.liveHoverWatchdog.shutdown();
this.liveChangeDetectionWatchdog.shutdown();
this.indexer.shutdown();
this.cuCache.dispose();
}

View File

@@ -0,0 +1,53 @@
/*******************************************************************************
* Copyright (c) 2018 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
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.utils;
import java.util.ArrayList;
import java.util.List;
import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean;
/**
* @author Martin Lippert
*/
public class Change {
private List<LiveBean> newBeans;
private List<LiveBean> deletedBeans;
public Change() {
}
public List<LiveBean> getNewBeans() {
return newBeans;
}
public List<LiveBean> getDeletedBeans() {
return deletedBeans;
}
public void addDeletedBean(LiveBean bean) {
if (deletedBeans == null) {
deletedBeans = new ArrayList<>();
}
deletedBeans.add(bean);
}
public void addNewBean(LiveBean bean) {
if (newBeans == null) {
newBeans = new ArrayList<>();
}
newBeans.add(bean);
}
}

View File

@@ -0,0 +1,51 @@
/*******************************************************************************
* Copyright (c) 2018 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
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.utils;
import java.util.HashMap;
import java.util.Map;
import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp;
/**
* @author Martin Lippert
*/
public class ChangeDetectionHistory {
private Map<String, ChangeHistory> changeHistory;
public ChangeDetectionHistory() {
this.changeHistory = new HashMap<>();
}
public Change checkForChanges(SpringBootApp app) {
String virtualID = getVirtualAppID(app);
if (!changeHistory.containsKey(virtualID)) {
ChangeHistory appHistory = new ChangeHistory(virtualID);
changeHistory.put(virtualID, appHistory);
}
ChangeHistory appHistory = changeHistory.get(virtualID);
appHistory.updateProcess(app);
Change result = appHistory.checkForUpdates();
return result;
}
private String getVirtualAppID(SpringBootApp app) {
// TODO: this needs a lot more work to make this a real VIRTUAL ID which detects the same app
// running across different app restarts, so the process ID is not the best way to do this (just
// a temporary solution)
return app.getProcessID();
}
}

View File

@@ -0,0 +1,121 @@
/*******************************************************************************
* Copyright (c) 2018 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
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.utils;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp;
import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean;
import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel;
/**
* @author Martin Lippert
*/
public class ChangeHistory {
private static final List<LiveBean> EMPTY_BEANS_LIST = new ArrayList<>(0);
private final String virtualAppID;
private SpringBootApp runningProcess;
private LiveBeansModel lastBeans;
public ChangeHistory(String virtualAppID) {
this.virtualAppID = virtualAppID;
}
public void updateProcess(SpringBootApp app) {
this.runningProcess = app;
}
public Change checkForUpdates() {
Change result = null;
LiveBeansModel currentBeans = this.runningProcess.getBeans();
if (lastBeans == null) {
lastBeans = currentBeans;
}
else if (currentBeans != null) {
result = calculateBeansDiff(lastBeans, currentBeans, result);
lastBeans = currentBeans;
}
return result;
}
private Change calculateBeansDiff(LiveBeansModel previous, LiveBeansModel current, Change result) {
if (previous == current) {
return result;
}
Set<String> currentNames = current.getBeanNames();
Set<String> previousNames = previous.getBeanNames();
Set<String> allNames = new HashSet<>(currentNames);
allNames.addAll(previousNames);
for (String name : allNames) {
List<LiveBean> currentBeans = current.getBeansOfName(name);
List<LiveBean> previousBeans = previous.getBeansOfName(name);
result = calculateBeansDiff(previousBeans, currentBeans, result);
}
return result;
}
private Change calculateBeansDiff(List<LiveBean> previousBeans, List<LiveBean> currentBeans, Change result) {
if (currentBeans == null) currentBeans = EMPTY_BEANS_LIST;
if (previousBeans == null) previousBeans = EMPTY_BEANS_LIST;
for (LiveBean bean : previousBeans) {
if (!contains(currentBeans, bean)) {
if (result == null) {
result = new Change();
}
result.addDeletedBean(bean);
}
}
for (LiveBean bean : currentBeans) {
if (!contains(previousBeans, bean)) {
if (result == null) {
result = new Change();
}
result.addNewBean(bean);
}
}
return result;
}
private boolean contains(List<LiveBean> beans, LiveBean bean) {
for (LiveBean beansFromList : beans) {
if (StringUtils.equals(beansFromList.getId(), bean.getId())
&& StringUtils.equals(beansFromList.getType(), bean.getType())
&& StringUtils.equals(beansFromList.getResource(), bean.getResource())) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,297 @@
/*******************************************************************************
* Copyright (c) 2018 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
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.utils;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.Range;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents;
import org.springframework.ide.vscode.boot.java.handlers.RunningAppProvider;
import org.springframework.ide.vscode.boot.java.links.SourceLinks;
import org.springframework.ide.vscode.boot.java.links.VSCodeSourceLinks;
import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp;
import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean;
import org.springframework.ide.vscode.commons.java.IClasspath;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver;
import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver.Listener;
import org.springframework.ide.vscode.commons.languageserver.jdt.ls.Classpath;
import org.springframework.ide.vscode.commons.languageserver.jdt.ls.Classpath.CPE;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
/**
* @author Martin Lippert
*/
public class SpringLiveChangeDetectionWatchdog {
public static final Duration DEFAULT_INTERVAL = Duration.ofMillis(5000);
Logger logger = LoggerFactory.getLogger(SpringLiveChangeDetectionWatchdog.class);
private final long POLLING_INTERVAL_MILLISECONDS;
private final SimpleLanguageServer server;
private final RunningAppProvider runningAppProvider;
private final SourceLinks sourceLinks;
private final ChangeDetectionHistory changeHistory;
private final Set<IJavaProject> observedProjects;
private boolean changeDetectionEnabled = true;
private Timer timer;
public SpringLiveChangeDetectionWatchdog(
BootJavaLanguageServerComponents bootJavaLanguageServerComponents,
SimpleLanguageServer server,
ProjectObserver projectObserver,
RunningAppProvider runningAppProvider,
JavaProjectFinder projectFinder,
Duration pollingInterval
) {
this.observedProjects = new HashSet<>();
this.server = server;
this.runningAppProvider = runningAppProvider;
this.POLLING_INTERVAL_MILLISECONDS = pollingInterval == null ? DEFAULT_INTERVAL.toMillis() : pollingInterval.toMillis();
this.changeHistory = new ChangeDetectionHistory();
this.sourceLinks = new VSCodeSourceLinks(bootJavaLanguageServerComponents);
if (projectObserver != null) {
projectObserver.addListener(new Listener() {
@Override
public void deleted(IJavaProject project) {
observedProjects.remove(project);
}
@Override
public void created(IJavaProject project) {
observedProjects.add(project);
}
@Override
public void changed(IJavaProject project) {
// do nothing
}
});
}
}
public synchronized void start() {
if (changeDetectionEnabled && timer == null) {
logger.debug("Starting SpringLiveChangeDetectionWatchdog");
this.timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
update();
}
};
timer.schedule(task, 0, POLLING_INTERVAL_MILLISECONDS);
}
}
public synchronized void shutdown() {
if (timer != null) {
logger.info("Shutting down SpringLiveChangeDetectionWatchdog");
timer.cancel();
timer = null;
}
}
public void update() {
if (changeDetectionEnabled) {
try {
SpringBootApp[] runningBootApps = runningAppProvider.getAllRunningSpringApps().toArray(new SpringBootApp[0]);
for (SpringBootApp app : runningBootApps) {
updateApp(app);
}
} catch (Exception e) {
logger.error("", e);
}
}
}
private void updateApp(SpringBootApp app) {
Change change = changeHistory.checkForChanges(app);
if (change != null) {
publishDetectedChange(change, app);
}
}
private void publishDetectedChange(Change change, SpringBootApp app) {
Map<String, List<Diagnostic>> diagnostics = new HashMap<>();
IJavaProject[] projects = findProjectsFor(app);
List<LiveBean> deletedBeans = change.getDeletedBeans();
if (deletedBeans != null) {
for (LiveBean liveBean : deletedBeans) {
Diagnostic diag = new Diagnostic();
diag.setSeverity(DiagnosticSeverity.Information);
diag.setRange(new Range(new Position(0, 0), new Position(0, 0)));
diag.setSource("Spring Boot Change Detection Mechanism");
diag.setMessage("bean removed from app: " + liveBean.getId());
String docURI = getDocURI(liveBean, projects);
if (docURI != null) {
List<Diagnostic> diags = diagnostics.computeIfAbsent(docURI, (s) -> new ArrayList<>());
diags.add(diag);
}
else {
logger.info("deleted bean could not be associated with a doc URI: " + liveBean.getId());
}
}
}
List<LiveBean> newBeans = change.getNewBeans();
if (newBeans != null) {
for (LiveBean liveBean : newBeans) {
Diagnostic diag = new Diagnostic();
diag.setSeverity(DiagnosticSeverity.Information);
diag.setRange(new Range(new Position(0, 0), new Position(0, 0)));
diag.setSource("Spring Boot Change Detection Mechanism");
diag.setMessage("new bean detected: " + liveBean.getId());
String docURI = getDocURI(liveBean, projects);
if (docURI != null) {
List<Diagnostic> diags = diagnostics.computeIfAbsent(docURI, (s) -> new ArrayList<>());
diags.add(diag);
}
else {
logger.info("new bean could not be associated with a doc URI: " + liveBean.getId());
}
}
}
for (String docURI : diagnostics.keySet()) {
PublishDiagnosticsParams params = new PublishDiagnosticsParams(docURI, diagnostics.get(docURI));
server.getClient().publishDiagnostics(params);
}
}
private IJavaProject[] findProjectsFor(SpringBootApp app) {
List<IJavaProject> result = new ArrayList<>();
try {
Set<String> runningClasspath = new HashSet<>();
Collections.addAll(runningClasspath, app.getClasspath());
for (IJavaProject project : this.observedProjects) {
IClasspath classpath = project.getClasspath();
Collection<CPE> entries = classpath.getClasspathEntries();
for (CPE cpe : entries) {
if (Classpath.ENTRY_KIND_SOURCE.equals(cpe.getKind())) {
String path = cpe.getOutputFolder();
if (runningClasspath.contains(path)) {
result.add(project);
break;
}
}
}
}
}
catch (Exception e) {
logger.error("find projects failed with: ", e);
}
return (IJavaProject[]) result.toArray(new IJavaProject[result.size()]);
}
private String getDocURI(LiveBean liveBean, IJavaProject[] projects) {
String result = null;
String resource = liveBean.getResource();
Pattern BRACKETS = Pattern.compile("\\[[^\\]]*\\]");
Matcher matcher = BRACKETS.matcher(resource);
if (matcher.find()) {
String type = resource.substring(0, matcher.start()).trim();
String path = resource.substring(matcher.start()+1, matcher.end()-1);
for (IJavaProject project : projects) {
if (SpringResource.FILE.equals(type)) {
String relativePath = SpringResource.projectRelativePath(project, path);
if (relativePath != path && path.endsWith(SourceLinks.CLASS)) {
result = sourceLinks.sourceLinkUrlForClasspathResource(project, relativePath).get();
break;
} else {
result = sourceLinks.sourceLinkForResourcePath(Paths.get(path)).get();
break;
}
}
else if (SpringResource.CLASS_PATH_RESOURCE.equals(type)) {
result = sourceLinks.sourceLinkUrlForClasspathResource(project, path).get();
break;
}
}
}
if (result != null) {
int position = result.lastIndexOf('#');
if (position > 0) {
result = result.substring(0, position);
}
}
return result;
}
public synchronized void enableHighlights() {
if (!changeDetectionEnabled) {
changeDetectionEnabled = true;
refreshEnablement();
}
}
public synchronized void disableHighlights() {
if (changeDetectionEnabled) {
changeDetectionEnabled = false;
refreshEnablement();
}
}
private void refreshEnablement() {
if (changeDetectionEnabled) {
start();
} else {
shutdown();
}
}
}

View File

@@ -37,7 +37,6 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument;
*/
public class SpringLiveHoverWatchdog {
public static final Duration DEFAULT_INTERVAL = Duration.ofMillis(5000);
Logger logger = LoggerFactory.getLogger(SpringLiveHoverWatchdog.class);

View File

@@ -29,8 +29,8 @@ import org.springframework.ide.vscode.commons.util.Renderables;
*/
public class SpringResource {
private static final String FILE = "file";
private static final String CLASS_PATH_RESOURCE = "class path resource";
public static final String FILE = "file";
public static final String CLASS_PATH_RESOURCE = "class path resource";
private SourceLinks sourceLinks;
private String type;
@@ -65,7 +65,7 @@ public class SpringResource {
Optional<String> linkUrl;
switch (type) {
case FILE:
String relativePath = projectRelativePath(path);
String relativePath = projectRelativePath(project, path);
if (relativePath != path && path.endsWith(SourceLinks.CLASS)) {
linkUrl = sourceLinks.sourceLinkUrlForClasspathResource(project, relativePath);
} else {
@@ -73,7 +73,7 @@ public class SpringResource {
}
// not a project relative path
return linkUrl.isPresent() ? Renderables.link(relativePath, linkUrl.get()).toMarkdown()
: "`" + projectRelativePath(path) + "`";
: "`" + projectRelativePath(project, path) + "`";
case CLASS_PATH_RESOURCE:
linkUrl = sourceLinks.sourceLinkUrlForClasspathResource(project, path);
return linkUrl.isPresent() ? Renderables.link(path, linkUrl.get()).toMarkdown() : "`"+path+"`";
@@ -82,7 +82,7 @@ public class SpringResource {
}
}
private String projectRelativePath(String pathStr) {
public static String projectRelativePath(IJavaProject project, String pathStr) {
Path path = Paths.get(pathStr);
IClasspath classpath = project.getClasspath();
Iterable<File> ofs = () -> IClasspathUtil.getOutputFolders(classpath).iterator();