| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License 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 org.netbeans.modules.java.lsp.server; |
| |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.Inet4Address; |
| import java.net.ServerSocket; |
| import java.net.Socket; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.ExecutionException; |
| import java.util.function.BiConsumer; |
| import java.util.function.BiFunction; |
| |
| import org.netbeans.api.sendopts.CommandException; |
| import org.openide.util.Exceptions; |
| import org.openide.util.NbBundle; |
| import org.openide.util.Pair; |
| |
| @NbBundle.Messages({ |
| "# {0} - specification to parse", |
| "MSG_ConnectionSpecError=Cannot parse '{0}' as connection. Use 'stdio', 'listen:<port>' or 'connect:<port>'", |
| "# {0} - specification to parse", |
| "# {1} - port to parse", |
| "MSG_PortParseError=Cannot parse '{1}' as port in '{0}'" |
| }) |
| final class ConnectionSpec implements Closeable { |
| private final Boolean listen; |
| private final int port; |
| private final List<Closeable> close = new ArrayList<>(); |
| |
| private ConnectionSpec(Boolean listen, int port) { |
| this.listen = listen; |
| this.port = port; |
| } |
| |
| public static ConnectionSpec parse(String spec) throws CommandException { |
| if (spec == null || spec.isEmpty() || spec.equals("stdio")) { // NOI18N |
| return new ConnectionSpec(null, -1); |
| } |
| final String listenPrefix = "listen:"; // NOI18N |
| if (spec.startsWith(listenPrefix)) { |
| int port = parsePort(spec.substring(listenPrefix.length()), spec); |
| return new ConnectionSpec(true, port); |
| } |
| final String connectPrefix = "connect:"; // NOI18N |
| if (spec.startsWith(connectPrefix)) { |
| int port = parsePort(spec.substring(connectPrefix.length()), spec); |
| return new ConnectionSpec(false, port); |
| |
| } |
| throw new CommandException(555, Bundle.MSG_ConnectionSpecError(spec)); |
| } |
| |
| private static Integer parsePort(String port, String spec) throws CommandException { |
| try { |
| return Integer.parseInt(port); |
| } catch (NumberFormatException ex) { |
| throw new CommandException(556, Bundle.MSG_PortParseError(port, spec)); |
| } |
| } |
| |
| public <ServerType extends LspSession.ScheduledServer> void prepare( |
| String prefix, InputStream in, OutputStream out, LspSession session, |
| BiConsumer<LspSession, ServerType> serverSetter, |
| BiFunction<Pair<InputStream, OutputStream>, LspSession, ServerType> launcher) throws IOException { |
| |
| if (listen == null) { |
| // stdio |
| ServerType connectionObject = launcher.apply(Pair.of(in, out), session); |
| serverSetter.accept(session, connectionObject); |
| try { |
| connectionObject.getRunningFuture().get(); |
| } catch (InterruptedException ex) { |
| Exceptions.printStackTrace(ex); |
| } catch (ExecutionException ex) { |
| Throwable cause = ex.getCause(); |
| if (cause instanceof Error) { |
| throw (Error) cause; |
| } |
| Exceptions.printStackTrace(ex); |
| } finally { |
| serverSetter.accept(session, null); |
| } |
| } else if (listen) { |
| // listen on TCP |
| ServerSocket server = new ServerSocket(port, 1, Inet4Address.getLoopbackAddress()); |
| close.add(server); |
| int localPort = server.getLocalPort(); |
| Thread listeningThread = new Thread(prefix + " listening at port " + localPort) { |
| @Override |
| public void run() { |
| while (true) { |
| try { |
| Socket socket = server.accept(); |
| close.add(socket); |
| connectToSocket(socket, prefix, session, serverSetter, launcher); |
| } catch (IOException ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| } |
| } |
| }; |
| listeningThread.start(); |
| out.write((prefix + " listening at port " + localPort).getBytes()); |
| out.flush(); |
| } else { |
| // connect to TCP |
| final Socket socket = new Socket(Inet4Address.getLoopbackAddress(), port); |
| connectToSocket(socket, prefix, session, serverSetter, launcher); |
| } |
| } |
| |
| private <ServerType extends LspSession.ScheduledServer> void connectToSocket( |
| final Socket socket, String prefix, LspSession session, |
| BiConsumer<LspSession, ServerType> serverSetter, |
| BiFunction<Pair<InputStream, OutputStream>, LspSession, ServerType> launcher) { |
| |
| final int connectTo = socket.getPort(); |
| Thread connectedThread = new Thread(prefix + " connected to " + connectTo) { |
| @Override |
| public void run() { |
| try { |
| ServerType connectionObject = launcher.apply(Pair.of(socket.getInputStream(), socket.getOutputStream()), session); |
| serverSetter.accept(session, connectionObject); |
| connectionObject.getRunningFuture().get(); |
| } catch (IOException | InterruptedException | ExecutionException ex) { |
| Exceptions.printStackTrace(ex); |
| } finally { |
| serverSetter.accept(session, null); |
| } |
| } |
| }; |
| connectedThread.start(); |
| } |
| |
| @Override |
| public void close() throws IOException { |
| for (Closeable c : close) { |
| c.close(); |
| } |
| } |
| } |