From 5976d36bd92628c4ed11a8597c857f42ac76aef8 Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Tue, 27 Jun 2017 17:00:45 -0700 Subject: [PATCH] Implement a 'fallback' lsp process reaper to cleanup errant processes. --- .../CloudFoundryManifestLanguageServer.java | 23 ++++ .../ls/LanguageServerProcessReaper.java | 100 ++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 eclipse-language-servers/org.springframework.tooling.cloudfoundry.manifest.ls/src/org/springframework/tooling/cloudfoundry/manifest/ls/LanguageServerProcessReaper.java diff --git a/eclipse-language-servers/org.springframework.tooling.cloudfoundry.manifest.ls/src/org/springframework/tooling/cloudfoundry/manifest/ls/CloudFoundryManifestLanguageServer.java b/eclipse-language-servers/org.springframework.tooling.cloudfoundry.manifest.ls/src/org/springframework/tooling/cloudfoundry/manifest/ls/CloudFoundryManifestLanguageServer.java index 7e776553c..7e7bf68d0 100644 --- a/eclipse-language-servers/org.springframework.tooling.cloudfoundry.manifest.ls/src/org/springframework/tooling/cloudfoundry/manifest/ls/CloudFoundryManifestLanguageServer.java +++ b/eclipse-language-servers/org.springframework.tooling.cloudfoundry.manifest.ls/src/org/springframework/tooling/cloudfoundry/manifest/ls/CloudFoundryManifestLanguageServer.java @@ -13,6 +13,7 @@ package org.springframework.tooling.cloudfoundry.manifest.ls; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Field; import java.net.URI; import java.nio.file.Files; import java.nio.file.StandardCopyOption; @@ -38,6 +39,8 @@ import org.osgi.framework.Bundle; @SuppressWarnings("restriction") public class CloudFoundryManifestLanguageServer extends ProcessStreamConnectionProvider { + private static LanguageServerProcessReaper processReaper = new LanguageServerProcessReaper(); + private LanguageServer languageServer; private URI rootPath; @@ -85,6 +88,7 @@ public class CloudFoundryManifestLanguageServer extends ProcessStreamConnectionP public void stop() { removeLanguageServer(this); super.stop(); + processReaper.removeProcess(this.getProcess()); } @Override @@ -108,6 +112,25 @@ public class CloudFoundryManifestLanguageServer extends ProcessStreamConnectionP return null; } + @Override + public void start() throws IOException { + super.start(); + processReaper.addProcess(getProcess()); + } + + private Process getProcess() { + try { + //The super class is doesn't provide a way to get at the process without using reflection... + // This method can be removed if / when the super-class provides a getProcess method we can call. + Field processField = ProcessStreamConnectionProvider.class.getDeclaredField("process"); + processField.setAccessible(true); + Process process = (Process) processField.get(this); + return process; + } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { + return null; + } + } + protected String getLanguageServerJARLocation() { String languageServer = "manifest-yaml-language-server-" + Constants.LANGUAGE_SERVER_VERSION; diff --git a/eclipse-language-servers/org.springframework.tooling.cloudfoundry.manifest.ls/src/org/springframework/tooling/cloudfoundry/manifest/ls/LanguageServerProcessReaper.java b/eclipse-language-servers/org.springframework.tooling.cloudfoundry.manifest.ls/src/org/springframework/tooling/cloudfoundry/manifest/ls/LanguageServerProcessReaper.java new file mode 100644 index 000000000..f50b3d46a --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.cloudfoundry.manifest.ls/src/org/springframework/tooling/cloudfoundry/manifest/ls/LanguageServerProcessReaper.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) 2017 Spring IDE Developers + * 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: + * Spring IDE Developers - initial API and implementation + *******************************************************************************/ +package org.springframework.tooling.cloudfoundry.manifest.ls; + +import java.util.Iterator; +import java.util.LinkedList; + +import org.eclipse.core.runtime.Platform; + +/** + * A 'last resort cleanup' utility for LSP server processes. This class, when instantiated + * registers a JVM shutdown hook. When the hook is executed it kills of all tracked processes + * with the most aggressive process termination method available through the JRE. + *

+ * This is meant only as a kind of 'fail-safe' as it is expected that lps4e does proper + * house-keeping and cleans up these processes automatically and cleanly. So... unless + * something goes wrong inside lsp4e then the `destroyProcesses` method really should + * just be iterating an empty collection of processes. I.e. the typical debug output + * on shutdown should look something like this: + * + *

+ *  LanguageServerProcessReaper: Destroying errant processes... 
+ *  LanguageServerProcessReaper: Number of alive processes = 0
+ *  LanguageServerProcessReaper: Destroying errant processes... DONE
+ * 
+ * + * @author Kris De Volder + */ +public class LanguageServerProcessReaper extends Thread { + + private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder"); + + private static void debug(String string) { + if (DEBUG) { + System.out.println("LanguageServerProcessReaper: "+string); + } + } + + LinkedList processes = new LinkedList<>(); + { + Runtime.getRuntime().addShutdownHook(this); + } + + public synchronized void addProcess(Process process) { + debug("added process: "+process); + if (process!=null) { + processes.add(process); + } + garbageCollect(); + } + + /** + * If processes are managed reasonably by lsp4e then we don't expect the list of + * actually 'alive' processes to keep on growing. This checks for and removes + * processes that are already dead. + */ + protected void garbageCollect() { + Process process; + Iterator iter = processes.iterator(); + while (iter.hasNext()) { + process = iter.next(); + if (!process.isAlive()) { + iter.remove(); + } + } + debug("Number of alive processes = "+processes.size()); + } + + public synchronized void removeProcess(Process process) { + debug("removeProcess "+process); + garbageCollect(); + } + + @Override + public void run() { + destroyProcesses(); + } + + private synchronized void destroyProcesses() { + debug("Destroying errant processes... "); + garbageCollect(); + for (Process process : processes) { + try { + debug("Destroying process "+process); + process.destroyForcibly(); + } catch (Throwable e) { + } + } + debug("Destroying errant processes... DONE"); + } + +}