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:
+ *
+ * - Runs as a Spring Boot CommandLineRunner (non-web application)
+ * - Logs to stderr to keep stdout clear for LSP communication
+ * - Monitors both exit() notifications and connection closure for shutdown
+ * - Terminates the JVM with System.exit(0) when the connection closes
+ *
*/
@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