Shutdown LS eventually if parent shell process is gone
This commit is contained in:
@@ -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<MessageConsumer, MessageConsumer> 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, MessageConsumer> messageConsumer;
|
||||
|
||||
public LanguageServerRunner(LanguageServerProperties properties, SimpleLanguageServer languageServer, Function<MessageConsumer, MessageConsumer> 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<MessageConsumer, MessageConsumer> wrapper = consumer -> {
|
||||
// MessageConsumer result = consumer;
|
||||
// return result;
|
||||
// };
|
||||
|
||||
Function<MessageConsumer, MessageConsumer> 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<STS4LanguageClient> 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<Void> runAsync(Connection connection) throws Exception {
|
||||
LanguageServer server = this.languageServer;
|
||||
ExecutorService executor = createServerThreads();
|
||||
Function<MessageConsumer, MessageConsumer> 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<STS4LanguageClient> launcher = Launcher.createLauncher(server,
|
||||
STS4LanguageClient.class,
|
||||
connection.in,
|
||||
connection.out,
|
||||
executor,
|
||||
wrapper
|
||||
messageConsumer
|
||||
);
|
||||
|
||||
if (server instanceof LanguageClientAware) {
|
||||
|
||||
@@ -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<MessageConsumer, MessageConsumer> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<InitializeResult> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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, MessageConsumer> 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, MessageConsumer> messageConsumer
|
||||
) {
|
||||
return new LanguageServerRunner(properties, languageServerFactory);
|
||||
return new LanguageServerRunner(properties, languageServerFactory, messageConsumer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user