View Javadoc
1   /*
2   Copyright (c) 2025 Christoph Läubrich All rights reserved.
3   
4   This program is licensed to you under the Apache License Version 2.0,
5   and you may not use this file except in compliance with the Apache License Version 2.0.
6   You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7   
8   Unless required by applicable law or agreed to in writing,
9   software distributed under the Apache License Version 2.0 is distributed on an
10  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12  */
13  package org.codehaus.plexus.build.connect;
14  
15  import javax.inject.Named;
16  import javax.inject.Singleton;
17  
18  import java.io.Closeable;
19  import java.io.DataInputStream;
20  import java.io.DataOutputStream;
21  import java.io.IOException;
22  import java.net.ServerSocket;
23  import java.net.Socket;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.UUID;
28  import java.util.WeakHashMap;
29  import java.util.concurrent.ExecutorService;
30  import java.util.concurrent.Executors;
31  import java.util.concurrent.atomic.AtomicBoolean;
32  import java.util.function.BiConsumer;
33  import java.util.function.Function;
34  
35  import org.apache.maven.execution.MavenSession;
36  import org.codehaus.plexus.build.connect.messages.Message;
37  
38  /**
39   * Default implementation using the system property
40   * <code>plexus.build.ipc.port</code> to communicate with an endpoint to
41   * exchange messages
42   */
43  @Named("default")
44  @Singleton
45  public class TcpBuildConnection implements BuildConnection {
46      private static final String PLEXUS_BUILD_IPC_PORT = "plexus.build.ipc.port";
47  
48      private static final int PORT = Integer.getInteger(PLEXUS_BUILD_IPC_PORT, 0);
49  
50      private final Map<MavenSession, String> sessionMap = new WeakHashMap<>();
51  
52      private final ThreadLocal<TcpClientConnection> connections =
53              ThreadLocal.withInitial(() -> new TcpClientConnection());
54  
55      @Override
56      public boolean isEnabled() {
57          return PORT > 0;
58      }
59  
60      @Override
61      public Message send(Message message, MavenSession mavenSession) {
62          if (isEnabled()) {
63              String sessionId = getId(mavenSession);
64              byte[] messageBytes = message.serialize(sessionId);
65              byte[] replyBytes = connections.get().send(messageBytes);
66              if (replyBytes.length > 0) {
67                  return Message.decode(replyBytes);
68              }
69          }
70          return null;
71      }
72  
73      private synchronized String getId(MavenSession session) {
74          if (session == null) {
75              return Thread.currentThread().getName();
76          }
77          return sessionMap.computeIfAbsent(session, x -> UUID.randomUUID().toString());
78      }
79  
80      /**
81       * Creates a new server that will receive messages from a remote endpoint and
82       * inform the consumer
83       *
84       * @param consumer the consumer of messages, might be called by different
85       *                 threads, if the consumer throws an exception while handling a
86       *                 message it will maybe no longer receive some messages. The
87       *                 returned map is used as a payload for the reply to the
88       *                 server, if <code>null</code> is returned a simple
89       *                 acknowledgement without any payload will be send to the
90       *                 endpoint. If the consumer performs blocking operations the
91       *                 further execution of the maven process might be halted
92       *                 depending on the message type, if that is not desired work
93       *                 should be offloaded by the consumer to a different thread.
94       * @return a {@link ServerConnection} that can be used to shutdown the server
95       *         and get properties that needs to be passed to the maven process
96       * @throws IOException if no local socket can be opened
97       */
98      public static ServerConnection createServer(Function<Message, Map<String, String>> consumer) throws IOException {
99          return new ServerConnection(new ServerSocket(0), consumer);
100     }
101 
102     /**
103      * Represents a server connection that must be created to communicate with the
104      * maven process using the {@link TcpBuildConnection}
105      */
106     public static final class ServerConnection implements AutoCloseable {
107 
108         private ServerSocket socket;
109         private ExecutorService executor = Executors.newCachedThreadPool();
110         private List<TcpServerConnection> connections = new ArrayList<>();
111 
112         ServerConnection(ServerSocket socket, Function<Message, Map<String, String>> consumer) {
113             this.socket = socket;
114             executor.execute(() -> {
115                 while (!Thread.currentThread().isInterrupted()) {
116                     try {
117                         TcpServerConnection connection = new TcpServerConnection(socket.accept(), consumer);
118                         connections.add(connection);
119                         executor.execute(connection);
120                     } catch (IOException e) {
121                         return;
122                     }
123                 }
124             });
125         }
126 
127         @Override
128         public void close() {
129             executor.shutdownNow();
130             for (TcpServerConnection connection : connections) {
131                 connection.close();
132             }
133             try {
134                 socket.close();
135             } catch (IOException e) {
136             }
137         }
138 
139         /**
140          * Given a consumer publishes required properties for a process to launch
141          *
142          * @param consumer the consumer for system properties
143          */
144         public void setupProcess(BiConsumer<String, String> consumer) {
145             // currently only one but might become more later (e.g. timeout, reconnects,
146             // ...)
147             consumer.accept(PLEXUS_BUILD_IPC_PORT, Integer.toString(socket.getLocalPort()));
148         }
149     }
150 
151     private static final class TcpServerConnection implements Runnable, Closeable {
152 
153         private Socket socket;
154         private Function<Message, Map<String, String>> consumer;
155         private DataInputStream in;
156         private DataOutputStream out;
157         private AtomicBoolean closed = new AtomicBoolean();
158 
159         public TcpServerConnection(Socket socket, Function<Message, Map<String, String>> consumer) throws IOException {
160             this.socket = socket;
161             this.consumer = consumer;
162             in = new DataInputStream(socket.getInputStream());
163             out = new DataOutputStream(socket.getOutputStream());
164         }
165 
166         @Override
167         public void run() {
168             try {
169                 while (!closed.get() && !Thread.currentThread().isInterrupted()) {
170                     try {
171                         int length = in.readInt();
172                         if (length == 0) {
173                             return;
174                         }
175                         byte[] bytes = new byte[length];
176                         in.readFully(bytes);
177                         Message message = Message.decode(bytes);
178                         Map<String, String> payload = consumer.apply(message);
179                         Message reply = Message.replyTo(message, payload);
180                         byte[] responseBytes = reply.serialize();
181                         synchronized (out) {
182                             out.writeInt(responseBytes.length);
183                             out.write(responseBytes);
184                             out.flush();
185                         }
186                     } catch (Exception e) {
187                         return;
188                     }
189                 }
190             } finally {
191                 close();
192             }
193         }
194 
195         @Override
196         public void close() {
197             if (closed.compareAndSet(false, true)) {
198                 try {
199                     synchronized (out) {
200                         out.writeInt(0);
201                         out.flush();
202                     }
203                 } catch (IOException e) {
204                 }
205                 try {
206                     socket.close();
207                 } catch (IOException e) {
208                 }
209             }
210         }
211     }
212 
213     private static final class TcpClientConnection {
214 
215         private Socket socket;
216         private boolean closed;
217         private DataInputStream in;
218         private DataOutputStream out;
219 
220         public byte[] send(byte[] messageBytes) {
221             if (!closed) {
222                 try {
223                     if (socket == null) {
224                         socket = new Socket("localhost", PORT);
225                         in = new DataInputStream(socket.getInputStream());
226                         out = new DataOutputStream(socket.getOutputStream());
227                     }
228                     out.writeInt(messageBytes.length);
229                     out.write(messageBytes);
230                     out.flush();
231                     int length = in.readInt();
232                     if (length == 0) {
233                         socket.close();
234                         closed = true;
235                     } else {
236                         byte[] bytes = new byte[length];
237                         in.readFully(bytes);
238                         return bytes;
239                     }
240                 } catch (IOException e) {
241                     closed = true;
242                     if (socket != null) {
243                         try {
244                             socket.close();
245                         } catch (IOException e1) {
246                         }
247                     }
248                 }
249             }
250             return new byte[0];
251         }
252     }
253 }