diff --git a/TShockAPI/Extensions/ExceptionExt.cs b/TShockAPI/Extensions/ExceptionExt.cs new file mode 100644 index 00000000..fcae21e4 --- /dev/null +++ b/TShockAPI/Extensions/ExceptionExt.cs @@ -0,0 +1,27 @@ +using System; +namespace TShockAPI.Extensions +{ + /// + /// Extensions for Exceptions + /// + public static class ExceptionExt + { + /// + /// Builds a formatted string containing the messages of the given exception, and any inner exceptions it contains + /// + /// + /// + public static string BuildExceptionString(this Exception ex) + { + string msg = ex.Message; + Exception inner = ex.InnerException; + while (inner != null) + { + msg += $"\r\n\t-> {inner.Message}"; + inner = inner.InnerException; + } + + return msg; + } + } +} diff --git a/TShockAPI/StatTracker.cs b/TShockAPI/StatTracker.cs index 24913e83..927986f8 100644 --- a/TShockAPI/StatTracker.cs +++ b/TShockAPI/StatTracker.cs @@ -16,67 +16,139 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System; using System.Net; using System.Threading; -using System.IO; using System.Web; using TerrariaApi.Server; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Net.Http; +using System.Threading.Tasks; +using TShockAPI.Extensions; namespace TShockAPI { + /// + /// StatTracker class for tracking various server metrics + /// public class StatTracker { + /// + /// Calls the GetPhysicallyInstalledSystemMemory Windows API function, returning the amount of RAM installed on the host PC + /// + /// The amount of memory (in kilobytes) present on the host PC + /// [DllImport("kernel32.dll")] [return: MarshalAs(UnmanagedType.Bool)] static extern bool GetPhysicallyInstalledSystemMemory(out long totalMemInKb); + /// + /// A provider token set on the command line. Used by the stats server to group providers + /// public string ProviderToken = ""; + /// + /// Whether or not to opt out of stat tracking + /// public bool OptOut = false; - private bool failed; - private bool initialized; - private long totalMem; + private long totalMem = 0; private string serverId = ""; + private HttpClient _client; + private const string TrackerUrl = "http://stats.tshock.co/submit/{0}"; + + /// + /// Creates a new instance of + /// public StatTracker() { - } - public void Initialize() + /// + /// Starts the stat tracker + /// + public void Start() { - if (!initialized && !OptOut) + if (OptOut) { - initialized = true; - serverId = Guid.NewGuid().ToString(); // Gets reset every server restart - // ThreadPool.QueueUserWorkItem(SendUpdate); - Thread t = new Thread(() => { - do { - Thread.Sleep(1000 * 60 * 5); - SendUpdate(null); - } while(true); - }); - t.IsBackground = true; - t.Name = "TShock Stat Tracker Thread"; - t.Start(); + //If opting out, return and do not start stat tracking + return; + } + + //HttpClient with a 5 second timeout + _client = new HttpClient() + { + Timeout = new TimeSpan(0, 0, 5) + }; + serverId = Guid.NewGuid().ToString(); + + Thread t = new Thread(async () => + { + do + { + //Wait 5 minutes + await Task.Delay(1000 * 60 * 5); + //Then update again + await SendUpdateAsync(); + } while (true); + }) + { + Name = "TShock Stat Tracker Thread", + IsBackground = true + }; + t.Start(); + } + + private async Task SendUpdateAsync() + { + JsonData data = PrepareJsonData(); + + var serialized = Newtonsoft.Json.JsonConvert.SerializeObject(data); + var encoded = HttpUtility.UrlEncode(serialized); + var uri = String.Format(TrackerUrl, encoded); + + try + { + HttpResponseMessage msg = await _client.GetAsync(uri); + if (msg.StatusCode != HttpStatusCode.OK) + { + string reason = msg.ReasonPhrase; + if (string.IsNullOrWhiteSpace(reason)) + { + reason = "none"; + } + throw new WebException("Stats server did not respond with an OK. " + + $"Server message: [error {msg.StatusCode}] {reason}"); + } + } + catch (Exception ex) + { + string msg = ex.BuildExceptionString(); + + //Give the console a brief + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"StatTracker warning: {msg}"); + Console.ForegroundColor = ConsoleColor.Gray; + + //And log the full exception + TShock.Log.Warn($"StatTracker warning: {ex.ToString()}"); + TShock.Log.ConsoleError("Retrying in 5 minutes."); } } - private void SendUpdate(object info) + private JsonData PrepareJsonData() { - JsonData data; - - if(ServerApi.RunningMono) + if (ServerApi.RunningMono) { - var pc = new PerformanceCounter("Mono Memory", "Total Physical Memory"); - totalMem = (pc.RawValue / 1024 / 1024 / 1024); - data = new JsonData + if (totalMem == 0) + { + //Only do this if we haven't already cached the total physicaly memory + var pc = new PerformanceCounter("Mono Memory", "Total Physical Memory"); + totalMem = (pc.RawValue / 1024 / 1024 / 1024); + } + + return new JsonData { port = Terraria.Netplay.ListenPort, currentPlayers = TShock.Utils.ActivePlayers(), @@ -88,62 +160,70 @@ namespace TShockAPI serverId = serverId, mono = true }; - } else + } + + if (totalMem == 0) { + //Again, only call this if it hasn't previously been cached GetPhysicallyInstalledSystemMemory(out totalMem); totalMem = (totalMem / 1024 / 1024); // Super hardcore maths to convert to Gb from Kb - data = new JsonData - { - port = Terraria.Netplay.ListenPort, - currentPlayers = TShock.Utils.ActivePlayers(), - maxPlayers = TShock.Config.MaxSlots, - systemRam = totalMem, - version = TShock.VersionNum.ToString(), - terrariaVersion = Terraria.Main.versionNumber2, - providerId = ProviderToken, - serverId = serverId, - mono = false - }; } - var serialized = Newtonsoft.Json.JsonConvert.SerializeObject(data); - var encoded = HttpUtility.UrlEncode(serialized); - var uri = String.Format("http://stats.tshock.co/submit/{0}", encoded); - var client = (HttpWebRequest)WebRequest.Create(uri); - client.Timeout = 5000; - try + return new JsonData { - using (var resp = TShock.Utils.GetResponseNoException(client)) - { - if (resp.StatusCode != HttpStatusCode.OK) - { - throw new IOException("Server did not respond with an OK."); - } - - failed = false; - } - } - catch (Exception e) - { - if (!failed) - { - TShock.Log.ConsoleError("StatTracker Exception: {0}", e); - failed = true; - } - } + port = Terraria.Netplay.ListenPort, + currentPlayers = TShock.Utils.ActivePlayers(), + maxPlayers = TShock.Config.MaxSlots, + systemRam = totalMem, + version = TShock.VersionNum.ToString(), + terrariaVersion = Terraria.Main.versionNumber2, + providerId = ProviderToken, + serverId = serverId, + mono = false + }; } } + /// + /// Contains JSON-Serializable information about a server + /// public struct JsonData { + /// + /// The port the server is running on + /// public int port; + /// + /// The number of players currently on the server + /// public int currentPlayers; + /// + /// The maximum number of player slots available on the server + /// public int maxPlayers; + /// + /// The amount of RAM installed on the server's host PC + /// public long systemRam; + /// + /// The TShock version being used by the server + /// public string version; + /// + /// The Terraria version supported by the server + /// public string terrariaVersion; + /// + /// The provider ID set for the server + /// public string providerId; + /// + /// The server ID set for the server + /// public string serverId; + /// + /// Whether or not the server is running with Mono + /// public bool mono; } } diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index 481cb903..dcf3272e 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -118,7 +118,7 @@ namespace TShockAPI public static RestManager RestManager; /// Utils - Static reference to the utilities class, which contains a variety of utility functions. public static Utils Utils = Utils.Instance; - /// StatTracker - Static reference to the stat tracker, which is created immediately when declared. + /// StatTracker - Static reference to the stat tracker, which sends some server metrics every 5 minutes. public static StatTracker StatTracker = new StatTracker(); /// UpdateManager - Static reference to the update checker, which checks for updates and notifies server admins of updates. public static UpdateManager UpdateManager; @@ -768,12 +768,12 @@ namespace TShockAPI } case "--provider-token": { - TShock.StatTracker.ProviderToken = parms[++i]; + StatTracker.ProviderToken = parms[++i]; break; } case "--stats-optout": { - TShock.StatTracker.OptOut = true; + StatTracker.OptOut = true; break; } case "--no-restart": @@ -894,7 +894,7 @@ namespace TShockAPI } UpdateManager = new UpdateManager(); - StatTracker.Initialize(); + StatTracker.Start(); } /// ComputeMaxStyles - Computes the max styles... diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index a4350b70..aa0d44b4 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -86,6 +86,7 @@ + @@ -200,7 +201,7 @@ - +