Updated the Stat Tracker to use System.Net.Http types.

Very similar to the previous Update Manager changes. The stat tracker now uses asynchronous threaded calls and manages exceptions better
This commit is contained in:
White 2017-03-06 21:35:14 +10:30
parent 26a5b00567
commit b3a2b24daa
5 changed files with 198 additions and 104 deletions

View file

@ -0,0 +1,27 @@
using System;
namespace TShockAPI.Extensions
{
/// <summary>
/// Extensions for Exceptions
/// </summary>
public static class ExceptionExt
{
/// <summary>
/// Builds a formatted string containing the messages of the given exception, and any inner exceptions it contains
/// </summary>
/// <param name="ex"></param>
/// <returns></returns>
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;
}
}
}

View file

@ -16,67 +16,139 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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
{
/// <summary>
/// StatTracker class for tracking various server metrics
/// </summary>
public class StatTracker
{
/// <summary>
/// Calls the GetPhysicallyInstalledSystemMemory Windows API function, returning the amount of RAM installed on the host PC
/// </summary>
/// <param name="totalMemInKb">The amount of memory (in kilobytes) present on the host PC</param>
/// <returns></returns>
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetPhysicallyInstalledSystemMemory(out long totalMemInKb);
/// <summary>
/// A provider token set on the command line. Used by the stats server to group providers
/// </summary>
public string ProviderToken = "";
/// <summary>
/// Whether or not to opt out of stat tracking
/// </summary>
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}";
/// <summary>
/// Creates a new instance of <see cref="StatTracker"/>
/// </summary>
public StatTracker()
{
}
public void Initialize()
/// <summary>
/// Starts the stat tracker
/// </summary>
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
};
}
}
/// <summary>
/// Contains JSON-Serializable information about a server
/// </summary>
public struct JsonData
{
/// <summary>
/// The port the server is running on
/// </summary>
public int port;
/// <summary>
/// The number of players currently on the server
/// </summary>
public int currentPlayers;
/// <summary>
/// The maximum number of player slots available on the server
/// </summary>
public int maxPlayers;
/// <summary>
/// The amount of RAM installed on the server's host PC
/// </summary>
public long systemRam;
/// <summary>
/// The TShock version being used by the server
/// </summary>
public string version;
/// <summary>
/// The Terraria version supported by the server
/// </summary>
public string terrariaVersion;
/// <summary>
/// The provider ID set for the server
/// </summary>
public string providerId;
/// <summary>
/// The server ID set for the server
/// </summary>
public string serverId;
/// <summary>
/// Whether or not the server is running with Mono
/// </summary>
public bool mono;
}
}

View file

@ -118,7 +118,7 @@ namespace TShockAPI
public static RestManager RestManager;
/// <summary>Utils - Static reference to the utilities class, which contains a variety of utility functions.</summary>
public static Utils Utils = Utils.Instance;
/// <summary>StatTracker - Static reference to the stat tracker, which is created immediately when declared.</summary>
/// <summary>StatTracker - Static reference to the stat tracker, which sends some server metrics every 5 minutes.</summary>
public static StatTracker StatTracker = new StatTracker();
/// <summary>UpdateManager - Static reference to the update checker, which checks for updates and notifies server admins of updates.</summary>
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();
}
/// <summary>ComputeMaxStyles - Computes the max styles...</summary>

View file

@ -86,6 +86,7 @@
<Compile Include="DB\ProjectileManager.cs" />
<Compile Include="DB\RegionManager.cs" />
<Compile Include="DB\TileManager.cs" />
<Compile Include="Extensions\ExceptionExt.cs" />
<Compile Include="Hooks\AccountHooks.cs" />
<Compile Include="Hooks\GeneralHooks.cs" />
<Compile Include="Hooks\PlayerHooks.cs" />
@ -200,7 +201,7 @@
</PropertyGroup>
<ProjectExtensions>
<VisualStudio>
<UserProperties BuildVersion_UpdateAssemblyVersion="True" BuildVersion_UpdateFileVersion="True" BuildVersion_BuildAction="Both" BuildVersion_BuildVersioningStyle="None.None.None.MonthAndDayStamp" BuildVersion_StartDate="2011/6/17" BuildVersion_IncrementBeforeBuild="False" />
<UserProperties BuildVersion_IncrementBeforeBuild="False" BuildVersion_StartDate="2011/6/17" BuildVersion_BuildVersioningStyle="None.None.None.MonthAndDayStamp" BuildVersion_BuildAction="Both" BuildVersion_UpdateFileVersion="True" BuildVersion_UpdateAssemblyVersion="True" />
</VisualStudio>
</ProjectExtensions>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View file

@ -24,6 +24,7 @@ using Newtonsoft.Json;
using Microsoft.Xna.Framework;
using System.Net.Http;
using System.Threading.Tasks;
using TShockAPI.Extensions;
namespace TShockAPI
{
@ -32,7 +33,7 @@ namespace TShockAPI
/// </summary>
public class UpdateManager
{
private string updateUrl = "http://update.tshock.co/latest/";
private const string UpdateUrl = "http://update.tshock.co/latest/";
private HttpClient _client = new HttpClient();
/// <summary>
@ -48,15 +49,20 @@ namespace TShockAPI
//5 second timeout
_client.Timeout = new TimeSpan(0, 0, 5);
Thread t = new Thread(async () => {
do {
await CheckForUpdatesAsync(null);
Thread t = new Thread(async () =>
{
do
{
//Sleep for X minutes
await Task.Delay(1000 * 60 * CheckXMinutes);
//Then check again
await CheckForUpdatesAsync(null);
} while (true);
});
t.Name = "TShock Update Thread";
t.IsBackground = true;
})
{
Name = "TShock Update Thread",
IsBackground = true
};
t.Start();
}
@ -64,14 +70,14 @@ namespace TShockAPI
{
try
{
await UpdateCheckAsync(state);
CheckXMinutes = 30;
await UpdateCheckAsync(state);
}
catch (Exception ex)
{
// Skip this run and check more frequently...
string msg = BuildExceptionString(ex);
string msg = ex.BuildExceptionString();
//Give the console a brief
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"UpdateManager warning: {msg}");
@ -81,8 +87,6 @@ namespace TShockAPI
TShock.Log.ConsoleError("Retrying in 5 minutes.");
CheckXMinutes = 5;
}
Thread.Sleep(CheckXMinutes * 60 * 1000);
}
/// <summary>
@ -105,7 +109,7 @@ namespace TShockAPI
/// <returns></returns>
private async Task<Dictionary<string, string>> ServerIsOutOfDateAsync()
{
var resp = await _client.GetAsync(updateUrl);
var resp = await _client.GetAsync(UpdateUrl);
if (resp.StatusCode != HttpStatusCode.OK)
{
string reason = resp.ReasonPhrase;
@ -150,23 +154,5 @@ namespace TShockAPI
player.SendMessage(changes[j], Color.Red);
}
}
/// <summary>
/// Produces a string containing all exception messages in an exception chain.
/// </summary>
/// <param name="ex"></param>
/// <returns></returns>
private string BuildExceptionString(Exception ex)
{
string msg = ex.Message;
Exception inner = ex.InnerException;
while (inner != null)
{
msg += $"\r\n\t-> {inner.Message}";
inner = inner.InnerException;
}
return msg;
}
}
}