View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.named.ipc;
20  
21  import java.io.BufferedReader;
22  import java.io.DataInputStream;
23  import java.io.DataOutputStream;
24  import java.io.IOException;
25  import java.io.InputStreamReader;
26  import java.net.SocketAddress;
27  import java.nio.channels.ByteChannel;
28  import java.nio.channels.Channels;
29  import java.nio.channels.ServerSocketChannel;
30  import java.nio.channels.SocketChannel;
31  import java.nio.charset.StandardCharsets;
32  import java.util.ArrayList;
33  import java.util.Iterator;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.concurrent.CompletableFuture;
37  import java.util.concurrent.ConcurrentHashMap;
38  import java.util.concurrent.CopyOnWriteArrayList;
39  import java.util.concurrent.TimeUnit;
40  import java.util.concurrent.atomic.AtomicInteger;
41  
42  /**
43   * Implementation of the server side.
44   * The server instance is bound to a given maven repository.
45   *
46   * @since 2.0.1
47   */
48  public class IpcServer {
49      /**
50       * Should the IPC server not fork? (i.e. for testing purposes)
51       *
52       * @configurationSource {@link System#getProperty(String, String)}
53       * @configurationType {@link java.lang.Boolean}
54       * @configurationDefaultValue {@link #DEFAULT_NO_FORK}
55       */
56      public static final String SYSTEM_PROP_NO_FORK = "aether.named.ipc.nofork";
57  
58      public static final boolean DEFAULT_NO_FORK = false;
59  
60      /**
61       * IPC idle timeout in seconds. If there is no IPC request during idle time, it will stop.
62       *
63       * @configurationSource {@link System#getProperty(String, String)}
64       * @configurationType {@link java.lang.Integer}
65       * @configurationDefaultValue {@link #DEFAULT_IDLE_TIMEOUT}
66       */
67      public static final String SYSTEM_PROP_IDLE_TIMEOUT = "aether.named.ipc.idleTimeout";
68  
69      public static final int DEFAULT_IDLE_TIMEOUT = 300;
70  
71      /**
72       * IPC socket family to use.
73       *
74       * @configurationSource {@link System#getProperty(String, String)}
75       * @configurationType {@link java.lang.String}
76       * @configurationDefaultValue {@link #DEFAULT_FAMILY}
77       */
78      public static final String SYSTEM_PROP_FAMILY = "aether.named.ipc.family";
79  
80      public static final String DEFAULT_FAMILY = "unix";
81  
82      /**
83       * Should the IPC server not use native executable?
84       *
85       * @configurationSource {@link System#getProperty(String, String)}
86       * @configurationType {@link java.lang.Boolean}
87       * @configurationDefaultValue {@link #DEFAULT_NO_NATIVE}
88       */
89      public static final String SYSTEM_PROP_NO_NATIVE = "aether.named.ipc.nonative";
90  
91      public static final boolean DEFAULT_NO_NATIVE = true;
92  
93      /**
94       * The name if the IPC server native executable (without file extension like ".exe")
95       *
96       * @configurationSource {@link System#getProperty(String, String)}
97       * @configurationType {@link java.lang.String}
98       * @configurationDefaultValue {@link #DEFAULT_NATIVE_NAME}
99       */
100     public static final String SYSTEM_PROP_NATIVE_NAME = "aether.named.ipc.nativeName";
101 
102     public static final String DEFAULT_NATIVE_NAME = "ipc-sync";
103 
104     /**
105      * Should the IPC server log debug messages? (i.e. for testing purposes)
106      *
107      * @configurationSource {@link System#getProperty(String, String)}
108      * @configurationType {@link java.lang.Boolean}
109      * @configurationDefaultValue {@link #DEFAULT_DEBUG}
110      */
111     public static final String SYSTEM_PROP_DEBUG = "aether.named.ipc.debug";
112 
113     public static final boolean DEFAULT_DEBUG = false;
114 
115     private final ServerSocketChannel serverSocket;
116     private final Map<SocketChannel, Thread> clients = new ConcurrentHashMap<>();
117     private final AtomicInteger counter = new AtomicInteger();
118     private final Map<String, Lock> locks = new ConcurrentHashMap<>();
119     private final Map<String, Context> contexts = new ConcurrentHashMap<>();
120     private static final boolean DEBUG =
121             Boolean.parseBoolean(System.getProperty(SYSTEM_PROP_DEBUG, Boolean.toString(DEFAULT_DEBUG)));
122     private final long idleTimeout;
123     private final String bootstrapToken;
124     private volatile long lastUsed;
125     private volatile boolean closing;
126 
127     /**
128      * @deprecated a server created without a bootstrap token refuses {@link IpcMessages#REQUEST_STOP} requests;
129      * use {@link #IpcServer(SocketFamily, String)} instead.
130      */
131     @Deprecated
132     public IpcServer(SocketFamily family) throws IOException {
133         this(family, null);
134     }
135 
136     /**
137      * Creates a server that honors a remote stop request only when it carries the given bootstrap token, which is
138      * shared exclusively with the client that spawned this server. The rest of the protocol is unauthenticated,
139      * but destructive cross-client operations (closing foreign contexts, stopping the daemon) are refused.
140      *
141      * @since 2.0.23
142      */
143     public IpcServer(SocketFamily family, String bootstrapToken) throws IOException {
144         this.bootstrapToken = bootstrapToken;
145         serverSocket = family.openServerSocket();
146         long timeout = TimeUnit.SECONDS.toNanos(DEFAULT_IDLE_TIMEOUT);
147         String str = System.getProperty(SYSTEM_PROP_IDLE_TIMEOUT);
148         if (str != null) {
149             try {
150                 TimeUnit unit = TimeUnit.SECONDS;
151                 if (str.endsWith("ms")) {
152                     unit = TimeUnit.MILLISECONDS;
153                     str = str.substring(0, str.length() - 2);
154                 }
155                 long dur = Long.parseLong(str);
156                 timeout = unit.toNanos(dur);
157             } catch (NumberFormatException e) {
158                 error("Property " + SYSTEM_PROP_IDLE_TIMEOUT + " specified with invalid value: " + str, e);
159             }
160         }
161         idleTimeout = timeout;
162     }
163 
164     public static void main(String[] args) throws Exception {
165         // When spawning a new process, the child process is create within
166         // the same process group.  This means that a few signals are sent
167         // to the whole group.  This is the case for SIGINT (Ctrl-C) and
168         // SIGTSTP (Ctrl-Z) which are both sent to all the processed in the
169         // group when initiated from the controlling terminal.
170         // This is only a problem when the client creates the daemon, but
171         // without ignoring those signals, a client being interrupted will
172         // also interrupt and kill the daemon.
173         try {
174             sun.misc.Signal.handle(new sun.misc.Signal("INT"), sun.misc.SignalHandler.SIG_IGN);
175             if (!IpcClient.IS_WINDOWS) {
176                 sun.misc.Signal.handle(new sun.misc.Signal("TSTP"), sun.misc.SignalHandler.SIG_IGN);
177             }
178         } catch (Throwable t) {
179             error("Unable to ignore INT and TSTP signals", t);
180         }
181 
182         String family = args[0];
183         String tmpAddress = args[1];
184         String rand = args[2];
185         if ("-".equals(rand)) {
186             // the bootstrap token is passed via stdin instead of argv: process arguments are commonly visible
187             // to other local users (e.g. /proc/<pid>/cmdline), and this token authorizes stopping the daemon
188             BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
189             rand = reader.readLine();
190             if (rand == null || rand.isEmpty()) {
191                 throw new IOException("Expected the bootstrap token on standard input");
192             }
193         }
194 
195         runServer(SocketFamily.valueOf(family), tmpAddress, rand);
196     }
197 
198     static IpcServer runServer(SocketFamily family, String tmpAddress, String rand) throws IOException {
199         IpcServer server = new IpcServer(family, rand);
200         run(server::run, false); // this is one-off
201         String address = SocketFamily.toString(server.getLocalAddress());
202         SocketAddress socketAddress = SocketFamily.fromString(tmpAddress);
203         try (SocketChannel socket = SocketChannel.open(socketAddress)) {
204             try (DataOutputStream dos = new DataOutputStream(Channels.newOutputStream(socket))) {
205                 dos.writeUTF(rand);
206                 dos.writeUTF(address);
207                 dos.flush();
208             }
209         }
210 
211         return server;
212     }
213 
214     private static void debug(String msg, Object... args) {
215         if (DEBUG) {
216             System.out.printf("[ipc] [debug] " + msg + "\n", args);
217         }
218     }
219 
220     private static void info(String msg, Object... args) {
221         System.out.printf("[ipc] [info] " + msg + "\n", args);
222     }
223 
224     private static void error(String msg, Throwable t) {
225         System.out.println("[ipc] [error] " + msg);
226         t.printStackTrace(System.out);
227     }
228 
229     private static void run(Runnable runnable, boolean daemon) {
230         Thread thread = new Thread(runnable);
231         if (daemon) {
232             thread.setDaemon(true);
233         }
234         thread.start();
235     }
236 
237     public SocketAddress getLocalAddress() throws IOException {
238         return serverSocket.getLocalAddress();
239     }
240 
241     public void run() {
242         try {
243             info("IpcServer started at %s", getLocalAddress().toString());
244             use();
245             run(this::expirationCheck, true);
246             while (!closing) {
247                 SocketChannel socket = this.serverSocket.accept();
248                 run(() -> client(socket), false);
249             }
250         } catch (Throwable t) {
251             if (!closing) {
252                 error("Error running sync server loop", t);
253             }
254         }
255     }
256 
257     private void client(SocketChannel socket) {
258         int c;
259         synchronized (clients) {
260             clients.put(socket, Thread.currentThread());
261             c = clients.size();
262         }
263         info("New client connected (%d connected)", c);
264         use();
265         Map<String, Context> clientContexts = new ConcurrentHashMap<>();
266         try {
267             ByteChannel wrapper = new ByteChannelWrapper(socket);
268             DataInputStream input = new DataInputStream(Channels.newInputStream(wrapper));
269             DataOutputStream output = new DataOutputStream(Channels.newOutputStream(wrapper));
270             while (!closing) {
271                 int requestId = input.readInt();
272                 int sz = input.readInt();
273                 if (sz < 0) {
274                     throw new IOException("Received invalid request size: " + sz);
275                 }
276                 // do not preallocate from an unauthenticated wire-supplied size; grow with actually received data
277                 List<String> request = new ArrayList<>(Math.min(sz, 1024));
278                 for (int i = 0; i < sz; i++) {
279                     request.add(input.readUTF());
280                 }
281                 if (request.isEmpty()) {
282                     throw new IOException("Received invalid request");
283                 }
284                 use();
285                 String contextId;
286                 Context context;
287                 String command = request.remove(0);
288                 switch (command) {
289                     case IpcMessages.REQUEST_CONTEXT:
290                         if (request.size() != 1) {
291                             throw new IOException("Expected one argument for " + command + " but got " + request);
292                         }
293                         boolean shared = Boolean.parseBoolean(request.remove(0));
294                         context = new Context(shared);
295                         contexts.put(context.id, context);
296                         clientContexts.put(context.id, context);
297                         synchronized (output) {
298                             debug("Created context %s", context.id);
299                             output.writeInt(requestId);
300                             output.writeInt(2);
301                             output.writeUTF(IpcMessages.RESPONSE_CONTEXT);
302                             output.writeUTF(context.id);
303                             output.flush();
304                         }
305                         break;
306                     case IpcMessages.REQUEST_ACQUIRE:
307                         if (request.isEmpty()) {
308                             throw new IOException(
309                                     "Expected at least one argument for " + command + " but got " + request);
310                         }
311                         contextId = request.remove(0);
312                         // contexts are scoped per connection: a client may only use contexts it created itself
313                         context = clientContexts.get(contextId);
314                         if (context == null) {
315                             throw new IOException(
316                                     "Unknown context: " + contextId + ". Known contexts = " + clientContexts.keySet());
317                         }
318                         context.lock(request).thenRun(() -> sendAcquireResponse(output, socket, requestId, context));
319                         break;
320                     case IpcMessages.REQUEST_CLOSE:
321                         if (request.size() != 1) {
322                             throw new IOException("Expected one argument for " + command + " but got " + request);
323                         }
324                         contextId = request.remove(0);
325                         // contexts are scoped per connection: a client may only close contexts it created itself
326                         context = clientContexts.remove(contextId);
327                         if (context == null) {
328                             throw new IOException(
329                                     "Unknown context: " + contextId + ". Known contexts = " + clientContexts.keySet());
330                         }
331                         contexts.remove(contextId);
332                         context.unlock();
333                         synchronized (output) {
334                             debug("Closing context %s", context.id);
335                             output.writeInt(requestId);
336                             output.writeInt(1);
337                             output.writeUTF(IpcMessages.RESPONSE_CLOSE);
338                             output.flush();
339                         }
340                         break;
341                     case IpcMessages.REQUEST_STOP:
342                         if (request.size() > 1) {
343                             throw new IOException(
344                                     "Expected at most one argument for " + command + " but got " + request);
345                         }
346                         String stopToken = request.isEmpty() ? null : request.remove(0);
347                         if (bootstrapToken == null || !bootstrapToken.equals(stopToken)) {
348                             // the protocol is otherwise unauthenticated: only the client that spawned this
349                             // server (and thus knows the bootstrap token) may stop it for everybody else
350                             throw new IOException("Stop request rejected: missing or invalid bootstrap token");
351                         }
352                         synchronized (output) {
353                             debug("Stopping server");
354                             output.writeInt(requestId);
355                             output.writeInt(1);
356                             output.writeUTF(IpcMessages.RESPONSE_STOP);
357                             output.flush();
358                         }
359                         close();
360                         break;
361                     default:
362                         throw new IOException("Unknown request: " + request.get(0));
363                 }
364             }
365         } catch (Throwable t) {
366             if (!closing) {
367                 error("Error processing request", t);
368             }
369         } finally {
370             if (!closing) {
371                 info("Client disconnecting...");
372             }
373             clientContexts.values().forEach(context -> {
374                 contexts.remove(context.id);
375                 context.unlock();
376             });
377             try {
378                 socket.close();
379             } catch (IOException ioException) {
380                 // ignore
381             }
382             synchronized (clients) {
383                 clients.remove(socket);
384                 c = clients.size();
385             }
386             if (!closing) {
387                 info("%d clients remained", c);
388             }
389         }
390     }
391 
392     private void sendAcquireResponse(DataOutputStream output, SocketChannel socket, int requestId, Context context) {
393         try {
394             synchronized (output) {
395                 debug("Locking in context %s", context.id);
396                 output.writeInt(requestId);
397                 output.writeInt(1);
398                 output.writeUTF(IpcMessages.RESPONSE_ACQUIRE);
399                 output.flush();
400             }
401         } catch (IOException e) {
402             try {
403                 socket.close();
404             } catch (IOException ioException) {
405                 e.addSuppressed(ioException);
406             }
407             error("Error writing lock response", e);
408         }
409     }
410 
411     private void use() {
412         lastUsed = System.nanoTime();
413     }
414 
415     private void expirationCheck() {
416         while (true) {
417             long current = System.nanoTime();
418             long left = (lastUsed + idleTimeout) - current;
419             if (clients.isEmpty() && left < 0) {
420                 info("IpcServer expired, closing");
421                 close();
422                 break;
423             } else {
424                 try {
425                     Thread.sleep(Math.max(1, TimeUnit.NANOSECONDS.toMillis(left)));
426                 } catch (InterruptedException e) {
427                     info("IpcServer expiration check interrupted, closing");
428                     close();
429                     break;
430                 }
431             }
432         }
433     }
434 
435     void close() {
436         closing = true;
437         try {
438             serverSocket.close();
439         } catch (IOException e) {
440             error("Error closing server socket", e);
441         }
442         clients.forEach((s, t) -> {
443             try {
444                 s.close();
445             } catch (IOException e) {
446                 // ignore
447             }
448             t.interrupt();
449         });
450     }
451 
452     static class Waiter {
453         final Context context;
454         final CompletableFuture<Void> future;
455 
456         Waiter(Context context, CompletableFuture<Void> future) {
457             this.context = context;
458             this.future = future;
459         }
460     }
461 
462     static class Lock {
463 
464         final String key;
465 
466         List<Context> holders;
467         List<Waiter> waiters;
468 
469         Lock(String key) {
470             this.key = key;
471         }
472 
473         public synchronized CompletableFuture<Void> lock(Context context) {
474             if (holders == null) {
475                 holders = new ArrayList<>();
476             }
477             if (holders.isEmpty() || holders.get(0).shared && context.shared) {
478                 holders.add(context);
479                 return CompletableFuture.completedFuture(null);
480             }
481             if (waiters == null) {
482                 waiters = new ArrayList<>();
483             }
484 
485             CompletableFuture<Void> future = new CompletableFuture<>();
486             waiters.add(new Waiter(context, future));
487             return future;
488         }
489 
490         public void unlock(Context context) {
491             List<CompletableFuture<Void>> toComplete;
492             synchronized (this) {
493                 toComplete = new ArrayList<>();
494                 if (holders.remove(context)) {
495                     while (waiters != null
496                             && !waiters.isEmpty()
497                             && (holders.isEmpty() || holders.get(0).shared && waiters.get(0).context.shared)) {
498                         Waiter waiter = waiters.remove(0);
499                         holders.add(waiter.context);
500                         toComplete.add(waiter.future);
501                     }
502                 } else if (waiters != null) {
503                     for (Iterator<Waiter> it = waiters.iterator(); it.hasNext(); ) {
504                         Waiter waiter = it.next();
505                         if (waiter.context == context) {
506                             it.remove();
507                             waiter.future.cancel(false);
508                         }
509                     }
510                 }
511             }
512             toComplete.forEach(f -> f.complete(null));
513         }
514 
515         public synchronized boolean isEmpty() {
516             return (holders == null || holders.isEmpty()) && (waiters == null || waiters.isEmpty());
517         }
518     }
519 
520     class Context {
521 
522         final String id;
523         final boolean shared;
524         final List<String> locks = new CopyOnWriteArrayList<>();
525 
526         Context(boolean shared) {
527             this.id = String.format("%08x", counter.incrementAndGet());
528             this.shared = shared;
529         }
530 
531         public CompletableFuture<?> lock(List<String> keys) {
532             locks.addAll(keys);
533             CompletableFuture<?>[] futures = keys.stream()
534                     .map(k -> IpcServer.this.locks.computeIfAbsent(k, Lock::new))
535                     .map(l -> l.lock(this))
536                     .toArray(CompletableFuture[]::new);
537             return CompletableFuture.allOf(futures);
538         }
539 
540         public void unlock() {
541             locks.stream()
542                     .map(k -> IpcServer.this.locks.computeIfAbsent(k, Lock::new))
543                     .forEach(l -> {
544                         l.unlock(this);
545                         IpcServer.this.locks.compute(l.key, (k, v) -> (v == l && v.isEmpty()) ? null : v);
546                     });
547         }
548     }
549 }