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