From 827bdcc7722c7d79fc21e335035a2ea2eb316a23 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Thu, 19 Feb 2026 10:47:04 -0500 Subject: [PATCH] fixes issues with shutting down when vs-code extension process stops --- .../org/opencds/cqf/cql/ls/service/Main.java | 101 ++++++++++++++++-- .../src/main/resources/application.properties | 3 +- 2 files changed, 97 insertions(+), 7 deletions(-) diff --git a/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/Main.java b/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/Main.java index 8ac1921..ba62e76 100644 --- a/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/Main.java +++ b/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/Main.java @@ -2,6 +2,9 @@ import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.Future; import org.cqframework.cql.cql2elm.CqlTranslator; import org.eclipse.lsp4j.jsonrpc.Launcher; @@ -18,7 +21,19 @@ import org.springframework.context.annotation.Import; /** - * This class starts a CqlLanguageServer running as a service listening on std-in/std-out + * Main entry point for the CQL Language Server service. + *

+ * This class starts a CqlLanguageServer running as a service that communicates + * via the Language Server Protocol (LSP) over stdin/stdout. It is designed to be + * launched by LSP clients such as VS Code extensions. + *

+ * Key behaviors: + *

*/ @Import(ServerConfig.class) public class Main implements CommandLineRunner { @@ -39,15 +54,26 @@ public static void main(String[] args) { CqlLanguageServer server; @Override - public void run(String... args) throws Exception { + public void run(String... args) { @SuppressWarnings("java:S106") Launcher launcher = LSPLauncher.createServerLauncher(server, System.in, System.out); LanguageClient client = launcher.getRemoteProxy(); - // We actually connect to the process std-err on the client side for logging. - // We could change that and append to the client's logMessage API from the - // server-side instead. + + // Logging Strategy: + // Currently logs are written to stderr (configured in logback.xml). + // The client can capture stderr and display it separately. + // + // Alternative: Send logs to the client's LSP window/logMessage API + // by uncommenting the line below. This would show logs in VS Code's + // Output panel for the language server channel. + // + // Trade-offs: + // - stderr: Simpler, works with any LSP client, easier to redirect to files + // - client API: Integrated into IDE, automatic log level filtering in UI + // // setupClientAppender(client); + server.connect(client); Future serverThread = launcher.startListening(); @@ -60,15 +86,78 @@ public void run(String... args) throws Exception { log.info("cql-language-server started"); - server.exited().get(); + // Shutdown Strategy: + // Monitor two conditions in parallel to handle both normal and abnormal shutdowns: + // 1. server.exited() - Completes when LSP exit() notification is received (normal shutdown) + // 2. connectionClosed - Completes when stdin/stdout closes (client disconnect/crash) + // + // We wait for whichever happens first. This ensures the server terminates properly + // when VS Code closes, even if the exit() notification never arrives. + + ExecutorService executor = Executors.newSingleThreadExecutor(); + CompletableFuture connectionClosed = CompletableFuture.runAsync( + () -> { + try { + // This blocks until the LSP connection closes (stdin/stdout closed) + serverThread.get(); + log.info("LSP connection closed"); + } catch (Exception e) { + log.debug("Server thread exception", e); + } + }, + executor); + + // Wait for whichever completes first: exit() or connection closure + try { + CompletableFuture.anyOf(server.exited(), connectionClosed).get(); + } catch (Exception e) { + log.error("Error waiting for shutdown", e); + } + + log.info("Shutting down language server"); serverThread.cancel(true); + executor.shutdownNow(); + + // Force JVM termination to ensure all threads (including Spring-managed ones) are stopped. + // Using System.exit(0) is necessary because Spring Boot may have non-daemon threads running. + System.exit(0); } + /** + * Configures SLF4J logging bridge to redirect java.util.logging to SLF4J. + * This ensures all logging from third-party libraries flows through our + * configured Logback appenders (currently configured to write to stderr). + */ public static void configureLogging() { SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); } + /** + * Alternative logging approach: Send logs to the LSP client via window/logMessage. + *

+ * When enabled, this appender sends all log messages to the language client's + * logMessage API, which displays them in the IDE's Output panel for the + * language server channel. + *

+ * Benefits: + *

    + *
  • Logs appear directly in VS Code's Output panel
  • + *
  • IDE provides UI controls for filtering log levels
  • + *
  • No need to configure separate stderr capture
  • + *
+ *

+ * Drawbacks: + *

    + *
  • Logs can't be easily redirected to files
  • + *
  • May clutter the client connection with high-volume logging
  • + *
  • Dependent on client implementation of window/logMessage
  • + *
+ *

+ * To enable: Uncomment the setupClientAppender(client) call on line 63. + * + * @param client The LSP language client to send log messages to + */ private static void setupClientAppender(LanguageClient client) { LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); LanguageClientAppender appender = new LanguageClientAppender(client); diff --git a/ls/service/src/main/resources/application.properties b/ls/service/src/main/resources/application.properties index 6a852bb..6ef1a14 100644 --- a/ls/service/src/main/resources/application.properties +++ b/ls/service/src/main/resources/application.properties @@ -1,3 +1,4 @@ spring.main.banner-mode=off spring.main.log-startup-info=false -spring.profiles.active=default \ No newline at end of file +spring.profiles.active=default +spring.main.web-application-type=none \ No newline at end of file