You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/10/18 18:25:53 UTC

(maven-mvnd) branch mvnd-1.x updated: Time out the daemon's token read and keep its state on a failed check

This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch mvnd-1.x
in repository https://gitbox.apache.org/repos/asf/maven-mvnd.git


The following commit(s) were added to refs/heads/mvnd-1.x by this push:
     new 486901d4 Time out the daemon's token read and keep its state on a failed check
486901d4 is described below

commit 486901d4f5be34eb79b3522b60b07458dabde84a
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Sun Oct 18 15:51:55 2026 +0200

    Time out the daemon's token read and keep its state on a failed check
---
 .../java/org/mvndaemon/mvnd/daemon/Server.java     | 45 +++++++++++----
 .../org/mvndaemon/mvnd/daemon/ServerTokenTest.java | 67 ++++++++++++++++++++++
 2 files changed, 101 insertions(+), 11 deletions(-)

diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/daemon/Server.java b/daemon/src/main/java/org/mvndaemon/mvnd/daemon/Server.java
index 09a67548..68eb487e 100644
--- a/daemon/src/main/java/org/mvndaemon/mvnd/daemon/Server.java
+++ b/daemon/src/main/java/org/mvndaemon/mvnd/daemon/Server.java
@@ -23,6 +23,8 @@ import java.io.InputStream;
 import java.io.InterruptedIOException;
 import java.lang.reflect.Field;
 import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
 import java.nio.channels.ServerSocketChannel;
 import java.nio.channels.SocketChannel;
 import java.nio.charset.Charset;
@@ -85,6 +87,7 @@ public class Server implements AutoCloseable, Runnable {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(Server.class);
     public static final int CANCEL_TIMEOUT = 10 * 1000;
+    static final long TOKEN_READ_TIMEOUT_MS = 5 * 1000;
 
     private final String daemonId;
     private final boolean noDaemon;
@@ -265,9 +268,8 @@ public class Server implements AutoCloseable, Runnable {
     private void client(SocketChannel socket) {
         LOGGER.info("Client connected");
         if (!checkToken(socket)) {
+            // Leave the state alone: a connection that cannot authenticate must not change it
             LOGGER.error("Received invalid token, dropping connection");
-            updateState(DaemonState.Idle);
-
             return;
         }
 
@@ -312,20 +314,41 @@ public class Server implements AutoCloseable, Runnable {
     }
 
     private boolean checkToken(SocketChannel socket) {
-        byte[] token = new byte[info.getToken().length];
-        ByteBuffer tokenBuffer = ByteBuffer.wrap(token);
+        byte[] token = readToken(socket, info.getToken().length, TOKEN_READ_TIMEOUT_MS);
+        return MessageDigest.isEqual(info.getToken(), token);
+    }
 
+    /**
+     * Reads up to {@code size} bytes, giving up after {@code timeoutMs} so that a peer that connects and sends
+     * nothing cannot hold the (serial) accept loop. Bytes not received stay zero.
+     */
+    static byte[] readToken(SocketChannel socket, int size, long timeoutMs) {
+        byte[] token = new byte[size];
+        ByteBuffer tokenBuffer = ByteBuffer.wrap(token);
         try {
-            do {
-                if (socket.read(tokenBuffer) == -1) {
-                    break;
+            socket.configureBlocking(false);
+            try (Selector selector = Selector.open()) {
+                socket.register(selector, SelectionKey.OP_READ);
+                long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs);
+                while (tokenBuffer.hasRemaining()) {
+                    long left = TimeUnit.NANOSECONDS.toMillis(deadline - System.nanoTime());
+                    if (left <= 0) {
+                        LOGGER.info("No token received within {} ms", timeoutMs);
+                        break;
+                    }
+                    if (selector.select(left) > 0) {
+                        selector.selectedKeys().clear();
+                        if (socket.read(tokenBuffer) == -1) {
+                            break;
+                        }
+                    }
                 }
-            } while (tokenBuffer.remaining() > 0);
+            }
+            socket.configureBlocking(true);
         } catch (final IOException e) {
-            LOGGER.debug("Discarding EOFException: {}", e.toString(), e);
+            LOGGER.debug("Discarding exception while reading the token: {}", e.toString(), e);
         }
-
-        return MessageDigest.isEqual(info.getToken(), token);
+        return token;
     }
 
     private void expirationCheck() {
diff --git a/daemon/src/test/java/org/mvndaemon/mvnd/daemon/ServerTokenTest.java b/daemon/src/test/java/org/mvndaemon/mvnd/daemon/ServerTokenTest.java
new file mode 100644
index 00000000..20c112be
--- /dev/null
+++ b/daemon/src/test/java/org/mvndaemon/mvnd/daemon/ServerTokenTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.mvndaemon.mvnd.daemon;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ServerTokenTest {
+
+    @Test
+    void readsTheFullToken() throws IOException {
+        byte[] sent = new byte[16];
+        for (int i = 0; i < sent.length; i++) {
+            sent[i] = (byte) (i + 1);
+        }
+        try (ServerSocketChannel server = listen();
+                SocketChannel client = SocketChannel.open(server.getLocalAddress());
+                SocketChannel accepted = server.accept()) {
+            client.write(ByteBuffer.wrap(sent));
+            assertArrayEquals(sent, Server.readToken(accepted, 16, 5_000));
+            assertTrue(accepted.isBlocking(), "the channel is handed back in blocking mode");
+        }
+    }
+
+    @Test
+    void givesUpOnASilentPeer() throws IOException {
+        try (ServerSocketChannel server = listen();
+                SocketChannel client = SocketChannel.open(server.getLocalAddress());
+                SocketChannel accepted = server.accept()) {
+            client.write(ByteBuffer.wrap(new byte[] {1, 2, 3}));
+            long start = System.nanoTime();
+            byte[] token = Server.readToken(accepted, 16, 300);
+            long elapsedMs = (System.nanoTime() - start) / 1_000_000;
+            assertTrue(elapsedMs < 5_000, "returned after " + elapsedMs + " ms");
+            assertArrayEquals(new byte[] {1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, token);
+        }
+    }
+
+    private static ServerSocketChannel listen() throws IOException {
+        return ServerSocketChannel.open().bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
+    }
+}