Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WebSocket support and launcher #108

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put --ws in front? I would assume here that 3000 is a positional argument that comes after named arguments, and those are usually last.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps it's also worth considering the deprecation of port as a positional argument and making it a --port option? For some time the server could support both styles, but eventually the positional way would be dropped.


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.
Expand Down
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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")
Expand Down
43 changes: 35 additions & 8 deletions src/main/java/software/amazon/smithy/lsp/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -61,9 +65,21 @@ public static void main(String[] args) {
Socket socket = null;
InputStream in;
OutputStream out;

List<String> argList = Arrays.asList(args);
try {
String port = args[0];
Optional<Exception> launchFailure;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declaration can go down with the assignment

String port = getOrDefault(argList, 0, "0");
String type = getOrDefault(argList, 1, null);
haydenbaker marked this conversation as resolved.
Show resolved Hide resolved
haydenbaker marked this conversation as resolved.
Show resolved Hide resolved

// 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;
Expand All @@ -73,9 +89,7 @@ public static void main(String[] args) {
in = socket.getInputStream();
out = socket.getOutputStream();
}

Optional<Exception> launchFailure = launch(in, out);

launchFailure = launch(in, out);
if (launchFailure.isPresent()) {
throw launchFailure.get();
} else {
Expand All @@ -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 {
Expand All @@ -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> T getOrDefault(List<T> list, int index, T t) {
if (isEmpty(list)) {
return t;
}
if (index < 0 || index >= list.size()) {
return t;
}
return list.get(index);
}
}
Original file line number Diff line number Diff line change
@@ -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<LanguageClient> {

@Override
protected void configure(Builder<LanguageClient> builder) {
builder.setLocalService(new SmithyLanguageServer());
builder.setRemoteInterface(LanguageClient.class);
}

@Override
protected void connect(Collection<Object> localServices, LanguageClient remoteProxy) {
localServices.stream()
.filter(LanguageClientAware.class::isInstance)
.forEach(languageClientAware -> ((LanguageClientAware) languageClientAware).connect(remoteProxy));
}

}
Original file line number Diff line number Diff line change
@@ -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<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> endpointClasses) {
ServerEndpointConfig conf =
ServerEndpointConfig.Builder.create(SmithyWebSocketEndpoint.class,
LSP_PATH).build();
return Collections.singleton(conf);
}

@Override
public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) {
return scanned;
}

}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}