login: probe the world port with a bind test instead of lsof

LoginServer.exec() -> checkServerHealth() -> isPortInUse() shelled out to
`lsof -i tcp:<port>` 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".
This commit is contained in:
evilight
2026-10-10 16:06:55 +08:00
parent 9f3ec7c553
commit 97eff6517e
+30 -17
View File
@@ -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:<port>` 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() {