diff --git a/WebContent/js/cah.constants.js b/WebContent/js/cah.constants.js index c3260ec..b926b5b 100644 --- a/WebContent/js/cah.constants.js +++ b/WebContent/js/cah.constants.js @@ -100,6 +100,7 @@ cah.$.DisconnectReason.BANNED = "B&"; cah.$.DisconnectReason.PING_TIMEOUT = "pt"; cah.$.DisconnectReason.KICKED = "k"; cah.$.DisconnectReason.MANUAL = "man"; +cah.$.DisconnectReason.IDLE_TIMEOUT = "it"; cah.$.ErrorCode = function() { // Dummy constructor to make Eclipse auto-complete. diff --git a/WebContent/js/cah.longpoll.handlers.js b/WebContent/js/cah.longpoll.handlers.js index 2fe972f..1b7195f 100644 --- a/WebContent/js/cah.longpoll.handlers.js +++ b/WebContent/js/cah.longpoll.handlers.js @@ -40,6 +40,7 @@ cah.longpoll.EventHandlers[cah.$.LongPollEvent.NEW_PLAYER] = function(data) { } }; +// TODO Not sure why this isn't done with localizable strings in constants. cah.longpoll.EventHandlers[cah.$.LongPollEvent.PLAYER_LEAVE] = function(data) { var friendly_reason = "Leaving"; var show = !cah.hideConnectQuit; @@ -48,6 +49,9 @@ cah.longpoll.EventHandlers[cah.$.LongPollEvent.PLAYER_LEAVE] = function(data) { friendly_reason = "Banned"; show = true; break; + case cah.$.DisconnectReason.IDLE_TIMEOUT: + friendly_reason = "Kicked due to idle"; + break; case cah.$.DisconnectReason.KICKED: friendly_reason = "Kicked by server administrator"; show = true; @@ -133,15 +137,15 @@ cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_PLAYER_LEAVE] = function(dat }; cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_SPECTATOR_JOIN] = function(data) { - cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.spectatorJoin, - data[cah.$.LongPollResponse.NICKNAME], - "spectator join (if you just joined a game this may be OK)"); - }; + cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.spectatorJoin, + data[cah.$.LongPollResponse.NICKNAME], + "spectator join (if you just joined a game this may be OK)"); +}; - cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_SPECTATOR_LEAVE] = function(data) { - cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.spectatorLeave, - data[cah.$.LongPollResponse.NICKNAME], "spectator leave"); - }; +cah.longpoll.EventHandlers[cah.$.LongPollEvent.GAME_SPECTATOR_LEAVE] = function(data) { + cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.spectatorLeave, + data[cah.$.LongPollResponse.NICKNAME], "spectator leave"); +}; cah.longpoll.EventHandlers[cah.$.LongPollEvent.HAND_DEAL] = function(data) { cah.longpoll.EventHandlers.__gameEvent(data, cah.Game.prototype.dealtCards, diff --git a/WebContent/js/cah.longpoll.js b/WebContent/js/cah.longpoll.js index 2e5ac02..31f7182 100644 --- a/WebContent/js/cah.longpoll.js +++ b/WebContent/js/cah.longpoll.js @@ -28,8 +28,7 @@ */ cah.longpoll = {}; -cah.longpoll.TIMEOUT = 45 * 1000; -// cah.longpoll.TIMEOUT = 30 * 1000; +cah.longpoll.TIMEOUT = 30 * 1000; /** * Backoff when there was an error. diff --git a/src/net/socialgamer/cah/Constants.java b/src/net/socialgamer/cah/Constants.java index 302764f..324616a 100644 --- a/src/net/socialgamer/cah/Constants.java +++ b/src/net/socialgamer/cah/Constants.java @@ -101,6 +101,10 @@ public class Constants { * The client was banned by the server administrator. */ BANNED("B&"), + /** + * The client made no user-caused requests within the timeout window. + */ + IDLE_TIMEOUT("it"), /** * The client was kicked by the server administrator. */ diff --git a/src/net/socialgamer/cah/UserPing.java b/src/net/socialgamer/cah/UserPing.java index 0e01e2f..0d97a08 100644 --- a/src/net/socialgamer/cah/UserPing.java +++ b/src/net/socialgamer/cah/UserPing.java @@ -31,7 +31,7 @@ import com.google.inject.Inject; /** - * Timer task to check for disconnected clients. + * Timer task to check for disconnected and idle clients. * * @author Andy Janata (ajanata@gmail.com) */ @@ -46,6 +46,6 @@ public class UserPing extends TimerTask { @Override public void run() { - users.checkForPingTimeouts(); + users.checkForPingAndIdleTimeouts(); } } diff --git a/src/net/socialgamer/cah/data/ConnectedUsers.java b/src/net/socialgamer/cah/data/ConnectedUsers.java index 758bb6f..b061481 100644 --- a/src/net/socialgamer/cah/data/ConnectedUsers.java +++ b/src/net/socialgamer/cah/data/ConnectedUsers.java @@ -26,10 +26,9 @@ package net.socialgamer.cah.data; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.Map; -import java.util.Set; +import java.util.Map.Entry; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -60,7 +59,12 @@ public class ConnectedUsers { /** * Duration of a ping timeout, in nanoseconds. */ - public static final long PING_TIMEOUT = TimeUnit.SECONDS.toNanos(45); + public static final long PING_TIMEOUT = TimeUnit.SECONDS.toNanos(90); + + /** + * Duration of an idle timeout, in nanoseconds. + */ + public static final long IDLE_TIMEOUT = TimeUnit.MINUTES.toNanos(60); /** * Key (username) must be stored in lower-case to facilitate case-insensitivity in nicks. @@ -158,25 +162,35 @@ public class ConnectedUsers { /** * Check for any users that have not communicated with the server within the ping timeout delay, - * and remove users which have not so communicated. + * and remove users which have not so communicated. Also remove clients which are still connected, + * but have not actually done anything for a long time. */ - public void checkForPingTimeouts() { - final Set removedUsers = new HashSet(); + public void checkForPingAndIdleTimeouts() { + final Map removedUsers = new HashMap(); synchronized (users) { final Iterator iterator = users.values().iterator(); while (iterator.hasNext()) { final User u = iterator.next(); + DisconnectReason reason = null; if (System.nanoTime() - u.getLastHeardFrom() > PING_TIMEOUT) { - removedUsers.add(u); + reason = DisconnectReason.PING_TIMEOUT; + } + else if (!u.isAdmin() && System.nanoTime() - u.getLastUserAction() > IDLE_TIMEOUT) { + reason = DisconnectReason.IDLE_TIMEOUT; + } + if (null != reason) { + removedUsers.put(u, reason); iterator.remove(); } } } // Do this later to not keep users locked - for (final User u : removedUsers) { + for (final Entry entry : removedUsers.entrySet()) { try { - u.noLongerVaild(); - notifyRemoveUser(u, DisconnectReason.PING_TIMEOUT); + entry.getKey().noLongerVaild(); + notifyRemoveUser(entry.getKey(), entry.getValue()); + logger.info(String.format("Automatically kicking user %s due to %s", entry.getKey(), + entry.getValue())); } catch (final Exception e) { logger.error("Unable to remove pinged-out user", e); } diff --git a/src/net/socialgamer/cah/data/User.java b/src/net/socialgamer/cah/data/User.java index fb5deca..d3c5193 100644 --- a/src/net/socialgamer/cah/data/User.java +++ b/src/net/socialgamer/cah/data/User.java @@ -46,6 +46,8 @@ public class User { private long lastHeardFrom = 0; + private long lastUserAction = 0; + private Game currentGame; private final String hostName; @@ -177,6 +179,14 @@ public class User { return lastHeardFrom; } + public void userDidSomething() { + lastUserAction = System.nanoTime(); + } + + public long getLastUserAction() { + return lastUserAction; + } + /** * @return False when this user object is no longer valid, probably because it pinged out. */ diff --git a/src/net/socialgamer/cah/servlets/AjaxServlet.java b/src/net/socialgamer/cah/servlets/AjaxServlet.java index 8e1282d..44c804c 100644 --- a/src/net/socialgamer/cah/servlets/AjaxServlet.java +++ b/src/net/socialgamer/cah/servlets/AjaxServlet.java @@ -65,6 +65,9 @@ public class AjaxServlet extends CahServlet { IOException { final PrintWriter out = response.getWriter(); final User user = (User) hSession.getAttribute(SessionAttribute.USER); + if (null != user) { + user.userDidSomething(); + } int serial = -1; if (request.getParameter(AjaxRequest.SERIAL.toString()) != null) { try {