package games.strategy.engine.framework.headlessGameServer; import java.io.File; import java.io.InputStream; import java.util.Date; import java.util.HashSet; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import games.strategy.debug.ClientLogger; import games.strategy.debug.DebugUtils; import games.strategy.engine.ClientContext; import games.strategy.engine.chat.Chat; import games.strategy.engine.chat.IChatPanel; import games.strategy.engine.data.GameData; import games.strategy.engine.data.properties.GameProperties; import games.strategy.engine.framework.GameRunner; import games.strategy.engine.framework.ServerGame; import games.strategy.engine.framework.startup.launcher.ILauncher; import games.strategy.engine.framework.startup.mc.GameSelectorModel; import games.strategy.engine.framework.startup.mc.ServerModel; import games.strategy.engine.framework.startup.mc.SetupPanelModel; import games.strategy.engine.framework.startup.ui.ClientSetupPanel; import games.strategy.engine.framework.startup.ui.ISetupPanel; import games.strategy.engine.framework.startup.ui.ServerSetupPanel; import games.strategy.engine.framework.ui.SaveGameFileChooser; import games.strategy.engine.lobby.server.LobbyServer; import games.strategy.net.INode; import games.strategy.net.IServerMessenger; import games.strategy.sound.ClipPlayer; import games.strategy.triplea.Constants; import games.strategy.util.MD5Crypt; import games.strategy.util.ThreadUtil; import games.strategy.util.TimeManager; /** * A way of hosting a game, but headless. */ public class HeadlessGameServer { static final Logger s_logger = Logger.getLogger(HeadlessGameServer.class.getName()); static HeadlessGameServerConsole s_console = null; private static HeadlessGameServer s_instance = null; private final AvailableGames m_availableGames; private final GameSelectorModel m_gameSelectorModel; private SetupPanelModel m_setupPanelModel = null; private final ScheduledExecutorService m_lobbyWatcherResetupThread = Executors.newScheduledThreadPool(1); private ServerGame m_iGame = null; private boolean m_shutDown = false; private final String m_startDate = TimeManager.getGMTString(new Date()); public static synchronized HeadlessGameServer getInstance() { return s_instance; } public static synchronized boolean headless() { if (getInstance() != null) { return true; } return Boolean.parseBoolean(System.getProperty(GameRunner.TRIPLEA_HEADLESS, "false")); } public Set<String> getAvailableGames() { return new HashSet<>(m_availableGames.getGameNames()); } public synchronized void setGameMapTo(final String gameName) { // don't change mid-game if (m_setupPanelModel.getPanel() != null && m_iGame == null) { if (!m_availableGames.getGameNames().contains(gameName)) { return; } m_gameSelectorModel.load(m_availableGames.getGameData(gameName), m_availableGames.getGameFilePath(gameName)); System.out.println("Changed to game map: " + gameName); } } public synchronized void loadGameSave(final File file) { // don't change mid-game if (m_setupPanelModel.getPanel() != null && m_iGame == null) { if (file == null || !file.exists()) { return; } m_gameSelectorModel.load(file, null); System.out.println("Changed to save: " + file.getName()); } } public synchronized void loadGameSave(final InputStream input, final String fileName) { // don't change mid-game if (m_setupPanelModel.getPanel() != null && m_iGame == null) { if (input == null || fileName == null) { return; } final GameData data = m_gameSelectorModel.getGameData(input); if (data == null) { System.out.println("Loading GameData failed for: " + fileName); return; } final String mapNameProperty = data.getProperties().get(Constants.MAP_NAME, ""); Set<String> availableMaps = m_availableGames.getAvailableMapFolderOrZipNames(); if (!availableMaps.contains(mapNameProperty) && !availableMaps.contains(mapNameProperty + "-master")) { System.out.println("Game mapName not in available games listing: " + mapNameProperty); return; } m_gameSelectorModel.load(data, fileName); System.out.println("Changed to user savegame: " + fileName); } } public synchronized void loadGameOptions(final byte[] bytes) { // don't change mid-game if (m_setupPanelModel.getPanel() != null && m_iGame == null) { if (bytes == null || bytes.length == 0) { return; } final GameData data = m_gameSelectorModel.getGameData(); if (data == null) { return; } final GameProperties props = data.getProperties(); if (props == null) { return; } GameProperties.applyByteMapToChangeProperties(bytes, props); System.out.println("Changed to user game options."); } } public static synchronized void setServerGame(final ServerGame serverGame) { final HeadlessGameServer instance = getInstance(); if (instance != null) { instance.m_iGame = serverGame; if (serverGame != null) { System.out.println("Game starting up: " + instance.m_iGame.isGameSequenceRunning() + ", GameOver: " + instance.m_iGame.isGameOver() + ", Players: " + instance.m_iGame.getPlayerManager().toString()); } } } public static synchronized void log(final String stdout) { final HeadlessGameServer instance = getInstance(); if (instance != null) { System.out.println(stdout); } } public static synchronized void sendChat(final String chatString) { final HeadlessGameServer instance = getInstance(); if (instance != null) { final Chat chat = instance.getChat(); if (chat != null) { try { chat.sendMessage(chatString, false); } catch (final Exception e) { ClientLogger.logQuietly(e); } } } } public String getSalt() { final String encryptedPassword = MD5Crypt.crypt(System.getProperty(GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, "")); final String salt = MD5Crypt.getSalt(MD5Crypt.MAGIC, encryptedPassword); return salt; } public String remoteShutdown(final String hashedPassword, final String salt) { final String password = System.getProperty(GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, ""); if (password.equals(GameRunner.NO_REMOTE_REQUESTS_ALLOWED)) { return "Host not accepting remote requests!"; } final String localPassword = System.getProperty(GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, ""); final String encryptedPassword = MD5Crypt.crypt(localPassword, salt); if (encryptedPassword.equals(hashedPassword)) { (new Thread(() -> { System.out.println("Remote Shutdown Initiated."); System.exit(0); })).start(); return null; } System.out.println("Attempted remote shutdown with invalid password."); return "Invalid password!"; } public String remoteStopGame(final String hashedPassword, final String salt) { final String password = System.getProperty(GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, ""); if (password.equals(GameRunner.NO_REMOTE_REQUESTS_ALLOWED)) { return "Host not accepting remote requests!"; } final String localPassword = System.getProperty(GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, ""); final String encryptedPassword = MD5Crypt.crypt(localPassword, salt); if (encryptedPassword.equals(hashedPassword)) { final ServerGame iGame = m_iGame; if (iGame != null) { (new Thread(() -> { System.out.println("Remote Stop Game Initiated."); SaveGameFileChooser.ensureMapsFolderExists(); 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.lastModified() > f2.lastModified()) { f = f2; } else { f = f1; } try { iGame.saveGame(f); } catch (final Exception e) { ClientLogger.logQuietly(e); } iGame.stopGame(); })).start(); } return null; } System.out.println("Attempted remote stop game with invalid password."); return "Invalid password!"; } public String remoteGetChatLog(final String hashedPassword, final String salt) { final String password = System.getProperty(GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, ""); if (password.equals(GameRunner.NO_REMOTE_REQUESTS_ALLOWED)) { return "Host not accepting remote requests!"; } final String localPassword = System.getProperty(GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, ""); final String encryptedPassword = MD5Crypt.crypt(localPassword, salt); if (encryptedPassword.equals(hashedPassword)) { final IChatPanel chat = getServerModel().getChatPanel(); if (chat == null || chat.getAllText() == null) { return "Empty or null chat"; } return chat.getAllText(); } System.out.println("Attempted remote get chat log with invalid password."); return "Invalid password!"; } public String remoteMutePlayer(final String playerName, final int minutes, final String hashedPassword, final String salt) { final String password = System.getProperty(GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, ""); if (password.equals(GameRunner.NO_REMOTE_REQUESTS_ALLOWED)) { return "Host not accepting remote requests!"; } final String localPassword = System.getProperty(GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, ""); final String encryptedPassword = MD5Crypt.crypt(localPassword, salt); // milliseconds (48 hours max) final long expire = System.currentTimeMillis() + (Math.max(0, Math.min(60 * 24 * 2, minutes)) * 1000 * 60); if (encryptedPassword.equals(hashedPassword)) { (new Thread(() -> { if (getServerModel() == null) { return; } final IServerMessenger messenger = getServerModel().getMessenger(); if (messenger == null) { return; } final Set<INode> nodes = messenger.getNodes(); if (nodes == null) { return; } try { for (final INode node : nodes) { final String realName = node.getName().split(" ")[0]; final String ip = node.getAddress().getHostAddress(); final String mac = messenger.getPlayerMac(node.getName()); if (realName.equals(playerName)) { System.out.println("Remote Mute of Player: " + playerName); messenger.NotifyUsernameMutingOfPlayer(realName, new Date(expire)); messenger.NotifyIPMutingOfPlayer(ip, new Date(expire)); messenger.NotifyMacMutingOfPlayer(mac, new Date(expire)); return; } } } catch (final Exception e) { ClientLogger.logQuietly(e); } })).start(); return null; } System.out.println("Attempted remote mute player with invalid password."); return "Invalid password!"; } public String remoteBootPlayer(final String playerName, final String hashedPassword, final String salt) { final String password = System.getProperty(GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, ""); if (password.equals(GameRunner.NO_REMOTE_REQUESTS_ALLOWED)) { return "Host not accepting remote requests!"; } final String localPassword = System.getProperty(GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, ""); final String encryptedPassword = MD5Crypt.crypt(localPassword, salt); if (encryptedPassword.equals(hashedPassword)) { (new Thread(() -> { if (getServerModel() == null) { return; } final IServerMessenger messenger = getServerModel().getMessenger(); if (messenger == null) { return; } final Set<INode> nodes = messenger.getNodes(); if (nodes == null) { return; } try { for (final INode node : nodes) { final String realName = node.getName().split(" ")[0]; if (realName.equals(playerName)) { System.out.println("Remote Boot of Player: " + playerName); messenger.removeConnection(node); } } } catch (final Exception e) { ClientLogger.logQuietly(e); } })).start(); return null; } System.out.println("Attempted remote boot player with invalid password."); return "Invalid password!"; } public String remoteBanPlayer(final String playerName, final int hours, final String hashedPassword, final String salt) { final String password = System.getProperty(GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, ""); if (password.equals(GameRunner.NO_REMOTE_REQUESTS_ALLOWED)) { return "Host not accepting remote requests!"; } final String localPassword = System.getProperty(GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, ""); final String encryptedPassword = MD5Crypt.crypt(localPassword, salt); // milliseconds (30 days max) final long expire = System.currentTimeMillis() + (Math.max(0, Math.min(24 * 30, hours)) * 1000 * 60 * 60); if (encryptedPassword.equals(hashedPassword)) { (new Thread(() -> { if (getServerModel() == null) { return; } final IServerMessenger messenger = getServerModel().getMessenger(); if (messenger == null) { return; } final Set<INode> nodes = messenger.getNodes(); if (nodes == null) { return; } try { for (final INode node : nodes) { final String realName = node.getName().split(" ")[0]; final String ip = node.getAddress().getHostAddress(); final String mac = messenger.getPlayerMac(node.getName()); if (realName.equals(playerName)) { System.out.println("Remote Ban of Player: " + playerName); try { messenger.NotifyUsernameMiniBanningOfPlayer(realName, new Date(expire)); } catch (final Exception e) { ClientLogger.logQuietly(e); } try { messenger.NotifyIPMiniBanningOfPlayer(ip, new Date(expire)); } catch (final Exception e) { ClientLogger.logQuietly(e); } try { messenger.NotifyMacMiniBanningOfPlayer(mac, new Date(expire)); } catch (final Exception e) { ClientLogger.logQuietly(e); } messenger.removeConnection(node); } } } catch (final Exception e) { ClientLogger.logQuietly(e); } })).start(); return null; } System.out.println("Attempted remote ban player with invalid password."); return "Invalid password!"; } ServerGame getIGame() { return m_iGame; } public boolean isShutDown() { return m_shutDown; } public HeadlessGameServer() { super(); if (s_instance != null) { throw new IllegalStateException("Instance already exists"); } s_instance = this; Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("Running ShutdownHook."); shutdown(); })); m_availableGames = new AvailableGames(); m_gameSelectorModel = new GameSelectorModel(); final String fileName = System.getProperty(GameRunner.TRIPLEA_GAME_PROPERTY, ""); if (fileName.length() > 0) { try { final File file = new File(fileName); m_gameSelectorModel.load(file, null); } catch (final Exception e) { m_gameSelectorModel.resetGameDataToNull(); } } final Runnable r = () -> { System.out.println("Headless Start"); m_setupPanelModel = new HeadlessServerSetupPanelModel(m_gameSelectorModel, null); m_setupPanelModel.showSelectType(); System.out.println("Waiting for users to connect."); waitForUsersHeadless(); }; final Thread t = new Thread(r, "Initialize Headless Server Setup Model"); t.start(); int reconnect; try { final String reconnectionSeconds = System.getProperty(GameRunner.LOBBY_GAME_RECONNECTION, "" + GameRunner.LOBBY_RECONNECTION_REFRESH_SECONDS_DEFAULT); reconnect = Math.max(Integer.parseInt(reconnectionSeconds), GameRunner.LOBBY_RECONNECTION_REFRESH_SECONDS_MINIMUM); } catch (final NumberFormatException e) { reconnect = GameRunner.LOBBY_RECONNECTION_REFRESH_SECONDS_DEFAULT; } m_lobbyWatcherResetupThread.scheduleAtFixedRate(() -> { try { restartLobbyWatcher(m_setupPanelModel, m_iGame); } catch (final Exception e) { ThreadUtil.sleep(10 * 60 * 1000); // try again, but don't catch it this time restartLobbyWatcher(m_setupPanelModel, m_iGame); } }, reconnect, reconnect, TimeUnit.SECONDS); s_logger.info("Game Server initialized"); } private static synchronized void restartLobbyWatcher(final SetupPanelModel setupPanelModel, final ServerGame iGame) { try { final ISetupPanel setup = setupPanelModel.getPanel(); if (setup == null) { return; } if (iGame != null) { return; } if (setup.canGameStart()) { return; } if (setup instanceof ServerSetupPanel) { ((ServerSetupPanel) setup).repostLobbyWatcher(iGame); } else if (setup instanceof HeadlessServerSetup) { ((HeadlessServerSetup) setup).repostLobbyWatcher(iGame); } } catch (final Exception e) { ClientLogger.logQuietly(e); } } public static void resetLobbyHostOldExtensionProperties() { for (final String property : getProperties()) { if (GameRunner.LOBBY_HOST.equals(property) || LobbyServer.TRIPLEA_LOBBY_PORT_PROPERTY.equals(property) || GameRunner.LOBBY_GAME_HOSTED_BY.equals(property)) { // for these 3 properties, we clear them after hosting, but back them up. final String oldValue = System.getProperty(property + GameRunner.OLD_EXTENSION); if (oldValue != null) { System.setProperty(property, oldValue); } } } } public static String[] getProperties() { return new String[] {GameRunner.TRIPLEA_GAME_PROPERTY, GameRunner.TRIPLEA_GAME_HOST_CONSOLE_PROPERTY, GameRunner.TRIPLEA_SERVER_PROPERTY, GameRunner.TRIPLEA_PORT_PROPERTY, GameRunner.TRIPLEA_NAME_PROPERTY, GameRunner.LOBBY_HOST, LobbyServer.TRIPLEA_LOBBY_PORT_PROPERTY, GameRunner.LOBBY_GAME_COMMENTS, GameRunner.LOBBY_GAME_HOSTED_BY, GameRunner.LOBBY_GAME_SUPPORT_EMAIL, GameRunner.LOBBY_GAME_SUPPORT_PASSWORD, GameRunner.LOBBY_GAME_RECONNECTION, GameRunner.TRIPLEA_SERVER_START_GAME_SYNC_WAIT_TIME, GameRunner.TRIPLEA_SERVER_OBSERVER_JOIN_WAIT_TIME, GameRunner.MAP_FOLDER}; } public String getStatus() { String message = "Server Start Date: " + m_startDate; final ServerGame game = getIGame(); if (game != null) { message += "\nIs currently running: " + game.isGameSequenceRunning() + "\nIs GameOver: " + game.isGameOver() + "\nGame: " + game.getData().getGameName() + "\nRound: " + game.getData().getSequence().getRound() + "\nPlayers: " + game.getPlayerManager().toString(); } else { message += "\nCurrently Waiting To Start A Game"; } return message; } public void printThreadDumpsAndStatus() { final StringBuilder sb = new StringBuilder(); sb.append("Dump to Log:"); sb.append("\n\nStatus:\n"); sb.append(getStatus()); sb.append("\n\nServer:\n"); sb.append(getServerModel()); sb.append("\n\n"); sb.append(DebugUtils.getThreadDumps()); sb.append("\n\n"); sb.append(DebugUtils.getMemory()); sb.append("\n\nDump finished.\n"); System.out.println(sb.toString()); } public synchronized void shutdown() { m_shutDown = true; printThreadDumpsAndStatus(); try { if (m_lobbyWatcherResetupThread != null) { m_lobbyWatcherResetupThread.shutdown(); } } catch (final Exception e) { ClientLogger.logQuietly(e); } try { if (m_iGame != null) { m_iGame.stopGame(); } } catch (final Exception e) { ClientLogger.logQuietly(e); } try { if (m_setupPanelModel != null) { final ISetupPanel setup = m_setupPanelModel.getPanel(); if (setup != null && setup instanceof ServerSetupPanel) { // this is causing a deadlock when in a shutdown hook, due to swing/awt // ((ServerSetupPanel) setup).shutDown(); } else if (setup != null && setup instanceof HeadlessServerSetup) { setup.shutDown(); } } } catch (final Exception e) { ClientLogger.logQuietly(e); } try { if (m_gameSelectorModel != null && m_gameSelectorModel.getGameData() != null) { m_gameSelectorModel.getGameData().clearAllListeners(); } } catch (final Exception e) { ClientLogger.logQuietly(e); } s_instance = null; m_setupPanelModel = null; m_iGame = null; System.out.println("Shutdown Script Finished."); } public void waitForUsersHeadless() { setServerGame(null); final Runnable r = () -> { while (!m_shutDown) { if (!ThreadUtil.sleep(8000)) { m_shutDown = true; break; } if (m_setupPanelModel != null && m_setupPanelModel.getPanel() != null && m_setupPanelModel.getPanel().canGameStart()) { final boolean started = startHeadlessGame(m_setupPanelModel); if (!started) { System.out.println("Error in launcher, going back to waiting."); } else { // TODO: need a latch instead? break; } } } }; final Thread t = new Thread(r, "Headless Server Waiting For Users To Connect And Start"); t.start(); } private static synchronized boolean startHeadlessGame(final SetupPanelModel setupPanelModel) { try { if (setupPanelModel != null && setupPanelModel.getPanel() != null && setupPanelModel.getPanel().canGameStart()) { System.out.println("Starting Game: " + setupPanelModel.getGameSelectorModel().getGameData().getGameName() + ", Round: " + setupPanelModel.getGameSelectorModel().getGameData().getSequence().getRound()); setupPanelModel.getPanel().preStartGame(); final ILauncher launcher = setupPanelModel.getPanel().getLauncher(); if (launcher != null) { launcher.launch(null); } setupPanelModel.getPanel().postStartGame(); return launcher != null; } } catch (final Exception e) { ClientLogger.logQuietly(e); final ServerModel model = getServerModel(setupPanelModel); if (model != null) { // if we do not do this, we can get into an infinite loop of launching a game, then crashing out, // then launching, etc. model.setAllPlayersToNullNodes(); } } return false; } public static void waitForUsersHeadlessInstance() { final HeadlessGameServer server = getInstance(); if (server == null) { System.err.println("Couldn't find instance."); System.exit(-1); } else { System.out.println("Waiting for users to connect."); server.waitForUsersHeadless(); } } SetupPanelModel getSetupPanelModel() { return m_setupPanelModel; } ServerModel getServerModel() { return getServerModel(m_setupPanelModel); } static ServerModel getServerModel(final SetupPanelModel setupPanelModel) { if (setupPanelModel == null) { return null; } final ISetupPanel setup = setupPanelModel.getPanel(); if (setup == null) { return null; } if (setup instanceof ServerSetupPanel) { return ((ServerSetupPanel) setup).getModel(); } else if (setup instanceof HeadlessServerSetup) { return ((HeadlessServerSetup) setup).getModel(); } return null; } /** * todo, replace with something better * Get the chat for the game, or null if there is no chat. */ public Chat getChat() { final ISetupPanel model = m_setupPanelModel.getPanel(); if (model instanceof ServerSetupPanel) { return model.getChatPanel().getChat(); } else if (model instanceof ClientSetupPanel) { return model.getChatPanel().getChat(); } else if (model instanceof HeadlessServerSetup) { return model.getChatPanel().getChat(); } else { return null; } } public static void main(final String[] args) { GameRunner.handleCommandLineArgs(args, getProperties(), GameRunner.GameMode.HEADLESS_BOT); ClipPlayer.setBeSilentInPreferencesWithoutAffectingCurrent(true); try { new HeadlessGameServer(); } catch (final Exception e) { ClientLogger.logError("Failed to start game server: " + e); } } }