diff --git a/TShockAPI/BackupManager.cs b/TShockAPI/BackupManager.cs index 8aa8c2f5..4bce32cc 100644 --- a/TShockAPI/BackupManager.cs +++ b/TShockAPI/BackupManager.cs @@ -63,11 +63,7 @@ namespace TShockAPI TShock.Utils.Broadcast("Server map saving, potential lag spike"); Console.WriteLine("Backing up world..."); - Thread SaveWorld = new Thread(TShock.Utils.SaveWorld); - SaveWorld.Start(); - - while (SaveWorld.ThreadState == ThreadState.Running) - Thread.Sleep(50); + SaveManager.Instance.SaveWorld(); Console.WriteLine("World backed up"); Console.ForegroundColor = ConsoleColor.Gray; Log.Info(string.Format("World backed up ({0})", Main.worldPathName)); diff --git a/TShockAPI/Commands.cs b/TShockAPI/Commands.cs index 5000f618..436ae4ee 100755 --- a/TShockAPI/Commands.cs +++ b/TShockAPI/Commands.cs @@ -1007,38 +1007,37 @@ namespace TShockAPI } } - TShock.Utils.ForceKickAll("Server shutting down!"); - WorldGen.saveWorld(); - Netplay.disconnect = true; + TShock.Utils.StopServer(); } - //Added restart command - private static void Restart(CommandArgs args) - { - if (Main.runningMono){ - Log.ConsoleInfo("Sorry, this command has not yet been implemented in Mono"); - }else{ - if (TShock.Config.ServerSideInventory) - { - foreach (TSPlayer player in TShock.Players) - { - if (player != null && player.IsLoggedIn && !player.IgnoreActionsForClearingTrashCan) - { - TShock.InventoryDB.InsertPlayerData(player); - } - } - } + //Added restart command + private static void Restart(CommandArgs args) + { + if (Main.runningMono) + { + Log.ConsoleInfo("Sorry, this command has not yet been implemented in Mono"); + } + else + { + if (TShock.Config.ServerSideInventory) + { + foreach (TSPlayer player in TShock.Players) + { + if (player != null && player.IsLoggedIn && !player.IgnoreActionsForClearingTrashCan) + { + TShock.InventoryDB.InsertPlayerData(player); + } + } + } - TShock.Utils.ForceKickAll("Server restarting!"); - WorldGen.saveWorld(); - Netplay.disconnect = true; - System.Diagnostics.Process.Start(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase); - Environment.Exit(0); - }} + TShock.Utils.StopServer(); + System.Diagnostics.Process.Start(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase); + Environment.Exit(0); + } + } private static void OffNoSave(CommandArgs args) { - TShock.Utils.ForceKickAll("Server shutting down!"); - Netplay.disconnect = true; + TShock.Utils.StopServer(false); } private static void CheckUpdates(CommandArgs args) @@ -2258,10 +2257,7 @@ namespace TShockAPI { Main.spawnTileX = args.Player.TileX + 1; Main.spawnTileY = args.Player.TileY + 3; - - TShock.Utils.Broadcast("Server map saving, potential lag spike"); - Thread SaveWorld = new Thread(TShock.Utils.SaveWorld); - SaveWorld.Start(); + SaveManager.Instance.SaveWorld(false); } private static void Reload(CommandArgs args) @@ -2288,9 +2284,7 @@ namespace TShockAPI private static void Save(CommandArgs args) { - TShock.Utils.Broadcast("Server map saving, potential lag spike"); - Thread SaveWorld = new Thread(TShock.Utils.SaveWorld); - SaveWorld.Start(); + SaveManager.Instance.SaveWorld(false); } private static void Settle(CommandArgs args) diff --git a/TShockAPI/RconHandler.cs b/TShockAPI/RconHandler.cs index 9da51328..539e87b7 100644 --- a/TShockAPI/RconHandler.cs +++ b/TShockAPI/RconHandler.cs @@ -253,9 +253,7 @@ namespace TShockAPI WorldGen.genRand = new Random(); if (text.StartsWith("exit")) { - TShock.Utils.ForceKickAll("Server shutting down!"); - WorldGen.saveWorld(false); - Netplay.disconnect = true; + TShock.Utils.StopServer(); return "Server shutting down."; } else if (text.StartsWith("playing") || text.StartsWith("/playing")) diff --git a/TShockAPI/Rest/RestManager.cs b/TShockAPI/Rest/RestManager.cs index 2efd2d65..edf5d8fe 100644 --- a/TShockAPI/Rest/RestManager.cs +++ b/TShockAPI/Rest/RestManager.cs @@ -102,16 +102,10 @@ namespace TShockAPI if (!GetBool(parameters["confirm"], false)) return RestInvalidParam("confirm"); - if (!GetBool(parameters["nosave"], false)) - WorldGen.saveWorld(); - Netplay.disconnect = true; - // Inform players the server is shutting down var msg = string.IsNullOrWhiteSpace(parameters["message"]) ? "Server is shutting down" : parameters["message"]; - foreach (TSPlayer player in TShock.Players.Where(p => null != p)) - { - TShock.Utils.ForceKick(player, msg); - } + TShock.Utils.StopServer(!GetBool(parameters["nosave"], false), msg); + return RestResponse("The server is shutting down"); } @@ -304,8 +298,8 @@ namespace TShockAPI if (ret is RestObject) return ret; - User user = (User)ret; - return new RestObject() { { "group", user.Group }, { "id", user.ID.ToString() } }; + User user = (User)ret; + return new RestObject() { { "group", user.Group }, { "id", user.ID.ToString() }, { "name", user.Name } }; } #endregion @@ -411,7 +405,7 @@ namespace TShockAPI private object WorldSave(RestVerbs verbs, IParameterCollection parameters) { - TShock.Utils.SaveWorld(); + SaveManager.Instance.SaveWorld(); return RestResponse("World saved"); } diff --git a/TShockAPI/SaveManager.cs b/TShockAPI/SaveManager.cs new file mode 100644 index 00000000..dca8628e --- /dev/null +++ b/TShockAPI/SaveManager.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; +using System.Diagnostics; +using Terraria; + +namespace TShockAPI +{ + class SaveManager : IDisposable + { + // Singleton + private static readonly SaveManager instance = new SaveManager(); + private SaveManager() + { + _saveThread = new Thread(SaveWorker); + _saveThread.Name = "TShock SaveManager Worker"; + _saveThread.Start(); + } + public static SaveManager Instance { get { return instance; } } + + // Producer Consumer + private EventWaitHandle _wh = new AutoResetEvent(false); + private Object _saveLock = new Object(); + private Queue _saveQueue = new Queue(); + private Thread _saveThread; + private int saveQueueCount { get { lock (_saveLock) return _saveQueue.Count; } } + + /// + /// SaveWorld event handler which notifies users that the server may lag + /// + public void OnSaveWorld(bool resettime = false, HandledEventArgs e = null) + { + TShock.Utils.Broadcast("Saving world. Momentary lag might result from this.", Color.Red); + } + + /// + /// Saves the map data + /// + /// wait for all pending saves to finish (default: true) + /// reset the last save time counter (default: false) + /// use the realsaveWorld method instead of saveWorld event (default: false) + public void SaveWorld(bool wait = true, bool resetTime = false, bool direct = false) + { + EnqueueTask(new SaveTask(resetTime, direct)); + if (!wait) + return; + + // Wait for all outstanding saves to complete + int count = saveQueueCount; + while (0 != count) + { + Thread.Sleep(50); + count = saveQueueCount; + } + } + + /// + /// Processes any outstanding saves, shutsdown the save thread and returns + /// + public void Dispose() + { + EnqueueTask(null); + _saveThread.Join(); + _wh.Close(); + } + + private void EnqueueTask(SaveTask task) + { + lock (_saveLock) + { + _saveQueue.Enqueue(task); + } + _wh.Set(); + } + + private void SaveWorker() + { + while (true) + { + lock (_saveLock) + { + // NOTE: lock for the entire process so wait works in SaveWorld + if (_saveQueue.Count > 0) + { + SaveTask task = _saveQueue.Dequeue(); + if (null == task) + return; + else + { + if (task.direct) + { + OnSaveWorld(); + WorldGen.realsaveWorld(task.resetTime); + } + else + WorldGen.saveWorld(task.resetTime); + TShock.Utils.Broadcast("World saved.", Color.Yellow); + Log.Info(string.Format("World saved at ({0})", Main.worldPathName)); + } + } + } + _wh.WaitOne(); + } + } + + class SaveTask + { + public bool resetTime { get; set; } + public bool direct { get; set; } + public SaveTask(bool resetTime, bool direct) + { + this.resetTime = resetTime; + this.direct = direct; + } + + public override string ToString() + { + return string.Format("resetTime {0}, direct {1}", resetTime, direct); + } + } + } +} \ No newline at end of file diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index 2a7824ed..218b9d87 100755 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -62,7 +62,7 @@ namespace TShockAPI public static GeoIPCountry Geo; public static SecureRest RestApi; public static RestManager RestManager; - public static Utils Utils = new Utils(); + public static Utils Utils = Utils.Instance; public static StatTracker StatTracker = new StatTracker(); /// /// Used for implementing REST Tokens prior to the REST system starting up. @@ -186,7 +186,6 @@ namespace TShockAPI if (Config.EnableGeoIP && File.Exists(geoippath)) Geo = new GeoIPCountry(geoippath); - Console.Title = string.Format("TerrariaShock Version {0} ({1})", Version, VersionCodename); Log.ConsoleInfo(string.Format("TerrariaShock Version {0} ({1}) now running.", Version, VersionCodename)); GameHooks.PostInitialize += OnPostInit; @@ -203,7 +202,7 @@ namespace TShockAPI NpcHooks.SetDefaultsInt += OnNpcSetDefaults; ProjectileHooks.SetDefaults += OnProjectileSetDefaults; WorldHooks.StartHardMode += OnStartHardMode; - WorldHooks.SaveWorld += OnSaveWorld; + WorldHooks.SaveWorld += SaveManager.Instance.OnSaveWorld; GetDataHandlers.InitGetDataHandler(); Commands.InitCommands(); @@ -260,10 +259,13 @@ namespace TShockAPI { if (disposing) { + // NOTE: order is important here if (Geo != null) { Geo.Dispose(); } + SaveManager.Instance.Dispose(); + GameHooks.PostInitialize -= OnPostInit; GameHooks.Update -= OnUpdate; ServerHooks.Connect -= OnConnect; @@ -278,15 +280,16 @@ namespace TShockAPI NpcHooks.SetDefaultsInt -= OnNpcSetDefaults; ProjectileHooks.SetDefaults -= OnProjectileSetDefaults; WorldHooks.StartHardMode -= OnStartHardMode; - WorldHooks.SaveWorld -= OnSaveWorld; + WorldHooks.SaveWorld -= SaveManager.Instance.OnSaveWorld; + if (File.Exists(Path.Combine(SavePath, "tshock.pid"))) { File.Delete(Path.Combine(SavePath, "tshock.pid")); } + RestApi.Dispose(); Log.Dispose(); } - base.Dispose(disposing); } @@ -322,7 +325,7 @@ namespace TShockAPI if (Main.worldPathName != null && Config.SaveWorldOnCrash) { Main.worldPathName += ".crash"; - WorldGen.saveWorld(); + SaveManager.Instance.SaveWorld(); } } } @@ -361,36 +364,33 @@ namespace TShockAPI { for (int i = 0; i < parms.Length; i++) { - if (parms[i].ToLower() == "-port") + switch(parms[i].ToLower()) { - int port = Convert.ToInt32(parms[++i]); - Netplay.serverPort = port; - Config.ServerPort = port; - OverridePort = true; - Log.ConsoleInfo("Port overridden by startup argument. Set to " + port); - } - if (parms[i].ToLower() == "-rest-token") - { - string token = Convert.ToString(parms[++i]); - RESTStartupTokens.Add(token, "null"); - Console.WriteLine("Startup parameter overrode REST token."); - } - if (parms[i].ToLower() == "-rest-enabled") - { - Config.RestApiEnabled = Convert.ToBoolean(parms[++i]); - Console.WriteLine("Startup parameter overrode REST enable."); - - } - if (parms[i].ToLower() == "-rest-port") - { - Config.RestApiPort = Convert.ToInt32(parms[++i]); - Console.WriteLine("Startup parameter overrode REST port."); - - } - if ((parms[i].ToLower() == "-maxplayers")||(parms[i].ToLower() == "-players")) - { - Config.MaxSlots = Convert.ToInt32(parms[++i]); - Console.WriteLine("Startup parameter overrode maximum player slot configuration value."); + case "-port": + int port = Convert.ToInt32(parms[++i]); + Netplay.serverPort = port; + Config.ServerPort = port; + OverridePort = true; + Log.ConsoleInfo("Port overridden by startup argument. Set to " + port); + break; + case "-rest-token": + string token = Convert.ToString(parms[++i]); + RESTStartupTokens.Add(token, "null"); + Console.WriteLine("Startup parameter overrode REST token."); + break; + case "-rest-enabled": + Config.RestApiEnabled = Convert.ToBoolean(parms[++i]); + Console.WriteLine("Startup parameter overrode REST enable."); + break; + case "-rest-port": + Config.RestApiPort = Convert.ToInt32(parms[++i]); + Console.WriteLine("Startup parameter overrode REST port."); + break; + case "-maxplayers": + case "-players": + Config.MaxSlots = Convert.ToInt32(parms[++i]); + Console.WriteLine("Startup parameter overrode maximum player slot configuration value."); + break; } } } @@ -404,6 +404,7 @@ namespace TShockAPI private void OnPostInit() { + SetConsoleTitle(); if (!File.Exists(Path.Combine(SavePath, "auth.lck")) && !File.Exists(Path.Combine(SavePath, "authcode.txt"))) { var r = new Random((int) DateTime.Now.ToBinary()); @@ -465,7 +466,6 @@ namespace TShockAPI StatTracker.CheckIn(); if (Backups.IsBackupTime) Backups.Backup(); - //call these every second, not every update if ((DateTime.UtcNow - LastCheck).TotalSeconds >= 1) { @@ -590,8 +590,13 @@ namespace TShockAPI } } } - Console.Title = string.Format("TerrariaShock Version {0} ({1}) ({2}/{3})", Version, VersionCodename, count, - Config.MaxSlots); + SetConsoleTitle(); + } + + private void SetConsoleTitle() + { + Console.Title = string.Format("{0} - {1}/{2} @ {3}:{4} (TerrariaShock v{5})", Config.ServerName, Utils.ActivePlayers(), + Config.MaxSlots, Netplay.serverListenIP, Config.ServerPort, Version); } private void OnConnect(int ply, HandledEventArgs handler) @@ -1012,17 +1017,6 @@ namespace TShockAPI e.Handled = true; } - void OnSaveWorld(bool resettime, HandledEventArgs e) - { - if (!Utils.saving) - { - Utils.Broadcast("Saving world. Momentary lag might result from this.", Color.Red); - var SaveWorld = new Thread(Utils.SaveWorld); - SaveWorld.Start(); - } - e.Handled = true; - } - /* * Useful stuff: * */ diff --git a/TShockAPI/Utils.cs b/TShockAPI/Utils.cs index df5c7207..b5048f67 100644 --- a/TShockAPI/Utils.cs +++ b/TShockAPI/Utils.cs @@ -29,11 +29,10 @@ namespace TShockAPI { public class Utils { - public static bool saving = false; - - public Utils() - { - } + // Utils is a Singleton + private static readonly Utils instance = new Utils(); + private Utils() {} + public static Utils Instance { get { return instance; } } public Random Random = new Random(); //private static List groups = new List(); @@ -135,11 +134,7 @@ namespace TShockAPI /// public void SaveWorld() { - saving = true; - WorldGen.realsaveWorld(); - Broadcast("World saved.", Color.Yellow); - Log.Info(string.Format("World saved at ({0})", Main.worldPathName)); - saving = false; + SaveManager.Instance.SaveWorld(); } /// @@ -186,15 +181,7 @@ namespace TShockAPI /// int playerCount public int ActivePlayers() { - int num = 0; - foreach (TSPlayer player in TShock.Players) - { - if (player != null && player.Active) - { - num++; - } - } - return num; + return Main.player.Where(p => null != p && p.active).Count(); } /// @@ -510,6 +497,27 @@ namespace TShockAPI } } + /// + /// Stops the server after kicking all players with a reason message, and optionally saving the world + /// + /// bool perform a world save before stop (default: true) + /// string reason (default: "Server shutting down!") + public void StopServer(bool save = true, string reason = "Server shutting down!") + { + ForceKickAll(reason); + if (save) + SaveManager.Instance.SaveWorld(); + + // Save takes a while so kick again + ForceKickAll(reason); + + // Broadcast so console can see we are shutting down as well + TShock.Utils.Broadcast(reason, Color.Red); + + // Disconnect after kick as that signifies server is exiting and could cause a race + Netplay.disconnect = true; + } + /// /// Kicks a player from the server without checking for immunetokick permission. ///