From c304f9449e036c508e01ab4805162e5c662ac39b Mon Sep 17 00:00:00 2001 From: Hayden Baker Date: Tue, 23 May 2023 08:54:02 -0700 Subject: [PATCH] Add WebSocket support and launcher --- README.md | 30 ++++++++++ build.gradle | 8 +++ .../java/software/amazon/smithy/lsp/Main.java | 43 ++++++++++++--- .../websocket/SmithyWebSocketEndpoint.java | 40 ++++++++++++++ .../SmithyWebSocketServerConfigProvider.java | 41 ++++++++++++++ .../smithy/lsp/websocket/WebSocketRunner.java | 55 +++++++++++++++++++ 6 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 src/main/java/software/amazon/smithy/lsp/websocket/SmithyWebSocketEndpoint.java create mode 100644 src/main/java/software/amazon/smithy/lsp/websocket/SmithyWebSocketServerConfigProvider.java create mode 100644 src/main/java/software/amazon/smithy/lsp/websocket/WebSocketRunner.java diff --git a/README.md b/README.md index 33c4b272..aef51c27 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,36 @@ A [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) implementation for the [Smithy IDL](https://awslabs.github.io/smithy/). + +### Running the LSP + +There are three ways to launch the LSP, and which you choose depends on your use case. + +In all cases, the communication protocol is JSON-RPC, the transport channels can are: + +#### Stdio + +Run `./gradlew run --args="0"` + +The LSP will use stdio (stdin, stdout) to communicate. + +#### Sockets + +Run `./gradlew run --args="12423"` + +The LSP will try to connect to the given port using a TCP socket - if it can't, it will fail. + +This is used by the VSCode extension to establish a connection between it and the LSP (which is launched +as a local process) + +#### WebSockets + +Run `./gradlew run --args="3000 --ws"` + +The LSP will start a WebSocket server, which listens on given port. + +This can be used to connect to a remote server running the LSP (more specifically from, but not limited to, the browser). + ## Security See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. diff --git a/build.gradle b/build.gradle index 4ab96091..e60deb69 100644 --- a/build.gradle +++ b/build.gradle @@ -157,6 +157,10 @@ publishing { dependencies { implementation "org.eclipse.lsp4j:org.eclipse.lsp4j:0.14.0" + implementation 'org.eclipse.lsp4j:org.eclipse.lsp4j.websocket:0.14.0' + implementation 'org.eclipse.lsp4j:org.eclipse.lsp4j.websocket.jakarta:0.14.0' + implementation 'org.glassfish.tyrus:tyrus-server:2.0.1' + implementation 'org.glassfish.tyrus:tyrus-container-grizzly-server:2.0.1' implementation "software.amazon.smithy:smithy-model:[1.31.0, 2.0[" implementation 'io.get-coursier:interface:1.0.4' implementation 'com.disneystreaming.smithy:smithytranslate-formatter-jvm-java-api:0.3.4' @@ -218,6 +222,10 @@ jar { exclude "META-INF/*.DSA" exclude "META-INF/*.RSA" exclude "reflect.properties" + exclude "META-INF/LICENSE.md" + exclude "META-INF/LICENSE.txt" + exclude "META-INF/NOTICE.md" + exclude "module-info.class" } manifest { attributes("Main-Class": "software.amazon.smithy.lsp.Main") diff --git a/src/main/java/software/amazon/smithy/lsp/Main.java b/src/main/java/software/amazon/smithy/lsp/Main.java index 5a7bf9f4..3f811762 100644 --- a/src/main/java/software/amazon/smithy/lsp/Main.java +++ b/src/main/java/software/amazon/smithy/lsp/Main.java @@ -18,10 +18,14 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; import java.util.Optional; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.launch.LSPLauncher; import org.eclipse.lsp4j.services.LanguageClient; +import software.amazon.smithy.lsp.websocket.WebSocketRunner; /** * Main launcher for the Language server, started by the editor. @@ -61,9 +65,21 @@ public static void main(String[] args) { Socket socket = null; InputStream in; OutputStream out; - + List argList = Arrays.asList(args); try { - String port = args[0]; + Optional launchFailure; + String port = getOrDefault(argList, 0, "0"); + String type = getOrDefault(argList, 1, null); + + // Check if websocket option is present + if ("--ws".equals(type)) { + WebSocketRunner webSocketRunner = new WebSocketRunner(); + String hostname = "localhost"; + String contextPath = "/"; + webSocketRunner.run(hostname, Integer.parseInt(port), contextPath); + return; + } + // If port is set to "0", use System.in/System.out. if (port.equals("0")) { in = System.in; @@ -73,9 +89,7 @@ public static void main(String[] args) { in = socket.getInputStream(); out = socket.getOutputStream(); } - - Optional launchFailure = launch(in, out); - + launchFailure = launch(in, out); if (launchFailure.isPresent()) { throw launchFailure.get(); } else { @@ -86,7 +100,7 @@ public static void main(String[] args) { } catch (NumberFormatException e) { System.out.println("Port number must be a valid integer"); } catch (Exception e) { - System.out.println(e); + System.out.println("Failed to start: " + e); e.printStackTrace(); } finally { @@ -95,9 +109,22 @@ public static void main(String[] args) { socket.close(); } } catch (Exception e) { - System.out.println("Failed to close the socket"); - System.out.println(e); + System.out.println("Failed to close the socket: " + e); } } } + + private static boolean isEmpty(Collection c) { + return c == null || c.isEmpty(); + } + + private static T getOrDefault(List list, int index, T t) { + if (isEmpty(list)) { + return t; + } + if (index < 0 || index >= list.size()) { + return t; + } + return list.get(index); + } } diff --git a/src/main/java/software/amazon/smithy/lsp/websocket/SmithyWebSocketEndpoint.java b/src/main/java/software/amazon/smithy/lsp/websocket/SmithyWebSocketEndpoint.java new file mode 100644 index 00000000..8424e6aa --- /dev/null +++ b/src/main/java/software/amazon/smithy/lsp/websocket/SmithyWebSocketEndpoint.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.lsp.websocket; + +import java.util.Collection; +import org.eclipse.lsp4j.jsonrpc.Launcher.Builder; +import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.services.LanguageClientAware; +import org.eclipse.lsp4j.websocket.jakarta.WebSocketEndpoint; +import software.amazon.smithy.lsp.SmithyLanguageServer; + +public class SmithyWebSocketEndpoint extends WebSocketEndpoint { + + @Override + protected void configure(Builder builder) { + builder.setLocalService(new SmithyLanguageServer()); + builder.setRemoteInterface(LanguageClient.class); + } + + @Override + protected void connect(Collection localServices, LanguageClient remoteProxy) { + localServices.stream() + .filter(LanguageClientAware.class::isInstance) + .forEach(languageClientAware -> ((LanguageClientAware) languageClientAware).connect(remoteProxy)); + } + +} diff --git a/src/main/java/software/amazon/smithy/lsp/websocket/SmithyWebSocketServerConfigProvider.java b/src/main/java/software/amazon/smithy/lsp/websocket/SmithyWebSocketServerConfigProvider.java new file mode 100644 index 00000000..02420390 --- /dev/null +++ b/src/main/java/software/amazon/smithy/lsp/websocket/SmithyWebSocketServerConfigProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.lsp.websocket; + +import jakarta.websocket.Endpoint; +import jakarta.websocket.server.ServerApplicationConfig; +import jakarta.websocket.server.ServerEndpointConfig; +import java.util.Collections; +import java.util.Set; + +public class SmithyWebSocketServerConfigProvider implements ServerApplicationConfig { + + private static final String LSP_PATH = "/"; + + @Override + public Set getEndpointConfigs(Set> endpointClasses) { + ServerEndpointConfig conf = + ServerEndpointConfig.Builder.create(SmithyWebSocketEndpoint.class, + LSP_PATH).build(); + return Collections.singleton(conf); + } + + @Override + public Set> getAnnotatedEndpointClasses(Set> scanned) { + return scanned; + } + +} diff --git a/src/main/java/software/amazon/smithy/lsp/websocket/WebSocketRunner.java b/src/main/java/software/amazon/smithy/lsp/websocket/WebSocketRunner.java new file mode 100644 index 00000000..87332d1a --- /dev/null +++ b/src/main/java/software/amazon/smithy/lsp/websocket/WebSocketRunner.java @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.lsp.websocket; + +import jakarta.websocket.DeploymentException; +import org.glassfish.tyrus.server.Server; +import software.amazon.smithy.lsp.ext.LspLog; + +public class WebSocketRunner { + private static final String DEFAULT_HOSTNAME = "localhost"; + private static final int DEFAULT_PORT = 3000; + private static final String DEFAULT_CONTEXT_PATH = "/"; + + /** + * Run the websocket server on port of given host and path. + * @param hostname hostname for server + * @param port port server will listen on + * @param contextPath path which routes to the lsp + */ + public void run(String hostname, int port, String contextPath) { + Server server = new Server( + hostname != null ? hostname : DEFAULT_HOSTNAME, + port > 0 ? port : DEFAULT_PORT, + contextPath != null ? contextPath : DEFAULT_CONTEXT_PATH, + null, + SmithyWebSocketServerConfigProvider.class + ); + Runtime.getRuntime().addShutdownHook(new Thread(server::stop, "smithy-lsp-websocket-server-shutdown-hook")); + + try { + server.start(); + Thread.currentThread().join(); + } catch (InterruptedException e) { + LspLog.println("Smithy LSP Websocket server has been interrupted."); + Thread.currentThread().interrupt(); + } catch (DeploymentException e) { + LspLog.println("Could not start Smithy LSP Websocket server."); + } finally { + server.stop(); + } + } +}