TShock/TShockAPI/StatTracker.cs

270 lines
No EOL
7.4 KiB
C#

/*
TShock, a server mod for Terraria
Copyright (C) 2011-2017 Nyx Studios (fka. The TShock Team)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
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.Net;
using System.Threading;
using System.Web;
using TerrariaApi.Server;
using System.Diagnostics;
using System.Linq;
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 PluginItem[] plugins;
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()
{
}
/// <summary>
/// Starts the stat tracker
/// </summary>
public void Start()
{
if (OptOut)
{
//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 JsonData PrepareJsonData()
{
return new JsonData()
{
port = Terraria.Netplay.ListenPort,
currentPlayers = TShock.Utils.ActivePlayers(),
maxPlayers = TShock.Config.MaxSlots,
systemRam = GetTotalSystemRam(ServerApi.RunningMono),
version = TShock.VersionNum.ToString(),
terrariaVersion = Terraria.Main.versionNumber2,
providerId = ProviderToken,
serverId = serverId,
mono = ServerApi.RunningMono,
ignorePluginVersion = ServerApi.IgnoreVersion,
loadedPlugins = GetLoadedPlugins()
};
}
private PluginItem[] GetLoadedPlugins()
{
if (plugins != null)
{
return plugins;//Return early
}
plugins = new PluginItem[ServerApi.Plugins.Count];//Initialize with enough room to store the ammount of plugins loaded.
for (var i = 0; i < ServerApi.Plugins.Count; i++)
{
var pluginItem = new PluginItem();
var apiAttribute = (ApiVersionAttribute)ServerApi.Plugins[i].Plugin.GetType().GetCustomAttributes(typeof(ApiVersionAttribute), false).FirstOrDefault();
//The current implementation of loading plugins doesn't allow for a plugin to be loaded without an ApiVersion, the UNKNOWN is there incase a change is made to allow it
pluginItem.apiVersion = apiAttribute?.ApiVersion.ToString() ?? "UNKNOWN";
pluginItem.name = ServerApi.Plugins[i].Plugin.Name;
pluginItem.version = ServerApi.Plugins[i].Plugin.Version.ToString();
plugins[i] = pluginItem;
}
return plugins;
}
private long GetTotalSystemRam(bool isMono)
{
if (totalMem != 0)
{
return totalMem;//Return early
}
if (isMono)//Set totalMem so it can be returned later
{
var pc = new PerformanceCounter("Mono Memory", "Total Physical Memory");
totalMem = (pc.RawValue / 1024 / 1024 / 1024);
}
else
{
GetPhysicallyInstalledSystemMemory(out totalMem);
totalMem = (totalMem / 1024 / 1024); // Super hardcore maths to convert to Gb from Kb
}
return totalMem;
}
}
/// <summary>
/// Holding information regarding loaded plugins
/// </summary>
public struct PluginItem
{
/// <summary>
/// Plugin name
/// </summary>
public string name;
/// <summary>
/// Assembly version
/// </summary>
public string version;
/// <summary>
/// Api version or UNKNOWN if attribute is missing, which is currently impossible
/// </summary>
public string apiVersion;
}
/// <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>
/// Whether or not server was started with --ignoreversion
/// </summary>
public bool ignorePluginVersion;
/// <summary>
/// List of loaded plugins and version name:
/// </summary>
public PluginItem[] loadedPlugins;
/// <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;
}
}