@@ -1,32 +1,36 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Globalization; | |||||
using System.IO; | using System.IO; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Net; | using System.Net; | ||||
using SimpleJson; | |||||
using System.Net.Http; | using System.Net.Http; | ||||
using System.Net.NetworkInformation; | using System.Net.NetworkInformation; | ||||
using System.Net.Sockets; | using System.Net.Sockets; | ||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using System.Windows.Forms; | |||||
using Shadowsocks.Model; | using Shadowsocks.Model; | ||||
using Shadowsocks.Util; | using Shadowsocks.Util; | ||||
using Timer = System.Threading.Timer; | |||||
namespace Shadowsocks.Controller | namespace Shadowsocks.Controller | ||||
{ | { | ||||
using DataUnit = KeyValuePair<string, string>; | using DataUnit = KeyValuePair<string, string>; | ||||
using DataList = List<KeyValuePair<string, string>>; | using DataList = List<KeyValuePair<string, string>>; | ||||
internal class AvailabilityStatistics | |||||
using RawStatistics = Dictionary<string, List<AvailabilityStatistics.RawStatisticsData>>; | |||||
using Statistics = Dictionary<string, List<AvailabilityStatistics.StatisticsData>>; | |||||
public class AvailabilityStatistics | |||||
{ | { | ||||
public static readonly string DateTimePattern = "yyyy-MM-dd HH:mm:ss"; | public static readonly string DateTimePattern = "yyyy-MM-dd HH:mm:ss"; | ||||
private const string StatisticsFilesName = "shadowsocks.availability.csv"; | private const string StatisticsFilesName = "shadowsocks.availability.csv"; | ||||
private const string Delimiter = ","; | private const string Delimiter = ","; | ||||
private const int Timeout = 500; | private const int Timeout = 500; | ||||
private const int DelayBeforeStart = 1000; | private const int DelayBeforeStart = 1000; | ||||
public RawStatistics rawStatistics { get; private set; } | |||||
public RawStatistics filteredStatistics { get; private set; } | |||||
private int _repeat => _config.RepeatTimesNum; | private int _repeat => _config.RepeatTimesNum; | ||||
private const int RetryInterval = 2*60*1000; //retry 2 minutes after failed | |||||
private int _interval => (int) TimeSpan.FromMinutes(_config.DataCollectionMinutes).TotalMilliseconds; | private int _interval => (int) TimeSpan.FromMinutes(_config.DataCollectionMinutes).TotalMilliseconds; | ||||
private Timer _timer; | private Timer _timer; | ||||
private State _state; | private State _state; | ||||
@@ -57,7 +61,7 @@ namespace Shadowsocks.Controller | |||||
if (_timer?.Change(DelayBeforeStart, _interval) == null) | if (_timer?.Change(DelayBeforeStart, _interval) == null) | ||||
{ | { | ||||
_state = new State(); | _state = new State(); | ||||
_timer = new Timer(Evaluate, _state, DelayBeforeStart, _interval); | |||||
_timer = new Timer(run, _state, DelayBeforeStart, _interval); | |||||
} | } | ||||
} | } | ||||
else | else | ||||
@@ -82,7 +86,7 @@ namespace Shadowsocks.Controller | |||||
var ret = new DataList | var ret = new DataList | ||||
{ | { | ||||
new DataUnit(State.Geolocation, State.Unknown), | new DataUnit(State.Geolocation, State.Unknown), | ||||
new DataUnit(State.ISP, State.Unknown), | |||||
new DataUnit(State.ISP, State.Unknown) | |||||
}; | }; | ||||
string jsonString; | string jsonString; | ||||
try | try | ||||
@@ -95,7 +99,7 @@ namespace Shadowsocks.Controller | |||||
return ret; | return ret; | ||||
} | } | ||||
dynamic obj; | dynamic obj; | ||||
if (!global::SimpleJson.SimpleJson.TryDeserializeObject(jsonString, out obj)) return ret; | |||||
if (!SimpleJson.SimpleJson.TryDeserializeObject(jsonString, out obj)) return ret; | |||||
string country = obj["country"]; | string country = obj["country"]; | ||||
string city = obj["city"]; | string city = obj["city"]; | ||||
string isp = obj["isp"]; | string isp = obj["isp"]; | ||||
@@ -140,7 +144,14 @@ namespace Shadowsocks.Controller | |||||
return ret; | return ret; | ||||
} | } | ||||
private async void Evaluate(object obj) | |||||
private void run(object obj) | |||||
{ | |||||
LoadRawStatistics(); | |||||
FilterRawStatistics(); | |||||
evaluate(); | |||||
} | |||||
private async void evaluate() | |||||
{ | { | ||||
var geolocationAndIsp = GetGeolocationAndIsp(); | var geolocationAndIsp = GetGeolocationAndIsp(); | ||||
foreach (var dataLists in await TaskEx.WhenAll(_servers.Select(ICMPTest))) | foreach (var dataLists in await TaskEx.WhenAll(_servers.Select(ICMPTest))) | ||||
@@ -184,6 +195,86 @@ namespace Shadowsocks.Controller | |||||
_servers = config.configs; | _servers = config.configs; | ||||
} | } | ||||
private void FilterRawStatistics() | |||||
{ | |||||
if (filteredStatistics == null) | |||||
{ | |||||
filteredStatistics = new RawStatistics(); | |||||
} | |||||
foreach (IEnumerable<RawStatisticsData> rawData in rawStatistics.Values) | |||||
{ | |||||
var filteredData = rawData; | |||||
if (_config.ByIsp) | |||||
{ | |||||
var current = GetGeolocationAndIsp().Result; | |||||
filteredData = | |||||
filteredData.Where( | |||||
data => | |||||
data.Geolocation == current[0].Value || | |||||
data.Geolocation == State.Unknown); | |||||
filteredData = | |||||
filteredData.Where( | |||||
data => data.ISP == current[1].Value || data.ISP == State.Unknown); | |||||
if (filteredData.LongCount() == 0) return; | |||||
} | |||||
if (_config.ByHourOfDay) | |||||
{ | |||||
var currentHour = DateTime.Now.Hour; | |||||
filteredData = filteredData.Where(data => | |||||
{ | |||||
DateTime dateTime; | |||||
DateTime.TryParseExact(data.Timestamp, DateTimePattern, null, | |||||
DateTimeStyles.None, out dateTime); | |||||
var result = dateTime.Hour.Equals(currentHour); | |||||
return result; | |||||
}); | |||||
if (filteredData.LongCount() == 0) return; | |||||
} | |||||
var dataList = filteredData as List<RawStatisticsData> ?? filteredData.ToList(); | |||||
var serverName = dataList[0].ServerName; | |||||
filteredStatistics[serverName] = dataList; | |||||
} | |||||
} | |||||
private void LoadRawStatistics() | |||||
{ | |||||
try | |||||
{ | |||||
var path = AvailabilityStatisticsFile; | |||||
Logging.Debug($"loading statistics from {path}"); | |||||
if (!File.Exists(path)) | |||||
{ | |||||
Console.WriteLine($"statistics file does not exist, try to reload {RetryInterval/60/1000} minutes later"); | |||||
_timer.Change(RetryInterval, _interval); | |||||
return; | |||||
} | |||||
rawStatistics = (from l in File.ReadAllLines(path) | |||||
.Skip(1) | |||||
let strings = l.Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries) | |||||
let rawData = new RawStatisticsData | |||||
{ | |||||
Timestamp = strings[0], | |||||
ServerName = strings[1], | |||||
ICMPStatus = strings[2], | |||||
RoundtripTime = int.Parse(strings[3]), | |||||
Geolocation = 5 > strings.Length ? | |||||
null | |||||
: strings[4], | |||||
ISP = 6 > strings.Length ? null : strings[5] | |||||
} | |||||
group rawData by rawData.ServerName into server | |||||
select new | |||||
{ | |||||
ServerName = server.Key, | |||||
data = server.ToList() | |||||
}).ToDictionary(server => server.ServerName, server=> server.data); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logging.LogUsefulException(e); | |||||
} | |||||
} | |||||
public class State | public class State | ||||
{ | { | ||||
public DataList dataList = new DataList(); | public DataList dataList = new DataList(); | ||||
@@ -191,5 +282,24 @@ namespace Shadowsocks.Controller | |||||
public const string ISP = "ISP"; | public const string ISP = "ISP"; | ||||
public const string Unknown = "Unknown"; | public const string Unknown = "Unknown"; | ||||
} | } | ||||
public class RawStatisticsData | |||||
{ | |||||
public string Timestamp; | |||||
public string ServerName; | |||||
public string ICMPStatus; | |||||
public int RoundtripTime; | |||||
public string Geolocation; | |||||
public string ISP ; | |||||
} | |||||
public class StatisticsData | |||||
{ | |||||
public float PackageLoss; | |||||
public int AverageResponse; | |||||
public int MinResponse; | |||||
public int MaxResponse; | |||||
} | |||||
} | } | ||||
} | } |
@@ -25,7 +25,7 @@ namespace Shadowsocks.Controller | |||||
private StrategyManager _strategyManager; | private StrategyManager _strategyManager; | ||||
private PolipoRunner polipoRunner; | private PolipoRunner polipoRunner; | ||||
private GFWListUpdater gfwListUpdater; | private GFWListUpdater gfwListUpdater; | ||||
private AvailabilityStatistics _availabilityStatics; | |||||
public AvailabilityStatistics availabilityStatistics { get; private set; } | |||||
public StatisticsStrategyConfiguration StatisticsConfiguration { get; private set; } | public StatisticsStrategyConfiguration StatisticsConfiguration { get; private set; } | ||||
private bool stopped = false; | private bool stopped = false; | ||||
@@ -260,8 +260,8 @@ namespace Shadowsocks.Controller | |||||
public void UpdateStatisticsConfiguration(bool enabled) | public void UpdateStatisticsConfiguration(bool enabled) | ||||
{ | { | ||||
if (_availabilityStatics == null) return; | |||||
_availabilityStatics.UpdateConfiguration(_config, StatisticsConfiguration); | |||||
if (availabilityStatistics == null) return; | |||||
availabilityStatistics.UpdateConfiguration(_config, StatisticsConfiguration); | |||||
_config.availabilityStatistics = enabled; | _config.availabilityStatistics = enabled; | ||||
SaveConfig(_config); | SaveConfig(_config); | ||||
} | } | ||||
@@ -311,11 +311,11 @@ namespace Shadowsocks.Controller | |||||
gfwListUpdater.Error += pacServer_PACUpdateError; | gfwListUpdater.Error += pacServer_PACUpdateError; | ||||
} | } | ||||
if (_availabilityStatics == null) | |||||
if (availabilityStatistics == null) | |||||
{ | { | ||||
_availabilityStatics = new AvailabilityStatistics(_config, StatisticsConfiguration); | |||||
availabilityStatistics = new AvailabilityStatistics(_config, StatisticsConfiguration); | |||||
} | } | ||||
_availabilityStatics.UpdateConfiguration(_config, StatisticsConfiguration); | |||||
availabilityStatistics.UpdateConfiguration(_config, StatisticsConfiguration); | |||||
if (_listener != null) | if (_listener != null) | ||||
{ | { | ||||
@@ -4,29 +4,21 @@ using System.Globalization; | |||||
using System.IO; | using System.IO; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Net; | using System.Net; | ||||
using System.Text; | |||||
using Shadowsocks.Model; | |||||
using System.IO; | |||||
using System.Net.NetworkInformation; | using System.Net.NetworkInformation; | ||||
using System.Windows.Forms; | |||||
using System.Threading; | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using Shadowsocks.Model; | using Shadowsocks.Model; | ||||
using Timer = System.Threading.Timer; | |||||
namespace Shadowsocks.Controller.Strategy | namespace Shadowsocks.Controller.Strategy | ||||
{ | { | ||||
using DataUnit = KeyValuePair<string, string>; | |||||
using DataList = List<KeyValuePair<string, string>>; | |||||
class StatisticsStrategy : IStrategy | class StatisticsStrategy : IStrategy | ||||
{ | { | ||||
private readonly ShadowsocksController _controller; | private readonly ShadowsocksController _controller; | ||||
private Server _currentServer; | private Server _currentServer; | ||||
private readonly Timer _timer; | private readonly Timer _timer; | ||||
private Dictionary<string, List<StatisticsRawData>> _rawStatistics; | |||||
private Dictionary<string, List<AvailabilityStatistics.RawStatisticsData>> _filteredStatistics; | |||||
private int ChoiceKeptMilliseconds | private int ChoiceKeptMilliseconds | ||||
=> (int) TimeSpan.FromMinutes(_controller.StatisticsConfiguration.ChoiceKeptMinutes).TotalMilliseconds; | => (int) TimeSpan.FromMinutes(_controller.StatisticsConfiguration.ChoiceKeptMinutes).TotalMilliseconds; | ||||
private const int RetryInterval = 2*60*1000; //retry 2 minutes after failed | |||||
public StatisticsStrategy(ShadowsocksController controller) | public StatisticsStrategy(ShadowsocksController controller) | ||||
{ | { | ||||
@@ -47,75 +39,21 @@ namespace Shadowsocks.Controller.Strategy | |||||
private void LoadStatistics() | private void LoadStatistics() | ||||
{ | { | ||||
try | |||||
{ | |||||
var path = AvailabilityStatistics.AvailabilityStatisticsFile; | |||||
Logging.Debug($"loading statistics from {path}"); | |||||
if (!File.Exists(path)) | |||||
{ | |||||
LogWhenEnabled($"statistics file does not exist, try to reload {RetryInterval/60/1000} minutes later"); | |||||
_timer.Change(RetryInterval, ChoiceKeptMilliseconds); | |||||
return; | |||||
} | |||||
_rawStatistics = (from l in File.ReadAllLines(path) | |||||
.Skip(1) | |||||
let strings = l.Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries) | |||||
let rawData = new StatisticsRawData | |||||
{ | |||||
Timestamp = strings[0], | |||||
ServerName = strings[1], | |||||
ICMPStatus = strings[2], | |||||
RoundtripTime = int.Parse(strings[3]), | |||||
Geolocation = 5 > strings.Length ? | |||||
null | |||||
: strings[4], | |||||
ISP = 6 > strings.Length ? null : strings[5] | |||||
} | |||||
group rawData by rawData.ServerName into server | |||||
select new | |||||
{ | |||||
ServerName = server.Key, | |||||
data = server.ToList() | |||||
}).ToDictionary(server => server.ServerName, server=> server.data); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logging.LogUsefulException(e); | |||||
} | |||||
_filteredStatistics = _controller.availabilityStatistics.rawStatistics ?? _filteredStatistics ?? new Dictionary<string, List<AvailabilityStatistics.RawStatisticsData>>(); | |||||
} | } | ||||
//return the score by data | //return the score by data | ||||
//server with highest score will be choosen | //server with highest score will be choosen | ||||
private float GetScore(IEnumerable<StatisticsRawData> rawDataList) | |||||
private float GetScore(string serverName) | |||||
{ | { | ||||
var config = _controller.StatisticsConfiguration; | var config = _controller.StatisticsConfiguration; | ||||
if (config.ByIsp) | |||||
{ | |||||
var current = AvailabilityStatistics.GetGeolocationAndIsp().Result; | |||||
rawDataList = rawDataList.Where(data => data.Geolocation == current[0].Value || data.Geolocation == AvailabilityStatistics.State.Unknown); | |||||
rawDataList = rawDataList.Where(data => data.ISP == current[1].Value || data.ISP == AvailabilityStatistics.State.Unknown); | |||||
if (rawDataList.LongCount() == 0) return 0; | |||||
} | |||||
if (config.ByHourOfDay) | |||||
{ | |||||
var currentHour = DateTime.Now.Hour; | |||||
rawDataList = rawDataList.Where(data => | |||||
{ | |||||
DateTime dateTime; | |||||
DateTime.TryParseExact(data.Timestamp, AvailabilityStatistics.DateTimePattern, null, | |||||
DateTimeStyles.None, out dateTime); | |||||
var result = dateTime.Hour.Equals(currentHour); | |||||
return result; | |||||
}); | |||||
if (rawDataList.LongCount() == 0) return 0; | |||||
} | |||||
var dataList = rawDataList as IList<StatisticsRawData> ?? rawDataList.ToList(); | |||||
var serverName = dataList[0]?.ServerName; | |||||
List<AvailabilityStatistics.RawStatisticsData> dataList; | |||||
if (_filteredStatistics == null || !_filteredStatistics.TryGetValue(serverName, out dataList)) return 0; | |||||
var SuccessTimes = (float) dataList.Count(data => data.ICMPStatus.Equals(IPStatus.Success.ToString())); | var SuccessTimes = (float) dataList.Count(data => data.ICMPStatus.Equals(IPStatus.Success.ToString())); | ||||
var TimedOutTimes = (float) dataList.Count(data => data.ICMPStatus.Equals(IPStatus.TimedOut.ToString())); | var TimedOutTimes = (float) dataList.Count(data => data.ICMPStatus.Equals(IPStatus.TimedOut.ToString())); | ||||
var statisticsData = new StatisticsData() | |||||
var statisticsData = new AvailabilityStatistics.StatisticsData() | |||||
{ | { | ||||
PackageLoss = TimedOutTimes / (SuccessTimes + TimedOutTimes) * 100, | |||||
PackageLoss = TimedOutTimes/(SuccessTimes + TimedOutTimes)*100, | |||||
AverageResponse = Convert.ToInt32(dataList.Average(data => data.RoundtripTime)), | AverageResponse = Convert.ToInt32(dataList.Average(data => data.RoundtripTime)), | ||||
MinResponse = dataList.Min(data => data.RoundtripTime), | MinResponse = dataList.Min(data => data.RoundtripTime), | ||||
MaxResponse = dataList.Max(data => data.RoundtripTime) | MaxResponse = dataList.Max(data => data.RoundtripTime) | ||||
@@ -134,27 +72,9 @@ namespace Shadowsocks.Controller.Strategy | |||||
return score; | return score; | ||||
} | } | ||||
class StatisticsRawData | |||||
{ | |||||
public string Timestamp; | |||||
public string ServerName; | |||||
public string ICMPStatus; | |||||
public int RoundtripTime; | |||||
public string Geolocation; | |||||
public string ISP ; | |||||
} | |||||
public class StatisticsData | |||||
{ | |||||
public float PackageLoss; | |||||
public int AverageResponse; | |||||
public int MinResponse; | |||||
public int MaxResponse; | |||||
} | |||||
private void ChooseNewServer(List<Server> servers) | private void ChooseNewServer(List<Server> servers) | ||||
{ | { | ||||
if (_rawStatistics == null || servers.Count == 0) | |||||
if (_filteredStatistics == null || servers.Count == 0) | |||||
{ | { | ||||
return; | return; | ||||
} | } | ||||
@@ -162,11 +82,11 @@ namespace Shadowsocks.Controller.Strategy | |||||
{ | { | ||||
var bestResult = (from server in servers | var bestResult = (from server in servers | ||||
let name = server.FriendlyName() | let name = server.FriendlyName() | ||||
where _rawStatistics.ContainsKey(name) | |||||
where _filteredStatistics.ContainsKey(name) | |||||
select new | select new | ||||
{ | { | ||||
server, | server, | ||||
score = GetScore(_rawStatistics[name]) | |||||
score = GetScore(name) | |||||
} | } | ||||
).Aggregate((result1, result2) => result1.score > result2.score ? result1 : result2); | ).Aggregate((result1, result2) => result1.score > result2.score ? result1 : result2); | ||||
@@ -62,8 +62,8 @@ namespace Shadowsocks.Model | |||||
public StatisticsStrategyConfiguration() | public StatisticsStrategyConfiguration() | ||||
{ | { | ||||
var statisticsStrategy = typeof (StatisticsStrategy); | |||||
var statisticsData = statisticsStrategy.GetNestedType("StatisticsData"); | |||||
var availabilityStatisticsType = typeof (AvailabilityStatistics); | |||||
var statisticsData = availabilityStatisticsType.GetNestedType("StatisticsData"); | |||||
var properties = statisticsData.GetFields(BindingFlags.Instance | BindingFlags.Public); | var properties = statisticsData.GetFields(BindingFlags.Instance | BindingFlags.Public); | ||||
Calculations = properties.ToDictionary(p => p.Name, _ => (float) 0); | Calculations = properties.ToDictionary(p => p.Name, _ => (float) 0); | ||||
} | } | ||||