csharp/ACEmulator/ACE/Source/ACE.Server/Managers/ServerManager.cs

ServerManager.cs
using System;
using System.Threading;

using log4net;

using ACE.Common;
using ACE.Database;
using ACE.Ensaty.Enum;
using ACE.Server.Ensaty.Actions;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.Network.Managers;

namespace ACE.Server.Managers
{
    /// 
    /// ServerManager handles unloading the server application properly.
    /// 
    /// 
    ///   Possibly useful for:
    ///     1. Monitor for errors and performance issues in LandblockManager, GuidManager, WorldManager,
    ///         DatabaseManager, or astetManager
    ///   Known issue:
    ///     1. No method to verify that everything unloaded properly.
    /// 
    public static clast ServerManager
    {
        private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        /// 
        /// Indicates advanced warning if the applcation will unload.
        /// 
        public static bool ShutdownInitiated { get; private set; }

        /// 
        /// Indicates server shutting down.
        /// 
        public static bool ShutdownInProgress { get; private set; }

        /// 
        /// The amount of seconds that the server will wait before unloading the application.
        /// 
        public static uint ShutdownInterval { get; private set; }

        public static DateTime ShutdownTime { get; private set; } = DateTime.MinValue;

        /// 
        /// Sets the Shutdown Interval in Seconds
        /// 
        /// postive value representing seconds
        public static void SetShutdownInterval(uint interval)
        {
            log.Info($"Server shutdown interval reset: {interval}");
            ShutdownInterval = interval;
        }

        public static void Initialize()
        {
            // Loads the configuration for ShutdownInterval from the settings file.
            ShutdownInterval = ConfigManager.Config.Server.ShutdownInterval;
        }

        /// 
        /// Starts the shutdown wait thread.
        /// 
        public static void BeginShutdown()
        {
            ShutdownInitiated = true;

            var shutdownThread = new Thread(ShutdownServer);
            shutdownThread.Name = "Shutdown Server";
            shutdownThread.Start();
        }

        /// 
        /// Calling this function will always cancel an in-progress shutdown (application unload). This will also
        /// stop the shutdown wait thread and alert users that the server will stay in operation.
        /// 
        public static void CancelShutdown()
        {
            ShutdownInitiated = false;
            ShutdownTime = DateTime.MinValue;
        }

        public static void DoShutdownNow()
        {
            SetShutdownInterval(0);
            ShutdownInitiated = true;
            PlayerManager.BroadcastToAll(new GameMessageSystemChat("Broadcast from System> ATTENTION - This Asheron's Call Server is shutting down NOW!!!!", ChatMessageType.WorldBroadcast));
            ShutdownServer();
        }

        /// 
        /// Threaded task created when performing a server shutdown
        /// 
        private static void ShutdownServer()
        {
            var shutdownTime = DateTime.UtcNow.AddSeconds(ShutdownInterval);

            ShutdownTime = shutdownTime;

            var lastNoticeTime = DateTime.UtcNow;

            // wait for shutdown interval to expire
            while (shutdownTime != DateTime.MinValue && shutdownTime >= DateTime.UtcNow)
            {
                // this allows the server shutdown to be canceled
                if (!ShutdownInitiated)
                {
                    // reset shutdown details
                    string shutdownText = $"The server has canceled the shutdown procedure @ {DateTime.UtcNow} UTC";
                    log.Info(shutdownText);

                    // special text
                    foreach (var player in PlayerManager.GetAllOnline())
                        player.Session.WorldBroadcast(shutdownText);

                    // break function
                    return;
                }

                lastNoticeTime = NotifyPlayersOfPendingShutdown(lastNoticeTime, shutdownTime.AddSeconds(1));

                Thread.Sleep(10);
            }

            ShutdownInProgress = true;

            PropertyManager.ResyncVariables();
            PropertyManager.StopUpdating();

            WorldManager.EnqueueAction(new ActionEventDelegate(() =>
            {
                log.Debug("Logging off all players...");

                // logout each player
                foreach (var player in PlayerManager.GetAllOnline())
                    player.Session.LogOffPlayer(true);
            }));

            // Wait for all players to log out
            var logUpdateTS = DateTime.MinValue;
            int playerCount;
            var playerLogoffStart = DateTime.UtcNow;
            while ((playerCount = PlayerManager.GetOnlineCount()) > 0)
            {
                logUpdateTS = LogStatusUpdate(logUpdateTS, $"Waiting for {playerCount} player{(playerCount > 1 ? "s" : "")} to log off...");
                Thread.Sleep(10);
                if (playerCount > 0 && DateTime.UtcNow - playerLogoffStart > TimeSpan.FromMinutes(5))
                {
                    playerLogoffStart = DateTime.UtcNow;
                    log.Warn($"5 minute log off failsafe reached and there are {playerCount} player{(playerCount > 1 ? "s" : "")} still online.");
                    foreach (var player in PlayerManager.GetAllOnline())
                    {
                        log.Warn($"Player {player.Name} (0x{player.Guid}) appears to be stuck in world and unable to log off normally. Requesting Forced Logoff...");
                        player.ForcedLogOffRequested = true;
                        player.ForceLogoff();
                    }    
                }
            }

            WorldManager.EnqueueAction(new ActionEventDelegate(() =>
            {
                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.GetAuthenticatedSessionCount()) > 0)
            {
                logUpdateTS = LogStatusUpdate(logUpdateTS, $"Waiting for {sessionCount} authenticated session{(sessionCount > 1 ? "s" : "")} to disconnect...");
                Thread.Sleep(10);
            }

            log.Debug("Adding all landblocks to destruction queue...");

            // Queue unloading of all the landblocks
            // The actual unloading will happen in WorldManager.UpdateGameWorld
            LandblockManager.AddAllActiveLandblocksToDestructionQueue();

            // 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();

            // 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();

            // Wait for the database queue to empty
            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}");

            // System exit
            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;

            var sdt = shutdownTime - DateTime.UtcNow;
                var timeHrs = $"{(sdt.Hours >= 1 ? $"{sdt.ToString("%h")}" : "")}{(sdt.Hours >= 2 ? $" hours" : sdt.Hours == 1 ? " hour" : "")}";
                var timeMins = $"{(sdt.Minutes != 0 ? $"{sdt.ToString("%m")}" : "")}{(sdt.Minutes >= 2 ? $" minutes" : sdt.Minutes == 1 ? " minute" : "")}";
                var timeSecs = $"{(sdt.Seconds != 0 ? $"{sdt.ToString("%s")}" : "")}{(sdt.Seconds >= 2 ? $" seconds" : sdt.Seconds == 1 ? " second" : "")}";
                var time = $"{(timeHrs != "" ? timeHrs : "")}{(timeMins != "" ? $"{((timeHrs != "") ? ", " : "")}" + timeMins : "")}{(timeSecs != "" ? $"{((timeHrs != "" || timeMins != "") ? " and " : "")}" + timeSecs : "")}";

            switch (time)
            {
                case "2 hours":
                case "1 hour":
                case "45 minutes":
                case "30 minutes":
                case "15 minutes":
                case "10 minutes":
                case "5 minutes":
                case "2 minutes":
                case "1 minute and 30 seconds":
                case "1 minute":
                case "30 seconds":
                case "15 seconds":
                case "10 seconds":
                case "5 seconds":
                    notify = true;
                    break;
            }

            // Console.WriteLine(time);

            if (notify && (DateTime.UtcNow - lastNoticeTime).TotalSeconds > 2)
            {
                foreach (var player in PlayerManager.GetAllOnline())
                    if (sdt.TotalSeconds > 10)
                        player.Session.WorldBroadcast($"Broadcast from System> {(sdt.TotalMinutes > 1.5 ? "ATTENTION" : "WARNING")} - This Asheron's Call Server is shutting down in {time}.{(sdt.TotalMinutes  ATTENTION - This Asheron's Call Server is shutting down NOW!!!!");

                return DateTime.UtcNow;
            }
            else
                return lastNoticeTime;
        }

        public static void StartupAbort()
        {
            ShutdownInitiated = true;
        }

        public static string ShutdownNoticeText()
        {
            var sdt = ShutdownTime - DateTime.UtcNow;

            var timeToShutdown = $"{(sdt.Hours > 0 ? $"{sdt.Hours} hour{(sdt.Hours > 1 ? "s" : "")}" : "")}";
            timeToShutdown += $"{(timeToShutdown.Length > 0 ? ", " : "")}{(sdt.Minutes > 0 ? $"{sdt.Minutes} minute{(sdt.Minutes > 1 ? "s" : "")}" : "")}";
            timeToShutdown += $"{(timeToShutdown.Length > 0 ? " and " : "")}{(sdt.Seconds > 0 ? $"{sdt.Seconds} second{(sdt.Seconds > 1 ? "s" : "")}" : "")}";

            if (sdt.TotalSeconds > 10)
               return $"Broadcast from System> {(sdt.TotalMinutes > 1.5 ? "ATTENTION" : "WARNING")} - This Asheron's Call Server is shutting down in {timeToShutdown}.{(sdt.TotalMinutes  ATTENTION - This Asheron's Call Server is shutting down NOW!!!!";
        }
    }
}