|
|
@@ -8,53 +8,40 @@ using System.Net.NetworkInformation; |
|
|
|
using System.Net.Sockets; |
|
|
|
using System.Threading; |
|
|
|
using System.Threading.Tasks; |
|
|
|
using Newtonsoft.Json; |
|
|
|
using Shadowsocks.Model; |
|
|
|
using Shadowsocks.Util; |
|
|
|
|
|
|
|
namespace Shadowsocks.Controller |
|
|
|
{ |
|
|
|
using DataUnit = KeyValuePair<string, string>; |
|
|
|
using DataList = List<KeyValuePair<string, string>>; |
|
|
|
|
|
|
|
using Statistics = Dictionary<string, List<AvailabilityStatistics.RawStatisticsData>>; |
|
|
|
|
|
|
|
using Statistics = Dictionary<string, List<StatisticsRecord>>; |
|
|
|
|
|
|
|
public sealed class AvailabilityStatistics |
|
|
|
{ |
|
|
|
// Static Singleton Initialization |
|
|
|
public static AvailabilityStatistics Instance { get; } = new AvailabilityStatistics(); |
|
|
|
private AvailabilityStatistics() { } |
|
|
|
|
|
|
|
public const string DateTimePattern = "yyyy-MM-dd HH:mm:ss"; |
|
|
|
private const string StatisticsFilesName = "shadowsocks.availability.csv"; |
|
|
|
private const string Delimiter = ","; |
|
|
|
private const string StatisticsFilesName = "shadowsocks.availability.json"; |
|
|
|
private const int TimeoutMilliseconds = 500; |
|
|
|
private readonly TimeSpan _delayBeforeStart = TimeSpan.FromSeconds(1); |
|
|
|
public Statistics RawStatistics { get; private set; } |
|
|
|
public Statistics FilteredStatistics { get; private set; } |
|
|
|
public static readonly DateTime UnknownDateTime = new DateTime(1970, 1, 1); |
|
|
|
private int Repeat => _config.RepeatTimesNum; |
|
|
|
private readonly TimeSpan _retryInterval = TimeSpan.FromMinutes(2); //retry 2 minutes after failed |
|
|
|
private TimeSpan Interval => TimeSpan.FromMinutes(_config.DataCollectionMinutes); |
|
|
|
private Timer _timer; |
|
|
|
private Timer _speedMonior; |
|
|
|
private State _state; |
|
|
|
private List<Server> _servers; |
|
|
|
private StatisticsStrategyConfiguration _config; |
|
|
|
|
|
|
|
private const string Empty = ""; |
|
|
|
public static readonly DateTime UnknownDateTime = new DateTime(1970, 1, 1); |
|
|
|
|
|
|
|
public static string AvailabilityStatisticsFile; |
|
|
|
//speed in KiB/s |
|
|
|
private int _inboundSpeed = 0; |
|
|
|
private int _outboundSpeed = 0; |
|
|
|
private int? _latency = 0; |
|
|
|
private Server _currentServer; |
|
|
|
private Configuration _globalConfig; |
|
|
|
private ShadowsocksController _controller; |
|
|
|
private long _lastInboundCounter = 0; |
|
|
|
private long _lastOutboundCounter = 0; |
|
|
|
private readonly TimeSpan _delayBeforeStart = TimeSpan.FromSeconds(1); |
|
|
|
private readonly TimeSpan _monitorInterval = TimeSpan.FromSeconds(1); |
|
|
|
private readonly TimeSpan _retryInterval = TimeSpan.FromMinutes(2); //retry 2 minutes after failed |
|
|
|
private readonly TimeSpan _writingInterval = TimeSpan.FromMinutes(1); |
|
|
|
private StatisticsStrategyConfiguration _config; |
|
|
|
private ShadowsocksController _controller; |
|
|
|
private Server _currentServer; |
|
|
|
//speed in KiB/s |
|
|
|
private List<int> _inboundSpeedRecords; |
|
|
|
private long _lastInboundCounter; |
|
|
|
private long _lastOutboundCounter; |
|
|
|
private List<int> _latencyRecords; |
|
|
|
private List<int> _outboundSpeedRecords; |
|
|
|
private Timer _recorder; |
|
|
|
private List<Server> _servers; |
|
|
|
private Timer _speedMonior; |
|
|
|
private Timer _writer; |
|
|
|
|
|
|
|
//static constructor to initialize every public static fields before refereced |
|
|
|
static AvailabilityStatistics() |
|
|
@@ -62,6 +49,18 @@ namespace Shadowsocks.Controller |
|
|
|
AvailabilityStatisticsFile = Utils.GetTempPath(StatisticsFilesName); |
|
|
|
} |
|
|
|
|
|
|
|
private AvailabilityStatistics() |
|
|
|
{ |
|
|
|
RawStatistics = new Statistics(); |
|
|
|
} |
|
|
|
|
|
|
|
// Static Singleton Initialization |
|
|
|
public static AvailabilityStatistics Instance { get; } = new AvailabilityStatistics(); |
|
|
|
public Statistics RawStatistics { get; private set; } |
|
|
|
public Statistics FilteredStatistics { get; private set; } |
|
|
|
private int Repeat => _config.RepeatTimesNum; |
|
|
|
private TimeSpan RecordingInterval => TimeSpan.FromMinutes(_config.DataCollectionMinutes); |
|
|
|
|
|
|
|
public bool Set(StatisticsStrategyConfiguration config) |
|
|
|
{ |
|
|
|
_config = config; |
|
|
@@ -69,15 +68,23 @@ namespace Shadowsocks.Controller |
|
|
|
{ |
|
|
|
if (config.StatisticsEnabled) |
|
|
|
{ |
|
|
|
if (_timer?.Change(_delayBeforeStart, Interval) == null) |
|
|
|
if (_recorder?.Change(_delayBeforeStart, RecordingInterval) == null) |
|
|
|
{ |
|
|
|
_recorder = new Timer(Run, null, _delayBeforeStart, RecordingInterval); |
|
|
|
} |
|
|
|
LoadRawStatistics(); |
|
|
|
if (_speedMonior?.Change(_delayBeforeStart, _monitorInterval) == null) |
|
|
|
{ |
|
|
|
_speedMonior = new Timer(UpdateSpeed, null, _delayBeforeStart, _monitorInterval); |
|
|
|
} |
|
|
|
if (_writer?.Change(_delayBeforeStart, RecordingInterval) == null) |
|
|
|
{ |
|
|
|
_state = new State(); |
|
|
|
_timer = new Timer(Run, _state, _delayBeforeStart, Interval); |
|
|
|
_writer = new Timer(Save, null, _delayBeforeStart, RecordingInterval); |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
_timer?.Dispose(); |
|
|
|
_recorder?.Dispose(); |
|
|
|
_speedMonior?.Dispose(); |
|
|
|
} |
|
|
|
return true; |
|
|
@@ -93,51 +100,49 @@ namespace Shadowsocks.Controller |
|
|
|
{ |
|
|
|
var bytes = _controller.inboundCounter - _lastInboundCounter; |
|
|
|
_lastInboundCounter = _controller.inboundCounter; |
|
|
|
var inboundSpeed = GetSpeedInKiBPerSecond(bytes ,_monitorInterval.TotalSeconds); |
|
|
|
var inboundSpeed = GetSpeedInKiBPerSecond(bytes, _monitorInterval.TotalSeconds); |
|
|
|
_inboundSpeedRecords.Add(inboundSpeed); |
|
|
|
|
|
|
|
bytes = _controller.outboundCounter - _lastOutboundCounter; |
|
|
|
_lastOutboundCounter = _controller.outboundCounter; |
|
|
|
var outboundSpeed = GetSpeedInKiBPerSecond(bytes, _monitorInterval.TotalSeconds); |
|
|
|
_outboundSpeedRecords.Add(outboundSpeed); |
|
|
|
|
|
|
|
if (inboundSpeed > _inboundSpeed) |
|
|
|
{ |
|
|
|
_inboundSpeed = inboundSpeed; |
|
|
|
} |
|
|
|
if (outboundSpeed > _outboundSpeed) |
|
|
|
{ |
|
|
|
_outboundSpeed = outboundSpeed; |
|
|
|
} |
|
|
|
Logging.Debug($"{_currentServer.FriendlyName()}: current/max inbound {inboundSpeed}/{_inboundSpeed} KiB/s, current/max outbound {outboundSpeed}/{_outboundSpeed} KiB/s"); |
|
|
|
Logging.Debug( |
|
|
|
$"{_currentServer.FriendlyName()}: current/max inbound {inboundSpeed}/{_inboundSpeedRecords.Max()} KiB/s, current/max outbound {outboundSpeed}/{_outboundSpeedRecords.Max()} KiB/s"); |
|
|
|
} |
|
|
|
|
|
|
|
private async Task<List<DataList>> ICMPTest(Server server) |
|
|
|
private async Task<ICMPResult> ICMPTest(Server server) |
|
|
|
{ |
|
|
|
Logging.Debug("Ping " + server.FriendlyName()); |
|
|
|
if (server.server == "") return null; |
|
|
|
var ret = new List<DataList>(); |
|
|
|
try { |
|
|
|
var IP = Dns.GetHostAddresses(server.server).First(ip => (ip.AddressFamily == AddressFamily.InterNetwork || ip.AddressFamily == AddressFamily.InterNetworkV6)); |
|
|
|
var result = new ICMPResult(server); |
|
|
|
try |
|
|
|
{ |
|
|
|
var IP = |
|
|
|
Dns.GetHostAddresses(server.server) |
|
|
|
.First( |
|
|
|
ip => |
|
|
|
ip.AddressFamily == AddressFamily.InterNetwork || |
|
|
|
ip.AddressFamily == AddressFamily.InterNetworkV6); |
|
|
|
var ping = new Ping(); |
|
|
|
|
|
|
|
foreach (var timestamp in Enumerable.Range(0, Repeat).Select(_ => DateTime.Now.ToString(DateTimePattern))) |
|
|
|
foreach (var _ in Enumerable.Range(0, Repeat)) |
|
|
|
{ |
|
|
|
//ICMP echo. we can also set options and special bytes |
|
|
|
try |
|
|
|
{ |
|
|
|
var reply = await ping.SendTaskAsync(IP, TimeoutMilliseconds); |
|
|
|
ret.Add(new List<KeyValuePair<string, string>> |
|
|
|
{ |
|
|
|
new KeyValuePair<string, string>("Timestamp", timestamp), |
|
|
|
new KeyValuePair<string, string>("Server", server.FriendlyName()), |
|
|
|
new KeyValuePair<string, string>("Status", reply?.Status.ToString()), |
|
|
|
new KeyValuePair<string, string>("RoundtripTime", reply?.RoundtripTime.ToString()), |
|
|
|
new KeyValuePair<string, string>("Latency", GetRecentLatency(server)), |
|
|
|
new KeyValuePair<string, string>("InboundSpeed", GetRecentInboundSpeed(server)), |
|
|
|
new KeyValuePair<string, string>("OutboundSpeed", GetRecentOutboundSpeed(server)) |
|
|
|
//new KeyValuePair<string, string>("data", reply.Buffer.ToString()); // The data of reply |
|
|
|
}); |
|
|
|
Thread.Sleep(TimeoutMilliseconds + new Random().Next() % TimeoutMilliseconds); |
|
|
|
if (!reply.Status.Equals(IPStatus.Success)) |
|
|
|
{ |
|
|
|
result.RoundtripTime.Add((int?) reply.RoundtripTime); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
result.RoundtripTime.Add(null); |
|
|
|
} |
|
|
|
|
|
|
|
//Do ICMPTest in a random frequency |
|
|
|
Thread.Sleep(TimeoutMilliseconds + new Random().Next()%TimeoutMilliseconds); |
|
|
|
} |
|
|
|
catch (Exception e) |
|
|
|
{ |
|
|
@@ -145,63 +150,73 @@ namespace Shadowsocks.Controller |
|
|
|
Logging.LogUsefulException(e); |
|
|
|
} |
|
|
|
} |
|
|
|
}catch(Exception e) |
|
|
|
} |
|
|
|
catch (Exception e) |
|
|
|
{ |
|
|
|
Logging.Error($"An exception occured while eveluating {server.FriendlyName()}"); |
|
|
|
Logging.LogUsefulException(e); |
|
|
|
} |
|
|
|
return ret; |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private string GetRecentOutboundSpeed(Server server) |
|
|
|
private void Reset() |
|
|
|
{ |
|
|
|
return server != _currentServer ? Empty : _outboundSpeed.ToString(); |
|
|
|
_inboundSpeedRecords = new List<int>(); |
|
|
|
_outboundSpeedRecords = new List<int>(); |
|
|
|
_latencyRecords = new List<int>(); |
|
|
|
} |
|
|
|
|
|
|
|
private string GetRecentInboundSpeed(Server server) |
|
|
|
private void Run(object _) |
|
|
|
{ |
|
|
|
return server != _currentServer ? Empty : _inboundSpeed.ToString(); |
|
|
|
AppendRecord(); |
|
|
|
Reset(); |
|
|
|
FilterRawStatistics(); |
|
|
|
} |
|
|
|
|
|
|
|
private string GetRecentLatency(Server server) |
|
|
|
private async void AppendRecord() |
|
|
|
{ |
|
|
|
if (server != _currentServer) return Empty; |
|
|
|
return _latency == null ? Empty : _latency.ToString(); |
|
|
|
} |
|
|
|
//todo: option for icmp test |
|
|
|
var icmpResults = TaskEx.WhenAll(_servers.Select(ICMPTest)); |
|
|
|
|
|
|
|
private void ResetSpeed() |
|
|
|
{ |
|
|
|
_currentServer = _controller.GetCurrentServer(); |
|
|
|
_latency = null; |
|
|
|
_inboundSpeed = 0; |
|
|
|
_outboundSpeed = 0; |
|
|
|
} |
|
|
|
var currentServerRecord = new StatisticsRecord(_currentServer.Identifier(), |
|
|
|
_inboundSpeedRecords, _outboundSpeedRecords, _latencyRecords); |
|
|
|
|
|
|
|
private void Run(object obj) |
|
|
|
{ |
|
|
|
if (_speedMonior?.Change(_delayBeforeStart, _monitorInterval) == null) |
|
|
|
foreach (var result in (await icmpResults).Where(result => result != null)) |
|
|
|
{ |
|
|
|
_speedMonior = new Timer(UpdateSpeed, null, _delayBeforeStart, _monitorInterval); |
|
|
|
List<StatisticsRecord> records; |
|
|
|
if (!RawStatistics.TryGetValue(result.Server.Identifier(), out records)) |
|
|
|
{ |
|
|
|
records = new List<StatisticsRecord>(); |
|
|
|
} |
|
|
|
|
|
|
|
if (result.Server.Equals(_currentServer)) |
|
|
|
{ |
|
|
|
currentServerRecord.setResponse(result.RoundtripTime); |
|
|
|
records.Add(currentServerRecord); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
records.Add(new StatisticsRecord(result.Server.Identifier(), result.RoundtripTime)); |
|
|
|
} |
|
|
|
RawStatistics[result.Server.Identifier()] = records; |
|
|
|
} |
|
|
|
LoadRawStatistics(); |
|
|
|
FilterRawStatistics(); |
|
|
|
Evaluate(); |
|
|
|
ResetSpeed(); |
|
|
|
} |
|
|
|
|
|
|
|
private async void Evaluate() |
|
|
|
private void Save(object _) |
|
|
|
{ |
|
|
|
foreach (var dataLists in await TaskEx.WhenAll(_servers.Select(ICMPTest))) |
|
|
|
try |
|
|
|
{ |
|
|
|
if (dataLists == null) continue; |
|
|
|
foreach (var dataList in dataLists.Where(dataList => dataList != null)) |
|
|
|
{ |
|
|
|
Append(dataList, Enumerable.Empty<DataUnit>()); |
|
|
|
} |
|
|
|
File.WriteAllText(AvailabilityStatisticsFile, |
|
|
|
JsonConvert.SerializeObject(RawStatistics, Formatting.None)); |
|
|
|
} |
|
|
|
catch (IOException e) |
|
|
|
{ |
|
|
|
Logging.LogUsefulException(e); |
|
|
|
_writer.Change(_retryInterval, _writingInterval); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/* |
|
|
|
private static void Append(DataList dataList, IEnumerable<DataUnit> extra) |
|
|
|
{ |
|
|
|
var data = dataList.Concat(extra); |
|
|
@@ -225,15 +240,28 @@ namespace Shadowsocks.Controller |
|
|
|
Logging.LogUsefulException(e); |
|
|
|
} |
|
|
|
} |
|
|
|
*/ |
|
|
|
|
|
|
|
internal void UpdateConfiguration(ShadowsocksController controller) |
|
|
|
{ |
|
|
|
_controller = controller; |
|
|
|
ResetSpeed(); |
|
|
|
_currentServer = _controller.GetCurrentServer(); |
|
|
|
Reset(); |
|
|
|
Set(controller.StatisticsConfiguration); |
|
|
|
_servers = _controller.GetCurrentConfiguration().configs; |
|
|
|
} |
|
|
|
|
|
|
|
private bool IsValidRecord(StatisticsRecord record) |
|
|
|
{ |
|
|
|
if (_config.ByHourOfDay) |
|
|
|
{ |
|
|
|
var currentHour = DateTime.Now.Hour; |
|
|
|
if (record.Timestamp == UnknownDateTime) return false; |
|
|
|
if (!record.Timestamp.Hour.Equals(DateTime.Now.Hour)) return false; |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
private void FilterRawStatistics() |
|
|
|
{ |
|
|
|
if (RawStatistics == null) return; |
|
|
@@ -241,20 +269,12 @@ namespace Shadowsocks.Controller |
|
|
|
{ |
|
|
|
FilteredStatistics = new Statistics(); |
|
|
|
} |
|
|
|
foreach (IEnumerable<RawStatisticsData> rawData in RawStatistics.Values) |
|
|
|
|
|
|
|
foreach (var serverAndRecords in RawStatistics) |
|
|
|
{ |
|
|
|
var filteredData = rawData; |
|
|
|
if (_config.ByHourOfDay) |
|
|
|
{ |
|
|
|
var currentHour = DateTime.Now.Hour; |
|
|
|
filteredData = filteredData.Where(data => |
|
|
|
data.Timestamp != UnknownDateTime && data.Timestamp.Hour.Equals(currentHour) |
|
|
|
); |
|
|
|
if (filteredData.LongCount() == 0) return; |
|
|
|
} |
|
|
|
var dataList = filteredData as List<RawStatisticsData> ?? filteredData.ToList(); |
|
|
|
var serverName = dataList[0].ServerName; |
|
|
|
FilteredStatistics[serverName] = dataList; |
|
|
|
var server = serverAndRecords.Key; |
|
|
|
var filteredRecords = serverAndRecords.Value.FindAll(IsValidRecord); |
|
|
|
FilteredStatistics[server] = filteredRecords; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@@ -266,36 +286,26 @@ namespace Shadowsocks.Controller |
|
|
|
Logging.Debug($"loading statistics from {path}"); |
|
|
|
if (!File.Exists(path)) |
|
|
|
{ |
|
|
|
try { |
|
|
|
try |
|
|
|
{ |
|
|
|
using (var fs = File.Create(path)) |
|
|
|
{ |
|
|
|
//do nothing |
|
|
|
} |
|
|
|
}catch(Exception e) |
|
|
|
} |
|
|
|
catch (Exception e) |
|
|
|
{ |
|
|
|
Logging.LogUsefulException(e); |
|
|
|
} |
|
|
|
if (!File.Exists(path)) { |
|
|
|
Console.WriteLine($"statistics file does not exist, try to reload {_retryInterval.TotalMinutes} minutes later"); |
|
|
|
_timer.Change(_retryInterval, Interval); |
|
|
|
if (!File.Exists(path)) |
|
|
|
{ |
|
|
|
Console.WriteLine( |
|
|
|
$"statistics file does not exist, try to reload {_retryInterval.TotalMinutes} minutes later"); |
|
|
|
_recorder.Change(_retryInterval, RecordingInterval); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
RawStatistics = (from l in File.ReadAllLines(path).Skip(1) |
|
|
|
let strings = l.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) |
|
|
|
let rawData = new RawStatisticsData |
|
|
|
{ |
|
|
|
Timestamp = ParseExactOrUnknown(strings[0]), |
|
|
|
ServerName = strings[1], |
|
|
|
ICMPStatus = strings[2], |
|
|
|
RoundtripTime = int.Parse(strings[3]) |
|
|
|
} |
|
|
|
group rawData by rawData.ServerName into server |
|
|
|
select new |
|
|
|
{ |
|
|
|
ServerName = server.Key, |
|
|
|
data = server.ToList() |
|
|
|
}).ToDictionary(server => server.ServerName, server => server.data); |
|
|
|
RawStatistics = JsonConvert.DeserializeObject<Statistics>(File.ReadAllText(path)) ?? RawStatistics; |
|
|
|
} |
|
|
|
catch (Exception e) |
|
|
|
{ |
|
|
@@ -306,41 +316,31 @@ namespace Shadowsocks.Controller |
|
|
|
private DateTime ParseExactOrUnknown(string str) |
|
|
|
{ |
|
|
|
DateTime dateTime; |
|
|
|
return !DateTime.TryParseExact(str, DateTimePattern, null, DateTimeStyles.None, out dateTime) ? UnknownDateTime : dateTime; |
|
|
|
return !DateTime.TryParseExact(str, DateTimePattern, null, DateTimeStyles.None, out dateTime) |
|
|
|
? UnknownDateTime |
|
|
|
: dateTime; |
|
|
|
} |
|
|
|
|
|
|
|
public class State |
|
|
|
public void UpdateLatency(int latency) |
|
|
|
{ |
|
|
|
public DataList dataList = new DataList(); |
|
|
|
public const string Unknown = "Unknown"; |
|
|
|
_latencyRecords.Add(latency); |
|
|
|
} |
|
|
|
|
|
|
|
//TODO: redesign model |
|
|
|
public class RawStatisticsData |
|
|
|
private static int GetSpeedInKiBPerSecond(long bytes, double seconds) |
|
|
|
{ |
|
|
|
public DateTime Timestamp; |
|
|
|
public string ServerName; |
|
|
|
public string ICMPStatus; |
|
|
|
public int RoundtripTime; |
|
|
|
var result = (int) (bytes/seconds)/1024; |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
public class StatisticsData |
|
|
|
private class ICMPResult |
|
|
|
{ |
|
|
|
public float PackageLoss; |
|
|
|
public int AverageResponse; |
|
|
|
public int MinResponse; |
|
|
|
public int MaxResponse; |
|
|
|
} |
|
|
|
internal readonly List<int?> RoundtripTime = new List<int?>(); |
|
|
|
internal readonly Server Server; |
|
|
|
|
|
|
|
public void UpdateLatency(int latency) |
|
|
|
{ |
|
|
|
_latency = latency; |
|
|
|
} |
|
|
|
|
|
|
|
private static int GetSpeedInKiBPerSecond(long bytes, double seconds) |
|
|
|
{ |
|
|
|
var result = (int) (bytes / seconds) / 1024; |
|
|
|
return result; |
|
|
|
internal ICMPResult(Server server) |
|
|
|
{ |
|
|
|
Server = server; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |