Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
import java.net.PasswordAuthentication;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Function;

import org.eclipse.lemminx.commons.ParentProcessWatcher;
import org.eclipse.lemminx.customservice.XMLLanguageClientAPI;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
import org.eclipse.lsp4j.launch.LSPLauncher.Builder;
import org.eclipse.lsp4j.launcher.multiclient.MultiClientLauncher;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageServer;

Expand All @@ -38,6 +38,31 @@ public class XMLServerLauncher {
*/
public static void main(String[] args) {

// Parse arguments for MCP mode
boolean mcpEnabled = false;
String workspacePath = null;
int socketPort = 0; // 0 = free port (OS chooses)
String clientName = null;
String clientVersion = null;

for (int i = 0; i < args.length; i++) {
if ("--mcp-enabled".equals(args[i])) {
mcpEnabled = true;
} else if ("--workspace".equals(args[i]) && i + 1 < args.length) {
workspacePath = args[i + 1];
i++;
} else if ("--port".equals(args[i]) && i + 1 < args.length) {
socketPort = Integer.parseInt(args[i + 1]);
i++;
} else if ("--client-name".equals(args[i]) && i + 1 < args.length) {
clientName = args[i + 1];
i++;
} else if ("--client-version".equals(args[i]) && i + 1 < args.length) {
clientVersion = args[i + 1];
i++;
}
}

final String HTTP_PROXY_HOST = System.getenv("HTTP_PROXY_HOST");
final String HTTP_PROXY_PORT = System.getenv("HTTP_PROXY_PORT");
final String HTTP_PROXY_USERNAME = System.getenv("HTTP_PROXY_USERNAME");
Expand Down Expand Up @@ -76,7 +101,14 @@ protected PasswordAuthentication getPasswordAuthentication() {
if (!LEMMINX_DEBUG) {
System.setErr(new NoOpPrintStream());
}
launch(in, out);

// If MCP enabled, launch with shared server mode
if (mcpEnabled && workspacePath != null) {
launchWithMCP(in, out, workspacePath, socketPort, clientName, clientVersion);
} else {
// Normal stdio-only mode
launch(in, out);
}
}

/**
Expand All @@ -88,17 +120,49 @@ protected PasswordAuthentication getPasswordAuthentication() {
* (I'm not 100% sure how it meant to be used though, as
* it's undocumented...)
*/
public static Future<?> launch(InputStream in, OutputStream out) {
public static void launch(InputStream in, OutputStream out) {
XMLLanguageServer server = new XMLLanguageServer();
Function<MessageConsumer, MessageConsumer> wrapper;
if ("false".equals(System.getProperty("watchParentProcess"))) {
wrapper = it -> it;
} else {
wrapper = new ParentProcessWatcher(server);
}
Launcher<LanguageClient> launcher = createServerLauncher(server, in, out, Executors.newCachedThreadPool(), wrapper);
Launcher<LanguageClient> launcher = createServerLauncher(server, in, out, Executors.newCachedThreadPool(),
wrapper);
server.setClient(launcher.getRemoteProxy());
launcher.startListening();
}

/**
* Launch with MCP mode enabled - stdio + socket for shared server.
*/
private static void launchWithMCP(InputStream in, OutputStream out, String workspacePath, int socketPort,
String clientName, String clientVersion) {
XMLLanguageServer server = new XMLLanguageServer();
Function<MessageConsumer, MessageConsumer> wrapper;
if ("false".equals(System.getProperty("watchParentProcess"))) {
wrapper = it -> it;
} else {
wrapper = new ParentProcessWatcher(server);
}
MultiClientLauncher.Builder<LanguageClient> builder = new MultiClientLauncher.Builder<LanguageClient>()
.setLocalService(server) //
.setRemoteInterface(XMLLanguageClientAPI.class) //
.setInput(in) //
.setOutput(out) //
.setExecutorService(Executors.newCachedThreadPool()) //
.wrapMessages(wrapper) //
.enableSocket(socketPort, workspacePath, "lemminx");

// Add client info if provided
if (clientName != null || clientVersion != null) {
builder.setClientInfo(clientName, clientVersion);
}

Launcher<LanguageClient> launcher = builder.create();
server.setClient(launcher.getRemoteProxy());
return launcher.startListening();
launcher.startListening();
}

/**
Expand All @@ -115,15 +179,14 @@ public static Future<?> launch(InputStream in, OutputStream out) {
* @param wrapper - a function for plugging in additional message
* consumers
*/
private static Launcher<LanguageClient> createServerLauncher(LanguageServer server, InputStream in, OutputStream out,
ExecutorService executorService, Function<MessageConsumer, MessageConsumer> wrapper) {
return new Builder<LanguageClient>().
setLocalService(server)
private static Launcher<LanguageClient> createServerLauncher(LanguageServer server, InputStream in,
OutputStream out, ExecutorService executorService, Function<MessageConsumer, MessageConsumer> wrapper) {
return new Builder<LanguageClient>().setLocalService(server) //
.setRemoteInterface(XMLLanguageClientAPI.class) // Set client as XML language client
.setInput(in)
.setOutput(out)
.setExecutorService(executorService)
.wrapMessages(wrapper)
.setInput(in) //
.setOutput(out) //
.setExecutorService(executorService) //
.wrapMessages(wrapper) //
.create();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,66 +11,43 @@
*/
package org.eclipse.lemminx;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.Channels;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.eclipse.lemminx.customservice.XMLLanguageClientAPI;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.launcher.multiclient.LspSocketLauncher;
import org.eclipse.lsp4j.launcher.multiclient.MultiClientProxy;

/**
* Launches {@link XMLLanguageServer} using asynchronous server-socket channel with multi-client support.
* Multiple clients can connect to the same server instance via TCP socket.
*/
public class XMLServerSocketLauncher extends LspSocketLauncher<XMLLanguageServer, XMLLanguageClientAPI> {

public class XMLServerSocketLauncher {
public XMLServerSocketLauncher() {
super("lemminx");
}

private static final int DEFAULT_PORT = 5_008;

/**
* Calls {@link #launch(String[])}
* Calls {@link #launch(String[], java.util.function.Function, Class)}
*/
public static void main(String[] args) throws Exception {
new XMLServerSocketLauncher().launch(args);
XMLServerSocketLauncher launcher = new XMLServerSocketLauncher();
launcher.launch(args, client -> {
XMLLanguageServer server = new XMLLanguageServer();
server.setClient(client);
return server;
}, XMLLanguageClientAPI.class);
}

/**
* Launches {@link XMLLanguageServer} using asynchronous server-socket channel and makes it accessible through the JSON
* RPC protocol defined by the LSP.
*
* @param args standard launch arguments. may contain <code>--port</code> argument to change the default port 5008
* Launch socket listener for MCP clients.
* Shares the same XMLLanguageServer instance with the stdio client.
*
* @param sharedServer the shared XMLLanguageServer instance
* @param port the port to listen on
* @param workspacePath the workspace path
* @param clientProxy the multi-client proxy to add socket clients to
*/
public void launch(String[] args) throws Exception {
AsynchronousServerSocketChannel _open = AsynchronousServerSocketChannel.open();
int _port = getPort(args);
InetSocketAddress _inetSocketAddress = new InetSocketAddress("0.0.0.0", _port);
final AsynchronousServerSocketChannel serverSocket = _open.bind(_inetSocketAddress);
while (true) {
final AsynchronousSocketChannel socketChannel = serverSocket.accept().get();
final InputStream in = Channels.newInputStream(socketChannel);
final OutputStream out = Channels.newOutputStream(socketChannel);
final ExecutorService executorService = Executors.newCachedThreadPool();
XMLLanguageServer languageServer = new XMLLanguageServer();
final Launcher<XMLLanguageClientAPI> launcher = Launcher.createIoLauncher(languageServer, XMLLanguageClientAPI.class,
in, out, executorService, (MessageConsumer it) -> {
return it;
});
languageServer.setClient(launcher.getRemoteProxy());
launcher.startListening();
}
}

protected int getPort(final String... args) {
for (int i = 0; (i < (args.length - 1)); i++) {
String _get = args[i];
boolean _equals = Objects.equals(_get, "--port");
if (_equals) {
return Integer.parseInt(args[(i + 1)]);
}
}
return DEFAULT_PORT;
public void launchForSharedServer(XMLLanguageServer sharedServer, int port, String workspacePath,
MultiClientProxy<XMLLanguageClientAPI> clientProxy) throws Exception {
super.launchForSharedServer(sharedServer, port, workspacePath, clientProxy, XMLLanguageClientAPI.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Copyright (c) 2024 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.lsp4j.launcher.multiclient;

import java.nio.file.Path;
import java.nio.file.Paths;

/**
* Holds pre-computed file paths for LSP server instance files.
* Calculates workspace-specific paths once in the constructor to avoid repeated string concatenation.
* Uses ${workspace}/.lsp-servers/{serverType}.json (e.g., .lsp-servers/lemminx.json)
*/
public class InstancePaths {

private static final String INSTANCES_DIR_NAME = ".lsp-servers";

private final String serverType;
private final String workspacePath;

// Pre-computed paths
private final String instanceFileName;
private final Path instanceDir;
private final Path instanceFilePath;

private InstancePaths(String serverType, String workspacePath) {
this.serverType = serverType;
this.workspacePath = workspacePath;

// Compute instance file name and path: ${workspace}/.lsp-servers/{serverType}.json
this.instanceFileName = serverType + ".json";
if (workspacePath != null) {
this.instanceDir = Paths.get(workspacePath, INSTANCES_DIR_NAME);
this.instanceFilePath = instanceDir.resolve(instanceFileName);
} else {
this.instanceDir = null;
this.instanceFilePath = null;
}
}

/**
* Creates a builder for instance paths.
*/
public static Builder builder() {
return new Builder();
}

/**
* Builder for InstancePaths.
*/
public static class Builder {
private String serverType;
private String workspacePath;

public Builder serverType(String serverType) {
this.serverType = serverType;
return this;
}

public Builder workspacePath(String workspacePath) {
this.workspacePath = workspacePath;
return this;
}

public InstancePaths build() {
if (serverType == null || serverType.isEmpty()) {
throw new IllegalArgumentException("serverType is required");
}
return new InstancePaths(serverType, workspacePath);
}
}

public String getServerType() {
return serverType;
}

public String getWorkspacePath() {
return workspacePath;
}

public String getInstanceFileName() {
return instanceFileName;
}

public Path getInstanceDir() {
return instanceDir;
}

public Path getInstanceFilePath() {
return instanceFilePath;
}

public boolean hasWorkspace() {
return workspacePath != null && !workspacePath.isEmpty();
}
}
Loading
Loading