From af24e74cf665807fb1e3d327c008a428128f2bca Mon Sep 17 00:00:00 2001 From: JackieTien97 Date: Wed, 4 Nov 2026 08:43:23 +0800 Subject: [PATCH] [To dev/1.3] [IT] Fix port occupation check that always reported ports as free EnvUtils.checkPortsAvailable() passed a shell pipeline to Runtime.exec(String), which does not use a shell: "lsof -iTCP -sTCP:LISTEN,TIME_WAIT -P -n | awk '{print $9}' | grep -E ...\"" on Unix and "netstat -aon -p tcp | findStr ..." on Windows. lsof received "|", "awk", ... as file names and exited with 1, which the method treats as "available". Every port range was therefore accepted even when one of its ports was in use, and a node could fail with BindException. Port the master implementation, which runs lsof / netstat without a shell and parses the output in Java: - the EnvUtils part of #15794 (88abd3e309): checkPortsAvailable() now delegates to listPortOccupation(...); - #19070 (2fd7d838cb): read the lsof address from column 8 instead of the state column 9. The port-check code now matches master. Adaptations: - explicit java.io imports instead of java.io.* (checkstyle AvoidStarImport); - the lsof state filter stays LISTEN,TIME_WAIT, as already on dev/1.3 (#17618) and on master (#17551); - getSearchAvailablePortCmd, getWindowsSearchPortCmd and getUnixSearchPortCmd become unused and are removed, as master did in #17551. The rest of #15794 (cluster restart handling and server-side changes) is not part of this change. --- .../apache/iotdb/it/env/cluster/EnvUtils.java | 81 +++++++++++++++---- 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/EnvUtils.java b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/EnvUtils.java index 136399a3778b0..511f4d62640d6 100644 --- a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/EnvUtils.java +++ b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/EnvUtils.java @@ -24,9 +24,14 @@ import org.apache.commons.lang3.SystemUtils; import org.apache.tsfile.utils.Pair; +import java.io.BufferedReader; +import java.io.EOFException; import java.io.File; import java.io.IOException; +import java.io.InputStreamReader; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -96,30 +101,74 @@ public static int[] searchAvailablePorts() { } private static boolean checkPortsAvailable(final List ports) { - final String cmd = getSearchAvailablePortCmd(ports); try { - return Runtime.getRuntime().exec(cmd).waitFor() == 1; - } catch (final IOException ignore) { - // ignore - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); + return listPortOccupation(ports).isEmpty(); + } catch (IOException e) { + IoTDBTestLogger.logger.error("Cannot check available ports", e); + return false; } - return false; } - private static String getSearchAvailablePortCmd(final List ports) { - return SystemUtils.IS_OS_WINDOWS ? getWindowsSearchPortCmd(ports) : getUnixSearchPortCmd(ports); + public static Map listPortOccupation(final List ports) + throws IOException { + return SystemUtils.IS_OS_WINDOWS + ? listPortOccupationWindows(ports) + : listPortOccupationUnix(ports); } - private static String getWindowsSearchPortCmd(final List ports) { - return "netstat -aon -p tcp | findStr " - + ports.stream().map(v -> "/C:'127.0.0.1:" + v + "'").collect(Collectors.joining(" ")); + public static Map listPortOccupation( + final List ports, + String cmd, + int targetColumnLength, + int addressColumnIndex, + int pidColumnIndex) + throws IOException { + Process process = Runtime.getRuntime().exec(cmd); + Map result = new HashMap<>(); + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + String[] split = line.trim().split("\\s+"); + if (split.length != targetColumnLength) { + continue; + } + String localAddress = split[addressColumnIndex]; + for (Integer port : ports) { + if (localAddress.endsWith(":" + port)) { + result.put(port, Long.parseLong(split[pidColumnIndex])); + break; + } + } + } + } catch (EOFException ignored) { + } + return result; + } + + /** + * List occupied port and the associated pid on windows. + * + * @param ports ports to be checked + * @return (occupiedPort, pid) pairs + */ + public static Map listPortOccupationWindows(final List ports) + throws IOException { + return listPortOccupation(ports, "netstat -aon -p tcp", 5, 1, 4); } - private static String getUnixSearchPortCmd(final List ports) { - return "lsof -iTCP -sTCP:LISTEN,TIME_WAIT -P -n | awk '{print $9}' | grep -E " - + ports.stream().map(String::valueOf).collect(Collectors.joining("|")) - + "\""; + /** + * List occupied port and the associated pid on Unix. + * + *

A line of the lsof output splits into 10 columns, e.g. {@code java 1234 user 77u IPv6 0x1a2b + * 0t0 TCP 127.0.0.1:6667 (LISTEN)}. The address is in column 8 and the state in column 9. + * + * @param ports ports to be checked + * @return (occupiedPort, pid) pairs + */ + public static Map listPortOccupationUnix(final List ports) + throws IOException { + return listPortOccupation(ports, "lsof -iTCP -sTCP:LISTEN,TIME_WAIT -P -n", 10, 8, 1); } private static Pair getClusterNodesNum(final int index) {