Skip to content

Commit

Permalink
Shutdown LS eventually if parent shell process is gone
Browse files Browse the repository at this point in the history
  • Loading branch information
BoykoAlex committed Mar 9, 2022
1 parent 58f4300 commit a588354
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
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;
Expand All @@ -49,6 +48,25 @@
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 {
Expand Down Expand Up @@ -78,10 +96,13 @@ public void run(String... args) throws Exception {
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 {
Expand Down Expand Up @@ -162,31 +183,8 @@ public void startAsServer() throws Exception {
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();
Expand Down Expand Up @@ -237,29 +235,12 @@ private static Connection connectToNode() throws IOException {
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand Down Expand Up @@ -284,6 +286,7 @@ public Mono<QuickfixEdit> quickfixResolve(QuickfixResolveParams params) {
@Override
public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
log.info("Initializing");
parentProcessId = params.getProcessId();
clientCapabilities.complete(params.getCapabilities());

// multi-root workspace handling
Expand Down Expand Up @@ -806,5 +809,9 @@ public boolean hasHierarchicalDocumentSymbolSupport() {
final public boolean hasCompletionSnippetSupport() {
return hasCompletionSnippetSupport;
}

final public Integer getParentProcessId() {
return parentProcessId;
}

}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
}

}

0 comments on commit a588354

Please sign in to comment.