package games.strategy.engine.framework.startup.launcher; import java.awt.Component; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import games.strategy.debug.ClientLogger; import games.strategy.engine.ClientContext; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.framework.GameDataManager; import games.strategy.engine.framework.GameRunner; import games.strategy.engine.framework.ServerGame; import games.strategy.engine.framework.headlessGameServer.HeadlessGameServer; import games.strategy.engine.framework.message.PlayerListing; import games.strategy.engine.framework.startup.mc.ClientModel; import games.strategy.engine.framework.startup.mc.GameSelectorModel; import games.strategy.engine.framework.startup.mc.IClientChannel; import games.strategy.engine.framework.startup.mc.IObserverWaitingToJoin; import games.strategy.engine.framework.startup.mc.ServerModel; import games.strategy.engine.framework.startup.ui.InGameLobbyWatcherWrapper; import games.strategy.engine.framework.ui.SaveGameFileChooser; import games.strategy.engine.gamePlayer.IGamePlayer; import games.strategy.engine.lobby.server.GameDescription; import games.strategy.engine.message.ConnectionLostException; import games.strategy.engine.message.IChannelMessenger; import games.strategy.engine.message.IRemoteMessenger; import games.strategy.engine.message.MessengerException; import games.strategy.engine.random.CryptoRandomSource; import games.strategy.net.IMessenger; import games.strategy.net.INode; import games.strategy.net.Messengers; import games.strategy.util.ThreadUtil; public class ServerLauncher extends AbstractLauncher { private static final Logger s_logger = Logger.getLogger(ServerLauncher.class.getName()); public static final String SERVER_ROOT_DIR_PROPERTY = "triplea.server.root.dir"; private final int m_clientCount; private final IRemoteMessenger m_remoteMessenger; private final IChannelMessenger m_channelMessenger; private final IMessenger m_messenger; private final PlayerListing m_playerListing; private final Map<String, INode> m_remotelPlayers; private final ServerModel m_serverModel; private ServerGame m_serverGame; private Component m_ui; private ServerReady m_serverReady; private final CountDownLatch m_errorLatch = new CountDownLatch(1); private volatile boolean m_isLaunching = true; private volatile boolean m_abortLaunch = false; private volatile boolean m_gameStopped = false; // a list of observers that tried to join the game during starup // we need to track these, because when we loose connections to them // we can ignore the connection lost private final List<INode> m_observersThatTriedToJoinDuringStartup = Collections.synchronizedList(new ArrayList<>()); private InGameLobbyWatcherWrapper m_inGameLobbyWatcher; public ServerLauncher(final int clientCount, final IRemoteMessenger remoteMessenger, final IChannelMessenger channelMessenger, final IMessenger messenger, final GameSelectorModel gameSelectorModel, final PlayerListing playerListing, final Map<String, INode> remotelPlayers, final ServerModel serverModel, final boolean headless) { super(gameSelectorModel, headless); m_clientCount = clientCount; m_remoteMessenger = remoteMessenger; m_channelMessenger = channelMessenger; m_messenger = messenger; m_playerListing = playerListing; m_remotelPlayers = remotelPlayers; m_serverModel = serverModel; } public void setInGameLobbyWatcher(final InGameLobbyWatcherWrapper watcher) { m_inGameLobbyWatcher = watcher; } private boolean testShouldWeAbort() { if (m_abortLaunch) { return true; } if (m_gameData == null || m_serverModel == null) { return true; } else { final Map<String, String> players = m_serverModel.getPlayersToNodeListing(); if (players == null || players.isEmpty()) { return true; } else { for (final String player : players.keySet()) { if (players.get(player) == null) { return true; } } } } if (m_serverGame != null && m_serverGame.getPlayerManager() != null) { if (m_serverGame.getPlayerManager().isEmpty()) { return true; } } return false; } @Override protected void launchInNewThread(final Component parent) { try { // the order of this stuff does matter m_serverModel.setServerLauncher(this); m_serverReady = new ServerReady(m_clientCount); if (m_inGameLobbyWatcher != null) { m_inGameLobbyWatcher.setGameStatus(GameDescription.GameStatus.LAUNCHING, null); } m_serverModel.allowRemoveConnections(); m_ui = parent; if (m_headless) { HeadlessGameServer.log("Game Status: Launching"); } m_remoteMessenger.registerRemote(m_serverReady, ClientModel.CLIENT_READY_CHANNEL); m_gameData.doPreGameStartDataModifications(m_playerListing); s_logger.fine("Starting server"); m_abortLaunch = testShouldWeAbort(); byte[] gameDataAsBytes; try { gameDataAsBytes = gameDataToBytes(m_gameData); } catch (final IOException e) { ClientLogger.logQuietly(e); throw new IllegalStateException(e.getMessage()); } final Set<IGamePlayer> localPlayerSet = m_gameData.getGameLoader().createPlayers(m_playerListing.getLocalPlayerTypes()); final Messengers messengers = new Messengers(m_messenger, m_remoteMessenger, m_channelMessenger); m_serverGame = new ServerGame(m_gameData, localPlayerSet, m_remotelPlayers, messengers); m_serverGame.setInGameLobbyWatcher(m_inGameLobbyWatcher); if (m_headless) { HeadlessGameServer.setServerGame(m_serverGame); } // tell the clients to start, // later we will wait for them to all // signal that they are ready. ((IClientChannel) m_channelMessenger.getChannelBroadcastor(IClientChannel.CHANNEL_NAME)) .doneSelectingPlayers(gameDataAsBytes, m_serverGame.getPlayerManager().getPlayerMapping()); final boolean useSecureRandomSource = !m_remotelPlayers.isEmpty(); if (useSecureRandomSource) { // server game. // try to find an opponent to be the other side of the crypto random source. final PlayerID remotePlayer = m_serverGame.getPlayerManager().getRemoteOpponent(m_messenger.getLocalNode(), m_gameData); final CryptoRandomSource randomSource = new CryptoRandomSource(remotePlayer, m_serverGame); m_serverGame.setRandomSource(randomSource); } try { m_gameData.getGameLoader().startGame(m_serverGame, localPlayerSet, m_headless); } catch (final Exception e) { ClientLogger.logError("Failed to launch", e); m_abortLaunch = true; if (m_gameLoadingWindow != null) { m_gameLoadingWindow.doneWait(); } } if (m_headless) { HeadlessGameServer.log("Game Successfully Loaded. " + (m_abortLaunch ? "Aborting Launch." : "Starting Game.")); } if (m_abortLaunch) { m_serverReady.countDownAll(); } if (!m_serverReady.await(GameRunner.getServerStartGameSyncWaitTime(), TimeUnit.SECONDS)) { System.out.println("Waiting for clients to be ready timed out!"); m_abortLaunch = true; } m_remoteMessenger.unregisterRemote(ClientModel.CLIENT_READY_CHANNEL); final Thread t = new Thread("Triplea, start server game") { @Override public void run() { try { m_isLaunching = false; m_abortLaunch = testShouldWeAbort(); if (!m_abortLaunch) { if (useSecureRandomSource) { warmUpCryptoRandomSource(); } if (m_gameLoadingWindow != null) { m_gameLoadingWindow.doneWait(); } if (m_headless) { HeadlessGameServer.log("Starting Game Delegates."); } m_serverGame.startGame(); } else { stopGame(); if (!m_headless) { SwingUtilities.invokeLater( () -> JOptionPane.showMessageDialog(m_ui, "Problem during startup, game aborted.")); } else { System.out.println("Problem during startup, game aborted."); } } } catch (final MessengerException me) { // if just connection lost, no need to scare the user with some giant stack trace if (me instanceof ConnectionLostException) { System.out.println("Game Player disconnection: " + me.getMessage()); } else { me.printStackTrace(System.out); } // we lost a connection // wait for the connection handler to notice, and shut us down try { // we are already aborting the launch if (!m_abortLaunch) { if (!m_errorLatch.await(GameRunner.getServerObserverJoinWaitTime() + GameRunner.ADDITIONAL_SERVER_ERROR_DISCONNECTION_WAIT_TIME, TimeUnit.SECONDS)) { System.err.println("Waiting on error latch timed out!"); } } } catch (final InterruptedException e) { ClientLogger.logQuietly(e); } stopGame(); } catch (final Exception e) { e.printStackTrace(System.err); if (m_headless) { System.out.println(games.strategy.debug.DebugUtils.getThreadDumps()); HeadlessGameServer.sendChat("If this is a repeatable issue or error, please make a copy of this savegame " + "and contact a Mod and/or file a bug report."); } stopGame(); } // having an oddball issue with the zip stream being closed while parsing to load default game. might be // caused by closing of stream while unloading map resources. ThreadUtil.sleep(200); // either game ended, or aborted, or a player left or disconnected if (m_headless) { try { System.out.println("Game ended, going back to waiting."); if (m_serverModel != null) { // if we do not do this, we can get into an infinite loop of launching a game, // then crashing out, then launching, etc. m_serverModel.setAllPlayersToNullNodes(); } final File f1 = new File(ClientContext.folderSettings().getSaveGamePath(), SaveGameFileChooser.getAutoSaveFileName()); final File f2 = new File(ClientContext.folderSettings().getSaveGamePath(), SaveGameFileChooser.getAutoSave2FileName()); final File f; if (!f1.exists() && !f2.exists()) { m_gameSelectorModel.resetGameDataToNull(); } else { if (!f1.exists() || f1.lastModified() < f2.lastModified()) { f = f2; } else { f = f1; } m_gameSelectorModel.load(f, null); } } catch (final Exception e) { ClientLogger.logQuietly(e); m_gameSelectorModel.resetGameDataToNull(); } } else { m_gameSelectorModel.loadDefaultGame(parent); } if (parent != null) { SwingUtilities.invokeLater(() -> JOptionPane.getFrameForComponent(parent).setVisible(true)); } m_serverModel.setServerLauncher(null); m_serverModel.newGame(); if (m_inGameLobbyWatcher != null) { m_inGameLobbyWatcher.setGameStatus(GameDescription.GameStatus.WAITING_FOR_PLAYERS, null); } if (m_headless) { // tell headless server to wait for new connections: HeadlessGameServer.waitForUsersHeadlessInstance(); HeadlessGameServer.log("Game Status: Waiting For Players"); } } }; t.start(); } finally { if (m_gameLoadingWindow != null) { m_gameLoadingWindow.doneWait(); } if (m_inGameLobbyWatcher != null) { m_inGameLobbyWatcher.setGameStatus(GameDescription.GameStatus.IN_PROGRESS, m_serverGame); } if (m_headless) { HeadlessGameServer.log("Game Status: In Progress"); } } } private void warmUpCryptoRandomSource() { // the first roll takes a while, initialize // here in the background so that the user doesnt notice final Thread t = new Thread("Warming up crypto random source") { @Override public void run() { try { m_serverGame.getRandomSource().getRandom(m_gameData.getDiceSides(), 2, "Warming up crypto random source"); } catch (final RuntimeException re) { re.printStackTrace(System.out); } } }; t.start(); } public void addObserver(final IObserverWaitingToJoin blockingObserver, final IObserverWaitingToJoin nonBlockingObserver, final INode newNode) { if (m_isLaunching) { m_observersThatTriedToJoinDuringStartup.add(newNode); nonBlockingObserver.cannotJoinGame("Game is launching, try again soon"); return; } m_serverGame.addObserver(blockingObserver, nonBlockingObserver, newNode); } private static byte[] gameDataToBytes(final GameData data) throws IOException { final ByteArrayOutputStream sink = new ByteArrayOutputStream(25000); new GameDataManager().saveGame(sink, data); sink.flush(); sink.close(); return sink.toByteArray(); } public void connectionLost(final INode node) { // System.out.println("Connection lost to: " + node); if (m_isLaunching) { // this is expected, we told the observer // he couldnt join, so now we loose the connection if (m_observersThatTriedToJoinDuringStartup.remove(node)) { return; } // a player has dropped out, abort m_abortLaunch = true; m_serverReady.countDownAll(); return; } // if we loose a connection to a player, shut down // the game (after saving) and go back to the main screen if (m_serverGame.getPlayerManager().isPlaying(node)) { if (m_serverGame.isGameSequenceRunning()) { saveAndEndGame(node); } else { stopGame(); } // if the game already exited do to a networking error // we need to let them continue m_errorLatch.countDown(); } else { // nothing to do // we just lost a connection to an observer // which is ok. } } private void stopGame() { if (!m_gameStopped) { m_gameStopped = true; if (m_serverGame != null) { m_serverGame.stopGame(); } } } private void saveAndEndGame(final INode node) { final DateFormat format = new SimpleDateFormat("MMM_dd_'at'_HH_mm"); SaveGameFileChooser.ensureMapsFolderExists(); // a hack, if headless save to the autosave to avoid polluting our savegames folder with a million saves final File f; if (m_headless) { final File f1 = new File(ClientContext.folderSettings().getSaveGamePath(), SaveGameFileChooser.getAutoSaveFileName()); final File f2 = new File(ClientContext.folderSettings().getSaveGamePath(), SaveGameFileChooser.getAutoSave2FileName()); if (f1.lastModified() > f2.lastModified()) { f = f2; } else { f = f1; } } else { f = new File(ClientContext.folderSettings().getSaveGamePath(), "connection_lost_on_" + format.format(new Date()) + ".tsvg"); } try { m_serverGame.saveGame(f); } catch (final Exception e) { ClientLogger.logQuietly(e); if (m_headless && HeadlessGameServer.getInstance() != null) { HeadlessGameServer.getInstance().printThreadDumpsAndStatus(); // TODO: We seem to be getting this bug once a week (1.8.0.1 and previous versions). Trying a fix for 1.8.0.3, // need to see if it // works. } } stopGame(); if (!m_headless) { SwingUtilities.invokeLater(() -> { final String message = "Connection lost to:" + node.getName() + " game is over. Game saved to:" + f.getName(); JOptionPane.showMessageDialog(JOptionPane.getFrameForComponent(m_ui), message); }); } else { System.out.println("Connection lost to:" + node.getName() + " game is over. Game saved to:" + f.getName()); } } } class ServerReady implements IServerReady { private final CountDownLatch m_latch; private final int m_clients; ServerReady(final int waitCount) { m_clients = waitCount; m_latch = new CountDownLatch(m_clients); } @Override public void clientReady() { m_latch.countDown(); } public void countDownAll() { for (int i = 0; i < m_clients; i++) { m_latch.countDown(); } } public void await() { try { m_latch.await(); } catch (final InterruptedException e) { ClientLogger.logQuietly(e); } } public boolean await(final long timeout, final TimeUnit timeUnit) { boolean didNotTimeOut = false; try { didNotTimeOut = m_latch.await(timeout, timeUnit); } catch (final InterruptedException e) { ClientLogger.logQuietly(e); } return didNotTimeOut; } }