diff --git a/.gitignore b/.gitignore index 82c89bb5..4a64d6be 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,9 @@ shadowsocks-csharp/shadowsocks-csharp.csproj.user TestResults *.suo +shadowsocks-csharp/3rd/* +!shadowsocks-csharp/3rd/zxing/ +!shadowsocks-csharp/3rd/SimpleJson.cs +packages/* + +shadowsocks-csharp.sln.DotSettings.user diff --git a/nuget.config b/nuget.config new file mode 100644 index 00000000..57b36f04 --- /dev/null +++ b/nuget.config @@ -0,0 +1,5 @@ + + + + + diff --git a/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs b/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs index 08125e92..49820f63 100644 --- a/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs +++ b/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs @@ -1,50 +1,72 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; +using System.Net; +using System.Net.Http; using System.Net.NetworkInformation; +using System.Net.Sockets; using System.Threading; +using System.Threading.Tasks; using Shadowsocks.Model; -using System.Reflection; using Shadowsocks.Util; namespace Shadowsocks.Controller { - class AvailabilityStatistics + using DataUnit = KeyValuePair; + using DataList = List>; + + using Statistics = Dictionary>; + + public class AvailabilityStatistics { - private static readonly string StatisticsFilesName = "shadowsocks.availability.csv"; - private static readonly string Delimiter = ","; - private static readonly int Timeout = 500; - private static readonly int Repeat = 4; //repeat times every evaluation - private static readonly int Interval = 10 * 60 * 1000; //evaluate proxies every 15 minutes - private Timer timer = null; - private State state = null; - private List servers; + public static readonly string DateTimePattern = "yyyy-MM-dd HH:mm:ss"; + private const string StatisticsFilesName = "shadowsocks.availability.csv"; + private const string Delimiter = ","; + private const int Timeout = 500; + private const int DelayBeforeStart = 1000; + 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 const int RetryInterval = 2*60*1000; //retry 2 minutes after failed + private int Interval => (int) TimeSpan.FromMinutes(_config.DataCollectionMinutes).TotalMilliseconds; + private Timer _timer; + private State _state; + private List _servers; + private StatisticsStrategyConfiguration _config; public static string AvailabilityStatisticsFile; //static constructor to initialize every public static fields before refereced static AvailabilityStatistics() { - string temppath = Utils.GetTempPath(); + var temppath = Utils.GetTempPath(); AvailabilityStatisticsFile = Path.Combine(temppath, StatisticsFilesName); } - public bool Set(bool enabled) + public AvailabilityStatistics(Configuration config, StatisticsStrategyConfiguration statisticsConfig) + { + UpdateConfiguration(config, statisticsConfig); + } + + public bool Set(StatisticsStrategyConfiguration config) { + _config = config; try { - if (enabled) + if (config.StatisticsEnabled) { - if (timer?.Change(0, Interval) == null) + if (_timer?.Change(DelayBeforeStart, Interval) == null) { - state = new State(); - timer = new Timer(Evaluate, state, 0, Interval); + _state = new State(); + _timer = new Timer(Run, _state, DelayBeforeStart, Interval); } } else { - timer?.Dispose(); + _timer?.Dispose(); } return true; } @@ -55,64 +77,240 @@ namespace Shadowsocks.Controller } } - private void Evaluate(object obj) + //hardcode + //TODO: backup reliable isp&geolocation provider or a local database is required + public static async Task GetGeolocationAndIsp() { - Ping ping = new Ping(); - State state = (State) obj; - foreach (var server in servers) + Logging.Debug("Retrive information of geolocation and isp"); + const string API = "http://ip-api.com/json"; + const string alternativeAPI = "http://www.telize.com/geoip"; //must be comptible with current API + var result = await GetInfoFromAPI(API); + if (result != null) return result; + result = await GetInfoFromAPI(alternativeAPI); + if (result != null) return result; + return new DataList + { + new DataUnit(State.Geolocation, State.Unknown), + new DataUnit(State.ISP, State.Unknown) + }; + } + + private static async Task GetInfoFromAPI(string API) + { + string jsonString; + try { - Logging.Debug("eveluating " + server.FriendlyName()); - foreach (var _ in Enumerable.Range(0, Repeat)) + jsonString = await new HttpClient().GetStringAsync(API); + } + catch (HttpRequestException e) + { + Logging.LogUsefulException(e); + return null; + } + dynamic obj; + if (!SimpleJson.SimpleJson.TryDeserializeObject(jsonString, out obj)) return null; + string country = obj["country"]; + string city = obj["city"]; + string isp = obj["isp"]; + if (country == null || city == null || isp == null) return null; + return new DataList { + new DataUnit(State.Geolocation, $"\"{country} {city}\""), + new DataUnit(State.ISP, $"\"{isp}\"") + }; + } + + private async Task> ICMPTest(Server server) + { + Logging.Debug("Ping " + server.FriendlyName()); + if (server.server == "") return null; + var IP = Dns.GetHostAddresses(server.server).First(ip => ip.AddressFamily == AddressFamily.InterNetwork); + var ping = new Ping(); + var ret = new List(); + foreach ( + var timestamp in Enumerable.Range(0, Repeat).Select(_ => DateTime.Now.ToString(DateTimePattern))) + { + //ICMP echo. we can also set options and special bytes + try { - //TODO: do simple analyze of data to provide friendly message, like package loss. - string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); - //ICMP echo. we can also set options and special bytes - //seems no need to use SendPingAsync - try - { - PingReply reply = ping.Send(server.server, Timeout); - state.data = new List>(); - state.data.Add(new KeyValuePair("Timestamp", timestamp)); - state.data.Add(new KeyValuePair("Server", server.FriendlyName())); - state.data.Add(new KeyValuePair("Status", reply.Status.ToString())); - state.data.Add(new KeyValuePair("RoundtripTime", reply.RoundtripTime.ToString())); - //state.data.Add(new KeyValuePair("data", reply.Buffer.ToString())); // The data of reply - Append(state.data); - } - catch (Exception e) + var reply = await ping.SendTaskAsync(IP, Timeout); + ret.Add(new List> { - Console.WriteLine($"An exception occured when eveluating {server.FriendlyName()}"); - Logging.LogUsefulException(e); - } + new KeyValuePair("Timestamp", timestamp), + new KeyValuePair("Server", server.FriendlyName()), + new KeyValuePair("Status", reply?.Status.ToString()), + new KeyValuePair("RoundtripTime", reply?.RoundtripTime.ToString()) + //new KeyValuePair("data", reply.Buffer.ToString()); // The data of reply + }); + Thread.Sleep(Timeout + new Random().Next() % Timeout); + //Do ICMPTest in a random frequency + } + catch (Exception e) + { + Console.WriteLine($"An exception occured when eveluating {server.FriendlyName()}"); + Logging.LogUsefulException(e); } } + return ret; + } + + private void Run(object obj) + { + LoadRawStatistics(); + FilterRawStatistics(); + evaluate(); } - private static void Append(List> data) + private async void evaluate() { - string dataLine = string.Join(Delimiter, data.Select(kv => kv.Value).ToArray()); + var geolocationAndIsp = GetGeolocationAndIsp(); + foreach (var dataLists in await TaskEx.WhenAll(_servers.Select(ICMPTest))) + { + if (dataLists == null) continue; + foreach (var dataList in dataLists.Where(dataList => dataList != null)) + { + await geolocationAndIsp; + Append(dataList, geolocationAndIsp.Result); + } + } + } + + private static void Append(DataList dataList, IEnumerable extra) + { + var data = dataList.Concat(extra); + var dataLine = string.Join(Delimiter, data.Select(kv => kv.Value).ToArray()); string[] lines; if (!File.Exists(AvailabilityStatisticsFile)) { - string headerLine = string.Join(Delimiter, data.Select(kv => kv.Key).ToArray()); - lines = new string[] { headerLine, dataLine }; + var headerLine = string.Join(Delimiter, data.Select(kv => kv.Key).ToArray()); + lines = new[] {headerLine, dataLine}; } else { - lines = new string[] { dataLine }; + lines = new[] {dataLine}; + } + try + { + File.AppendAllLines(AvailabilityStatisticsFile, lines); + } + catch (IOException e) + { + Logging.LogUsefulException(e); } - File.AppendAllLines(AvailabilityStatisticsFile, lines); } - internal void UpdateConfiguration(Configuration _config) + internal void UpdateConfiguration(Configuration config, StatisticsStrategyConfiguration statisticsConfig) { - Set(_config.availabilityStatistics); - servers = _config.configs; + Set(statisticsConfig); + _servers = config.configs; } - private class State + private async void FilterRawStatistics() { - public List> data = new List>(); + if (RawStatistics == null) return; + if (FilteredStatistics == null) + { + FilteredStatistics = new Statistics(); + } + foreach (IEnumerable rawData in RawStatistics.Values) + { + var filteredData = rawData; + if (_config.ByIsp) + { + var current = await GetGeolocationAndIsp(); + 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 => + data.Timestamp != UnknownDateTime && data.Timestamp.Hour.Equals(currentHour) + ); + if (filteredData.LongCount() == 0) return; + } + var dataList = filteredData as List ?? 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 = ParseExactOrUnknown(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); + } + } + + private DateTime ParseExactOrUnknown(string str) + { + DateTime dateTime; + return !DateTime.TryParseExact(str, DateTimePattern, null, DateTimeStyles.None, out dateTime) ? UnknownDateTime : dateTime; + } + + public class State + { + public DataList dataList = new DataList(); + public const string Geolocation = "Geolocation"; + public const string ISP = "ISP"; + public const string Unknown = "Unknown"; + } + + public class RawStatisticsData + { + public DateTime 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; + } + } } diff --git a/shadowsocks-csharp/Controller/ShadowsocksController.cs b/shadowsocks-csharp/Controller/ShadowsocksController.cs index 40496cfe..7761e3f8 100755 --- a/shadowsocks-csharp/Controller/ShadowsocksController.cs +++ b/shadowsocks-csharp/Controller/ShadowsocksController.cs @@ -25,7 +25,9 @@ namespace Shadowsocks.Controller private StrategyManager _strategyManager; private PolipoRunner polipoRunner; private GFWListUpdater gfwListUpdater; - private AvailabilityStatistics _availabilityStatics; + public AvailabilityStatistics availabilityStatistics { get; private set; } + public StatisticsStrategyConfiguration StatisticsConfiguration { get; private set; } + private bool stopped = false; private bool _systemProxyIsDirty = false; @@ -53,10 +55,12 @@ namespace Shadowsocks.Controller public ShadowsocksController() { _config = Configuration.Load(); + StatisticsConfiguration = StatisticsStrategyConfiguration.Load(); _strategyManager = new StrategyManager(this); StartReleasingMemory(); } + public void Start() { Reload(); @@ -125,6 +129,12 @@ namespace Shadowsocks.Controller Configuration.Save(_config); } + public void SaveStrategyConfigurations(StatisticsStrategyConfiguration configuration) + { + StatisticsConfiguration = configuration; + StatisticsStrategyConfiguration.Save(configuration); + } + public bool AddServerBySSURL(string ssURL) { try @@ -248,14 +258,12 @@ namespace Shadowsocks.Controller } } - public void ToggleAvailabilityStatistics(bool enabled) + public void UpdateStatisticsConfiguration(bool enabled) { - if (_availabilityStatics != null) - { - _availabilityStatics.Set(enabled); - _config.availabilityStatistics = enabled; - SaveConfig(_config); - } + if (availabilityStatistics == null) return; + availabilityStatistics.UpdateConfiguration(_config, StatisticsConfiguration); + _config.availabilityStatistics = enabled; + SaveConfig(_config); } public void SavePACUrl(string pacUrl) @@ -296,6 +304,7 @@ namespace Shadowsocks.Controller { // some logic in configuration updated the config when saving, we need to read it again _config = Configuration.Load(); + StatisticsConfiguration = StatisticsStrategyConfiguration.Load(); if (polipoRunner == null) { @@ -314,17 +323,16 @@ namespace Shadowsocks.Controller gfwListUpdater.Error += pacServer_PACUpdateError; } - if (_listener != null) + if (availabilityStatistics == null) { - _listener.Stop(); + availabilityStatistics = new AvailabilityStatistics(_config, StatisticsConfiguration); } + availabilityStatistics.UpdateConfiguration(_config, StatisticsConfiguration); - if (_availabilityStatics == null) + if (_listener != null) { - _availabilityStatics = new AvailabilityStatistics(); - _availabilityStatics.UpdateConfiguration(_config); + _listener.Stop(); } - // don't put polipoRunner.Start() before pacServer.Stop() // or bind will fail when switching bind address from 0.0.0.0 to 127.0.0.1 // though UseShellExecute is set to true now diff --git a/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs b/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs deleted file mode 100644 index 0cdfcfc5..00000000 --- a/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using Shadowsocks.Model; -using System.IO; -using System.Net.NetworkInformation; -using System.Threading; - -namespace Shadowsocks.Controller.Strategy -{ - class SimplyChooseByStatisticsStrategy : IStrategy - { - private ShadowsocksController _controller; - private Server _currentServer; - private Timer timer; - private Dictionary statistics; - private static readonly int CachedInterval = 30 * 60 * 1000; //choose a new server every 30 minutes - - public SimplyChooseByStatisticsStrategy(ShadowsocksController controller) - { - _controller = controller; - var servers = controller.GetCurrentConfiguration().configs; - int randomIndex = new Random().Next() % servers.Count(); - _currentServer = servers[randomIndex]; //choose a server randomly at first - timer = new Timer(ReloadStatisticsAndChooseAServer); - } - - private void ReloadStatisticsAndChooseAServer(object obj) - { - Logging.Debug("Reloading statistics and choose a new server...."); - List servers = _controller.GetCurrentConfiguration().configs; - LoadStatistics(); - ChooseNewServer(servers); - } - - /* - return a dict: - { - 'ServerFriendlyName1':StatisticsData, - 'ServerFriendlyName2':... - } - */ - private void LoadStatistics() - { - try - { - var path = AvailabilityStatistics.AvailabilityStatisticsFile; - Logging.Debug(string.Format("loading statistics from{0}", path)); - statistics = (from l in File.ReadAllLines(path) - .Skip(1) - let strings = l.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) - let rawData = new - { - ServerName = strings[1], - IPStatus = strings[2], - RoundtripTime = int.Parse(strings[3]) - } - group rawData by rawData.ServerName into server - select new - { - ServerName = server.Key, - data = new StatisticsData - { - SuccessTimes = server.Count(data => IPStatus.Success.ToString().Equals(data.IPStatus)), - TimedOutTimes = server.Count(data => IPStatus.TimedOut.ToString().Equals(data.IPStatus)), - AverageResponse = Convert.ToInt32(server.Average(data => data.RoundtripTime)), - MinResponse = server.Min(data => data.RoundtripTime), - MaxResponse = server.Max(data => data.RoundtripTime) - } - }).ToDictionary(server => server.ServerName, server => server.data); - } - catch (Exception e) - { - Logging.LogUsefulException(e); - } - } - - //return the score by data - //server with highest score will be choosen - private static double GetScore(StatisticsData data) - { - return (double)data.SuccessTimes / (data.SuccessTimes + data.TimedOutTimes); //simply choose min package loss - } - - private class StatisticsData - { - public int SuccessTimes; - public int TimedOutTimes; - public int AverageResponse; - public int MinResponse; - public int MaxResponse; - } - - private void ChooseNewServer(List servers) - { - if (statistics == null) - { - return; - } - try - { - var bestResult = (from server in servers - let name = server.FriendlyName() - where statistics.ContainsKey(name) - select new - { - server, - score = GetScore(statistics[name]) - } - ).Aggregate((result1, result2) => result1.score > result2.score ? result1 : result2); - - if (_controller.GetCurrentStrategy().ID == ID && _currentServer != bestResult.server) //output when enabled - { - Console.WriteLine("Switch to server: {0} by package loss:{1}", bestResult.server.FriendlyName(), 1 - bestResult.score); - } - _currentServer = bestResult.server; - } - catch (Exception e) - { - Logging.LogUsefulException(e); - } - } - - public string ID - { - get { return "com.shadowsocks.strategy.scbs"; } - } - - public string Name - { - get { return I18N.GetString("Choose By Total Package Loss"); } - } - - public Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint) - { - var oldServer = _currentServer; - if (oldServer == null) - { - ChooseNewServer(_controller.GetCurrentConfiguration().configs); - } - if (oldServer != _currentServer) - { - } - return _currentServer; //current server cached for CachedInterval - } - - public void ReloadServers() - { - ChooseNewServer(_controller.GetCurrentConfiguration().configs); - timer?.Change(0, CachedInterval); - } - - public void SetFailure(Server server) - { - Logging.Debug(String.Format("failure: {0}", server.FriendlyName())); - } - - public void UpdateLastRead(Server server) - { - //TODO: combine this part of data with ICMP statics - } - - public void UpdateLastWrite(Server server) - { - //TODO: combine this part of data with ICMP statics - } - - public void UpdateLatency(Server server, TimeSpan latency) - { - //TODO: combine this part of data with ICMP statics - } - - } -} diff --git a/shadowsocks-csharp/Controller/Strategy/StatisticsStrategy.cs b/shadowsocks-csharp/Controller/Strategy/StatisticsStrategy.cs new file mode 100644 index 00000000..d3b5e26b --- /dev/null +++ b/shadowsocks-csharp/Controller/Strategy/StatisticsStrategy.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Threading; +using Newtonsoft.Json; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller.Strategy +{ + class StatisticsStrategy : IStrategy + { + private readonly ShadowsocksController _controller; + private Server _currentServer; + private readonly Timer _timer; + private Dictionary> _filteredStatistics; + private int ChoiceKeptMilliseconds + => (int) TimeSpan.FromMinutes(_controller.StatisticsConfiguration.ChoiceKeptMinutes).TotalMilliseconds; + + public StatisticsStrategy(ShadowsocksController controller) + { + _controller = controller; + var servers = controller.GetCurrentConfiguration().configs; + var randomIndex = new Random().Next() % servers.Count(); + _currentServer = servers[randomIndex]; //choose a server randomly at first + _timer = new Timer(ReloadStatisticsAndChooseAServer); + } + + private void ReloadStatisticsAndChooseAServer(object obj) + { + Logging.Debug("Reloading statistics and choose a new server...."); + var servers = _controller.GetCurrentConfiguration().configs; + LoadStatistics(); + ChooseNewServer(servers); + } + + private void LoadStatistics() + { + _filteredStatistics = _controller.availabilityStatistics.RawStatistics ?? _filteredStatistics ?? new Dictionary>(); + } + + //return the score by data + //server with highest score will be choosen + private float GetScore(string serverName) + { + var config = _controller.StatisticsConfiguration; + List dataList; + if (_filteredStatistics == null || !_filteredStatistics.TryGetValue(serverName, out dataList)) return 0; + 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 statisticsData = new AvailabilityStatistics.StatisticsData + { + PackageLoss = timedOutTimes/(successTimes + timedOutTimes)*100, + AverageResponse = Convert.ToInt32(dataList.Average(data => data.RoundtripTime)), + MinResponse = dataList.Min(data => data.RoundtripTime), + MaxResponse = dataList.Max(data => data.RoundtripTime) + }; + float factor; + float score = 0; + if (!config.Calculations.TryGetValue("PackageLoss", out factor)) factor = 0; + score += statisticsData.PackageLoss*factor; + if (!config.Calculations.TryGetValue("AverageResponse", out factor)) factor = 0; + score += statisticsData.AverageResponse*factor; + if (!config.Calculations.TryGetValue("MinResponse", out factor)) factor = 0; + score += statisticsData.MinResponse*factor; + if (!config.Calculations.TryGetValue("MaxResponse", out factor)) factor = 0; + score += statisticsData.MaxResponse*factor; + Logging.Debug($"{serverName} {JsonConvert.SerializeObject(statisticsData)}"); + return score; + } + + private void ChooseNewServer(List servers) + { + if (_filteredStatistics == null || servers.Count == 0) + { + return; + } + try + { + var bestResult = (from server in servers + let name = server.FriendlyName() + where _filteredStatistics.ContainsKey(name) + select new + { + server, + score = GetScore(name) + } + ).Aggregate((result1, result2) => result1.score > result2.score ? result1 : result2); + + LogWhenEnabled($"Switch to server: {bestResult.server.FriendlyName()} by statistics: score {bestResult.score}"); + _currentServer = bestResult.server; + } + catch (Exception e) + { + Logging.LogUsefulException(e); + } + } + + private void LogWhenEnabled(string log) + { + if (_controller.GetCurrentStrategy()?.ID == ID) //output when enabled + { + Console.WriteLine(log); + } + } + + public string ID => "com.shadowsocks.strategy.scbs"; + + public string Name => I18N.GetString("Choose By Total Package Loss"); + + public Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint) + { + var oldServer = _currentServer; + if (oldServer == null) + { + ChooseNewServer(_controller.GetCurrentConfiguration().configs); + } + if (oldServer != _currentServer) + { + } + return _currentServer; //current server cached for CachedInterval + } + + public void ReloadServers() + { + ChooseNewServer(_controller.GetCurrentConfiguration().configs); + _timer?.Change(0, ChoiceKeptMilliseconds); + } + + public void SetFailure(Server server) + { + Logging.Debug($"failure: {server.FriendlyName()}"); + } + + public void UpdateLastRead(Server server) + { + //TODO: combine this part of data with ICMP statics + } + + public void UpdateLastWrite(Server server) + { + //TODO: combine this part of data with ICMP statics + } + + public void UpdateLatency(Server server, TimeSpan latency) + { + //TODO: combine this part of data with ICMP statics + } + + } +} diff --git a/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs b/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs index dd1f705c..81a00e4d 100644 --- a/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs +++ b/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs @@ -13,7 +13,7 @@ namespace Shadowsocks.Controller.Strategy _strategies = new List(); _strategies.Add(new BalancingStrategy(controller)); _strategies.Add(new HighAvailabilityStrategy(controller)); - _strategies.Add(new SimplyChooseByStatisticsStrategy(controller)); + _strategies.Add(new StatisticsStrategy(controller)); // TODO: load DLL plugins } public IList GetStrategies() diff --git a/shadowsocks-csharp/FodyWeavers.xml b/shadowsocks-csharp/FodyWeavers.xml new file mode 100644 index 00000000..2e6d4a7a --- /dev/null +++ b/shadowsocks-csharp/FodyWeavers.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/shadowsocks-csharp/Model/Configuration.cs b/shadowsocks-csharp/Model/Configuration.cs index cf506072..58f9f941 100755 --- a/shadowsocks-csharp/Model/Configuration.cs +++ b/shadowsocks-csharp/Model/Configuration.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Text; using System.Windows.Forms; +using SimpleJson; namespace Shadowsocks.Model { diff --git a/shadowsocks-csharp/Model/StatisticsStrategyConfiguration.cs b/shadowsocks-csharp/Model/StatisticsStrategyConfiguration.cs new file mode 100644 index 00000000..62a48c2e --- /dev/null +++ b/shadowsocks-csharp/Model/StatisticsStrategyConfiguration.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Shadowsocks.Controller; +using Shadowsocks.Controller.Strategy; +using SimpleJson; +using Newtonsoft.Json; + +namespace Shadowsocks.Model +{ + [Serializable] + public class StatisticsStrategyConfiguration + { + public static readonly string ID = "com.shadowsocks.strategy.statistics"; + private bool _statisticsEnabled = true; + private bool _byIsp = false; + private bool _byHourOfDay = false; + private int _choiceKeptMinutes = 10; + private int _dataCollectionMinutes = 10; + private int _repeatTimesNum = 4; + + + private const string ConfigFile = "statistics-config.json"; + + public static StatisticsStrategyConfiguration Load() + { + try + { + var content = File.ReadAllText(ConfigFile); + var configuration = JsonConvert.DeserializeObject(content); + return configuration; + } + catch (FileNotFoundException e) + { + var configuration = new StatisticsStrategyConfiguration(); + Save(configuration); + return configuration; + } + catch (Exception e) + { + Logging.LogUsefulException(e); + return new StatisticsStrategyConfiguration(); + } + } + + public static void Save(StatisticsStrategyConfiguration configuration) + { + try + { + var content = JsonConvert.SerializeObject(configuration, Formatting.Indented); + File.WriteAllText(ConfigFile, content); + } + catch (Exception e) + { + Logging.LogUsefulException(e); + } + } + + public Dictionary Calculations; + + public StatisticsStrategyConfiguration() + { + var availabilityStatisticsType = typeof (AvailabilityStatistics); + var statisticsData = availabilityStatisticsType.GetNestedType("StatisticsData"); + var properties = statisticsData.GetFields(BindingFlags.Instance | BindingFlags.Public); + Calculations = properties.ToDictionary(p => p.Name, _ => (float) 0); + } + + public bool StatisticsEnabled + { + get { return _statisticsEnabled; } + set { _statisticsEnabled = value; } + } + + public bool ByIsp + { + get { return _byIsp; } + set { _byIsp = value; } + } + + public bool ByHourOfDay + { + get { return _byHourOfDay; } + set { _byHourOfDay = value; } + } + + public int ChoiceKeptMinutes + { + get { return _choiceKeptMinutes; } + set { _choiceKeptMinutes = value; } + } + + public int DataCollectionMinutes + { + get { return _dataCollectionMinutes; } + set { _dataCollectionMinutes = value; } + } + + public int RepeatTimesNum + { + get { return _repeatTimesNum; } + set { _repeatTimesNum = value; } + } + } +} diff --git a/shadowsocks-csharp/Properties/DataSources/Shadowsocks.Model.StatisticsStrategyConfiguration.datasource b/shadowsocks-csharp/Properties/DataSources/Shadowsocks.Model.StatisticsStrategyConfiguration.datasource new file mode 100644 index 00000000..a1a52e80 --- /dev/null +++ b/shadowsocks-csharp/Properties/DataSources/Shadowsocks.Model.StatisticsStrategyConfiguration.datasource @@ -0,0 +1,10 @@ + + + + Shadowsocks.Model.StatisticsStrategyConfiguration, Shadowsocks, Version=2.5.2.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file diff --git a/shadowsocks-csharp/View/CalculationControl.Designer.cs b/shadowsocks-csharp/View/CalculationControl.Designer.cs new file mode 100644 index 00000000..b5a9bfab --- /dev/null +++ b/shadowsocks-csharp/View/CalculationControl.Designer.cs @@ -0,0 +1,111 @@ +namespace Shadowsocks.View +{ + partial class CalculationControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.factorNum = new System.Windows.Forms.NumericUpDown(); + this.multiply = new System.Windows.Forms.Label(); + this.plus = new System.Windows.Forms.Label(); + this.valueLabel = new System.Windows.Forms.Label(); + ((System.ComponentModel.ISupportInitialize)(this.factorNum)).BeginInit(); + this.SuspendLayout(); + // + // factorNum + // + this.factorNum.DecimalPlaces = 2; + this.factorNum.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.factorNum.Increment = new decimal(new int[] { + 1, + 0, + 0, + 131072}); + this.factorNum.Location = new System.Drawing.Point(285, 5); + this.factorNum.Minimum = new decimal(new int[] { + 1000, + 0, + 0, + -2147418112}); + this.factorNum.Name = "factorNum"; + this.factorNum.Size = new System.Drawing.Size(86, 34); + this.factorNum.TabIndex = 6; + // + // multiply + // + this.multiply.AutoSize = true; + this.multiply.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.multiply.Location = new System.Drawing.Point(251, 7); + this.multiply.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); + this.multiply.Name = "multiply"; + this.multiply.Size = new System.Drawing.Size(26, 28); + this.multiply.TabIndex = 2; + this.multiply.Text = "×"; + // + // plus + // + this.plus.AutoSize = true; + this.plus.Font = new System.Drawing.Font("Segoe UI", 10F); + this.plus.Location = new System.Drawing.Point(5, 7); + this.plus.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); + this.plus.Name = "plus"; + this.plus.Size = new System.Drawing.Size(26, 28); + this.plus.TabIndex = 3; + this.plus.Text = "+"; + // + // valueLabel + // + this.valueLabel.AutoSize = true; + this.valueLabel.Font = new System.Drawing.Font("Microsoft YaHei", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.valueLabel.Location = new System.Drawing.Point(39, 11); + this.valueLabel.Name = "valueLabel"; + this.valueLabel.Size = new System.Drawing.Size(118, 24); + this.valueLabel.TabIndex = 7; + this.valueLabel.Text = "PackageLoss"; + // + // CalculationControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 18F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.valueLabel); + this.Controls.Add(this.factorNum); + this.Controls.Add(this.multiply); + this.Controls.Add(this.plus); + this.Name = "CalculationControl"; + this.Size = new System.Drawing.Size(380, 46); + ((System.ComponentModel.ISupportInitialize)(this.factorNum)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + private System.Windows.Forms.NumericUpDown factorNum; + private System.Windows.Forms.Label multiply; + private System.Windows.Forms.Label plus; + private System.Windows.Forms.Label valueLabel; + } +} diff --git a/shadowsocks-csharp/View/CalculationControl.cs b/shadowsocks-csharp/View/CalculationControl.cs new file mode 100644 index 00000000..c7f8fdf2 --- /dev/null +++ b/shadowsocks-csharp/View/CalculationControl.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Data; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Windows.Forms; + +namespace Shadowsocks.View +{ + public partial class CalculationControl : UserControl + { + public CalculationControl(string text, float value) + { + InitializeComponent(); + valueLabel.Text = text; + factorNum.Value = (decimal) value; + } + + public string Value => valueLabel.Text; + public float Factor => (float) factorNum.Value; + } +} diff --git a/shadowsocks-csharp/View/CalculationControl.resx b/shadowsocks-csharp/View/CalculationControl.resx new file mode 100644 index 00000000..1af7de15 --- /dev/null +++ b/shadowsocks-csharp/View/CalculationControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/shadowsocks-csharp/View/MenuViewController.cs b/shadowsocks-csharp/View/MenuViewController.cs index d4e8a837..b19a244f 100755 --- a/shadowsocks-csharp/View/MenuViewController.cs +++ b/shadowsocks-csharp/View/MenuViewController.cs @@ -30,7 +30,6 @@ namespace Shadowsocks.View private MenuItem enableItem; private MenuItem modeItem; private MenuItem AutoStartupItem; - private MenuItem AvailabilityStatistics; private MenuItem ShareOverLANItem; private MenuItem SeperatorItem; private MenuItem ConfigItem; @@ -172,6 +171,7 @@ namespace Shadowsocks.View this.ServersItem = CreateMenuGroup("Servers", new MenuItem[] { this.SeperatorItem = new MenuItem("-"), this.ConfigItem = CreateMenuItem("Edit Servers...", new EventHandler(this.Config_Click)), + CreateMenuItem("Statistics Config...", StatisticsConfigItem_Click), CreateMenuItem("Show QRCode...", new EventHandler(this.QRCodeItem_Click)), CreateMenuItem("Scan QRCode from Screen...", new EventHandler(this.ScanQRCodeItem_Click)) }), @@ -186,7 +186,6 @@ namespace Shadowsocks.View }), new MenuItem("-"), this.AutoStartupItem = CreateMenuItem("Start on Boot", new EventHandler(this.AutoStartupItem_Click)), - this.AvailabilityStatistics = CreateMenuItem("Availability Statistics", new EventHandler(this.AvailabilityStatisticsItem_Click)), this.ShareOverLANItem = CreateMenuItem("Allow Clients from LAN", new EventHandler(this.ShareOverLANItem_Click)), new MenuItem("-"), CreateMenuItem("Show Logs...", new EventHandler(this.ShowLogItem_Click)), @@ -201,6 +200,7 @@ namespace Shadowsocks.View }); } + private void controller_ConfigChanged(object sender, EventArgs e) { LoadCurrentConfiguration(); @@ -285,7 +285,6 @@ namespace Shadowsocks.View PACModeItem.Checked = !config.global; ShareOverLANItem.Checked = config.shareOverLan; AutoStartupItem.Checked = AutoStartup.Check(); - AvailabilityStatistics.Checked = config.availabilityStatistics; onlinePACItem.Checked = onlinePACItem.Enabled && config.useOnlinePac; localPACItem.Checked = !onlinePACItem.Checked; UpdatePACItemsEnabledStatus(); @@ -441,6 +440,12 @@ namespace Shadowsocks.View new LogForm(controller, argument).Show(); } + + private void StatisticsConfigItem_Click(object sender, EventArgs e) + { + StatisticsStrategyConfigurationForm form = new StatisticsStrategyConfigurationForm(controller); + form.Show(); + } private void QRCodeItem_Click(object sender, EventArgs e) { @@ -551,11 +556,6 @@ namespace Shadowsocks.View } } - private void AvailabilityStatisticsItem_Click(object sender, EventArgs e) { - AvailabilityStatistics.Checked = !AvailabilityStatistics.Checked; - controller.ToggleAvailabilityStatistics(AvailabilityStatistics.Checked); - } - private void LocalPACItem_Click(object sender, EventArgs e) { if (!localPACItem.Checked) diff --git a/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.Designer.cs b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.Designer.cs new file mode 100644 index 00000000..a207f309 --- /dev/null +++ b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.Designer.cs @@ -0,0 +1,531 @@ +namespace Shadowsocks.View +{ + partial class StatisticsStrategyConfigurationForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + System.Windows.Forms.DataVisualization.Charting.ChartArea chartArea1 = new System.Windows.Forms.DataVisualization.Charting.ChartArea(); + System.Windows.Forms.DataVisualization.Charting.Legend legend1 = new System.Windows.Forms.DataVisualization.Charting.Legend(); + System.Windows.Forms.DataVisualization.Charting.Series series1 = new System.Windows.Forms.DataVisualization.Charting.Series(); + System.Windows.Forms.DataVisualization.Charting.Series series2 = new System.Windows.Forms.DataVisualization.Charting.Series(); + this.StatisticsChart = new System.Windows.Forms.DataVisualization.Charting.Chart(); + this.byISPCheckBox = new System.Windows.Forms.CheckBox(); + this.label2 = new System.Windows.Forms.Label(); + this.label3 = new System.Windows.Forms.Label(); + this.label4 = new System.Windows.Forms.Label(); + this.chartModeSelector = new System.Windows.Forms.GroupBox(); + this.allMode = new System.Windows.Forms.RadioButton(); + this.dayMode = new System.Windows.Forms.RadioButton(); + this.splitContainer1 = new System.Windows.Forms.SplitContainer(); + this.splitContainer2 = new System.Windows.Forms.SplitContainer(); + this.label9 = new System.Windows.Forms.Label(); + this.label8 = new System.Windows.Forms.Label(); + this.dataCollectionMinutesNum = new System.Windows.Forms.NumericUpDown(); + this.StatisticsEnabledCheckBox = new System.Windows.Forms.CheckBox(); + this.choiceKeptMinutesNum = new System.Windows.Forms.NumericUpDown(); + this.byHourOfDayCheckBox = new System.Windows.Forms.CheckBox(); + this.repeatTimesNum = new System.Windows.Forms.NumericUpDown(); + this.label6 = new System.Windows.Forms.Label(); + this.splitContainer3 = new System.Windows.Forms.SplitContainer(); + this.label1 = new System.Windows.Forms.Label(); + this.calculationContainer = new System.Windows.Forms.FlowLayoutPanel(); + this.serverSelector = new System.Windows.Forms.ComboBox(); + this.CancelButton = new System.Windows.Forms.Button(); + this.OKButton = new System.Windows.Forms.Button(); + this.bindingConfiguration = new System.Windows.Forms.BindingSource(this.components); + ((System.ComponentModel.ISupportInitialize)(this.StatisticsChart)).BeginInit(); + this.chartModeSelector.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); + this.splitContainer1.Panel1.SuspendLayout(); + this.splitContainer1.Panel2.SuspendLayout(); + this.splitContainer1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer2)).BeginInit(); + this.splitContainer2.Panel1.SuspendLayout(); + this.splitContainer2.Panel2.SuspendLayout(); + this.splitContainer2.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.dataCollectionMinutesNum)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.choiceKeptMinutesNum)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.repeatTimesNum)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer3)).BeginInit(); + this.splitContainer3.Panel1.SuspendLayout(); + this.splitContainer3.Panel2.SuspendLayout(); + this.splitContainer3.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.bindingConfiguration)).BeginInit(); + this.SuspendLayout(); + // + // StatisticsChart + // + this.StatisticsChart.BackColor = System.Drawing.Color.Transparent; + chartArea1.AxisX.MajorGrid.Enabled = false; + chartArea1.AxisY.MajorGrid.Enabled = false; + chartArea1.AxisY2.MajorGrid.Enabled = false; + chartArea1.BackColor = System.Drawing.Color.Transparent; + chartArea1.Name = "DataArea"; + this.StatisticsChart.ChartAreas.Add(chartArea1); + this.StatisticsChart.Dock = System.Windows.Forms.DockStyle.Fill; + legend1.BackColor = System.Drawing.Color.Transparent; + legend1.Name = "ChartLegend"; + this.StatisticsChart.Legends.Add(legend1); + this.StatisticsChart.Location = new System.Drawing.Point(0, 0); + this.StatisticsChart.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.StatisticsChart.Name = "StatisticsChart"; + this.StatisticsChart.Palette = System.Windows.Forms.DataVisualization.Charting.ChartColorPalette.Pastel; + series1.ChartArea = "DataArea"; + series1.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Bubble; + series1.Color = System.Drawing.Color.FromArgb(((int)(((byte)(221)))), ((int)(((byte)(88)))), ((int)(((byte)(0))))); + series1.Legend = "ChartLegend"; + series1.Name = "Package Loss"; + series1.XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.DateTime; + series1.YValuesPerPoint = 2; + series2.BorderWidth = 4; + series2.ChartArea = "DataArea"; + series2.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line; + series2.Color = System.Drawing.Color.FromArgb(((int)(((byte)(155)))), ((int)(((byte)(77)))), ((int)(((byte)(150))))); + series2.Legend = "ChartLegend"; + series2.Name = "Ping"; + series2.XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.DateTime; + this.StatisticsChart.Series.Add(series1); + this.StatisticsChart.Series.Add(series2); + this.StatisticsChart.Size = new System.Drawing.Size(1077, 303); + this.StatisticsChart.TabIndex = 2; + // + // byISPCheckBox + // + this.byISPCheckBox.AutoSize = true; + this.byISPCheckBox.DataBindings.Add(new System.Windows.Forms.Binding("Checked", this.bindingConfiguration, "ByIsp", true)); + this.byISPCheckBox.Location = new System.Drawing.Point(13, 54); + this.byISPCheckBox.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.byISPCheckBox.Name = "byISPCheckBox"; + this.byISPCheckBox.Size = new System.Drawing.Size(220, 31); + this.byISPCheckBox.TabIndex = 5; + this.byISPCheckBox.Text = "By ISP/geolocation"; + this.byISPCheckBox.UseVisualStyleBackColor = true; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(8, 136); + this.label2.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(167, 27); + this.label2.TabIndex = 8; + this.label2.Text = "Keep choice for "; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(285, 136); + this.label3.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(87, 27); + this.label3.TabIndex = 9; + this.label3.Text = "minutes"; + // + // label4 + // + this.label4.AutoSize = true; + this.label4.Location = new System.Drawing.Point(8, 218); + this.label4.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); + this.label4.Name = "label4"; + this.label4.Size = new System.Drawing.Size(54, 27); + this.label4.TabIndex = 10; + this.label4.Text = "Ping"; + // + // chartModeSelector + // + this.chartModeSelector.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.chartModeSelector.Controls.Add(this.allMode); + this.chartModeSelector.Controls.Add(this.dayMode); + this.chartModeSelector.Location = new System.Drawing.Point(801, 104); + this.chartModeSelector.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.chartModeSelector.Name = "chartModeSelector"; + this.chartModeSelector.Padding = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.chartModeSelector.Size = new System.Drawing.Size(261, 103); + this.chartModeSelector.TabIndex = 3; + this.chartModeSelector.TabStop = false; + this.chartModeSelector.Text = "Chart Mode"; + this.chartModeSelector.Enter += new System.EventHandler(this.chartModeSelector_Enter); + // + // allMode + // + this.allMode.AutoSize = true; + this.allMode.Checked = true; + this.allMode.Location = new System.Drawing.Point(11, 61); + this.allMode.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.allMode.Name = "allMode"; + this.allMode.Size = new System.Drawing.Size(58, 31); + this.allMode.TabIndex = 1; + this.allMode.TabStop = true; + this.allMode.Text = "all"; + this.allMode.UseVisualStyleBackColor = true; + this.allMode.CheckedChanged += new System.EventHandler(this.allMode_CheckedChanged); + // + // dayMode + // + this.dayMode.AutoSize = true; + this.dayMode.Location = new System.Drawing.Point(11, 29); + this.dayMode.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.dayMode.Name = "dayMode"; + this.dayMode.Size = new System.Drawing.Size(73, 31); + this.dayMode.TabIndex = 0; + this.dayMode.Text = "24h"; + this.dayMode.UseVisualStyleBackColor = true; + this.dayMode.CheckedChanged += new System.EventHandler(this.dayMode_CheckedChanged); + // + // splitContainer1 + // + this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer1.IsSplitterFixed = true; + this.splitContainer1.Location = new System.Drawing.Point(0, 0); + this.splitContainer1.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.splitContainer1.Name = "splitContainer1"; + this.splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal; + // + // splitContainer1.Panel1 + // + this.splitContainer1.Panel1.Controls.Add(this.splitContainer2); + // + // splitContainer1.Panel2 + // + this.splitContainer1.Panel2.Controls.Add(this.serverSelector); + this.splitContainer1.Panel2.Controls.Add(this.CancelButton); + this.splitContainer1.Panel2.Controls.Add(this.OKButton); + this.splitContainer1.Panel2.Controls.Add(this.chartModeSelector); + this.splitContainer1.Panel2.Controls.Add(this.StatisticsChart); + this.splitContainer1.Size = new System.Drawing.Size(1077, 614); + this.splitContainer1.SplitterDistance = 301; + this.splitContainer1.SplitterWidth = 10; + this.splitContainer1.TabIndex = 12; + // + // splitContainer2 + // + this.splitContainer2.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer2.FixedPanel = System.Windows.Forms.FixedPanel.Panel1; + this.splitContainer2.IsSplitterFixed = true; + this.splitContainer2.Location = new System.Drawing.Point(0, 0); + this.splitContainer2.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.splitContainer2.Name = "splitContainer2"; + // + // splitContainer2.Panel1 + // + this.splitContainer2.Panel1.Controls.Add(this.label9); + this.splitContainer2.Panel1.Controls.Add(this.label8); + this.splitContainer2.Panel1.Controls.Add(this.dataCollectionMinutesNum); + this.splitContainer2.Panel1.Controls.Add(this.StatisticsEnabledCheckBox); + this.splitContainer2.Panel1.Controls.Add(this.choiceKeptMinutesNum); + this.splitContainer2.Panel1.Controls.Add(this.byHourOfDayCheckBox); + this.splitContainer2.Panel1.Controls.Add(this.repeatTimesNum); + this.splitContainer2.Panel1.Controls.Add(this.label6); + this.splitContainer2.Panel1.Controls.Add(this.label2); + this.splitContainer2.Panel1.Controls.Add(this.label4); + this.splitContainer2.Panel1.Controls.Add(this.byISPCheckBox); + this.splitContainer2.Panel1.Controls.Add(this.label3); + // + // splitContainer2.Panel2 + // + this.splitContainer2.Panel2.Controls.Add(this.splitContainer3); + this.splitContainer2.Size = new System.Drawing.Size(1077, 301); + this.splitContainer2.SplitterDistance = 384; + this.splitContainer2.SplitterWidth = 5; + this.splitContainer2.TabIndex = 7; + // + // label9 + // + this.label9.AutoSize = true; + this.label9.Location = new System.Drawing.Point(8, 175); + this.label9.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); + this.label9.Name = "label9"; + this.label9.Size = new System.Drawing.Size(162, 27); + this.label9.TabIndex = 20; + this.label9.Text = "Collect data per"; + // + // label8 + // + this.label8.AutoSize = true; + this.label8.Font = new System.Drawing.Font("Microsoft YaHei", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label8.Location = new System.Drawing.Point(285, 176); + this.label8.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); + this.label8.Name = "label8"; + this.label8.Size = new System.Drawing.Size(87, 27); + this.label8.TabIndex = 19; + this.label8.Text = "minutes"; + // + // dataCollectionMinutesNum + // + this.dataCollectionMinutesNum.DataBindings.Add(new System.Windows.Forms.Binding("Value", this.bindingConfiguration, "DataCollectionMinutes", true)); + this.dataCollectionMinutesNum.Increment = new decimal(new int[] { + 10, + 0, + 0, + 0}); + this.dataCollectionMinutesNum.Location = new System.Drawing.Point(176, 173); + this.dataCollectionMinutesNum.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); + this.dataCollectionMinutesNum.Maximum = new decimal(new int[] { + 120, + 0, + 0, + 0}); + this.dataCollectionMinutesNum.Minimum = new decimal(new int[] { + 5, + 0, + 0, + 0}); + this.dataCollectionMinutesNum.Name = "dataCollectionMinutesNum"; + this.dataCollectionMinutesNum.Size = new System.Drawing.Size(100, 34); + this.dataCollectionMinutesNum.TabIndex = 18; + this.dataCollectionMinutesNum.Value = new decimal(new int[] { + 10, + 0, + 0, + 0}); + // + // StatisticsEnabledCheckBox + // + this.StatisticsEnabledCheckBox.AutoSize = true; + this.StatisticsEnabledCheckBox.DataBindings.Add(new System.Windows.Forms.Binding("Checked", this.bindingConfiguration, "StatisticsEnabled", true)); + this.StatisticsEnabledCheckBox.Location = new System.Drawing.Point(13, 12); + this.StatisticsEnabledCheckBox.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.StatisticsEnabledCheckBox.Name = "StatisticsEnabledCheckBox"; + this.StatisticsEnabledCheckBox.Size = new System.Drawing.Size(189, 31); + this.StatisticsEnabledCheckBox.TabIndex = 17; + this.StatisticsEnabledCheckBox.Text = "Enable Statistics"; + this.StatisticsEnabledCheckBox.UseVisualStyleBackColor = true; + // + // choiceKeptMinutesNum + // + this.choiceKeptMinutesNum.DataBindings.Add(new System.Windows.Forms.Binding("Value", this.bindingConfiguration, "ChoiceKeptMinutes", true)); + this.choiceKeptMinutesNum.Increment = new decimal(new int[] { + 10, + 0, + 0, + 0}); + this.choiceKeptMinutesNum.Location = new System.Drawing.Point(176, 134); + this.choiceKeptMinutesNum.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); + this.choiceKeptMinutesNum.Maximum = new decimal(new int[] { + 120, + 0, + 0, + 0}); + this.choiceKeptMinutesNum.Minimum = new decimal(new int[] { + 5, + 0, + 0, + 0}); + this.choiceKeptMinutesNum.Name = "choiceKeptMinutesNum"; + this.choiceKeptMinutesNum.Size = new System.Drawing.Size(100, 34); + this.choiceKeptMinutesNum.TabIndex = 16; + this.choiceKeptMinutesNum.Value = new decimal(new int[] { + 10, + 0, + 0, + 0}); + // + // byHourOfDayCheckBox + // + this.byHourOfDayCheckBox.AutoSize = true; + this.byHourOfDayCheckBox.DataBindings.Add(new System.Windows.Forms.Binding("Checked", this.bindingConfiguration, "ByHourOfDay", true)); + this.byHourOfDayCheckBox.Location = new System.Drawing.Point(13, 95); + this.byHourOfDayCheckBox.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.byHourOfDayCheckBox.Name = "byHourOfDayCheckBox"; + this.byHourOfDayCheckBox.Size = new System.Drawing.Size(180, 31); + this.byHourOfDayCheckBox.TabIndex = 15; + this.byHourOfDayCheckBox.Text = "By hour of day"; + this.byHourOfDayCheckBox.UseVisualStyleBackColor = true; + // + // repeatTimesNum + // + this.repeatTimesNum.DataBindings.Add(new System.Windows.Forms.Binding("Value", this.bindingConfiguration, "RepeatTimesNum", true)); + this.repeatTimesNum.Location = new System.Drawing.Point(72, 216); + this.repeatTimesNum.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); + this.repeatTimesNum.Maximum = new decimal(new int[] { + 10, + 0, + 0, + 0}); + this.repeatTimesNum.Name = "repeatTimesNum"; + this.repeatTimesNum.Size = new System.Drawing.Size(99, 34); + this.repeatTimesNum.TabIndex = 14; + this.repeatTimesNum.Value = new decimal(new int[] { + 4, + 0, + 0, + 0}); + // + // label6 + // + this.label6.AutoSize = true; + this.label6.Font = new System.Drawing.Font("Microsoft YaHei", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label6.Location = new System.Drawing.Point(178, 218); + this.label6.Name = "label6"; + this.label6.Size = new System.Drawing.Size(201, 27); + this.label6.TabIndex = 13; + this.label6.Text = "packages everytime"; + // + // splitContainer3 + // + this.splitContainer3.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer3.FixedPanel = System.Windows.Forms.FixedPanel.Panel1; + this.splitContainer3.IsSplitterFixed = true; + this.splitContainer3.Location = new System.Drawing.Point(0, 0); + this.splitContainer3.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.splitContainer3.Name = "splitContainer3"; + this.splitContainer3.Orientation = System.Windows.Forms.Orientation.Horizontal; + // + // splitContainer3.Panel1 + // + this.splitContainer3.Panel1.Controls.Add(this.label1); + // + // splitContainer3.Panel2 + // + this.splitContainer3.Panel2.Controls.Add(this.calculationContainer); + this.splitContainer3.Size = new System.Drawing.Size(688, 301); + this.splitContainer3.SplitterDistance = 46; + this.splitContainer3.SplitterWidth = 10; + this.splitContainer3.TabIndex = 6; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(5, 12); + this.label1.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(262, 27); + this.label1.TabIndex = 0; + this.label1.Text = "Design evaluation method"; + // + // calculationContainer + // + this.calculationContainer.AutoScroll = true; + this.calculationContainer.Dock = System.Windows.Forms.DockStyle.Fill; + this.calculationContainer.Location = new System.Drawing.Point(0, 0); + this.calculationContainer.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.calculationContainer.Name = "calculationContainer"; + this.calculationContainer.Size = new System.Drawing.Size(688, 245); + this.calculationContainer.TabIndex = 1; + // + // serverSelector + // + this.serverSelector.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.serverSelector.FormattingEnabled = true; + this.serverSelector.Location = new System.Drawing.Point(801, 67); + this.serverSelector.Name = "serverSelector"; + this.serverSelector.Size = new System.Drawing.Size(260, 35); + this.serverSelector.TabIndex = 6; + this.serverSelector.SelectedIndexChanged += new System.EventHandler(this.serverSelector_SelectedIndexChanged); + // + // CancelButton + // + this.CancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.CancelButton.Location = new System.Drawing.Point(960, 220); + this.CancelButton.Name = "CancelButton"; + this.CancelButton.Size = new System.Drawing.Size(101, 41); + this.CancelButton.TabIndex = 5; + this.CancelButton.Text = "Cancel"; + this.CancelButton.UseVisualStyleBackColor = true; + this.CancelButton.Click += new System.EventHandler(this.CancelButton_Click); + // + // OKButton + // + this.OKButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.OKButton.Location = new System.Drawing.Point(852, 220); + this.OKButton.Name = "OKButton"; + this.OKButton.Size = new System.Drawing.Size(101, 41); + this.OKButton.TabIndex = 4; + this.OKButton.Text = "OK"; + this.OKButton.UseVisualStyleBackColor = true; + this.OKButton.Click += new System.EventHandler(this.OKButton_Click); + // + // bindingConfiguration + // + this.bindingConfiguration.DataSource = typeof(Shadowsocks.Model.StatisticsStrategyConfiguration); + // + // StatisticsStrategyConfigurationForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 27F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.AutoSize = true; + this.ClientSize = new System.Drawing.Size(1077, 614); + this.Controls.Add(this.splitContainer1); + this.Font = new System.Drawing.Font("Microsoft YaHei", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.Margin = new System.Windows.Forms.Padding(5, 10, 5, 10); + this.MinimumSize = new System.Drawing.Size(1059, 498); + this.Name = "StatisticsStrategyConfigurationForm"; + this.Text = "StatisticsStrategyConfigurationForm"; + ((System.ComponentModel.ISupportInitialize)(this.StatisticsChart)).EndInit(); + this.chartModeSelector.ResumeLayout(false); + this.chartModeSelector.PerformLayout(); + this.splitContainer1.Panel1.ResumeLayout(false); + this.splitContainer1.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit(); + this.splitContainer1.ResumeLayout(false); + this.splitContainer2.Panel1.ResumeLayout(false); + this.splitContainer2.Panel1.PerformLayout(); + this.splitContainer2.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer2)).EndInit(); + this.splitContainer2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.dataCollectionMinutesNum)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.choiceKeptMinutesNum)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.repeatTimesNum)).EndInit(); + this.splitContainer3.Panel1.ResumeLayout(false); + this.splitContainer3.Panel1.PerformLayout(); + this.splitContainer3.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer3)).EndInit(); + this.splitContainer3.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.bindingConfiguration)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + private System.Windows.Forms.DataVisualization.Charting.Chart StatisticsChart; + private System.Windows.Forms.CheckBox byISPCheckBox; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.Label label4; + private System.Windows.Forms.GroupBox chartModeSelector; + private System.Windows.Forms.RadioButton allMode; + private System.Windows.Forms.RadioButton dayMode; + private System.Windows.Forms.SplitContainer splitContainer1; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.SplitContainer splitContainer2; + private System.Windows.Forms.FlowLayoutPanel calculationContainer; + private System.Windows.Forms.SplitContainer splitContainer3; + private System.Windows.Forms.NumericUpDown repeatTimesNum; + private System.Windows.Forms.Label label6; + private System.Windows.Forms.CheckBox byHourOfDayCheckBox; + private System.Windows.Forms.NumericUpDown choiceKeptMinutesNum; + private System.Windows.Forms.CheckBox StatisticsEnabledCheckBox; + private System.Windows.Forms.Label label9; + private System.Windows.Forms.Label label8; + private System.Windows.Forms.NumericUpDown dataCollectionMinutesNum; + private System.Windows.Forms.BindingSource bindingConfiguration; + private new System.Windows.Forms.Button CancelButton; + private System.Windows.Forms.Button OKButton; + private System.Windows.Forms.ComboBox serverSelector; + } +} \ No newline at end of file diff --git a/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.cs b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.cs new file mode 100644 index 00000000..88e32309 --- /dev/null +++ b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Windows.Forms; +using System.Windows.Forms.DataVisualization.Charting; +using Shadowsocks.Controller; +using Shadowsocks.Model; +using SimpleJson; +using System.Net.NetworkInformation; + +namespace Shadowsocks.View +{ + public partial class StatisticsStrategyConfigurationForm: Form + { + private readonly ShadowsocksController _controller; + private StatisticsStrategyConfiguration _configuration; + private DataTable _dataTable = new DataTable(); + private List _servers; + + public StatisticsStrategyConfigurationForm(ShadowsocksController controller) + { + if (controller == null) return; + InitializeComponent(); + _controller = controller; + _controller.ConfigChanged += (sender, args) => LoadConfiguration(); + LoadConfiguration(); + Load += (sender, args) => InitData(); + } + + private void LoadConfiguration() + { + var configs = _controller.GetCurrentConfiguration().configs; + _servers = configs.Select(server => server.FriendlyName()).ToList(); + _configuration = _controller.StatisticsConfiguration + ?? new StatisticsStrategyConfiguration(); + if (_configuration.Calculations == null) + { + _configuration = new StatisticsStrategyConfiguration(); + } + } + + private void InitData() + { + bindingConfiguration.Add(_configuration); + foreach (var kv in _configuration.Calculations) + { + var calculation = new CalculationControl(kv.Key, kv.Value); + calculationContainer.Controls.Add(calculation); + } + + serverSelector.DataSource = _servers; + + _dataTable.Columns.Add("Timestamp", typeof (DateTime)); + _dataTable.Columns.Add("Package Loss", typeof (int)); + _dataTable.Columns.Add("Ping", typeof (int)); + + StatisticsChart.Series["Package Loss"].XValueMember = "Timestamp"; + StatisticsChart.Series["Package Loss"].YValueMembers = "Package Loss"; + StatisticsChart.Series["Ping"].XValueMember = "Timestamp"; + StatisticsChart.Series["Ping"].YValueMembers = "Ping"; + StatisticsChart.DataSource = _dataTable; + loadChartData(); + StatisticsChart.DataBind(); + } + + + private void CancelButton_Click(object sender, EventArgs e) + { + Close(); + } + + private void OKButton_Click(object sender, EventArgs e) + { + foreach (CalculationControl calculation in calculationContainer.Controls) + { + _configuration.Calculations[calculation.Value] = calculation.Factor; + } + _controller?.SaveStrategyConfigurations(_configuration); + _controller?.UpdateStatisticsConfiguration(StatisticsEnabledCheckBox.Checked); + Close(); + } + + private void loadChartData() + { + string serverName = _servers[serverSelector.SelectedIndex]; + _dataTable.Rows.Clear(); + List statistics; + if (!_controller.availabilityStatistics.FilteredStatistics.TryGetValue(serverName, out statistics)) return; + IEnumerable> dataGroups; + if (allMode.Checked) + { + dataGroups = statistics.GroupBy(data => data.Timestamp.DayOfYear); + StatisticsChart.ChartAreas["DataArea"].AxisX.LabelStyle.Format = "MM/dd/yyyy"; + StatisticsChart.ChartAreas["DataArea"].AxisX2.LabelStyle.Format = "MM/dd/yyyy"; + } + else + { + dataGroups = statistics.GroupBy(data => data.Timestamp.Hour); + StatisticsChart.ChartAreas["DataArea"].AxisX.LabelStyle.Format = "HH:00"; + StatisticsChart.ChartAreas["DataArea"].AxisX2.LabelStyle.Format = "HH:00"; + } + var finalData = from dataGroup in dataGroups + orderby dataGroup.Key + select new + { + Timestamp = dataGroup.First().Timestamp, + Ping = (int)dataGroup.Average(data => data.RoundtripTime), + PackageLoss = (int) + (dataGroup.Count(data => data.ICMPStatus.Equals(IPStatus.TimedOut.ToString())) + / (float)dataGroup.Count() * 100) + }; + foreach (var data in finalData) + { + _dataTable.Rows.Add(data.Timestamp, data.PackageLoss, data.Ping); + } + StatisticsChart.DataBind(); + } + + private void serverSelector_SelectedIndexChanged(object sender, EventArgs e) + { + loadChartData(); + } + + private void chartModeSelector_Enter(object sender, EventArgs e) + { + + } + + private void dayMode_CheckedChanged(object sender, EventArgs e) + { + loadChartData(); + } + + private void allMode_CheckedChanged(object sender, EventArgs e) + { + loadChartData(); + } + } +} diff --git a/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.resx b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.resx new file mode 100644 index 00000000..5f9d5c44 --- /dev/null +++ b/shadowsocks-csharp/View/StatisticsStrategyConfigurationForm.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 1, 30 + + + 63 + + \ No newline at end of file diff --git a/shadowsocks-csharp/app.config b/shadowsocks-csharp/app.config index 867ff468..a7ba1069 100755 --- a/shadowsocks-csharp/app.config +++ b/shadowsocks-csharp/app.config @@ -1,6 +1,19 @@ - + - - - + + + + + + + + + + + + + + + + diff --git a/shadowsocks-csharp/app.manifest b/shadowsocks-csharp/app.manifest index 2f6c64aa..3cffa5a2 100755 --- a/shadowsocks-csharp/app.manifest +++ b/shadowsocks-csharp/app.manifest @@ -10,7 +10,7 @@ - true + True/PM \ No newline at end of file diff --git a/shadowsocks-csharp/packages.config b/shadowsocks-csharp/packages.config new file mode 100644 index 00000000..b309fb97 --- /dev/null +++ b/shadowsocks-csharp/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/shadowsocks-csharp/shadowsocks-csharp.csproj b/shadowsocks-csharp/shadowsocks-csharp.csproj index dd957ffb..423268d8 100644 --- a/shadowsocks-csharp/shadowsocks-csharp.csproj +++ b/shadowsocks-csharp/shadowsocks-csharp.csproj @@ -36,6 +36,8 @@ 1.0.0.%2a false true + + true @@ -62,12 +64,56 @@ app.manifest + + + 3rd\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll + True + False + + + 3rd\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll + True + False + + + 3rd\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll + True + False + + + 3rd\Newtonsoft.Json.7.0.1\lib\net40\Newtonsoft.Json.dll + True + + + + + 3rd\Microsoft.Bcl.1.1.8\lib\net40\System.IO.dll + True + False + + + + + 3rd\Microsoft.Bcl.1.1.8\lib\net40\System.Runtime.dll + True + False + + + 3rd\Microsoft.Bcl.1.1.8\lib\net40\System.Threading.Tasks.dll + True + False + + + + + + @@ -125,7 +171,7 @@ - + @@ -148,6 +194,7 @@ + True True @@ -169,6 +216,12 @@ + + UserControl + + + CalculationControl.cs + Form @@ -185,6 +238,12 @@ Form + + Form + + + StatisticsStrategyConfigurationForm.cs + ConfigForm.cs Designer @@ -194,12 +253,18 @@ Designer Resources.Designer.cs + + CalculationControl.cs + LogForm.cs QRCodeForm.cs + + StatisticsStrategyConfigurationForm.cs + Designer @@ -211,6 +276,8 @@ + + @@ -218,6 +285,9 @@ + + Designer + @@ -253,6 +323,18 @@ + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + +