Skip to content

Commit

Permalink
Add WebSocket support and launcher
Browse files Browse the repository at this point in the history
  • Loading branch information
haydenbaker committed May 24, 2023
1 parent 708bfc4 commit c304f94
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 8 deletions.
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"`

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;
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;
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();
}
}
}

0 comments on commit c304f94

Please sign in to comment.