using System; using System.Threading; using log4net; using ACE.Common; using ACE.Common.Extensions; using ACE.Database; using ACE.Entity.Enum; using ACE.Server.Entity.Actions; using ACE.Server.Network.GameMessages.Messages; using ACE.Server.Network.Managers; using ACE.Server.Mods; 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 AssetManager /// Known issue: /// 1. No method to verify that everything unloaded properly. /// public static class 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 shut down has been cancelled @ {DateTime.Now.ToCommonString()} ({DateTime.UtcNow.ToCommonString()} UTC)"; log.Info(shutdownText); // special text foreach (var player in PlayerManager.GetAllOnline()) player.Session.WorldBroadcast($"Broadcast from System> ATTENTION - This Asheron's Call Server shut down has been cancelled."); // 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.WarnFormat("5 minute log off failsafe reached and there are {0} player{1} still online.", playerCount, (playerCount > 1 ? "s" : "")); foreach (var player in PlayerManager.GetAllOnline()) { log.WarnFormat("Player {0} (0x{1}) appears to be stuck in world and unable to log off normally. Requesting Forced Logoff...", player.Name, player.Guid); 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(); // Halt mods ModManager.Shutdown(); // 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.ToCommonString()}"); // 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 will be shutting down in {time}{(sdt.TotalMinutes <= 1 ? "!" : ".")}{(sdt.TotalMinutes <= 3 ? $" Please log out{(sdt.TotalMinutes <= 1 ? "!" : ".")}" : "")}"); else player.Session.WorldBroadcast($"Broadcast from System> 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 will be shutting down in {timeToShutdown}{(sdt.TotalMinutes <= 1 ? "!" : ".")}{(sdt.TotalMinutes <= 3 ? $" Please log out{(sdt.TotalMinutes <= 1 ? "!" : ".")}" : "")}"; else return $"Broadcast from System> ATTENTION - This Asheron's Call Server is shutting down NOW!!!!"; } } }