/*
* Games.java
*
* Copyright (C) 2015 Pixelgaffer
*
* This work is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation; either version 2 of the License, or any later
* version.
*
* This work is distributed in the hope that it will be useful, but without
* any warranty; without even the implied warranty of merchantability or
* fitness for a particular purpose. See version 2 and version 3 of the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.pixelgaffer.turnierserver.backend;
import static org.pixelgaffer.turnierserver.PropertyUtils.BACKEND_RESTART_ATTEMPTS;
import static org.pixelgaffer.turnierserver.PropertyUtils.getInt;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.apache.commons.io.FileUtils;
import org.pixelgaffer.turnierserver.Airbrake;
import org.pixelgaffer.turnierserver.Parsers;
import org.pixelgaffer.turnierserver.backend.Games.GameImpl.GameFinishedListener;
import org.pixelgaffer.turnierserver.backend.Games.GameImpl.GameState;
import org.pixelgaffer.turnierserver.backend.server.BackendFrontendConnectionHandler;
import org.pixelgaffer.turnierserver.backend.server.message.BackendFrontendCommandProcessed;
import org.pixelgaffer.turnierserver.backend.server.message.BackendFrontendResult;
import org.pixelgaffer.turnierserver.gamelogic.GameLogic;
import org.pixelgaffer.turnierserver.gamelogic.interfaces.Frontend;
import org.pixelgaffer.turnierserver.networking.DatastoreFtpClient;
import org.pixelgaffer.turnierserver.networking.messages.MessageForward;
import it.sauronsoftware.ftp4j.FTPAbortedException;
import it.sauronsoftware.ftp4j.FTPDataTransferException;
import it.sauronsoftware.ftp4j.FTPException;
import it.sauronsoftware.ftp4j.FTPIllegalReplyException;
import it.sauronsoftware.ftp4j.FTPListParseException;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Games
{
@AllArgsConstructor
static class FrontendWrapper implements Frontend
{
@Getter
private int requestId;
public void sendMessage (byte[] message) throws IOException
{
BackendFrontendConnectionHandler.getFrontend().sendMessage(message);
}
}
private static final Object lock = new Games();
/** Alle aktuell benutzten UUIDs. */
private static final Set<UUID> uuids = new HashSet<>();
/** Die zur UUID gehörende KI. */
private static final Map<UUID, AiWrapper> aiWrappers = new HashMap<>();
/** Die AI mit der angegebenen UUID hat sich disconnected. */
public static void aiDisconnected (UUID uuid)
{
new Thread( () -> {
// die ki noch 1 min speichern um laggs in der verbindung zu
// vermeiden
try
{
Thread.sleep(60000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
finally
{
synchronized (lock)
{
aiWrappers.remove(uuid);
uuids.remove(uuid);
}
}
}).start();
}
/**
* Die AI mit der angegebenen UUID wurde von Backend/Worker/Sandbox
* beendet und das Spiel oder die KI muss neu gestartet werden.
*/
public static void aiTerminated (UUID uuid)
{
new Thread( () -> {
AiWrapper aiw;
synchronized (lock)
{
aiw = getAiWrapper(uuid);
}
if (aiw == null)
{
BackendMain.getLogger().critical("Konnte KI mit der UUID " + uuid + " nicht finden");
return;
}
GameImpl game = aiw.getGame();
if (game == null)
{
BackendMain.getLogger().critical("Die KI " + uuid + " hat kein Spiel");
return;
}
game.attempts++;
if (game.attempts > getInt(BACKEND_RESTART_ATTEMPTS, 5))
{
BackendMain.getLogger().warning("Maximales Limit an Spiel-Restart übertroffen");
BackendFrontendResult result = new BackendFrontendResult(game.getRequestId(), false,
"The maximum limit of restart attempts was hit.", game.getUuid());
try
{
BackendFrontendConnectionHandler.getFrontend().sendMessage(Parsers.getFrontend().parse(result, false));
game.finishGame(false);
}
catch (IOException e1)
{
Airbrake.log(e1).printStackTrace();
}
return;
}
if (game.getState() == GameState.WAITING)
{
aiDisconnected(uuid);
aiw.setUuid(randomUuid());
synchronized (lock)
{
aiWrappers.put(aiw.getUuid(), aiw);
}
WorkerConnection w = Workers.getStartableWorker(aiw.getLang(), game.isTournament());
try
{
w.addJob(aiw, game.getGameId());
}
catch (Exception e)
{
Airbrake.log(e).printStackTrace();
BackendFrontendResult result = new BackendFrontendResult(game.getRequestId(), false, game.getUuid(), e);
try
{
BackendFrontendConnectionHandler.getFrontend().sendMessage(Parsers.getFrontend().parse(result, false));
game.finishGame(false);
}
catch (IOException e1)
{
Airbrake.log(e1).printStackTrace();
}
}
aiw.setConnection(w);
}
else
{
try
{
game.restart();
}
catch (Exception e)
{
Airbrake.log(e).printStackTrace();
BackendFrontendResult result = new BackendFrontendResult(game.getRequestId(), false, game.getUuid(), e);
try
{
BackendFrontendConnectionHandler.getFrontend().sendMessage(Parsers.getFrontend().parse(result, false));
game.finishGame(false);
}
catch (IOException e1)
{
Airbrake.log(e1).printStackTrace();
}
}
}
}).start();
}
/** Das zur UUID gehörende Spiel. */
private static final Map<UUID, GameImpl> games = new HashMap<>();
/**
* Generiert eine aktuell freie UUID.
*/
private static UUID randomUuid ()
{
UUID uuid;
synchronized (lock)
{
do
{
uuid = UUID.randomUUID();
} while (uuids.contains(uuid));
uuids.add(uuid);
}
return uuid;
}
/**
* Findet den AiWrapper mit der angegebenen UUID.
*/
public static AiWrapper getAiWrapper (UUID uuid)
{
synchronized (lock)
{
return aiWrappers.get(uuid);
}
}
/**
* Lässt den AiWrapper mit der angegebenen UUID das Packet empfangen.
*/
public static void receiveMessage (UUID uuid, byte message[])
{
getAiWrapper(uuid).receiveMessage(message);
}
/**
* Lässt den im MessageForward angegebenen AiWrapper das Packet empfangen.
*/
public static void receiveMessage (MessageForward mf)
{
receiveMessage(mf.getAi(), mf.getMessage());
}
/**
* Diese Klasse ist die Implementation des Game-Interfaces der Game-Logic
* Bibliothek.
*/
public static class GameImpl implements org.pixelgaffer.turnierserver.gamelogic.interfaces.Game
{
static enum GameState
{
WAITING,
STARTED,
FINISHED
}
static interface GameFinishedListener
{
public void gameFinished (boolean success);
}
@Getter
@Setter
private GameFinishedListener gameFinishedListener;
/** Die UUID dieses Spiels. */
@Getter
@NonNull
private UUID uuid;
/** Die ID dieses Spiels auf dem Datastore. */
@Getter
private int gameId;
/** Die ID des Request vom Frontend. */
@Getter
private int requestId;
/** Gibt an ob dieses Spiel zu einem Turnier gehört. */
@Getter
private boolean tournament;
/** Die Liste mit allen teilnehmenden KIs. */
@Getter
private List<AiWrapper> ais = new ArrayList<>();
/** Die Spiellogik. */
@Getter
private GameLogic<?, ?> logic;
/**
* Die Anzahl der Versuche, das Spiel zu starten. Dies wird NICHT von
* der GameImpl Klasse selbst verwaltet.
*/
@Getter
@Setter(AccessLevel.PACKAGE)
private int attempts = 0;
/** Gibt an ob das Spiel schon gestartet wurde. */
@Getter
private GameState state = GameState.WAITING;
private GameImpl (int gameId, @NonNull GameLogic<?, ?> logic, @NonNull UUID uuid, int requestId,
boolean tournament, String[] languages, String ... ais)
throws IOException
{
this.gameId = gameId;
this.logic = logic;
this.uuid = uuid;
this.requestId = requestId;
this.tournament = tournament;
// die KIs erstellen
for (int i = 0; i < ais.length; i++)
{
String ai = ais[i];
// AiWrapper erstellen
AiWrapper aiw = new AiWrapper(this, randomUuid(), languages[i]);
aiw.setIndex(this.ais.size());
// den String ai parsen (<ai-id>v<version>)
String s[] = ai.split("v");
aiw.setId(ai);
aiw.setAiId(Integer.valueOf(s[0]));
aiw.setVersion(Integer.valueOf(s[1]));
// die KI zur Liste hinzufügen
this.ais.add(aiw);
synchronized (lock)
{
aiWrappers.put(aiw.getUuid(), aiw);
}
// einen Worker mit der KI beauftragen
WorkerConnection w = Workers.getStartableWorker(aiw.getLang(), tournament);
w.addJob(aiw, gameId);
aiw.setConnection(w);
}
}
@Override
public Frontend getFrontend ()
{
return new FrontendWrapper(getRequestId());
}
@Override
public void finishGame () throws IOException
{
finishGame(true);
}
public void finishGame (boolean success) throws IOException
{
BackendMain.getLogger().stacktrace("finishGame() wurde für das Spiel " + getUuid() + " aufgerufen");
for (AiWrapper ai : ais)
ai.disconnect();
if (state == GameState.STARTED) // ist beim restart auf false
{
if (gameFinishedListener != null)
gameFinishedListener.gameFinished(success);
BackendMain.getLogger().info("alle kis disconnected, sende success an frontend");
BackendFrontendConnectionHandler.getFrontend().sendMessage(
Parsers.getFrontend().parse(new BackendFrontendResult(getRequestId(), success, getUuid()), false));
synchronized (lock)
{
games.remove(getUuid());
uuids.remove(getUuid());
}
}
state = GameState.FINISHED;
}
/**
* Diese Methode wird aufgerufen wenn eine KI sich mit dem Worker
* verbunden hat. Wenn alle KIs verbunden sind wird das Spiel
* gestartet.
*/
public synchronized void aiConnected ()
{
if (state != GameState.WAITING)
return;
for (AiWrapper ai : ais)
if (!ai.isConnected())
return;
BackendMain.getLogger().info("Alle KIs verbunden, starte Spiel " + getUuid());
getLogic().startGame(this);
state = GameState.STARTED;
try
{
BackendFrontendConnectionHandler.getFrontend().sendMessage(
Parsers.getFrontend().parse(new BackendFrontendCommandProcessed(getRequestId(), getUuid(), "started"), false));
}
catch (IOException e)
{
Airbrake.log(e).printStackTrace();
}
}
/**
* Diese Methode startet das Spiel neu.
*/
public synchronized void restart () throws IOException, InstantiationException, IllegalAccessException
{
if (state != GameState.STARTED)
{
BackendMain.getLogger().warning("Weigere mich nicht-laufendes Spiel " + getUuid() + " neu zu starten");
return;
}
BackendMain.getLogger().info("Starte Spiel " + uuid + " neu");
state = GameState.WAITING;
logic = logic.getClass().newInstance();
BackendFrontendConnectionHandler.getFrontend().sendMessage(
Parsers.getFrontend().parse(new BackendFrontendCommandProcessed(getRequestId(), getUuid(), "restarted"), false));
for (int i = 0; i < ais.size(); i++)
{
AiWrapper aiw = ais.get(i);
aiw.disconnect();
// da aiDisconnected aufgerufen wurde wird die alte UUID
// entfernt
aiw.setUuid(randomUuid());
synchronized (lock)
{
aiWrappers.put(aiw.getUuid(), aiw);
}
// einen Worker mit der KI beauftragen
WorkerConnection w = Workers.getStartableWorker(aiw.getLang(), tournament);
w.addJob(aiw, gameId);
aiw.setConnection(w);
}
}
}
@RequiredArgsConstructor
@AllArgsConstructor
static class LoadedGameLogic implements GameFinishedListener
{
@NonNull
@Getter
private GameLogic<?, ?> gameLogic;
@Getter
private URLClassLoader classloader;
@Getter
private final List<File> toDelete = new LinkedList<>();
@Override
public void gameFinished (boolean success)
{
try
{
if (classloader != null)
classloader.close();
}
catch (Exception e)
{
Airbrake.log(e).printStackTrace();
}
for (File f : toDelete)
if (!FileUtils.deleteQuietly(f))
BackendMain.getLogger().warning("Failed to delete " + f);
}
}
/**
* Lädt die Jar-Datei der GameLogic für das angegebene Spiel herunter,
* liest die Manifest-Datei, und lädt die GameLogic-Klasse.
*/
public static LoadedGameLogic loadGameLogic (int gameId) // keep in sync
// with codr
throws IOException, FTPIllegalReplyException, FTPException, FTPDataTransferException, FTPAbortedException,
ReflectiveOperationException, FTPListParseException
{
// jar runterladen
File jar = Files.createTempFile("logic", ".jar").toFile();
DatastoreFtpClient.retrieveGameLogic(gameId, jar);
// manifest lesen
JarFile jarFile = new JarFile(jar);
Manifest mf = jarFile.getManifest();
String classname = mf.getMainAttributes().getValue("Logic-Class");
BackendMain.getLogger().info("Lade Logik-Klasse " + classname);
String requiredLibs[] = mf.getMainAttributes().getValue("Required-Libs").split("\\s+");
File libDir = Files.createTempDirectory("libs").toFile();
for (String lib : requiredLibs)
if (!lib.isEmpty())
DatastoreFtpClient.retrieveLibrary(lib, "Java", libDir);
jarFile.close();
// klasse laden
List<URL> urls = new ArrayList<>();
urls.add(jar.toURI().toURL());
for (File entry : libDir.listFiles())
urls.add(entry.toURI().toURL());
URLClassLoader cl = new URLClassLoader(urls.toArray(new URL[0]));
Class<?> clazz = cl.loadClass(classname);
LoadedGameLogic logic = new LoadedGameLogic((GameLogic<?, ?>)clazz.newInstance(), cl);
logic.getToDelete().add(jar);
logic.getToDelete().add(libDir);
return logic;
}
/**
* Startet ein Spiel des angegebenen Typs mit den angegebenen KIs.
*/
public static GameImpl startGame (int gameId, int requestId, boolean tournament, String[] languages, String ... ais)
throws ReflectiveOperationException, IOException, FTPIllegalReplyException, FTPException,
FTPDataTransferException, FTPAbortedException, FTPListParseException
{
LoadedGameLogic logic = loadGameLogic(gameId);
UUID uuid = randomUuid();
GameImpl game = new GameImpl(gameId, logic.getGameLogic(), uuid, requestId, tournament, languages, ais);
game.setGameFinishedListener(logic);
synchronized (lock)
{
games.put(uuid, game);
}
return game;
}
/**
* Startet ein Qualifikations-Spiel des angegebenen Typs mit der
* angegegebenen KI.
*/
public static GameImpl startQualifyGame (int gameId, int requestId, String language, String ai, String qualilang)
throws IOException, FTPIllegalReplyException, FTPException, FTPDataTransferException, FTPAbortedException,
ReflectiveOperationException, FTPListParseException
{
LoadedGameLogic logic = loadGameLogic(gameId);
UUID uuid = randomUuid();
// die KIs herausfinden. QualiKI: -gameId version 1
int numAis = logic.getGameLogic().playerAmt();
String ais[] = new String[numAis];
ais[0] = ai;
for (int i = 1; i < numAis; i++)
ais[1] = "-" + gameId + "v1";
String[] languages = { language, qualilang };
// Spiel starten
GameImpl game = new GameImpl(gameId, logic.getGameLogic(), uuid, requestId, false, languages, ais);
game.setGameFinishedListener(logic);
synchronized (lock)
{
games.put(uuid, game);
}
return game;
}
}