package net.sf.colossus.webserver;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.colossus.webcommon.GameInfo;
import net.sf.colossus.webcommon.IPortProvider;
/**
* This class keeps track of which ports are currently occupied
* by ongoing games and which are free for new games.
*
* @author Clemens Katzer
*/
public class PortBookKeeper implements IPortProvider
{
private static final Logger LOGGER = Logger.getLogger(PortBookKeeper.class
.getName());
private final int portRangeFrom;
/**
* total nr of ports we are allowed to use according to options file;
* but only every 2nd is used as a game port
*/
private final int totalPorts;
/** Nr of ports that are actually available for game serving
* (so, this value == 5 means there can be 5 games)
*/
private final int gamePorts;
/** Bookkeeping which (game) ports are currently in use
*/
private final ArrayList<GameInfo> portInUse;
/**
* A placeholder for the bookkeping table, if it's somehow used but we
* don't know by what or whom
*/
private final GameInfo NOT_A_REAL_GAME = new GameInfo("00000", true);
public PortBookKeeper(int portRangeStart, int availablePorts)
{
this.portRangeFrom = portRangeStart;
this.totalPorts = availablePorts;
portInUse = new ArrayList<GameInfo>(totalPorts);
for (int i = 0; i < availablePorts; i++)
{
portInUse.add(i, null);
}
int freePorts = 0;
for (int i = 0; i < availablePorts; i += 2)
{
int port = realPortForIndex(i);
boolean free = testWhetherPortFree(port);
if (free)
{
markPortFree(port);
freePorts++;
}
else
{
LOGGER.warning("Free port table initialization: Port " + port
+ " seems to be in use! Marking it as in use.");
markPortUsed(port, NOT_A_REAL_GAME);
}
}
this.gamePorts = freePorts;
}
private int realPortForIndex(int portIndex)
{
return portRangeFrom + portIndex;
}
private int indexForRealPort(int portNumber)
{
return portNumber - portRangeFrom;
}
private void markPortUsed(int portNr, GameInfo gi)
{
portInUse.set(indexForRealPort(portNr), gi);
}
private void markPortFree(int portNr)
{
portInUse.set(indexForRealPort(portNr), null);
}
private GameInfo getGameAtPort(int portNr)
{
return portInUse.get(indexForRealPort(portNr));
}
private boolean isPortInUse(int portNr)
{
return getGameAtPort(portNr) != null;
}
/**
* Get a free port number, chosen randomly; to reduce the risk
* that a resumed game gets same port => clients from suspended
* game trying to connect to this new one.
*/
public int getFreePort(GameInfo gi)
{
Random rand = new Random();
int offset = rand.nextInt(totalPorts) * 2;
String purpose = "game " + gi.getGameId();
int port = -1;
synchronized (portInUse)
{
for (int i = 0; i < totalPorts && port == -1; i += 2)
{
int j = (i + offset) % totalPorts;
int tryPort = realPortForIndex(j);
if (!isPortInUse(tryPort))
{
boolean ok = testThatPortReallyFree(tryPort);
if (ok)
{
markPortUsed(tryPort, gi);
port = tryPort;
}
else
{
LOGGER.log(Level.SEVERE, "port " + tryPort
+ " is supposed to be free "
+ "but test shows it is in use?");
}
}
}
}
if (port > 0)
{
LOGGER
.log(Level.INFO, "Reserved port " + port + " for " + purpose);
}
ensureSomeFreePortsRemain();
return port;
}
public int calculateUsedPorts()
{
return totalPorts / 2 - countFreePorts();
}
public int countFreePorts()
{
int free = 0;
synchronized (portInUse)
{
for (int i = 0; i < totalPorts; i += 2)
{
int tryPort = realPortForIndex(i);
if (!isPortInUse(tryPort))
{
boolean ok = testThatPortReallyFree(tryPort);
if (ok)
{
free++;
}
else
{
LOGGER.log(Level.SEVERE, "countFreePorts: port "
+ tryPort + " is supposed to be free "
+ "but test shows it is in use?");
}
}
}
}
return free;
}
/** Check that it's really free, as expected, log a warning if not */
private boolean testThatPortReallyFree(int port)
{
if (!testWhetherPortFree(port))
{
LOGGER.warning("Port " + port
+ " is supposed to be free but it is not!");
markPortUsed(port, NOT_A_REAL_GAME);
return false;
}
else
{
return true;
}
}
/** Just check it, whether it's free or not */
private boolean testWhetherPortFree(int port)
{
boolean ok = false;
ServerSocket serverSocket = null;
try
{
serverSocket = new ServerSocket(port, 1);
serverSocket.setReuseAddress(true);
serverSocket.close();
ok = true;
}
catch (IOException e)
{
LOGGER.info("Caught IOException "
+ "while attempting to creating socket on port " + port);
String msg = e.getMessage();
if (msg == null || !msg.equals("Address already in use: JVM_Bind"))
{
LOGGER.log(Level.WARNING,
"Unrecognized exception while checking port " + port
+ " whether it is free: ", e);
}
}
return ok;
}
private void ensureSomeFreePortsRemain()
{
int seemsFree = 0;
synchronized (portInUse)
{
for (int i = 0; i < totalPorts; i += 2)
{
if (!isPortInUse(realPortForIndex(i)))
{
seemsFree++;
}
}
}
if (seemsFree < 3)
{
LOGGER.info("Only " + seemsFree
+ " ports are registered as free. Rechecking...");
reCheckPorts();
}
}
private void reCheckPorts()
{
for (int i = 0; i < totalPorts; i += 2)
{
int port = realPortForIndex(i);
boolean shouldBeFree = isPortInUse(port);
boolean free = testWhetherPortFree(port);
if (free != shouldBeFree)
{
LOGGER.warning("Port " + port + " was marked as "
+ (shouldBeFree ? "free" : "not free")
+ " but actually it is " + (free ? "free" : "not free")
+ "! Updating table.");
if (free)
{
markPortFree(port);
}
else
{
markPortUsed(port, NOT_A_REAL_GAME);
}
}
}
}
public void releasePort(GameInfo gi)
{
int port = gi.getPort();
String purpose = "game " + gi.getGameId();
synchronized (portInUse)
{
GameInfo supposedGI = portInUse.get(indexForRealPort(port));
int index = indexForRealPort(port);
if (index < 0 || index > totalPorts)
{
LOGGER.log(Level.WARNING, "attempt to release invalid port "
+ port + " (index = " + index + ")!");
}
else if (supposedGI == null)
{
LOGGER.log(Level.WARNING, "attempt to release port " + port
+ " (" + purpose
+ ") but port book keeper has not marked port as used!");
}
else if (supposedGI != gi)
{
LOGGER.log(Level.WARNING, "attempt to release port " + port
+ " (" + purpose
+ ") but port book keeper thinks it's used by "
+ "a different game: " + supposedGI.getGameId());
}
else if (!testWhetherPortFree(port))
{
LOGGER.log(Level.WARNING, "attempt to release port " + port
+ " (" + purpose
+ ") but test indicates that it is still in use!");
}
else
{
markPortFree(port);
LOGGER.info("Released port " + port + " (" + purpose + ")");
}
}
}
private String buildPortTableReport()
{
StringBuilder sb = new StringBuilder("");
synchronized (portInUse)
{
for (int i = 0; i < totalPorts; i += 2)
{
int tryPort = realPortForIndex(i);
if (sb.length() != 0)
{
sb.append(", ");
}
sb.append(tryPort + ": ");
GameInfo gi = getGameAtPort(tryPort);
if (gi == null)
{
sb.append("free");
}
else
{
sb.append(gi.getGameId());
}
}
}
return sb.toString();
}
public String getStatus()
{
StringBuilder st = new StringBuilder();
st.append("Ports configured/available for games: " + totalPorts + "/"
+ gamePorts + "; still free for games: " + countFreePorts() + "\n");
st.append("Port usage: " + buildPortTableReport());
st.append("\n");
return st.toString();
}
}