From 97eff6517e5eb77c21c4b9c24d9efc0bdca9aa1c Mon Sep 17 00:00:00 2001 From: evilight Date: Sat, 10 Oct 2026 16:06:55 +0800 Subject: [PATCH] login: probe the world port with a bind test instead of lsof LoginServer.exec() -> checkServerHealth() -> isPortInUse() shelled out to `lsof -i tcp:` through /bin/bash and read the child's stdout until EOF. There is no timeout and no waitFor(), so a child that blocks is fatal: in the container lsof was observed stuck in uninterruptible I/O (D state) while it was scanning the busy world server. The login server then never returned from its startup path, the accept loop never started, and clients could open a TCP connection to 6000 and wait forever for the 100 byte DH handshake, surfacing as "Failed to open a server connection" after the client's own timeout. A container restart was the only way out. Replace the external probe with a bind probe: it cannot hang, needs no external tool, and SO_REUSEADDR still does not allow stealing a port that is actively listening, so a successful bind keeps meaning "free". --- src/engine/server/login/LoginServer.java | 47 +++++++++++++++--------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/engine/server/login/LoginServer.java b/src/engine/server/login/LoginServer.java index e0ab95ee..4802652d 100644 --- a/src/engine/server/login/LoginServer.java +++ b/src/engine/server/login/LoginServer.java @@ -150,29 +150,42 @@ public class LoginServer { } + /** + * Returns true when something is already listening on the given TCP port. + * + * This used to shell out to `lsof -i tcp:` through /bin/bash and read the + * child's output until EOF. With no timeout and no waitFor() a child that blocks + * is fatal: lsof has been observed stuck in uninterruptible I/O (D state) while + * scanning the busy world server inside the container, which left the login + * server permanently inside its startup path. LoginServer.exec() never returned + * from checkServerHealth(), the accept loop never started, and clients could open + * a TCP connection to the port and then wait forever for the 100 byte DH + * handshake - surfacing as "Failed to open a server connection" after the + * client's own timeout. + * + * A bind probe is equivalent for this purpose, needs no external tool and cannot + * hang. SO_REUSEADDR does not allow stealing a port that is actively listening, + * so a successful bind still means the port is free. + */ public static boolean isPortInUse(int port) { - ProcessBuilder builder = new ProcessBuilder("/bin/bash", "-c", "lsof -i tcp:" + port + " | tail -n +2 | awk '{print $2}'"); - builder.redirectErrorStream(true); - Process process = null; - String line = null; - boolean portInUse = false; + java.net.ServerSocket probe = null; try { - process = builder.start(); - - InputStream is = process.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - - while ((line = reader.readLine()) != null) { - portInUse = true; - } - + probe = new java.net.ServerSocket(); + probe.setReuseAddress(true); + probe.bind(new java.net.InetSocketAddress(port)); + return false; } catch (IOException e) { - e.printStackTrace(); + return true; + } finally { + if (probe != null) + try { + probe.close(); + } catch (IOException e) { + // the bind result is what matters; closing cannot change it + } } - - return portInUse; } private void exec() {