Improved stability and other related fixes:

* server: added limit to max chat message (related to #11285);
* gui: fixed possible error after auto-submit deck;
* gui: fixed possible error after end game dialog;
* refactor: other code improves;
This commit is contained in:
Oleg Agafonov 2023-11-25 12:29:54 +04:00
parent 81f97c3b0e
commit d1f9e9cc90
15 changed files with 86 additions and 48 deletions

View file

@ -5,6 +5,7 @@ import mage.client.SessionHandler;
import mage.client.cards.BigCard;
import mage.client.dialog.PreferencesDialog;
import mage.client.util.GUISizeHelper;
import mage.constants.Constants;
import mage.view.ChatMessage.MessageColor;
import mage.view.ChatMessage.MessageType;
import org.mage.card.arcane.ManaSymbols;
@ -103,7 +104,6 @@ public class ChatPanelBasic extends javax.swing.JPanel {
jScrollPaneTxt.getViewport().setBackground(new Color(0, 0, 0, CHAT_ALPHA));
jScrollPaneTxt.setViewportBorder(null);
}
}
public void cleanUp() {
@ -398,6 +398,12 @@ public class ChatPanelBasic extends javax.swing.JPanel {
public void handleKeyTyped(java.awt.event.KeyEvent evt) {
if (evt.getKeyChar() == KeyEvent.VK_ENTER) {
if (this.txtMessage.getText().length() > Constants.MAX_CHAT_MESSAGE_SIZE) {
JOptionPane.showMessageDialog(null, "Can't send too long message", "Chat", JOptionPane.WARNING_MESSAGE);
return;
}
if (parentChatRef != null) {
SessionHandler.sendChatMessage(parentChatRef.chatId, this.txtMessage.getText());
} else {

View file

@ -1497,13 +1497,13 @@ public class DeckEditorPanel extends javax.swing.JPanel {
timeToSubmit = 60;
this.btnSubmitTimer.setEnabled(false);
ScheduledFuture scheduledFuture = scheduledExecutorService.schedule((Callable) () -> {
scheduledExecutorService.schedule(() -> {
if (updateDeckTask != null) {
updateDeckTask.cancel(true);
}
if (SessionHandler.submitDeck(mode, tableId, deck.getDeckCardLists())) {
removeDeckEditor();
SwingUtilities.invokeLater(this::removeDeckEditor);
}
return null;
}, 60, TimeUnit.SECONDS);

View file

@ -11,6 +11,7 @@ import mage.constants.PlayerAction;
import mage.constants.TurnPhase;
import org.apache.log4j.Logger;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.Serializable;
@ -40,6 +41,7 @@ public class FeedbackPanel extends javax.swing.JPanel {
private ChatPanelBasic connectedChatPanel;
private Map<String, Serializable> lastOptions = new HashMap<>();
private static final int AUTO_CLOSE_END_DIALOG_TIMEOUT_SECS = 8;
private static final ScheduledExecutorService WORKER = Executors.newSingleThreadScheduledExecutor();
/**
@ -153,16 +155,18 @@ public class FeedbackPanel extends javax.swing.JPanel {
*/
private void endWithTimeout() {
Runnable task = () -> {
LOGGER.info("Ending game...");
Component c = MageFrame.getGame(gameId);
while (c != null && !(c instanceof GamePane)) {
c = c.getParent();
}
if (c != null && c.isVisible()) { // check if GamePanel still visible
FeedbackPanel.this.btnRight.doClick();
}
SwingUtilities.invokeLater(() -> {
LOGGER.info("Ending game...");
Component c = MageFrame.getGame(gameId);
while (c != null && !(c instanceof GamePane)) {
c = c.getParent();
}
if (c != null && c.isVisible()) { // check if GamePanel still visible
FeedbackPanel.this.btnRight.doClick();
}
});
};
WORKER.schedule(task, 8, TimeUnit.SECONDS);
WORKER.schedule(task, AUTO_CLOSE_END_DIALOG_TIMEOUT_SECS, TimeUnit.SECONDS);
}
public void updateOptions(Map<String, Serializable> options) {

View file

@ -87,12 +87,12 @@ public class MageActionCallback implements ActionCallback {
private Date enlargeredViewOpened;
private volatile EnlargedWindowState enlargedWindowState = EnlargedWindowState.CLOSED;
//private volatile boolean enlargedImageWindowOpen = false;
// shows the alternative card the normal card or the alternative card (copy source, other flip side, other transformed side)
private volatile EnlargeMode enlargeMode;
private static final ScheduledExecutorService timeoutExecutor = Executors.newScheduledThreadPool(1);
private ScheduledFuture<?> hideTimeout;
private static final ScheduledExecutorService hideEnlargedCardWorker = Executors.newScheduledThreadPool(1);
private ScheduledFuture<?> hideEnlagedCardTask;
private static final int HIDE_ENLARGED_CARD_TIMEOUT_MS = 700;
private MageCard prevCardPanel;
private boolean startedDragging;
@ -472,7 +472,7 @@ public class MageActionCallback implements ActionCallback {
}
hideTooltipPopup();
cancelTimeout();
cancelHidingEnlagedCard();
Component parentComponent = SwingUtilities.getRoot(cardPanel);
if (parentComponent == null) {
// virtual card (example: show card popup in non cards panel like PickChoiceDialog)
@ -514,7 +514,7 @@ public class MageActionCallback implements ActionCallback {
popupTextWindowOpen = true;
}
if (enlargedWindowState != EnlargedWindowState.CLOSED) {
cancelTimeout();
cancelHidingEnlagedCard();
displayEnlargedCard(cardPanel.getOriginal(), data);
}
}
@ -552,7 +552,7 @@ public class MageActionCallback implements ActionCallback {
public void hideAll(UUID gameId) {
hideTooltipPopup();
startHideTimeout();
startHidingEnlagedCard();
this.popupTextWindowOpen = false;
if (gameId != null) {
ArrowBuilder.getBuilder().removeArrowsByType(gameId, ArrowBuilder.Type.TARGET);
@ -740,14 +740,16 @@ public class MageActionCallback implements ActionCallback {
return PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_RENDERING_ABILITY_TEXT_OVERLAY, "true").equals("true");
}
private synchronized void startHideTimeout() {
cancelTimeout();
hideTimeout = timeoutExecutor.schedule(this::hideEnlargedCard, 700, TimeUnit.MILLISECONDS);
private synchronized void startHidingEnlagedCard() {
cancelHidingEnlagedCard();
hideEnlagedCardTask = hideEnlargedCardWorker.schedule(
() -> SwingUtilities.invokeLater(this::hideEnlargedCard), HIDE_ENLARGED_CARD_TIMEOUT_MS, TimeUnit.MILLISECONDS
);
}
private synchronized void cancelTimeout() {
if (hideTimeout != null) {
hideTimeout.cancel(false);
private synchronized void cancelHidingEnlagedCard() {
if (hideEnlagedCardTask != null) {
hideEnlagedCardTask.cancel(false);
}
}

View file

@ -647,7 +647,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
while (!executor.isTerminated()) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException ie) {
} catch (InterruptedException ignore) {
}
}
}

View file

@ -43,6 +43,8 @@ public final class Constants {
public static final int MAX_AVATAR_ID = 32;
public static final int DEFAULT_AVATAR_ID = 10;
public static final int MAX_CHAT_MESSAGE_SIZE = 500; // ignore too big messages
/**
* Time each player has during the game to play using his\her priority.
*/

View file

@ -45,16 +45,20 @@ public interface MageServer {
Object serverGetPromotionMessages(String sessionId) throws MageException;
// sync cards send sets db
// TODO: outdated, no more client/server sync, can be removed?
@Deprecated // TODO: outdated, no more client/server sync, can be removed?
List<ExpansionInfo> syncGetMissingExpansionData(List<String> codes);
@Deprecated // TODO: outdated, no more client/server sync, can be removed?
List<CardInfo> syncGetMissingCardsData(List<String> classNames);
ServerState getServerState() throws MageException; // TODO: need stable update process, so rename it after few releases
// TODO: miss session
UUID serverGetMainRoomId() throws MageException;
// TODO: miss session
List<RoomUsersView> roomGetUsers(UUID roomId) throws MageException;
// TODO: miss session
List<MatchView> roomGetFinishedMatches(UUID roomId) throws MageException;
TableView roomCreateTable(String sessionId, UUID roomId, MatchOptions matchOptions) throws MageException;
@ -81,22 +85,29 @@ public interface MageServer {
boolean tableIsOwner(String sessionId, UUID roomId, UUID tableId) throws MageException;
// TODO: miss session
TableView roomGetTableById(UUID roomId, UUID tableId) throws MageException;
// TODO: miss session
List<TableView> roomGetAllTables(UUID roomId) throws MageException;
// TODO: miss session
void chatSendMessage(UUID chatId, String userName, String message) throws MageException;
void chatJoin(UUID chatId, String sessionId, String userName) throws MageException;
void chatLeave(UUID chatId, String sessionId) throws MageException;
// TODO: miss session
UUID chatFindByGame(UUID gameId) throws MageException;
// TODO: miss session
UUID chatFindByTable(UUID tableId) throws MageException;
// TODO: miss session
UUID chatFindByTournament(UUID tournamentId) throws MageException;
// TODO: miss session
UUID chatFindByRoom(UUID roomId) throws MageException;
boolean matchStart(String sessionId, UUID roomId, UUID tableId) throws MageException;
@ -135,6 +146,7 @@ public interface MageServer {
void tournamentQuit(UUID tournamentId, String sessionId) throws MageException;
// TODO: miss session
TournamentView tournamentFindById(UUID tournamentId) throws MageException;
void draftJoin(UUID draftId, String sessionId) throws MageException;

View file

@ -261,6 +261,7 @@ public class Connection {
return userData;
}
@Deprecated // TODO: server side cards do not supports now, so remove outdated code (db sync with server)
public boolean isForceDBComparison() {
return forceDBComparison;
}

View file

@ -424,12 +424,8 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ {
* @return
*/
protected Integer addActionsTimed() {
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return addActions(root, maxDepth, Integer.MIN_VALUE, Integer.MAX_VALUE);
}
});
// run new game simulation in parallel thread
FutureTask<Integer> task = new FutureTask<>(() -> addActions(root, maxDepth, Integer.MIN_VALUE, Integer.MAX_VALUE));
pool.execute(task);
try {
int maxSeconds = maxThink;
@ -437,18 +433,19 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ {
maxSeconds = 3600;
}
logger.debug("maxThink: " + maxSeconds + " seconds ");
if (task.get(maxSeconds, TimeUnit.SECONDS) != null) {
return task.get(maxSeconds, TimeUnit.SECONDS);
Integer res = task.get(maxSeconds, TimeUnit.SECONDS);
if (res != null) {
return res;
}
} catch (TimeoutException e) {
logger.info("simulating - timed out");
task.cancel(true);
} catch (ExecutionException e) {
// exception error in simulated game
e.printStackTrace();
task.cancel(true);
// real games: must catch
// real games: must catch and log
// unit tests: must raise again for test fail
logger.error("AI simulation game catch error: " + e.getCause(), e);
if (this.isTestsMode()) {
throw new IllegalStateException("One of the simulated games raise the error: " + e.getCause());
}

View file

@ -7,6 +7,7 @@ import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.cards.repository.ExpansionInfo;
import mage.cards.repository.ExpansionRepository;
import mage.constants.Constants;
import mage.constants.ManaType;
import mage.constants.PlayerAction;
import mage.constants.TableState;
@ -488,7 +489,13 @@ public class MageServerImpl implements MageServer {
@Override
//FIXME: why no sessionId here???
public void chatSendMessage(final UUID chatId, final String userName, final String message) throws MageException {
if (message.length() > Constants.MAX_CHAT_MESSAGE_SIZE) {
logger.error("Chat message too big: " + message.length() + ", from user " + userName);
return;
}
try {
// TODO: check and replace all usage of callExecutor.execute() by execute("actionName")
callExecutor.execute(
() -> managerFactory.chatManager().broadcast(chatId, userName, HtmlEscape.escapeHtml4(message), MessageColor.BLUE, true, null, ChatMessage.MessageType.TALK, null)
);
@ -953,8 +960,14 @@ public class MageServerImpl implements MageServer {
}
@Override
//TODO: check how often it is used
public ServerState getServerState() throws MageException {
// called one time per login, must work without auth and with diff versions
try {
// some ddos protection
Thread.sleep(1000);
} catch (InterruptedException ignore) {
}
try {
return new ServerState(
GameFactory.instance.getGameTypes(),

View file

@ -36,7 +36,6 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
public class TableManagerImpl implements TableManager {
protected final ScheduledExecutorService expireExecutor = Executors.newSingleThreadScheduledExecutor();
// protected static ScheduledExecutorService expireExecutor = ThreadExecutorImpl.getInstance().getExpireExecutor();
private final ManagerFactory managerFactory;
private final Logger logger = Logger.getLogger(TableManagerImpl.class);
private final DateFormat formatter = new SimpleDateFormat("HH:mm:ss");

View file

@ -26,12 +26,15 @@ public class UserManagerImpl implements UserManager {
private static final int SERVER_TIMEOUTS_USER_DISCONNECT_FROM_SERVER_AFTER_SECS = 3 * 60; // removes from all games and chats too (can be seen in users list with disconnected status)
private static final int SERVER_TIMEOUTS_USER_REMOVE_FROM_SERVER_AFTER_SECS = 8 * 60; // removes from users list
private static final int SERVER_TIMEOUTS_USER_EXPIRE_CHECK_SECS = 60;
private static final int SERVER_TIMEOUTS_USERS_LIST_UPDATE_SECS = 4; // server side updates (client use own timeouts to request users list)
private static final Logger logger = Logger.getLogger(UserManagerImpl.class);
protected final ScheduledExecutorService expireExecutor = Executors.newSingleThreadScheduledExecutor();
protected final ScheduledExecutorService userListExecutor = Executors.newSingleThreadScheduledExecutor();
private List<UserView> userInfoList = new ArrayList<>();
private List<UserView> userInfoList = new ArrayList<>(); // all users list for main room/chat
private final ManagerFactory managerFactory;
@ -46,9 +49,8 @@ public class UserManagerImpl implements UserManager {
public void init() {
USER_EXECUTOR = managerFactory.threadExecutor().getCallExecutor();
expireExecutor.scheduleAtFixedRate(this::checkExpired, 60, 60, TimeUnit.SECONDS);
userListExecutor.scheduleAtFixedRate(this::updateUserInfoList, 4, 4, TimeUnit.SECONDS);
expireExecutor.scheduleAtFixedRate(this::checkExpired, SERVER_TIMEOUTS_USER_EXPIRE_CHECK_SECS, SERVER_TIMEOUTS_USER_EXPIRE_CHECK_SECS, TimeUnit.SECONDS);
userListExecutor.scheduleAtFixedRate(this::updateUserInfoList, SERVER_TIMEOUTS_USERS_LIST_UPDATE_SECS, SERVER_TIMEOUTS_USERS_LIST_UPDATE_SECS, TimeUnit.SECONDS);
}
@Override
@ -254,7 +256,7 @@ public class UserManagerImpl implements UserManager {
}
/**
* This method recreated the user list that will be send to all clients
* This method recreated the user list that will be sent to all clients
*/
private void updateUserInfoList() {
try {

View file

@ -335,7 +335,7 @@ public class GameController implements GameCallback {
gameFuture = gameExecutor.submit(worker);
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
} catch (InterruptedException ignore) {
}
if (game.getState().getChoosingPlayerId() != null) {
// start timer to force player to choose starting player otherwise loosing by being idle

View file

@ -37,7 +37,7 @@ public class GameSessionPlayer extends GameSessionWatcher {
public GameSessionPlayer(ManagerFactory managerFactory, Game game, UUID userId, UUID playerId) {
super(managerFactory.userManager(), userId, game, true);
this.userManager = managerFactory.userManager();
callExecutor = managerFactory.threadExecutor().getCallExecutor();
this.callExecutor = managerFactory.threadExecutor().getCallExecutor();
this.playerId = playerId;
}

View file

@ -45,11 +45,11 @@ public class GamesRoomImpl extends RoomImpl implements GamesRoom, Serializable {
UPDATE_EXECUTOR.scheduleAtFixedRate(() -> {
try {
update();
} catch (Exception ex) {
LOGGER.fatal("Games room update exception! " + ex.toString(), ex);
} catch (Exception e) {
LOGGER.fatal("Games room update error: " + e.getMessage(), e);
}
}, 2, 2, TimeUnit.SECONDS);
}, 2, 2, TimeUnit.SECONDS); // TODO: is it ok for performance?
}
@Override