From ae8c9549c0bdc59b06cd4cb002e149b211f67fd3 Mon Sep 17 00:00:00 2001 From: azerr Date: Wed, 22 Jul 2026 10:05:22 +0200 Subject: [PATCH] fix: IDE freezes (stuck in LanguageServerWrapper.lambda$start$6) Fxes #1620 Signed-off-by: azerr --- .../devtools/lsp4ij/LanguageServerWrapper.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java index a13cfcb4f..1ea9df83c 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java @@ -108,6 +108,7 @@ public class LanguageServerWrapper implements Disposable { private final LanguageServerDefinition serverDefinition; private final ExecutorService dispatcher; private final ExecutorService listener; + private final ExecutorService messageWriter; private final SimpleModificationTracker modificationTracker = new SimpleModificationTracker(); /** * Map containing unregistration handlers for dynamic capability registrations. @@ -179,6 +180,16 @@ public LanguageServerWrapper(@NotNull Project project, // Executor service passed through to the LSP4j layer when we attempt to start the LS. It will be used // to create a listener that sits on the input stream and processes inbound messages (responses, or server-initiated // requests). + String messageWriterThreadNameFormat = "LS-" + serverDefinition.getId() + projectName + "#messageWriter"; //$NON-NLS-1$ //$NON-NLS-2$ + this.messageWriter = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() + .setNameFormat(messageWriterThreadNameFormat) + .setThreadFactory(r -> { + var t = new Thread(r); + t.setContextClassLoader(workerCtxClassLoader); + return t; + }) + .build()); + String listenerThreadNameFormat = "LS-" + serverDefinition.getId() + projectName + "#listener-%d"; //$NON-NLS-1$ //$NON-NLS-2$ this.listener = Executors.newCachedThreadPool(new ThreadFactoryBuilder() .setNameFormat(listenerThreadNameFormat) @@ -225,6 +236,7 @@ public Project getProject() { void stopDispatcher() { this.dispatcher.shutdownNow(); + this.messageWriter.shutdownNow(); // Only really needed for testing - the listener (an instance of ConcurrentMessageProcessor) should exit // as soon as the input stream from the LS is closed, and a cached thread pool will recycle idle @@ -239,6 +251,10 @@ void stopDispatcher() { LOGGER.warn("Dispatcher executor for language server '{}' did not terminate within 2s; classloader unload may be blocked", serverDefinition.getId()); } + if (!this.messageWriter.awaitTermination(2, TimeUnit.SECONDS)) { + LOGGER.warn("Message writer executor for language server '{}' did not terminate within 2s; classloader unload may be blocked", + serverDefinition.getId()); + } if (!this.listener.awaitTermination(2, TimeUnit.SECONDS)) { LOGGER.warn("Listener executor for language server '{}' did not terminate within 2s; classloader unload may be blocked", serverDefinition.getId()); @@ -459,7 +475,7 @@ public synchronized void start() throws LanguageServerException { // To avoid having some lock problem when message is written in the stream output // (when there are a lot of messages to write it) // we consume the message in async mode - CompletableFuture.runAsync(() -> consumer.consume(message)) + CompletableFuture.runAsync(() -> consumer.consume(message), messageWriter) .exceptionally(e -> { // Log in the LSP console the error getLanguageServerLifecycleManager().onError(this, e);