From af97c1b4d5dbe23792cceb7967c513c9a672cd6c Mon Sep 17 00:00:00 2001
From: Ty Conner <tyconner@itcproductions.com>
Date: Sun, 12 Apr 2020 18:54:32 -0400
Subject: [PATCH] Disallow [re]entry into world if shutdown is in progress
 (#2794)

* Disallow [re]entry into world if shutdown is in progress

* Update Session.cs

* Update Session.cs

* Update ServerManager.cs

* session management

* Update ServerManager.cs

* Update Session.cs
---
 Source/ACE.Server/Managers/ServerManager.cs   | 67 ++++++++++++++++---
 .../Network/Enum/SessionTerminationReason.cs  |  2 +
 .../Network/Handlers/CharacterHandler.cs      | 30 +++++++++
 .../Network/Managers/NetworkManager.cs        | 13 ++++
 Source/ACE.Server/Network/Session.cs          | 15 +++--
 5 files changed, 111 insertions(+), 16 deletions(-)

diff --git a/Source/ACE.Server/Managers/ServerManager.cs b/Source/ACE.Server/Managers/ServerManager.cs
index 4c8ceca6fb..c49f587839 100644
--- a/Source/ACE.Server/Managers/ServerManager.cs
+++ b/Source/ACE.Server/Managers/ServerManager.cs
@@ -7,6 +7,7 @@
 using ACE.Database;
 using ACE.Entity.Enum;
 using ACE.Server.Network.GameMessages.Messages;
+using ACE.Server.Network.Managers;
 
 namespace ACE.Server.Managers
 {
@@ -29,6 +30,11 @@ public static class ServerManager
         /// </summary>
         public static bool ShutdownInitiated { get; private set; }
 
+        /// <summary>
+        /// Indicates server shutting down.
+        /// </summary>
+        public static bool ShutdownInProgress { get; private set; }
+
         /// <summary>
         /// The amount of seconds that the server will wait before unloading the application.
         /// </summary>
@@ -116,6 +122,8 @@ private static void ShutdownServer()
                 Thread.Sleep(10);
             }
 
+            ShutdownInProgress = true;
+
             PropertyManager.ResyncVariables();
             PropertyManager.StopUpdating();
 
@@ -125,11 +133,28 @@ private static void ShutdownServer()
             foreach (var player in PlayerManager.GetAllOnline())
                 player.Session.LogOffPlayer(true);
 
-            log.Info("Waiting for all players to log off...");
+            // Wait for all players to log out
+            var logUpdateTS = DateTime.MinValue;
+            int playerCount;
+            while ((playerCount = PlayerManager.GetOnlineCount()) > 0)
+            {
+                logUpdateTS = LogStatusUpdate(logUpdateTS, $"Waiting for {playerCount} player{(playerCount > 1 ? "s" : "")} to log off...");
+                Thread.Sleep(10);
+            }
 
-            // wait 10 seconds for log-off
-            while (PlayerManager.GetOnlineCount() > 0)
+            log.Debug("Disconnecting all sessions...");
+
+            // disconnect each session
+            NetworkManager.DisconnectAllSessionsForShutdown();
+
+            // Wait for all sessions to drop out
+            logUpdateTS = DateTime.MinValue;
+            int sessionCount;
+            while ((sessionCount = NetworkManager.GetSessionCount()) > 0)
+            {
+                logUpdateTS = LogStatusUpdate(logUpdateTS, $"Waiting for {sessionCount} session{(sessionCount > 1 ? "s" : "")} to disconnect...");
                 Thread.Sleep(10);
+            }
 
             log.Debug("Adding all landblocks to destruction queue...");
 
@@ -137,30 +162,39 @@ private static void ShutdownServer()
             // The actual unloading will happen in WorldManager.UpdateGameWorld
             LandblockManager.AddAllActiveLandblocksToDestructionQueue();
 
-            log.Info("Waiting for all active landblocks to unload...");
-
-            while (LandblockManager.GetLoadedLandblocks().Count > 0)
+            // Wait for all landblocks to unload
+            logUpdateTS = DateTime.MinValue;
+            int landblockCount;
+            while ((landblockCount = LandblockManager.GetLoadedLandblocks().Count) > 0)
+            {
+                logUpdateTS = LogStatusUpdate(logUpdateTS, $"Waiting for {landblockCount} loaded landblock{(landblockCount > 1 ? "s" : "")} to unload...");
                 Thread.Sleep(10);
+            }
 
             log.Debug("Stopping world...");
 
             // Disabled thread update loop
             WorldManager.StopWorld();
 
-            log.Info("Waiting for world to stop...");
-
             // Wait for world to end
+            logUpdateTS = DateTime.MinValue;
             while (WorldManager.WorldActive)
+            {
+                logUpdateTS = LogStatusUpdate(logUpdateTS, "Waiting for world to stop...");
                 Thread.Sleep(10);
+            }
 
             log.Info("Saving OfflinePlayers that have unsaved changes...");
             PlayerManager.SaveOfflinePlayersWithChanges();
 
-            log.Info("Waiting for database queue to empty...");
-
             // Wait for the database queue to empty
-            while (DatabaseManager.Shard.QueueCount > 0)
+            logUpdateTS = DateTime.MinValue;
+            int shardQueueCount;
+            while ((shardQueueCount = DatabaseManager.Shard.QueueCount) > 0)
+            {
+                logUpdateTS = LogStatusUpdate(logUpdateTS, $"Waiting for database queue ({shardQueueCount}) to empty...");
                 Thread.Sleep(10);
+            }
 
             // Write exit to console/log
             log.Info($"Exiting at {DateTime.UtcNow}");
@@ -169,6 +203,17 @@ private static void ShutdownServer()
             Environment.Exit(Environment.ExitCode);
         }
 
+        private static DateTime LogStatusUpdate(DateTime logUpdateTS, string logMessage)
+        {
+            if (logUpdateTS == DateTime.MinValue || DateTime.UtcNow > logUpdateTS.ToUniversalTime())
+            {
+                log.Info(logMessage);
+                logUpdateTS = DateTime.UtcNow.AddSeconds(10);
+            }
+
+            return logUpdateTS;
+        }
+
         private static DateTime NotifyPlayersOfPendingShutdown(DateTime lastNoticeTime, DateTime shutdownTime)
         {
             var notify = false;
diff --git a/Source/ACE.Server/Network/Enum/SessionTerminationReason.cs b/Source/ACE.Server/Network/Enum/SessionTerminationReason.cs
index f29fb4971e..6463d73890 100644
--- a/Source/ACE.Server/Network/Enum/SessionTerminationReason.cs
+++ b/Source/ACE.Server/Network/Enum/SessionTerminationReason.cs
@@ -27,6 +27,7 @@ public enum SessionTerminationReason
         WorldClosed,
         AbnormalSequenceReceived,
         AccountLoggedIn,
+        ServerShuttingDown,
         AccountBanned
     }
     public static class SessionTerminationReasonHelper
@@ -52,6 +53,7 @@ public static class SessionTerminationReasonHelper
             "World is closed",
             "Client supplied an abnormal sequence",
             "Account was logged in, booting currently connected account in favor of new connection",
+            "Server is shutting down",
             "Account is banned"
         };
         public static string GetDescription(this SessionTerminationReason reason)
diff --git a/Source/ACE.Server/Network/Handlers/CharacterHandler.cs b/Source/ACE.Server/Network/Handlers/CharacterHandler.cs
index 5387ae0202..b7465d46d9 100644
--- a/Source/ACE.Server/Network/Handlers/CharacterHandler.cs
+++ b/Source/ACE.Server/Network/Handlers/CharacterHandler.cs
@@ -31,6 +31,12 @@ public static void CharacterCreate(ClientMessage message, Session session)
             if (clientString != session.Account)
                 return;
 
+            if (ServerManager.ShutdownInProgress)
+            {
+                session.SendCharacterError(CharacterError.LogonServerFull);
+                return;
+            }
+
             if (WorldManager.WorldStatus == WorldManager.WorldStatusState.Open || session.AccessLevel > AccessLevel.Player)
                 CharacterCreateEx(message, session);
             else
@@ -177,6 +183,12 @@ private static void CharacterCreateEx(ClientMessage message, Session session)
         [GameMessage(GameMessageOpcode.CharacterEnterWorldRequest, SessionState.AuthConnected)]
         public static void CharacterEnterWorldRequest(ClientMessage message, Session session)
         {
+            if (ServerManager.ShutdownInProgress)
+            {
+                session.SendCharacterError(CharacterError.LogonServerFull);
+                return;
+            }
+
             if (WorldManager.WorldStatus == WorldManager.WorldStatusState.Open || session.AccessLevel > AccessLevel.Player)
                 session.Network.EnqueueSend(new GameMessageCharacterEnterWorldServerReady());
             else
@@ -190,6 +202,12 @@ public static void CharacterEnterWorld(ClientMessage message, Session session)
 
             string clientString = message.Payload.ReadString16L();
 
+            if (ServerManager.ShutdownInProgress)
+            {
+                session.SendCharacterError(CharacterError.LogonServerFull);
+                return;
+            }
+
             if (clientString != session.Account)
             {
                 session.SendCharacterError(CharacterError.EnterGameCharacterNotOwned);
@@ -252,6 +270,12 @@ public static void CharacterDelete(ClientMessage message, Session session)
             string clientString = message.Payload.ReadString16L();
             uint characterSlot = message.Payload.ReadUInt32();
 
+            if (ServerManager.ShutdownInProgress)
+            {
+                session.SendCharacterError(CharacterError.Delete);
+                return;
+            }
+
             if (WorldManager.WorldStatus == WorldManager.WorldStatusState.Closed && session.AccessLevel < AccessLevel.Advocate)
             {
                 session.SendCharacterError(CharacterError.LogonServerFull);
@@ -303,6 +327,12 @@ public static void CharacterRestore(ClientMessage message, Session session)
         {
             var guid = message.Payload.ReadUInt32();
 
+            if (ServerManager.ShutdownInProgress)
+            {
+                session.SendCharacterError(CharacterError.EnterGameCouldntPlaceCharacter);
+                return;
+            }
+
             if (WorldManager.WorldStatus == WorldManager.WorldStatusState.Closed && session.AccessLevel < AccessLevel.Advocate)
             {
                 session.SendCharacterError(CharacterError.LogonServerFull);
diff --git a/Source/ACE.Server/Network/Managers/NetworkManager.cs b/Source/ACE.Server/Network/Managers/NetworkManager.cs
index 226d96120c..38ce383db8 100644
--- a/Source/ACE.Server/Network/Managers/NetworkManager.cs
+++ b/Source/ACE.Server/Network/Managers/NetworkManager.cs
@@ -100,6 +100,11 @@ public static void ProcessPacket(ConnectionListener connectionListener, ClientPa
                         log.InfoFormat("Login Request from {0} rejected. Server full.", endPoint);
                         SendLoginRequestReject(connectionListener, endPoint, CharacterError.LogonServerFull);
                     }
+                    else if (ServerManager.ShutdownInProgress)
+                    {
+                        log.InfoFormat("Login Request from {0} rejected. Server is shutting down.", endPoint);
+                        SendLoginRequestReject(connectionListener, endPoint, CharacterError.ServerCrash1);
+                    }
                     else if (ServerManager.ShutdownInitiated && (ServerManager.ShutdownTime - DateTime.UtcNow).TotalMinutes < 2)
                     {
                         log.InfoFormat("Login Request from {0} rejected. Server shutting down in less than 2 minutes.", endPoint);
@@ -359,5 +364,13 @@ public static int DoSessionWork()
             }
             return sessionCount;
         }
+
+        public static void DisconnectAllSessionsForShutdown()
+        {
+            foreach (var session in sessionMap)
+            {
+                session?.Terminate(SessionTerminationReason.ServerShuttingDown, new GameMessages.Messages.GameMessageCharacterError(CharacterError.ServerCrash1));
+            }
+        }
     }
 }
diff --git a/Source/ACE.Server/Network/Session.cs b/Source/ACE.Server/Network/Session.cs
index 8d84926850..56de2fa4b4 100644
--- a/Source/ACE.Server/Network/Session.cs
+++ b/Source/ACE.Server/Network/Session.cs
@@ -175,6 +175,8 @@ public void SetPlayer(Player player)
         /// </summary>
         public void LogOffPlayer(bool forceImmediate = false)
         {
+            if (Player == null) return;
+
             if (logOffRequestTime == DateTime.MinValue)
             {
                 var result = Player.LogOut(false, forceImmediate);
@@ -201,14 +203,17 @@ private void SendFinalLogOffMessages()
 
             Player = null;
 
-            Network.EnqueueSend(new GameMessageCharacterLogOff());
+            if (!ServerManager.ShutdownInProgress)
+            {
+                Network.EnqueueSend(new GameMessageCharacterLogOff());
 
-            CheckCharactersForDeletion();
+                CheckCharactersForDeletion();
 
-            Network.EnqueueSend(new GameMessageCharacterList(Characters, this));
+                Network.EnqueueSend(new GameMessageCharacterList(Characters, this));
 
-            GameMessageServerName serverNameMessage = new GameMessageServerName(ConfigManager.Config.Server.WorldName, PlayerManager.GetOnlineCount(), (int)ConfigManager.Config.Server.Network.MaximumAllowedSessions);
-            Network.EnqueueSend(serverNameMessage);
+                GameMessageServerName serverNameMessage = new GameMessageServerName(ConfigManager.Config.Server.WorldName, PlayerManager.GetOnlineCount(), (int)ConfigManager.Config.Server.Network.MaximumAllowedSessions);
+                Network.EnqueueSend(serverNameMessage);
+            }
 
             State = SessionState.AuthConnected;
         }