001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.named.ipc;
020
021import java.io.BufferedReader;
022import java.io.DataInputStream;
023import java.io.DataOutputStream;
024import java.io.IOException;
025import java.io.InputStreamReader;
026import java.net.SocketAddress;
027import java.nio.channels.ByteChannel;
028import java.nio.channels.Channels;
029import java.nio.channels.ServerSocketChannel;
030import java.nio.channels.SocketChannel;
031import java.nio.charset.StandardCharsets;
032import java.util.ArrayList;
033import java.util.Iterator;
034import java.util.List;
035import java.util.Map;
036import java.util.concurrent.CompletableFuture;
037import java.util.concurrent.ConcurrentHashMap;
038import java.util.concurrent.CopyOnWriteArrayList;
039import java.util.concurrent.TimeUnit;
040import java.util.concurrent.atomic.AtomicInteger;
041
042import org.slf4j.LoggerFactory;
043
044/**
045 * Implementation of the server side.
046 * The server instance is bound to a given maven repository.
047 *
048 * @since 2.0.1
049 */
050public class IpcServer {
051    static volatile boolean forkedProcess;
052
053    /**
054     * Should the IPC server not fork? (i.e. for testing purposes)
055     *
056     * @configurationSource {@link System#getProperty(String, String)}
057     * @configurationType {@link java.lang.Boolean}
058     * @configurationDefaultValue {@link #DEFAULT_NO_FORK}
059     */
060    public static final String SYSTEM_PROP_NO_FORK = "aether.named.ipc.nofork";
061
062    public static final boolean DEFAULT_NO_FORK = false;
063
064    /**
065     * IPC idle timeout in seconds. If there is no IPC request during idle time, it will stop.
066     *
067     * @configurationSource {@link System#getProperty(String, String)}
068     * @configurationType {@link java.lang.Integer}
069     * @configurationDefaultValue {@link #DEFAULT_IDLE_TIMEOUT}
070     */
071    public static final String SYSTEM_PROP_IDLE_TIMEOUT = "aether.named.ipc.idleTimeout";
072
073    public static final int DEFAULT_IDLE_TIMEOUT = 300;
074
075    /**
076     * IPC socket family to use.
077     *
078     * @configurationSource {@link System#getProperty(String, String)}
079     * @configurationType {@link java.lang.String}
080     * @configurationDefaultValue {@link #DEFAULT_FAMILY}
081     */
082    public static final String SYSTEM_PROP_FAMILY = "aether.named.ipc.family";
083
084    public static final String DEFAULT_FAMILY = "unix";
085
086    /**
087     * Should the IPC server not use native executable?
088     *
089     * @configurationSource {@link System#getProperty(String, String)}
090     * @configurationType {@link java.lang.Boolean}
091     * @configurationDefaultValue {@link #DEFAULT_NO_NATIVE}
092     */
093    public static final String SYSTEM_PROP_NO_NATIVE = "aether.named.ipc.nonative";
094
095    public static final boolean DEFAULT_NO_NATIVE = true;
096
097    /**
098     * The name if the IPC server native executable (without file extension like ".exe")
099     *
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}