diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/LanguageServerRunner.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/LanguageServerRunner.java index d0b668018..38c3bc6be 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/LanguageServerRunner.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/LanguageServerRunner.java @@ -26,7 +26,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.function.Function; -import org.apache.commons.lang3.StringUtils; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.jsonrpc.MessageConsumer; import org.eclipse.lsp4j.services.LanguageClient; @@ -49,6 +48,25 @@ import org.springframework.ide.vscode.commons.protocol.STS4LanguageClient; public class LanguageServerRunner implements CommandLineRunner { final static Logger log = LoggerFactory.getLogger(LanguageServerRunner.class); + + final public static Function DEFAULT_MESSAGE_CONSUMER = (MessageConsumer consumer) -> { + return (msg) -> { + try { +// long beforeConsumingMessage = System.currentTimeMillis(); + + consumer.consume(msg); + +// long afterConsumingMessage = System.currentTimeMillis(); +// String shortMessage = StringUtils.left(msg.toString(), 140); +// log.info("working on message took " + (afterConsumingMessage - beforeConsumingMessage) + "ms - message content: " + shortMessage); + + } catch (UnsupportedOperationException e) { + //log a warning and ignore. We are getting some messages from vsCode the server doesn't know about + log.warn("Unsupported message was ignored!", e); + } + }; + }; + @Override public void run(String... args) throws Exception { @@ -78,10 +96,13 @@ public class LanguageServerRunner implements CommandLineRunner { private LanguageServerProperties properties; private final SimpleLanguageServer languageServer; - public LanguageServerRunner(LanguageServerProperties properties, SimpleLanguageServer languageServer) { + private Function messageConsumer; + + public LanguageServerRunner(LanguageServerProperties properties, SimpleLanguageServer languageServer, Function messageConsumer) { super(); this.properties = properties; this.languageServer = languageServer; + this.messageConsumer = messageConsumer; } public void start() throws Exception { @@ -162,31 +183,8 @@ public class LanguageServerRunner implements CommandLineRunner { int serverPort = properties.getStandalonePort(); log.info("Starting LS as standlone server port = {}", serverPort); -// Function wrapper = consumer -> { -// MessageConsumer result = consumer; -// return result; -// }; - - Function wrapper = (MessageConsumer consumer) -> { - return (msg) -> { - try { -// long beforeConsumingMessage = System.currentTimeMillis(); - - consumer.consume(msg); - -// long afterConsumingMessage = System.currentTimeMillis(); -// String shortMessage = StringUtils.left(msg.toString(), 140); -// log.info("working on message took " + (afterConsumingMessage - beforeConsumingMessage) + "ms - message content: " + shortMessage); - - } catch (UnsupportedOperationException e) { - //log a warning and ignore. We are getting some messages from vsCode the server doesn't know about - log.warn("Unsupported message was ignored!", e); - } - }; - }; - Launcher launcher = createSocketLauncher(languageServer, STS4LanguageClient.class, - new InetSocketAddress("localhost", serverPort), createServerThreads(), wrapper); + new InetSocketAddress("localhost", serverPort), createServerThreads(), messageConsumer); languageServer.connect(launcher.getRemoteProxy()); launcher.startListening().get(); @@ -237,29 +235,12 @@ public class LanguageServerRunner implements CommandLineRunner { private Future runAsync(Connection connection) throws Exception { LanguageServer server = this.languageServer; ExecutorService executor = createServerThreads(); - Function wrapper = (MessageConsumer consumer) -> { - return (msg) -> { - try { -// long beforeConsumingMessage = System.currentTimeMillis(); - - consumer.consume(msg); - -// long afterConsumingMessage = System.currentTimeMillis(); -// String shortMessage = StringUtils.left(msg.toString(), 140); -// log.info("working on message took " + (afterConsumingMessage - beforeConsumingMessage) + "ms - message content: " + shortMessage); - - } catch (UnsupportedOperationException e) { - //log a warning and ignore. We are getting some messages from vsCode the server doesn't know about - log.warn("Unsupported message was ignored!", e); - } - }; - }; Launcher launcher = Launcher.createLauncher(server, STS4LanguageClient.class, connection.in, connection.out, executor, - wrapper + messageConsumer ); if (server instanceof LanguageClientAware) { diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ParentProcessWatcher.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ParentProcessWatcher.java new file mode 100644 index 000000000..f3505d2d9 --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ParentProcessWatcher.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * 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.commons.languageserver.util; + +import java.io.IOException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import org.eclipse.lsp4j.jsonrpc.MessageConsumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.io.Closeables; + +/** + * Watches the parent process PID and invokes exit if it is no longer available. + * This implementation waits for periods of inactivity to start querying the PIDs. + * Copied from JDT LS: + * https://github.com/eclipse/eclipse.jdt.ls/blob/64b15c5a9e5b11f62ceb5163ceb6930d5dea7129/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/ParentProcessWatcher.java + */ +public final class ParentProcessWatcher implements Runnable, Function { + + private static Logger logger = LoggerFactory.getLogger(ParentProcessWatcher.class); + + private static final long INACTIVITY_DELAY = 30_000; + private static final boolean isJava1x = System.getProperty("java.version").startsWith("1."); + private static final int POLL_DELAY_SECS = 10; + private volatile long lastActivityTime; + private final SimpleLanguageServer server; + private ScheduledFuture task; + private ScheduledExecutorService service; + + public ParentProcessWatcher(SimpleLanguageServer server ) { + this.server = server; + service = Executors.newScheduledThreadPool(1); + task = service.scheduleWithFixedDelay(this, POLL_DELAY_SECS, POLL_DELAY_SECS, TimeUnit.SECONDS); + } + + public void run() { + if (!parentProcessStillRunning()) { + logger.info("Parent process stopped running, forcing server exit"); + task.cancel(true); + server.exit(); + } + } + + /** + * Checks whether the parent process is still running. + * If not, then we assume it has crashed, and we have to terminate the Java Language Server. + * + * @return true if the parent process is still running + */ + private boolean parentProcessStillRunning() { + // Wait until parent process id is available + final Integer pid = server.getParentProcessId(); + if (pid == null || lastActivityTime > (System.currentTimeMillis() - INACTIVITY_DELAY)) { + return true; + } + String command; + if (isWindows()) { + command = "cmd /c \"tasklist /FI \"PID eq " + pid + "\" | findstr " + pid + "\""; + } else { + command = "kill -0 " + pid; + } + Process process = null; + boolean finished = false; + try { + process = Runtime.getRuntime().exec(command); + finished = process.waitFor(POLL_DELAY_SECS, TimeUnit.SECONDS); + if (!finished) { + process.destroy(); + finished = process.waitFor(POLL_DELAY_SECS, TimeUnit.SECONDS); // wait for the process to stop + } + if (isWindows() && finished && process.exitValue() > 1) { + // the tasklist command should return 0 (parent process exists) or 1 (parent process doesn't exist) + logger.info("The tasklist command: '{}' returns {}", command, process.exitValue()); + return true; + } + return !finished || process.exitValue() == 0; + } catch (IOException | InterruptedException e) { + logger.error("", e); + return true; + } finally { + if (process != null) { + if (!finished) { + process.destroyForcibly(); + } + // Terminating or destroying the Process doesn't close the process handle on Windows. + // It is only closed when the Process object is garbage collected (in its finalize() method). + // On Windows, when the Java LS is idle, we need to explicitly request a GC, + // to prevent an accumulation of zombie processes, as finalize() will be called. + if (isWindows()) { + // Java >= 9 doesn't close the handle when the process is garbage collected + // We need to close the opened streams + if (!isJava1x) { + Closeables.closeQuietly(process.getInputStream()); + Closeables.closeQuietly(process.getErrorStream()); + try { + Closeables.close(process.getOutputStream(), false); + } catch (IOException e) { + } + } + System.gc(); + } + } + } + } + + @Override + public MessageConsumer apply(final MessageConsumer consumer) { + //inject our own consumer to refresh the timestamp + return message -> { + lastActivityTime = System.currentTimeMillis(); + try { + consumer.consume(message); + } catch (UnsupportedOperationException e) { + //log a warning and ignore. We are getting some messages from vsCode the server doesn't know about + logger.warn("Unsupported message was ignored!", e); + } + }; + } + + private static boolean isWindows() { + String os = System.getProperty("os.name"); + if (os != null) { + return os.toLowerCase().indexOf("win") >= 0; + } + return false; + } +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java index 37487a7e9..d561bf006 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java @@ -125,6 +125,8 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC private SimpleWorkspaceService workspace; private STS4LanguageClient client; private final LanguageServerProperties props; + + private Integer parentProcessId; private ProgressService progressService = new ProgressService() { @@ -284,6 +286,7 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC @Override public CompletableFuture initialize(InitializeParams params) { log.info("Initializing"); + parentProcessId = params.getProcessId(); clientCapabilities.complete(params.getCapabilities()); // multi-root workspace handling @@ -806,5 +809,9 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC final public boolean hasCompletionSnippetSupport() { return hasCompletionSnippetSupport; } + + final public Integer getParentProcessId() { + return parentProcessId; + } } diff --git a/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerRunnerAutoConf.java b/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerRunnerAutoConf.java index b0033ae61..b9282a765 100644 --- a/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerRunnerAutoConf.java +++ b/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerRunnerAutoConf.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018, 2019 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 @@ -10,23 +10,37 @@ *******************************************************************************/ package org.springframework.ide.vscode.languageserver.starter; +import java.util.function.Function; + +import org.eclipse.lsp4j.jsonrpc.MessageConsumer; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.ide.vscode.commons.languageserver.LanguageServerRunner; import org.springframework.ide.vscode.commons.languageserver.config.LanguageServerProperties; +import org.springframework.ide.vscode.commons.languageserver.util.ParentProcessWatcher; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; @Configuration(proxyBeanMethods = false) public class LanguageServerRunnerAutoConf { - + + @ConditionalOnMissingClass("org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness") + @Bean + Function messageConsumer(SimpleLanguageServer languageServer, LanguageServerProperties properties) { + if (!properties.isStandalone()) { + return new ParentProcessWatcher(languageServer); + } + return LanguageServerRunner.DEFAULT_MESSAGE_CONSUMER; + } + @ConditionalOnMissingClass("org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness") @Bean public LanguageServerRunner serverApp( LanguageServerProperties properties, - SimpleLanguageServer languageServerFactory + SimpleLanguageServer languageServerFactory, + Function messageConsumer ) { - return new LanguageServerRunner(properties, languageServerFactory); + return new LanguageServerRunner(properties, languageServerFactory, messageConsumer); } }