-
Notifications
You must be signed in to change notification settings - Fork 352
Implement a more robust IPC system between the launcher and client #4159
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
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
4244fc8
Implement a more robust IPC system between the launcher and client
Gaming32 36a0435
Clippy fix and cargo fmt
Gaming32 6eb0539
Switch to cached JsonReader with LENIENT parsing to avoid race condit…
Gaming32 c942649
Make RPC send messages in lines
Gaming32 87f7116
Try to bind to either IPv4 or IPv6 and communicate version
Gaming32 057ff78
Move message handling into a separate function to avoid too much code…
Gaming32 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 12 additions & 36 deletions
48
packages/app-lib/java/src/main/java/com/modrinth/theseus/MinecraftLaunch.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
packages/app-lib/java/src/main/java/com/modrinth/theseus/rpc/RpcHandlers.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.modrinth.theseus.rpc; | ||
|
|
||
| import com.google.gson.JsonElement; | ||
| import com.google.gson.JsonNull; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import java.util.function.BiConsumer; | ||
| import java.util.function.Function; | ||
|
|
||
| public class RpcHandlers { | ||
| private final Map<String, Function<JsonElement[], JsonElement>> handlers = new HashMap<>(); | ||
| private boolean frozen; | ||
|
|
||
| public RpcHandlers handler(String functionName, Runnable handler) { | ||
| return addHandler(functionName, args -> { | ||
| handler.run(); | ||
| return JsonNull.INSTANCE; | ||
| }); | ||
| } | ||
|
|
||
| public <A, B> RpcHandlers handler( | ||
| String functionName, Class<A> arg1Type, Class<B> arg2Type, BiConsumer<A, B> handler) { | ||
| return addHandler(functionName, args -> { | ||
| if (args.length != 2) { | ||
| throw new IllegalArgumentException(functionName + " expected 2 arguments"); | ||
| } | ||
| final A arg1 = TheseusRpc.GSON.fromJson(args[0], arg1Type); | ||
| final B arg2 = TheseusRpc.GSON.fromJson(args[1], arg2Type); | ||
| handler.accept(arg1, arg2); | ||
| return JsonNull.INSTANCE; | ||
| }); | ||
| } | ||
|
|
||
| private RpcHandlers addHandler(String functionName, Function<JsonElement[], JsonElement> handler) { | ||
| if (frozen) { | ||
| throw new IllegalStateException("Cannot add handler to frozen RpcHandlers instance"); | ||
| } | ||
| handlers.put(functionName, handler); | ||
| return this; | ||
| } | ||
|
|
||
| Map<String, Function<JsonElement[], JsonElement>> build() { | ||
| frozen = true; | ||
| return handlers; | ||
| } | ||
| } |
9 changes: 9 additions & 0 deletions
9
packages/app-lib/java/src/main/java/com/modrinth/theseus/rpc/RpcMethodException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.modrinth.theseus.rpc; | ||
|
|
||
| public class RpcMethodException extends RuntimeException { | ||
| private static final long serialVersionUID = 1922360184188807964L; | ||
|
|
||
| public RpcMethodException(String message) { | ||
| super(message); | ||
| } | ||
| } |
183 changes: 183 additions & 0 deletions
183
packages/app-lib/java/src/main/java/com/modrinth/theseus/rpc/TheseusRpc.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| package com.modrinth.theseus.rpc; | ||
|
|
||
| import com.google.gson.*; | ||
| import com.google.gson.reflect.TypeToken; | ||
| import java.io.*; | ||
| import java.net.Socket; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Map; | ||
| import java.util.UUID; | ||
| import java.util.concurrent.BlockingQueue; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.LinkedBlockingQueue; | ||
| import java.util.concurrent.atomic.AtomicReference; | ||
| import java.util.function.Function; | ||
|
|
||
| public final class TheseusRpc { | ||
| static final Gson GSON = new GsonBuilder() | ||
| .setStrictness(Strictness.STRICT) | ||
| .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) | ||
| .disableHtmlEscaping() | ||
| .create(); | ||
| private static final TypeToken<RpcMessage> MESSAGE_TYPE = TypeToken.get(RpcMessage.class); | ||
|
|
||
| private static final AtomicReference<TheseusRpc> RPC = new AtomicReference<>(); | ||
|
|
||
| private final BlockingQueue<RpcMessage> mainThreadQueue = new LinkedBlockingQueue<>(); | ||
| private final Map<UUID, ResponseWaiter<?>> awaitingResponse = new ConcurrentHashMap<>(); | ||
| private final Map<String, Function<JsonElement[], JsonElement>> handlers; | ||
| private final Socket socket; | ||
|
|
||
| private TheseusRpc(Socket socket, RpcHandlers handlers) { | ||
| this.socket = socket; | ||
| this.handlers = handlers.build(); | ||
| } | ||
|
|
||
| public static void connectAndStart(String host, int port, RpcHandlers handlers) throws IOException { | ||
| if (RPC.get() != null) { | ||
| throw new IllegalStateException("Can only connect to RPC once"); | ||
| } | ||
|
|
||
| final Socket socket = new Socket(host, port); | ||
| final TheseusRpc rpc = new TheseusRpc(socket, handlers); | ||
| final Thread mainThread = new Thread(rpc::mainThread, "Theseus RPC Main"); | ||
| final Thread readThread = new Thread(rpc::readThread, "Theseus RPC Read"); | ||
| mainThread.setDaemon(true); | ||
| readThread.setDaemon(true); | ||
| mainThread.start(); | ||
| readThread.start(); | ||
| RPC.set(rpc); | ||
| } | ||
|
|
||
| public static TheseusRpc getRpc() { | ||
| final TheseusRpc rpc = RPC.get(); | ||
| if (rpc == null) { | ||
| throw new IllegalStateException("Called getRpc before RPC initialized"); | ||
| } | ||
| return rpc; | ||
| } | ||
|
|
||
| public <T> CompletableFuture<T> callMethod(TypeToken<T> returnType, String method, Object... args) { | ||
| final JsonElement[] jsonArgs = new JsonElement[args.length]; | ||
| for (int i = 0; i < args.length; i++) { | ||
| jsonArgs[i] = GSON.toJsonTree(args[i]); | ||
| } | ||
|
|
||
| final RpcMessage message = new RpcMessage(method, jsonArgs); | ||
| final ResponseWaiter<T> responseWaiter = new ResponseWaiter<>(returnType); | ||
| awaitingResponse.put(message.id, responseWaiter); | ||
| mainThreadQueue.add(message); | ||
| return responseWaiter.future; | ||
| } | ||
|
|
||
| private void mainThread() { | ||
| try { | ||
| final Writer writer = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8); | ||
| while (true) { | ||
| final RpcMessage message = mainThreadQueue.take(); | ||
| final RpcMessage toSend; | ||
| if (message.isForSending) { | ||
| toSend = message; | ||
| } else { | ||
| final Function<JsonElement[], JsonElement> handler = handlers.get(message.method); | ||
| if (handler == null) { | ||
| System.err.println("Unknown theseus RPC method " + message.method); | ||
| continue; | ||
| } | ||
| RpcMessage response; | ||
| try { | ||
| response = new RpcMessage(message.id, handler.apply(message.args)); | ||
| } catch (Exception e) { | ||
| response = new RpcMessage(message.id, e.toString()); | ||
| } | ||
| toSend = response; | ||
| } | ||
| GSON.toJson(toSend, writer); | ||
| writer.write('\n'); | ||
| writer.flush(); | ||
| } | ||
| } catch (IOException e) { | ||
| throw new UncheckedIOException(e); | ||
| } catch (InterruptedException ignored) { | ||
| } | ||
| } | ||
|
|
||
| private void readThread() { | ||
| try { | ||
| final BufferedReader reader = | ||
| new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); | ||
| while (true) { | ||
| final RpcMessage message = GSON.fromJson(reader.readLine(), MESSAGE_TYPE); | ||
| if (message.method == null) { | ||
| final ResponseWaiter<?> waiter = awaitingResponse.get(message.id); | ||
| if (waiter != null) { | ||
| handleResponse(waiter, message); | ||
| } | ||
| } else { | ||
| mainThreadQueue.put(message); | ||
| } | ||
| } | ||
| } catch (IOException e) { | ||
| throw new UncheckedIOException(e); | ||
| } catch (InterruptedException ignored) { | ||
| } | ||
| } | ||
|
|
||
| private <T> void handleResponse(ResponseWaiter<T> waiter, RpcMessage message) { | ||
| if (message.error != null) { | ||
| waiter.future.completeExceptionally(new RpcMethodException(message.error)); | ||
| return; | ||
| } | ||
| try { | ||
| waiter.future.complete(GSON.fromJson(message.response, waiter.type)); | ||
| } catch (JsonSyntaxException e) { | ||
| waiter.future.completeExceptionally(e); | ||
| } | ||
| } | ||
|
|
||
| private static class RpcMessage { | ||
| final UUID id; | ||
| final String method; // Optional | ||
| final JsonElement[] args; // Optional | ||
| final JsonElement response; // Optional | ||
| final String error; // Optional | ||
| final transient boolean isForSending; | ||
|
|
||
| RpcMessage(String method, JsonElement[] args) { | ||
| id = UUID.randomUUID(); | ||
| this.method = method; | ||
| this.args = args; | ||
| response = null; | ||
| error = null; | ||
| isForSending = true; | ||
| } | ||
|
|
||
| RpcMessage(UUID id, JsonElement response) { | ||
| this.id = id; | ||
| method = null; | ||
| args = null; | ||
| this.response = response; | ||
| error = null; | ||
| isForSending = true; | ||
| } | ||
|
|
||
| RpcMessage(UUID id, String error) { | ||
| this.id = id; | ||
| method = null; | ||
| args = null; | ||
| response = null; | ||
| this.error = error; | ||
| isForSending = true; | ||
| } | ||
| } | ||
|
|
||
| private static class ResponseWaiter<T> { | ||
| final TypeToken<T> type; | ||
| final CompletableFuture<T> future = new CompletableFuture<>(); | ||
|
|
||
| ResponseWaiter(TypeToken<T> type) { | ||
| this.type = type; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.