Skip to content

Commit c304f94

Browse files
committed
Add WebSocket support and launcher
1 parent 708bfc4 commit c304f94

File tree

6 files changed

+209
-8
lines changed

6 files changed

+209
-8
lines changed

README.md

+30
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@
44
A [Language Server Protocol](https://microsoft.github.io/language-server-protocol/)
55
implementation for the [Smithy IDL](https://awslabs.github.io/smithy/).
66

7+
8+
### Running the LSP
9+
10+
There are three ways to launch the LSP, and which you choose depends on your use case.
11+
12+
In all cases, the communication protocol is JSON-RPC, the transport channels can are:
13+
14+
#### Stdio
15+
16+
Run `./gradlew run --args="0"`
17+
18+
The LSP will use stdio (stdin, stdout) to communicate.
19+
20+
#### Sockets
21+
22+
Run `./gradlew run --args="12423"`
23+
24+
The LSP will try to connect to the given port using a TCP socket - if it can't, it will fail.
25+
26+
This is used by the VSCode extension to establish a connection between it and the LSP (which is launched
27+
as a local process)
28+
29+
#### WebSockets
30+
31+
Run `./gradlew run --args="3000 --ws"`
32+
33+
The LSP will start a WebSocket server, which listens on given port.
34+
35+
This can be used to connect to a remote server running the LSP (more specifically from, but not limited to, the browser).
36+
737
## Security
838

939
See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.

build.gradle

+8
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ publishing {
157157

158158
dependencies {
159159
implementation "org.eclipse.lsp4j:org.eclipse.lsp4j:0.14.0"
160+
implementation 'org.eclipse.lsp4j:org.eclipse.lsp4j.websocket:0.14.0'
161+
implementation 'org.eclipse.lsp4j:org.eclipse.lsp4j.websocket.jakarta:0.14.0'
162+
implementation 'org.glassfish.tyrus:tyrus-server:2.0.1'
163+
implementation 'org.glassfish.tyrus:tyrus-container-grizzly-server:2.0.1'
160164
implementation "software.amazon.smithy:smithy-model:[1.31.0, 2.0["
161165
implementation 'io.get-coursier:interface:1.0.4'
162166
implementation 'com.disneystreaming.smithy:smithytranslate-formatter-jvm-java-api:0.3.4'
@@ -218,6 +222,10 @@ jar {
218222
exclude "META-INF/*.DSA"
219223
exclude "META-INF/*.RSA"
220224
exclude "reflect.properties"
225+
exclude "META-INF/LICENSE.md"
226+
exclude "META-INF/LICENSE.txt"
227+
exclude "META-INF/NOTICE.md"
228+
exclude "module-info.class"
221229
}
222230
manifest {
223231
attributes("Main-Class": "software.amazon.smithy.lsp.Main")

src/main/java/software/amazon/smithy/lsp/Main.java

+35-8
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818
import java.io.InputStream;
1919
import java.io.OutputStream;
2020
import java.net.Socket;
21+
import java.util.Arrays;
22+
import java.util.Collection;
23+
import java.util.List;
2124
import java.util.Optional;
2225
import org.eclipse.lsp4j.jsonrpc.Launcher;
2326
import org.eclipse.lsp4j.launch.LSPLauncher;
2427
import org.eclipse.lsp4j.services.LanguageClient;
28+
import software.amazon.smithy.lsp.websocket.WebSocketRunner;
2529

2630
/**
2731
* Main launcher for the Language server, started by the editor.
@@ -61,9 +65,21 @@ public static void main(String[] args) {
6165
Socket socket = null;
6266
InputStream in;
6367
OutputStream out;
64-
68+
List<String> argList = Arrays.asList(args);
6569
try {
66-
String port = args[0];
70+
Optional<Exception> launchFailure;
71+
String port = getOrDefault(argList, 0, "0");
72+
String type = getOrDefault(argList, 1, null);
73+
74+
// Check if websocket option is present
75+
if ("--ws".equals(type)) {
76+
WebSocketRunner webSocketRunner = new WebSocketRunner();
77+
String hostname = "localhost";
78+
String contextPath = "/";
79+
webSocketRunner.run(hostname, Integer.parseInt(port), contextPath);
80+
return;
81+
}
82+
6783
// If port is set to "0", use System.in/System.out.
6884
if (port.equals("0")) {
6985
in = System.in;
@@ -73,9 +89,7 @@ public static void main(String[] args) {
7389
in = socket.getInputStream();
7490
out = socket.getOutputStream();
7591
}
76-
77-
Optional<Exception> launchFailure = launch(in, out);
78-
92+
launchFailure = launch(in, out);
7993
if (launchFailure.isPresent()) {
8094
throw launchFailure.get();
8195
} else {
@@ -86,7 +100,7 @@ public static void main(String[] args) {
86100
} catch (NumberFormatException e) {
87101
System.out.println("Port number must be a valid integer");
88102
} catch (Exception e) {
89-
System.out.println(e);
103+
System.out.println("Failed to start: " + e);
90104

91105
e.printStackTrace();
92106
} finally {
@@ -95,9 +109,22 @@ public static void main(String[] args) {
95109
socket.close();
96110
}
97111
} catch (Exception e) {
98-
System.out.println("Failed to close the socket");
99-
System.out.println(e);
112+
System.out.println("Failed to close the socket: " + e);
100113
}
101114
}
102115
}
116+
117+
private static boolean isEmpty(Collection<?> c) {
118+
return c == null || c.isEmpty();
119+
}
120+
121+
private static <T> T getOrDefault(List<T> list, int index, T t) {
122+
if (isEmpty(list)) {
123+
return t;
124+
}
125+
if (index < 0 || index >= list.size()) {
126+
return t;
127+
}
128+
return list.get(index);
129+
}
103130
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.smithy.lsp.websocket;
17+
18+
import java.util.Collection;
19+
import org.eclipse.lsp4j.jsonrpc.Launcher.Builder;
20+
import org.eclipse.lsp4j.services.LanguageClient;
21+
import org.eclipse.lsp4j.services.LanguageClientAware;
22+
import org.eclipse.lsp4j.websocket.jakarta.WebSocketEndpoint;
23+
import software.amazon.smithy.lsp.SmithyLanguageServer;
24+
25+
public class SmithyWebSocketEndpoint extends WebSocketEndpoint<LanguageClient> {
26+
27+
@Override
28+
protected void configure(Builder<LanguageClient> builder) {
29+
builder.setLocalService(new SmithyLanguageServer());
30+
builder.setRemoteInterface(LanguageClient.class);
31+
}
32+
33+
@Override
34+
protected void connect(Collection<Object> localServices, LanguageClient remoteProxy) {
35+
localServices.stream()
36+
.filter(LanguageClientAware.class::isInstance)
37+
.forEach(languageClientAware -> ((LanguageClientAware) languageClientAware).connect(remoteProxy));
38+
}
39+
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.smithy.lsp.websocket;
17+
18+
import jakarta.websocket.Endpoint;
19+
import jakarta.websocket.server.ServerApplicationConfig;
20+
import jakarta.websocket.server.ServerEndpointConfig;
21+
import java.util.Collections;
22+
import java.util.Set;
23+
24+
public class SmithyWebSocketServerConfigProvider implements ServerApplicationConfig {
25+
26+
private static final String LSP_PATH = "/";
27+
28+
@Override
29+
public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> endpointClasses) {
30+
ServerEndpointConfig conf =
31+
ServerEndpointConfig.Builder.create(SmithyWebSocketEndpoint.class,
32+
LSP_PATH).build();
33+
return Collections.singleton(conf);
34+
}
35+
36+
@Override
37+
public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) {
38+
return scanned;
39+
}
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.smithy.lsp.websocket;
17+
18+
import jakarta.websocket.DeploymentException;
19+
import org.glassfish.tyrus.server.Server;
20+
import software.amazon.smithy.lsp.ext.LspLog;
21+
22+
public class WebSocketRunner {
23+
private static final String DEFAULT_HOSTNAME = "localhost";
24+
private static final int DEFAULT_PORT = 3000;
25+
private static final String DEFAULT_CONTEXT_PATH = "/";
26+
27+
/**
28+
* Run the websocket server on port of given host and path.
29+
* @param hostname hostname for server
30+
* @param port port server will listen on
31+
* @param contextPath path which routes to the lsp
32+
*/
33+
public void run(String hostname, int port, String contextPath) {
34+
Server server = new Server(
35+
hostname != null ? hostname : DEFAULT_HOSTNAME,
36+
port > 0 ? port : DEFAULT_PORT,
37+
contextPath != null ? contextPath : DEFAULT_CONTEXT_PATH,
38+
null,
39+
SmithyWebSocketServerConfigProvider.class
40+
);
41+
Runtime.getRuntime().addShutdownHook(new Thread(server::stop, "smithy-lsp-websocket-server-shutdown-hook"));
42+
43+
try {
44+
server.start();
45+
Thread.currentThread().join();
46+
} catch (InterruptedException e) {
47+
LspLog.println("Smithy LSP Websocket server has been interrupted.");
48+
Thread.currentThread().interrupt();
49+
} catch (DeploymentException e) {
50+
LspLog.println("Could not start Smithy LSP Websocket server.");
51+
} finally {
52+
server.stop();
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)