package net.sf.colossus.webserver;
import java.awt.GraphicsEnvironment;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sf.colossus.util.BuildInfo;
import net.sf.colossus.webclient.WebClient;
import net.sf.colossus.webcommon.FormatWhen;
import net.sf.colossus.webcommon.GameInfo;
import net.sf.colossus.webcommon.GameInfo.GameState;
import net.sf.colossus.webcommon.IColossusMail;
import net.sf.colossus.webcommon.IGameRunner;
import net.sf.colossus.webcommon.IPortProvider;
import net.sf.colossus.webcommon.IRunWebServer;
import net.sf.colossus.webcommon.IWebClient;
import net.sf.colossus.webcommon.IWebServer;
import net.sf.colossus.webcommon.User;
import net.sf.colossus.webcommon.UserDB;
/**
* The main class for the WebServer
* - brings up the WebServer GUI
* - starts the ServerSocket and listens there for WebClients
* - based on actions coming from clients, keeps book of
* "instant" and "running" games (both GameInfo objects),
* and tell the GameInfo objects when to start the game.
*
* @author Clemens Katzer
*/
public class WebServer implements IWebServer, IRunWebServer
{
private static final Logger LOGGER = Logger.getLogger(WebServer.class
.getName());
private WebServerOptions options = null;
private PortBookKeeper portBookKeeper = null;
private IWebServerGUI gui = null;
private final IColossusMail mailObject;
private final static int MIN_FREE_GAME_PORTS = 5;
private final int INACTIVITY_CHECK_INTERVAL = 10;
private final int INACTIVITY_WARNING_INTERVAL = 30;
private final int INACTIVITY_TIMEOUT = 90;
private final static ArrayList<String> loginMessage = new ArrayList<String>();
/**
* Controls whether the GUI is shown or not.
*
* At the moment this is configured only by the possibility of doing so:
* if the environment supports running a GUI, we will, if not, we won't.
* It could be combined with a command line option to suppress the GUI
* even if it would be possible to show one.
*/
private final boolean runGUI = !GraphicsEnvironment.isHeadless();
private boolean shutdownRequested = false;
private String proposedGamesFilename;
private boolean proposedGamesListModified = false;
private final int maxClients;
private UserDB userDB;
private final HashMap<String, GameInfo> allGames = new HashMap<String, GameInfo>();
private final ArrayList<GameInfo> proposedGames = new ArrayList<GameInfo>();
private final ArrayList<GameInfo> runningGames = new ArrayList<GameInfo>();
private final ArrayList<GameInfo> suspendedGames = new ArrayList<GameInfo>();
private final ArrayList<GameInfo> endingGames = new ArrayList<GameInfo>();
// Used also as separator for storing proposed games to file:
private final static String sep = IWebServer.WebProtocolSeparator;
/** Server port where we listen for WebClient connections */
private final int serverPort;
/** Server actual socket where we listen for WebClient connections */
private ServerSocket serverSocket;
private final ChatChannel generalChat;
private final ClientWatchDog watchDog;
private final FormatWhen whenFormatter;
private PrintWriter dumpInfoFile;
public static void main(String[] args)
{
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
LOGGER.log(
Level.INFO,
"Start for ColossusWeb version '"
+ BuildInfo.getFullBuildInfoString() + "' at "
+ dateFormat.format(new Date()));
String optionsFileName = WebServerConstants.defaultOptionsFilename;
if (args.length > 0)
{
optionsFileName = args[0];
}
WebServer server = new WebServer(optionsFileName);
LOGGER.log(Level.FINEST, "before init socket");
server.runSocketServer();
// execution comes to here only when server is shut down
// (shutDownRequested set to true, so that the loop quits)
server = null;
LOGGER.log(Level.ALL, "WebServer.main() will end...");
// JVM should do a clean exit now, no System.exit() needed.
// @TODO: does it work on all platforms, all Java versions?
// If not, build here a demon timer that does the
// System.exit() after a few seconds...?
}
public WebServer(String optionsFile)
{
this.options = new WebServerOptions(optionsFile);
options.loadOptions();
this.whenFormatter = new FormatWhen();
try
{
PrintWriter p = new PrintWriter(new FileOutputStream(
"dump-info.txt", true));
dumpInfoFile = p;
dumpInfoFile.println("");
dumpInfoFile.println("WebServer started.");
}
catch (IOException e)
{
LOGGER.warning("Can't create/append to dump-info.txt");
dumpInfoFile = null;
}
this.serverPort = options
.getIntOptionNoUndef(WebServerConstants.optServerPort);
this.maxClients = options
.getIntOptionNoUndef(WebServerConstants.optMaxClients);
int portRangeFrom = options
.getIntOptionNoUndef(WebServerConstants.optPortRangeFrom);
int availablePorts = options
.getIntOptionNoUndef(WebServerConstants.optAvailablePorts);
if (availablePorts % 2 != 0)
{
LOGGER.warning("Suspicious option value " + availablePorts
+ " for available ports - should be an even (nr % 2 == 0) "
+ "value (every 2nd port is used as file serving port)!");
}
int availableGamePorts = availablePorts / 2;
if (availableGamePorts < MIN_FREE_GAME_PORTS)
{
LOGGER
.severe("Available ports from cf file is "
+ availableGamePorts
+ " but according to MIN_FREE_GAME_PORTS it should be at least "
+ MIN_FREE_GAME_PORTS + "! Exiting.");
System.exit(0);
}
mailObject = new ColossusMail(options);
portBookKeeper = new PortBookKeeper(portRangeFrom, availablePorts);
int freePorts = portBookKeeper.countFreePorts();
LOGGER.info("Actually free ports: " + freePorts);
if (freePorts < MIN_FREE_GAME_PORTS)
{
LOGGER.severe("Too few (only " + freePorts
+ ") free playing ports! Exiting.");
System.exit(0);
}
if (freePorts < availableGamePorts)
{
LOGGER.warning("Only " + freePorts + " free ports, instead of "
+ availableGamePorts);
}
else if (freePorts < (availablePorts / 2))
{
LOGGER.severe("Not even half the amount of expected ports ("
+ freePorts + " of " + availablePorts + ") is free!");
System.exit(0);
}
LOGGER.log(Level.INFO, "Server started: port " + serverPort
+ ", maxClients " + maxClients);
doReadUsersFromFile();
doReadGamesFromFile();
doReadLoginMessage();
this.generalChat = new ChatChannel(IWebServer.generalChatName,
options, userDB);
generalChat.createWelcomeMessage();
if (runGUI)
{
this.gui = new WebServerGUI(this);
}
else
{
this.gui = new NullWebServerGUI();
}
watchDog = new ClientWatchDog(userDB);
watchDog.start();
updateGUI();
/*
boolean runGameConsole = false;
if (runGameConsole)
{
console();
}
*/
LOGGER.log(Level.FINEST, "WebServer instantiated, maxClients = "
+ maxClients + " , port = " + serverPort);
}
/**
*
*/
private void doReadGamesFromFile()
{
proposedGamesFilename = options
.getStringOption(WebServerConstants.optGamesFile);
if (proposedGamesFilename == null)
{
proposedGamesFilename = WebServerConstants.DEFAULT_GAMES_FILE;
LOGGER
.warning("Filename for storing games not defined in cfg file!"
+ " Using default " + proposedGamesFilename);
}
readGamesFromFile(proposedGamesFilename);
}
/**
*
*/
private void doReadUsersFromFile()
{
String usersFile = options
.getStringOption(WebServerConstants.optUsersFile);
int maxUsers = options.getIntOption(WebServerConstants.optMaxUsers);
userDB = new UserDB(usersFile, maxUsers);
}
/**
*
*/
private void doReadLoginMessage()
{
String LoginMessageFilename = options
.getStringOption(WebServerConstants.optLoginMessageFile);
if (LoginMessageFilename != null)
{
readLoginMessageFromFile(LoginMessageFilename);
}
}
/**
* Triggered by remode admin connection
*/
public void rereadLoginMessage()
{
doReadLoginMessage();
}
void runSocketServer()
{
int socketQueueLen = options
.getIntOptionNoUndef(WebServerConstants.optSocketQueueLen);
LOGGER.log(Level.FINE, "About to create web server socket on port "
+ serverPort);
try
{
if (serverSocket != null)
{
serverSocket.close();
serverSocket = null;
}
serverSocket = new ServerSocket(serverPort, socketQueueLen);
serverSocket.setReuseAddress(true);
}
catch (IOException ex)
{
LOGGER.log(Level.SEVERE, "Could not create socket on port "
+ serverPort + ": " + ex.getMessage());
System.exit(1);
}
LOGGER.log(Level.INFO, "User-server started, waiting for clients");
while (!shutdownRequested)
{
boolean rejected = waitForUser();
if (rejected)
{
LOGGER.log(Level.WARNING, "accepted one client but "
+ "rejected it - maxClients limit reached.");
}
}
writeBackUsers();
userDB.cleanup();
generalChat.dispose();
gui.shutdown();
gui = null;
options = null;
portBookKeeper = null;
LOGGER.log(Level.FINE, "Web Server after main loop.");
}
public ChatChannel getGeneralChat()
{
return this.generalChat;
}
public void writeBackUsers()
{
userDB.storeUsersToFile();
}
public void updateLoggedinStatus(User u, WebServerClient wsc)
{
userDB.updateLoggedinStatus(u, wsc);
}
// called by WebServerGUI.closeWindow() event
// OR by WebServerSocketThread.shutdownServer().
// If the latter ( = admin user requested it remotely), need to close
// also the GUI window -- if there is one.
public void initiateShutdown(String byUserName)
{
if (byUserName == null)
{
LOGGER.info("Web Server shut down by GUI");
}
else
{
LOGGER.info("Web server shut down remotely by user '" + byUserName
+ "'");
}
try
{
shutdownServer();
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "exception in initiateShutdown() ", e);
}
}
public void shutdownServer()
{
shutdownRequested = true;
watchDog.shutdown();
closeAllWebServerClientSocketThreads();
makeDummyConnection();
}
public void makeDummyConnection()
{
// make a dummy connection, to get the thread out of the
// accept().
try
{
Socket socket = new Socket("localhost", serverPort);
socket.close();
}
// UnknownHostException, IOException, IllegalBlockingModeException
catch (Exception e)
{
LOGGER.log(Level.WARNING, "exception in makeDummyConnection", e);
}
}
private boolean waitForUser()
{
boolean rejected = false;
Socket clientSocket = null;
try
{
clientSocket = serverSocket.accept();
LOGGER.log(Level.FINEST, "Got client connection from IP: "
+ clientSocket.getInetAddress().toString());
if (shutdownRequested)
{
serverSocket.close();
return false;
}
else if (userDB.getLoggedInCount() >= maxClients)
{
rejected = true;
reject(clientSocket);
}
else
{
// ok, can log in
}
if (!rejected)
{
WebServerClient client = new WebServerClient(this,
clientSocket);
client.startThread();
updateUserCounts();
}
}
catch (IOException ex)
{
if (shutdownRequested)
{
LOGGER.log(Level.SEVERE,
"Waiting for user did throw exception: " + ex.toString());
}
else
{
// does not matter
LOGGER.log(
Level.FINEST,
"ShutdownRequested, closing caused an exception: "
+ ex.toString());
}
return false;
}
catch (Throwable any)
{
LOGGER.log(Level.SEVERE,
"!!! WebServer waitForUser loop caught throwable: ", any);
}
return rejected;
}
private void reject(Socket socket)
{
try
{
PrintWriter rejectedClientWriter = new PrintWriter(
new BufferedWriter(new OutputStreamWriter(
socket.getOutputStream())), true);
rejectedClientWriter.println(IWebClient.tooManyUsers);
rejectedClientWriter.println(IWebClient.connectionClosed);
// give client some time to process the response
try
{
Thread.sleep(500);
}
catch (InterruptedException e)
{
/* ignore */
}
socket.close();
}
catch (IOException ex)
{
LOGGER.log(Level.WARNING,
"Rejecting a user did throw exception: ", ex);
}
}
private void closeAllWebServerClientSocketThreads()
{
Collection<User> users = userDB.getLoggedInUsers();
for (User u : users)
{
u.updateLastLogout();
// TODO: should this really deal directly with the actual
// WebServerClientSocketThread, or indirectly via the
// WebServerClient instead?
WebServerClient wsc = (WebServerClient)u.getWebserverClient();
WebServerClientSocketThread cst = wsc.getWSCSThread();
if (cst == null)
{
LOGGER.log(Level.FINE,
"Thread for user is empty - skipping interrupt and join.");
continue;
}
cst.tellToTerminate();
LOGGER.log(Level.FINEST,
"WebServer.closeAllWebServerClientSocketThreads: before join");
try
{
cst.join();
}
catch (InterruptedException e)
{
LOGGER.log(Level.FINE,
"thread.join() interrupted?? Ignoring it.", e);
}
}
}
/** Each user server thread's name is set to it user's name. */
String getPlayerName()
{
return Thread.currentThread().getName();
}
public UserDB getUserDB()
{
return userDB;
}
public User findUserByName(String name)
{
return userDB.findUserByName(name);
}
public String verifyLogin(String username, String password)
{
return userDB.verifyLogin(username, password);
}
public PortBookKeeper getPortBookKeeper()
{
return this.portBookKeeper;
}
public IPortProvider getPortProvider()
{
return this.portBookKeeper;
}
public void updateGUI()
{
if (gui == null)
{
// skip it if called too early or already going down...
return;
}
gui.setScheduledGamesInfo(countProposedGames(true)
+ " scheduled games stored");
gui.setInstantGamesInfo(countProposedGames(false)
+ " instant games stored");
gui.setRunningGamesInfo(runningGames.size() + " running games");
gui.setEndingGamesInfo(endingGames.size() + " games just ending");
gui.setSuspendedGamesInfo(suspendedGames.size() + " games suspended");
gui.setUsedPortsInfo(portBookKeeper.calculateUsedPorts()
+ " ports in use");
}
public void watchGame(String gameId, String userName)
{
LOGGER.info("Got request from client " + userName + " to watch game "
+ gameId);
GameInfo gi = findFromRunningGames(gameId);
if (gi != null && gi.isRunning())
{
String host = gi.getHostingHost();
int port = gi.getPort();
LOGGER.info("Sending connect info: host=" + host + ", port="
+ port);
sendWatchGameInfo(userName, gameId, host, port);
}
else
{
LOGGER.warning("No running game with id " + gameId + " found!");
}
}
public void sendWatchGameInfo(String userName, String gameId, String host,
int port)
{
IWebClient client = null;
String reasonFail = null;
User user = userDB.findUserByName(userName);
if (user != null)
{
client = user.getWebserverClient();
if (client != null)
{
client.watchGameInfo(gameId, host, port);
}
else
{
reasonFail = "User " + userName + " is not online";
}
}
else
{
reasonFail = "Unknown user '" + userName + "'";
}
if (reasonFail != null)
{
LOGGER.warning("Sending watchGameInfo for game " + gameId
+ " to user " + userName + " failed: " + reasonFail);
}
}
/**
* Dump server state info into a static file, at the moment dump-info.txt
*/
public void dumpInfo()
{
if (dumpInfoFile != null)
{
dumpInfo(dumpInfoFile);
dumpInfoFile.flush();
}
else
{
LOGGER.warning("Can't dump info because dumpInfoFile is null!");
}
}
/**
* Dump info into given PrintWriter
* @param pw PrintWriter for the file to print dump to
*/
public void dumpInfo(PrintWriter pw)
{
long now = new Date().getTime();
String nowString = whenFormatter.timeAndDateAsString(now);
pw.println("Dump at " + nowString);
StringBuilder ul = new StringBuilder();
Collection<User> users = userDB.getLoggedInUsers();
for (User u : users)
{
String name;
if (u != null)
{
name = u.getName();
}
else
{
name = "<null user?>";
}
if (ul.length() > 0)
{
ul.append(", ");
}
ul.append(name);
}
pw.println(users.size() + " users connected: " + ul.toString());
pw.println("All games:");
for (GameInfo gi : allGames.values())
{
pw.println(" #" + gi.getGameId() + ", state "
+ gi.getStateString());
}
ArrayList<String> igList = new ArrayList<String>();
ArrayList<String> sgList = new ArrayList<String>();
for (GameInfo gi : proposedGames)
{
(gi.isScheduledGame() ? sgList : igList).add(gi.getGameId());
}
pw.println(sgList.size() + " proposed scheduled games stored: "
+ sgList.toString());
pw.println(igList.size() + " proposed instant games stored:"
+ igList.toString());
ArrayList<String> temporaryGameList = new ArrayList<String>();
for (GameInfo gi : runningGames)
{
temporaryGameList.add(gi.getGameId());
}
pw.println(runningGames.size() + " running games: "
+ temporaryGameList.toString());
temporaryGameList.clear();
for (GameInfo gi : suspendedGames)
{
temporaryGameList.add(gi.getGameId());
}
pw.println(suspendedGames.size() + " suspended games: "
+ temporaryGameList.toString());
temporaryGameList.clear();
for (GameInfo gi : endingGames)
{
temporaryGameList.add(gi.getGameId());
}
pw.println(endingGames.size() + " games just ending: "
+ temporaryGameList.toString());
pw.println(portBookKeeper.getStatus());
pw.println("");
pw.println("");
}
private GameInfo isInvolvedInInstantGame(String initiatorName)
{
ArrayList<GameInfo> games = new ArrayList<GameInfo>(allGames.values());
for (GameInfo gi : games)
{
if (!gi.isScheduledGame()
&& (gi.isEnrolled(initiatorName) || gi.getInitiator().equals(
initiatorName))
// This relies on that that the client side does not allow
// proposing if one is actually still playing; but allow it
// here, because we can't distinct still playing from being
// dead, and thus one could never propose as long as the other
// game is still ongoing.
// TODO: fix when webserver has better knowledge of game
// state, i.e. which players are alive in which game...
&& !gi.getGameState().equals(GameState.RUNNING)
&& !gi.getGameState().equals(GameState.SUSPENDED)
&& !gi.getGameState().equals(GameState.DELETED))
{
return gi;
}
}
return null;
}
public GameInfo proposeGame(String initiator, String variant,
String viewmode, long startAt, int duration, String summary,
String expire, List<String> gameOptions, List<String> teleportOptions,
int min, int target, int max)
{
if (GameInfo.wouldBeInstantGame(startAt))
{
GameInfo involvedGame = isInvolvedInInstantGame(initiator);
if (involvedGame != null)
{
LOGGER.warning("User " + initiator
+ " proposes instant game, "
+ "but user is already involved in instant game "
+ involvedGame.getGameId() + "!");
// no game created from proposal
return null;
}
else
{
LOGGER.info("User " + initiator + " proposed instant game "
+ "- not involved in game yet, so that's ok!");
}
}
GameInfo gi = new GameInfo(initiator, variant, viewmode, startAt,
duration, summary, expire, gameOptions, teleportOptions, min,
target, max);
String scheduleType = gi.isScheduledGame() ? "scheduled" : "instant";
LOGGER.info("Game " + gi.getGameId() + " (" + scheduleType
+ ") was proposed by " + initiator + ". Adding to list.");
allGames.put(gi.getGameId(), gi);
proposedGames.add(gi);
proposedGamesListModified = true;
updateGUI();
allTellGameInfo(gi);
systemMessageToAll("Game " + gi.getGameId() + " (" + scheduleType
+ ") was proposed by " + initiator + ": " + gi.getVariant() + ", "
+ gi.getViewmode());
return gi;
}
public void cancelIfNecessary(User user)
{
LOGGER.fine("Checking if any cancelling is needed for user "
+ user.getName());
ArrayList<GameInfo> games = new ArrayList<GameInfo>(allGames.values());
for (GameInfo gi : games)
{
if (gi.getInitiator().equals(user.getName())
&& !gi.isScheduledGame()
&& !gi.getGameState().equals(GameState.RUNNING)
&& !gi.getGameState().equals(GameState.SUSPENDED)
&& !gi.getGameState().equals(GameState.ENDING))
{
LOGGER.info("Auto-cancelling instant game " + gi.getGameId()
+ ", state=" + gi.getGameState().toString()
+ " because initiator " + user.getName()
+ " is going to be gone...");
cancelGame(gi.getGameId(), user.getName());
}
}
// if players connection broke unexpectedly, due to the exception the
// saveGameIsNeeded at the end of parseLine will not be done
saveGamesIfNeeded();
}
public void reEnrollIfNecessary(WebServerClient newclient)
{
IWebClient client = newclient;
User newUser = newclient.getUser();
ArrayList<GameInfo> games = new ArrayList<GameInfo>(allGames.values());
for (GameInfo gi : games)
{
if (gi.reEnrollIfNecessary(newUser) && gi.isProposedOrDue())
{
LOGGER.log(Level.FINEST, "Telling user " + newUser.getName()
+ " that he is still enrolled in game " + gi.getGameId());
// userMap finds already new user for that name
LOGGER.fine("Player " + newUser.getName()
+ " re-enrolled to game " + gi.getGameId());
client.didEnroll(gi.getGameId(), newUser.getName());
allTellGameInfo(gi);
}
}
}
public void tellAllGamesFromListToOne(WebServerClient client,
ArrayList<GameInfo> games)
{
Iterator<GameInfo> it = games.iterator();
while (it.hasNext())
{
GameInfo gi = it.next();
client.gameInfo(gi);
}
}
public void tellAllProposedGamesToOne(WebServerClient client)
{
tellAllGamesFromListToOne(client, proposedGames);
}
public void tellAllRunningGamesToOne(WebServerClient client)
{
tellAllGamesFromListToOne(client, runningGames);
}
public void tellAllSuspendedGamesToOne(WebServerClient client)
{
tellAllGamesFromListToOne(client, suspendedGames);
}
public void allTellGameInfo(GameInfo gi)
{
Collection<User> users = userDB.getLoggedInUsers();
for (User u : users)
{
IWebClient client = u.getWebserverClient();
if (client != null)
{
client.gameInfo(gi);
}
}
}
public void tellEnrolledGameStartsSoon(GameInfo gi)
{
String gameId = gi.getGameId();
ArrayList<User> players = gi.getPlayers();
Iterator<User> it = players.iterator();
// should have been set, but who knows...
String byUserName = "unknown";
User byUser = gi.getStartingUser();
if (byUser != null)
{
byUserName = byUser.getName();
}
while (it.hasNext())
{
User u = it.next();
IWebClient client = u.getWebserverClient();
if (client != null)
{
LOGGER.finest("Sending gameStartsSoon to client for user "
+ u.getName());
client.gameInfo(gi);
client.gameStartsSoon(gameId, byUserName);
((WebServerClient)client).requestPingNow();
}
else
{
LOGGER.warning("getThread for user " + u.getName()
+ " (of game " + gi.getGameId()
+ ") returned null client!");
}
}
}
public void tellEnrolledGameStartsNow(GameInfo gi, String host, int port)
{
String gameId = gi.getGameId();
gi.setState(GameState.READY_TO_CONNECT);
ArrayList<User> players = gi.getPlayers();
Iterator<User> it = players.iterator();
while (it.hasNext())
{
User u = it.next();
IWebClient client = u.getWebserverClient();
if (client != null)
{
client.gameInfo(gi);
LOGGER.info("sending gamestartsnow to client " + u.getName());
client.gameStartsNow(gameId, port, host,
INACTIVITY_CHECK_INTERVAL, INACTIVITY_WARNING_INTERVAL,
INACTIVITY_TIMEOUT);
((WebServerClient)client).requestPingNow();
}
else
{
LOGGER.warning("getThread for user " + u.getName()
+ " (of game " + gi.getGameId()
+ ") returned null client!");
}
}
}
public void gameStarted(GameInfo gi)
{
gi.setState(GameState.RUNNING);
proposedGames.remove(gi);
suspendedGames.remove(gi);
runningGames.add(gi);
proposedGamesListModified = true;
updateGUI();
allTellGameInfo(gi);
}
public void gameFailed(GameInfo gi, String reason)
{
LOGGER.log(Level.WARNING, "GAME starting/running failed!!! Reason: "
+ reason);
}
// =========== Client actions ==========
public void enrollUserToGame(String gameId, String username)
{
User user = userDB.findUserByName(username);
GameInfo gi = findByGameId(gameId);
if (gi != null)
{
synchronized (gi)
{
if (!gi.isStarting())
{
String reasonFail = gi.enroll(user);
proposedGamesListModified = true;
if (reasonFail == null)
{
updateOnline(gi);
allTellGameInfo(gi);
IWebClient client = user.getWebserverClient();
LOGGER.fine("Player " + username
+ " enrolled to game " + gameId);
client.didEnroll(gameId, user.getName());
}
else
{
LOGGER.info("Player " + username
+ " failed to enroll to game " + gameId
+ ", reason=" + reasonFail);
long when = 0;
IWebClient webClient = user.getWebserverClient();
if (webClient != null)
{
webClient.deliverGeneralMessage(when, false,
"Can't enroll!", reasonFail);
}
}
}
else
{
LOGGER.warning("Player " + username
+ " tried to enroll to game " + gameId
+ " but game is already starting!");
IWebClient webClient = user.getWebserverClient();
if (webClient != null)
{
String message = "Enrolling to " + gi.getGameId()
+ " failed, game is already starting.";
long when = 0;
webClient.deliverGeneralMessage(when, false,
"Can't enroll!", message);
}
}
}
}
}
public void unenrollUserFromGame(String gameId, String username)
{
GameInfo gi = findByGameId(gameId);
User user = userDB.findUserByName(username);
if (gi != null)
{
synchronized (gi)
{
// TODO HACK!! don't check for now, otherwise can't unenroll
// from games that failed to start.
boolean preventUnenroll = false;
if (gi.isStarting() && preventUnenroll)
{
LOGGER.warning("Player " + username
+ " tried to unenroll from game " + gameId
+ ", but it is already starting.");
IWebClient webClient = user.getWebserverClient();
if (webClient != null)
{
String message = "Unenrolling from " + gi.getGameId()
+ " failed, game is already starting.";
long when = 0;
webClient.deliverGeneralMessage(when, false,
"Can't unenroll!", message);
}
}
else
{
String reasonFail = gi.unenroll(user);
proposedGamesListModified = true;
if (reasonFail == null)
{
updateOnline(gi);
allTellGameInfo(gi);
IWebClient client = user.getWebserverClient();
LOGGER.fine("Player " + username
+ " unenrolled from game " + gameId);
client.didUnenroll(gameId, user.getName());
}
}
}
}
}
public void cancelGame(String gameId, String byUser)
{
LOGGER.info("User " + byUser + " requests to cancel game " + gameId);
GameInfo gi = findByGameId(gameId);
if (gi == null)
{
LOGGER.info("Attempt to cancel game with id " + gameId
+ " but no GameInfo found for that id.");
return;
}
if (gi.wasAlreadyStarted())
{
LOGGER.warning("Attempt to cancel game " + gameId
+ " but it is/was already starting! Ignoring that attempt.");
return;
}
if (gi.getPort() != -1)
{
portBookKeeper.releasePort(gi);
}
IGameRunner gr = gi.getGameRunner();
if (gr != null)
{
LOGGER.info("For Cancel: game " + gameId
+ " has already GameRunner, not touching it.");
}
else
{
LOGGER.info("For Cancel: no GameRunner for GameInfo with gameId "
+ gameId);
}
Collection<User> users = userDB.getLoggedInUsers();
for (User u : users)
{
IWebClient client = u.getWebserverClient();
if (client != null)
{
client.gameCancelled(gameId, byUser);
}
}
allGames.remove(gi.getGameId());
proposedGames.remove(gi);
proposedGamesListModified = true;
updateGUI();
}
public void startGame(String gameId, User byUser)
{
GameInfo gi = findByGameId(gameId);
if (gi != null)
{
synchronized (gi)
{
attemptStartOnServer(gi, byUser);
}
}
else
{
LOGGER.warning("Did not find a GameInfo for gameId " + gameId
+ " to start it on the server!");
}
}
public void resumeGame(String gameId, String loadGame, User byUser)
{
LOGGER.info("User " + byUser.getName() + " wants to resume game "
+ gameId + ", from file " + loadGame);
GameInfo gi = findFromSuspendedGames(gameId);
if (gi != null)
{
gi.setResumeFromFilename(loadGame);
synchronized (gi)
{
attemptStartOnServer(gi, byUser);
}
}
else
{
LOGGER.warning("Did not find a GameInfo for gameId " + gameId
+ " to resume it on the server!");
}
}
public void deleteSuspendedGame(String gameId, User user)
{
LOGGER.info("User " + user.getName()
+ " wants to delete suspended game " + gameId);
GameInfo gi = findFromSuspendedGames(gameId);
{
if (gi != null)
{
gi.setState(GameState.DELETED);
allGames.remove(gi.getGameId());
proposedGames.remove(gi);
suspendedGames.remove(gi);
proposedGamesListModified = true;
allTellGameInfo(gi);
updateGUI();
}
}
}
private void attemptStartOnServer(GameInfo gi, User byUser)
{
// if (!gi.allEnrolledOnline())
// {
// LOGGER.warning("User " + byUser.getName()
// + " requested to start game " + gi.getGameId()
// + ", but not all enrolled players are online.");
// String reason = "Not all enrolled players online!";
// informAllEnrolledThatStartFailed(gi, reason, byUser);
//}
// else if (!gi.isStartable())
if (!gi.isStartable())
{
LOGGER.warning("User " + byUser.getName() + " attempted to start"
+ " game " + gi.getGameId() + ", but it is already running or"
+ " start attempt in progress (state=" + gi.getGameState()
+ ")!");
}
else
{
gi.markStarting(byUser);
allTellGameInfo(gi);
String reason = startOneGame(gi);
proposedGamesListModified = true;
if (reason == null)
{
LOGGER.log(Level.FINE, "Found gi, got port " + gi.getPort());
}
else
{
LOGGER.warning("starting/running game " + gi.getGameId()
+ " failed!! Reason: " + reason);
informAllEnrolledThatStartFailed(gi, reason, byUser);
gi.cancelStarting();
}
}
}
/**
* A game was started by a WebClient user locally on his computer
* and is ready to accept the other players as remote client;
* so we notify them and tell them host and port to where to connect.
*
* AT THE MOMENT THIS FUNCTIONALITY IS NOT IN USE AT ALL!
*/
public void startGameOnPlayerHost(String gameId, String hostingPlayer,
String playerHost, int port)
{
GameInfo gi = findByGameId(gameId);
if (gi != null)
{
ArrayList<User> users = gi.getPlayers();
for (User u : users)
{
LOGGER.info("Informing player " + u.getName()
+ " that game starts at host of hosting player "
+ hostingPlayer);
IWebClient webClient = u.getWebserverClient();
webClient.gameStartsNow(gameId, port, playerHost,
INACTIVITY_CHECK_INTERVAL, INACTIVITY_WARNING_INTERVAL,
INACTIVITY_TIMEOUT);
}
}
else
{
LOGGER.warning("Did not find a GameInfo for gameId " + gameId
+ " to inform the other players to connect to host "
+ playerHost + " port " + port);
}
proposedGamesListModified = true;
}
public void informStartedByPlayer(String gameId)
{
LOGGER.info("Tell enrolled players that game " + gameId
+ " was started by a player.");
GameInfo gi = findByGameId(gameId);
if (gi != null)
{
gameStarted(gi);
}
else
{
LOGGER.severe("Got request informGameStarted but did not find "
+ "any game for gameId " + gameId);
}
proposedGamesListModified = true;
}
public void informAllEnrolledAbout(GameInfo gi, String message)
{
long when = new Date().getTime();
ArrayList<User> users = gi.getPlayers();
for (User u : users)
{
IWebClient webClient = u.getWebserverClient();
if (webClient == null)
{
LOGGER.warning("Skip informing player " + u.getName()
+ " (webclient null) about: " + message);
}
else
{
webClient.systemMessage(when, message);
}
}
}
public void systemMessageToAll(String message)
{
long when = new Date().getTime();
Collection<User> users = userDB.getLoggedInUsers();
for (User u : users)
{
IWebClient client = u.getWebserverClient();
if (client == null)
{
LOGGER.warning("Skip sending systemMessage to player "
+ u.getName() + " (webclient null): " + message);
}
else
{
client.systemMessage(when, message);
}
}
}
public void informAllEnrolledThatStartFailed(GameInfo gi, String reason,
User byUser)
{
ArrayList<User> users = gi.getPlayers();
String message = "Starting game with gameId " + gi.getGameId()
+ " (initiated by player " + byUser.getName()
+ ") failed. Reason: " + reason;
for (User u : users)
{
IWebClient webClient = u.getWebserverClient();
if (webClient == null)
{
LOGGER.warning("Skip informing player " + u.getName()
+ " (webclient null): " + message);
}
else if (webClient.getClientVersion() >= WebClient.WC_VERSION_GENERAL_MESSAGE)
{
LOGGER
.info("Informing player " + u.getName() + ": " + message);
// for the starting user it's an error, others just info
boolean error = u.getName().equals(byUser.getName());
// when == 0: do not show a specific time
long when = 0;
webClient.deliverGeneralMessage(when, error,
"Game start failed!", message);
}
else
{
LOGGER.warning("Skip informing player " + u.getName()
+ " (too old webclient): " + message);
}
}
}
public void requestUserAttention(long when, String sender,
boolean isAdmin, String recipient, String message, int beepCount,
long beepInterval, boolean windows)
{
IWebClient recipientClient = null;
String reasonFail = null;
User user = userDB.findUserByName(recipient);
if (user != null)
{
recipientClient = user.getWebserverClient();
if (recipientClient != null)
{
recipientClient.requestAttention(when, sender, isAdmin,
message, beepCount, beepInterval, windows);
informPingDone(sender, recipient, message);
}
else
{
reasonFail = "User " + recipient + " is not online";
}
}
else
{
reasonFail = "Unknown user '" + recipient + "'";
}
if (reasonFail != null)
{
informPingFailed(sender, reasonFail, message);
}
}
private void informPingDone(String sender, String recipient, String message)
{
if ("SYSTEM".equals(sender))
{
return;
}
User senderUser = userDB.findUserByName(sender);
IWebClient senderWebClient = senderUser.getWebserverClient();
String[] lines = new String[] { "You /ping'ed to " + recipient + ": "
+ message };
ChatChannel gc = getGeneralChat();
if (senderWebClient != null)
{
gc.sendLinesToClient(gc.getChannelId(), senderWebClient,
Arrays.asList(lines), false, "");
}
else
{
LOGGER.warning("requestUserAttention to " + recipient
+ " done ok, but could not find client for sender " + sender
+ " to send confirmation message!");
LOGGER.info("Failed message was: " + message);
}
}
private void informPingFailed(String sender, String reasonFail,
String message)
{
User senderUser = userDB.findUserByName(sender);
IWebClient senderWebClient = senderUser.getWebserverClient();
String[] lines = new String[] {
"Sorry, your ping request failed, reason: " + reasonFail,
"Your text was: " + message };
ChatChannel gc = getGeneralChat();
if (senderWebClient != null)
{
gc.sendLinesToClient(gc.getChannelId(), senderWebClient,
Arrays.asList(lines), true, "");
}
else
{
LOGGER.warning("requestUserAttention failed (" + reasonFail
+ ") but could not find client"
+ " to send error message to sender either!");
}
}
public void informLocallyGameOver(String gameId)
{
LOGGER.info("WebServer informLocallyGameOver id " + gameId);
GameInfo gi = findFromRunningGames(gameId);
unregisterGamePlayerPC(gi);
LOGGER.info("WebServer informLocallyGameOver id " + gameId + " ENDS");
}
public void updateUserCounts()
{
int connected = userDB.getLoggedInCount();
allTellUserCounts();
gui.setUserInfo(connected + " users connected.");
ArrayList<GameInfo> games = new ArrayList<GameInfo>(allGames.values());
for (GameInfo gi : games)
{
// returns true if changed
if (updateOnline(gi))
{
allTellGameInfo(gi);
}
}
}
public void allTellUserCounts()
{
if (userDB.getLoggedInCount() > 0)
{
int loggedin = userDB.getLoggedInCount();
// the other five are still dummies.
int enrolled = userDB.getEnrolledCount();
int playing = userDB.getPlayingCount();
int dead = userDB.getDeadCount();
long ago = 0;
StringBuffer text = new StringBuffer("");
Collection<User> users = userDB.getLoggedInUsers();
for (User u : users)
{
if (text.length() != 0)
{
text.append(", ");
}
text.append(u.getName());
}
for (User u : users)
{
IWebClient client = u.getWebserverClient();
if (client != null)
{
client.userInfo(loggedin, enrolled, playing, dead, ago,
text.toString());
}
}
}
}
public void chatSubmit(String chatId, String sender, String message)
{
generalChat.createStoreAndDeliverMessage(sender, message);
}
public void handlePingQuotedName(String sender, String pingCommand)
{
long when = new Date().getTime();
boolean isAdmin = userDB.findUserByName(sender).isAdmin();
// /ping "
// 01234567
String args = pingCommand.substring(7);
// split at the closing quote, eat up trailing spaces in name and
// leading spaces of the message
String[] tokens = args.split(" *\" *", 2);
if (tokens.length != 2)
{
LOGGER.warning("invalid pingCommand with quotes '" + pingCommand
+ "' from user " + sender + "!");
String reasonFail = "Invalid /ping syntax. Use: /ping \"RECIPIENT NAME\" [optionally some message]";
informPingFailed(sender, reasonFail, pingCommand);
}
else
{
String recipient = tokens[0];
String message = "<no message specified by sender>";
String msg = tokens[1];
if (msg != null && !msg.matches(" *"))
{
message = msg;
}
requestUserAttention(when, sender, isAdmin, recipient, message, 3,
500, true);
}
}
public void handlePing(String sender, String pingCommand)
{
long when = new Date().getTime();
boolean isAdmin = userDB.findUserByName(sender).isAdmin();
String[] tokens = pingCommand.split(" +", 3);
if (tokens.length < 2)
{
// Just /ping : inform about the usage:
String reasonFail = "Invalid /ping syntax. Use: /ping RECIPIENT [optionally some message]";
informPingFailed(sender, reasonFail, pingCommand);
}
else
{
String recipient = tokens[1];
String message = "<no message specified by sender>";
if (tokens.length >= 3)
{
message = tokens[2];
}
requestUserAttention(when, sender, isAdmin, recipient, message, 3,
500, true);
}
}
public void tellLastChatMessagesToOne(WebServerClient client, String chatId)
{
if (!chatId.equals(IWebServer.generalChatName))
{
LOGGER.log(Level.WARNING, "tellLastChatMessagesToOne: "
+ "illegal chat id " + chatId + " - doing nothing");
return;
}
generalChat.tellLastMessagesToOne(client);
}
public void sendMessageOfTheDayToOne(WebServerClient client, String chatId)
{
if (!chatId.equals(IWebServer.generalChatName))
{
LOGGER.log(Level.WARNING, "sendMessageOfTheDayToOne: "
+ "illegal chat id " + chatId + " - doing nothing");
return;
}
generalChat.deliverMessageOfTheDayToClient(chatId, client,
loginMessage);
}
public void sendOldVersionWarningToOne(WebServerClient client,
String userName, String chatId)
{
if (!chatId.equals(IWebServer.generalChatName))
{
LOGGER.log(Level.WARNING, "sendMessageOfTheDayToOne: "
+ "illegal chat id " + chatId + " - doing nothing");
return;
}
generalChat.deliverOldVersionWarning(chatId, userName, client);
}
private void readLoginMessageFromFile(String filename)
{
try
{
File loginMessageFile = new File(filename);
if (!loginMessageFile.exists())
{
return;
}
else
{
LOGGER.info("Reading login message from file " + filename);
}
ArrayList<String> temp = new ArrayList<String>();
BufferedReader loginMessagesReader = new BufferedReader(
new InputStreamReader(new FileInputStream(loginMessageFile),
WebServerConstants.charset));
String line = null;
while ((line = loginMessagesReader.readLine()) != null)
{
temp.add(line);
}
loginMessagesReader.close();
loginMessage.clear();
loginMessage.addAll(temp);
LOGGER.info("Read " + loginMessage.size() + " lines from file "
+ filename);
}
catch (FileNotFoundException e)
{
LOGGER.log(Level.SEVERE, "Login message file " + filename
+ " not found!", e);
}
catch (IOException e)
{
LOGGER.log(Level.SEVERE,
"IOException while reading login message file " + filename
+ "!", e);
}
}
public void logout()
{
// Handled by WebServerSocketClientThread main loop;
// only listed here to satisfy the interface.
}
public void messageToAdmin(long when, String fromUser, String fromMail,
List<String> message)
{
generalChat.writeMessageToAdminToChatlog(when, fromUser, fromMail, message);
mailObject.sendMessageToAdminMail(when, fromUser, fromMail, message);
}
public String registerUser(String username, String password, String email)
{
String reason = userDB.registerUser(username, password, email,
mailObject);
return reason;
}
public String confirmRegistration(String username, String confirmationCode)
{
String reason = userDB.confirmRegistration(username, confirmationCode);
return reason;
}
public String changeProperties(String username, String oldPW,
String newPW, String email, Boolean isAdminObj)
{
String reason = userDB.changeProperties(username, oldPW, newPW, email,
isAdminObj);
return reason;
}
// =========== internal workers ============
/**
* When a user logged in or out, this is called for every GameInfo to update
* how many of the enrolled players are currently online.
* @param userDB TODO
*
* @return true if the count of online users was changed i.e. GameInfo
* needs to be updated to all clients
*/
boolean updateOnline(GameInfo gi)
{
int found = 0;
for (User u : gi.getPlayers())
{
if (userDB.isUserOnline(u))
{
found++;
}
}
// TODO in reEnrollIfNecessary case this is now wrong??
// perhaps because in the moment of update user is just not online...
return gi.updateOnlineCount(found);
}
private int countProposedGames(boolean shallBeScheduled)
{
int count = 0;
for (GameInfo gi : proposedGames)
{
if (gi.isScheduledGame() == shallBeScheduled)
{
count++;
}
}
return count;
}
private GameInfo findByGameId(String gameId)
{
return allGames.get(gameId);
}
private GameInfo findFromRunningGames(String gameId)
{
GameInfo foundGi = null;
for (GameInfo gi : runningGames)
{
if (gi.getGameId().equals(gameId))
{
foundGi = gi;
break;
}
}
return foundGi;
}
private GameInfo findFromSuspendedGames(String gameId)
{
GameInfo foundGi = null;
for (GameInfo gi : suspendedGames)
{
if (gi.getGameId().equals(gameId))
{
foundGi = gi;
break;
}
}
return foundGi;
}
private IGameRunner getGameOnServer(GameInfo gi)
{
assert gi != null : "Cannot find GameOnServer for GameInfo that is null!";
IGameRunner gr = gi.getGameRunner();
if (gr == null)
{
LOGGER.severe("GameInfo with GameId " + gi.getGameId()
+ " returned null as GameRunner");
}
return gr;
}
private String startOneGame(GameInfo gi)
{
// Reason for failure
String reason = null;
RunGameInOwnJVM gr = new RunGameInOwnJVM(this, options, gi);
boolean ok = gr.makeRunningGame();
if (!ok)
{
reason = gr.getReasonStartFailed();
return reason;
}
else
{
gr.tryToStart();
LOGGER.fine("Returned from starter for game " + gi.getGameId());
updateGUI();
}
LOGGER.fine("Successfully started game " + gi.getGameId()
+ " on port " + gi.getPort());
// failureReason == null means success
return reason;
}
/**
* unregister a game from runningGames (or proposedGames),
* and keep in endingGames until it's reaped
*/
public void unregisterGame(GameInfo gi, int port)
{
synchronized (allGames)
{
LOGGER.log(Level.FINEST, "unregister: trying to remove...");
if (runningGames.contains(gi))
{
LOGGER.log(Level.FINEST, "Removing game " + gi.getGameId()
+ " from running games list");
runningGames.remove(gi);
if (gi.getGameState().equals(GameState.SUSPENDED))
{
synchronized (suspendedGames)
{
LOGGER.log(Level.FINEST,
"Adding game " + gi.getGameId()
+ " to suspended games list");
suspendedGames.add(gi);
}
}
else
{
gi.setState(GameState.ENDING);
}
}
else if (suspendedGames.contains(gi))
{
LOGGER.log(Level.FINEST,
"Reaper: first removing game " + gi.getGameId()
+ " from suspended list");
suspendedGames.remove(gi);
if (gi.getGameState().equals(GameState.SUSPENDED))
{
LOGGER
.log(Level.FINEST,
"When reaping, still/again suspended; adding game "
+ gi.getGameId()
+ " back to suspended games list");
suspendedGames.add(gi);
}
else
{
gi.setState(GameState.ENDING);
}
}
// If game starting did not succeed might still be in proposed list
else if (proposedGames.contains(gi))
{
LOGGER.log(Level.FINEST, "Removing game " + gi.getGameId()
+ " from proposed games list");
proposedGames.remove(gi);
proposedGamesListModified = true;
}
else
{
LOGGER.warning("Neither proposed, running nor suspended games"
+ " list contains game " + gi.getGameId());
}
}
synchronized (endingGames)
{
LOGGER.log(Level.FINEST, "Putting game " + gi.getGameId()
+ " to ending games list");
endingGames.add(gi);
}
proposedGamesListModified = true;
saveGamesIfNeeded();
allTellGameInfo(gi);
boolean suspended = gi.getGameState().equals(GameState.SUSPENDED);
GameThreadReaper r = new GameThreadReaper();
r.start();
LOGGER.finest("GameThreadReaper started for"
+ (suspended ? " suspended" : " ending ") + " game "
+ gi.getGameId());
updateGUI();
}
/**
* unregister a game (run on player's PC) from runningGames,
* keep in endingGames until it's reaped
*/
public void unregisterGamePlayerPC(GameInfo gi)
{
if (gi == null)
{
LOGGER
.warning("unregisterGamePlayerPC called with a null GameInfo object?");
return;
}
synchronized (runningGames)
{
LOGGER.log(Level.FINEST, "trying to remove...");
if (runningGames.contains(gi))
{
LOGGER.log(Level.FINEST, "removing...");
runningGames.remove(gi);
}
// TODO: also add the "if start failed check proposed games list, too" here?
else
{
LOGGER.warning("runningGames does not contain game "
+ gi.getGameId());
}
}
gi.setState(GameState.ENDING);
allTellGameInfo(gi);
updateGUI();
}
private void readGamesFromFile(String filename)
{
int maximumFileId = getMaximumGameIdFromFiles();
GameInfo.setNextFreeGameId(maximumFileId + 1);
try
{
File gamesFile = new File(filename);
if (!gamesFile.exists())
{
LOGGER.warning("Games file " + filename
+ " does not exist yet. I'll create an empty one now.");
gamesFile.createNewFile();
}
else
{
LOGGER.info("Reading games from file " + filename);
}
BufferedReader games = new BufferedReader(new InputStreamReader(
new FileInputStream(gamesFile), WebServerConstants.charset));
String line = null;
while ((line = games.readLine()) != null)
{
if (line.startsWith("#"))
{
// ignore comment line
}
else if (line.matches("\\s*"))
{
// ignore empty line
}
else
{
// GameInfo.fromString expects the token[0]
// to be the command name:
String lineWithCmd = "Dummy" + sep + line;
String[] tokens = lineWithCmd.split(sep);
GameInfo gi = GameInfo.fromString(tokens, allGames, true);
if (gi.getGameState().equals(GameState.PROPOSED))
{
proposedGames.add(gi);
}
else if (gi.getGameState().equals(GameState.SUSPENDED))
{
suspendedGames.add(gi);
}
else
{
LOGGER.warning("restored game " + gi.getGameId()
+ " is " + gi.getGameState()
+ ", should be proposed or suspended?");
}
}
}
games.close();
LOGGER.info("Restored " + allGames.size() + " games from file "
+ filename);
}
catch (FileNotFoundException e)
{
LOGGER.log(Level.SEVERE, "Games file " + filename + " not found!",
e);
System.exit(1);
}
catch (IOException e)
{
LOGGER.log(Level.SEVERE, "IOException while reading games file "
+ filename + "!", e);
System.exit(1);
}
proposedGamesListModified = false;
}
public void saveGamesIfNeeded()
{
if (proposedGamesListModified)
{
storeGamesToFile(proposedGamesFilename);
}
}
private void storeGamesToFile(String filename)
{
if (filename == null)
{
LOGGER.log(Level.SEVERE, "filename must not be null, but it is!");
throw new RuntimeException("gameFile filename is null!");
}
PrintWriter out = null;
try
{
out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(
filename), WebServerConstants.charset));
ArrayList<GameInfo> games = new ArrayList<GameInfo>(
allGames.values());
for (GameInfo gi : games)
{
if (gi.relevantForSaving())
{
String asString = gi.toString(sep);
out.println(asString);
}
}
out.close();
}
catch (FileNotFoundException e)
{
LOGGER.log(Level.SEVERE, "Writing games file " + filename
+ "failed: FileNotFoundException: ", e);
throw new RuntimeException("FileNotFound exception while "
+ "creating/closing scheduled-Games file!");
}
proposedGamesListModified = false;
}
/**
* A Null Object for the web server GUI interface.
*
* Avoids having to check for null everywhere.
*/
private static final class NullWebServerGUI implements IWebServerGUI
{
public void setScheduledGamesInfo(String s)
{
// nothing
}
public void setEndingGamesInfo(String s)
{
// nothing
}
public void setSuspendedGamesInfo(String s)
{
// nothing
}
public void setUsedPortsInfo(String s)
{
// nothing
}
public void setInstantGamesInfo(String s)
{
// nothing
}
public void setRunningGamesInfo(String s)
{
// nothing
}
public void setUserInfo(String s)
{
// nothing
}
public void shutdown()
{
// nothing
}
}
public String getStringOption(String key)
{
return this.options.getStringOption(key);
}
/**
* Check whether any game executed in an own process has been
* finished.
*
*/
class GameThreadReaper extends Thread
{
public GameThreadReaper()
{
// nothing to do
}
@Override
public void run()
{
boolean didSomething = false;
LOGGER.finest("GameThreadReaper started running.");
synchronized (endingGames)
{
didSomething = handleGamesFromList(endingGames, "ending games");
}
if (didSomething)
{
updateGUI();
}
LOGGER.log(Level.FINEST, "GameThreadReaper run() ends");
}
}
private boolean handleGamesFromList(List<GameInfo> list, String listName)
{
boolean didSomething = false;
if (!list.isEmpty())
{
LOGGER.finest("There are " + list.size() + " games in " + listName
+ " list.");
didSomething = true;
Iterator<GameInfo> it = list.iterator();
while (it.hasNext())
{
GameInfo gi = it.next();
IGameRunner gos = getGameOnServer(gi);
if (gos == null)
{
LOGGER.warning("No GameRunner found for GameInfo"
+ " with id " + gi.getGameId()
+ " to reap it's process.");
}
else if (gos instanceof RunGameInOwnJVM)
{
String name = ((RunGameInOwnJVM)gos).getName();
LOGGER.log(Level.FINE, "REAPER: wait for '" + name
+ "' to end...");
try
{
((RunGameInOwnJVM)gos).join();
LOGGER.log(
Level.FINE,
" ok, ended... releasing port "
+ gi.getPort());
portBookKeeper.releasePort(gi);
}
catch (InterruptedException e)
{
LOGGER.log(Level.WARNING, "Ups??? Caught exception ",
e);
}
}
else
{
LOGGER.warning("GameThreadReaper can handle only "
+ "GameRunners of type RunGameInOwnJVM, "
+ "but we got something else!");
}
if (!gi.getGameState().equals(GameState.SUSPENDED))
{
LOGGER.finest("Removing ending game " + gi.getGameId()
+ " from allGames.");
allGames.remove(gi.getGameId());
}
else
{
LOGGER.finest("NOT removing suspended game "
+ gi.getGameId() + " from allGames.");
}
it.remove();
}
LOGGER.log(Level.INFO, "Reaper loop for " + listName
+ "list ended");
}
else
{
LOGGER.finest("List for " + listName + " is empty, nothing to do");
// nothing to do
}
return didSomething;
}
/**
* Searches the game directory tree for highest game number for which
* a game directory had been earlier created.
* Tree is expected to have groups per each 100 games; example:
*
* base
* base/nn00-nn99
* base/nn00-nn99/nn00
* base/nn00-nn99/nn04
* base/nn00-nn99/nn98
* base/mm00-mm99/mm12
* base/mm00-mm99/mm87
*
* Eventually, when we reach gameId 10000+, groupdirs will have the form
* base/kkk00-kkk99
* base/kkk00-kkk99/kkk02
* base/kkk00-kkk99/kkk87
*
* Example, if last created game dir was 6789 ( "base/6700-6799/6789" ),
* this returns 6789.
*
* @return The highest game number for which a directory already exists,
* (otherwise 0 if therre is no dir at all)
*/
private int getMaximumGameIdFromFiles()
{
// Server will create next as maxId + 1
int maxId = 0;
String workFilesBaseDir = options
.getStringOption(WebServerConstants.optWorkFilesBaseDir);
File baseDir = new File(workFilesBaseDir);
if (baseDir.isDirectory())
{
String maxGroup = null;
int maxNumber = -1;
Pattern p = Pattern.compile("(\\d+)-\\d+");
String[] dirNames = baseDir.list();
if (dirNames != null && dirNames.length > 0)
{
for (int grp = 0; grp < dirNames.length; grp++)
{
String dirName = dirNames[grp];
Matcher m = p.matcher(dirName);
if (m.matches())
{
String firstNumber = m.group(1);
int number = Integer.parseInt(firstNumber);
if (number > maxNumber)
{
maxNumber = number;
maxGroup = dirName;
}
}
}
if (maxGroup != null)
{
File groupDir = new File(baseDir, maxGroup);
if (groupDir.isDirectory())
{
String[] names = groupDir.list();
for (int i = 0; i < names.length; i++)
{
String name = names[i];
try
{
int number = Integer.parseInt(name);
if (number > maxId)
{
maxId = number;
}
}
catch (NumberFormatException e)
{
// Ignore non-number filenames
}
}
}
else
{
LOGGER.severe("Group '" + groupDir
+ "' is not a directory?!?");
}
}
}
}
return maxId;
}
}